Compare commits
6 commits
0c3b96d5ae
...
905c267c47
| Author | SHA1 | Date | |
|---|---|---|---|
| 905c267c47 | |||
| b1c7054467 | |||
| 67ae0046de | |||
| 9922631d1f | |||
| 2633e8d5e1 | |||
| f763317af7 |
4 changed files with 265 additions and 3 deletions
148
.forgejo/workflows/delta-gate.yml
Normal file
148
.forgejo/workflows/delta-gate.yml
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
name: Delta Gate
|
||||||
|
|
||||||
|
# On-demand candidate-vs-control gate on the capped functional CI lane
|
||||||
|
# (label: gate-functional). That lane is Bun-only host-mode — there is no
|
||||||
|
# Node.js runtime available to it, so this workflow deliberately avoids every
|
||||||
|
# JS-based action (checkout/setup-node/setup-bun/upload-artifact all require
|
||||||
|
# one) and does everything with plain git + bun in shell steps instead.
|
||||||
|
#
|
||||||
|
# Verdict lines a caller should grep for in the run log:
|
||||||
|
# COLLECTED patch=<n> control=<n> — collection-truncation guard inputs
|
||||||
|
# NEW-RED-COUNT:<n> — failures on candidate absent from control
|
||||||
|
# DELTA-GATE: CLEAN | NEW REDS | INVALID | STOPPED-BY-REGISTRY-TRIPWIRE
|
||||||
|
#
|
||||||
|
# The lane's own housekeeping stops the runner and drops a marker file when
|
||||||
|
# host pressure (I/O, registry latency, disk budget) trips — never ours to
|
||||||
|
# interpret as a red or a green. The final step checks for that marker before
|
||||||
|
# it says anything about pass/fail.
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
candidate:
|
||||||
|
description: 'Candidate ref (branch or sha) to gate'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
control:
|
||||||
|
description: 'Control sha to diff against'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
# workflow_dispatch needs Actions-unit write on the dispatching credential;
|
||||||
|
# push does not (it runs from the pushed ref's own tree), so a plain push
|
||||||
|
# to a release or CI branch is the fallback trigger while that grant is
|
||||||
|
# outstanding — see the ref-resolution step below for what it gates against.
|
||||||
|
push:
|
||||||
|
branches: ['rel/**', 'ci/**']
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: delta-gate
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
delta-gate:
|
||||||
|
name: Delta gate — candidate vs control
|
||||||
|
runs-on: gate-functional
|
||||||
|
timeout-minutes: 120
|
||||||
|
steps:
|
||||||
|
- name: Resolve candidate/control refs
|
||||||
|
id: refs
|
||||||
|
run: |
|
||||||
|
candidate="${{ github.event.inputs.candidate }}"
|
||||||
|
control="${{ github.event.inputs.control }}"
|
||||||
|
# workflow_dispatch supplies both explicitly; a push event carries
|
||||||
|
# neither — fall back to the pushed commit as candidate and the
|
||||||
|
# last released, known-good tip (10.4.9) as control, so a plain
|
||||||
|
# push still produces a meaningful gate instead of an empty ref.
|
||||||
|
if [ -z "$candidate" ]; then candidate="${{ github.sha }}"; fi
|
||||||
|
if [ -z "$control" ]; then control="eec90bdd"; fi
|
||||||
|
echo "candidate=$candidate" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "control=$control" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Resolved (trigger=${{ github.event_name }}): candidate=$candidate control=$control"
|
||||||
|
|
||||||
|
- name: Clean any residue from a prior run
|
||||||
|
run: rm -rf "ob-cand-${{ github.run_id }}" "ob-ctrl-${{ github.run_id }}" "/tmp/ob-${{ github.run_id }}-"*
|
||||||
|
|
||||||
|
- name: Clone + test — candidate
|
||||||
|
id: patch
|
||||||
|
run: |
|
||||||
|
set -o pipefail
|
||||||
|
git clone --quiet "https://source.soulcraft.com/soulcraftlabs/open-brainy.git" "ob-cand-${{ github.run_id }}"
|
||||||
|
cd "ob-cand-${{ github.run_id }}"
|
||||||
|
git checkout --quiet "${{ steps.refs.outputs.candidate }}"
|
||||||
|
git log --oneline -1
|
||||||
|
bun install
|
||||||
|
rc=0
|
||||||
|
bun x vitest run > "/tmp/ob-${{ github.run_id }}-patch.log" 2>&1 || rc=$?
|
||||||
|
echo "PATCH-RC:$rc"
|
||||||
|
grep -aE "Tests .*(passed|failed)" "/tmp/ob-${{ github.run_id }}-patch.log" | tail -1
|
||||||
|
grep -aE "^ FAIL |^\s+×" "/tmp/ob-${{ github.run_id }}-patch.log" | sed -E "s/ [0-9]+ms$//" | sed -E "s/^\s+//" | sort -u > "/tmp/ob-${{ github.run_id }}-patch.fail"
|
||||||
|
echo "PATCH-FAILING:$(wc -l < "/tmp/ob-${{ github.run_id }}-patch.fail")"
|
||||||
|
|
||||||
|
- name: Clone + test — control
|
||||||
|
id: control
|
||||||
|
run: |
|
||||||
|
set -o pipefail
|
||||||
|
git clone --quiet "https://source.soulcraft.com/soulcraftlabs/open-brainy.git" "ob-ctrl-${{ github.run_id }}"
|
||||||
|
cd "ob-ctrl-${{ github.run_id }}"
|
||||||
|
git checkout --quiet "${{ steps.refs.outputs.control }}"
|
||||||
|
git log --oneline -1
|
||||||
|
bun install
|
||||||
|
rc=0
|
||||||
|
bun x vitest run > "/tmp/ob-${{ github.run_id }}-control.log" 2>&1 || rc=$?
|
||||||
|
echo "CONTROL-RC:$rc"
|
||||||
|
grep -aE "Tests .*(passed|failed)" "/tmp/ob-${{ github.run_id }}-control.log" | tail -1
|
||||||
|
grep -aE "^ FAIL |^\s+×" "/tmp/ob-${{ github.run_id }}-control.log" | sed -E "s/ [0-9]+ms$//" | sed -E "s/^\s+//" | sort -u > "/tmp/ob-${{ github.run_id }}-control.fail"
|
||||||
|
echo "CONTROL-FAILING:$(wc -l < "/tmp/ob-${{ github.run_id }}-control.fail")"
|
||||||
|
|
||||||
|
- name: Delta gate verdict
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
# The lane's own tripwire wins over anything we would otherwise say:
|
||||||
|
# a bare failure/timeout above with this marker present is host
|
||||||
|
# pressure, never a real red and never a real green.
|
||||||
|
if [ -f /srv/gate-lane/TRIPWIRE-STOPPED ]; then
|
||||||
|
echo "DELTA-GATE: STOPPED-BY-REGISTRY-TRIPWIRE"
|
||||||
|
head -1 /srv/gate-lane/TRIPWIRE-STOPPED
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
patch_log="/tmp/ob-${{ github.run_id }}-patch.log"
|
||||||
|
control_log="/tmp/ob-${{ github.run_id }}-control.log"
|
||||||
|
patch_fail="/tmp/ob-${{ github.run_id }}-patch.fail"
|
||||||
|
control_fail="/tmp/ob-${{ github.run_id }}-control.fail"
|
||||||
|
|
||||||
|
if [ ! -s "$patch_log" ] || [ ! -s "$control_log" ]; then
|
||||||
|
echo "DELTA-GATE: INVALID — a leg produced no log (see the two steps above for the real cause)"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
pt=$(grep -aoE "\(([0-9]+)\)$" "$patch_log" | tail -1 | tr -d "()")
|
||||||
|
ct=$(grep -aoE "\(([0-9]+)\)$" "$control_log" | tail -1 | tr -d "()")
|
||||||
|
echo "COLLECTED patch=${pt:-0} control=${ct:-0}"
|
||||||
|
if [ "${pt:-0}" -lt 3000 ] || [ "${ct:-0}" -lt 3000 ]; then
|
||||||
|
echo "DELTA-GATE: INVALID — truncated collection"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== NEW REDS ==="
|
||||||
|
comm -23 "$patch_fail" "$control_fail"
|
||||||
|
new=$(comm -23 "$patch_fail" "$control_fail" | wc -l)
|
||||||
|
echo "NEW-RED-COUNT:$new"
|
||||||
|
|
||||||
|
echo "=== full candidate fail list ==="
|
||||||
|
cat "$patch_fail"
|
||||||
|
echo "=== full control fail list ==="
|
||||||
|
cat "$control_fail"
|
||||||
|
|
||||||
|
if [ "$new" -eq 0 ]; then
|
||||||
|
echo "DELTA-GATE: CLEAN"
|
||||||
|
else
|
||||||
|
echo "DELTA-GATE: NEW REDS"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Clean up (mind the lane's disk budget)
|
||||||
|
if: always()
|
||||||
|
run: rm -rf "ob-cand-${{ github.run_id }}" "ob-ctrl-${{ github.run_id }}" "/tmp/ob-${{ github.run_id }}-"*
|
||||||
|
|
@ -1040,6 +1040,17 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory hook for the generation store, so an engine built on top of this
|
||||||
|
* reference implementation can substitute a `GenerationStore` that keeps
|
||||||
|
* the same behavioural contract (for example, one backed by a native
|
||||||
|
* implementation) — overriding it never changes this engine's own
|
||||||
|
* behaviour, since the default implementation is unchanged.
|
||||||
|
*/
|
||||||
|
protected createGenerationStore(storage: BaseStorage): GenerationStore {
|
||||||
|
return new GenerationStore(storage)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize Brainy.
|
* Initialize Brainy.
|
||||||
*
|
*
|
||||||
|
|
@ -1297,7 +1308,7 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
||||||
// guarantees indexes never observe rolled-back state. Reader-mode
|
// guarantees indexes never observe rolled-back state. Reader-mode
|
||||||
// instances skip recovery (readers never write; the next writer
|
// instances skip recovery (readers never write; the next writer
|
||||||
// repairs).
|
// repairs).
|
||||||
this.generationStore = new GenerationStore(this.storage)
|
this.generationStore = this.createGenerationStore(this.storage)
|
||||||
const generationOpenResult = await step(
|
const generationOpenResult = await step(
|
||||||
'generation-store.open',
|
'generation-store.open',
|
||||||
'reading the generation manifest and committed ranges, opening the fact log and the ' +
|
'reading the generation manifest and committed ranges, opening the fact log and the ' +
|
||||||
|
|
|
||||||
|
|
@ -460,8 +460,10 @@ export interface MetadataIndexProvider {
|
||||||
* @param params - The find params, already normalized by `find()`
|
* @param params - The find params, already normalized by `find()`
|
||||||
* (natural-language parsed, `connected` anchors resolved to canonical ids,
|
* (natural-language parsed, `connected` anchors resolved to canonical ids,
|
||||||
* an empty `where` dropped).
|
* an empty `where` dropped).
|
||||||
* @param hiddenIds - Ids this read must not return; apply BEFORE paging so
|
* @param hiddenIds - Ids this read must not return. The contract is the ANSWER, not the
|
||||||
* `limit` stays exact.
|
* mechanism: a provider may subtract this set before paging, or derive the
|
||||||
|
* same exclusion from the params' visibility tiers itself — either way the
|
||||||
|
* page must equal the engine's own answer with none of these ids in it.
|
||||||
* @param graphIndex - The active graph provider, for a `connected` plan.
|
* @param graphIndex - The active graph provider, for a `connected` plan.
|
||||||
* @returns The page's ids plus the stage that emptied it, or `null`.
|
* @returns The page's ids plus the stage that emptied it, or `null`.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
101
tests/integration/generation-store-factory.test.ts
Normal file
101
tests/integration/generation-store-factory.test.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
/**
|
||||||
|
* @module tests/integration/generation-store-factory
|
||||||
|
* @description Pins the `createGenerationStore` protected factory hook on
|
||||||
|
* `Brainy` ({@link Brainy.createGenerationStore}). The hook exists so an
|
||||||
|
* engine built on top of this reference implementation can substitute a
|
||||||
|
* `GenerationStore` that keeps the same behavioural contract; this suite
|
||||||
|
* proves two things:
|
||||||
|
*
|
||||||
|
* 1. A subclass overriding the hook is the ONLY path that constructs the
|
||||||
|
* generation store — it is called exactly once, with the same storage
|
||||||
|
* instance `performInit` holds — and the store the brain actually uses
|
||||||
|
* is the one the override returned.
|
||||||
|
* 2. The default (non-overridden) path is unaffected — proven here by
|
||||||
|
* confirming the base class still produces a plain `GenerationStore`
|
||||||
|
* wired to `brain.storage`, and separately by running the existing
|
||||||
|
* `db-mvcc` and `brainy-core.integration` suites unmodified against this
|
||||||
|
* change (they exercise generation-store behaviour end to end).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, afterEach } from 'vitest'
|
||||||
|
import { Brainy } from '../../src/brainy.js'
|
||||||
|
import { GenerationStore } from '../../src/db/generationStore.js'
|
||||||
|
import type { BaseStorage } from '../../src/storage/baseStorage.js'
|
||||||
|
|
||||||
|
/** Typed access to the brain's private storage + generation-store fields (test injection point). */
|
||||||
|
function internalsOf(brain: Brainy): { storage: BaseStorage; generationStore: GenerationStore } {
|
||||||
|
return brain as unknown as { storage: BaseStorage; generationStore: GenerationStore }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A `GenerationStore` subclass that counts its own construction and
|
||||||
|
* remembers the storage instance it was built with, so the test can prove
|
||||||
|
* the hook is the sole construction path without mocking the module.
|
||||||
|
*/
|
||||||
|
class SpyGenerationStore extends GenerationStore {
|
||||||
|
static constructCount = 0
|
||||||
|
static lastStorage: BaseStorage | undefined
|
||||||
|
|
||||||
|
constructor(storage: BaseStorage) {
|
||||||
|
super(storage)
|
||||||
|
SpyGenerationStore.constructCount++
|
||||||
|
SpyGenerationStore.lastStorage = storage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A Brainy subclass overriding the factory hook — stands in for an engine built on the reference. */
|
||||||
|
class BrainyWithSpyStore extends Brainy {
|
||||||
|
hookCallCount = 0
|
||||||
|
hookStorageArg: BaseStorage | undefined
|
||||||
|
|
||||||
|
protected override createGenerationStore(storage: BaseStorage): GenerationStore {
|
||||||
|
this.hookCallCount++
|
||||||
|
this.hookStorageArg = storage
|
||||||
|
return new SpyGenerationStore(storage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Brainy.createGenerationStore — protected factory hook', () => {
|
||||||
|
const brains: Brainy[] = []
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
SpyGenerationStore.constructCount = 0
|
||||||
|
SpyGenerationStore.lastStorage = undefined
|
||||||
|
for (const brain of brains.splice(0)) {
|
||||||
|
try {
|
||||||
|
await brain.close()
|
||||||
|
} catch {
|
||||||
|
// already closed by the test
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('a subclass override is the sole construction path: called once, same storage instance, its store is the one the brain uses', async () => {
|
||||||
|
const brain = new BrainyWithSpyStore({ requireSubtype: false, storage: { type: 'memory' } })
|
||||||
|
await brain.init()
|
||||||
|
brains.push(brain)
|
||||||
|
|
||||||
|
// Called exactly once, through the hook.
|
||||||
|
expect(brain.hookCallCount).toBe(1)
|
||||||
|
expect(SpyGenerationStore.constructCount).toBe(1)
|
||||||
|
|
||||||
|
// Same storage instance the base class holds — not a copy, not a different adapter.
|
||||||
|
const { storage, generationStore } = internalsOf(brain)
|
||||||
|
expect(brain.hookStorageArg).toBe(storage)
|
||||||
|
expect(SpyGenerationStore.lastStorage).toBe(storage)
|
||||||
|
|
||||||
|
// The store the brain actually uses is the one the override returned.
|
||||||
|
expect(generationStore).toBeInstanceOf(SpyGenerationStore)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('the default (non-overridden) path still produces a plain GenerationStore wired to the same storage', async () => {
|
||||||
|
const brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' } })
|
||||||
|
await brain.init()
|
||||||
|
brains.push(brain)
|
||||||
|
|
||||||
|
const { storage, generationStore } = internalsOf(brain)
|
||||||
|
expect(generationStore).toBeInstanceOf(GenerationStore)
|
||||||
|
// The default implementation constructs from the same storage the brain holds.
|
||||||
|
expect((generationStore as unknown as { storage: BaseStorage }).storage).toBe(storage)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in a new issue