/** * @module tests/integration/sync-fail-compensation * @description The non-monotonic refusal-loop cure (a production adoption's * second defect): when the at-ack covering SYNC fails AFTER a successful * append, the counter must NOT rewind unless the appended fact is provably * removed — rewinding while the log carries the generation re-mints the * same number and every later append refuses non-monotonic, wedging the * write path in a refusal loop ("writes REFUSED until it drains"). */ import { describe, it, expect, afterEach, vi } 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' const dirs: string[] = [] const brains: Brainy[] = [] afterEach(async () => { vi.restoreAllMocks() for (const b of brains.splice(0)) await b.close().catch(() => {}) for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true }) }) describe('at-ack sync-failure compensation', () => { it('a one-shot sync failure never wedges the write path: the next write mints a FRESH generation and succeeds', async () => { const dir = mkdtempSync(join(tmpdir(), 'brainy-syncfail-')) dirs.push(dir) const brain = new Brainy({ storage: { type: 'filesystem', path: dir }, requireSubtype: false }) await brain.init() // adopt-default: log authority, at-ack brains.push(brain) expect(brain.logAuthority().authority).toBe('log') await brain.add({ data: 'baseline', type: NounType.Document, metadata: { n: 0 } }) // Fail exactly ONE covering sync (after its append lands). // Target ensureSynced (the ACK path's covering sync) — mocking sync() // itself gets eaten by background flushes before the victim write. const factLog = (brain as unknown as { generationStore: { getFactLog(): { ensureSynced(): Promise } } }).generationStore.getFactLog() const realEnsure = factLog.ensureSynced.bind(factLog) let failed = false vi.spyOn(factLog, 'ensureSynced').mockImplementation(async () => { if (!failed) { failed = true throw new Error('injected sync failure (device hiccup)') } return realEnsure() }) // The write whose sync fails: LOUD failure to the caller — never silent. await expect( brain.add({ data: 'sync victim', type: NounType.Document, metadata: { n: 1 } }) ).rejects.toThrow(/sync failure/) // THE PIN: the very next write mints a fresh generation and SUCCEEDS — // no non-monotonic refusal, no refusal loop, regardless of whether the // failed write's fact was dropped or retained (both are legal outcomes; // an equal-generation re-mint is not). const survivor = await brain.add({ data: 'after the storm', type: NounType.Document, metadata: { n: 2 } }) expect((await brain.get(survivor))!.data).toContain('after the storm') await brain.flush() expect(Number.isSafeInteger(brain.generation())).toBe(true) // And the log scans clean end-to-end (no torn ordering). const scan = brain.scanFacts() let last = 0 if (scan) { for await (const batch of (scan as { batches(): AsyncIterable<{ facts: Array<{ generation: number }> }> }).batches()) { for (const f of batch.facts) { expect(f.generation, 'strictly ascending').toBeGreaterThan(last) last = f.generation } } } expect(last).toBeGreaterThan(0) }, 120000) })