From 110f7d55e3beb1b2324596038ca9fc98bfedc705 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Mon, 1 Jun 2026 16:25:05 +0200 Subject: [PATCH] fix(scripts): clean Z.AI fallback repro temp state --- scripts/zai-fallback-repro.ts | 193 ++++++++++++++---------- test/scripts/zai-fallback-repro.test.ts | 47 ++++++ 2 files changed, 161 insertions(+), 79 deletions(-) diff --git a/scripts/zai-fallback-repro.ts b/scripts/zai-fallback-repro.ts index a4f5b1c6f0b..d68246a6169 100644 --- a/scripts/zai-fallback-repro.ts +++ b/scripts/zai-fallback-repro.ts @@ -35,6 +35,22 @@ type ResolvePnpmCommandOptions = { }; const COMMAND_OUTPUT_MAX_CHARS = 512 * 1024; +type ReproLog = (message: string) => void; +type RunCommand = typeof runCommand; + +type RunZaiFallbackReproDeps = { + env?: NodeJS.ProcessEnv; + error?: ReproLog; + log?: ReproLog; + mkdtemp?: typeof fs.mkdtemp; + mkdir?: typeof fs.mkdir; + randomUUID?: typeof randomUUID; + readFile?: typeof fs.readFile; + rm?: typeof fs.rm; + runCommand?: RunCommand; + warn?: ReproLog; + writeFile?: typeof fs.writeFile; +}; function resolveEnvValue(env: NodeJS.ProcessEnv, name: string): string | undefined { const key = Object.keys(env).find((candidate) => candidate.toLowerCase() === name.toLowerCase()); @@ -81,20 +97,20 @@ export function resolveZaiFallbackPnpmCommand( return command; } -function pickAnthropicEnv(): { type: "oauth" | "api"; value: string } | null { - const oauth = process.env.ANTHROPIC_OAUTH_TOKEN?.trim(); +function pickAnthropicEnv(env: NodeJS.ProcessEnv): { type: "oauth" | "api"; value: string } | null { + const oauth = env.ANTHROPIC_OAUTH_TOKEN?.trim(); if (oauth) { return { type: "oauth", value: oauth }; } - const api = process.env.ANTHROPIC_API_KEY?.trim(); + const api = env.ANTHROPIC_API_KEY?.trim(); if (api) { return { type: "api", value: api }; } return null; } -function pickZaiKey(): string | null { - return process.env.ZAI_API_KEY?.trim() ?? process.env.Z_AI_API_KEY?.trim() ?? null; +function pickZaiKey(env: NodeJS.ProcessEnv): string | null { + return env.ZAI_API_KEY?.trim() ?? env.Z_AI_API_KEY?.trim() ?? null; } async function runCommand( @@ -143,97 +159,116 @@ async function runCommand( }); } -async function main() { - const anthropic = pickAnthropicEnv(); - const zaiKey = pickZaiKey(); +export async function runZaiFallbackRepro(deps: RunZaiFallbackReproDeps = {}): Promise { + const env = deps.env ?? process.env; + const log = deps.log ?? console.log; + const warn = deps.warn ?? console.warn; + const error = deps.error ?? console.error; + const mkdtemp = deps.mkdtemp ?? fs.mkdtemp; + const mkdir = deps.mkdir ?? fs.mkdir; + const readFile = deps.readFile ?? fs.readFile; + const rm = deps.rm ?? fs.rm; + const writeFile = deps.writeFile ?? fs.writeFile; + const run = deps.runCommand ?? runCommand; + const createUuid = deps.randomUUID ?? randomUUID; + const anthropic = pickAnthropicEnv(env); + const zaiKey = pickZaiKey(env); if (!anthropic) { - console.error("Missing ANTHROPIC_OAUTH_TOKEN or ANTHROPIC_API_KEY."); - process.exit(1); + error("Missing ANTHROPIC_OAUTH_TOKEN or ANTHROPIC_API_KEY."); + return 1; } if (!zaiKey) { - console.error("Missing ZAI_API_KEY or Z_AI_API_KEY."); - process.exit(1); + error("Missing ZAI_API_KEY or Z_AI_API_KEY."); + return 1; } - const baseDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-zai-fallback-")); + const baseDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-zai-fallback-")); const stateDir = path.join(baseDir, "state"); const configPath = path.join(baseDir, "openclaw.json"); - await fs.mkdir(stateDir, { recursive: true }); + try { + await mkdir(stateDir, { recursive: true }); - const config = { - agents: { - defaults: { - model: { - primary: "anthropic/claude-opus-4-6", - fallbacks: ["zai/glm-4.7"], - }, - models: { - "anthropic/claude-opus-4-6": {}, - "anthropic/claude-opus-4-5": {}, - "zai/glm-4.7": {}, + const config = { + agents: { + defaults: { + model: { + primary: "anthropic/claude-opus-4-6", + fallbacks: ["zai/glm-4.7"], + }, + models: { + "anthropic/claude-opus-4-6": {}, + "anthropic/claude-opus-4-5": {}, + "zai/glm-4.7": {}, + }, }, }, - }, - }; - await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf8"); + }; + await writeFile(configPath, JSON.stringify(config, null, 2), "utf8"); - const sessionId = process.env.OPENCLAW_ZAI_FALLBACK_SESSION_ID ?? randomUUID(); + const sessionId = env.OPENCLAW_ZAI_FALLBACK_SESSION_ID ?? createUuid(); - const baseEnv: NodeJS.ProcessEnv = { - ...process.env, - OPENCLAW_CONFIG_PATH: configPath, - OPENCLAW_STATE_DIR: stateDir, - ZAI_API_KEY: zaiKey, - Z_AI_API_KEY: "", - }; + const baseEnv: NodeJS.ProcessEnv = { + ...env, + OPENCLAW_CONFIG_PATH: configPath, + OPENCLAW_STATE_DIR: stateDir, + ZAI_API_KEY: zaiKey, + Z_AI_API_KEY: "", + }; - const envValidAnthropic: NodeJS.ProcessEnv = { - ...baseEnv, - ANTHROPIC_OAUTH_TOKEN: anthropic.type === "oauth" ? anthropic.value : "", - ANTHROPIC_API_KEY: anthropic.type === "api" ? anthropic.value : "", - }; + const envValidAnthropic: NodeJS.ProcessEnv = { + ...baseEnv, + ANTHROPIC_OAUTH_TOKEN: anthropic.type === "oauth" ? anthropic.value : "", + ANTHROPIC_API_KEY: anthropic.type === "api" ? anthropic.value : "", + }; - const envInvalidAnthropic: NodeJS.ProcessEnv = { - ...baseEnv, - ANTHROPIC_OAUTH_TOKEN: anthropic.type === "oauth" ? "invalid" : "", - ANTHROPIC_API_KEY: anthropic.type === "api" ? "invalid" : "", - }; + const envInvalidAnthropic: NodeJS.ProcessEnv = { + ...baseEnv, + ANTHROPIC_OAUTH_TOKEN: anthropic.type === "oauth" ? "invalid" : "", + ANTHROPIC_API_KEY: anthropic.type === "api" ? "invalid" : "", + }; - console.log("== Run 1: create tool history (primary only)"); - const toolPrompt = - "Use the exec tool to create a file named zai-fallback-tool.txt with the content tool-ok. " + - "Then use the read tool to display the file contents. Reply with just the file contents."; - const run1 = await runCommand( - "run1", - ["openclaw", "agent", "--local", "--session-id", sessionId, "--message", toolPrompt], - envValidAnthropic, - ); - if (run1.code !== 0) { - process.exit(run1.code ?? 1); + log("== Run 1: create tool history (primary only)"); + const toolPrompt = + "Use the exec tool to create a file named zai-fallback-tool.txt with the content tool-ok. " + + "Then use the read tool to display the file contents. Reply with just the file contents."; + const run1 = await run( + "run1", + ["openclaw", "agent", "--local", "--session-id", sessionId, "--message", toolPrompt], + envValidAnthropic, + ); + if (run1.code !== 0) { + return run1.code ?? 1; + } + + const sessionFile = path.join(stateDir, "agents", "main", "sessions", `${sessionId}.jsonl`); + const transcript = await readFile(sessionFile, "utf8").catch(() => ""); + if (!transcript.includes('"toolResult"')) { + warn("Warning: no toolResult entries detected in session history."); + } + + log("== Run 2: force auth failover to Z.AI"); + const followupPrompt = + "What is the content of zai-fallback-tool.txt? Reply with just the contents."; + const run2 = await run( + "run2", + ["openclaw", "agent", "--local", "--session-id", sessionId, "--message", followupPrompt], + envInvalidAnthropic, + ); + + if (run2.code === 0) { + log("PASS: fallback succeeded."); + return 0; + } + + error("FAIL: fallback failed."); + return run2.code ?? 1; + } finally { + await rm(baseDir, { force: true, recursive: true }); } +} - const sessionFile = path.join(stateDir, "agents", "main", "sessions", `${sessionId}.jsonl`); - const transcript = await fs.readFile(sessionFile, "utf8").catch(() => ""); - if (!transcript.includes('"toolResult"')) { - console.warn("Warning: no toolResult entries detected in session history."); - } - - console.log("== Run 2: force auth failover to Z.AI"); - const followupPrompt = - "What is the content of zai-fallback-tool.txt? Reply with just the contents."; - const run2 = await runCommand( - "run2", - ["openclaw", "agent", "--local", "--session-id", sessionId, "--message", followupPrompt], - envInvalidAnthropic, - ); - - if (run2.code === 0) { - console.log("PASS: fallback succeeded."); - process.exit(0); - } - - console.error("FAIL: fallback failed."); - process.exit(run2.code ?? 1); +async function main() { + process.exitCode = await runZaiFallbackRepro(); } function isCliEntrypoint() { diff --git a/test/scripts/zai-fallback-repro.test.ts b/test/scripts/zai-fallback-repro.test.ts index 05c7234d0c2..063e9bc07a4 100644 --- a/test/scripts/zai-fallback-repro.test.ts +++ b/test/scripts/zai-fallback-repro.test.ts @@ -1,6 +1,10 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; import { describe, expect, it } from "vitest"; import { appendBoundedReproOutput, + runZaiFallbackRepro, resolveZaiFallbackPnpmCommand, } from "../../scripts/zai-fallback-repro.ts"; @@ -32,4 +36,47 @@ describe("zai fallback repro command resolution", () => { expect(first).toEqual({ text: "bcdef", truncatedChars: 1 }); expect(second).toEqual({ text: "fghij", truncatedChars: 5 }); }); + + it("cleans temporary repro state after fallback proof", async () => { + const tempRoots: string[] = []; + const calls: string[] = []; + + const exitCode = await runZaiFallbackRepro({ + env: { + ANTHROPIC_API_KEY: "anthropic-test-key", + OPENCLAW_ZAI_FALLBACK_SESSION_ID: "session-test", + PATH: process.env.PATH, + ZAI_API_KEY: "zai-test-key", + }, + error: () => {}, + log: () => {}, + mkdtemp: async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-zai-fallback-test-")); + tempRoots.push(root); + return root; + }, + randomUUID: () => "uuid-test", + runCommand: async (label, _args, env) => { + calls.push(label); + if (label === "run1") { + const sessionFile = path.join( + String(env.OPENCLAW_STATE_DIR), + "agents", + "main", + "sessions", + "session-test.jsonl", + ); + await fs.mkdir(path.dirname(sessionFile), { recursive: true }); + await fs.writeFile(sessionFile, '{"toolResult":true}\n', "utf8"); + } + return { code: 0, signal: null, stderr: "", stdout: "" }; + }, + warn: () => {}, + }); + + expect(exitCode).toBe(0); + expect(calls).toEqual(["run1", "run2"]); + expect(tempRoots).toHaveLength(1); + await expect(fs.stat(tempRoots[0])).rejects.toMatchObject({ code: "ENOENT" }); + }); });