Ctrl K
ComunidadGuías…

test/sweep.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 { sweepDeck } from "../src/ai/generate.js";

// A minimal deck exercising the sweep: a title divider (kept untouched), a
// bullets content slide (rewritten), a cards slide (rewritten).
const DECK = {
  title: "Test deck",
  sections: ["Intro", "Body"],
  slides: [
    { type: "title", headline: "Test deck", subtitle: "sub" },
    {
      type: "bullets",
      section: 0,
      presenter: "Alice",
      headline: "Key points",
      bullets: ["one short point", "another short point", "a third short point", "a fourth short point"],
    },
    {
      type: "cards",
      section: 1,
      presenter: "Bob",
      headline: "Facts",
      cards: [{ title: "A", body: "body" }, { title: "B", body: "more" }],
    },
  ],
};

/** A fake model client that returns the given ops, mirroring chatJSON's shape. */
function fakeChat(opsByType) {
  return async ({ schema, messages }) => {
    const slide = messages[1]?.content;
    const type = schema.properties.ops.items.properties.slide.properties.type.enum[0];
    const ops = opsByType[type];
    return { data: { ops: ops ?? [] } };
  };
}

test("sweepDeck rejects an unknown density", async () => {
  await assert.rejects(sweepDeck({ deck: DECK, density: "huge" }), /sparse\|balanced\|dense/);
});

test("sweepDeck gives each slide only its own content — no neighbour leak", async () => {
  // Capture every prompt; each must name only its own slide's content and
  // must carry the explicit independence rule (which lives in the system
  // message, like the rest of the sweep contract).
  const seen = [];
  const chat = async ({ schema, messages }) => {
    seen.push({ user: messages[1]?.content ?? "", system: messages[0]?.content ?? "" });
    const type = schema.properties.ops.items.properties.slide.properties.type.enum[0];
    return { data: { ops: [{ op: "update_slide", index: seen.length - 1, patch: { headline: "H", bullets: ["a", "b", "c", "d"] } }] } };
  };
  await sweepDeck({ deck: DECK, density: "balanced", model: "mock", chat });
  assert.equal(seen.length, 2, "two content slides swept");
  const bullets = seen[0];
  const cards = seen[1];
  // The bullets slide's context carries its own headline but not the cards
  // slide's content, and vice versa.
  assert.ok(bullets.user.includes("Key points"));
  assert.ok(!bullets.user.includes("Facts"), "cards headline must not leak into the bullets context");
  assert.ok(cards.user.includes("Facts"));
  assert.ok(!cards.user.includes("Key points"), "bullets headline must not leak into the cards context");
  assert.ok(bullets.system.includes("Do not copy or carry"), "independence rule present");
  assert.ok(cards.system.includes("Do not copy or carry"), "independence rule present on every slide");
});

test("sweepDeck rewrites content slides and keeps dividers, types and presenters", async () => {
  const chat = fakeChat({
    bullets: [{ op: "update_slide", index: 1, patch: { headline: "Key points", bullets: ["a fuller point one", "a fuller point two", "a third point", "a fourth point"] } }],
    cards: [{ op: "update_slide", index: 2, patch: { headline: "Facts", cards: [{ title: "A", body: "a longer body" }, { title: "B", body: "another" }, { title: "C", body: "third" }] } }],
  });

  const r = await sweepDeck({ deck: DECK, density: "dense", model: "mock", chat });
  assert.deepEqual(r.problems, []);

  // Structure preserved: same types, same count, title divider untouched.
  assert.equal(r.deck.slides.length, 3);
  assert.equal(r.deck.slides[0].type, "title");
  assert.deepEqual(r.deck.slides[0].headline, "Test deck");
  assert.equal(r.deck.slides[1].type, "bullets");
  assert.equal(r.deck.slides[1].presenter, "Alice");
  assert.equal(r.deck.slides[1].section, 0);
  assert.equal(r.deck.slides[2].type, "cards");
  assert.equal(r.deck.slides[2].presenter, "Bob");

  // Content rewritten to dense.
  assert.equal(r.deck.slides[1].bullets.length, 4);
  assert.equal(r.deck.slides[2].cards.length, 3);
  assert.equal(r.swept.length, 2);
});

test("sweepDeck keeps the original slide when a rewrite fails validation", async () => {
  const chat = fakeChat({
    // Ops target the wrong index — the sweep only accepts update_slide at i.
    bullets: [{ op: "update_slide", index: 9, patch: { bullets: ["x"] } }],
    cards: [{ op: "delete_slide", index: 2 }],
  });
  const r = await sweepDeck({ deck: DECK, density: "sparse", model: "mock", chat });
  assert.ok(r.problems.length >= 1);
  // The deck is unchanged.
  assert.deepEqual(r.deck.slides[1].bullets, DECK.slides[1].bullets);
  assert.deepEqual(r.deck.slides[2].cards, DECK.slides[2].cards);
  assert.equal(r.deck.slides.length, 3);
});
test("sweepDeck re-asserts type and presenter even if the patch tries to change them", async () => {
  const chat = fakeChat({
    bullets: [{ op: "update_slide", index: 1, patch: { type: "stats", presenter: "Mallory", bullets: ["x", "y"] } }],
  });
  const r = await sweepDeck({ deck: DECK, density: "balanced", model: "mock", chat });
  assert.equal(r.deck.slides[1].type, "bullets");
  assert.equal(r.deck.slides[1].presenter, "Alice");
});

test("sweepDeck accepts an update_slide that omits the index (the model's drift)", async () => {
  const chat = async ({ schema }) => {
    const type = schema.properties.ops.items.properties.slide.properties.type.enum[0];
    return { data: { ops: [{ op: "update_slide", patch: { headline: "H", bullets: ["a", "b", "c", "d"] } }] } };
  };
  const r = await sweepDeck({ deck: DECK, density: "balanced", model: "mock", chat });
  // Both content slides rewrote despite the missing index.
  assert.equal(r.swept.length, 2);
  assert.equal(r.deck.slides[1].headline, "H");
});

test("sweepDeck accepts a full slide object on an update_slide op", async () => {
  const chat = async ({ schema }) => {
    const type = schema.properties.ops.items.properties.slide.properties.type.enum[0];
    return { data: { ops: [{ op: "update_slide", slide: { type, headline: "Full", bullets: ["a", "b", "c", "d"] } }] } };
  };
  const r = await sweepDeck({ deck: DECK, density: "dense", model: "mock", chat });
  assert.equal(r.deck.slides[1].headline, "Full");
  assert.equal(r.deck.slides[1].type, "bullets", "type survives the full-slide form");
});