Files
learnharness/tests/render-composer.test.tsx
T
amosblomqvist 573d89ae66 practice: the between-problems pause — conclude ends the turn, the learner's go-ahead paces the stream (round 16)
Owner ruling (2026-10-06): every concluded problem opens a pause. The
agent may speak its closing words about the learner's last thinking
after the conclude result, then ENDS ITS TURN — a system-prompt law
deliberately, per owner correction, NOT tool-level terminate. The next
problem is never posed until the learner explicitly moves on; the pause
is the session's teaching space (repairs, clarifications, follow-ups —
still surgical, never re-teaching).

The move-on signal is a first-class input: { type: "next" } appends a
practice-next marker (content = single-sourced NEXT_PROBLEM_TEXT — the
model's unambiguous go-ahead: pose per the selection rules or wrap if
the arc is complete). Honest 400s: practice sessions only, nothing
active (mid-problem input rides the dock), riding after any pending
dismissal synthesis exactly like text.

The fast path (owner approved: composer-band chip + Enter-on-empty):
- Composer.enterAction gains "next" — Enter on an EMPTY composer in
  the pause fires the signal; typing anything makes Enter a send again;
  busy (steer/stop) outranks the pause.
- The Next chip hovers at the composer band's top-right (pick-pill
  chrome register) whenever the pause is live and a problem has ever
  been posed; the placeholder becomes the hint ("Ask about this
  problem — or press Enter for the next one…", exact);
- the transcript renders the signal as a compact centered NEXT PROBLEM
  line (mono-caps chrome); session-core appends it optimistically.

CONCLUDE_ACK_TEXT rewritten to carry the law; doctrine §VIII gains the
pause law (owner-sanctioned edit #3); 00-base's conclude/dismissal
bullets rewritten + the between-problems section added.
2026-10-06 03:47:05 +03:00

96 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Composer render tests — the chat input row (content only; the persistent
* rounded shell lives in the chat view's overlay so it can morph into the
* question dock without remounting).
*
* Same tsx classic-JSX shim as render-cards.test.tsx.
*/
import React from "react";
(globalThis as Record<string, unknown>).React = React;
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { renderToStaticMarkup } from "react-dom/server";
import { Composer, enterAction } from "../components/composer.tsx";
function render(opts: { busy?: boolean; canSend?: boolean; placeholder?: string } = {}) {
return renderToStaticMarkup(
<Composer
busy={opts.busy ?? false}
value=""
onChange={() => {}}
onSend={() => {}}
onStop={() => {}}
canSend={opts.canSend ?? true}
{...(opts.placeholder ? { placeholder: opts.placeholder } : {})}
/>,
);
}
describe("Composer", () => {
it("chat placeholder + send button", () => {
const html = render();
assert.match(html, /Ask, answer, or steer the lesson/, "the default — active sessions");
assert.ok(html.includes('aria-label="Send"'), "send button present");
});
it("placeholder is overridable — the new-session page asks the graph view's question", () => {
const html = render({ placeholder: "What do you want to learn?" });
assert.match(html, /What do you want to learn\?/);
assert.ok(!html.includes("Ask, answer"), "the session placeholder must not ride along");
});
it("busy with text: the button STAYS Send (click = steer); busy and empty: it becomes Stop", () => {
const steering = render({ busy: true, canSend: true });
assert.ok(steering.includes('aria-label="Send"'), "the steer affordance is clickable mid-turn");
assert.ok(!steering.includes('aria-label="Stop"'), "no morph while there is text to steer with");
const idleStop = render({ busy: true, canSend: false });
assert.ok(idleStop.includes('aria-label="Stop"'), "empty composer: stop replaces send while streaming");
});
it("send button radius is 10px = 25% of the 40px button (concentric with the shell)", () => {
const html = render();
assert.ok(html.includes("rounded-[10px]"), "send radius = shell radius − row padding");
assert.ok(html.includes("size-10"), "40px send button (10px = 25%)");
});
it("disabled when nothing to send", () => {
const html = render({ canSend: false });
assert.ok(/aria-label="Send"[^>]*disabled|disabled[^>]*aria-label="Send"/.test(html), "send disabled");
});
});
// The Enter decision (composer steering, 2026-10-03): while a turn runs,
// typed text makes Enter a STEER; Enter with an empty composer stays the
// stop shortcut. (The plan-verdict accept branch died 2026-09-26 with the
// verdict itself — the post-plan reply is a natural text message.) Pure
// function so the static-render harness can pin it (key events can't be
// simulated in renderToStaticMarkup).
describe("enterAction", () => {
it("busy stream with text: Enter steers (the queued send)", () => {
assert.equal(enterAction({ busy: true, canSend: true }), "send");
});
it("busy stream, empty composer: Enter stops", () => {
assert.equal(enterAction({ busy: true, canSend: false }), "stop");
});
it("idle: send when there is the text, nothing when there is not", () => {
assert.equal(enterAction({ busy: false, canSend: true }), "send");
assert.equal(enterAction({ busy: false, canSend: false }), "none");
});
it("the practice pause (owner round 16): Enter on an EMPTY composer fires the move-on signal", () => {
assert.equal(enterAction({ busy: false, canSend: false, nextProblem: true }), "next");
});
it("typing anything outranks the pause — text makes Enter a send again", () => {
assert.equal(enterAction({ busy: false, canSend: true, nextProblem: true }), "send");
});
it("busy outranks the pause — steering and Stop keep their lanes", () => {
assert.equal(enterAction({ busy: true, canSend: false, nextProblem: true }), "stop");
assert.equal(enterAction({ busy: true, canSend: true, nextProblem: true }), "send");
});
});