← Files DataARCHIVED FILE

tests/data-app-build.test.mjs

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

↓ Download file

import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import {
  cpSync,
  existsSync,
  lstatSync,
  mkdirSync,
  mkdtempSync,
  readFileSync,
  readdirSync,
  readlinkSync,
  realpathSync,
  rmSync,
  symlinkSync,
  writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, relative, resolve, sep } from "node:path";
import test, { after } from "node:test";
import { fileURLToPath, pathToFileURL } from "node:url";
import { createContext, Script } from "node:vm";

import { assembleDataAppHtml, embeddedScript, embeddedStyle, localDataThreadId, projectLocalDataThreadId } from "../scripts/data-app-build.mjs";
import { dataNodeEnvironment } from "../scripts/data-app-runtime.mjs";
import { readSeparateDataBundle } from "../scripts/data-app-separate.mjs";
import { RELEASE_INPUTS, sha256 } from "../scripts/prebuilt/manifest.mjs";

const PLUGIN_ROOT = resolve(fileURLToPath(new URL("../", import.meta.url)));
const BASE = "templates/data-app/base";
const ASSETS = "assets/data-app-runtime";
const PACKAGER = "skills/publish-artifact-to-sites/scripts/package-data-app-for-sites.mjs";
const RUNTIME_HASH = "a".repeat(64);
const SESSION_ID = "550e8400-e29b-41d4-a716-446655440000";
const THREAD_ID = "550e8400-e29b-41d4-a716-446655440001";
const SNAPSHOT_ID = "data-app-reviewed-snapshot";
const JAVASCRIPT_URL = "data:text/javascript;charset=utf-8;base64,";
const CSS_URL = "data:text/css;charset=utf-8;base64,";

function input(overrides = {}) {
  return {
    appCode: `globalThis.execution = ["runtime"];
globalThis.CodexDataAppRuntime = {
  apiVersion: 1,
  mount(options) {
    const content = options.createContent?.(options.reviewedSnapshot) ?? {};
    globalThis.captured = {...options, ...content}; execution.push("mount");
  }
};`,
    protectedStyles: ".protected{color:blue}",
    printStyles: "@media print{.printed{color:black}}",
    authored: {
      themeCss: ":root{--accent:red}",
      conventionalCss: ".conventional{color:green}",
      importedCss: ".imported{color:purple}",
      factorySource: `(function(runtime, snapshot) {
  execution.push("factory");
  globalThis.factorySnapshot = snapshot;
  return {
    DashboardContent() { return snapshot.title; },
    ReportContent() { return snapshot.queries; }
  };
})`,
    },
    snapshotBytes: Buffer.from('{\n  "title": "Reviewed fixture",\n  "queries": {}\n}\n'),
    runtimeSha256: RUNTIME_HASH,
    ...overrides,
  };
}

// This reads only the assembler's fixed output shape. It is deliberately not a
// general HTML parser: hazardous script/style source must already be a data URL.
function scriptElements(html) {
  return [...html.matchAll(/<script\b([^>]*)>([\s\S]*?)<\/script\s*>/giu)].map(([, attributes, body]) => {
    const type = /\btype="([^"]*)"/iu.exec(attributes)?.[1] ?? "";
    const src = /\bsrc="([^"]*)"/iu.exec(attributes)?.[1];
    let code = body;
    if (src !== undefined) {
      assert.ok(src.startsWith(JAVASCRIPT_URL), `Unexpected script URL: ${src.slice(0, 80)}`);
      assert.equal(body, "");
      code = Buffer.from(src.slice(JAVASCRIPT_URL.length), "base64").toString("utf8");
    }
    return { attributes, type, src, code };
  });
}

function executeHtml(html) {
  const elements = scriptElements(html);
  const documents = elements.filter(({ type }) => type === "application/json");
  assert.equal(documents.length, 1);
  const context = createContext({
    document: {
      getElementById(id) {
        assert.equal(id, SNAPSHOT_ID);
        return { textContent: documents[0].code };
      },
    },
  });
  for (const element of elements.filter(({ type }) => type !== "application/json")) {
    assert.equal(element.type, "", "The assembler must emit classic, ordered scripts.");
    assert.doesNotMatch(element.attributes, /\b(?:async|defer)\b/iu);
    new Script(element.code).runInContext(context, { timeout: 1000 });
  }
  return { context, elements, snapshotText: documents[0].code };
}

function styleSource(html) {
  const inline = /<style>([\s\S]*?)<\/style>/iu.exec(html);
  if (inline) return inline[1];
  const href = /<link rel="stylesheet" href="([^"]*)">/u.exec(html)?.[1];
  assert.ok(href?.startsWith(CSS_URL), "Expected one embedded stylesheet.");
  return Buffer.from(href.slice(CSS_URL.length), "base64").toString("utf8");
}

function marker(html, name) {
  const prefix = `<meta name="${name}" content="`;
  const matches = html.split(prefix);
  assert.equal(matches.length, 2, `Expected precisely one ${name} marker.`);
  return matches[1].split('">', 1)[0];
}

test("assembler binds the exact reviewed bytes and runtime to one ordered document", () => {
  const options = input({ localThreadId: SESSION_ID });
  const { html, snapshotSha256 } = assembleDataAppHtml(options);
  const pluginManifest = JSON.parse(readFileSync(join(PLUGIN_ROOT, ".codex-plugin/plugin.json"), "utf8"));
  const icon = readFileSync(join(PLUGIN_ROOT, pluginManifest.interface.composerIcon)).toString("base64");
  assert.match(html, new RegExp(`<link rel="icon" type="image/svg\\+xml" href="data:image/svg\\+xml;base64,${icon}">`));
  assert.equal(snapshotSha256, sha256(options.snapshotBytes));
  assert.equal(marker(html, "data-app-snapshot-sha256"), snapshotSha256);
  assert.equal(marker(html, "data-app-runtime-sha256"), RUNTIME_HASH);
  assert.equal(marker(html, "data-app-local-thread"), SESSION_ID);
  assert.equal(
    styleSource(html),
    [
      options.authored.themeCss,
      options.protectedStyles,
      options.authored.conventionalCss,
      options.printStyles,
      options.authored.importedCss,
    ].join("\n"),
  );
  const { context, elements } = executeHtml(html);
  assert.equal(elements.length, 3);
  assert.deepEqual(Array.from(context.execution), ["runtime", "factory", "mount"]);
  assert.equal(context.factorySnapshot, context.captured.reviewedSnapshot);
  assert.equal(context.captured.DashboardContent(), "Reviewed fixture");
  assert.equal(context.captured.ReportContent(), context.captured.reviewedSnapshot.queries);

  const compact = assembleDataAppHtml(
    input({ snapshotBytes: Buffer.from(JSON.stringify(JSON.parse(options.snapshotBytes))) }),
  );
  assert.notEqual(
    compact.snapshotSha256,
    snapshotSha256,
    "Whitespace changes must invalidate the reviewed-byte marker.",
  );
  assert.doesNotMatch(compact.html, /data-app-local-thread|data-app-local-reference/u);
  assert.doesNotMatch(compact.html, /(?:file:\/\/|\/Users\/|[A-Z]:\\)/u);
});

