mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-03 14:14:08 +00:00
test: dedupe redundant test coverage
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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("");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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"] });
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user