test/briefing.test.js
Rama main · réplica de lectura del repositorio original.
import { test } from "node:test";
import assert from "node:assert/strict";
import {
PRESET_KEYS, initialBriefing, briefingFromPreset, applyPresetToBriefing,
presetPayload, effectiveBriefStep, nextBriefStep, BRIEFING_QUESTIONS, REPORT_QUESTIONS,
questionsFor, tierQuestions, optionalAnswered, echoAnswer, applyFreeText,
} from "../app/web/src/lib/briefing.js";
test("PRESET_KEYS fixes team, slide counts, part count, density, theme and branding — never guide or academic", () => {
assert.deepEqual(PRESET_KEYS.sort(), ["branding", "density", "maxSlides", "slidesPerMember", "subtopicCount", "team", "theme"]);
assert.ok(!PRESET_KEYS.includes("guide"));
assert.ok(!PRESET_KEYS.includes("academic"));
assert.ok(!PRESET_KEYS.includes("mode"));
});
test("initialBriefing pre-fills the guide from identity but never team or academic", () => {
const b = initialBriefing({
institution: { short: "EIT" },
guide: { name: "Dr. A. Sharma", designation: "Professor" },
team: { label: "old", members: [{ name: "Stale", presenting: true }] },
academic: { subject: "Old", year: "2025-26" },
});
assert.equal(b.guide.name, "Dr. A. Sharma");
assert.equal(b.guide.designation, "Professor");
assert.deepEqual(b.team, { label: "", members: [] });
assert.deepEqual(b.academic.subject, "");
assert.equal(b.maxSlides, 0);
assert.equal(b.slidesPerMember, null);
});
test("a preset pre-fills team, maxSlides, density, theme and branding — and not guide or academic", () => {
const preset = {
name: "IE preset",
team: { label: "G2", members: [{ name: "Alice", presenting: true }] },
maxSlides: 16,
density: "dense",
theme: "swiss-international",
branding: "minimal",
slidesPerMember: 2,
guide: { name: "should not apply" }, // legacy shape — ignored
academic: { subject: "should not apply" },
};
const b = briefingFromPreset(preset, { guide: { name: "Dr. G" } });
assert.equal(b.team.members.length, 1);
assert.equal(b.maxSlides, 16);
assert.equal(b.density, "dense");
assert.equal(b.theme, "swiss-international");
assert.equal(b.branding, "minimal");
assert.equal(b.slidesPerMember, 2);
assert.equal(b.guide.name, "Dr. G"); // guide comes from identity, not the preset
assert.equal(b.academic.subject, "");});
test("presetPayload captures only the preset-fixable fields", () => {
const payload = presetPayload({
team: { label: "G", members: [{ name: "A" }] },
maxSlides: 12,
slidesPerMember: 2,
theme: "x",
density: "sparse",
branding: "none",
guide: { name: "G" },
academic: { subject: "S" },
});
assert.equal(payload.maxSlides, 12);
assert.equal(payload.guide, undefined);
assert.equal(payload.academic, undefined);
});
test("a picked preset skips its fixed questions in the briefing walk", () => {
const briefing = {
presetId: "p1",
team: {},
maxSlides: 16,
slidesPerMember: 2,
density: "dense",
theme: "x",
branding: "none",
};
// Derived from the list, never from hardcoded positions: this test named
// "0 preset, 1 title, 2 team…" and broke the moment the questions were
// retiered, which tested the ordering rather than the skipping.
const at = (key) => BRIEFING_QUESTIONS.findIndex((q) => q.key === key);
const keyAt = (i) => BRIEFING_QUESTIONS[i]?.key ?? "(end)";
// From a preset-fixed question, the walk lands on the next question that is
// not preset-fixed — whatever that happens to be after a reorder.
for (const fixed of PRESET_KEYS) {
const landed = effectiveBriefStep(briefing, at(fixed), BRIEFING_QUESTIONS);
assert.ok(landed > at(fixed), `${fixed} is preset-fixed and must be stepped over`);
const key = keyAt(landed);
assert.ok(key === "(end)" || !PRESET_KEYS.includes(key), `landed on ${key}, which is also preset-fixed`);
}
// A question a preset never fixes is not moved at all.
for (const open of BRIEFING_QUESTIONS.map((q) => q.key).filter((k) => !PRESET_KEYS.includes(k))) {
assert.equal(effectiveBriefStep(briefing, at(open), BRIEFING_QUESTIONS), at(open), `${open} must not be skipped`);
}
// An un-skipped preset question is editable without abandoning the preset.
const rewound = { ...briefing, unskip: ["theme"] };
assert.equal(effectiveBriefStep(rewound, at("theme"), BRIEFING_QUESTIONS), at("theme"));
});
test("applyPresetToBriefing refills fixed fields without touching the user's answers", () => {
const b = {
title: "My deck",
audience: "the class",
team: { label: "", members: [] },
maxSlides: 0,
};
const out = applyPresetToBriefing(b, { team: { label: "G", members: [{ name: "A" }] }, maxSlides: 10 });
assert.equal(out.title, "My deck");
assert.equal(out.audience, "the class");
assert.equal(out.maxSlides, 10);
assert.equal(out.team.members.length, 1);
});
/**
* Walk the briefing the way the view does: show the question at the effective
* step, then store nextBriefStep(). Returns the keys in the order asked.
*
* The skip function was always right and its unit tests always passed — the
* caller advanced the STORED step instead of the one on screen, so questions
* repeated. Nothing that tested the function alone could see it; only walking
* the whole thing can.
*/
function walk(briefing, questions, start = 0) {
const asked = [];
let step = start;
for (let guard = 0; guard < questions.length * 3; guard++) {
const eff = effectiveBriefStep(briefing, step, questions);
if (eff >= questions.length) break;
asked.push(questions[eff].key);
step = nextBriefStep(briefing, step, questions);
}
return asked;
}
test("a briefing with no preset asks every question exactly once, in order", () => {
const asked = walk({}, BRIEFING_QUESTIONS);
assert.deepEqual(asked, BRIEFING_QUESTIONS.map((q) => q.key));
});
test("a preset skips the fixed questions and repeats none of the rest", () => {
// pickPreset stores briefStep 1, so the walk resumes after the preset card.
const asked = walk({ presetId: "p1" }, BRIEFING_QUESTIONS, 1);
const expected = BRIEFING_QUESTIONS.slice(1).map((q) => q.key).filter((k) => !PRESET_KEYS.includes(k));
assert.deepEqual(asked, expected);
// The regression, stated as itself: "guide" was asked twice and "research"
// six times, because the walk kept re-resolving to an answered question.
assert.equal(new Set(asked).size, asked.length, `a question was asked twice: ${asked.join(" -> ")}`);
for (const key of PRESET_KEYS) assert.ok(!asked.includes(key), `${key} is preset-fixed and must be skipped`);
});
test("the report briefing walks its own list without repeating either", () => {
const fresh = walk({}, REPORT_QUESTIONS);
assert.deepEqual(fresh, REPORT_QUESTIONS.map((q) => q.key));
const withPreset = walk({ presetId: "p1" }, REPORT_QUESTIONS, 1);
assert.equal(new Set(withPreset).size, withPreset.length, `a question was asked twice: ${withPreset.join(" -> ")}`);
});
test("an un-skipped question is asked once, not twice, on the way past", () => {
const asked = walk({ presetId: "p1", unskip: ["theme"] }, BRIEFING_QUESTIONS, 1);
assert.ok(asked.includes("theme"), "a rewound preset field must be asked again");
assert.equal(new Set(asked).size, asked.length, `a question was asked twice: ${asked.join(" -> ")}`);
});
/**
* The two tiers.
*
* The briefing was fifteen questions walked one card at a time, and a user who
* answered none of them still clicked through all fifteen. The tier is what
* lets the surface render each group as one form instead of a sequence, so
* these assert the split itself rather than any particular ordering.
*/
test("every question declares a tier, and required stays small enough to be one card", () => {
for (const list of [BRIEFING_QUESTIONS, REPORT_QUESTIONS]) {
for (const q of list) {
assert.ok(q.tier === "required" || q.tier === "optional", `${q.key} has no tier`);
}
}
// The point of the split is that the first screen is answerable at a glance.
assert.ok(tierQuestions("deck", "required", {}, []).length <= 5);
assert.ok(tierQuestions("report", "required", {}, []).length <= 5);
});
test("the preset question disappears when the account has no presets", () => {
// "Use a saved format, or start fresh?" offered exactly one answer on a new
// account — a question that cannot be answered two ways is not a question.
assert.ok(!tierQuestions("deck", "required", {}, []).some((q) => q.key === "preset"));
assert.ok(tierQuestions("deck", "required", {}, [{ id: "p1" }]).some((q) => q.key === "preset"));
});
test("a chosen preset drops the fields it fixes out of the required tier", () => {
const withPreset = tierQuestions("deck", "required", { presetId: "p1" }, [{ id: "p1" }]).map((q) => q.key);
for (const key of PRESET_KEYS) assert.ok(!withPreset.includes(key), `${key} is preset-fixed`);
// ...and rewinding one brings it back, so a preset is a starting point rather
// than a lock.
const rewound = tierQuestions("deck", "required", { presetId: "p1", unskip: ["theme"] }, [{ id: "p1" }]).map((q) => q.key);
assert.ok(rewound.includes("theme"));
});
test("no question is in both tiers, and every question is in one", () => {
for (const kind of ["deck", "report"]) {
const all = questionsFor(kind).map((q) => q.key);
const req = tierQuestions(kind, "required", {}, [{ id: "p1" }]).map((q) => q.key);
const opt = tierQuestions(kind, "optional", {}, [{ id: "p1" }]).map((q) => q.key);
assert.deepEqual([...req, ...opt].sort(), [...all].sort(), `${kind}: the tiers must partition the questions`);
assert.deepEqual(req.filter((k) => opt.includes(k)), [], `${kind}: a question is in both tiers`);
}
});
test("optionalAnswered counts only answers actually given", () => {
assert.equal(optionalAnswered("deck", {}), 0);
assert.equal(optionalAnswered("deck", { title: " ", thesis: "" }), 0, "whitespace is not an answer");
assert.equal(optionalAnswered("deck", { title: "Solar", thesis: "It scales" }), 2);
// The shaped fields count only when they carry something real — an empty
// team is what made the summary read "0 members" as though it were an answer.
assert.equal(optionalAnswered("deck", { team: { members: [] } }), 0);
assert.equal(optionalAnswered("deck", { team: { members: [{ name: "A" }] } }), 1);
assert.equal(optionalAnswered("deck", { guide: { name: "" } }), 0);
assert.equal(optionalAnswered("deck", { academic: { subject: "", year: "" } }), 0);
});
test("image supply is asked in the required tier, beside research", () => {
// A tier is one FORM, so this costs no extra turn — which is the whole reason
// it belongs next to research rather than behind "add detail".
const required = tierQuestions("deck", "required", {}, null).map((q) => q.key);
assert.ok(required.includes("images"), `expected images in the required tier, got ${required.join(", ")}`);
assert.equal(
required.indexOf("images"), required.indexOf("research") + 1,
"it is the same question as research — where the content comes from — so it sits next to it",
);
});
test("a report briefing does not ask about slide images", () => {
// Reports have no image slides; asking would be a question with no effect.
const keys = REPORT_QUESTIONS.map((q) => q.key);
assert.ok(!keys.includes("images"));
});
test("solo mode defaults team-sized parts off and drops the per-member question", () => {
const b = initialBriefing({});
assert.equal(b.mode, "team");
assert.equal(b.subtopicCount, 0);
assert.equal(echoAnswer(b, "mode"), "team deck");
assert.equal(echoAnswer(b, "subtopicCount"), "auto — team-sized");
const solo = { ...b, mode: "solo" };
assert.equal(echoAnswer(solo, "slidesPerMember"), "n/a — solo deep dive");
const keys = tierQuestions("deck", "optional", solo, []).map((q) => q.key);
assert.ok(keys.includes("mode") && keys.includes("subtopicCount"));
assert.ok(!keys.includes("slidesPerMember"), "per-member sizing is meaningless solo");
const teamKeys = tierQuestions("deck", "optional", b, []).map((q) => q.key);
assert.ok(teamKeys.includes("slidesPerMember"));
});
test("free text understands team/solo and part counts", () => {
assert.equal(applyFreeText({}, "mode", "just me, solo deep dive").briefing.mode, "solo");
assert.equal(applyFreeText({}, "mode", "team of four").briefing.mode, "team");
assert.equal(applyFreeText({}, "mode", "maybe"), null, "unmatched text is rejected");
assert.equal(applyFreeText({}, "subtopicCount", "6").briefing.subtopicCount, 6);
assert.equal(applyFreeText({}, "subtopicCount", "auto").briefing.subtopicCount, 0);
assert.equal(applyFreeText({}, "subtopicCount", "lots"), null);
});
test("a preset carries the part count but never the mode", () => {
const b = briefingFromPreset({ subtopicCount: 5, mode: "solo" }, {});
assert.equal(b.subtopicCount, 5);
assert.equal(b.mode, "team", "mode is always asked, never preset-fixed");
const payload = presetPayload({ subtopicCount: 5, mode: "solo" });
assert.equal(payload.subtopicCount, 5);
assert.equal(payload.mode, undefined);
});