← Files DataARCHIVED FILE

tests/data-app-separated-package.test.mjs

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

↓ Download file

import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { dirname, join, relative } from "node:path";
import test from "node:test";
import { assembleDataAppHtml } from "../scripts/data-app-build.mjs";
import { exportOfflineDataApp, readSeparateDataBundle } from "../scripts/data-app-separate.mjs";
import { documentSnapshot, inspectDataAppDocument } from "../skills/publish-artifact-to-sites/scripts/publication-assets.mjs";
import { createPublicationSource } from "../skills/publish-artifact-to-sites/scripts/publication-source.mjs";
import { createStreamingJsonParser } from "../templates/data-app/base/src/streaming-json.js";
import { fixture, packageDataApp } from "./data-app-sites-package-fixtures.mjs";

const hash = bytes => createHash("sha256").update(bytes).digest("hex");
const runtimeHash = "a".repeat(64);
const syntheticCredential = "sk-proj-" + "a".repeat(24);
const rawJson = value => JSON.stringify(value, null, 2) + "\n";
const safeJson = text => text.replaceAll("<", "\\u003c").replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029");
const fixtureOptions = {
  appCode: "globalThis.CodexDataAppRuntime = {apiVersion:1,mount(options){globalThis.reviewedMount = options;}};",
  protectedStyles: ".protected{color:blue}", printStyles: "@media print{.printed{color:black}}",
  authored: { themeCss: ":root{--accent:red}", conventionalCss: ".conventional{color:green}", importedCss: ".source-control{color:purple}",
    factorySource: "(function(runtime,snapshot){const completeRows=snapshot.queries.reviewed.rows;return {DashboardContent(){return completeRows;},ReportContent(){return snapshot;}};})" },
  runtimeSha256: runtimeHash,
};
function snapshot(rowCount = 4) {
  return { id: "split-reviewed-artifact", title: "Reviewed split dashboard", surface: "dashboard", buildStatus: "complete", generatedAt: "2026-09-07T00:00:00Z",
    queries: { reviewed: { title: "Complete real-shaped results", source: { sql: "SELECT date, amount, note FROM reviewed_source", status: "available" },
      rows: Array.from({ length: rowCount }, (_, index) => ({ date: "2026-09-07", amount: index / 7, note: `café 日本語 🧪 row ${index}`, optional: null })) },
    empty: { title: "A legitimate empty result", rows: [] }, unavailable: { status: "permission_denied", error: "Missing permissions.", rows: [] } } };
}
function writeSplit(item, raw = rawJson(snapshot())) {
  const bytes = Buffer.from(raw), snapshotSha256 = hash(bytes), snapshotPath = `snapshot.${snapshotSha256}.json`;
  const { html } = assembleDataAppHtml({ ...fixtureOptions, snapshotBytes: bytes, separateData: true });
  const manifest = { version: 1, kind: "separate-data-v1", html: { path: "index.html", sha256: hash(html), bytes: Buffer.byteLength(html) },
    snapshot: { path: snapshotPath, sha256: snapshotSha256, bytes: bytes.length }, runtimeSha256: runtimeHash, sourceSnapshotSha256: snapshotSha256 };
  writeFileSync(join(item.project, "src/data.json"), bytes);
  writeFileSync(join(item.project, "dist/index.html"), html);
  writeFileSync(join(item.project, "dist", snapshotPath), bytes);
  writeFileSync(join(item.project, "dist/data-app-build.json"), rawJson(manifest));
  return { raw, bytes, html, manifest, snapshotPath: join(item.project, "dist", snapshotPath), manifestPath: join(item.project, "dist/data-app-build.json") };
}
function state(t, raw) {
  const item = fixture();
  t.after(() => { rmSync(item.project, { recursive: true, force: true }); rmSync(join(dirname(item.helperPath), "../../.."), { recursive: true, force: true }); });
  return { ...item, split: writeSplit(item, raw) };
}
function packaged(item) { return JSON.parse(packageDataApp(item)); }
function configuration(project) {
  const worker = readFileSync(join(project, "dist/server/index.js"), "utf8"), prefix = "\nexport default createDataAppWorker(JSON.parse(";
  return JSON.parse(JSON.parse(worker.slice(worker.lastIndexOf(prefix) + prefix.length, -4)));
}
function files(root, directory = root) {
  const result = {};
  for (const entry of readdirSync(directory)) {
    const path = join(directory, entry), key = relative(root, path), stat = lstatSync(path);
    if (stat.isSymbolicLink()) result[key] = { symlink: readlinkSync(path) };
    else if (stat.isDirectory()) Object.assign(result, files(root, path));
    else result[key] = { bytes: stat.size, sha256: hash(readFileSync(path)) };
  }
  return result;
}
function rejection(item, before = files(item.project), pattern = /separate Data app|regular files|symlinks|ENOENT/) {
  assert.throws(() => packageDataApp(item), error => {
    assert.match(error.stderr.toString(), pattern);
    assert.ok(!error.stderr.toString().includes(syntheticCredential), "Failure diagnostics must not echo matched credential values");
    return true;
  });
  assert.deepEqual(files(item.project), before, "Rejected publication must leave every existing file and symlink unchanged");
}
function changeManifest(item, transform) {
  const manifest = JSON.parse(readFileSync(item.split.manifestPath)); transform(manifest);
  writeFileSync(item.split.manifestPath, rawJson(manifest));
}

