Ctrl K
ComunidadGuías…

test/coherence.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 { coherenceReview, coherencePass, digestSlide, buildFixInstruction, nearDuplicateHeadlines, repetitionFindings, coherenceFindings } from "../src/ai/coherence.js";

const DECK = {
  title: "First Impressions and Networking Behaviour Among College Students",
  sections: ["Bias, Face & Blind Spots"],
  slides: [
    { type: "title", headline: "First Impressions and Networking" },
    {
      type: "chart",
      section: 0,
      headline: "The Young Flight Attendant Is a Stereotype the Data No Longer Supports",
      standfirst: "Nearly half of U.S. flight attendants are 45 or older.",
      chart: { kind: "bar", categories: ["16-24", "45+"], series: [{ name: "Share", values: [4.9, 21.4] }], unit: "%" },
    },
    {
      type: "bullets",
      section: 0,
      headline: "Stereotypes shape your first impression before you speak",
      bullets: ["Bias arrives before the handshake.", "What you expect changes how you read the other person.", "A warm read of a stranger is a skill.", "Stereotypes close the door before it opens."],
    },
  ],
};

/** A fake model: the review call flags slide 2 (index 1), the fix turn
 *  rewrites it to tie the stereotype data to first impressions. Distinguishes
 *  the review prompt (asks to review coherence) from the ops prompt (carries
 *  the current deck state). */
function fakeChat(round) {
  return async ({ schema, messages }) => {
    const joined = messages.map((m) => m.content).join("\n");
    if (joined.includes("Review the deck's coherence")) {
      return round === 0
        ? {
            data: {
              findings: [{
                index: 1,
                kind: "topic",
                detail: "flight attendant demographics do not serve a first-impressions deck",
                fix: "re-frame the stereotype data as how expectations bias a first impression",
              }],
            },
          }
        : { data: { findings: [] } };
    }
    // The fix turn — rewrite slide 2 (index 1) to serve the deck's topic.
    return {
      data: {
        ops: [{
          op: "replace_slide",
          index: 1,
          slide: {
            type: "chart",
            section: 0,
            headline: "Stereotypes bias a first impression before you speak",
            standfirst: "A stereotype is a shortcut the mind applies in the first seconds of contact.",
            chart: { kind: "bar", categories: ["16-24", "45+"], series: [{ name: "Share", values: [4.9, 21.4] }], unit: "%" },
            aside: ["Age skews older; expectation, not evidence, shapes the first read."],
          },
        }],
      },
    };
  };
}

test("digestSlide renders a compact per-slide digest", () => {
  const s = DECK.slides[1];
  const d = digestSlide(s, 1);
  assert.ok(d.includes('[1] chart "The Young Flight Attendant'));
});

test("coherenceReview returns findings for a topically drifting slide", async () => {
  const findings = await coherenceReview({ deck: DECK, sections: DECK.sections, model: "mock", chat: fakeChat(0) });
  assert.equal(findings.length, 1);
  assert.equal(findings[0].index, 1);
  assert.equal(findings[0].kind, "topic");
});

test("buildFixInstruction names the slide and the required reframe", () => {
  const instr = buildFixInstruction([{ index: 1, kind: "topic", detail: "drift", fix: "tie it to first impressions" }], DECK.sections);
  assert.ok(instr.includes("Slide 2"));
  assert.ok(instr.includes("topically disconnected"));
  assert.ok(instr.includes("tie it to first impressions"));
});

test("coherencePass rewrites a flagged slide and converges on round two", async () => {
  const r = await coherencePass({ deck: DECK, sections: DECK.sections, model: "mock", chat: fakeChat(0) });
  assert.ok(r.findings.length >= 1, "found the drift");
  // The fix turn replaced slide 2's headline to serve the deck's topic.
  const fixed = r.deck.slides.find((s) => s.type === "chart");
  assert.ok(fixed.headline.includes("Stereotypes bias a first impression"), "headline re-framed to the deck's topic");
  assert.equal(r.problems.length, 0);
});

test("coherencePass leaves a coherent deck untouched", async () => {
  const clean = { ...DECK, slides: [DECK.slides[0], DECK.slides[2]] };
  const chat = async ({ messages }) => {
    const isReview = messages[1]?.content?.includes("Review the deck's coherence");
    return isReview ? { data: { findings: [] } } : { data: { ops: [] } };
  };
  const r = await coherencePass({ deck: clean, sections: DECK.sections, model: "mock", chat });
  assert.equal(r.findings.length, 0);
  assert.equal(r.deck.slides.length, 2);
});

/* -------------------------------------------------- repetition, computed */

