Files
learnharness/tests/record-adapter.test.ts
T
amosblomqvist 1d45d77303 feat: graph context injection — the session's prior knowledge, steered in at session start (vision-context-injection.md)
One shot per chat, fired at the first turn's onFinish: the session's
topic (its only typed message) is embedded as an EmbeddingGemma retrieval
query; the semantically closest graph propositions are selected under the
session's token budget (pure budget, no similarity threshold — the
decided doctrine) and delivered as a marker-only steer through the wake
pipeline, queuing behind pending questions like every sub-agent result.

- contracts: ChatSettings.graphContextTokens (default 1024, max 4096,
  step 128, 0 = off); GraphContextData + graphContextText; the
  graph-context JobKind + buildGraphContextSteer (id steer_ctx_<chatId>)
- core/graph-context.ts: brute-force cosine top-K, greedy chars/4 fill
  under budget, most-recent UnderstandingLevel fold, count cap 64,
  parse-don't-cast at the marker seam
- server: embedQuery (role-asymmetric query format, same warm singleton
  — vector-space parity by construction); the runner (startGraphContext-
  Injection one-shot gates; runGraphContextJob validates ledger parity);
  wake branches (chatIdOf/jobIdOf, finalizeJob done, recoverStaleJobs
  re-deliver or contextRespawn); abortJob kills context jobs SILENTLY
  (nothing was promised in the lesson); turn-service trigger seam
- adapter: data-graph-context flattens to the canonical block (malformed
  parts have no model face)
- UI: the budget slider (home dock + /chat/new draft, the session-data
  strip); the quiet SystemLine "~537 · 10 propositions"; dock kind
  "Knowledge context"; model-picker PUT now spreads settings (a model
  pick can no longer clear the sibling override)

Live-smoked end to end on the real embedder + real GLM: seeded ledger
(10 propositions), session on "modal logic: possible worlds and the
necessity box" — job in the dock, queued behind the prober's question,
delivered on the answer, SystemLine in the transcript, wake turn reads
the block and continues the probe. Gates: tsc, lint, 459/459, build.
2026-09-15 07:54:50 +03:00

120 lines
4.7 KiB
TypeScript

/**
* Unit tests — the record→pi adapter's SYSTEM MESSAGE flattening (the
* custom-message scheme, user-decided 2026-09-12).
*
* Wake and verdict messages are user-role messages carrying ONLY a marker
* data part — no human text exists. The adapter owns their model-facing
* wording (flattens the marker to the canonical text from core contracts):
* the transcript never persists fake human speech, and the wording can't
* drift between the seam, the tests, and reality.
*/
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import type { UIMessage } from "ai";
import { recordToPiMessages } from "../src/server/pi/record-adapter.ts";
import { HANDOFF_TEXT, verdictText } from "../src/core/contracts/chat.ts";
import { buildAbortSteer, buildProbeHandoffSteer } from "../src/core/contracts/jobs.ts";
function userText(text: string): UIMessage {
return { id: `u_${text.length}`, role: "user", parts: [{ type: "text", text }] };
}
function userParts(parts: unknown[]): UIMessage {
return { id: "u_sys", role: "user", parts: parts as UIMessage["parts"] };
}
const texts = (msgs: ReturnType<typeof recordToPiMessages>): string[] => {
const out: string[] = [];
for (const m of msgs) {
if (m.role === "user") {
for (const c of m.content) {
if ((c as { type?: string }).type === "text") out.push((c as { text: string }).text);
}
}
}
return out;
};
describe("record-adapter: system message flattening", () => {
it("an ACCEPT verdict flattens to the canonical model-facing text (no human speech persisted)", () => {
const out = recordToPiMessages([
userText("teach me"),
userParts([{ type: "data-verdict", id: "vd_1", data: { accepted: true, at: "…" } }]),
]);
assert.deepEqual(texts(out), ["teach me", verdictText(true)]);
assert.equal(texts(out)[1], "The learner accepted the plan — begin teaching.");
});
it("a DECLINE verdict flattens to the canonical revision text", () => {
const out = recordToPiMessages([
userParts([{ type: "data-verdict", id: "vd_2", data: { accepted: false, at: "…" } }]),
]);
assert.deepEqual(texts(out), [verdictText(false)]);
assert.match(texts(out)[0]!, /revised/);
});
it("the probe handoff steer flattens to the canonical plan-authoring instruction", () => {
const handoff = buildProbeHandoffSteer("chat_x");
assert.equal(handoff.parts.length, 1, "the handoff is marker-ONLY — no text part");
assert.equal(handoff.parts[0]!.type, "data-probe-handoff");
const out = recordToPiMessages([handoff]);
assert.deepEqual(texts(out), [HANDOFF_TEXT]);
assert.match(texts(out)[0]!, /plan_init/);
});
it("note + verdict (the two-message accept) reach the model as two user texts, in order", () => {
const out = recordToPiMessages([
userText("Start with the basics, skip the history."),
userParts([{ type: "data-verdict", id: "vd_3", data: { accepted: true, at: "…" } }]),
]);
assert.deepEqual(texts(out), ["Start with the basics, skip the history.", verdictText(true)]);
});
it("sub-agent steers keep their text (payload, not boilerplate): the artifact part adds nothing", () => {
const abort = buildAbortSteer({ id: "j1", kind: "research", toolCallId: "c1" });
const out = recordToPiMessages([abort]);
const got = texts(out);
assert.equal(got.length, 1, "one user text — the marker part is UI-only");
assert.match(got[0]!, /aborted/i);
});
it("a pure UI data part with no system meaning still never reaches the model", () => {
const out = recordToPiMessages([
userParts([{ type: "data-plan-block", id: "pb", data: { kind: "full", at: "…", current: null, nodes: [], edges: [] } }]),
]);
assert.equal(out.length, 0, "no model face at all — skipped");
});
it("the graph-context steer flattens to the canonical prior-knowledge block", () => {
const out = recordToPiMessages([
userParts([
{
type: "data-graph-context",
id: "ctx1",
data: {
chatId: "chat_1",
budget: 1024,
tokens: 64,
propositions: [
{ id: "n1", statement: "Water boils at 100°C.", level: "solid" },
{ id: "n2", statement: "Evaporation cools things.", level: null },
],
},
},
]),
]);
assert.equal(out.length, 1, "one user message — the marker part is the model face");
const got = texts(out);
assert.equal(got.length, 1);
assert.match(got[0]!, /Prior-knowledge context/);
assert.match(got[0]!, /\[solid\] Water boils at 100°C\./);
});
it("a malformed graph-context part has no model face — the message is skipped wholesale", () => {
const out = recordToPiMessages([
userParts([{ type: "data-graph-context", id: "ctx2", data: { chatId: "chat_1" } }]),
]);
assert.equal(out.length, 0, "parse-don't-cast: no validated data, no face");
});
});