test("actual split assembler output packages full raw bytes with canonical seed identity and no data sidecar in dist", t => {
  const item = state(t, rawJson(snapshot(18_000))), result = packaged(item), config = configuration(item.project);
  assert.equal(result.layout, "separate-data-v1");
  assert.equal(result.snapshotSource, "separate-data-manifest");
  assert.equal(result.scanStats.complete, true);
  assert.equal(result.offlineHtmlPath, null);
  assert.equal(result.snapshotSha256, hash(item.split.bytes));
  assert.equal(config.seedSnapshotSha256, hash(JSON.stringify(JSON.parse(item.split.raw))));
  assert.notEqual(config.seedSnapshotSha256, result.snapshotSha256, "Raw formatting identity is separate from the canonical stored seed identity");
  assert.deepEqual(readFileSync(result.assetFiles.snapshot), item.split.bytes);
  const manifest = JSON.parse(readFileSync(result.assetManifestPath));
  assert.equal(manifest.seedSnapshotSha256, config.seedSnapshotSha256);
  assert.deepEqual(manifest.snapshotResponse, { sha256: hash(item.split.bytes), bytes: item.split.bytes.length,
    queryCount: 3, rowCount: 18_000, encoding: "indexed-raw-v1" });
  const canonical = JSON.stringify(JSON.parse(item.split.raw));
  assert.deepEqual(manifest.legacySnapshotResponse, { sha256: hash(canonical), bytes: Buffer.byteLength(canonical),
    queryCount: 3, rowCount: 18_000 });
  assert.equal(config.snapshotIndex.sha256, manifest.assets.snapshot.sha256);
  assert.equal(config.snapshotIndex.bytes, manifest.assets.snapshot.bytes);
  assert.equal(JSON.parse(item.split.bytes.subarray(...config.snapshotIndex.queries.reviewed.rows)).length, 18_000);
  assert.equal(manifest.snapshotResponse.queryCount, 3);
  assert.equal(manifest.snapshotResponse.rowCount, 18_000);
  assert.ok(result.workerBytes < 12_000, "Fixture Worker code remains independent of full data size");
  assert.equal(Object.hasOwn(config, "seedSnapshot"), false);
  assert.equal(Object.hasOwn(config, "html"), false);
  const hosted = readFileSync(join(item.project, "dist/index.html"), "utf8");
  assert.ok(hosted.includes(fixtureOptions.authored.factorySource));
  assert.ok(hosted.includes(fixtureOptions.appCode));
  assert.doesNotMatch(hosted, /row 17999/);
  assert.ok(!inspectDataAppDocument(hosted).metadata.some(meta => meta.names.some(name => ["data-app-local-snapshot", "data-app-build-layout"].includes(name))));
  assert.deepEqual(JSON.parse(documentSnapshot(hosted).embedded).queries, {});
  assert.deepEqual(Object.keys(files(join(item.project, "dist"))).sort(), [".openai/hosting.json", "index.html", "server/index.js"]);
  const preserved = readSeparateDataBundle({ projectDir: item.project, manifestPath: result.buildManifestPath });
  assert.deepEqual(preserved.htmlBytes, Buffer.from(item.split.html));
  assert.deepEqual(readFileSync(preserved.snapshotPath), item.split.bytes);
  assert.deepEqual(JSON.parse(readFileSync(preserved.manifestPath)), item.split.manifest);
});

