@codervisor/devlog-ai
Version:
AI Chat History Extractor & Docker-based Automation - TypeScript implementation for GitHub Copilot and other AI coding assistants with automated testing capabilities
280 lines (279 loc) • 10.4 kB
JavaScript
/**
* Docker-based GitHub Copilot Automation
*
* Main orchestrator for automated Copilot testing using containerized VS Code
*/
import { VSCodeContainer } from './vscode-container.js';
import { RealTimeCaptureParser } from '../capture/real-time-parser.js';
export class DockerCopilotAutomation {
container;
captureParser;
config;
sessionId;
constructor(config) {
this.config = config;
this.container = new VSCodeContainer(config);
this.captureParser = new RealTimeCaptureParser();
this.sessionId = `automation-${Date.now()}`;
}
/**
* Run a complete automation session with multiple test scenarios
*/
async runSession(scenarios) {
const startTime = new Date();
let containerInfo;
const results = [];
try {
// Start the container
if (this.config.debug) {
console.log('Starting automation session...');
}
containerInfo = await this.container.start();
// Wait for container to be fully ready
await this.waitForContainerReady();
// Run each test scenario
for (const scenario of scenarios) {
if (this.config.debug) {
console.log(`Running scenario: ${scenario.name}`);
}
try {
const result = await this.runScenario(scenario);
results.push(result);
}
catch (error) {
// Create failed result
results.push({
scenarioId: scenario.id,
startTime: new Date(),
endTime: new Date(),
success: false,
interactions: [],
generatedCode: '',
metrics: {
totalSuggestions: 0,
acceptedSuggestions: 0,
rejectedSuggestions: 0,
averageResponseTime: 0,
},
error: error instanceof Error ? error.message : String(error),
});
}
}
}
finally {
// Always clean up the container
try {
await this.container.stop();
containerInfo = this.container.getStatus();
}
catch (error) {
console.error('Error stopping container:', error);
containerInfo = { id: '', status: 'error', error: String(error) };
}
}
const endTime = new Date();
const successful = results.filter((r) => r.success).length;
const totalInteractions = results.reduce((sum, r) => sum + r.interactions.length, 0);
return {
sessionId: this.sessionId,
startTime,
endTime,
scenarios: results,
containerInfo,
summary: {
totalScenarios: scenarios.length,
successfulScenarios: successful,
failedScenarios: scenarios.length - successful,
totalInteractions,
overallSuccessRate: scenarios.length > 0 ? successful / scenarios.length : 0,
},
};
}
/**
* Run a single test scenario
*/
async runScenario(scenario) {
const startTime = new Date();
const interactions = [];
try {
// Create test file in container
await this.createTestFile(scenario);
// Start capture parser
this.captureParser.startCapture();
// Execute the test scenario
await this.executeScenarioSteps(scenario, interactions);
// Stop capture and get interactions
const capturedInteractions = await this.captureParser.stopCapture();
interactions.push(...capturedInteractions);
// Get the generated code
const generatedCode = await this.getGeneratedCode(scenario);
// Calculate metrics
const metrics = this.calculateMetrics(interactions);
return {
scenarioId: scenario.id,
startTime,
endTime: new Date(),
success: true,
interactions,
generatedCode,
metrics,
};
}
catch (error) {
return {
scenarioId: scenario.id,
startTime,
endTime: new Date(),
success: false,
interactions,
generatedCode: '',
metrics: {
totalSuggestions: 0,
acceptedSuggestions: 0,
rejectedSuggestions: 0,
averageResponseTime: 0,
},
error: error instanceof Error ? error.message : String(error),
};
}
}
/**
* Wait for the container to be fully ready for automation
*/
async waitForContainerReady() {
// Wait for VS Code extensions to be fully loaded
await new Promise((resolve) => setTimeout(resolve, 10000));
// Verify Copilot extension is active
try {
const checkCommand = ['code-insiders', '--list-extensions', '--show-versions'];
const output = await this.container.executeInContainer(checkCommand);
if (!output.includes('GitHub.copilot')) {
throw new Error('GitHub Copilot extension not found');
}
if (this.config.debug) {
console.log('Container is ready for automation');
}
}
catch (error) {
throw new Error(`Container readiness check failed: ${error}`);
}
}
/**
* Create test file for scenario in container
*/
async createTestFile(scenario) {
const fileName = `test-${scenario.id}.${this.getFileExtension(scenario.language)}`;
const filePath = `/workspace/automation-test/src/${fileName}`;
// Create the file with initial code
const createFileCommand = [
'sh',
'-c',
`echo '${scenario.initialCode.replace(/'/g, "'\\''")}' > ${filePath}`,
];
await this.container.executeInContainer(createFileCommand);
if (this.config.debug) {
console.log(`Created test file: ${filePath}`);
}
}
/**
* Execute the steps for a test scenario
*/
async executeScenarioSteps(scenario, interactions) {
const fileName = `test-${scenario.id}.${this.getFileExtension(scenario.language)}`;
const filePath = `/workspace/automation-test/src/${fileName}`;
// Open file in VS Code
const openCommand = ['code-insiders', filePath, '--wait', '--new-window'];
// This would need to use VS Code API or automation tools
// For now, we'll simulate the process
for (const prompt of scenario.expectedPrompts) {
// Simulate typing the prompt
await this.simulateTyping(filePath, prompt);
// Wait for Copilot suggestion
await new Promise((resolve) => setTimeout(resolve, 2000));
// Capture interaction (this would be done by real-time parser)
interactions.push({
timestamp: new Date(),
trigger: 'keystroke',
context: {
fileName,
fileContent: scenario.initialCode + prompt,
cursorPosition: { line: 0, character: prompt.length },
precedingText: scenario.initialCode,
followingText: '',
},
suggestion: {
text: `// Generated suggestion for: ${prompt}`,
confidence: 0.8,
accepted: true,
},
});
}
}
/**
* Simulate typing in VS Code (placeholder implementation)
*/
async simulateTyping(filePath, text) {
// This would need actual VS Code automation
// For now, append to file as simulation
const appendCommand = ['sh', '-c', `echo '${text.replace(/'/g, "'\\''")}' >> ${filePath}`];
await this.container.executeInContainer(appendCommand);
}
/**
* Get the final generated code from the test file
*/
async getGeneratedCode(scenario) {
const fileName = `test-${scenario.id}.${this.getFileExtension(scenario.language)}`;
const filePath = `/workspace/automation-test/src/${fileName}`;
const readCommand = ['cat', filePath];
return await this.container.executeInContainer(readCommand);
}
/**
* Calculate metrics from interactions
*/
calculateMetrics(interactions) {
const suggestions = interactions.filter((i) => i.suggestion);
const accepted = suggestions.filter((i) => i.suggestion?.accepted);
const responseTimes = interactions
.map((i) => i.metadata?.responseTime)
.filter((t) => typeof t === 'number');
const averageResponseTime = responseTimes.length > 0
? responseTimes.reduce((sum, time) => sum + time, 0) / responseTimes.length
: 0;
return {
totalSuggestions: suggestions.length,
acceptedSuggestions: accepted.length,
rejectedSuggestions: suggestions.length - accepted.length,
averageResponseTime,
};
}
/**
* Get file extension for language
*/
getFileExtension(language) {
const extensions = {
javascript: 'js',
typescript: 'ts',
python: 'py',
java: 'java',
csharp: 'cs',
cpp: 'cpp',
c: 'c',
go: 'go',
rust: 'rs',
php: 'php',
ruby: 'rb',
};
return extensions[language.toLowerCase()] || 'txt';
}
/**
* Clean up resources
*/
async cleanup() {
try {
await this.container.stop();
}
catch (error) {
console.error('Cleanup error:', error);
}
}
}