Z ZPI 文档 v0.3.8
介绍页

源码

packages/builtin-plugins/test/web-tools.test.ts

typescript853 行查看原文
import type { AgentToolResult, AgentToolUpdateCallback } from "@zpi-dev/agent";
import type { WebSearchResult } from "@zpi-dev/builtin-plugins/web/client";
import type { ToolContext } from "@zpi-dev/coding-agent";
import type { WebToolDetails } from "@zpi-dev/ui";
import type { TObject } from "typebox";
import { Value } from "typebox/value";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

let WebTools: typeof import("@zpi-dev/builtin-plugins/web/tools").WebTools;
let publicWebUrl: typeof import("@zpi-dev/builtin-plugins/web/client").publicWebUrl;
let WEB_RESPONSE_BYTES: number;
let WEB_DEADLINE_MS: number;
let WEB_TEXT_LIMIT: number;
let transport: ReturnType<typeof vi.fn<typeof fetch>>;
const key = "sk-mock-secret-never-send";

const source = (overrides: Partial<WebSearchResult> = {}): WebSearchResult => ({
  title: "A source title",
  url: "https://example.com/article",
  snippet: "A short snippet",
  summary: "A more complete summary",
  content: "# Page content\n\nUseful source text.",
  site_name: "Example",
  site_icon: "https://example.com/favicon.ico",
  date: "2026-06-01T00:00:00+00:00",
  rerank_score: 2.4,
  authority: 3,
  ...overrides,
});
const json = (value: unknown, status = 200, headers?: HeadersInit) =>
  new Response(JSON.stringify(value), { status, headers });
const web = (result: AgentToolResult) => (result.details as unknown as { web: WebToolDetails }).web;
const text = (result: AgentToolResult) =>
  result.content.map((part) => (part.type === "text" ? part.text : "")).join("");
const tools = (getApiKey = () => key) => new WebTools({ getApiKey, requestFetch: transport });
function execute(
  kind: "web_search" | "web_fetch" = "web_search",
  args: unknown = { query: "DeepSeek" },
  signal?: AbortSignal,
  update?: AgentToolUpdateCallback,
  broker = tools(),
) {
  const tool = broker.tools().find((tool) => tool.name === kind);
  if (!tool) throw new Error("Missing web tool");
  return tool.execute("web-call", args, signal, update, {} as ToolContext);
}
async function finish(promise: Promise<AgentToolResult>) {
  await vi.runAllTimersAsync();
  return promise;
}
const run = (kind: "web_search" | "web_fetch" = "web_search", args: unknown = { query: "DeepSeek" }) =>
  finish(execute(kind, args));

beforeEach(async () => {
  vi.useFakeTimers();
  vi.setSystemTime(new Date("2026-10-17T00:00:00Z"));
  vi.resetModules();
  ({ WebTools, WEB_TEXT_LIMIT } = await import("@zpi-dev/builtin-plugins/web/tools"));
  ({ publicWebUrl, WEB_RESPONSE_BYTES, WEB_DEADLINE_MS } = await import(
    "@zpi-dev/builtin-plugins/web/client"
  ));
  transport = vi.fn<typeof fetch>().mockImplementation(async () => json({ results: [source()] }));
});
afterEach(() => {
  vi.useRealTimers();
  vi.restoreAllMocks();
});