test("repeat publication uses the exact compiled split evidence after mutable src data changes", t => {
  const item = state(t), first = packaged(item), previousFiles = files(item.project);
  writeFileSync(join(item.project, "src/data.json"), '{"id":"new-unreviewed-source","queries":{}}\n');
  const afterEdit = files(item.project), repeated = packaged(item);
  assert.deepEqual(repeated.deploymentAssets, first.deploymentAssets);
  assert.equal(repeated.seedSnapshotSha256, first.seedSnapshotSha256);
  assert.deepEqual(readFileSync(repeated.assetFiles.snapshot), item.split.bytes);
  for (const [path, descriptor] of Object.entries(previousFiles)) {
    if (path !== "src/data.json") assert.deepEqual(files(item.project)[path], descriptor, path);
  }
  assert.deepEqual(files(item.project)["src/data.json"], afterEdit["src/data.json"], "Publication does not rewrite later authoring work");
});

test("missing, malformed, stale or escaping split manifests never fall back to empty bootstrap data", async t => {
  const cases = [
    ["missing manifest", item => rmSync(item.split.manifestPath)],
    ["invalid manifest JSON", item => writeFileSync(item.split.manifestPath, '{"invalid')],
    ["unknown version", item => changeManifest(item, m => m.version = 2)],
    ["unknown field", item => changeManifest(item, m => m.extra = "unsupported")],
    ["stale HTML", item => writeFileSync(join(item.project, "dist/index.html"), item.split.html + "<!-- altered -->")],
    ["stale snapshot", item => writeFileSync(item.split.snapshotPath, item.split.raw + " ")],
    ["missing snapshot", item => rmSync(item.split.snapshotPath)],
    ["escaping snapshot path", item => changeManifest(item, m => m.snapshot.path = "../../private.json")],
    ["absolute snapshot path", item => changeManifest(item, m => m.snapshot.path = item.split.snapshotPath)],
    ["altered layout marker", item => writeFileSync(join(item.project, "dist/index.html"), item.split.html.replace('content="separate-data-v1"', 'content="unknown-layout"'))],
    ["removed layout marker", item => writeFileSync(join(item.project, "dist/index.html"), item.split.html.replace('<meta name="data-app-build-layout" content="separate-data-v1">', ""))],
    ["removed layout and local marker", item => writeFileSync(join(item.project, "dist/index.html"), item.split.html.replace('<meta name="data-app-build-layout" content="separate-data-v1">', "").replace(`<meta name="data-app-local-snapshot" content="${item.split.manifest.snapshot.path}">`, ""))],
  ];
  for (const [name, mutate] of cases) await t.test(name, t => { const item = state(t); mutate(item); rejection(item); });
});

test("split inputs and preserved output paths refuse symlinks without touching their targets", async t => {
  for (const target of ["snapshot", "manifest", "html", "preserved-directory"]) await t.test(target, t => {
    const item = state(t), unrelated = join(item.project, "unrelated"); mkdirSync(unrelated);
    if (target === "preserved-directory") {
      mkdirSync(join(item.project, ".data-app-offline"));
      symlinkSync(unrelated, join(item.project, ".data-app-offline/separate-v1"), "dir");
    } else {
      const path = target === "snapshot" ? item.split.snapshotPath : target === "manifest" ? item.split.manifestPath : join(item.project, "dist/index.html");
      const other = join(unrelated, target); writeFileSync(other, readFileSync(path)); rmSync(path); symlinkSync(other, path);
    }
    rejection(item);
  });
});

