mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
- entity-salience.ts: extractCandidatesFromWindow(turns) — runs the existing per-turn extractor across the last N turns (oldest→newest), merges by the normalizeAlias form with occurrence/newest-turn/user-mention metadata, and orders by salience (recency > frequency > user-role) so the MAX_CANDIDATES cap drops stale assistant chatter, not the entity the user just named. Closes the filed assistant-introduced-entities recall TODO; true pronoun coreference (never-named antecedents) stays out of scope. - retrieval-reflex.ts: ReflexPointer gains source_id + arm + confidence + matchedNorm. ARM_CONFIDENCE (alias 0.9 / title 0.8 / slug-suffix 0.6) lives next to the arm definitions so identity and score can't drift. Arm-2 provenance is classified in JS (codex D8 — the combined OR can't report which predicate matched). Federated sourceIds[] scope (alias arm loops per source; arm 2 uses source_id = ANY — no engine-interface change). Suppression gains 'slug-only' mode (codex D7, REQUIRED for windowing): the legacy title-whole-word rule would suppress every entity merely MENTIONED in a prior window turn, breaking the feature by construction — slugs only enter context when a pointer/page was actually surfaced. Default stays 'slug-and-title' for the window=1 legacy path. - volunteer.ts (new): parseWindow (lenient user:/assistant: prefixes, CRLF, unprefixed → one user turn), volunteerContext (zero-LLM: extract → resolve → +0.05 multi-turn/newest-turn boost → min_confidence 0.7 gate → cap 3/5; deterministic rationale strings, never raw conversation text), and volunteerUsageStats (per-arm/channel precision from the last_retrieved_at join, labeled approximate — 5-min throttle false negatives, unrelated-read false positives; codex D9). Tests: 35 green across volunteer-context (window parsing, pronoun follow-up via assistant-introduced entity, confidence gating, slug-only suppression, takes-fence privacy, multi-source scope, caps, stats join math) + retrieval-reflex back-compat + resolve-ipc. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
77 lines
2.8 KiB
TypeScript
77 lines
2.8 KiB
TypeScript
/**
|
|
* Retrieval Reflex resolve IPC round-trip tests (#1981, T3/T5).
|
|
*/
|
|
import { describe, test, expect, afterEach } from 'bun:test';
|
|
import { mkdtempSync, rmSync, existsSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import {
|
|
resolveSocketPath,
|
|
startResolveIpcServer,
|
|
resolveViaIpc,
|
|
IPC_UNAVAILABLE,
|
|
} from '../../src/core/context/resolve-ipc.ts';
|
|
import type { PointerBlock } from '../../src/core/context/retrieval-reflex.ts';
|
|
|
|
const servers: Array<{ close: () => void }> = [];
|
|
afterEach(() => {
|
|
for (const s of servers.splice(0)) { try { s.close(); } catch { /* noop */ } }
|
|
});
|
|
|
|
function tmpDir(): string {
|
|
return mkdtempSync(join(tmpdir(), 'rr-ipc-'));
|
|
}
|
|
|
|
describe('resolve IPC', () => {
|
|
test('round-trip: client gets the pointer block the server returns', async () => {
|
|
const dir = tmpDir();
|
|
const sock = resolveSocketPath(dir);
|
|
const block: PointerBlock = {
|
|
pointers: [{ display: 'Alice', slug: 'people/alice', source_id: 'default', synopsis: 'x', arm: 'alias', confidence: 0.9 }],
|
|
text: 'BLOCK',
|
|
};
|
|
const server = await startResolveIpcServer(sock, async (req) => {
|
|
expect(req.candidates[0].query).toBe('Alice');
|
|
return block;
|
|
});
|
|
expect(server).not.toBeNull();
|
|
servers.push(server!);
|
|
|
|
const got = await resolveViaIpc(sock, { candidates: [{ display: 'Alice', query: 'Alice' }] });
|
|
expect(got).not.toBe(IPC_UNAVAILABLE);
|
|
expect((got as PointerBlock).text).toBe('BLOCK');
|
|
rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('absent socket → IPC_UNAVAILABLE (caller falls through ladder)', async () => {
|
|
const dir = tmpDir();
|
|
const got = await resolveViaIpc(resolveSocketPath(dir), { candidates: [{ display: 'A', query: 'A' }] });
|
|
expect(got).toBe(IPC_UNAVAILABLE);
|
|
rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('server returning null relays as null (resolved, nothing found)', async () => {
|
|
const dir = tmpDir();
|
|
const sock = resolveSocketPath(dir);
|
|
const server = await startResolveIpcServer(sock, async () => null);
|
|
servers.push(server!);
|
|
const got = await resolveViaIpc(sock, { candidates: [{ display: 'A', query: 'A' }] });
|
|
expect(got).toBeNull();
|
|
rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('stale socket file is cleaned up so a fresh server can bind', async () => {
|
|
const dir = tmpDir();
|
|
const sock = resolveSocketPath(dir);
|
|
const s1 = await startResolveIpcServer(sock, async () => null);
|
|
servers.push(s1!);
|
|
s1!.close();
|
|
// bind again at the same path — startResolveIpcServer must unlink the stale file
|
|
const s2 = await startResolveIpcServer(sock, async () => null);
|
|
expect(s2).not.toBeNull();
|
|
servers.push(s2!);
|
|
expect(existsSync(sock)).toBe(true);
|
|
rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
});
|