feat(ai): compat entrypoint, core-only root barrel (phase 5)
The root barrel is now core-only and side-effect free: types, createModels/createProvider, auth substrate, lazyStream/lazyApi, faux, utils. Generated catalogs, api-registry, env-api-keys, images, global stream functions, and per-API lazy wrappers leave the root. New @earendil-works/pi-ai/compat preserves the old surface verbatim as a strict superset of the root: api-dispatch stream/complete with env key injection, the builtin registration side effect (skip-if-present so it cannot clobber earlier overrides), deprecated getModel/getModels/ getProviders aliases of the new getBuiltin* reads in providers/all, lazy api wrappers + setBedrockProviderModule, and image generation. Compat dies with the coding-agent ModelManager migration. Packaging: exports map gains ./compat, ./providers/*, ./api/*; sideEffects array lists only the effectful modules. Old-global imports across agent/coding-agent/examples and pi-ai tests switch to /compat (path-only; compat is a superset). The coding-agent extension loader resolves the pi-ai ROOT specifier to compat, so existing user extensions using the old global API keep working at runtime until compat is removed. vitest configs alias /compat to src; browser smoke imports old globals from /compat.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { loginAnthropic, refreshAnthropicToken } from "../src/utils/oauth/anthropic.ts";
|
||||
import type { AuthEvent, AuthPrompt } from "../src/auth/types.ts";
|
||||
import { anthropicOAuth, loginAnthropic, refreshAnthropicToken } from "../src/utils/oauth/anthropic.ts";
|
||||
|
||||
function jsonResponse(body: unknown, status: number = 200): Response {
|
||||
return new Response(JSON.stringify(body), {
|
||||
@@ -96,4 +97,38 @@ describe.sequential("Anthropic OAuth", () => {
|
||||
expect(credentials.refresh).toBe("new-refresh-token");
|
||||
expect(fetchMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("anthropicOAuth.login resolves through the manual_code prompt and aborts it after settling", async () => {
|
||||
const fetchMock = vi.fn(async (input: unknown): Promise<Response> => {
|
||||
const url = typeof input === "string" ? input : String(input);
|
||||
if (url.includes("/oauth/token")) {
|
||||
return jsonResponse({ access_token: "access", refresh_token: "refresh", expires_in: 3600 });
|
||||
}
|
||||
throw new Error(`Unexpected fetch: ${url}`);
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const events: AuthEvent[] = [];
|
||||
const prompts: AuthPrompt[] = [];
|
||||
let manualSignal: AbortSignal | undefined;
|
||||
|
||||
const credential = await anthropicOAuth.login({
|
||||
notify: (event) => events.push(event),
|
||||
prompt: async (prompt) => {
|
||||
prompts.push(prompt);
|
||||
if (prompt.type === "manual_code") {
|
||||
manualSignal = prompt.signal;
|
||||
return "the-code";
|
||||
}
|
||||
throw new Error(`Unexpected prompt: ${prompt.type}`);
|
||||
},
|
||||
});
|
||||
|
||||
expect(credential.type).toBe("oauth");
|
||||
expect(credential.access).toBe("access");
|
||||
expect(events.some((e) => e.type === "auth_url")).toBe(true);
|
||||
expect(prompts.some((p) => p.type === "manual_code")).toBe(true);
|
||||
// the prompt's signal is aborted once login settles, so UIs can dismiss it
|
||||
expect(manualSignal?.aborted).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user