Files
gbrain/test/orphans-pure-fn.test.ts
Anton Senkovskiy 0a4f062cac fix(orphans): exclude life/events/ chronicle volume from orphan_ratio (#2264) (#3214)
orphan_ratio's denominator is swamped on auto_chronicle brains by the
machine-generated chronicle events (life/events/<day>-<hash>, written
per eligible event) — no inbound links by design. The shipped policy
already excludes raw/atoms/skills/dreaming/daily and extracts/, but
life/events/ was still counted; on a 1,657-page auto_chronicle brain it
was ~72% of the orphan mass, enough to pin the ratio red.

Add 'life/events/' to DENY_PREFIXES — a scoped prefix, NOT the whole
`life/` first-segment, so human-authored life/diary/ (gbrain capture
--type diary) stays IN the denominator. Same shipped hardcoded-class
mechanism as the existing entries; not the #2215 user-config route
(closed not_planned). Knowledge classes (concepts/people/notes/projects)
also stay in, so genuine graph decay still trips.

Regression in test/orphans-pure-fn.test.ts: life/events/ now excluded
(fails before, passes after); life/diary/ and concepts//notes//projects/
pinned as still-counted. doctor's orphan_ratio uses the same shouldExclude
path (getOrphansData; local + doctor-remote MCP), covered transitively.
2026-07-23 14:33:02 -07:00

300 lines
12 KiB
TypeScript

/**
* IRON RULE regression test (per D1 from /plan-eng-review for v0.42.0.0).
*
* Pins byte-identical output between:
* - `gbrain orphans --json` (CLI orchestrator `runOrphans`)
* - `findOrphans(engine, opts)` (canonical pure data fn)
* - `getOrphansData(engine, opts)` (v0.42.0.0 alias for findOrphans)
*
* If a future refactor lets the CLI filter results differently after
* `findOrphans` returns, this test catches the drift. Doctor's
* `orphan_ratio` check imports `getOrphansData`; this test guarantees
* the doctor count cannot disagree with `gbrain orphans --count`.
*
* Hermetic via PGLite. No DATABASE_URL needed.
*/
import { describe, test, expect, beforeAll, afterAll, beforeEach } from 'bun:test';
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
import {
findOrphans,
getOrphansData,
shouldExclude,
runOrphans,
} from '../src/commands/orphans.ts';
let engine: PGLiteEngine;
let logBuffer: string[];
const originalLog = console.log;
function captureConsoleLog(): void {
logBuffer = [];
console.log = (msg?: unknown) => {
logBuffer.push(typeof msg === 'string' ? msg : String(msg));
};
}
function restoreConsoleLog(): void {
console.log = originalLog;
}
beforeAll(async () => {
engine = new PGLiteEngine();
await engine.connect({});
await engine.initSchema();
}, 60_000);
afterAll(async () => {
await engine.disconnect();
restoreConsoleLog();
});
beforeEach(async () => {
// Clean slate per test — keeps the IRON RULE assertions deterministic
// across the file's test suite.
await engine.executeRaw('DELETE FROM links');
await engine.executeRaw('DELETE FROM pages');
});
async function seedFixture(): Promise<void> {
// 5 entity pages: 2 will have inbound links, 3 will be orphans.
// 2 pseudo-pages: should be excluded by default filter.
// 1 content page: links to person-1 + company-1.
await engine.putPage('people/person-1', {
type: 'person', title: 'Person 1', compiled_truth: 'p1', timeline: '', frontmatter: { domain: 'people' },
});
await engine.putPage('people/person-2', {
type: 'person', title: 'Person 2', compiled_truth: 'p2', timeline: '', frontmatter: { domain: 'people' },
});
await engine.putPage('people/person-3', {
type: 'person', title: 'Person 3', compiled_truth: 'p3', timeline: '', frontmatter: { domain: 'people' },
});
await engine.putPage('companies/company-1', {
type: 'company', title: 'Company 1', compiled_truth: 'c1', timeline: '', frontmatter: { domain: 'companies' },
});
await engine.putPage('companies/company-2', {
type: 'company', title: 'Company 2', compiled_truth: 'c2', timeline: '', frontmatter: { domain: 'companies' },
});
// Pseudo-pages — should be excluded from default orphan results.
await engine.putPage('_atlas', {
type: 'note', title: 'Atlas', compiled_truth: 'atlas', timeline: '', frontmatter: {},
});
await engine.putPage('templates/meeting', {
type: 'note', title: 'Meeting template', compiled_truth: 'tmpl', timeline: '', frontmatter: {},
});
// Content page that links to person-1 + company-1.
await engine.putPage('writing/post-1', {
type: 'note', title: 'Post 1', compiled_truth: 'content', timeline: '', frontmatter: {},
});
await engine.addLinksBatch([
{ from_slug: 'writing/post-1', to_slug: 'people/person-1', link_type: 'mentions', link_source: 'markdown', context: '' },
{ from_slug: 'writing/post-1', to_slug: 'companies/company-1', link_type: 'mentions', link_source: 'markdown', context: '' },
]);
}
describe('orphans pure data fn — IRON RULE byte-identical contract', () => {
test('getOrphansData is the same function reference as findOrphans', () => {
expect(getOrphansData).toBe(findOrphans);
});
test('findOrphans and getOrphansData produce deep-equal output', async () => {
await seedFixture();
const viaFindOrphans = await findOrphans(engine, { includePseudo: false });
const viaGetOrphansData = await getOrphansData(engine, { includePseudo: false });
expect(viaGetOrphansData).toEqual(viaFindOrphans);
});
test('includePseudo: false vs true changes excluded count', async () => {
await seedFixture();
const def = await findOrphans(engine, { includePseudo: false });
const all = await findOrphans(engine, { includePseudo: true });
expect(all.excluded).toBe(0);
expect(def.excluded).toBeGreaterThan(0);
expect(all.total_orphans).toBeGreaterThanOrEqual(def.total_orphans);
});
test('CLI --json output deep-equals findOrphans return value', async () => {
await seedFixture();
const direct = await findOrphans(engine, { includePseudo: false });
captureConsoleLog();
try {
await runOrphans(engine, ['--json']);
} finally {
restoreConsoleLog();
}
expect(logBuffer.length).toBe(1);
const cliOutput = JSON.parse(logBuffer[0]!);
// IRON RULE: CLI --json output must deep-equal the pure-fn output.
// If a future change adds CLI-side post-filtering, this fires.
expect(cliOutput).toEqual(direct);
});
test('CLI --count matches total_orphans from pure fn', async () => {
await seedFixture();
const direct = await findOrphans(engine, { includePseudo: false });
captureConsoleLog();
try {
await runOrphans(engine, ['--count']);
} finally {
restoreConsoleLog();
}
expect(logBuffer.length).toBe(1);
expect(logBuffer[0]).toBe(String(direct.total_orphans));
});
test('CLI --count with --include-pseudo matches pure-fn total_orphans (includePseudo: true)', async () => {
await seedFixture();
const direct = await findOrphans(engine, { includePseudo: true });
captureConsoleLog();
try {
await runOrphans(engine, ['--count', '--include-pseudo']);
} finally {
restoreConsoleLog();
}
expect(logBuffer[0]).toBe(String(direct.total_orphans));
});
});
describe('shouldExclude — orphan filter regression (preserve curation)', () => {
test('pseudo-pages are excluded', () => {
expect(shouldExclude('_atlas')).toBe(true);
expect(shouldExclude('_index')).toBe(true);
expect(shouldExclude('_orphans')).toBe(true);
});
test('auto-suffix patterns are excluded', () => {
expect(shouldExclude('people/_index')).toBe(true);
expect(shouldExclude('writing/log')).toBe(true);
});
test('raw segment is excluded', () => {
expect(shouldExclude('media/x/raw/post')).toBe(true);
expect(shouldExclude('raw/chats/claude-code/session')).toBe(true);
});
test('leading raw/ segment is excluded (same archive convention)', () => {
expect(shouldExclude('raw/whatsapp/2025-01/chat-log')).toBe(true);
expect(shouldExclude('raw/transcripts/meeting')).toBe(true);
// 'rawhide/...' must NOT match — prefix is 'raw/', not 'raw'.
expect(shouldExclude('rawhide/notes')).toBe(false);
});
test('daily-log pages are excluded (calendar/email integrations write these)', () => {
expect(shouldExclude('daily/calendar/2025/2025-01-01')).toBe(true);
expect(shouldExclude('daily/x/2025-06-13')).toBe(true);
});
test('outputs/ plural prefix is excluded like output/', () => {
expect(shouldExclude('outputs/render-batch-3')).toBe(true);
});
test('readme folder descriptors are excluded at any depth', () => {
expect(shouldExclude('readme')).toBe(true);
expect(shouldExclude('index')).toBe(true);
expect(shouldExclude('projects/readme')).toBe(true);
expect(shouldExclude('media/readme')).toBe(true);
// A page merely mentioning readme in its name is NOT excluded.
expect(shouldExclude('concepts/readme-driven-development')).toBe(false);
});
test('machine-generated extracts pages are excluded', () => {
expect(shouldExclude('extracts/2026-06-12/takes.proposed/host/propose-x/round-single')).toBe(true);
});
test('inbox intake-tray pages are excluded (same rationale as daily)', () => {
expect(shouldExclude('inbox/some-renewal-notice-2026-06-22')).toBe(true);
});
test('root schema and log pages are excluded', () => {
expect(shouldExclude('schema')).toBe(true);
expect(shouldExclude('log')).toBe(true);
expect(shouldExclude('concepts/schema-design')).toBe(false);
});
test('deny-prefixes are excluded', () => {
expect(shouldExclude('templates/meeting')).toBe(true);
expect(shouldExclude('dashboards/_index')).toBe(true);
expect(shouldExclude('scripts/build')).toBe(true);
expect(shouldExclude('output/foo')).toBe(true);
// #2264 — auto_chronicle event volume (life/events/…) is a machine leaf.
expect(shouldExclude('life/events/2026-08-01-abc123')).toBe(true);
});
test('first-segment exclusions fire', () => {
expect(shouldExclude('scratch/notes')).toBe(true);
expect(shouldExclude('thoughts/today')).toBe(true);
expect(shouldExclude('catalog/movies')).toBe(true);
expect(shouldExclude('entities/anonymous')).toBe(true);
expect(shouldExclude('atoms/fact-123')).toBe(true);
expect(shouldExclude('skills/gbrain-operations')).toBe(true);
expect(shouldExclude('dreaming/light/2026-07-20')).toBe(true);
expect(shouldExclude('daily/2026-07-20')).toBe(true);
expect(shouldExclude('agent-openclaw/daily/2026-07-20')).toBe(true);
});
test('workspace convention slugs are excluded', () => {
expect(shouldExclude('_brain-conventions')).toBe(true);
expect(shouldExclude('_templates/decision')).toBe(true);
expect(shouldExclude('extracts/2026-06-30/takes.proposed/round-single')).toBe(true);
expect(shouldExclude('2026-07-20')).toBe(true);
expect(shouldExclude('2026-07-20-qa-sweep')).toBe(true);
expect(shouldExclude('agents/arya/identity')).toBe(true);
expect(shouldExclude('agents/arya/memory/dreaming/deep/2026-07-20')).toBe(true);
});
test('regular slugs are NOT excluded', () => {
expect(shouldExclude('people/alice')).toBe(false);
expect(shouldExclude('companies/acme')).toBe(false);
expect(shouldExclude('writing/post-1')).toBe(false);
expect(shouldExclude('agents/arya/qa-reports/launch-review')).toBe(false);
// #2264 — knowledge classes must STAY in the denominator so real graph decay still trips.
expect(shouldExclude('concepts/information-architecture')).toBe(false);
expect(shouldExclude('notes/some-note')).toBe(false);
expect(shouldExclude('projects/proj-x')).toBe(false);
// #2264 — only life/events/ is excluded; human-authored life/diary/ stays counted.
expect(shouldExclude('life/diary/2026-08-01-xyz')).toBe(false);
});
});
describe('getHealth orphan_pages uses shared exclusion policy', () => {
test('excluded convention islands do not count against health', async () => {
await engine.putPage('_templates/decision', {
type: 'template', title: 'Decision', compiled_truth: 'template', timeline: '', frontmatter: {},
});
await engine.putPage('skills/arya/source-check', {
type: 'concept', title: 'Skill', compiled_truth: 'skill', timeline: '', frontmatter: {},
});
await engine.putPage('agents/arya/identity', {
type: 'note', title: 'Identity', compiled_truth: 'identity', timeline: '', frontmatter: {},
});
await engine.putPage('people/alice', {
type: 'person', title: 'Alice', compiled_truth: 'real island', timeline: '', frontmatter: {},
});
const health = await engine.getHealth();
expect(health.orphan_pages).toBe(1);
});
test('per-brain config overrides (orphans.exclude_*) also apply to health', async () => {
await engine.putPage('my-private-folder/secret-ref', {
type: 'note', title: 'Ref', compiled_truth: 'ref', timeline: '', frontmatter: {},
});
await engine.putPage('one-off-fixture-page', {
type: 'note', title: 'Fixture', compiled_truth: 'fixture', timeline: '', frontmatter: {},
});
await engine.putPage('people/alice', {
type: 'person', title: 'Alice', compiled_truth: 'real island', timeline: '', frontmatter: {},
});
expect((await engine.getHealth()).orphan_pages).toBe(3);
await engine.setConfig('orphans.exclude_prefixes', 'my-private-folder/');
await engine.setConfig('orphans.exclude_slugs', 'one-off-fixture-page');
expect((await engine.getHealth()).orphan_pages).toBe(1);
await engine.unsetConfig('orphans.exclude_prefixes');
await engine.unsetConfig('orphans.exclude_slugs');
});
});