test("external raw data is scanned completely for late literals, keys, metadata and encoded inline credentials", async t => {
  const late = "reviewed analytical text ".repeat(14_000);
  const cases = [
    ["late literal", seed => { seed.queries.reviewed.rows[3].note = late + syntheticCredential; }],
    ["literal in a key", seed => { seed.queries.reviewed.rows[3][syntheticCredential] = "ordinary value"; }],
    ["late sensitive field", seed => { seed.queries.reviewed.rows[3].note = late; seed.queries.reviewed.rows[3].api_key = "synthetic-nonempty-test-value"; }],
    ["generic metadata field", seed => { seed.queries.reviewed.source.token = "synthetic-metadata-value"; }],
    ["base64 inline credential", seed => { seed.queries.reviewed.rows[3].note = "data:text/javascript;base64," + Buffer.from(`const value="${syntheticCredential}"`).toString("base64"); }],
    ["percent inline JSON credential", seed => { seed.queries.reviewed.rows[3].note = "data:application/json," + encodeURIComponent('{"authorization":"synthetic-value"}'); }],
  ];
  for (const [name, mutate] of cases) await t.test(name, t => {
    const seed = snapshot(); mutate(seed); const item = state(t, rawJson(seed));
    rejection(item, files(item.project), /Publication contains a possible credential/);
  });
  await t.test("overwritten duplicate value remains in published raw bytes", t => {
    const raw = rawJson(snapshot()).replace('"optional": null', `"overwritten": "${syntheticCredential}", "overwritten": "", "optional": null`);
    assert.equal(JSON.parse(raw).queries.reviewed.rows[0].overwritten, "");
    const item = state(t, raw); rejection(item, files(item.project), /Publication contains a possible credential/);
  });
  await t.test("JSON-escaped overwritten duplicate value remains in published raw bytes", t => {
    const escaped = "\\u0073" + syntheticCredential.slice(1);
    const raw = rawJson(snapshot()).replace('"optional": null', `"overwritten": "${escaped}", "overwritten": "", "optional": null`);
    assert.equal(JSON.parse(raw).queries.reviewed.rows[0].overwritten, "");
    const item = state(t, raw); rejection(item, files(item.project), /Publication contains a possible credential/);
  });
  for (const encoding of ["base64", "percent"]) await t.test(`JSON-escaped overwritten ${encoding} data URL remains in raw bytes`, t => {
    const uri = encoding === "base64"
      ? "\\u0064ata:text/javascript;base64," + Buffer.from(syntheticCredential).toString("base64")
      : "\\u0064ata:text/javascript," + Array.from(Buffer.from(syntheticCredential), byte => `%${byte.toString(16).padStart(2, "0")}`).join("");
    const raw = rawJson(snapshot()).replace('"optional": null', `"overwritten": "${uri}", "overwritten": "", "optional": null`);
    assert.equal(JSON.parse(raw).queries.reviewed.rows[0].overwritten, "");
    const item = state(t, raw); rejection(item, files(item.project), /Publication contains a possible credential/);
  });
  await t.test("overwritten ancestor metadata remains sensitive beside legitimate analytical token columns", t => {
    const raw = rawJson(snapshot()).replace('"source": {', '"source": {"token":"synthetic-metadata-value"}, "source": {');
    const item = state(t, raw); rejection(item, files(item.project), /Publication contains a possible credential/);
  });
});

test("ordinary analytical token metrics remain allowed after data separation", t => {
  const seed = snapshot(); seed.queries.reviewed.rows[0].token = "completion"; seed.queries.reviewed.rows[0].secret = "metric-category";
  const item = state(t, rawJson(seed)), result = packaged(item);
  assert.deepEqual(JSON.parse(readFileSync(result.assetFiles.snapshot)), seed);
  assert.equal(result.scanStats.complete, true);
});

