mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-09 02:24:11 +08:00
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:
@@ -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 = [
|
||||
|
||||
@@ -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',
|
||||
|
||||
1332
ccw/src/core/routes/loop-v2-routes.ts
Normal file
1332
ccw/src/core/routes/loop-v2-routes.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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) => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user