import { HarnessError, InMemoryEventStore } from "@agent-studio/harness"; import type { FastifyInstance } from "fastify"; import { vi } from "vitest"; import { buildApp } from "../src/app.js"; export function createTestApp(): { app: FastifyInstance; session: { startRun: ReturnType; cancelRun: ReturnType }; modelStore: { configure: ReturnType; getRedacted: ReturnType; clearCredentials: ReturnType; resolveConnection: ReturnType; }; attachmentStore: { add: ReturnType; remove: ReturnType }; approvalBroker: { approve: ReturnType; deny: ReturnType }; eventStore: InMemoryEventStore; } { const session = { sessionId: "ses_1", workspaceRoot: "/safe/session", startRun: vi.fn(() => Promise.resolve({ runId: "run_1" })), cancelRun: vi.fn(() => Promise.resolve(true)), dispose: vi.fn(() => Promise.resolve()), }; const registry = { create: vi.fn(() => Promise.resolve(session)), get: vi.fn((sessionId: string) => sessionId === "ses_1" ? session : undefined), delete: vi.fn((sessionId: string) => Promise.resolve(sessionId === "ses_1")), disposeAll: vi.fn(() => Promise.resolve()), }; const modelStore = { configure: vi.fn(() => Promise.resolve()), getRedacted: vi.fn(() => ({ providerId: "training", api: "openai-responses" as const, baseUrl: "https://api.example.com/v1", modelId: "training-model", supportsImages: true, configured: true, source: "session" as const, keyHint: "••••cret", })), clearCredentials: vi.fn(), resolveConnection: vi.fn(() => ({ connection: { providerId: "training", api: "openai-responses" as const, modelId: "training-model", supportsImages: true }, apiKey: "server-only-secret", source: "session" as const, })), }; const attachmentStore = { add: vi.fn(async (_sessionId: string, upload: { stream: AsyncIterable }) => { for await (const _chunk of upload.stream) { // Consume the multipart stream like the real attachment store. void _chunk; } return { attachmentId: "att_1", sessionId: "ses_1", name: "chart.png", mediaType: "image/png" as const, byteSize: 12, width: 4, height: 3, status: "ready" as const, }; }), remove: vi.fn(() => Promise.resolve(true)), }; const approvalBroker = { approve: vi.fn((approvalId: string) => ({ approvalId, status: "approved" as const })), deny: vi.fn((approvalId: string) => ({ approvalId, status: "denied" as const })), }; const eventStore = new InMemoryEventStore(); const app = buildApp({ sessionRegistry: registry, skillCatalog: { list: () => [{ name: "ads-analysis", description: "Analyze", source: "project", filePath: ".agents/skills/ads-analysis/SKILL.md", diagnostics: [] }] }, modelStore, attachmentStore, approvalBroker, eventStore, testModelConnection: () => Promise.resolve({ status: "deferred", message: "Saved; validation will occur on first run." }), }); return { app, session, modelStore, attachmentStore, approvalBroker, eventStore }; } export function makeSessionBusy(session: { startRun: ReturnType }): void { session.startRun.mockRejectedValueOnce(new HarnessError("SESSION_BUSY", "Session is busy")); }