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