test("hosted bootstrap defers module data capture until the complete snapshot is supplied", () => {
  const options = input({
    appCode: `globalThis.execution=[]; globalThis.CodexDataAppRuntime={apiVersion:1,
      mount(options){globalThis.captured=options;}};`,
  });
  const { html } = assembleDataAppHtml(options);
  assert.equal(marker(html, "data-app-bootstrap"), "deferred-content-v1");
  const { context } = executeHtml(html);
  assert.equal(context.factorySnapshot, undefined);
  const fullSnapshot = { title: "Complete hosted rows", queries: { q: { rows: [{ value: 42 }] } } };
  const content = context.captured.createContent(fullSnapshot);
  assert.equal(context.factorySnapshot, fullSnapshot);
  assert.equal(content.DashboardContent(), fullSnapshot.title);
  assert.equal(content.ReportContent().q.rows[0].value, 42);
});

test("explicit source entry loads starter CSS before traversing authored component imports", () => {
  const source = readFileSync(join(PLUGIN_ROOT, BASE, "src/main.jsx"), "utf8");
  const imports = [
    'import "./theme.css";',
    'import "./styles.css";',
    'import "./content/dashboard/dashboard.css";',
    'import "./content/report/report.css";',
    'import "./print.css";',
    'import "./theme-runtime.js";',
    'import { App } from "./App.jsx";',
  ];
  let previous = -1;
  for (const declaration of imports) {
    const position = source.indexOf(declaration);
    assert.ok(position > previous, `${declaration} must follow the preceding stylesheet/runtime imports.`);
    previous = position;
  }
});

test("assembler keeps __proto__, HTML closing tags, and Unicode separators as inert JSON", () => {
  const snapshot = JSON.parse('{"queries":{},"__proto__":{"polluted":true}}');
  const row = JSON.parse('{"__proto__":{"rowPolluted":true}}');
  row.text = "</script><script>globalThis.injected = true</script><!--\u2028\u2029";
  snapshot.queries.reviewed = { rows: [row] };
  snapshot.title = "</title><script>globalThis.injected = true</script>&\"'";
  const bytes = Buffer.from(`${JSON.stringify(snapshot, null, 2)}\n`);
  const { html } = assembleDataAppHtml(input({ snapshotBytes: bytes }));
  const { context, snapshotText } = executeHtml(html);
  assert.doesNotMatch(snapshotText, /[<\u2028\u2029]/u);
  assert.match(snapshotText, /\\u003c\/script>/u);
  assert.match(snapshotText, /\\u2028\\u2029/u);
  assert.deepEqual(JSON.parse(snapshotText), snapshot);
  assert.equal(context.injected, undefined);
  assert.equal(Object.hasOwn(context.captured.reviewedSnapshot, "__proto__"), true);
  assert.equal(context.captured.reviewedSnapshot.__proto__.polluted, true);
  assert.equal(Object.hasOwn(context.captured.reviewedSnapshot.queries.reviewed.rows[0], "__proto__"), true);
  assert.equal(
    new Script("Object.getPrototypeOf(captured.reviewedSnapshot) === Object.prototype").runInContext(context),
    true,
  );
  assert.equal(new Script("Object.prototype.polluted").runInContext(context), undefined);
  assert.equal(marker(html, "data-app-snapshot-sha256"), sha256(bytes));
});

test("assembler HTML-escapes titles and supplies the empty-title fallback", () => {
  const title = "&<>\"' reviewed";
  const { html } = assembleDataAppHtml(input({ snapshotBytes: Buffer.from(JSON.stringify({ title, queries: {} })) }));
  assert.match(html, /<title>&amp;&lt;&gt;&quot;&#39; reviewed<\/title>/u);
  const fallback = assembleDataAppHtml(input({ snapshotBytes: Buffer.from('{"title":"","queries":{}}') }));
  assert.match(fallback.html, /<title>Data app<\/title>/u);
});

test("assembler accepts a UTF-8 BOM while hashing the original snapshot bytes", () => {
  const json = Buffer.from('{"title":"BOM fixture","queries":{}}\n');
  const snapshotBytes = Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), json]);
  const { html, snapshotSha256 } = assembleDataAppHtml(input({ snapshotBytes }));
  assert.equal(snapshotSha256, sha256(snapshotBytes));
  assert.notEqual(snapshotSha256, sha256(json));
  assert.equal(marker(html, "data-app-snapshot-sha256"), snapshotSha256);
  const { context, snapshotText } = executeHtml(html);
  assert.equal(snapshotText.startsWith("\ufeff"), false);
  assert.equal(context.captured.reviewedSnapshot.title, "BOM fixture");
});

test("assembler rejects invalid snapshot bytes, shapes, integrity markers, and task IDs", () => {
  for (const snapshotBytes of [
    Buffer.from([0x7b, 0xc3, 0x28, 0x7d]),
    Buffer.from("not JSON"),
    ...[null, [], {}, { queries: null }, { queries: [] }].map((value) => Buffer.from(JSON.stringify(value))),
  ]) {
    assert.throws(() => assembleDataAppHtml(input({ snapshotBytes })), /valid UTF-8 JSON|reviewed query snapshot/u);
  }
  for (const runtimeSha256 of [undefined, "", "a".repeat(63), "G".repeat(64)]) {
    assert.throws(() => assembleDataAppHtml(input({ runtimeSha256 })), /verified runtime hash/u);
  }
  assert.throws(
    () => assembleDataAppHtml(input({ localThreadId: `${SESSION_ID}\"` })),
    /Invalid local Data app task identifier/u,
  );
  assert.throws(() => embeddedScript("export const unsupported = true;"), SyntaxError);
  assert.throws(
    () => assembleDataAppHtml(input({ authored: { ...input().authored, factorySource: "(function(){" } })),
    SyntaxError,
  );
});

test("assembler preserves script bytes and semantics through HTML-tokenizer hazards", () => {
  const cases = [
    ["ordinary Unicode", 'globalThis.value = "π🙂\u2028\u2029";', false],
    ["closing tag", 'globalThis.value = "</ScRiPt><script>not executed</script>";', true],
    ["comment opener", 'globalThis.value = "<!--";', true],
    ["tagged raw template", "globalThis.value = String.raw`</script>\\n\\u{1f642}`;", true],
    ["regular expression", "globalThis.value = /<script[ >]/giu.source;", true],
    ["NUL", 'globalThis.value = "before\0after";', true],
    ["CRLF", "globalThis.value = `before\r\nafter`;", true],
    ["Unicode plus hazard", 'globalThis.value = "☕️🙂\u2028\u2029</script>";', true],
  ];
  for (const [name, source, encoded] of cases) {
    const html = embeddedScript(source);
    const elements = scriptElements(html);
    assert.equal(elements.length, 1, name);
    assert.equal(Boolean(elements[0].src), encoded, name);
    assert.deepEqual(Buffer.from(elements[0].code), Buffer.from(source), `${name}: exact UTF-8 bytes`);
    assert.doesNotMatch(elements[0].attributes, /\b(?:async|defer|type)\b/iu, name);
    const expected = createContext({});
    const actual = createContext({});
    new Script(source).runInContext(expected, { timeout: 1000 });
    new Script(elements[0].code).runInContext(actual, { timeout: 1000 });
    assert.equal(actual.value, expected.value, `${name}: JavaScript semantics`);
  }
});

