claude-code-collective
Version:
Sub-agent collective framework for Claude Code with TDD validation, TaskMaster Task ID integration, hub-spoke coordination, and deterministic handoffs
407 lines (352 loc) • 11.5 kB
JavaScript
/**
* TestContractValidator.js
*
* Core system for executing test contracts during agent handoffs.
* Implements TRUE Test-Driven Development for agent coordination.
*
* Based on Test-Contracts-Reference.md specification
*/
const fs = require('fs-extra');
const path = require('path');
class TestContractValidator {
constructor(options = {}) {
this.options = {
timeout: 30000, // 30 second timeout for contract validation
logFile: '/tmp/contract-validation.log',
maxRetries: 3,
...options
};
this.validationHistory = [];
this.contractCache = new Map();
}
/**
* Parse test contract from agent output
* Expects contract in format:
* ```
* TEST_CONTRACT: {
* preconditions: [...],
* postconditions: [...],
* rollback: function...
* }
* ```
*/
parseContract(agentOutput) {
try {
// Look for TEST_CONTRACT block - find balanced braces
const contractStart = agentOutput.indexOf('TEST_CONTRACT:');
if (contractStart === -1) {
return null;
}
// Find the opening brace after TEST_CONTRACT:
const openBraceIndex = agentOutput.indexOf('{', contractStart);
if (openBraceIndex === -1) {
return null;
}
// Find matching closing brace
let braceCount = 0;
let currentIndex = openBraceIndex;
while (currentIndex < agentOutput.length) {
const char = agentOutput[currentIndex];
if (char === '{') braceCount++;
if (char === '}') braceCount--;
if (braceCount === 0) {
// Found matching closing brace
const contractJson = agentOutput.substring(openBraceIndex, currentIndex + 1);
const contract = this.safeEval(contractJson);
// Validate contract structure
if (!this.isValidContract(contract)) {
throw new Error('Invalid contract structure');
}
return contract;
}
currentIndex++;
}
throw new Error('No matching closing brace found for TEST_CONTRACT');
} catch (error) {
this.log(`Contract parsing failed: ${error.message}`);
return null;
}
}
/**
* Validate that contract has required structure
*/
isValidContract(contract) {
if (!contract || typeof contract !== 'object') return false;
// Must have either preconditions or postconditions
return (Array.isArray(contract.preconditions) ||
Array.isArray(contract.postconditions));
}
/**
* Execute preconditions - validate previous agent's work
*/
async executePreconditions(contract, handoffData) {
if (!contract.preconditions) return { passed: true, results: [] };
const results = [];
let allPassed = true;
for (const condition of contract.preconditions) {
try {
const result = await this.executeCondition(condition, handoffData);
results.push(result);
if (!result.passed && condition.critical) {
allPassed = false;
break; // Stop on critical failure
}
} catch (error) {
const result = {
name: condition.name || 'Unknown condition',
passed: false,
error: error.message,
critical: condition.critical || false
};
results.push(result);
if (condition.critical) {
allPassed = false;
break;
}
}
}
return { passed: allPassed, results };
}
/**
* Execute postconditions - validate current agent's work
*/
async executePostconditions(contract, result, handoffData) {
if (!contract.postconditions) return { passed: true, results: [] };
const results = [];
let allPassed = true;
for (const condition of contract.postconditions) {
try {
const conditionResult = await this.executeCondition(condition, result, handoffData);
results.push(conditionResult);
if (!conditionResult.passed && condition.critical) {
allPassed = false;
break;
}
} catch (error) {
const conditionResult = {
name: condition.name || 'Unknown condition',
passed: false,
error: error.message,
critical: condition.critical || false
};
results.push(conditionResult);
if (condition.critical) {
allPassed = false;
break;
}
}
}
return { passed: allPassed, results };
}
/**
* Execute individual condition test
*/
async executeCondition(condition, ...args) {
const startTime = Date.now();
try {
// Execute the test function
let testResult;
if (typeof condition.test === 'function') {
testResult = await Promise.resolve(condition.test(...args));
} else if (typeof condition.test === 'string') {
// String-based test (eval with safety)
testResult = this.safeEval(condition.test, { data: args[0] });
} else {
throw new Error('Invalid test function');
}
const duration = Date.now() - startTime;
return {
name: condition.name || 'Unnamed condition',
passed: Boolean(testResult),
duration,
critical: condition.critical || false,
errorMessage: condition.errorMessage || 'Condition failed'
};
} catch (error) {
return {
name: condition.name || 'Unnamed condition',
passed: false,
duration: Date.now() - startTime,
error: error.message,
critical: condition.critical || false,
errorMessage: condition.errorMessage || error.message
};
}
}
/**
* Execute rollback if handoff fails
*/
async executeRollback(contract, handoffData, error) {
if (!contract.rollback) {
// Default rollback - just log
this.log(`No rollback defined for failed handoff: ${error.message}`);
return { rolled_back: false, reason: 'No rollback function' };
}
try {
const rollbackResult = await Promise.resolve(contract.rollback(handoffData, error));
this.log(`Rollback executed: ${JSON.stringify(rollbackResult)}`);
return rollbackResult;
} catch (rollbackError) {
this.log(`Rollback failed: ${rollbackError.message}`);
return { rolled_back: false, error: rollbackError.message };
}
}
/**
* Complete handoff validation workflow
*/
async validateHandoff(fromAgent, toAgent, agentOutput, handoffData) {
const validationId = this.generateValidationId();
const startTime = Date.now();
this.log(`Starting handoff validation: ${fromAgent} → ${toAgent} (${validationId})`);
try {
// 1. Parse contract from agent output
const contract = this.parseContract(agentOutput);
if (!contract) {
return {
validationId,
success: false,
error: 'No valid contract found in agent output',
duration: Date.now() - startTime
};
}
// 2. Execute preconditions (validate previous agent's work)
const preconditionResults = await this.executePreconditions(contract, handoffData);
if (!preconditionResults.passed) {
// Preconditions failed - trigger rollback
const rollbackResult = await this.executeRollback(contract, handoffData,
new Error('Precondition validation failed'));
return {
validationId,
success: false,
phase: 'preconditions',
preconditions: preconditionResults,
rollback: rollbackResult,
duration: Date.now() - startTime
};
}
// 3. If we get here, handoff can proceed
// Postconditions will be validated after the receiving agent completes work
return {
validationId,
success: true,
contract,
preconditions: preconditionResults,
duration: Date.now() - startTime
};
} catch (error) {
this.log(`Handoff validation error: ${error.message}`);
return {
validationId,
success: false,
error: error.message,
duration: Date.now() - startTime
};
} finally {
// Record validation in history
this.validationHistory.push({
validationId,
fromAgent,
toAgent,
timestamp: new Date().toISOString(),
duration: Date.now() - startTime
});
}
}
/**
* Validate agent's completed work against postconditions
*/
async validateCompletion(validationId, agentResult, contract) {
this.log(`Validating completion for ${validationId}`);
try {
const postconditionResults = await this.executePostconditions(
contract,
agentResult,
{} // handoff data context
);
if (!postconditionResults.passed) {
// Postconditions failed - trigger rollback
const rollbackResult = await this.executeRollback(contract, {},
new Error('Postcondition validation failed'));
return {
validationId,
success: false,
phase: 'postconditions',
postconditions: postconditionResults,
rollback: rollbackResult
};
}
return {
validationId,
success: true,
postconditions: postconditionResults
};
} catch (error) {
return {
validationId,
success: false,
error: error.message
};
}
}
/**
* Safe evaluation of contract code with limited scope
*/
safeEval(code, context = {}) {
// Create limited evaluation context
const safeContext = {
Date,
JSON,
console,
require: (module) => {
// Allow necessary modules for contract validation
const allowedModules = ['fs', 'fs-extra', 'path'];
// Allow local file requires relative to project root
const projectRoot = process.cwd() + path.sep;
if (module.startsWith(projectRoot)) {
return require(module);
}
if (allowedModules.includes(module)) {
return require(module);
}
throw new Error(`Module ${module} not allowed`);
},
...context
};
// Use Function constructor for safer eval
const func = new Function(...Object.keys(safeContext), `return ${code}`);
return func(...Object.values(safeContext));
}
/**
* Generate unique validation ID
*/
generateValidationId() {
return `validation_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Log validation events
*/
log(message) {
const timestamp = new Date().toISOString();
const logEntry = `[${timestamp}] TestContractValidator: ${message}\n`;
// Write to log file
fs.appendFileSync(this.options.logFile, logEntry);
// Also log to console if in debug mode
if (process.env.DEBUG === 'true') {
console.log(logEntry.trim());
}
}
/**
* Get validation statistics
*/
getValidationStats() {
const total = this.validationHistory.length;
const avgDuration = total > 0 ?
this.validationHistory.reduce((sum, v) => sum + v.duration, 0) / total : 0;
return {
totalValidations: total,
averageDuration: Math.round(avgDuration),
recentValidations: this.validationHistory.slice(-10)
};
}
}
module.exports = TestContractValidator;