Download src/cli/agent-cli-recovery.process.test.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 8.79 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/cli/agent-cli-recovery.process.test.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/cli/agent-cli-recovery.process.test.ts
-
curl -L -o agent-cli-recovery.process.test.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/cli/agent-cli-recovery.process.test.ts
8.79 kB
| import fs from "node:fs/promises"; | |
| import path from "node:path"; | |
| import { afterEach, describe, expect, it } from "vitest"; | |
| import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js"; | |
| import { setCliSessionBinding } from "../agents/cli-session.js"; | |
| import { | |
| loadSessionEntryReadOnly, | |
| replaceSessionEntry, | |
| } from "../config/sessions/session-accessor.js"; | |
| import type { InternalSessionEntry } from "../config/sessions/types.js"; | |
| import { resolveRuntimeWorkerArgv, resolveRuntimeWorkerUrl } from "../infra/runtime-worker-url.js"; | |
| import { closeOpenClawAgentDatabasesForTest } from "../state/openclaw-agent-db.js"; | |
| import { cliRecoveryEntrypoints } from "./cli-entrypoint.test-support.js"; | |
| import { runCliProcessChild } from "./cli-process-child.test-helpers.js"; | |
| const tempDirs = useAutoCleanupTempDirTracker(afterEach); | |
| afterEach(() => { | |
| closeOpenClawAgentDatabasesForTest(); | |
| }); | |
| describe("CLI fork recovery process", () => { | |
| it("keeps a concurrent durable rebind when a stale fork reports its successor", async () => { | |
| const root = tempDirs.make("openclaw-cli-fork-recovery-"); | |
| const stateDir = path.join(root, "state"); | |
| const tmpDir = path.join(root, "tmp"); | |
| const workspaceDir = path.join(root, "workspace"); | |
| const pluginDir = path.join(root, "plugin"); | |
| const storePath = path.join(stateDir, "agents/main/sessions/sessions.json"); | |
| const sessionKey = "agent:main:cli-fork-process"; | |
| const sourceCliSessionId = "source-cli-session"; | |
| const newerCliSessionId = "newer-cli-session"; | |
| const successorCliSessionId = "stale-successor-session"; | |
| const checkpointId = "source-checkpoint"; | |
| const backendScript = path.join(root, "backend.mjs"); | |
| const rebindScript = path.join(root, "rebind.mjs"); | |
| const spawnLog = path.join(root, "spawn.jsonl"); | |
| await Promise.all([ | |
| fs.mkdir(tmpDir, { recursive: true }), | |
| fs.mkdir(workspaceDir, { recursive: true }), | |
| fs.mkdir(pluginDir, { recursive: true }), | |
| ]); | |
| await writeProofPlugin({ | |
| pluginDir, | |
| backendScript, | |
| rebindScript, | |
| }); | |
| const configPath = path.join(root, "openclaw.json"); | |
| await fs.writeFile( | |
| configPath, | |
| JSON.stringify({ | |
| session: { store: path.join(stateDir, "agents/{agentId}/sessions/sessions.json") }, | |
| plugins: { | |
| allow: ["cli-fork-process-proof"], | |
| load: { paths: [pluginDir] }, | |
| entries: { "cli-fork-process-proof": { enabled: true } }, | |
| }, | |
| agents: { | |
| defaults: { | |
| workspace: workspaceDir, | |
| model: { primary: "proof-cli/proof-model" }, | |
| models: { "proof-cli/proof-model": { agentRuntime: { id: "proof-cli" } } }, | |
| }, | |
| }, | |
| }), | |
| ); | |
| const entry: InternalSessionEntry = { | |
| sessionId: "openclaw-process-session", | |
| lifecycleRevision: "process-lifecycle", | |
| activeWriterRunId: "process-writer", | |
| updatedAt: 1, | |
| }; | |
| setCliSessionBinding(entry, "proof-cli", { | |
| sessionId: sourceCliSessionId, | |
| forceReuse: true, | |
| forkNextResume: true, | |
| resumeCheckpointId: checkpointId, | |
| }); | |
| await replaceSessionEntry({ sessionKey, storePath }, entry); | |
| closeOpenClawAgentDatabasesForTest(); | |
| const result = await runCliProcessChild({ | |
| nodeArgs: [ | |
| ...resolveRuntimeWorkerArgv(resolveRuntimeWorkerUrl(cliRecoveryEntrypoints.cli)), | |
| "agent", | |
| "--local", | |
| "--session-key", | |
| sessionKey, | |
| "--model", | |
| "proof-cli/proof-model", | |
| "--message", | |
| "prove stale fork ownership", | |
| "--json", | |
| ], | |
| env: { | |
| ...process.env, | |
| HOME: root, | |
| USERPROFILE: root, | |
| TMPDIR: tmpDir, | |
| NODE_DISABLE_COMPILE_CACHE: "1", | |
| NODE_ENV: undefined, | |
| NODE_OPTIONS: undefined, | |
| OPENCLAW_CONFIG_PATH: configPath, | |
| OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1", | |
| OPENCLAW_HOME: root, | |
| OPENCLAW_NO_RESPAWN: "1", | |
| OPENCLAW_STATE_DIR: stateDir, | |
| PR135168_BACKEND_SCRIPT: backendScript, | |
| PR135168_NEWER_CLI_SESSION_ID: newerCliSessionId, | |
| PR135168_REBIND_SCRIPT: rebindScript, | |
| PR135168_SESSION_KEY: sessionKey, | |
| PR135168_SPAWN_LOG: spawnLog, | |
| PR135168_STORE_PATH: storePath, | |
| PR135168_SUCCESSOR_CLI_SESSION_ID: successorCliSessionId, | |
| VITEST: undefined, | |
| }, | |
| }); | |
| expect(result, JSON.stringify(result)).toMatchObject({ code: 1, signal: null }); | |
| expect(result.stderr).toContain( | |
| "CLI turn failed and its fork successor could not be persisted", | |
| ); | |
| closeOpenClawAgentDatabasesForTest(); | |
| expect( | |
| loadSessionEntryReadOnly({ sessionKey, storePath })?.cliSessionBindings?.["proof-cli"] | |
| ?.sessionId, | |
| ).toBe(newerCliSessionId); | |
| const spawn = JSON.parse((await fs.readFile(spawnLog, "utf8")).trim()) as { | |
| argv: string[]; | |
| }; | |
| expect(spawn.argv).toEqual([ | |
| "--resume", | |
| sourceCliSessionId, | |
| "--fork-session", | |
| "--resume-session-at", | |
| checkpointId, | |
| ]); | |
| }); | |
| }); | |
| async function writeProofPlugin(params: { | |
| pluginDir: string; | |
| backendScript: string; | |
| rebindScript: string; | |
| }): Promise<void> { | |
| await Promise.all([ | |
| fs.writeFile( | |
| path.join(params.pluginDir, "package.json"), | |
| JSON.stringify({ | |
| name: "cli-fork-process-proof", | |
| private: true, | |
| type: "module", | |
| openclaw: { extensions: ["./index.js"] }, | |
| }), | |
| ), | |
| fs.writeFile( | |
| path.join(params.pluginDir, "openclaw.plugin.json"), | |
| JSON.stringify({ | |
| id: "cli-fork-process-proof", | |
| name: "CLI fork process proof", | |
| cliBackends: ["proof-cli"], | |
| activation: { onStartup: false }, | |
| modelCatalog: { | |
| providers: { | |
| "proof-cli": { | |
| models: [ | |
| { | |
| id: "proof-model", | |
| name: "Proof model", | |
| reasoning: false, | |
| input: ["text"], | |
| contextWindow: 8192, | |
| maxTokens: 1024, | |
| }, | |
| ], | |
| }, | |
| }, | |
| discovery: { "proof-cli": "static" }, | |
| }, | |
| configSchema: { type: "object", additionalProperties: false }, | |
| }), | |
| ), | |
| fs.writeFile( | |
| path.join(params.pluginDir, "index.js"), | |
| `import { execFile } from "node:child_process"; | |
| import { promisify } from "node:util"; | |
| const execFileAsync = promisify(execFile); | |
| const need = (name) => { | |
| const value = process.env[name]?.trim(); | |
| if (!value) throw new Error("missing " + name); | |
| return value; | |
| }; | |
| export default { | |
| id: "cli-fork-process-proof", | |
| name: "CLI fork process proof", | |
| register(api) { | |
| api.registerCliBackend({ | |
| id: "proof-cli", | |
| modelProvider: "proof-cli", | |
| config: { | |
| command: process.execPath, | |
| args: [need("PR135168_BACKEND_SCRIPT")], | |
| resumeArgs: [need("PR135168_BACKEND_SCRIPT"), "--resume", "{sessionId}"], | |
| output: "json", | |
| resumeOutput: "json", | |
| input: "stdin", | |
| sessionMode: "existing", | |
| sessionIdFields: ["session_id"], | |
| forkArg: "--fork-session", | |
| resumeAtArg: "--resume-session-at", | |
| systemPromptWhen: "never", | |
| serialize: true | |
| }, | |
| async prepareExecution() { | |
| return { | |
| beforeExecution: async () => { | |
| await execFileAsync(process.execPath, ["--import", "tsx", need("PR135168_REBIND_SCRIPT")]); | |
| } | |
| }; | |
| } | |
| }); | |
| } | |
| }; | |
| `, | |
| ), | |
| fs.writeFile( | |
| params.backendScript, | |
| `import fs from "node:fs/promises"; | |
| const stdin = []; | |
| for await (const chunk of process.stdin) stdin.push(chunk); | |
| await fs.appendFile(process.env.PR135168_SPAWN_LOG, JSON.stringify({ argv: process.argv.slice(2) }) + "\\n"); | |
| process.stdout.write(JSON.stringify({ result: "stale recovery ran", session_id: process.env.PR135168_SUCCESSOR_CLI_SESSION_ID }) + "\\n"); | |
| `, | |
| ), | |
| fs.writeFile( | |
| params.rebindScript, | |
| `const accessor = await import(${JSON.stringify(resolveRuntimeWorkerUrl(cliRecoveryEntrypoints.sessionAccessor).href)}); | |
| const cliSession = await import(${JSON.stringify(resolveRuntimeWorkerUrl(cliRecoveryEntrypoints.cliSession).href)}); | |
| const scope = { sessionKey: process.env.PR135168_SESSION_KEY, storePath: process.env.PR135168_STORE_PATH }; | |
| const current = accessor.loadSessionEntry({ ...scope, readConsistency: "latest" }); | |
| if (!current) throw new Error("proof session row missing"); | |
| const rebound = structuredClone(current); | |
| cliSession.setCliSessionBinding(rebound, "proof-cli", { sessionId: process.env.PR135168_NEWER_CLI_SESSION_ID, forceReuse: true }); | |
| if (!await accessor.replaceSessionEntry(scope, rebound)) throw new Error("proof rebind failed"); | |
| `, | |
| ), | |
| ]); | |
| } | |