Merge remote-tracking branch 'origin/staging' into devin/BUS-1449-1752896149

This commit is contained in:
dal 2025-07-21 01:15:41 -06:00
commit 5f4230fa8a
No known key found for this signature in database
GPG Key ID: 16F4B0E1E9F61122
5 changed files with 646 additions and 0 deletions

View File

@ -0,0 +1,170 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import { createFilesSafely, generateFileCreateCode, type FileCreateParams } from './create-file-functions';
vi.mock('node:fs/promises');
const mockFs = vi.mocked(fs);
describe('create-file-functions', () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('createFilesSafely', () => {
it('should create files successfully', async () => {
const fileParams: FileCreateParams[] = [
{ path: '/test/file1.txt', content: 'content1' },
{ path: '/test/file2.txt', content: 'content2' },
];
mockFs.mkdir.mockResolvedValue(undefined);
mockFs.writeFile.mockResolvedValue(undefined);
const results = await createFilesSafely(fileParams);
expect(results).toHaveLength(2);
expect(results[0]).toEqual({
success: true,
filePath: '/test/file1.txt',
});
expect(results[1]).toEqual({
success: true,
filePath: '/test/file2.txt',
});
expect(mockFs.mkdir).toHaveBeenCalledTimes(2);
expect(mockFs.writeFile).toHaveBeenCalledTimes(2);
expect(mockFs.writeFile).toHaveBeenCalledWith('/test/file1.txt', 'content1', 'utf-8');
expect(mockFs.writeFile).toHaveBeenCalledWith('/test/file2.txt', 'content2', 'utf-8');
});
it('should handle relative paths correctly', async () => {
const fileParams: FileCreateParams[] = [
{ path: 'relative/file.txt', content: 'content' },
];
mockFs.mkdir.mockResolvedValue(undefined);
mockFs.writeFile.mockResolvedValue(undefined);
const results = await createFilesSafely(fileParams);
expect(results).toHaveLength(1);
expect(results[0]).toEqual({
success: true,
filePath: 'relative/file.txt',
});
const expectedPath = path.join(process.cwd(), 'relative/file.txt');
const expectedDir = path.dirname(expectedPath);
expect(mockFs.mkdir).toHaveBeenCalledWith(expectedDir, { recursive: true });
expect(mockFs.writeFile).toHaveBeenCalledWith(expectedPath, 'content', 'utf-8');
});
it('should handle directory creation errors', async () => {
const fileParams: FileCreateParams[] = [
{ path: '/test/file.txt', content: 'content' },
];
mockFs.mkdir.mockRejectedValue(new Error('Permission denied'));
const results = await createFilesSafely(fileParams);
expect(results).toHaveLength(1);
expect(results[0]).toEqual({
success: false,
filePath: '/test/file.txt',
error: 'Failed to create directory: Permission denied',
});
});
it('should handle file write errors', async () => {
const fileParams: FileCreateParams[] = [
{ path: '/test/file.txt', content: 'content' },
];
mockFs.mkdir.mockResolvedValue(undefined);
mockFs.writeFile.mockRejectedValue(new Error('Disk full'));
const results = await createFilesSafely(fileParams);
expect(results).toHaveLength(1);
expect(results[0]).toEqual({
success: false,
filePath: '/test/file.txt',
error: 'Disk full',
});
});
it('should continue processing other files when one fails', async () => {
const fileParams: FileCreateParams[] = [
{ path: '/test/file1.txt', content: 'content1' },
{ path: '/test/file2.txt', content: 'content2' },
];
mockFs.mkdir.mockResolvedValue(undefined);
mockFs.writeFile
.mockRejectedValueOnce(new Error('File 1 error'))
.mockResolvedValueOnce(undefined);
const results = await createFilesSafely(fileParams);
expect(results).toHaveLength(2);
expect(results[0]).toEqual({
success: false,
filePath: '/test/file1.txt',
error: 'File 1 error',
});
expect(results[1]).toEqual({
success: true,
filePath: '/test/file2.txt',
});
});
it('should handle empty file params array', async () => {
const results = await createFilesSafely([]);
expect(results).toEqual([]);
});
});
describe('generateFileCreateCode', () => {
it('should generate valid TypeScript code for file creation', () => {
const fileParams: FileCreateParams[] = [
{ path: '/test/file1.txt', content: 'content1' },
{ path: '/test/file2.txt', content: 'content2' },
];
const code = generateFileCreateCode(fileParams);
expect(code).toContain('const fs = require(\'fs\');');
expect(code).toContain('const path = require(\'path\');');
expect(code).toContain('function createSingleFile(fileParams)');
expect(code).toContain('function createFilesConcurrently(fileParams)');
expect(code).toContain('fs.mkdirSync(dirPath, { recursive: true });');
expect(code).toContain('fs.writeFileSync(resolvedPath, content, \'utf-8\');');
expect(code).toContain('console.log(JSON.stringify(results));');
expect(code).toContain(JSON.stringify(fileParams));
});
it('should handle empty file params array', () => {
const code = generateFileCreateCode([]);
expect(code).toContain('const fileParams = [];');
expect(code).toContain('console.log(JSON.stringify(results));');
});
it('should escape special characters in file content', () => {
const fileParams: FileCreateParams[] = [
{ path: '/test/file.txt', content: 'line1\nline2\ttab' },
];
const code = generateFileCreateCode(fileParams);
expect(code).toContain('"content":"line1\\nline2\\ttab"');
});
});
});

