mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:20:43 +00:00
test: trim agent test hotspots
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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>) => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user