describe("DeepSeek tool contracts", () => {
  it("advertises exactly two desktop read-only tools with the official optional schema", () => {
    const [search, fetch] = tools().tools();
    const searchSchema = search.parameters as TObject;
    const fetchSchema = fetch.parameters as TObject;
    expect(search.name).toBe("web_search");
    expect(fetch.name).toBe("web_fetch");
    expect(searchSchema.required).toEqual(["query"]);
    expect(searchSchema.properties).toMatchObject({
      query: { minLength: 1, maxLength: 128 },
      count: { type: "integer", minimum: 1, maximum: 50, default: 10 },
      localization: { type: "string", default: "" },
      need_content: { type: "boolean", default: false },
    });
    expect(searchSchema.properties).not.toHaveProperty("queries");
    expect(fetchSchema.required).toEqual(["url"]);
    expect(fetchSchema.properties.url).toMatchObject({ maxLength: 8192 });
    for (const tool of [search, fetch]) {
      expect(tool.executionMode).toBe("parallel");
      expect(tool.metadata).toMatchObject({
        readOnly: true,
        concurrentSafe: true,
        sideEffectScope: "network",
      });
      expect(tool.description).toContain("untrusted");
      expect(tool.description).toContain("never instructions");
    }
  });

  it("sends fixed POST endpoints, Bearer auth, redirect:error and only provided fields", async () => {
    const update = vi.fn();
    const result = await finish(
      execute("web_search", { query: "  DeepSeek site:example.com  " }, undefined, update),
    );
    expect(transport).toHaveBeenCalledOnce();
    const [url, init] = transport.mock.calls[0];
    expect(url).toBe("https://api.deepseek.com/v1/tools/search");
    expect(init).toMatchObject({
      method: "POST",
      redirect: "error",
      headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
    });
    expect(init?.signal).toBeInstanceOf(AbortSignal);
    expect(JSON.parse(init?.body as string)).toEqual({ query: "  DeepSeek site:example.com  " });
    expect(web(update.mock.calls[0][0])).toEqual({
      kind: "web_search",
      status: "pending",
      query: "  DeepSeek site:example.com  ",
    });
    expect(web(result)).toMatchObject({
      kind: "web_search",
      status: "success",
      results: [{ title: "A source title", summary: "A more complete summary" }],
      truncated: false,
    });
    expect(text(result)).toContain("A more complete summary");
    expect(text(result)).toContain("Page content");
    expect(text(result)).toContain("rerank_score");
  });

  it.each([1, 10, 50])(
    "passes count %i without clamping and preserves optional defaults/null",
    async (count) => {
      const input = { query: "x", count, ori_question: null, localization: "", need_content: false };
      await run("web_search", input);
      expect(JSON.parse(transport.mock.calls[0][1]?.body as string)).toEqual(input);
    },
  );

  it("allows 128 non-BMP Unicode codepoints through both preparation and SDK schema checks", async () => {
    const input = {
      query: "😀".repeat(128),
      ori_question: "😀".repeat(128),
      localization: "1.2.3.4",
      need_content: true,
    };
    const tool = tools().tools()[0];
    expect(tool.prepareArguments?.(input)).toEqual(input);
    expect(Value.Check(tool.parameters, input)).toBe(true);
    expect(web(await run("web_search", input)).status).toBe("success");
  });

  it("allows whitespace in ori_question, unlike a whitespace-only query", async () => {
    expect(web(await run("web_search", { query: "valid", ori_question: " " })).status).toBe("success");
  });

  it.each([
    {},
    { queries: ["x"] },
    { query: null },
    { query: 123 },
    { query: "" },
    { query: " \t\n" },
    { query: "x".repeat(129) },
    { query: "😀".repeat(129) },
    { query: "e\u0301".repeat(65) },
    { query: "x", count: 0 },
    { query: "x", count: 51 },
    { query: "x", count: 1.5 },
    { query: "x", count: "10" },
    { query: "x", count: null },
    { query: "x", count: NaN },
    { query: "x", ori_question: "" },
    { query: "x", ori_question: "😀".repeat(129) },
    { query: "x", ori_question: 1 },
    { query: "x", localization: null },
    { query: "x", need_content: "true" },
    { query: "x", need_content: null },
    { query: "x", unknown: true },
  ])("rejects invalid search input before the network: %j", async (input) => {
    expect(() => tools().tools()[0].prepareArguments?.(input)).toThrow("Invalid web_search");
    const result = await run("web_search", input);
    expect(result.isError).toBe(true);
    expect(web(result).status).toBe("error");
    expect(transport).not.toHaveBeenCalled();
  });

  it("reads an empty title and Markdown content from the fetch endpoint", async () => {
    transport.mockResolvedValue(json({ title: "", content: "# Markdown\n\nHello, world." }));
    const result = await run("web_fetch", { url: "https://example.com/article" });
    expect(transport.mock.calls[0][0]).toBe("https://api.deepseek.com/v1/tools/fetch");
    expect(JSON.parse(transport.mock.calls[0][1]?.body as string)).toEqual({
      url: "https://example.com/article",
    });
    expect(web(result)).toMatchObject({
      kind: "web_fetch",
      status: "success",
      title: "",
      content: "# Markdown\n\nHello, world.",
      truncated: false,
    });
    expect(text(result)).toContain("untrusted source data");
  });
});

