From 43440200facf9f7c51ba4f4638e69e7d651dd50d Mon Sep 17 00:00:00 2001 From: "Claude Sonnet 4.6" Date: Sat, 14 Mar 2026 18:11:44 +0000 Subject: feat(Phase4): add file changes for changestats executor wiring Files changed: CLAUDE.md, internal/api/changestats.go, internal/executor/executor.go, internal/executor/executor_test.go, internal/task/changestats.go (new) Co-Authored-By: Claude Sonnet 4.6 --- internal/executor/executor_test.go | 154 ++++++++++++++++++++++++++++++++++++- 1 file changed, 151 insertions(+), 3 deletions(-) (limited to 'internal/executor/executor_test.go') diff --git a/internal/executor/executor_test.go b/internal/executor/executor_test.go index a6c4ad8..610ed3b 100644 --- a/internal/executor/executor_test.go +++ b/internal/executor/executor_test.go @@ -921,9 +921,13 @@ type minimalMockStore struct { executions map[string]*storage.Execution stateUpdates []struct{ id string; state task.State } questionUpdates []string - subtasksFunc func(parentID string) ([]*task.Task, error) - updateExecErr error - updateStateErr error + changestatCalls []struct { + execID string + stats *task.Changestats + } + subtasksFunc func(parentID string) ([]*task.Task, error) + updateExecErr error + updateStateErr error } func newMinimalMockStore() *minimalMockStore { @@ -977,6 +981,15 @@ func (m *minimalMockStore) AppendTaskInteraction(taskID string, _ task.Interacti return nil } func (m *minimalMockStore) UpdateTaskAgent(id string, agent task.AgentConfig) error { return nil } +func (m *minimalMockStore) UpdateExecutionChangestats(execID string, stats *task.Changestats) error { + m.mu.Lock() + m.changestatCalls = append(m.changestatCalls, struct { + execID string + stats *task.Changestats + }{execID, stats}) + m.mu.Unlock() + return nil +} func (m *minimalMockStore) lastStateUpdate() (string, task.State, bool) { m.mu.Lock() @@ -1203,3 +1216,138 @@ func TestPool_SpecificAgent_PersistsToDB(t *testing.T) { t.Errorf("expected agent type gemini in DB, got %q", reloaded.Agent.Type) } } + +// TestExecute_ExtractAndStoreChangestats verifies that when the execution stdout +// contains a git diff --stat summary line, the changestats are parsed and stored. +func TestExecute_ExtractAndStoreChangestats(t *testing.T) { + store := testStore(t) + logDir := t.TempDir() + + runner := &logPatherMockRunner{logDir: logDir} + runner.onRun = func(tk *task.Task, e *storage.Execution) error { + if err := os.MkdirAll(filepath.Dir(e.StdoutPath), 0755); err != nil { + return err + } + content := "some output\n5 files changed, 127 insertions(+), 43 deletions(-)\n" + return os.WriteFile(e.StdoutPath, []byte(content), 0644) + } + + runners := map[string]Runner{"claude": runner} + logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError})) + pool := NewPool(2, runners, store, logger) + + tk := makeTask("cs-extract-1") + store.CreateTask(tk) + + if err := pool.Submit(context.Background(), tk); err != nil { + t.Fatalf("submit: %v", err) + } + result := <-pool.Results() + if result.Err != nil { + t.Fatalf("unexpected error: %v", result.Err) + } + + execs, err := store.ListExecutions(tk.ID) + if err != nil { + t.Fatalf("list executions: %v", err) + } + if len(execs) == 0 { + t.Fatal("no executions found") + } + cs := execs[0].Changestats + if cs == nil { + t.Fatal("expected changestats to be populated, got nil") + } + if cs.FilesChanged != 5 { + t.Errorf("FilesChanged: want 5, got %d", cs.FilesChanged) + } + if cs.LinesAdded != 127 { + t.Errorf("LinesAdded: want 127, got %d", cs.LinesAdded) + } + if cs.LinesRemoved != 43 { + t.Errorf("LinesRemoved: want 43, got %d", cs.LinesRemoved) + } +} + +// TestExecute_NoChangestats verifies that when execution output contains no git +// diff stat line, changestats are not stored (remain nil). +func TestExecute_NoChangestats(t *testing.T) { + store := testStore(t) + logDir := t.TempDir() + + runner := &logPatherMockRunner{logDir: logDir} + runner.onRun = func(tk *task.Task, e *storage.Execution) error { + if err := os.MkdirAll(filepath.Dir(e.StdoutPath), 0755); err != nil { + return err + } + return os.WriteFile(e.StdoutPath, []byte("no git output here\n"), 0644) + } + + runners := map[string]Runner{"claude": runner} + logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError})) + pool := NewPool(2, runners, store, logger) + + tk := makeTask("cs-none-1") + store.CreateTask(tk) + + if err := pool.Submit(context.Background(), tk); err != nil { + t.Fatalf("submit: %v", err) + } + result := <-pool.Results() + if result.Err != nil { + t.Fatalf("unexpected error: %v", result.Err) + } + + execs, err := store.ListExecutions(tk.ID) + if err != nil { + t.Fatalf("list executions: %v", err) + } + if len(execs) == 0 { + t.Fatal("no executions found") + } + if execs[0].Changestats != nil { + t.Errorf("expected changestats to be nil for output with no git stats, got %+v", execs[0].Changestats) + } +} + +// TestExecute_MalformedChangestats verifies that malformed git-stat-like output +// does not produce changestats (parser returns nil, nothing is stored). +func TestExecute_MalformedChangestats(t *testing.T) { + store := testStore(t) + logDir := t.TempDir() + + runner := &logPatherMockRunner{logDir: logDir} + runner.onRun = func(tk *task.Task, e *storage.Execution) error { + if err := os.MkdirAll(filepath.Dir(e.StdoutPath), 0755); err != nil { + return err + } + // Looks like a git stat line but doesn't match the regex. + return os.WriteFile(e.StdoutPath, []byte("lots of cheese changed, many insertions\n"), 0644) + } + + runners := map[string]Runner{"claude": runner} + logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError})) + pool := NewPool(2, runners, store, logger) + + tk := makeTask("cs-malformed-1") + store.CreateTask(tk) + + if err := pool.Submit(context.Background(), tk); err != nil { + t.Fatalf("submit: %v", err) + } + result := <-pool.Results() + if result.Err != nil { + t.Fatalf("unexpected error: %v", result.Err) + } + + execs, err := store.ListExecutions(tk.ID) + if err != nil { + t.Fatalf("list executions: %v", err) + } + if len(execs) == 0 { + t.Fatal("no executions found") + } + if execs[0].Changestats != nil { + t.Errorf("expected nil changestats for malformed output, got %+v", execs[0].Changestats) + } +} -- cgit v1.2.3