← Files DataARCHIVED FILE

templates/data-app/base/tests/report-contextual-stories.test.mjs

15.7 KB · Sep 30, 2026 · 23:19 UTC

↓ Download file

import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { spawnSync } from "node:child_process";
import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import { contextualCases, contextualAnnotations, contextualAnswer, reconcileContextAnnotations } from "../examples/reports/contextual-stories/context.mjs";
import { createContextualStories, joinContextRows, reproduceContextualStories } from "../examples/reports/contextual-stories/stories.mjs";
import { resolveChartAnnotations } from "../src/charting/chart-annotations.js";
import { normalizeInlineChartInput } from "../../../../skills/visualize-data/scripts/inline-chart-input.mjs";

const input = readFileSync(new URL("../examples/reports/contextual-stories/evidence.json", import.meta.url), "utf8");
const evidence = JSON.parse(input);
const generatedAt = "2026-08-20T00:00:00Z";
const { snapshot, results } = createContextualStories(input, generatedAt);
const chartSpec = (story) => ({ type: story.chartType, x: "date", y: story.valueField,
  fields: [story.valueField], yLabel: story.yLabel, showLegend: false });
const rowsFor = (story) => snapshot.queries[story.queryId].rows;
const reviseContext = (story, rows, context) => rows.map((row) => row.contextId === story.recordId ? { ...row, context } : row);

test("three new fictional cases retain exact observations, records, dates, and source hashes", () => {
  assert.equal(evidence.fictional, true);
  assert.equal(results.inputSha256, createHash("sha256").update(input).digest("hex"));
  assert.equal(results.inputSha256, "1d25fd6add04db4ac7202fac21d47b47c5b4710a8508e893b0e7975516ddacaf");
  assert.deepEqual(Object.keys(snapshot.queries), ["operations_history", "adjustment_history", "coverage_history"]);
  assert.deepEqual(results.cases.map(({ rowCount }) => rowCount), [8, 6, 6]);
  for (const story of contextualCases) {
    const source = evidence.cases.find(({ id }) => id === story.id);
    const original = structuredClone(source);
    assert.deepEqual(rowsFor(story), joinContextRows(source.observations, source.contextRecords));
    assert.deepEqual(source, original, "The join does not mutate source evidence");
    assert.ok(rowsFor(story).every((row) => row.date <= generatedAt.slice(0, 10)));
    assert.ok(snapshot.queries[story.queryId].source.metricDefinitions.every((definition) =>
      definition.componentIds.every((id) => [`context-${story.id}-chart`, `context-${story.id}-answer`].includes(id))));
    assert.equal(snapshot.queries[story.queryId].source.sql, undefined, "A file computation must not invent executed SQL");
  }
  assert.equal(results.cases.find(({ id }) => id === "adjustment").withoutOneTime[2].postedBillingsUsd, 13200);
});

test("annotations state contextual records absent from the plotted measures", () => {
  for (const story of contextualCases) {
    const rows = rowsFor(story);
    const [annotation] = contextualAnnotations(story, rows);
    const record = evidence.cases.find(({ id }) => id === story.id).contextRecords[0];
    assert.equal(annotation.label, record.text);
    assert.equal(annotation.at, record.date);
    assert.equal(annotation.field, story.id === "adjustment" ? story.valueField : "context");
    assert.equal(annotation.kind, story.id === "adjustment" ? "point" : "event");
    assert.equal(rows.find((row) => row.context).contextId, story.recordId);
    const [resolved] = resolveChartAnnotations({ ...chartSpec(story), annotations: [annotation] }, rows);
    assert.equal(resolved.x, record.date);
    if (story.id === "adjustment") assert.equal(resolved.y, rows.find(row => row.date === record.date).postedBillingsUsd);
    assert.ok(!contextualAnswer(story, rows).includes(annotation.label), "The prose supplies implications instead of repeating the note");
  }
});