describe("public URL screening", () => {
  it.each([
    "file:///etc/passwd",
    "data:text/plain,hello",
    "javascript:alert(1)",
    "ftp://example.com/a",
    "http:example.com",
    "https:///example.com",
    "https://user:pass@example.com/a",
    "https://@example.com/a",
    "https://example.com@127.0.0.1/",
    "http://localhost/",
    "http://sub.localhost./",
    "http://printer/",
    "http://service.local/",
    "http://service.internal/",
    "http://router.home.arpa/",
    "http://site.invalid/",
    "http://127.0.0.1/",
    "http://127.1/",
    "http://2130706433/",
    "http://0x7f000001/",
    "http://0177.0.0.1/",
    "http://0.0.0.0/",
    "http://10.1.2.3/",
    "http://172.16.1.1/",
    "http://172.31.255.255/",
    "http://192.168.1.1/",
    "http://169.254.169.254/",
    "http://100.64.0.1/",
    "http://224.0.0.1/",
    "http://198.18.0.1/",
    "http://192.0.2.1/",
    "http://[::1]/",
    "http://[::]/",
    "http://[fc00::1]/",
    "http://[fe80::1]/",
    "http://[::ffff:127.0.0.1]/",
    "http://[2002:7f00:1::]/",
    "http://[2001:db8::1]/",
    "http://[2001::1]/",
    "http://[3fff::1]/",
    "https://example.com/a\nb",
    "https://example.com\\@127.0.0.1",
    "https://example.com/\u0000",
    "https://example.com/\u007f",
    `https://example.com/${"x".repeat(8192)}`,
  ])("rejects nonpublic, malformed or credential-bearing URL %s", async (url) => {
    expect(publicWebUrl(url)).toBeNull();
    const result = await run("web_fetch", { url });
    expect(result.isError).toBe(true);
    expect(web(result)).not.toHaveProperty("url");
    expect(transport).not.toHaveBeenCalled();
  });

  it.each([
    "https://example.com/hello?x=1#part",
    "http://example.com:8080/path",
    "https://例子.中国/a",
    "https://8.8.8.8/",
    "https://[2001:4860:4860::8888]/",
    "https://example.com./",
  ])("allows ordinary public URLs %s", (url) => {
    expect(publicWebUrl(url)).toBe(url);
  });

  it("preserves a valid Unicode URL instead of percent-encoding past the schema length", async () => {
    const url = `https://example.com/${"汉".repeat(1000)}`;
    expect(new URL(url).href.length).toBeGreaterThan(8192);
    const tool = tools().tools()[1];
    const prepared = tool.prepareArguments?.({ url });
    expect(Value.Check(tool.parameters, prepared)).toBe(true);
    transport.mockResolvedValue(json({ title: "Page", content: "Text" }));
    const result = await run("web_fetch", { url });
    expect(result.isError).toBe(false);
    expect(JSON.parse(transport.mock.calls[0][1]?.body as string)).toEqual({ url });
    expect(web(result).url).toBe(url);
  });

  it("accepts the exact 8192-character URL boundary", () => {
    const base = "https://example.com/";
    const url = base + "x".repeat(8192 - base.length);
    expect(publicWebUrl(url)).toBe(url);
  });

  it.each([{}, { url: 1 }, { url: null }, { url: "https://example.com", content: "x" }])(
    "rejects malformed fetch arguments %j",
    async (input) => {
      expect(() => tools().tools()[1].prepareArguments?.(input)).toThrow("Invalid web_fetch");
      expect((await run("web_fetch", input)).isError).toBe(true);
      expect(transport).not.toHaveBeenCalled();
    },
  );
});