View File

@ -0,0 +1,99 @@
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
export interface FileCreateResult {
success: boolean;
filePath: string;
error?: string;
}
export interface FileCreateParams {
path: string;
content: string;
}
async function createSingleFile(fileParams: FileCreateParams): Promise<FileCreateResult> {
try {
const { path: filePath, content } = fileParams;
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.join(process.cwd(), filePath);
const dirPath = path.dirname(resolvedPath);
try {
await fs.mkdir(dirPath, { recursive: true });
} catch (error) {
return {
success: false,
filePath,
error: `Failed to create directory: ${error instanceof Error ? error.message : 'Unknown error'}`,
};
}
await fs.writeFile(resolvedPath, content, 'utf-8');
return {
success: true,
filePath,
};
} catch (error) {
return {
success: false,
filePath: fileParams.path,
error: error instanceof Error ? error.message : 'Unknown error occurred',
};
}
}
export async function createFilesSafely(fileParams: FileCreateParams[]): Promise<FileCreateResult[]> {
const fileCreatePromises = fileParams.map((params) => createSingleFile(params));
return Promise.all(fileCreatePromises);
}
/**
* Generates TypeScript code that can be executed in a sandbox to create files
* The generated code is self-contained and outputs results as JSON to stdout
*/
export function generateFileCreateCode(fileParams: FileCreateParams[]): string {
return `
const fs = require('fs');
const path = require('path');
function createSingleFile(fileParams) {
try {
const { path: filePath, content } = fileParams;
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.join(process.cwd(), filePath);
const dirPath = path.dirname(resolvedPath);
try {
fs.mkdirSync(dirPath, { recursive: true });
} catch (error) {
return {
success: false,
filePath,
error: \`Failed to create directory: \${error instanceof Error ? error.message : 'Unknown error'}\`,
};
}
fs.writeFileSync(resolvedPath, content, 'utf-8');
return {
success: true,
filePath,
};
} catch (error) {
return {
success: false,
filePath: fileParams.path,
error: error instanceof Error ? error.message : 'Unknown error occurred',
};
}
}
function createFilesConcurrently(fileParams) {
return fileParams.map((params) => createSingleFile(params));
}
const fileParams = ${JSON.stringify(fileParams)};
const results = createFilesConcurrently(fileParams);
console.log(JSON.stringify(results));
`.trim();
}

View File

