Files
gbrain/test/audit/content-sanity-audit.test.ts
T
0bfe0d0c7e v0.42.8.0 feat: content-quality gate on sync — quarantine junk + flag boilerplate (#1699) (#1756)
* feat: content-quality gate on sync — quarantine junk + flag boilerplate (#1699)

Three-tier disposition at the importFromContent narrow waist:
- High-confidence junk (Cloudflare/CAPTCHA interstitial patterns + operator
  literals) -> quarantine (hidden from search, zero chunks) or reject.
- Fuzzy markup-heavy (prose-vs-markup ratio, warn-tier window, code-exempt)
  -> content_flag marker, stays searchable, agent warned.
- Oversize -> existing embed_skip soft-block + content_flag:oversized warning.

Agent-warning channel: SearchResult.content_flag (stamped in hybridSearch +
the keyword-only search op) and a top-level content_flag on get_page.
New quarantine.ts markers, gbrain quarantine CLI (list/clear/scan), doctor
quarantined_pages + flagged_pages checks (engine.executeRaw, works on PGLite),
sources-audit disposition awareness, markup-heavy lint rule, config keys.

Security: gate-owned markers stripped from untrusted (remote MCP) frontmatter
so a write-scoped client can't hide pages or inject the warning channel.
Markers excluded from content_hash so flagged pages don't re-embed every sync.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: document content-quality gate (quarantine + content_flag) for v0.42.8.0

Add CLAUDE.md Key Files + Commands entries for the #1699 content-quality
gate: src/core/quarantine.ts, gbrain quarantine CLI (list/clear/scan),
the agent-warning channel (SearchResult.content_flag + get_page), doctor
quarantined_pages/flagged_pages checks, the markup-heavy lint rule,
sources-audit disposition awareness, and the three new content_sanity
config keys. Regenerate llms-full.txt from CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 23:03:09 -07:00

229 lines
8.7 KiB
TypeScript

import { describe, test, expect } from 'bun:test';
import { mkdtempSync, rmSync, existsSync, readFileSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { withEnv } from '../helpers/with-env.ts';
import {
logContentSanityAssessment,
readRecentContentSanityEvents,
summarizeContentSanityEvents,
computeContentSanityAuditFilename,
type ContentSanityAuditEvent,
} from '../../src/core/audit/content-sanity-audit.ts';
import type { ContentSanityResult } from '../../src/core/content-sanity.ts';
function makeResult(opts: {
bytes?: number;
hard?: boolean;
soft?: boolean;
warn?: boolean;
pattern?: string;
literal?: string;
}): ContentSanityResult {
const junk_pattern_matches: string[] = opts.pattern ? [opts.pattern] : [];
const literal_substring_matches: string[] = opts.literal ? [opts.literal] : [];
const reasons: ContentSanityResult['reasons'] = [];
const reason_messages: string[] = [];
if (opts.soft) {
reasons.push('oversize_block');
reason_messages.push('PAGE_OVERSIZED: body 600000 bytes');
} else if (opts.warn) {
reasons.push('oversize_warn');
reason_messages.push('PAGE_OVERSIZE_WARN: body 100000 bytes');
}
if (junk_pattern_matches.length > 0) {
reasons.push('junk_pattern');
reason_messages.push(`PAGE_JUNK_PATTERN: matched ${junk_pattern_matches.join(', ')}`);
}
if (literal_substring_matches.length > 0) {
reasons.push('literal_substring');
reason_messages.push(`PAGE_JUNK_PATTERN: literal ${literal_substring_matches.join(', ')}`);
}
const shouldQuarantine = !!opts.hard || junk_pattern_matches.length > 0 || literal_substring_matches.length > 0;
const shouldSkipEmbed = !!opts.soft && !shouldQuarantine;
return {
bytes: opts.bytes ?? 1000,
oversize: !!opts.soft,
junk_pattern_matches,
literal_substring_matches,
prose_chars: null,
markup_ratio: null,
reasons,
reason_messages,
shouldQuarantine,
shouldHardBlock: shouldQuarantine,
shouldFlag: shouldSkipEmbed,
flag_reason: shouldSkipEmbed ? 'oversized' : null,
shouldSkipEmbed,
};
}
describe('computeContentSanityAuditFilename', () => {
test('emits the ISO-week prefix shape', () => {
const name = computeContentSanityAuditFilename(new Date('2026-05-24T07:00:00Z'));
expect(name).toMatch(/^content-sanity-\d{4}-W\d{2}\.jsonl$/);
});
});
describe('logContentSanityAssessment (E2E via tempdir)', () => {
test('writes hard-block event', async () => {
const dir = mkdtempSync(join(tmpdir(), 'cs-audit-hard-'));
try {
await withEnv({ GBRAIN_AUDIT_DIR: dir }, async () => {
const result = makeResult({ hard: true, pattern: 'cloudflare_attention_required', bytes: 287 });
logContentSanityAssessment('media/articles/foo', 'straylight-brain', result);
const events = readRecentContentSanityEvents(7);
expect(events.length).toBe(1);
expect(events[0].event_type).toBe('hard_block');
expect(events[0].slug).toBe('media/articles/foo');
expect(events[0].source_id).toBe('straylight-brain');
expect(events[0].junk_pattern_matches).toContain('cloudflare_attention_required');
expect(events[0].bytes).toBe(287);
});
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('writes soft-block event', async () => {
const dir = mkdtempSync(join(tmpdir(), 'cs-audit-soft-'));
try {
await withEnv({ GBRAIN_AUDIT_DIR: dir }, async () => {
const result = makeResult({ soft: true, bytes: 890_000 });
logContentSanityAssessment('media/big-transcript', 'default', result);
const events = readRecentContentSanityEvents(7);
expect(events.length).toBe(1);
expect(events[0].event_type).toBe('soft_block');
expect(events[0].bytes).toBe(890_000);
});
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('writes warn event', async () => {
const dir = mkdtempSync(join(tmpdir(), 'cs-audit-warn-'));
try {
await withEnv({ GBRAIN_AUDIT_DIR: dir }, async () => {
const result = makeResult({ warn: true, bytes: 100_000 });
logContentSanityAssessment('notes/long', 'default', result);
const events = readRecentContentSanityEvents(7);
expect(events.length).toBe(1);
expect(events[0].event_type).toBe('warn');
});
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('skips no-op rows (no reasons + no bypass)', async () => {
const dir = mkdtempSync(join(tmpdir(), 'cs-audit-noop-'));
try {
await withEnv({ GBRAIN_AUDIT_DIR: dir }, async () => {
const result = makeResult({}); // no reasons fire
logContentSanityAssessment('normal-page', 'default', result);
const events = readRecentContentSanityEvents(7);
expect(events.length).toBe(0);
});
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('bypass active overrides hard/soft → records as warn with bypass_active flag', async () => {
const dir = mkdtempSync(join(tmpdir(), 'cs-audit-bypass-'));
try {
await withEnv({ GBRAIN_AUDIT_DIR: dir }, async () => {
const result = makeResult({ hard: true, pattern: 'access_denied' });
logContentSanityAssessment('bypassed', 'default', result, { bypass: true });
const events = readRecentContentSanityEvents(7);
expect(events.length).toBe(1);
expect(events[0].event_type).toBe('warn');
expect(events[0].bypass_active).toBe(true);
});
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('multiple events accumulate in one file', async () => {
const dir = mkdtempSync(join(tmpdir(), 'cs-audit-multi-'));
try {
await withEnv({ GBRAIN_AUDIT_DIR: dir }, async () => {
logContentSanityAssessment('a', 'src', makeResult({ hard: true, pattern: 'access_denied' }));
logContentSanityAssessment('b', 'src', makeResult({ soft: true, bytes: 600000 }));
logContentSanityAssessment('c', 'src', makeResult({ warn: true, bytes: 70000 }));
const events = readRecentContentSanityEvents(7);
expect(events.length).toBe(3);
});
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
});
describe('summarizeContentSanityEvents', () => {
function event(over: Partial<ContentSanityAuditEvent>): ContentSanityAuditEvent {
return {
ts: new Date().toISOString(),
event_type: 'hard_block',
slug: 'test',
source_id: 'default',
bytes: 100,
junk_pattern_matches: [],
literal_substring_matches: [],
reason_messages: [],
...over,
};
}
test('empty input returns zero summary', () => {
const s = summarizeContentSanityEvents([]);
expect(s.total_events).toBe(0);
expect(s.by_type).toEqual({ hard_block: 0, quarantine: 0, reject: 0, flag: 0, soft_block: 0, warn: 0 });
expect(s.top_patterns).toEqual([]);
});
test('counts by type (v0.42 quarantine/reject/flag)', () => {
const s = summarizeContentSanityEvents([
event({ event_type: 'quarantine' }),
event({ event_type: 'quarantine' }),
event({ event_type: 'reject' }),
event({ event_type: 'flag' }),
event({ event_type: 'soft_block' }),
event({ event_type: 'warn' }),
]);
expect(s.by_type).toEqual({ hard_block: 0, quarantine: 2, reject: 1, flag: 1, soft_block: 1, warn: 1 });
expect(s.total_events).toBe(6);
});
test('counts by source', () => {
const s = summarizeContentSanityEvents([
event({ source_id: 'straylight-brain' }),
event({ source_id: 'straylight-brain' }),
event({ source_id: 'default' }),
]);
expect(s.by_source['straylight-brain']).toBe(2);
expect(s.by_source['default']).toBe(1);
});
test('top_patterns sorted desc by count', () => {
const s = summarizeContentSanityEvents([
event({ junk_pattern_matches: ['cloudflare_attention_required'] }),
event({ junk_pattern_matches: ['cloudflare_attention_required'] }),
event({ junk_pattern_matches: ['cloudflare_attention_required'] }),
event({ junk_pattern_matches: ['access_denied'] }),
]);
expect(s.top_patterns[0]).toEqual({ name: 'cloudflare_attention_required', count: 3 });
expect(s.top_patterns[1]).toEqual({ name: 'access_denied', count: 1 });
});
test('literal substring hits count alongside pattern hits', () => {
const s = summarizeContentSanityEvents([
event({ literal_substring_matches: ['reddit_blocked', 'linkedin_wall'] }),
event({ literal_substring_matches: ['reddit_blocked'] }),
]);
expect(s.top_patterns).toContainEqual({ name: 'reddit_blocked', count: 2 });
expect(s.top_patterns).toContainEqual({ name: 'linkedin_wall', count: 1 });
});
});