e689351597
- Centralized OAuth access lifecycle in `AuthStorage`, returning identity metadata and new access-result types. - Added 60-second skew and strict expiry checks, returning undefined/throws for stale or expired OAuth credentials. - Removed provider-local token refresh flows from Gemini, Gemini CLI, Antigravity, Kimi, and related OAuth helpers. - Migrated web-search providers from `AgentStorage` to `AuthStorage` session-aware lookup with `authStorage`/`sessionId`/`signal` flow. - Replaced `findAnthropicAuth`/DB auth lookup with `buildAnthropicAuthConfig` and explicit base-url override/env fallback ordering.
92 lines
2.7 KiB
TypeScript
92 lines
2.7 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test";
|
|
import type { AuthStorage } from "@oh-my-pi/pi-ai";
|
|
import { hookFetch } from "@oh-my-pi/pi-utils";
|
|
import { searchWithKagi } from "../../src/web/kagi";
|
|
import { searchKagi } from "../../src/web/search/providers/kagi";
|
|
import { SearchProviderError } from "../../src/web/search/types";
|
|
|
|
const fakeAuthStorage = {
|
|
async getApiKey() {
|
|
return process.env.KAGI_API_KEY ?? undefined;
|
|
},
|
|
hasAuth() {
|
|
return Boolean(process.env.KAGI_API_KEY);
|
|
},
|
|
} as unknown as AuthStorage;
|
|
|
|
describe("Kagi web search error handling", () => {
|
|
beforeEach(() => {
|
|
process.env.KAGI_API_KEY = "test-kagi-key";
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
delete process.env.KAGI_API_KEY;
|
|
});
|
|
|
|
it("surfaces beta access denial messages from JSON error bodies", async () => {
|
|
const providerMessage =
|
|
"Kagi Search API is in beta. Please contact support@kagi.com to enable API access for your account.";
|
|
|
|
using _hook = hookFetch(
|
|
() =>
|
|
new Response(JSON.stringify({ error: [{ code: 401, msg: providerMessage }] }), {
|
|
status: 401,
|
|
headers: { "Content-Type": "application/json" },
|
|
}),
|
|
);
|
|
|
|
try {
|
|
await searchKagi({ query: "kagi beta", authStorage: fakeAuthStorage });
|
|
expect.unreachable("expected searchKagi to throw");
|
|
} catch (error) {
|
|
expect(error).toBeInstanceOf(SearchProviderError);
|
|
expect(error).toMatchObject({ provider: "kagi", status: 401 });
|
|
expect((error as Error).message).toBe("kagi: 401 unauthorized");
|
|
}
|
|
});
|
|
|
|
it("falls back to plain text for non-JSON error bodies", async () => {
|
|
using _hook = hookFetch(() => new Response("upstream unavailable", { status: 503 }));
|
|
|
|
await expect(searchWithKagi("plain text error", {}, fakeAuthStorage)).rejects.toThrow(
|
|
"Kagi API error (503): upstream unavailable",
|
|
);
|
|
});
|
|
|
|
it("preserves successful search parsing", async () => {
|
|
using _hook = hookFetch(
|
|
() =>
|
|
new Response(
|
|
JSON.stringify({
|
|
meta: { id: "req-kagi-success" },
|
|
data: [
|
|
{
|
|
t: 0,
|
|
url: "https://example.com/article",
|
|
title: "Example Article",
|
|
snippet: "Example snippet",
|
|
published: "2025-01-01T00:00:00Z",
|
|
},
|
|
{ t: 1, list: ["What is Kagi Search API beta access?"] },
|
|
],
|
|
}),
|
|
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
),
|
|
);
|
|
|
|
await expect(searchWithKagi("success case", {}, fakeAuthStorage)).resolves.toEqual({
|
|
requestId: "req-kagi-success",
|
|
sources: [
|
|
{
|
|
title: "Example Article",
|
|
url: "https://example.com/article",
|
|
snippet: "Example snippet",
|
|
publishedDate: "2025-01-01T00:00:00Z",
|
|
},
|
|
],
|
|
relatedQuestions: ["What is Kagi Search API beta access?"],
|
|
});
|
|
});
|
|
});
|