test("JSON escape spelling cannot weaken the standalone credential coverage when data moves outside HTML", async t => {
  const cases = [
    ["escaped data URL prefix", () => {
      const seed = snapshot(); seed.queries.reviewed.rows[3].note = "data:text/javascript;base64," + Buffer.from(`const value="${syntheticCredential}"`).toString("base64");
      return rawJson(seed).replace("data:text/javascript", "\\u0064ata:text/javascript");
    }],
    ["escaped credential literal in a key", () => {
      const seed = snapshot(); seed.queries.reviewed.rows[3][syntheticCredential] = "ordinary value";
      return rawJson(seed).replace(syntheticCredential, "\\u0073" + syntheticCredential.slice(1));
    }],
  ];
  for (const [name, makeRaw] of cases) await t.test(name, t => {
    const raw = makeRaw(), legacy = state(t, raw), split = state(t, raw);
    rmSync(legacy.split.manifestPath); rmSync(legacy.split.snapshotPath);
    writeFileSync(join(legacy.project, "dist/index.html"), assembleDataAppHtml({ ...fixtureOptions, snapshotBytes: Buffer.from(raw) }).html);
    rejection(legacy, files(legacy.project), /Publication contains a possible credential/);
    rejection(split, files(split.project), /Publication contains a possible credential/);
  });
});

test("late package installation failure rolls back existing outputs, split inputs and removals", t => {
  const item = state(t);
  for (const [path, content] of [
    ["dist/server/index.js", "previous Worker bytes"],
    [".data-app-assets/html.html", "previous hosted HTML"],
    [".data-app-assets/snapshot.json", "previous asset snapshot"],
    [".data-app-assets/manifest.json", '{"version":1,"thinBootstrap":false}'],
    [".data-app-offline/separate-v1/index.html", "previous preserved HTML"],
    [".data-app-offline/separate-v1/data-app-build.json", "previous preserved manifest"],
  ]) { mkdirSync(dirname(join(item.project, path)), { recursive: true }); writeFileSync(join(item.project, path), content); }
  // The last output cannot replace a directory. This fails after installation
  // has already replaced prior outputs and removed the original split inputs.
  mkdirSync(join(item.project, "dist/.openai/hosting.json"), { recursive: true });
  const before = files(item.project);
  rejection(item, before, /Package outputs must replace regular files/);
  assert.deepEqual(readFileSync(item.split.snapshotPath), item.split.bytes);
  assert.equal(existsSync(item.split.manifestPath), true);
  assert.ok(!readdirSync(item.project).some(name => name.startsWith(".data-app-package-")), "Completed rollback removes temporary backups");
});

test("post-publication export reconstructs complete offline data from preserved inputs without rebuilding", async t => {
  const seed = snapshot(); seed.queries.reviewed.rows[1].note = "</script>\u2028\u2029 café";
  const item = state(t, rawJson(seed)), result = packaged(item);
  writeFileSync(join(item.project, "src/data.json"), "not the reviewed source anymore");
  const exported = await exportOfflineDataApp({ projectDir: item.project, outputPath: ".data-app-offline/exports/reviewed.html" });
  const html = readFileSync(exported.htmlPath, "utf8");
  const inspection = inspectDataAppDocument(item.split.html), element = inspection.snapshotElement;
  let expected = item.split.html.slice(0, element.contentStart) + safeJson(item.split.raw) + item.split.html.slice(element.contentEnd);
  expected = expected.replace('<meta name="data-app-build-layout" content="separate-data-v1">', "").replace(`<meta name="data-app-local-snapshot" content="${item.split.manifest.snapshot.path}">`, "");
  assert.equal(html, expected);
  assert.equal(exported.htmlSha256, hash(html));
  assert.equal(exported.snapshotSha256, result.snapshotSha256);
  assert.deepEqual(JSON.parse(documentSnapshot(html).embedded), seed);
  assert.ok(html.includes(fixtureOptions.authored.factorySource));
  assert.ok(html.includes(fixtureOptions.appCode));
  assert.ok(!inspectDataAppDocument(html).metadata.some(meta => meta.names.some(name => ["data-app-local-snapshot", "data-app-snapshot-storage"].includes(name))));
  const priorExport = readFileSync(exported.htmlPath);
  writeFileSync(join(item.project, ".data-app-offline/separate-v1", item.split.manifest.snapshot.path), "corrupted preserved data");
  await assert.rejects(exportOfflineDataApp({ projectDir: item.project, outputPath: ".data-app-offline/exports/reviewed.html" }), /separate Data app/);
  assert.deepEqual(readFileSync(exported.htmlPath), priorExport);
});

