/** * Test helpers for integration tests. * * Each test gets a clean DB (tables truncated before the test), a mock * FeishuRuntime (sendText/sendCard are no-ops that record calls), and a mock * AgentProvider (chat() returns a fixed "stop" response — no real network). */ import { PrismaClient } from "@prisma/client"; import type { FastifyBaseLogger } from "fastify"; import type { FeishuRuntime } from "../src/feishu/client.js"; import type { AgentProvider, ChatRequest, ChatResponse, Message } from "../src/agent/provider.js"; export const TEST_DATABASE_URL = "postgresql://paradigm:paradigm@127.0.0.1:5432/cph_hub_test"; export const prisma = new PrismaClient({ datasources: { db: { url: TEST_DATABASE_URL } }, }); /** Truncate all tables before each test for isolation. */ export async function resetDb(): Promise { const tables = [ "AuditEntry", "PermissionSettings", "PermissionGrant", "ProjectAgentLock", "AgentRun", "AgentSession", "ProjectGroupBinding", "PlatformRoleAssignment", "User", "Project", ]; // Truncate with CASCADE to wipe dependent rows in one shot. await prisma.$executeRawUnsafe(`TRUNCATE TABLE ${tables.map((t) => `"${t}"`).join(", ")} RESTART IDENTITY CASCADE`); } /** A logger that discards everything (tests don't need fastify's pino). */ export const silentLogger: FastifyBaseLogger = { info() {}, warn() {}, error() {}, debug() {}, fatal() {}, child() { return this; }, level: "silent", } as unknown as FastifyBaseLogger; /** Records sendText/sendCard calls so tests can assert on them. */ export interface MockFeishuRuntime extends FeishuRuntime { readonly sentTexts: string[]; readonly sentCards: unknown[]; } export function mockFeishuRuntime(): MockFeishuRuntime { const sentTexts: string[] = []; const sentCards: unknown[] = []; // Mock the client — sendText/sendCard are in client.ts, not on the runtime // object directly. We patch by providing a runtime whose client is a stub; // the actual send functions cast through shape, so a minimal stub works. const rt: MockFeishuRuntime = { client: { im: { v1: { message: { create: async (p: unknown) => { const payload = p as { data?: { msg_type?: string; content?: string } }; if (payload.data?.msg_type === "interactive") { sentCards.push(payload.data.content ? JSON.parse(payload.data.content) : null); } else { try { const c = JSON.parse(payload.data?.content ?? "{}") as { text?: string }; sentTexts.push(c.text ?? payload.data?.content ?? ""); } catch { sentTexts.push(payload.data?.content ?? ""); } } return { data: { message_id: "mock-msg-id" } }; }, }, }, }, } as unknown as FeishuRuntime["client"], logger: silentLogger, sentTexts, sentCards, }; return rt; } /** A provider that always returns a fixed "stop" response — no network. */ export class MockProvider implements AgentProvider { readonly id = "mock"; readonly calls: ChatRequest[] = []; async chat(req: ChatRequest): Promise { this.calls.push(req); const message: Message = { role: "assistant", parts: [{ type: "text", text: "mock response" }], }; return { finishReason: "stop", message, usage: { inputTokens: 10, outputTokens: 5 } }; } } /** Create a project + binding + user + grant for a test. */ export async function seedProject( projectId: string, chatId: string, options: { principal?: string; role?: "READ" | "EDIT" | "MANAGE" } = {}, ): Promise { const principal = options.principal ?? "ou_test_user"; const role = options.role ?? "EDIT"; await prisma.project.create({ data: { id: projectId, name: `Test ${projectId}`, workspaceDir: `/tmp/test-${projectId}`, }, }); await prisma.user.create({ data: { id: "u_" + projectId, feishuOpenId: principal, displayName: "Test User", platformRoles: { create: { role: "TEACHER" } }, permissionGrants: { create: { resourceType: "PROJECT", resourceId: projectId, principal, role, }, }, }, }); await prisma.projectGroupBinding.create({ data: { projectId, chatId, createdByUserId: "u_" + projectId }, }); }