← Files DataARCHIVED FILE

tests/ci/inline-chart-ci.test.mjs

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

↓ Download file

import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import test from "node:test";

import { runDataAppFixtureBuild } from "../browser-helpers.mjs";
import { packageDataAppForSites } from "../../skills/publish-artifact-to-sites/scripts/package-data-app-for-sites.mjs";
import { assertNoCustomerToolchain } from "./verify-prebuilt-runtime.mjs";
import { workerFactory, writePublicationFixture } from "./fixtures/publication-fixture.mjs";

const ciRoot = fileURLToPath(new URL("./", import.meta.url));
const pluginRoot = resolve(ciRoot, "../..");
const catalogRoot = resolve(pluginRoot, "../..");
const repoRoot = resolve(pluginRoot, "../../../..");
const nodeVersion = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8")).engines.node;
const pluginPath = "chatgpt/oai-maintained-plugins/plugins/data-analytics";
const expectedSteps = [
  "install-node",
  "ci-regression",
  "hydrate-assets",
  "copy-offline-plugin",
  "verify-runtime",
  "build-app-offline",
  "capture-offline-app",
  "verify-prebuilt-runtime",
  ...[1, 2, 3].flatMap((repeat) => [`warm-build-app-offline-${repeat}`, `verify-warm-app-${repeat}`]),
  "package-sites-offline",
  "verify-sites-package",
  "prepare-inline",
  "plugin-dependencies",
  "prepare-template",
  "template-source-dependencies",
  "install-browser",
  "unit-tests",
  "serialization-browser-tests",
  "feature-coverage-browser-tests",
  "mobile-browser-tests",
  "dashboard-browser-tests",
  "report-browser-tests",
  "browser-tests",
  "dashboard-polish",
  "shared-components",
];
const digest = (contents) => createHash("sha256").update(contents).digest("hex");

function shellQuote(value) {
  return `'${value.replaceAll("'", "'\\''")}'`;
}

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

function writeSourceFixture(project) {
  const files = {
    "package.json": "{}\n",
    "package-lock.json": "{}\n",
    "scripts/verify-protected-runtime.mjs": "// verified fixture\n",
    "src/data-app-owner.js": 'export const dataAppOwnerEmailSha256 = "";\n',
    ".openai/hosting.json": `${JSON.stringify({ d1: "DB", r2: null }, null, 2)}\n`,
  };
  for (const [name, contents] of Object.entries(files)) write(join(project, name), contents);
  write(
    join(project, "protected-runtime.json"),
    JSON.stringify({
      version: 1,
      files: Object.fromEntries(Object.entries(files).map(([name, contents]) => [name, digest(contents)])),
    }),
  );
  write(join(project, "src/data.json"), '{ "id": "offline-ci", "queries": { "reviewed": { "rows": [{ "category": "A", "value": 3 }] } } }\n');
  write(join(project, "src/theme.css"), ":root { --chart-1: red; }\n");
  write(join(project, "src/content/dashboard/DashboardContent.jsx"), "export const DashboardContent = () => null;\n");
}

function fixtureHtml(project, runtimeHash = "a".repeat(64)) {
  return (
    "<!doctype html><html><head>" +
    `<meta name="data-app-snapshot-sha256" content="${digest(readFileSync(join(project, "src/data.json")))}">` +
    `<meta name="data-app-runtime-sha256" content="${runtimeHash}">` +
    '<meta name="data-app-bootstrap" content="deferred-content-v1"></head><body>reviewed app' +
    `<script id="data-app-reviewed-snapshot" type="application/json">${readFileSync(join(project, "src/data.json"), "utf8")}</script>` +
    "<script>globalThis.reviewedFixture = 1;</script></body></html>\n"
  );
}

function sitesPackageFixture(project) {
  return {
    projectRoot: project,
    htmlPath: join(project, "dist/index.html"),
    serverPath: join(project, "dist/server/index.js"),
  };
}

function writeWorkerFixture(project, responseOverride) {
  const packaged = writePublicationFixture(project, { responseOverride });
  write(join(dirname(project), "package-result.json"), JSON.stringify(packaged));
  return packaged;
}

