Skip to content

Commit 8abfef7

Browse files
committed
test message
1 parent 09965cc commit 8abfef7

5 files changed

Lines changed: 478 additions & 5 deletions

File tree

cmd/iterate/features_watch.go

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ var watchConfig = struct {
2828
// exclude is a list of path substrings to ignore.
2929
exclude []string
3030
}{
31-
debounce: 300 * time.Millisecond,
31+
// 2-second debounce: collect all file change events in the window and send
32+
// ONE batched prompt listing every changed file instead of per-file prompts.
33+
debounce: 2 * time.Second,
3234
exclude: []string{".git", "node_modules", ".iterate"},
3335
}
3436

@@ -57,11 +59,16 @@ func startWatch(repoPath string) {
5759
}
5860

5961
// runWatcher polls for file modifications using mtimes (no fsnotify dependency).
62+
// File change events are batched: all files that change within the debounce window
63+
// are collected and trigger a single test run, rather than one run per file.
6064
func runWatcher(ctx context.Context, repoPath string) {
6165
snapshots := snapshotMTimes(repoPath)
6266

6367
var debounceTimer *time.Timer
6468
var debounceMu sync.Mutex
69+
// pendingChanged accumulates file paths seen during the current debounce window.
70+
var pendingChanged []string
71+
pendingSet := make(map[string]struct{})
6572

6673
ticker := time.NewTicker(500 * time.Millisecond)
6774
defer ticker.Stop()
@@ -79,16 +86,29 @@ func runWatcher(ctx context.Context, repoPath string) {
7986
continue
8087
}
8188

82-
// Debounce: reset timer on each burst of changes.
89+
// Accumulate changed files across the debounce window (deduplicated).
8390
debounceMu.Lock()
91+
for _, p := range changed {
92+
if _, seen := pendingSet[p]; !seen {
93+
pendingSet[p] = struct{}{}
94+
pendingChanged = append(pendingChanged, p)
95+
}
96+
}
97+
98+
// Reset the timer: fire after debounce elapses with no new changes.
8499
if debounceTimer != nil {
85100
debounceTimer.Stop()
86101
}
87-
changedCopy := changed
102+
// Capture the batch for the closure.
103+
batchSnapshot := append([]string(nil), pendingChanged...)
104+
pendingChanged = pendingChanged[:0]
105+
for k := range pendingSet {
106+
delete(pendingSet, k)
107+
}
88108
debounceTimer = time.AfterFunc(watchConfig.debounce, func() {
89109
fmt.Printf("\n%s[watch] %d file(s) changed — running tests…%s\n",
90-
colorYellow, len(changedCopy), colorReset)
91-
for _, p := range changedCopy {
110+
colorYellow, len(batchSnapshot), colorReset)
111+
for _, p := range batchSnapshot {
92112
rel, _ := filepath.Rel(repoPath, p)
93113
fmt.Printf(" %s%s%s\n", colorDim, rel, colorReset)
94114
}

internal/commands/autofix.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package commands
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os/exec"
7+
"strconv"
8+
)
9+
10+
// RegisterAutofixCommands adds the /autofix command.
11+
func RegisterAutofixCommands(r *Registry) {
12+
r.Register(Command{
13+
Name: "/autofix",
14+
Aliases: []string{"/af"},
15+
Description: "run tests, ask agent to fix failures, repeat up to N times (default 3)",
16+
Category: "dev",
17+
Handler: cmdAutofix,
18+
})
19+
}
20+
21+
// detectTestCmd returns the test command and args appropriate for the repo.
22+
func detectTestCmd(repoPath string) (string, []string) {
23+
// Check for go.mod (most common for this project)
24+
if _, err := exec.LookPath("go"); err == nil {
25+
gomod := exec.Command("go", "env", "GOMOD")
26+
gomod.Dir = repoPath
27+
if out, err := gomod.Output(); err == nil && len(out) > 0 {
28+
return "go", []string{"test", "./..."}
29+
}
30+
}
31+
if _, err := exec.LookPath("npm"); err == nil {
32+
pkg := exec.Command("npm", "run", "test", "--if-present")
33+
pkg.Dir = repoPath
34+
return "npm", []string{"test"}
35+
}
36+
if _, err := exec.LookPath("pytest"); err == nil {
37+
return "pytest", []string{}
38+
}
39+
// Default fallback
40+
return "go", []string{"test", "./..."}
41+
}
42+
43+
func cmdAutofix(ctx Context) Result {
44+
maxAttempts := 3
45+
if ctx.HasArg(1) {
46+
if v, err := strconv.Atoi(ctx.Arg(1)); err == nil && v > 0 {
47+
maxAttempts = v
48+
}
49+
}
50+
51+
if ctx.REPL.StreamAndPrint == nil {
52+
PrintError("agent not available for autofix")
53+
return Result{Handled: true}
54+
}
55+
56+
testCmd, testArgs := detectTestCmd(ctx.RepoPath)
57+
58+
for attempt := 1; attempt <= maxAttempts; attempt++ {
59+
fmt.Printf("%s[attempt %d/%d]%s running %s %v\n",
60+
ColorDim, attempt, maxAttempts, ColorReset, testCmd, testArgs)
61+
62+
cmd := exec.Command(testCmd, testArgs...)
63+
cmd.Dir = ctx.RepoPath
64+
output, err := cmd.CombinedOutput()
65+
66+
if err == nil {
67+
PrintSuccess("tests passed on attempt %d/%d", attempt, maxAttempts)
68+
return Result{Handled: true}
69+
}
70+
71+
// Tests failed — print failure output
72+
fmt.Printf("%s── Test failures ───────────────────%s\n", ColorDim, ColorReset)
73+
fmt.Println(string(output))
74+
fmt.Printf("%s──────────────────────────────────%s\n\n", ColorDim, ColorReset)
75+
76+
if attempt == maxAttempts {
77+
break
78+
}
79+
80+
// Ask agent to fix
81+
prompt := fmt.Sprintf("The following tests are failing. Fix them:\n\n%s", string(output))
82+
ctx.REPL.StreamAndPrint(context.Background(), ctx.Agent, prompt, ctx.RepoPath)
83+
}
84+
85+
fmt.Printf("%sautofix gave up after %d attempts%s\n\n", ColorRed, maxAttempts, ColorReset)
86+
return Result{Handled: true}
87+
}

0 commit comments

Comments
 (0)