From e30df720459b2f31fe035ab05f731ae371b133dc Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 11 Aug 2026 21:50:56 -0700 Subject: [PATCH] fix(worker): keep source bundles npm-installable (#122430) --- packages/ai/package.json | 4 +- packages/ai/src/package-dependencies.test.ts | 5 +- pnpm-lock.yaml | 7 +- .../worker-environments/bundle.test.ts | 71 +++++++++++++++++++ 4 files changed, 81 insertions(+), 6 deletions(-) diff --git a/packages/ai/package.json b/packages/ai/package.json index b7748a8b048c..1af4b08ceaa3 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -101,11 +101,13 @@ "@anthropic-ai/sdk": "0.115.0", "@google/genai": "2.13.0", "@mistralai/mistralai": "2.5.0", - "@openclaw/normalization-core": "workspace:*", "openai": "6.49.0", "partial-json": "0.1.7", "typebox": "1.3.6" }, + "devDependencies": { + "@openclaw/normalization-core": "workspace:*" + }, "engines": { "node": ">=22.19.0" }, diff --git a/packages/ai/src/package-dependencies.test.ts b/packages/ai/src/package-dependencies.test.ts index 3fccc9c2206f..4fd2ab7aba6f 100644 --- a/packages/ai/src/package-dependencies.test.ts +++ b/packages/ai/src/package-dependencies.test.ts @@ -44,7 +44,7 @@ async function productionImportsPackage(packageName: string): Promise { } describe("@openclaw/ai source dependency contract", () => { - it("declares normalization-core while production source imports it", async () => { + it("declares bundled normalization-core imports as a workspace dev dependency", async () => { const manifest = JSON.parse( await fs.readFile(path.join(PACKAGE_ROOT, "package.json"), "utf8"), ) as { @@ -53,6 +53,7 @@ describe("@openclaw/ai source dependency contract", () => { }; expect(await productionImportsPackage("@openclaw/normalization-core")).toBe(true); - expect(manifest.dependencies?.["@openclaw/normalization-core"]).toBe("workspace:*"); + expect(manifest.dependencies?.["@openclaw/normalization-core"]).toBeUndefined(); + expect(manifest.devDependencies?.["@openclaw/normalization-core"]).toBe("workspace:*"); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 04deda2564f5..79f8a00f8f09 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2167,9 +2167,6 @@ importers: '@mistralai/mistralai': specifier: 2.5.0 version: 2.5.0(@opentelemetry/api@1.9.1) - '@openclaw/normalization-core': - specifier: workspace:* - version: link:../normalization-core openai: specifier: 6.49.0 version: 6.49.0(@aws-sdk/credential-provider-node@3.972.72)(@smithy/hash-node@4.4.14)(@smithy/signature-v4@5.6.10)(ws@8.21.1)(zod@4.4.3) @@ -2179,6 +2176,10 @@ importers: typebox: specifier: 1.3.6 version: 1.3.6 + devDependencies: + '@openclaw/normalization-core': + specifier: workspace:* + version: link:../normalization-core packages/gateway-client: dependencies: diff --git a/src/gateway/worker-environments/bundle.test.ts b/src/gateway/worker-environments/bundle.test.ts index 34a48fe75ce5..edfad23d64b2 100644 --- a/src/gateway/worker-environments/bundle.test.ts +++ b/src/gateway/worker-environments/bundle.test.ts @@ -2,6 +2,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import * as tar from "tar"; import { describe, expect, it, vi } from "vitest"; +import { runCommandWithTimeout } from "../../process/exec.js"; import { withTestDir } from "../../test-helpers/temp-dir.js"; import { createWorkerBundleProducer, @@ -246,6 +247,76 @@ describe("worker bundle producer", () => { }); }); + it("installs a source bundle when the AI workspace import is bundled", async () => { + await withTestDir({ prefix: "openclaw-worker-bundle-npm-install-" }, async (root) => { + const repoRoot = path.resolve(import.meta.dirname, "../../.."); + const aiManifest = JSON.parse( + await fs.readFile(path.join(repoRoot, "packages/ai/package.json"), "utf8"), + ) as { + dependencies?: Record; + devDependencies?: Record; + }; + const dependencyFields = (["dependencies", "devDependencies"] as const).filter( + (field) => aiManifest[field]?.["@openclaw/normalization-core"] !== undefined, + ); + if (dependencyFields.length !== 1) { + throw new Error( + "@openclaw/ai must classify normalization-core in exactly one dependency field", + ); + } + const dependencyField = dependencyFields[0]!; + const normalizationCoreSpec = aiManifest[dependencyField]?.["@openclaw/normalization-core"]; + if (!normalizationCoreSpec?.startsWith("workspace:")) { + throw new Error("@openclaw/ai must use a workspace normalization-core dependency"); + } + const packageRoot = path.join(root, "package"); + await writeFixture(packageRoot, [["dist/entry.js", 'import "@openclaw/ai";\nexport {};\n']]); + await fs.writeFile( + path.join(packageRoot, "package.json"), + `${JSON.stringify({ + name: "openclaw", + version: "1.2.3", + type: "module", + files: ["dist/"], + dependencies: { "@openclaw/ai": "workspace:*" }, + })}\n`, + "utf8", + ); + const vendorSource = path.join(packageRoot, "node_modules/@openclaw/ai"); + await fs.mkdir(path.join(vendorSource, "dist"), { recursive: true }); + await fs.writeFile( + path.join(vendorSource, "package.json"), + `${JSON.stringify({ + name: "@openclaw/ai", + version: "1.2.3", + type: "module", + main: "./dist/index.js", + [dependencyField]: { "@openclaw/normalization-core": normalizationCoreSpec }, + })}\n`, + "utf8", + ); + await fs.writeFile(path.join(vendorSource, "dist/index.js"), "export {};\n", "utf8"); + + const bundle = await createWorkerBundleProducer({ + packageRoot, + cacheDir: path.join(root, "cache"), + }).prepare(); + const extractRoot = path.join(root, "extract"); + await fs.mkdir(extractRoot); + await tar.extract({ file: bundle.tarballPath, cwd: extractRoot }); + + const install = await runCommandWithTimeout( + ["npm", "install", "--ignore-scripts", "--omit=dev", "--no-audit", "--no-fund"], + { + cwd: extractRoot, + env: { NPM_CONFIG_CACHE: path.join(root, "npm-cache") }, + timeoutMs: 30_000, + }, + ); + expect(install.code, install.stderr).toBe(0); + }); + }); + it("fails closed when a dist-referenced workspace package is not installed", async () => { await withTestDir({ prefix: "openclaw-worker-bundle-vendor-missing-" }, async (root) => { const packageRoot = path.join(root, "package");