Skip to content

Commit ed500dd

Browse files
committed
fix(qa): use corepack for lab docker build fallback
1 parent bc754b3 commit ed500dd

2 files changed

Lines changed: 104 additions & 1 deletion

File tree

extensions/qa-lab/src/docker-up.runtime.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,77 @@ describe("runQaDockerUp", () => {
133133
}
134134
});
135135

136+
it("falls back to Corepack for the QA UI build when pnpm is unavailable", async () => {
137+
const calls: string[] = [];
138+
const outputDir = await mkdtemp(path.join(os.tmpdir(), "qa-docker-up-"));
139+
const repoRoot = path.resolve("/repo/openclaw");
140+
const composeFile = path.join(outputDir, "docker-compose.qa.yml");
141+
142+
try {
143+
await runQaDockerUp(
144+
{
145+
repoRoot,
146+
outputDir,
147+
usePrebuiltImage: true,
148+
},
149+
{
150+
async runCommand(command, args, cwd) {
151+
calls.push([command, ...args, `@${cwd}`].join(" "));
152+
if (command === "pnpm") {
153+
throw Object.assign(new Error("spawn pnpm ENOENT"), { code: "ENOENT" });
154+
}
155+
if (args.join(" ").includes("ps --format json openclaw-qa-gateway")) {
156+
return { stdout: '{"Health":"healthy","State":"running"}\n', stderr: "" };
157+
}
158+
return { stdout: "", stderr: "" };
159+
},
160+
fetchImpl: vi.fn(async () => ({ ok: true })),
161+
sleepImpl: vi.fn(async () => {}),
162+
},
163+
);
164+
165+
expect(calls).toEqual([
166+
`pnpm qa:lab:build @${repoRoot}`,
167+
`corepack pnpm qa:lab:build @${repoRoot}`,
168+
`docker compose -f ${composeFile} down --remove-orphans @${repoRoot}`,
169+
`docker compose -f ${composeFile} up -d @${repoRoot}`,
170+
`docker compose -f ${composeFile} ps --format json openclaw-qa-gateway @${repoRoot}`,
171+
]);
172+
} finally {
173+
await rm(outputDir, { recursive: true, force: true });
174+
}
175+
});
176+
177+
it("does not hide real QA UI build failures behind the Corepack fallback", async () => {
178+
const calls: string[] = [];
179+
const outputDir = await mkdtemp(path.join(os.tmpdir(), "qa-docker-up-"));
180+
const repoRoot = path.resolve("/repo/openclaw");
181+
182+
try {
183+
await expect(
184+
runQaDockerUp(
185+
{
186+
repoRoot,
187+
outputDir,
188+
usePrebuiltImage: true,
189+
},
190+
{
191+
async runCommand(command, args, cwd) {
192+
calls.push([command, ...args, `@${cwd}`].join(" "));
193+
throw Object.assign(new Error("qa lab build failed"), { code: 1 });
194+
},
195+
fetchImpl: vi.fn(async () => ({ ok: true })),
196+
sleepImpl: vi.fn(async () => {}),
197+
},
198+
),
199+
).rejects.toThrow("qa lab build failed");
200+
201+
expect(calls).toEqual([`pnpm qa:lab:build @${repoRoot}`]);
202+
} finally {
203+
await rm(outputDir, { recursive: true, force: true });
204+
}
205+
});
206+
136207
it("uses a repo-root-relative default output dir when none is provided", async () => {
137208
const calls: string[] = [];
138209
const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-docker-root-"));

extensions/qa-lab/src/docker-up.runtime.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Qa Lab plugin module implements docker up behavior.
22
import path from "node:path";
33
import { setTimeout as sleep } from "node:timers/promises";
4+
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
45
import { writeQaDockerHarnessFiles } from "./docker-harness.js";
56
import {
67
execCommand,
@@ -40,6 +41,37 @@ async function isQaLabDockerHealthReachable(url: string, fetchImpl: FetchLike) {
4041
}
4142
}
4243

44+
function isMissingCommandError(error: unknown, command: string, seen = new Set<unknown>()): boolean {
45+
if (!error || seen.has(error)) {
46+
return false;
47+
}
48+
seen.add(error);
49+
if (typeof error !== "object") {
50+
return formatErrorMessage(error).includes(`spawn ${command} ENOENT`);
51+
}
52+
const candidate = error as { cause?: unknown; code?: unknown; message?: unknown };
53+
const message = typeof candidate.message === "string" ? candidate.message : "";
54+
if (
55+
candidate.code === "ENOENT" ||
56+
message.includes(`spawn ${command} ENOENT`) ||
57+
message.includes(`${command}: command not found`)
58+
) {
59+
return true;
60+
}
61+
return isMissingCommandError(candidate.cause, command, seen);
62+
}
63+
64+
async function runQaLabBuild(repoRoot: string, runCommand: RunCommand) {
65+
try {
66+
await runCommand("pnpm", ["qa:lab:build"], repoRoot);
67+
} catch (error) {
68+
if (!isMissingCommandError(error, "pnpm")) {
69+
throw error;
70+
}
71+
await runCommand("corepack", ["pnpm", "qa:lab:build"], repoRoot);
72+
}
73+
}
74+
4375
export async function runQaDockerUp(
4476
params: {
4577
repoRoot?: string;
@@ -72,7 +104,7 @@ export async function runQaDockerUp(
72104
const sleepImpl = deps?.sleepImpl ?? sleep;
73105

74106
if (!params.skipUiBuild) {
75-
await runCommand("pnpm", ["qa:lab:build"], repoRoot);
107+
await runQaLabBuild(repoRoot, runCommand);
76108
}
77109

78110
await writeQaDockerHarnessFiles({

0 commit comments

Comments
 (0)