@ -0,0 +1,233 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { z } from 'zod';
import { RuntimeContext } from '@mastra/core/runtime-context';
import { createFiles } from './create-file-tool';
import { SandboxContextKey, type SandboxContext } from '../../../context/sandbox-context';
vi.mock('@buster/sandbox', () => ({
runTypescript: vi.fn(),
}));
vi.mock('./create-file-functions', () => ({
generateFileCreateCode: vi.fn(),
createFilesSafely: vi.fn(),
}));
import { runTypescript } from '@buster/sandbox';
import { generateFileCreateCode, createFilesSafely } from './create-file-functions';
const mockRunTypescript = vi.mocked(runTypescript);
const mockGenerateFileCreateCode = vi.mocked(generateFileCreateCode);
const mockCreateFilesSafely = vi.mocked(createFilesSafely);
describe('create-file-tool', () => {
let runtimeContext: RuntimeContext<SandboxContext>;
beforeEach(() => {
vi.clearAllMocks();
runtimeContext = new RuntimeContext<SandboxContext>();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('createFiles tool', () => {
it('should have correct tool configuration', () => {
expect(createFiles.id).toBe('create-files');
expect(createFiles.description).toContain('Create one or more files');
expect(createFiles.inputSchema).toBeDefined();
expect(createFiles.outputSchema).toBeDefined();
});
it('should validate input schema correctly', () => {
const validInput = {
files: [
{ path: '/test/file1.txt', content: 'content1' },
{ path: '/test/file2.txt', content: 'content2' },
],
};
expect(() => createFiles.inputSchema.parse(validInput)).not.toThrow();
});
it('should reject invalid input schema', () => {
const invalidInput = {
files: [
{ path: '/test/file1.txt' }, // missing content
],
};
expect(() => createFiles.inputSchema.parse(invalidInput)).toThrow();
});
it('should execute with sandbox when available', async () => {
const mockSandbox = { process: { codeRun: vi.fn() } };
runtimeContext.set(SandboxContextKey.Sandbox, mockSandbox as any);
const input = {
files: [
{ path: '/test/file.txt', content: 'test content' },
],
};
const mockCode = 'generated typescript code';
const mockSandboxResult = {
result: JSON.stringify([{ success: true, filePath: '/test/file.txt' }]),
exitCode: 0,
stderr: '',
};
mockGenerateFileCreateCode.mockReturnValue(mockCode);
mockRunTypescript.mockResolvedValue(mockSandboxResult);
const result = await createFiles.execute({
context: input,
runtimeContext,
});
expect(mockGenerateFileCreateCode).toHaveBeenCalledWith(input.files);
expect(mockRunTypescript).toHaveBeenCalledWith(mockSandbox, mockCode);
expect(result.results).toHaveLength(1);
expect(result.results[0]).toEqual({
status: 'success',
filePath: '/test/file.txt',
});
});
it('should fallback to local execution when sandbox not available', async () => {
const input = {
files: [
{ path: '/test/file.txt', content: 'test content' },
],
};
const mockLocalResult = [
{ success: true, filePath: '/test/file.txt' },
];
mockCreateFilesSafely.mockResolvedValue(mockLocalResult);
const result = await createFiles.execute({
context: input,
runtimeContext,
});
expect(mockCreateFilesSafely).toHaveBeenCalledWith(input.files);
expect(result.results).toHaveLength(1);
expect(result.results[0]).toEqual({
status: 'success',
filePath: '/test/file.txt',
});
});
it('should handle sandbox execution errors', async () => {
const mockSandbox = { process: { codeRun: vi.fn() } };
runtimeContext.set(SandboxContextKey.Sandbox, mockSandbox as any);
const input = {
files: [
{ path: '/test/file.txt', content: 'test content' },
],
};
const mockCode = 'generated typescript code';
const mockSandboxResult = {
result: 'error output',
exitCode: 1,
stderr: 'Execution failed',
};
mockGenerateFileCreateCode.mockReturnValue(mockCode);
mockRunTypescript.mockResolvedValue(mockSandboxResult);
const result = await createFiles.execute({
context: input,
runtimeContext,
});
expect(result.results).toHaveLength(1);
expect(result.results[0]).toEqual({
status: 'error',
filePath: '/test/file.txt',
errorMessage: 'Execution error: Sandbox execution failed: Execution failed',
});
});
it('should handle mixed success and error results', async () => {
const input = {
files: [
{ path: '/test/file1.txt', content: 'content1' },
{ path: '/test/file2.txt', content: 'content2' },
],
};
const mockLocalResult = [
{ success: true, filePath: '/test/file1.txt' },
{ success: false, filePath: '/test/file2.txt', error: 'Permission denied' },
];
mockCreateFilesSafely.mockResolvedValue(mockLocalResult);
const result = await createFiles.execute({
context: input,
runtimeContext,
});
expect(result.results).toHaveLength(2);
expect(result.results[0]).toEqual({
status: 'success',
filePath: '/test/file1.txt',
});
expect(result.results[1]).toEqual({
status: 'error',
filePath: '/test/file2.txt',
errorMessage: 'Permission denied',
});
});
it('should handle empty files array', async () => {
const input = { files: [] };
const result = await createFiles.execute({
context: input,
runtimeContext,
});
expect(result.results).toEqual([]);
});
it('should handle JSON parse errors from sandbox', async () => {
const mockSandbox = { process: { codeRun: vi.fn() } };
runtimeContext.set(SandboxContextKey.Sandbox, mockSandbox as any);
const input = {
files: [
{ path: '/test/file.txt', content: 'test content' },
],
};
const mockCode = 'generated typescript code';
const mockSandboxResult = {
result: 'invalid json output',
exitCode: 0,
stderr: '',
};
mockGenerateFileCreateCode.mockReturnValue(mockCode);
mockRunTypescript.mockResolvedValue(mockSandboxResult);
const result = await createFiles.execute({
context: input,
runtimeContext,
});
expect(result.results).toHaveLength(1);
expect(result.results[0]).toEqual({
status: 'error',
filePath: '/test/file.txt',
errorMessage: expect.stringContaining('Failed to parse sandbox output'),
});
});
});
});

View File

@ -0,0 +1,143 @@
import { runTypescript } from '@buster/sandbox';
import type { RuntimeContext } from '@mastra/core/runtime-context';
import { createTool } from '@mastra/core/tools';
import { wrapTraced } from 'braintrust';
import { z } from 'zod';
import { type SandboxContext, SandboxContextKey } from '../../../context/sandbox-context';
const fileCreateParamsSchema = z.object({
path: z.string().describe('The relative or absolute path to create the file at'),
content: z.string().describe('The content to write to the file'),
});
const createFilesInputSchema = z.object({
files: z
.array(fileCreateParamsSchema)
.describe('Array of file creation operations to perform'),
});
const createFilesOutputSchema = z.object({
results: z.array(
z.discriminatedUnion('status', [
z.object({
status: z.literal('success'),
filePath: z.string(),
}),
z.object({
status: z.literal('error'),
filePath: z.string(),
errorMessage: z.string(),
}),
])
),
});
const createFilesExecution = wrapTraced(
async (
params: z.infer<typeof createFilesInputSchema>,
runtimeContext: RuntimeContext<SandboxContext>
): Promise<z.infer<typeof createFilesOutputSchema>> => {
const { files } = params;
if (!files || files.length === 0) {
return { results: [] };
}
try {
// Check if sandbox is available in runtime context
const sandbox = runtimeContext.get(SandboxContextKey.Sandbox);
if (sandbox) {
// Execute in sandbox
const { generateFileCreateCode } = await import('./create-file-functions');
const code = generateFileCreateCode(files);
const result = await runTypescript(sandbox, code);
if (result.exitCode !== 0) {
console.error('Sandbox execution failed. Exit code:', result.exitCode);
console.error('Stderr:', result.stderr);
console.error('Stdout:', result.result);
throw new Error(`Sandbox execution failed: ${result.stderr || 'Unknown error'}`);
}
// Parse the JSON output from sandbox
let fileResults: Array<{
success: boolean;
filePath: string;
error?: string;
}>;
try {
fileResults = JSON.parse(result.result.trim());
} catch (parseError) {
console.error('Failed to parse sandbox output:', result.result);
throw new Error(
`Failed to parse sandbox output: ${parseError instanceof Error ? parseError.message : 'Unknown parse error'}`
);
}
return {
results: fileResults.map((fileResult) => {
if (fileResult.success) {
return {
status: 'success' as const,
filePath: fileResult.filePath,
};
}
return {
status: 'error' as const,
filePath: fileResult.filePath,
errorMessage: fileResult.error || 'Unknown error',
};
}),
};
}
// Fallback to local execution
const { createFilesSafely } = await import('./create-file-functions');
const fileResults = await createFilesSafely(files);
return {
results: fileResults.map((fileResult) => {
if (fileResult.success) {
return {
status: 'success' as const,
filePath: fileResult.filePath,
};
}
return {
status: 'error' as const,
filePath: fileResult.filePath,
errorMessage: fileResult.error || 'Unknown error',
};
}),
};
} catch (error) {
return {
results: files.map((file) => ({
status: 'error' as const,
filePath: file.path,
errorMessage: `Execution error: ${error instanceof Error ? error.message : 'Unknown error'}`,
})),
};
}
},
{ name: 'create-files' }
);
export const createFiles = createTool({
id: 'create-files',
description: `Create one or more files at specified paths with provided content. Supports both absolute and relative file paths. Creates directories if they don't exist and overwrites existing files. Handles errors gracefully by continuing to process other files even if some fail. Returns both successful operations and failed operations with detailed error messages.`,
inputSchema: createFilesInputSchema,
outputSchema: createFilesOutputSchema,
execute: async ({
context,
runtimeContext,
}: {
context: z.infer<typeof createFilesInputSchema>;
runtimeContext: RuntimeContext<SandboxContext>;
}) => {
return await createFilesExecution(context, runtimeContext);
},
});
export default createFiles;

View File

@ -11,3 +11,4 @@ export { executeSql } from './database-tools/execute-sql';
export { createTodoList } from './planning-thinking-tools/create-todo-item-tool';
export { editFiles } from './file-tools/edit-files-tool/edit-files-tool';
export { readFiles } from './file-tools/read-files-tool/read-files-tool';
export { createFiles } from './file-tools/create-files-tool/create-file-tool';