@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
331 lines (286 loc) • 9.7 kB
text/typescript
/**
* 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';
import type {
AutomationConfig,
TestScenario,
TestScenarioResult,
AutomationSessionResult,
ContainerStatus,
} from '../types/index.js';
export class DockerCopilotAutomation {
private container: VSCodeContainer;
private captureParser: RealTimeCaptureParser;
private config: AutomationConfig;
private sessionId: string;
constructor(config: AutomationConfig) {
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: TestScenario[]): Promise<AutomationSessionResult> {
const startTime = new Date();
let containerInfo: ContainerStatus;
const results: TestScenarioResult[] = [];
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: TestScenario): Promise<TestScenarioResult> {
const startTime = new Date();
const interactions: TestScenarioResult['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
*/
private async waitForContainerReady(): Promise<void> {
// 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
*/
private async createTestFile(scenario: TestScenario): Promise<void> {
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
*/
private async executeScenarioSteps(
scenario: TestScenario,
interactions: TestScenarioResult['interactions'],
): Promise<void> {
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)
*/
private async simulateTyping(filePath: string, text: string): Promise<void> {
// 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
*/
private async getGeneratedCode(scenario: TestScenario): Promise<string> {
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
*/
private calculateMetrics(
interactions: TestScenarioResult['interactions'],
): TestScenarioResult['metrics'] {
const suggestions = interactions.filter((i) => i.suggestion);
const accepted = suggestions.filter((i) => i.suggestion?.accepted);
const responseTimes = interactions
.map((i) => i.metadata?.responseTime as number)
.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
*/
private getFileExtension(language: string): string {
const extensions: Record<string, string> = {
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(): Promise<void> {
try {
await this.container.stop();
} catch (error) {
console.error('Cleanup error:', error);
}
}
}