/** Tests ACP runtime mode/config option persistence and backend control calls. */ import { describe, expect, it, vi } from "vitest"; import { installMutableAcpSessionMetaUpsert, type AcpRuntime, AcpRuntimeError, AcpSessionManager, baseCfg, createRuntime, disposeAcpSessionManagerInstance, expectMockCallFields, expectNoMockCallFields, expectRecordFields, expectRejectedRecord, extractRuntimeOptionsFromUpserts, hoisted, installAcpSessionManagerTestLifecycle, mockCallArg, readySessionMeta, type SessionAcpMeta, } from "./manager.test-helpers.js"; describe("AcpSessionManager runtime config", () => { installAcpSessionManagerTestLifecycle(); it("persists runtime mode changes through setSessionRuntimeMode", async () => { const runtimeState = createRuntime(); hoisted.requireAcpRuntimeBackendMock.mockReturnValue({ id: "acpx", runtime: runtimeState.runtime, }); hoisted.readAcpSessionEntryMock.mockReturnValue({ sessionKey: "agent:codex:acp:session-1", storeSessionKey: "agent:codex:acp:session-1", acp: readySessionMeta(), }); const manager = new AcpSessionManager(); const options = await manager.setSessionRuntimeMode({ cfg: baseCfg, sessionKey: "agent:codex:acp:session-1", runtimeMode: "plan", }); expectMockCallFields(runtimeState.setMode, { mode: "plan", }); expect(options.runtimeMode).toBe("plan"); const persistedRuntimeModes = extractRuntimeOptionsFromUpserts().map( (entry) => entry?.runtimeMode, ); expect(persistedRuntimeModes).toContain("plan"); }); it("reapplies persisted controls on next turn after runtime option updates", async () => { const runtimeState = createRuntime(); hoisted.requireAcpRuntimeBackendMock.mockReturnValue({ id: "acpx", runtime: runtimeState.runtime, }); const metaState: { currentMeta: SessionAcpMeta } = { currentMeta: { ...readySessionMeta(), runtimeOptions: { runtimeMode: "plan", }, }, }; hoisted.readAcpSessionEntryMock.mockImplementation((paramsUnknown: unknown) => { const sessionKey = (paramsUnknown as { sessionKey?: string }).sessionKey ?? "agent:codex:acp:session-1"; return { sessionKey, storeSessionKey: sessionKey, acp: metaState.currentMeta, }; }); installMutableAcpSessionMetaUpsert(metaState); const manager = new AcpSessionManager(); await manager.setSessionConfigOption({ cfg: baseCfg, sessionKey: "agent:codex:acp:session-1", key: "model", value: "openai/gpt-5.4", }); expect(runtimeState.setMode).not.toHaveBeenCalled(); await manager.runTurn({ provenance: "system", cfg: baseCfg, sessionKey: "agent:codex:acp:session-1", text: "do work", mode: "prompt", requestId: "run-1", }); expectMockCallFields(runtimeState.setMode, { mode: "plan", }); }); it("reconciles persisted ACP session identifiers from runtime status after a turn", async () => { const runtimeState = createRuntime(); runtimeState.ensureSession.mockResolvedValue({ sessionKey: "agent:codex:acp:session-1", backend: "acpx", runtimeSessionName: "runtime-1", backendSessionId: "acpx-stale", agentSessionId: "agent-stale", }); runtimeState.getStatus.mockResolvedValue({ summary: "status=alive", backendSessionId: "acpx-fresh", agentSessionId: "agent-fresh", details: { status: "alive" }, }); hoisted.requireAcpRuntimeBackendMock.mockReturnValue({ id: "acpx", runtime: runtimeState.runtime, }); const metaState: { currentMeta: SessionAcpMeta } = { currentMeta: { ...readySessionMeta(), identity: { state: "resolved", source: "status", acpxSessionId: "acpx-stale", agentSessionId: "agent-stale", lastUpdatedAt: Date.now(), }, }, }; hoisted.readAcpSessionEntryMock.mockImplementation((paramsUnknown: unknown) => { const sessionKey = (paramsUnknown as { sessionKey?: string }).sessionKey ?? "agent:codex:acp:session-1"; return { sessionKey, storeSessionKey: sessionKey, acp: metaState.currentMeta, }; }); installMutableAcpSessionMetaUpsert(metaState); const manager = new AcpSessionManager(); await manager.runTurn({ provenance: "system", cfg: baseCfg, sessionKey: "agent:codex:acp:session-1", text: "do work", mode: "prompt", requestId: "run-1", }); expect(runtimeState.getStatus).toHaveBeenCalledTimes(1); expect(metaState.currentMeta.identity?.acpxSessionId).toBe("acpx-fresh"); expect(metaState.currentMeta.identity?.agentSessionId).toBe("agent-fresh"); }); it("reconciles oneshot ACP identity from runtime status before closing after a turn", async () => { const runtimeState = createRuntime(); runtimeState.ensureSession.mockResolvedValue({ sessionKey: "agent:codex:acp:session-1", backend: "acpx", runtimeSessionName: "runtime-1", backendSessionId: "acpx-oneshot", }); runtimeState.getStatus.mockResolvedValue({ summary: "status=done", backendSessionId: "acpx-oneshot", agentSessionId: "agent-oneshot", details: { status: "done" }, }); hoisted.requireAcpRuntimeBackendMock.mockReturnValue({ id: "acpx", runtime: runtimeState.runtime, }); const metaState: { currentMeta: SessionAcpMeta | undefined } = { currentMeta: undefined }; hoisted.readAcpSessionEntryMock.mockImplementation((paramsUnknown: unknown) => { const sessionKey = (paramsUnknown as { sessionKey?: string }).sessionKey ?? "agent:codex:acp:session-1"; return { sessionKey, storeSessionKey: sessionKey, acp: metaState.currentMeta, }; }); installMutableAcpSessionMetaUpsert(metaState); const manager = new AcpSessionManager(); await manager.initializeSession({ cfg: baseCfg, sessionKey: "agent:codex:acp:session-1", agent: "codex", mode: "oneshot", }); expectRecordFields(metaState.currentMeta?.identity, { state: "pending", acpxSessionId: "acpx-oneshot", source: "ensure", }); await manager.runTurn({ provenance: "system", cfg: baseCfg, sessionKey: "agent:codex:acp:session-1", text: "do work", mode: "prompt", requestId: "run-1", }); expect(runtimeState.getStatus).toHaveBeenCalledTimes(2); const closeInput = mockCallArg(runtimeState.close); expectRecordFields(closeInput, { reason: "oneshot-complete", }); expectRecordFields(closeInput.handle, { backendSessionId: "acpx-oneshot", agentSessionId: "agent-oneshot", }); expectRecordFields(metaState.currentMeta?.identity, { state: "resolved", acpxSessionId: "acpx-oneshot", agentSessionId: "agent-oneshot", source: "status", }); }); it("reconciles prompt-learned agent session IDs even when runtime status omits them", async () => { const runtimeState = createRuntime(); runtimeState.ensureSession.mockResolvedValue({ sessionKey: "agent:gemini:acp:session-1", backend: "acpx", runtimeSessionName: "runtime-3", backendSessionId: "acpx-stale", }); runtimeState.runTurn.mockImplementation(async function* (inputUnknown: unknown) { const input = inputUnknown as { handle: { agentSessionId?: string; }; }; input.handle.agentSessionId = "gemini-session-1"; yield { type: "done" as const }; }); runtimeState.getStatus.mockResolvedValue({ summary: "status=alive", details: { status: "alive" }, }); hoisted.requireAcpRuntimeBackendMock.mockReturnValue({ id: "acpx", runtime: runtimeState.runtime, }); const metaState: { currentMeta: SessionAcpMeta } = { currentMeta: { ...readySessionMeta(), agent: "gemini", identity: { state: "pending", source: "ensure", acpxSessionId: "acpx-stale", lastUpdatedAt: Date.now(), }, }, }; const sessionKey = "agent:gemini:acp:session-1"; hoisted.readAcpSessionEntryMock.mockImplementation((paramsUnknown: unknown) => { const key = (paramsUnknown as { sessionKey?: string }).sessionKey ?? sessionKey; return { sessionKey: key, storeSessionKey: key, acp: metaState.currentMeta, }; }); installMutableAcpSessionMetaUpsert(metaState); const manager = new AcpSessionManager(); await manager.runTurn({ provenance: "system", cfg: baseCfg, sessionKey, text: "learn prompt session", mode: "prompt", requestId: "run-prompt-learned-agent-id", }); expect(metaState.currentMeta.identity?.state).toBe("resolved"); expect(metaState.currentMeta.identity?.agentSessionId).toBe("gemini-session-1"); expect(metaState.currentMeta.identity?.acpxSessionId).toBe("acpx-stale"); }); it("preserves existing ACP session identifiers when ensure returns none", async () => { const runtimeState = createRuntime(); runtimeState.ensureSession.mockResolvedValue({ sessionKey: "agent:codex:acp:session-1", backend: "acpx", runtimeSessionName: "runtime-2", }); runtimeState.getStatus.mockResolvedValue({ summary: "status=alive", details: { status: "alive" }, }); hoisted.requireAcpRuntimeBackendMock.mockReturnValue({ id: "acpx", runtime: runtimeState.runtime, }); hoisted.readAcpSessionEntryMock.mockReturnValue({ sessionKey: "agent:codex:acp:session-1", storeSessionKey: "agent:codex:acp:session-1", acp: { ...readySessionMeta(), identity: { state: "resolved", source: "status", acpxSessionId: "acpx-stable", agentSessionId: "agent-stable", lastUpdatedAt: Date.now(), }, }, }); const manager = new AcpSessionManager(); const status = await manager.getSessionStatus({ cfg: baseCfg, sessionKey: "agent:codex:acp:session-1", }); expect(status.identity?.acpxSessionId).toBe("acpx-stable"); expect(status.identity?.agentSessionId).toBe("agent-stable"); }); it("applies persisted runtime options before running turns", async () => { const runtimeState = createRuntime(); hoisted.requireAcpRuntimeBackendMock.mockReturnValue({ id: "acpx", runtime: runtimeState.runtime, }); hoisted.readAcpSessionEntryMock.mockReturnValue({ sessionKey: "agent:codex:acp:session-1", storeSessionKey: "agent:codex:acp:session-1", acp: { ...readySessionMeta(), runtimeOptions: { runtimeMode: "plan", model: "openai/gpt-5.4", thinking: "high", permissionProfile: "strict", timeoutSeconds: 120, }, }, }); const manager = new AcpSessionManager(); await manager.runTurn({ provenance: "system", cfg: baseCfg, sessionKey: "agent:codex:acp:session-1", text: "do work", mode: "prompt", requestId: "run-1", }); expectMockCallFields(runtimeState.setMode, { mode: "plan", }); expectMockCallFields(runtimeState.setConfigOption, { key: "model", value: "openai/gpt-5.4", }); expectMockCallFields(runtimeState.setConfigOption, { key: "thinking", value: "high", }); expectMockCallFields(runtimeState.setConfigOption, { key: "approval_policy", value: "strict", }); expectMockCallFields(runtimeState.setConfigOption, { key: "timeout", value: "120", }); }); it("continues turns when adapters reject optional timeout config", async () => { const runtimeState = createRuntime(); runtimeState.setConfigOption.mockImplementation(async (input: { key: string }) => { if (input.key === "timeout") { throw new AcpRuntimeError( "ACP_TURN_FAILED", 'Agent rejected session/set_config_option for "timeout": ACP -32602 Invalid params', ); } }); hoisted.requireAcpRuntimeBackendMock.mockReturnValue({ id: "acpx", runtime: runtimeState.runtime, }); hoisted.readAcpSessionEntryMock.mockReturnValue({ sessionKey: "agent:opencode:acp:session-1", storeSessionKey: "agent:opencode:acp:session-1", acp: { ...readySessionMeta({ agent: "opencode" }), runtimeOptions: { timeoutSeconds: 120, }, }, }); const manager = new AcpSessionManager(); await manager.runTurn({ provenance: "system", cfg: baseCfg, sessionKey: "agent:opencode:acp:session-1", text: "do work", mode: "prompt", requestId: "run-opencode", }); expectMockCallFields(runtimeState.setConfigOption, { key: "timeout", value: "120", }); expect(runtimeState.runTurn).toHaveBeenCalledTimes(1); }); it("fails turns when optional timeout config writes hit runtime failures", async () => { const runtimeState = createRuntime(); runtimeState.setConfigOption.mockImplementation(async (input: { key: string }) => { if (input.key === "timeout") { throw new AcpRuntimeError("ACP_BACKEND_UNAVAILABLE", "ACP backend unavailable"); } }); hoisted.requireAcpRuntimeBackendMock.mockReturnValue({ id: "acpx", runtime: runtimeState.runtime, }); hoisted.readAcpSessionEntryMock.mockReturnValue({ sessionKey: "agent:opencode:acp:session-1", storeSessionKey: "agent:opencode:acp:session-1", acp: { ...readySessionMeta({ agent: "opencode" }), runtimeOptions: { timeoutSeconds: 120, }, }, }); const manager = new AcpSessionManager(); await expectRejectedRecord( manager.runTurn({ provenance: "system", cfg: baseCfg, sessionKey: "agent:opencode:acp:session-1", text: "do work", mode: "prompt", requestId: "run-opencode", }), { code: "ACP_BACKEND_UNAVAILABLE", }, ); expectMockCallFields(runtimeState.setConfigOption, { key: "timeout", value: "120", }); expect(runtimeState.runTurn).not.toHaveBeenCalled(); }); it("fails turns when adapters reject required runtime config", async () => { const runtimeState = createRuntime(); runtimeState.setConfigOption.mockImplementation(async (input: { key: string }) => { if (input.key === "model") { throw new AcpRuntimeError( "ACP_TURN_FAILED", 'Agent rejected session/set_config_option for "model": ACP -32602 Invalid params', ); } }); hoisted.requireAcpRuntimeBackendMock.mockReturnValue({ id: "acpx", runtime: runtimeState.runtime, }); hoisted.readAcpSessionEntryMock.mockReturnValue({ sessionKey: "agent:opencode:acp:session-1", storeSessionKey: "agent:opencode:acp:session-1", acp: { ...readySessionMeta({ agent: "opencode" }), runtimeOptions: { model: "opencode/gpt-5.4", }, }, }); const manager = new AcpSessionManager(); await expectRejectedRecord( manager.runTurn({ provenance: "system", cfg: baseCfg, sessionKey: "agent:opencode:acp:session-1", text: "do work", mode: "prompt", requestId: "run-opencode", }), { code: "ACP_TURN_FAILED", }, ); expect(runtimeState.runTurn).not.toHaveBeenCalled(); }); it("maps persisted thinking runtime options to advertised effort config keys before running turns", async () => { const runtimeState = createRuntime(); runtimeState.getCapabilities.mockResolvedValue({ controls: ["session/set_mode", "session/set_config_option", "session/status"], configOptionKeys: ["mode", "model", "effort"], }); hoisted.requireAcpRuntimeBackendMock.mockReturnValue({ id: "acpx", runtime: runtimeState.runtime, }); hoisted.readAcpSessionEntryMock.mockReturnValue({ sessionKey: "agent:claude:acp:session-1", storeSessionKey: "agent:claude:acp:session-1", acp: { ...readySessionMeta({ agent: "claude" }), runtimeOptions: { thinking: "high", }, }, }); const manager = new AcpSessionManager(); await manager.runTurn({ provenance: "system", cfg: baseCfg, sessionKey: "agent:claude:acp:session-1", text: "do work", mode: "prompt", requestId: "run-1", }); expectMockCallFields(runtimeState.setConfigOption, { key: "effort", value: "high", }); expectNoMockCallFields(runtimeState.setConfigOption, { key: "thinking", }); }); it("maps persisted runtime options to backend-advertised aliases before running turns", async () => { const runtimeState = createRuntime(); runtimeState.getCapabilities.mockResolvedValue({ controls: ["session/set_config_option", "session/status"], configOptionKeys: ["model", "thought_level", "permissions", "timeout_seconds"], }); hoisted.requireAcpRuntimeBackendMock.mockReturnValue({ id: "acpx", runtime: runtimeState.runtime, }); hoisted.readAcpSessionEntryMock.mockReturnValue({ sessionKey: "agent:gemini:acp:session-1", storeSessionKey: "agent:gemini:acp:session-1", acp: { ...readySessionMeta({ agent: "gemini" }), runtimeOptions: { model: "gemini-3-flash-preview", thinking: "high", permissionProfile: "strict", timeoutSeconds: 120, }, }, }); const manager = new AcpSessionManager(); await manager.runTurn({ provenance: "system", cfg: baseCfg, sessionKey: "agent:gemini:acp:session-1", text: "do work", mode: "prompt", requestId: "run-1", }); expectMockCallFields(runtimeState.setConfigOption, { key: "thought_level", value: "high", }); expectMockCallFields(runtimeState.setConfigOption, { key: "permissions", value: "strict", }); expectMockCallFields(runtimeState.setConfigOption, { key: "timeout_seconds", value: "120", }); expectNoMockCallFields(runtimeState.setConfigOption, { key: "thinking", }); expectNoMockCallFields(runtimeState.setConfigOption, { key: "approval_policy", }); expectNoMockCallFields(runtimeState.setConfigOption, { key: "timeout", }); }); it.each(["next turn", "shutdown"])( "closes the previous runtime after cwd updates before %s", async (operation) => { const runtimeState = createRuntime(); const lifecycle: string[] = []; runtimeState.ensureSession.mockImplementation(async (input) => { lifecycle.push(`ensure:${input.cwd ?? "default"}`); return { sessionKey: input.sessionKey, backend: "acpx", runtimeSessionName: `runtime:${input.cwd ?? "default"}`, cwd: input.cwd, }; }); runtimeState.close.mockImplementation(async ({ handle }) => { lifecycle.push(`close:${handle.cwd ?? "default"}`); }); hoisted.requireAcpRuntimeBackendMock.mockReturnValue({ id: "acpx", runtime: runtimeState.runtime, }); const sessionKey = "agent:codex:acp:session-cwd-update"; let currentEntry = { sessionKey, storeSessionKey: sessionKey, acp: readySessionMeta(), }; hoisted.readAcpSessionEntryMock.mockImplementation(() => currentEntry); hoisted.upsertAcpSessionMetaMock.mockImplementation((paramsUnknown: unknown) => { const params = paramsUnknown as { mutate: ( current: SessionAcpMeta | undefined, entry: { acp?: SessionAcpMeta } | undefined, ) => SessionAcpMeta | null | undefined; }; const nextMeta = params.mutate(currentEntry.acp, currentEntry); if (nextMeta === null) { return null; } currentEntry = { ...currentEntry, acp: nextMeta ?? currentEntry.acp, }; return currentEntry; }); const manager = new AcpSessionManager(); await manager.runTurn({ provenance: "system", cfg: baseCfg, sessionKey, text: "first", mode: "prompt", requestId: "r1", }); await expect( manager.updateSessionRuntimeOptions({ cfg: baseCfg, sessionKey, patch: { cwd: "/workspace/next" }, }), ).resolves.toEqual({ cwd: "/workspace/next", }); expect(currentEntry.acp.runtimeOptions).toEqual({ cwd: "/workspace/next", }); expect(currentEntry.acp.cwd).toBe("/workspace/next"); if (operation === "shutdown") { await disposeAcpSessionManagerInstance(manager, "gateway-shutdown"); expect(lifecycle).toEqual(["ensure:default", "close:default"]); return; } await manager.runTurn({ provenance: "system", cfg: baseCfg, sessionKey, text: "second", mode: "prompt", requestId: "r2", }); expect(runtimeState.ensureSession).toHaveBeenCalledTimes(2); expectRecordFields(mockCallArg(runtimeState.ensureSession, 1), { sessionKey, cwd: "/workspace/next", }); expect(lifecycle).toEqual(["ensure:default", "close:default", "ensure:/workspace/next"]); }, ); it("returns unsupported-control error when backend does not support set_config_option", async () => { const runtimeState = createRuntime(); const unsupportedRuntime: AcpRuntime = { ensureSession: runtimeState.ensureSession as AcpRuntime["ensureSession"], runTurn: runtimeState.runTurn as AcpRuntime["runTurn"], getCapabilities: vi.fn(async () => ({ controls: [] })), cancel: runtimeState.cancel as AcpRuntime["cancel"], close: runtimeState.close as AcpRuntime["close"], }; hoisted.requireAcpRuntimeBackendMock.mockReturnValue({ id: "acpx", runtime: unsupportedRuntime, }); hoisted.readAcpSessionEntryMock.mockReturnValue({ sessionKey: "agent:codex:acp:session-1", storeSessionKey: "agent:codex:acp:session-1", acp: readySessionMeta(), }); const manager = new AcpSessionManager(); await expectRejectedRecord( manager.setSessionConfigOption({ cfg: baseCfg, sessionKey: "agent:codex:acp:session-1", key: "model", value: "gpt-5.4", }), { code: "ACP_BACKEND_UNSUPPORTED_CONTROL" }, ); }); it("does not send automatic thinking when the backend advertises no thinking control", async () => { const runtimeState = createRuntime(); runtimeState.getCapabilities.mockResolvedValue({ controls: ["session/set_config_option"], configOptionKeys: ["mode", "model"], }); hoisted.requireAcpRuntimeBackendMock.mockReturnValue({ id: "acpx", runtime: runtimeState.runtime, }); hoisted.readAcpSessionEntryMock.mockReturnValue({ sessionKey: "agent:opencode:acp:session-1", storeSessionKey: "agent:opencode:acp:session-1", acp: readySessionMeta({ agent: "opencode", runtimeOptions: { thinking: "high" }, }), }); const manager = new AcpSessionManager(); await manager.runTurn({ provenance: "system", cfg: baseCfg, sessionKey: "agent:opencode:acp:session-1", text: "do work", mode: "prompt", requestId: "run-opencode-no-thinking", }); expect(runtimeState.setConfigOption).not.toHaveBeenCalled(); expect(runtimeState.runTurn).toHaveBeenCalledOnce(); }); it("maps automatic Codex thinking to the advertised reasoning effort control", async () => { const runtimeState = createRuntime(); runtimeState.getCapabilities.mockResolvedValue({ controls: ["session/set_config_option"], configOptionKeys: ["model", "reasoning_effort"], }); hoisted.requireAcpRuntimeBackendMock.mockReturnValue({ id: "acpx", runtime: runtimeState.runtime, }); hoisted.readAcpSessionEntryMock.mockReturnValue({ sessionKey: "agent:codex:acp:session-1", storeSessionKey: "agent:codex:acp:session-1", acp: readySessionMeta({ runtimeOptions: { thinking: "high" } }), }); const manager = new AcpSessionManager(); await manager.runTurn({ provenance: "system", cfg: baseCfg, sessionKey: "agent:codex:acp:session-1", text: "do work", mode: "prompt", requestId: "run-codex-reasoning", }); expectMockCallFields(runtimeState.setConfigOption, { key: "reasoning_effort", value: "high", }); }); it("rejects an explicit thinking write when the backend does not advertise it", async () => { const runtimeState = createRuntime(); runtimeState.getCapabilities.mockResolvedValue({ controls: ["session/set_config_option"], configOptionKeys: ["mode", "model"], }); hoisted.requireAcpRuntimeBackendMock.mockReturnValue({ id: "acpx", runtime: runtimeState.runtime, }); hoisted.readAcpSessionEntryMock.mockReturnValue({ sessionKey: "agent:opencode:acp:session-1", storeSessionKey: "agent:opencode:acp:session-1", acp: readySessionMeta({ agent: "opencode" }), }); const manager = new AcpSessionManager(); await expectRejectedRecord( manager.setSessionConfigOption({ cfg: baseCfg, sessionKey: "agent:opencode:acp:session-1", key: "thinking", value: "high", }), { code: "ACP_BACKEND_UNSUPPORTED_CONTROL" }, ); expect(runtimeState.setConfigOption).not.toHaveBeenCalled(); }); it("maps explicit thinking config updates to advertised effort keys", async () => { const runtimeState = createRuntime(); runtimeState.getCapabilities.mockResolvedValue({ controls: ["session/set_config_option", "session/status"], configOptionKeys: ["effort"], }); hoisted.requireAcpRuntimeBackendMock.mockReturnValue({ id: "acpx", runtime: runtimeState.runtime, }); hoisted.readAcpSessionEntryMock.mockReturnValue({ sessionKey: "agent:claude:acp:session-1", storeSessionKey: "agent:claude:acp:session-1", acp: readySessionMeta({ agent: "claude" }), }); const manager = new AcpSessionManager(); const nextOptions = await manager.setSessionConfigOption({ cfg: baseCfg, sessionKey: "agent:claude:acp:session-1", key: "thinking", value: "high", }); expectMockCallFields(runtimeState.setConfigOption, { key: "effort", value: "high", }); expect(nextOptions).toEqual({ thinking: "high" }); }); it("maps thinking config updates using status config options when capabilities omit keys", async () => { const runtimeState = createRuntime(); runtimeState.getCapabilities.mockResolvedValue({ controls: ["session/set_config_option", "session/status"], }); runtimeState.getStatus.mockResolvedValue({ summary: "status=alive", details: { configOptions: [{ id: "mode" }, { id: "model" }, { id: "effort" }], }, }); hoisted.requireAcpRuntimeBackendMock.mockReturnValue({ id: "acpx", runtime: runtimeState.runtime, }); hoisted.readAcpSessionEntryMock.mockReturnValue({ sessionKey: "agent:claude:acp:session-1", storeSessionKey: "agent:claude:acp:session-1", acp: readySessionMeta({ agent: "claude" }), }); const manager = new AcpSessionManager(); const nextOptions = await manager.setSessionConfigOption({ cfg: baseCfg, sessionKey: "agent:claude:acp:session-1", key: "thinking", value: "high", }); expect(runtimeState.getStatus).toHaveBeenCalled(); expectMockCallFields(runtimeState.setConfigOption, { key: "effort", value: "high", }); expect(nextOptions).toEqual({ thinking: "high" }); }); it("persists explicit native effort config updates as canonical thinking options", async () => { const runtimeState = createRuntime(); runtimeState.getCapabilities.mockResolvedValue({ controls: ["session/set_config_option", "session/status"], configOptionKeys: ["effort"], }); hoisted.requireAcpRuntimeBackendMock.mockReturnValue({ id: "acpx", runtime: runtimeState.runtime, }); hoisted.readAcpSessionEntryMock.mockReturnValue({ sessionKey: "agent:claude:acp:session-1", storeSessionKey: "agent:claude:acp:session-1", acp: readySessionMeta({ agent: "claude" }), }); const manager = new AcpSessionManager(); const nextOptions = await manager.setSessionConfigOption({ cfg: baseCfg, sessionKey: "agent:claude:acp:session-1", key: "effort", value: "high", }); expectMockCallFields(runtimeState.setConfigOption, { key: "effort", value: "high", }); expect(nextOptions).toEqual({ thinking: "high" }); }); it("persists explicit native permission_mode config updates as canonical permission profiles", async () => { const runtimeState = createRuntime(); runtimeState.getCapabilities.mockResolvedValue({ controls: ["session/set_config_option", "session/status"], configOptionKeys: ["permission_mode"], }); hoisted.requireAcpRuntimeBackendMock.mockReturnValue({ id: "acpx", runtime: runtimeState.runtime, }); hoisted.readAcpSessionEntryMock.mockReturnValue({ sessionKey: "agent:claude:acp:session-1", storeSessionKey: "agent:claude:acp:session-1", acp: readySessionMeta({ agent: "claude" }), }); const manager = new AcpSessionManager(); const nextOptions = await manager.setSessionConfigOption({ cfg: baseCfg, sessionKey: "agent:claude:acp:session-1", key: "permission_mode", value: "strict", }); expectMockCallFields(runtimeState.setConfigOption, { key: "permission_mode", value: "strict", }); expect(nextOptions).toEqual({ permissionProfile: "strict" }); }); });