435 lines
13 KiB
TypeScript
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')
|
|
}
|
|
)
|