test("assembler keeps data-URL runtime and authored bootstrap scripts in classic order", () => {
  const options = input();
  options.appCode = `globalThis.runtimeHazard = "</script>";\n${options.appCode}`;
  options.authored.factorySource = `(function(runtime, snapshot) {
    execution.push("factory");
    globalThis.rawValue = String.raw\`<script>\\n\`;
    return {DashboardContent() {return snapshot.title;}, ReportContent() {return null;}};
  })`;
  const { html } = assembleDataAppHtml(options);
  const { context, elements } = executeHtml(html);
  const executable = elements.filter(({ type }) => type !== "application/json");
  assert.equal(executable.length, 2);
  assert.ok(executable.every(({ src }) => src?.startsWith(JAVASCRIPT_URL)));
  assert.equal(executable[0].code, options.appCode);
  assert.equal(context.runtimeHazard, "</script>");
  assert.equal(context.rawValue, "<script>\\n");
  assert.deepEqual(Array.from(context.execution), ["runtime", "factory", "mount"]);
});

test("assembler embeds CSS end tags, NUL, and CR without rewriting stylesheet bytes", () => {
  const safe = ".safe{color:red}";
  assert.equal(embeddedStyle(safe), `<style>${safe}</style>`);
  for (const css of [
    '.fixture::after{content:"</StYlE><script>not executed</script>"}',
    ".fixture{--value:before\0after}",
    ".fixture{\r\ncolor:red}",
  ]) {
    const html = embeddedStyle(css);
    assert.match(html, /^<link rel="stylesheet" href="data:text\/css;charset=utf-8;base64,[A-Za-z\d+/=]+">$/u);
    assert.doesNotMatch(html, /<style|<script/iu);
    assert.deepEqual(Buffer.from(styleSource(html)), Buffer.from(css));
  }
});

test("assembler selects a valid session ID before a valid thread ID", () => {
  assert.equal(localDataThreadId({ CODEX_SESSION_ID: SESSION_ID, CODEX_THREAD_ID: THREAD_ID }), SESSION_ID);
  assert.equal(localDataThreadId({ CODEX_SESSION_ID: "not-a-uuid", CODEX_THREAD_ID: THREAD_ID }), THREAD_ID);
  assert.equal(localDataThreadId({ CODEX_SESSION_ID: ` ${SESSION_ID}`, CODEX_THREAD_ID: THREAD_ID }), THREAD_ID);
  assert.equal(localDataThreadId({ CODEX_SESSION_ID: SESSION_ID.toUpperCase() }), SESSION_ID.toUpperCase());
  assert.equal(localDataThreadId({ CODEX_SESSION_ID: "", CODEX_THREAD_ID: "invalid" }), "");
  assert.equal(localDataThreadId({}), "");
});

test("local origin recovery ignores ambiguous metadata and marker-like document content", async (t) => {
  const project = realpathSync(mkdtempSync(join(tmpdir(), "data-app-local-origin-")));
  t.after(() => rmSync(project, { recursive: true, force: true }));
  const original = `<meta name="data-app-local-thread" content="${SESSION_ID}">`;
  for (const [head, body, expected] of [
    [original, "", SESSION_ID],
    [`<meta name="data-app-local-thread" content="invalid">${original}`, "", THREAD_ID],
    [`<!-- ${original} --><script>${JSON.stringify(original)}</script>`, original, THREAD_ID],
    [`<!-- ${original} -->${original}`, "", SESSION_ID],
  ]) {
    write(join(project, "dist/index.html"), `<html><head>${head}</head><body>${body}</body></html>`);
    assert.equal(await projectLocalDataThreadId(project, { CODEX_SESSION_ID: THREAD_ID }), expected);
  }
});

test("local origin recovery skips large raw-text payloads and still checks the whole head for duplicates", async (t) => {
  const project = realpathSync(mkdtempSync(join(tmpdir(), "data-app-large-local-origin-")));
  t.after(() => rmSync(project, { recursive: true, force: true }));
  const original = `<meta name="data-app-local-thread" content="${SESSION_ID}">`;
  const decoy = `<meta name="data-app-local-thread" content="${THREAD_ID}">`;
  const large = "x".repeat(15 * 1024 * 1024);
  for (const [name, html, expected] of [
    ["large body script", `<head>${original}</head><body><script>${large}${decoy}</script></body>`, SESSION_ID],
    ["large mixed-case head script", `<head><ScRiPt data-example=">">${decoy}${large}</scripture>${decoy}</sCrIpT>${original}</head>`, SESSION_ID],
    ["late duplicate after large head style", `<head>${original}<style>${large}${decoy}</style>${original}</head>`, THREAD_ID],
    ["large head style preserves the original", `<head>${original}<style>${large}${decoy}</style></head>`, SESSION_ID],
  ]) {
    write(join(project, "dist/index.html"), html);
    assert.equal(await projectLocalDataThreadId(project, { CODEX_SESSION_ID: THREAD_ID }), expected, name);
  }
});

test("local origin recovery respects quoted tags and fails closed on incomplete raw text or head markup", async (t) => {
  const project = realpathSync(mkdtempSync(join(tmpdir(), "data-app-local-origin-boundaries-")));
  t.after(() => rmSync(project, { recursive: true, force: true }));
  const original = `<meta name="data-app-local-thread" content="${SESSION_ID}">`;
  const decoy = `<meta name="data-app-local-thread" content="${THREAD_ID}">`;
  for (const name of ["script", "style", "title", "textarea"]) {
    write(join(project, "dist/index.html"), `<head><${name} data-decoy='>${original}'>${decoy}</${name}>${original}</head>`);
    assert.equal(await projectLocalDataThreadId(project, { CODEX_SESSION_ID: THREAD_ID }), SESSION_ID, name);
    write(join(project, "dist/index.html"), `<head>${original}<${name}>${decoy}</head>`);
    assert.equal(await projectLocalDataThreadId(project, { CODEX_SESSION_ID: THREAD_ID }), THREAD_ID, `unclosed ${name}`);
  }
  for (const [html, expected] of [
    [`<!-- <head>${decoy}</head> --><head><link data-decoy='${decoy}'>${original}</head>`, SESSION_ID],
    [`<head><link data-decoy='${original}'></head>`, THREAD_ID],
    [`<head>${original}<!-- unclosed </head>`, THREAD_ID],
    [`<head>${original}<link data-decoy='unclosed </head>`, THREAD_ID],
    [`<head>${original}`, THREAD_ID],
    [`<head>${original}</head>${decoy}<script>unclosed`, SESSION_ID],
  ]) {
    write(join(project, "dist/index.html"), html);
    assert.equal(await projectLocalDataThreadId(project, { CODEX_SESSION_ID: THREAD_ID }), expected);
  }
});

