diff --git a/scripts/e2e/lib/doctor-install-switch/scenario.sh b/scripts/e2e/lib/doctor-install-switch/scenario.sh index 9870108cd695..03f8faf1c4fd 100644 --- a/scripts/e2e/lib/doctor-install-switch/scenario.sh +++ b/scripts/e2e/lib/doctor-install-switch/scenario.sh @@ -22,6 +22,7 @@ mkdir -p "$git_root" # The git-style install fixture is unpacked from the tarball so this lane does # not depend on checkout source files being present in the Docker image. tar -xzf "$package_tgz" -C "$git_root" --strip-components=1 +node scripts/e2e/lib/package-git-fixture.mjs prepare "$git_root" ( cd "$git_root" openclaw_e2e_maybe_timeout "${OPENCLAW_E2E_NPM_INSTALL_TIMEOUT:-600s}" npm install --omit=optional --no-fund --no-audit >/tmp/openclaw-git-install.log 2>&1 diff --git a/scripts/e2e/lib/package-git-fixture.mjs b/scripts/e2e/lib/package-git-fixture.mjs new file mode 100644 index 000000000000..22504f8a878b --- /dev/null +++ b/scripts/e2e/lib/package-git-fixture.mjs @@ -0,0 +1,64 @@ +#!/usr/bin/env node +// Prepares package-derived Docker E2E fixtures for git-style npm installs. +import fs from "node:fs"; +import path from "node:path"; + +const [command, rootArg] = process.argv.slice(2); + +function usage() { + console.error("usage: package-git-fixture.mjs prepare "); + process.exit(2); +} + +function readJson(file) { + return JSON.parse(fs.readFileSync(file, "utf8")); +} + +function writeJson(file, value) { + fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`); +} + +function withoutAiRuntimeDependency(value) { + if (!Array.isArray(value)) { + return value; + } + const next = value.filter((entry) => entry !== "@openclaw/ai"); + return next.length > 0 ? next : undefined; +} + +function prepare(root) { + const packageJsonPath = path.join(root, "package.json"); + const packageJson = readJson(packageJsonPath); + const aiRuntimeSource = path.join(root, "node_modules", "@openclaw", "ai"); + const aiRuntimePackageJson = path.join(aiRuntimeSource, "package.json"); + if (!fs.existsSync(aiRuntimePackageJson)) { + return; + } + + const aiRuntimeTarget = path.join(root, ".openclaw-fixture", "packages", "ai"); + fs.rmSync(aiRuntimeTarget, { force: true, recursive: true }); + fs.mkdirSync(path.dirname(aiRuntimeTarget), { recursive: true }); + fs.renameSync(aiRuntimeSource, aiRuntimeTarget); + + packageJson.dependencies ??= {}; + packageJson.dependencies["@openclaw/ai"] = "file:.openclaw-fixture/packages/ai"; + packageJson.bundleDependencies = withoutAiRuntimeDependency(packageJson.bundleDependencies); + packageJson.bundledDependencies = withoutAiRuntimeDependency(packageJson.bundledDependencies); + if (packageJson.bundleDependencies === undefined) { + delete packageJson.bundleDependencies; + } + if (packageJson.bundledDependencies === undefined) { + delete packageJson.bundledDependencies; + } + writeJson(packageJsonPath, packageJson); + + // The shipped shrinkwrap points at the published package graph. This fixture is + // intentionally local-git shaped, so let npm resolve the staged file dependency. + fs.rmSync(path.join(root, "npm-shrinkwrap.json"), { force: true }); +} + +if (command !== "prepare" || !rootArg) { + usage(); +} + +prepare(path.resolve(rootArg)); diff --git a/scripts/e2e/update-channel-switch-docker.sh b/scripts/e2e/update-channel-switch-docker.sh index 890517d7be61..500b6b0f08d5 100755 --- a/scripts/e2e/update-channel-switch-docker.sh +++ b/scripts/e2e/update-channel-switch-docker.sh @@ -55,6 +55,7 @@ git_root="/tmp/openclaw-git" mkdir -p "$git_root" # Build the fake git install from the packed package contents, not the checkout. tar -xzf "$package_tgz" -C "$git_root" --strip-components=1 +node scripts/e2e/lib/package-git-fixture.mjs prepare "$git_root" # The package-derived fixture can carry patchedDependencies whose targets are # absent from the trimmed tarball install; that should not block update preflight. node scripts/e2e/lib/update-channel-switch/assertions.mjs prepare-git-fixture "$git_root" diff --git a/test/scripts/package-git-fixture.test.ts b/test/scripts/package-git-fixture.test.ts new file mode 100644 index 000000000000..17425141967b --- /dev/null +++ b/test/scripts/package-git-fixture.test.ts @@ -0,0 +1,51 @@ +// Package git fixture tests cover package-derived Docker git install fixtures. +import { spawnSync } from "node:child_process"; +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js"; + +describe("package git fixture", () => { + const tempDirs = useAutoCleanupTempDirTracker(afterEach); + + it("stages bundled ai runtime as a local file dependency", async () => { + const root = tempDirs.make("openclaw-package-git-fixture-"); + mkdirSync(path.join(root, "node_modules", "@openclaw", "ai"), { recursive: true }); + writeFileSync( + path.join(root, "package.json"), + `${JSON.stringify( + { + dependencies: { "@openclaw/ai": "2026.6.11", chalk: "5.6.2" }, + bundleDependencies: ["@openclaw/ai", "chalk"], + }, + null, + 2, + )}\n`, + ); + writeFileSync(path.join(root, "npm-shrinkwrap.json"), "{}\n"); + writeFileSync( + path.join(root, "node_modules", "@openclaw", "ai", "package.json"), + `${JSON.stringify({ name: "@openclaw/ai", version: "2026.6.11" })}\n`, + ); + + const result = spawnSync( + process.execPath, + ["scripts/e2e/lib/package-git-fixture.mjs", "prepare", root], + { cwd: process.cwd(), encoding: "utf8" }, + ); + + expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0); + const packageJson = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")); + expect(packageJson.dependencies["@openclaw/ai"]).toBe("file:.openclaw-fixture/packages/ai"); + expect(packageJson.bundleDependencies).toEqual(["chalk"]); + expect(() => readFileSync(path.join(root, "npm-shrinkwrap.json"), "utf8")).toThrow(); + expect( + JSON.parse( + readFileSync( + path.join(root, ".openclaw-fixture", "packages", "ai", "package.json"), + "utf8", + ), + ).name, + ).toBe("@openclaw/ai"); + }); +});