function runCleanCheckout(t, { failStep = "", reviewedNodeManifest = true, suite } = {}) {
  const root = realpathSync(mkdtempSync(join(tmpdir(), "data-inline-ci-regression-")));
  t.after(() => rmSync(root, { recursive: true, force: true }));
  const helper = join(root, "chatgpt/oai-maintained-plugins/plugins/data-analytics/tests/ci/run-inline-chart-ci.sh");
  const log = join(root, "commands.log");
  const bin = join(root, "bin");
  write(helper, readFileSync(join(ciRoot, "run-inline-chart-ci.sh")));
  write(join(root, "package.json"), JSON.stringify({ engines: { node: nodeVersion } }));
  if (reviewedNodeManifest) {
    write(join(root, `lib/js/oai_js/oai_js/node/v${nodeVersion}/manifest.json`), "{}\n");
  }
  for (const path of [
    "package-lock.json",
    "scripts/data-app.mjs",
    "assets/data-app-runtime/manifest.json",
    "skills/publish-artifact-to-sites/scripts/package-data-app-for-sites.mjs",
    "skills/visualize-data/scripts/render-inline-chart.mjs",
    "tests/data-app-serialization-browser.smoke.mjs",
    "tests/data-app-feature-coverage-browser.smoke.mjs",
  ])
    write(join(root, pluginPath, path), "{}\n");
  writeSourceFixture(join(root, pluginPath, "templates/data-app/base"));
  write(
    join(root, pluginPath, "templates/data-app/base/scripts/protected-file-digest.mjs"),
    readFileSync(join(pluginRoot, "templates/data-app/base/scripts/protected-file-digest.mjs")),
  );
  for (const name of ["verify-offline-build.mjs", "verify-prebuilt-runtime.mjs"])
    write(join(root, pluginPath, "tests/ci", name), readFileSync(join(ciRoot, name)));
  // A reused worker may retain unrelated test packages or old build output.
  // Neither may enter the isolated customer-plugin copy.
  write(join(root, pluginPath, "node_modules/ambient-fixture/package.json"), "{}\n");
  write(join(root, pluginPath, "templates/data-app/base/dist/stale.html"), "stale\n");
  write(join(root, "chatgpt/oai-maintained-plugins/manage/blob_data.hashes"), "fixture\n");
  write(join(root, "chatgpt/oai-maintained-plugins/pyproject.toml"), "fixture\n");
  write(
    join(root, "plugins/visualize/skills/visualize/assets/visualize.html"),
    "reviewed Codex host fixture\n",
  );
  const copiedTest = join(
    root,
    "chatgpt/oai-maintained-plugins/plugins/data-analytics/tests/ci/inline-chart-ci.test.mjs",
  );
  mkdirSync(dirname(copiedTest), { recursive: true });
  copyFileSync(fileURLToPath(import.meta.url), copiedTest);

  const commandFixture = fileURLToPath(new URL("fixtures/ci-command.mjs", import.meta.url));
  for (const command of ["uname", "oaipkg", "node", "npm", "blobdata"]) {
    write(
      join(bin, command),
      `#!/bin/sh\nexec ${shellQuote(process.execPath)} ${shellQuote(commandFixture)} ${shellQuote(command)} "$@"\n`,
      { mode: 0o755 },
    );
  }
  const result = spawnSync("bash", [helper, ...(suite ? ["--suite", suite] : [])], {
    cwd: root,
    encoding: "utf8",
    timeout: 30_000,
    env: {
      ...process.env,
      PATH: `${bin}:${process.env.PATH}`,
      DATA_INLINE_MONOREPO_ROOT: "/wrong/inherited-checkout",
      DATA_INLINE_CI_TEST_ROOT: root,
      DATA_INLINE_CI_TEST_LOG: log,
      DATA_INLINE_CI_TEST_FAIL: failStep,
      DATA_INLINE_CI_TEST_SUITE: suite || "all",
    },
  });
  const steps = (() => {
    try {
      return readFileSync(log, "utf8").trim().split("\n").filter(Boolean);
    } catch (error) {
      if (error.code === "ENOENT") return [];
      throw error;
    }
  })();
  return { ...result, steps };
}

test("CI proves the prebuilt runtime, repeats cold and warm builds, and packages its Worker offline", (t) => {
  const result = runCleanCheckout(t);
  assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
  assert.deepEqual(result.steps, expectedSteps);
});

