Files
e2b-dev--e2b/packages/js-sdk/tests/template/stacktrace.test.ts
T
2026-07-13 12:47:58 +08:00

435 lines
13 KiB
TypeScript

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<string, number | undefined> = {
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<void>,
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')
}
)