diff --git a/extensions/qa-lab/src/scenario-catalog.test.ts b/extensions/qa-lab/src/scenario-catalog.test.ts index 0226bf295da6..07d416d79baf 100644 --- a/extensions/qa-lab/src/scenario-catalog.test.ts +++ b/extensions/qa-lab/src/scenario-catalog.test.ts @@ -455,7 +455,9 @@ describe("qa scenario catalog", () => { expect(interruptedStatusAssertion).toBeDefined(); const interruptedStatusContract = JSON.stringify(interruptedStatusAssertion); expect(interruptedStatusContract).toContain("waited.stopReason === 'restart'"); - expect(interruptedStatusContract).toContain("waited.stopReason === 'aborted'"); + expect(interruptedStatusContract).toContain( + "env.gateway.runtimeEnv.OPENCLAW_QA_FORCE_RUNTIME === 'codex' && waited.stopReason === 'aborted'", + ); expect(interruptedStatusContract).toContain("EmbeddedAttemptSessionTakeoverError"); expect(interruptedStatusContract).toContain("AbortError"); expect(interruptedStatusContract).toContain("This operation was aborted"); diff --git a/qa/scenarios/runtime/gateway-restart-inflight-run.yaml b/qa/scenarios/runtime/gateway-restart-inflight-run.yaml index 3455b0e9c470..83fbeabdaeb3 100644 --- a/qa/scenarios/runtime/gateway-restart-inflight-run.yaml +++ b/qa/scenarios/runtime/gateway-restart-inflight-run.yaml @@ -96,7 +96,7 @@ flow: - expr: started.runId - expr: liveTurnTimeoutMs(env, 180000) - assert: - expr: "waited.status === 'ok' || waited.status === 'timeout' || (waited.status === 'error' && (waited.stopReason === 'restart' || waited.stopReason === 'aborted' || String(waited.error ?? '').includes('EmbeddedAttemptSessionTakeoverError') || String(waited.error ?? '').includes('AbortError') || String(waited.error ?? '').includes('This operation was aborted')))" + expr: "waited.status === 'ok' || waited.status === 'timeout' || (waited.status === 'error' && (waited.stopReason === 'restart' || (env.gateway.runtimeEnv.OPENCLAW_QA_FORCE_RUNTIME === 'codex' && waited.stopReason === 'aborted') || String(waited.error ?? '').includes('EmbeddedAttemptSessionTakeoverError') || String(waited.error ?? '').includes('AbortError') || String(waited.error ?? '').includes('This operation was aborted')))" message: expr: "`interrupted agent run ended with unexpected status: ${JSON.stringify(waited)}`" - set: interruptedMatches diff --git a/scripts/docker/install-sh-smoke/run.sh b/scripts/docker/install-sh-smoke/run.sh index ad7d038f3993..fa9491811152 100755 --- a/scripts/docker/install-sh-smoke/run.sh +++ b/scripts/docker/install-sh-smoke/run.sh @@ -481,6 +481,18 @@ if (Number(updateStep.exitCode ?? 1) !== 0) { if (typeof updateStep.command !== "string" || !updateStep.command.includes(expectedUrl)) { throw new Error(`global update step missing expected tgz URL: ${JSON.stringify(updateStep)}`); } +const doctorStep = steps.find((step) => step?.name === "openclaw doctor"); +if (!doctorStep) { + throw new Error("missing openclaw doctor step in update JSON"); +} +// Exit 86 is the updater's explicit recoverable post-install doctor contract. +const doctorSucceeded = doctorStep.exitCode === 0; +const doctorWasAdvisory = + doctorStep.exitCode === 86 && + doctorStep.advisory?.kind === "package-post-install-doctor"; +if (!doctorSucceeded && !doctorWasAdvisory) { + throw new Error(`openclaw doctor step failed: ${JSON.stringify(doctorStep)}`); +} NODE echo "==> Verify updated version" diff --git a/scripts/e2e/lib/upgrade-survivor/run.sh b/scripts/e2e/lib/upgrade-survivor/run.sh index e7e5954025ab..000a38e072f9 100644 --- a/scripts/e2e/lib/upgrade-survivor/run.sh +++ b/scripts/e2e/lib/upgrade-survivor/run.sh @@ -425,7 +425,8 @@ fs.writeFileSync( ); NODE tar -czf "$tarball" -C "$fixture_root" package - node scripts/e2e/lib/plugins/npm-registry-server.mjs \ + OPENCLAW_NPM_REGISTRY_UPSTREAM=https://registry.npmjs.org \ + node scripts/e2e/lib/plugins/npm-registry-server.mjs \ "$port_file" \ "@openclaw/brave-plugin" \ "2026.5.2" \ diff --git a/scripts/e2e/upgrade-survivor-docker.sh b/scripts/e2e/upgrade-survivor-docker.sh index 7bd57f5c980b..4b0d11ee1bce 100755 --- a/scripts/e2e/upgrade-survivor-docker.sh +++ b/scripts/e2e/upgrade-survivor-docker.sh @@ -314,7 +314,8 @@ fs.writeFileSync( ); NODE tar -czf "$tarball" -C "$fixture_root" package - node scripts/e2e/lib/plugins/npm-registry-server.mjs \ + OPENCLAW_NPM_REGISTRY_UPSTREAM=https://registry.npmjs.org \ + node scripts/e2e/lib/plugins/npm-registry-server.mjs \ "$port_file" \ "@openclaw/brave-plugin" \ "2026.5.2" \ diff --git a/test/scripts/docker-build-helper.test.ts b/test/scripts/docker-build-helper.test.ts index 474d90b7e54c..3110d01c011a 100644 --- a/test/scripts/docker-build-helper.test.ts +++ b/test/scripts/docker-build-helper.test.ts @@ -2062,6 +2062,16 @@ grep -qx -- "OPENCLAW_E2E_COMMAND_TIMEOUT=23s" "$TMPDIR/package-args" } }); + it("lets upgrade survivor fixture registries resolve transitive public packages", () => { + const runner = readFileSync(UPGRADE_SURVIVOR_DOCKER_E2E_PATH, "utf8"); + const publishedRunner = readFileSync(UPGRADE_SURVIVOR_RUN_SCRIPT, "utf8"); + + for (const script of [runner, publishedRunner]) { + expect(script).toContain("OPENCLAW_NPM_REGISTRY_UPSTREAM=https://registry.npmjs.org"); + expect(script).toContain("node scripts/e2e/lib/plugins/npm-registry-server.mjs"); + } + }); + it("wraps package-backed scenario OpenClaw CLI calls with the shared timeout helper", () => { const paths = [ CODEX_ON_DEMAND_DOCKER_E2E_PATH, diff --git a/test/scripts/test-install-sh-docker.test.ts b/test/scripts/test-install-sh-docker.test.ts index 5c5790f1c5fb..d8bec1928f09 100644 --- a/test/scripts/test-install-sh-docker.test.ts +++ b/test/scripts/test-install-sh-docker.test.ts @@ -149,6 +149,45 @@ function normalizeInstallE2eAgentOutput(output: string) { } } +function extractInstallSmokeUpdateJsonParser(): string { + const script = readFileSync(SMOKE_RUNNER_PATH, "utf8"); + const match = script.match( + /UPDATE_JSON="\$UPDATE_JSON" \\\n[\s\S]*?node - <<'NODE'\n([\s\S]*?)\nNODE\n\n echo "==> Verify updated version"/u, + ); + if (!match) { + throw new Error("install smoke update JSON parser was not found"); + } + return match[1]; +} + +function validateInstallSmokeUpdateJson(doctorStep?: Record) { + const updateUrl = "http://candidate.invalid/openclaw.tgz"; + const payload = { + status: "ok", + before: { version: "2026.7.0" }, + after: { version: "2026.7.1" }, + steps: [ + { + name: "global update", + exitCode: 0, + command: `npm install ${updateUrl}`, + }, + ...(doctorStep ? [doctorStep] : []), + ], + }; + return spawnSync(process.execPath, ["-"], { + encoding: "utf8", + input: extractInstallSmokeUpdateJsonParser(), + env: { + ...process.env, + UPDATE_JSON: JSON.stringify(payload), + UPDATE_EXPECT_VERSION: payload.after.version, + UPDATE_BASELINE_VERSION: payload.before.version, + UPDATE_TAG_URL: updateUrl, + }, + }); +} + function expectInstallDockerfileContract( dockerfilePath: string, runnerPath: string, @@ -1014,6 +1053,47 @@ describe("install-sh smoke runner", () => { expect(script).toContain("openclaw infer image providers --json"); }); + it.each([ + ["successful", { name: "openclaw doctor", exitCode: 0 }], + [ + "recoverable advisory", + { + name: "openclaw doctor", + exitCode: 86, + advisory: { kind: "package-post-install-doctor", message: "repair deferred" }, + }, + ], + ])("accepts a %s package post-install doctor result", (_label, doctorStep) => { + const result = validateInstallSmokeUpdateJson(doctorStep); + + expect(result.status, result.stderr).toBe(0); + }); + + it.each([ + ["missing", undefined, "missing openclaw doctor step"], + ["fatal", { name: "openclaw doctor", exitCode: 1 }, "openclaw doctor step failed"], + ["untyped advisory", { name: "openclaw doctor", exitCode: 86 }, "openclaw doctor step failed"], + [ + "wrong advisory kind", + { name: "openclaw doctor", exitCode: 86, advisory: { kind: "other" } }, + "openclaw doctor step failed", + ], + [ + "wrong advisory exit", + { + name: "openclaw doctor", + exitCode: 1, + advisory: { kind: "package-post-install-doctor" }, + }, + "openclaw doctor step failed", + ], + ])("rejects a %s package post-install doctor result", (_label, doctorStep, error) => { + const result = validateInstallSmokeUpdateJson(doctorStep); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain(error); + }); + it.each([ ["command timeout", "OPENCLAW_INSTALL_SMOKE_COMMAND_TIMEOUT", "900s"], ["heartbeat interval", "OPENCLAW_INSTALL_SMOKE_HEARTBEAT_INTERVAL", "60s"],