test: speed up agent hotspot tests

This commit is contained in:
Peter Steinberger
2026-04-19 01:17:14 +01:00
parent f40bd56793
commit 1bef457cb6
6 changed files with 104 additions and 77 deletions

View File

@@ -25,6 +25,16 @@ vi.mock("../infra/outbound/message.js", () => ({
sendMessage: vi.fn(async () => ({ ok: true })),
}));
vi.mock("../infra/shell-env.js", async () => {
const mod =
await vi.importActual<typeof import("../infra/shell-env.js")>("../infra/shell-env.js");
return {
...mod,
getShellPathFromLoginShell: vi.fn(() => null),
resolveShellEnvFallbackTimeoutMs: vi.fn(() => 0),
};
});
function buildPreparedSystemRunPayload(rawInvokeParams: unknown) {
const invoke = (rawInvokeParams ?? {}) as {
params?: {

View File

@@ -19,6 +19,8 @@ const CORE_NON_SECRET_API_KEY_MARKERS = [
CUSTOM_LOCAL_AUTH_MARKER,
NON_ENV_SECRETREF_MARKER,
] as const;
let knownEnvApiKeyMarkersCache: Set<string> | undefined;
let knownNonSecretApiKeyMarkersCache: string[] | undefined;
// Legacy marker names kept for backward compatibility with existing models.json files.
const LEGACY_ENV_API_KEY_MARKERS = [
@@ -33,18 +35,24 @@ const LEGACY_ENV_API_KEY_MARKERS = [
];
function listKnownEnvApiKeyMarkers(): Set<string> {
return new Set([
knownEnvApiKeyMarkersCache ??= new Set([
...listKnownProviderEnvApiKeyNames(),
...LEGACY_ENV_API_KEY_MARKERS,
...AWS_SDK_ENV_MARKERS,
]);
return knownEnvApiKeyMarkersCache;
}
export function listKnownNonSecretApiKeyMarkers(): string[] {
const bundledMarkers = loadPluginManifestRegistry({ cache: true }).plugins.flatMap((plugin) =>
plugin.origin === "bundled" ? (plugin.nonSecretAuthMarkers ?? []) : [],
);
return [...new Set([...CORE_NON_SECRET_API_KEY_MARKERS, ...bundledMarkers])];
knownNonSecretApiKeyMarkersCache ??= [
...new Set([
...CORE_NON_SECRET_API_KEY_MARKERS,
...loadPluginManifestRegistry({ cache: true }).plugins.flatMap((plugin) =>
plugin.origin === "bundled" ? (plugin.nonSecretAuthMarkers ?? []) : [],
),
]),
];
return [...knownNonSecretApiKeyMarkersCache];
}
export function isAwsSdkAuthMarker(value: string): boolean {

View File

@@ -133,6 +133,12 @@ const OPENAI_RESPONSES_APIS = new Set([
const OPENAI_RESPONSES_PROVIDERS = new Set(["openai", "azure-openai", "azure-openai-responses"]);
const MOONSHOT_COMPAT_PROVIDERS = new Set(["moonshot", "kimi"]);
const MANIFEST_PROVIDER_ENDPOINT_CLASSES = new Set<ProviderEndpointClass>(["xai-native"]);
type ManifestProviderEndpointCacheEntry = {
endpointClass: ProviderEndpointClass;
hosts: readonly string[];
normalizedBaseUrls: readonly string[];
};
let manifestProviderEndpointCache: ManifestProviderEndpointCacheEntry[] | null = null;
function formatOpenClawUserAgent(version: string): string {
return `${OPENCLAW_ATTRIBUTION_ORIGINATOR}/${version}`;
@@ -192,27 +198,42 @@ function isManifestProviderEndpointClass(value: string): value is ProviderEndpoi
return MANIFEST_PROVIDER_ENDPOINT_CLASSES.has(value as ProviderEndpointClass);
}
function loadManifestProviderEndpointCache(): ManifestProviderEndpointCacheEntry[] {
if (!manifestProviderEndpointCache) {
const registry = loadPluginManifestRegistry({ cache: true });
const entries: ManifestProviderEndpointCacheEntry[] = [];
for (const plugin of registry.plugins) {
for (const endpoint of plugin.providerEndpoints ?? []) {
if (!isManifestProviderEndpointClass(endpoint.endpointClass)) {
continue;
}
entries.push({
endpointClass: endpoint.endpointClass,
hosts: (endpoint.hosts ?? []).map((host) => host.toLowerCase()),
normalizedBaseUrls: (endpoint.baseUrls ?? [])
.map((baseUrl) => normalizeComparableBaseUrl(baseUrl))
.filter((baseUrl): baseUrl is string => baseUrl !== undefined),
});
}
}
manifestProviderEndpointCache = entries;
}
return manifestProviderEndpointCache;
}
function resolveManifestProviderEndpoint(params: {
host: string;
normalizedBaseUrl?: string;
}): ProviderEndpointResolution | undefined {
const registry = loadPluginManifestRegistry({ cache: true });
for (const plugin of registry.plugins) {
for (const endpoint of plugin.providerEndpoints ?? []) {
if (!isManifestProviderEndpointClass(endpoint.endpointClass)) {
continue;
}
if (endpoint.hosts?.some((host) => host.toLowerCase() === params.host)) {
return { endpointClass: endpoint.endpointClass, hostname: params.host };
}
if (
params.normalizedBaseUrl &&
endpoint.baseUrls?.some(
(baseUrl) => normalizeComparableBaseUrl(baseUrl) === params.normalizedBaseUrl,
)
) {
return { endpointClass: endpoint.endpointClass, hostname: params.host };
}
for (const endpoint of loadManifestProviderEndpointCache()) {
if (endpoint.hosts.includes(params.host)) {
return { endpointClass: endpoint.endpointClass, hostname: params.host };
}
if (
params.normalizedBaseUrl &&
endpoint.normalizedBaseUrls.includes(params.normalizedBaseUrl)
) {
return { endpointClass: endpoint.endpointClass, hostname: params.host };
}
}
return undefined;

View File

@@ -2,7 +2,7 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import * as sessionStore from "../config/sessions/store.js";
import type { SessionEntry } from "../config/sessions/types.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { CallGatewayOptions } from "../gateway/call.js";
import {
@@ -112,6 +112,15 @@ function setSubagentControlDepsForTest(
__testing.setDepsForTest({
abortEmbeddedPiRun: () => false,
clearSessionQueues: () => ({ followupCleared: 0, laneCleared: 0, keys: [] }),
updateSessionStore: async <T>(
storePath: string,
mutator: (store: Record<string, SessionEntry>) => Promise<T> | T,
) => {
const store = JSON.parse(fs.readFileSync(storePath, "utf-8")) as Record<string, SessionEntry>;
const result = await mutator(store);
fs.writeFileSync(storePath, JSON.stringify(store, null, 2), "utf-8");
return result;
},
...overrides,
});
}
@@ -623,26 +632,24 @@ describe("killSubagentRunAdmin", () => {
startedAt: Date.now() - 4_000,
});
const updateSessionStoreSpy = vi
.spyOn(sessionStore, "updateSessionStore")
.mockRejectedValueOnce(new Error("session store unavailable"));
setSubagentControlDepsForTest({
updateSessionStore: async () => {
throw new Error("session store unavailable");
},
});
try {
const result = await killSubagentRunAdmin({
cfg: cfgWithSessionStore(storePath),
sessionKey: childSessionKey,
});
const result = await killSubagentRunAdmin({
cfg: cfgWithSessionStore(storePath),
sessionKey: childSessionKey,
});
expect(result).toMatchObject({
found: true,
killed: true,
runId: "run-worker-store-fail",
sessionKey: childSessionKey,
});
expect(getSubagentRunByChildSessionKey(childSessionKey)?.endedAt).toBeTypeOf("number");
} finally {
updateSessionStoreSpy.mockRestore();
}
expect(result).toMatchObject({
found: true,
killed: true,
runId: "run-worker-store-fail",
sessionKey: childSessionKey,
});
expect(getSubagentRunByChildSessionKey(childSessionKey)?.endedAt).toBeTypeOf("number");
});
});

View File

@@ -57,15 +57,18 @@ const SUBAGENT_REPLY_HISTORY_LIMIT = 50;
const steerRateLimit = new Map<string, number>();
type GatewayCaller = typeof callGateway;
type UpdateSessionStore = typeof updateSessionStore;
type AbortEmbeddedPiRun = (sessionId: string) => boolean;
type ClearSessionQueues = (keys: Array<string | undefined>) => ClearSessionQueueResult;
const defaultSubagentControlDeps = {
callGateway,
updateSessionStore,
};
let subagentControlDeps: {
callGateway: GatewayCaller;
updateSessionStore: UpdateSessionStore;
abortEmbeddedPiRun?: AbortEmbeddedPiRun;
clearSessionQueues?: ClearSessionQueues;
} = defaultSubagentControlDeps;
@@ -191,7 +194,7 @@ async function killSubagentRun(params: {
}
if (resolved.entry) {
try {
await updateSessionStore(resolved.storePath, (store) => {
await subagentControlDeps.updateSessionStore(resolved.storePath, (store) => {
const current = store[childSessionKey];
if (!current) {
return;
@@ -744,6 +747,7 @@ export const __testing = {
setDepsForTest(
overrides?: Partial<{
callGateway: GatewayCaller;
updateSessionStore: UpdateSessionStore;
abortEmbeddedPiRun: AbortEmbeddedPiRun;
clearSessionQueues: ClearSessionQueues;
}>,

View File

@@ -123,29 +123,6 @@ describe("subagent registry steer restarts", () => {
await vi.waitFor(assertion, { interval: 1, timeout: 1_000 });
};
const withPendingAgentWait = async <T>(run: () => Promise<T>): Promise<T> => {
const callGateway = vi.mocked((await import("../gateway/call.js")).callGateway);
const originalCallGateway = callGateway.getMockImplementation();
callGateway.mockImplementation(async (request: unknown) => {
const typed = request as { method?: string };
if (typed.method === "agent.wait") {
return new Promise<unknown>(() => undefined);
}
if (originalCallGateway) {
return originalCallGateway(request as Parameters<typeof callGateway>[0]);
}
return {};
});
try {
return await run();
} finally {
if (originalCallGateway) {
callGateway.mockImplementation(originalCallGateway);
}
}
};
const createDeferredAnnounceResolver = (): ((value: boolean) => void) => {
let resolveAnnounce!: (value: boolean) => void;
announceSpy.mockImplementationOnce(
@@ -254,7 +231,7 @@ describe("subagent registry steer restarts", () => {
});
it("suppresses announce for interrupted runs and only announces the replacement run", async () => {
await withPendingAgentWait(async () => {
{
registerRun({
runId: "run-old",
childSessionKey: "agent:main:subagent:steer",
@@ -303,11 +280,11 @@ describe("subagent registry steer restarts", () => {
const announce = (announceSpy.mock.calls[0]?.[0] ?? {}) as { childRunId?: string };
expect(announce.childRunId).toBe("run-new");
});
}
});
it("defers subagent_ended hook for completion-mode runs until announce delivery resolves", async () => {
await withPendingAgentWait(async () => {
{
const resolveAnnounce = createDeferredAnnounceResolver();
registerCompletionModeRun(
"run-completion-delayed",
@@ -337,11 +314,11 @@ describe("subagent registry steer restarts", () => {
requesterSessionKey: MAIN_REQUESTER_SESSION_KEY,
}),
);
});
}
});
it("does not emit subagent_ended on completion for persistent session-mode runs", async () => {
await withPendingAgentWait(async () => {
{
const resolveAnnounce = createDeferredAnnounceResolver();
registerCompletionModeRun(
"run-persistent-session",
@@ -363,11 +340,11 @@ describe("subagent registry steer restarts", () => {
expect(run?.runId).toBe("run-persistent-session");
expect(run?.cleanupCompletedAt).toBeTypeOf("number");
expect(run?.endedHookEmittedAt).toBeUndefined();
});
}
});
it("clears announce retry state when replacing after steer restart", async () => {
await withPendingAgentWait(async () => {
{
registerRun({
runId: "run-retry-reset-old",
childSessionKey: "agent:main:subagent:retry-reset",
@@ -388,11 +365,11 @@ describe("subagent registry steer restarts", () => {
});
expect(run.announceRetryCount).toBeUndefined();
expect(run.lastAnnounceRetryAt).toBeUndefined();
});
}
});
it("clears terminal lifecycle state when replacing after steer restart", async () => {
await withPendingAgentWait(async () => {
{
registerRun({
runId: "run-terminal-state-old",
childSessionKey: "agent:main:subagent:terminal-state",
@@ -434,7 +411,7 @@ describe("subagent registry steer restarts", () => {
reason: "subagent-status",
}),
);
});
}
});
it("clears frozen completion fields when replacing after steer restart", () => {
@@ -707,7 +684,7 @@ describe("subagent registry steer restarts", () => {
});
it("retries completion-mode announce delivery with backoff and then gives up after retry limit", async () => {
await withPendingAgentWait(async () => {
{
vi.useFakeTimers();
try {
announceSpy.mockResolvedValue(false);
@@ -742,7 +719,7 @@ describe("subagent registry steer restarts", () => {
} finally {
vi.useRealTimers();
}
});
}
});
it("keeps completion cleanup pending while descendants are still active", async () => {