Archived
Migrate test doubles + testSupport to ModelRuntime/InMemoryCredentialStore (slice 5)
Replace AuthStorage.inMemory / ModelRegistry.create|inMemory across all test
and support code with the pi-ai InMemoryCredentialStore + async
ModelRuntime.create({ credentials }). Add shared test-runtime seams
(createTestModelRuntime, testModelRuntime, seedCredential) in testSupport.ts
and thread modelRuntime into fakeRuntime and every PiSessionService
construction (now a required dependency). Rework the anthropic subscription
warning tests onto a temp auth.json seam read via readStoredCredential, and
the auth-loss warning test onto a live credential store + runtime refresh.
Make getLoginProviderOptions synchronous and fix associated await/lint sites.
npm run verify green (typecheck + lint + knip + 1390 tests).
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { AuthStorage, ModelRegistry } from "@earendil-works/pi-coding-agent";
|
||||
import { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
||||
import { InMemoryCredentialStore, type Credential } from "@earendil-works/pi-ai";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OAuthFlowState } from "../../shared/apiTypes.js";
|
||||
import { AuthService, type AuthChange } from "./authService.js";
|
||||
@@ -14,85 +15,84 @@ afterEach(async () => {
|
||||
});
|
||||
|
||||
describe("AuthService", () => {
|
||||
it("saves API keys and emits a global auth change", () => {
|
||||
const { auth, authStorage, changes } = createAuthService();
|
||||
it("saves API keys and emits a global auth change", async () => {
|
||||
const { auth, credentials, changes } = await createAuthService();
|
||||
|
||||
expect(auth.saveApiKey("anthropic", "sk-test")).toEqual({ accepted: true });
|
||||
await expect(auth.saveApiKey("anthropic", "sk-test")).resolves.toEqual({ accepted: true });
|
||||
|
||||
expect(authStorage.get("anthropic")).toEqual({ type: "api_key", key: "sk-test" });
|
||||
await expect(credentials.read("anthropic")).resolves.toEqual({ type: "api_key", key: "sk-test" });
|
||||
expect(changes).toEqual([{}]);
|
||||
auth.dispose();
|
||||
});
|
||||
|
||||
it("logs out providers and emits the removed provider id", () => {
|
||||
const { auth, authStorage, changes } = createAuthService({ anthropic: { type: "api_key", key: "sk-test" } });
|
||||
it("logs out providers and emits the removed provider id", async () => {
|
||||
const { auth, credentials, changes } = await createAuthService({ anthropic: { type: "api_key", key: "sk-test" } });
|
||||
|
||||
expect(auth.logoutProvider("anthropic")).toEqual({ accepted: true });
|
||||
await expect(auth.logoutProvider("anthropic")).resolves.toEqual({ accepted: true });
|
||||
|
||||
expect(authStorage.get("anthropic")).toBeUndefined();
|
||||
await expect(credentials.read("anthropic")).resolves.toBeUndefined();
|
||||
expect(changes).toEqual([{ removedProviderId: "anthropic" }]);
|
||||
auth.dispose();
|
||||
});
|
||||
|
||||
it("rejects blank API keys", () => {
|
||||
const { auth, changes } = createAuthService();
|
||||
it("rejects blank API keys", async () => {
|
||||
const { auth, changes } = await createAuthService();
|
||||
|
||||
expect(() => { auth.saveApiKey("anthropic", " "); }).toThrow("API key is required");
|
||||
await expect(auth.saveApiKey("anthropic", " ")).rejects.toThrow("API key is required");
|
||||
expect(changes).toEqual([]);
|
||||
auth.dispose();
|
||||
});
|
||||
|
||||
it("stores credentials in the configured agent directory", async () => {
|
||||
const agentDir = await tempAgentDir();
|
||||
const auth = new AuthService({ agentDir });
|
||||
const auth = await AuthService.create({ agentDir });
|
||||
|
||||
auth.saveApiKey("anthropic", "sk-test");
|
||||
await auth.saveApiKey("anthropic", "sk-test");
|
||||
|
||||
await expect(readFile(join(agentDir, "auth.json"), "utf8")).resolves.toContain("sk-test");
|
||||
auth.dispose();
|
||||
});
|
||||
|
||||
it("refreshes auth state after OAuth login completes", () => {
|
||||
const authStorage = AuthStorage.inMemory();
|
||||
const modelRegistry = ModelRegistry.create(authStorage);
|
||||
it("refreshes auth state after OAuth login completes", async () => {
|
||||
const runtime = await ModelRuntime.create({ credentials: new InMemoryCredentialStore() });
|
||||
const authFlows = new CapturingOAuthLoginFlowService();
|
||||
const auth = new AuthService({ modelRegistry, authFlows });
|
||||
const auth = await AuthService.create({ runtime, authFlows });
|
||||
const changes: AuthChange[] = [];
|
||||
auth.subscribe((change) => { changes.push(change); });
|
||||
const reload = vi.spyOn(authStorage, "reload");
|
||||
const refresh = vi.spyOn(modelRegistry, "refresh");
|
||||
const provider = authStorage.getOAuthProviders().find((option) => option.id === "anthropic");
|
||||
const refresh = vi.spyOn(runtime, "refresh");
|
||||
const provider = runtime.getProviders().find((option) => option.id === "anthropic" && option.auth.oauth !== undefined);
|
||||
if (provider === undefined) throw new Error("Expected built-in OAuth provider");
|
||||
|
||||
expect(auth.startOAuthLogin(provider.id)).toMatchObject({ providerId: provider.id, providerName: provider.name, status: "running" });
|
||||
await expect(auth.startOAuthLogin(provider.id)).resolves.toMatchObject({ providerId: provider.id, providerName: provider.name, status: "running" });
|
||||
|
||||
const startOptions = authFlows.startCalls.at(0);
|
||||
if (startOptions === undefined) throw new Error("Expected OAuth flow to start");
|
||||
expect(startOptions.providerId).toBe(provider.id);
|
||||
expect(startOptions.providerName).toBe(provider.name);
|
||||
expect(startOptions.authStorage).toBe(authStorage);
|
||||
expect(startOptions.runtime).toBe(runtime);
|
||||
expect(changes).toEqual([]);
|
||||
|
||||
reload.mockClear();
|
||||
refresh.mockClear();
|
||||
if (startOptions.onComplete === undefined) throw new Error("Expected OAuth completion callback");
|
||||
startOptions.onComplete();
|
||||
await vi.waitFor(() => { expect(changes).toEqual([{}]); });
|
||||
|
||||
expect(reload).toHaveBeenCalledOnce();
|
||||
expect(refresh).toHaveBeenCalledOnce();
|
||||
expect(changes).toEqual([{}]);
|
||||
auth.dispose();
|
||||
expect(authFlows.disposed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
function createAuthService(data: Parameters<typeof AuthStorage.inMemory>[0] = {}) {
|
||||
const authStorage = AuthStorage.inMemory(data);
|
||||
const modelRegistry = ModelRegistry.create(authStorage);
|
||||
const auth = new AuthService({ modelRegistry });
|
||||
async function createAuthService(seed: Record<string, Credential> = {}) {
|
||||
const credentials = new InMemoryCredentialStore();
|
||||
for (const [providerId, credential] of Object.entries(seed)) {
|
||||
await credentials.modify(providerId, () => Promise.resolve(credential));
|
||||
}
|
||||
const runtime = await ModelRuntime.create({ credentials });
|
||||
const auth = await AuthService.create({ runtime });
|
||||
const changes: AuthChange[] = [];
|
||||
auth.subscribe((change) => { changes.push(change); });
|
||||
return { auth, authStorage, changes };
|
||||
return { auth, credentials, changes };
|
||||
}
|
||||
|
||||
async function tempAgentDir(): Promise<string> {
|
||||
|
||||
Reference in New Issue
Block a user