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

@@ -73,8 +73,27 @@ function createHarness(params?: {
emoji?: Array<[string, string]>;
};
} | null>;
sasNoticeRetryDelayMs?: number;
}) {
const listeners = new Map<string, (...args: unknown[]) => void>();
const pendingTasks = new Set<Promise<void>>();
const runDetachedTask = vi.fn((_label: string, task: () => Promise<void>) => {
const promise = Promise.resolve()
.then(task)
.catch((error) => {
throw error;
})
.finally(() => {
pendingTasks.delete(promise);
});
pendingTasks.add(promise);
return promise;
});
const flushTasks = async () => {
while (pendingTasks.size > 0) {
await Promise.all(Array.from(pendingTasks));
}
};
const onRoomMessage = vi.fn(async () => {});
const listVerifications = vi.fn(async () => params?.verifications ?? []);
const ensureVerificationDmTracked = vi.fn(
@@ -154,6 +173,8 @@ function createHarness(params?: {
(typeof params?.startupMs === "number" ? () => params.startupMs : undefined),
formatNativeDependencyHint,
onRoomMessage,
runDetachedTask,
sasNoticeRetryDelayMs: params?.sasNoticeRetryDelayMs ?? 0,
});
const roomEventListener = listeners.get("room.event") as RoomEventListener | undefined;
@@ -172,6 +193,8 @@ function createHarness(params?: {
logger,
formatNativeDependencyHint,
logVerboseMessage,
flushTasks,
runDetachedTask,
roomMessageListener: listeners.get("room.message") as RoomEventListener | undefined,
failedDecryptListener: listeners.get("room.failed_decryption") as
| FailedDecryptListener
@@ -885,6 +908,7 @@ describe("registerMatrixMonitorEvents verification routing", () => {
"!dm:example.org": ["@alice:example.org", "@bot:example.org"],
},
verifications,
sasNoticeRetryDelayMs: 750,
});
try {

View File

@@ -186,6 +186,7 @@ export function registerMatrixMonitorEvents(params: {
formatNativeDependencyHint: PluginRuntime["system"]["formatNativeDependencyHint"];
onRoomMessage: (roomId: string, event: MatrixRawEvent) => void | Promise<void>;
runDetachedTask?: (label: string, task: () => Promise<void>) => Promise<void>;
sasNoticeRetryDelayMs?: number;
}): void {
const {
cfg,
@@ -205,6 +206,7 @@ export function registerMatrixMonitorEvents(params: {
formatNativeDependencyHint,
onRoomMessage,
runDetachedTask,
sasNoticeRetryDelayMs,
} = params;
const postHealthySyncDecryptFailureTracker = createMatrixPostHealthySyncDecryptFailureTracker({
getHealthySyncSinceMs,
@@ -217,6 +219,8 @@ export function registerMatrixMonitorEvents(params: {
dmPolicy,
readStoreAllowFrom,
logVerboseMessage,
runDetachedTask,
sasNoticeRetryDelayMs,
});
const runMonitorTask = (label: string, task: () => Promise<void>) => {

View File

@@ -280,6 +280,7 @@ async function resolveVerificationSasNoticeForSignal(
senderId: string;
flowId: string | null;
stage: MatrixVerificationStage;
sasNoticeRetryDelayMs?: number;
},
): Promise<{ summary: MatrixVerificationSummaryLike | null; sasNotice: string | null }> {
const summary = await resolveVerificationSummaryForSignal(client, params);
@@ -292,7 +293,9 @@ async function resolveVerificationSasNoticeForSignal(
};
}
await new Promise((resolve) => setTimeout(resolve, SAS_NOTICE_RETRY_DELAY_MS));
await new Promise((resolve) =>
setTimeout(resolve, params.sasNoticeRetryDelayMs ?? SAS_NOTICE_RETRY_DELAY_MS),
);
const retriedSummary = await resolveVerificationSummaryForSignal(client, params);
return {
summary: retriedSummary,
@@ -385,6 +388,8 @@ export function createMatrixVerificationEventRouter(params: {
dmPolicy: "open" | "pairing" | "allowlist" | "disabled";
readStoreAllowFrom: () => Promise<string[]>;
logVerboseMessage: (message: string) => void;
sasNoticeRetryDelayMs?: number;
runDetachedTask?: (label: string, task: () => Promise<void>) => Promise<void>;
}) {
const routerStartedAtMs = Date.now();
const routedVerificationEvents = new Set<string>();
@@ -539,7 +544,7 @@ export function createMatrixVerificationEventRouter(params: {
}
rememberVerificationRoom(roomId, event, signal.flowId);
void (async () => {
const routeTask = async () => {
if (!shouldEmitVerificationEventNotice(event)) {
params.logVerboseMessage(
`matrix: ignoring historical verification event room=${roomId} id=${event.event_id ?? "unknown"} type=${event.type ?? "unknown"}`,
@@ -586,6 +591,7 @@ export function createMatrixVerificationEventRouter(params: {
senderId,
flowId,
stage: signal.stage,
sasNoticeRetryDelayMs: params.sasNoticeRetryDelayMs,
}).catch(() => ({ summary: null, sasNotice: null }));
const notices: string[] = [];
@@ -613,9 +619,17 @@ export function createMatrixVerificationEventRouter(params: {
logVerboseMessage: params.logVerboseMessage,
});
}
})().catch((err) => {
params.logVerboseMessage(`matrix: failed routing verification event: ${String(err)}`);
});
};
if (params.runDetachedTask) {
void params.runDetachedTask(
`verification event handler room=${roomId} id=${event.event_id ?? "unknown"}`,
routeTask,
);
} else {
void routeTask().catch((err) => {
params.logVerboseMessage(`matrix: failed routing verification event: ${String(err)}`);
});
}
return true;
}

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);
}
}