import { env } from "@code/env/convex"; import { type TestConvex, convexTest } from "convex-test"; import { anyApi } from "convex/server"; import { describe, expect, test } from "vitest"; import { internal } from "./_generated/api"; import type { Id } from "./_generated/dataModel"; import schema from "./schema"; // `import.meta.glob` is provided by the Vitest/Vite test runner at runtime; it // has no type definition under the Convex tsconfig (which scopes `types` to // node), so declare the minimal shape the test relies on. declare global { interface ImportMeta { readonly glob: (pattern: string) => Record Promise>; } } const modules = import.meta.glob("./**/*.ts"); const api = anyApi; // `agentWorkspace` gates agent-controlled mutations on `env.FLUE_DB_TOKEN`. // Vitest loads the repo `.env`, so resolve the real value from the env module // rather than hardcoding a constant that would mismatch the loaded secret. const FLUE_DB_TOKEN = env.FLUE_DB_TOKEN; const ID_A = "https://convex.test|user-a"; const identityA = { tokenIdentifier: ID_A }; const repository = { defaultBranch: "main", id: 12345, name: "zopu", owner: "puter", url: "https://github.com/puter/zopu", }; // Read every projectEvents row for a project via the same indexes the control // plane writes through, returning them oldest-first. Reads MUST go through the // same test instance that performed the writes; a fresh `convexTest()` is an // isolated in-memory backend with no shared state. const eventsForProject = async ( t: TestConvex, projectId: Id<"projects"> ) => { return await t.query((ctx) => ctx.db .query("projectEvents") .withIndex("by_project_and_createdAt", (q) => q.eq("projectId", projectId) ) .collect() ); }; describe("projectEvents", () => { test("project connect emits a project.connected event", async () => { const t = convexTest({ schema, modules }); const projectId = await t.mutation(internal.projects.storeGitHubProject, { ownerId: ID_A, repository, }); const events = await eventsForProject(t, projectId); expect(events).toHaveLength(1); expect(events[0].kind).toBe("project.connected"); expect(events[0].projectId).toBe(projectId); expect(events[0].issueId).toBeUndefined(); expect(events[0].runId).toBeUndefined(); expect(events[0].data).toMatchObject({ provider: "github", repository: repository.url, }); }); test("issue create and begin emit issue.created then issue.queued events", async () => { const t = convexTest({ schema, modules }); const projectId = await t.mutation(internal.projects.storeGitHubProject, { ownerId: ID_A, repository, }); const issueId = await t .withIdentity(identityA) .mutation(api.projectIssues.create, { body: "A representative issue body long enough to pass validation", projectId, title: "Representative issue", }); const afterCreate = await eventsForProject(t, projectId); expect(afterCreate.map((e) => e.kind)).toEqual([ "project.connected", "issue.created", ]); const created = afterCreate[1]; expect(created.issueId).toBe(issueId); expect(created.data).toMatchObject({ number: 1, title: "Representative issue", }); const status = await t .withIdentity(identityA) .mutation(api.projectIssues.begin, { issueId, }); expect(status).toBe("queued"); const afterBegin = await eventsForProject(t, projectId); expect(afterBegin.map((e) => e.kind)).toEqual([ "project.connected", "issue.created", "issue.queued", ]); expect(afterBegin[2].issueId).toBe(issueId); expect(afterBegin[2].data).toMatchObject({ number: 1 }); }); test("artifact update emits an artifact.updated event", async () => { const t = convexTest({ schema, modules }); const projectId = await t.mutation(internal.projects.storeGitHubProject, { ownerId: ID_A, repository, }); const issueId = await t .withIdentity(identityA) .mutation(api.projectIssues.create, { body: "A representative issue body long enough to pass validation", projectId, title: "Representative issue", }); const revision = await t.mutation(api.agentWorkspace.updateArtifact, { content: "# Updated work\n\nRevised content.", issueId, path: "work.md", token: FLUE_DB_TOKEN, }); expect(revision).toBe(3); const events = await eventsForProject(t, projectId); const artifactEvent = events.find((e) => e.kind === "artifact.updated"); expect(artifactEvent).toBeDefined(); expect(artifactEvent?.issueId).toBe(issueId); expect(artifactEvent?.data).toMatchObject({ path: "work.md", revision: 3 }); }); test("agent status emits an agent. event linked to the run", async () => { const t = convexTest({ schema, modules }); const projectId = await t.mutation(internal.projects.storeGitHubProject, { ownerId: ID_A, repository, }); const issueId = await t .withIdentity(identityA) .mutation(api.projectIssues.create, { body: "A representative issue body long enough to pass validation", projectId, title: "Representative issue", }); await t .withIdentity(identityA) .mutation(api.projectIssues.begin, { issueId }); await t.mutation(api.agentWorkspace.ensureRun, { daemonId: "daemon-1", issueId, token: FLUE_DB_TOKEN, }); await t.mutation(api.agentWorkspace.setStatus, { issueId, status: "completed", summary: "Done.", token: FLUE_DB_TOKEN, }); const events = await eventsForProject(t, projectId); const workspaceEvent = events.find( (e) => e.kind === "agent.workspace.created" ); expect(workspaceEvent).toBeDefined(); expect(workspaceEvent?.runId).toBeDefined(); const statusEvent = events.find((e) => e.kind === "agent.completed"); expect(statusEvent).toBeDefined(); expect(statusEvent?.issueId).toBe(issueId); expect(statusEvent?.runId).toBe(workspaceEvent?.runId); expect(statusEvent?.data).toMatchObject({ summary: "Done." }); }); test("wrong agent token is rejected before any event is written", async () => { const t = convexTest({ schema, modules }); const projectId = await t.mutation(internal.projects.storeGitHubProject, { ownerId: ID_A, repository, }); const issueId = await t .withIdentity(identityA) .mutation(api.projectIssues.create, { body: "A representative issue body long enough to pass validation", projectId, title: "Representative issue", }); await expect( t.mutation(api.agentWorkspace.setStatus, { issueId, status: "completed", summary: "Done.", token: "wrong-token", }) ).rejects.toThrow(/Invalid agent control token/u); const events = await eventsForProject(t, projectId); expect(events.some((e) => e.kind === "agent.completed")).toBe(false); }); });