From b9f5548b29ad6136d2425f203fb1493df8748ba5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 12 Aug 2026 09:31:23 -0700 Subject: [PATCH] test(commands): isolate agent command owner boundaries (#122704) Amp-Thread-ID: https://ampcode.com/threads/T-019fee8d-665d-707b-a380-23f2a6a1ce03 Co-authored-by: Amp --- src/commands/agent.test.ts | 390 +++++++++++-------------------------- 1 file changed, 117 insertions(+), 273 deletions(-) diff --git a/src/commands/agent.test.ts b/src/commands/agent.test.ts index 83d55509ef8d..01001c88c14b 100644 --- a/src/commands/agent.test.ts +++ b/src/commands/agent.test.ts @@ -2,7 +2,6 @@ import fs from "node:fs"; import path from "node:path"; import { expectDefined } from "@openclaw/normalization-core"; -import { buildChannelOutboundSessionRoute } from "openclaw/plugin-sdk/core"; import { withTempHome as withTempHomeBase } from "openclaw/plugin-sdk/test-env"; import { beforeEach, describe, expect, it, type MockInstance, vi } from "vitest"; // Register shared mocks before imports bind their production exports. @@ -31,6 +30,7 @@ import { clearSessionStoreCacheForTest } from "../config/sessions/store-writer-s import type { InternalSessionEntry as SessionEntry } from "../config/sessions/types.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { emitAgentEvent, onAgentEvent, resetAgentEventsForTest } from "../infra/agent-events.js"; +import { buildOutboundBaseSessionKey } from "../infra/outbound/base-session-key.js"; import type { PluginProviderRegistration } from "../plugins/registry.test-fixtures.js"; import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../plugins/runtime.js"; import type { RuntimeEnv } from "../runtime.js"; @@ -46,7 +46,6 @@ import { deliveryContextFromSession, normalizeSessionDeliveryState, } from "../utils/delivery-context.shared.js"; -import { getAgentHarnessPluginMocks } from "./agent-command-state.test-mocks.js"; import { agentCommand, agentCommandFromIngress } from "./agent.js"; import { createThrowingTestRuntime } from "./test-runtime-config-helpers.js"; @@ -54,7 +53,6 @@ const configIoMocks = vi.hoisted(() => ({ loadConfig: vi.fn(), readConfigFileSnapshotForWrite: vi.fn(), })); -const agentHarnessPluginMocks = getAgentHarnessPluginMocks(); vi.mock("../config/io.js", () => ({ getRuntimeConfig: configIoMocks.loadConfig, @@ -81,6 +79,99 @@ vi.mock("../agents/auth-profiles/source-check.js", () => ({ hasAnyAuthProfileStoreSource: vi.fn(() => false), })); +vi.mock("../auto-reply/reply/session-stable-reply-mode.js", () => ({ + // Session-stable policy has owner coverage in the reply resolver suite. This + // command suite only owns forwarding its result into CLI binding facts. + resolveSessionStableReplyMode: vi.fn(() => "automatic"), +})); + +vi.mock("../auto-reply/reply/source-reply-delivery-mode.js", () => ({ + // Source-reply policy has focused owner coverage. Command preparation only + // needs to distinguish synthetic turns before forwarding stable facts. + isSyntheticSourceReplyTurn: (params: { + inputProvenance?: { kind?: string }; + isHeartbeat?: boolean; + }) => + params.isHeartbeat === true || + params.inputProvenance?.kind === "inter_session" || + params.inputProvenance?.kind === "internal_system", +})); + +vi.mock("../agents/harness/selection.js", () => ({ + // Availability fallback has focused owner coverage in selection.test.ts. The + // command suite only needs a stable policy for auth-profile validation. + resolveAvailableAgentHarnessPolicy: vi.fn(() => ({ + runtime: "openclaw", + runtimeSource: "implicit", + })), +})); + +vi.mock("../agents/harness/hook-helpers.js", () => ({ + // Tool and transcript hook dispatch are exercised by their integration + // suites. No command fixture in this file registers either hook. + runAgentHarnessAfterToolCallHook: vi.fn(async () => undefined), + runAgentHarnessBeforeMessageWriteHook: ({ message }: { message: unknown }) => message, +})); + +vi.mock("../agents/thinking-runtime.js", () => ({ + // Runtime selection and catalog normalization have focused owner coverage in + // thinking-runtime.test.ts. Command tests only need stable policy handoffs. + hasResolvedThinkingCatalogEntry: (params: { + catalog?: Array<{ id: string; provider: string; reasoning?: boolean }>; + provider: string; + model: string; + }) => + params.catalog?.some( + (entry) => + entry.provider.toLowerCase() === params.provider.toLowerCase() && + entry.id === params.model && + entry.reasoning !== undefined, + ) ?? false, + normalizeThinkingCatalogProviders: (catalog: T[]) => + catalog.map((entry) => ({ ...entry, provider: entry.provider.toLowerCase() })), + resolveCandidateThinkingLevel: ({ level }: { level?: string }) => level, + resolveEffectiveAgentRuntime: () => "openclaw", +})); + +vi.mock("../agents/main-session-recovery/main-session-recovery-store.js", () => ({ + // Recovery-store fencing has dedicated store-backed coverage. None of these + // command cases enters a persisted recovery cycle. + claimMainSessionRecoveryOwner: vi.fn(async () => ({ kind: "not_required" })), + commitMainSessionRecovery: vi.fn(async () => undefined), + inspectMainSessionRecoveryRequired: vi.fn(async () => ({ kind: "not_required" })), + refreshMainSessionRecoveryOwner: vi.fn(async () => undefined), + releaseMainSessionRecoveryOwner: vi.fn(async () => undefined), +})); + +vi.mock("../cli/command-secret-targets.js", () => ({ + // Secret target discovery has dedicated owner coverage. These command + // fixtures contain no SecretRefs and only need empty discovery results. + getAgentRuntimeCommandSecretTargetIds: () => new Set(), + getAgentRuntimeOptionalCommandSecretPaths: () => new Set(), + getScopedChannelsCommandSecretTargets: () => ({ targetIds: new Set() }), +})); + +vi.mock("../infra/outbound/channel-bootstrap.runtime.js", () => ({ + // Every channel fixture in this suite is already active. Bootstrap discovery + // and its plugin-loader graph have focused owner coverage. + bootstrapOutboundChannelPlugin: vi.fn(() => undefined), + resetOutboundChannelBootstrapStateForTests: vi.fn(), +})); + +vi.mock("../config/sessions/inbound.runtime.js", () => ({ + // Explicit-recipient cases own route selection, not the downstream session + // persistence exercised by outbound-session owner tests. + resolveSessionStorePathCore: vi.fn(() => ""), + updateSessionLastRoute: vi.fn(async () => null), +})); + +vi.mock("../agents/command/assistant-transcript-repair.js", () => ({ + // Repair persistence, replay, and failure barriers have a focused owner + // suite. These command cases contain no pending transcript repair records. + persistAssistantTranscriptRepairRecord: vi.fn(async () => undefined), + repairPendingAssistantTranscriptTurns: vi.fn(async () => undefined), +})); + vi.mock("../agents/command/session-store.runtime.js", async () => { const accessor = await import("../config/sessions/session-accessor.js"); return { @@ -386,6 +477,27 @@ function installThinkingTestProviders(channels: Parameters { vi.clearAllMocks(); resetPluginRuntimeStateForTest(); @@ -405,39 +517,6 @@ beforeEach(() => { }); describe("agentCommand", () => { - it("passes one-shot OpenAI model overrides to harness plugin preparation", async () => { - await withTempHome(async (home) => { - const storePath = path.join(home, "sessions.json"); - const cfg = mockConfig(home, storePath, { models: undefined }); - - await agentCommand( - { - message: "hi", - agentId: "main", - model: "openai/gpt-5.2", - allowModelOverride: true, - }, - runtime, - ); - - expect(agentHarnessPluginMocks.ensureSelectedAgentHarnessPlugin).toHaveBeenCalledTimes(2); - const expectedPreparation = expect.objectContaining({ - config: cfg, - provider: "openai", - modelId: "gpt-5.2", - agentId: "main", - workspaceDir: path.join(home, "openclaw"), - }); - for (const callIndex of [1, 2] as const) { - expect(agentHarnessPluginMocks.ensureSelectedAgentHarnessPlugin).toHaveBeenNthCalledWith( - callIndex, - expectedPreparation, - ); - } - expectLastRunProviderModel("openai", "gpt-5.2"); - }); - }); - it("enforces ingress model override authorization", async () => { await expect( // Runtime guard for non-TS callers; TS callsites are statically typed. @@ -872,133 +951,6 @@ describe("agentCommand", () => { }); }); - it("installs a local gateway request scope for embedded agent dispatch", async () => { - await withTempHome(async (home) => { - const store = path.join(home, "sessions.json"); - mockConfig(home, store); - const { getPluginRuntimeGatewayRequestScope } = - await import("../plugins/runtime/gateway-request-scope.js"); - vi.mocked(attemptExecutionRuntime.runAgentAttempt).mockImplementationOnce(async () => { - const scope = getPluginRuntimeGatewayRequestScope(); - expect(scope?.context?.getRuntimeConfig()).toMatchObject({ - session: { store }, - }); - return createDefaultAgentResult(); - }); - - await agentCommand({ message: "ping", agentId: "main" }, runtime); - - expect(getPluginRuntimeGatewayRequestScope()).toBeUndefined(); - }); - }); - - it("runs direct ingress with a configured plugin-owned harness", async () => { - await withTempHome(async (home) => { - const store = path.join(home, "sessions.json"); - const workspaceDir = path.join(home, "openclaw"); - const pluginDir = path.join(home, "plugins", "ingress-proof"); - fs.mkdirSync(pluginDir, { recursive: true }); - fs.writeFileSync( - path.join(pluginDir, "openclaw.plugin.json"), - JSON.stringify({ - id: "ingress-proof", - name: "Ingress proof harness", - activation: { onStartup: false, onAgentHarnesses: ["ingress-proof"] }, - configSchema: { type: "object", additionalProperties: false }, - }), - ); - fs.writeFileSync( - path.join(pluginDir, "package.json"), - JSON.stringify({ - name: "ingress-proof", - version: "1.0.0", - type: "module", - openclaw: { extensions: ["./index.js"] }, - }), - ); - fs.writeFileSync( - path.join(pluginDir, "index.js"), - `export default { - id: "ingress-proof", - register(api) { - api.registerAgentHarness({ - id: "ingress-proof", - label: "Ingress proof harness", - supports: () => ({ supported: true }), - async runAttempt() { throw new Error("unused"); }, - }); - }, - };\n`, - ); - const cfg = { - meta: { migrations: { modelPolicyAllowlist: true } }, - plugins: { - allow: ["ingress-proof"], - entries: { "ingress-proof": { enabled: true } }, - load: { paths: [pluginDir] }, - }, - models: { - providers: { - "ingress-proof": { - api: "openai-responses", - baseUrl: "https://example.invalid/v1", - models: [ - { - id: "proof-model", - name: "Proof model", - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 128_000, - maxTokens: 4096, - agentRuntime: { id: "ingress-proof" }, - }, - ], - }, - }, - }, - agents: { - defaults: { - model: { primary: "ingress-proof/proof-model" }, - workspace: workspaceDir, - }, - }, - session: { store, mainKey: "main" }, - } as OpenClawConfig; - configIoMocks.loadConfig.mockReturnValue(cfg); - const actualRuntimePlugins = await vi.importActual< - typeof import("../agents/runtime-plugins.js") - >("../agents/runtime-plugins.js"); - const runtimePlugins = await import("../agents/runtime-plugins.js"); - vi.spyOn(runtimePlugins, "withAgentPluginRegistry").mockImplementationOnce( - actualRuntimePlugins.withAgentPluginRegistry, - ); - await agentCommandFromIngress( - { - message: "ping", - agentId: "main", - allowModelOverride: false, - }, - runtime, - ); - - expect(agentHarnessPluginMocks.ensureSelectedAgentHarnessPlugin).toHaveBeenCalledTimes(2); - const harnessSelectionCalls = agentHarnessPluginMocks.ensureSelectedAgentHarnessPlugin.mock - .calls as unknown as Array< - [ - Parameters< - typeof import("../agents/harness/runtime-plugin.js").ensureSelectedAgentHarnessPlugin - >[0], - ] - >; - for (const [{ pluginRegistry }] of harnessSelectionCalls) { - expect( - pluginRegistry?.agentHarnesses.some((entry) => entry.harness.id === "ingress-proof"), - ).toBe(true); - } - }); - }); - it("persists local overrides", async () => { await withTempHome(async (home) => { const store = path.join(home, "sessions.json"); @@ -1094,7 +1046,7 @@ describe("agentCommand", () => { }, resolveOutboundSessionRoute: (params) => { const chatId = params.target.replace(/^telegram:/i, ""); - return buildChannelOutboundSessionRoute({ + return createOutboundSessionRouteFixture({ cfg: params.cfg, agentId: params.agentId, channel: "telegram", @@ -1146,25 +1098,6 @@ describe("agentCommand", () => { }); }); - it("passes configured fast mode to embedded runs", async () => { - await withTempHome(async (home) => { - const store = path.join(home, "sessions.json"); - mockConfig(home, store, { - model: "openai/gpt-5.5", - models: { - "openai/gpt-5.5": { params: { fastMode: true } }, - }, - }); - - await agentCommand({ message: "ping", agentId: "main" }, runtime); - - const callArgs = getLastEmbeddedCall(); - expect(callArgs?.provider).toBe("openai"); - expect(callArgs?.model).toBe("gpt-5.5"); - expect(callArgs?.fastMode).toBe(true); - }); - }); - it("does not load the full model catalog for trusted explicit overrides without an allowlist", async () => { await withTempHome(async (home) => { const store = path.join(home, "sessions.json"); @@ -1189,31 +1122,6 @@ describe("agentCommand", () => { }); }); - it("uses no-tools plain prompt mode for one-shot model runs", async () => { - await withTempHome(async (home) => { - const store = path.join(home, "sessions.json"); - mockConfig(home, store, { models: {} }); - - await agentCommand( - { - message: "Reply with exactly OPENCLAW-MODEL-OK", - agentId: "main", - model: "openrouter/auto", - modelRun: true, - promptMode: "none", - }, - runtime, - ); - - const callArgs = getLastEmbeddedCall(); - expect(callArgs?.provider).toBe("openrouter"); - expect(callArgs?.model).toBe("openrouter/auto"); - expect(callArgs?.modelRun).toBe(true); - expect(callArgs?.promptMode).toBe("none"); - expect(callArgs?.disableTools).toBe(true); - }); - }); - it("bypasses ACP sessions for one-shot model runs", async () => { await withTempHome(async (home) => { const store = path.join(home, "sessions.json"); @@ -1420,44 +1328,6 @@ describe("agentCommand", () => { }); }); - it("does not publish Codex app-server events from the core command callback", async () => { - await withTempHome(async (home) => { - const store = path.join(home, "sessions.json"); - mockConfig(home, store); - - const codexEvents: Array<{ runId: string; phase?: string }> = []; - const stop = onAgentEvent((evt) => { - if (evt.stream !== "codex_app_server.lifecycle") { - return; - } - codexEvents.push({ - runId: evt.runId, - phase: typeof evt.data?.phase === "string" ? evt.data.phase : undefined, - }); - }); - - vi.mocked(runEmbeddedAgent).mockImplementationOnce(async (params) => { - ( - params as { - onAgentEvent?: (evt: { stream: string; data: Record }) => void; - } - ).onAgentEvent?.({ - stream: "codex_app_server.lifecycle", - data: { phase: "startup" }, - }); - return { - payloads: [{ text: "hello" }], - meta: { agentMeta: { provider: "p", model: "m" } }, - } as never; - }); - - await agentCommand({ message: "hi", to: "+1555", thinking: "low" }, runtime); - stop(); - - expect(codexEvents).toHaveLength(0); - }); - }); - it("probes the configured primary first for origin-backed auto session model overrides", async () => { await withTempHome(async (home) => { const store = path.join(home, "sessions.json"); @@ -2015,32 +1885,6 @@ describe("agentCommand", () => { }); }); - it("passes resolved default thinking level to embedded runs", async () => { - await withTempHome(async (home) => { - const store = path.join(home, "sessions.json"); - mockConfig(home, store, { - model: { primary: "openai/gpt-4.1-mini" }, - models: { - "anthropic/claude-opus-4-6": {}, - "openai/gpt-4.1-mini": {}, - }, - }); - mockModelCatalogOnce([ - { - id: "gpt-4.1-mini", - name: "GPT-4.1 Mini", - provider: "openai", - reasoning: true, - }, - ]); - - await agentCommand({ message: "hi", to: "+1555" }, runtime); - - expect(getLastEmbeddedCall()?.thinkLevel).toBe("low"); - expectLastRunProviderModel("openai", "gpt-4.1-mini"); - }); - }); - it("passes routing context to embedded runs", async () => { await withTempHome(async (home) => { const store = path.join(home, "sessions.json"); @@ -2095,7 +1939,7 @@ describe("agentCommand", () => { messaging: { resolveOutboundSessionRoute: (params) => { const chatType = params.target.endsWith("@g.us") ? "group" : "direct"; - return buildChannelOutboundSessionRoute({ + return createOutboundSessionRouteFixture({ cfg: params.cfg, agentId: params.agentId, channel: "whatsapp",