/**
 * The coherence pass asked a model about topical drift and about data with no
 * point, and never about repetition — so three slides of the V2G deck each
 * saying a version of "Scaling V2G Requires Cross-Sector Collaboration" went
 * through untouched.
 *
 * It is also the one of the three a reviewer model is worst at: deciding
 * whether slide 21 restates slide 18 means holding the whole deck in view, and
 * the reviewer sees a digest. So it is computed.
 */

const slide = (type, headline) => ({ type, headline });

test("a content slide restating its own section divider is caught", async () => {
  // The real case: slide 18 is the section, slide 21 repeats it verbatim.
  const deck = { slides: [
    slide("title", "Vehicle-to-Grid"),
    slide("section", "Scaling V2G Requires Cross-Sector Collaboration"),
    slide("flow", "Scale Isolated Tests Into A Nationwide Network"),
    slide("framework", "Scaling V2G Requires Cross-Sector Collaboration"),
  ] };
  const found = repetitionFindings(deck);
  assert.equal(found.length, 1);
  assert.equal(found[0].index, 3, "the CONTENT slide is the one at fault");
  assert.equal(found[0].kind, "repetition");
  assert.match(found[0].detail, /repeats slide 2/);
});

test("structure is compared but never rewritten", async () => {
  // A section and a chapter card echoing the part's name is them doing their
  // job. Excluding dividers from the comparison entirely would have hidden the
  // test above, so they are compared and then filtered out of the findings.
  const deck = { slides: [
    slide("section", "Scaling And Collaboration"),
    slide("chapter", "Scaling And Collaboration"),
    slide("closing", "Scaling And Collaboration"),
  ] };
  assert.deepEqual(repetitionFindings(deck), []);
});

test("it matches what two headlines say, not how they spell it", async () => {
  // Raw tokens share three of seven here — under any threshold worth setting.
  const deck = { slides: [
    slide("bullets", "Scaling V2G Requires Cross-Sector Collaboration"),
    slide("bullets", "Cross-Sector Collaboration Is Required To Scale V2G"),
  ] };
  assert.equal(repetitionFindings(deck).length, 1);
});

test("slides making different points are left alone", async () => {
  const deck = { slides: [
    slide("bullets", "Depot Revenue Is Measurable Today"),
    slide("chart", "Tariff Design Decides The Spread"),
    slide("bullets", "Battery Degradation Stays Inside Warranty"),
    slide("callout", "Commissioning Took Eleven Weeks"),
  ] };
  assert.deepEqual(nearDuplicateHeadlines(deck.slides), []);
});

test("the deck's own title is not a duplicate of its parts", async () => {
  const deck = { slides: [
    slide("title", "Vehicle-to-Grid Integration"),
    slide("agenda", "Vehicle-to-Grid Integration"),
    slide("bullets", "Vehicle-to-Grid Integration In Practice"),
  ] };
  assert.deepEqual(repetitionFindings(deck), []);
});

test("only the first of a run survives; the rest are rewritten", async () => {
  const deck = { slides: [
    slide("bullets", "Scaling Requires Cross-Sector Collaboration"),
    slide("bullets", "Cross-Sector Collaboration Is Required For Scaling"),
    slide("bullets", "Scaling Requires Collaboration Across Sectors"),
  ] };
  const found = repetitionFindings(deck);
  assert.deepEqual(found.map((f) => f.index), [1, 2], "the first states the point and stays");
});

test("the fix instruction tells a repeat to make a different point", async () => {
  const msg = buildFixInstruction(
    [{ index: 4, kind: "repetition", detail: "x repeats slide 2", fix: "Advance the argument." }],
    [],
  );
  assert.match(msg, /Slide 5/);
  assert.match(msg, /repeats another slide/);
  assert.match(msg, /DIFFERENT point, not the same one/);
});

test("computed repetition does not double up on a slide the reviewer flagged", async () => {
  // One instruction per slide, or the rewrite is told two different things
  // about the same slide in the same breath.
  const deck = { slides: [
    slide("bullets", "Scaling Requires Cross-Sector Collaboration"),
    slide("bullets", "Cross-Sector Collaboration Enables Scaling"),
  ] };
  const chat = async () => ({ data: { findings: [{ index: 1, kind: "topic", detail: "off topic" }] } });
  const found = await coherenceFindings({ deck, chat });
  assert.equal(found.filter((f) => f.index === 1).length, 1);
  assert.equal(found[0].kind, "topic", "the reviewer's own finding wins");
});

test("a source-map slide is never a coherence finding, even when flagged", async () => {
  const deck = { slides: [
    slide("bullets", "Scaling Requires Cross-Sector Collaboration"),
    { type: "references", headline: "References", items: ["A source (example.com) — used on slide 1"] },
  ] };
  const chat = async () => ({ data: { findings: [{ index: 1, kind: "topic", detail: "data without a point" }] } });
  const found = await coherenceFindings({ deck, chat });
  assert.ok(!found.some((f) => f.index === 1), "the source map survives review verbatim");
});