Files
gbrain/test/lint.test.ts
Garry Tan baf3517868 feat: v0.9.0 -- smart file storage, publish, production-grade skills (#62)
* feat: battle-tested skill patterns from production deployment

Backport production-learned brain-operations patterns:
- Iron Law of Back-Linking (mandatory bidirectional linking)
- Brain filing rules (file by primary subject, not format)
- Enrichment protocol (7-step pipeline, 3-tier system, person/company templates)
- Media ingest workflows (articles, videos, podcasts, PDFs, screenshots)
- Citation requirements (mandatory [Source: ...] on every fact)
- Test Before Bulk operating principle
- Voice recipe: unicode crash fix, PII scrub, identity-first prompt, DIY STT+LLM+TTS
- X-to-Brain recipe: image OCR, Filtered Stream, tweet rating rubric, cron stagger

* chore: bump version and changelog (v0.8.1)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add _brain-filing-rules.md to CLAUDE.md key files

* feat: smart file upload with TUS resumable and .redirect.yaml pointers

- Supabase Storage auto-selects upload method by file size:
  < 100 MB standard POST, >= 100 MB TUS resumable (6 MB chunks + retry)
- Signed URL generation for private bucket access (1-hour expiry)
- New `upload-raw` command with size routing: small text stays in git,
  large/media files go to cloud with .redirect.yaml pointer
- New `signed-url` command for generating access links
- File resolver supports both .redirect.yaml (v0.9+) and .redirect (legacy)
- Redirect format upgraded: 10 fields with full metadata
- All migration commands (mirror, redirect, restore, clean) handle both formats

* feat: skills reference actual gbrain file commands

- Filing rules document upload-raw, signed-url, and .redirect.yaml format
- Ingest skill uses gbrain files upload-raw for raw source preservation
- Maintain skill adds file storage health checks
- Setup skill adds storage configuration phase with migration guidance
- Voice recipe uses upload-raw for call audio storage
- Migration v0.9.0 with complete storage setup instructions

* chore: bump version and changelog (v0.9.0)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: gbrain publish -- shareable HTML with password protection

First code+skill pair: deterministic code does the work (strip private data,
encrypt with AES-256-GCM, generate self-contained HTML), the skill tells the
agent when and how to use it. 34 new tests.

See: https://x.com/garrytan/status/2042925773300908103

* feat: backlinks check/fix, page lint, and report commands

Three new deterministic tools (zero LLM calls):

- gbrain backlinks check/fix -- scans brain for entity mentions without
  back-links, creates them. Enforces the Iron Law from the skills.
- gbrain lint [--fix] -- catches LLM preambles, code fence wrapping,
  placeholder dates, missing frontmatter, broken citations, empty sections.
  --fix auto-strips fixable artifacts.
- gbrain report --type <name> -- saves timestamped reports to
  brain/reports/{type}/YYYY-MM-DD-HHMM.md for audit trails.

33 new tests (409 total, 0 fail).

* feat: v0.9.0 migration tells agents to swap scripts for built-in commands

Migration file now:
- Lists all 5 new deterministic commands with usage examples
- Includes a script-to-command replacement table (old -> new)
- Tells the agent to find custom script references in AGENTS.md,
  skills, and cron jobs and replace with gbrain commands
- Adds recommended cron jobs for daily backlink fix + weekly lint
- References the Thin Harness, Fat Skills thread

* fix: CLI routing bugs found during DX review

- Fixed subArgs reference error in handleCliOnly (used wrong variable name)
- Renamed gbrain backlinks check/fix to gbrain check-backlinks to avoid
  conflict with existing backlinks operation (per-page incoming links)
- Added TOOLS section to --help output showing publish, check-backlinks,
  lint, report
- Added upload-raw and signed-url to FILES section in --help
- Updated all docs/migration references to use check-backlinks

* fix: security hardening from adversarial review

- XSS: sanitize marked.parse() output (strip script/iframe/on* attrs)
- Path traversal: validate report --type against [a-z0-9-] pattern
- TUS: HEAD request before retry to get server's actual offset (TUS spec)
- Pointer: upload-raw now includes pointer content in JSON output
- Symlinks: use lstatSync in all walkers to prevent directory escape

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 21:46:07 -10:00

119 lines
5.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('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');
});
});