|
| 1 | +import { containerTest } from "@internal/testcontainers"; |
| 2 | +import type { PrismaClient } from "@trigger.dev/database"; |
| 3 | +import { describe, expect, vi } from "vitest"; |
| 4 | +import type { AuthenticatedEnvironment } from "~/services/apiAuth.server"; |
| 5 | +import { syncDeclarativeSchedules } from "~/v3/services/createBackgroundWorker.server"; |
| 6 | + |
| 7 | +vi.setConfig({ testTimeout: 60_000 }); |
| 8 | + |
| 9 | +type WorkerArg = Parameters<typeof syncDeclarativeSchedules>[1]; |
| 10 | +const noWorker = {} as unknown as WorkerArg; |
| 11 | + |
| 12 | +async function seedProjectWithEnvs(prisma: PrismaClient) { |
| 13 | + const slug = `sds_${Math.random().toString(36).slice(2, 10)}`; |
| 14 | + const organization = await prisma.organization.create({ data: { title: slug, slug } }); |
| 15 | + const project = await prisma.project.create({ |
| 16 | + data: { name: slug, slug, organizationId: organization.id, externalRef: slug }, |
| 17 | + }); |
| 18 | + const mkEnv = (envSlug: string, type: "PRODUCTION" | "DEVELOPMENT") => |
| 19 | + prisma.runtimeEnvironment.create({ |
| 20 | + data: { |
| 21 | + slug: envSlug, |
| 22 | + type, |
| 23 | + projectId: project.id, |
| 24 | + organizationId: organization.id, |
| 25 | + apiKey: `tr_${envSlug}_${slug}`, |
| 26 | + pkApiKey: `pk_${envSlug}_${slug}`, |
| 27 | + shortcode: `${envSlug[0]}${slug.slice(0, 5)}`, |
| 28 | + }, |
| 29 | + }); |
| 30 | + const prodEnv = await mkEnv("prod", "PRODUCTION"); |
| 31 | + const devEnv = await mkEnv("dev", "DEVELOPMENT"); |
| 32 | + return { organization, project, prodEnv, devEnv }; |
| 33 | +} |
| 34 | + |
| 35 | +function makeDeclarativeSchedule( |
| 36 | + prisma: PrismaClient, |
| 37 | + projectId: string, |
| 38 | + environmentIds: string[], |
| 39 | + taskIdentifier = "my-task" |
| 40 | +) { |
| 41 | + return prisma.taskSchedule.create({ |
| 42 | + data: { |
| 43 | + friendlyId: `sched_${Math.random().toString(36).slice(2, 10)}`, |
| 44 | + taskIdentifier, |
| 45 | + projectId, |
| 46 | + generatorExpression: "0 * * * *", |
| 47 | + generatorDescription: "every hour", |
| 48 | + type: "DECLARATIVE", |
| 49 | + instances: { |
| 50 | + create: environmentIds.map((environmentId) => ({ environmentId, projectId })), |
| 51 | + }, |
| 52 | + }, |
| 53 | + include: { instances: true }, |
| 54 | + }); |
| 55 | +} |
| 56 | + |
| 57 | +function countingPrisma(prisma: PrismaClient) { |
| 58 | + const counts = { instanceDeleteMany: 0, scheduleDelete: 0, scheduleDeleteMany: 0 }; |
| 59 | + const client = prisma.$extends({ |
| 60 | + query: { |
| 61 | + taskScheduleInstance: { |
| 62 | + deleteMany({ args, query }) { |
| 63 | + counts.instanceDeleteMany++; |
| 64 | + return query(args); |
| 65 | + }, |
| 66 | + }, |
| 67 | + taskSchedule: { |
| 68 | + delete({ args, query }) { |
| 69 | + counts.scheduleDelete++; |
| 70 | + return query(args); |
| 71 | + }, |
| 72 | + deleteMany({ args, query }) { |
| 73 | + counts.scheduleDeleteMany++; |
| 74 | + return query(args); |
| 75 | + }, |
| 76 | + }, |
| 77 | + }, |
| 78 | + }); |
| 79 | + return { client: client as unknown as PrismaClient, counts }; |
| 80 | +} |
| 81 | + |
| 82 | +const asEnv = (env: { id: string; projectId: string; type: string }) => |
| 83 | + env as unknown as AuthenticatedEnvironment; |
| 84 | + |
| 85 | +describe("syncDeclarativeSchedules deletion path", () => { |
| 86 | + containerTest( |
| 87 | + "does not issue any instance delete when the env owns no instance of the missing schedules", |
| 88 | + async ({ prisma }) => { |
| 89 | + const { project, prodEnv, devEnv } = await seedProjectWithEnvs(prisma); |
| 90 | + |
| 91 | + for (let i = 0; i < 5; i++) { |
| 92 | + await makeDeclarativeSchedule(prisma, project.id, [prodEnv.id], `task-${i}`); |
| 93 | + } |
| 94 | + |
| 95 | + const { client, counts } = countingPrisma(prisma); |
| 96 | + await syncDeclarativeSchedules([], noWorker, asEnv(devEnv), client); |
| 97 | + |
| 98 | + expect(counts.instanceDeleteMany).toBe(0); |
| 99 | + expect(counts.scheduleDelete).toBe(0); |
| 100 | + |
| 101 | + const remaining = await prisma.taskScheduleInstance.count({ |
| 102 | + where: { projectId: project.id }, |
| 103 | + }); |
| 104 | + expect(remaining).toBe(5); |
| 105 | + } |
| 106 | + ); |
| 107 | + |
| 108 | + containerTest( |
| 109 | + "collapses N per-schedule instance deletes into a single batched deleteMany", |
| 110 | + async ({ prisma }) => { |
| 111 | + const { project, prodEnv, devEnv } = await seedProjectWithEnvs(prisma); |
| 112 | + |
| 113 | + for (let i = 0; i < 5; i++) { |
| 114 | + await makeDeclarativeSchedule(prisma, project.id, [prodEnv.id, devEnv.id], `task-${i}`); |
| 115 | + } |
| 116 | + |
| 117 | + const { client, counts } = countingPrisma(prisma); |
| 118 | + await syncDeclarativeSchedules([], noWorker, asEnv(devEnv), client); |
| 119 | + |
| 120 | + expect(counts.instanceDeleteMany).toBe(1); |
| 121 | + |
| 122 | + const devInstances = await prisma.taskScheduleInstance.count({ |
| 123 | + where: { projectId: project.id, environmentId: devEnv.id }, |
| 124 | + }); |
| 125 | + expect(devInstances).toBe(0); |
| 126 | + |
| 127 | + const prodInstances = await prisma.taskScheduleInstance.count({ |
| 128 | + where: { projectId: project.id, environmentId: prodEnv.id }, |
| 129 | + }); |
| 130 | + expect(prodInstances).toBe(5); |
| 131 | + |
| 132 | + const remainingSchedules = await prisma.taskSchedule.count({ |
| 133 | + where: { projectId: project.id }, |
| 134 | + }); |
| 135 | + expect(remainingSchedules).toBe(5); |
| 136 | + } |
| 137 | + ); |
| 138 | + |
| 139 | + containerTest( |
| 140 | + "deletes schedules whose only instance is in the current env", |
| 141 | + async ({ prisma }) => { |
| 142 | + const { project, devEnv } = await seedProjectWithEnvs(prisma); |
| 143 | + |
| 144 | + for (let i = 0; i < 3; i++) { |
| 145 | + await makeDeclarativeSchedule(prisma, project.id, [devEnv.id], `task-${i}`); |
| 146 | + } |
| 147 | + |
| 148 | + const { client } = countingPrisma(prisma); |
| 149 | + await syncDeclarativeSchedules([], noWorker, asEnv(devEnv), client); |
| 150 | + |
| 151 | + const schedules = await prisma.taskSchedule.count({ where: { projectId: project.id } }); |
| 152 | + expect(schedules).toBe(0); |
| 153 | + const instances = await prisma.taskScheduleInstance.count({ |
| 154 | + where: { projectId: project.id }, |
| 155 | + }); |
| 156 | + expect(instances).toBe(0); |
| 157 | + } |
| 158 | + ); |
| 159 | +}); |
0 commit comments