test/finalize-presenters.test.js
Rama main · réplica de lectura del repositorio original.
import { test } from "node:test";
import assert from "node:assert/strict";
import { mkdtemp, mkdir, writeFile, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import YAML from "yaml";
import { DIVIDER_TYPES } from "../src/ai/team.js";
// finalizeDeck reads DECKS from src/paths.js at import time, so this test
// imports the pipeline fresh after pointing the env at a scratch dir.
const scratch = await mkdtemp(path.join(tmpdir(), "forge-finalize-"));
process.env.FORGE_DECKS_DIR = scratch;
// Pinned for the same reason FORGE_DECKS_DIR is: finalize now records the
// deck's score, and an unpinned config dir writes that row into the real
// config/scores.jsonl — a test polluting the operator's history.
process.env.FORGE_CONFIG_DIR = process.env.FORGE_CONFIG_DIR ?? path.join(scratch, "config");
const { finalizeDeck } = await import("../src/ai/pipeline.js");
/** A fake model client: coherence review finds nothing, field-length rewrites
* nothing. A deck with short content never flags, so no model calls occur —
* the fake exists to make the pipeline not touch Ollama even if one does. */
const fakeChat = async () => ({ data: { findings: [] } });
async function makeDeck(slug) {
const dir = path.join(scratch, slug);
await mkdir(path.join(dir, "research"), { recursive: true });
const meta = {
slug,
brief: "finalize presenter test",
theme: "warm-humanist",
slidesPerMember: 1,
team: {
label: "Group T",
members: [
{ name: "A", roll: "1", presenting: true },
{ name: "B", roll: "2", presenting: true },
{ name: "C", roll: "3", presenting: true },
],
},
};
await writeFile(path.join(dir, "meta.yaml"), YAML.stringify(meta));
const plan = {
title: "finalize presenter test",
sections: ["Intro", "Body", "Close"],
slides: [
{ type: "title", section: 0, purpose: "open" },
{ type: "section", section: 0, purpose: "intro part" },
{ type: "bullets", section: 0, purpose: "key points" },
{ type: "bullets", section: 1, purpose: "body points" },
{ type: "section", section: 1, purpose: "body part" },
{ type: "bullets", section: 2, purpose: "close points" },
{ type: "closing", section: 2, purpose: "close" },
],
};
await writeFile(path.join(dir, "plan.yaml"), YAML.stringify(plan));
// The deck as the resume shortcut would leave it: content written, but with
// NO presenters — the write half assigns them in memory only at the end, and
// finalize re-reads deck.yaml from disk.
const deck = {
title: plan.title,
sections: plan.sections,
theme: "warm-humanist",
slides: [
{ type: "title", headline: "finalize presenter test", subtitle: "s" },
{ type: "section", section: 0, headline: "Intro" },
{ type: "bullets", section: 0, headline: "Key points", bullets: ["a", "b", "c", "d"] },
{ type: "bullets", section: 1, headline: "Body points", bullets: ["a", "b", "c", "d"] },
{ type: "section", section: 1, headline: "Body" },
{ type: "bullets", section: 2, headline: "Close points", bullets: ["a", "b", "c", "d"] },
{ type: "closing", headline: "Thanks" },
],
};
await writeFile(path.join(dir, "deck.yaml"), YAML.stringify(deck));
return dir;
}
test("finalizeDeck assigns presenters to a deck that reached finalize presenter-less", async () => {
const dir = await makeDeck("finalize-presenters");
try {
const r = await finalizeDeck({ slug: "finalize-presenters", model: "mock", chat: fakeChat });
const content = r.deck.slides.filter((s) => !DIVIDER_TYPES.has(s.type));
assert.equal(content.length, 3, "three content slides");
for (const s of content) assert.ok(s.presenter, `content slide gets a presenter (${s.type})`);
// All three presenters are the team's — one per member, in order.
assert.deepEqual(content.map((s) => s.presenter), ["A", "B", "C"]);
// Dividers never carry a presenter.
for (const s of r.deck.slides) {
if (DIVIDER_TYPES.has(s.type)) assert.equal(s.presenter, undefined, `divider ${s.type} carries nothing`);
}
// The persisted deck.yaml carries the assignment too — finalize re-reads it
// from disk, so the assignment must be on the file render reads.
const persisted = YAML.parse(await readFile(path.join(dir, "deck.yaml"), "utf8"));
const pContent = persisted.slides.filter((s) => !DIVIDER_TYPES.has(s.type));
for (const s of pContent) assert.ok(s.presenter, `persisted content slide gets a presenter`);
} finally {
await rm(dir, { recursive: true, force: true });
}
});