Ctrl K
ComunidadGuías…

test/fieldlength.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 { fieldLengthPass, fieldInventory, looksCutAtCap } from "../src/ai/fieldlength.js";
import { slideCatalog, catalogForType } from "../src/ai/catalog.js";
import { readFile } from "node:fs/promises";

/** The schema's own cap for a field, so a corrected cap does not fail a test
 *  that is about the inventory rather than about the number. */
async function schemaCap(type, path) {
  const schema = JSON.parse(await readFile(new URL("../schema/deck.schema.json", import.meta.url), "utf8"));
  const rule = schema.definitions.slide.allOf.find((r) => r.if?.properties?.type?.const === type);
  let node = rule.then.properties;
  for (const seg of path) node = seg === "[]" ? node.items : (node.properties ?? node)[seg];
  return node.maxLength;
}

const DECK = {
  title: "Field length",
  theme: "warm-humanist",
  slides: [
    {
      type: "framework",
      headline: "One Node, Four Parallel Levels",
      concept: { title: "Hybrid Node Layout", body: "MPI ranks own NUMA domains, OpenMP…" },
      elements: [
        { title: "MPI Ranks", body: "One rank per NUMA domain, not per core…" },
        { title: "OpenMP Threads", body: "Threads inside a rank share its memory…" },
        { title: "CUDA", body: "GPUs attached per rank" },
      ],
    },
    {
      type: "flow",
      headline: "Decompose",
      direction: "ltr",
      steps: [
        { title: "Decompose", body: "Split the application into fine-grained tasks that carry their own data use." },
        { title: "Express", body: "Task inputs and outputs form a directed acyclic graph (DAG) the…" },
      ],
    },
  ],
};

/** A fake chat returning a full-slide update_slide op (the shape this local
 *  model actually drifts toward) for both flagged slides. */
function fakeChat() {
  return async ({ schema }) => {
    const type = schema.properties.ops.items.properties.slide.properties.type.enum[0];
    if (type === "framework") {
      return {
        data: {
          ops: [{
            op: "update_slide",
            slide: {
              type: "framework",
              // A whole-slide replacement must carry the headline too: it is
              // required on every content type, so an update that drops it is
              // rejected and the repair discarded — which is the behaviour, not
              // a fixture detail.
              headline: "One Node, Four Parallel Levels",
              concept: { title: "Hybrid Node Layout", body: "MPI ranks own NUMA domains, OpenMP threads share rank memory." },
              elements: [
                { title: "MPI Ranks", body: "One rank per NUMA domain, not per core." },
                { title: "OpenMP Threads", body: "Threads inside a rank share its memory." },
                { title: "CUDA", body: "GPUs attached per rank" },
              ],
            },
          }],
        },
      };
    }
    return {
      data: {
        ops: [{
          op: "update_slide",
          slide: {
            type: "flow",
            headline: "Decompose",
            direction: "ltr",
            steps: [
              { title: "Decompose", body: "Split the application into fine-grained tasks that carry their own data use." },
              { title: "Express", body: "Task inputs and outputs form a directed acyclic graph the scheduler orders." },
            ],
          },
        }],
      },
    };
  };
}

test("fieldInventory states the schema cap per field — the writer's budget", async () => {
  const slide = DECK.slides[0];
  const inv = await fieldInventory(slide);
  const concept = inv.find((f) => f.path === "concept.body");
  assert.ok(concept, "concept.body is inventoried");
  assert.equal(concept.cap, await schemaCap("framework", ["concept", "body"]));
  const elem = inv.find((f) => f.path === "elements[].body");
  const elemCap = await schemaCap("framework", ["elements", "[]", "body"]);
  assert.equal(elem.cap, elemCap, "nested element bodies carry their own cap");
  assert.notEqual(elem.cap, concept.cap, "and not the parent's");
});

test("the catalog spells out the per-field caps, not just per-type", async () => {
  const catalog = await slideCatalog();
  assert.ok(catalog.includes("concept"), "framework concept field named");
  assert.ok(catalog.includes("≤120"), "a char cap is in the catalog");
  const flow = await catalogForType("flow");
  assert.ok(flow.includes("body ≤120") || flow.includes("≤120"), "flow step body cap reaches the writer");
});

test("fieldLengthPass rewrites mid-sentence ellipses into complete sentences", async () => {
  const r = await fieldLengthPass({ deck: DECK, model: "mock", chat: fakeChat() });
  const s6 = r.deck.slides[0];
  assert.ok(!s6.concept.body.includes("…"), "no ellipsis left in the concept body");
  assert.ok(s6.concept.body.length <= 120, "within the cap");
  assert.equal(s6.concept.body, "MPI ranks own NUMA domains, OpenMP threads share rank memory.");
  const rep = r.repaired.filter((x) => x.index === 0);
  assert.ok(rep.length >= 1, "the repair is recorded");
  assert.ok(r.problems.length === 0);
  // The flow slide's mid-sentence ellipsis is repaired too.
  assert.ok(!r.deck.slides[1].steps[1].body.includes("…"), "no ellipsis left in the flow step");
  assert.ok(r.deck.slides[1].steps[1].body.length <= 120);
});

test("fieldLengthPass leaves a deck with nothing overlong alone", async () => {
  const clean = {
    title: "t",
    theme: "warm-humanist",
    slides: [{ type: "bullets", headline: "Points", bullets: ["one", "two", "three", "four"] }],
  };
  const r = await fieldLengthPass({ deck: clean, model: "mock", chat: () => { throw new Error("no model call expected"); } });
  assert.deepEqual(r.repaired, []);
  assert.deepEqual(r.deck, clean);
});

/* ------------------------------------------------- what a grammar cut looks like */

/**
 * `looksCutAtCap` used to be "at cap and no terminal punctuation", which is
 * true of every headline ever written — a headline is a noun phrase and ends
 * without a full stop by design. Each false positive is a rewrite call that was
 * not needed, and since tokens stopped being free that is money.
 */
test("a headline at its cap is not a cut", () => {
  for (const h of [
    "Weighing V2G Economics Against Grid Impact",
    "Bidirectional Charging Cuts Depot Costs",
  ]) {
    assert.equal(looksCutAtCap(h, h.length), false, h);
  }
});

test("a single-token value at its cap is not a cut", () => {
  // Its cap is small so it sits at cap constantly, and it is complete.
  for (const v of ["$10.9M", "27.6%", "RTX-4090", "1000h"]) {
    assert.equal(looksCutAtCap(v, v.length), false, v);
  }
});

test("a phrase ending where no writer would end is a cut", () => {
  // Both observed on a real feature-grid.
  for (const c of [
    "Depot charging improves stability and P",
    "cheaper to run than spiro-OM",
    "Grid revenue averaged EUR 41 per bus per",
    "Costs fall as fleets scale, and utilisation",
  ]) {
    assert.equal(looksCutAtCap(c, c.length), true, c);
  }
});

test("a field under its cap is never a cut, however it ends", () => {
  assert.equal(looksCutAtCap("Ends on and", 200), false);
});

test("a finished sentence at its cap is not a cut", () => {
  const s = "Depot charging improves operational stability.";
  assert.equal(looksCutAtCap(s, s.length), false);
});