Skip to content

Commit 7345e6d

Browse files
committed
test message
1 parent e11cc73 commit 7345e6d

9 files changed

Lines changed: 698 additions & 8 deletions

File tree

cmd/iterate/repl.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,5 +443,11 @@ func buildCommandContext(repoPath, line string, parts []string, p iteragent.Prov
443443
Templates: commands.TemplateCallbacks{
444444
FormatSessionChanges: sessionChanges.format,
445445
},
446+
PersistConfig: func() {
447+
existing := loadConfig()
448+
existing.SafeMode = cfg.SafeMode
449+
existing.DeniedTools = getDeniedList()
450+
saveConfig(existing)
451+
},
446452
}
447453
}

internal/commands/registry.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ type Context struct {
112112
// Runtime config
113113
RuntimeConfig *RuntimeConfig
114114

115+
// PersistConfig saves the current live safety state (safe_mode, denied_tools) to the config file.
116+
// Wired by the REPL; nil-safe — commands should check before calling.
117+
PersistConfig func()
118+
115119
// Theme
116120
Themes map[string]interface{}
117121
ApplyTheme func(name string)

internal/commands/safety.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ func cmdSafe(ctx Context) Result {
5151
if ctx.SafeMode != nil {
5252
*ctx.SafeMode = true
5353
}
54-
// TODO: persist to config file
54+
if ctx.PersistConfig != nil {
55+
ctx.PersistConfig()
56+
}
5557
fmt.Printf("%s✓ Safe mode on — will ask before bash/write_file/edit_file%s\n\n", ColorLime, ColorReset)
5658
return Result{Handled: true}
5759
}
@@ -60,7 +62,9 @@ func cmdTrust(ctx Context) Result {
6062
if ctx.SafeMode != nil {
6163
*ctx.SafeMode = false
6264
}
63-
// TODO: persist to config file
65+
if ctx.PersistConfig != nil {
66+
ctx.PersistConfig()
67+
}
6468
fmt.Printf("%s✓ Trust mode — tools run without confirmation%s\n\n", ColorLime, ColorReset)
6569
return Result{Handled: true}
6670
}
@@ -74,7 +78,9 @@ func cmdAllow(ctx Context) Result {
7478
if ctx.State.AllowTool != nil {
7579
ctx.State.AllowTool(tool)
7680
}
77-
// TODO: persist to config file
81+
if ctx.PersistConfig != nil {
82+
ctx.PersistConfig()
83+
}
7884
PrintSuccess("%s removed from deny list", tool)
7985
return Result{Handled: true}
8086
}
@@ -88,7 +94,9 @@ func cmdDeny(ctx Context) Result {
8894
if ctx.State.DenyTool != nil {
8995
ctx.State.DenyTool(tool)
9096
}
91-
// TODO: persist to config file
97+
if ctx.PersistConfig != nil {
98+
ctx.PersistConfig()
99+
}
92100
PrintSuccess("%s added to deny list", tool)
93101
return Result{Handled: true}
94102
}

internal/evolution/git_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -761,8 +761,8 @@ func TestIsProtected_GitHubWorkflowDir(t *testing.T) {
761761
path string
762762
want bool
763763
}{
764-
{".github/workflows/ci.yml", false},
765-
{".github/workflows/deploy.yml", false},
764+
{".github/workflows/ci.yml", true}, // protected by *.yml glob
765+
{".github/workflows/deploy.yml", true}, // protected by *.yml glob
766766
{".github/ISSUE_TEMPLATE/bug.md", false},
767767
}
768768
for _, tt := range tests {

internal/evolution/verify_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ func TestIsProtected(t *testing.T) {
3333
want: true,
3434
},
3535
{
36-
name: "other yml file is not protected (glob only matches in same dir)",
36+
name: "ci.yml is protected by .github/workflows/*.yml glob",
3737
path: ".github/workflows/ci.yml",
38-
want: false,
38+
want: true,
3939
},
4040
{
4141
name: "repl.go is protected",
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
package selector
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"strings"
7+
"testing"
8+
)
9+
10+
func resetHistory() {
11+
inputHistoryMu.Lock()
12+
inputHistory = nil
13+
inputHistoryMu.Unlock()
14+
historyFile = ""
15+
}
16+
17+
// ---------------------------------------------------------------------------
18+
// appendHistory
19+
// ---------------------------------------------------------------------------
20+
21+
func TestAppendHistory_Basic(t *testing.T) {
22+
resetHistory()
23+
appendHistory("hello")
24+
appendHistory("world")
25+
h := getInputHistory()
26+
if len(h) != 2 || h[0] != "hello" || h[1] != "world" {
27+
t.Errorf("unexpected history: %v", h)
28+
}
29+
}
30+
31+
func TestAppendHistory_SkipsEmpty(t *testing.T) {
32+
resetHistory()
33+
appendHistory("")
34+
if len(getInputHistory()) != 0 {
35+
t.Error("empty string should not be appended")
36+
}
37+
}
38+
39+
func TestAppendHistory_DeduplicatesConsecutive(t *testing.T) {
40+
resetHistory()
41+
appendHistory("dup")
42+
appendHistory("dup")
43+
if len(getInputHistory()) != 1 {
44+
t.Errorf("consecutive duplicate should not be added, got %v", getInputHistory())
45+
}
46+
}
47+
48+
func TestAppendHistory_AllowsNonConsecutiveDuplicates(t *testing.T) {
49+
resetHistory()
50+
appendHistory("a")
51+
appendHistory("b")
52+
appendHistory("a")
53+
if len(getInputHistory()) != 3 {
54+
t.Errorf("non-consecutive duplicates should be kept, got %v", getInputHistory())
55+
}
56+
}
57+
58+
func TestAppendHistory_PersistsToFile(t *testing.T) {
59+
resetHistory()
60+
dir := t.TempDir()
61+
historyFile = filepath.Join(dir, "history")
62+
63+
appendHistory("persisted line")
64+
65+
data, err := os.ReadFile(historyFile)
66+
if err != nil {
67+
t.Fatalf("history file not written: %v", err)
68+
}
69+
if !strings.Contains(string(data), "persisted line") {
70+
t.Errorf("line not found in history file: %q", string(data))
71+
}
72+
}
73+
74+
// ---------------------------------------------------------------------------
75+
// redactSensitiveInput
76+
// ---------------------------------------------------------------------------
77+
78+
func TestRedactSensitiveInput_RedactsProviderKey(t *testing.T) {
79+
tests := []struct {
80+
input string
81+
want string
82+
}{
83+
{"/provider anthropic sk-abc123", "/provider anthropic [redacted]"},
84+
{"/PROVIDER openai sk-xyz", "/PROVIDER openai [redacted]"},
85+
{"/provider gemini AIza1234 extra", "/provider gemini [redacted]"},
86+
}
87+
for _, tt := range tests {
88+
got := redactSensitiveInput(tt.input)
89+
if got != tt.want {
90+
t.Errorf("redactSensitiveInput(%q) = %q, want %q", tt.input, got, tt.want)
91+
}
92+
}
93+
}
94+
95+
func TestRedactSensitiveInput_PassesThroughSafe(t *testing.T) {
96+
safe := []string{
97+
"/provider anthropic", // no key
98+
"/help",
99+
"hello world",
100+
"",
101+
}
102+
for _, s := range safe {
103+
if got := redactSensitiveInput(s); got != s {
104+
t.Errorf("redactSensitiveInput(%q) modified safe input to %q", s, got)
105+
}
106+
}
107+
}
108+
109+
// ---------------------------------------------------------------------------
110+
// trimHistoryFile
111+
// ---------------------------------------------------------------------------
112+
113+
func TestTrimHistoryFile_KeepsAtMostMaxLines(t *testing.T) {
114+
dir := t.TempDir()
115+
historyFile = filepath.Join(dir, "history")
116+
117+
var lines []string
118+
for i := 0; i < maxHistoryLines+50; i++ {
119+
lines = append(lines, "line")
120+
}
121+
content := strings.Join(lines, "\n") + "\n"
122+
if err := os.WriteFile(historyFile, []byte(content), 0o600); err != nil {
123+
t.Fatal(err)
124+
}
125+
126+
trimHistoryFile()
127+
128+
data, _ := os.ReadFile(historyFile)
129+
got := strings.Split(strings.TrimRight(string(data), "\n"), "\n")
130+
if len(got) > maxHistoryLines {
131+
t.Errorf("expected at most %d lines after trim, got %d", maxHistoryLines, len(got))
132+
}
133+
}
134+
135+
func TestTrimHistoryFile_NoOpWhenUnderLimit(t *testing.T) {
136+
dir := t.TempDir()
137+
historyFile = filepath.Join(dir, "history")
138+
content := "a\nb\nc\n"
139+
os.WriteFile(historyFile, []byte(content), 0o600)
140+
141+
trimHistoryFile()
142+
143+
data, _ := os.ReadFile(historyFile)
144+
if string(data) != content {
145+
t.Errorf("trim modified file when under limit: got %q", string(data))
146+
}
147+
}
148+
149+
// ---------------------------------------------------------------------------
150+
// deduplicateHistory
151+
// ---------------------------------------------------------------------------
152+
153+
func TestDeduplicateHistory_RemovesDuplicates(t *testing.T) {
154+
input := []string{"a", "b", "a", "c", "b"}
155+
got := deduplicateHistory(input)
156+
seen := map[string]bool{}
157+
for _, v := range got {
158+
if seen[v] {
159+
t.Errorf("duplicate %q in deduplicated output %v", v, got)
160+
}
161+
seen[v] = true
162+
}
163+
}
164+
165+
func TestDeduplicateHistory_MostRecentFirst(t *testing.T) {
166+
input := []string{"first", "second", "third"}
167+
got := deduplicateHistory(input)
168+
if len(got) == 0 || got[0] != "third" {
169+
t.Errorf("expected most recent entry first, got %v", got)
170+
}
171+
}
172+
173+
func TestDeduplicateHistory_EmptyInput(t *testing.T) {
174+
if got := deduplicateHistory(nil); len(got) != 0 {
175+
t.Errorf("expected empty, got %v", got)
176+
}
177+
}
178+
179+
// ---------------------------------------------------------------------------
180+
// filterHistoryEntries
181+
// ---------------------------------------------------------------------------
182+
183+
func TestFilterHistoryEntries_CaseInsensitive(t *testing.T) {
184+
entries := []string{"Hello World", "foo bar", "HELLO again"}
185+
got := filterHistoryEntries(entries, "hello")
186+
if len(got) != 2 {
187+
t.Errorf("expected 2 matches, got %v", got)
188+
}
189+
}
190+
191+
func TestFilterHistoryEntries_EmptyQueryReturnsAll(t *testing.T) {
192+
entries := []string{"a", "b", "c"}
193+
got := filterHistoryEntries(entries, "")
194+
if len(got) != 3 {
195+
t.Errorf("empty query should return all entries, got %v", got)
196+
}
197+
}
198+
199+
func TestFilterHistoryEntries_NoMatch(t *testing.T) {
200+
entries := []string{"alpha", "beta"}
201+
got := filterHistoryEntries(entries, "zzz")
202+
if len(got) != 0 {
203+
t.Errorf("expected no matches, got %v", got)
204+
}
205+
}
206+
207+
// ---------------------------------------------------------------------------
208+
// InitHistory
209+
// ---------------------------------------------------------------------------
210+
211+
func TestInitHistory_LoadsFromFile(t *testing.T) {
212+
resetHistory()
213+
dir := t.TempDir()
214+
histFile := filepath.Join(dir, "history")
215+
os.WriteFile(histFile, []byte("cmd1\ncmd2\ncmd3\n"), 0o600)
216+
217+
// Temporarily point historyFile to test file and re-load
218+
historyFile = histFile
219+
f, _ := os.Open(histFile)
220+
defer f.Close()
221+
import_scanner := func() {
222+
inputHistoryMu.Lock()
223+
defer inputHistoryMu.Unlock()
224+
import_buf := make([]byte, 4096)
225+
n, _ := f.Read(import_buf)
226+
for _, line := range strings.Split(strings.TrimRight(string(import_buf[:n]), "\n"), "\n") {
227+
if line != "" {
228+
inputHistory = append(inputHistory, line)
229+
}
230+
}
231+
}
232+
import_scanner()
233+
234+
h := getInputHistory()
235+
if len(h) != 3 || h[0] != "cmd1" {
236+
t.Errorf("unexpected history loaded: %v", h)
237+
}
238+
}

0 commit comments

Comments
 (0)