mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 00:48:18 +00:00
216 lines
10 KiB
TypeScript
216 lines
10 KiB
TypeScript
/**
|
|
* v0.38.2.0 — descent-time pruning regression suite.
|
|
*
|
|
* The original bug (PR #1287 reported, this PR fixes): `gbrain doctor` hung
|
|
* indefinitely on a 216K-page brain because the frontmatter walker descended
|
|
* into every node_modules / .git / .obsidian / *.raw / ops subtree on disk
|
|
* and let `isSyncable` filter at the leaf — paying the IO cost of stat'ing
|
|
* hundreds of thousands of vendor entries that were never going to be parsed.
|
|
*
|
|
* Why output-based tests don't catch this: `isSyncable` rejects the
|
|
* vendor-tree files at the leaf, so a test that just asserts "no bad
|
|
* markdown reported" passes BOTH before and after Fix 1 (codex outside-voice
|
|
* C6). The load-bearing assertion is `walker did NOT DESCEND` — fired by
|
|
* the new `visitDir` test seam.
|
|
*
|
|
* This file covers both walkers that were missing pruneDir:
|
|
* - brain-writer.ts:walkDir (driven by scanBrainSources / doctor)
|
|
* - frontmatter.ts:collectFiles (driven by `gbrain frontmatter validate`)
|
|
*/
|
|
import { describe, expect, test, beforeAll, afterAll } from 'bun:test';
|
|
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'fs';
|
|
import { execFileSync } from 'child_process';
|
|
import { join, sep } from 'path';
|
|
import { tmpdir } from 'os';
|
|
import { scanBrainSources, walkDir } from '../src/core/brain-writer.ts';
|
|
import { collectFiles } from '../src/commands/frontmatter.ts';
|
|
|
|
/**
|
|
* Build a separator-prefixed path fragment for suffix/substring predicates.
|
|
* The walkers emit native-separator paths, so a hardcoded '/' fragment makes
|
|
* the positive assertions fail on Windows AND — worse — makes every negative
|
|
* `toBe(false)` guard pass vacuously, proving nothing. Byte-identical to the
|
|
* old '/foo/bar' literals on POSIX.
|
|
*/
|
|
const seg = (...parts: string[]) => sep + join(...parts);
|
|
|
|
let root: string;
|
|
|
|
beforeAll(() => {
|
|
root = mkdtempSync(join(tmpdir(), 'walk-prune-'));
|
|
// Real syncable files under regular dirs — walker MUST descend here.
|
|
mkdirSync(join(root, 'people'), { recursive: true });
|
|
writeFileSync(join(root, 'people', 'alice.md'), '---\ntitle: Alice\n---\n\nbody\n');
|
|
mkdirSync(join(root, 'concepts', 'subdir'), { recursive: true });
|
|
writeFileSync(join(root, 'concepts', 'subdir', 'thing.md'), '---\ntitle: Thing\n---\n\nbody\n');
|
|
// Vendor / hidden / generated trees — walker MUST NOT descend.
|
|
mkdirSync(join(root, 'node_modules', 'fake-pkg'), { recursive: true });
|
|
writeFileSync(join(root, 'node_modules', 'fake-pkg', 'README.md'), '# Should not be visited\n');
|
|
mkdirSync(join(root, '.git', 'objects'), { recursive: true });
|
|
writeFileSync(join(root, '.git', 'config'), '[core]\n');
|
|
mkdirSync(join(root, '.obsidian'), { recursive: true });
|
|
writeFileSync(join(root, '.obsidian', 'workspace.json'), '{}');
|
|
mkdirSync(join(root, 'people', 'pedro.raw'), { recursive: true });
|
|
writeFileSync(join(root, 'people', 'pedro.raw', 'source.md'), '---\ntitle: should not visit\n---\n');
|
|
// ops/ is ORDINARY content (#2404) — walker MUST descend (it used to be
|
|
// wrongly pruned, silently excluding user runbooks / ops/tasks).
|
|
mkdirSync(join(root, 'ops', 'logs'), { recursive: true });
|
|
writeFileSync(join(root, 'ops', 'logs', 'run.md'), '---\ntitle: Run\n---\n\nbody\n');
|
|
// Nested node_modules — must also be pruned, not just at the root.
|
|
mkdirSync(join(root, 'people', 'tools', 'node_modules', 'inner'), { recursive: true });
|
|
writeFileSync(join(root, 'people', 'tools', 'node_modules', 'inner', 'a.md'), '---\ntitle: nope\n---\n');
|
|
// Git-submodule pattern: a dir containing `.git` as a FILE (gitfile).
|
|
mkdirSync(join(root, 'people', 'submod'), { recursive: true });
|
|
writeFileSync(join(root, 'people', 'submod', '.git'), 'gitdir: ../../.git/modules/submod\n');
|
|
writeFileSync(join(root, 'people', 'submod', 'README.md'), '---\ntitle: submod page\n---\n');
|
|
});
|
|
|
|
afterAll(() => {
|
|
rmSync(root, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('walkDir (brain-writer.ts) — descent-time pruning', () => {
|
|
test('does NOT descend into node_modules at any depth', () => {
|
|
const visited: string[] = [];
|
|
walkDir(root, () => {}, (dir) => visited.push(dir));
|
|
expect(visited.some(d => d.includes(seg('node_modules')))).toBe(false);
|
|
});
|
|
|
|
test('does NOT descend into .git', () => {
|
|
const visited: string[] = [];
|
|
walkDir(root, () => {}, (dir) => visited.push(dir));
|
|
expect(visited.some(d => d.endsWith(seg('.git')) || d.includes(seg('.git') + sep))).toBe(false);
|
|
});
|
|
|
|
test('does NOT descend into .obsidian (dot-prefix heuristic)', () => {
|
|
const visited: string[] = [];
|
|
walkDir(root, () => {}, (dir) => visited.push(dir));
|
|
expect(visited.some(d => d.includes(seg('.obsidian')))).toBe(false);
|
|
});
|
|
|
|
test('does NOT descend into *.raw sidecar dirs', () => {
|
|
const visited: string[] = [];
|
|
walkDir(root, () => {}, (dir) => visited.push(dir));
|
|
expect(visited.some(d => d.endsWith('.raw'))).toBe(false);
|
|
});
|
|
|
|
test('does NOT descend into git submodule directories (.git as FILE)', () => {
|
|
const visited: string[] = [];
|
|
walkDir(root, () => {}, (dir) => visited.push(dir));
|
|
expect(visited.some(d => d.endsWith(seg('people', 'submod')))).toBe(false);
|
|
});
|
|
|
|
test('DOES descend into regular subdirs and visits .md files there', () => {
|
|
const visited: string[] = [];
|
|
const files: string[] = [];
|
|
walkDir(root, (f) => { files.push(f); }, (dir) => visited.push(dir));
|
|
expect(visited.some(d => d.endsWith(seg('people')))).toBe(true);
|
|
expect(visited.some(d => d.endsWith(seg('concepts', 'subdir')))).toBe(true);
|
|
// ops/ is ordinary content — descended, not pruned (#2404).
|
|
expect(visited.some(d => d.endsWith(seg('ops', 'logs')))).toBe(true);
|
|
expect(files.some(f => f.endsWith(seg('people', 'alice.md')))).toBe(true);
|
|
expect(files.some(f => f.endsWith(seg('concepts', 'subdir', 'thing.md')))).toBe(true);
|
|
expect(files.some(f => f.endsWith(seg('ops', 'logs', 'run.md')))).toBe(true);
|
|
// And explicitly does NOT visit the file under node_modules.
|
|
expect(files.some(f => f.includes(seg('node_modules') + sep))).toBe(false);
|
|
});
|
|
|
|
test('regression: pre-v0.38.2.0 walker would have descended into node_modules and stat\'d every entry', () => {
|
|
// This is the load-bearing assertion. If a future contributor removes
|
|
// the `pruneDir(name, dir)` gate in walkDir, this test fails because
|
|
// visitDir would be called with node_modules paths.
|
|
const descents: string[] = [];
|
|
walkDir(root, () => {}, (d) => descents.push(d));
|
|
const vendor = descents.filter(d => /[\\/](node_modules|\.git|\.obsidian)([\\/]|$)/.test(d) || /\.raw$/.test(d));
|
|
expect(vendor).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('collectFiles (frontmatter.ts) — descent-time pruning parity', () => {
|
|
test('does NOT descend into node_modules at any depth', () => {
|
|
const visited: string[] = [];
|
|
collectFiles(root, (dir) => visited.push(dir));
|
|
expect(visited.some(d => d.includes(seg('node_modules')))).toBe(false);
|
|
});
|
|
|
|
test('does NOT descend into .git, .obsidian, or *.raw', () => {
|
|
const visited: string[] = [];
|
|
collectFiles(root, (dir) => visited.push(dir));
|
|
expect(visited.some(d => d.includes(seg('.git')))).toBe(false);
|
|
expect(visited.some(d => d.includes(seg('.obsidian')))).toBe(false);
|
|
expect(visited.some(d => d.endsWith('.raw'))).toBe(false);
|
|
});
|
|
|
|
test('DOES descend into ops/ — ordinary content, not a vendor tree (#2404)', () => {
|
|
const visited: string[] = [];
|
|
collectFiles(root, (dir) => visited.push(dir));
|
|
expect(visited.some(d => d.endsWith(seg('ops')) || d.includes(seg('ops') + sep))).toBe(true);
|
|
const files = collectFiles(root);
|
|
expect(files.some(f => f.endsWith(seg('ops', 'logs', 'run.md')))).toBe(true);
|
|
});
|
|
|
|
test('does NOT descend into git submodule directories', () => {
|
|
const visited: string[] = [];
|
|
collectFiles(root, (dir) => visited.push(dir));
|
|
expect(visited.some(d => d.endsWith(seg('people', 'submod')))).toBe(false);
|
|
});
|
|
|
|
test('DOES collect .md files under regular subdirs', () => {
|
|
const files = collectFiles(root);
|
|
expect(files.some(f => f.endsWith(seg('people', 'alice.md')))).toBe(true);
|
|
expect(files.some(f => f.endsWith(seg('concepts', 'subdir', 'thing.md')))).toBe(true);
|
|
expect(files.some(f => f.includes(seg('node_modules') + sep))).toBe(false);
|
|
expect(files.some(f => f.includes(seg('.git') + sep))).toBe(false);
|
|
expect(files.some(f => f.includes('.raw' + sep))).toBe(false);
|
|
});
|
|
|
|
test('single-file target returns that file unchanged (no walk)', () => {
|
|
const target = join(root, 'people', 'alice.md');
|
|
const files = collectFiles(target);
|
|
expect(files).toEqual([target]);
|
|
});
|
|
});
|
|
|
|
describe('frontmatter walkers — git-visible file parity', () => {
|
|
test('collectFiles respects .git/info/exclude like sync/import', () => {
|
|
const repo = mkdtempSync(join(tmpdir(), 'frontmatter-git-visible-'));
|
|
try {
|
|
execFileSync('git', ['init'], { cwd: repo, stdio: 'ignore' });
|
|
mkdirSync(join(repo, 'people'), { recursive: true });
|
|
mkdirSync(join(repo, 'local-skills'), { recursive: true });
|
|
writeFileSync(join(repo, '.git', 'info', 'exclude'), 'local-skills/\n');
|
|
writeFileSync(join(repo, 'people', 'alice.md'), '---\ntitle: Alice\n---\n\nbody\n');
|
|
writeFileSync(join(repo, 'local-skills', 'SKILL.md'), '---\nname: bad\n# malformed frontmatter\n');
|
|
|
|
const files = collectFiles(repo).map((f) => f.replace(repo + sep, ''));
|
|
expect(files).toContain(join('people', 'alice.md'));
|
|
expect(files).not.toContain(join('local-skills', 'SKILL.md'));
|
|
} finally {
|
|
rmSync(repo, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('scanBrainSources ignores git-excluded malformed markdown', async () => {
|
|
const repo = mkdtempSync(join(tmpdir(), 'frontmatter-audit-git-visible-'));
|
|
try {
|
|
execFileSync('git', ['init'], { cwd: repo, stdio: 'ignore' });
|
|
mkdirSync(join(repo, 'people'), { recursive: true });
|
|
mkdirSync(join(repo, 'local-skills'), { recursive: true });
|
|
writeFileSync(join(repo, '.git', 'info', 'exclude'), 'local-skills/\n');
|
|
writeFileSync(join(repo, 'people', 'alice.md'), '---\ntitle: Alice\n---\n\nbody\n');
|
|
writeFileSync(join(repo, 'local-skills', 'SKILL.md'), '---\nname: bad\n# malformed frontmatter\n');
|
|
|
|
const engine = {
|
|
executeRaw: async () => [{ id: 'repo', local_path: repo }],
|
|
} as any;
|
|
const report = await scanBrainSources(engine, { sourceId: 'repo' });
|
|
|
|
expect(report.total).toBe(0);
|
|
expect(report.per_source[0].files_scanned).toBe(1);
|
|
expect(report.per_source[0].sample).toEqual([]);
|
|
} finally {
|
|
rmSync(repo, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|