fix(scripts): clean Z.AI fallback repro temp state

This commit is contained in:
Vincent Koc
2026-06-01 16:25:05 +02:00
parent 645c7dc40b
commit 110f7d55e3
2 changed files with 161 additions and 79 deletions

View File

@@ -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<number> {
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() {

View File

@@ -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" });
});
});