diff --git a/extensions/qa-lab/src/docker-up.runtime.test.ts b/extensions/qa-lab/src/docker-up.runtime.test.ts index 78f765fd4efd..3b8bd3e1f3a2 100644 --- a/extensions/qa-lab/src/docker-up.runtime.test.ts +++ b/extensions/qa-lab/src/docker-up.runtime.test.ts @@ -21,6 +21,10 @@ function createHealthyDockerDeps(calls: string[]): QaDockerUpDeps { }; } +function quoteForShell(value: string) { + return `'${value.replaceAll("'", `'"'"'`)}'`; +} + describe("runQaDockerUp", () => { it("builds the QA UI, writes the harness, starts compose, and waits for health", async () => { const calls: string[] = []; @@ -68,12 +72,39 @@ describe("runQaDockerUp", () => { expect(result.qaLabUrl).toBe("http://127.0.0.1:43124"); expect(result.gatewayUrl).toBe("http://127.0.0.1:18889/"); expect(result.composeFile).toBe(composeFile); - expect(result.stopCommand).toBe(`docker compose -f ${composeFile} down`); + expect(result.stopCommand).toBe(`docker compose -f ${quoteForShell(composeFile)} down`); } finally { await rm(outputDir, { recursive: true, force: true }); } }); + it("quotes the printed stop command when the compose path is shell-sensitive", async () => { + const calls: string[] = []; + const tempRoot = await mkdtemp(path.join(os.tmpdir(), "qa-docker-up-")); + const outputDir = path.join(tempRoot, "mac path's qa lab"); + const repoRoot = path.resolve("/repo/openclaw"); + const composeFile = path.join(outputDir, "docker-compose.qa.yml"); + + try { + const result = await runQaDockerUp( + { + repoRoot, + outputDir, + usePrebuiltImage: true, + skipUiBuild: true, + }, + createHealthyDockerDeps(calls), + ); + + expect(result.stopCommand).toBe(`docker compose -f ${quoteForShell(composeFile)} down`); + expect(calls).toContain( + `docker compose -f ${composeFile} down --remove-orphans @${repoRoot}`, + ); + } finally { + await rm(tempRoot, { recursive: true, force: true }); + } + }); + it("skips UI build and compose --build for prebuilt images", async () => { const calls: string[] = []; const outputDir = await mkdtemp(path.join(os.tmpdir(), "qa-docker-up-")); diff --git a/extensions/qa-lab/src/docker-up.runtime.ts b/extensions/qa-lab/src/docker-up.runtime.ts index 6c03a2d16e5d..19c37682bfa6 100644 --- a/extensions/qa-lab/src/docker-up.runtime.ts +++ b/extensions/qa-lab/src/docker-up.runtime.ts @@ -25,6 +25,10 @@ function resolveDefaultQaDockerDir(repoRoot: string) { return path.resolve(repoRoot, ".artifacts/qa-docker"); } +function quoteForShell(value: string) { + return `'${value.replaceAll("'", `'"'"'`)}'`; +} + async function isQaLabDockerHealthReachable(url: string, fetchImpl: FetchLike) { let response: Awaited> | undefined; try { @@ -147,6 +151,6 @@ export async function runQaDockerUp( composeFile, qaLabUrl, gatewayUrl, - stopCommand: `docker compose -f ${composeFile} down`, + stopCommand: `docker compose -f ${quoteForShell(composeFile)} down`, }; }