Two consumer-driven cures sharing one stamp. (1) TX-LOG ORIGIN: engine-
originated commits stamp an optional origin on their tx-log entry AND the
commit fact's meta — 'system:embed-landing' (the deferred vector landing),
'system:adoption-backfill' (baseline re-commits), 'system:reconcile'. A
downstream activity feed showed a double tick because the landing commit was
indistinguishable from a user save, and the consumer rightly refused a
time-window collapse as a quiet loss; feeds now filter on fact. User writes
stay unstamped — absent origin is the user shape, every existing consumer
unchanged. (2) reconcileLogDivergence(id, {attest}): the human's door for
log-live-canonical-absent, the one class adoption refuses by design because
a lost-tombstone deletion is indistinguishable from canonical loss.
'deleted' mints the missing tombstone (history keeps the earlier live
record); 'restore' folds the log's only copy back into canonical; wrong-
class calls refuse typed with nothing written. Loud, narrated, single-row,
origin-stamped. From a production adoption's one surviving divergence.
142 lines
5.9 KiB
TypeScript
142 lines
5.9 KiB
TypeScript
/**
|
||
* @module tests/integration/txlog-origin-and-reconcile
|
||
* @description Two consumer-driven cures, pinned together because they share
|
||
* the origin stamp:
|
||
*
|
||
* 1. TX-LOG ORIGIN — engine-originated commits stamp `origin` on their
|
||
* tx-log entry (and the commit fact's meta) so activity feeds filter on
|
||
* fact: a downstream feed showed a "double tick" because the deferred
|
||
* vector-landing commit was indistinguishable from a user save, and the
|
||
* consumer rightly refused a time-window collapse as a quiet loss. User
|
||
* writes stay UNSTAMPED (absent origin) — the pre-existing reading of
|
||
* every consumer is exact.
|
||
*
|
||
* 2. THE RECONCILE DOOR — `log-live-canonical-absent` refuses auto-cure by
|
||
* design (a legitimate lost-tombstone deletion is indistinguishable from
|
||
* canonical loss); `reconcileLogDivergence(id, {attest})` is the human's
|
||
* door: 'deleted' mints the missing tombstone, 'restore' folds the log's
|
||
* copy back, wrong-class calls refuse typed with nothing written.
|
||
*/
|
||
import { describe, it, expect, afterEach } from 'vitest'
|
||
import { mkdtempSync, rmSync } from 'node:fs'
|
||
import { tmpdir } from 'node:os'
|
||
import { join } from 'node:path'
|
||
import { Brainy } from '../../src/index.js'
|
||
import { NounType } from '../../src/types/graphTypes.js'
|
||
|
||
type RawBox = {
|
||
storage: {
|
||
readNounRaw(id: string): Promise<{ metadata: unknown; vector: unknown }>
|
||
writeNounRaw(id: string, r: { metadata: unknown; vector: unknown }): Promise<void>
|
||
}
|
||
}
|
||
|
||
const dirs: string[] = []
|
||
const brains: Brainy[] = []
|
||
afterEach(async () => {
|
||
for (const b of brains.splice(0)) await b.close().catch(() => {})
|
||
for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true })
|
||
})
|
||
|
||
async function fsBrain(): Promise<Brainy> {
|
||
const dir = mkdtempSync(join(tmpdir(), 'brainy-origin-reconcile-'))
|
||
dirs.push(dir)
|
||
const brain = new Brainy({
|
||
storage: { type: 'filesystem', path: dir },
|
||
requireSubtype: false
|
||
})
|
||
await brain.init()
|
||
brains.push(brain)
|
||
return brain
|
||
}
|
||
|
||
describe('tx-log origin stamp', () => {
|
||
it('the deferred-embed landing commit is stamped system:embed-landing; the user write is not', async () => {
|
||
const brain = await fsBrain()
|
||
await brain.add({
|
||
data: 'a row whose vector lands later',
|
||
type: NounType.Document,
|
||
metadata: { k: 1 },
|
||
deferEmbedding: true
|
||
})
|
||
await brain.awaitPendingEmbeds()
|
||
await brain.flush()
|
||
|
||
const entries = await brain.transactionLog()
|
||
const system = entries.filter((e) => (e as { origin?: string }).origin === 'system:embed-landing')
|
||
const user = entries.filter((e) => !(e as { origin?: string }).origin)
|
||
expect(system.length, 'the landing commit is stamped').toBeGreaterThanOrEqual(1)
|
||
expect(user.length, 'the user add stays unstamped').toBeGreaterThanOrEqual(1)
|
||
// The feed cure in one line: filtering !origin removes the double tick.
|
||
expect(user.length).toBeLessThan(entries.length)
|
||
}, 120000)
|
||
})
|
||
|
||
describe('reconcileLogDivergence — the attested door', () => {
|
||
/** Manufacture the class: a live log record whose canonical row is gone. */
|
||
async function manufactureDivergence(brain: Brainy): Promise<string> {
|
||
const id = await brain.add({
|
||
data: 'pre-era row whose deletion the log never saw',
|
||
type: NounType.Document,
|
||
metadata: { era: 'pre-spine' }
|
||
})
|
||
await brain.flush()
|
||
// Delete canonical BEHIND the log's back (raw write, no generation) —
|
||
// exactly the shape a deferred-durability-era crash left behind.
|
||
const storage = (brain as unknown as RawBox).storage
|
||
await storage.writeNounRaw(id, { metadata: null, vector: null })
|
||
return id
|
||
}
|
||
|
||
it("attest:'deleted' mints the missing tombstone — the oracle goes green and the commit is stamped system:reconcile", async () => {
|
||
const brain = await fsBrain()
|
||
const id = await manufactureDivergence(brain)
|
||
const before = await brain.verifyLogAuthority()
|
||
expect(
|
||
before.mismatches.some((m) => m.id === id && m.reason === 'log-live-canonical-absent'),
|
||
'the manufactured divergence is oracle-visible as the refused class'
|
||
).toBe(true)
|
||
|
||
const result = await brain.reconcileLogDivergence(id, { attest: 'deleted' })
|
||
expect(result.reconciled).toBe('tombstoned')
|
||
|
||
const after = await brain.verifyLogAuthority()
|
||
expect(after.mismatches.some((m) => m.id === id), 'the id no longer diverges').toBe(false)
|
||
expect(await brain.get(id), 'canonical stays absent').toBeNull()
|
||
|
||
await brain.flush()
|
||
const entries = await brain.transactionLog()
|
||
expect(
|
||
entries.some((e) => (e as { origin?: string }).origin === 'system:reconcile'),
|
||
'the reconcile commit is origin-stamped'
|
||
).toBe(true)
|
||
}, 120000)
|
||
|
||
it("attest:'restore' folds the log's copy back into canonical", async () => {
|
||
const brain = await fsBrain()
|
||
const id = await manufactureDivergence(brain)
|
||
|
||
const result = await brain.reconcileLogDivergence(id, { attest: 'restore' })
|
||
expect(result.reconciled).toBe('restored')
|
||
|
||
const row = await brain.get(id)
|
||
expect(row, 'the log’s only copy lives again').not.toBeNull()
|
||
expect((row!.metadata as { era: string }).era).toBe('pre-spine')
|
||
expect((await brain.verifyLogAuthority()).mismatches.some((m) => m.id === id)).toBe(false)
|
||
}, 120000)
|
||
|
||
it('wrong-class calls refuse typed with nothing written', async () => {
|
||
const brain = await fsBrain()
|
||
const id = await brain.add({ data: 'healthy row', type: NounType.Document, metadata: { n: 1 } })
|
||
await brain.flush()
|
||
// Canonical present + log agrees: not the class — refuse, name the state.
|
||
await expect(brain.reconcileLogDivergence(id, { attest: 'deleted' })).rejects.toThrow(
|
||
/canonical is PRESENT/
|
||
)
|
||
expect(await brain.get(id), 'nothing was written').not.toBeNull()
|
||
// Unknown id: no log record at all — refuse, name it.
|
||
await expect(
|
||
brain.reconcileLogDivergence('00000000-0000-7000-8000-00000000dead', { attest: 'restore' })
|
||
).rejects.toThrow(/no record at all/)
|
||
}, 120000)
|
||
})
|