test: consolidate redundant suites and speed attachment tests

This commit is contained in:
Peter Steinberger
2026-02-23 04:55:43 +00:00
parent 86a8b65e9d
commit 48f327c206
7 changed files with 118 additions and 152 deletions

View File

@@ -1,5 +1,9 @@
import type { PluginRuntime } from "openclaw/plugin-sdk";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { downloadMSTeamsAttachments } from "./attachments/download.js";
import { buildMSTeamsGraphMessageUrls, downloadMSTeamsGraphMedia } from "./attachments/graph.js";
import { buildMSTeamsAttachmentPlaceholder } from "./attachments/html.js";
import { buildMSTeamsMediaPayload } from "./attachments/payload.js";
import { setMSTeamsRuntime } from "./runtime.js";
/** Mock DNS resolver that always returns a public IP (for anti-SSRF validation in tests). */
@@ -49,10 +53,6 @@ const runtimeStub = {
} as unknown as PluginRuntime;
describe("msteams attachments", () => {
const load = async () => {
return await import("./attachments.js");
};
beforeEach(() => {
detectMimeMock.mockClear();
saveMediaBufferMock.mockClear();
@@ -62,13 +62,11 @@ describe("msteams attachments", () => {
describe("buildMSTeamsAttachmentPlaceholder", () => {
it("returns empty string when no attachments", async () => {
const { buildMSTeamsAttachmentPlaceholder } = await load();
expect(buildMSTeamsAttachmentPlaceholder(undefined)).toBe("");
expect(buildMSTeamsAttachmentPlaceholder([])).toBe("");
});
it("returns image placeholder for image attachments", async () => {
const { buildMSTeamsAttachmentPlaceholder } = await load();
expect(
buildMSTeamsAttachmentPlaceholder([
{ contentType: "image/png", contentUrl: "https://x/img.png" },
@@ -83,7 +81,6 @@ describe("msteams attachments", () => {
});
it("treats Teams file.download.info image attachments as images", async () => {
const { buildMSTeamsAttachmentPlaceholder } = await load();
expect(
buildMSTeamsAttachmentPlaceholder([
{
@@ -95,7 +92,6 @@ describe("msteams attachments", () => {
});
it("returns document placeholder for non-image attachments", async () => {
const { buildMSTeamsAttachmentPlaceholder } = await load();
expect(
buildMSTeamsAttachmentPlaceholder([
{ contentType: "application/pdf", contentUrl: "https://x/x.pdf" },
@@ -110,7 +106,6 @@ describe("msteams attachments", () => {
});
it("counts inline images in text/html attachments", async () => {
const { buildMSTeamsAttachmentPlaceholder } = await load();
expect(
buildMSTeamsAttachmentPlaceholder([
{
@@ -132,7 +127,6 @@ describe("msteams attachments", () => {
describe("downloadMSTeamsAttachments", () => {
it("downloads and stores image contentUrl attachments", async () => {
const { downloadMSTeamsAttachments } = await load();
const fetchMock = vi.fn(async () => {
return new Response(Buffer.from("png"), {
status: 200,
@@ -155,7 +149,6 @@ describe("msteams attachments", () => {
});
it("supports Teams file.download.info downloadUrl attachments", async () => {
const { downloadMSTeamsAttachments } = await load();
const fetchMock = vi.fn(async () => {
return new Response(Buffer.from("png"), {
status: 200,
@@ -181,7 +174,6 @@ describe("msteams attachments", () => {
});
it("downloads non-image file attachments (PDF)", async () => {
const { downloadMSTeamsAttachments } = await load();
const fetchMock = vi.fn(async () => {
return new Response(Buffer.from("pdf"), {
status: 200,
@@ -209,7 +201,6 @@ describe("msteams attachments", () => {
});
it("downloads inline image URLs from html attachments", async () => {
const { downloadMSTeamsAttachments } = await load();
const fetchMock = vi.fn(async () => {
return new Response(Buffer.from("png"), {
status: 200,
@@ -235,7 +226,6 @@ describe("msteams attachments", () => {
});
it("stores inline data:image base64 payloads", async () => {
const { downloadMSTeamsAttachments } = await load();
const base64 = Buffer.from("png").toString("base64");
const media = await downloadMSTeamsAttachments({
attachments: [
@@ -253,7 +243,6 @@ describe("msteams attachments", () => {
});
it("retries with auth when the first request is unauthorized", async () => {
const { downloadMSTeamsAttachments } = await load();
const fetchMock = vi.fn(async (_url: string, opts?: RequestInit) => {
const headers = new Headers(opts?.headers);
const hasAuth = Boolean(headers.get("Authorization"));
@@ -281,7 +270,6 @@ describe("msteams attachments", () => {
});
it("skips auth retries when the host is not in auth allowlist", async () => {
const { downloadMSTeamsAttachments } = await load();
const tokenProvider = { getAccessToken: vi.fn(async () => "token") };
const fetchMock = vi.fn(async (_url: string, opts?: RequestInit) => {
const headers = new Headers(opts?.headers);
@@ -313,7 +301,6 @@ describe("msteams attachments", () => {
});
it("skips urls outside the allowlist", async () => {
const { downloadMSTeamsAttachments } = await load();
const fetchMock = vi.fn();
const media = await downloadMSTeamsAttachments({
attachments: [{ contentType: "image/png", contentUrl: "https://evil.test/img" }],
@@ -329,7 +316,6 @@ describe("msteams attachments", () => {
describe("buildMSTeamsGraphMessageUrls", () => {
it("builds channel message urls", async () => {
const { buildMSTeamsGraphMessageUrls } = await load();
const urls = buildMSTeamsGraphMessageUrls({
conversationType: "channel",
conversationId: "19:thread@thread.tacv2",
@@ -340,7 +326,6 @@ describe("msteams attachments", () => {
});
it("builds channel reply urls when replyToId is present", async () => {
const { buildMSTeamsGraphMessageUrls } = await load();
const urls = buildMSTeamsGraphMessageUrls({
conversationType: "channel",
messageId: "reply-id",
@@ -353,7 +338,6 @@ describe("msteams attachments", () => {
});
it("builds chat message urls", async () => {
const { buildMSTeamsGraphMessageUrls } = await load();
const urls = buildMSTeamsGraphMessageUrls({
conversationType: "groupChat",
conversationId: "19:chat@thread.v2",
@@ -365,7 +349,6 @@ describe("msteams attachments", () => {
describe("downloadMSTeamsGraphMedia", () => {
it("downloads hostedContents images", async () => {
const { downloadMSTeamsGraphMedia } = await load();
const base64 = Buffer.from("png").toString("base64");
const fetchMock = vi.fn(async (url: string) => {
if (url.endsWith("/hostedContents")) {
@@ -401,7 +384,6 @@ describe("msteams attachments", () => {
});
it("merges SharePoint reference attachments with hosted content", async () => {
const { downloadMSTeamsGraphMedia } = await load();
const hostedBase64 = Buffer.from("png").toString("base64");
const shareUrl = "https://contoso.sharepoint.com/site/file";
const fetchMock = vi.fn(async (url: string) => {
@@ -469,7 +451,6 @@ describe("msteams attachments", () => {
});
it("blocks SharePoint redirects to hosts outside allowHosts", async () => {
const { downloadMSTeamsGraphMedia } = await load();
const shareUrl = "https://contoso.sharepoint.com/site/file";
const escapedUrl = "https://evil.example/internal.pdf";
fetchRemoteMediaMock.mockImplementationOnce(async (params) => {
@@ -553,7 +534,6 @@ describe("msteams attachments", () => {
describe("buildMSTeamsMediaPayload", () => {
it("returns single and multi-file fields", async () => {
const { buildMSTeamsMediaPayload } = await load();
const payload = buildMSTeamsMediaPayload([
{ path: "/tmp/a.png", contentType: "image/png" },
{ path: "/tmp/b.png", contentType: "image/png" },

View File

@@ -1,35 +0,0 @@
import { describe, expect, it } from "vitest";
import { isAnthropicBillingError } from "./live-auth-keys.js";
describe("isAnthropicBillingError", () => {
it("does not false-positive on plain 'a 402' prose", () => {
const samples = [
"Use a 402 stainless bolt",
"Book a 402 room",
"There is a 402 near me",
"The building at 402 Main Street",
];
for (const sample of samples) {
expect(isAnthropicBillingError(sample)).toBe(false);
}
});
it("matches real 402 billing payload contexts including JSON keys", () => {
const samples = [
"HTTP 402 Payment Required",
"status: 402",
"error code 402",
'{"status":402,"type":"error"}',
'{"code":402,"message":"payment required"}',
'{"error":{"code":402,"message":"billing hard limit reached"}}',
"got a 402 from the API",
"returned 402",
"received a 402 response",
];
for (const sample of samples) {
expect(isAnthropicBillingError(sample)).toBe(true);
}
});
});

View File

@@ -2,6 +2,8 @@ import type { Api, Model } from "@mariozechner/pi-ai";
import { describe, expect, it } from "vitest";
import { isModernModelRef } from "./live-model-filter.js";
import { normalizeModelCompat } from "./model-compat.js";
import { resolveForwardCompatModel } from "./model-forward-compat.js";
import type { ModelRegistry } from "./pi-model-discovery.js";
const baseModel = (): Model<Api> =>
({
@@ -17,6 +19,28 @@ const baseModel = (): Model<Api> =>
maxTokens: 1024,
}) as Model<Api>;
function createTemplateModel(provider: string, id: string): Model<Api> {
return {
id,
name: id,
provider,
api: "anthropic-messages",
input: ["text"],
reasoning: true,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200_000,
maxTokens: 8_192,
} as Model<Api>;
}
function createRegistry(models: Record<string, Model<Api>>): ModelRegistry {
return {
find(provider: string, modelId: string) {
return models[`${provider}/${modelId}`] ?? null;
},
} as ModelRegistry;
}
describe("normalizeModelCompat", () => {
it("forces supportsDeveloperRole off for z.ai models", () => {
const model = baseModel();
@@ -59,3 +83,36 @@ describe("isModernModelRef", () => {
expect(isModernModelRef({ provider: "opencode", id: "gemini-3-pro" })).toBe(true);
});
});
describe("resolveForwardCompatModel", () => {
it("resolves anthropic opus 4.6 via 4.5 template", () => {
const registry = createRegistry({
"anthropic/claude-opus-4-5": createTemplateModel("anthropic", "claude-opus-4-5"),
});
const model = resolveForwardCompatModel("anthropic", "claude-opus-4-6", registry);
expect(model?.id).toBe("claude-opus-4-6");
expect(model?.name).toBe("claude-opus-4-6");
expect(model?.provider).toBe("anthropic");
});
it("resolves anthropic sonnet 4.6 dot variant with suffix", () => {
const registry = createRegistry({
"anthropic/claude-sonnet-4.5-20260219": createTemplateModel(
"anthropic",
"claude-sonnet-4.5-20260219",
),
});
const model = resolveForwardCompatModel("anthropic", "claude-sonnet-4.6-20260219", registry);
expect(model?.id).toBe("claude-sonnet-4.6-20260219");
expect(model?.name).toBe("claude-sonnet-4.6-20260219");
expect(model?.provider).toBe("anthropic");
});
it("does not resolve anthropic 4.6 fallback for other providers", () => {
const registry = createRegistry({
"anthropic/claude-opus-4-5": createTemplateModel("anthropic", "claude-opus-4-5"),
});
const model = resolveForwardCompatModel("openai", "claude-opus-4-6", registry);
expect(model).toBeUndefined();
});
});

View File

@@ -7,6 +7,7 @@ import type { OpenClawConfig } from "../config/config.js";
import type { AuthProfileStore } from "./auth-profiles.js";
import { saveAuthProfileStore } from "./auth-profiles.js";
import { AUTH_STORE_VERSION } from "./auth-profiles/constants.js";
import { isAnthropicBillingError } from "./live-auth-keys.js";
import { runWithModelFallback } from "./model-fallback.js";
import { makeModelFallbackCfg } from "./test-helpers/model-fallback-config-fixture.js";
@@ -656,3 +657,36 @@ describe("runWithModelFallback", () => {
expect(result.model).toBe("gpt-4.1-mini");
});
});
describe("isAnthropicBillingError", () => {
it("does not false-positive on plain 'a 402' prose", () => {
const samples = [
"Use a 402 stainless bolt",
"Book a 402 room",
"There is a 402 near me",
"The building at 402 Main Street",
];
for (const sample of samples) {
expect(isAnthropicBillingError(sample)).toBe(false);
}
});
it("matches real 402 billing payload contexts including JSON keys", () => {
const samples = [
"HTTP 402 Payment Required",
"status: 402",
"error code 402",
'{"status":402,"type":"error"}',
'{"code":402,"message":"payment required"}',
'{"error":{"code":402,"message":"billing hard limit reached"}}',
"got a 402 from the API",
"returned 402",
"received a 402 response",
];
for (const sample of samples) {
expect(isAnthropicBillingError(sample)).toBe(true);
}
});
});

View File

@@ -1,59 +0,0 @@
import type { Api, Model } from "@mariozechner/pi-ai";
import { describe, expect, it } from "vitest";
import { resolveForwardCompatModel } from "./model-forward-compat.js";
import type { ModelRegistry } from "./pi-model-discovery.js";
function createTemplateModel(provider: string, id: string): Model<Api> {
return {
id,
name: id,
provider,
api: "anthropic-messages",
input: ["text"],
reasoning: true,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200_000,
maxTokens: 8_192,
} as Model<Api>;
}
function createRegistry(models: Record<string, Model<Api>>): ModelRegistry {
return {
find(provider: string, modelId: string) {
return models[`${provider}/${modelId}`] ?? null;
},
} as ModelRegistry;
}
describe("agents/model-forward-compat", () => {
it("resolves anthropic opus 4.6 via 4.5 template", () => {
const registry = createRegistry({
"anthropic/claude-opus-4-5": createTemplateModel("anthropic", "claude-opus-4-5"),
});
const model = resolveForwardCompatModel("anthropic", "claude-opus-4-6", registry);
expect(model?.id).toBe("claude-opus-4-6");
expect(model?.name).toBe("claude-opus-4-6");
expect(model?.provider).toBe("anthropic");
});
it("resolves anthropic sonnet 4.6 dot variant with suffix", () => {
const registry = createRegistry({
"anthropic/claude-sonnet-4.5-20260219": createTemplateModel(
"anthropic",
"claude-sonnet-4.5-20260219",
),
});
const model = resolveForwardCompatModel("anthropic", "claude-sonnet-4.6-20260219", registry);
expect(model?.id).toBe("claude-sonnet-4.6-20260219");
expect(model?.name).toBe("claude-sonnet-4.6-20260219");
expect(model?.provider).toBe("anthropic");
});
it("does not resolve anthropic 4.6 fallback for other providers", () => {
const registry = createRegistry({
"anthropic/claude-opus-4-5": createTemplateModel("anthropic", "claude-opus-4-5"),
});
const model = resolveForwardCompatModel("openai", "claude-opus-4-6", registry);
expect(model).toBeUndefined();
});
});

View File

@@ -1,34 +0,0 @@
import { describe, expect, it, vi } from "vitest";
import { withFetchPreconnect } from "../../test-utils/fetch-mock.js";
import {
createBaseWebFetchToolConfig,
installWebFetchSsrfHarness,
} from "./web-fetch.test-harness.js";
import "./web-fetch.test-mocks.js";
import { createWebFetchTool } from "./web-tools.js";
const baseToolConfig = createBaseWebFetchToolConfig({ maxResponseBytes: 1024 });
installWebFetchSsrfHarness();
describe("web_fetch response size limits", () => {
it("caps response bytes and does not hang on endless streams", async () => {
const chunk = new TextEncoder().encode("<html><body><div>hi</div></body></html>");
const stream = new ReadableStream<Uint8Array>({
pull(controller) {
controller.enqueue(chunk);
},
});
const response = new Response(stream, {
status: 200,
headers: { "content-type": "text/html; charset=utf-8" },
});
const fetchSpy = vi.fn().mockResolvedValue(response);
global.fetch = withFetchPreconnect(fetchSpy);
const tool = createWebFetchTool(baseToolConfig);
const result = await tool?.execute?.("call", { url: "https://example.com/stream" });
const details = result?.details as { warning?: string } | undefined;
expect(details?.warning).toContain("Response body truncated");
});
});

View File

@@ -233,6 +233,29 @@ describe("web_fetch extraction fallbacks", () => {
expect(details.truncated).toBe(true);
});
it("caps response bytes and does not hang on endless streams", async () => {
const chunk = new TextEncoder().encode("<html><body><div>hi</div></body></html>");
const stream = new ReadableStream<Uint8Array>({
pull(controller) {
controller.enqueue(chunk);
},
});
const response = new Response(stream, {
status: 200,
headers: { "content-type": "text/html; charset=utf-8" },
});
const fetchSpy = vi.fn().mockResolvedValue(response);
global.fetch = withFetchPreconnect(fetchSpy);
const tool = createFetchTool({
maxResponseBytes: 1024,
firecrawl: { enabled: false },
});
const result = await tool?.execute?.("call", { url: "https://example.com/stream" });
const details = result?.details as { warning?: string } | undefined;
expect(details?.warning).toContain("Response body truncated");
});
// NOTE: Test for wrapping url/finalUrl/warning fields requires DNS mocking.
// The sanitization of these fields is verified by external-content.test.ts tests.