describe("runtime response validation and budgets", () => {
  it("treats a successful empty search as no results", async () => {
    transport.mockResolvedValue(json({ results: [] }));
    const result = await run();
    expect(result.isError).toBe(false);
    expect(web(result)).toMatchObject({ status: "success", results: [], message: "No matching results." });
  });

  it("preserves nullable source metadata and removes unsafe icons without loading them", async () => {
    transport.mockResolvedValue(
      json({ results: [source({ site_name: null, site_icon: "http://127.0.0.1/icon", date: null })] }),
    );
    const result = await run();
    expect(web(result).results?.[0]).toMatchObject({ site_name: null, site_icon: null, date: null });
    expect(transport).toHaveBeenCalledOnce();
  });

  it.each(Object.keys(source()))("rejects a search result missing required field %s", async (field) => {
    const item = { ...source() } as Record<string, unknown>;
    delete item[field];
    transport.mockResolvedValue(json({ results: [item] }));
    expect((await run()).isError).toBe(true);
  });

  it.each([
    null,
    [],
    {},
    { results: null },
    { results: [null] },
    { results: ["wrong"] },
    { results: Array.from({ length: 51 }, () => source()) },
    { results: [source({ title: null as unknown as string })] },
    { results: [source({ date: "yesterday" })] },
    { results: [source({ date: "2026-99-99T00:00:00Z" })] },
    { results: [source({ rerank_score: -1 })] },
    { results: [source({ rerank_score: 3.1 })] },
    { results: [source({ authority: 4.1 })] },
    { results: [source({ authority: -2 })] },
    { results: [source({ url: "file:///private" })] },
    { results: [source({ site_name: false as unknown as string })] },
  ])("rejects invalid search responses %j", async (value) => {
    transport.mockResolvedValue(json(value));
    const result = await run();
    expect(result.isError).toBe(true);
    expect(web(result).status).toBe("error");
    expect(text(result)).toContain("invalid web response");
  });

  it.each([
    null,
    {},
    { title: "page" },
    { content: "body" },
    { title: null, content: "body" },
    { title: "", content: "" },
    { title: "", content: " \n" },
  ])("rejects missing or empty fetch content %j", async (value) => {
    transport.mockResolvedValue(json(value));
    expect((await run("web_fetch", { url: "https://example.com" })).isError).toBe(true);
  });

  it.each(["", "not JSON", '{"results":'])(
    "rejects empty or malformed JSON without exposing it: %s",
    async (body) => {
      transport.mockResolvedValue(new Response(body));
      const result = await run();
      expect(result.isError).toBe(true);
      expect(text(result)).toContain("invalid web response");
    },
  );

  it("rejects a missing body", async () => {
    transport.mockResolvedValue(new Response(null));
    expect((await run()).isError).toBe(true);
  });

  it("keeps 50 compact sources", async () => {
    transport.mockResolvedValue(
      json({
        results: Array.from({ length: 50 }, (_, index) => source({ title: `Source ${index}`, content: "" })),
      }),
    );
    const result = await run("web_search", { query: "all", count: 50 });
    expect(web(result).results).toHaveLength(50);
    expect(web(result).truncated).toBe(false);
    expect(text(result).length).toBeLessThanOrEqual(WEB_TEXT_LIMIT);
  });

  it("bounds model search text including JSON escaping and marks truncation", async () => {
    transport.mockResolvedValue(
      json({
        results: Array.from({ length: 50 }, () =>
          source({ content: "\u0000\n😀".repeat(1000), summary: "long summary ".repeat(1000) }),
        ),
      }),
    );
    const result = await run("web_search", { query: "all", count: 50, need_content: true });
    expect(result.isError).toBe(false);
    expect(text(result).length).toBeLessThanOrEqual(WEB_TEXT_LIMIT);
    expect(web(result).truncated).toBe(true);
    expect(text(result)).toContain("truncated");
    expect(JSON.stringify(result.details).length).toBeLessThanOrEqual(WEB_TEXT_LIMIT + 1000);
  });

  it("bounds fetched Markdown and title without splitting surrogate pairs", async () => {
    transport.mockResolvedValue(json({ title: "😀".repeat(1000), content: "😀".repeat(100_000) }));
    const result = await run("web_fetch", { url: "https://example.com/article" });
    expect(text(result).length).toBeLessThanOrEqual(WEB_TEXT_LIMIT);
    expect(web(result).truncated).toBe(true);
    expect(web(result).content?.isWellFormed()).toBe(true);
    expect(web(result).title?.isWellFormed()).toBe(true);
  });

  it("rejects an oversized content-length before reading", async () => {
    const cancel = vi.fn();
    const body = new ReadableStream({ cancel });
    transport.mockResolvedValue(
      new Response(body, { headers: { "content-length": String(WEB_RESPONSE_BYTES + 1) } }),
    );
    const result = await run();
    expect(result.isError).toBe(true);
    expect(web(result).truncated).toBe(true);
    expect(cancel).toHaveBeenCalled();
  });

  it("bounds streamed bytes even with no or incorrect content-length", async () => {
    const cancel = vi.fn();
    const body = new ReadableStream<Uint8Array>({
      start(controller) {
        controller.enqueue(new Uint8Array(WEB_RESPONSE_BYTES));
        controller.enqueue(new Uint8Array(1));
      },
      cancel,
    });
    transport.mockResolvedValue(new Response(body, { headers: { "content-length": "1" } }));
    const result = await run();
    expect(web(result)).toMatchObject({ status: "error", truncated: true });
    expect(cancel).toHaveBeenCalled();
    expect(transport).toHaveBeenCalledOnce();
  });

  it("decodes split multibyte UTF-8 chunks correctly", async () => {
    const bytes = new TextEncoder().encode(JSON.stringify({ title: "你好😀", content: "正文😀" }));
    const body = new ReadableStream<Uint8Array>({
      start(controller) {
        for (const byte of bytes) controller.enqueue(Uint8Array.of(byte));
        controller.close();
      },
    });
    transport.mockResolvedValue(new Response(body));
    const result = await run("web_fetch", { url: "https://example.com" });
    expect(web(result).content).toBe("正文😀");
  });
});

