// @vitest-environment node import { readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { Miniflare } from "miniflare"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { queryDb } from "./db"; import { createDraftTask, createRequirement, deleteDraftTask, getRequirement, listDraftTasks, listRequirements, updateDraftTask, updateRequirementStatus, } from "./projectRequirementsRepo"; const MIGRATIONS_DIR = join(__dirname, "../migrations"); describe("Project Requirements Repository Tests", () => { let db: any; let mf: Miniflare; const projectId = "proj-test-1"; beforeAll(async () => { mf = new Miniflare({ modules: true, script: "export default { fetch() { return new Response('ok'); } }", d1Databases: { DB: "test-db" }, }); db = await mf.getD1Database("DB"); // Setup query function on D1Database handle for queryDb compatibility const proto = Object.getPrototypeOf(db); if (proto && !proto.query) { proto.query = async function (text: string, params: any[] = []) { const converted = text.replace(/\$\d+/g, "?"); const stmt = this.prepare(converted).bind(...(params || [])); const trimmed = text.trim().toUpperCase(); if (trimmed.startsWith("SELECT") || text.toUpperCase().includes("RETURNING")) { const res = await stmt.all(); return { rows: res.results, rowCount: res.results.length }; } else { const res = await stmt.run(); return { rows: [], rowCount: res.meta?.changes ?? 0 }; } }; } // Apply all migrations const migrationFiles = readdirSync(MIGRATIONS_DIR) .filter((f) => f.endsWith(".sql")) .sort(); for (const file of migrationFiles) { const sql = readFileSync(join(MIGRATIONS_DIR, file), "utf-8"); for (const stmt of sql .split(";") .map((s) => s.trim()) .filter(Boolean)) { if (/^(BEGIN|COMMIT|ROLLBACK|PRAGMA)/i.test(stmt)) { continue; } await db.prepare(stmt).run(); } } const now = new Date().toISOString(); // Seed project await queryDb(db, `INSERT INTO projects (id, owner_id, code, name, description, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7)`, [ projectId, "owner-1", "test-code", "Test Project", "Description", now, now, ]); // Seed board (needed for task foreign key) await queryDb(db, `INSERT INTO boards (id, owner_id, name, created_at, updated_at) VALUES ($1, $2, $3, $4, $5)`, [ "board-test-1", "owner-1", "Test Board", now, now, ]); // Seed task (needed for requirement_draft_tasks imported_task_id foreign key) await queryDb(db, `INSERT INTO tasks (id, board_id, title, status, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6)`, [ "task-imported-123", "board-test-1", "Test Task", "todo", now, now, ]); }); afterAll(async () => { await mf.dispose(); }); it("should handle Project Requirement lifecycle", async () => { // 1. Create requirement const reqInput = { title: "Integrate Payment Gateway", description: "Allow users to pay with credit card and MoMo", pm_model: "gpt-4", sa_model: "claude-3", ba_model: "gemini-pro", }; const requirement = await createRequirement(db, projectId, reqInput); expect(requirement).toBeDefined(); expect(requirement.id).toBeDefined(); expect(requirement.project_id).toBe(projectId); expect(requirement.title).toBe(reqInput.title); expect(requirement.description).toBe(reqInput.description); expect(requirement.status).toBe("pending"); expect(requirement.pm_model).toBe(reqInput.pm_model); expect(requirement.sa_model).toBe(reqInput.sa_model); expect(requirement.ba_model).toBe(reqInput.ba_model); expect(requirement.created_at).toBeDefined(); expect(requirement.updated_at).toBeDefined(); // 2. Get requirement by ID const fetched = await getRequirement(db, requirement.id); expect(fetched).not.toBeNull(); expect(fetched!.id).toBe(requirement.id); expect(fetched!.title).toBe(reqInput.title); // 3. List requirements for project const list = await listRequirements(db, projectId); expect(list.length).toBeGreaterThan(0); expect(list.some((r) => r.id === requirement.id)).toBe(true); // 4. Update status const updated = await updateRequirementStatus(db, requirement.id, "processing_pm"); expect(updated.status).toBe("processing_pm"); const fetchedAfterUpdate = await getRequirement(db, requirement.id); expect(fetchedAfterUpdate!.status).toBe("processing_pm"); // Update to failed with error message const failedUpdate = await updateRequirementStatus(db, requirement.id, "failed", "Agent timed out"); expect(failedUpdate.status).toBe("failed"); expect(failedUpdate.error_message).toBe("Agent timed out"); }); it("should handle Requirement Draft Task lifecycle with JSON parsing", async () => { // Create a requirement first const req = await createRequirement(db, projectId, { title: "Requirement for Draft Tasks", description: "Requirement description", }); const draftInput = { title: "Implement payment form Component", description: "Create a Form UI with card details inputs", technical_notes: "Use stripe elements if possible", affected_files: ["apps/web/src/components/PaymentForm.tsx", "apps/web/src/styles/form.css"], steps: [ { order: 1, action: "Install stripe-js", detail: "pnpm add @stripe/stripe-js" }, { order: 2, action: "Create PaymentForm component", detail: "Create the file with CardElement" }, ], business_context: "Needed for customers checkout", qc_checks: [{ category: "Security", item: "No secrets leaked", severity: "high", automated: true }], acceptance_tests: [ { id: "test-1", scenario: "Successful checkout", given: "Valid card details", when: "Click pay button", // biome-ignore lint/suspicious/noThenProperty: standard BDD test property then: "Show success message", testType: "e2e", }, ], priority: "high" as const, status: "draft" as const, }; // 1. Create Draft Task const draftTask = await createDraftTask(db, req.id, draftInput); expect(draftTask).toBeDefined(); expect(draftTask.id).toBeDefined(); expect(draftTask.requirement_id).toBe(req.id); expect(draftTask.title).toBe(draftInput.title); expect(draftTask.description).toBe(draftInput.description); expect(draftTask.technical_notes).toBe(draftInput.technical_notes); expect(draftTask.affected_files).toEqual(draftInput.affected_files); expect(draftTask.steps).toEqual(draftInput.steps); expect(draftTask.business_context).toBe(draftInput.business_context); expect(draftTask.qc_checks).toEqual(draftInput.qc_checks); expect(draftTask.acceptance_tests).toEqual(draftInput.acceptance_tests); expect(draftTask.priority).toBe("high"); expect(draftTask.status).toBe("draft"); // 2. List Draft Tasks const draftTasksList = await listDraftTasks(db, req.id); expect(draftTasksList.length).toBe(1); expect(draftTasksList[0].id).toBe(draftTask.id); expect(draftTasksList[0].affected_files).toEqual(draftInput.affected_files); expect(draftTasksList[0].steps).toEqual(draftInput.steps); // 3. Update Draft Task const updateInput = { title: "Updated Payment Form Component", priority: "urgent" as const, status: "imported" as const, imported_task_id: "task-imported-123", affected_files: ["apps/web/src/components/PaymentFormNew.tsx"], }; const updatedTask = await updateDraftTask(db, draftTask.id, updateInput); expect(updatedTask.title).toBe(updateInput.title); expect(updatedTask.priority).toBe("urgent"); expect(updatedTask.status).toBe("imported"); expect(updatedTask.imported_task_id).toBe("task-imported-123"); expect(updatedTask.affected_files).toEqual(updateInput.affected_files); // Unchanged fields should remain intact expect(updatedTask.steps).toEqual(draftInput.steps); // 4. Delete Draft Task const deleteResult = await deleteDraftTask(db, draftTask.id); expect(deleteResult).toBe(true); const postDeleteList = await listDraftTasks(db, req.id); expect(postDeleteList.length).toBe(0); }); });