import { execSync } from 'child_process' import { existsSync } from 'fs' import * as fs from 'fs/promises' import * as path from 'path' import { afterEach, beforeEach, describe, expect, test } from 'vitest' import { Language } from '../../../src/commands/template/generators' describe('Template Init', () => { let testDir: string const cliPath = path.join(process.cwd(), 'dist', 'index.js') beforeEach(async () => { // Use Node.js built-in temp directory handling testDir = await fs.mkdtemp('e2b-init-test-') }) afterEach(async () => { // Clean up test directory if (testDir) { await fs.rm(testDir, { recursive: true, force: true }) } }) describe('CLI Options', () => { Object.values(Language).forEach((language) => { test(`should generate files with --name and --language ${language}`, async () => { const templateName = 'my-test-template' // Run init command with CLI options execSync( `node "${cliPath}" template init --name "${templateName}" --language "${language}" --path "${testDir}"`, { stdio: 'inherit' } ) // Verify template directory was created const templateDir = path.join(testDir, templateName) expect(existsSync(templateDir)).toBe(true) // Verify files were created in the template directory const expectedFiles = getExpectedFiles(language) for (const file of expectedFiles) { const filePath = path.join(templateDir, file) expect(existsSync(filePath)).toBe(true) // Verify file content is not empty const content = await fs.readFile(filePath, 'utf8') expect(content.trim().length).toBeGreaterThan(0) } // Verify template name is used in build files await verifyTemplateNameInBuildFiles( templateDir, language, templateName ) }) }) test('should validate template name format', async () => { // Matches the server-side rule in e2b-dev/infra (id.identifierRegex): // only lowercase letters, numbers, dashes and underscores are allowed. const invalidNames = [ 'invalid space', // contains space 'invalid.dot', // contains a dot '', // empty 'a'.repeat(129), // exceeds the 128 character limit ] for (const invalidName of invalidNames) { expect(() => { execSync( `node "${cliPath}" template init --name "${invalidName}" --language "typescript" --path "${testDir}"`, { stdio: 'pipe' } ) }).toThrow() } }) test('should validate language parameter', async () => { expect(() => { execSync( `node "${cliPath}" template init --name "test" --language "invalid-lang" --path "${testDir}"`, { stdio: 'pipe' } ) }).toThrow() }) test('should work with valid template names', async () => { const validNames = [ 'a', // single character 'abc', // simple 'my-template', // with hyphens 'my_template', // with underscores 'my-template_name', // with hyphens and underscores 'test123', // with numbers '123test', // starting with number 'a-b-c-d-e', // multiple hyphens 'a_b_c_d_e', // multiple underscores 'api-server_v2', // mixed hyphens and underscores ] for (const validName of validNames) { // Clean directory before each test const files = await fs.readdir(testDir) for (const file of files) { await fs.rm(path.join(testDir, file), { recursive: true, force: true, }) } // Should not throw execSync( `node "${cliPath}" template init --name "${validName}" --language "typescript" --path "${testDir}"`, { stdio: 'pipe' } ) // Verify template directory was created const templateDir = path.join(testDir, validName) expect(existsSync(templateDir)).toBe(true) // Verify files were created in the template directory const expectedFiles = getExpectedFiles(Language.TypeScript) for (const file of expectedFiles) { expect(existsSync(path.join(templateDir, file))).toBe(true) } } }) }) describe('Package.json Integration', () => { test('should add scripts to existing package.json', async () => { // Create a package.json file in the parent directory const packageJson = { name: 'test-project', version: '1.0.0', scripts: { test: 'echo "test"', }, } const packageJsonPath = path.join(testDir, 'package.json') await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)) // Run init command execSync( `node "${cliPath}" template init --name "test-template" --language "typescript" --path "${testDir}"`, { stdio: 'inherit' } ) // Verify package.json was updated (it should remain in the parent directory) const updatedPackageJson = JSON.parse( await fs.readFile(packageJsonPath, 'utf8') ) expect(updatedPackageJson.scripts).toHaveProperty('e2b:build:dev') expect(updatedPackageJson.scripts).toHaveProperty('e2b:build:prod') expect(updatedPackageJson.scripts.test).toBe('echo "test"') // existing script preserved }) test('should work without package.json', async () => { // Run init command without package.json execSync( `node "${cliPath}" template init --name "test-template" --language "typescript" --path "${testDir}"`, { stdio: 'inherit' } ) // Verify template directory was created const templateDir = path.join(testDir, 'test-template') expect(existsSync(templateDir)).toBe(true) // Verify files were created in the template directory const expectedFiles = getExpectedFiles(Language.TypeScript) for (const file of expectedFiles) { expect(existsSync(path.join(templateDir, file))).toBe(true) } // Verify package.json was created in the template directory const createdPackageJSON = JSON.parse( await fs.readFile(path.join(templateDir, 'package.json'), 'utf8') ) expect(createdPackageJSON.scripts).toHaveProperty('e2b:build:dev') expect(createdPackageJSON.scripts).toHaveProperty('e2b:build:prod') }) }) describe('Makefile Integration', () => { test('should add scripts to existing Makefile', async () => { // Create a Makefile file in the parent directory const makefile = ` .PHONY: build build/%: \tCGO_ENABLED=1 go build ` const makefilePath = path.join(testDir, 'Makefile') await fs.writeFile(makefilePath, makefile) // Run init command execSync( `node "${cliPath}" template init --name "test-template" --language "python-sync" --path "${testDir}"`, { stdio: 'inherit' } ) // Verify Makefile was updated (it should remain in the parent directory) const updatedMakefile = await fs.readFile(makefilePath, 'utf8') expect(updatedMakefile).toContain('e2b:build:dev') expect(updatedMakefile).toContain('e2b:build:prod') expect(updatedMakefile).toContain(`.PHONY: build build/%: \tCGO_ENABLED=1 go build`) // existing script preserved }) test('should work without Makefile', async () => { // Run init command without Makefile execSync( `node "${cliPath}" template init --name "test-template" --language "python-sync" --path "${testDir}"`, { stdio: 'inherit' } ) // Verify template directory was created const templateDir = path.join(testDir, 'test-template') expect(existsSync(templateDir)).toBe(true) // Verify files were created in the template directory const expectedFiles = getExpectedFiles(Language.PythonSync) for (const file of expectedFiles) { expect(existsSync(path.join(templateDir, file))).toBe(true) } // Verify Makefile was created in the template directory const createdMakefile = await fs.readFile( path.join(templateDir, 'Makefile'), 'utf8' ) expect(createdMakefile).toContain('e2b:build:dev') expect(createdMakefile).toContain('e2b:build:prod') }) }) describe('File Content Validation', () => { test('should generate correct TypeScript template content', async () => { execSync( `node "${cliPath}" template init --name "test-ts" --language "typescript" --path "${testDir}"`, { stdio: 'inherit' } ) const templateDir = path.join(testDir, 'test-ts') const templateContent = await fs.readFile( path.join(templateDir, 'template.ts'), 'utf8' ) // Verify basic structure expect(templateContent).toContain("import { Template } from 'e2b'") expect(templateContent).toContain('export const template = Template()') expect(templateContent).toContain('fromImage') expect(templateContent).toContain('e2bdev/base') }) test('should generate correct Python template content', async () => { execSync( `node "${cliPath}" template init --name "test-py" --language "python-sync" --path "${testDir}"`, { stdio: 'inherit' } ) const templateDir = path.join(testDir, 'test-py') const templateContent = await fs.readFile( path.join(templateDir, 'template.py'), 'utf8' ) // Verify basic structure expect(templateContent).toContain('from e2b import Template') expect(templateContent).toContain('template = (') expect(templateContent).toContain('Template()') expect(templateContent).toContain('from_image') expect(templateContent).toContain('e2bdev/base') }) test('should generate correct async Python template content', async () => { execSync( `node "${cliPath}" template init --name "test-py-async" --language "python-async" --path "${testDir}"`, { stdio: 'inherit' } ) const templateContent = await fs.readFile( path.join(testDir, 'test-py-async', 'template.py'), 'utf8' ) // Verify async structure expect(templateContent).toContain('from e2b import AsyncTemplate') expect(templateContent).toContain('AsyncTemplate()') }) }) describe('Directory Conflict Handling', () => { test('should fail when directory already exists', async () => { // Create a directory that would conflict const conflictDir = path.join(testDir, 'test') await fs.mkdir(conflictDir) await fs.writeFile( path.join(conflictDir, 'existing.txt'), 'existing content' ) // Should fail when trying to create template with existing directory name expect(() => { execSync( `node "${cliPath}" template init --name "test" --language "typescript" --path "${testDir}"`, { stdio: 'pipe' } ) }).toThrow() // Verify original directory is preserved and unchanged expect(existsSync(path.join(conflictDir, 'existing.txt'))).toBe(true) // Verify no template files were created in the existing directory expect(existsSync(path.join(conflictDir, 'template.ts'))).toBe(false) }) }) }) // Helper functions function getExpectedFiles(language: Language): string[] { const extension = language === Language.TypeScript ? '.ts' : '.py' const buildDevName = language === Language.TypeScript ? 'build.dev' : 'build_dev' const buildProdName = language === Language.TypeScript ? 'build.prod' : 'build_prod' return [ `template${extension}`, `${buildDevName}${extension}`, `${buildProdName}${extension}`, ] } async function verifyTemplateNameInBuildFiles( testDir: string, language: Language, templateName: string ): Promise { const extension = language === Language.TypeScript ? '.ts' : '.py' const buildDevName = language === Language.TypeScript ? 'build.dev' : 'build_dev' const buildProdName = language === Language.TypeScript ? 'build.prod' : 'build_prod' // Check dev build file contains template name with -dev suffix const devContent = await fs.readFile( path.join(testDir, `${buildDevName}${extension}`), 'utf8' ) expect(devContent).toContain(`${templateName}-dev`) // Check prod build file contains template name const prodContent = await fs.readFile( path.join(testDir, `${buildProdName}${extension}`), 'utf8' ) expect(prodContent).toContain(templateName) expect(prodContent).not.toContain(`${templateName}-dev`) // should not have -dev suffix }