test/coherence.test.js
Rama main · réplica de lectura del repositorio original.
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");
});