test: stabilize ci test harnesses

This commit is contained in:
Peter Steinberger
2026-03-23 07:57:51 +00:00
parent dc90d3b1d3
commit 771a78cc77
3 changed files with 104 additions and 83 deletions

View File

@@ -27,9 +27,23 @@ type SessionStoreEntry = {
accountId?: string;
};
type GatewayAgentInternalEvent = {
status?: string;
statusLabel?: string;
result?: string;
};
type GatewayAgentRequestParams = {
sessionKey?: string;
inputProvenance?: {
sourceSessionKey?: string;
};
internalEvents?: GatewayAgentInternalEvent[];
};
type GatewayRequest = {
method?: string;
params?: Record<string, unknown>;
params?: GatewayAgentRequestParams;
timeoutMs?: number;
expectFinal?: boolean;
};
@@ -271,9 +285,7 @@ describe("subagent registry lifecycle error grace", () => {
function readFirstAnnounceOutcome() {
const first = getAgentCalls()[0];
const event = first?.params?.internalEvents?.[0] as
| { status?: string; statusLabel?: string }
| undefined;
const event = first?.params?.internalEvents?.[0];
return {
status: event?.status,
error: event?.statusLabel,
@@ -298,10 +310,7 @@ describe("subagent registry lifecycle error grace", () => {
function getAgentResultsForChildSession(childSessionKey: string): string[] {
return getAgentCalls()
.filter((request) => request.params?.inputProvenance?.sourceSessionKey === childSessionKey)
.map((request) => {
const event = request.params?.internalEvents?.[0] as { result?: string } | undefined;
return event?.result ?? "";
});
.map((request) => request.params?.internalEvents?.[0]?.result ?? "");
}
it("ignores transient lifecycle errors when run retries and then ends successfully", async () => {
@@ -330,6 +339,7 @@ describe("subagent registry lifecycle error grace", () => {
await waitForAgentCallCount(1);
expect(readFirstAnnounceOutcome()?.status).toBe("ok");
await waitForCleanupCompleted("run-transient-error");
});
it("announces error when lifecycle error remains terminal after grace window", async () => {
@@ -350,6 +360,7 @@ describe("subagent registry lifecycle error grace", () => {
await waitForAgentCallCount(1);
expect(readFirstAnnounceOutcome()?.status).toBe("error");
expect(readFirstAnnounceOutcome()?.error).toContain("fatal failure");
await waitForCleanupCompleted("run-terminal-error");
});
it("freezes completion result at run termination across deferred announce retries", async () => {

View File

@@ -1,18 +1,8 @@
import { setTimeout as sleep } from "node:timers/promises";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { DEFAULT_GEMINI_EMBEDDING_MODEL } from "./embeddings-gemini.js";
import { mockPublicPinnedHostname } from "./test-helpers/ssrf.js";
vi.mock("../agents/model-auth.js", async () => {
const { createModelAuthMockModule } = await import("../test-utils/model-auth-mock.js");
return createModelAuthMockModule();
});
const importNodeLlamaCppMock = vi.fn();
vi.mock("./node-llama.js", () => ({
importNodeLlamaCpp: (...args: unknown[]) => importNodeLlamaCppMock(...args),
}));
const createFetchMock = () =>
vi.fn(async (_input?: unknown, _init?: unknown) => ({
ok: true,
@@ -37,11 +27,16 @@ type AuthModule = typeof import("../agents/model-auth.js");
type ResolvedProviderAuth = Awaited<ReturnType<AuthModule["resolveApiKeyForProvider"]>>;
let authModule: AuthModule;
let nodeLlamaModule: typeof import("./node-llama.js");
let createEmbeddingProvider: EmbeddingsModule["createEmbeddingProvider"];
let DEFAULT_LOCAL_MODEL: EmbeddingsModule["DEFAULT_LOCAL_MODEL"];
beforeAll(async () => {
beforeEach(async () => {
vi.resetModules();
authModule = await import("../agents/model-auth.js");
nodeLlamaModule = await import("./node-llama.js");
vi.spyOn(authModule, "resolveApiKeyForProvider");
vi.spyOn(nodeLlamaModule, "importNodeLlamaCpp");
({ createEmbeddingProvider, DEFAULT_LOCAL_MODEL } = await import("./embeddings.js"));
});
@@ -66,7 +61,7 @@ function mockResolvedProviderKey(apiKey = "provider-key") {
}
function mockMissingLocalEmbeddingDependency() {
importNodeLlamaCppMock.mockRejectedValue(
vi.mocked(nodeLlamaModule.importNodeLlamaCpp).mockRejectedValue(
Object.assign(new Error("Cannot find package 'node-llama-cpp'"), {
code: "ERR_MODULE_NOT_FOUND",
}),
@@ -456,7 +451,7 @@ describe("local embedding normalization", () => {
resolveModelFile: (modelPath: string, modelDirectory?: string) => Promise<string> = async () =>
"/fake/model.gguf",
): void {
importNodeLlamaCppMock.mockResolvedValue({
vi.mocked(nodeLlamaModule.importNodeLlamaCpp).mockResolvedValue({
getLlama: async () => ({
loadModel: vi.fn().mockResolvedValue({
createEmbeddingContext: vi.fn().mockResolvedValue({
@@ -468,7 +463,7 @@ describe("local embedding normalization", () => {
}),
resolveModelFile,
LlamaLogLevel: { error: 0 },
});
} as never);
}
it("normalizes local embeddings to magnitude ~1.0", async () => {
@@ -523,7 +518,7 @@ describe("local embedding normalization", () => {
[1.0, 1.0, 1.0, 1.0],
];
importNodeLlamaCppMock.mockResolvedValue({
vi.mocked(nodeLlamaModule.importNodeLlamaCpp).mockResolvedValue({
getLlama: async () => ({
loadModel: vi.fn().mockResolvedValue({
createEmbeddingContext: vi.fn().mockResolvedValue({
@@ -537,7 +532,7 @@ describe("local embedding normalization", () => {
}),
resolveModelFile: async () => "/fake/model.gguf",
LlamaLogLevel: { error: 0 },
});
} as never);
const result = await createLocalProviderForTest();

View File

@@ -1,10 +1,8 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createEmptyPluginRegistry } from "./registry.js";
import { setActivePluginRegistry } from "./runtime.js";
import {
resolvePluginWebSearchProviders,
resolveRuntimeWebSearchProviders,
} from "./web-search-providers.runtime.js";
type RegistryModule = typeof import("./registry.js");
type RuntimeModule = typeof import("./runtime.js");
type WebSearchProvidersRuntimeModule = typeof import("./web-search-providers.runtime.js");
const BUNDLED_WEB_SEARCH_PROVIDERS = [
{ pluginId: "brave", id: "brave", order: 10 },
@@ -15,69 +13,85 @@ const BUNDLED_WEB_SEARCH_PROVIDERS = [
{ pluginId: "firecrawl", id: "firecrawl", order: 60 },
{ pluginId: "exa", id: "exa", order: 65 },
{ pluginId: "tavily", id: "tavily", order: 70 },
{ pluginId: "duckduckgo", id: "duckduckgo", order: 100 },
] as const;
const { loadOpenClawPluginsMock } = vi.hoisted(() => ({
loadOpenClawPluginsMock: vi.fn((params?: { config?: { plugins?: Record<string, unknown> } }) => {
const plugins = params?.config?.plugins as
| {
enabled?: boolean;
allow?: string[];
entries?: Record<string, { enabled?: boolean }>;
}
| undefined;
if (plugins?.enabled === false) {
return { webSearchProviders: [] };
}
const allow = Array.isArray(plugins?.allow) && plugins.allow.length > 0 ? plugins.allow : null;
const entries = plugins?.entries ?? {};
const webSearchProviders = BUNDLED_WEB_SEARCH_PROVIDERS.filter((provider) => {
if (allow && !allow.includes(provider.pluginId)) {
return false;
}
if (entries[provider.pluginId]?.enabled === false) {
return false;
}
return true;
}).map((provider) => ({
pluginId: provider.pluginId,
pluginName: provider.pluginId,
source: "test" as const,
provider: {
id: provider.id,
label: provider.id,
hint: `${provider.id} provider`,
envVars: [`${provider.id.toUpperCase()}_API_KEY`],
placeholder: `${provider.id}-...`,
signupUrl: `https://example.com/${provider.id}`,
autoDetectOrder: provider.order,
credentialPath: `plugins.entries.${provider.pluginId}.config.webSearch.apiKey`,
getCredentialValue: () => "configured",
setCredentialValue: () => {},
createTool: () => ({
description: provider.id,
parameters: {},
execute: async () => ({}),
}),
},
}));
return { webSearchProviders };
}),
}));
let createEmptyPluginRegistry: RegistryModule["createEmptyPluginRegistry"];
let setActivePluginRegistry: RuntimeModule["setActivePluginRegistry"];
let resolvePluginWebSearchProviders: WebSearchProvidersRuntimeModule["resolvePluginWebSearchProviders"];
let resolveRuntimeWebSearchProviders: WebSearchProvidersRuntimeModule["resolveRuntimeWebSearchProviders"];
let loadOpenClawPluginsMock: ReturnType<typeof vi.fn>;
vi.mock("./loader.js", () => ({
loadOpenClawPlugins: loadOpenClawPluginsMock,
}));
function buildMockedWebSearchProviders(params?: {
config?: { plugins?: Record<string, unknown> };
}) {
const plugins = params?.config?.plugins as
| {
enabled?: boolean;
allow?: string[];
entries?: Record<string, { enabled?: boolean }>;
}
| undefined;
if (plugins?.enabled === false) {
return [];
}
const allow = Array.isArray(plugins?.allow) && plugins.allow.length > 0 ? plugins.allow : null;
const entries = plugins?.entries ?? {};
const webSearchProviders = BUNDLED_WEB_SEARCH_PROVIDERS.filter((provider) => {
if (allow && !allow.includes(provider.pluginId)) {
return false;
}
if (entries[provider.pluginId]?.enabled === false) {
return false;
}
return true;
}).map((provider) => ({
pluginId: provider.pluginId,
pluginName: provider.pluginId,
source: "test" as const,
provider: {
id: provider.id,
label: provider.id,
hint: `${provider.id} provider`,
envVars: [`${provider.id.toUpperCase()}_API_KEY`],
placeholder: `${provider.id}-...`,
signupUrl: `https://example.com/${provider.id}`,
autoDetectOrder: provider.order,
credentialPath: `plugins.entries.${provider.pluginId}.config.webSearch.apiKey`,
getCredentialValue: () => "configured",
setCredentialValue: () => {},
createTool: () => ({
description: provider.id,
parameters: {},
execute: async () => ({}),
}),
},
}));
return webSearchProviders;
}
describe("resolvePluginWebSearchProviders", () => {
beforeEach(() => {
loadOpenClawPluginsMock.mockClear();
beforeEach(async () => {
vi.resetModules();
({ createEmptyPluginRegistry } = await import("./registry.js"));
const loaderModule = await import("./loader.js");
loadOpenClawPluginsMock = vi
.spyOn(loaderModule, "loadOpenClawPlugins")
.mockImplementation((params) => {
const registry = createEmptyPluginRegistry();
registry.webSearchProviders = buildMockedWebSearchProviders(params);
return registry;
});
({ setActivePluginRegistry } = await import("./runtime.js"));
({ resolvePluginWebSearchProviders, resolveRuntimeWebSearchProviders } =
await import("./web-search-providers.runtime.js"));
setActivePluginRegistry(createEmptyPluginRegistry());
vi.useRealTimers();
});
afterEach(() => {
setActivePluginRegistry(createEmptyPluginRegistry());
vi.restoreAllMocks();
});
it("loads bundled providers through the plugin loader in auto-detect order", () => {
@@ -92,6 +106,7 @@ describe("resolvePluginWebSearchProviders", () => {
"firecrawl:firecrawl",
"exa:exa",
"tavily:tavily",
"duckduckgo:duckduckgo",
]);
expect(loadOpenClawPluginsMock).toHaveBeenCalledTimes(1);
});