describe("failure handling, retries, and redaction", () => {
  it.each([400, 401, 402, 403, 404, 405, 413, 422])(
    "does not retry HTTP %i or echo its body",
    async (status) => {
      transport.mockImplementation(async () =>
        json({ error: { message: `private server error ${key}` } }, status),
      );
      const update = vi.fn();
      const result = await finish(execute("web_search", { query: "x" }, undefined, update));
      expect(transport).toHaveBeenCalledOnce();
      expect(result.isError).toBe(true);
      expect(web(result).status).toBe("error");
      expect(web(update.mock.calls.at(-1)?.[0])).toEqual(web(result));
      expect(JSON.stringify(result)).not.toContain(key);
      expect(JSON.stringify(result)).not.toContain("private server error");
    },
  );

  it.each([408, 429, 500, 502, 503, 504])(
    "limits retries for HTTP %i to three total attempts",
    async (status) => {
      transport.mockImplementation(async () => json({ error: key }, status));
      const result = await run();
      expect(transport).toHaveBeenCalledTimes(3);
      expect(result.isError).toBe(true);
      expect(text(result)).toContain("after limited retries");
    },
  );

  it("recovers from a retryable response while preserving request fields", async () => {
    transport
      .mockResolvedValueOnce(json({ error: "limited" }, 429, { "retry-after": "1" }))
      .mockResolvedValueOnce(json({ results: [source()] }));
    const result = await run("web_search", { query: "x", count: 3 });
    expect(result.isError).toBe(false);
    expect(transport).toHaveBeenCalledTimes(2);
    expect(transport.mock.calls[0][1]?.body).toBe(transport.mock.calls[1][1]?.body);
  });

  it.each(["30", "999999", "9".repeat(400), "Sat, 17 Oct 2026 00:10:00 GMT"])(
    "does not retry early when Retry-After %s exceeds the deadline",
    async (header) => {
      const times: number[] = [];
      transport.mockImplementation(async () => {
        times.push(Date.now());
        return times.length === 1 ? json({}, 429, { "retry-after": header }) : json({ results: [] });
      });
      const result = await run();
      expect(times).toHaveLength(1);
      expect(result.isError).toBe(true);
      expect(text(result)).toContain("retry wait");
    },
  );

  it("honors a ten-second Retry-After without retrying at the fallback cap", async () => {
    const times: number[] = [];
    transport.mockImplementation(async () => {
      times.push(Date.now());
      return times.length === 1 ? json({}, 429, { "retry-after": "10" }) : json({ results: [] });
    });
    const pending = execute();
    await vi.advanceTimersByTimeAsync(9999);
    expect(times).toHaveLength(1);
    await vi.advanceTimersByTimeAsync(1);
    expect((await pending).isError).toBe(false);
    expect(times[1] - times[0]).toBe(10_000);
  });

  it("uses Retry-After HTTP dates and defaults invalid values", async () => {
    const times: number[] = [];
    transport.mockImplementation(async () => {
      times.push(Date.now());
      if (times.length === 1) return json({}, 503, { "retry-after": "Sat, 17 Oct 2026 00:00:02 GMT" });
      if (times.length === 2) return json({}, 503, { "retry-after": "nonsense" });
      return json({ results: [] });
    });
    await run();
    expect(times[1] - times[0]).toBe(2000);
    expect(times[2] - times[1]).toBe(1000);
  });

  it.each([301, 302, 303, 307, 308])(
    "stops endpoint redirects (HTTP %i) without visiting Location",
    async (status) => {
      transport.mockResolvedValue(
        new Response(null, { status, headers: { Location: "https://evil.example/steal" } }),
      );
      const result = await run();
      expect(transport).toHaveBeenCalledOnce();
      expect(text(result)).toContain("unexpected redirect");
      expect(JSON.stringify(result)).not.toContain("evil.example");
    },
  );

  it("rejects an already redirected transport response", async () => {
    const response = json({ results: [] });
    Object.defineProperty(response, "redirected", { value: true });
    transport.mockResolvedValue(response);
    expect((await run()).isError).toBe(true);
  });

  it.each([new Error(`request failed: Bearer ${key}`), `raw ${key}`, { message: key }])(
    "sanitizes arbitrary network failures without retries",
    async (error) => {
      transport.mockRejectedValue(error);
      const result = await run();
      expect(transport).toHaveBeenCalledOnce();
      expect(text(result)).toBe(
        "Unable to reach DeepSeek's web service. Check your connection and try again.",
      );
      expect(JSON.stringify(result)).not.toContain(key);
    },
  );

  it("sanitizes stream read errors", async () => {
    transport.mockResolvedValue(
      new Response(
        new ReadableStream({
          pull(controller) {
            controller.error(new Error(key));
          },
        }),
      ),
    );
    const result = await run();
    expect(result.isError).toBe(true);
    expect(JSON.stringify(result)).not.toContain(key);
  });

  it.each(["", " \t", "secret\ninjected"])("rejects missing or invalid configured keys %j", async (value) => {
    const result = await finish(
      execute(
        "web_search",
        { query: "x" },
        undefined,
        undefined,
        tools(() => value),
      ),
    );
    expect(result.isError).toBe(true);
    expect(text(result)).toContain("API key in Settings");
    expect(transport).not.toHaveBeenCalled();
  });

  it("reports locked credentials as configuration failure without echoing their exception", async () => {
    const result = await finish(
      execute(
        "web_search",
        { query: "x" },
        undefined,
        undefined,
        tools(() => {
          throw new Error(`locked ${key}`);
        }),
      ),
    );
    expect(text(result)).toContain("credentials");
    expect(text(result)).not.toContain("connection");
    expect(JSON.stringify(result)).not.toContain(key);
    expect(transport).not.toHaveBeenCalled();
  });

  it("redacts a key echoed in otherwise valid source content", async () => {
    transport.mockResolvedValue(
      json({ results: [source({ title: key, snippet: key, summary: key, content: key, site_name: key })] }),
    );
    const result = await run();
    expect(result.isError).toBe(false);
    expect(JSON.stringify(result)).not.toContain(key);
    expect(text(result)).toContain("[redacted]");
  });

  it("reads the key at execution time, never from tool arguments or response data", async () => {
    let current = "first-key";
    const broker = tools(() => current);
    const tool = broker.tools()[0];
    current = "second-key";
    await finish(tool.execute("call", { query: "x" }, undefined, undefined, {} as ToolContext));
    expect(transport.mock.calls[0][1]?.headers).toMatchObject({ Authorization: "Bearer second-key" });
  });
});

