143 lines
5.9 KiB
TypeScript
143 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)
|
|||
|
|
})
|