Ctrl K
ComunidadGuías…

test/briefing.test.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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);
});