test: dedupe redundant test coverage

This commit is contained in:
Peter Steinberger
2026-05-30 06:27:01 +01:00
parent 9090f6b1c4
commit fcdc25ba64
19 changed files with 248 additions and 514 deletions

View File

@@ -36,6 +36,21 @@ function makeMinimalResponse(threadOverrides: Record<string, unknown> = {}) {
};
}
describe("Codex thread response validators", () => {
it("normalizes missing sessionId from id for start and resume responses", () => {
for (const assertResponse of [
assertCodexThreadStartResponse,
assertCodexThreadResumeResponse,
]) {
const response = makeMinimalResponse({ sessionId: undefined });
delete (response.thread as Record<string, unknown>).sessionId;
const result = assertResponse(response);
expect(result.thread.id).toBe("thread-1");
expect(result.thread.sessionId).toBe("thread-1");
}
});
});
describe("assertCodexThreadStartResponse", () => {
it("accepts response with both id and sessionId", () => {
const response = makeMinimalResponse();
@@ -44,15 +59,6 @@ describe("assertCodexThreadStartResponse", () => {
expect(result.thread.sessionId).toBe("session-1");
});
it("normalizes missing sessionId from id", () => {
const response = makeMinimalResponse({ sessionId: undefined });
// Remove the sessionId key entirely
delete (response.thread as Record<string, unknown>).sessionId;
const result = assertCodexThreadStartResponse(response);
expect(result.thread.id).toBe("thread-1");
expect(result.thread.sessionId).toBe("thread-1");
});
it("normalizes missing id from sessionId", () => {
const response = makeMinimalResponse({ id: undefined, sessionId: "session-1" });
delete (response.thread as Record<string, unknown>).id;
@@ -66,16 +72,6 @@ describe("assertCodexThreadStartResponse", () => {
});
});
describe("assertCodexThreadResumeResponse", () => {
it("normalizes missing sessionId from id", () => {
const response = makeMinimalResponse({ sessionId: undefined });
delete (response.thread as Record<string, unknown>).sessionId;
const result = assertCodexThreadResumeResponse(response);
expect(result.thread.id).toBe("thread-1");
expect(result.thread.sessionId).toBe("thread-1");
});
});
describe("readCodexModelListResponse", () => {
it("applies defaults from generated schemas behind local refs", () => {
const response = readCodexModelListResponse({

View File

@@ -35,16 +35,18 @@ describe("requestCodexAppServerJson sandbox guard", () => {
});
it("fails closed before raw app-server bypass methods when exec host=node is active", async () => {
await expect(
requestCodexAppServerJson({
method: "command/exec",
requestParams: { command: ["sh", "-lc", "id"] },
config: { tools: { exec: { host: "node", node: "worker-1" } } },
sessionKey: "node-session",
}),
).rejects.toThrow(
"Codex-native app-server method `command/exec` is unavailable because OpenClaw exec host=node is active for this session.",
);
for (const method of ["command/exec", "process/spawn"]) {
await expect(
requestCodexAppServerJson({
method,
requestParams: { command: ["sh", "-lc", "id"] },
config: { tools: { exec: { host: "node", node: "worker-1" } } },
sessionKey: "node-session",
}),
).rejects.toThrow(
`Codex-native app-server method \`${method}\` is unavailable because OpenClaw exec host=node is active for this session.`,
);
}
expect(sharedClientMocks.getSharedCodexAppServerClient).not.toHaveBeenCalled();
});
@@ -65,21 +67,6 @@ describe("requestCodexAppServerJson sandbox guard", () => {
expect(request).toHaveBeenCalledWith("thread/list", { limit: 10 }, { timeoutMs: 60_000 });
});
it("fails closed before raw app-server bypass methods when exec host=node is active", async () => {
await expect(
requestCodexAppServerJson({
method: "process/spawn",
requestParams: { command: ["sh", "-lc", "id"] },
config: { tools: { exec: { host: "node", node: "worker-1" } } },
sessionKey: "node-session",
}),
).rejects.toThrow(
"Codex-native app-server method `process/spawn` is unavailable because OpenClaw exec host=node is active for this session.",
);
expect(sharedClientMocks.getSharedCodexAppServerClient).not.toHaveBeenCalled();
});
it("fails closed for config-level exec host=node even without a session key", async () => {
await expect(
requestCodexAppServerJson({

View File

@@ -48,10 +48,7 @@ const surfaceEntry = (id: string, surfaceTag: string, extra: Record<string, unkn
},
});
async function withLiveFetch(
mockFetch: ReturnType<typeof vi.fn>,
run: () => Promise<void>,
) {
async function withLiveFetch(mockFetch: ReturnType<typeof vi.fn>, run: () => Promise<void>) {
const env = { ...process.env };
delete process.env.NODE_ENV;
delete process.env.VITEST;
@@ -79,12 +76,14 @@ async function withLiveFetch(
}
}
describe("listDeepInfraImageGenCatalog", () => {
it("returns null when no discoveryApiKey is configured", async () => {
const result = await listDeepInfraImageGenCatalog(makeCtx());
expect(result).toBeNull();
describe("DeepInfra generation catalogs", () => {
it("return null when no discoveryApiKey is configured", async () => {
await expect(listDeepInfraImageGenCatalog(makeCtx())).resolves.toBeNull();
await expect(listDeepInfraVideoGenCatalog(makeCtx())).resolves.toBeNull();
});
});
describe("listDeepInfraImageGenCatalog", () => {
it("returns null when live discovery succeeds but the response has zero image-gen entries", async () => {
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
@@ -155,11 +154,6 @@ describe("listDeepInfraImageGenCatalog", () => {
});
describe("listDeepInfraVideoGenCatalog", () => {
it("returns null when no discoveryApiKey is configured", async () => {
const result = await listDeepInfraVideoGenCatalog(makeCtx());
expect(result).toBeNull();
});
it("returns null when live discovery succeeds but the response has zero video-gen entries", async () => {
// Current production state: TTS/STT/T2V models lack the OPENAI tag the
// backend filter requires, so a key-authenticated discovery still
@@ -208,10 +202,7 @@ describe("listDeepInfraVideoGenCatalog", () => {
await withLiveFetch(mockFetch, async () => {
const result = await listDeepInfraVideoGenCatalog(withKeyCtx());
expect(result).not.toBeNull();
expect(result?.map((e) => e.model)).toEqual([
"Wan-AI/Wan2.6-T2V",
"ByteDance/Seedance-2.0",
]);
expect(result?.map((e) => e.model)).toEqual(["Wan-AI/Wan2.6-T2V", "ByteDance/Seedance-2.0"]);
const first = result?.[0];
expect(first?.kind).toBe("video_generation");
expect(first?.capabilities?.generate?.supportsAspectRatio).toBe(true);

View File

@@ -846,6 +846,7 @@ describe("containerSendReaction", () => {
emoji: "👍",
targetAuthor: "+15550001111",
targetTimestamp: 1699999999999,
groupId: "group-123",
});
expect(result).toEqual({ timestamp: 1700000000000 });
@@ -856,30 +857,10 @@ describe("containerSendReaction", () => {
reaction: "👍",
target_author: "+15550001111",
timestamp: 1699999999999,
group_id: "group-123",
}),
);
});
it("includes group_id when provided", async () => {
mockFetch.mockResolvedValue({
ok: true,
status: 200,
text: async () => JSON.stringify({}),
});
await containerSendReaction({
baseUrl: "http://localhost:8080",
account: "+14259798283",
recipient: "+15550001111",
emoji: "❤️",
targetAuthor: "+15550001111",
targetTimestamp: 1699999999999,
groupId: "group-123",
});
const body = parseFetchBody();
expect(body.group_id).toBe("group-123");
});
});
describe("containerRpcRequest reactions", () => {
@@ -933,6 +914,7 @@ describe("containerRemoveReaction", () => {
emoji: "👍",
targetAuthor: "+15550001111",
targetTimestamp: 1699999999999,
groupId: "group-123",
});
expect(result).toEqual({ timestamp: 1700000000000 });
@@ -946,28 +928,8 @@ describe("containerRemoveReaction", () => {
reaction: "👍",
target_author: "+15550001111",
timestamp: 1699999999999,
group_id: "group-123",
}),
);
});
it("includes group_id when provided", async () => {
mockFetch.mockResolvedValue({
ok: true,
status: 200,
text: async () => JSON.stringify({}),
});
await containerRemoveReaction({
baseUrl: "http://localhost:8080",
account: "+14259798283",
recipient: "+15550001111",
emoji: "❤️",
targetAuthor: "+15550001111",
targetTimestamp: 1699999999999,
groupId: "group-123",
});
const body = parseFetchBody();
expect(body.group_id).toBe("group-123");
});
});

View File

@@ -149,58 +149,72 @@ describe("normalizeModelCompat — Anthropic baseUrl", () => {
});
describe("normalizeModelCompat", () => {
it("forces supportsDeveloperRole off for z.ai models", () => {
expectSupportsDeveloperRoleForcedOff();
});
it.each([
["z.ai models", undefined],
["moonshot models", { provider: "moonshot", baseUrl: "https://api.moonshot.ai/v1" }],
[
"custom moonshot-compatible endpoints",
{ provider: "custom-kimi", baseUrl: "https://api.moonshot.cn/v1" },
],
[
"DashScope provider ids",
{ provider: "dashscope", baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1" },
],
[
"DashScope-compatible endpoints",
{
provider: "custom-qwen",
baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
},
],
[
"Azure OpenAI chat completions",
{ provider: "azure-openai", baseUrl: "https://my-deployment.openai.azure.com/openai" },
],
[
"generic custom openai-completions providers",
{ provider: "custom-cpa", baseUrl: "https://cpa.example.com/v1" },
],
[
"Qwen proxy via openai-completions",
{ provider: "qwen-proxy", baseUrl: "https://qwen-api.example.org/compatible-mode/v1" },
],
[
"malformed baseUrl values",
{ provider: "custom-cpa", baseUrl: "://api.openai.com malformed" },
],
] satisfies Array<[string, Partial<Model> | undefined]>)(
"forces supportsDeveloperRole off for %s",
(_name, overrides) => {
expectSupportsDeveloperRoleForcedOff(overrides);
},
);
it("forces supportsDeveloperRole off for moonshot models", () => {
expectSupportsDeveloperRoleForcedOff({
provider: "moonshot",
baseUrl: "https://api.moonshot.ai/v1",
});
});
it("forces supportsDeveloperRole off for custom moonshot-compatible endpoints", () => {
expectSupportsDeveloperRoleForcedOff({
provider: "custom-kimi",
baseUrl: "https://api.moonshot.cn/v1",
});
});
it("forces supportsDeveloperRole off for DashScope provider ids", () => {
expectSupportsDeveloperRoleForcedOff({
provider: "dashscope",
baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1",
});
});
it("forces supportsDeveloperRole off for DashScope-compatible endpoints", () => {
expectSupportsDeveloperRoleForcedOff({
provider: "custom-qwen",
baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
});
});
it("keeps supportsUsageInStreaming on for native Qwen endpoints", () => {
expectNativeStreamingSupported({
provider: "qwen",
baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
});
});
it("keeps supportsUsageInStreaming on for DashScope-compatible endpoints regardless of provider id", () => {
expectNativeStreamingSupported({
provider: "custom-qwen",
baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
});
});
it("keeps supportsUsageInStreaming on for Moonshot-native endpoints regardless of provider id", () => {
expectNativeStreamingSupported({
provider: "custom-kimi",
baseUrl: "https://api.moonshot.ai/v1",
});
});
it.each([
[
"native Qwen endpoints",
{
provider: "qwen",
baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
},
],
[
"DashScope-compatible endpoints regardless of provider id",
{
provider: "custom-qwen",
baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
},
],
[
"Moonshot-native endpoints regardless of provider id",
{ provider: "custom-kimi", baseUrl: "https://api.moonshot.ai/v1" },
],
] satisfies Array<[string, Partial<Model>]>)(
"keeps supportsUsageInStreaming on for %s",
(_name, overrides) => {
expectNativeStreamingSupported(overrides);
},
);
it("leaves native api.openai.com model untouched", () => {
const model = {
@@ -213,19 +227,6 @@ describe("normalizeModelCompat", () => {
expect(normalized.compat).toBeUndefined();
});
it("forces supportsDeveloperRole off for Azure OpenAI (Chat Completions, not Responses API)", () => {
expectSupportsDeveloperRoleForcedOff({
provider: "azure-openai",
baseUrl: "https://my-deployment.openai.azure.com/openai",
});
});
it("forces supportsDeveloperRole off for generic custom openai-completions provider", () => {
expectSupportsDeveloperRoleForcedOff({
provider: "custom-cpa",
baseUrl: "https://cpa.example.com/v1",
});
});
it("forces supportsUsageInStreaming off for generic custom openai-completions provider", () => {
expectSupportsUsageInStreamingForcedOff({
provider: "custom-cpa",
@@ -233,23 +234,18 @@ describe("normalizeModelCompat", () => {
});
});
it("forces supportsStrictMode off for z.ai models", () => {
expectSupportsStrictModeForcedOff();
});
it("forces supportsStrictMode off for custom openai-completions provider", () => {
expectSupportsStrictModeForcedOff({
provider: "custom-cpa",
baseUrl: "https://cpa.example.com/v1",
});
});
it("forces supportsDeveloperRole off for Qwen proxy via openai-completions", () => {
expectSupportsDeveloperRoleForcedOff({
provider: "qwen-proxy",
baseUrl: "https://qwen-api.example.org/compatible-mode/v1",
});
});
it.each([
["z.ai models", undefined],
[
"custom openai-completions providers",
{ provider: "custom-cpa", baseUrl: "https://cpa.example.com/v1" },
],
] satisfies Array<[string, Partial<Model> | undefined]>)(
"forces supportsStrictMode off for %s",
(_name, overrides) => {
expectSupportsStrictModeForcedOff(overrides);
},
);
it("leaves openai-completions model with empty baseUrl untouched", () => {
const model = {
@@ -262,13 +258,6 @@ describe("normalizeModelCompat", () => {
expect(normalized.compat).toBeUndefined();
});
it("forces supportsDeveloperRole off for malformed baseUrl values", () => {
expectSupportsDeveloperRoleForcedOff({
provider: "custom-cpa",
baseUrl: "://api.openai.com malformed",
});
});
it("respects explicit supportsDeveloperRole true on non-native endpoints", () => {
const model = {
...baseModel(),

View File

@@ -702,24 +702,6 @@ describe("commands-acp context", () => {
expect(resolveAcpCommandConversationId(params)).toBe("iMessage;+;chat123");
});
it("resolves iMessage DM conversation ids from current targets", () => {
const params = buildCommandTestParams("/acp status", baseCfg, {
Provider: "imessage",
Surface: "imessage",
OriginatingChannel: "imessage",
OriginatingTo: "imessage:+15555550123",
});
expect(resolveAcpCommandBindingContext(params)).toEqual({
channel: "imessage",
accountId: "default",
threadId: undefined,
conversationId: "+15555550123",
parentConversationId: undefined,
});
expect(resolveAcpCommandConversationId(params)).toBe("+15555550123");
});
it("resolves iMessage group conversation ids from chat_id targets", () => {
const params = buildCommandTestParams("/acp status", baseCfg, {
Provider: "imessage",

View File

@@ -161,34 +161,22 @@ describe("model provider localService config", () => {
});
it("accepts bundled provider timeout overlays without custom provider fields", () => {
const result = validateConfigObjectRaw({
models: {
providers: {
openai: {
timeoutSeconds: 600,
for (const provider of ["openai", "zai"] as const) {
const result = validateConfigObjectRaw({
models: {
providers: {
[provider]: {
timeoutSeconds: 600,
},
},
},
},
});
});
expect(result.ok).toBe(true);
});
it("accepts bundled provider timeout overlays without custom provider fields", () => {
const result = validateConfigObjectRaw({
models: {
providers: {
zai: {
timeoutSeconds: 600,
},
},
},
});
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.config.models?.providers?.zai?.models).toEqual([]);
expect(result.config.models?.providers?.zai?.baseUrl).toBe("");
expect(result.ok).toBe(true);
if (provider === "zai" && result.ok) {
expect(result.config.models?.providers?.zai?.models).toEqual([]);
expect(result.config.models?.providers?.zai?.baseUrl).toBe("");
}
}
});

View File

@@ -33,6 +33,15 @@ async function collect(iter: AsyncGenerator<string>): Promise<string[]> {
return out;
}
describe("transcript stream empty files", () => {
it("returns empty iterators for empty files in both directions", async () => {
fs.writeFileSync(transcriptPath, "", "utf-8");
await expect(collect(streamSessionTranscriptLines(transcriptPath))).resolves.toEqual([]);
await expect(collect(streamSessionTranscriptLinesReverse(transcriptPath))).resolves.toEqual([]);
});
});
describe("streamSessionTranscriptLines", () => {
it("yields trimmed non-empty lines in file order", async () => {
fs.writeFileSync(transcriptPath, " alpha \n\nbeta\n \r\ngamma\n", "utf-8");
@@ -48,14 +57,6 @@ describe("streamSessionTranscriptLines", () => {
expect(lines).toEqual([]);
});
it("returns an empty iterator for an empty file", async () => {
fs.writeFileSync(transcriptPath, "", "utf-8");
const lines = await collect(streamSessionTranscriptLines(transcriptPath));
expect(lines).toEqual([]);
});
it("forwards malformed JSON lines as raw text so callers can choose to skip them", async () => {
fs.writeFileSync(
transcriptPath,
@@ -112,14 +113,6 @@ describe("streamSessionTranscriptLinesReverse", () => {
expect(lines).toEqual([]);
});
it("returns an empty iterator for an empty file", async () => {
fs.writeFileSync(transcriptPath, "", "utf-8");
const lines = await collect(streamSessionTranscriptLinesReverse(transcriptPath));
expect(lines).toEqual([]);
});
it("preserves complete lines across chunk boundaries", async () => {
const longLine = "x".repeat(2048);
fs.writeFileSync(transcriptPath, `${longLine}\nbeta\ngamma\n`, "utf-8");

View File

@@ -746,18 +746,17 @@ describe("resolveGatewayLiveModelTimeoutMs", () => {
it("defaults to the release live model budget", () => {
expect(resolveGatewayLiveModelTimeoutMs("", undefined, 90_000)).toBe(300_000);
});
it("never goes below the probe timeout", () => {
expect(resolveGatewayLiveModelTimeoutMs("45000", undefined, 90_000)).toBe(90_000);
});
});
describe("resolveGatewayLiveTranscriptTimeoutMs", () => {
it("uses the model budget for transcript waits", () => {
expect(resolveGatewayLiveTranscriptTimeoutMs(90_000, 180_000)).toBe(180_000);
});
});
describe("gateway live timeout floors", () => {
it("never goes below the probe timeout", () => {
expect(resolveGatewayLiveModelTimeoutMs("45000", undefined, 90_000)).toBe(90_000);
expect(resolveGatewayLiveTranscriptTimeoutMs(240_000, 180_000)).toBe(240_000);
});
});

View File

@@ -861,6 +861,26 @@ describe("POST /tools/invoke", () => {
const body = await expectOkInvokeResponse(res);
expect(body.result).toEqual({ ok: true, result: [] });
setMainAllowedTools({ allow: ["write_scoped_test"] });
vi.mocked(authorizeHttpGatewayConnect).mockResolvedValueOnce({
ok: true,
method: "token",
});
const writeScopedRes = await invokeTool({
port: sharedPort,
headers: {
authorization: "Bearer secret",
"x-openclaw-scopes": "operator.approvals",
},
tool: "write_scoped_test",
sessionKey: "main",
});
const writeScopedBody = await expectOkInvokeResponse(writeScopedRes);
expect(writeScopedBody.result).toEqual({ ok: true, result: "write-scoped" });
expect(lastCreateOpenClawToolsContext?.senderIsOwner).toBe(true);
});
it("executes tools for write-scoped callers on the HTTP path", async () => {
@@ -899,28 +919,6 @@ describe("POST /tools/invoke", () => {
expect(lastCreateOpenClawToolsContext?.senderIsOwner).toBe(true);
});
it("treats shared-secret bearer auth as full operator access on /tools/invoke", async () => {
setMainAllowedTools({ allow: ["write_scoped_test"] });
vi.mocked(authorizeHttpGatewayConnect).mockResolvedValueOnce({
ok: true,
method: "token",
});
const res = await invokeTool({
port: sharedPort,
headers: {
authorization: "Bearer secret",
"x-openclaw-scopes": "operator.approvals",
},
tool: "write_scoped_test",
sessionKey: "main",
});
const body = await expectOkInvokeResponse(res);
expect(body.result).toEqual({ ok: true, result: "write-scoped" });
expect(lastCreateOpenClawToolsContext?.senderIsOwner).toBe(true);
});
it("extends the HTTP deny list to high-risk execution and file tools", async () => {
setMainAllowedTools({ allow: ["exec", "apply_patch", "nodes"] });

View File

@@ -29,6 +29,25 @@ describe("buildOutboundResultEnvelope", () => {
meta: { ok: true },
},
},
{
input: {
payloads: [],
delivery,
meta: { delivered: true },
},
expected: {
payloads: [],
meta: { delivered: true },
delivery,
},
},
{
input: {
delivery,
flattenDelivery: false,
},
expected: { delivery },
},
])("formats outbound envelope for %j", ({ input, expected }) => {
const envelope = buildOutboundResultEnvelope(input);
expect(envelope).toEqual(expected);

View File

@@ -2,10 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import { setActivePluginRegistry } from "../../plugins/runtime.js";
import { createTestRegistry } from "../../test-utils/channel-plugins.js";
import { typedCases } from "../../test-utils/typed-cases.js";
import { DirectoryCache } from "./directory-cache.js";
import { buildOutboundResultEnvelope } from "./envelope.js";
import type { OutboundDeliveryJson } from "./format.js";
beforeEach(() => {
setActivePluginRegistry(createTestRegistry([]));
@@ -58,70 +55,3 @@ describe("DirectoryCache", () => {
expect(cache.get("c", cfg)).toBe(expected.c);
});
});
describe("buildOutboundResultEnvelope", () => {
const directChatDelivery: OutboundDeliveryJson = {
channel: "directchat",
via: "gateway",
to: "+1",
messageId: "m1",
mediaUrl: null,
};
const alphaDelivery: OutboundDeliveryJson = {
channel: "alpha",
via: "direct",
to: "123",
messageId: "m2",
mediaUrl: null,
chatId: "c1",
};
const richChatDelivery: OutboundDeliveryJson = {
channel: "richchat",
via: "gateway",
to: "channel:C1",
messageId: "m3",
mediaUrl: null,
channelId: "C1",
};
it.each(
typedCases<{
name: string;
input: Parameters<typeof buildOutboundResultEnvelope>[0];
expected: unknown;
}>([
{
name: "flatten delivery by default",
input: { delivery: directChatDelivery },
expected: directChatDelivery,
},
{
name: "keep payloads + meta",
input: {
payloads: [{ text: "hi", mediaUrl: null, mediaUrls: undefined }],
meta: { foo: "bar" },
},
expected: {
payloads: [{ text: "hi", mediaUrl: null, mediaUrls: undefined }],
meta: { foo: "bar" },
},
},
{
name: "include delivery when payloads exist",
input: { payloads: [], delivery: alphaDelivery, meta: { ok: true } },
expected: {
payloads: [],
meta: { ok: true },
delivery: alphaDelivery,
},
},
{
name: "keep wrapped delivery when flatten disabled",
input: { delivery: richChatDelivery, flattenDelivery: false },
expected: { delivery: richChatDelivery },
},
]),
)("$name", ({ input, expected }) => {
expect(buildOutboundResultEnvelope(input)).toEqual(expected);
});
});

View File

@@ -1,9 +1,4 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
expectBrowserHostInspectionDelegation,
expectBrowserHostInspectionFacadeUnavailable,
mockBrowserHostInspectionFacade,
} from "./browser-facade-test-helpers.js";
const loadBundledPluginPublicSurfaceModuleSync = vi.hoisted(() => vi.fn());
@@ -104,24 +99,4 @@ describe("plugin-sdk browser facades", () => {
"missing browser control auth facade",
);
});
it("delegates browser host inspection helpers to the browser facade", async () => {
const executable: import("./browser-host-inspection.js").BrowserExecutable = {
kind: "chrome",
path: "/usr/bin/google-chrome",
};
mockBrowserHostInspectionFacade(loadBundledPluginPublicSurfaceModuleSync, executable);
const hostInspection = await import("./browser-host-inspection.js");
expectBrowserHostInspectionDelegation({
executable,
hostInspection,
loadBundledPluginPublicSurfaceModuleSync,
});
});
it("hard-fails when browser host inspection facade is unavailable", async () => {
await expectBrowserHostInspectionFacadeUnavailable(loadBundledPluginPublicSurfaceModuleSync);
});
});

View File

@@ -23,41 +23,34 @@ describe("discovery threading", () => {
discoverOpenClawPluginsMock.mockReturnValue(emptyDiscovery);
});
describe("loadPluginManifestRegistry", () => {
it("skips internal discoverOpenClawPlugins when discovery is supplied", () => {
loadPluginManifestRegistry({ discovery: emptyDiscovery });
expect(discoverOpenClawPluginsMock).not.toHaveBeenCalled();
});
it("skips internal discoverOpenClawPlugins when discovery is supplied", () => {
loadPluginManifestRegistry({ discovery: emptyDiscovery });
expect(discoverOpenClawPluginsMock).not.toHaveBeenCalled();
it("calls discoverOpenClawPlugins when neither discovery nor candidates supplied", () => {
loadPluginManifestRegistry({});
expect(discoverOpenClawPluginsMock).toHaveBeenCalledTimes(1);
});
it("prefers explicit candidates over discovery when both are supplied", () => {
loadPluginManifestRegistry({ candidates: [], diagnostics: [], discovery: emptyDiscovery });
expect(discoverOpenClawPluginsMock).not.toHaveBeenCalled();
});
discoverOpenClawPluginsMock.mockClear();
resolveInstalledPluginIndexRegistry({ discovery: emptyDiscovery, installRecords: {} });
expect(discoverOpenClawPluginsMock).not.toHaveBeenCalled();
});
describe("resolveInstalledPluginIndexRegistry", () => {
it("skips internal discoverOpenClawPlugins when discovery is supplied", () => {
resolveInstalledPluginIndexRegistry({ discovery: emptyDiscovery, installRecords: {} });
expect(discoverOpenClawPluginsMock).not.toHaveBeenCalled();
});
it("calls discoverOpenClawPlugins when neither discovery nor candidates supplied", () => {
loadPluginManifestRegistry({});
expect(discoverOpenClawPluginsMock).toHaveBeenCalledTimes(1);
it("calls discoverOpenClawPlugins when neither discovery nor candidates supplied", () => {
resolveInstalledPluginIndexRegistry({ installRecords: {} });
expect(discoverOpenClawPluginsMock).toHaveBeenCalledTimes(1);
});
discoverOpenClawPluginsMock.mockClear();
resolveInstalledPluginIndexRegistry({ installRecords: {} });
expect(discoverOpenClawPluginsMock).toHaveBeenCalledTimes(1);
});
it("prefers explicit candidates over discovery when both are supplied", () => {
resolveInstalledPluginIndexRegistry({
candidates: [],
discovery: emptyDiscovery,
installRecords: {},
});
expect(discoverOpenClawPluginsMock).not.toHaveBeenCalled();
it("prefers explicit candidates over discovery when both are supplied", () => {
loadPluginManifestRegistry({ candidates: [], diagnostics: [], discovery: emptyDiscovery });
expect(discoverOpenClawPluginsMock).not.toHaveBeenCalled();
discoverOpenClawPluginsMock.mockClear();
resolveInstalledPluginIndexRegistry({
candidates: [],
discovery: emptyDiscovery,
installRecords: {},
});
expect(discoverOpenClawPluginsMock).not.toHaveBeenCalled();
});
});

View File

@@ -1,41 +1,5 @@
import { describe, expect, it, vi } from "vitest";
import {
MAX_SAFE_TIMEOUT_DELAY_MS,
resolveFiniteTimeoutDelayMs,
resolveSafeTimeoutDelayMs,
setSafeTimeout,
} from "./timer-delay.js";
describe("resolveSafeTimeoutDelayMs", () => {
it("clamps to Node's signed-32-bit timer ceiling", () => {
expect(resolveSafeTimeoutDelayMs(3_000_000_000)).toBe(MAX_SAFE_TIMEOUT_DELAY_MS);
});
it("respects custom minimums", () => {
expect(resolveSafeTimeoutDelayMs(10, { minMs: 250 })).toBe(250);
expect(resolveSafeTimeoutDelayMs(10, { minMs: 0 })).toBe(10);
});
it("falls back to the minimum for non-finite input", () => {
expect(resolveSafeTimeoutDelayMs(Number.POSITIVE_INFINITY, { minMs: 250 })).toBe(250);
expect(resolveSafeTimeoutDelayMs(Number.NaN)).toBe(1);
});
});
describe("resolveFiniteTimeoutDelayMs", () => {
it("uses the fallback for missing or non-finite overrides", () => {
expect(resolveFiniteTimeoutDelayMs(undefined, 10_000, { minMs: 0 })).toBe(10_000);
expect(resolveFiniteTimeoutDelayMs(Number.NaN, 10_000, { minMs: 0 })).toBe(10_000);
expect(resolveFiniteTimeoutDelayMs(Number.POSITIVE_INFINITY, 10_000, { minMs: 0 })).toBe(
10_000,
);
});
it("still clamps finite overrides through safe timer bounds", () => {
expect(resolveFiniteTimeoutDelayMs(3_000_000_000, 10_000)).toBe(MAX_SAFE_TIMEOUT_DELAY_MS);
expect(resolveFiniteTimeoutDelayMs(-5, 10_000, { minMs: 0 })).toBe(0);
});
});
import { MAX_SAFE_TIMEOUT_DELAY_MS, setSafeTimeout } from "./timer-delay.js";
describe("setSafeTimeout", () => {
it("arms setTimeout with the clamped delay", () => {

View File

@@ -456,28 +456,18 @@ describe("runNpmReleaseCheckCommand", () => {
});
describe("resolveNpmReleaseCheckCommandTimeoutMs", () => {
it("uses a positive environment timeout", () => {
expect(
resolveNpmReleaseCheckCommandTimeoutMs({
OPENCLAW_NPM_RELEASE_CHECK_COMMAND_TIMEOUT_MS: "1234",
}),
).toBe(1234);
});
it("falls back when the environment timeout is invalid", () => {
expect(
resolveNpmReleaseCheckCommandTimeoutMs({
OPENCLAW_NPM_RELEASE_CHECK_COMMAND_TIMEOUT_MS: "nope",
}),
).toBe(10 * 60 * 1000);
});
it("falls back when the environment timeout has a numeric prefix", () => {
expect(
resolveNpmReleaseCheckCommandTimeoutMs({
OPENCLAW_NPM_RELEASE_CHECK_COMMAND_TIMEOUT_MS: "10m",
}),
).toBe(10 * 60 * 1000);
it("parses only positive integer environment timeouts", () => {
for (const [raw, expected] of [
["1234", 1234],
["nope", 10 * 60 * 1000],
["10m", 10 * 60 * 1000],
] as const) {
expect(
resolveNpmReleaseCheckCommandTimeoutMs({
OPENCLAW_NPM_RELEASE_CHECK_COMMAND_TIMEOUT_MS: raw,
}),
).toBe(expected);
}
});
});

View File

@@ -53,21 +53,15 @@ describe("runPrepackCommand", () => {
});
describe("resolvePrepackCommandTimeoutMs", () => {
it("uses a positive environment timeout", () => {
expect(resolvePrepackCommandTimeoutMs({ OPENCLAW_PREPACK_COMMAND_TIMEOUT_MS: "1234" })).toBe(
1234,
);
});
it("falls back when the environment timeout is invalid", () => {
expect(resolvePrepackCommandTimeoutMs({ OPENCLAW_PREPACK_COMMAND_TIMEOUT_MS: "nope" })).toBe(
30 * 60 * 1000,
);
});
it("falls back when the environment timeout has a numeric prefix", () => {
expect(resolvePrepackCommandTimeoutMs({ OPENCLAW_PREPACK_COMMAND_TIMEOUT_MS: "10m" })).toBe(
30 * 60 * 1000,
);
it("parses only positive integer environment timeouts", () => {
for (const [raw, expected] of [
["1234", 1234],
["nope", 30 * 60 * 1000],
["10m", 30 * 60 * 1000],
] as const) {
expect(resolvePrepackCommandTimeoutMs({ OPENCLAW_PREPACK_COMMAND_TIMEOUT_MS: raw })).toBe(
expected,
);
}
});
});

View File

@@ -167,21 +167,15 @@ describe("ensure-cli-startup-build", () => {
});
describe("resolveCliStartupBuildTimeoutMs", () => {
it("uses a positive environment timeout", () => {
expect(resolveCliStartupBuildTimeoutMs({ OPENCLAW_CLI_STARTUP_BUILD_TIMEOUT_MS: "4321" })).toBe(
4321,
);
});
it("falls back when the environment timeout is invalid", () => {
expect(resolveCliStartupBuildTimeoutMs({ OPENCLAW_CLI_STARTUP_BUILD_TIMEOUT_MS: "nope" })).toBe(
10 * 60 * 1000,
);
});
it("falls back when the environment timeout has a numeric prefix", () => {
expect(resolveCliStartupBuildTimeoutMs({ OPENCLAW_CLI_STARTUP_BUILD_TIMEOUT_MS: "10m" })).toBe(
10 * 60 * 1000,
);
it("parses only positive integer environment timeouts", () => {
for (const [raw, expected] of [
["4321", 4321],
["nope", 10 * 60 * 1000],
["10m", 10 * 60 * 1000],
] as const) {
expect(resolveCliStartupBuildTimeoutMs({ OPENCLAW_CLI_STARTUP_BUILD_TIMEOUT_MS: raw })).toBe(
expected,
);
}
});
});

View File

@@ -144,27 +144,17 @@ describe("ensure-extension-memory-build", () => {
});
describe("resolveExtensionMemoryBuildTimeoutMs", () => {
it("uses a positive environment timeout", () => {
expect(
resolveExtensionMemoryBuildTimeoutMs({
OPENCLAW_EXTENSION_MEMORY_BUILD_TIMEOUT_MS: "4321",
}),
).toBe(4321);
});
it("falls back when the environment timeout is invalid", () => {
expect(
resolveExtensionMemoryBuildTimeoutMs({
OPENCLAW_EXTENSION_MEMORY_BUILD_TIMEOUT_MS: "nope",
}),
).toBe(10 * 60 * 1000);
});
it("falls back when the environment timeout has a numeric prefix", () => {
expect(
resolveExtensionMemoryBuildTimeoutMs({
OPENCLAW_EXTENSION_MEMORY_BUILD_TIMEOUT_MS: "10m",
}),
).toBe(10 * 60 * 1000);
it("parses only positive integer environment timeouts", () => {
for (const [raw, expected] of [
["4321", 4321],
["nope", 10 * 60 * 1000],
["10m", 10 * 60 * 1000],
] as const) {
expect(
resolveExtensionMemoryBuildTimeoutMs({
OPENCLAW_EXTENSION_MEMORY_BUILD_TIMEOUT_MS: raw,
}),
).toBe(expected);
}
});
});