import { convexTest } from "convex-test"; import { anyApi } from "convex/server"; import { afterEach, beforeEach, describe, expect, test, vi } 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 identityA = { tokenIdentifier: "https://convex.test|timeline-a" }; const identityB = { tokenIdentifier: "https://convex.test|timeline-b" }; const newTest = () => convexTest({ modules, schema }); describe("timeline", () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); test("persists a prompt once and creates the deterministic first Work flow", async () => { const t = newTest(); await t .withIdentity(identityA) .mutation(api.organizations.ensurePersonalOrganization, {}); const first = await t .withIdentity(identityA) .mutation(api.timeline.sendPrompt, { clientIdempotencyKey: "prompt-1", scope: { kind: "global" }, text: "Improve the onboarding flow", }); await vi.runAllTimersAsync(); await t.finishInProgressScheduledFunctions(); const duplicate = await t .withIdentity(identityA) .mutation(api.timeline.sendPrompt, { clientIdempotencyKey: "prompt-1", scope: { kind: "global" }, text: "Improve the onboarding flow", }); expect(duplicate._id).toBe(first._id); const global = await t .withIdentity(identityA) .query(api.timeline.getGlobal, {}); expect( global.map((event: { readonly type: string }) => event.type) ).toEqual(["work.created", "assistant.acknowledged", "client.prompt"]); const works = await t .withIdentity(identityA) .query(api.works.listRecent, {}); expect(works).toHaveLength(1); expect(works[0]?.title).toBe("Improve the onboarding flow"); const workTimeline = await t .withIdentity(identityA) .query(api.timeline.getWork, { workId: works[0]!._id }); expect( workTimeline.map((event: { readonly type: string }) => event.type) ).toEqual(["artifact.published", "thread.started"]); expect(workTimeline[0]?.artifacts).toHaveLength(1); }); test("does not expose a Work to another organization", async () => { const t = newTest(); await t .withIdentity(identityA) .mutation(api.organizations.ensurePersonalOrganization, {}); const prompt = await t .withIdentity(identityA) .mutation(api.timeline.sendPrompt, { clientIdempotencyKey: "prompt-private", scope: { kind: "global" }, text: "Private work", }); await vi.runAllTimersAsync(); await t.finishInProgressScheduledFunctions(); const works = await t .withIdentity(identityA) .query(api.works.listRecent, {}); expect(works).toHaveLength(1); await t .withIdentity(identityB) .mutation(api.organizations.ensurePersonalOrganization, {}); await expect( t .withIdentity(identityB) .query(api.timeline.getWork, { workId: works[0]!._id }) ).rejects.toThrow(/Organization membership required/u); expect(prompt.payload).toEqual({ text: "Private work" }); }); test("stores environment metadata and setup events against a Project", async () => { const t = newTest(); await t .withIdentity(identityA) .mutation(api.organizations.ensurePersonalOrganization, {}); const project = await t.mutation(api.projects.persistPublicGitImport, { remote: { documents: [], warnings: [] }, source: { host: "github.com", normalizedUrl: "https://github.com/example/setup-project", projectName: "setup-project", repositoryPath: "example/setup-project", url: "https://github.com/example/setup-project.git", }, userId: identityA.tokenIdentifier, }); await t .withIdentity(identityA) .mutation(api.projectEnvironmentVariables.upsert, { category: "required", name: "DATABASE_URL", projectId: project.id, secretReference: "project-secret-database-url", source: "detected", status: "missing", }); await t.mutation(api.projects.appendSetupEvent, { correlationId: "setup-1", idempotencyKey: "setup-event-1", payload: { missing: ["DATABASE_URL"] }, projectId: project.id, type: "project.setup.blocked", visibility: "timeline", }); const variables = await t .withIdentity(identityA) .query(api.projectEnvironmentVariables.list, { projectId: project.id }); expect(variables).toMatchObject([ { name: "DATABASE_URL", status: "missing" }, ]); expect(variables[0]).not.toHaveProperty("secretValue"); const setup = await t .withIdentity(identityA) .query(api.projects.getSetup, { projectId: project.id }); expect( setup.events.map((event: { readonly type: string }) => event.type) ).toEqual(["project.setup.blocked"]); }); test("retries a failed runtime once while preserving stale-attempt safety", async () => { const t = newTest(); const organization = await t .withIdentity(identityA) .mutation(api.organizations.ensurePersonalOrganization, {}); const project = await t.mutation(api.projects.persistPublicGitImport, { remote: { documents: [], warnings: [] }, source: { host: "github.com", normalizedUrl: "https://github.com/example/retry-project", projectName: "retry-project", repositoryPath: "example/retry-project", url: "https://github.com/example/retry-project.git", }, userId: identityA.tokenIdentifier, }); const runtimeId = await t.mutation(api.projectRuntimes.requestRuntime, { idempotencyKey: `project:${project.id}:vm:v1`, organizationId: organization._id, projectId: project.id, provider: "agentos", }); await t.mutation(api.projectRuntimes.markStatus, { attempt: 1, lastError: "worker unavailable", runtimeRowId: runtimeId, status: "failed", }); const retried = await t.mutation(api.projectRuntimes.retry, { runtimeRowId: runtimeId, }); expect(retried).toMatchObject({ retried: true, runtime: { attempt: 2, status: "requested" }, }); expect(retried.runtime.lastError).toBeUndefined(); const staleWrite = await t.mutation(api.projectRuntimes.markStatus, { attempt: 1, lastError: "late failure", runtimeRowId: runtimeId, status: "failed", }); expect(staleWrite).toMatchObject({ attempt: 2, status: "requested" }); const duplicateRetry = await t.mutation(api.projectRuntimes.retry, { runtimeRowId: runtimeId, }); expect(duplicateRetry).toMatchObject({ retried: false, runtime: { attempt: 2, status: "requested" }, }); }); });