Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions internal/ai/analysis_refiner.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ import (
// The refiner uses the AI supervisor to iteratively improve the analysis,
// incorporating feedback from previous iterations to find missed work.
type AnalysisRefiner struct {
supervisor *Supervisor
issue *types.Issue
supervisor *Supervisor
issue *types.Issue
agentOutput string
success bool
success bool

// minConfidence is the minimum confidence for convergence (0.0-1.0)
minConfidence float64
Expand Down
18 changes: 9 additions & 9 deletions internal/ai/analysis_refiner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import (
func TestNewAnalysisRefiner(t *testing.T) {
// Create test issue
issue := &types.Issue{
ID: "test-1",
Title: "Test Issue",
Description: "Test description",
ID: "test-1",
Title: "Test Issue",
Description: "Test description",
AcceptanceCriteria: "1. Should work\n2. Should be tested",
}

Expand Down Expand Up @@ -81,9 +81,9 @@ func TestNewAnalysisRefiner(t *testing.T) {
// TestSerializeAnalysis tests analysis serialization
func TestSerializeAnalysis(t *testing.T) {
analysis := &Analysis{
Completed: true,
Confidence: 0.95,
Summary: "Test summary",
Completed: true,
Confidence: 0.95,
Summary: "Test summary",
PuntedItems: []string{"Item 1", "Item 2"},
DiscoveredIssues: []DiscoveredIssue{
{
Expand Down Expand Up @@ -363,9 +363,9 @@ func TestDeserializeAnalysis(t *testing.T) {
// TestSerializeAnalysisEdgeCases tests serialization edge cases
func TestSerializeAnalysisEdgeCases(t *testing.T) {
tests := []struct {
name string
analysis *Analysis
expected []string
name string
analysis *Analysis
expected []string
notExpected []string
}{
{
Expand Down
28 changes: 14 additions & 14 deletions internal/ai/assessment.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,18 @@ import (

// Assessment represents an AI assessment of an issue before execution
type Assessment struct {
Strategy string `json:"strategy"` // High-level strategy for completing the issue
Steps []string `json:"steps"` // Specific steps to take
Risks []string `json:"risks"` // Potential risks or challenges
Confidence float64 `json:"confidence"` // Confidence score (0.0-1.0)
Reasoning string `json:"reasoning"` // Detailed reasoning
ShouldDecompose bool `json:"should_decompose"` // Whether this issue should be split into child issues (vc-rzqe)
Strategy string `json:"strategy"` // High-level strategy for completing the issue
Steps []string `json:"steps"` // Specific steps to take
Risks []string `json:"risks"` // Potential risks or challenges
Confidence float64 `json:"confidence"` // Confidence score (0.0-1.0)
Reasoning string `json:"reasoning"` // Detailed reasoning
ShouldDecompose bool `json:"should_decompose"` // Whether this issue should be split into child issues (vc-rzqe)
DecompositionPlan *DecompositionPlan `json:"decomposition_plan,omitempty"` // Plan for decomposing into child issues (vc-rzqe)
}

// DecompositionPlan describes how to break an issue into child issues (vc-rzqe)
type DecompositionPlan struct {
Reasoning string `json:"reasoning"` // Why decomposition is recommended
Reasoning string `json:"reasoning"` // Why decomposition is recommended
ChildIssues []ChildIssue `json:"child_issues"` // Proposed child issues
}

Expand Down Expand Up @@ -137,14 +137,14 @@ func (s *Supervisor) AssessIssueStateWithRefinement(ctx context.Context, issue *
// Record metrics for skipped iteration (vc-642z)
if collector != nil {
metrics := &iterative.ArtifactMetrics{
ArtifactType: "assessment",
Priority: fmt.Sprintf("P%d", issue.Priority),
TotalIterations: 0,
Converged: true,
ArtifactType: "assessment",
Priority: fmt.Sprintf("P%d", issue.Priority),
TotalIterations: 0,
Converged: true,
ConvergenceReason: "selectivity skip",
TotalDuration: time.Since(startTime),
IterationSkipped: true,
SkipReason: skipReason,
TotalDuration: time.Since(startTime),
IterationSkipped: true,
SkipReason: skipReason,
}
collector.RecordArtifactComplete(&iterative.ConvergenceResult{
Iterations: 0,
Expand Down
72 changes: 36 additions & 36 deletions internal/ai/assessment_refiner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@ func TestNewAssessmentRefiner(t *testing.T) {

func TestSerializeAssessment(t *testing.T) {
assessment := &Assessment{
Strategy: "Implement feature X",
Steps: []string{"Step 1", "Step 2", "Step 3"},
Risks: []string{"Risk A", "Risk B"},
Confidence: 0.85,
Reasoning: "This is the best approach",
Strategy: "Implement feature X",
Steps: []string{"Step 1", "Step 2", "Step 3"},
Risks: []string{"Risk A", "Risk B"},
Confidence: 0.85,
Reasoning: "This is the best approach",
ShouldDecompose: false,
}

Expand Down Expand Up @@ -106,11 +106,11 @@ func TestSerializeAssessment(t *testing.T) {

func TestSerializeAssessmentWithDecomposition(t *testing.T) {
assessment := &Assessment{
Strategy: "Decompose into smaller tasks",
Steps: []string{"Analyze", "Break down"},
Risks: []string{"Complexity"},
Confidence: 0.70,
Reasoning: "Too large for one task",
Strategy: "Decompose into smaller tasks",
Steps: []string{"Analyze", "Break down"},
Risks: []string{"Complexity"},
Confidence: 0.70,
Reasoning: "Too large for one task",
ShouldDecompose: true,
DecompositionPlan: &DecompositionPlan{
Reasoning: "Multiple independent components",
Expand Down Expand Up @@ -197,9 +197,9 @@ func TestShouldIterateAssessment_Mission(t *testing.T) {
func TestShouldIterateAssessment_SimpleIssue(t *testing.T) {
supervisor := &Supervisor{}
issue := &types.Issue{
ID: "vc-test",
Priority: 2,
Title: "Simple fix",
ID: "vc-test",
Priority: 2,
Title: "Simple fix",
IssueType: types.TypeTask,
}

Expand Down Expand Up @@ -228,10 +228,10 @@ func TestBuildIterationContext(t *testing.T) {
}

assessment := &Assessment{
Strategy: "New strategy",
Steps: []string{"Step 1", "Step 2", "Step 3"},
Risks: []string{"Risk A", "Risk B", "Risk C"},
Confidence: 0.85,
Strategy: "New strategy",
Steps: []string{"Step 1", "Step 2", "Step 3"},
Risks: []string{"Risk A", "Risk B", "Risk C"},
Confidence: 0.85,
ShouldDecompose: false,
}

Expand Down Expand Up @@ -276,10 +276,10 @@ func TestBuildIterationContextEmptyRisks(t *testing.T) {
}

assessment := &Assessment{
Strategy: "Simple strategy",
Steps: []string{"Step 1"},
Risks: []string{},
Confidence: 0.90,
Strategy: "Simple strategy",
Steps: []string{"Step 1"},
Risks: []string{},
Confidence: 0.90,
ShouldDecompose: false,
}

Expand Down Expand Up @@ -414,11 +414,11 @@ func TestAssessmentRefinerRefineNilArtifact(t *testing.T) {
// TestSerializeAssessmentMinimal tests minimal assessment serialization
func TestSerializeAssessmentMinimal(t *testing.T) {
assessment := &Assessment{
Strategy: "Simple strategy",
Steps: []string{},
Risks: []string{},
Confidence: 0.5,
Reasoning: "Basic reasoning",
Strategy: "Simple strategy",
Steps: []string{},
Risks: []string{},
Confidence: 0.5,
Reasoning: "Basic reasoning",
ShouldDecompose: false,
}

Expand Down Expand Up @@ -454,18 +454,18 @@ func TestSelectivityMetrics(t *testing.T) {

// Record a skipped artifact
metrics := &iterative.ArtifactMetrics{
ArtifactType: "assessment",
Priority: "P2",
TotalIterations: 0,
Converged: true,
ArtifactType: "assessment",
Priority: "P2",
TotalIterations: 0,
Converged: true,
ConvergenceReason: "selectivity skip",
IterationSkipped: true,
SkipReason: "simple issue (no complexity triggers)",
IterationSkipped: true,
SkipReason: "simple issue (no complexity triggers)",
}

collector.RecordArtifactComplete(&iterative.ConvergenceResult{
Iterations: 0,
Converged: true,
Iterations: 0,
Converged: true,
}, metrics)

agg := collector.GetAggregateMetrics()
Expand Down Expand Up @@ -500,8 +500,8 @@ func TestSelectivityMetrics(t *testing.T) {
}

collector.RecordArtifactComplete(&iterative.ConvergenceResult{
Iterations: 4,
Converged: true,
Iterations: 4,
Converged: true,
}, metrics)

agg := collector.GetAggregateMetrics()
Expand Down
52 changes: 52 additions & 0 deletions internal/ai/baseurl_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package ai

import (
"context"
"os"
"testing"
"time"
)

// TestBaseURLIntegration tests actual API connectivity with custom base URL
// Run with: VC_API_BASE=http://localhost:8000 go test ./internal/ai/... -run TestBaseURLIntegration -v
func TestBaseURLIntegration(t *testing.T) {
baseURL := os.Getenv("VC_API_BASE")
if baseURL == "" {
t.Skip("Skipping integration test: VC_API_BASE not set")
}

apiKey := os.Getenv("ANTHROPIC_API_KEY")
if apiKey == "" {
// For wrapper, we may not need a real key - use a placeholder
apiKey = "test-wrapper-key"
}

cfg := &Config{
APIKey: apiKey,
BaseURL: baseURL,
Store: newMockStorage(),
}

sup, err := NewSupervisor(cfg)
if err != nil {
t.Fatalf("NewSupervisor failed: %v", err)
}

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

// Make a simple API call
resp, err := sup.CallAPI(ctx, "Say 'hello' and nothing else.", "", 50)
if err != nil {
t.Fatalf("CallAPI failed: %v", err)
}

if resp == nil {
t.Fatal("CallAPI returned nil response")
}

t.Logf("API response received successfully via custom endpoint %s", baseURL)
if len(resp.Content) > 0 {
t.Logf("Response content: %+v", resp.Content[0])
}
}
69 changes: 69 additions & 0 deletions internal/ai/baseurl_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package ai

import (
"os"
"testing"
)

// TestBaseURLConfiguration verifies that the BaseURL configuration works correctly
func TestBaseURLConfiguration(t *testing.T) {
t.Run("config BaseURL takes precedence over env var", func(t *testing.T) {
// Set env var
os.Setenv("VC_API_BASE", "http://env-url:8000")
defer os.Unsetenv("VC_API_BASE")

// Config with explicit BaseURL should use that
cfg := &Config{
APIKey: "test-key",
BaseURL: "http://config-url:9000",
Store: newMockStorage(),
}

// NewSupervisor will use cfg.BaseURL, not env var
// We can't easily test the client was configured correctly without
// making a real API call, but we can verify the config is accepted
sup, err := NewSupervisor(cfg)
if err != nil {
t.Fatalf("NewSupervisor failed: %v", err)
}
if sup == nil {
t.Fatal("NewSupervisor returned nil")
}
})

t.Run("env var used when config BaseURL empty", func(t *testing.T) {
os.Setenv("VC_API_BASE", "http://localhost:8000")
defer os.Unsetenv("VC_API_BASE")

cfg := &Config{
APIKey: "test-key",
Store: newMockStorage(),
// BaseURL intentionally empty
}

sup, err := NewSupervisor(cfg)
if err != nil {
t.Fatalf("NewSupervisor failed: %v", err)
}
if sup == nil {
t.Fatal("NewSupervisor returned nil")
}
})

t.Run("no BaseURL uses default Anthropic endpoint", func(t *testing.T) {
os.Unsetenv("VC_API_BASE")

cfg := &Config{
APIKey: "test-key",
Store: newMockStorage(),
}

sup, err := NewSupervisor(cfg)
if err != nil {
t.Fatalf("NewSupervisor failed: %v", err)
}
if sup == nil {
t.Fatal("NewSupervisor returned nil")
}
})
}
Loading