draft: the pick bar comes to the new-practice screen (round 10 slice 2)

The SAME pill the home screen carries — above the composer, dashed
'Pick propositions from the map' when empty, count + Clear when picked
— now lives on the /chat/new?mode=practice draft. The draft has no
graph behind it, so the pill opens the standalone ChainTableOverlay
(select mode, controlled selection, Done bar) over the /api/graph
snapshot, fetched lazily on first open. The first send carries the
picks as graphContext: the created session is born WITH its ids-first
context marker (target validation's truth set) instead of pickless —
the same contract as the home screen's practice send. Teach drafts
render no pill; practice with no picks stays lean on the wire.
This commit is contained in:
amosblomqvist
2026-10-05 06:51:23 +03:00
parent a1596556df
commit 93fdf71d4d
2 changed files with 166 additions and 3 deletions
+55
View File
@@ -124,6 +124,61 @@ describe("the practice draft (?mode=practice, 2026-10-04)", () => {
assert.match(html, /What do you want to practice\?/);
});
it("THE PICK BAR (owner round 10): the practice draft carries the same pill above its composer", () => {
const html = renderToStaticMarkup(
<DraftShell
busy={false}
text=""
onChange={() => {}}
onSend={() => {}}
mode="practice"
/>,
);
assert.match(html, /Pick propositions from the map/);
// picked: count + Clear, the home register verbatim
const picked = renderToStaticMarkup(
<DraftShell
busy={false}
text=""
onChange={() => {}}
onSend={() => {}}
mode="practice"
picks={[{ id: "n_1", name: "Parity", statement: "Odd + odd = even." }]}
/>,
);
assert.match(picked, /1 proposition selected/);
assert.match(picked, /Clear/);
// teach mode with nothing picked renders nothing (home's law)
const teach = renderToStaticMarkup(
<DraftShell busy={false} text="" onChange={() => {}} onSend={() => {}} mode="teach" />,
);
assert.ok(!teach.includes("Pick propositions from the map"));
});
it("startDraftSession carries the picks as graphContext (the home practice contract)", async () => {
let body: Record<string, unknown> | null = null;
const f = (async (_url: string, init?: RequestInit) => {
body = JSON.parse(String(init?.body));
return new Response(JSON.stringify({ id: "c10" }), { status: 201 });
}) as unknown as typeof fetch;
await startDraftSession("drill me", f, "practice", [{ id: "n_1", name: "Parity", statement: "Odd + odd = even." }]);
assert.deepEqual(body, {
title: "drill me",
kind: "practice",
graphContext: { propositions: [{ id: "n_1", name: "Parity", statement: "Odd + odd = even." }] },
});
});
it("practice with NO picks stays lean — no graphContext key on the wire", async () => {
let body: Record<string, unknown> | null = null;
const f = (async (_url: string, init?: RequestInit) => {
body = JSON.parse(String(init?.body));
return new Response(JSON.stringify({ id: "c11" }), { status: 201 });
}) as unknown as typeof fetch;
await startDraftSession("free practice", f, "practice", []);
assert.deepEqual(body, { title: "free practice", kind: "practice" });
});
it("startDraftSession carries kind:\"practice\" to the create route", async () => {
let body: Record<string, unknown> | null = null;
const f = (async (_url: string, init?: RequestInit) => {