import fs from 'node:fs' import { assert, afterAll, afterEach, beforeAll } from 'vitest' import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { Template, waitForTimeout } from '../../src' import { apiUrl, buildTemplateTest } from '../setup' import { randomUUID } from 'node:crypto' const __fileContent = fs.readFileSync(__filename, 'utf8') // read current file content const nonExistentPath = 'nonexistent/path' // map template alias -> failed step index const failureMap: Record = { fromImage: 0, fromTemplate: 0, fromDockerfile: 0, fromImageRegistry: 0, fromAWSRegistry: 0, fromGCPRegistry: 0, copy: undefined, copyItems: undefined, // multi-source copy produces two COPY instructions (steps 1 and 2), // the runCmd after it is step 3 multiSourceCopySecondSource: 2, multiSourceCopyNextStep: 3, copyItemsSecondItem: 2, copyItemsNextStep: 3, remove: 1, rename: 1, makeDir: 1, makeSymlink: 1, runCmd: 1, setWorkdir: 1, setUser: 1, pipInstall: 1, npmInstall: 1, aptInstall: 1, gitClone: 1, setStartCmd: 1, addMcpServer: undefined, betaDevContainerPrebuild: 1, betaSetDevContainerStart: 1, } export const restHandlers = [ http.post(apiUrl('/v3/templates'), async ({ request }) => { const { name } = (await request.clone().json()) as { name: string } return HttpResponse.json({ buildID: randomUUID(), templateID: name, tags: [], }) }), http.post(apiUrl('/v2/templates/:templateID/builds/:buildID'), () => { return HttpResponse.json({}) }), http.get(apiUrl('/templates/:templateID/files/:hash'), () => { return HttpResponse.json({ present: true }) }), http.get<{ templateID: string; buildID: string }>( apiUrl('/templates/:templateID/builds/:buildID/status'), ({ params }) => { const { templateID } = params return HttpResponse.json({ status: 'error', reason: { message: 'Mocked API build error', step: failureMap[templateID], }, logEntries: [], }) } ), ] const server = setupServer(...restHandlers) beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) afterAll(() => server.close()) afterEach(() => server.resetHandlers()) function getStackTraceCallerMethod( fileContent: string, stackTrace: string | undefined ) { if (!stackTrace) { return null } const stackTraceLines = stackTrace.split('\n') if (stackTraceLines.length === 0) { return null } const callerTrace = stackTraceLines[0] // Match line and column numbers at the end of the stack trace line // Format: ...file.ts:123:45) or ...file.ts:123:45 // This handles Windows paths (C:\Users\...) and Unix paths const lineColumnMatch = callerTrace.match(/:(\d+):(\d+)\)?$/) if (!lineColumnMatch) { return null } const lineNumber = parseInt(lineColumnMatch[1]) const columnNumber = parseInt(lineColumnMatch[2]) const lines = fileContent.split('\n') const parsedLine = lines[lineNumber - 1] if (!parsedLine) { return null } // Extract the method name from the line const methodNameMatch = parsedLine .slice(columnNumber - 1) .match(/^(\w+)\s*\(/) if (methodNameMatch) { return methodNameMatch[1] } return null } async function expectToThrowAndCheckTrace( func: (...args: any[]) => Promise, expectedMethod: string ) { try { await func() assert.fail('Expected Template.build to throw an error') } catch (error) { const callerMethod = getStackTraceCallerMethod(__fileContent, error.stack) if (!callerMethod) { throw error } assert.include(callerMethod, expectedMethod) } } buildTemplateTest('traces on fromImage', async ({ buildTemplate }) => { const template = Template().fromImage('e2b.dev/this-image-does-not-exist') await expectToThrowAndCheckTrace(async () => { await buildTemplate(template, { name: 'fromImage', skipCache: true }) }, 'fromImage') }) buildTemplateTest('traces on fromTemplate', async ({ buildTemplate }) => { const template = Template().fromTemplate('this-template-does-not-exist') await expectToThrowAndCheckTrace(async () => { await buildTemplate(template, { name: 'fromTemplate', skipCache: true }) }, 'fromTemplate') }) buildTemplateTest('traces on fromDockerfile', async ({ buildTemplate }) => { const template = Template().fromDockerfile( 'FROM ubuntu:22.04\nRUN nonexistent' ) await expectToThrowAndCheckTrace(async () => { await buildTemplate(template, { name: 'fromDockerfile', skipCache: true }) }, 'fromDockerfile') }) buildTemplateTest('traces on fromImage registry', async ({ buildTemplate }) => { const template = Template().fromImage( 'registry.example.com/nonexistent:latest', { username: 'test', password: 'test', } ) await expectToThrowAndCheckTrace(async () => { await buildTemplate(template, { name: 'fromImageRegistry', }) }, 'fromImage') }) buildTemplateTest('traces on fromAWSRegistry', async ({ buildTemplate }) => { const template = Template().fromAWSRegistry( '123456789.dkr.ecr.us-east-1.amazonaws.com/nonexistent:latest', { accessKeyId: 'test', secretAccessKey: 'test', region: 'us-east-1', } ) await expectToThrowAndCheckTrace(async () => { await buildTemplate(template, { name: 'fromAWSRegistry' }) }, 'fromAWSRegistry') }) buildTemplateTest('traces on fromGCPRegistry', async ({ buildTemplate }) => { const template = Template().fromGCPRegistry( 'gcr.io/nonexistent-project/nonexistent:latest', { serviceAccountJSON: { type: 'service_account' }, } ) await expectToThrowAndCheckTrace(async () => { await buildTemplate(template, { name: 'fromGCPRegistry' }) }, 'fromGCPRegistry') }) buildTemplateTest('traces on fromImage credentials', async () => { await expectToThrowAndCheckTrace(async () => { // @ts-expect-error - testing runtime validation with partial credentials Template().fromImage('ubuntu:22.04', { username: 'user' }) }, 'fromImage') }) buildTemplateTest('traces on copy', async ({ buildTemplate }) => { let template = Template().fromBaseImage() template = template.skipCache().copy(nonExistentPath, nonExistentPath) await expectToThrowAndCheckTrace(async () => { await buildTemplate(template, { name: 'copy' }) }, 'copy') }) buildTemplateTest('traces on copyItems', async ({ buildTemplate }) => { let template = Template().fromBaseImage() template = template .skipCache() .copyItems([{ src: nonExistentPath, dest: nonExistentPath }]) await expectToThrowAndCheckTrace(async () => { await buildTemplate(template, { name: 'copyItems' }) }, 'copyItems') }) buildTemplateTest( 'traces on second source of multi-source copy', async ({ buildTemplate }) => { let template = Template().fromBaseImage() template = template.copy(['stacktrace.test.ts', 'tags.test.ts'], '.') await expectToThrowAndCheckTrace(async () => { await buildTemplate(template, { name: 'multiSourceCopySecondSource' }) }, 'copy') } ) buildTemplateTest( 'traces on step after multi-source copy', async ({ buildTemplate }) => { let template = Template().fromBaseImage() template = template .copy(['stacktrace.test.ts', 'tags.test.ts'], '.') .runCmd(`./${nonExistentPath}`) await expectToThrowAndCheckTrace(async () => { await buildTemplate(template, { name: 'multiSourceCopyNextStep' }) }, 'runCmd') } ) buildTemplateTest( 'traces on second item of copyItems', async ({ buildTemplate }) => { let template = Template().fromBaseImage() template = template.copyItems([ { src: 'stacktrace.test.ts', dest: '.' }, { src: 'tags.test.ts', dest: '.' }, ]) await expectToThrowAndCheckTrace(async () => { await buildTemplate(template, { name: 'copyItemsSecondItem' }) }, 'copyItems') } ) buildTemplateTest( 'traces on step after copyItems', async ({ buildTemplate }) => { let template = Template().fromBaseImage() template = template .copyItems([ { src: 'stacktrace.test.ts', dest: '.' }, { src: 'tags.test.ts', dest: '.' }, ]) .runCmd(`./${nonExistentPath}`) await expectToThrowAndCheckTrace(async () => { await buildTemplate(template, { name: 'copyItemsNextStep' }) }, 'runCmd') } ) buildTemplateTest('traces on copy absolute path', async () => { await expectToThrowAndCheckTrace(async () => { Template().fromBaseImage().copy('/absolute/path', '/absolute/path') }, 'copy') }) buildTemplateTest('traces on copyItems absolute path', async () => { await expectToThrowAndCheckTrace(async () => { Template() .fromBaseImage() .copyItems([{ src: '/absolute/path', dest: '/absolute/path' }]) }, 'copyItems') }) buildTemplateTest('traces on remove', async ({ buildTemplate }) => { let template = Template().fromBaseImage() template = template.skipCache().remove(nonExistentPath) await expectToThrowAndCheckTrace(async () => { await buildTemplate(template, { name: 'remove' }) }, 'remove') }) buildTemplateTest('traces on rename', async ({ buildTemplate }) => { let template = Template().fromBaseImage() template = template.skipCache().rename(nonExistentPath, '/tmp/dest.txt') await expectToThrowAndCheckTrace(async () => { await buildTemplate(template, { name: 'rename' }) }, 'rename') }) buildTemplateTest('traces on makeDir', async ({ buildTemplate }) => { let template = Template().fromBaseImage() template = template.skipCache().makeDir('.bashrc') await expectToThrowAndCheckTrace(async () => { await buildTemplate(template, { name: 'makeDir' }) }, 'makeDir') }) buildTemplateTest('traces on makeSymlink', async ({ buildTemplate }) => { let template = Template().fromBaseImage() template = template.skipCache().makeSymlink('.bashrc', '.bashrc') await expectToThrowAndCheckTrace(async () => { await buildTemplate(template, { name: 'makeSymlink' }) }, 'makeSymlink') }) buildTemplateTest('traces on runCmd', async ({ buildTemplate }) => { let template = Template().fromBaseImage() template = template.skipCache().runCmd(`./${nonExistentPath}`) await expectToThrowAndCheckTrace(async () => { await buildTemplate(template, { name: 'runCmd' }) }, 'runCmd') }) buildTemplateTest('traces on setWorkdir', async ({ buildTemplate }) => { let template = Template().fromBaseImage() template = template.skipCache().setWorkdir('/root/.bashrc') await expectToThrowAndCheckTrace(async () => { await buildTemplate(template, { name: 'setWorkdir' }) }, 'setWorkdir') }) buildTemplateTest('traces on setUser', async ({ buildTemplate }) => { let template = Template().fromBaseImage() template = template.skipCache().setUser('; exit 1') await expectToThrowAndCheckTrace(async () => { await buildTemplate(template, { name: 'setUser' }) }, 'setUser') }) buildTemplateTest('traces on pipInstall', async ({ buildTemplate }) => { let template = Template().fromBaseImage() template = template.skipCache().pipInstall('nonexistent-package') await expectToThrowAndCheckTrace(async () => { await buildTemplate(template, { name: 'pipInstall' }) }, 'pipInstall') }) buildTemplateTest('traces on npmInstall', async ({ buildTemplate }) => { let template = Template().fromBaseImage() template = template.skipCache().npmInstall('nonexistent-package') await expectToThrowAndCheckTrace(async () => { await buildTemplate(template, { name: 'npmInstall' }) }, 'npmInstall') }) buildTemplateTest('traces on aptInstall', async ({ buildTemplate }) => { let template = Template().fromBaseImage() template = template.skipCache().aptInstall('nonexistent-package') await expectToThrowAndCheckTrace(async () => { await buildTemplate(template, { name: 'aptInstall' }) }, 'aptInstall') }) buildTemplateTest('traces on gitClone', async ({ buildTemplate }) => { let template = Template().fromBaseImage() template = template .skipCache() .gitClone('https://github.com/nonexistent/repo.git') await expectToThrowAndCheckTrace(async () => { await buildTemplate(template, { name: 'gitClone' }) }, 'gitClone') }) buildTemplateTest('traces on setStartCmd', async ({ buildTemplate }) => { let template: any = Template().fromBaseImage() template = template.setStartCmd( `./${nonExistentPath}`, waitForTimeout(10_000) ) await expectToThrowAndCheckTrace(async () => { await buildTemplate(template, { name: 'setStartCmd' }) }, 'setStartCmd') }) buildTemplateTest('traces on addMcpServer', async () => { // needs mcp-gateway as base template, without it no mcp servers can be added await expectToThrowAndCheckTrace(async () => { Template().fromBaseImage().skipCache().addMcpServer('exa') }, 'addMcpServer') }) buildTemplateTest( 'traces on betaDevContainerPrebuild', async ({ buildTemplate }) => { const template = Template() .fromTemplate('devcontainer') .skipCache() .betaDevContainerPrebuild(nonExistentPath) await expectToThrowAndCheckTrace(async () => { await buildTemplate(template, { name: 'betaDevContainerPrebuild', }) }, 'betaDevContainerPrebuild') } ) buildTemplateTest( 'traces on betaSetDevContainerStart', async ({ buildTemplate }) => { const template = Template() .fromTemplate('devcontainer') .betaSetDevContainerStart(nonExistentPath) await expectToThrowAndCheckTrace(async () => { await buildTemplate(template, { name: 'betaSetDevContainerStart', }) }, 'betaSetDevContainerStart') } )