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 <amp@ampcode.com>
This commit is contained in:
Peter Steinberger
2026-08-12 09:31:23 -07:00
committed by GitHub
parent 9594fe4623
commit b9f5548b29
+117 -273
View File
@@ -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: <T extends { provider: string }>(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<string>(),
getAgentRuntimeOptionalCommandSecretPaths: () => new Set<string>(),
getScopedChannelsCommandSecretTargets: () => ({ targetIds: new Set<string>() }),
}));
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<typeof createTestRegi
setActivePluginRegistry(registry);
}
function createOutboundSessionRouteFixture(params: {
cfg: OpenClawConfig;
agentId: string;
channel: string;
accountId?: string | null;
peer: { kind: "direct" | "group" | "channel"; id: string };
chatType: "direct" | "group" | "channel";
from: string;
to: string;
}) {
const baseSessionKey = buildOutboundBaseSessionKey(params);
return {
sessionKey: baseSessionKey,
baseSessionKey,
peer: params.peer,
chatType: params.chatType,
from: params.from,
to: params.to,
};
}
beforeEach(() => {
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<string, unknown> }) => 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",