quick-fixes round: quiz-only teach-check reveal, ESC-abort confirm popup + question dismissal, confidence prompt diet

- requiresReveal gates on pending.tool === 'quiz' (owner: only graded quizzes
  arrive collapsed; ask/explain always open)
- ESC-abort chain opens AbortTurnDialog everywhere (owner ruling); from a
  pending question abort IS dismissal — dismissedAnswer rides the answer
  path, the teacher acknowledges the skip in its next turn; popup-Esc is
  claimed via the state gate (two window listeners fire per Esc)
- 25-confidence.md: stripped the slider/JSON/calibration mechanics — pure
  reading doctrine remains
- toolQuestionKind named once in core (dock parity), dock switched to it
This commit is contained in:
amosblomqvist
2026-10-03 01:03:29 +03:00
parent 60971ca6fe
commit 3e5b857c63
8 changed files with 271 additions and 10 deletions
+32
View File
@@ -12,6 +12,7 @@ import {
askOptionProblems,
buildAnswer,
compactAnswerForModel,
dismissedAnswer,
coerceCorrectAnswer,
CONFIDENCE_STOPS,
DEFAULT_CONFIDENCE_INDEX,
@@ -31,6 +32,7 @@ import {
type QuestionData,
toClientQuestion,
} from "../src/core/question-core.ts";
import { toolQuestionKind } from "../src/core/pending.ts";
// ─── the authoring contract (TypeBox schemas + execute-time refinements) ──
// Schema-valid ⇔ gradeable: a quiz whose correctAnswer can't be resolved
@@ -458,6 +460,36 @@ describe("compactAnswerForModel", () => {
});
});
// ─── dismissedAnswer / toolQuestionKind (ESC-abort dismissal, 2026-10-02) ──
describe("dismissedAnswer (the ESC-abort record)", () => {
it("the minimum honest answer: kind, questionText, dismissed — nothing else claimed", () => {
const ad = dismissedAnswer("ask", "What drives you?");
assert.equal(ad.kind, "ask");
assert.equal(ad.questionText, "What drives you?");
assert.equal(ad.dismissed, true);
assert.equal(ad.dontKnow, false); // a skip is not a gap claim
assert.deepEqual(ad.answers, []);
assert.ok(ad.answeredAt);
});
it("compacts to the one-field model diet", () => {
assert.deepEqual(compactAnswerForModel(dismissedAnswer("quiz", "Pick one")), {
kind: "quiz",
dismissed: true,
});
});
});
describe("toolQuestionKind (tool→kind parity with the dock)", () => {
it("the three question tools map; unknown tools fall to ask", () => {
assert.equal(toolQuestionKind("quiz"), "quiz");
assert.equal(toolQuestionKind("explain"), "explain");
assert.equal(toolQuestionKind("ask_user_question"), "ask");
assert.equal(toolQuestionKind("anything-else"), "ask");
});
});
// ─── toClientQuestion (leak safety) ─────────────────────────────────────────
describe("toClientQuestion", () => {
+47
View File
@@ -0,0 +1,47 @@
/**
* SSR pins — abort-turn-dialog (ESC-abort guard, owner 2026-10-02):
* the confirm popup EVERYWHERE ruling. AbortTurnBody is the pure seam
* (the DeleteNodeBody pattern); the wrapper owns effects + portal.
*/
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { renderToStaticMarkup } from "react-dom/server";
import { AbortTurnBody } from "@/components/abort-turn-dialog";
describe("AbortTurnBody", () => {
it("turn mode: names the turn, the partial-stays promise, Abort", () => {
const html = renderToStaticMarkup(
<AbortTurnBody mode="turn" onClose={() => {}} onConfirm={() => {}} />,
);
assert.ok(html.includes('data-testid="abort-turn-dialog"'));
assert.match(html, /Abort this turn\?/);
assert.match(html, /The partial answer stays in the transcript\./);
assert.match(html, /Cancel/);
assert.match(html, /Abort/);
});
it("question mode: names the dismissal, the skip, Dismiss", () => {
const html = renderToStaticMarkup(
<AbortTurnBody mode="question" onClose={() => {}} onConfirm={() => {}} />,
);
assert.match(html, /Dismiss this question\?/);
assert.match(html, /Skipped — the teacher moves on without it\./);
assert.match(html, /Dismiss/);
// the abort copy must not leak into the dismissal mode
assert.ok(!html.includes("Abort this turn"));
});
it("the confirm action is the destructive pair, never the cancel", () => {
const html = renderToStaticMarkup(
<AbortTurnBody mode="turn" onClose={() => {}} onConfirm={() => {}} />,
);
const confirmAt = html.indexOf('data-testid="abort-turn-confirm"');
const cancelAt = html.indexOf('data-testid="abort-turn-cancel"');
assert.ok(confirmAt > -1 && cancelAt > -1);
const confirmBtn = html.slice(confirmAt, html.indexOf("</button>", confirmAt));
assert.match(confirmBtn, /destructive/);
const cancelBtn = html.slice(cancelAt, html.indexOf("</button>", cancelAt));
assert.ok(!cancelBtn.includes("destructive"));
});
});