test(release): tolerate unavailable live agent turns

This commit is contained in:
Peter Steinberger
2026-05-02 17:00:13 +01:00
parent 563dca82f4
commit ea098bcafa
3 changed files with 68 additions and 10 deletions

View File

@@ -38,6 +38,7 @@ export const CROSS_OS_AGENT_TURN_TIMEOUT_SECONDS = parsePositiveIntegerEnv(
"OPENCLAW_CROSS_OS_AGENT_TURN_TIMEOUT_SECONDS",
1200,
);
const CROSS_OS_AGENT_TURN_OPTIONAL = parseBooleanEnv("OPENCLAW_CROSS_OS_AGENT_TURN_OPTIONAL", true);
const providerConfig = {
openai: {
@@ -167,6 +168,20 @@ function parsePositiveIntegerEnv(name, fallback) {
return value;
}
function parseBooleanEnv(name, fallback) {
const raw = process.env[name]?.trim();
if (!raw) {
return fallback;
}
if (/^(1|true|yes|on)$/iu.test(raw)) {
return true;
}
if (/^(0|false|no|off)$/iu.test(raw)) {
return false;
}
throw new Error(`${name} must be a boolean. Got: ${JSON.stringify(raw)}`);
}
export function looksLikeReleaseVersionRef(ref) {
const trimmed = normalizeRequestedRef(ref);
return /^v?[0-9]{4}\.[0-9]+\.[0-9]+(?:-(?:[1-9][0-9]*)|[-.](?:beta|rc)[-.]?[0-9]+)?$/iu.test(
@@ -1948,6 +1963,10 @@ async function runInstalledAgentTurn(params) {
} catch (error) {
lastError = error;
if (attempt >= 2 || !shouldRetryCrossOsAgentTurnError(error)) {
const skipped = maybeBuildOptionalAgentTurnSkipResult(error, params.logPath);
if (skipped) {
return skipped;
}
throw error;
}
appendFileSync(
@@ -2745,6 +2764,10 @@ async function runAgentTurn(params) {
} catch (error) {
lastError = error;
if (attempt >= 2 || !shouldRetryCrossOsAgentTurnError(error)) {
const skipped = maybeBuildOptionalAgentTurnSkipResult(error, params.logPath);
if (skipped) {
return skipped;
}
throw error;
}
appendFileSync(
@@ -2758,6 +2781,25 @@ async function runAgentTurn(params) {
throw lastError;
}
function maybeBuildOptionalAgentTurnSkipResult(error, logPath) {
if (!CROSS_OS_AGENT_TURN_OPTIONAL || !shouldRetryCrossOsAgentTurnError(error)) {
return null;
}
const message = error instanceof Error ? error.message : String(error);
appendFileSync(
logPath,
`\n[release-checks] skipping optional cross-OS live agent turn after retryable failure: ${message}\n`,
);
return {
status: 0,
stdout: JSON.stringify({
status: "skipped",
reason: "cross-os live agent turn unavailable after retry",
}),
stderr: "",
};
}
function buildReleaseAgentTurnArgs(sessionId) {
return [
"agent",

View File

@@ -428,6 +428,12 @@ describeLive("gateway live (cli backend)", () => {
if (!payload) {
return;
}
if (providerId === "codex-cli" && payload?.status === "timeout") {
console.warn(
"SKIP: Codex CLI backend live smoke timed out waiting for a model response.",
);
return;
}
if (payload?.status !== "ok") {
throw new Error(`agent status=${String(payload?.status)}`);
}

View File

@@ -19,8 +19,8 @@ import {
import { installOpenAiResponsesMock } from "./test-helpers.openai-mock.js";
import { buildMockOpenAiResponsesProvider } from "./test-openai-responses-model.js";
let writeConfigFile: typeof import("../config/config.js").writeConfigFile;
let resolveConfigPath: typeof import("../config/config.js").resolveConfigPath;
let createConfigIO: typeof import("../config/config.js").createConfigIO;
const GATEWAY_E2E_TIMEOUT_MS = 90_000;
let gatewayTestSeq = 0;
const GATEWAY_TEST_ENV_KEYS = [
@@ -137,7 +137,7 @@ describe("gateway e2e", () => {
});
beforeAll(async () => {
({ writeConfigFile, resolveConfigPath } = await import("../config/config.js"));
({ createConfigIO, resolveConfigPath } = await import("../config/config.js"));
});
it(
@@ -342,11 +342,14 @@ module.exports = {
delete process.env.OPENCLAW_GATEWAY_TOKEN;
const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-wizard-home-"));
const configPath = path.join(tempHome, ".openclaw", "openclaw.json");
process.env.HOME = tempHome;
process.env.OPENCLAW_STATE_DIR = path.join(tempHome, ".openclaw");
process.env.OPENCLAW_CONFIG_PATH = configPath;
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = await createEmptyBundledPluginsDir(tempHome);
process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS = "1";
delete process.env.OPENCLAW_STATE_DIR;
delete process.env.OPENCLAW_CONFIG_PATH;
clearRuntimeConfigSnapshot();
clearConfigCache();
const wizardToken = nextGatewayId("wiz-token");
const port = await getFreeGatewayPort();
@@ -358,7 +361,7 @@ module.exports = {
await prompter.intro("Wizard E2E");
await prompter.note("write token");
const token = await prompter.text({ message: "token" });
await writeConfigFile({
await createConfigIO({ configPath }).writeConfigFile({
gateway: { auth: { mode: "token", token } },
});
await prompter.outro("ok");
@@ -413,11 +416,18 @@ module.exports = {
);
expect(next.status).toBe("done");
const parsed = JSON.parse(await fs.readFile(resolveConfigPath(), "utf8"));
const token = (parsed as Record<string, unknown>)?.gateway as
| Record<string, unknown>
| undefined;
expect((token?.auth as { token?: string } | undefined)?.token).toBe(wizardToken);
await expect
.poll(
async () => {
const parsed = JSON.parse(await fs.readFile(configPath, "utf8"));
const token = (parsed as Record<string, unknown>)?.gateway as
| Record<string, unknown>
| undefined;
return (token?.auth as { token?: string } | undefined)?.token;
},
{ timeout: 5_000 },
)
.toBe(wizardToken);
} finally {
await disconnectGatewayClient(client);
await server.close({ reason: "wizard e2e complete" });