let seedRoot;

function write(path, content, options) {
  mkdirSync(dirname(path), { recursive: true });
  writeFileSync(path, content, options);
}

function copySource(from, to, { omitTests = false } = {}) {
  cpSync(from, to, {
    recursive: true,
    filter(path) {
      const parts = relative(from, path).split(sep);
      return (
        !parts.some((part) => part === "node_modules" || part === "dist" || part.startsWith(".data-app-build-")) &&
        !(omitTests && parts[0] === "tests")
      );
    },
  });
}

function sourceSeed() {
  if (!seedRoot) {
    seedRoot = realpathSync(mkdtempSync(join(tmpdir(), "data-app-build-source-")));
    for (const directory of [
      "scripts", "templates/data-app/inline", "skills/visualize-data/scripts", dirname(PACKAGER), ASSETS,
    ]) {
      copySource(join(PLUGIN_ROOT, directory), join(seedRoot, directory));
    }
    copySource(join(PLUGIN_ROOT, BASE), join(seedRoot, BASE), { omitTests: true });
    for (const file of RELEASE_INPUTS) write(join(seedRoot, file), readFileSync(join(PLUGIN_ROOT, file)));
    // Keep the actual released manifest. Re-signing fixture bytes here would
    // hide a stale or incomplete shipped runtime from the integration test.
    assert.ok(existsSync(join(seedRoot, ASSETS, "manifest.json")));
  }
  return seedRoot;
}

after(() => {
  if (seedRoot) rmSync(seedRoot, { recursive: true, force: true });
});

function fixture(t) {
  const plugin = sourceSeed();
  const root = realpathSync(mkdtempSync(join(tmpdir(), "data-app-build-test-")));
  t.after(() => rmSync(root, { recursive: true, force: true }));
  const state = {
    root,
    plugin,
    project: join(root, "project"),
    bin: join(root, "blocked-package-managers"),
    cache: join(root, "unused-npm-cache"),
    packageManagerLog: join(root, "package-manager.log"),
    sourceLog: join(root, "source-vite.log"),
    manifest: JSON.parse(readFileSync(join(plugin, ASSETS, "manifest.json"), "utf8")),
  };
  copySource(join(plugin, BASE), state.project);
  for (const executable of ["npm", "npx", "pnpm", "pnpx", "yarn", "corepack", "bun"]) {
    write(
      join(state.bin, executable),
      '#!/bin/sh\nprintf \'%s\\n\' "$0" >> "$DATA_APP_TEST_PACKAGE_MANAGER_LOG"\nexit 97\n',
      { mode: 0o755 },
    );
    write(
      join(state.bin, `${executable}.cmd`),
      '@echo off\r\necho %~nx0>>"%DATA_APP_TEST_PACKAGE_MANAGER_LOG%"\r\nexit /b 97\r\n',
    );
  }
  return state;
}

function runCli(state, command, args = [], environment = {}, nodeArgs = []) {
  const helper = command === "package" ? PACKAGER : "scripts/data-app.mjs";
  const options = command === "package" ? ["--project-id", "appgprj_offline_fixture"] : [command];
  const result = spawnSync(
    process.execPath,
    [...nodeArgs, join(state.plugin, helper), ...options, "--project-dir", state.project, ...args],
    {
      cwd: state.project,
      encoding: "utf8",
      timeout: 45_000,
      maxBuffer: 8 * 1024 * 1024,
      windowsHide: true,
      env: {
        ...dataNodeEnvironment(process.env),
        PATH: state.bin,
        NODE_PATH: "",
        CODEX_SESSION_ID: "",
        CODEX_THREAD_ID: "",
        DATA_APP_TEST_PACKAGE_MANAGER_LOG: state.packageManagerLog,
        DATA_APP_TEST_SOURCE_LOG: state.sourceLog,
        npm_config_cache: state.cache,
        npm_config_offline: "true",
        npm_config_registry: "http://127.0.0.1:9/unavailable",
        ...environment,
      },
    },
  );
  return result;
}

function successful(result) {
  assert.equal(result.error, undefined);
  assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
  return JSON.parse(result.stdout.trim());
}

function failed(result, pattern) {
  assert.equal(result.error, undefined);
  assert.notEqual(result.status, 0, result.stdout);
  assert.match(result.stderr, pattern);
}

function inventory(root) {
  const result = {};
  function visit(path) {
    const name = relative(root, path).split(sep).join("/") || ".";
    const info = lstatSync(path);
    if (info.isSymbolicLink()) result[name] = { type: "link", target: readlinkSync(path) };
    else if (info.isDirectory()) {
      result[name] = { type: "directory", mtime: info.mtimeMs };
      for (const child of readdirSync(path).sort()) visit(join(path, child));
    } else {
      assert.ok(info.isFile(), `Unexpected fixture entry: ${path}`);
      result[name] = { type: "file", sha256: sha256(readFileSync(path)), mode: info.mode, mtime: info.mtimeMs };
    }
  }
  visit(root);
  return result;
}

function assertNoPackageManager(state) {
  assert.equal(existsSync(state.packageManagerLog), false, "The customer build invoked a package manager.");
  assert.equal(existsSync(state.cache), false, "The customer build populated a dependency cache.");
}

function assertNoDependencies(state) {
  assertNoPackageManager(state);
  assert.equal(
    existsSync(join(state.project, "node_modules")),
    false,
    "The customer build created project node_modules.",
  );
  assert.deepEqual(
    readdirSync(state.project).filter((name) => name.startsWith(".data-app-build-")),
    [],
  );
}

function assertNoMachinePaths(html, state) {
  for (const path of [state.root, state.project, state.plugin]) {
    for (const variant of new Set([
      path,
      path.split(sep).join("/"),
      pathToFileURL(path).href,
      encodeURIComponent(path),
      JSON.stringify(path).slice(1, -1),
    ])) {
      assert.equal(html.includes(variant), false, `The portable HTML leaked ${variant}.`);
    }
  }
  assert.doesNotMatch(html, /<meta\s+name="data-app-local-reference"/iu);
}

function createLink(t, target, path, directory = false) {
  try {
    symlinkSync(target, path, directory ? (process.platform === "win32" ? "junction" : "dir") : "file");
    return true;
  } catch (error) {
    if (process.platform !== "win32" || !["EPERM", "EACCES", "ENOTSUP"].includes(error.code)) throw error;
    t.skip("This Windows environment does not permit creation of the test symlink.");
    return false;
  }
}

