Files
gbrain/test/turn-context.test.ts
T
Garry TanandClaude Fable 5 c2cc8b0207 v0.45.1.0 feat(context): hook-lane feedback loop + cross-turn dedupe for the harness hooks (#4025)
* refactor(context): extract pure gateVolunteeredPointers (no behavior change)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(context): volunteer-shaped resolve IPC — channel/cwd/volunteer fields, handler-result widening, server-side gate + validated source + delivery-point logging; listener on both engines

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(cli): gbrain volunteer-hook — one-shot harness hook entry (IPC-first; Postgres-only fallback; startup-hook-exempt)

T0 evidence: a UserPromptSubmit hook's additionalContext is recorded in the
session transcript as a structured hook_additional_context attachment and
transcript_path is stable across turns — cross-turn dedupe reads ONLY those
attachments (never raw tail substrings). Live smoke: seeded brain + running
serve → pointer injected via IPC at 0.43s wall, turn 2 suppressed, one event
banked channel=claude-code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(recipes): claude-code-reflex + codex-reflex (experimental) hook adapters

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(doctor): volunteer_channels per-adapter visibility

Engine-aware sibling of retrieval_reflex_health: groups
context_volunteer_events by channel (7d) so operators can see which push
adapters fire; message separates installed-but-unregistered from
registered-but-quiet; pre-v117 tolerant; info-only status.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(context): hook-lane feedback loop + cross-turn dedupe — converge cathedral-3 onto the #3975 harness hooks

The #3975 hook lane volunteered pages invisibly (nothing logged to
context_volunteer_events) and re-volunteered the same page every turn an
entity was mentioned (no priorContextText). This convergence ports the
cathedral-3 branch's two gap-fills into the blessed lane and retires its
parallel surface:

- transcripts: parseTranscript extracts injectedContextBlocks (structured
  hook_additional_context attachments — the T0-verified recorded shape of
  our own injections; never raw-turn substring matching)
- hook user-prompt: feeds those blocks as priorContextText (volunteer once
  per session, not once per mention) + sends a feedback-loop channel
  (--harness <claude-code|codex>, default claude-code); hook joins
  STARTUP_HOOK_SKIP_COMMANDS (per-prompt check-update spawn class, grep-pinned)
- turn-context: result exposes post-trim volunteered survivors
- resolve-ipc: additive TurnContextRequest.channel + onTurnContextDelivered
  seam (fires post-write only — abandoned blocks are never counted)
- serve: delivery callback logs volunteered pages + pointers per channel
- doctor: volunteer_channels check (per-channel 7d visibility; separates
  installed-but-unregistered from registered-but-quiet; pre-v117 tolerant)

Retired (never released): gbrain volunteer-hook, hook-adapters.ts,
claude-code-reflex + codex-reflex recipes, the volunteer-shaped resolve wire
extension. Kept: gateVolunteeredPointers extraction, VolunteerChannel
claude-code/codex values, the real hook-transcript fixture.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: regenerate llms bundles for the hook-lane doc updates

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: coverage for the hook-lane delivery wiring

Extract logTurnContextDeliveryFireAndForget into volunteer-events.ts so the
shipped serve callback is unit-testable (channel fallback, sessionId clamp,
pointer+page rows in ONE synchronous registration — the double dynamic import
could register the pending write too late to survive process exit). Direct
units for isVolunteerChannel, explicit-channel logDeliveredReflexPointers,
and the pure gate (idempotency pin; stale parity claim in its doc corrected).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: pre-landing review fixes — 7-reviewer batch

