test: trim agent test hotspots

This commit is contained in:
Peter Steinberger
2026-04-17 04:59:26 +01:00
parent 7ae670e501
commit 35dcd06764
12 changed files with 126 additions and 116 deletions

View File

@@ -4,6 +4,7 @@ import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import * as acpSessionManager from "../acp/control-plane/manager.js";
import type { AcpInitializeSessionInput } from "../acp/control-plane/manager.types.js";
import * as channelPlugins from "../channels/plugins/index.js";
import {
clearRuntimeConfigSnapshot,
setRuntimeConfigSnapshot,
@@ -82,6 +83,7 @@ const hoisted = vi.hoisted(() => {
});
const callGatewaySpy = vi.spyOn(gatewayCall, "callGateway");
const getChannelPluginSpy = vi.spyOn(channelPlugins, "getChannelPlugin");
const getAcpSessionManagerSpy = vi.spyOn(acpSessionManager, "getAcpSessionManager");
const loadSessionStoreSpy = vi.spyOn(sessionStore, "loadSessionStore");
const resolveStorePathSpy = vi.spyOn(sessionPaths, "resolveStorePath");
@@ -350,6 +352,7 @@ describe("spawnAcpDirect", () => {
replaceSpawnConfig(createDefaultSpawnConfig());
resetTaskRegistryForTests();
hoisted.areHeartbeatsEnabledMock.mockReset().mockReturnValue(true);
getChannelPluginSpy.mockReset().mockReturnValue(undefined);
hoisted.callGatewayMock.mockReset();
hoisted.callGatewayMock.mockImplementation(async (argsUnknown: unknown) => {

View File

@@ -88,7 +88,8 @@ export function normalizeAgentCommandReplyPayloads(params: {
if (!channel) {
return payloads as ReplyPayload[];
}
const deliveryPlugin = getChannelPlugin(channel);
const applyChannelTransforms = params.applyChannelTransforms ?? true;
const deliveryPlugin = applyChannelTransforms ? getChannelPlugin(channel) : undefined;
const sessionKey = params.outboundSession?.key ?? params.opts.sessionKey;
const agentId =
@@ -113,7 +114,6 @@ export function normalizeAgentCommandReplyPayloads(params: {
});
}
const responsePrefixContext = replyPrefix.responsePrefixContextProvider();
const applyChannelTransforms = params.applyChannelTransforms ?? true;
const transformReplyPayload = deliveryPlugin?.messaging?.transformReplyPayload
? (payload: ReplyPayload) =>
deliveryPlugin.messaging?.transformReplyPayload?.({
@@ -186,9 +186,10 @@ export async function deliverAgentCommandResult(params: {
resolvedChannel: deliveryChannel,
};
// Channel docking: delivery channels are resolved via plugin registry.
const deliveryPlugin = !isInternalMessageChannel(deliveryChannel)
? getChannelPlugin(normalizeChannelId(deliveryChannel) ?? deliveryChannel)
: undefined;
const deliveryPlugin =
deliver && !isInternalMessageChannel(deliveryChannel)
? getChannelPlugin(normalizeChannelId(deliveryChannel) ?? deliveryChannel)
: undefined;
const isDeliveryChannelKnown =
isInternalMessageChannel(deliveryChannel) || Boolean(deliveryPlugin);

View File

@@ -12,6 +12,10 @@ vi.mock("../../plugins/provider-runtime.js", () => ({
resolveProviderRuntimePlugin: () => undefined,
}));
vi.mock("../../plugins/provider-hook-runtime.js", () => ({
resolveProviderRuntimePlugin: () => undefined,
}));
function buildSafeguardFactories(cfg: OpenClawConfig) {
const sessionManager = {} as SessionManager;
const model = {

View File

@@ -5,6 +5,7 @@ import {
clearMemoryPluginState,
registerMemoryPromptSection,
} from "../../../plugins/memory-state.js";
import { derivePromptTokens } from "../../usage.js";
import {
type AttemptContextEngine,
buildLoopPromptCacheInfo,
@@ -15,9 +16,8 @@ import {
resolvePromptCacheTouchTimestamp,
runAttemptContextEngineBootstrap,
} from "./attempt.context-engine-helpers.js";
import { buildAfterTurnRuntimeContext } from "./attempt.prompt-helpers.js";
import {
cleanupTempPaths,
createContextEngineAttemptRunner,
createContextEngineBootstrapAndAssemble,
expectCalledWithSessionKey,
getHoisted,
@@ -113,7 +113,6 @@ async function finalizeTurn(
describe("runEmbeddedAttempt context engine sessionKey forwarding", () => {
const sessionKey = "agent:main:discord:channel:test-ctx-engine";
const tempPaths: string[] = [];
beforeEach(() => {
resetEmbeddedAttemptHarness();
clearMemoryPluginState();
@@ -121,7 +120,6 @@ describe("runEmbeddedAttempt context engine sessionKey forwarding", () => {
});
afterEach(async () => {
await cleanupTempPaths(tempPaths);
clearMemoryPluginState();
vi.restoreAllMocks();
});
@@ -493,33 +491,48 @@ describe("runEmbeddedAttempt context engine sessionKey forwarding", () => {
}) => {},
);
await createContextEngineAttemptRunner({
sessionKey,
tempPaths,
contextEngine: {
assemble: async ({ messages }) => ({
messages,
estimatedTokens: 1,
}),
afterTurn,
},
sessionPrompt: async (session) => {
session.messages = [
...session.messages,
{
role: "assistant",
content: "done",
timestamp: 2,
usage: {
input: 10,
output: 5,
cacheRead: 40,
cacheWrite: 2,
total: 57,
},
} as unknown as AgentMessage,
];
},
const messagesSnapshot = [
seedMessage,
{
role: "assistant",
content: "done",
timestamp: 2,
usage: {
input: 10,
output: 5,
cacheRead: 40,
cacheWrite: 2,
total: 57,
},
} as unknown as AgentMessage,
];
const promptCache = buildLoopPromptCacheInfo({
messagesSnapshot,
prePromptMessageCount: 1,
});
await finalizeTurn(sessionKey, createTestContextEngine({ afterTurn }), {
messagesSnapshot,
prePromptMessageCount: 1,
runtimeContext: buildAfterTurnRuntimeContext({
attempt: {
sessionKey,
config: {} as never,
skillsSnapshot: undefined,
senderIsOwner: true,
provider: "openai",
modelId: "gpt-test",
thinkLevel: "off",
reasoningLevel: undefined,
extraSystemPrompt: undefined,
ownerNumbers: undefined,
},
workspaceDir: "/tmp/workspace",
agentDir: "/tmp/agent",
tokenBudget: 2048,
currentTokenCount: derivePromptTokens(promptCache?.lastCallUsage),
promptCache,
}),
});
expect(afterTurn).toHaveBeenCalledWith(

View File

@@ -11,6 +11,10 @@ vi.mock("../../plugins/provider-runtime.js", () => ({
validateProviderReplayTurnsWithPlugin: () => undefined,
}));
vi.mock("../../plugins/provider-hook-runtime.js", () => ({
resolveProviderRuntimePlugin: () => undefined,
}));
describe("sanitizeSessionHistory toolResult details stripping", () => {
it("strips toolResult.details so untrusted payloads are not fed back to the model", async () => {
const sm = SessionManager.inMemory();

View File

@@ -5,7 +5,6 @@ import { describe, expect, it } from "vitest";
import { resolvePiCredentialMapFromStore } from "./pi-auth-credentials.js";
import {
addEnvBackedPiCredentials,
normalizeDiscoveredPiModel,
scrubLegacyStaticAuthJsonEntriesForDiscovery,
} from "./pi-model-discovery.js";
@@ -154,57 +153,4 @@ describe("discoverAuthStorage", () => {
}
}
});
it("normalizes stale discovered openai-codex rows when api metadata is missing", () => {
const normalized = normalizeDiscoveredPiModel(
{
id: "gpt-5.4",
name: "gpt-5.4",
provider: "openai-codex",
baseUrl: "https://chatgpt.com/backend-api",
reasoning: true,
input: ["text", "image"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 1_050_000,
contextTokens: 272_000,
maxTokens: 128_000,
},
"/tmp/agent",
) as {
api?: string;
baseUrl?: string;
};
expect(normalized).toMatchObject({
api: "openai-codex-responses",
baseUrl: "https://chatgpt.com/backend-api",
});
});
it("canonicalizes stale discovered openai-codex backend-api/v1 rows", () => {
const normalized = normalizeDiscoveredPiModel(
{
id: "gpt-5.4",
name: "gpt-5.4",
provider: "openai-codex",
api: "openai-codex-responses",
baseUrl: "https://chatgpt.com/backend-api/v1",
reasoning: true,
input: ["text", "image"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 1_050_000,
contextTokens: 272_000,
maxTokens: 128_000,
},
"/tmp/agent",
) as {
api?: string;
baseUrl?: string;
};
expect(normalized).toMatchObject({
api: "openai-codex-responses",
baseUrl: "https://chatgpt.com/backend-api",
});
});
});

View File

@@ -337,7 +337,6 @@ describe("message tool agent routing", () => {
describe("message tool explicit target guard", () => {
it("requires an explicit target for upload-file when configured", async () => {
const tool = createMessageTool({
config: {} as never,
runMessageAction: mocks.runMessageAction as never,
requireExplicitTarget: true,
currentChannelProvider: "slack",
@@ -365,7 +364,6 @@ describe("message tool explicit target guard", () => {
});
const tool = createMessageTool({
config: {} as never,
runMessageAction: mocks.runMessageAction as never,
requireExplicitTarget: true,
currentChannelProvider: "slack",

View File

@@ -685,6 +685,21 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool {
const action = readStringParam(params, "action", {
required: true,
}) as ChannelMessageActionName;
const requireExplicitTarget = options?.requireExplicitTarget === true;
if (requireExplicitTarget && actionNeedsExplicitTarget(action)) {
const explicitTarget =
(typeof params.target === "string" && params.target.trim().length > 0) ||
(typeof params.to === "string" && params.to.trim().length > 0) ||
(typeof params.channelId === "string" && params.channelId.trim().length > 0) ||
(Array.isArray(params.targets) &&
params.targets.some((value) => typeof value === "string" && value.trim().length > 0));
if (!explicitTarget) {
throw new Error(
"Explicit message target required for this run. Provide target/targets (and channel when needed).",
);
}
}
let cfg = options?.config;
if (!cfg) {
const loadedRaw = loadConfigForTool();
@@ -711,20 +726,6 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool {
})
).resolvedConfig;
}
const requireExplicitTarget = options?.requireExplicitTarget === true;
if (requireExplicitTarget && actionNeedsExplicitTarget(action)) {
const explicitTarget =
(typeof params.target === "string" && params.target.trim().length > 0) ||
(typeof params.to === "string" && params.to.trim().length > 0) ||
(typeof params.channelId === "string" && params.channelId.trim().length > 0) ||
(Array.isArray(params.targets) &&
params.targets.some((value) => typeof value === "string" && value.trim().length > 0));
if (!explicitTarget) {
throw new Error(
"Explicit message target required for this run. Provide target/targets (and channel when needed).",
);
}
}
const accountId = readStringParam(params, "accountId") ?? agentAccountId;
if (accountId) {

View File

@@ -151,11 +151,9 @@ export async function readResponseText(
// Best-effort: return whatever we decoded so far.
} finally {
if (truncated) {
try {
await reader.cancel();
} catch {
// ignore
}
// Some mocked or non-compliant streams never settle cancel(); do not
// let cleanup turn a bounded read into a hung fetch.
void reader.cancel().catch(() => undefined);
}
}