Files
gbrain/test/e2e/quarantine-search-exclusion.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

155 lines
6.5 KiB
TypeScript

/**
* Quarantine search-exclusion E2E (issue #1699, PGLite, no API keys).
*
* Pins the Q1=A confidence-split contract end-to-end:
* - quarantined (junk) page is ABSENT from search but present in get_page.
* - flagged (markup-heavy) page is PRESENT in search WITH content_flag set
* (the agent-warning channel) — flag does not hide.
* - clearing a quarantine (force re-import) re-surfaces the page.
*
* No embedding provider in tests → hybridSearch takes the keyword path,
* which also runs stampContentFlags, so the content_flag contract holds there.
*/
import { describe, test, expect, beforeAll, afterAll, beforeEach } from 'bun:test';
import { PGLiteEngine } from '../../src/core/pglite-engine.ts';
import { resetPgliteState } from '../helpers/reset-pglite.ts';
import { withEnv } from '../helpers/with-env.ts';
import { mkdtempSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { importFromContent } from '../../src/core/import-file.ts';
import { hybridSearch } from '../../src/core/search/hybrid.ts';
import { isQuarantined } from '../../src/core/quarantine.ts';
import { operations } from '../../src/core/operations.ts';
import type { OperationContext } from '../../src/core/operations.ts';
let engine: PGLiteEngine;
beforeAll(async () => {
engine = new PGLiteEngine();
await engine.connect({});
await engine.initSchema();
});
afterAll(async () => {
await engine.disconnect();
});
beforeEach(async () => {
await resetPgliteState(engine);
});
async function withHome<T>(fn: () => Promise<T>): Promise<T> {
const home = mkdtempSync(join(tmpdir(), 'q-search-home-'));
const audit = mkdtempSync(join(tmpdir(), 'q-search-audit-'));
try {
return await withEnv({ GBRAIN_HOME: home, GBRAIN_AUDIT_DIR: audit }, fn);
} finally {
rmSync(home, { recursive: true, force: true });
rmSync(audit, { recursive: true, force: true });
}
}
const KW = 'zubernaut'; // distinctive keyword present in every seeded page
describe('quarantine search exclusion (Q1=A)', () => {
test('quarantined absent from search, flagged present with content_flag, clean present', async () => {
await withHome(async () => {
// Clean page — plain prose with the keyword.
await importFromContent(
engine,
'notes/clean',
`---\ntitle: Clean\ntype: note\n---\n\nA normal note about ${KW} and its properties, written in real sentences.`,
{ noEmbed: true },
);
// Quarantined page — Cloudflare junk containing the keyword.
const qres = await importFromContent(
engine,
'notes/junk',
`---\ntitle: Blocked\ntype: note\n---\n\nCloudflare Ray ID: deadbeef. ${KW} appears here too but this is junk.`,
{ noEmbed: true },
);
expect(qres.quarantined).toBe(true);
// Flagged page — markup-heavy nav blob in the warn window, with the keyword.
const navRow = `| [${KW}](http://a) | [b](http://b) | [c](http://c) | [d](http://d) |\n`;
const fres = await importFromContent(
engine,
'notes/nav',
`---\ntitle: Nav\ntype: note\n---\n\n${navRow.repeat(1200)}`,
{ noEmbed: true },
);
expect(fres.flagged).toBe(true);
expect(fres.flag_reason).toBe('markup_heavy');
const results = await hybridSearch(engine, KW, { limit: 20 });
const slugs = results.map((r) => r.slug);
// Quarantined page is hidden (zero chunks + visibility clause).
expect(slugs).not.toContain('notes/junk');
// Clean + flagged pages are present.
expect(slugs).toContain('notes/clean');
expect(slugs).toContain('notes/nav');
// Agent-warning channel: the flagged result carries content_flag.
const navResult = results.find((r) => r.slug === 'notes/nav');
expect(navResult?.content_flag?.reason).toBe('markup_heavy');
// The clean result does NOT.
const cleanResult = results.find((r) => r.slug === 'notes/clean');
expect(cleanResult?.content_flag).toBeUndefined();
// get_page still returns the quarantined page (reviewable).
const junkPage = await engine.getPage('notes/junk');
expect(junkPage).not.toBeNull();
expect(isQuarantined(junkPage!.frontmatter as Record<string, unknown>)).toBe(true);
});
});
test('get_page op surfaces content_flag for a flagged page (agent-warning channel)', async () => {
await withHome(async () => {
const navRow = `| [${KW}](http://a) | [b](http://b) | [c](http://c) | [d](http://d) |\n`;
await importFromContent(engine, 'notes/navflag', `---\ntitle: Nav\ntype: note\n---\n\n${navRow.repeat(1200)}`, { noEmbed: true });
await importFromContent(engine, 'notes/cleanflag', `---\ntitle: C\ntype: note\n---\n\nplain prose with ${KW}.`, { noEmbed: true });
const getPageOp = operations.find((o) => o.name === 'get_page')!;
const ctx = { engine, config: {}, logger: console, dryRun: false, remote: false } as unknown as OperationContext;
const flagged = (await getPageOp.handler(ctx, { slug: 'notes/navflag' })) as { content_flag?: { reason: string } };
expect(flagged.content_flag?.reason).toBe('markup_heavy');
const clean = (await getPageOp.handler(ctx, { slug: 'notes/cleanflag' })) as { content_flag?: unknown };
expect(clean.content_flag).toBeUndefined();
});
});
test('clearing a quarantine (force re-import) re-surfaces the page in search', async () => {
await withHome(async () => {
const q = await importFromContent(
engine,
'notes/junk2',
`---\ntitle: Blocked\ntype: note\n---\n\nCloudflare Ray ID: cafe. ${KW} keyword present.`,
{ noEmbed: true },
);
expect(q.quarantined).toBe(true);
let slugs = (await hybridSearch(engine, KW, { limit: 20 })).map((r) => r.slug);
expect(slugs).not.toContain('notes/junk2');
// Operator force-clears: re-import the SAME slug with clean content
// (simulating an edit) so the page becomes searchable again.
const cleared = await importFromContent(
engine,
'notes/junk2',
`---\ntitle: Fixed\ntype: note\n---\n\nThis page is now legitimate prose about ${KW} with real content.`,
{ noEmbed: true, forceRechunk: true },
);
expect(cleared.quarantined).toBeUndefined();
const page = await engine.getPage('notes/junk2');
expect(isQuarantined(page!.frontmatter as Record<string, unknown>)).toBe(false);
slugs = (await hybridSearch(engine, KW, { limit: 20 })).map((r) => r.slug);
expect(slugs).toContain('notes/junk2');
});
});
});