import { runInDurableObject } from "cloudflare:test"; import { env } from "cloudflare:workers"; import { BranchName, DispatchMode, ExecutionRuntime } from "@/contracts"; import { ProjectDO } from "@/worker/durable"; import { acceptManualRun as acceptManualRunCommand, claimRunWork as claimRunWorkCommand, finalizeRunExecution as finalizeRunExecutionCommand, requestRunCancel as requestRunCancelCommand, } from "@/worker/durable/project-do/commands"; import { getProjectConfigRow } from "@/worker/durable/project-do/repo"; import { transitionAcceptManualRun } from "@/worker/durable/project-do/transitions"; import { expectTrusted } from "@/worker/contracts"; import type { ProjectDoContext } from "@/worker/durable/project-do/types"; import { createLogger } from "@/worker/services/logger"; type ProjectDoInternals = Pick; export const getProjectDoInternals = (instance: ProjectDO): ProjectDoInternals => instance as unknown as ProjectDoInternals; export const createTestProjectDoContext = (instance: ProjectDO, envOverride?: Env): ProjectDoContext => { const { ctx, env, db } = getProjectDoInternals(instance); return { ctx, env: envOverride ?? env, db, logger: createLogger("test.project-do"), cacheProjectId: (projectId) => { (instance as unknown as { cachedProjectId?: string | null }).cachedProjectId = projectId; }, }; }; export const createBoundStorageProxy = ( storage: DurableObjectStorage, overrides: Partial>, ): DurableObjectStorage => new Proxy(storage, { get(target, prop, receiver) { if (Object.prototype.hasOwnProperty.call(overrides, prop)) { return overrides[prop as keyof typeof overrides]; } const value = Reflect.get(target, prop, receiver); return typeof value === "function" ? value.bind(target) : value; }, }); export const createBoundTransactionProxy = ( txn: DurableObjectTransaction, overrides: Partial>, ): DurableObjectTransaction => new Proxy(txn, { get(target, prop, receiver) { if (Object.prototype.hasOwnProperty.call(overrides, prop)) { return overrides[prop as keyof typeof overrides]; } const value = Reflect.get(target, prop, receiver); return typeof value === "function" ? value.bind(target) : value; }, }); export const withPatchedStorage = (context: ProjectDoContext, storage: DurableObjectStorage): ProjectDoContext => ({ ...context, ctx: Object.assign(Object.create(context.ctx), { storage }) as DurableObjectState, }); export const createAlarmSchedulingSuppressedContext = (context: ProjectDoContext): ProjectDoContext => { const transaction: DurableObjectStorage["transaction"] = async (closure) => context.ctx.storage.transaction( async (txn) => await closure(createBoundTransactionProxy(txn, { setAlarm: async () => {} })), ); const storage = createBoundStorageProxy(context.ctx.storage, { transaction }); return withPatchedStorage(context, storage); }; export const createAlarmWriteFailingContext = ( context: ProjectDoContext, errorMessage = "alarm write failed", ): ProjectDoContext => { const transaction: DurableObjectStorage["transaction"] = async (closure) => context.ctx.storage.transaction(async (txn) => { const failingTxn = createBoundTransactionProxy(txn, { setAlarm: async () => { throw new Error(errorMessage); }, }); return await closure(failingTxn); }); const storage = createBoundStorageProxy(context.ctx.storage, { transaction }); return withPatchedStorage(context, storage); }; export const expectAcceptedManualRun = ( result: Awaited>, ): Extract>, { kind: "accepted" }> => { if (result.kind !== "accepted") { throw new Error(`Expected accepted manual run, got ${result.kind}:${result.reason}.`); } return result; }; export const acceptManualRunWithoutAlarm = async ( projectStub: ReturnType, input: Parameters[1], ): Promise>, { kind: "accepted" }>> => { let accepted: Awaited> | null = null; await runInDurableObject(projectStub, async (instance: ProjectDO) => { const context = createAlarmSchedulingSuppressedContext(createTestProjectDoContext(instance)); accepted = await acceptManualRunCommand(context, input); }); if (!accepted) { throw new Error("acceptManualRunWithoutAlarm did not return an accepted run."); } return expectAcceptedManualRun(accepted); }; export const fillManualRunQueueWithoutAlarmOrRunInitialization = async ( projectStub: ReturnType, input: Parameters[1], count: number, ): Promise => { await runInDurableObject(projectStub, async (instance: ProjectDO) => { const context = createAlarmSchedulingSuppressedContext(createTestProjectDoContext(instance)); context.db.transaction((tx) => { const projectConfigRow = getProjectConfigRow(tx, input.projectId); if (!projectConfigRow) { throw new Error(`Project config ${input.projectId} is missing during test queue fill.`); } // Queue saturation tests only need ProjectDO rows. Skipping eager RunDO // initialization removes a large amount of setup overhead on busy CI. const resolvedInput = { projectId: input.projectId, triggeredByUserId: input.triggeredByUserId, branch: input.branch ?? expectTrusted(BranchName, projectConfigRow.defaultBranch, "BranchName"), repoUrl: projectConfigRow.repoUrl, configPath: projectConfigRow.configPath, dispatchMode: expectTrusted(DispatchMode, projectConfigRow.dispatchMode, "DispatchMode"), executionRuntime: expectTrusted(ExecutionRuntime, projectConfigRow.executionRuntime, "ExecutionRuntime"), }; const now = Date.now(); for (let index = 0; index < count; index += 1) { const accepted = transitionAcceptManualRun(context, tx, resolvedInput, now + index); if (accepted.kind !== "accepted") { throw new Error(`Expected accepted manual run during test queue fill, got ${accepted.reason}.`); } } }); }); }; export const claimRunWorkWithoutAlarm = async ( projectStub: ReturnType, input: Parameters[1], ): Promise>> => { let claim: Awaited> | null = null; await runInDurableObject(projectStub, async (instance: ProjectDO) => { const context = createAlarmSchedulingSuppressedContext(createTestProjectDoContext(instance)); claim = await claimRunWorkCommand(context, input); }); if (!claim) { throw new Error("claimRunWorkWithoutAlarm did not return a claim result."); } return claim; }; export const finalizeRunExecutionWithoutAlarm = async ( projectStub: ReturnType, input: Omit[1], "sandboxDestroyed"> & { sandboxDestroyed?: boolean; }, ): Promise>> => { let result: Awaited> | null = null; await runInDurableObject(projectStub, async (instance: ProjectDO) => { const context = createAlarmSchedulingSuppressedContext(createTestProjectDoContext(instance)); result = await finalizeRunExecutionCommand(context, { ...input, sandboxDestroyed: input.sandboxDestroyed ?? true, }); }); if (!result) { throw new Error("finalizeRunExecutionWithoutAlarm did not return a finalize result."); } return result; }; export const requestRunCancelWithoutAlarm = async ( projectStub: ReturnType, input: Parameters[1], ): Promise>> => { let result: Awaited> | null = null; await runInDurableObject(projectStub, async (instance: ProjectDO) => { const context = createAlarmSchedulingSuppressedContext(createTestProjectDoContext(instance)); result = await requestRunCancelCommand(context, input); }); if (!result) { throw new Error("requestRunCancelWithoutAlarm did not return a cancel result."); } return result; };