mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-28 14:59:47 +00:00
* fix(cycle): drain PGLite synth subagents inline (takeover of #2699) PGLite holds an exclusive file lock on its embedded data-dir, so no separate Minions worker can serve the subagent children the synthesize phase enqueues — they sat in 'waiting' until waitForCompletion timed out. Drain a private per-run child queue inline (claim → run → complete/fail, plus the promote/stall/timeout housekeeping a worker would perform). No-op on Postgres, where children stay on the shared 'default' queue. Rebased onto the reworked synthesize (config.subagentTimeoutMs, #1586 source scoping): the inline job context now carries deadlineAtMs from the claim-time timeout_at stamp, and opts.yieldDuringPhase is ticked on a 60s keepalive while each child runs so the 5-min cycle lock TTL refreshes across long (up to 30-min) children. Co-authored-by: TheRealMrSystem <TheRealMrSystem@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(lint): --exclude flag for mixed-content repos (takeover of #2649) Adds --exclude=a,b (and LintOpts.exclude) so mixed-content repos can skip software trees and repo metafiles by basename when collecting pages. The only built-in default is node_modules — vendored dependency trees are never knowledge pages; dot/underscore entries were already skipped by the walk. Diverges from #2649 deliberately: the original hardcoded an opinionated default list (README.md, CHANGELOG.md, CLAUDE.md, test/ dirs at any depth, plus fork-specific filenames), which silently changed lint counts for every existing repo. Those are repo policy — pass --exclude. Co-authored-by: ryangu00 <ryangu00@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cycle): enforce per-job timeout_ms in the PGLite inline subagent drain The inline drain claimed children with deadlineAtMs derived from timeout_at but never armed the worker's timeout timer — and the handleTimeouts sweep only runs between jobs, so nothing could stop a child that blew past its 30-min timeout_ms. A hung LLM call wedged the drain loop (and the whole cycle) indefinitely, with the 60s keepalive refreshing the cycle lock forever. Worker.ts parity: arm a timer from the claim-time timeout_at stamp, abort ctx.signal on fire, and dead-letter (never delayed-retry) timed-out children, mirroring handleTimeouts' stall→retry / timeout→dead split. Regression test: a child with timeout_ms=100 whose handler only ends on ctx.signal abort is dead-lettered with 'timeout exceeded'; pre-fix the test hangs to its 30s timeout. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Garry Tan <garrytan@gmail.com> Co-authored-by: TheRealMrSystem <TheRealMrSystem@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: ryangu00 <ryangu00@users.noreply.github.com>
187 lines
8.1 KiB
TypeScript
187 lines
8.1 KiB
TypeScript
import { describe, test, expect } from 'bun:test';
|
|
import { lintContent, fixContent } from '../src/commands/lint.ts';
|
|
|
|
describe('lintContent', () => {
|
|
test('detects LLM preamble "Of course"', () => {
|
|
const content = 'Of course. Here is a detailed brain page for Jane Doe.\n\n# Jane Doe\n\nContent.';
|
|
const issues = lintContent(content, 'test.md');
|
|
expect(issues.some(i => i.rule === 'llm-preamble')).toBe(true);
|
|
});
|
|
|
|
test('detects LLM preamble "I\'ve created"', () => {
|
|
const content = "I've created a comprehensive brain page for the company.\n\n# Acme\n\nContent.";
|
|
const issues = lintContent(content, 'test.md');
|
|
expect(issues.some(i => i.rule === 'llm-preamble')).toBe(true);
|
|
});
|
|
|
|
test('detects LLM preamble "Certainly"', () => {
|
|
const content = 'Certainly. Here is the brain page.\n\n# Page\n\nContent.';
|
|
const issues = lintContent(content, 'test.md');
|
|
expect(issues.some(i => i.rule === 'llm-preamble')).toBe(true);
|
|
});
|
|
|
|
test('no false positive on normal content', () => {
|
|
const content = '---\ntitle: Test\ntype: person\ncreated: 2026-04-11\n---\n\n# Test\n\nNormal content.';
|
|
const issues = lintContent(content, 'test.md');
|
|
expect(issues.filter(i => i.rule === 'llm-preamble')).toHaveLength(0);
|
|
});
|
|
|
|
test('detects wrapping code fences', () => {
|
|
const content = '```markdown\n---\ntitle: Test\n---\n\n# Test\n\nContent.\n```';
|
|
const issues = lintContent(content, 'test.md');
|
|
expect(issues.some(i => i.rule === 'code-fence-wrap')).toBe(true);
|
|
});
|
|
|
|
test('no false positive: page CONTAINS an inner ```markdown code block', () => {
|
|
// Real-world case: a docs/SKILL page that shows a markdown example inline.
|
|
// Before this fix, the detector used the /m flag so ^/$ matched start/end
|
|
// of any line, which fired on any file that simply contained a ```markdown
|
|
// line. But fixContent's regex has no /m flag and can only strip whole-file
|
|
// wrappers, so the issue was reported as "fixable: true" yet never fixed.
|
|
const content =
|
|
'---\ntitle: Skill\n---\n\n# Skill\n\nExample input shape:\n\n' +
|
|
'```markdown\n# Inner page\nContent.\n```\n\nThat ends the example.\n';
|
|
const issues = lintContent(content, 'test.md');
|
|
expect(issues.filter(i => i.rule === 'code-fence-wrap')).toHaveLength(0);
|
|
});
|
|
|
|
test('no false positive: multiple inner ```markdown blocks', () => {
|
|
// Documentation pages frequently include several markdown examples.
|
|
const content =
|
|
'---\ntitle: Examples\n---\n\n# Examples\n\nFirst:\n\n' +
|
|
'```markdown\nfoo\n```\n\nSecond:\n\n' +
|
|
'```markdown\nbar\n```\n\nDone.\n';
|
|
const issues = lintContent(content, 'test.md');
|
|
expect(issues.filter(i => i.rule === 'code-fence-wrap')).toHaveLength(0);
|
|
});
|
|
|
|
test('detects placeholder dates', () => {
|
|
const content = '---\ntitle: Test\ntype: person\ncreated: YYYY-MM-DD\n---\n\n# Test';
|
|
const issues = lintContent(content, 'test.md');
|
|
expect(issues.some(i => i.rule === 'placeholder-date')).toBe(true);
|
|
});
|
|
|
|
test('detects XX-XX placeholder dates', () => {
|
|
const content = '---\ntitle: Test\ntype: person\ncreated: 2026-04-11\n---\n\n# Test\n\n- 2026-XX-XX | Something happened';
|
|
const issues = lintContent(content, 'test.md');
|
|
expect(issues.some(i => i.rule === 'placeholder-date')).toBe(true);
|
|
});
|
|
|
|
test('detects missing frontmatter title', () => {
|
|
const content = '---\ntype: person\ncreated: 2026-04-11\n---\n\n# Test';
|
|
const issues = lintContent(content, 'test.md');
|
|
expect(issues.some(i => i.rule === 'missing-title')).toBe(true);
|
|
});
|
|
|
|
test('detects missing frontmatter type', () => {
|
|
const content = '---\ntitle: Test\ncreated: 2026-04-11\n---\n\n# Test';
|
|
const issues = lintContent(content, 'test.md');
|
|
expect(issues.some(i => i.rule === 'missing-type')).toBe(true);
|
|
});
|
|
|
|
test('detects no frontmatter at all', () => {
|
|
const content = '# Test\n\nContent without frontmatter.';
|
|
const issues = lintContent(content, 'test.md');
|
|
expect(issues.some(i => i.rule === 'no-frontmatter')).toBe(true);
|
|
});
|
|
|
|
test('detects empty sections', () => {
|
|
const content = '---\ntitle: Test\ntype: person\ncreated: 2026-04-11\n---\n\n# Test\n\n## What They Believe\n\n[No data yet]\n\n## State\n\nReal content here.';
|
|
const issues = lintContent(content, 'test.md');
|
|
expect(issues.some(i => i.rule === 'empty-section' && i.message.includes('What They Believe'))).toBe(true);
|
|
});
|
|
|
|
test('detects agent placeholder sections', () => {
|
|
const content = '---\ntitle: Test\ntype: person\ncreated: 2026-04-11\n---\n\n# Test\n\n## Summary\n\n*[To be filled by agent]*\n\n## State\n\nContent.';
|
|
const issues = lintContent(content, 'test.md');
|
|
expect(issues.some(i => i.rule === 'empty-section' && i.message.includes('Summary'))).toBe(true);
|
|
});
|
|
|
|
test('clean page has no issues', () => {
|
|
const content = '---\ntitle: Jane Doe\ntype: person\ncreated: 2026-04-11\n---\n\n# Jane Doe\n\n## State\n\nCTO of Acme Corp.\n\n## Timeline\n\n- **2026-04-11** | Met at event [Source: User]';
|
|
const issues = lintContent(content, 'test.md');
|
|
expect(issues).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('fixContent', () => {
|
|
test('removes LLM preamble', () => {
|
|
const input = 'Of course. Here is a detailed brain page for Jane.\n\n# Jane Doe\n\nContent.';
|
|
const fixed = fixContent(input);
|
|
expect(fixed).not.toContain('Of course');
|
|
expect(fixed).toContain('# Jane Doe');
|
|
expect(fixed).toContain('Content.');
|
|
});
|
|
|
|
test('removes wrapping code fences', () => {
|
|
const input = '```markdown\n# Title\n\nContent.\n```';
|
|
const fixed = fixContent(input);
|
|
expect(fixed).not.toContain('```');
|
|
expect(fixed).toContain('# Title');
|
|
});
|
|
|
|
test('cleans up excessive blank lines after fix', () => {
|
|
const input = 'Of course. Here is the brain page.\n\n\n\n# Title\n\nContent.';
|
|
const fixed = fixContent(input);
|
|
expect(fixed).not.toMatch(/\n{3,}/);
|
|
});
|
|
|
|
test('preserves content that needs no fixing', () => {
|
|
const input = '# Normal Title\n\nNormal content.\n';
|
|
expect(fixContent(input)).toBe(input);
|
|
});
|
|
|
|
test('handles multiple preambles', () => {
|
|
const input = 'Sure! Here is the page.\nCertainly. Here is the brain page.\n\n# Title\n\nContent.';
|
|
const fixed = fixContent(input);
|
|
expect(fixed).not.toContain('Sure');
|
|
expect(fixed).not.toContain('Certainly');
|
|
expect(fixed).toContain('# Title');
|
|
});
|
|
});
|
|
|
|
describe('runLintCore exclude (takeover of #2649)', () => {
|
|
const { mkdtempSync, rmSync, mkdirSync, writeFileSync } = require('node:fs') as typeof import('node:fs');
|
|
const { tmpdir } = require('node:os') as typeof import('node:os');
|
|
const { join } = require('node:path') as typeof import('node:path');
|
|
const { runLintCore } = require('../src/commands/lint.ts') as typeof import('../src/commands/lint.ts');
|
|
|
|
const PAGE = '---\ntitle: T\ntype: note\ncreated: 2026-04-11\n---\n\n# T\n\nBody.\n';
|
|
|
|
function makeRepo(): string {
|
|
const dir = mkdtempSync(join(tmpdir(), 'gbrain-lint-excl-'));
|
|
writeFileSync(join(dir, 'page.md'), PAGE);
|
|
writeFileSync(join(dir, 'README.md'), PAGE);
|
|
mkdirSync(join(dir, 'node_modules', 'dep'), { recursive: true });
|
|
writeFileSync(join(dir, 'node_modules', 'dep', 'vendor.md'), PAGE);
|
|
mkdirSync(join(dir, 'software'));
|
|
writeFileSync(join(dir, 'software', 'notes.md'), PAGE);
|
|
return dir;
|
|
}
|
|
|
|
test('node_modules is excluded by default; nothing else is', async () => {
|
|
const dir = makeRepo();
|
|
try {
|
|
const result = await runLintCore({ target: dir, contentSanity: { disabled: true } });
|
|
// page.md + README.md + software/notes.md — vendor.md skipped.
|
|
expect(result.pages_scanned).toBe(3);
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('--exclude basenames skip dirs and files', async () => {
|
|
const dir = makeRepo();
|
|
try {
|
|
const result = await runLintCore({
|
|
target: dir,
|
|
contentSanity: { disabled: true },
|
|
exclude: ['software', 'README.md'],
|
|
});
|
|
expect(result.pages_scanned).toBe(1); // only page.md
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|