mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-14 02:42:04 +08:00
Add comprehensive tests for vector/semantic search functionality
- Implement full coverage tests for Embedder model loading and embedding generation - Add CRUD operations and caching tests for VectorStore - Include cosine similarity computation tests - Validate semantic search accuracy and relevance through various queries - Establish performance benchmarks for embedding and search operations - Ensure edge cases and error handling are covered - Test thread safety and concurrent access scenarios - Verify availability of semantic search dependencies
This commit is contained in:
406
ccw/src/core/routes/session-routes.ts
Normal file
406
ccw/src/core/routes/session-routes.ts
Normal file
@@ -0,0 +1,406 @@
|
||||
// @ts-nocheck
|
||||
/**
|
||||
* Session Routes Module
|
||||
* Handles all Session/Task-related API endpoints
|
||||
*/
|
||||
import type { IncomingMessage, ServerResponse } from 'http';
|
||||
import { readFileSync, writeFileSync, existsSync, readdirSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
export interface RouteContext {
|
||||
pathname: string;
|
||||
url: URL;
|
||||
req: IncomingMessage;
|
||||
res: ServerResponse;
|
||||
initialPath: string;
|
||||
handlePostRequest: (req: IncomingMessage, res: ServerResponse, handler: (body: unknown) => Promise<any>) => void;
|
||||
broadcastToClients: (data: unknown) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session detail data (context, summaries, impl-plan, review)
|
||||
* @param {string} sessionPath - Path to session directory
|
||||
* @param {string} dataType - Type of data to load ('all', 'context', 'tasks', 'summary', 'plan', 'explorations', 'conflict', 'impl-plan', 'review')
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function getSessionDetailData(sessionPath, dataType) {
|
||||
const result = {};
|
||||
|
||||
// Normalize path
|
||||
const normalizedPath = sessionPath.replace(/\\/g, '/');
|
||||
|
||||
try {
|
||||
// Load context-package.json (in .process/ subfolder)
|
||||
if (dataType === 'context' || dataType === 'all') {
|
||||
// Try .process/context-package.json first (common location)
|
||||
let contextFile = join(normalizedPath, '.process', 'context-package.json');
|
||||
if (!existsSync(contextFile)) {
|
||||
// Fallback to session root
|
||||
contextFile = join(normalizedPath, 'context-package.json');
|
||||
}
|
||||
if (existsSync(contextFile)) {
|
||||
try {
|
||||
result.context = JSON.parse(readFileSync(contextFile, 'utf8'));
|
||||
} catch (e) {
|
||||
result.context = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load task JSONs from .task/ folder
|
||||
if (dataType === 'tasks' || dataType === 'all') {
|
||||
const taskDir = join(normalizedPath, '.task');
|
||||
result.tasks = [];
|
||||
if (existsSync(taskDir)) {
|
||||
const files = readdirSync(taskDir).filter(f => f.endsWith('.json') && f.startsWith('IMPL-'));
|
||||
for (const file of files) {
|
||||
try {
|
||||
const content = JSON.parse(readFileSync(join(taskDir, file), 'utf8'));
|
||||
result.tasks.push({
|
||||
filename: file,
|
||||
task_id: file.replace('.json', ''),
|
||||
...content
|
||||
});
|
||||
} catch (e) {
|
||||
// Skip unreadable files
|
||||
}
|
||||
}
|
||||
// Sort by task ID
|
||||
result.tasks.sort((a, b) => a.task_id.localeCompare(b.task_id));
|
||||
}
|
||||
}
|
||||
|
||||
// Load summaries from .summaries/
|
||||
if (dataType === 'summary' || dataType === 'all') {
|
||||
const summariesDir = join(normalizedPath, '.summaries');
|
||||
result.summaries = [];
|
||||
if (existsSync(summariesDir)) {
|
||||
const files = readdirSync(summariesDir).filter(f => f.endsWith('.md'));
|
||||
for (const file of files) {
|
||||
try {
|
||||
const content = readFileSync(join(summariesDir, file), 'utf8');
|
||||
result.summaries.push({ name: file.replace('.md', ''), content });
|
||||
} catch (e) {
|
||||
// Skip unreadable files
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load plan.json (for lite tasks)
|
||||
if (dataType === 'plan' || dataType === 'all') {
|
||||
const planFile = join(normalizedPath, 'plan.json');
|
||||
if (existsSync(planFile)) {
|
||||
try {
|
||||
result.plan = JSON.parse(readFileSync(planFile, 'utf8'));
|
||||
} catch (e) {
|
||||
result.plan = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load explorations (exploration-*.json files) - check .process/ first, then session root
|
||||
if (dataType === 'context' || dataType === 'explorations' || dataType === 'all') {
|
||||
result.explorations = { manifest: null, data: {} };
|
||||
|
||||
// Try .process/ first (standard workflow sessions), then session root (lite tasks)
|
||||
const searchDirs = [
|
||||
join(normalizedPath, '.process'),
|
||||
normalizedPath
|
||||
];
|
||||
|
||||
for (const searchDir of searchDirs) {
|
||||
if (!existsSync(searchDir)) continue;
|
||||
|
||||
// Look for explorations-manifest.json
|
||||
const manifestFile = join(searchDir, 'explorations-manifest.json');
|
||||
if (existsSync(manifestFile)) {
|
||||
try {
|
||||
result.explorations.manifest = JSON.parse(readFileSync(manifestFile, 'utf8'));
|
||||
|
||||
// Load each exploration file based on manifest
|
||||
const explorations = result.explorations.manifest.explorations || [];
|
||||
for (const exp of explorations) {
|
||||
const expFile = join(searchDir, exp.file);
|
||||
if (existsSync(expFile)) {
|
||||
try {
|
||||
result.explorations.data[exp.angle] = JSON.parse(readFileSync(expFile, 'utf8'));
|
||||
} catch (e) {
|
||||
// Skip unreadable exploration files
|
||||
}
|
||||
}
|
||||
}
|
||||
break; // Found manifest, stop searching
|
||||
} catch (e) {
|
||||
result.explorations.manifest = null;
|
||||
}
|
||||
} else {
|
||||
// Fallback: scan for exploration-*.json files directly
|
||||
try {
|
||||
const files = readdirSync(searchDir).filter(f => f.startsWith('exploration-') && f.endsWith('.json'));
|
||||
if (files.length > 0) {
|
||||
// Create synthetic manifest
|
||||
result.explorations.manifest = {
|
||||
exploration_count: files.length,
|
||||
explorations: files.map((f, i) => ({
|
||||
angle: f.replace('exploration-', '').replace('.json', ''),
|
||||
file: f,
|
||||
index: i + 1
|
||||
}))
|
||||
};
|
||||
|
||||
// Load each file
|
||||
for (const file of files) {
|
||||
const angle = file.replace('exploration-', '').replace('.json', '');
|
||||
try {
|
||||
result.explorations.data[angle] = JSON.parse(readFileSync(join(searchDir, file), 'utf8'));
|
||||
} catch (e) {
|
||||
// Skip unreadable files
|
||||
}
|
||||
}
|
||||
break; // Found explorations, stop searching
|
||||
}
|
||||
} catch (e) {
|
||||
// Directory read failed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load conflict resolution decisions (conflict-resolution-decisions.json)
|
||||
if (dataType === 'context' || dataType === 'conflict' || dataType === 'all') {
|
||||
result.conflictResolution = null;
|
||||
|
||||
// Try .process/ first (standard workflow sessions)
|
||||
const conflictFiles = [
|
||||
join(normalizedPath, '.process', 'conflict-resolution-decisions.json'),
|
||||
join(normalizedPath, 'conflict-resolution-decisions.json')
|
||||
];
|
||||
|
||||
for (const conflictFile of conflictFiles) {
|
||||
if (existsSync(conflictFile)) {
|
||||
try {
|
||||
result.conflictResolution = JSON.parse(readFileSync(conflictFile, 'utf8'));
|
||||
break; // Found file, stop searching
|
||||
} catch (e) {
|
||||
// Skip unreadable file
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load IMPL_PLAN.md
|
||||
if (dataType === 'impl-plan' || dataType === 'all') {
|
||||
const implPlanFile = join(normalizedPath, 'IMPL_PLAN.md');
|
||||
if (existsSync(implPlanFile)) {
|
||||
try {
|
||||
result.implPlan = readFileSync(implPlanFile, 'utf8');
|
||||
} catch (e) {
|
||||
result.implPlan = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load review data from .review/
|
||||
if (dataType === 'review' || dataType === 'all') {
|
||||
const reviewDir = join(normalizedPath, '.review');
|
||||
result.review = {
|
||||
state: null,
|
||||
dimensions: [],
|
||||
severityDistribution: null,
|
||||
totalFindings: 0
|
||||
};
|
||||
|
||||
if (existsSync(reviewDir)) {
|
||||
// Load review-state.json
|
||||
const stateFile = join(reviewDir, 'review-state.json');
|
||||
if (existsSync(stateFile)) {
|
||||
try {
|
||||
const state = JSON.parse(readFileSync(stateFile, 'utf8'));
|
||||
result.review.state = state;
|
||||
result.review.severityDistribution = state.severity_distribution || {};
|
||||
result.review.totalFindings = state.total_findings || 0;
|
||||
result.review.phase = state.phase || 'unknown';
|
||||
result.review.dimensionSummaries = state.dimension_summaries || {};
|
||||
result.review.crossCuttingConcerns = state.cross_cutting_concerns || [];
|
||||
result.review.criticalFiles = state.critical_files || [];
|
||||
} catch (e) {
|
||||
// Skip unreadable state
|
||||
}
|
||||
}
|
||||
|
||||
// Load dimension findings
|
||||
const dimensionsDir = join(reviewDir, 'dimensions');
|
||||
if (existsSync(dimensionsDir)) {
|
||||
const files = readdirSync(dimensionsDir).filter(f => f.endsWith('.json'));
|
||||
for (const file of files) {
|
||||
try {
|
||||
const dimName = file.replace('.json', '');
|
||||
const data = JSON.parse(readFileSync(join(dimensionsDir, file), 'utf8'));
|
||||
|
||||
// Handle array structure: [ { findings: [...] } ]
|
||||
let findings = [];
|
||||
let summary = null;
|
||||
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
const dimData = data[0];
|
||||
findings = dimData.findings || [];
|
||||
summary = dimData.summary || null;
|
||||
} else if (data.findings) {
|
||||
findings = data.findings;
|
||||
summary = data.summary || null;
|
||||
}
|
||||
|
||||
result.review.dimensions.push({
|
||||
name: dimName,
|
||||
findings: findings,
|
||||
summary: summary,
|
||||
count: findings.length
|
||||
});
|
||||
} catch (e) {
|
||||
// Skip unreadable files
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error: unknown) {
|
||||
console.error('Error loading session detail:', error);
|
||||
result.error = (error as Error).message;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update task status in a task JSON file
|
||||
* @param {string} sessionPath - Path to session directory
|
||||
* @param {string} taskId - Task ID (e.g., IMPL-001)
|
||||
* @param {string} newStatus - New status (pending, in_progress, completed)
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function updateTaskStatus(sessionPath, taskId, newStatus) {
|
||||
// Normalize path (handle both forward and back slashes)
|
||||
let normalizedPath = sessionPath.replace(/\\/g, '/');
|
||||
|
||||
// Handle Windows drive letter format
|
||||
if (normalizedPath.match(/^[a-zA-Z]:\//)) {
|
||||
// Already in correct format
|
||||
} else if (normalizedPath.match(/^\/[a-zA-Z]\//)) {
|
||||
// Convert /D/path to D:/path
|
||||
normalizedPath = normalizedPath.charAt(1).toUpperCase() + ':' + normalizedPath.slice(2);
|
||||
}
|
||||
|
||||
const taskDir = join(normalizedPath, '.task');
|
||||
|
||||
// Check if task directory exists
|
||||
if (!existsSync(taskDir)) {
|
||||
throw new Error(`Task directory not found: ${taskDir}`);
|
||||
}
|
||||
|
||||
// Try to find the task file
|
||||
let taskFile = join(taskDir, `${taskId}.json`);
|
||||
|
||||
if (!existsSync(taskFile)) {
|
||||
// Try without .json if taskId already has it
|
||||
if (taskId.endsWith('.json')) {
|
||||
taskFile = join(taskDir, taskId);
|
||||
}
|
||||
if (!existsSync(taskFile)) {
|
||||
throw new Error(`Task file not found: ${taskId}.json in ${taskDir}`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const content = JSON.parse(readFileSync(taskFile, 'utf8'));
|
||||
const oldStatus = content.status || 'pending';
|
||||
content.status = newStatus;
|
||||
|
||||
// Add status change timestamp
|
||||
if (!content.status_history) {
|
||||
content.status_history = [];
|
||||
}
|
||||
content.status_history.push({
|
||||
from: oldStatus,
|
||||
to: newStatus,
|
||||
changed_at: new Date().toISOString()
|
||||
});
|
||||
|
||||
writeFileSync(taskFile, JSON.stringify(content, null, 2), 'utf8');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
taskId,
|
||||
oldStatus,
|
||||
newStatus,
|
||||
file: taskFile
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
throw new Error(`Failed to update task ${taskId}: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Session routes
|
||||
* @returns true if route was handled, false otherwise
|
||||
*/
|
||||
export async function handleSessionRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
const { pathname, url, req, res, handlePostRequest } = ctx;
|
||||
|
||||
// API: Get session detail data (context, summaries, impl-plan, review)
|
||||
if (pathname === '/api/session-detail') {
|
||||
const sessionPath = url.searchParams.get('path');
|
||||
const dataType = url.searchParams.get('type') || 'all';
|
||||
|
||||
if (!sessionPath) {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Session path is required' }));
|
||||
return true;
|
||||
}
|
||||
|
||||
const detail = await getSessionDetailData(sessionPath, dataType);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(detail));
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Update task status
|
||||
if (pathname === '/api/update-task-status' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
const { sessionPath, taskId, newStatus } = body;
|
||||
|
||||
if (!sessionPath || !taskId || !newStatus) {
|
||||
return { error: 'sessionPath, taskId, and newStatus are required', status: 400 };
|
||||
}
|
||||
|
||||
return await updateTaskStatus(sessionPath, taskId, newStatus);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Bulk update task status
|
||||
if (pathname === '/api/bulk-update-task-status' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
const { sessionPath, taskIds, newStatus } = body;
|
||||
|
||||
if (!sessionPath || !taskIds || !newStatus) {
|
||||
return { error: 'sessionPath, taskIds, and newStatus are required', status: 400 };
|
||||
}
|
||||
|
||||
const results = [];
|
||||
for (const taskId of taskIds) {
|
||||
try {
|
||||
const result = await updateTaskStatus(sessionPath, taskId, newStatus);
|
||||
results.push(result);
|
||||
} catch (err) {
|
||||
results.push({ taskId, error: err.message });
|
||||
}
|
||||
}
|
||||
return { success: true, results };
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user