test("separate CI groups retain cold setup and run every regression exactly once", (t) => {
  const setup = expectedSteps.slice(0, expectedSteps.indexOf("unit-tests"));
  const regressionSteps = expectedSteps.slice(setup.length);
  const actual = [];
  for (const suite of ["app", "inline"]) {
    const result = runCleanCheckout(t, { suite });
    assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
    assert.deepEqual(result.steps.slice(0, setup.length), setup);
    const regressions = result.steps.slice(setup.length);
    assert.deepEqual(regressions, regressionSteps.filter((step) => (step === "browser-tests") === (suite === "inline")));
    actual.push(...regressions);
  }
  assert.deepEqual(actual.sort(), [...regressionSteps].sort());
});

test("unknown CI groups fail before setup rather than silently skipping regressions", (t) => {
  const result = runCleanCheckout(t, { suite: "unknown" });
  assert.equal(result.status, 1);
  assert.match(result.stderr, /Usage:/u);
  assert.deepEqual(result.steps, []);
});

test("the standalone browser smoke accepts the reviewed Codex checkout explicitly", () => {
  const smoke = readFileSync(join(pluginRoot, "tests/inline-chart-browser.smoke.mjs"), "utf8");
  assert.match(
    smoke,
    /const monorepoRoot = resolve\(process\.env\.DATA_INLINE_MONOREPO_ROOT \|\| resolve\(pluginRoot, "\.\.\/\.\.\/\.\.\/\.\."\)\)/u,
    "An explicit host checkout must take precedence over the plugin's original monorepo layout",
  );
  assert.match(smoke, /const appRoot = join\(monorepoRoot, "codex\/codex-apps"\)/u);
});

test("the actual Codex browser host uses only explicit maintainer dependencies", () => {
  const smoke = readFileSync(join(pluginRoot, "tests/inline-chart-browser.smoke.mjs"), "utf8");
  assert.match(smoke, /DATA_INLINE_HOST_DEPENDENCY_ROOT/u);
  assert.match(smoke, /createRequire\(join\(hostDependencyRoot, "package\.json"\)\)/u);
  const hostBuild = smoke.match(/async function buildActualVisualizationHost\(\) \{([\s\S]*?)\n\}/u)?.[1];
  assert.ok(hostBuild, "The browser regression must build the actual Visualize host");
  assert.match(hostBuild, /dependencyRequire\.resolve\("vite"\)/u);
  assert.doesNotMatch(smoke, /data-app-toolchain|withBundledNativeBinding|dependencyDirectory|requireDependencies/u);
});