test("default build prepare is read-only and cold/warm builds need no npm or node_modules", (t) => {
  const state = fixture(t);
  const projectBefore = inventory(state.project);
  const pluginBefore = inventory(state.plugin);
  const prepared = successful(runCli(state, "prepare", ["--offline"]));
  assert.deepEqual(prepared, {
    projectRoot: state.project,
    prebuilt: true,
    apiVersion: 1,
    runtimeSha256: state.manifest.artifacts.app.sha256,
    compilerSha256: state.manifest.artifacts.compiler.sha256,
    documentation: {
      entryPoint: join(state.plugin, BASE, "docs/components/README.md"),
      source: "installed-plugin",
      apiVersion: state.manifest.apiVersion,
      runtimeSha256: state.manifest.artifacts.app.sha256,
    },
  });
  assert.deepEqual(inventory(state.project), projectBefore);
  assert.deepEqual(inventory(state.plugin), pluginBefore);
  assert.equal(existsSync(join(state.project, "dist")), false);
  assertNoDependencies(state);

  const cold = successful(runCli(state, "build"));
  const html = readFileSync(cold.htmlPath, "utf8");
  const snapshotHash = sha256(readFileSync(join(state.project, "src/data.json")));
  assert.equal(cold.prebuilt, true);
  assert.equal(cold.htmlPath, join(state.project, "dist/index.html"));
  assert.equal(cold.htmlSha256, sha256(html));
  assert.equal(cold.snapshotSha256, snapshotHash);
  assert.equal(cold.runtimeSha256, state.manifest.artifacts.app.sha256);
  assert.equal(cold.compilerSha256, state.manifest.artifacts.compiler.sha256);
  assert.deepEqual(cold.documentation, prepared.documentation);
  assert.ok(cold.moduleCount >= 2);
  assert.ok(Number.isSafeInteger(cold.assetCount) && cold.assetCount >= 0);
  assert.equal(marker(html, "data-app-snapshot-sha256"), snapshotHash);
  assert.equal(marker(html, "data-app-runtime-sha256"), cold.runtimeSha256);
  assert.equal(sha256(scriptElements(html).find(({ type }) => type !== "application/json").code), cold.runtimeSha256);
  assertNoMachinePaths(html, state);
  assertNoDependencies(state);

  const warm = successful(runCli(state, "build", ["--offline"]));
  assert.deepEqual(warm, cold);
  assert.deepEqual(readdirSync(join(state.project, "dist")), ["index.html"]);
  assert.deepEqual(inventory(state.plugin), pluginBefore);
  assertNoDependencies(state);
});

test("stale copied runtime is not a publishing dependency and remains untouched", (t) => {
  const state = fixture(t);
  successful(runCli(state, "build"));
  write(join(state.project, ".data-plugin-version"), "old-runtime-cache-no-longer-installed\n");
  write(join(state.project, "AGENTS.md"), "Previous release authoring guide.\n");
  rmSync(join(state.project, "docs"), { recursive: true });
  write(join(state.project, "src/content/COMPONENTS.md"), "Previous release component API.\n");
  write(
    join(state.project, "scripts/verify-protected-runtime.mjs"),
    'throw new Error("Do not execute copied runtime scripts");\n',
  );
  rmSync(join(state.project, "src/App.jsx"));
  rmSync(join(state.project, "src/worker.js"));
  const hosting = { d1: "DB", r2: null, project_id: "appgprj_offline_fixture" };
  write(join(state.project, ".openai/hosting.json"), `${JSON.stringify(hosting, null, 2)}\n`);
  const sourceHashes = () => Object.fromEntries(
    Object.entries(inventory(state.project))
      .filter(([name, entry]) => entry.type === "file" && !name.startsWith("dist/")
        && !name.startsWith(".data-app-assets/") && !name.startsWith(".data-app-offline/") && name !== ".openai/hosting.json")
      .map(([name, entry]) => [name, entry.sha256]),
  );
  const projectBefore = inventory(state.project);
  const sourcesBefore = sourceHashes();
  const pluginBefore = inventory(state.plugin);
  const prepared = successful(runCli(state, "prepare", ["--offline"]));
  assert.deepEqual(prepared.documentation, {
    entryPoint: join(state.plugin, BASE, "docs/components/README.md"),
    source: "installed-plugin",
    apiVersion: state.manifest.apiVersion,
    runtimeSha256: state.manifest.artifacts.app.sha256,
  }, "Old apps must discover the docs for the installed runtime they will build against");
  assert.ok(existsSync(prepared.documentation.entryPoint));
  assert.deepEqual(inventory(state.project), projectBefore);
  const packaged = successful(runCli(state, "package"));
  const html = readFileSync(packaged.htmlPath, "utf8");
  assert.equal(packaged.buildMode, "existing-page");
  assert.equal(packaged.snapshotSource, "compiled-html");
  assert.equal(marker(html, "data-app-runtime-sha256"), state.manifest.artifacts.app.sha256);
  assert.equal(marker(html, "data-app-snapshot-sha256"), sha256(readFileSync(join(state.project, "src/data.json"))));
  assert.deepEqual(sourceHashes(), sourcesBefore);
  assert.equal(JSON.parse(readFileSync(join(state.project, ".openai/hosting.json"))).r2, "BUCKET");
  successful(runCli(state, "package"));
  assert.equal(readFileSync(packaged.htmlPath, "utf8"), html, "Compatible reviewed HTML should be reused.");
  assert.deepEqual(sourceHashes(), sourcesBefore);
  assert.deepEqual(inventory(state.plugin), pluginBefore);
  assertNoMachinePaths(html, state);
  assertNoDependencies(state);
});

test("publication and rebuilds retain the originating local task across other tasks and ordinary shells", (t) => {
  const state = fixture(t);
  const first = successful(runCli(state, "build", [], { CODEX_SESSION_ID: SESSION_ID }));
  const localHtml = readFileSync(first.htmlPath, "utf8");
  assert.equal(marker(localHtml, "data-app-local-thread"), SESSION_ID);
  const packaged = successful(runCli(state, "package", [], { CODEX_SESSION_ID: THREAD_ID }));
  assert.equal(packaged.htmlPath, first.htmlPath);
  assert.equal(packaged.thinBootstrap, true);
  assert.equal(readFileSync(packaged.offlineHtmlPath, "utf8"), localHtml);
  assert.ok(!readFileSync(first.htmlPath, "utf8").includes("data-app-local-thread"));
  for (const environment of [{ CODEX_SESSION_ID: THREAD_ID }, {}]) {
    const rebuilt = successful(runCli(state, "build", [], environment));
    assert.equal(marker(readFileSync(rebuilt.htmlPath, "utf8"), "data-app-local-thread"), SESSION_ID);
  }
  assertNoDependencies(state);
});

