import fs from "node:fs/promises"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { useAutoCleanupTempDirTracker } from "../../../test/helpers/temp-dir.js"; import { appendTranscriptMessage, loadSessionEntryReadOnly, loadTranscriptEventsSync, upsertSessionEntryCore, } from "../../config/sessions/session-accessor.js"; import { emitAgentEvent } from "../../infra/agent-events.js"; import { createTestAdmittedRunContext } from "../admitted-run-context.test-support.js"; import { loadCliSessionHistoryMessages } from "../cli-runner/session-history.js"; import type { RunCliAgentParams } from "../cli-runner/types.js"; import { SessionManager } from "../sessions/index.js"; import { resolveEmbeddedCliBackendDispatchEligibility } from "./cli-backend-dispatch-eligibility.js"; import { runEmbeddedAgentViaCliBackendIfEligible } from "./cli-backend-dispatch.js"; import type { EmbeddedAgentRunResult } from "./types.js"; const ensureAuthProfileStore = vi.hoisted(() => vi.fn()); const resolveAuthProfileOrder = vi.hoisted(() => vi.fn()); const resolveModelAuthMode = vi.hoisted(() => vi.fn()); const resolveRuntimeCliBackends = vi.hoisted(() => vi.fn()); const resolveCliRuntimeExecutionProvider = vi.hoisted(() => vi.fn()); const runCliAgent = vi.hoisted(() => vi.fn()); const retireSessionMcpRuntime = vi.hoisted(() => vi.fn()); const retireSessionMcpRuntimeForSessionKey = vi.hoisted(() => vi.fn()); const tempDirs = useAutoCleanupTempDirTracker(afterEach); vi.mock("../model-auth.js", () => ({ ensureAuthProfileStore, resolveAuthProfileOrder, resolveModelAuthMode, })); vi.mock("../model-runtime-aliases.js", () => ({ resolveCliRuntimeExecutionProvider, })); vi.mock("../../plugins/cli-backends.runtime.js", () => ({ resolveRuntimeCliBackends, })); vi.mock("../cli-runner.runtime.js", () => ({ runCliAgent, })); vi.mock("../agent-bundle-mcp-tools.js", () => ({ retireSessionMcpRuntime, retireSessionMcpRuntimeForSessionKey, })); const transcriptRecorder = vi.hoisted(() => ({ noteToolEvent: vi.fn(), noteAssistantText: vi.fn(), flushAssistantSnapshot: vi.fn(), finalize: vi.fn(async () => undefined), })); const createCliDispatchTranscriptRecorder = vi.hoisted(() => vi.fn( () => transcriptRecorder, ), ); vi.mock("./cli-backend-dispatch-transcript.js", () => ({ createCliDispatchTranscriptRecorder, })); type CliDispatchParams = Parameters[0]; function baseRunParams(overrides: Partial = {}): CliDispatchParams { const runId = overrides.runId ?? "run-cli-dispatch-test"; return { admittedRunContext: createTestAdmittedRunContext(runId), sessionId: "recall-session", sessionKey: "agent:main:recall", sessionTarget: { agentId: "main", sessionId: overrides.sessionId ?? "recall-session", sessionKey: overrides.sessionKey ?? "agent:main:recall", storePath: "/tmp/recall/openclaw-agent.sqlite", }, sessionFile: "/tmp/recall/session.jsonl", workspaceDir: "/tmp/recall/workspace", prompt: "recall prompt", provider: "claude-cli", model: "claude-opus-4-8", timeoutMs: 30_000, runId, cliBackendDispatch: "subscription-auth" as const, toolsAllow: ["memory_search"], ...overrides, }; } function cliRunResult(meta: Partial = {}): EmbeddedAgentRunResult { return { payloads: [{ text: "recall summary" }], meta: meta as EmbeddedAgentRunResult["meta"], } as EmbeddedAgentRunResult; } beforeEach(() => { ensureAuthProfileStore.mockReset(); resolveAuthProfileOrder.mockReset(); resolveModelAuthMode.mockReset(); resolveRuntimeCliBackends.mockReset(); runCliAgent.mockReset(); retireSessionMcpRuntime.mockReset(); retireSessionMcpRuntimeForSessionKey.mockReset(); resolveCliRuntimeExecutionProvider.mockReset(); ensureAuthProfileStore.mockReturnValue({ profiles: {} }); resolveAuthProfileOrder.mockReturnValue([]); resolveModelAuthMode.mockReturnValue("oauth"); // Default registry: the claude-cli backend declares the capability, a // gemini backend does not. resolveRuntimeCliBackends.mockReturnValue([ { id: "claude-cli", subscriptionAuthDispatch: true }, { id: "google-gemini-cli" }, ]); resolveCliRuntimeExecutionProvider.mockReturnValue(undefined); retireSessionMcpRuntimeForSessionKey.mockResolvedValue(true); retireSessionMcpRuntime.mockResolvedValue(true); runCliAgent.mockResolvedValue(cliRunResult()); transcriptRecorder.noteToolEvent.mockReset(); transcriptRecorder.noteAssistantText.mockReset(); transcriptRecorder.flushAssistantSnapshot.mockReset(); transcriptRecorder.finalize.mockClear(); createCliDispatchTranscriptRecorder.mockClear(); }); describe("resolveEmbeddedCliBackendDispatchEligibility", () => { // The recall timeout default consumes this decision directly; these cases // pin that API-key and missing-backend setups stay ineligible so callers // budgeting on it keep the passthrough default. it("resolves the provider for subscription (oauth) credentials", () => { expect(resolveEmbeddedCliBackendDispatchEligibility({ provider: "claude-cli" })).toEqual({ provider: "claude-cli", }); }); it("returns undefined for API-key credentials", () => { resolveModelAuthMode.mockReturnValue("api-key"); expect( resolveEmbeddedCliBackendDispatchEligibility({ provider: "claude-cli" }), ).toBeUndefined(); }); it("returns undefined without a registered claude-cli backend", () => { resolveRuntimeCliBackends.mockReturnValue([]); expect( resolveEmbeddedCliBackendDispatchEligibility({ provider: "claude-cli" }), ).toBeUndefined(); }); it("honors an explicitly pinned API-key profile over subscription-first order", () => { // A pinned profile is the credential the run executes on; order must not // override it in either direction. ensureAuthProfileStore.mockReturnValue({ profiles: { "anthropic:claude-cli": { type: "oauth", provider: "claude-cli" }, "anthropic:api": { type: "api_key", provider: "anthropic" }, }, }); resolveAuthProfileOrder.mockReturnValue(["anthropic:claude-cli", "anthropic:api"]); expect( resolveEmbeddedCliBackendDispatchEligibility({ provider: "claude-cli", authProfileId: "anthropic:api", }), ).toBeUndefined(); expect(resolveModelAuthMode).not.toHaveBeenCalled(); }); it("honors an explicitly pinned OAuth profile over API-key-first order", () => { ensureAuthProfileStore.mockReturnValue({ profiles: { "anthropic:api": { type: "api_key", provider: "anthropic" }, "anthropic:claude-cli": { type: "oauth", provider: "claude-cli" }, }, }); resolveAuthProfileOrder.mockReturnValue(["anthropic:api", "anthropic:claude-cli"]); expect( resolveEmbeddedCliBackendDispatchEligibility({ provider: "claude-cli", authProfileId: "anthropic:claude-cli", }), ).toEqual({ provider: "claude-cli" }); }); it("falls back to ordered selection when the pinned profile is unknown", () => { ensureAuthProfileStore.mockReturnValue({ profiles: { "anthropic:api": { type: "api_key", provider: "anthropic" } }, }); resolveAuthProfileOrder.mockReturnValue(["anthropic:api"]); expect( resolveEmbeddedCliBackendDispatchEligibility({ provider: "claude-cli", authProfileId: "anthropic:missing", }), ).toBeUndefined(); }); it("returns undefined when the backend does not declare the capability", () => { // The provider plugin owns the subscription-billing claim; a registered // backend without it (e.g. gemini) keeps the passthrough. expect( resolveEmbeddedCliBackendDispatchEligibility({ provider: "google-gemini-cli" }), ).toBeUndefined(); expect(resolveModelAuthMode).not.toHaveBeenCalled(); }); it("resolves canonical refs through the configured runtime", () => { resolveCliRuntimeExecutionProvider.mockReturnValue("claude-cli"); expect( resolveEmbeddedCliBackendDispatchEligibility({ provider: "anthropic", model: "claude-opus-4-8", agentId: "main", }), ).toEqual({ provider: "claude-cli" }); }); it("forwards the pinned auth profile into runtime resolution", () => { // The pin can be what maps a canonical ref onto its CLI runtime; the // resolver must see it or dispatch strands the run on the passthrough. resolveCliRuntimeExecutionProvider.mockReturnValue("claude-cli"); ensureAuthProfileStore.mockReturnValue({ profiles: { "anthropic:claude-cli": { type: "oauth", provider: "claude-cli" } }, }); expect( resolveEmbeddedCliBackendDispatchEligibility({ provider: "anthropic", model: "claude-opus-4-8", agentId: "main", authProfileId: "anthropic:claude-cli", }), ).toEqual({ provider: "claude-cli" }); expect(resolveCliRuntimeExecutionProvider).toHaveBeenCalledWith( expect.objectContaining({ authProfileId: "anthropic:claude-cli" }), ); }); }); describe("runEmbeddedAgentViaCliBackendIfEligible gate", () => { const runGate = (overrides: Partial = {}) => runEmbeddedAgentViaCliBackendIfEligible(baseRunParams(overrides)); it("returns undefined without the opt-in", async () => { expect(await runGate({ cliBackendDispatch: undefined })).toBeUndefined(); expect(resolveModelAuthMode).not.toHaveBeenCalled(); expect(runCliAgent).not.toHaveBeenCalled(); }); it("keeps message-tool-only source replies on the embedded delivery owner", async () => { expect( await runGate({ sourceReplyDeliveryMode: "message_tool_only", toolsAllow: ["message"], messageChannel: "telegram", currentChannelId: "telegram:source-chat", }), ).toBeUndefined(); expect(runCliAgent).not.toHaveBeenCalled(); }); it("dispatches claude-cli runs with subscription (oauth) credentials", async () => { expect(await runGate({ agentDir: "/agents/main", workspaceDir: "/workspace" })).toBeDefined(); expect(ensureAuthProfileStore).toHaveBeenCalledWith("/agents/main", expect.anything()); expect(resolveModelAuthMode).toHaveBeenCalledWith( "claude-cli", undefined, expect.anything(), expect.objectContaining({ workspaceDir: "/workspace" }), ); expect(runCliAgent.mock.calls[0]?.[0]).toMatchObject({ provider: "claude-cli" }); }); it("dispatches when no credential mode resolves for the passthrough", async () => { resolveModelAuthMode.mockReturnValue(undefined); expect(await runGate()).toBeDefined(); expect(runCliAgent).toHaveBeenCalledTimes(1); }); it("dispatches when the credential store is unreadable", async () => { ensureAuthProfileStore.mockImplementation(() => { throw new Error("store unreadable"); }); expect(await runGate()).toBeDefined(); expect(runCliAgent).toHaveBeenCalledTimes(1); }); it.each(["api-key", "mixed", "aws-sdk"] as const)( "keeps the passthrough when the auth mode is %s", async (mode) => { resolveModelAuthMode.mockReturnValue(mode); expect(await runGate()).toBeUndefined(); expect(runCliAgent).not.toHaveBeenCalled(); }, ); it("dispatches when the ordered profile selection picks a subscription credential in a mixed store", async () => { // The passthrough follows auth.order; a store-wide "mixed" aggregate must // not suppress dispatch when the selected profile is oauth. ensureAuthProfileStore.mockReturnValue({ profiles: { "anthropic:claude-cli": { type: "oauth", provider: "claude-cli" }, "anthropic:api": { type: "api_key", provider: "anthropic" }, }, }); resolveAuthProfileOrder.mockReturnValue(["anthropic:claude-cli", "anthropic:api"]); resolveModelAuthMode.mockReturnValue("mixed"); expect(await runGate()).toBeDefined(); expect(resolveModelAuthMode).not.toHaveBeenCalled(); }); it("keeps the passthrough when the ordered profile selection picks an API key", async () => { ensureAuthProfileStore.mockReturnValue({ profiles: { "anthropic:api": { type: "api_key", provider: "anthropic" }, "anthropic:claude-cli": { type: "oauth", provider: "claude-cli" }, }, }); resolveAuthProfileOrder.mockReturnValue(["anthropic:api", "anthropic:claude-cli"]); expect(await runGate()).toBeUndefined(); expect(runCliAgent).not.toHaveBeenCalled(); }); it.each([undefined, "raw", "resolved"] as const)( "dispatches canonical refs through claude-cli with %s logical route resolution", async (requestedRouteResolution) => { resolveCliRuntimeExecutionProvider.mockReturnValue("claude-cli"); expect( await runGate({ provider: "anthropic", model: "claude-opus-4-8", agentId: "main", requestedRouteResolution, }), ).toBeDefined(); expect(resolveCliRuntimeExecutionProvider).toHaveBeenCalledWith( expect.objectContaining({ provider: "anthropic", agentId: "main", modelId: "claude-opus-4-8", }), ); const dispatched = runCliAgent.mock.calls[0]?.[0]; expect(dispatched).toMatchObject({ provider: "claude-cli" }); expect(dispatched.requesterModel).toEqual( requestedRouteResolution === "resolved" ? { provider: "anthropic", model: "claude-opus-4-8" } : undefined, ); }, ); it("keeps the passthrough for canonical refs without a claude-cli runtime", async () => { resolveCliRuntimeExecutionProvider.mockReturnValue(undefined); expect(await runGate({ provider: "anthropic", model: "claude-opus-4-8" })).toBeUndefined(); resolveCliRuntimeExecutionProvider.mockReturnValue("google-gemini-cli"); expect(await runGate({ provider: "google", model: "gemini-3.1-pro-preview" })).toBeUndefined(); expect(runCliAgent).not.toHaveBeenCalled(); }); it("keeps the passthrough for other CLI runtimes until verified", async () => { expect(await runGate({ provider: "google-gemini-cli" })).toBeUndefined(); expect(resolveModelAuthMode).not.toHaveBeenCalled(); }); it("keeps the passthrough without a caller-owned session file", async () => { expect(await runGate({ sessionFile: undefined })).toBeUndefined(); }); it("keeps the passthrough when no claude-cli backend is registered", async () => { resolveRuntimeCliBackends.mockReturnValue([]); expect(await runGate()).toBeUndefined(); }); }); describe("runEmbeddedAgentViaCliBackendIfEligible execution", () => { it("reads and mirrors the dispatched turn in the selected custom SQLite store", async () => { const dir = tempDirs.make("cli-dispatch-history-"); const sessionTarget = { agentId: "main", sessionId: "recall-session", sessionKey: "agent:main:recall", storePath: path.join(dir, "openclaw-agent.sqlite"), }; await upsertSessionEntryCore(sessionTarget, { sessionId: sessionTarget.sessionId, updatedAt: 1, }); await appendTranscriptMessage(sessionTarget, { message: { role: "user", content: "persisted prior context", timestamp: 1 }, cwd: dir, }); const actualRecorder = await vi.importActual< typeof import("./cli-backend-dispatch-transcript.js") >("./cli-backend-dispatch-transcript.js"); createCliDispatchTranscriptRecorder.mockImplementationOnce( actualRecorder.createCliDispatchTranscriptRecorder, ); runCliAgent.mockImplementationOnce(async (cliParams: RunCliAgentParams) => { expect .soft(await loadCliSessionHistoryMessages({ sessionTarget: cliParams.sessionTarget })) .toEqual( expect.arrayContaining([expect.objectContaining({ content: "persisted prior context" })]), ); return cliRunResult(); }); await runEmbeddedAgentViaCliBackendIfEligible( baseRunParams({ sessionTarget, workspaceDir: dir }), ); expect(loadTranscriptEventsSync(sessionTarget)).toEqual( expect.arrayContaining([ expect.objectContaining({ message: expect.objectContaining({ role: "user", content: [{ type: "text", text: "recall prompt" }], }), }), expect.objectContaining({ message: expect.objectContaining({ role: "assistant", content: [{ type: "text", text: "recall summary" }], }), }), ]), ); }); it("maps the embedded run onto a one-shot restricted CLI run", async () => { runCliAgent.mockResolvedValue(cliRunResult()); const sessionTarget = { agentId: "main", sessionId: "recall-session", sessionKey: "agent:main:recall", storePath: "/tmp/recall/custom/openclaw-agent.sqlite", }; const params = baseRunParams({ sessionTarget, toolsAllow: ["memory_search", "memory_get", "notes_retrieve_context"], fastMode: "auto", fastModeStartedAtMs: 1000, fastModeAutoOnSeconds: 15, authProfileId: "selected-subscription-profile", }); const result = await runEmbeddedAgentViaCliBackendIfEligible(params); expect(result?.payloads?.[0]?.text).toBe("recall summary"); expect(runCliAgent).toHaveBeenCalledTimes(1); const cliParams = runCliAgent.mock.calls[0]?.[0]; expect(cliParams?.sessionTarget).toBe(sessionTarget); expect(cliParams).toMatchObject({ provider: "claude-cli", model: "claude-opus-4-8", sessionFile: "/tmp/recall/session.jsonl", timeoutMs: 30_000, runTimeoutOverrideMs: 30_000, fastMode: "auto", fastModeStartedAtMs: 1000, fastModeAutoOnSeconds: 15, authProfileId: "selected-subscription-profile", disableCliLiveSession: true, cleanupCliLiveSessionOnRunEnd: true, requireExplicitMessageTarget: true, cliToolAvailability: { native: [], openClaw: ["memory_search", "memory_get", "notes_retrieve_context"], }, }); // Embedded toolsAllow must never reach the CLI runner: it fails closed. expect(cliParams).not.toHaveProperty("toolsAllow"); }); it.each(["group", "channel"] as const)( "forwards authoritative %s type through embedded-to-CLI dispatch for opaque keys", async (chatType) => { await runEmbeddedAgentViaCliBackendIfEligible( baseRunParams({ sessionKey: "agent:main:opaque:binding", chatType, }), ); expect(runCliAgent.mock.calls[0]?.[0]).toMatchObject({ sessionKey: "agent:main:opaque:binding", chatType, }); }, ); // Fail-closed tool policy: only a non-empty named allowlist is expressible // on the CLI surface. Every other embedded tool state keeps the passthrough // so no closed state silently widens. it.each([ ["a wildcard allowlist", { toolsAllow: ["*"] }], ["a mixed wildcard allowlist", { toolsAllow: ["memory_search", "*"] }], ["a deny-all allowlist", { toolsAllow: [] }], ["no allowlist", { toolsAllow: undefined }], ["disableTools", { disableTools: true }], ["a raw model run", { modelRun: true }], ] as const)("refuses dispatch for %s", async (_label, overrides) => { expect( await runEmbeddedAgentViaCliBackendIfEligible( baseRunParams(overrides as Partial), ), ).toBeUndefined(); expect(runCliAgent).not.toHaveBeenCalled(); }); it("invokes onExecutionStarted once at the dispatch boundary", async () => { runCliAgent.mockResolvedValue(cliRunResult()); const onExecutionStarted = vi.fn(); await runEmbeddedAgentViaCliBackendIfEligible( baseRunParams({ onExecutionStarted, lifecycleGeneration: "gen-1" }), ); expect(onExecutionStarted).toHaveBeenCalledTimes(1); expect(onExecutionStarted).toHaveBeenCalledWith({ lifecycleGeneration: "gen-1" }); }); it("retains the prepared vision capability with ordered prompt images and media", async () => { const images = [{ type: "image" as const, data: "aGVsbG8=", mimeType: "image/png" }]; const imageOrder = ["inline" as const]; const media = [{ path: "/tmp/recall.png", contentType: "image/png" }]; await runEmbeddedAgentViaCliBackendIfEligible( baseRunParams({ modelHasVision: true, images, imageOrder, media }), ); expect(runCliAgent.mock.calls[0]?.[0]).toMatchObject({ prompt: "recall prompt", modelHasVision: true, images, imageOrder, media, }); }); it("forwards execution phases from the CLI backend", async () => { const onExecutionPhase = vi.fn(); runCliAgent.mockImplementation( async (cliParams: { onExecutionPhase?: CliDispatchParams["onExecutionPhase"] }) => { cliParams.onExecutionPhase?.({ phase: "model_call_started", provider: "anthropic", model: "claude-opus-4-8", backend: "claude-cli", firstModelCallStarted: true, }); return cliRunResult(); }, ); await runEmbeddedAgentViaCliBackendIfEligible(baseRunParams({ onExecutionPhase })); expect(onExecutionPhase).toHaveBeenCalledWith({ phase: "model_call_started", provider: "anthropic", model: "claude-opus-4-8", backend: "claude-cli", firstModelCallStarted: true, }); }); it("bridges CLI tool result events to onAgentToolResult without the MCP prefix", async () => { const observed: Array<{ toolName: string; isError: boolean }> = []; const params = baseRunParams({ toolsAllow: ["memory_search"], onAgentToolResult: (event) => { observed.push({ toolName: event.toolName, isError: event.isError }); }, }); runCliAgent.mockImplementation(async (cliParams: { runId: string }) => { emitAgentEvent({ runId: cliParams.runId, stream: "tool", data: { phase: "result", name: "mcp__openclaw__memory_search", result: { content: [] }, isError: false, resultContentSource: "network", }, }); // Soft tool failures must surface as isError like the native path. emitAgentEvent({ runId: cliParams.runId, stream: "tool", data: { phase: "result", name: "mcp__openclaw__memory_get", result: { details: { status: "error" } }, isError: false, }, }); emitAgentEvent({ runId: "other-run", stream: "tool", data: { phase: "result", name: "mcp__openclaw__memory_get", isError: true }, }); return cliRunResult(); }); await runEmbeddedAgentViaCliBackendIfEligible(params); expect(observed).toEqual([ { toolName: "memory_search", isError: false }, { toolName: "memory_get", isError: true }, ]); // The bridge must not outlive the run. emitAgentEvent({ runId: params.runId, stream: "tool", data: { phase: "result", name: "mcp__openclaw__memory_search", isError: false }, }); expect(observed).toHaveLength(2); }); it.each([true, undefined])( "delegates MCP lifetime policy %s to the CLI settlement owner", async (cleanupBundleMcpOnRunEnd) => { runCliAgent.mockResolvedValue(cliRunResult()); await runEmbeddedAgentViaCliBackendIfEligible(baseRunParams({ cleanupBundleMcpOnRunEnd })); expect(runCliAgent.mock.calls[0]?.[0]?.cleanupBundleMcpOnRunEnd).toBe( cleanupBundleMcpOnRunEnd, ); expect(retireSessionMcpRuntime).not.toHaveBeenCalled(); expect(retireSessionMcpRuntimeForSessionKey).not.toHaveBeenCalled(); }, ); it("mirrors the run into the transcript recorder", async () => { runCliAgent.mockImplementation(async (cliParams: { runId: string }) => { emitAgentEvent({ runId: cliParams.runId, stream: "assistant", data: { text: "partial answer" }, }); emitAgentEvent({ runId: cliParams.runId, stream: "tool", data: { phase: "start", name: "mcp__openclaw__memory_search", toolCallId: "call-1", args: { query: "wings" }, }, }); emitAgentEvent({ runId: cliParams.runId, stream: "tool", data: { phase: "result", name: "mcp__openclaw__memory_search", toolCallId: "call-1", result: { content: [] }, isError: false, resultContentSource: "network", }, }); return cliRunResult(); }); await runEmbeddedAgentViaCliBackendIfEligible(baseRunParams({ toolsAllow: ["memory_search"] })); expect(createCliDispatchTranscriptRecorder).toHaveBeenCalledWith( expect.objectContaining({ sessionFile: "/tmp/recall/session.jsonl", sessionId: "recall-session", prompt: "recall prompt", provider: "claude-cli", }), ); expect(transcriptRecorder.noteAssistantText).toHaveBeenCalledWith("partial answer"); expect(transcriptRecorder.noteToolEvent).toHaveBeenCalledWith( expect.objectContaining({ phase: "start", toolName: "memory_search", toolCallId: "call-1", args: { query: "wings" }, }), ); expect(transcriptRecorder.noteToolEvent).toHaveBeenCalledWith( expect.objectContaining({ phase: "result", toolName: "memory_search", resultContentSource: "network", }), ); expect(transcriptRecorder.finalize).toHaveBeenCalledWith("recall summary"); }); it("flushes the assistant snapshot the moment the run aborts", async () => { const abortController = new AbortController(); runCliAgent.mockImplementation(async () => { abortController.abort(new Error("recall timeout")); expect(transcriptRecorder.flushAssistantSnapshot).toHaveBeenCalledTimes(1); throw new Error("aborted"); }); await expect( runEmbeddedAgentViaCliBackendIfEligible( baseRunParams({ abortSignal: abortController.signal }), ), ).rejects.toThrow("aborted"); expect(transcriptRecorder.finalize).toHaveBeenCalledWith(undefined); }); it("finalizes the transcript when the CLI run fails", async () => { runCliAgent.mockRejectedValue(new Error("boom")); await expect(runEmbeddedAgentViaCliBackendIfEligible(baseRunParams())).rejects.toThrow("boom"); expect(transcriptRecorder.finalize).toHaveBeenCalledWith(undefined); }); it("drops CLI session bindings from the run result", async () => { runCliAgent.mockResolvedValue( cliRunResult({ agentMeta: { sessionId: "native-session", cliSessionBinding: { sessionId: "native-session" }, }, } as Partial), ); const result = await runEmbeddedAgentViaCliBackendIfEligible(baseRunParams()); expect(result?.meta.agentMeta?.cliSessionBinding).toBeUndefined(); }); }); describe("detached CLI transcript ownership", () => { it.each([false, true])( "does not mirror a detached turn into durable metadata (existing: %s)", async (existing) => { const root = tempDirs.make("openclaw-detached-cli-"); const storePath = path.join(root, "final", "openclaw-agent.sqlite"); const params = baseRunParams({ agentId: "main", sessionPersistence: "detached", sessionManager: SessionManager.inMemory(root), sessionTarget: { agentId: "main", sessionId: "recall-session", sessionKey: "agent:main:recall", storePath, }, }); if (existing) { await upsertSessionEntryCore(params.sessionTarget, { sessionId: params.sessionId, updatedAt: 1, liveModelSwitchPending: true, }); } const before = loadSessionEntryReadOnly(params.sessionTarget); const recorder = await vi.importActual( "./cli-backend-dispatch-transcript.js", ); createCliDispatchTranscriptRecorder.mockImplementationOnce( recorder.createCliDispatchTranscriptRecorder, ); await expect(runEmbeddedAgentViaCliBackendIfEligible(params)).resolves.toMatchObject({ payloads: [{ text: "recall summary" }], }); expect(runCliAgent).toHaveBeenCalledOnce(); expect(loadSessionEntryReadOnly(params.sessionTarget)).toEqual(before); if (!existing) { await expect(fs.access(path.dirname(storePath))).rejects.toThrow(); } else { expect(loadTranscriptEventsSync(params.sessionTarget)).toEqual([]); } }, ); });