test("missing, ambiguous, or contradictory context removes the annotation instead of trusting its anchor", () => {
  for (const story of contextualCases) {
    const rows = rowsFor(story);
    const recordRow = rows.find((row) => row.contextId === story.recordId);
    for (const context of ["", " ", null, "x".repeat(161)])
      assert.deepEqual(contextualAnnotations(story, reviseContext(story, rows, context)), []);
    assert.deepEqual(contextualAnnotations(story, rows.map((row) => ({ ...row, contextId: "" }))), []);
    assert.deepEqual(contextualAnnotations(story, [...rows, { ...recordRow }]), []);
    assert.deepEqual(contextualAnnotations(story, rows.filter((row) => row !== recordRow)), []);
    assert.deepEqual(contextualAnnotations(story, rows.map((row) => row === recordRow ? { ...row, date: "2026-02-30" } : row)), []);
  }
  const coverage = contextualCases.find(({ id }) => id === "coverage");
  assert.deepEqual(contextualAnnotations(coverage, rowsFor(coverage).map((row) => ({ ...row, coveredLocations: 10 }))), []);
  const adjustment = contextualCases.find(({ id }) => id === "adjustment");
  assert.deepEqual(contextualAnnotations(adjustment, rowsFor(adjustment).map((row) => ({ ...row, oneTimeAmount: 0 }))), []);
});

test("exact joins and receipts omit missing or conflicting date matches", () => {
  for (const change of [
    (source) => { source.contextRecords = []; },
    (source) => { source.contextRecords[0].date = "2026-01-01"; },
    (source) => { source.contextRecords.push({ ...source.contextRecords[0], id: "OTHER-RECORD", text: "Conflicting context" }); },
  ]) {
    const revised = structuredClone(evidence);
    change(revised.cases[0]);
    const result = createContextualStories(JSON.stringify(revised), generatedAt);
    assert.equal(result.results.cases[0].annotationLabel, null);
    assert.equal(result.results.cases[0].annotationDate, null);
    assert.equal(result.snapshot.contextualStories[0].annotationLabel, null);
    assert.deepEqual(contextualAnnotations(contextualCases[0], result.snapshot.queries.operations_history.rows), []);
  }
});

test("generated annotation revisions refresh after repeated context corrections and saved chart edits", () => {
  for (const story of contextualCases) {
    const base = chartSpec(story);
    const rows = rowsFor(story);
    let saved = reconcileContextAnnotations(story, base, undefined, rows);
    saved = { ...saved, colors: { [story.valueField]: "#123456" }, showXAxisLabel: false };
    const revisions = new Set([saved.annotations[0].id]);
    for (const context of ["Additional shift recorded", "Shift schedule revised", "Original change withdrawn"]) {
      const original = structuredClone(saved);
      const next = reconcileContextAnnotations(story, base, saved, reviseContext(story, rows, context));
      assert.equal(next.annotations.length, 1);
      assert.equal(next.annotations[0].label, context);
      assert.equal(next.annotations[0].at, saved.annotations[0].at);
      assert.equal(next.colors, saved.colors);
      assert.equal(next.showXAxisLabel, false);
      assert.deepEqual(saved, original, "Saved presentation objects remain immutable");
      revisions.add(next.annotations[0].id);
      saved = next;
    }
    assert.equal(revisions.size, 4, "The annotation ID identifies the context revision");
    assert.deepEqual(reconcileContextAnnotations(story, base, saved, reviseContext(story, rows, "")).annotations, []);
  }
});

test("custom annotation edits, unknown IDs, and explicit empty lists survive reconciliation", () => {
  for (const story of contextualCases) {
    const base = chartSpec(story);
    const rows = rowsFor(story);
    const [original] = contextualAnnotations(story, rows);
    const revised = reviseContext(story, rows, "Updated context record");
    for (const changes of [{ label: "Author's custom context" }, { at: rows[0].date },
      { field: "customEvidence" }, { kind: original.kind === "point" ? "event" : "point" }, { id: "user-authored-note" }]) {
      const custom = { ...original, ...changes };
      assert.deepEqual(reconcileContextAnnotations(story, base, { ...base, annotations: [custom] }, revised).annotations, [custom]);
      assert.deepEqual(reconcileContextAnnotations(story, base, { ...base, annotations: [custom] }, []).annotations, [custom]);
    }
    const unknown = { ...original, id: "independent-note", label: "Separate source context" };
    const saved = { ...base, annotations: [original, unknown] };
    assert.deepEqual(reconcileContextAnnotations(story, base, saved, []).annotations, [unknown]);
    assert.deepEqual(reconcileContextAnnotations(story, base, { ...base, annotations: [] }, rows).annotations, []);
    assert.equal(reconcileContextAnnotations(story, base, { ...base, type: "area" }, rows).type, "area");
  }
});

