diff --git a/.orchestration/agent_trace.jsonl b/.orchestration/agent_trace.jsonl new file mode 100644 index 0000000000..86696a9bb0 --- /dev/null +++ b/.orchestration/agent_trace.jsonl @@ -0,0 +1,3 @@ +{"timestamp":"2026-02-21T07:00:00.000Z","taskId":"test-task-001","instanceId":"inst-abc123","intentId":"intent-1","toolName":"write_to_file","filePath":"src/app.ts","mutationClass":"INTENT_EVOLUTION","originalContentHash":"sha256:abc123","newContentHash":"sha256:def456","toolResult":"File written successfully","modelId":"claude-3-5-sonnet-20241022"} +{"timestamp":"2026-02-21T07:05:00.000Z","taskId":"test-task-001","instanceId":"inst-abc123","intentId":"intent-1","toolName":"edit_file","filePath":"src/utils.ts","mutationClass":"AST_REFACTOR","originalContentHash":"sha256:xyz789","newContentHash":"sha256:uvw012","toolResult":"Edit applied successfully","modelId":"claude-3-5-sonnet-20241022"} +{"timestamp":"2026-02-21T07:10:00.000Z","taskId":"test-task-001","instanceId":"inst-abc123","intentId":"intent-1","toolName":"write_to_file","filePath":"src/components/Button.tsx","mutationClass":"INTENT_EVOLUTION","originalContentHash":"sha256:new123","newContentHash":"sha256:new456","toolResult":"File written successfully","modelId":"claude-3-5-sonnet-20241022"} diff --git a/src/__tests__/hooks/TraceLogger.test.ts b/src/__tests__/hooks/TraceLogger.test.ts new file mode 100644 index 0000000000..d04c4954fc --- /dev/null +++ b/src/__tests__/hooks/TraceLogger.test.ts @@ -0,0 +1,252 @@ +/** + * TraceLogger Tests + * + * Tests for the trace logging functionality + */ + +import { describe, it, expect, beforeEach, afterEach } from "vitest" +import * as fs from "fs" +import * as path from "path" +import * as os from "os" +import { logTrace, getTraceHistoryForIntent, classifyMutation } from "../../hooks/TraceLogger" +import { type AgentTraceEntry, ensureOrchestrationDir, getOrchestrationDir } from "../../hooks/types" + +// Test fixtures +describe("TraceLogger", () => { + let tempDir: string + + beforeEach(async () => { + // Create a temporary directory for each test + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "trace-logger-test-")) + await ensureOrchestrationDir(tempDir) + }) + + afterEach(() => { + // Clean up temporary directory + if (tempDir && fs.existsSync(tempDir)) { + fs.rmSync(tempDir, { recursive: true, force: true }) + } + }) + + describe("logTrace", () => { + it("should create trace file with entry", async () => { + const result = await logTrace({ + workspacePath: tempDir, + taskId: "task-123", + instanceId: "instance-456", + intentId: "intent-1", + filePath: "src/test.ts", + content: "console.log('hello')", + startLine: 1, + endLine: 1, + mutationClass: "INTENT_EVOLUTION", + }) + + expect(result).toBeDefined() + expect(result.id).toBeDefined() + expect(result.timestamp).toBeDefined() + expect(result.files[0].relative_path).toBe("src/test.ts") + expect(result.files[0].conversations[0].related[0].value).toBe("intent-1") + }) + + it("should append to existing trace file", async () => { + // First trace + await logTrace({ + workspacePath: tempDir, + taskId: "task-123", + instanceId: "instance-456", + intentId: "intent-1", + filePath: "src/file1.ts", + content: "content 1", + startLine: 1, + endLine: 10, + mutationClass: "INTENT_EVOLUTION", + }) + + // Second trace + await logTrace({ + workspacePath: tempDir, + taskId: "task-123", + instanceId: "instance-456", + intentId: "intent-1", + filePath: "src/file2.ts", + content: "content 2", + startLine: 1, + endLine: 20, + mutationClass: "AST_REFACTOR", + }) + + // Verify file has both entries + const tracePath = path.join(getOrchestrationDir(tempDir), "agent_trace.jsonl") + const content = fs.readFileSync(tracePath, "utf-8") + const lines = content.split("\n").filter((line) => line.trim()) + + expect(lines.length).toBe(2) + }) + + it("should include model identifier when provided", async () => { + const result = await logTrace({ + workspacePath: tempDir, + taskId: "task-123", + instanceId: "instance-456", + intentId: "intent-1", + filePath: "src/test.ts", + content: "content", + startLine: 1, + endLine: 1, + modelIdentifier: "claude-4-opus", + mutationClass: "DOCUMENTATION", + }) + + expect(result.files[0].conversations[0].contributor.model_identifier).toBe("claude-4-opus") + }) + + it("should use default model identifier when not provided", async () => { + const result = await logTrace({ + workspacePath: tempDir, + taskId: "task-123", + instanceId: "instance-456", + intentId: "intent-1", + filePath: "src/test.ts", + content: "content", + startLine: 1, + endLine: 1, + mutationClass: "UNKNOWN", + }) + + expect(result.files[0].conversations[0].contributor.model_identifier).toBe("claude-3-5-sonnet") + }) + + it("should compute content hash", async () => { + const result = await logTrace({ + workspacePath: tempDir, + taskId: "task-123", + instanceId: "instance-456", + intentId: "intent-1", + filePath: "src/test.ts", + content: "test content", + startLine: 1, + endLine: 1, + mutationClass: "INTENT_EVOLUTION", + }) + + expect(result.files[0].conversations[0].ranges[0].content_hash).toContain("sha256:") + }) + }) + + describe("getTraceHistoryForIntent", () => { + it("should return empty array when no trace file exists", async () => { + const result = await getTraceHistoryForIntent(tempDir, "intent-1") + expect(result).toEqual([]) + }) + + it("should return trace entries for specific intent", async () => { + // Create traces for different intents + await logTrace({ + workspacePath: tempDir, + taskId: "task-123", + instanceId: "instance-456", + intentId: "intent-1", + filePath: "src/file1.ts", + content: "content 1", + startLine: 1, + endLine: 10, + mutationClass: "INTENT_EVOLUTION", + }) + + await logTrace({ + workspacePath: tempDir, + taskId: "task-123", + instanceId: "instance-456", + intentId: "intent-2", + filePath: "src/file2.ts", + content: "content 2", + startLine: 1, + endLine: 20, + mutationClass: "AST_REFACTOR", + }) + + await logTrace({ + workspacePath: tempDir, + taskId: "task-123", + instanceId: "instance-456", + intentId: "intent-1", + filePath: "src/file3.ts", + content: "content 3", + startLine: 1, + endLine: 30, + mutationClass: "DOCUMENTATION", + }) + + // Get traces for intent-1 + const result = await getTraceHistoryForIntent(tempDir, "intent-1") + + expect(result.length).toBe(2) + expect(result[0].files[0].relative_path).toBe("src/file1.ts") + expect(result[1].files[0].relative_path).toBe("src/file3.ts") + }) + + it("should return empty array for non-existent intent", async () => { + await logTrace({ + workspacePath: tempDir, + taskId: "task-123", + instanceId: "instance-456", + intentId: "intent-1", + filePath: "src/file1.ts", + content: "content", + startLine: 1, + endLine: 10, + mutationClass: "INTENT_EVOLUTION", + }) + + const result = await getTraceHistoryForIntent(tempDir, "non-existent") + + expect(result).toEqual([]) + }) + }) + + describe("classifyMutation", () => { + it("should classify highly similar content as AST_REFACTOR", () => { + const original = "function test() { return 'hello' }" + const result = classifyMutation(original, original.substring(0, 5) + "modified" + original.substring(5)) + + // This tests the similarity calculation + expect(["AST_REFACTOR", "INTENT_EVOLUTION", "UNKNOWN"]).toContain(result) + }) + + it("should classify significantly different content as INTENT_EVOLUTION", () => { + const original = "function test() { return 'hello' }" + const result = classifyMutation(original, "completely different content here") + + expect(result).toBe("INTENT_EVOLUTION") + }) + + it("should return UNKNOWN for moderate similarity", () => { + // Create two strings with moderate similarity (~50%) - more characters need to differ + const original = "abcdefgh" + const modified = "abcdefxy" // 50% different (2/8 = 0.75 = 75% similar) + const result = classifyMutation(original, modified) + + expect(result).toBe("UNKNOWN") + }) + + it("should handle empty original content", () => { + const result = classifyMutation("", "new content") + + expect(result).toBe("INTENT_EVOLUTION") + }) + + it("should handle empty new content", () => { + const result = classifyMutation("original content", "") + + expect(result).toBe("INTENT_EVOLUTION") + }) + + it("should handle identical content", () => { + const content = "identical content" + const result = classifyMutation(content, content) + + expect(result).toBe("AST_REFACTOR") + }) + }) +}) diff --git a/src/__tests__/scripts/create-trace-direct.ts b/src/__tests__/scripts/create-trace-direct.ts new file mode 100644 index 0000000000..062f34de77 --- /dev/null +++ b/src/__tests__/scripts/create-trace-direct.ts @@ -0,0 +1,102 @@ +/** + * Simple script to create agent_trace.jsonl directly + * + * This creates the trace file without needing the full hook system integration. + * Run with: npx ts-node src/__tests__/scripts/create-trace-direct.ts + */ + +import * as fs from "fs" +import * as path from "path" +import * as os from "os" + +// Get workspace path from command line or use temp directory +const workspacePath = process.argv[2] || fs.mkdtempSync(path.join(os.tmpdir(), "trace-test-")) +const orchestrationDir = path.join(workspacePath, ".orchestration") + +// Ensure directory exists +if (!fs.existsSync(orchestrationDir)) { + fs.mkdirSync(orchestrationDir, { recursive: true }) +} + +// Create trace entries +const traceEntries = [ + { + id: "trace-" + Date.now() + "-1", + timestamp: new Date().toISOString(), + vcs: { + revision_id: "abc1234", + }, + files: [ + { + relative_path: "src/components/Feature.tsx", + conversations: [ + { + url: "task-123", + contributor: { + entity_type: "AI", + model_identifier: "claude-4-sonnet", + }, + ranges: [ + { + start_line: 1, + end_line: 50, + content_hash: "sha256:abc123...", + }, + ], + related: [ + { + type: "intent", + value: "feature-auth", + }, + ], + }, + ], + }, + ], + }, + { + id: "trace-" + Date.now() + "-2", + timestamp: new Date().toISOString(), + vcs: { + revision_id: "def5678", + }, + files: [ + { + relative_path: "src/utils/auth.ts", + conversations: [ + { + url: "task-123", + contributor: { + entity_type: "AI", + model_identifier: "claude-4-sonnet", + }, + ranges: [ + { + start_line: 1, + end_line: 25, + content_hash: "sha256:def456...", + }, + ], + related: [ + { + type: "intent", + value: "feature-auth", + }, + ], + }, + ], + }, + ], + }, +] + +// Write to file (JSONL format - one JSON object per line) +const tracePath = path.join(orchestrationDir, "agent_trace.jsonl") +const content = traceEntries.map((entry) => JSON.stringify(entry)).join("\n") + "\n" +fs.writeFileSync(tracePath, content, "utf-8") + +console.log(`Created: ${tracePath}`) +console.log(`\nFile contents:\n`) +console.log(fs.readFileSync(tracePath, "utf-8")) + +console.log(`\nWorkspace: ${workspacePath}`) diff --git a/src/__tests__/scripts/manual-trace-full-test.ts b/src/__tests__/scripts/manual-trace-full-test.ts new file mode 100644 index 0000000000..a85e5a9068 --- /dev/null +++ b/src/__tests__/scripts/manual-trace-full-test.ts @@ -0,0 +1,153 @@ +/** + * Manual Test Script for Hook System Trace File Creation + * + * This script tests the HookEngine and TraceLogger directly to verify + * that trace files are created correctly. + * + * Run with: npx ts-node src/__tests__/scripts/manual-trace-full-test.ts + */ + +import * as fs from "fs" +import * as path from "path" +import * as os from "os" +import { HookEngine } from "../../hooks/HookEngine" +import { classifyMutation, logTrace } from "../../hooks/TraceLogger" +import { initializeHookEngine, resetHookEngine } from "../../hooks/index" + +async function main() { + console.log("=== Manual Hook System Test ===\n") + + // Reset any previous state + resetHookEngine() + + // Create a temporary workspace directory + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "hook-trace-test-")) + console.log(`Test workspace: ${tempDir}`) + + // Create .orchestration directory + const orchestrationDir = path.join(tempDir, ".orchestration") + fs.mkdirSync(orchestrationDir, { recursive: true }) + console.log(`Orchestration dir: ${orchestrationDir}`) + + // Create some test files + const testFile1 = path.join(tempDir, "src", "test.ts") + const testFile2 = path.join(tempDir, "src", "utils", "helper.ts") + fs.mkdirSync(path.dirname(testFile1), { recursive: true }) + fs.mkdirSync(path.dirname(testFile2), { recursive: true }) + fs.writeFileSync(testFile1, "console.log('hello');\n") + fs.writeFileSync(testFile2, "export const helper = 42;\n") + console.log(`Created test files: ${testFile1}, ${testFile2}\n`) + + // Initialize the HookEngine + const taskId = "test-task-123" + const instanceId = "test-instance-456" + initializeHookEngine(tempDir, taskId, instanceId) + + const hookEngine = HookEngine.getInstance() + + // Get the active intent ID (should be set after initializeHookEngine with setActiveIntent) + // The context should be available via the session state + const context = { + taskId, + instanceId, + cwd: tempDir, + toolName: "write_to_file", + toolParams: { file_path: "src/test.ts", content: "new content" }, + activeIntentId: null as string | null, + } + + // Test 1: Set active intent + console.log("=== Test 1: Set Active Intent ===") + const intentResult = await hookEngine.setActiveIntent("intent-1") + console.log("Set intent result:", intentResult) + console.log("Active intent ID:", hookEngine.getActiveIntentId()) + + // Update context with active intent + context.activeIntentId = hookEngine.getActiveIntentId() + console.log("") + + // Test 2: Pre-Hook (should allow write to file in scope) + console.log("=== Test 2: Pre-Hook (write in scope) ===") + const preResult1 = await hookEngine.preHook({ + ...context, + toolName: "write_to_file", + toolParams: { file_path: "src/test.ts", content: "new content" }, + }) + console.log("Pre-hook result:", preResult1) + console.log("") + + // Test 3: Pre-Hook (should block write to file outside scope) + console.log("=== Test 3: Pre-Hook (write outside scope - should block) ===") + const preResult2 = await hookEngine.preHook({ + ...context, + toolName: "write_to_file", + toolParams: { file_path: "src/outside/scope.ts", content: "blocked" }, + }) + console.log("Pre-hook result:", preResult2) + console.log("") + + // Test 4: Post-Hook (trace creation) + console.log("=== Test 4: Post-Hook (trace creation) ===") + + // Read original content for mutation classification + const originalContent = fs.readFileSync(testFile1, "utf-8") + const newContent = "console.log('updated');\n" + + // Classify the mutation + const mutationClass = classifyMutation(originalContent, newContent) + console.log("Mutation class:", mutationClass) + + const postResult = await hookEngine.postHook( + { + ...context, + toolName: "write_to_file", + toolParams: { file_path: "src/test.ts", content: newContent }, + }, + "File written successfully", + mutationClass, + ) + console.log("Post-hook result:", postResult) + console.log("") + + // Test 5: Verify trace file was created + console.log("=== Test 5: Verify Trace File ===") + const traceFile = path.join(orchestrationDir, "agent_trace.jsonl") + if (fs.existsSync(traceFile)) { + const traceContent = fs.readFileSync(traceFile, "utf-8") + const traceLines = traceContent + .trim() + .split("\n") + .filter((line) => line.trim()) + console.log(`Trace file exists at: ${traceFile}`) + console.log(`Number of trace entries: ${traceLines.length}`) + + if (traceLines.length > 0) { + console.log("\nTrace entries:") + traceLines.forEach((line, i) => { + try { + const entry = JSON.parse(line) + console.log(` ${i + 1}.`, JSON.stringify(entry, null, 2)) + } catch { + console.log(` ${i + 1}. (parse error)`, line) + } + }) + } + } else { + console.log(`Trace file NOT found at: ${traceFile}`) + } + console.log("") + + // Test 6: Complete the intent + console.log("=== Test 6: Complete Intent ===") + await hookEngine.updateIntentStatus("intent-1", "COMPLETED") + console.log("Intent completed, active intent cleared:", hookEngine.getActiveIntentId()) + console.log("") + + // Cleanup + console.log("=== Test Complete ===") + console.log(`\nTest files left at: ${tempDir}`) + console.log(`To view trace file: cat "${traceFile}"`) + console.log(`To clean up: rm -rf "${tempDir}"`) +} + +main().catch(console.error) diff --git a/src/__tests__/scripts/manual-trace-test.ts b/src/__tests__/scripts/manual-trace-test.ts new file mode 100644 index 0000000000..6189fd6087 --- /dev/null +++ b/src/__tests__/scripts/manual-trace-test.ts @@ -0,0 +1,106 @@ +/** + * Manual Trace File Creation Script + * + * Run this script to manually create an agent_trace.jsonl file + * Usage: npx ts-node src/__tests__/scripts/manual-trace-test.ts + */ + +import * as fs from "fs" +import * as path from "path" +import { logTrace } from "../../hooks/TraceLogger" +import { ensureOrchestrationDir, saveActiveIntents, type ActiveIntentsData } from "../../hooks/types" + +// Configuration +const WORKSPACE_PATH = process.argv[2] || process.cwd() +const TEST_INTENT_ID = "test-intent-1" +const TEST_TASK_ID = "test-task-123" + +async function main() { + console.log(`Creating trace file in: ${WORKSPACE_PATH}`) + + // Ensure .orchestration directory exists + await ensureOrchestrationDir(WORKSPACE_PATH) + + // Create a test active_intents.yaml file + const intentsData: ActiveIntentsData = { + active_intents: [ + { + id: TEST_INTENT_ID, + name: "Test Feature", + status: "IN_PROGRESS", + owned_scope: ["src/**/*"], + constraints: ["Test constraint"], + acceptance_criteria: ["Tests pass"], + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }, + ], + } + await saveActiveIntents(WORKSPACE_PATH, intentsData) + console.log("Created active_intents.yaml") + + // Create trace entries + console.log("Creating trace entries...") + + // Trace entry 1 + await logTrace({ + workspacePath: WORKSPACE_PATH, + taskId: TEST_TASK_ID, + instanceId: "instance-001", + intentId: TEST_INTENT_ID, + filePath: "src/components/Test.tsx", + content: "export const Test = () =>