2025-07-29 21:39:09 +08:00
|
|
|
import { createSandbox } from '@buster/sandbox';
|
2025-07-29 02:41:07 +08:00
|
|
|
import { RuntimeContext } from '@mastra/core/runtime-context';
|
2025-07-30 04:21:13 +08:00
|
|
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
2025-08-07 01:47:12 +08:00
|
|
|
import { DocsAgentContextKeys } from '../../../agents/docs-agent/docs-agent-context';
|
2025-07-29 02:41:07 +08:00
|
|
|
import { createFiles } from './create-file-tool';
|
|
|
|
|
2025-07-30 04:21:13 +08:00
|
|
|
describe.sequential('create-file-tool integration test', () => {
|
2025-07-29 02:41:07 +08:00
|
|
|
const hasApiKey = !!process.env.DAYTONA_API_KEY;
|
2025-07-30 04:21:13 +08:00
|
|
|
let sharedSandbox: any;
|
2025-07-29 02:41:07 +08:00
|
|
|
|
2025-07-30 04:21:13 +08:00
|
|
|
beforeAll(async () => {
|
|
|
|
if (hasApiKey) {
|
|
|
|
sharedSandbox = await createSandbox({
|
|
|
|
language: 'typescript',
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}, 120000);
|
|
|
|
|
|
|
|
afterAll(async () => {
|
|
|
|
if (sharedSandbox) {
|
|
|
|
await sharedSandbox.delete();
|
|
|
|
}
|
|
|
|
}, 65000);
|
|
|
|
|
|
|
|
function getTestDir() {
|
|
|
|
return `test-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
2025-07-29 21:39:09 +08:00
|
|
|
}
|
2025-07-29 02:41:07 +08:00
|
|
|
|
2025-07-30 04:21:13 +08:00
|
|
|
(hasApiKey ? it : it.skip)(
|
2025-07-29 23:29:20 +08:00
|
|
|
'should create files in sandbox environment',
|
|
|
|
async () => {
|
2025-07-30 04:21:13 +08:00
|
|
|
const testDir = getTestDir();
|
|
|
|
|
|
|
|
// Create isolated test directory
|
|
|
|
await sharedSandbox.process.codeRun(`
|
|
|
|
const fs = require('fs');
|
|
|
|
fs.mkdirSync('${testDir}', { recursive: true });
|
|
|
|
`);
|
|
|
|
|
|
|
|
const runtimeContext = new RuntimeContext();
|
|
|
|
runtimeContext.set(DocsAgentContextKeys.Sandbox, sharedSandbox);
|
|
|
|
|
|
|
|
const result = await createFiles.execute({
|
|
|
|
context: {
|
|
|
|
files: [
|
|
|
|
{ path: `${testDir}/test1.txt`, content: 'Hello from test1' },
|
|
|
|
{ path: `${testDir}/test2.txt`, content: 'Hello from test2' },
|
|
|
|
{ path: `${testDir}/subdir/test3.txt`, content: 'Hello from subdirectory' },
|
|
|
|
],
|
|
|
|
},
|
|
|
|
runtimeContext,
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(result.results).toHaveLength(3);
|
|
|
|
expect(result.results[0]).toEqual({
|
|
|
|
status: 'success',
|
|
|
|
filePath: `${testDir}/test1.txt`,
|
|
|
|
});
|
|
|
|
expect(result.results[1]).toEqual({
|
|
|
|
status: 'success',
|
|
|
|
filePath: `${testDir}/test2.txt`,
|
|
|
|
});
|
|
|
|
expect(result.results[2]).toEqual({
|
|
|
|
status: 'success',
|
|
|
|
filePath: `${testDir}/subdir/test3.txt`,
|
|
|
|
});
|
|
|
|
|
|
|
|
// Verify files were actually created by reading them
|
|
|
|
const verifyCode = `
|
|
|
|
const fs = require('fs');
|
|
|
|
const files = ['${testDir}/test1.txt', '${testDir}/test2.txt', '${testDir}/subdir/test3.txt'];
|
|
|
|
const results: Record<string, string> = {};
|
|
|
|
for (const file of files) {
|
|
|
|
try {
|
|
|
|
results[file] = fs.readFileSync(file, 'utf-8');
|
|
|
|
} catch (err: any) {
|
|
|
|
results[file] = 'ERROR: ' + err.message;
|
|
|
|
}
|
2025-07-29 02:41:07 +08:00
|
|
|
}
|
2025-07-30 04:21:13 +08:00
|
|
|
console.log(JSON.stringify(results));
|
|
|
|
`;
|
2025-07-29 02:41:07 +08:00
|
|
|
|
2025-07-30 04:21:13 +08:00
|
|
|
const verifyResult = await sharedSandbox.process.codeRun(verifyCode);
|
2025-07-29 02:41:07 +08:00
|
|
|
|
2025-07-30 04:21:13 +08:00
|
|
|
// Parse the result
|
|
|
|
const fileContents = JSON.parse(verifyResult.result.trim());
|
|
|
|
|
|
|
|
expect(fileContents[`${testDir}/test1.txt`]).toBe('Hello from test1');
|
|
|
|
expect(fileContents[`${testDir}/test2.txt`]).toBe('Hello from test2');
|
|
|
|
expect(fileContents[`${testDir}/subdir/test3.txt`]).toBe('Hello from subdirectory');
|
2025-07-29 23:29:20 +08:00
|
|
|
},
|
|
|
|
65000
|
|
|
|
);
|
|
|
|
|
2025-07-30 04:21:13 +08:00
|
|
|
(hasApiKey ? it : it.skip)(
|
2025-07-29 23:29:20 +08:00
|
|
|
'should handle absolute paths in sandbox',
|
|
|
|
async () => {
|
2025-07-30 04:21:13 +08:00
|
|
|
const testDir = getTestDir();
|
|
|
|
const runtimeContext = new RuntimeContext();
|
|
|
|
runtimeContext.set(DocsAgentContextKeys.Sandbox, sharedSandbox);
|
|
|
|
|
|
|
|
const result = await createFiles.execute({
|
|
|
|
context: {
|
|
|
|
files: [{ path: `/tmp/${testDir}/absolute-test.txt`, content: 'Absolute path content' }],
|
|
|
|
},
|
|
|
|
runtimeContext,
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(result.results).toHaveLength(1);
|
|
|
|
expect(result.results[0]).toEqual({
|
|
|
|
status: 'success',
|
|
|
|
filePath: `/tmp/${testDir}/absolute-test.txt`,
|
|
|
|
});
|
|
|
|
|
|
|
|
// Verify the file was created
|
|
|
|
const verifyCode = `
|
|
|
|
const fs = require('fs');
|
|
|
|
try {
|
|
|
|
const content = fs.readFileSync('/tmp/${testDir}/absolute-test.txt', 'utf-8');
|
|
|
|
console.log(JSON.stringify({ success: true, content: content }));
|
|
|
|
} catch (error: any) {
|
|
|
|
console.log(JSON.stringify({ success: false, error: error.message }));
|
|
|
|
}
|
|
|
|
`;
|
|
|
|
|
|
|
|
const verifyResult = await sharedSandbox.process.codeRun(verifyCode);
|
|
|
|
const verification = JSON.parse(verifyResult.result.trim());
|
|
|
|
expect(verification.success).toBe(true);
|
|
|
|
expect(verification.content).toBe('Absolute path content');
|
2025-07-29 23:29:20 +08:00
|
|
|
},
|
|
|
|
65000
|
|
|
|
);
|
|
|
|
|
2025-07-30 04:21:13 +08:00
|
|
|
(hasApiKey ? it : it.skip)(
|
2025-07-29 23:29:20 +08:00
|
|
|
'should overwrite existing files',
|
|
|
|
async () => {
|
2025-07-30 04:21:13 +08:00
|
|
|
const testDir = getTestDir();
|
|
|
|
|
|
|
|
// Create isolated test directory
|
|
|
|
await sharedSandbox.process.codeRun(`
|
|
|
|
const fs = require('fs');
|
|
|
|
fs.mkdirSync('${testDir}', { recursive: true });
|
|
|
|
`);
|
|
|
|
|
|
|
|
const runtimeContext = new RuntimeContext();
|
|
|
|
runtimeContext.set(DocsAgentContextKeys.Sandbox, sharedSandbox);
|
|
|
|
|
|
|
|
// First create a file
|
|
|
|
await createFiles.execute({
|
|
|
|
context: {
|
|
|
|
files: [{ path: `${testDir}/overwrite-test.txt`, content: 'Original content' }],
|
|
|
|
},
|
|
|
|
runtimeContext,
|
|
|
|
});
|
|
|
|
|
|
|
|
// Then overwrite it
|
|
|
|
const result = await createFiles.execute({
|
|
|
|
context: {
|
|
|
|
files: [{ path: `${testDir}/overwrite-test.txt`, content: 'New content' }],
|
|
|
|
},
|
|
|
|
runtimeContext,
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(result.results).toHaveLength(1);
|
|
|
|
expect(result.results[0]).toEqual({
|
|
|
|
status: 'success',
|
|
|
|
filePath: `${testDir}/overwrite-test.txt`,
|
|
|
|
});
|
|
|
|
|
|
|
|
// Verify the content was overwritten
|
|
|
|
const verifyCode = `
|
|
|
|
const fs = require('fs');
|
|
|
|
const content = fs.readFileSync('${testDir}/overwrite-test.txt', 'utf-8');
|
|
|
|
console.log(JSON.stringify(content));
|
|
|
|
`;
|
|
|
|
|
|
|
|
const verifyResult = await sharedSandbox.process.codeRun(verifyCode);
|
|
|
|
expect(JSON.parse(verifyResult.result.trim())).toBe('New content');
|
2025-07-29 23:29:20 +08:00
|
|
|
},
|
|
|
|
65000
|
|
|
|
);
|
|
|
|
|
2025-07-30 04:21:13 +08:00
|
|
|
(hasApiKey ? it : it.skip)(
|
2025-07-29 23:29:20 +08:00
|
|
|
'should handle special characters in content',
|
|
|
|
async () => {
|
2025-07-30 04:21:13 +08:00
|
|
|
const testDir = getTestDir();
|
|
|
|
|
|
|
|
// Create isolated test directory
|
|
|
|
await sharedSandbox.process.codeRun(`
|
|
|
|
const fs = require('fs');
|
|
|
|
fs.mkdirSync('${testDir}', { recursive: true });
|
|
|
|
`);
|
|
|
|
|
|
|
|
const runtimeContext = new RuntimeContext();
|
|
|
|
runtimeContext.set(DocsAgentContextKeys.Sandbox, sharedSandbox);
|
|
|
|
|
|
|
|
const specialContent = 'Line 1\nLine 2\tTabbed\n"Quoted"\n\'Single quoted\'';
|
|
|
|
|
|
|
|
const result = await createFiles.execute({
|
|
|
|
context: {
|
|
|
|
files: [{ path: `${testDir}/special-chars.txt`, content: specialContent }],
|
|
|
|
},
|
|
|
|
runtimeContext,
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(result.results[0]).toEqual({
|
|
|
|
status: 'success',
|
|
|
|
filePath: `${testDir}/special-chars.txt`,
|
|
|
|
});
|
|
|
|
|
|
|
|
// Verify special characters were preserved
|
|
|
|
const verifyCode = `
|
|
|
|
const fs = require('fs');
|
|
|
|
const content = fs.readFileSync('${testDir}/special-chars.txt', 'utf-8');
|
|
|
|
console.log(JSON.stringify(content));
|
|
|
|
`;
|
|
|
|
|
|
|
|
const verifyResult = await sharedSandbox.process.codeRun(verifyCode);
|
|
|
|
expect(JSON.parse(verifyResult.result.trim())).toBe(specialContent);
|
2025-07-29 23:29:20 +08:00
|
|
|
},
|
|
|
|
65000
|
|
|
|
);
|
|
|
|
|
2025-07-30 04:21:13 +08:00
|
|
|
(hasApiKey ? it : it.skip)(
|
2025-07-29 23:29:20 +08:00
|
|
|
'should handle permission errors gracefully',
|
|
|
|
async () => {
|
2025-07-30 04:21:13 +08:00
|
|
|
const testDir = getTestDir();
|
|
|
|
|
|
|
|
// Create isolated test directory
|
|
|
|
await sharedSandbox.process.codeRun(`
|
|
|
|
const fs = require('fs');
|
|
|
|
fs.mkdirSync('${testDir}', { recursive: true });
|
|
|
|
`);
|
|
|
|
|
|
|
|
const runtimeContext = new RuntimeContext();
|
|
|
|
runtimeContext.set(DocsAgentContextKeys.Sandbox, sharedSandbox);
|
|
|
|
|
|
|
|
// Try to create a file in a restricted directory
|
|
|
|
const result = await createFiles.execute({
|
|
|
|
context: {
|
|
|
|
files: [
|
|
|
|
{ path: '/root/restricted.txt', content: 'This should fail' },
|
|
|
|
{ path: `${testDir}/valid-file.txt`, content: 'This should succeed' },
|
|
|
|
],
|
|
|
|
},
|
|
|
|
runtimeContext,
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(result.results).toHaveLength(2);
|
|
|
|
// First file should fail
|
|
|
|
expect(result.results[0]?.status).toBe('error');
|
|
|
|
if (result.results[0]?.status === 'error') {
|
|
|
|
expect(result.results[0].errorMessage).toBeTruthy();
|
2025-07-29 23:29:20 +08:00
|
|
|
}
|
2025-07-30 04:21:13 +08:00
|
|
|
// Second file should succeed
|
|
|
|
expect(result.results[1]).toEqual({
|
|
|
|
status: 'success',
|
|
|
|
filePath: `${testDir}/valid-file.txt`,
|
|
|
|
});
|
2025-07-29 23:29:20 +08:00
|
|
|
},
|
|
|
|
65000
|
|
|
|
);
|
2025-07-29 02:41:07 +08:00
|
|
|
});
|