test("all Data app browser fixtures build through the package-manager-free default", () => {
  for (const name of [
    "data-app-browser.smoke.mjs",
    "data-app-browser-dashboard-composition.mjs",
    "data-report-browser.smoke.mjs",
    "data-app-url-state-browser.smoke.mjs",
    "data-app-authored-tab-url-browser.smoke.mjs",
    "data-app-permalink-tabs-browser.smoke.mjs",
    "data-app-block-drag-browser.smoke.mjs",
    "data-app-creative-layout-browser.smoke.mjs",
    "data-app-custom-layout-browser.smoke.mjs",
    "data-app-ranked-list-browser.smoke.mjs",
    "data-app-feature-coverage-browser.smoke.mjs",
    "data-app-theme-contrast.smoke.mjs",
    "data-app-header-alignment.smoke.mjs",
  ]) {
    const source = readFileSync(join(pluginRoot, "tests", name), "utf8");
    assert.match(source, /runDataAppFixtureBuild\(/u, `${name} must exercise the customer default`);
    assert.doesNotMatch(
      source,
      /spawnSync\("npm"|symlinkSync\(|templates\/data-app\/base\/dist/u,
      `${name} must not borrow a previous source build or node_modules`,
    );
  }
  const dashboard = readFileSync(join(pluginRoot, "tests/data-app-browser.smoke.mjs"), "utf8");
  assert.match(dashboard, /buildAuthoredPublishedFixture\("secondary-tab"/u);
  assert.match(dashboard, /buildAuthoredPublishedFixture\("unsafe-ids"/u);
  assert.doesNotMatch(
    dashboard,
    /replaceUniquePublishedFixture|activeTabBinding|dashboardContextSnapshot/u,
    "Browser behavior fixtures must edit authored source, not compiler-specific minified output",
  );
});

test("CI stops on a failed hydration, offline build, template preparation, or test", async (t) => {
  for (const failStep of [
    "hydrate-assets",
    "copy-offline-plugin",
    "build-app-offline",
    "verify-prebuilt-runtime",
    "warm-build-app-offline-1",
    "verify-warm-app-1",
    "warm-build-app-offline-3",
    "verify-warm-app-3",
    "package-sites-offline",
    "verify-sites-package",
    "prepare-inline",
    "prepare-template",
    "template-source-dependencies",
    "unit-tests",
    "serialization-browser-tests",
    "feature-coverage-browser-tests",
    "mobile-browser-tests",
    "dashboard-browser-tests",
    "report-browser-tests",
    "browser-tests",
    "dashboard-polish",
    "shared-components",
  ]) {
    await t.test(failStep, (t) => {
      const result = runCleanCheckout(t, { failStep });
      assert.equal(result.status, 29, `${result.stdout}\n${result.stderr}`);
      assert.deepEqual(result.steps, expectedSteps.slice(0, expectedSteps.indexOf(failStep) + 1));
    });
  }
});

test("the prebuilt probe rejects installed packages and native/WASM compilers", (t) => {
  const projectRoot = realpathSync(mkdtempSync(join(tmpdir(), "data-prebuilt-probe-")));
  t.after(() => rmSync(projectRoot, { recursive: true, force: true }));
  const clean = {
    projectRoot,
    loadedModules: ["/plugin/scripts/prebuilt/compiler.cjs"],
    sharedObjects: ["/usr/lib/libSystem.B.dylib"],
  };
  assert.doesNotThrow(() => assertNoCustomerToolchain(clean));
  for (const filename of [
    "/ambient/node_modules/react/index.js",
    "C:\\ambient\\node_modules\\vite\\dist\\node\\index.js",
    "/ambient/node_modules/@rolldown/binding-wasm32-wasi/index.js",
  ]) {
    assert.throws(
      () => assertNoCustomerToolchain({ ...clean, loadedModules: [filename] }),
      /must not load an installed npm package/u,
    );
  }
  for (const filename of [
    "/ambient/rolldown-binding.linux-x64-gnu.node",
    "C:\\ambient\\lightningcss.win32-x64-msvc.node",
    "/ambient/esbuild.wasm",
  ]) {
    assert.throws(
      () => assertNoCustomerToolchain({ ...clean, sharedObjects: [filename] }),
      /must not load a native or WASM build toolchain/u,
    );
  }
  mkdirSync(join(projectRoot, "node_modules"));
  assert.throws(() => assertNoCustomerToolchain(clean), /must not contain node_modules/u);
});

function browserBuildFixture(t, source) {
  const root = realpathSync(mkdtempSync(join(tmpdir(), "data-browser-build-contract-")));
  t.after(() => rmSync(root, { recursive: true, force: true }));
  const plugin = join(root, "plugin");
  const project = join(root, "app");
  mkdirSync(project, { recursive: true });
  write(join(plugin, "scripts/data-app.mjs"), source);
  return () => runDataAppFixtureBuild(project, { pluginRoot: plugin });
}

test("browser fixture builds invoke absolute Node with no ambient package environment", (t) => {
  const run = browserBuildFixture(
    t,
    `
import assert from "node:assert/strict";
assert.deepEqual(process.argv.slice(2), ["build", "--project-dir", process.cwd()]);
assert.equal(process.env.NODE_PATH, "");
assert.equal(process.env.NODE_OPTIONS, "");
assert.equal(process.env.npm_config_offline, "true");
console.log("node-only fixture");
`,
  );
  const result = run();
  assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
  assert.match(result.stdout, /node-only fixture/u);
});

test("browser fixture builds detect swallowed package-manager calls", (t) => {
  const run = browserBuildFixture(
    t,
    `
import { spawnSync } from "node:child_process";
spawnSync("npm", ["--version"], { shell: process.platform === "win32" });
`,
  );
  assert.throws(run, /invoked a package manager/u);
});

test("browser fixture builds reject creating a dependency tree", (t) => {
  const run = browserBuildFixture(
    t,
    `
import { mkdirSync } from "node:fs";
mkdirSync("node_modules");
`,
  );
  assert.throws(run, /must not create or borrow node_modules/u);
});

function offlineBuildFixture(t) {
  const root = realpathSync(mkdtempSync(join(tmpdir(), "data-offline-build-check-")));
  t.after(() => rmSync(root, { recursive: true, force: true }));
  const project = join(root, "app");
  const state = join(root, "state.json");
  const packageResult = join(root, "package-result.json");
  writeSourceFixture(project);
  write(join(project, "dist/index.html"), fixtureHtml(project));
  const packaged = sitesPackageFixture(project);
  write(packageResult, JSON.stringify(packaged));
  const run = (command, extra = []) =>
    spawnSync(
      process.execPath,
      [join(ciRoot, "verify-offline-build.mjs"), command, "--project-dir", project, "--state-file", state, ...extra,
        ...(extra.includes("--require-worker") ? ["--package-result-file", packageResult] : [])],
      { encoding: "utf8" },
    );
  const captured = run("capture");
  assert.equal(captured.status, 0, `${captured.stdout}\n${captured.stderr}`);
  return { project, run };
}

test("offline app checks preserve reviewed source and validate a standalone Worker", (t) => {
  const fixture = offlineBuildFixture(t);
  const warm = fixture.run("verify");
  assert.equal(warm.status, 0, `${warm.stdout}\n${warm.stderr}`);
  writeWorkerFixture(fixture.project);
  const packaged = fixture.run("verify", ["--require-worker"]);
  assert.equal(packaged.status, 0, `${packaged.stdout}\n${packaged.stderr}`);
});

test("offline app checks accept the real packager's immutable asset and source-preservation contract", (t) => {
  const fixture = offlineBuildFixture(t);
  // This suite runs before release blob hydration. Use the real packager with a
  // small Worker factory, while the CI shell later covers the shipped build.
  const runtime = join(dirname(fixture.project), "runtime");
  write(join(runtime, "assets/data-app-runtime/worker.mjs"), workerFactory);
  const packaged = packageDataAppForSites({ "project-dir": fixture.project,
    "project-id": "appgprj_offline_real" }, runtime);
  write(join(dirname(fixture.project), "package-result.json"), JSON.stringify(packaged));
  const result = fixture.run("verify", ["--require-worker"]);
  assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
});

test("offline app checks reject source drift, dependency trees, stale HTML, and incomplete Workers", async (t) => {
  const cases = [
    {
      name: "missing Worker",
      mutate: ({ project }) => rmSync(writeWorkerFixture(project).serverPath),
      extra: ["--require-worker"],
      error: /Missing or invalid Sites Worker build/u,
    },
    {
      name: "changed package lock",
      mutate: ({ project }) => write(join(project, "package-lock.json"), '{"changed":true}\n'),
      extra: [],
      error: /reviewed Data app source was modified/u,
    },
    {
      name: "changed authored content",
      mutate: ({ project }) =>
        write(join(project, "src/content/dashboard/DashboardContent.jsx"), "export const changed = true;\n"),
      extra: [],
      error: /reviewed Data app source was modified/u,
    },
    {
      name: "created dependency tree",
      mutate: ({ project }) => write(join(project, "node_modules/.vite-temp/config.mjs"), "temporary\n"),
      extra: [],
      error: /must not create project node_modules/u,
    },
    {
      name: "changed browser runtime",
      mutate: ({ project }) => write(join(project, "dist/index.html"), fixtureHtml(project, "b".repeat(64))),
      extra: [],
      error: /prebuilt browser runtime changed/u,
    },
    {
      name: "stale reviewed snapshot marker",
      mutate: ({ project }) => write(join(project, "src/data.json"), '{"queries":{"changed":{}}}\n'),
      extra: [],
      error: /must match the exact reviewed snapshot bytes/u,
    },
    {
      name: "nondeterministic warm output",
      mutate: ({ project }) => write(join(project, "dist/index.html"), `${fixtureHtml(project)}<!-- changed -->\n`),
      extra: [],
      error: /same HTML bytes/u,
    },
    {
      name: "packaging changes local HTML",
      mutate: ({ project }) => {
        writeWorkerFixture(project);
        write(join(project, "dist/index.html"), `${fixtureHtml(project)}<!-- changed -->\n`);
      },
      extra: ["--require-worker"],
      error: /HTML|asset|client code/u,
    },
    {
      name: "packaging changes unrelated hosting settings",
      mutate: ({ project }) => {
        writeWorkerFixture(project);
        const path = join(project, ".openai/hosting.json");
        write(path, JSON.stringify({ ...JSON.parse(readFileSync(path, "utf8")), unrelated: true }));
      },
      extra: ["--require-worker"],
      error: /hosting|source/u,
    },
    {
      name: "self-consistent hosted assets substitute different executable code",
      mutate: ({ project }) => {
        const packaged = writeWorkerFixture(project);
        const original = readFileSync(packaged.htmlPath, "utf8");
        const html = original.replace("globalThis.reviewedFixture = 1;", "globalThis.reviewedFixture = 2;");
        assert.notEqual(html, original);
        const sha256 = digest(html);
        const descriptor = { key: `data-app/html/${sha256}`, sha256, bytes: Buffer.byteLength(html) };
        write(packaged.htmlPath, html);
        write(packaged.assetFiles.html, html);
        packaged.deploymentAssets.html = descriptor;
        packaged.hostedHtmlSha256 = sha256;
        write(join(dirname(project), "package-result.json"), JSON.stringify(packaged));
        const manifest = JSON.parse(readFileSync(packaged.assetManifestPath, "utf8"));
        manifest.assets.html = { ...descriptor, path: "html.html" };
        write(packaged.assetManifestPath, JSON.stringify(manifest));
        write(packaged.serverPath, `${workerFactory}\nexport default createDataAppWorker(${JSON.stringify({ deploymentAssets: packaged.deploymentAssets })});\n`);
      },
      extra: ["--require-worker"],
      error: /reviewed|HTML|application|executable/u,
    },
    {
      name: "incorrect bucket binding",
      mutate: ({ project }) => {
        writeWorkerFixture(project);
        const path = join(project, ".openai/hosting.json");
        write(path, JSON.stringify({ ...JSON.parse(readFileSync(path, "utf8")), r2: "WRONG" }));
      },
      extra: ["--require-worker"],
      error: /hosting|BUCKET|source/u,
    },
    {
      name: "changed preserved offline HTML",
      mutate: ({ project }) => write(writeWorkerFixture(project).offlineHtmlPath, fixtureHtml(project) + "<!-- changed -->"),
      extra: ["--require-worker"],
      error: /offline|HTML/u,
    },
    {
      name: "missing hosted HTML asset",
      mutate: ({ project }) => rmSync(writeWorkerFixture(project).assetFiles.html),
      extra: ["--require-worker"],
      error: /asset|ENOENT/u,
    },
    {
      name: "corrupted snapshot asset",
      mutate: ({ project }) => write(writeWorkerFixture(project).assetFiles.snapshot, '{"queries":{}}'),
      extra: ["--require-worker"],
      error: /asset|snapshot/u,
    },
    {
      name: "validly hashed asset substitutes different reviewed rows",
      mutate: ({ project }) => {
        const packaged = writeWorkerFixture(project);
        const bytes = '{"id":"offline-ci","queries":{"reviewed":{"rows":[{"category":"A","value":999}]}}}';
        const sha256 = digest(bytes);
        const descriptor = { key: `data-app/snapshot/${sha256}`, sha256, bytes: Buffer.byteLength(bytes) };
        write(packaged.assetFiles.snapshot, bytes);
        packaged.deploymentAssets.snapshot = descriptor;
        packaged.snapshotSha256 = sha256;
        write(join(dirname(project), "package-result.json"), JSON.stringify(packaged));
        const manifest = JSON.parse(readFileSync(packaged.assetManifestPath, "utf8"));
        manifest.assets.snapshot = { ...descriptor, path: "snapshot.json" };
        write(packaged.assetManifestPath, JSON.stringify(manifest));
      },
      extra: ["--require-worker"],
      error: /reviewed|snapshot/u,
    },
    {
      name: "incorrect Sites attribution",
      mutate: ({ project }) => {
        writeWorkerFixture(project);
        const path = join(project, "dist/.openai/hosting.json");
        const hosting = JSON.parse(readFileSync(path, "utf8"));
        hosting.artifact_metadata.producer = "customer-authored-value";
        write(path, JSON.stringify(hosting));
      },
      extra: ["--require-worker"],
      error: /Sites attribution must identify the reviewed Data artifact/u,
    },
    {
      name: "Worker serves different HTML",
      mutate: ({ project }) => writeWorkerFixture(project, "wrong reviewed HTML"),
      extra: ["--require-worker"],
      error: /must serve the exact reviewed HTML/u,
    },
    {
      name: "Worker omits its Sites project marker",
      mutate: ({ project }) => writeWorkerFixture(project, fixtureHtml(project)),
      extra: ["--require-worker"],
      error: /must serve the exact reviewed HTML/u,
    },
    {
      name: "Worker identifies another Sites project",
      mutate: ({ project }) => writeWorkerFixture(project, fixtureHtml(project).replace(
        "<head>", '<head><meta name="data-app-sites-project" content="appgprj_other">',
      )),
      extra: ["--require-worker"],
      error: /must serve the exact reviewed HTML/u,
    },
    {
      name: "Worker duplicates its Sites project marker",
      mutate: ({ project }) => writeWorkerFixture(project, fixtureHtml(project).replace(
        "<head>", '<head>' + '<meta name="data-app-sites-project" content="appgprj_offline_fixture">'.repeat(2),
      )),
      extra: ["--require-worker"],
      error: /must serve the exact reviewed HTML/u,
    },
    {
      name: "Worker has no fetch handler",
      mutate: ({ project }) => {
        const packaged = writeWorkerFixture(project);
        write(packaged.serverPath, "export default {};\n");
      },
      extra: ["--require-worker"],
      error: /must export a real Worker/u,
    },
  ];
  for (const scenario of cases) {
    await t.test(scenario.name, (t) => {
      const fixture = offlineBuildFixture(t);
      scenario.mutate(fixture);
      const result = fixture.run("verify", scenario.extra);
      assert.notEqual(result.status, 0);
      assert.match(result.stderr, scenario.error);
    });
  }
});

test("CI refuses to install an unreviewed Node archive", (t) => {
  const result = runCleanCheckout(t, { reviewedNodeManifest: false });
  assert.equal(result.status, 1);
  assert.match(result.stderr, /Missing reviewed Node archive manifest/u);
  assert.deepEqual(result.steps, []);
});

test("the parent CI gate keeps its tools CI-only and watches its pinned inputs", () => {
  const pipeline = readFileSync(join(catalogRoot, "manage/applied_spec.py"), "utf8");
  const metadata = readFileSync(join(catalogRoot, "pyproject.toml"), "utf8");
  assert.match(pipeline, /oaipkg install oai_js applied-blob-data/u);
  assert.match(pipeline, /run-during-merge/u);
  assert.match(pipeline, /eligible_for_test_selection_pruning=False/u);
  assert.match(metadata, /direct-test-project-dependencies\s*=\s*\[\s*"applied-blob-data",\s*"oai_js"\s*\]/u);
  assert.doesNotMatch(
    metadata,
    /^monorepo-dependencies\s*=\s*\[[^\]]*"(?:applied-blob-data|oai_js)"/mu,
    "CI-only tools must not change the catalog's shared Python runtime lock",
  );
  assert.doesNotMatch(
    metadata,
    /dependency-check-ignore\s*=\s*\[[^\]]*"applied-blob-data"/u,
    "Selection-only dependencies do not need an import-check exception",
  );
  for (const path of [
    "chatgpt/oai-maintained-plugins/pyproject.toml",
    "chatgpt/oai-maintained-plugins/manage/blob_data.hashes",
  ])
    assert.ok(pipeline.includes(`"${path}"`), `CI must watch ${path}`);
  assert.ok(pipeline.includes(`"${pluginPath}"`), "CI must watch the Data plugin");
  assert.ok(
    pipeline.includes('"chatgpt/oai-maintained-plugins-publisher/pyproject.toml"'),
    "The publisher tests must use their project's default Bazel selection",
  );
});

SHA-256: ccb43f140f70a1d8c0d1ca2b8bab30da3e64c4f8ae3a5ec8d971ce16f955febb