Files
learnharness/tests/merge-adopted.test.ts
T
amosblomqvist 6b13583e96 wave 7: the prober reads its map — graph-context injection dies whole
The owner's zero-configuration ruling (2026-09-23): nothing configures
the graph beyond chat models. The prior-knowledge injection — its budget
dial, its runner, its marker part, its dock/dispatch entries — is
removed across all bands; the prober now reads the learner's persistent
knowledge map on demand through two gated tools instead:

- list_sessions: every birth session, names + kinds + counts + dates
  (graphSessionIndex, Platform's core seam)
- list_propositions(session): what was born there + the session's own
  latest understanding level + that evaluation's date
  (graphSessionPropositions) — names/levels/dating ONLY, no statement
  text (owner ruling: the map's organization is the prober's business,
  its prose is not)

Registered via createProberTools when chat-handler passes graphStore
(the graphToolsEnabled gate decided server-side, per-turn fresh); the
prober doctrine gains the map-review nudge at session start.

Also: runner's JobKind special-casing collapses (KIND_ROLE/KIND_RUNNERS
are plain records again), semantic-clusters.ts dies with its test, the
embedder dep's doc tells the wave-7 stub story, and ports.ts's
readClusterLabels/readNamingRuns surface stays untouched for Platform's
ledger-port interlude.

848/848 tests, typecheck/lint/build green.
2026-09-23 15:24:48 +03:00

74 lines
3.1 KiB
TypeScript

/**
* mergeAdopted (core/ui-message.ts) — the append-only record-adoption seam
* (owner ground rules, 2026-09-15): server-added steer messages must become
* visible mid-turn without the full snapshot swap that breaks the SDK's
* continuation merge. Pure, no React.
*/
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { mergeAdopted, type TeacherUIMessage } from "../src/core/index.ts";
function userText(id: string, text: string): TeacherUIMessage {
return { id, role: "user", parts: [{ type: "text", text }] };
}
function steer(id: string): TeacherUIMessage {
// Wave 7 killed the graph-context injection; its marker part is gone.
// The steer-shaped user message here exercises mergeAdopted's
// non-text user parts path with the handoff marker instead.
return { id, role: "user", parts: [{ type: "data-probe-handoff", data: { chatId: "c" } }] };
}
function assistant(id: string): TeacherUIMessage {
return { id, role: "assistant", parts: [{ type: "text", text: "reply" }] };
}
describe("mergeAdopted — append-only adoption", () => {
it("inserts a server-added steer between the persisted core and the in-flight tail", () => {
const local = [userText("u1", "topic"), userText("u2", "answer"), assistant("a_inflight")];
const record = [userText("u1", "topic"), userText("u2", "answer"), steer("steer_ctx_c")];
const merged = mergeAdopted(local, record);
assert.ok(merged);
assert.deepEqual(
merged.map((m) => m.id),
["u1", "u2", "steer_ctx_c", "a_inflight"],
"marker between the answer and the streaming bubble",
);
});
it("adopts into an idle client (no in-flight tail)", () => {
const local = [userText("u1", "topic")];
const record = [userText("u1", "topic"), steer("s1"), assistant("a1")];
const merged = mergeAdopted(local, record);
assert.deepEqual(
merged?.map((m) => m.id),
["u1", "s1", "a1"],
);
});
it("nothing new → null (no needless re-render)", () => {
const local = [userText("u1", "topic")];
assert.equal(mergeAdopted(local, [userText("u1", "topic")]), null);
// The tail alone is not 'new' — nothing to adopt.
assert.equal(mergeAdopted([assistant("a_x")], []), null);
});
it("a missing core id (record rewrite) → null: defer to the full sync at idle", () => {
// A local message vanished from the record — mid-turn this means a
// rewrite, and the append-only shortcut must not paper over it.
const local = [userText("u1", "a"), userText("u2", "b"), assistant("tail")];
const record = [userText("u2", "b"), steer("s1")];
assert.equal(mergeAdopted(local, record), null);
});
it("only the LAST local message may be the absent in-flight tail", () => {
// Two absent local messages: not a simple append — bail.
const local = [userText("u1", "a"), userText("u2", "gone"), assistant("tail")];
const record = [userText("u1", "a"), steer("s1")];
assert.equal(mergeAdopted(local, record), null);
});
it("an empty local list adopts the whole record", () => {
const record = [steer("s1"), assistant("a1")];
assert.deepEqual(mergeAdopted([], record)?.map((m) => m.id), ["s1", "a1"]);
});
});