test("thin publication origin recovery rejects changed offline or hosted bytes", async (t) => {
  const state = fixture(t);
  successful(runCli(state, "build", [], { CODEX_SESSION_ID: SESSION_ID }));
  const packaged = successful(runCli(state, "package", [], { CODEX_SESSION_ID: THREAD_ID }));
  const original = readFileSync(packaged.offlineHtmlPath);
  const hosted = readFileSync(packaged.htmlPath);
  const current = { CODEX_SESSION_ID: THREAD_ID };
  assert.equal(await projectLocalDataThreadId(state.project, current), SESSION_ID);
  writeFileSync(packaged.offlineHtmlPath, original.toString().replaceAll(SESSION_ID, THREAD_ID));
  assert.equal(await projectLocalDataThreadId(state.project, current), THREAD_ID, "A changed offline backup must not supply stale task metadata");
  writeFileSync(packaged.offlineHtmlPath, original);
  writeFileSync(packaged.htmlPath, Buffer.concat([hosted, Buffer.from("<!-- rebuilt or changed page -->")]));
  assert.equal(await projectLocalDataThreadId(state.project, current), THREAD_ID, "A different current page must not recover an older offline task");
  writeFileSync(packaged.htmlPath, hosted);
  assert.equal(await projectLocalDataThreadId(state.project, current), SESSION_ID);
});

test("publishing an older compiled client preserves its bytes without rebuilding edited source", (t) => {
  const state = fixture(t);
  const built = successful(runCli(state, "build"));
  const oldHtml = readFileSync(built.htmlPath, "utf8").replace(
    `name="data-app-runtime-sha256" content="${built.runtimeSha256}"`,
    `name="data-app-runtime-sha256" content="${RUNTIME_HASH}"`,
  ).replace('<meta name="data-app-bootstrap" content="deferred-content-v1">', "");
  writeFileSync(built.htmlPath, oldHtml);
  const content = join(state.project, "src/content/dashboard/DashboardContent.jsx");
  const editedSource = "export function DashboardContent( {\n";
  writeFileSync(content, editedSource);
  const packaged = successful(runCli(state, "package"));
  assert.equal(packaged.buildMode, "existing-page");
  assert.equal(readFileSync(packaged.htmlPath, "utf8"), oldHtml);
  assert.equal(readFileSync(content, "utf8"), editedSource);
  assertNoDependencies(state);
});

test("publication requires an existing compiled page and never builds a missing one", (t) => {
  const state = fixture(t);
  const before = inventory(state.project);
  failed(runCli(state, "package"), /ENOENT|existing compiled/u);
  assert.deepEqual(inventory(state.project), before);
  assertNoDependencies(state);
});

test("default build replaces the complete dist tree and removes stale output", (t) => {
  const state = fixture(t);
  const initial = successful(runCli(state, "build"));
  write(join(state.project, "dist/stale.js"), "old output");
  write(join(state.project, "dist/assets/stale.css"), "old CSS");
  write(join(state.project, "dist/server/index.js"), "old Worker");
  const snapshotPath = join(state.project, "src/data.json");
  const snapshot = JSON.parse(readFileSync(snapshotPath, "utf8"));
  snapshot.title = "Rebuilt offline fixture";
  writeFileSync(snapshotPath, `${JSON.stringify(snapshot, null, 2)}\n`);
  const rebuilt = successful(runCli(state, "build"));
  assert.notEqual(rebuilt.htmlSha256, initial.htmlSha256);
  assert.equal(rebuilt.snapshotSha256, sha256(readFileSync(snapshotPath)));
  assert.deepEqual(readdirSync(join(state.project, "dist")), ["index.html"]);
  assert.match(readFileSync(rebuilt.htmlPath, "utf8"), /<title>Rebuilt offline fixture<\/title>/u);
  assertNoDependencies(state);
});