test("standalone and split packaging preserve parsed data and fingerprint their actual streamed bytes", t => {
  const seed = snapshot(), raw = rawJson(seed), split = state(t, raw), standalone = state(t, raw);
  rmSync(standalone.split.manifestPath); rmSync(standalone.split.snapshotPath);
  writeFileSync(join(standalone.project, "dist/index.html"), assembleDataAppHtml({ ...fixtureOptions, snapshotBytes: Buffer.from(raw) }).html);
  const a = packaged(split), b = packaged(standalone);
  const aManifest = JSON.parse(readFileSync(a.assetManifestPath)), bManifest = JSON.parse(readFileSync(b.assetManifestPath));
  for (const [result, manifest] of [[a, aManifest], [b, bManifest]]) {
    const bytes = readFileSync(result.assetFiles.snapshot);
    assert.deepEqual(manifest.snapshotResponse, { sha256: hash(bytes), bytes: bytes.length,
      queryCount: 3, rowCount: 4, encoding: "indexed-raw-v1" });
  }
  assert.notEqual(aManifest.snapshotResponse.sha256, bManifest.snapshotResponse.sha256,
    "The formatted raw source and compact standalone asset have distinct byte identities");
  assert.deepEqual(aManifest.legacySnapshotResponse, bManifest.legacySnapshotResponse);
  assert.equal(aManifest.legacySnapshotResponse.sha256, hash(JSON.stringify(seed)));
  assert.equal(a.seedSnapshotSha256, b.snapshotSha256);
  assert.deepEqual(JSON.parse(readFileSync(a.assetFiles.snapshot)), JSON.parse(readFileSync(b.assetFiles.snapshot)));
});


test("portable offline exports preserve every row but never enter publication source or its Git index", async t => {
  const seed = snapshot(12001), item = state(t, rawJson(seed)), baseline = state(t, rawJson(seed));
  const originalBuild = files(join(item.project, "dist"));
  const ordinaryHtml = "<p>Authored reference asset</p>";
  mkdirSync(join(item.project, "src/content"), { recursive: true });
  writeFileSync(join(item.project, "src/content/reference.html"), ordinaryHtml);
  const exported = await exportOfflineDataApp({ projectDir: item.project });
  const custom = await exportOfflineDataApp({ projectDir: item.project,
    outputPath: join(item.project, ".data-app-offline/exports/custom-review.html") });
  assert.equal(exported.htmlPath, join(item.project, ".data-app-offline/exports/data-app-offline.html"));
  for (const output of [exported, custom]) {
    const { embedded, snapshotChunks } = documentSnapshot(readFileSync(output.htmlPath, "utf8"));
    const parser = createStreamingJsonParser();
    for (const chunk of snapshotChunks ?? [embedded]) parser.write(chunk);
    assert.deepEqual(parser.finish(), seed);
  }
  assert.deepEqual(files(join(item.project, "dist")), originalBuild, "Export does not change any split build bytes");
  const result = packaged(item), baselineResult = packaged(baseline);
  assert.deepEqual(result.deploymentAssets, baselineResult.deploymentAssets);
  assert.deepEqual(readFileSync(result.serverPath), readFileSync(baselineResult.serverPath), "Export does not change packaged Worker bytes");
  const publicationProjectDir = `${item.project}-publication`;
  t.after(() => rmSync(publicationProjectDir, { recursive: true, force: true }));
  execFileSync("git", ["init", "--quiet"], { cwd: item.project });
  const projection = await createPublicationSource({ authoringProjectDir: item.project, publicationProjectDir,
    projectId: result.projectId, sourceRevision: "a".repeat(40), siteUrl: null });
  assert.equal(existsSync(join(publicationProjectDir, ".data-app-offline")), false);
  assert.ok(!Object.keys(projection.manifest.files).some(name => name.startsWith(".data-app-offline/")));
  assert.equal(readFileSync(join(publicationProjectDir, "src/content/reference.html"), "utf8"), ordinaryHtml);
  const git = args => execFileSync("git", ["-c", "core.fsmonitor=false", ...args], { cwd: publicationProjectDir, encoding: "utf8" });
  git(["init", "--quiet"]); git(["add", "-A"]);
  const indexed = git(["ls-files", "-z"]).split("\0").filter(Boolean);
  assert.ok(indexed.includes("src/content/reference.html"), "Ordinary authored HTML remains source");
  assert.ok(indexed.includes(".openai/data-app-publication-source.json"));
  assert.ok(!indexed.some(name => name === "src/data.json" || name.startsWith("dist/") || name.startsWith(".data-app-")));
  assert.equal(existsSync(exported.htmlPath), true); assert.equal(existsSync(custom.htmlPath), true);
});

