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"; declare global { interface ImportMeta { readonly glob: (pattern: string) => Record Promise>; } } const modules = import.meta.glob("./**/*.ts"); const api = anyApi; const FLUE_DB_TOKEN = env.FLUE_DB_TOKEN; const ID_A = "https://convex.test|user-a"; const identityA = { tokenIdentifier: ID_A }; const SOURCE = { host: "github.com", projectName: "zopu", repositoryPath: "puter/zopu", normalizedUrl: "https://github.com/puter/zopu", url: "https://github.com/puter/zopu", }; const REMOTE = { defaultBranch: "main", documents: [ { kind: "readme" as const, path: "README.md", content: "# zopu\n\nRepository: https://github.com/puter/zopu\n", }, ], warnings: [], }; const createTestProject = async ( t: TestConvex ): Promise> => { // Ensure personal organization for the user await t .withIdentity(identityA) .mutation(api.organizations.ensurePersonalOrganization); // Persist the public Git import const outcome = await t .withIdentity(identityA) .mutation(internal.projects.persistPublicGitImport, { userId: ID_A, source: SOURCE, remote: REMOTE, }); return outcome.id as unknown as Id<"projects">; }; 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 createTestProject(t); 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(); }); test("issue create and begin emit issue.created then issue.queued events", async () => { const t = convexTest({ schema, modules }); const projectId = await createTestProject(t); 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 createTestProject(t); 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 createTestProject(t); 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 }); const run = await t.mutation(api.agentWorkspace.ensureRun, { daemonId: "daemon-1", issueId, token: FLUE_DB_TOKEN, }); expect(run).toMatchObject({ baseBranch: "main", checkoutPath: "/workspace/repository", sourceUrl: SOURCE.url, }); expect(run?.branchName).toMatch(/^work\/issue-1-/u); 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 createTestProject(t); 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); }); test("Gitea lifecycle persists PR metadata in an event and artifact", async () => { const t = convexTest({ schema, modules }); const projectId = await createTestProject(t); 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.mutation(api.agentWorkspace.ensureRun, { daemonId: "daemon-1", issueId, token: FLUE_DB_TOKEN, }); await t.mutation(api.agentWorkspace.recordGiteaLifecycle, { baseBranch: "main", branch: "work/issue-1", commitSha: "abc123", issueId, pullRequest: { baseBranch: "main", branch: "work/issue-1", number: 5, status: "open", url: "https://git.openputer.com/puter/zopu-code/pulls/5", }, status: "pull_request_open", token: FLUE_DB_TOKEN, }); const events = await eventsForProject(t, projectId); const event = events.find( (item) => item.kind === "gitea.pull_request.created" ); expect(event?.data).toMatchObject({ branch: "work/issue-1", commitSha: "abc123", pullRequest: { number: 5, status: "open", }, status: "pull_request_open", }); const artifacts = await t.query((ctx) => ctx.db .query("projectArtifacts") .withIndex("by_project_and_path", (q) => q.eq("projectId", projectId).eq("path", "artifacts.md") ) .unique() ); expect(artifacts?.content).toContain( "https://git.openputer.com/puter/zopu-code/pulls/5" ); }); });