/** * @module tests/integration/graph-audit * @description brain.auditGraph() — the read-only graph-truth instrument. * Laws under test: * (1) a healthy brain audits COHERENT: every canonical verb is returned by the * read path of its source, endpoints exist, no ghosts; * (2) design-hidden edges (internal/system visibility) are counted separately * and never misclassified as index loss; * (3) a verb whose endpoint entity was destroyed at the storage layer (the * scar class) is flagged as a dangling endpoint, loudly; * (4) the classification core flags present-but-invisible and ghost edges * exactly (exercised via injected seams — manufacturing a genuinely stale * adjacency index end-to-end would require corrupting internals the * public API rightly refuses to corrupt). */ import { describe, it, expect, beforeEach, afterEach } from 'vitest' import * as fs from 'node:fs' import * as os from 'node:os' import * as path from 'node:path' import { Brainy } from '../../src/index.js' import { NounType, VerbType } from '../../src/types/graphTypes.js' import { runGraphAudit, type AuditVerbRecord } from '../../src/graph/graphAudit.js' describe('brain.auditGraph() — graph-truth audit', () => { let dir: string let brain: any beforeEach(async () => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'brainy-graph-audit-')) brain = new Brainy({ requireSubtype: false, storage: { type: 'filesystem', path: dir }, silent: true }) await brain.init() }) afterEach(async () => { await brain.close().catch(() => {}) fs.rmSync(dir, { recursive: true, force: true }) }) async function seedGraph(): Promise<{ ids: string[]; verbIds: string[] }> { const ids: string[] = [] for (let i = 0; i < 5; i++) { ids.push( await brain.add({ data: `entity ${i}`, type: NounType.Concept, metadata: { n: i } }) ) } const verbIds: string[] = [] verbIds.push(await brain.relate({ from: ids[0], to: ids[1], type: VerbType.RelatedTo })) verbIds.push(await brain.relate({ from: ids[0], to: ids[2], type: VerbType.Contains })) verbIds.push(await brain.relate({ from: ids[1], to: ids[3], type: VerbType.DependsOn })) verbIds.push(await brain.relate({ from: ids[3], to: ids[4], type: VerbType.RelatedTo })) return { ids, verbIds } } it('audits a healthy brain as coherent, with exact counts', async () => { const { ids } = await seedGraph() const report = await brain.auditGraph() expect(report.coherent).toBe(true) expect(report.verbsInCanonical).toBe(4) expect(report.entitiesInCanonical).toBeGreaterThanOrEqual(ids.length) // VFS root etc. may add system nouns expect(report.sourcesChecked).toBe(3) // ids[0], ids[1], ids[3] expect(report.missingFromReadsCount).toBe(0) expect(report.danglingEndpointsCount).toBe(0) expect(report.readOnlyCount).toBe(0) expect(report.truncatedExamples).toBe(false) }) it('counts design-hidden edges separately and stays coherent', async () => { const { ids } = await seedGraph() await brain.relate({ from: ids[2], to: ids[4], type: VerbType.RelatedTo, visibility: 'internal' }) const report = await brain.auditGraph() expect(report.coherent).toBe(true) // hidden-by-design is NOT a discrepancy expect(report.verbsInCanonical).toBe(5) expect(report.visibilityHiddenCount).toBeGreaterThanOrEqual(1) }) it('flags a destroyed endpoint as a dangling verb (the scar class)', async () => { const { ids } = await seedGraph() // Destroy ids[4] at the STORAGE layer (bypassing remove(), which would // also delete its verbs) — the historical partial-delete scar shape. await brain.storage.deleteNoun(ids[4]) const report = await brain.auditGraph() expect(report.coherent).toBe(false) expect(report.danglingEndpointsCount).toBe(1) expect(report.danglingEndpoints[0].to).toBe(ids[4]) expect(report.danglingEndpoints[0].missingEnd).toBe('to') }) }) describe('runGraphAudit classification core (injected seams)', () => { const verb = (id: string, from: string, to: string): AuditVerbRecord => ({ id, type: 'relatedTo', sourceId: from, targetId: to }) const deps = (opts: { nouns: string[] verbs: AuditVerbRecord[] reads: Record // sourceId -> verb ids the read path returns }) => ({ eachNounId: async (consume: (id: string) => void) => { for (const id of opts.nouns) consume(id) }, eachVerb: async (consume: (v: AuditVerbRecord) => void) => { for (const v of opts.verbs) consume(v) }, readRelationsFrom: async (sourceId: string) => (opts.reads[sourceId] ?? []).map((id) => ({ id })) }) it('flags a canonical verb the read path omits — present but invisible', async () => { const report = await runGraphAudit( deps({ nouns: ['A', 'B', 'C'], verbs: [verb('v1', 'A', 'B'), verb('v2', 'A', 'C')], reads: { A: ['v1'] } // v2 exists canonically but reads miss it }) ) expect(report.coherent).toBe(false) expect(report.missingFromReadsCount).toBe(1) expect(report.missingFromReads[0].verbId).toBe('v2') }) it('flags a read-path edge with no canonical record — a ghost', async () => { const report = await runGraphAudit( deps({ nouns: ['A', 'B'], verbs: [verb('v1', 'A', 'B')], reads: { A: ['v1', 'ghost-9'] } }) ) expect(report.coherent).toBe(false) expect(report.readOnlyCount).toBe(1) expect(report.readOnlyVerbIds).toEqual(['ghost-9']) }) it('caps example lists but keeps counts exact, and says so', async () => { const verbs = Array.from({ length: 10 }, (_, i) => verb(`v${i}`, 'A', 'B')) const report = await runGraphAudit( deps({ nouns: ['A', 'B'], verbs, reads: { A: [] } }), { maxExamples: 3 } ) expect(report.missingFromReadsCount).toBe(10) expect(report.missingFromReads.length).toBe(3) expect(report.truncatedExamples).toBe(true) }) })