describe("cancellation, deadline and shared throttling", () => {
  it("returns visible cancellation without exposing abort reasons", async () => {
    const controller = new AbortController();
    const update = vi.fn();
    controller.abort(new Error(key));
    const result = await finish(execute("web_search", { query: "x" }, controller.signal, update));
    expect(result.isError).toBe(true);
    expect(web(result).status).toBe("cancelled");
    expect(web(update.mock.calls.at(-1)?.[0]).status).toBe("cancelled");
    expect(JSON.stringify(result)).not.toContain(key);
    expect(transport).not.toHaveBeenCalled();
  });

  it("aborts an in-flight request even if the injected fetch ignores its signal", async () => {
    transport.mockImplementation(() => new Promise(() => {}));
    const controller = new AbortController();
    const promise = execute("web_search", { query: "x" }, controller.signal);
    await vi.advanceTimersByTimeAsync(0);
    const signal = transport.mock.calls[0][1]?.signal;
    controller.abort();
    const result = await promise;
    expect(signal?.aborted).toBe(true);
    expect(web(result).status).toBe("cancelled");
    expect(transport).toHaveBeenCalledOnce();
  });

  it("cancels response streaming and does not report partial content as success", async () => {
    const cancel = vi.fn();
    transport.mockResolvedValue(
      new Response(
        new ReadableStream({
          start(controller) {
            controller.enqueue(new TextEncoder().encode('{"results":'));
          },
          cancel,
        }),
      ),
    );
    const controller = new AbortController();
    const promise = execute("web_search", { query: "x" }, controller.signal);
    await vi.advanceTimersByTimeAsync(0);
    controller.abort();
    const result = await promise;
    expect(web(result).status).toBe("cancelled");
    expect(cancel).toHaveBeenCalled();
    expect(transport).toHaveBeenCalledOnce();
  });

  it("cancels a queued limiter wait without starting that request or blocking later work", async () => {
    const first = execute();
    const controller = new AbortController();
    const second = execute("web_search", { query: "cancel me" }, controller.signal);
    await vi.advanceTimersByTimeAsync(0);
    controller.abort();
    expect(web(await second).status).toBe("cancelled");
    const third = execute("web_search", { query: "third" });
    await finish(third);
    await first;
    expect(transport).toHaveBeenCalledTimes(2);
    expect(JSON.parse(transport.mock.calls[1][1]?.body as string).query).toBe("third");
  });

  it("cancels a retry wait without another HTTP request", async () => {
    transport.mockImplementation(async () => json({}, 429, { "retry-after": "20" }));
    const controller = new AbortController();
    const promise = execute("web_search", { query: "x" }, controller.signal);
    await vi.advanceTimersByTimeAsync(0);
    controller.abort();
    expect(web(await promise).status).toBe("cancelled");
    await vi.runAllTimersAsync();
    expect(transport).toHaveBeenCalledOnce();
  });

  it.each(["request", "body"])("applies a single total deadline to a stalled %s", async (stage) => {
    if (stage === "request") transport.mockImplementation(() => new Promise(() => {}));
    else transport.mockResolvedValue(new Response(new ReadableStream()));
    const promise = execute();
    await vi.advanceTimersByTimeAsync(WEB_DEADLINE_MS - 1);
    expect(transport.mock.calls[0][1]?.signal?.aborted).toBe(false);
    await vi.advanceTimersByTimeAsync(1);
    const result = await promise;
    expect(web(result).status).toBe("error");
    expect(text(result)).toContain("30-second deadline");
    expect(transport.mock.calls[0][1]?.signal?.aborted).toBe(true);
  });

  it("discards a response that arrives after cancellation", async () => {
    let resolve!: (response: Response) => void;
    transport.mockImplementation(
      () =>
        new Promise((done) => {
          resolve = done;
        }),
    );
    const controller = new AbortController();
    const promise = execute("web_search", { query: "x" }, controller.signal);
    await vi.advanceTimersByTimeAsync(0);
    controller.abort();
    expect(web(await promise).status).toBe("cancelled");
    const cancel = vi.fn();
    resolve(new Response(new ReadableStream({ cancel })));
    await vi.advanceTimersByTimeAsync(0);
    expect(cancel).toHaveBeenCalled();
  });

  it("shares a five-QPS operation limiter across instances and keys", async () => {
    const times: number[] = [];
    transport.mockImplementation(async () => {
      times.push(Date.now());
      return json({ results: [] });
    });
    const pending = Array.from({ length: 8 }, (_, index) =>
      execute(
        "web_search",
        { query: String(index) },
        undefined,
        undefined,
        tools(() => `key-${index}`),
      ),
    );
    await vi.runAllTimersAsync();
    await Promise.all(pending);
    expect(times).toHaveLength(8);
    for (let index = 1; index < times.length; index++)
      expect(times[index] - times[index - 1]).toBeGreaterThanOrEqual(200);
    expect(times[5] - times[0]).toBeGreaterThanOrEqual(1000);
  });

  it("uses independent search and fetch quotas", async () => {
    const times: number[] = [];
    transport.mockImplementation(async (url) => {
      times.push(Date.now());
      return String(url).endsWith("/fetch")
        ? json({ title: "Page", content: "Markdown" })
        : json({ results: [] });
    });
    const search = execute();
    const fetch = execute("web_fetch", { url: "https://example.com" });
    await vi.runAllTimersAsync();
    await Promise.all([search, fetch]);
    expect(times[0]).toBe(times[1]);
  });
});

