import { convexTest } from "convex-test"; import { anyApi } from "convex/server"; import { describe, expect, test } from "vitest"; import schema from "./schema"; declare global { interface ImportMeta { readonly glob: (pattern: string) => Record Promise>; } } const modules = import.meta.glob("./**/*.ts"); const api = anyApi; const identity = { tokenIdentifier: "https://convex.test|planner-user" }; const seedProject = async (t: ReturnType) => await t.withIdentity(identity).run(async (ctx) => { const organizationId = await ctx.db.insert("organizations", { createdAt: 1, createdBy: identity.tokenIdentifier, kind: "personal", name: "Personal", }); await ctx.db.insert("organizationMembers", { createdAt: 1, organizationId, role: "owner", userId: identity.tokenIdentifier, }); const projectId = await ctx.db.insert("projects", { createdAt: 2, defaultBranch: "main", name: "Test Project", normalizedSourceUrl: "https://git.example.com/test/repo", organizationId, repositoryPath: "test/repo", sourceHost: "gitea", sourceUrl: "https://git.example.com/test/repo.git", updatedAt: 2, }); return { organizationId, projectId }; }); const seedReadyWork = async ( t: ReturnType, organizationId: string, projectId: string, workIdOverride?: string ) => await t.withIdentity(identity).run(async (ctx) => { const workId = workIdOverride ?? (await ctx.db.insert("works", { createdAt: 3, definitionVersion: 1, designVersion: 1, objective: "Build it", organizationId, projectId, status: "ready", title: "Ready Work", updatedAt: 3, })); await ctx.db.insert("workDefinitions", { createdAt: 3, createdBy: "test", payloadJson: JSON.stringify({ acceptanceCriteria: ["Works"], assumptions: [], constraints: [], desiredOutcome: "Ship", inScope: [], outOfScope: [], problem: "Missing", questions: [], requiredArtifacts: [], risk: "low", version: 1, }), risk: "low", status: "current", version: 1, workId, }); await ctx.db.insert("designPackets", { createdAt: 3, createdBy: "test", definitionVersion: 1, payloadJson: JSON.stringify({ architectureSummary: "Simple", callFlowDelta: [], concerns: [], definitionVersion: 1, evidenceRequirements: ["Tests pass"], fileTreeDelta: [], impactMap: { files: [], modules: [], risks: [], summary: "Low", }, invariants: ["No breaking changes"], keyInterfaces: [], slices: [ { codeBoundaries: ["src/index.ts"], dependsOn: [], evidenceRequirements: ["Build succeeds"], id: "s1", objective: "Implement", observableBehavior: "Feature works", reviewRequired: false, title: "Slice 1", verification: ["Run tests"], }, ], tradeoffs: [], version: 1, }), status: "current", version: 1, workId, }); await ctx.db.insert("workSlices", { createdAt: 3, designVersion: 1, objective: "Implement", observableBehavior: "It works", ordinal: 0, payloadJson: "{}", sliceId: "s1", status: "ready", title: "Slice 1", workId, }); return workId; }); describe("Work planning without approvals", () => { test("creates proposed Work and starts Definition planning on request", async () => { const t = convexTest({ modules, schema }); const { organizationId, projectId } = await seedProject(t); const workId = await t.run( async (ctx) => await ctx.db.insert("works", { createdAt: 4, objective: "Implement the feature", organizationId, projectId, status: "proposed", title: "Feature Work", updatedAt: 4, }) ); await t .withIdentity(identity) .mutation(api.workPlanning.requestDefinition, { workId, }); const work = await t.run(async (ctx) => await ctx.db.get(workId)); expect(work?.status).toBe("defining"); }); test("Definition persistence auto-advances to Design phase", async () => { const t = convexTest({ modules, schema }); const { organizationId, projectId } = await seedProject(t); const workId = await t.run( async (ctx) => await ctx.db.insert("works", { createdAt: 1, objective: "Build it", organizationId, projectId, status: "defining", title: "Auto-advance test", updatedAt: 1, }) ); const definition = { acceptanceCriteria: ["Works end to end"], assumptions: [], constraints: [], desiredOutcome: "Feature is live", inScope: ["Backend"], outOfScope: ["Mobile"], problem: "Missing feature", questions: [], requiredArtifacts: ["PR"], risk: "low", }; await t .withIdentity(identity) .mutation(api.workPlanning.saveDefinitionProposal, { payloadJson: JSON.stringify(definition), workId, }); const work = await t.run(async (ctx) => await ctx.db.get(workId)); expect(work?.status).toBe("designing"); expect(work?.definitionVersion).toBe(1); }); test("Design persistence makes Work ready for execution", async () => { const t = convexTest({ modules, schema }); const { organizationId, projectId } = await seedProject(t); const workId = await t.run( async (ctx) => await ctx.db.insert("works", { createdAt: 1, definitionVersion: 1, objective: "Build it", organizationId, projectId, status: "designing", title: "Ready test", updatedAt: 1, }) ); await t.run(async (ctx) => { await ctx.db.insert("workDefinitions", { createdAt: 2, createdBy: "test", payloadJson: JSON.stringify({ acceptanceCriteria: [], assumptions: [], constraints: [], desiredOutcome: "Ship", inScope: [], outOfScope: [], problem: "Missing", questions: [], requiredArtifacts: [], risk: "low", version: 1, }), risk: "low", status: "current", version: 1, workId, }); }); const design = { architectureSummary: "Simple", callFlowDelta: [], concerns: [], evidenceRequirements: ["Tests pass"], fileTreeDelta: [], impactMap: { files: [], modules: [], risks: [], summary: "Low", }, invariants: ["No breaking changes"], keyInterfaces: [], slices: [ { codeBoundaries: ["src/index.ts"], dependsOn: [], evidenceRequirements: ["Build succeeds"], id: "s1", objective: "Implement", observableBehavior: "Feature works", reviewRequired: false, title: "Slice 1", verification: ["Run tests"], }, ], tradeoffs: [], }; await t .withIdentity(identity) .mutation(api.workPlanning.saveDesignProposal, { payloadJson: JSON.stringify(design), workId, }); const work = await t.run(async (ctx) => await ctx.db.get(workId)); expect(work?.status).toBe("ready"); expect(work?.designVersion).toBe(1); }); test("revising a Definition invalidates existing Design and restarts planning", async () => { const t = convexTest({ modules, schema }); const { organizationId, projectId } = await seedProject(t); const workId = await t.run( async (ctx) => await ctx.db.insert("works", { createdAt: 1, definitionVersion: 1, designVersion: 1, objective: "Build it", organizationId, projectId, status: "designing", title: "Revision test", updatedAt: 1, }) ); await t.run(async (ctx) => { await ctx.db.insert("designPackets", { createdAt: 2, createdBy: "test", definitionVersion: 1, payloadJson: "{}", status: "current", version: 1, workId, }); }); const revisedDefinition = { acceptanceCriteria: ["Works"], assumptions: [], constraints: [], desiredOutcome: "Ship", inScope: ["All"], outOfScope: [], problem: "Missing", questions: [], requiredArtifacts: [], risk: "medium", }; await t .withIdentity(identity) .mutation(api.workPlanning.saveDefinitionProposal, { payloadJson: JSON.stringify(revisedDefinition), workId, }); const work = await t.run(async (ctx) => await ctx.db.get(workId)); expect(work?.status).toBe("designing"); expect(work?.definitionVersion).toBe(2); expect(work?.designVersion).toBeUndefined(); const designs = await t.run( async (ctx) => await ctx.db .query("designPackets") .withIndex("by_work_and_version", (q) => q.eq("workId", workId)) .collect() ); expect(designs.every((d) => d.status === "superseded")).toBe(true); }); test("rejects execution when Work is not ready", async () => { const t = convexTest({ modules, schema }); const { organizationId, projectId } = await seedProject(t); const workId = await t.run( async (ctx) => await ctx.db.insert("works", { createdAt: 3, objective: "Test", organizationId, projectId, status: "proposed", title: "Proposed Work", updatedAt: 3, }) ); await expect( t.withIdentity(identity).mutation(api.workExecution.start, { workId }) ).rejects.toThrow(/Ready/u); }); test("rejects execution when Work lacks Definition or Design", async () => { const t = convexTest({ modules, schema }); const { organizationId, projectId } = await seedProject(t); const workId = await t.run( async (ctx) => await ctx.db.insert("works", { createdAt: 3, objective: "Test", organizationId, projectId, status: "ready", title: "Ready but unplanned", updatedAt: 3, }) ); await expect( t.withIdentity(identity).mutation(api.workExecution.start, { workId }) ).rejects.toThrow(/Definition and Design/u); }); test("execution mutations accept service token for agent calls", async () => { const t = convexTest({ modules, schema }); const { organizationId, projectId } = await seedProject(t); const workId = await seedReadyWork(t, organizationId, projectId); // Service-auth path: token + organizationId, no user identity needed. // This should not throw an auth error (it may fail later for missing // git connection, but auth itself must pass). await expect( t.mutation(api.workExecution.start, { organizationId, token: "invalid-token", workId, }) ).rejects.toThrow(/agent control token/u); }); });