165 lines
6 KiB
TypeScript
165 lines
6 KiB
TypeScript
|
|
/**
|
||
|
|
* @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<string, string[]> // 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)
|
||
|
|
})
|
||
|
|
})
|