test("default build places conventional CSS before print and component-import CSS after print", (t) => {
  const state = fixture(t);
  const component = join(state.project, "src/content/dashboard/DashboardContent.jsx");
  write(component, 'import "./css-order-probe.css";\n' + readFileSync(component, "utf8"));
  write(
    join(state.project, "src/content/dashboard/dashboard.css"),
    ".page { padding: 24px; --data-css-order: dashboard; }\n@page { margin: 18mm; }\n",
  );
  write(join(state.project, "src/content/report/report.css"), ".page { --data-css-order: report; }\n");
  write(
    join(state.project, "src/content/dashboard/css-order-probe.css"),
    ".page { padding-top: 48px; --data-css-order: imported; }\n@page { margin: 20mm; }\n",
  );

  const result = successful(runCli(state, "build"));
  const css = styleSource(readFileSync(result.htmlPath, "utf8"));
  const print = readFileSync(join(state.plugin, ASSETS, state.manifest.artifacts.print.path), "utf8");
  const position = (pattern) => {
    const match = pattern.exec(css);
    assert.ok(match, `Missing CSS probe: ${pattern}`);
    return match.index;
  };
  const dashboardPosition = position(/--data-css-order:\s*dashboard/u);
  const reportPosition = position(/--data-css-order:\s*report/u);
  const conventionalMarginPosition = position(/@page\s*\{\s*margin:\s*18mm\b/u);
  const importedPosition = position(/--data-css-order:\s*imported/u);
  const importedMarginPosition = position(/@page\s*\{\s*margin:\s*20mm\b/u);
  const printPosition = css.indexOf(print);
  assert.ok(print.length > 0 && printPosition >= 0, "The complete protected print stylesheet must be preserved.");
  assert.ok(dashboardPosition < reportPosition, "Dashboard CSS must precede report CSS.");
  assert.ok(reportPosition < printPosition, "Both conventional stylesheets must precede print CSS.");
  assert.ok(conventionalMarginPosition < printPosition, "Print CSS must still override conventional page margins.");
  assert.ok(importedPosition >= printPosition + print.length, "Component imports must follow the complete print CSS.");
  assert.ok(importedMarginPosition > importedPosition, "The authored page-margin override must remain last.");
  assertNoDependencies(state);
});

test("default build preserves the previous dist when authored compilation fails", (t) => {
  const state = fixture(t);
  successful(runCli(state, "build"));
  write(join(state.project, "dist/previous.txt"), "preserve this too");
  const previous = inventory(join(state.project, "dist"));
  writeFileSync(
    join(state.project, "src/content/dashboard/DashboardContent.jsx"),
    "export function DashboardContent( {\n",
  );
  failed(runCli(state, "build"), /parse authored JavaScript|bundle authored modules/u);
  assert.deepEqual(inventory(join(state.project, "dist")), previous);
  assertNoDependencies(state);
});

test("default build preserves detached previous output when publish and destination probing fail", (t) => {
  const state = fixture(t);
  successful(runCli(state, "build"));
  write(join(state.project, "dist/previous.txt"), "the only previous copy");
  const previous = inventory(join(state.project, "dist"));
  const loader = join(state.root, "publish-fault.mjs");
  const faultLog = join(state.root, "publish-fault.log");
  write(
    loader,
    `import fs from "node:fs/promises";
import { appendFileSync } from "node:fs";
import { syncBuiltinESMExports } from "node:module";
import { basename, dirname, join } from "node:path";
const destination = join(process.env.DATA_APP_TEST_PROJECT, "dist");
const originalRename = fs.rename;
const originalLstat = fs.lstat;
let scratch = "";
let failedPublish = false;
function record(value) { appendFileSync(process.env.DATA_APP_TEST_PUBLISH_FAULT_LOG, value + "\\n"); }
function fault(message, code) { return Object.assign(new Error(message), {code}); }
fs.rename = async function(from, to, ...rest) {
  if (from === destination && basename(to) === "previous" && basename(dirname(to)).startsWith(".data-app-build-")) {
    const result = await originalRename(from, to, ...rest);
    scratch = dirname(to);
    record("detached previous");
    return result;
  }
  if (scratch && from === join(scratch, "next") && to === destination) {
    failedPublish = true;
    record("candidate rename failed");
    throw fault("injected candidate publish failure", "EIO");
  }
  return originalRename(from, to, ...rest);
};
fs.lstat = async function(path, ...rest) {
  if (failedPublish && path === destination) {
    record("destination probe failed");
    throw fault("injected destination probe failure", "EACCES");
  }
  return originalLstat(path, ...rest);
};
syncBuiltinESMExports();
`,
  );
  const result = runCli(
    state,
    "build",
    [],
    {
      DATA_APP_TEST_PROJECT: state.project,
      DATA_APP_TEST_PUBLISH_FAULT_LOG: faultLog,
    },
    ["--import", loader],
  );
  failed(result, /previous output is preserved at/u);
  assert.equal(
    readFileSync(faultLog, "utf8"),
    "detached previous\ncandidate rename failed\ndestination probe failed\n",
  );
  assert.equal(existsSync(join(state.project, "dist")), false);
  const backups = readdirSync(state.project).filter((name) => name.startsWith(".data-app-build-"));
  assert.equal(backups.length, 1, "The old output must remain available for recovery.");
  const scratch = join(state.project, backups[0]);
  assert.ok(result.stderr.includes(join(scratch, "previous")));
  assert.deepEqual(inventory(join(scratch, "previous")), previous);
  assert.equal(existsSync(join(scratch, "next/index.html")), true);
  assert.equal(existsSync(join(state.project, "node_modules")), false);
  assertNoPackageManager(state);
});

test("default build rejects a redirected dist directory without touching its target", (t) => {
  const state = fixture(t);
  const outside = join(state.root, "outside-output");
  write(join(outside, "keep.txt"), "unrelated output");
  const previous = inventory(outside);
  if (!createLink(t, outside, join(state.project, "dist"), true)) return;
  failed(runCli(state, "build"), /dist must be a regular directory inside the project/u);
  assert.equal(lstatSync(join(state.project, "dist")).isSymbolicLink(), true);
  assert.deepEqual(inventory(outside), previous);
  assertNoDependencies(state);
});

test("default build rejects authored symlinks before producing any output", (t) => {
  const state = fixture(t);
  const content = join(state.project, "src/content/dashboard/DashboardContent.jsx");
  const outside = join(state.root, "outside-content.jsx");
  write(outside, readFileSync(content));
  rmSync(content);
  if (!createLink(t, outside, content)) return;
  failed(runCli(state, "build"), /source must not contain symlinks|nonsymlinked file/u);
  assert.equal(existsSync(join(state.project, "dist")), false);
  assert.equal(lstatSync(content).isSymbolicLink(), true);
  assertNoDependencies(state);
});

function installSourceViteFixture(state, directory = join(state.project, "node_modules/vite")) {
  write(join(directory, "package.json"), '{"name":"vite","type":"module"}\n');
  write(
    join(directory, "bin/vite.js"),
    `import { appendFileSync, mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
if (process.argv[2] !== "build") throw new Error("Unexpected source Vite command");
appendFileSync(process.env.DATA_APP_TEST_SOURCE_LOG, JSON.stringify(process.argv.slice(2)) + "\\n");
mkdirSync(join(process.cwd(), "dist"), {recursive:true});
writeFileSync(join(process.cwd(), "dist/index.html"), "<!doctype html><head><title>Explicit local Vite fixture</title>" +
  (process.env.CODEX_SESSION_ID ? '<meta name="data-app-local-thread" content="' + process.env.CODEX_SESSION_ID + '">' : '') + '</head>\\n');
`,
  );
  return join(directory, "bin/vite.js");
}

test("default build never activates local Vite automatically and --source still verifies integrity", (t) => {
  const state = fixture(t);
  failed(runCli(state, "prepare", ["--source"]), /requires an already-installed local Vite toolchain/u);
  assertNoDependencies(state);
  const vitePath = installSourceViteFixture(state);
  const prepared = successful(runCli(state, "prepare", ["--source"]));
  assert.deepEqual(prepared, {
    projectRoot: state.project, prebuilt: false, source: true, vitePath,
    documentation: {
      entryPoint: join(state.project, "docs/components/README.md"),
      source: "project-source",
      protectedRuntimeSha256: sha256(readFileSync(join(state.project, "protected-runtime.json"))),
    },
  });
  assert.equal(existsSync(state.sourceLog), false, "Source prepare must not run Vite build.");
  writeFileSync(
    join(state.project, "src/content/dashboard/DashboardContent.jsx"),
    'import optional from "unbundled-fixture-package";\nexport function DashboardContent(){return optional;}\n',
  );
  failed(runCli(state, "build"), /package or URL import.*unbundled-fixture-package/u);
  assert.equal(existsSync(state.sourceLog), false, "A default failure must not fall back to Vite.");
  assert.equal(existsSync(join(state.project, "dist")), false);

  const built = successful(runCli(state, "build", ["--source", "--offline"]));
  assert.equal(built.prebuilt, false);
  assert.equal(built.source, true);
  assert.deepEqual(built.documentation, prepared.documentation);
  assert.equal(built.htmlSha256, sha256(readFileSync(built.htmlPath)));
  assert.equal(built.snapshotSha256, sha256(readFileSync(join(state.project, "src/data.json"))));
  assert.equal(readFileSync(state.sourceLog, "utf8"), '["build"]\n');
  const previous = inventory(join(state.project, "dist"));
  const protectedPath = join(state.project, "src/styles.css");
  writeFileSync(protectedPath, `${readFileSync(protectedPath, "utf8")}\n/* unapproved protected change */\n`);
  failed(runCli(state, "build", ["--source"]), /Protected Data app runtime file was modified: src\/styles\.css/u);
  assert.equal(readFileSync(state.sourceLog, "utf8"), '["build"]\n');
  assert.deepEqual(inventory(join(state.project, "dist")), previous);
  assertNoPackageManager(state);
});

test("source rebuilds pass the original local task to Vite", (t) => {
  const state = fixture(t);
  installSourceViteFixture(state);
  const first = successful(runCli(state, "build", ["--source"], { CODEX_SESSION_ID: SESSION_ID }));
  assert.equal(marker(readFileSync(first.htmlPath, "utf8"), "data-app-local-thread"), SESSION_ID);
  const rebuilt = successful(runCli(state, "build", ["--source"], { CODEX_SESSION_ID: THREAD_ID }));
  assert.equal(marker(readFileSync(rebuilt.htmlPath, "utf8"), "data-app-local-thread"), SESSION_ID);
  assertNoPackageManager(state);
});

test("default build source escape hatch rejects Vite resolved outside the project", (t) => {
  const state = fixture(t);
  const outside = join(state.root, "outside-vite");
  installSourceViteFixture(state, outside);
  mkdirSync(join(state.project, "node_modules"));
  if (!createLink(t, outside, join(state.project, "node_modules/vite"), true)) return;
  failed(runCli(state, "build", ["--source"]), /source-build Vite must be contained in the Data app project/u);
  assert.equal(existsSync(state.sourceLog), false);
  assert.equal(existsSync(join(state.project, "dist")), false);
  assertNoPackageManager(state);
});

test("separate-data CLI publishes a complete bundle and exports reviewed bytes after source edits", (t) => {
  const state = fixture(t);
  const source = join(state.project, "src/data.json"), original = readFileSync(source);
  const built = successful(runCli(state, "build", ["--separate-data"], { CODEX_SESSION_ID: SESSION_ID }));
  const bundle = readSeparateDataBundle({ projectDir: state.project });
  assert.equal(built.buildKind, "separate-data-v1");
  assert.equal(built.buildManifestPath, bundle.manifestPath);
  assert.equal(built.snapshotPath, bundle.snapshotPath);
  assert.deepEqual(readFileSync(bundle.snapshotPath), original);
  assert.deepEqual(readdirSync(join(state.project, "dist")).sort(), ["data-app-build.json", "index.html", bundle.manifest.snapshot.path].sort());
  const script = scriptElements(bundle.htmlBytes.toString()).find(element => element.type === "application/json");
  assert.deepEqual(JSON.parse(script.code).queries, {});
  assert.equal(marker(bundle.htmlBytes.toString(), "data-app-local-thread"), SESSION_ID);
  write(source, '{"title":"Later source","queries":{}}');
  const exported = successful(runCli(state, "export-offline", ["--output", ".data-app-offline/exports/reviewed.html"]));
  const offline = readFileSync(exported.htmlPath, "utf8");
  const complete = scriptElements(offline).filter(element => element.type === "application/json");
  assert.deepEqual(JSON.parse(complete.map(element => element.code).join("")), JSON.parse(original));
  assert.equal(exported.snapshotSha256, sha256(original));
  assert.doesNotMatch(offline, /<meta name="data-app-local-snapshot"/u);
  assert.deepEqual(readFileSync(bundle.snapshotPath), original);
  assertNoDependencies(state);
});

test("separate-data writes all files before replacement and preserves the old dist on a candidate failure", (t) => {
  const state = fixture(t);
  successful(runCli(state, "build", ["--separate-data"]));
  const previous = inventory(join(state.project, "dist"));
  const loader = join(state.root, "separate-write-fault.mjs");
  write(loader, `import fs from "node:fs/promises";
import { syncBuiltinESMExports } from "node:module";
const original = fs.writeFile;
fs.writeFile = async function(path, ...args) {
  if (String(path).endsWith("/next/data-app-build.json")) throw new Error("injected split manifest write failure");
  return original.call(this, path, ...args);
};
syncBuiltinESMExports();`);
  failed(runCli(state, "build", ["--separate-data"], {}, ["--import", pathToFileURL(loader).href]), /injected split manifest write failure/u);
  assert.deepEqual(inventory(join(state.project, "dist")), previous);
  assert.equal(readdirSync(state.project).some(name => name.startsWith(".data-app-build-")), false);
  const rebuilt = successful(runCli(state, "build"));
  assert.deepEqual(readdirSync(join(state.project, "dist")), ["index.html"]);
  assert.equal(rebuilt.buildKind, undefined);
});

test("CLI rejects unsupported split/source combinations before invoking any build", (t) => {
  const state = fixture(t);
  failed(runCli(state, "build", ["--source", "--separate-data"]), /distinct build path/u);
  failed(runCli(state, "prepare", ["--separate-data"]), /only for a prebuilt build/u);
  failed(runCli(state, "export-offline", ["--source"]), /requires a verified separate-data build/u);
  failed(runCli(state, "build", ["--output", "offline.html"]), /only for export-offline/u);
  assert.equal(existsSync(join(state.project, "dist")), false);
  assertNoDependencies(state);
});

test("publication clones build only after exact snapshot hydration", (t) => {
  const state = fixture(t), source = join(state.project, "src/data.json"), original = readFileSync(source);
  write(join(state.project, ".openai/data-app-publication-source.json"), JSON.stringify({
    version: 1, kind: "data-app-publication-source-v1", sourceSnapshotSha256: sha256(original),
    snapshot: { sha256: sha256(original), bytes: original.length },
  }));
  rmSync(source);
  failed(runCli(state, "build", ["--separate-data"]), /exact verified snapshot hydration/u);
  assert.equal(existsSync(join(state.project, "dist")), false);
  write(source, '{"title":"Unverified replacement","queries":{}}');
  failed(runCli(state, "build", ["--separate-data"]), /exact verified snapshot hydration/u);
  write(source, original);
  const result = successful(runCli(state, "build", ["--separate-data"]));
  assert.equal(result.snapshotSha256, sha256(original));
  assert.deepEqual(readFileSync(readSeparateDataBundle({ projectDir: state.project }).snapshotPath), original);

  const previous = inventory(join(state.project, "dist")), loader = join(state.root, "hydration-race.mjs");
  write(loader, `import fs from "node:fs/promises";
import { syncBuiltinESMExports } from "node:module";
const originalRead = fs.readFile, source = ${JSON.stringify(source)};
let changed = false;
fs.readFile = async function(path, ...args) {
  if (String(path) === source && !changed) { changed = true; await fs.writeFile(source, '{"title":"Changed after initial pin check","queries":{}}'); }
  return originalRead.call(this, path, ...args);
};
syncBuiltinESMExports();`);
  failed(runCli(state, "build", ["--separate-data"], {}, ["--import", pathToFileURL(loader).href]), /exact verified snapshot hydration|publication source identity changed/u);
  assert.deepEqual(inventory(join(state.project, "dist")), previous);
});

SHA-256: b08bc6e734e70c1979984a6a98283807baa50bd9b37894d80978bd6b0827fb4a