feat: Add comprehensive tests for CCW Loop System flow state

- Implemented loop control tasks in JSON format for testing.
- Created comprehensive test scripts for loop flow and standalone tests.
- Developed a shell script to automate the testing of the entire loop system flow, including mock endpoints and state transitions.
- Added error handling and execution history tests to ensure robustness.
- Established variable substitution and success condition evaluations in tests.
- Set up cleanup and workspace management for test environments.
This commit is contained in:
catlog22
2026-01-22 10:13:00 +08:00
parent d9f1d14d5e
commit 60eab98782
37 changed files with 12347 additions and 917 deletions

View File

@@ -60,9 +60,35 @@ interface ActiveExecution {
startTime: number;
output: string;
status: 'running' | 'completed' | 'error';
completedTimestamp?: number; // When execution completed (for 5-minute retention)
}
const activeExecutions = new Map<string, ActiveExecution>();
const EXECUTION_RETENTION_MS = 5 * 60 * 1000; // 5 minutes
/**
* Cleanup stale completed executions older than retention period
* Runs periodically to prevent memory buildup
*/
export function cleanupStaleExecutions(): void {
const now = Date.now();
const staleIds: string[] = [];
for (const [id, exec] of activeExecutions.entries()) {
if (exec.completedTimestamp && (now - exec.completedTimestamp) > EXECUTION_RETENTION_MS) {
staleIds.push(id);
}
}
staleIds.forEach(id => {
activeExecutions.delete(id);
console.log(`[ActiveExec] Cleaned up stale execution: ${id}`);
});
if (staleIds.length > 0) {
console.log(`[ActiveExec] Cleaned up ${staleIds.length} stale execution(s), remaining: ${activeExecutions.size}`);
}
}
/**
* Get all active CLI executions
@@ -113,19 +139,12 @@ export function updateActiveExecution(event: {
activeExec.output += output;
}
} else if (type === 'completed') {
// Mark as completed instead of immediately deleting
// Keep execution visible for 5 minutes to allow page refreshes to see it
// Mark as completed with timestamp for retention-based cleanup
const activeExec = activeExecutions.get(executionId);
if (activeExec) {
activeExec.status = success ? 'completed' : 'error';
// Auto-cleanup after 5 minutes
setTimeout(() => {
activeExecutions.delete(executionId);
console.log(`[ActiveExec] Auto-cleaned completed execution: ${executionId}`);
}, 5 * 60 * 1000);
console.log(`[ActiveExec] Marked as ${activeExec.status}, will auto-clean in 5 minutes`);
activeExec.completedTimestamp = Date.now();
console.log(`[ActiveExec] Marked as ${activeExec.status}, retained for ${EXECUTION_RETENTION_MS / 1000}s`);
}
}
}
@@ -139,7 +158,10 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
// API: Get Active CLI Executions (for state recovery)
if (pathname === '/api/cli/active' && req.method === 'GET') {
const executions = getActiveExecutions();
const executions = getActiveExecutions().map(exec => ({
...exec,
isComplete: exec.status !== 'running'
}));
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ executions }));
return true;
@@ -664,8 +686,13 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
});
});
// Remove from active executions on completion
activeExecutions.delete(executionId);
// Mark as completed with timestamp for retention-based cleanup (not immediate delete)
const activeExec = activeExecutions.get(executionId);
if (activeExec) {
activeExec.status = result.success ? 'completed' : 'error';
activeExec.completedTimestamp = Date.now();
console.log(`[ActiveExec] Direct execution ${executionId} marked as ${activeExec.status}, retained for ${EXECUTION_RETENTION_MS / 1000}s`);
}
// Broadcast completion
broadcastToClients({
@@ -684,8 +711,13 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
};
} catch (error: unknown) {
// Remove from active executions on error
activeExecutions.delete(executionId);
// Mark as completed with timestamp for retention-based cleanup (not immediate delete)
const activeExec = activeExecutions.get(executionId);
if (activeExec) {
activeExec.status = 'error';
activeExec.completedTimestamp = Date.now();
console.log(`[ActiveExec] Direct execution ${executionId} marked as error, retained for ${EXECUTION_RETENTION_MS / 1000}s`);
}
broadcastToClients({
type: 'CLI_EXECUTION_ERROR',

File diff suppressed because it is too large Load Diff

View File

@@ -152,6 +152,48 @@ export async function handleTaskRoutes(ctx: RouteContext): Promise<boolean> {
return true;
}
// GET /api/tasks/:taskId - Get single task
const taskDetailMatch = pathname.match(/^\/api\/tasks\/([^\/]+)$/);
if (taskDetailMatch && req.method === 'GET') {
const taskId = decodeURIComponent(taskDetailMatch[1]);
// Sanitize taskId to prevent path traversal
if (taskId.includes('/') || taskId.includes('\\') || taskId === '..' || taskId === '.') {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, error: 'Invalid task ID format' }));
return true;
}
try {
const taskPath = join(taskDir, taskId + '.json');
if (!existsSync(taskPath)) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, error: 'Task not found: ' + taskId }));
return true;
}
const content = await readFile(taskPath, 'utf-8');
const task = JSON.parse(content) as Task;
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
data: {
task: task
}
}));
return true;
} catch (error) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: false,
error: (error as Error).message
}));
return true;
}
}
// POST /api/tasks/validate - Validate task loop_control configuration
if (pathname === '/api/tasks/validate' && req.method === 'POST') {
handlePostRequest(req, res, async (body) => {