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:
@@ -24,8 +24,8 @@ describe("auth provider options", () => {
|
|||||||
expect(isApiKeyLoginProvider("openai", new Set(["openai-codex"]))).toBe(true);
|
expect(isApiKeyLoginProvider("openai", new Set(["openai-codex"]))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("builds login options for OAuth-only, dual-auth, and API-key providers", async () => {
|
it("builds login options for OAuth-only, dual-auth, and API-key providers", () => {
|
||||||
const options = await getLoginProviderOptions(runtime());
|
const options = getLoginProviderOptions(runtime());
|
||||||
expect(options).toEqual(expect.arrayContaining([
|
expect(options).toEqual(expect.arrayContaining([
|
||||||
expect.objectContaining({ id: "anthropic", authType: "oauth" }),
|
expect.objectContaining({ id: "anthropic", authType: "oauth" }),
|
||||||
expect.objectContaining({ id: "anthropic", authType: "api_key" }),
|
expect.objectContaining({ id: "anthropic", authType: "api_key" }),
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export interface AuthProviderRuntime {
|
|||||||
getProviderAuthStatus(providerId: string): AuthProviderStatus;
|
getProviderAuthStatus(providerId: string): AuthProviderStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLoginProviderOptions(runtime: AuthProviderRuntime, authType?: AuthType): Promise<AuthProviderOption[]> {
|
export function getLoginProviderOptions(runtime: AuthProviderRuntime, authType?: AuthType): AuthProviderOption[] {
|
||||||
const providers = runtime.getProviders();
|
const providers = runtime.getProviders();
|
||||||
const oauthProviderIds = new Set(providers.filter((provider) => provider.auth.oauth !== undefined).map((provider) => provider.id));
|
const oauthProviderIds = new Set(providers.filter((provider) => provider.auth.oauth !== undefined).map((provider) => provider.id));
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { AuthService } from "./authService.js";
|
|||||||
export function registerAuthRoutes(app: FastifyInstance, auth: AuthService, prefix = ""): void {
|
export function registerAuthRoutes(app: FastifyInstance, auth: AuthService, prefix = ""): void {
|
||||||
app.get<{ Querystring: { mode?: "login" | "logout"; authType?: "oauth" | "api_key" } }>(`${prefix}/auth/providers`, async (request, reply) => {
|
app.get<{ Querystring: { mode?: "login" | "logout"; authType?: "oauth" | "api_key" } }>(`${prefix}/auth/providers`, async (request, reply) => {
|
||||||
try {
|
try {
|
||||||
return auth.authProviders(request.query.mode ?? "login", request.query.authType);
|
return await auth.authProviders(request.query.mode ?? "login", request.query.authType);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return reply.code(404).send({ error: error instanceof Error ? error.message : String(error) });
|
return reply.code(404).send({ error: error instanceof Error ? error.message : String(error) });
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@ export function registerAuthRoutes(app: FastifyInstance, auth: AuthService, pref
|
|||||||
|
|
||||||
app.post<{ Body: { providerId: string; key: string } }>(`${prefix}/auth/api-key`, async (request, reply) => {
|
app.post<{ Body: { providerId: string; key: string } }>(`${prefix}/auth/api-key`, async (request, reply) => {
|
||||||
try {
|
try {
|
||||||
return auth.saveApiKey(request.body.providerId, request.body.key);
|
return await auth.saveApiKey(request.body.providerId, request.body.key);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return reply.code(400).send({ error: error instanceof Error ? error.message : String(error) });
|
return reply.code(400).send({ error: error instanceof Error ? error.message : String(error) });
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,7 @@ export function registerAuthRoutes(app: FastifyInstance, auth: AuthService, pref
|
|||||||
|
|
||||||
app.post<{ Body: { providerId: string } }>(`${prefix}/auth/logout`, async (request, reply) => {
|
app.post<{ Body: { providerId: string } }>(`${prefix}/auth/logout`, async (request, reply) => {
|
||||||
try {
|
try {
|
||||||
return auth.logoutProvider(request.body.providerId);
|
return await auth.logoutProvider(request.body.providerId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return reply.code(400).send({ error: error instanceof Error ? error.message : String(error) });
|
return reply.code(400).send({ error: error instanceof Error ? error.message : String(error) });
|
||||||
}
|
}
|
||||||
@@ -28,7 +28,7 @@ export function registerAuthRoutes(app: FastifyInstance, auth: AuthService, pref
|
|||||||
|
|
||||||
app.post<{ Body: { providerId: string } }>(`${prefix}/auth/oauth`, async (request, reply) => {
|
app.post<{ Body: { providerId: string } }>(`${prefix}/auth/oauth`, async (request, reply) => {
|
||||||
try {
|
try {
|
||||||
return auth.startOAuthLogin(request.body.providerId);
|
return await auth.startOAuthLogin(request.body.providerId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return reply.code(400).send({ error: error instanceof Error ? error.message : String(error) });
|
return reply.code(400).send({ error: error instanceof Error ? error.message : String(error) });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
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 { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import type { OAuthFlowState } from "../../shared/apiTypes.js";
|
import type { OAuthFlowState } from "../../shared/apiTypes.js";
|
||||||
import { AuthService, type AuthChange } from "./authService.js";
|
import { AuthService, type AuthChange } from "./authService.js";
|
||||||
@@ -14,85 +15,84 @@ afterEach(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("AuthService", () => {
|
describe("AuthService", () => {
|
||||||
it("saves API keys and emits a global auth change", () => {
|
it("saves API keys and emits a global auth change", async () => {
|
||||||
const { auth, authStorage, changes } = createAuthService();
|
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([{}]);
|
expect(changes).toEqual([{}]);
|
||||||
auth.dispose();
|
auth.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("logs out providers and emits the removed provider id", () => {
|
it("logs out providers and emits the removed provider id", async () => {
|
||||||
const { auth, authStorage, changes } = createAuthService({ anthropic: { type: "api_key", key: "sk-test" } });
|
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" }]);
|
expect(changes).toEqual([{ removedProviderId: "anthropic" }]);
|
||||||
auth.dispose();
|
auth.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects blank API keys", () => {
|
it("rejects blank API keys", async () => {
|
||||||
const { auth, changes } = createAuthService();
|
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([]);
|
expect(changes).toEqual([]);
|
||||||
auth.dispose();
|
auth.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("stores credentials in the configured agent directory", async () => {
|
it("stores credentials in the configured agent directory", async () => {
|
||||||
const agentDir = await tempAgentDir();
|
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");
|
await expect(readFile(join(agentDir, "auth.json"), "utf8")).resolves.toContain("sk-test");
|
||||||
auth.dispose();
|
auth.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("refreshes auth state after OAuth login completes", () => {
|
it("refreshes auth state after OAuth login completes", async () => {
|
||||||
const authStorage = AuthStorage.inMemory();
|
const runtime = await ModelRuntime.create({ credentials: new InMemoryCredentialStore() });
|
||||||
const modelRegistry = ModelRegistry.create(authStorage);
|
|
||||||
const authFlows = new CapturingOAuthLoginFlowService();
|
const authFlows = new CapturingOAuthLoginFlowService();
|
||||||
const auth = new AuthService({ modelRegistry, authFlows });
|
const auth = await AuthService.create({ runtime, authFlows });
|
||||||
const changes: AuthChange[] = [];
|
const changes: AuthChange[] = [];
|
||||||
auth.subscribe((change) => { changes.push(change); });
|
auth.subscribe((change) => { changes.push(change); });
|
||||||
const reload = vi.spyOn(authStorage, "reload");
|
const refresh = vi.spyOn(runtime, "refresh");
|
||||||
const refresh = vi.spyOn(modelRegistry, "refresh");
|
const provider = runtime.getProviders().find((option) => option.id === "anthropic" && option.auth.oauth !== undefined);
|
||||||
const provider = authStorage.getOAuthProviders().find((option) => option.id === "anthropic");
|
|
||||||
if (provider === undefined) throw new Error("Expected built-in OAuth provider");
|
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);
|
const startOptions = authFlows.startCalls.at(0);
|
||||||
if (startOptions === undefined) throw new Error("Expected OAuth flow to start");
|
if (startOptions === undefined) throw new Error("Expected OAuth flow to start");
|
||||||
expect(startOptions.providerId).toBe(provider.id);
|
expect(startOptions.providerId).toBe(provider.id);
|
||||||
expect(startOptions.providerName).toBe(provider.name);
|
expect(startOptions.providerName).toBe(provider.name);
|
||||||
expect(startOptions.authStorage).toBe(authStorage);
|
expect(startOptions.runtime).toBe(runtime);
|
||||||
expect(changes).toEqual([]);
|
expect(changes).toEqual([]);
|
||||||
|
|
||||||
reload.mockClear();
|
|
||||||
refresh.mockClear();
|
refresh.mockClear();
|
||||||
if (startOptions.onComplete === undefined) throw new Error("Expected OAuth completion callback");
|
if (startOptions.onComplete === undefined) throw new Error("Expected OAuth completion callback");
|
||||||
startOptions.onComplete();
|
startOptions.onComplete();
|
||||||
|
await vi.waitFor(() => { expect(changes).toEqual([{}]); });
|
||||||
|
|
||||||
expect(reload).toHaveBeenCalledOnce();
|
|
||||||
expect(refresh).toHaveBeenCalledOnce();
|
expect(refresh).toHaveBeenCalledOnce();
|
||||||
expect(changes).toEqual([{}]);
|
|
||||||
auth.dispose();
|
auth.dispose();
|
||||||
expect(authFlows.disposed).toBe(true);
|
expect(authFlows.disposed).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function createAuthService(data: Parameters<typeof AuthStorage.inMemory>[0] = {}) {
|
async function createAuthService(seed: Record<string, Credential> = {}) {
|
||||||
const authStorage = AuthStorage.inMemory(data);
|
const credentials = new InMemoryCredentialStore();
|
||||||
const modelRegistry = ModelRegistry.create(authStorage);
|
for (const [providerId, credential] of Object.entries(seed)) {
|
||||||
const auth = new AuthService({ modelRegistry });
|
await credentials.modify(providerId, () => Promise.resolve(credential));
|
||||||
|
}
|
||||||
|
const runtime = await ModelRuntime.create({ credentials });
|
||||||
|
const auth = await AuthService.create({ runtime });
|
||||||
const changes: AuthChange[] = [];
|
const changes: AuthChange[] = [];
|
||||||
auth.subscribe((change) => { changes.push(change); });
|
auth.subscribe((change) => { changes.push(change); });
|
||||||
return { auth, authStorage, changes };
|
return { auth, credentials, changes };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function tempAgentDir(): Promise<string> {
|
async function tempAgentDir(): Promise<string> {
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export class AuthService {
|
|||||||
|
|
||||||
async authProviders(mode: "login" | "logout", authType?: AuthType): Promise<AuthProvidersResponse> {
|
async authProviders(mode: "login" | "logout", authType?: AuthType): Promise<AuthProvidersResponse> {
|
||||||
await this.runtime.refresh();
|
await this.runtime.refresh();
|
||||||
const providers = mode === "logout" ? await getLogoutProviderOptions(this.runtime) : await getLoginProviderOptions(this.runtime, authType);
|
const providers = mode === "logout" ? await getLogoutProviderOptions(this.runtime) : getLoginProviderOptions(this.runtime, authType);
|
||||||
return { providers };
|
return { providers };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,8 +61,8 @@ export class AuthService {
|
|||||||
// credential through the runtime's credential store; feed the key back via a
|
// credential through the runtime's credential store; feed the key back via a
|
||||||
// non-interactive AuthInteraction.
|
// non-interactive AuthInteraction.
|
||||||
const interaction: AuthInteraction = {
|
const interaction: AuthInteraction = {
|
||||||
prompt: async () => key,
|
prompt: () => Promise.resolve(key),
|
||||||
notify: () => {},
|
notify: () => undefined,
|
||||||
};
|
};
|
||||||
await this.runtime.login(providerId, "api_key", interaction);
|
await this.runtime.login(providerId, "api_key", interaction);
|
||||||
await this.refreshAuthState();
|
await this.refreshAuthState();
|
||||||
@@ -110,7 +110,7 @@ export class AuthService {
|
|||||||
|
|
||||||
private async requireOAuthLoginProvider(providerId: string) {
|
private async requireOAuthLoginProvider(providerId: string) {
|
||||||
await this.runtime.refresh();
|
await this.runtime.refresh();
|
||||||
const provider = (await getLoginProviderOptions(this.runtime, "oauth")).find((option) => option.id === providerId);
|
const provider = getLoginProviderOptions(this.runtime, "oauth").find((option) => option.id === providerId);
|
||||||
if (provider === undefined) throw new Error(`OAuth provider not found: ${providerId}`);
|
if (provider === undefined) throw new Error(`OAuth provider not found: ${providerId}`);
|
||||||
return provider;
|
return provider;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { PiSessionService } from "./piSessionService.js";
|
import { PiSessionService } from "./piSessionService.js";
|
||||||
import { CapturingSessionEventHub, fakeRuntime, fakeSessionManager, runtimeCreator, sessionGateway, sessionRecord, sessionRef } from "./piSessionService.testSupport.js";
|
import { CapturingSessionEventHub, fakeRuntime, fakeSessionManager, runtimeCreator, sessionGateway, sessionRecord, sessionRef, testModelRuntime } from "./piSessionService.testSupport.js";
|
||||||
|
|
||||||
const TEST_AGENT_DIR = "/tmp/pi-web-test-agent";
|
const TEST_AGENT_DIR = "/tmp/pi-web-test-agent";
|
||||||
|
|
||||||
@@ -15,6 +15,7 @@ describe("PiSessionService archive and cleanup", () => {
|
|||||||
const fake = fakeRuntime("root", { sessionFile: root.path });
|
const fake = fakeRuntime("root", { sessionFile: root.path });
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||||
archiveStore: {
|
archiveStore: {
|
||||||
list: () => Promise.resolve([{ sessionId: "archived-child", cwd: "/workspace", archivedAt: "2026-01-02T00:00:00.000Z", originalPath: archivedChild.path, archivePath: "/archive/archived-child.jsonl", created: "2026-01-01T00:00:00.000Z", modified: "2026-01-01T00:01:00.000Z", messageCount: 1, firstMessage: "archived", parentSessionPath: root.path }]),
|
list: () => Promise.resolve([{ sessionId: "archived-child", cwd: "/workspace", archivedAt: "2026-01-02T00:00:00.000Z", originalPath: archivedChild.path, archivePath: "/archive/archived-child.jsonl", created: "2026-01-01T00:00:00.000Z", modified: "2026-01-01T00:01:00.000Z", messageCount: 1, firstMessage: "archived", parentSessionPath: root.path }]),
|
||||||
@@ -49,6 +50,7 @@ describe("PiSessionService archive and cleanup", () => {
|
|||||||
const deletedSessionIds: string[] = [];
|
const deletedSessionIds: string[] = [];
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
archiveStore: {
|
archiveStore: {
|
||||||
list: () => Promise.resolve([]),
|
list: () => Promise.resolve([]),
|
||||||
get: (sessionId) => Promise.resolve(sessionId === "archived" || "archived".startsWith(sessionId)
|
get: (sessionId) => Promise.resolve(sessionId === "archived" || "archived".startsWith(sessionId)
|
||||||
@@ -83,6 +85,7 @@ describe("PiSessionService archive and cleanup", () => {
|
|||||||
const archiveMany = vi.fn((inputs: readonly { sessionId: string; cwd: string }[]) => Promise.resolve(inputs.map((input) => ({ sessionId: input.sessionId, cwd: input.cwd, archivedAt: "2026-01-03T00:00:00.000Z" }))));
|
const archiveMany = vi.fn((inputs: readonly { sessionId: string; cwd: string }[]) => Promise.resolve(inputs.map((input) => ({ sessionId: input.sessionId, cwd: input.cwd, archivedAt: "2026-01-03T00:00:00.000Z" }))));
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
archiveStore: {
|
archiveStore: {
|
||||||
list: () => Promise.resolve([]),
|
list: () => Promise.resolve([]),
|
||||||
get: () => Promise.resolve(undefined),
|
get: () => Promise.resolve(undefined),
|
||||||
@@ -118,6 +121,7 @@ describe("PiSessionService archive and cleanup", () => {
|
|||||||
const archiveMany = vi.fn((inputs: readonly { sessionId: string; cwd: string }[]) => Promise.resolve(inputs.map((input) => ({ sessionId: input.sessionId, cwd: input.cwd, archivedAt: "2026-01-03T00:00:00.000Z" }))));
|
const archiveMany = vi.fn((inputs: readonly { sessionId: string; cwd: string }[]) => Promise.resolve(inputs.map((input) => ({ sessionId: input.sessionId, cwd: input.cwd, archivedAt: "2026-01-03T00:00:00.000Z" }))));
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: () => {
|
createAgentRuntime: () => {
|
||||||
createCalls += 1;
|
createCalls += 1;
|
||||||
return Promise.resolve(busy.runtime);
|
return Promise.resolve(busy.runtime);
|
||||||
@@ -159,6 +163,7 @@ describe("PiSessionService archive and cleanup", () => {
|
|||||||
const deleteArchivedMany = vi.fn((sessionIds: readonly string[]) => Promise.resolve([...sessionIds]));
|
const deleteArchivedMany = vi.fn((sessionIds: readonly string[]) => Promise.resolve([...sessionIds]));
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: runtimeCreator(busy.runtime),
|
createAgentRuntime: runtimeCreator(busy.runtime),
|
||||||
archiveStore: {
|
archiveStore: {
|
||||||
list: () => Promise.resolve([busyRecord, idleRecord]),
|
list: () => Promise.resolve([busyRecord, idleRecord]),
|
||||||
@@ -196,6 +201,7 @@ describe("PiSessionService archive and cleanup", () => {
|
|||||||
const listCalls: string[] = [];
|
const listCalls: string[] = [];
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
archiveStore: {
|
archiveStore: {
|
||||||
list: () => Promise.resolve([
|
list: () => Promise.resolve([
|
||||||
{ sessionId: "legacy-a", cwd: "/workspace", archivedAt: "2026-01-02T00:00:00.000Z" },
|
{ sessionId: "legacy-a", cwd: "/workspace", archivedAt: "2026-01-02T00:00:00.000Z" },
|
||||||
@@ -239,6 +245,7 @@ describe("PiSessionService archive and cleanup", () => {
|
|||||||
const otherArchived = { sessionId: "archived-other", cwd: "/other-project", archivedAt: "2026-04-01T00:00:00.000Z", archivePath: "/archive/archived-other.jsonl" };
|
const otherArchived = { sessionId: "archived-other", cwd: "/other-project", archivedAt: "2026-04-01T00:00:00.000Z", archivePath: "/archive/archived-other.jsonl" };
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
now: () => new Date("2026-06-25T00:00:00.000Z"),
|
now: () => new Date("2026-06-25T00:00:00.000Z"),
|
||||||
archiveStore: {
|
archiveStore: {
|
||||||
list: () => Promise.resolve([archived, otherArchived]),
|
list: () => Promise.resolve([archived, otherArchived]),
|
||||||
@@ -292,6 +299,7 @@ describe("PiSessionService archive and cleanup", () => {
|
|||||||
const deleteArchivedMany = vi.fn((sessionIds: readonly string[]) => Promise.resolve([...sessionIds]));
|
const deleteArchivedMany = vi.fn((sessionIds: readonly string[]) => Promise.resolve([...sessionIds]));
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
now: () => new Date("2026-06-25T00:00:00.000Z"),
|
now: () => new Date("2026-06-25T00:00:00.000Z"),
|
||||||
archiveStore: {
|
archiveStore: {
|
||||||
list: () => Promise.resolve([
|
list: () => Promise.resolve([
|
||||||
@@ -334,6 +342,7 @@ describe("PiSessionService archive and cleanup", () => {
|
|||||||
const archivedInputs: string[] = [];
|
const archivedInputs: string[] = [];
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
now: () => new Date("2026-06-25T00:00:00.000Z"),
|
now: () => new Date("2026-06-25T00:00:00.000Z"),
|
||||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||||
archiveStore: {
|
archiveStore: {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { tmpdir } from "node:os";
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { PiSessionService, type PiAgentSession, type PiSessionRuntime } from "./piSessionService.js";
|
import { PiSessionService, type PiAgentSession, type PiSessionRuntime } from "./piSessionService.js";
|
||||||
import { CapturingSessionEventHub, emptyArchiveStore, fakeRuntime, fakeSessionManager, runtimeCreator, sessionGateway, sessionRecord, sessionRef, type RuntimeCreator } from "./piSessionService.testSupport.js";
|
import { CapturingSessionEventHub, emptyArchiveStore, fakeRuntime, fakeSessionManager, runtimeCreator, sessionGateway, sessionRecord, sessionRef, testModelRuntime, type RuntimeCreator } from "./piSessionService.testSupport.js";
|
||||||
|
|
||||||
const TEST_AGENT_DIR = "/tmp/pi-web-test-agent";
|
const TEST_AGENT_DIR = "/tmp/pi-web-test-agent";
|
||||||
|
|
||||||
@@ -31,6 +31,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
|||||||
};
|
};
|
||||||
const service = new PiSessionService(hub, {
|
const service = new PiSessionService(hub, {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime,
|
createAgentRuntime,
|
||||||
sessionManager: sessionGateway([]),
|
sessionManager: sessionGateway([]),
|
||||||
heartbeatIntervalMs: 60_000,
|
heartbeatIntervalMs: 60_000,
|
||||||
@@ -60,6 +61,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
|||||||
try {
|
try {
|
||||||
service = new PiSessionService(hub, {
|
service = new PiSessionService(hub, {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||||
sessionManager: sessionGateway([]),
|
sessionManager: sessionGateway([]),
|
||||||
heartbeatIntervalMs: 60_000,
|
heartbeatIntervalMs: 60_000,
|
||||||
@@ -87,6 +89,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
|||||||
const open = vi.fn(() => fakeSessionManager());
|
const open = vi.fn(() => fakeSessionManager());
|
||||||
const service = new PiSessionService(hub, {
|
const service = new PiSessionService(hub, {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||||
sessionManager: {
|
sessionManager: {
|
||||||
create: () => fakeSessionManager(),
|
create: () => fakeSessionManager(),
|
||||||
@@ -136,6 +139,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
|||||||
const open = vi.spyOn(gateway, "open");
|
const open = vi.spyOn(gateway, "open");
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
archiveStore: emptyArchiveStore(),
|
archiveStore: emptyArchiveStore(),
|
||||||
createAgentRuntime,
|
createAgentRuntime,
|
||||||
sessionManager: gateway,
|
sessionManager: gateway,
|
||||||
@@ -190,6 +194,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
|||||||
};
|
};
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
archiveStore: emptyArchiveStore(),
|
archiveStore: emptyArchiveStore(),
|
||||||
createAgentRuntime,
|
createAgentRuntime,
|
||||||
sessionManager: sessionGateway([sessionRecord(sessionId)]),
|
sessionManager: sessionGateway([sessionRecord(sessionId)]),
|
||||||
@@ -230,6 +235,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
|||||||
const fake = fakeRuntime(sessionId);
|
const fake = fakeRuntime(sessionId);
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
archiveStore: emptyArchiveStore(),
|
archiveStore: emptyArchiveStore(),
|
||||||
createAgentRuntime: () => {
|
createAgentRuntime: () => {
|
||||||
createStarted.resolve();
|
createStarted.resolve();
|
||||||
@@ -264,6 +270,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
|||||||
fake.runtime.setRebindSession = (callback) => { rebindSession = callback; };
|
fake.runtime.setRebindSession = (callback) => { rebindSession = callback; };
|
||||||
const service = new PiSessionService(hub, {
|
const service = new PiSessionService(hub, {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||||
sessionManager: sessionGateway([]),
|
sessionManager: sessionGateway([]),
|
||||||
heartbeatIntervalMs: 60_000,
|
heartbeatIntervalMs: 60_000,
|
||||||
@@ -291,6 +298,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
|||||||
});
|
});
|
||||||
const service = new PiSessionService(hub, {
|
const service = new PiSessionService(hub, {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||||
sessionManager: sessionGateway([]),
|
sessionManager: sessionGateway([]),
|
||||||
heartbeatIntervalMs: 60_000,
|
heartbeatIntervalMs: 60_000,
|
||||||
@@ -326,6 +334,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
|||||||
});
|
});
|
||||||
service = new PiSessionService(hub, {
|
service = new PiSessionService(hub, {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||||
sessionManager: sessionGateway([sessionRecord("idle-session")]),
|
sessionManager: sessionGateway([sessionRecord("idle-session")]),
|
||||||
heartbeatIntervalMs: 1_000,
|
heartbeatIntervalMs: 1_000,
|
||||||
@@ -362,6 +371,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
|||||||
});
|
});
|
||||||
const service = new PiSessionService(hub, {
|
const service = new PiSessionService(hub, {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||||
sessionManager: sessionGateway([sessionRecord("completion-session")]),
|
sessionManager: sessionGateway([sessionRecord("completion-session")]),
|
||||||
heartbeatIntervalMs: 60_000,
|
heartbeatIntervalMs: 60_000,
|
||||||
@@ -381,6 +391,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
|||||||
it("uses injected archive and session-manager gateways for listing", async () => {
|
it("uses injected archive and session-manager gateways for listing", async () => {
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
archiveStore: {
|
archiveStore: {
|
||||||
list: () => Promise.resolve([{ sessionId: "archived", cwd: "/workspace", archivedAt: "2026-01-01T00:00:00.000Z" }]),
|
list: () => Promise.resolve([{ sessionId: "archived", cwd: "/workspace", archivedAt: "2026-01-01T00:00:00.000Z" }]),
|
||||||
get: () => Promise.resolve(undefined),
|
get: () => Promise.resolve(undefined),
|
||||||
@@ -411,6 +422,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
|||||||
it("lists archived records that have been moved out of the active session directory", async () => {
|
it("lists archived records that have been moved out of the active session directory", async () => {
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
archiveStore: {
|
archiveStore: {
|
||||||
list: () => Promise.resolve([{ sessionId: "archived", cwd: "/workspace", archivedAt: "2026-01-02T00:00:00.000Z", originalPath: "/sessions/archived.jsonl", archivePath: "/archive/archived.jsonl", created: "2026-01-01T00:00:00.000Z", modified: "2026-01-01T00:01:00.000Z", messageCount: 2, firstMessage: "bye" }]),
|
list: () => Promise.resolve([{ sessionId: "archived", cwd: "/workspace", archivedAt: "2026-01-02T00:00:00.000Z", originalPath: "/sessions/archived.jsonl", archivePath: "/archive/archived.jsonl", created: "2026-01-01T00:00:00.000Z", modified: "2026-01-01T00:01:00.000Z", messageCount: 2, firstMessage: "bye" }]),
|
||||||
get: () => Promise.resolve(undefined),
|
get: () => Promise.resolve(undefined),
|
||||||
@@ -442,6 +454,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
|||||||
const fake = fakeRuntime("runtime-reload-session");
|
const fake = fakeRuntime("runtime-reload-session");
|
||||||
const service = new PiSessionService(hub, {
|
const service = new PiSessionService(hub, {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||||
sessionManager: sessionGateway([sessionRecord("runtime-reload-session")]),
|
sessionManager: sessionGateway([sessionRecord("runtime-reload-session")]),
|
||||||
heartbeatIntervalMs: 60_000,
|
heartbeatIntervalMs: 60_000,
|
||||||
@@ -475,6 +488,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
|||||||
};
|
};
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime,
|
createAgentRuntime,
|
||||||
sessionManager: sessionGateway([sessionRecord("reload-session")]),
|
sessionManager: sessionGateway([sessionRecord("reload-session")]),
|
||||||
heartbeatIntervalMs: 60_000,
|
heartbeatIntervalMs: 60_000,
|
||||||
@@ -499,6 +513,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
|||||||
const fake = fakeRuntime("busy-session", { isStreaming: true });
|
const fake = fakeRuntime("busy-session", { isStreaming: true });
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||||
sessionManager: sessionGateway([sessionRecord("busy-session")]),
|
sessionManager: sessionGateway([sessionRecord("busy-session")]),
|
||||||
heartbeatIntervalMs: 60_000,
|
heartbeatIntervalMs: 60_000,
|
||||||
@@ -514,6 +529,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
|||||||
it("refuses to reload an archived session", async () => {
|
it("refuses to reload an archived session", async () => {
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
archiveStore: {
|
archiveStore: {
|
||||||
list: () => Promise.resolve([]),
|
list: () => Promise.resolve([]),
|
||||||
get: (sessionId) => Promise.resolve(sessionId === "archived" || "archived".startsWith(sessionId)
|
get: (sessionId) => Promise.resolve(sessionId === "archived" || "archived".startsWith(sessionId)
|
||||||
@@ -536,6 +552,7 @@ describe("PiSessionService lifecycle, listing, and reload", () => {
|
|||||||
const reconciliations: { cwd: string; sessionIds: string[] }[] = [];
|
const reconciliations: { cwd: string; sessionIds: string[] }[] = [];
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
archiveStore: {
|
archiveStore: {
|
||||||
list: () => Promise.resolve([{ sessionId: "archived", cwd: "/workspace", archivedAt: "2026-01-02T00:00:00.000Z", originalPath: "/sessions/archived.jsonl", archivePath: "/archive/archived.jsonl", created: "2026-01-01T00:00:00.000Z", modified: "2026-01-01T00:01:00.000Z", messageCount: 2, firstMessage: "bye" }]),
|
list: () => Promise.resolve([{ sessionId: "archived", cwd: "/workspace", archivedAt: "2026-01-02T00:00:00.000Z", originalPath: "/sessions/archived.jsonl", archivePath: "/archive/archived.jsonl", created: "2026-01-01T00:00:00.000Z", modified: "2026-01-01T00:01:00.000Z", messageCount: 2, firstMessage: "bye" }]),
|
||||||
get: () => Promise.resolve(undefined),
|
get: () => Promise.resolve(undefined),
|
||||||
@@ -573,6 +590,7 @@ describe("PiSessionService.streamSnapshot", () => {
|
|||||||
const fake = fakeRuntime("snap-idle");
|
const fake = fakeRuntime("snap-idle");
|
||||||
const service = new PiSessionService(hub, {
|
const service = new PiSessionService(hub, {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||||
sessionManager: sessionGateway([]),
|
sessionManager: sessionGateway([]),
|
||||||
heartbeatIntervalMs: 60_000,
|
heartbeatIntervalMs: 60_000,
|
||||||
@@ -601,6 +619,7 @@ describe("PiSessionService.streamSnapshot", () => {
|
|||||||
const fake = fakeRuntime("snap-live", { state: { streamingMessage } });
|
const fake = fakeRuntime("snap-live", { state: { streamingMessage } });
|
||||||
const service = new PiSessionService(hub, {
|
const service = new PiSessionService(hub, {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||||
sessionManager: sessionGateway([]),
|
sessionManager: sessionGateway([]),
|
||||||
heartbeatIntervalMs: 60_000,
|
heartbeatIntervalMs: 60_000,
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { createAssistantMessageEventStream, type AssistantMessage } from "@earendil-works/pi-ai";
|
import { createAssistantMessageEventStream, InMemoryCredentialStore, type AssistantMessage } from "@earendil-works/pi-ai";
|
||||||
import type { StreamFn } from "@earendil-works/pi-agent-core";
|
import type { StreamFn } from "@earendil-works/pi-agent-core";
|
||||||
import { AuthStorage, ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { PiSessionService } from "./piSessionService.js";
|
import { PiSessionService } from "./piSessionService.js";
|
||||||
import { CapturingSessionEventHub, fakeRuntime, runtimeCreator, sessionGateway, sessionRecord, sessionRef, TEST_MODEL_ID, TEST_MODEL_PROVIDER, testModel, type RuntimeCreator } from "./piSessionService.testSupport.js";
|
import { CapturingSessionEventHub, createTestModelRuntime, fakeRuntime, runtimeCreator, seedCredential, sessionGateway, sessionRecord, sessionRef, TEST_MODEL_ID, TEST_MODEL_PROVIDER, testModel, testModelRuntime, type RuntimeCreator } from "./piSessionService.testSupport.js";
|
||||||
|
|
||||||
const TEST_AGENT_DIR = "/tmp/pi-web-test-agent";
|
const TEST_AGENT_DIR = "/tmp/pi-web-test-agent";
|
||||||
|
|
||||||
@@ -12,6 +11,7 @@ describe("PiSessionService prompt, queue, and auth warnings", () => {
|
|||||||
const fake = fakeRuntime("prompt-session");
|
const fake = fakeRuntime("prompt-session");
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||||
sessionManager: sessionGateway([sessionRecord("prompt-session")]),
|
sessionManager: sessionGateway([sessionRecord("prompt-session")]),
|
||||||
heartbeatIntervalMs: 60_000,
|
heartbeatIntervalMs: 60_000,
|
||||||
@@ -30,6 +30,7 @@ describe("PiSessionService prompt, queue, and auth warnings", () => {
|
|||||||
const hub = new CapturingSessionEventHub();
|
const hub = new CapturingSessionEventHub();
|
||||||
const service = new PiSessionService(hub, {
|
const service = new PiSessionService(hub, {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||||
sessionManager: sessionGateway([sessionRecord("echo-session")]),
|
sessionManager: sessionGateway([sessionRecord("echo-session")]),
|
||||||
heartbeatIntervalMs: 60_000,
|
heartbeatIntervalMs: 60_000,
|
||||||
@@ -60,6 +61,7 @@ describe("PiSessionService prompt, queue, and auth warnings", () => {
|
|||||||
};
|
};
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime,
|
createAgentRuntime,
|
||||||
sessionManager: sessionGateway([sessionRecord("prompt-session")]),
|
sessionManager: sessionGateway([sessionRecord("prompt-session")]),
|
||||||
heartbeatIntervalMs: 60_000,
|
heartbeatIntervalMs: 60_000,
|
||||||
@@ -96,6 +98,7 @@ describe("PiSessionService prompt, queue, and auth warnings", () => {
|
|||||||
const fake = fakeRuntime("name-session", { model, agent: { streamFn } });
|
const fake = fakeRuntime("name-session", { model, agent: { streamFn } });
|
||||||
const service = new PiSessionService(hub, {
|
const service = new PiSessionService(hub, {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||||
sessionManager: sessionGateway([sessionRecord("name-session")]),
|
sessionManager: sessionGateway([sessionRecord("name-session")]),
|
||||||
heartbeatIntervalMs: 60_000,
|
heartbeatIntervalMs: 60_000,
|
||||||
@@ -118,6 +121,7 @@ describe("PiSessionService prompt, queue, and auth warnings", () => {
|
|||||||
});
|
});
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||||
sessionManager: sessionGateway([sessionRecord("status-session")]),
|
sessionManager: sessionGateway([sessionRecord("status-session")]),
|
||||||
heartbeatIntervalMs: 60_000,
|
heartbeatIntervalMs: 60_000,
|
||||||
@@ -139,6 +143,7 @@ describe("PiSessionService prompt, queue, and auth warnings", () => {
|
|||||||
});
|
});
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||||
sessionManager: sessionGateway([sessionRecord("dedupe-session")]),
|
sessionManager: sessionGateway([sessionRecord("dedupe-session")]),
|
||||||
heartbeatIntervalMs: 60_000,
|
heartbeatIntervalMs: 60_000,
|
||||||
@@ -155,6 +160,7 @@ describe("PiSessionService prompt, queue, and auth warnings", () => {
|
|||||||
const fake = fakeRuntime("queued-session", { isStreaming: true });
|
const fake = fakeRuntime("queued-session", { isStreaming: true });
|
||||||
const service = new PiSessionService(hub, {
|
const service = new PiSessionService(hub, {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||||
sessionManager: sessionGateway([sessionRecord("queued-session")]),
|
sessionManager: sessionGateway([sessionRecord("queued-session")]),
|
||||||
heartbeatIntervalMs: 60_000,
|
heartbeatIntervalMs: 60_000,
|
||||||
@@ -181,6 +187,7 @@ describe("PiSessionService prompt, queue, and auth warnings", () => {
|
|||||||
};
|
};
|
||||||
const service = new PiSessionService(hub, {
|
const service = new PiSessionService(hub, {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||||
sessionManager: sessionGateway([sessionRecord("compacting-session")]),
|
sessionManager: sessionGateway([sessionRecord("compacting-session")]),
|
||||||
heartbeatIntervalMs: 60_000,
|
heartbeatIntervalMs: 60_000,
|
||||||
@@ -245,6 +252,7 @@ describe("PiSessionService prompt, queue, and auth warnings", () => {
|
|||||||
fake.session.clearQueue = clearRuntimeQueue;
|
fake.session.clearQueue = clearRuntimeQueue;
|
||||||
const service = new PiSessionService(hub, {
|
const service = new PiSessionService(hub, {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||||
sessionManager: sessionGateway([sessionRecord("clear-queue-session")]),
|
sessionManager: sessionGateway([sessionRecord("clear-queue-session")]),
|
||||||
heartbeatIntervalMs: 60_000,
|
heartbeatIntervalMs: 60_000,
|
||||||
@@ -285,6 +293,7 @@ describe("PiSessionService prompt, queue, and auth warnings", () => {
|
|||||||
const fake = fakeRuntime("clear-empty-queue-session");
|
const fake = fakeRuntime("clear-empty-queue-session");
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||||
sessionManager: sessionGateway([sessionRecord("clear-empty-queue-session")]),
|
sessionManager: sessionGateway([sessionRecord("clear-empty-queue-session")]),
|
||||||
heartbeatIntervalMs: 60_000,
|
heartbeatIntervalMs: 60_000,
|
||||||
@@ -304,6 +313,7 @@ describe("PiSessionService prompt, queue, and auth warnings", () => {
|
|||||||
const fake = fakeRuntime("abort-session");
|
const fake = fakeRuntime("abort-session");
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||||
sessionManager: sessionGateway([sessionRecord("abort-session")]),
|
sessionManager: sessionGateway([sessionRecord("abort-session")]),
|
||||||
heartbeatIntervalMs: 60_000,
|
heartbeatIntervalMs: 60_000,
|
||||||
@@ -321,6 +331,7 @@ describe("PiSessionService prompt, queue, and auth warnings", () => {
|
|||||||
const fake = fakeRuntime("abort-compaction-session", { isCompacting: true });
|
const fake = fakeRuntime("abort-compaction-session", { isCompacting: true });
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||||
sessionManager: sessionGateway([sessionRecord("abort-compaction-session")]),
|
sessionManager: sessionGateway([sessionRecord("abort-compaction-session")]),
|
||||||
heartbeatIntervalMs: 60_000,
|
heartbeatIntervalMs: 60_000,
|
||||||
@@ -338,15 +349,20 @@ describe("PiSessionService prompt, queue, and auth warnings", () => {
|
|||||||
|
|
||||||
it("refreshes auth state and dedupes warnings when logout removes the current model's credentials", async () => {
|
it("refreshes auth state and dedupes warnings when logout removes the current model's credentials", async () => {
|
||||||
const hub = new CapturingSessionEventHub();
|
const hub = new CapturingSessionEventHub();
|
||||||
const authStorage = AuthStorage.inMemory({ anthropic: { type: "api_key", key: "sk-test" } });
|
// The shared model runtime reads a live credential store; auth changes are
|
||||||
const modelRegistry = ModelRegistry.inMemory(authStorage);
|
// simulated by mutating the store and refreshing the runtime (the same
|
||||||
const model = modelRegistry.find(TEST_MODEL_PROVIDER, TEST_MODEL_ID);
|
// sequence AuthService performs before emitting an AuthChange), then
|
||||||
|
// notifying the service via applyAuthChange.
|
||||||
|
const credentials = new InMemoryCredentialStore();
|
||||||
|
await seedCredential(credentials, "anthropic", { type: "api_key", key: "sk-test" });
|
||||||
|
const modelRuntime = await createTestModelRuntime(credentials);
|
||||||
|
const model = modelRuntime.getModel(TEST_MODEL_PROVIDER, TEST_MODEL_ID);
|
||||||
if (model === undefined) throw new Error("Expected Anthropic model fixture");
|
if (model === undefined) throw new Error("Expected Anthropic model fixture");
|
||||||
const fake = fakeRuntime("auth-session", { model, modelRegistry });
|
const fake = fakeRuntime("auth-session", { model, modelRuntime });
|
||||||
|
|
||||||
const service = new PiSessionService(hub, {
|
const service = new PiSessionService(hub, {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
modelRegistry,
|
modelRuntime,
|
||||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||||
sessionManager: sessionGateway([sessionRecord("auth-session")]),
|
sessionManager: sessionGateway([sessionRecord("auth-session")]),
|
||||||
heartbeatIntervalMs: 60_000,
|
heartbeatIntervalMs: 60_000,
|
||||||
@@ -356,7 +372,8 @@ describe("PiSessionService prompt, queue, and auth warnings", () => {
|
|||||||
hub.sessionEvents.length = 0;
|
hub.sessionEvents.length = 0;
|
||||||
hub.globalEvents.length = 0;
|
hub.globalEvents.length = 0;
|
||||||
|
|
||||||
authStorage.logout("anthropic");
|
await credentials.delete("anthropic");
|
||||||
|
await modelRuntime.refresh();
|
||||||
service.applyAuthChange({ removedProviderId: "anthropic" });
|
service.applyAuthChange({ removedProviderId: "anthropic" });
|
||||||
service.applyAuthChange({ removedProviderId: "anthropic" });
|
service.applyAuthChange({ removedProviderId: "anthropic" });
|
||||||
|
|
||||||
@@ -364,9 +381,11 @@ describe("PiSessionService prompt, queue, and auth warnings", () => {
|
|||||||
expect(warningCount()).toBe(1);
|
expect(warningCount()).toBe(1);
|
||||||
expect(hub.globalEvents.some((event) => event.type === "status.update" && event.status.sessionId === "auth-session")).toBe(true);
|
expect(hub.globalEvents.some((event) => event.type === "status.update" && event.status.sessionId === "auth-session")).toBe(true);
|
||||||
|
|
||||||
authStorage.set("anthropic", { type: "api_key", key: "sk-new" });
|
await seedCredential(credentials, "anthropic", { type: "api_key", key: "sk-new" });
|
||||||
|
await modelRuntime.refresh();
|
||||||
service.applyAuthChange();
|
service.applyAuthChange();
|
||||||
authStorage.logout("anthropic");
|
await credentials.delete("anthropic");
|
||||||
|
await modelRuntime.refresh();
|
||||||
service.applyAuthChange({ removedProviderId: "anthropic" });
|
service.applyAuthChange({ removedProviderId: "anthropic" });
|
||||||
expect(warningCount()).toBe(2);
|
expect(warningCount()).toBe(2);
|
||||||
|
|
||||||
@@ -377,6 +396,7 @@ describe("PiSessionService prompt, queue, and auth warnings", () => {
|
|||||||
const fake = fakeRuntime("stop-session");
|
const fake = fakeRuntime("stop-session");
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||||
sessionManager: sessionGateway([sessionRecord("stop-session")]),
|
sessionManager: sessionGateway([sessionRecord("stop-session")]),
|
||||||
heartbeatIntervalMs: 60_000,
|
heartbeatIntervalMs: 60_000,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { PiSessionService, type PiAgentSession } from "./piSessionService.js";
|
import { PiSessionService, type PiAgentSession } from "./piSessionService.js";
|
||||||
import type { SpawnTargetDecision } from "./spawnTargetResolver.js";
|
import type { SpawnTargetDecision } from "./spawnTargetResolver.js";
|
||||||
import { CapturingSessionEventHub, fakeRuntime, runtimeCreator, sessionGateway, testModel, type RuntimeCreator } from "./piSessionService.testSupport.js";
|
import { CapturingSessionEventHub, fakeRuntime, runtimeCreator, sessionGateway, testModel, testModelRuntime, type RuntimeCreator } from "./piSessionService.testSupport.js";
|
||||||
|
|
||||||
const TEST_AGENT_DIR = "/tmp/pi-web-test-agent";
|
const TEST_AGENT_DIR = "/tmp/pi-web-test-agent";
|
||||||
|
|
||||||
@@ -12,6 +12,7 @@ describe("PiSessionService", () => {
|
|||||||
const log: { details: Record<string, unknown>; message: string }[] = [];
|
const log: { details: Record<string, unknown>; message: string }[] = [];
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||||
sessionManager: sessionGateway([]),
|
sessionManager: sessionGateway([]),
|
||||||
spawnTargets: { resolveSpawnTarget: () => Promise.resolve(decision) },
|
spawnTargets: { resolveSpawnTarget: () => Promise.resolve(decision) },
|
||||||
@@ -45,6 +46,7 @@ describe("PiSessionService", () => {
|
|||||||
};
|
};
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime,
|
createAgentRuntime,
|
||||||
sessionManager: sessionGateway([]),
|
sessionManager: sessionGateway([]),
|
||||||
spawnTargets: { resolveSpawnTarget: () => Promise.resolve({ allowed: true, cwd: "/workspace-feature" }) },
|
spawnTargets: { resolveSpawnTarget: () => Promise.resolve({ allowed: true, cwd: "/workspace-feature" }) },
|
||||||
@@ -80,6 +82,7 @@ describe("PiSessionService", () => {
|
|||||||
const fake = fakeRuntime("spawned-x");
|
const fake = fakeRuntime("spawned-x");
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||||
sessionManager: sessionGateway([]),
|
sessionManager: sessionGateway([]),
|
||||||
heartbeatIntervalMs: 60_000,
|
heartbeatIntervalMs: 60_000,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { join } from "node:path";
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { PiSessionService, type PiAgentSession } from "./piSessionService.js";
|
import { PiSessionService, type PiAgentSession } from "./piSessionService.js";
|
||||||
import type { SpawnTargetDecision } from "./spawnTargetResolver.js";
|
import type { SpawnTargetDecision } from "./spawnTargetResolver.js";
|
||||||
import { CapturingSessionEventHub, emptyArchiveStore, fakeRuntime, fakeSessionManager, runtimeCreator, sessionGateway, sessionRecord, sessionRef, testModel, type RuntimeCreator } from "./piSessionService.testSupport.js";
|
import { CapturingSessionEventHub, emptyArchiveStore, fakeRuntime, fakeSessionManager, runtimeCreator, sessionGateway, sessionRecord, sessionRef, testModel, testModelRuntime, type RuntimeCreator } from "./piSessionService.testSupport.js";
|
||||||
|
|
||||||
const TEST_AGENT_DIR = "/tmp/pi-web-test-agent";
|
const TEST_AGENT_DIR = "/tmp/pi-web-test-agent";
|
||||||
|
|
||||||
@@ -40,6 +40,7 @@ describe("PiSessionService", () => {
|
|||||||
};
|
};
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime,
|
createAgentRuntime,
|
||||||
sessionManager: sessionGateway([]),
|
sessionManager: sessionGateway([]),
|
||||||
archiveStore,
|
archiveStore,
|
||||||
@@ -81,6 +82,7 @@ describe("PiSessionService", () => {
|
|||||||
};
|
};
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime,
|
createAgentRuntime,
|
||||||
sessionManager: sessionGateway([]),
|
sessionManager: sessionGateway([]),
|
||||||
archiveStore: emptyArchiveStore(),
|
archiveStore: emptyArchiveStore(),
|
||||||
@@ -121,6 +123,7 @@ describe("PiSessionService", () => {
|
|||||||
let index = 0;
|
let index = 0;
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: () => {
|
createAgentRuntime: () => {
|
||||||
const runtime = runtimes[index] ?? child.runtime;
|
const runtime = runtimes[index] ?? child.runtime;
|
||||||
index += 1;
|
index += 1;
|
||||||
@@ -173,6 +176,7 @@ describe("PiSessionService", () => {
|
|||||||
const open = vi.fn(() => childManager);
|
const open = vi.fn(() => childManager);
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: () => {
|
createAgentRuntime: () => {
|
||||||
const runtime = runtimes[index] ?? child.runtime;
|
const runtime = runtimes[index] ?? child.runtime;
|
||||||
index += 1;
|
index += 1;
|
||||||
@@ -215,6 +219,7 @@ describe("PiSessionService", () => {
|
|||||||
});
|
});
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: runtimeCreator(parent.runtime),
|
createAgentRuntime: runtimeCreator(parent.runtime),
|
||||||
sessionManager: { create: () => parent.session.sessionManager, list: () => Promise.resolve([]), listAll: () => Promise.resolve([]), open: () => fakeSessionManager() },
|
sessionManager: { create: () => parent.session.sessionManager, list: () => Promise.resolve([]), listAll: () => Promise.resolve([]), open: () => fakeSessionManager() },
|
||||||
archiveStore: emptyArchiveStore(),
|
archiveStore: emptyArchiveStore(),
|
||||||
@@ -240,6 +245,7 @@ describe("PiSessionService", () => {
|
|||||||
});
|
});
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: runtimeCreator(parent.runtime),
|
createAgentRuntime: runtimeCreator(parent.runtime),
|
||||||
sessionManager: { create: () => parent.session.sessionManager, list: () => Promise.resolve([]), listAll: () => Promise.resolve([]), open: () => fakeSessionManager() },
|
sessionManager: { create: () => parent.session.sessionManager, list: () => Promise.resolve([]), listAll: () => Promise.resolve([]), open: () => fakeSessionManager() },
|
||||||
archiveStore: emptyArchiveStore(),
|
archiveStore: emptyArchiveStore(),
|
||||||
@@ -262,6 +268,7 @@ describe("PiSessionService", () => {
|
|||||||
});
|
});
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: runtimeCreator(parent.runtime),
|
createAgentRuntime: runtimeCreator(parent.runtime),
|
||||||
sessionManager: { create: () => parent.session.sessionManager, list: () => Promise.resolve([]), listAll: () => Promise.resolve([]), open: () => fakeSessionManager() },
|
sessionManager: { create: () => parent.session.sessionManager, list: () => Promise.resolve([]), listAll: () => Promise.resolve([]), open: () => fakeSessionManager() },
|
||||||
archiveStore: emptyArchiveStore(),
|
archiveStore: emptyArchiveStore(),
|
||||||
@@ -283,6 +290,7 @@ describe("PiSessionService", () => {
|
|||||||
});
|
});
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: runtimeCreator(parent.runtime),
|
createAgentRuntime: runtimeCreator(parent.runtime),
|
||||||
sessionManager: { create: () => parent.session.sessionManager, list: () => Promise.resolve([]), listAll: () => Promise.resolve([childRecord]), open: () => fakeSessionManager() },
|
sessionManager: { create: () => parent.session.sessionManager, list: () => Promise.resolve([]), listAll: () => Promise.resolve([childRecord]), open: () => fakeSessionManager() },
|
||||||
archiveStore: emptyArchiveStore(),
|
archiveStore: emptyArchiveStore(),
|
||||||
@@ -304,6 +312,7 @@ describe("PiSessionService", () => {
|
|||||||
});
|
});
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: runtimeCreator(forkedParent.runtime),
|
createAgentRuntime: runtimeCreator(forkedParent.runtime),
|
||||||
sessionManager: { create: () => forkedParent.session.sessionManager, list: () => Promise.resolve([]), listAll: () => Promise.resolve([]), open: () => fakeSessionManager() },
|
sessionManager: { create: () => forkedParent.session.sessionManager, list: () => Promise.resolve([]), listAll: () => Promise.resolve([]), open: () => fakeSessionManager() },
|
||||||
archiveStore: emptyArchiveStore(),
|
archiveStore: emptyArchiveStore(),
|
||||||
@@ -343,6 +352,7 @@ describe("PiSessionService", () => {
|
|||||||
const open = vi.fn((path: string) => path === parentFile ? parentManager : childManager);
|
const open = vi.fn((path: string) => path === parentFile ? parentManager : childManager);
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: (_createRuntime, options) => {
|
createAgentRuntime: (_createRuntime, options) => {
|
||||||
delegationCapabilities.push(options.delegationToolsEnabled);
|
delegationCapabilities.push(options.delegationToolsEnabled);
|
||||||
const runtime = runtimes[index] ?? parent.runtime;
|
const runtime = runtimes[index] ?? parent.runtime;
|
||||||
@@ -406,6 +416,7 @@ describe("PiSessionService", () => {
|
|||||||
});
|
});
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: () => {
|
createAgentRuntime: () => {
|
||||||
const runtime = runtimes[index] ?? parent.runtime;
|
const runtime = runtimes[index] ?? parent.runtime;
|
||||||
index += 1;
|
index += 1;
|
||||||
@@ -464,6 +475,7 @@ describe("PiSessionService", () => {
|
|||||||
const open = vi.fn((path: string) => path === parentFile ? parentManager : childManager);
|
const open = vi.fn((path: string) => path === parentFile ? parentManager : childManager);
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: () => {
|
createAgentRuntime: () => {
|
||||||
const runtime = runtimes[index] ?? parent.runtime;
|
const runtime = runtimes[index] ?? parent.runtime;
|
||||||
index += 1;
|
index += 1;
|
||||||
@@ -529,6 +541,7 @@ describe("PiSessionService", () => {
|
|||||||
});
|
});
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime,
|
createAgentRuntime,
|
||||||
sessionManager: {
|
sessionManager: {
|
||||||
create: () => parentManager,
|
create: () => parentManager,
|
||||||
@@ -605,6 +618,7 @@ describe("PiSessionService", () => {
|
|||||||
});
|
});
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime,
|
createAgentRuntime,
|
||||||
sessionManager: {
|
sessionManager: {
|
||||||
create: () => copiedParentManager,
|
create: () => copiedParentManager,
|
||||||
@@ -663,6 +677,7 @@ describe("PiSessionService", () => {
|
|||||||
const open = vi.fn((path: string) => path === parentFile ? parentManager : childManager);
|
const open = vi.fn((path: string) => path === parentFile ? parentManager : childManager);
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: () => {
|
createAgentRuntime: () => {
|
||||||
const runtime = runtimes[index] ?? parent.runtime;
|
const runtime = runtimes[index] ?? parent.runtime;
|
||||||
index += 1;
|
index += 1;
|
||||||
@@ -716,6 +731,7 @@ describe("PiSessionService", () => {
|
|||||||
const open = vi.fn((path: string) => path === actualParentFile ? parent.session.sessionManager : childManager);
|
const open = vi.fn((path: string) => path === actualParentFile ? parent.session.sessionManager : childManager);
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: () => {
|
createAgentRuntime: () => {
|
||||||
const runtime = runtimes[index] ?? parent.runtime;
|
const runtime = runtimes[index] ?? parent.runtime;
|
||||||
index += 1;
|
index += 1;
|
||||||
@@ -757,6 +773,7 @@ describe("PiSessionService", () => {
|
|||||||
const open = vi.fn(() => childManager);
|
const open = vi.fn(() => childManager);
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: runtimeCreator(child.runtime),
|
createAgentRuntime: runtimeCreator(child.runtime),
|
||||||
sessionManager: {
|
sessionManager: {
|
||||||
create: () => childManager,
|
create: () => childManager,
|
||||||
@@ -929,6 +946,7 @@ describe("PiSessionService", () => {
|
|||||||
const fake = fakeRuntime("nope");
|
const fake = fakeRuntime("nope");
|
||||||
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
const service = new PiSessionService(new CapturingSessionEventHub(), {
|
||||||
agentDir: TEST_AGENT_DIR,
|
agentDir: TEST_AGENT_DIR,
|
||||||
|
modelRuntime: testModelRuntime,
|
||||||
createAgentRuntime: runtimeCreator(fake.runtime),
|
createAgentRuntime: runtimeCreator(fake.runtime),
|
||||||
sessionManager: sessionGateway([]),
|
sessionManager: sessionGateway([]),
|
||||||
heartbeatIntervalMs: 60_000,
|
heartbeatIntervalMs: 60_000,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { AuthStorage, ModelRegistry } from "@earendil-works/pi-coding-agent";
|
import { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
||||||
|
import { InMemoryCredentialStore, type Credential, type CredentialStore } from "@earendil-works/pi-ai";
|
||||||
import type { GlobalSessionEvent, SessionUiEvent } from "../../shared/apiTypes.js";
|
import type { GlobalSessionEvent, SessionUiEvent } from "../../shared/apiTypes.js";
|
||||||
import { SessionEventHub } from "../realtime/sessionEventHub.js";
|
import { SessionEventHub } from "../realtime/sessionEventHub.js";
|
||||||
import type { PiAgentSession, PiSessionManager, PiSessionRuntime, PiSessionServiceDependencies } from "./piSessionService.js";
|
import type { PiAgentSession, PiSessionManager, PiSessionRuntime, PiSessionServiceDependencies } from "./piSessionService.js";
|
||||||
@@ -62,8 +63,34 @@ export function sessionRef(id: string, cwd = "/workspace") {
|
|||||||
export const TEST_MODEL_PROVIDER = "anthropic";
|
export const TEST_MODEL_PROVIDER = "anthropic";
|
||||||
export const TEST_MODEL_ID = "claude-sonnet-4-5-20250929";
|
export const TEST_MODEL_ID = "claude-sonnet-4-5-20250929";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed a credential into an {@link InMemoryCredentialStore}. `modify` is the
|
||||||
|
* only write path on the pi-ai `CredentialStore` contract, so tests that need a
|
||||||
|
* pre-populated store go through it rather than mutating internals.
|
||||||
|
*/
|
||||||
|
export async function seedCredential(store: InMemoryCredentialStore, providerId: string, credential: Credential): Promise<void> {
|
||||||
|
await store.modify(providerId, () => Promise.resolve(credential));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a real {@link ModelRuntime} over an in-memory credential store — the
|
||||||
|
* async test seam that replaces the removed `ModelRegistry.create(AuthStorage
|
||||||
|
* .inMemory())`. Pass a pre-seeded store to exercise credential-dependent
|
||||||
|
* behavior (e.g. auth-loss warnings).
|
||||||
|
*/
|
||||||
|
export function createTestModelRuntime(credentials: CredentialStore = new InMemoryCredentialStore()): Promise<ModelRuntime> {
|
||||||
|
return ModelRuntime.create({ credentials });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared runtime for the common case where a test only needs model catalog
|
||||||
|
* reads and no configured auth. Built once so the many `fakeRuntime` sessions
|
||||||
|
* and `PiSessionService` constructions can inject it synchronously.
|
||||||
|
*/
|
||||||
|
export const testModelRuntime = await createTestModelRuntime();
|
||||||
|
|
||||||
export function testModel(): NonNullable<PiAgentSession["model"]> {
|
export function testModel(): NonNullable<PiAgentSession["model"]> {
|
||||||
const model = ModelRegistry.inMemory(AuthStorage.inMemory()).find(TEST_MODEL_PROVIDER, TEST_MODEL_ID);
|
const model = testModelRuntime.getModel(TEST_MODEL_PROVIDER, TEST_MODEL_ID);
|
||||||
if (model === undefined) throw new Error("test model not found");
|
if (model === undefined) throw new Error("test model not found");
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
@@ -88,7 +115,7 @@ export function fakeRuntime(sessionId = "session-1", patch: Partial<TestSession>
|
|||||||
pendingMessageCount: 0,
|
pendingMessageCount: 0,
|
||||||
sessionManager: fakeSessionManager(),
|
sessionManager: fakeSessionManager(),
|
||||||
settingsManager: { getWarnings: () => ({}), setWarnings: () => undefined },
|
settingsManager: { getWarnings: () => ({}), setWarnings: () => undefined },
|
||||||
modelRegistry: ModelRegistry.create(AuthStorage.inMemory()),
|
modelRuntime: testModelRuntime,
|
||||||
scopedModels: [],
|
scopedModels: [],
|
||||||
extensionRunner: { getRegisteredCommands: () => [] },
|
extensionRunner: { getRegisteredCommands: () => [] },
|
||||||
promptTemplates: [],
|
promptTemplates: [],
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||||
import { AuthStorage, ModelRegistry, type AgentSessionRuntimeDiagnostic, type ResourceDiagnostic } from "@earendil-works/pi-coding-agent";
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
|
import { type AgentSessionRuntimeDiagnostic, type ResourceDiagnostic } from "@earendil-works/pi-coding-agent";
|
||||||
import { anthropicSubscriptionWarning, collectRuntimeWarnings, dismissSessionWarning, type RuntimeWarningSources } from "./piSessionService.js";
|
import { anthropicSubscriptionWarning, collectRuntimeWarnings, dismissSessionWarning, type RuntimeWarningSources } from "./piSessionService.js";
|
||||||
|
import { testModel } from "./piSessionService.testSupport.js";
|
||||||
import type { PiAgentSession } from "./piSessionService.js";
|
import type { PiAgentSession } from "./piSessionService.js";
|
||||||
import type { SessionWarning } from "../../shared/apiTypes.js";
|
import type { SessionWarning } from "../../shared/apiTypes.js";
|
||||||
|
|
||||||
@@ -83,47 +87,55 @@ describe("collectRuntimeWarnings", () => {
|
|||||||
const ANTHROPIC_SUBSCRIPTION_AUTH_WARNING =
|
const ANTHROPIC_SUBSCRIPTION_AUTH_WARNING =
|
||||||
"Anthropic subscription auth is active. Third-party harness usage draws from extra usage and is billed per token, not your Claude plan limits. Manage extra usage at https://claude.ai/settings/usage.";
|
"Anthropic subscription auth is active. Third-party harness usage draws from extra usage and is billed per token, not your Claude plan limits. Manage extra usage at https://claude.ai/settings/usage.";
|
||||||
|
|
||||||
type SubscriptionSession = Pick<PiAgentSession, "model" | "modelRegistry" | "settingsManager">;
|
type SubscriptionSession = Pick<PiAgentSession, "model" | "settingsManager">;
|
||||||
|
|
||||||
function anthropicModel(provider: string): PiAgentSession["model"] {
|
function anthropicModel(provider: string): PiAgentSession["model"] {
|
||||||
const registry = ModelRegistry.inMemory(AuthStorage.inMemory());
|
// anthropicSubscriptionWarning only reads `model.provider`, so any built-in
|
||||||
const model = registry.getAll().find((candidate) => candidate.provider === provider) ?? registry.getAll()[0];
|
// model re-tagged with the desired provider is a sufficient fixture.
|
||||||
if (model === undefined) throw new Error("expected at least one built-in model");
|
return { ...testModel(), provider };
|
||||||
return { ...model, provider };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function subscriptionSession(options: {
|
function subscriptionSession(options: {
|
||||||
provider?: string;
|
provider?: string;
|
||||||
anthropicExtraUsage?: boolean;
|
anthropicExtraUsage?: boolean;
|
||||||
credential?: AuthStorage;
|
|
||||||
}): SubscriptionSession {
|
}): SubscriptionSession {
|
||||||
const authStorage = options.credential ?? AuthStorage.inMemory();
|
|
||||||
return {
|
return {
|
||||||
model: options.provider === undefined ? undefined : anthropicModel(options.provider),
|
model: options.provider === undefined ? undefined : anthropicModel(options.provider),
|
||||||
settingsManager: {
|
settingsManager: {
|
||||||
getWarnings: () => (options.anthropicExtraUsage === undefined ? {} : { anthropicExtraUsage: options.anthropicExtraUsage }),
|
getWarnings: () => (options.anthropicExtraUsage === undefined ? {} : { anthropicExtraUsage: options.anthropicExtraUsage }),
|
||||||
setWarnings: () => undefined,
|
setWarnings: () => undefined,
|
||||||
},
|
},
|
||||||
modelRegistry: ModelRegistry.create(authStorage),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function anthropicAuth(credential: { type: "oauth" } | { type: "api_key"; key: string }): AuthStorage {
|
const tempDirs: string[] = [];
|
||||||
const authStorage = AuthStorage.inMemory();
|
|
||||||
if (credential.type === "oauth") {
|
afterEach(async () => {
|
||||||
authStorage.set("anthropic", { type: "oauth", access: "a", refresh: "r", expires: Date.now() + 3_600_000 });
|
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
|
||||||
} else {
|
});
|
||||||
authStorage.set("anthropic", { type: "api_key", key: credential.key });
|
|
||||||
}
|
/**
|
||||||
return authStorage;
|
* Write an `auth.json` holding a single anthropic credential and return its
|
||||||
|
* path. `anthropicSubscriptionWarning` reads it via `readStoredCredential`, so
|
||||||
|
* the credential seam is the on-disk auth file rather than an in-memory store.
|
||||||
|
*/
|
||||||
|
async function anthropicAuthPath(credential: { type: "oauth" } | { type: "api_key"; key: string }): Promise<string> {
|
||||||
|
const dir = await mkdtemp(join(tmpdir(), "pi-web-warnings-"));
|
||||||
|
tempDirs.push(dir);
|
||||||
|
const authPath = join(dir, "auth.json");
|
||||||
|
const stored = credential.type === "oauth"
|
||||||
|
? { type: "oauth", access: "a", refresh: "r", expires: Date.now() + 3_600_000 }
|
||||||
|
: { type: "api_key", key: credential.key };
|
||||||
|
await writeFile(authPath, JSON.stringify({ anthropic: stored }));
|
||||||
|
return authPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("anthropicSubscriptionWarning", () => {
|
describe("anthropicSubscriptionWarning", () => {
|
||||||
it("warns with the verbatim SDK wording for a stored oauth credential", () => {
|
it("warns with the verbatim SDK wording for a stored oauth credential", async () => {
|
||||||
expect(anthropicSubscriptionWarning(subscriptionSession({
|
expect(anthropicSubscriptionWarning(
|
||||||
provider: "anthropic",
|
subscriptionSession({ provider: "anthropic" }),
|
||||||
credential: anthropicAuth({ type: "oauth" }),
|
await anthropicAuthPath({ type: "oauth" }),
|
||||||
}))).toEqual({
|
)).toEqual({
|
||||||
severity: "warning",
|
severity: "warning",
|
||||||
message: ANTHROPIC_SUBSCRIPTION_AUTH_WARNING,
|
message: ANTHROPIC_SUBSCRIPTION_AUTH_WARNING,
|
||||||
source: "anthropic",
|
source: "anthropic",
|
||||||
@@ -131,37 +143,41 @@ describe("anthropicSubscriptionWarning", () => {
|
|||||||
} satisfies SessionWarning);
|
} satisfies SessionWarning);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("warns for an sk-ant-oat subscription API key", () => {
|
it("warns for an sk-ant-oat subscription API key", async () => {
|
||||||
expect(anthropicSubscriptionWarning(subscriptionSession({
|
expect(anthropicSubscriptionWarning(
|
||||||
provider: "anthropic",
|
subscriptionSession({ provider: "anthropic" }),
|
||||||
credential: anthropicAuth({ type: "api_key", key: "sk-ant-oat-abc123" }),
|
await anthropicAuthPath({ type: "api_key", key: "sk-ant-oat-abc123" }),
|
||||||
}))?.message).toBe(ANTHROPIC_SUBSCRIPTION_AUTH_WARNING);
|
)?.message).toBe(ANTHROPIC_SUBSCRIPTION_AUTH_WARNING);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not warn for a standard anthropic API key", () => {
|
it("does not warn for a standard anthropic API key", async () => {
|
||||||
expect(anthropicSubscriptionWarning(subscriptionSession({
|
expect(anthropicSubscriptionWarning(
|
||||||
provider: "anthropic",
|
subscriptionSession({ provider: "anthropic" }),
|
||||||
credential: anthropicAuth({ type: "api_key", key: "sk-ant-api-abc123" }),
|
await anthropicAuthPath({ type: "api_key", key: "sk-ant-api-abc123" }),
|
||||||
}))).toBeUndefined();
|
)).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("respects the anthropicExtraUsage suppression gate", () => {
|
it("respects the anthropicExtraUsage suppression gate", async () => {
|
||||||
expect(anthropicSubscriptionWarning(subscriptionSession({
|
expect(anthropicSubscriptionWarning(
|
||||||
provider: "anthropic",
|
subscriptionSession({ provider: "anthropic", anthropicExtraUsage: false }),
|
||||||
anthropicExtraUsage: false,
|
await anthropicAuthPath({ type: "oauth" }),
|
||||||
credential: anthropicAuth({ type: "oauth" }),
|
)).toBeUndefined();
|
||||||
}))).toBeUndefined();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not warn when the active provider is not anthropic", () => {
|
it("does not warn when the active provider is not anthropic", async () => {
|
||||||
expect(anthropicSubscriptionWarning(subscriptionSession({
|
expect(anthropicSubscriptionWarning(
|
||||||
provider: "openai",
|
subscriptionSession({ provider: "openai" }),
|
||||||
credential: anthropicAuth({ type: "oauth" }),
|
await anthropicAuthPath({ type: "oauth" }),
|
||||||
}))).toBeUndefined();
|
)).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not warn when no anthropic credential is stored", () => {
|
it("does not warn when no anthropic credential is stored", async () => {
|
||||||
expect(anthropicSubscriptionWarning(subscriptionSession({ provider: "anthropic" }))).toBeUndefined();
|
const dir = await mkdtemp(join(tmpdir(), "pi-web-warnings-"));
|
||||||
|
tempDirs.push(dir);
|
||||||
|
expect(anthropicSubscriptionWarning(
|
||||||
|
subscriptionSession({ provider: "anthropic" }),
|
||||||
|
join(dir, "auth.json"),
|
||||||
|
)).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|||||||
import type { MessagePage, SessionBulkArchiveResponse, SessionBulkDeleteArchivedResponse, SessionBulkMutationRef, SessionCleanupExecuteResponse, SessionCleanupPreviewResponse, SessionStatus, SessionStreamSnapshot } from "../../shared/apiTypes.js";
|
import type { MessagePage, SessionBulkArchiveResponse, SessionBulkDeleteArchivedResponse, SessionBulkMutationRef, SessionCleanupExecuteResponse, SessionCleanupPreviewResponse, SessionStatus, SessionStreamSnapshot } from "../../shared/apiTypes.js";
|
||||||
import { SessionEventHub } from "../realtime/sessionEventHub.js";
|
import { SessionEventHub } from "../realtime/sessionEventHub.js";
|
||||||
import { PiSessionService, type PiSessionManagerGateway } from "./piSessionService.js";
|
import { PiSessionService, type PiSessionManagerGateway } from "./piSessionService.js";
|
||||||
|
import { testModelRuntime } from "./piSessionService.testSupport.js";
|
||||||
import type { SessionRouteLookup, SessionRouteService } from "./sessionService.js";
|
import type { SessionRouteLookup, SessionRouteService } from "./sessionService.js";
|
||||||
import { registerSessionRoutes } from "./sessionRoutes.js";
|
import { registerSessionRoutes } from "./sessionRoutes.js";
|
||||||
import type { NormalizedSessionCleanupRequest } from "./sessionCleanup.js";
|
import type { NormalizedSessionCleanupRequest } from "./sessionCleanup.js";
|
||||||
@@ -20,7 +21,7 @@ beforeEach(async () => {
|
|||||||
await app.register(fastifyWebsocket);
|
await app.register(fastifyWebsocket);
|
||||||
sessionManager = new RejectingSessionManager();
|
sessionManager = new RejectingSessionManager();
|
||||||
const eventHub = new SessionEventHub();
|
const eventHub = new SessionEventHub();
|
||||||
service = new PiSessionService(eventHub, { agentDir: TEST_AGENT_DIR, sessionManager, heartbeatIntervalMs: 60_000 });
|
service = new PiSessionService(eventHub, { agentDir: TEST_AGENT_DIR, modelRuntime: testModelRuntime, sessionManager, heartbeatIntervalMs: 60_000 });
|
||||||
registerSessionRoutes(app, service, eventHub);
|
registerSessionRoutes(app, service, eventHub);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user