describe("web provider runtime authorization", () => {
  it.each(["disabled", "removed", "replaced"])(
    "rechecks settings immediately before the first request after %s",
    async (change) => {
      let current = key;
      let disabled = false;
      const broker = tools(() => {
        if (disabled) throw new Error("Private credential details");
        return current;
      });
      const pending = execute(
        "web_search",
        { query: "Example" },
        undefined,
        () => {
          disabled = change === "disabled";
          current = change === "removed" ? "" : change === "replaced" ? "new-fake-key" : key;
        },
        broker,
      );
      const result = await finish(pending);
      expect(result.isError).toBe(true);
      expect(text(result)).toContain("settings changed");
      expect(text(result)).not.toMatch(/Private|new-fake-key/);
      expect(transport).not.toHaveBeenCalled();
    },
  );

  it.each(["disabled", "removed", "replaced"])(
    "stops retrying when %s during Retry-After",
    async (change) => {
      let enabled = true;
      let current = key;
      transport.mockResolvedValue(json({}, 429, { "retry-after": "1" }));
      const pending = execute(
        "web_search",
        { query: "Example" },
        undefined,
        undefined,
        tools(() => {
          if (!enabled) throw new Error("Disabled");
          return current;
        }),
      );
      await vi.advanceTimersByTimeAsync(500);
      expect(transport).toHaveBeenCalledOnce();
      enabled = change !== "disabled";
      current = change === "removed" ? "" : change === "replaced" ? "new-fake-key" : key;
      const result = await finish(pending);
      expect(result.isError).toBe(true);
      expect(text(result)).toContain("settings changed");
      expect(transport).toHaveBeenCalledOnce();
    },
  );

  it("blocks a throttled request when the provider is disabled during its wait", async () => {
    let enabled = true;
    const broker = tools(() => {
      if (!enabled) throw new Error("Disabled");
      return key;
    });
    const first = execute("web_search", { query: "First" }, undefined, undefined, broker);
    await vi.advanceTimersByTimeAsync(0);
    expect((await first).isError).toBe(false);
    const pending = execute("web_search", { query: "Queued" }, undefined, undefined, broker);
    await vi.advanceTimersByTimeAsync(100);
    expect(transport).toHaveBeenCalledOnce();
    enabled = false;
    expect((await finish(pending)).isError).toBe(true);
    expect(transport).toHaveBeenCalledOnce();
  });
});