test("the owned legacy billing event migrates to the bar value without replacing saved edits", () => {
  const story = contextualCases.find(entry => entry.id === "adjustment");
  const rows = rowsFor(story), base = chartSpec(story);
  const legacy = { id: "context-adjustment-BILL-104-5b78af45", kind: "event", field: "context",
    at: "2026-07-20", label: "One-time catch-up invoice for June work posted" };
  const saved = { ...base, type: "area", colors: { postedBillingsUsd: "#123456" }, annotations: [legacy] };
  const next = reconcileContextAnnotations(story, base, saved, rows);
  assert.deepEqual(next.annotations, contextualAnnotations(story, rows));
  assert.equal(next.type, saved.type);
  assert.equal(next.colors, saved.colors);
  assert.equal(saved.annotations[0], legacy);
  for (const changes of [{ label: "User's invoice note" }, { at: "2026-07-13" }, { field: "custom" }, { kind: "point" }]) {
    const custom = { ...legacy, ...changes };
    assert.deepEqual(reconcileContextAnnotations(story, base, { ...saved, annotations: [custom] }, rows).annotations, [custom]);
  }
  assert.deepEqual(reconcileContextAnnotations(story, base, { ...saved, annotations: [] }, rows).annotations, []);
});

test("interpretations respond to corrected context and observations", () => {
  for (const story of contextualCases) {
    const rows = rowsFor(story);
    const baseline = contextualAnswer(story, rows);
    assert.notEqual(contextualAnswer(story, reviseContext(story, rows, "Corrected record")), baseline);
    assert.notEqual(contextualAnswer(story, reviseContext(story, rows, "")), baseline);
    const revised = story.id === "coverage" ? rows.map((row) => ({ ...row, coveredLocations: 10 }))
      : rows.map((row) => ({ ...row, [story.valueField]: 1 }));
    assert.notEqual(contextualAnswer(story, revised), baseline);
  }
});

test("actual inline projection retains explicitly approved contextual text and record provenance without extra plotted fields", () => {
  for (const story of contextualCases) {
    const query = snapshot.queries[story.queryId];
    const chart = reconcileContextAnnotations(story, chartSpec(story), undefined, query.rows);
    const projected = normalizeInlineChartInput({ schemaVersion: 1, id: `context-${story.id}-chart`,
      queryId: story.queryId, title: story.title, description: "Fictional example.",
      chart, rows: query.rows, source: query.source,
      columns: story.id === "adjustment" ? [] : ["context"] });
    if (story.id !== "adjustment") assert.ok(projected.rows.some((row) => row.context === story.originalContext));
    assert.ok(projected.rows.every((row) => !Object.hasOwn(row, "contextId")), "Record IDs belong in provenance, not a forced plotted field");
    const provenance = JSON.stringify(projected.query.source);
    assert.ok(provenance.includes(story.originalContext), "Export retains the contextual source statement even for numeric point anchors");
    assert.ok(provenance.includes(story.recordId));
    if (story.id === "adjustment") assert.ok(provenance.includes("24,000"), "The exact ledger amount remains in source context");
    assert.ok(JSON.stringify(projected.query.source).includes(results.inputSha256));
    assert.ok(JSON.stringify(projected.query.source).includes("src/content/report/evidence/evidence.json"));
    assert.equal(resolveChartAnnotations(projected.component.chart, projected.rows).length, 1);
  }
});

