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

@@ -99,7 +99,10 @@ const MODULE_CSS_FILES = [
'29-help.css',
'30-core-memory.css',
'31-api-settings.css',
'34-discovery.css'
'32-issue-manager.css',
'33-cli-stream-viewer.css',
'34-discovery.css',
'36-loop-monitor.css'
];
const MODULE_FILES = [

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) => {

View File

@@ -6,7 +6,7 @@ import { resolvePath, getRecentPaths, normalizePathForDisplay } from '../utils/p
// Import route handlers
import { handleStatusRoutes } from './routes/status-routes.js';
import { handleCliRoutes } from './routes/cli-routes.js';
import { handleCliRoutes, cleanupStaleExecutions } from './routes/cli-routes.js';
import { handleCliSettingsRoutes } from './routes/cli-settings-routes.js';
import { handleMemoryRoutes } from './routes/memory-routes.js';
import { handleCoreMemoryRoutes } from './routes/core-memory-routes.js';
@@ -29,6 +29,7 @@ import { handleLiteLLMApiRoutes } from './routes/litellm-api-routes.js';
import { handleNavStatusRoutes } from './routes/nav-status-routes.js';
import { handleAuthRoutes } from './routes/auth-routes.js';
import { handleLoopRoutes } from './routes/loop-routes.js';
import { handleLoopV2Routes } from './routes/loop-v2-routes.js';
import { handleTestLoopRoutes } from './routes/test-loop-routes.js';
import { handleTaskRoutes } from './routes/task-routes.js';
@@ -568,7 +569,12 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
if (await handleCcwRoutes(routeContext)) return;
}
// Loop routes (/api/loops*)
// Loop V2 routes (/api/loops/v2/*) - must be checked before v1
if (pathname.startsWith('/api/loops/v2')) {
if (await handleLoopV2Routes(routeContext)) return;
}
// Loop V1 routes (/api/loops/*) - backward compatibility
if (pathname.startsWith('/api/loops')) {
if (await handleLoopRoutes(routeContext)) return;
}
@@ -717,6 +723,14 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
console.log(`WebSocket endpoint available at ws://${host}:${serverPort}/ws`);
console.log(`Hook endpoint available at POST http://${host}:${serverPort}/api/hook`);
// Start periodic cleanup of stale CLI executions (every 2 minutes)
const CLEANUP_INTERVAL_MS = 2 * 60 * 1000;
const cleanupInterval = setInterval(cleanupStaleExecutions, CLEANUP_INTERVAL_MS);
server.on('close', () => {
clearInterval(cleanupInterval);
console.log('[Server] Stopped CLI execution cleanup interval');
});
// Start health check service for all enabled providers
try {
const healthCheckService = getHealthCheckService();