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 ID_A = "https://convex.test|project-request-a"; const ID_B = "https://convex.test|project-request-b"; const identityA = { tokenIdentifier: ID_A }; const identityB = { tokenIdentifier: ID_B }; const createProject = async ( t: TestConvex ): Promise> => { await t .withIdentity(identityA) .mutation(api.organizations.ensurePersonalOrganization, {}); const outcome = await t .withIdentity(identityA) .mutation(internal.projects.persistPublicGitImport, { userId: ID_A, source: { host: "github.com", normalizedUrl: "https://github.com/test/project-request", projectName: "project-request", repositoryPath: "test/project-request", url: "https://github.com/test/project-request", }, remote: { defaultBranch: "main", documents: [ { content: "# Project Request\n", kind: "readme" as const, path: "README.md", }, ], warnings: [], }, }); return outcome.id as unknown as Id<"projects">; }; describe("project issue request smoke", () => { test("project Signal becomes a queued project issue with durable evidence", async () => { const t = convexTest({ schema, modules }); const projectId = await createProject(t); const organization = await t .withIdentity(identityA) .mutation(api.organizations.ensurePersonalOrganization, {}); const begun = await t .withIdentity(identityA) .mutation(api.conversationMessages.beginUserMessage, { clientRequestId: "project-request-1", organizationId: organization._id, rawText: "Staging deploys fail during DNS resolution.", }); await t .withIdentity(identityA) .mutation(api.conversationMessages.markAdmitted, { clientRequestId: "project-request-1", organizationId: organization._id, submissionId: "submission-project-request-1", }); const signal = await t .withIdentity(identityA) .mutation(api.signals.createFromMessages, { conversationId: organization._id, messageIds: [begun.messageId], organizationId: organization._id, problemStatement: { constraints: ["Do not change production"], desiredOutcome: "Staging deploys successfully", summary: "The staging deploy fails during DNS resolution.", title: "Fix the staging deploy DNS failure", }, processedBy: { agentInstanceId: String(organization._id), agentName: "zopu", }, projectId, }); const created = await t .withIdentity(identityA) .mutation(api.projectIssues.createFromSignal, { signalId: signal.signalId, }); const issue = await t.run(async (ctx) => ctx.db.get(created.issueId)); expect(issue).toMatchObject({ body: expect.stringContaining("Source Signal"), projectId, status: "open", title: "Fix the staging deploy DNS failure", }); const status = await t .withIdentity(identityA) .mutation(api.projectIssues.begin, { issueId: created.issueId }); expect(status).toBe("queued"); const events = await t.run(async (ctx) => ctx.db .query("projectEvents") .withIndex("by_project_and_createdAt", (q) => q.eq("projectId", projectId) ) .collect() ); expect(events.map((event) => event.kind)).toEqual([ "project.connected", "issue.created", "issue.queued", ]); }); test("a different organization cannot consume the project Signal", async () => { const t = convexTest({ schema, modules }); const projectId = await createProject(t); await t .withIdentity(identityB) .mutation(api.organizations.ensurePersonalOrganization, {}); const organization = await t .withIdentity(identityA) .mutation(api.organizations.ensurePersonalOrganization, {}); const begun = await t .withIdentity(identityA) .mutation(api.conversationMessages.beginUserMessage, { clientRequestId: "project-request-cross-org", organizationId: organization._id, rawText: "Private project request", }); await t .withIdentity(identityA) .mutation(api.conversationMessages.markAdmitted, { clientRequestId: "project-request-cross-org", organizationId: organization._id, submissionId: "submission-project-request-cross-org", }); const signal = await t .withIdentity(identityA) .mutation(api.signals.createFromMessages, { conversationId: organization._id, messageIds: [begun.messageId], organizationId: organization._id, problemStatement: { constraints: [], desiredOutcome: "Keep the request private", summary: "This request belongs to the first organization.", title: "Private project request", }, processedBy: { agentInstanceId: String(organization._id), agentName: "zopu", }, projectId, }); await expect( t.withIdentity(identityB).mutation(api.projectIssues.createFromSignal, { signalId: signal.signalId, }) ).rejects.toThrow(/membership required/u); }); });