test("new artifacts get fresh IDs while reproduction preserves identity, title, dates, and metadata", () => {
  assert.notEqual(createContextualStories(input, generatedAt).snapshot.id, snapshot.id);
  const existing = { ...snapshot, title: "Saved report title", generatedAt: "2026-08-19T10:00:00Z",
    legacyPresentationTitle: "Earlier title", report: { asOf: "2026-08-10", audience: "technical" },
    publication: { destination: "existing-private-site" }, customMetadata: { preserve: true } };
  const before = structuredClone(existing);
  const revised = structuredClone(evidence);
  revised.cases[0].contextRecords[0].text = "Updated intake record";
  const { snapshot: reproduced } = reproduceContextualStories(existing, JSON.stringify(revised));
  assert.deepEqual(existing, before);
  for (const key of ["id", "title", "generatedAt", "legacyPresentationTitle", "report", "publication", "customMetadata"])
    assert.deepEqual(reproduced[key], existing[key]);
  assert.equal(reproduced.contextualStories[0].annotationLabel, "Updated intake record");
  assert.ok(reproduced.queries.operations_history.rows.some((row) => row.context === "Updated intake record"));
  const { id: _, legacyPresentationTitle: __, ...legacy } = existing;
  const migrated = reproduceContextualStories(legacy, input).snapshot;
  assert.ok(migrated.id);
  assert.equal(migrated.legacyPresentationTitle, existing.title);
});

test("the generated-project CLI reproduces through a directory alias", () => {
  const project = mkdtempSync(join(tmpdir(), "contextual-reproducer-test-"));
  try {
    const content = join(project, "src/content/report");
    mkdirSync(join(content, "evidence"), { recursive: true });
    for (const file of ["stories.mjs", "context.mjs"])
      cpSync(new URL(`../examples/reports/contextual-stories/${file}`, import.meta.url), join(content, file));
    writeFileSync(join(content, "evidence/evidence.json"), input);
    const existing = { ...snapshot, title: "Saved CLI title", report: { asOf: "2026-08-10" },
      publication: { destination: "existing-private-site" } };
    writeFileSync(join(project, "src/data.json"), JSON.stringify(existing));
    const alias = join(project, "report-alias");
    symlinkSync(content, alias, process.platform === "win32" ? "junction" : "dir");
    const run = spawnSync(process.execPath, [join(alias, "stories.mjs")], { cwd: project, encoding: "utf8" });
    assert.equal(run.status, 0, run.stderr);
    assert.equal(JSON.parse(run.stdout).inputSha256, results.inputSha256, "The main guard must execute, not silently exit");
    assert.deepEqual(JSON.parse(readFileSync(join(project, "src/data.json"), "utf8")), existing);
    assert.deepEqual(JSON.parse(readFileSync(join(content, "evidence/story-results.json"), "utf8")), results);
  } finally {
    rmSync(project, { recursive: true, force: true });
  }
});

test("the new gallery keeps shared editing, source wrappers, export disclosure, and a 720px column", () => {
  const root = new URL("../examples/reports/contextual-stories/", import.meta.url);
  const report = readFileSync(new URL("ReportContent.jsx", root), "utf8");
  const css = readFileSync(new URL("report.css", root), "utf8");
  const build = readFileSync(new URL("build.mjs", root), "utf8");
  for (const expected of [/RichNarrative/u, /ReportSection/u, /DataComponent/u, /ChartRenderer/u,
    /chartOverrides\[chartId\]/u, /chartProps\(chartId\)/u, /visible\(chartId\)/u,
    /setAppTitle/u, /description=\{`Fictional example/u, /showLegend: false/u]) assert.match(report, expected);
  assert.match(css, /--data-app-content-width: 720px/u);
  assert.match(build, /Refusing to overwrite an existing project/u);
  assert.match(build, /\["ReportContent.jsx", "context.mjs", "stories.mjs"\]/u);
  assert.doesNotMatch(build, /integrity:authorize|integrity:update|protected-runtime\.json/u);
});

SHA-256: 2c6754497583ce503b80d8b3726df36c751102569ea0b031470efeed935bc932