From e486a1d1cf7e1de98ccc533302d6cfc974e3fc3a Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 21 Jun 2026 12:03:08 +0200 Subject: [PATCH] test(scripts): route Codex install assertions --- .../e2e/lib/codex-on-demand/assertions.mjs | 15 ++ scripts/test-projects.test-support.mjs | 5 + test/scripts/codex-install-assertions.test.ts | 204 ++++++++++++++++++ test/scripts/test-projects.test.ts | 8 + 4 files changed, 232 insertions(+) create mode 100644 test/scripts/codex-install-assertions.test.ts diff --git a/scripts/e2e/lib/codex-on-demand/assertions.mjs b/scripts/e2e/lib/codex-on-demand/assertions.mjs index b67a92b8bf81..a4d0c4abf9df 100644 --- a/scripts/e2e/lib/codex-on-demand/assertions.mjs +++ b/scripts/e2e/lib/codex-on-demand/assertions.mjs @@ -54,6 +54,21 @@ if (!openAiCodexPackageJson) { throw new Error("missing @openai/codex dependency under managed npm root"); } assertPathInside(npmRoot, openAiCodexPackageJson, "@openai/codex dependency"); +const openAiCodexPackage = readJson(openAiCodexPackageJson); +const codexBinPath = + typeof openAiCodexPackage.bin === "string" + ? openAiCodexPackage.bin + : openAiCodexPackage.bin && typeof openAiCodexPackage.bin.codex === "string" + ? openAiCodexPackage.bin.codex + : undefined; +if (!codexBinPath) { + throw new Error(`@openai/codex package has no codex bin: ${openAiCodexPackageJson}`); +} +const codexBin = path.resolve(path.dirname(openAiCodexPackageJson), codexBinPath); +if (!fs.existsSync(codexBin)) { + throw new Error(`missing managed Codex binary: ${codexBin}`); +} +assertPathInside(npmRoot, codexBin, "managed Codex binary"); const list = readJson("/tmp/openclaw-plugins-list.json"); const plugin = (list.plugins || []).find((entry) => entry.id === "codex"); diff --git a/scripts/test-projects.test-support.mjs b/scripts/test-projects.test-support.mjs index 3cd3b21aed1a..2e1415d05749 100644 --- a/scripts/test-projects.test-support.mjs +++ b/scripts/test-projects.test-support.mjs @@ -1540,6 +1540,11 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([ "scripts/e2e/lib/codex-npm-plugin-live/assertions.mjs", ["test/scripts/docker-build-helper.test.ts"], ], + ["scripts/e2e/lib/codex-install-utils.mjs", ["test/scripts/codex-install-assertions.test.ts"]], + [ + "scripts/e2e/lib/codex-on-demand/assertions.mjs", + ["test/scripts/codex-install-assertions.test.ts"], + ], [ "scripts/e2e/lib/clawhub-fixture-server.cjs", [ diff --git a/test/scripts/codex-install-assertions.test.ts b/test/scripts/codex-install-assertions.test.ts new file mode 100644 index 000000000000..4cfabfa9c16b --- /dev/null +++ b/test/scripts/codex-install-assertions.test.ts @@ -0,0 +1,204 @@ +// Codex Install Assertions tests cover Codex plugin install E2E helpers. +import { spawnSync } from "node:child_process"; +import { chmodSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { DatabaseSync } from "node:sqlite"; +import { afterEach, describe, expect, it } from "vitest"; +import { + assertPathInside, + findPackageJson, + npmProjectRootForInstalledPackage, +} from "../../scripts/e2e/lib/codex-install-utils.mjs"; +import { cleanupTempDirs, makeTempDir } from "../helpers/temp-dir.js"; + +const ASSERTIONS_SCRIPT = "scripts/e2e/lib/codex-on-demand/assertions.mjs"; +const DISABLE_EXPERIMENTAL_WARNING = "--disable-warning=ExperimentalWarning"; +const tempDirs: string[] = []; +const tmpFixtureFiles = ["/tmp/openclaw-codex-inspect.json", "/tmp/openclaw-plugins-list.json"]; + +afterEach(() => { + for (const file of tmpFixtureFiles) { + rmSync(file, { force: true }); + } + cleanupTempDirs(tempDirs); +}); + +function nodeOptionsWithoutExperimentalWarnings(): string { + const current = process.env.NODE_OPTIONS ?? ""; + return current.includes(DISABLE_EXPERIMENTAL_WARNING) + ? current + : [current, DISABLE_EXPERIMENTAL_WARNING].filter(Boolean).join(" "); +} + +function writeJson(filePath: string, value: unknown) { + mkdirSync(path.dirname(filePath), { recursive: true }); + writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8"); +} + +function writeAuthProfileStoreSqlite(agentDir: string) { + mkdirSync(agentDir, { recursive: true }); + const db = new DatabaseSync(path.join(agentDir, "openclaw-agent.sqlite")); + try { + db.exec(` + CREATE TABLE IF NOT EXISTS auth_profile_store ( + store_key TEXT NOT NULL PRIMARY KEY, + store_json TEXT NOT NULL, + updated_at INTEGER NOT NULL + ); + `); + db.prepare( + ` + INSERT INTO auth_profile_store (store_key, store_json, updated_at) + VALUES (?, ?, ?) + `, + ).run( + "primary", + JSON.stringify({ + version: 1, + profiles: { + "openai:api-key": { + type: "api_key", + provider: "openai", + keyRef: { source: "env", provider: "default", id: "OPENAI_API_KEY" }, + }, + }, + }), + Date.now(), + ); + } finally { + db.close(); + } +} + +function runCodexOnDemandAssertions(root: string) { + return spawnSync(process.execPath, [ASSERTIONS_SCRIPT], { + encoding: "utf8", + env: { + ...process.env, + HOME: path.join(root, "home"), + NODE_OPTIONS: nodeOptionsWithoutExperimentalWarnings(), + OPENCLAW_CONFIG_PATH: path.join(root, "state", "openclaw.json"), + OPENCLAW_STATE_DIR: path.join(root, "state"), + }, + }); +} + +function createCodexInstallFixture(root: string) { + const stateDir = path.join(root, "state"); + const npmRoot = path.join(stateDir, "npm"); + const installPath = path.join(npmRoot, "projects", "codex", "node_modules", "@openclaw", "codex"); + const projectRoot = npmProjectRootForInstalledPackage(installPath, "@openclaw/codex"); + writeJson(path.join(installPath, "package.json"), { name: "@openclaw/codex" }); + const openAiCodexRoot = path.join(projectRoot, "node_modules", "@openai", "codex"); + writeJson(path.join(openAiCodexRoot, "package.json"), { + name: "@openai/codex", + bin: { codex: "bin/codex.js" }, + }); + const codexBin = path.join(openAiCodexRoot, "bin", "codex.js"); + mkdirSync(path.dirname(codexBin), { recursive: true }); + writeFileSync(codexBin, "#!/usr/bin/env node\n", { mode: 0o755 }); + chmodSync(codexBin, 0o755); + writeJson(path.join(stateDir, "openclaw.json"), { + agents: { defaults: { model: { primary: "openai/gpt-5.5" } } }, + models: { providers: { openai: { agentRuntime: { id: "codex" } } } }, + plugins: { + installs: { + codex: { + installPath, + source: "npm", + spec: "npm:@openclaw/codex", + }, + }, + }, + }); + writeJson("/tmp/openclaw-codex-inspect.json", { + plugin: { id: "codex", status: "loaded", agentHarnessIds: ["codex"] }, + }); + writeJson("/tmp/openclaw-plugins-list.json", { + plugins: [{ id: "codex", enabled: true, status: "loaded" }], + }); + writeAuthProfileStoreSqlite(path.join(stateDir, "agents", "main", "agent")); +} + +describe("Codex install helpers", () => { + it("resolves package roots and package manifests inside managed npm installs", () => { + const root = makeTempDir(tempDirs, "openclaw-codex-install-utils-"); + const packageRoot = path.join( + root, + "state", + "npm", + "projects", + "codex", + "node_modules", + "@openclaw", + "codex", + ); + const projectRoot = npmProjectRootForInstalledPackage(packageRoot, "@openclaw/codex"); + const dependencyPackage = path.join( + projectRoot, + "node_modules", + "@openai", + "codex", + "package.json", + ); + writeJson(dependencyPackage, { name: "@openai/codex" }); + + expect(projectRoot).toBe(path.join(root, "state", "npm", "projects", "codex")); + expect(findPackageJson("@openai/codex", [packageRoot, projectRoot])).toBe(dependencyPackage); + expect(() => + assertPathInside(projectRoot, dependencyPackage, "codex dependency"), + ).not.toThrow(); + expect(() => assertPathInside(projectRoot, os.tmpdir(), "outside path")).toThrow( + "outside path resolved outside", + ); + }); + + it("accepts a complete on-demand Codex npm install fixture", () => { + const root = makeTempDir(tempDirs, "openclaw-codex-on-demand-"); + createCodexInstallFixture(root); + + const result = runCodexOnDemandAssertions(root); + + expect(result.status).toBe(0); + expect(result.stderr).toBe(""); + }); + + it("rejects on-demand fixtures missing the managed @openai/codex dependency", () => { + const root = makeTempDir(tempDirs, "openclaw-codex-on-demand-missing-"); + createCodexInstallFixture(root); + rmSync(path.join(root, "state", "npm", "projects", "codex", "node_modules", "@openai"), { + force: true, + recursive: true, + }); + + const result = runCodexOnDemandAssertions(root); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain("missing @openai/codex dependency under managed npm root"); + }); + + it("rejects on-demand fixtures missing the managed Codex executable", () => { + const root = makeTempDir(tempDirs, "openclaw-codex-on-demand-missing-bin-"); + createCodexInstallFixture(root); + rmSync( + path.join( + root, + "state", + "npm", + "projects", + "codex", + "node_modules", + "@openai", + "codex", + "bin", + ), + { force: true, recursive: true }, + ); + + const result = runCodexOnDemandAssertions(root); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain("missing managed Codex binary:"); + }); +}); diff --git a/test/scripts/test-projects.test.ts b/test/scripts/test-projects.test.ts index 5f963140ba1c..018f7ec577ef 100644 --- a/test/scripts/test-projects.test.ts +++ b/test/scripts/test-projects.test.ts @@ -477,6 +477,14 @@ describe("scripts/test-projects changed-target routing", () => { "scripts/e2e/lib/codex-npm-plugin-live/assertions.mjs", ["test/scripts/docker-build-helper.test.ts"], ], + [ + "scripts/e2e/lib/codex-install-utils.mjs", + ["test/scripts/codex-install-assertions.test.ts"], + ], + [ + "scripts/e2e/lib/codex-on-demand/assertions.mjs", + ["test/scripts/codex-install-assertions.test.ts"], + ], [ "scripts/e2e/lib/clawhub-fixture-server.cjs", [