Multi-confirmed: volunteer_channels now runs on the LOCAL doctor path too
(buildChecks; docs pointed there); transient DB errors no longer misreported
as pre-v117; priorContextText deduped + 32KB-capped hook-side. Red team:
delivery counts reconciled against the hook heartbeat (CAUTION when mostly
degraded — write-accept isn't injection); IPC clamp drops advisory
priorContextText BEFORE any window turn; foreign hook_additional_context
blocks (no gbrain marker) excluded from dedupe input; engine-aware quiet
guidance (Postgres brains aren't told to chase hook registration); 2MB dedupe
horizon documented. Security: wire channel claims restricted to harness
channels. DRY: VOLUNTEER_CHANNELS single source, shared reflexPointerRationale,
SESSION_ID_MAX_LEN shared with the op clamp; dead channel param dropped from
logDeliveredReflexPointers. Tests: budget-trim volunteered invariant under
real trim pressure, poll/ordering-based delivery-seam tests (no fixed sleeps),
doctor branch + engine-parity-row tests, wiring source pins, cap pins;
fixture pruned to load-bearing lines (environment inventory removed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: adversarial-review fixes — cross-model P1 + Claude adversarial batch

P1 (codex structured review + Claude adversarial, cross-model): the remote
run_doctor path now threads the caller's source scope into
volunteer_channels — a source-bound token can no longer see other sources'
activity counts/timestamps (must-never-violate source-isolation rule);
local trusted doctor stays brain-wide. Claude adversarial: IPC connection
handler gains a one-request re-entrancy guard (trailing bytes mid-await
double-processed the same line → duplicate handler work + duplicate
delivery logging); partial client-side trims now heartbeat as reason
'trimmed' (delivery-count drift visibility); heartbeat reconciliation uses
the same 7-day window as the counts + a 5-sample floor; one oversized
dedupe block no longer evicts all smaller ones (continue, not break);
quiet-channel guidance no longer claims old serves attribute hook traffic
to reflex (they log nothing); 'transient' claim softened to evidence;
marker-filter + unknown-channel-default comments state their honest limits.
Pins: scoped-query SQL/params, re-entrancy single-fire, updated wiring grep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: restamp bootstrap runbook + regenerate vendored template repo for v0.45.1.0

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: update project documentation for v0.45.1.0

Post-ship /document-release pass: fold the post-doc review fixes into the
reference docs (KEY_FILES: source-scoped remote volunteer_channels + heartbeat
reconciliation, 32KB-capped dedupe input, marker filter, IPC one-request
guard + clamp order), fix BRAINBENCH's now-stale memoryless-hook rationale,
and mark the doctor-check half of the push-context TODO shipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: cross-model doc-review fixes for v0.45.1.0

Codex doc review against the shipped diff: push-context channel table gains
the harness-hook row (three-channels claim was stale), the guide states the
PGLite-socket topology limit and the bounded dedupe horizon, delivery-point
claims are hedged to match the code (write-accept isn't proof of injection;
the doctor reconciles against the hook heartbeat, which can't inspect
registration), BRAINBENCH's future-tense hooks-PR sentence updated now the
hook has landed, and the adapter-flip + resolver-channel TODOS reflect the
current lane count. llms-full.txt regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(hook): document --harness in the user-prompt usage text

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 22:45:44 -07:00

394 lines
18 KiB
TypeScript

/**
* Turn-context assembly tests (agent-bootstrap plan: S3#1 visibility fence,
* ENG-1 8KB budget, ENG-11 hot-memory cache reuse, CX-P1.2 subordinate
* envelope, CX2-11 typed session identity + two-session cache isolation).
*
* Hermetic in-memory PGLite; engine-agnostic assembly (no engine-specific SQL
* in the module under test).
*/
import { describe, test, expect, beforeAll, afterAll, beforeEach } from 'bun:test';
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
import {
assembleTurnContext,
TURN_CONTEXT_ENVELOPE,
TURN_CONTEXT_DEFAULT_MAX_BYTES,
} from '../src/core/context/turn-context.ts';
import {
getBrainHotMemoryMeta,
bumpHotMemoryCache,
__resetHotMemoryCacheForTests,
} from '../src/core/facts/meta-hook.ts';
import { buildOperationContext } from '../src/mcp/dispatch.ts';
import type { OperationContext } from '../src/core/operations.ts';
import type { GBrainConfig } from '../src/core/config.ts';
import type { BrainEngine } from '../src/core/engine.ts';
import type { WindowTurn } from '../src/core/context/entity-salience.ts';
import {
loadCorpusPages,
loadCorpusBeliefs,
loadCorpusBeliefData,
loadCorpusQueries,
} from './helpers/bootstrap-corpus.ts';
let engine: PGLiteEngine;
beforeAll(async () => {
engine = new PGLiteEngine();
await engine.connect({});
await engine.initSchema();
}, 120_000);
afterAll(async () => {
await engine.disconnect();
});
beforeEach(async () => {
__resetHotMemoryCacheForTests();
await engine.executeRaw('DELETE FROM page_aliases').catch(() => {});
await engine.executeRaw('DELETE FROM facts').catch(() => {});
await engine.executeRaw('DELETE FROM pages').catch(() => {});
});
async function seedPage(slug: string, title: string, body: string) {
await engine.executeRaw(
`INSERT INTO pages (slug, source_id, type, title, compiled_truth, timeline)
VALUES ($1, 'default', 'person', $2, $3, '')`,
[slug, title, body],
);
}
async function seedFact(fact: string, visibility: 'private' | 'world', opts: { session?: string; entity?: string } = {}) {
await engine.insertFact(
{
fact,
kind: 'fact',
entity_slug: opts.entity ?? 'people/alice-example',
source: 'test',
visibility,
source_session: opts.session ?? null,
},
{ source_id: 'default' },
);
}
describe('assembleTurnContext', () => {
test('envelope first, pointers + world facts in; private facts NEVER [CX-P1.2, S3#1]', async () => {
await seedPage('people/alice-example', 'Alice Example', 'Alice Example is a founder at acme-example.');
await seedFact('WORLD-FACT alice prefers async updates', 'world');
await seedFact('PRIVATE-FACT sensitive valuation note', 'private');
const r = await assembleTurnContext(engine, {
sourceId: 'default',
window: [{ role: 'user', text: 'catching up with Alice Example tomorrow' }],
});
expect(r.text.startsWith(TURN_CONTEXT_ENVELOPE)).toBe(true);
expect(r.pointers.length).toBe(1);
expect(r.pointers[0].slug).toBe('people/alice-example');
expect(r.text).toContain('people/alice-example');
expect(r.text).toContain('WORLD-FACT');
expect(r.text).not.toContain('PRIVATE-FACT');
expect(r.text).not.toContain('sensitive valuation note');
expect(r.factsCount).toBe(1);
expect(r.degradedReason).toBeUndefined();
expect(Buffer.byteLength(r.text, 'utf8')).toBeLessThanOrEqual(TURN_CONTEXT_DEFAULT_MAX_BYTES);
});
test('volunteer section dedupes against reflex pointers (slug appears once)', async () => {
await seedPage('people/alice-example', 'Alice Example', 'Alice Example founder profile.');
const r = await assembleTurnContext(engine, {
sourceId: 'default',
window: [
{ role: 'user', text: 'Alice Example pinged me' },
{ role: 'assistant', text: 'noted — Alice Example wants a follow-up' },
],
});
const hits = r.text.split('`people/alice-example`').length - 1;
expect(hits).toBe(1);
});
test('result.volunteered carries the POST-trim survivors (feedback-loop input, never the pre-budget pool)', async () => {
// Two distinct entities: one resolves as a reflex pointer (subject of the
// newest turn), the other only via the volunteer arm.
await seedPage('people/alice-example', 'Alice Example', 'Alice Example founder profile.');
await seedPage('companies/widget-co', 'Widget Co', 'Widget Co company page.');
const r = await assembleTurnContext(engine, {
sourceId: 'default',
window: [
{ role: 'user', text: 'Widget Co update?' },
{ role: 'user', text: 'and what about Widget Co and Alice Example today?' },
],
});
// Whatever the split between arms, the union of pointers + volunteered is
// exactly what the rendered text carries — the delivery-point logger's
// contract (logging a trimmed-out page would corrupt precision stats).
const surfaced = [...r.pointers.map((p) => p.slug), ...(r.volunteered ?? []).map((v) => v.slug)];
for (const slug of surfaced) {
expect(r.text).toContain(slug);
}
expect(surfaced.length).toBeGreaterThan(0);
// No overlap between the arms (volunteer dedupes against pointers).
expect(new Set(surfaced).size).toBe(surfaced.length);
// volunteered is always present on a fresh assembly (empty when none).
expect(Array.isArray(r.volunteered)).toBe(true);
});
test('budget-trimmed volunteered pages are excluded from result.volunteered (never the pre-budget pool)', async () => {
// Five distinct entities: pointers arm takes its cap, the volunteer arm
// takes the rest; a tight maxBytes then forces the volunteered while-loop
// to drop at least one. The invariant under test: result.volunteered is
// EXACTLY the set present in the rendered text — a copy-before-trim
// refactor would silently log trimmed-out pages to the precision stats.
// Lowercase lead-in: a capitalized verb would merge into the first
// candidate's capitalized run and drop it from extraction.
const names = ['Aaa Corp', 'Bbb Corp', 'Ccc Corp', 'Ddd Corp', 'Eee Corp', 'Fff Corp', 'Ggg Corp'];
for (const n of names) {
const slug = `companies/${n.split(' ')[0].toLowerCase()}`;
await seedPage(slug, n, `${n}${'synopsis filler '.repeat(20)}.`);
}
const window = [{ role: 'user' as const, text: `we saw ${names.join(', ')} today` }];
const full = await assembleTurnContext(engine, { sourceId: 'default', window });
const fullVolunteered = full.volunteered ?? [];
expect(fullVolunteered.length).toBeGreaterThan(1); // needs ≥2 to trim meaningfully
// Budget sized to keep the block but force dropping ≥1 volunteered page.
const tight = Math.max(600, Buffer.byteLength(full.text, 'utf8') - 150);
const r = await assembleTurnContext(engine, { sourceId: 'default', window, maxBytes: tight });
expect(r.degradedReason).toBe('budget_trimmed');
const trimmed = r.volunteered ?? [];
expect(trimmed.length).toBeLessThan(fullVolunteered.length); // ≥1 dropped
for (const v of trimmed) expect(r.text).toContain(v.slug); // survivors ARE in the text
const droppedSlugs = fullVolunteered.map((v) => v.slug).filter((s) => !trimmed.some((v) => v.slug === s));
expect(droppedSlugs.length).toBeGreaterThan(0);
for (const s of droppedSlugs) expect(r.text).not.toContain(s); // dropped are NOT in the text
});
test('budget trims facts BEFORE pointers, lowest confidence first [ENG-1]', async () => {
await seedPage('people/alice-example', 'Alice Example', 'Alice Example founder profile.');
for (let i = 0; i < 8; i++) {
await seedFact(`WORLD-FACT-${i} ${'detail '.repeat(20)}`, 'world');
}
const maxBytes = 500;
const r = await assembleTurnContext(engine, {
sourceId: 'default',
window: [{ role: 'user', text: 'sync with Alice Example' }],
maxBytes,
});
expect(Buffer.byteLength(r.text, 'utf8')).toBeLessThanOrEqual(maxBytes);
expect(r.degradedReason).toBe('budget_trimmed');
// The pointer survives — facts absorb the trim first.
expect(r.pointers.length).toBe(1);
expect(r.text).toContain('people/alice-example');
expect(r.factsCount).toBeLessThan(8);
});
test('absurdly small budget → empty text rather than a broken envelope', async () => {
await seedPage('people/alice-example', 'Alice Example', 'Alice Example founder profile.');
const r = await assembleTurnContext(engine, {
sourceId: 'default',
window: [{ role: 'user', text: 'ping Alice Example' }],
maxBytes: 10,
});
expect(r.text).toBe('');
expect(r.degradedReason).toBe('budget_trimmed');
});
test('empty window still serves the hot-memory digest (session-start case)', async () => {
await seedFact('WORLD-FACT standup moved to 9am', 'world');
const r = await assembleTurnContext(engine, { sourceId: 'default', window: [] });
expect(r.pointers.length).toBe(0);
expect(r.factsCount).toBe(1);
expect(r.text).toContain('WORLD-FACT standup moved to 9am');
expect(r.text.startsWith(TURN_CONTEXT_ENVELOPE)).toBe(true);
});
test('nothing to inject → empty text, zero counts, no degradation', async () => {
const r = await assembleTurnContext(engine, {
sourceId: 'default',
window: [{ role: 'user', text: 'no entities here, just vibes' }],
});
expect(r.text).toBe('');
expect(r.pointers.length).toBe(0);
expect(r.factsCount).toBe(0);
expect(r.degradedReason).toBeUndefined();
});
});
describe('session cache isolation [CX2-11]', () => {
function metaCtx(sessionId?: string): OperationContext {
return {
engine,
config: {} as GBrainConfig,
logger: { info: () => {}, warn: () => {}, error: () => {} },
dryRun: false,
remote: true,
sourceId: 'default',
sessionId,
takesHoldersAllowList: ['world'],
};
}
test('two sessions on one source cache DIFFERENT fact sets per session key', async () => {
await seedFact('SESSION-A-FACT prefers morning meetings', 'world', { session: 'sess-a' });
await seedFact('SESSION-B-FACT prefers evening meetings', 'world', { session: 'sess-b' });
const a = await getBrainHotMemoryMeta('get_stats', metaCtx('sess-a'));
const b = await getBrainHotMemoryMeta('get_stats', metaCtx('sess-b'));
const aFacts = (a!.brain_hot_memory as { facts: Array<{ fact: string }> }).facts.map((f) => f.fact);
const bFacts = (b!.brain_hot_memory as { facts: Array<{ fact: string }> }).facts.map((f) => f.fact);
expect(aFacts).toContain('SESSION-A-FACT prefers morning meetings');
expect(aFacts).not.toContain('SESSION-B-FACT prefers evening meetings');
expect(bFacts).toContain('SESSION-B-FACT prefers evening meetings');
expect(bFacts).not.toContain('SESSION-A-FACT prefers morning meetings');
expect((a!.brain_hot_memory as { session_id: string }).session_id).toBe('sess-a');
expect((b!.brain_hot_memory as { session_id: string }).session_id).toBe('sess-b');
});
test('cache entries are per-session: bumping one session leaves the other cached', async () => {
await seedFact('SESSION-A-FACT original', 'world', { session: 'sess-a' });
await seedFact('SESSION-B-FACT original', 'world', { session: 'sess-b' });
// Prime both cache entries.
await getBrainHotMemoryMeta('get_stats', metaCtx('sess-a'));
await getBrainHotMemoryMeta('get_stats', metaCtx('sess-b'));
// New session-A fact is invisible until session A's entry is bumped.
await seedFact('SESSION-A-FACT fresher', 'world', { session: 'sess-a' });
const cachedA = await getBrainHotMemoryMeta('get_stats', metaCtx('sess-a'));
const cachedAFacts = (cachedA!.brain_hot_memory as { facts: Array<{ fact: string }> }).facts.map((f) => f.fact);
expect(cachedAFacts).not.toContain('SESSION-A-FACT fresher'); // still cached
bumpHotMemoryCache('default', 'sess-a');
const freshA = await getBrainHotMemoryMeta('get_stats', metaCtx('sess-a'));
const freshAFacts = (freshA!.brain_hot_memory as { facts: Array<{ fact: string }> }).facts.map((f) => f.fact);
expect(freshAFacts).toContain('SESSION-A-FACT fresher');
// Session B's cached entry was untouched by the session-A bump.
const stillB = await getBrainHotMemoryMeta('get_stats', metaCtx('sess-b'));
const stillBFacts = (stillB!.brain_hot_memory as { facts: Array<{ fact: string }> }).facts.map((f) => f.fact);
expect(stillBFacts).toEqual(['SESSION-B-FACT original']);
});
});
describe('dispatch typed session identity [CX2-11]', () => {
const fakeEngine = {} as unknown as BrainEngine;
test('_meta.session_id inside tool arguments lands on ctx.sessionId', () => {
const ctx = buildOperationContext(fakeEngine, { _meta: { session_id: 'topic-42' } }, { remote: true, sourceId: 'default' });
expect(ctx.sessionId).toBe('topic-42');
});
test('session id is clamped to 256 chars', () => {
const long = 'x'.repeat(300);
const ctx = buildOperationContext(fakeEngine, { _meta: { session_id: long } }, { remote: true, sourceId: 'default' });
expect(ctx.sessionId!.length).toBe(256);
const viaOpts = buildOperationContext(fakeEngine, {}, { remote: true, sourceId: 'default', sessionId: long });
expect(viaOpts.sessionId!.length).toBe(256);
});
test('transport-resolved opts.sessionId wins over arguments-level _meta', () => {
const ctx = buildOperationContext(
fakeEngine,
{ _meta: { session_id: 'from-args' } },
{ remote: true, sourceId: 'default', sessionId: 'from-transport' },
);
expect(ctx.sessionId).toBe('from-transport');
});
test('non-string / absent _meta.session_id → undefined (never a crash)', () => {
expect(buildOperationContext(fakeEngine, {}, { sourceId: 'default' }).sessionId).toBeUndefined();
expect(buildOperationContext(fakeEngine, { _meta: { session_id: 42 } }, { sourceId: 'default' }).sessionId).toBeUndefined();
expect(buildOperationContext(fakeEngine, { _meta: 'nope' }, { sourceId: 'default' }).sessionId).toBeUndefined();
expect(buildOperationContext(fakeEngine, { _meta: { session_id: '' } }, { sourceId: 'default' }).sessionId).toBeUndefined();
});
});
describe('corpus recall on a multi-entity brain [S3#1 fence, real recall]', () => {
// Deterministic proper-case surface for a slug tail so the zero-LLM,
// proper-case-biased entity resolver fires on a page-kind query.
// 'concepts/graph-traversal' → 'Graph Traversal'
const nameFromSlug = (slug: string): string =>
slug
.split('/')
.pop()!
.split('-')
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
.join(' ');
test('every gold query: world answer surfaces, private beliefs are fenced, named pages resolve to pointers', async () => {
// beforeEach cleared pages/facts/aliases. Seed the WHOLE synthetic world
// through the real handlers: put_page fires auto-link + search vector,
// insertFact honors each belief's declared visibility.
const slugs = await loadCorpusPages(engine, { sourceId: 'default' });
expect(slugs.length).toBeGreaterThanOrEqual(12);
const inserted = await loadCorpusBeliefs(engine, { sourceId: 'default' });
expect(inserted).toBe(loadCorpusBeliefData().length);
// Beliefs were inserted after any prior meta read this test — start the
// hot-memory cache clean so the world/private tiers are (re)computed.
__resetHotMemoryCacheForTests();
const privateTexts = loadCorpusBeliefData()
.filter((b) => b.visibility === 'private')
.map((b) => b.text);
expect(privateTexts.length).toBeGreaterThanOrEqual(3);
const queries = loadCorpusQueries();
expect(queries.length).toBeGreaterThan(0);
// Collect every violation with its gold-case id so a regression names the
// exact query + assertion that broke, instead of a bare boolean.
const violations: string[] = [];
for (const q of queries) {
// Page-kind: name the entity so the reflex resolver runs against the real
// 12-page graph. belief / fence: the raw query — only the world/private
// hot-memory fence decides what surfaces.
const window: WindowTurn[] =
q.kind === 'page' && q.expect_slug
? [{ role: 'user', text: `the latest on ${nameFromSlug(q.expect_slug)}` }]
: [{ role: 'user', text: q.query }];
const r = await assembleTurnContext(engine, { sourceId: 'default', window });
// Fence (S3#1): NO private belief text ever crosses into the block,
// regardless of what the user asked.
for (const pt of privateTexts) {
if (r.text.includes(pt)) violations.push(`${q.id}: leaked private belief "${pt.slice(0, 40)}…"`);
}
if (q.must_not_substring && r.text.includes(q.must_not_substring)) {
violations.push(`${q.id}: fenced substring surfaced "${q.must_not_substring}"`);
}
if (q.kind === 'belief' && q.expect_substring && !r.text.includes(q.expect_substring)) {
violations.push(`${q.id}: world belief not recalled "${q.expect_substring}"`);
}
if (q.kind === 'page' && q.expect_slug) {
if (!r.pointers.some((p) => p.slug === q.expect_slug)) {
violations.push(`${q.id}: entity did not resolve to a pointer (${q.expect_slug})`);
}
if (!r.text.includes(q.expect_slug)) {
violations.push(`${q.id}: expected slug missing from the block (${q.expect_slug})`);
}
}
}
expect(violations).toEqual([]);
}, 120_000);
test('reflex pointer synopsis carries real page-body content, not a generic stub', async () => {
await loadCorpusPages(engine, { sourceId: 'default' });
// Named entity → pointer, and the pointer's synopsis is drawn from THAT
// page's body (the world-fenced first prose line), so distinctive body
// content reaches the assembled block.
const cases: Array<[name: string, slug: string, needle: string]> = [
['Summit Robotics', 'companies/summit-robotics', 'warehouse'],
['Graph Traversal', 'concepts/graph-traversal', 'path-planning'],
['Hybrid Retrieval', 'concepts/hybrid-retrieval', 'dense embeddings'],
];
for (const [name, slug, needle] of cases) {
const r = await assembleTurnContext(engine, {
sourceId: 'default',
window: [{ role: 'user', text: `context on ${name}` }],
});
expect(r.pointers.map((p) => p.slug)).toContain(slug);
expect(r.text).toContain(needle);
}
}, 120_000);
});