diff --git a/pkg/cli/commands_compile_workflow_test.go b/pkg/cli/commands_compile_workflow_test.go new file mode 100644 index 0000000000..b9f6bd7617 --- /dev/null +++ b/pkg/cli/commands_compile_workflow_test.go @@ -0,0 +1,401 @@ +package cli + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestCompileWorkflow(t *testing.T) { + tests := []struct { + name string + setupWorkflow func(string) (string, error) + verbose bool + engineOverride string + expectError bool + errorContains string + }{ + { + name: "successful compilation with valid workflow", + setupWorkflow: func(tmpDir string) (string, error) { + workflowContent := `--- +name: Test Workflow +on: + push: + branches: [main] +permissions: + contents: read +--- + +# Test Workflow + +This is a test workflow for compilation. +` + workflowsDir := filepath.Join(tmpDir, ".github", "workflows") + err := os.MkdirAll(workflowsDir, 0755) + if err != nil { + return "", err + } + + workflowFile := filepath.Join(workflowsDir, "test.md") + err = os.WriteFile(workflowFile, []byte(workflowContent), 0644) + return workflowFile, err + }, + verbose: false, + engineOverride: "", + expectError: false, + }, + { + name: "successful compilation with verbose mode", + setupWorkflow: func(tmpDir string) (string, error) { + workflowContent := `--- +name: Verbose Test +on: + schedule: + - cron: "0 9 * * 1" +permissions: + contents: write +--- + +# Verbose Test Workflow + +Test workflow with verbose compilation. +` + workflowsDir := filepath.Join(tmpDir, ".github", "workflows") + err := os.MkdirAll(workflowsDir, 0755) + if err != nil { + return "", err + } + + workflowFile := filepath.Join(workflowsDir, "verbose-test.md") + err = os.WriteFile(workflowFile, []byte(workflowContent), 0644) + return workflowFile, err + }, + verbose: true, + engineOverride: "", + expectError: false, + }, + { + name: "compilation with engine override", + setupWorkflow: func(tmpDir string) (string, error) { + workflowContent := `--- +name: Engine Override Test +on: + push: + branches: [main] +permissions: + contents: read +--- + +# Engine Override Test + +Test compilation with specific engine. +` + workflowsDir := filepath.Join(tmpDir, ".github", "workflows") + err := os.MkdirAll(workflowsDir, 0755) + if err != nil { + return "", err + } + + workflowFile := filepath.Join(workflowsDir, "engine-test.md") + err = os.WriteFile(workflowFile, []byte(workflowContent), 0644) + return workflowFile, err + }, + verbose: false, + engineOverride: "claude", + expectError: false, + }, + { + name: "compilation with invalid workflow file", + setupWorkflow: func(tmpDir string) (string, error) { + workflowContent := `--- +invalid yaml: [unclosed +--- + +# Invalid Workflow + +This workflow has invalid frontmatter. +` + workflowsDir := filepath.Join(tmpDir, ".github", "workflows") + err := os.MkdirAll(workflowsDir, 0755) + if err != nil { + return "", err + } + + workflowFile := filepath.Join(workflowsDir, "invalid.md") + err = os.WriteFile(workflowFile, []byte(workflowContent), 0644) + return workflowFile, err + }, + verbose: false, + engineOverride: "", + expectError: true, + errorContains: "yaml", + }, + { + name: "compilation with nonexistent file", + setupWorkflow: func(tmpDir string) (string, error) { + return filepath.Join(tmpDir, "nonexistent.md"), nil + }, + verbose: false, + engineOverride: "", + expectError: true, + errorContains: "no such file", + }, + { + name: "compilation with invalid engine override", + setupWorkflow: func(tmpDir string) (string, error) { + workflowContent := `--- +name: Invalid Engine Test +on: + push: + branches: [main] +permissions: + contents: read +--- + +# Invalid Engine Test + +Test compilation with invalid engine. +` + workflowsDir := filepath.Join(tmpDir, ".github", "workflows") + err := os.MkdirAll(workflowsDir, 0755) + if err != nil { + return "", err + } + + workflowFile := filepath.Join(workflowsDir, "invalid-engine.md") + err = os.WriteFile(workflowFile, []byte(workflowContent), 0644) + return workflowFile, err + }, + verbose: false, + engineOverride: "invalid-engine", + expectError: true, + errorContains: "invalid engine", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create temporary directory + tmpDir := t.TempDir() + + // Initialize git repository in tmp directory + if err := initTestGitRepo(tmpDir); err != nil { + t.Fatalf("Failed to initialize git repo: %v", err) + } + + // Change to temporary directory + oldDir, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get current directory: %v", err) + } + defer func() { + if err := os.Chdir(oldDir); err != nil { + t.Errorf("Failed to restore directory: %v", err) + } + }() + + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("Failed to change to temp directory: %v", err) + } + + // Setup workflow file + workflowFile, err := tt.setupWorkflow(tmpDir) + if err != nil { + t.Fatalf("Failed to setup workflow: %v", err) + } + + // Test compileWorkflow function + err = compileWorkflow(workflowFile, tt.verbose, tt.engineOverride) + + if tt.expectError { + if err == nil { + t.Errorf("Expected error but got none") + } else if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) { + t.Errorf("Expected error to contain '%s', but got: %v", tt.errorContains, err) + } + } else { + if err != nil { + t.Errorf("Expected no error but got: %v", err) + } else { + // Verify lock file was created + lockFile := strings.TrimSuffix(workflowFile, ".md") + ".lock.yml" + if _, err := os.Stat(lockFile); os.IsNotExist(err) { + t.Errorf("Expected lock file %s to be created", lockFile) + } + } + } + }) + } +} + +func TestStageWorkflowChanges(t *testing.T) { + tests := []struct { + name string + setupRepo func(string) error + expectNoError bool + }{ + { + name: "successful staging in git repo with workflows", + setupRepo: func(tmpDir string) error { + // Initialize git repo + if err := initTestGitRepo(tmpDir); err != nil { + return err + } + + // Create workflows directory with test files + workflowsDir := filepath.Join(tmpDir, ".github", "workflows") + if err := os.MkdirAll(workflowsDir, 0755); err != nil { + return err + } + + testFile := filepath.Join(workflowsDir, "test.lock.yml") + return os.WriteFile(testFile, []byte("test: content"), 0644) + }, + expectNoError: true, + }, + { + name: "staging works even without workflows directory", + setupRepo: func(tmpDir string) error { + return initTestGitRepo(tmpDir) + }, + expectNoError: true, + }, + { + name: "staging in non-git directory falls back gracefully", + setupRepo: func(tmpDir string) error { + // Don't initialize git repo - should use fallback + workflowsDir := filepath.Join(tmpDir, ".github", "workflows") + if err := os.MkdirAll(workflowsDir, 0755); err != nil { + return err + } + + testFile := filepath.Join(workflowsDir, "test.lock.yml") + return os.WriteFile(testFile, []byte("test: content"), 0644) + }, + expectNoError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create temporary directory + tmpDir := t.TempDir() + + // Setup repository + if err := tt.setupRepo(tmpDir); err != nil { + t.Fatalf("Failed to setup repo: %v", err) + } + + // Change to temporary directory + oldDir, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get current directory: %v", err) + } + defer func() { + if err := os.Chdir(oldDir); err != nil { + t.Errorf("Failed to restore directory: %v", err) + } + }() + + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("Failed to change to temp directory: %v", err) + } + + // Test stageWorkflowChanges function - should not panic + func() { + defer func() { + if r := recover(); r != nil { + if tt.expectNoError { + t.Errorf("Function panicked unexpectedly: %v", r) + } + } + }() + stageWorkflowChanges() + }() + }) + } +} + +func TestStageGitAttributesIfChanged(t *testing.T) { + tests := []struct { + name string + setupRepo func(string) error + expectError bool + errorContains string + }{ + { + name: "successful staging in git repo", + setupRepo: func(tmpDir string) error { + if err := initTestGitRepo(tmpDir); err != nil { + return err + } + + // Create .gitattributes file + gitattributesPath := filepath.Join(tmpDir, ".gitattributes") + return os.WriteFile(gitattributesPath, []byte("*.lock.yml linguist-generated=true"), 0644) + }, + expectError: false, + }, + { + name: "staging without .gitattributes file", + setupRepo: func(tmpDir string) error { + return initTestGitRepo(tmpDir) + }, + expectError: true, // git add may fail on missing files in some git versions + errorContains: "exit status", + }, + { + name: "error in non-git directory", + setupRepo: func(tmpDir string) error { + // Don't initialize git repo + return nil + }, + expectError: true, + errorContains: "git", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create temporary directory + tmpDir := t.TempDir() + + // Setup repository + if err := tt.setupRepo(tmpDir); err != nil { + t.Fatalf("Failed to setup repo: %v", err) + } + + // Change to temporary directory + oldDir, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get current directory: %v", err) + } + defer func() { + if err := os.Chdir(oldDir); err != nil { + t.Errorf("Failed to restore directory: %v", err) + } + }() + + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("Failed to change to temp directory: %v", err) + } + + // Test stageGitAttributesIfChanged function + err = stageGitAttributesIfChanged() + + if tt.expectError { + if err == nil { + t.Errorf("Expected error but got none") + } else if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) { + t.Errorf("Expected error to contain '%s', but got: %v", tt.errorContains, err) + } + } else { + if err != nil { + t.Errorf("Expected no error but got: %v", err) + } + } + }) + } +} diff --git a/pkg/parser/frontmatter_mcp_test.go b/pkg/parser/frontmatter_mcp_test.go new file mode 100644 index 0000000000..4fbdc4378e --- /dev/null +++ b/pkg/parser/frontmatter_mcp_test.go @@ -0,0 +1,381 @@ +package parser + +import ( + "reflect" + "testing" +) + +func TestMergeMCPTools(t *testing.T) { + tests := []struct { + name string + existing map[string]any + new map[string]any + expected map[string]any + expectError bool + errorMsg string + }{ + { + name: "merge with empty existing map", + existing: map[string]any{}, + new: map[string]any{ + "server1": map[string]any{ + "command": []string{"python", "-m", "server1"}, + "allowed": []any{"tool1", "tool2"}, + }, + }, + expected: map[string]any{ + "server1": map[string]any{ + "command": []string{"python", "-m", "server1"}, + "allowed": []any{"tool1", "tool2"}, + }, + }, + expectError: false, + }, + { + name: "merge with empty new map", + existing: map[string]any{ + "server1": map[string]any{ + "command": []string{"python", "-m", "server1"}, + "allowed": []any{"tool1", "tool2"}, + }, + }, + new: map[string]any{}, + expected: map[string]any{ + "server1": map[string]any{ + "command": []string{"python", "-m", "server1"}, + "allowed": []any{"tool1", "tool2"}, + }, + }, + expectError: false, + }, + { + name: "merge with different servers", + existing: map[string]any{ + "server1": map[string]any{ + "command": []string{"python", "-m", "server1"}, + "allowed": []any{"tool1"}, + }, + }, + new: map[string]any{ + "server2": map[string]any{ + "command": []string{"node", "server2.js"}, + "allowed": []any{"tool3"}, + }, + }, + expected: map[string]any{ + "server1": map[string]any{ + "command": []string{"python", "-m", "server1"}, + "allowed": []any{"tool1"}, + }, + "server2": map[string]any{ + "command": []string{"node", "server2.js"}, + "allowed": []any{"tool3"}, + }, + }, + expectError: false, + }, + { + name: "merge server config with conflicts", + existing: map[string]any{ + "server1": map[string]any{ + "command": []string{"python", "-m", "server1"}, + "allowed": []any{"tool1", "tool2"}, + }, + }, + new: map[string]any{ + "server1": map[string]any{ + "allowed": []any{"tool2", "tool3"}, // This will cause a conflict + }, + }, + expected: nil, + expectError: true, + errorMsg: "conflict", + }, + { + name: "conflict detection for non-allowed properties", + existing: map[string]any{ + "server1": map[string]any{ + "command": []string{"python", "-m", "server1"}, + "timeout": 30, + }, + }, + new: map[string]any{ + "server1": map[string]any{ + "timeout": 60, // Different value - should cause conflict + }, + }, + expected: nil, + expectError: true, + errorMsg: "conflict", + }, + { + name: "merge with new server only", + existing: map[string]any{ + "server1": map[string]any{ + "command": []string{"python", "-m", "server1"}, + "allowed": []any{"tool1", "tool2"}, + "env": map[string]string{"VAR1": "value1"}, + }, + "server2": map[string]any{ + "command": []string{"node", "server2.js"}, + }, + }, + new: map[string]any{ + "server3": map[string]any{ + "command": []string{"go", "run", "server3.go"}, + "allowed": []any{"tool5"}, + }, + }, + expected: map[string]any{ + "server1": map[string]any{ + "command": []string{"python", "-m", "server1"}, + "allowed": []any{"tool1", "tool2"}, + "env": map[string]string{"VAR1": "value1"}, + }, + "server2": map[string]any{ + "command": []string{"node", "server2.js"}, + }, + "server3": map[string]any{ + "command": []string{"go", "run", "server3.go"}, + "allowed": []any{"tool5"}, + }, + }, + expectError: false, + }, + { + name: "merge with nil allowed arrays - conflicts", + existing: map[string]any{ + "server1": map[string]any{ + "command": []string{"python", "-m", "server1"}, + "allowed": nil, + }, + }, + new: map[string]any{ + "server1": map[string]any{ + "allowed": []any{"tool1"}, + }, + }, + expected: nil, + expectError: true, + errorMsg: "conflict", + }, + { + name: "merge with non-array allowed values", + existing: map[string]any{ + "server1": map[string]any{ + "command": []string{"python", "-m", "server1"}, + "allowed": "not-an-array", + }, + }, + new: map[string]any{ + "server1": map[string]any{ + "allowed": []any{"tool1"}, + }, + }, + expected: nil, + expectError: true, + errorMsg: "conflict", + }, + { + name: "merge with duplicate values causes conflict", + existing: map[string]any{ + "server1": map[string]any{ + "command": []string{"python", "-m", "server1"}, + "allowed": []any{"tool1", "tool2", "tool1"}, // Duplicate tool1 + }, + }, + new: map[string]any{ + "server1": map[string]any{ + "allowed": []any{"tool2", "tool3", "tool2"}, // Duplicate tool2 + }, + }, + expected: nil, + expectError: true, + errorMsg: "conflict", + }, + { + name: "both maps nil", + existing: nil, + new: nil, + expected: map[string]any{}, + expectError: false, + }, + { + name: "existing nil, new has content", + existing: nil, + new: map[string]any{ + "server1": map[string]any{ + "command": []string{"python", "-m", "server1"}, + }, + }, + expected: map[string]any{ + "server1": map[string]any{ + "command": []string{"python", "-m", "server1"}, + }, + }, + expectError: false, + }, + { + name: "new nil, existing has content", + existing: map[string]any{ + "server1": map[string]any{ + "command": []string{"python", "-m", "server1"}, + }, + }, + new: nil, + expected: map[string]any{ + "server1": map[string]any{ + "command": []string{"python", "-m", "server1"}, + }, + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := mergeMCPTools(tt.existing, tt.new) + + if tt.expectError { + if err == nil { + t.Errorf("Expected error but got none") + } else if tt.errorMsg != "" && !contains(err.Error(), tt.errorMsg) { + t.Errorf("Expected error to contain '%s', but got: %v", tt.errorMsg, err) + } + } else { + if err != nil { + t.Errorf("Expected no error but got: %v", err) + } + + if !mapsEqual(result, tt.expected) { + t.Errorf("Expected result %+v, got %+v", tt.expected, result) + } + } + }) + } +} + +// Test the helper function mergeAllowedArrays directly +func TestMergeAllowedArrays(t *testing.T) { + tests := []struct { + name string + existing []any + new []any + expected []string + }{ + { + name: "merge with no overlap", + existing: []any{"tool1", "tool2"}, + new: []any{"tool3", "tool4"}, + expected: []string{"tool1", "tool2", "tool3", "tool4"}, + }, + { + name: "merge with overlap", + existing: []any{"tool1", "tool2"}, + new: []any{"tool2", "tool3"}, + expected: []string{"tool1", "tool2", "tool3"}, + }, + { + name: "merge with empty existing", + existing: []any{}, + new: []any{"tool1", "tool2"}, + expected: []string{"tool1", "tool2"}, + }, + { + name: "merge with empty new", + existing: []any{"tool1", "tool2"}, + new: []any{}, + expected: []string{"tool1", "tool2"}, + }, + { + name: "merge with both empty", + existing: []any{}, + new: []any{}, + expected: []string{}, + }, + { + name: "merge with duplicates in input", + existing: []any{"tool1", "tool1", "tool2"}, + new: []any{"tool2", "tool3", "tool3"}, + expected: []string{"tool1", "tool2", "tool3"}, + }, + { + name: "merge with nil arrays", + existing: nil, + new: []any{"tool1"}, + expected: []string{"tool1"}, + }, + { + name: "merge with non-string values (should be converted)", + existing: []any{"tool1", 123, true}, + new: []any{"tool2", 456, false}, + expected: []string{"tool1", "tool2"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := mergeAllowedArrays(tt.existing, tt.new) + + if !stringSlicesEqual(result, tt.expected) { + t.Errorf("Expected %v, got %v", tt.expected, result) + } + }) + } +} + +// Helper functions for testing + +func contains(haystack, needle string) bool { + return len(haystack) >= len(needle) && + (haystack == needle || + containsSubStr(haystack, needle)) +} + +func containsSubStr(haystack, needle string) bool { + if len(needle) == 0 { + return true + } + if len(haystack) < len(needle) { + return false + } + + for i := 0; i <= len(haystack)-len(needle); i++ { + if haystack[i:i+len(needle)] == needle { + return true + } + } + return false +} + +func mapsEqual(a, b map[string]any) bool { + if len(a) != len(b) { + return false + } + + for k, v := range a { + if bv, exists := b[k]; !exists || !valuesEqual(v, bv) { + return false + } + } + + return true +} + +func valuesEqual(a, b any) bool { + return reflect.DeepEqual(a, b) +} + +func stringSlicesEqual(a, b []string) bool { + if len(a) != len(b) { + return false + } + + for i, v := range a { + if v != b[i] { + return false + } + } + + return true +} diff --git a/pkg/workflow/compiler_additional_simple_test.go b/pkg/workflow/compiler_additional_simple_test.go new file mode 100644 index 0000000000..fe25518d1c --- /dev/null +++ b/pkg/workflow/compiler_additional_simple_test.go @@ -0,0 +1,84 @@ +package workflow + +import ( + "os" + "path/filepath" + "testing" +) + +func TestCompiler_SetFileTracker_Basic(t *testing.T) { + // Create compiler + compiler := NewCompiler(false, "", "test-version") + + // Initial state should have nil tracker + if compiler.fileTracker != nil { + t.Errorf("Expected initial fileTracker to be nil") + } + + // Create mock tracker + mockTracker := &SimpleBasicMockFileTracker{} + + // Set tracker + compiler.SetFileTracker(mockTracker) + + // Verify tracker was set + if compiler.fileTracker != mockTracker { + t.Errorf("Expected tracker to be set") + } + + // Set to nil + compiler.SetFileTracker(nil) + + // Verify tracker is nil + if compiler.fileTracker != nil { + t.Errorf("Expected tracker to be nil after setting to nil") + } +} + +func TestCompiler_WriteReactionAction_Basic(t *testing.T) { + // Create compiler + compiler := NewCompiler(false, "", "test-version") + + // Create temporary directory for testing + tmpDir := t.TempDir() + + // Create a test markdown file path (doesn't need to actually exist) + markdownPath := filepath.Join(tmpDir, "test.md") + + // Set up file tracker to verify file creation + mockTracker := &SimpleBasicMockFileTracker{} + compiler.SetFileTracker(mockTracker) + + // Test that writeReactionAction succeeds + err := compiler.writeReactionAction(markdownPath) + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + // Verify that the action file was created + expectedActionFile := filepath.Join(tmpDir, ".github", "actions", "reaction", "action.yml") + if _, err := os.Stat(expectedActionFile); os.IsNotExist(err) { + t.Errorf("Expected action file to be created at: %s", expectedActionFile) + } + + // Verify that file tracker was called + if len(mockTracker.tracked) != 1 { + t.Errorf("Expected file tracker to track 1 file, got %d", len(mockTracker.tracked)) + } + + if len(mockTracker.tracked) > 0 && mockTracker.tracked[0] != expectedActionFile { + t.Errorf("Expected tracker to track %s, got %s", expectedActionFile, mockTracker.tracked[0]) + } +} + +// SimpleBasicMockFileTracker is a basic implementation for testing +type SimpleBasicMockFileTracker struct { + tracked []string +} + +func (s *SimpleBasicMockFileTracker) TrackCreated(filePath string) { + if s.tracked == nil { + s.tracked = make([]string, 0) + } + s.tracked = append(s.tracked, filePath) +} diff --git a/pkg/workflow/engine_parsing_simple_test.go b/pkg/workflow/engine_parsing_simple_test.go new file mode 100644 index 0000000000..8eab27b090 --- /dev/null +++ b/pkg/workflow/engine_parsing_simple_test.go @@ -0,0 +1,211 @@ +package workflow + +import ( + "testing" +) + +func TestClaudeEngine_ParseLogMetrics_Basic(t *testing.T) { + engine := NewClaudeEngine() + + tests := []struct { + name string + logContent string + verbose bool + expectNoCrash bool + }{ + { + name: "empty log content", + logContent: "", + verbose: false, + expectNoCrash: true, + }, + { + name: "whitespace only", + logContent: " \n\t \n ", + verbose: false, + expectNoCrash: true, + }, + { + name: "simple log with errors", + logContent: `Starting process... +Error: Something went wrong +Warning: Deprecated feature +Process completed`, + verbose: false, + expectNoCrash: true, + }, + { + name: "verbose mode", + logContent: `Debug: Starting +Processing... +Debug: Completed`, + verbose: true, + expectNoCrash: true, + }, + { + name: "multiline content", + logContent: `Line 1 +Line 2 +Line 3 +Line 4 +Line 5`, + verbose: false, + expectNoCrash: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // The main test is that this doesn't crash + func() { + defer func() { + if r := recover(); r != nil { + if tt.expectNoCrash { + t.Errorf("ParseLogMetrics crashed unexpectedly: %v", r) + } + } + }() + + metrics := engine.ParseLogMetrics(tt.logContent, tt.verbose) + + // Basic validation - should return valid struct + if metrics.ErrorCount < 0 { + t.Errorf("ErrorCount should not be negative, got %d", metrics.ErrorCount) + } + if metrics.WarningCount < 0 { + t.Errorf("WarningCount should not be negative, got %d", metrics.WarningCount) + } + if metrics.TokenUsage < 0 { + t.Errorf("TokenUsage should not be negative, got %d", metrics.TokenUsage) + } + if metrics.EstimatedCost < 0 { + t.Errorf("EstimatedCost should not be negative, got %f", metrics.EstimatedCost) + } + }() + }) + } +} + +func TestCodexEngine_ParseLogMetrics_Basic(t *testing.T) { + engine := NewCodexEngine() + + tests := []struct { + name string + logContent string + verbose bool + expectNoCrash bool + }{ + { + name: "empty log content", + logContent: "", + verbose: false, + expectNoCrash: true, + }, + { + name: "whitespace only", + logContent: " \n\t \n ", + verbose: false, + expectNoCrash: true, + }, + { + name: "simple log with errors", + logContent: `Starting process... +Error: Something went wrong +Warning: Deprecated feature +Process completed`, + verbose: false, + expectNoCrash: true, + }, + { + name: "verbose mode", + logContent: `Debug: Starting +Processing... +Debug: Completed`, + verbose: true, + expectNoCrash: true, + }, + { + name: "multiline content", + logContent: `Line 1 +Line 2 +Line 3 +Line 4 +Line 5`, + verbose: false, + expectNoCrash: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // The main test is that this doesn't crash + func() { + defer func() { + if r := recover(); r != nil { + if tt.expectNoCrash { + t.Errorf("ParseLogMetrics crashed unexpectedly: %v", r) + } + } + }() + + metrics := engine.ParseLogMetrics(tt.logContent, tt.verbose) + + // Basic validation - should return valid struct + if metrics.ErrorCount < 0 { + t.Errorf("ErrorCount should not be negative, got %d", metrics.ErrorCount) + } + if metrics.WarningCount < 0 { + t.Errorf("WarningCount should not be negative, got %d", metrics.WarningCount) + } + if metrics.TokenUsage < 0 { + t.Errorf("TokenUsage should not be negative, got %d", metrics.TokenUsage) + } + // Codex engine doesn't track cost, so it should be 0 + if metrics.EstimatedCost != 0 { + t.Errorf("Codex engine should have 0 cost, got %f", metrics.EstimatedCost) + } + }() + }) + } +} + +func TestCompiler_SetFileTracker_Simple(t *testing.T) { + // Create compiler + compiler := NewCompiler(false, "", "test-version") + + // Initial state should have nil tracker + if compiler.fileTracker != nil { + t.Errorf("Expected initial fileTracker to be nil") + } + + // Create mock tracker + mockTracker := &SimpleMockFileTracker{} + + // Set tracker + compiler.SetFileTracker(mockTracker) + + // Verify tracker was set + if compiler.fileTracker != mockTracker { + t.Errorf("Expected tracker to be set") + } + + // Set to nil + compiler.SetFileTracker(nil) + + // Verify tracker is nil + if compiler.fileTracker != nil { + t.Errorf("Expected tracker to be nil after setting to nil") + } +} + +// SimpleMockFileTracker is a basic implementation for testing +type SimpleMockFileTracker struct { + tracked []string +} + +func (s *SimpleMockFileTracker) TrackCreated(filePath string) { + if s.tracked == nil { + s.tracked = make([]string, 0) + } + s.tracked = append(s.tracked, filePath) +}