test("explicit on-demand CLI packaging preserves raw snapshots, canonical identity and repeatability", t => {
  const full = snapshot(12);
  full.filters = [{ id: "note", field: "note", queryIds: ["reviewed"] }];
  const item = state(t, rawJson(full));
  const before = packaged(item);
  const raw = readFileSync(before.assetFiles.snapshot);
  const enabled = JSON.parse(packageDataApp(item, { extra: ["--query-loading=on-demand"] }));
  const manifest = JSON.parse(readFileSync(enabled.assetManifestPath));
  const bootstrap = JSON.parse(documentSnapshot(readFileSync(enabled.assetFiles.html, "utf8")).embedded);
  assert.equal(enabled.queryLoading, "on-demand");
  assert.equal(manifest.queryLoading, "on-demand");
  assert.equal(bootstrap._dataAppQueryLoading.snapshotSha256, before.snapshotSha256);
  assert.equal(bootstrap._dataAppQueryLoading.queries.reviewed.rowCount, 12);
  assert.deepEqual(bootstrap._dataAppQueryLoading.queries.reviewed.columns, ["date", "amount", "note", "optional"]);
  assert.deepEqual(bootstrap._dataAppQueryLoading.filterChoices.note, full.queries.reviewed.rows.map(row => row.note));
  assert.ok(Object.values(bootstrap.queries).every(query => !Object.hasOwn(query, "rows")));
  assert.deepEqual(readFileSync(enabled.assetFiles.snapshot), raw);
  assert.equal(enabled.seedSnapshotSha256, before.seedSnapshotSha256);
  const repeated = JSON.parse(packageDataApp(item, { extra: ["--query-loading=on-demand"] }));
  assert.deepEqual(repeated.deploymentAssets, enabled.deploymentAssets);
  const disabled = packaged(item);
  assert.deepEqual(disabled.deploymentAssets, before.deploymentAssets);
  assert.equal(Object.hasOwn(disabled, "queryLoading"), false);
});

test("on-demand packaging still scans complete reviewed rows before changing any output", t => {
  const full = snapshot(2);
  full.queries.reviewed.rows[1].credential = syntheticCredential;
  const item = state(t, rawJson(full)), before = files(item.project);
  assert.throws(() => packageDataApp(item, { extra: ["--query-loading=on-demand"] }), error => {
    assert.match(error.stderr.toString(), /credential|secret/iu);
    assert.ok(!error.stderr.toString().includes(syntheticCredential));
    return true;
  });
  assert.deepEqual(files(item.project), before);
});

SHA-256: aee626a894a7fa34c1afb5dbb11ff7e39d5cb66d90a1beec053598c99e16c91e