test/insert-slide.test.js
Rama main · réplica de lectura del repositorio original.
import { test } from "node:test";
import assert from "node:assert/strict";
import { chooseInsertType, insertPurpose, insertSlide } from "../src/ai/generate.js";
import { familyFor } from "../src/ai/catalog.js";
/**
* Adding a slide to a finished deck.
*
* `insert_slide` has always been in the ops grammar and a chat turn can emit
* it, which is why this looked like it already existed. What came back was
* written by the CHAT path — the merged ops grammar of every type at once,
* whatever research the turn happened to carry, and none of the post-passes —
* so the slide arrived visibly unlike its neighbours. These tests hold the
* three things that make it a sibling instead: a type chosen for variety, a
* purpose derived from where it lands, and a write through the slide writer.
*/
const deckOf = (...types) => ({
title: "Vehicle-to-Grid",
slides: [
{ type: "title", headline: "Vehicle-to-Grid" },
{ type: "section", headline: "Economics", section: 0 },
...types.map((t, i) => ({ type: t, headline: `Point ${i + 1}`, section: 0 })),
],
});
const PLAN = { title: "Vehicle-to-Grid", sections: ["Economics", "Hardware"], slides: [] };
/* ------------------------------------------------------------ the type */
test("the inserted type is never one of its immediate neighbours", () => {
const deck = deckOf("bullets", "chart", "bullets");
for (let after = 2; after < deck.slides.length; after++) {
const t = chooseInsertType(deck, after);
assert.notEqual(t, deck.slides[after]?.type, `repeats the slide before (after ${after})`);
assert.notEqual(t, deck.slides[after + 1]?.type, `repeats the slide after (after ${after})`);
}
});
test("the inserted type is never structural — a divider is not content", () => {
const deck = deckOf("bullets", "bullets");
for (let after = 1; after < deck.slides.length; after++) {
const t = chooseInsertType(deck, after);
assert.ok(
!["title", "section", "chapter", "closing", "agenda", "epigraph", "references", "contact"].includes(t),
`chose the structural type "${t}"`,
);
}
});
test("repeated inserts do not all come out the same", () => {
// The deck leans away from families it already uses, so five inserts in a
// row give five different families rather than five of whatever sorts first.
let deck = deckOf("bullets", "bullets");
const families = [];
for (let i = 0; i < 5; i++) {
const t = chooseInsertType(deck, 2);
families.push(familyFor(t));
deck = { ...deck, slides: [...deck.slides.slice(0, 3), { type: t, section: 0 }, ...deck.slides.slice(3)] };
}
assert.equal(new Set(families).size, families.length, `families repeated: ${families.join(", ")}`);
});
test("an explicit type is honoured — the choice is a default, not a policy", async () => {
const deck = deckOf("bullets", "bullets");
let seen = null;
const chat = async ({ schema }) => {
seen = schema.properties.ops.items.properties.slide.properties.type;
return { data: { ops: [{ op: "append_slide", slide: { type: "chart", headline: "H", chart: { kind: "bar", series: [{ label: "a", value: 1 }] } } }] } };
};
await insertSlide({ deck, plan: PLAN, after: 2, type: "chart", chat });
assert.deepEqual(seen, { enum: ["chart"] }, "the grammar must be scoped to the chosen type");
});
/* --------------------------------------------------------- the purpose */
test("the purpose names the part and both neighbours", () => {
const deck = deckOf("bullets", "bullets");
deck.slides[2].headline = "Depot revenue is real";
deck.slides[3].headline = "Tariffs decide the spread";
const { section, purpose } = insertPurpose(deck, PLAN, 2);
assert.equal(section, 0, "an inserted slide belongs to the part it lands in");
assert.match(purpose, /Economics/, "the part's argument");
assert.match(purpose, /Depot revenue is real/, "what it follows");
assert.match(purpose, /Tariffs decide the spread/, "what it precedes");
assert.match(purpose, /without restating either/);
});
test("a purpose at the end of a deck names only what it follows", () => {
const deck = deckOf("bullets");
deck.slides[2].headline = "The last point";
const { purpose } = insertPurpose(deck, PLAN, 2);
assert.match(purpose, /The last point/);
assert.ok(!/is followed by/.test(purpose), purpose);
});
test("a deck with no plan still yields a usable purpose", () => {
const { purpose, section } = insertPurpose(deckOf("bullets"), null, 2);
assert.ok(purpose.trim().length > 0);
assert.equal(section, 0);
});
/* ------------------------------------------------------------ the write */
const writeChat = (slide) => async () => ({ data: { ops: [{ op: "append_slide", slide }] } });
test("the new slide lands in the right place and keeps the deck intact", async () => {
const deck = deckOf("bullets", "chart", "bullets");
const before = deck.slides.length;
const r = await insertSlide({
deck, plan: PLAN, after: 2, type: "stats",
chat: writeChat({ type: "stats", headline: "Measured", stats: [
{ value: "41", label: "EUR per bus" }, { value: "91%", label: "retention" },
{ value: "14", label: "months" }, { value: "32", label: "buses" },
] }),
});
assert.equal(r.index, 3);
assert.equal(r.deck.slides.length, before + 1);
assert.equal(r.deck.slides[3].type, "stats");
// Everything either side is untouched and in order.
assert.deepEqual(r.deck.slides.slice(0, 3).map((s) => s.type), deck.slides.slice(0, 3).map((s) => s.type));
assert.deepEqual(r.deck.slides.slice(4).map((s) => s.type), deck.slides.slice(3).map((s) => s.type));
});
test("the new slide inherits the section it was inserted into", async () => {
const deck = deckOf("bullets", "bullets");
deck.slides[2].section = 1;
deck.slides[3].section = 1;
const r = await insertSlide({
deck, plan: PLAN, after: 2, type: "bullets",
chat: writeChat({ type: "bullets", headline: "H", bullets: ["a", "b", "c"] }),
});
assert.equal(r.deck.slides[3].section, 1, "or assignPresenters cannot give it an owner");
});
test("a presenter the model volunteered is dropped, as everywhere else", async () => {
// Presenters are assigned centrally by distributePresenters after the fact.
const r = await insertSlide({
deck: deckOf("bullets"), plan: PLAN, after: 2, type: "bullets",
chat: writeChat({ type: "bullets", headline: "H", bullets: ["a", "b", "c"], presenter: "Someone" }),
});
assert.equal(r.slide.presenter, undefined);
});
test("the writer is shown the deck UP TO the insertion point, not past it", async () => {
// "SLIDES SO FAR" has to mean what precedes this slide, or the model writes
// as though the later slides had already been said.
const deck = deckOf("bullets", "chart", "timeline");
let user = null;
await insertSlide({
deck, plan: PLAN, after: 2, type: "stats",
chat: async ({ messages }) => {
user = messages.at(-1).content;
return { data: { ops: [{ op: "append_slide", slide: { type: "stats", headline: "H", stats: [
{ value: "1", label: "a" }, { value: "2", label: "b" }, { value: "3", label: "c" }, { value: "4", label: "d" },
] } }] } };
},
});
assert.match(user, /Point 1/, "the slide it follows should be in context");
assert.ok(!/Point 3/.test(user), "a slide that comes AFTER must not be listed as already written");
});
test("an out-of-range insertion point is refused, not clamped", async () => {
const deck = deckOf("bullets");
const chat = writeChat({ type: "bullets", headline: "H", bullets: ["a", "b", "c"] });
await assert.rejects(() => insertSlide({ deck, plan: PLAN, after: 99, chat }), /cannot insert/);
await assert.rejects(() => insertSlide({ deck, plan: PLAN, after: -5, chat }), /cannot insert/);
});
test("a model that produces nothing fails loudly rather than silently", async () => {
await assert.rejects(
() => insertSlide({ deck: deckOf("bullets"), plan: PLAN, after: 2, chat: async () => ({ data: { ops: [] } }) }),
/no slide to insert/,
);
});