import { alwaysExternal } from "@trigger.dev/core/v3/build"; import type { BuildManifest, WorkerManifest } from "@trigger.dev/core/v3/schemas"; import * as fs from "node:fs"; import { mkdir, rename, rm } from "node:fs/promises"; import * as path from "node:path"; import { rimraf } from "rimraf"; import { buildWorker, rewriteBuildManifestPaths } from "../src/build/buildWorker.js"; import { loadConfig } from "../src/config.js"; import { indexWorkerManifest } from "../src/indexing/indexWorkerManifest.js"; import { writeJSONFile } from "../src/utilities/fileSystem.js"; import { logger } from "../src/utilities/logger.js"; import { normalizeImportPath } from "../src/utilities/normalizeImportPath.js"; import { getTmpDir } from "../src/utilities/tempDirectories.js"; import type { TestCase } from "./fixtures.js"; import { fixturesConfig } from "./fixtures.js"; import type { E2EOptions } from "./schemas.js"; import { E2EOptionsSchema } from "./schemas.js"; import type { PackageManager } from "./utils.js"; import { executeTestCaseRun, installFixtureDeps, LOCKFILES, parsePackageManager, runTsc, } from "./utils.js"; const TIMEOUT = 120_000; interface E2EFixtureTest extends TestCase { fixtureDir: string; packageManager: PackageManager; tempDir: string; workspaceDir: string; } const testCases: TestCase[] = process.env.MOD ? fixturesConfig.filter(({ id }) => process.env.MOD === id) : fixturesConfig; let options: E2EOptions; try { options = E2EOptionsSchema.parse({ logLevel: process.env.LOG, packageManager: process.env.PM, }); } catch (_e) { options = { logLevel: "log", }; } logger.loggerLevel = options.logLevel; if (testCases.length === 0) { if (process.env.MOD) { throw new Error(`No test case found for ${process.env.MOD}`); } else { throw new Error("Nothing to test"); } } describe("buildWorker", async () => { beforeEach(async ({ fixtureDir, skip, packageManager, workspaceDir }) => { await rimraf(path.join(workspaceDir, "**/node_modules"), { glob: true, }); await rimraf(path.join(workspaceDir, ".yarn"), { glob: true }); if ( packageManager === "npm" && (fs.existsSync(path.resolve(path.join(workspaceDir, "yarn.lock"))) || fs.existsSync(path.resolve(path.join(workspaceDir, "yarn.lock.copy")))) ) { // `npm ci` & `npm install` will update an existing yarn.lock try { await rename( path.resolve(path.join(workspaceDir, "yarn.lock")), path.resolve(path.join(workspaceDir, "yarn.lock.copy")) ); } catch (_e) { await rename( path.resolve(path.join(workspaceDir, "yarn.lock.copy")), path.resolve(path.join(workspaceDir, "yarn.lock")) ); } } if ( options.packageManager && !fs.existsSync(path.resolve(fixtureDir, LOCKFILES[options.packageManager])) ) { skip(); } await installFixtureDeps({ fixtureDir, packageManager, workspaceDir }); }, TIMEOUT); afterEach(async ({ packageManager, workspaceDir }) => { if (packageManager === "npm") { try { await rename( path.resolve(path.join(workspaceDir, "yarn.lock.copy")), path.resolve(path.join(workspaceDir, "yarn.lock")) ); } catch {} } vi.unstubAllEnvs(); }); for (let testCase of testCases) { test.extend({ // Seed `workspaceRelativeDir` before the spread so the key always exists. // vitest 4 resolves fixture-to-fixture dependencies strictly at // `test.extend()` time: the `workspaceDir` fixture below destructures // `workspaceRelativeDir`, so it must be a defined fixture even for test // cases that don't set it (it's an optional `TestCase` field). The spread // overrides this default when the case provides its own value. workspaceRelativeDir: "", ...testCase, fixtureDir: async ({ id }, use) => await use(path.resolve(path.join(process.cwd(), "e2e/fixtures", id))), workspaceDir: async ({ fixtureDir, workspaceRelativeDir }, use) => await use(path.resolve(path.join(fixtureDir, workspaceRelativeDir))), packageManager: async ({ workspaceDir }, use) => await use(await parsePackageManager(options.packageManager, workspaceDir)), tempDir: async ({ workspaceDir }, use) => { const existingTempDir = path.resolve(path.join(workspaceDir, ".trigger")); if (fs.existsSync(existingTempDir)) { await rm(existingTempDir, { force: true, recursive: true }); } await use( (await mkdir(path.join(workspaceDir, ".trigger"), { recursive: true })) as string ); }, })( `fixture ${testCase.id}`, { timeout: TIMEOUT }, async ({ id, tempDir, tsconfig, packageManager, fixtureDir, workspaceDir, wantConfigInvalidError, wantConfigNotFoundError, wantBuildWorkerError, wantIndexingError, buildManifestMatcher, workerManifestMatcher, runs, }) => { let resolvedConfig: Awaited>; const configExpect = expect( (async () => { resolvedConfig = await loadConfig({ cwd: workspaceDir, }); })(), wantConfigNotFoundError || wantConfigInvalidError ? "does not resolve config" : "resolves config" ); if (wantConfigNotFoundError) { await configExpect.rejects.toThrowError(); return; } await configExpect.resolves.not.toThrowError(); if (wantConfigInvalidError) { expect(resolvedConfig!).toBeUndefined(); return; } expect(resolvedConfig!).toBeTruthy(); if (tsconfig) { const tscResult = await runTsc( workspaceDir, tsconfig, packageManager === "yarn" ? fixtureDir : undefined ); expect(tscResult.success).toBe(true); } const destination = getTmpDir(workspaceDir, "build"); let buildManifest: BuildManifest; const buildExpect = expect( (async () => { buildManifest = await buildWorker({ target: "deploy", environment: "test", destination: destination.path, resolvedConfig: resolvedConfig!, rewritePaths: false, forcedExternals: alwaysExternal, }); })(), wantBuildWorkerError ? "does not build" : "builds" ); if (wantBuildWorkerError) { await buildExpect.rejects.toThrowError(); return; } await buildExpect.resolves.not.toThrowError(); if (buildManifestMatcher) { for (const external of buildManifestMatcher.externals ?? []) { expect(buildManifest!.externals).toContainEqual(external); } for (const file of buildManifestMatcher.files ?? []) { const found = (buildManifestMatcher.files ?? []).find((f) => f?.entry === file?.entry); expect(found).toBeTruthy(); } } else { expect(buildManifest!).toBeTruthy(); } logger.debug("Build manifest", buildManifest!); const rewrittenManifest = rewriteBuildManifestPaths(buildManifest!, destination.path); if (resolvedConfig!.instrumentedPackageNames?.length ?? 0 > 0) { expect(rewrittenManifest.loaderEntryPoint).toBe("/app/src/entryPoints/loader.mjs"); } else { expect(rewrittenManifest.loaderEntryPoint).toBeUndefined(); } expect(rewrittenManifest.indexWorkerEntryPoint).toBe( "/app/src/entryPoints/managed-index-worker.mjs" ); const stdout: string[] = []; const stderr: string[] = []; let workerManifest: WorkerManifest; const indexExpect = expect( (async () => { workerManifest = await indexWorkerManifest({ runtime: buildManifest!.runtime, indexWorkerPath: buildManifest!.indexWorkerEntryPoint, buildManifestPath: path.join(destination.path, "build.json"), nodeOptions: buildManifest!.loaderEntryPoint ? `--import=${normalizeImportPath(buildManifest!.loaderEntryPoint)}` : undefined, env: testCase.envVars ?? {}, otelHookExclude: buildManifest!.otelImportHook?.exclude, otelHookInclude: buildManifest!.otelImportHook?.include, handleStdout(data) { stdout.push(data); logger.debug("indexWorkerManifest handleStdout"); logger.debug(data); }, handleStderr(data) { if (!data.includes("DeprecationWarning")) { stderr.push(data); logger.debug("indexWorkerManifest handleStderr"); logger.debug(data); } }, }); })(), wantIndexingError ? "does not index" : "indexes" ); if (wantIndexingError) { await indexExpect.rejects.toThrowError(); return; } await indexExpect.resolves.not.toThrowError(); if (workerManifestMatcher) { expect(workerManifest!).toMatchObject(workerManifestMatcher); } else { expect(workerManifest!).toBeTruthy(); } logger.debug("Worker manifest", workerManifest!); if (runs && runs.length > 0) { await writeJSONFile(path.join(destination.path, "index.json"), workerManifest!); } for (const taskRun of runs || []) { const { result, totalDurationMs, spans } = await executeTestCaseRun({ run: taskRun, testCase, destination: destination.path, workerManifest: workerManifest!, contentHash: buildManifest!.contentHash, }); logger.debug("Task run result", result); expect(result.ok).toBe(taskRun.result.ok); if (result.ok) { if (taskRun.result.durationMs) { expect(totalDurationMs).toBeGreaterThanOrEqual(taskRun.result.durationMs); } if (taskRun.result.output) { expect(result.output).toEqual(taskRun.result.output); } if (taskRun.result.outputType) { expect(result.outputType).toEqual(taskRun.result.outputType); } if (taskRun.result.spans) { for (const spanName of taskRun.result.spans) { const foundSpan = spans.find((span) => span.name === spanName); expect(foundSpan).toBeTruthy(); } } } } } ); } });