Ctrl K
ComunidadGuías…

test/insert-slide.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 { 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/,
  );
});