diff --git a/scripts/package-openclaw-for-docker.mjs b/scripts/package-openclaw-for-docker.mjs index 74c8d9470dda..6cd6015bc266 100644 --- a/scripts/package-openclaw-for-docker.mjs +++ b/scripts/package-openclaw-for-docker.mjs @@ -435,6 +435,7 @@ export async function prepareBundledAiRuntimePackage( options = {}, ) { const packageJsonPath = path.join(sourceDir, "package.json"); + const aiRuntimePackageJsonPath = path.join(sourceDir, "packages", "ai", "package.json"); const aiRuntimePath = path.join(sourceDir, "node_modules", "@openclaw", "ai"); const aiRuntimeBackupPath = path.join( sourceDir, @@ -453,7 +454,24 @@ export async function prepareBundledAiRuntimePackage( } catch (error) { throw new Error(`failed to parse ${packageJsonPath}`, { cause: error }); } - if (typeof packageJson.dependencies?.[AI_RUNTIME_PACKAGE] !== "string") { + const aiRuntimeDependency = packageJson.dependencies?.[AI_RUNTIME_PACKAGE]; + let hasAiRuntimeWorkspace = false; + try { + await fs.access(aiRuntimePackageJsonPath); + hasAiRuntimeWorkspace = true; + } catch (error) { + if (error?.code !== "ENOENT") { + throw error; + } + } + // Release checks can package refs from before the AI runtime was split into a workspace package. + if (!hasAiRuntimeWorkspace && aiRuntimeDependency === undefined) { + return async () => {}; + } + if (!hasAiRuntimeWorkspace) { + throw new Error("@openclaw/ai dependency requires the packages/ai workspace"); + } + if (typeof aiRuntimeDependency !== "string") { throw new Error("root package.json must declare @openclaw/ai as a dependency"); } diff --git a/test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts b/test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts index a2db1051c977..7cdbd59e4aee 100644 --- a/test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts +++ b/test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts @@ -234,6 +234,7 @@ describe("package-openclaw-for-docker", () => { )}\n`; const installedAiPath = path.join(sourceDir, "node_modules", "@openclaw", "ai"); fs.mkdirSync(path.join(sourceDir, "packages", "ai"), { recursive: true }); + fs.writeFileSync(path.join(sourceDir, "packages", "ai", "package.json"), "{}\n"); fs.mkdirSync(installedAiPath, { recursive: true }); fs.writeFileSync(path.join(installedAiPath, "original-marker"), "workspace package"); fs.writeFileSync(packageJsonPath, originalPackageJson); @@ -291,6 +292,67 @@ describe("package-openclaw-for-docker", () => { } }); + it("leaves pre-AI-workspace package sources unchanged", async () => { + const sourceDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-docker-legacy-source-")); + const outputDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-docker-legacy-output-")); + const packageJsonPath = path.join(sourceDir, "package.json"); + const originalPackageJson = `${JSON.stringify({ + dependencies: { "dep-a": "1.2.3" }, + name: "openclaw", + version: "2026.7.1", + })}\n`; + fs.writeFileSync(packageJsonPath, originalPackageJson); + const runCapture = vi.fn(); + + try { + const cleanup = await prepareBundledAiRuntimePackage(sourceDir, outputDir, runCapture); + + expect(runCapture).not.toHaveBeenCalled(); + expect(fs.readFileSync(packageJsonPath, "utf8")).toBe(originalPackageJson); + await cleanup(); + } finally { + fs.rmSync(sourceDir, { recursive: true, force: true }); + fs.rmSync(outputDir, { recursive: true, force: true }); + } + }); + + it("rejects incomplete AI workspace package sources", async () => { + const cases = [ + { + dependencies: { "@openclaw/ai": "workspace:*" }, + expected: "@openclaw/ai dependency requires the packages/ai workspace", + withWorkspace: false, + }, + { + dependencies: {}, + expected: "root package.json must declare @openclaw/ai as a dependency", + withWorkspace: true, + }, + ]; + + for (const testCase of cases) { + const sourceDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-docker-invalid-source-")); + const outputDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-docker-invalid-output-")); + fs.writeFileSync( + path.join(sourceDir, "package.json"), + `${JSON.stringify({ dependencies: testCase.dependencies, name: "openclaw" })}\n`, + ); + if (testCase.withWorkspace) { + fs.mkdirSync(path.join(sourceDir, "packages", "ai"), { recursive: true }); + fs.writeFileSync(path.join(sourceDir, "packages", "ai", "package.json"), "{}\n"); + } + + try { + await expect(prepareBundledAiRuntimePackage(sourceDir, outputDir, vi.fn())).rejects.toThrow( + testCase.expected, + ); + } finally { + fs.rmSync(sourceDir, { recursive: true, force: true }); + fs.rmSync(outputDir, { recursive: true, force: true }); + } + } + }); + it("trims and restores the changelog around ignore-scripts package artifacts", async () => { const calls: string[] = []; const tarball = await packOpenClawPackageForDocker("/repo", "/out", {