Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
517 lines
15 KiB
JavaScript
517 lines
15 KiB
JavaScript
/**
|
|
* Code Execution API Tests
|
|
* ADR-032: Code Execution Engine — Piston Integration
|
|
*
|
|
* BEHAVIOR: AI Agent can execute code in multiple languages via Piston
|
|
*
|
|
* Acceptance Criteria:
|
|
* - POST /api/v3/code/execute - executes code and returns result
|
|
* - GET /api/v3/code/languages - returns list of supported languages
|
|
* - POST /api/v3/code/validate - validates code syntax
|
|
* - POST /api/v3/code/execute-loop - executes with Agent callback
|
|
*/
|
|
|
|
import { describe, it, expect, beforeAll, vi, beforeEach } from 'vitest';
|
|
import express from 'express';
|
|
import request from 'supertest';
|
|
|
|
// Mock axios before importing the router
|
|
vi.mock('axios');
|
|
|
|
let codeRouter;
|
|
let app;
|
|
|
|
beforeAll(async () => {
|
|
// Set up test environment
|
|
process.env.PISTON_URL = 'http://localhost:2000';
|
|
process.env.BACKEND_URL = 'http://localhost:5001';
|
|
|
|
// Import the router
|
|
const module = await import('../code.js');
|
|
codeRouter = module.default;
|
|
|
|
// Create Express app for testing
|
|
app = express();
|
|
app.use(express.json());
|
|
app.use('/api/v3/code', codeRouter);
|
|
});
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('Code Execution API - ADR-032 (Piston)', () => {
|
|
|
|
// ============================================
|
|
// GET /api/v3/code/languages
|
|
// ============================================
|
|
|
|
describe('GET /api/v3/code/languages', () => {
|
|
|
|
it('Given Piston is available, When requested, Then returns list of runtimes', async () => {
|
|
// Arrange
|
|
const axios = (await import('axios')).default;
|
|
axios.get = vi.fn().mockResolvedValue({
|
|
data: [
|
|
{ language: 'python', version: '3.10.0', aliases: ['py', 'py3'] },
|
|
{ language: 'javascript', version: '20.11.1', aliases: ['js', 'node'] },
|
|
{ language: 'typescript', version: '5.0.3', aliases: ['ts'] }
|
|
]
|
|
});
|
|
|
|
// Act
|
|
const response = await request(app)
|
|
.get('/api/v3/code/languages');
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(Array.isArray(response.body)).toBe(true);
|
|
expect(response.body.length).toBe(3);
|
|
expect(response.body[0]).toHaveProperty('language');
|
|
expect(response.body[0]).toHaveProperty('version');
|
|
});
|
|
|
|
it('Given Piston is unavailable, When requested, Then returns fallback aliases', async () => {
|
|
// Arrange
|
|
const axios = (await import('axios')).default;
|
|
axios.get = vi.fn().mockRejectedValue(new Error('ECONNREFUSED'));
|
|
|
|
// Act
|
|
const response = await request(app)
|
|
.get('/api/v3/code/languages');
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(Array.isArray(response.body)).toBe(true);
|
|
expect(response.body.some(lang => lang.language === 'python')).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// POST /api/v3/code/execute
|
|
// ============================================
|
|
|
|
describe('POST /api/v3/code/execute', () => {
|
|
|
|
it('Given valid Python code, When executed, Then returns successful result', async () => {
|
|
// Arrange
|
|
const axios = (await import('axios')).default;
|
|
axios.post = vi.fn().mockResolvedValue({
|
|
data: {
|
|
run: {
|
|
stdout: 'Hello, World!\n',
|
|
stderr: '',
|
|
code: 0,
|
|
signal: null,
|
|
cpu_time: 15,
|
|
wall_time: 42,
|
|
memory: 3500000
|
|
},
|
|
language: 'python',
|
|
version: '3.10.0'
|
|
}
|
|
});
|
|
|
|
// Act
|
|
const response = await request(app)
|
|
.post('/api/v3/code/execute')
|
|
.send({
|
|
source_code: 'print("Hello, World!")',
|
|
language: 'python'
|
|
});
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.stdout).toBe('Hello, World!\n');
|
|
expect(response.body.stderr).toBe('');
|
|
expect(response.body.code).toBe(0);
|
|
expect(response.body.time).toEqual({ cpu_ms: 15, wall_ms: 42 });
|
|
expect(response.body.memory_bytes).toBe(3500000);
|
|
expect(response.body.language).toBe('python');
|
|
});
|
|
|
|
it('Given code with syntax error, When executed, Then returns error with stderr', async () => {
|
|
// Arrange
|
|
const axios = (await import('axios')).default;
|
|
axios.post = vi.fn().mockResolvedValue({
|
|
data: {
|
|
run: {
|
|
stdout: '',
|
|
stderr: 'SyntaxError: invalid syntax\n',
|
|
code: 1,
|
|
signal: null,
|
|
cpu_time: 10,
|
|
wall_time: 20,
|
|
memory: 2000000
|
|
},
|
|
language: 'python',
|
|
version: '3.10.0'
|
|
}
|
|
});
|
|
|
|
// Act
|
|
const response = await request(app)
|
|
.post('/api/v3/code/execute')
|
|
.send({
|
|
source_code: 'print("Hello',
|
|
language: 'python'
|
|
});
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.success).toBe(false);
|
|
expect(response.body.code).toBe(1);
|
|
expect(response.body.stderr).toContain('SyntaxError');
|
|
});
|
|
|
|
it('Given runtime error, When executed, Then returns error details', async () => {
|
|
// Arrange
|
|
const axios = (await import('axios')).default;
|
|
axios.post = vi.fn().mockResolvedValue({
|
|
data: {
|
|
run: {
|
|
stdout: '',
|
|
stderr: 'ZeroDivisionError: division by zero\n',
|
|
code: 1,
|
|
signal: null,
|
|
cpu_time: 12,
|
|
wall_time: 25,
|
|
memory: 3000000
|
|
},
|
|
language: 'python',
|
|
version: '3.10.0'
|
|
}
|
|
});
|
|
|
|
// Act
|
|
const response = await request(app)
|
|
.post('/api/v3/code/execute')
|
|
.send({
|
|
source_code: 'print(1/0)',
|
|
language: 'python'
|
|
});
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.success).toBe(false);
|
|
expect(response.body.stderr).toContain('ZeroDivisionError');
|
|
});
|
|
|
|
it('Given JavaScript code with "js" alias, When executed, Then uses javascript runtime', async () => {
|
|
// Arrange
|
|
const axios = (await import('axios')).default;
|
|
axios.post = vi.fn().mockResolvedValue({
|
|
data: {
|
|
run: {
|
|
stdout: '42\n',
|
|
stderr: '',
|
|
code: 0,
|
|
cpu_time: 30,
|
|
wall_time: 50,
|
|
memory: 7000000
|
|
},
|
|
language: 'javascript',
|
|
version: '20.11.1'
|
|
}
|
|
});
|
|
|
|
// Act
|
|
const response = await request(app)
|
|
.post('/api/v3/code/execute')
|
|
.send({
|
|
source_code: 'console.log(42)',
|
|
language: 'js'
|
|
});
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.stdout).toBe('42\n');
|
|
|
|
// Verify Piston was called with correct language
|
|
expect(axios.post).toHaveBeenCalledWith(
|
|
expect.stringContaining('/api/v2/execute'),
|
|
expect.objectContaining({
|
|
language: 'javascript',
|
|
version: '20.11.1'
|
|
})
|
|
);
|
|
});
|
|
|
|
it('Given no source_code, When executed, Then returns 400 bad request', async () => {
|
|
// Act
|
|
const response = await request(app)
|
|
.post('/api/v3/code/execute')
|
|
.send({
|
|
language: 'python'
|
|
});
|
|
|
|
// Assert
|
|
expect(response.status).toBe(400);
|
|
});
|
|
|
|
it('Given Piston unavailable, When executed, Then returns 500 error', async () => {
|
|
// Arrange
|
|
const axios = (await import('axios')).default;
|
|
axios.post = vi.fn().mockRejectedValue(new Error('ECONNREFUSED'));
|
|
|
|
// Act
|
|
const response = await request(app)
|
|
.post('/api/v3/code/execute')
|
|
.send({
|
|
source_code: 'print(1)',
|
|
language: 'python'
|
|
});
|
|
|
|
// Assert
|
|
expect(response.status).toBe(500);
|
|
expect(response.body.error.message).toContain('ECONNREFUSED');
|
|
});
|
|
|
|
it('Given code with stdin, When executed, Then passes stdin to Piston', async () => {
|
|
// Arrange
|
|
const axios = (await import('axios')).default;
|
|
axios.post = vi.fn().mockResolvedValue({
|
|
data: {
|
|
run: {
|
|
stdout: 'Hello\n',
|
|
stderr: '',
|
|
code: 0,
|
|
cpu_time: 10,
|
|
wall_time: 20,
|
|
memory: 3000000
|
|
},
|
|
language: 'python',
|
|
version: '3.10.0'
|
|
}
|
|
});
|
|
|
|
// Act
|
|
const response = await request(app)
|
|
.post('/api/v3/code/execute')
|
|
.send({
|
|
source_code: 'print(input())',
|
|
language: 'python',
|
|
stdin: 'Hello'
|
|
});
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(axios.post).toHaveBeenCalledWith(
|
|
expect.any(String),
|
|
expect.objectContaining({
|
|
stdin: 'Hello'
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// POST /api/v3/code/validate
|
|
// ============================================
|
|
|
|
describe('POST /api/v3/code/validate', () => {
|
|
|
|
it('Given valid Python syntax, When validated, Then returns valid: true', async () => {
|
|
// Arrange
|
|
const axios = (await import('axios')).default;
|
|
axios.post = vi.fn().mockResolvedValue({
|
|
data: {
|
|
run: {
|
|
stdout: 'valid\n',
|
|
stderr: '',
|
|
code: 0
|
|
}
|
|
}
|
|
});
|
|
|
|
// Act
|
|
const response = await request(app)
|
|
.post('/api/v3/code/validate')
|
|
.send({
|
|
source_code: 'def hello(): pass',
|
|
language: 'python'
|
|
});
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.valid).toBe(true);
|
|
expect(response.body.errors).toEqual([]);
|
|
});
|
|
|
|
it('Given invalid Python syntax, When validated, Then returns valid: false', async () => {
|
|
// Arrange
|
|
const axios = (await import('axios')).default;
|
|
axios.post = vi.fn().mockResolvedValue({
|
|
data: {
|
|
run: {
|
|
stdout: 'SyntaxError: unexpected EOF at line 1\n',
|
|
stderr: '',
|
|
code: 0 // ast.parse catches error, prints it, exits 0
|
|
}
|
|
}
|
|
});
|
|
|
|
// Act
|
|
const response = await request(app)
|
|
.post('/api/v3/code/validate')
|
|
.send({
|
|
source_code: 'def hello(',
|
|
language: 'python'
|
|
});
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.valid).toBe(false);
|
|
expect(response.body.errors.length).toBeGreaterThan(0);
|
|
expect(response.body.errors[0].message).toContain('SyntaxError');
|
|
});
|
|
|
|
it('Given non-Python language, When validated, Then returns valid: true (not implemented)', async () => {
|
|
// Act
|
|
const response = await request(app)
|
|
.post('/api/v3/code/validate')
|
|
.send({
|
|
source_code: 'console.log("hi")',
|
|
language: 'javascript'
|
|
});
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.valid).toBe(true);
|
|
expect(response.body.message).toContain('not implemented');
|
|
});
|
|
|
|
it('Given no source_code, When validated, Then returns 400', async () => {
|
|
// Act
|
|
const response = await request(app)
|
|
.post('/api/v3/code/validate')
|
|
.send({
|
|
language: 'python'
|
|
});
|
|
|
|
// Assert
|
|
expect(response.status).toBe(400);
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// POST /api/v3/code/execute-loop
|
|
// ============================================
|
|
|
|
describe('POST /api/v3/code/execute-loop', () => {
|
|
|
|
it('Given code execution succeeds, When executed with callback, Then returns success', async () => {
|
|
// Arrange
|
|
const axios = (await import('axios')).default;
|
|
axios.post = vi.fn()
|
|
// First call: Piston execute
|
|
.mockResolvedValueOnce({
|
|
data: {
|
|
run: {
|
|
stdout: '4\n',
|
|
stderr: '',
|
|
code: 0,
|
|
cpu_time: 15,
|
|
wall_time: 30,
|
|
memory: 3000000
|
|
},
|
|
language: 'python',
|
|
version: '3.10.0'
|
|
}
|
|
})
|
|
// Second call: Agent callback (might fail, that's ok)
|
|
.mockRejectedValueOnce(new Error('Agent not running'));
|
|
|
|
// Act
|
|
const response = await request(app)
|
|
.post('/api/v3/code/execute-loop')
|
|
.send({
|
|
source_code: 'print(2+2)',
|
|
language: 'python',
|
|
conversation_id: 'conv-123',
|
|
agent_id: 'agent-456',
|
|
next_prompt: 'Continue processing'
|
|
});
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.status).toBe('completed');
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.stdout).toBe('4\n');
|
|
});
|
|
|
|
it('Given no callback configured, When executed, Then still returns result', async () => {
|
|
// Arrange
|
|
const axios = (await import('axios')).default;
|
|
axios.post = vi.fn().mockResolvedValue({
|
|
data: {
|
|
run: {
|
|
stdout: '100\n',
|
|
stderr: '',
|
|
code: 0,
|
|
cpu_time: 10,
|
|
wall_time: 20,
|
|
memory: 3000000
|
|
},
|
|
language: 'python',
|
|
version: '3.10.0'
|
|
}
|
|
});
|
|
|
|
// Act
|
|
const response = await request(app)
|
|
.post('/api/v3/code/execute-loop')
|
|
.send({
|
|
source_code: 'print(10*10)',
|
|
language: 'python'
|
|
// No conversation_id, agent_id
|
|
});
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.message).toContain('No callback');
|
|
// Only one axios call (no callback attempt)
|
|
expect(axios.post).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('Given code execution fails, When executed with callback, Then returns error info', async () => {
|
|
// Arrange
|
|
const axios = (await import('axios')).default;
|
|
axios.post = vi.fn()
|
|
.mockResolvedValueOnce({
|
|
data: {
|
|
run: {
|
|
stdout: '',
|
|
stderr: 'NameError: name "x" is not defined\n',
|
|
code: 1,
|
|
cpu_time: 8,
|
|
wall_time: 15,
|
|
memory: 2500000
|
|
},
|
|
language: 'python',
|
|
version: '3.10.0'
|
|
}
|
|
})
|
|
.mockRejectedValueOnce(new Error('Agent not running'));
|
|
|
|
// Act
|
|
const response = await request(app)
|
|
.post('/api/v3/code/execute-loop')
|
|
.send({
|
|
source_code: 'print(x)',
|
|
language: 'python',
|
|
conversation_id: 'conv-123',
|
|
agent_id: 'agent-456'
|
|
});
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.status).toBe('completed');
|
|
expect(response.body.success).toBe(false);
|
|
expect(response.body.stderr).toContain('NameError');
|
|
});
|
|
});
|
|
});
|