Skip to content

Commit 87a59c7

Browse files
committed
increase test coverage: ClassifyPath/URL, Description, Buffer.Cap/Len, NewFactStore edges, fetchLocal, resource.Search/Load, episode Write/truncate
1 parent 433969f commit 87a59c7

7 files changed

Lines changed: 462 additions & 8 deletions

File tree

internal/danger/classifier_test.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package danger
22

33
import (
4+
"os"
45
"testing"
56
)
67

@@ -781,3 +782,150 @@ func TestClassify_SystemRedirectTarget(t *testing.T) {
781782
})
782783
}
783784
}
785+
786+
func TestClassifyPath_Destructive_Paths(t *testing.T) {
787+
tests := []struct {
788+
path string
789+
want RiskClass
790+
}{
791+
{"/boot/vmlinuz", Destructive},
792+
{"/dev/sda1", Destructive},
793+
{"/proc/1/cmdline", Destructive},
794+
{"/sys/class/power_supply", Destructive},
795+
{"/mnt/backup", Destructive},
796+
{"/media/usb", Destructive},
797+
}
798+
for _, tt := range tests {
799+
t.Run(tt.path, func(t *testing.T) {
800+
got := ClassifyPath(tt.path)
801+
if got != tt.want {
802+
t.Errorf("ClassifyPath(%q) = %s, want %s", tt.path, got, tt.want)
803+
}
804+
})
805+
}
806+
}
807+
808+
func TestClassifyPath_SystemWrite_Paths(t *testing.T) {
809+
tests := []struct {
810+
path string
811+
want RiskClass
812+
}{
813+
{"/etc/hosts", SystemWrite},
814+
{"/etc/nginx/nginx.conf", SystemWrite},
815+
{"/root/.bashrc", SystemWrite},
816+
{"/var/log/syslog", SystemWrite},
817+
{"/var/lib/docker", SystemWrite},
818+
{"/run/systemd", SystemWrite},
819+
{"/lib/systemd/system", SystemWrite},
820+
{"/usr/local/bin/app", SystemWrite},
821+
}
822+
for _, tt := range tests {
823+
t.Run(tt.path, func(t *testing.T) {
824+
got := ClassifyPath(tt.path)
825+
if got != tt.want {
826+
t.Errorf("ClassifyPath(%q) = %s, want %s", tt.path, got, tt.want)
827+
}
828+
})
829+
}
830+
}
831+
832+
func TestClassifyPath_LocalWrite_Paths(t *testing.T) {
833+
tests := []struct {
834+
path string
835+
want RiskClass
836+
}{
837+
{"/tmp/test.txt", LocalWrite},
838+
{"/tmp/foo/bar", LocalWrite},
839+
{"/home/user/code/main.go", LocalWrite},
840+
}
841+
for _, tt := range tests {
842+
t.Run(tt.path, func(t *testing.T) {
843+
got := ClassifyPath(tt.path)
844+
if got != tt.want {
845+
t.Errorf("ClassifyPath(%q) = %s, want %s", tt.path, got, tt.want)
846+
}
847+
})
848+
}
849+
}
850+
851+
func TestClassifyPath_HomeSensitiveDirs(t *testing.T) {
852+
home, _ := os.UserHomeDir()
853+
if home == "" {
854+
t.Skip("no home dir")
855+
}
856+
tests := []struct {
857+
path string
858+
want RiskClass
859+
}{
860+
{home + "/.ssh/id_rsa", SystemWrite},
861+
{home + "/.config/gh/config.yml", SystemWrite},
862+
{home + "/.gnupg/private.key", SystemWrite},
863+
{home + "/.aws/credentials", SystemWrite},
864+
{home + "/.kube/config", SystemWrite},
865+
{home + "/.docker/config.json", SystemWrite},
866+
{home + "/.gitconfig", SystemWrite},
867+
}
868+
for _, tt := range tests {
869+
t.Run(tt.path, func(t *testing.T) {
870+
got := ClassifyPath(tt.path)
871+
if got != tt.want {
872+
t.Errorf("ClassifyPath(%q) = %s, want %s", tt.path, got, tt.want)
873+
}
874+
})
875+
}
876+
}
877+
878+
func TestClassifyPath_LongPath(t *testing.T) {
879+
// Long path under /tmp — should still be local_write
880+
got := ClassifyPath("/tmp/a/b/c/d/e/f/g/h/file.txt")
881+
if got != LocalWrite {
882+
t.Errorf("ClassifyPath(long tmp path) = %s, want local_write", got)
883+
}
884+
}
885+
886+
func TestClassifyURL_InternalIPs(t *testing.T) {
887+
tests := []struct {
888+
url string
889+
want RiskClass
890+
}{
891+
{"http://127.0.0.1:8080", SystemWrite},
892+
{"http://localhost:3000", SystemWrite},
893+
{"http://10.0.0.1/api", SystemWrite},
894+
{"http://172.16.0.1", SystemWrite},
895+
{"http://192.168.1.1", SystemWrite},
896+
{"http://[::1]:8080", SystemWrite},
897+
{"https://127.0.0.1", SystemWrite},
898+
{"https://10.0.0.5", SystemWrite},
899+
{"https://172.20.0.1", SystemWrite},
900+
{"https://192.168.0.1", SystemWrite},
901+
}
902+
for _, tt := range tests {
903+
t.Run(tt.url, func(t *testing.T) {
904+
got := ClassifyURL(tt.url)
905+
if got != tt.want {
906+
t.Errorf("ClassifyURL(%q) = %s, want %s", tt.url, got, tt.want)
907+
}
908+
})
909+
}
910+
}
911+
912+
func TestClassifyURL_ExternalURLs(t *testing.T) {
913+
tests := []struct {
914+
url string
915+
want RiskClass
916+
}{
917+
{"https://example.com", NetworkEgress},
918+
{"http://api.github.com", NetworkEgress},
919+
{"https://google.com/search", NetworkEgress},
920+
{"https://8.8.8.8", NetworkEgress},
921+
{"http://1.2.3.4", NetworkEgress},
922+
}
923+
for _, tt := range tests {
924+
t.Run(tt.url, func(t *testing.T) {
925+
got := ClassifyURL(tt.url)
926+
if got != tt.want {
927+
t.Errorf("ClassifyURL(%q) = %s, want %s", tt.url, got, tt.want)
928+
}
929+
})
930+
}
931+
}

internal/memory/buffer_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,61 @@ func TestBufferCapZero(t *testing.T) {
113113
}
114114
}
115115

116+
func TestBufferCap(t *testing.T) {
117+
b := NewBuffer(10)
118+
if b.Cap() != 10 {
119+
t.Errorf("expected cap 10, got %d", b.Cap())
120+
}
121+
}
122+
123+
func TestBufferLen_Empty(t *testing.T) {
124+
b := NewBuffer(10)
125+
if b.Len() != 0 {
126+
t.Errorf("expected len 0, got %d", b.Len())
127+
}
128+
}
129+
130+
func TestBufferLen_WithItems(t *testing.T) {
131+
b := NewBuffer(10)
132+
b.Append("a")
133+
b.Append("b")
134+
b.Append("c")
135+
if b.Len() != 3 {
136+
t.Errorf("expected len 3, got %d", b.Len())
137+
}
138+
}
139+
140+
func TestBufferLen_AfterEviction(t *testing.T) {
141+
b := NewBuffer(3)
142+
b.Append("1")
143+
b.Append("2")
144+
b.Append("3")
145+
b.Append("4") // evicts 1, still 3 items
146+
if b.Len() != 3 {
147+
t.Errorf("expected len 3 after eviction, got %d", b.Len())
148+
}
149+
}
150+
151+
func TestBufferLen_AfterClear(t *testing.T) {
152+
b := NewBuffer(5)
153+
b.Append("something")
154+
b.Clear()
155+
if b.Len() != 0 {
156+
t.Errorf("expected len 0 after clear, got %d", b.Len())
157+
}
158+
}
159+
160+
func TestBufferNegativeCap(t *testing.T) {
161+
b := NewBuffer(-1)
162+
if b.Cap() != 0 {
163+
t.Errorf("expected cap 0 for negative input, got %d", b.Cap())
164+
}
165+
b.Append("should be discarded")
166+
if b.Len() != 0 {
167+
t.Errorf("expected len 0 for disabled buffer, got %d", b.Len())
168+
}
169+
}
170+
116171
func TestBufferFormatLine(t *testing.T) {
117172
line := FormatBufferLine("user", "fix TOCTOU race")
118173
if !strings.Contains(line, "user") {

internal/memory/episodes_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,3 +304,41 @@ func TestNewLLMRanker_DeduplicatesIndices(t *testing.T) {
304304
t.Fatalf("expected 2 deduplicated results, got %d", len(results))
305305
}
306306
}
307+
308+
func TestEpisodeStore_Write_TruncatesSummary(t *testing.T) {
309+
dir := t.TempDir()
310+
es := NewEpisodeStore(dir, func(q string, eps []EpisodeMeta) ([]EpisodeMeta, error) {
311+
return eps, nil
312+
})
313+
// Generate a summary longer than 1024 bytes
314+
longSummary := strings.Repeat("This is a very long summary that should be truncated. ", 50)
315+
err := es.Write("sess-trunc", longSummary, 5)
316+
if err != nil {
317+
t.Fatal(err)
318+
}
319+
content, err := es.Read("sess-trunc")
320+
if err != nil {
321+
t.Fatal(err)
322+
}
323+
if len(content) > 1100 {
324+
t.Errorf("summary too long after truncation: %d bytes", len(content))
325+
}
326+
if !strings.HasSuffix(content, "...") {
327+
t.Errorf("expected truncated suffix '...', got %q", content[len(content)-10:])
328+
}
329+
}
330+
331+
func TestEpisodeStore_SkipShortSessions(t *testing.T) {
332+
dir := t.TempDir()
333+
es := NewEpisodeStore(dir, nil)
334+
// 2 turns < threshold 3
335+
err := es.WriteIfEnough("sess-short", "too brief", 2)
336+
if err != nil {
337+
t.Fatal(err)
338+
}
339+
// Verify nothing was written — no file should exist
340+
path := filepath.Join(dir, "sess-short.md")
341+
if _, err := os.Stat(path); err == nil {
342+
t.Errorf("file should not exist for skipped session: %s", path)
343+
}
344+
}

internal/memory/facts_test.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,115 @@ func TestFactStore_AddToEnvNotUser(t *testing.T) {
205205
t.Errorf("user should be empty, got %q", userContent)
206206
}
207207
}
208+
209+
func TestFactStore_NewFactStoreZeroCaps(t *testing.T) {
210+
// Zero caps should use defaults
211+
dir := t.TempDir()
212+
fs := NewFactStore(dir, 0, 0)
213+
if fs.capUser != 1500 {
214+
t.Errorf("expected default capUser 1500, got %d", fs.capUser)
215+
}
216+
if fs.capEnv != 2500 {
217+
t.Errorf("expected default capEnv 2500, got %d", fs.capEnv)
218+
}
219+
}
220+
221+
func TestFactStore_AddEmptyContent(t *testing.T) {
222+
dir := t.TempDir()
223+
fs := NewFactStore(dir, 5000, 5000)
224+
err := fs.Add("user", "")
225+
if err == nil {
226+
t.Fatal("expected error for empty content")
227+
}
228+
if !strings.Contains(err.Error(), "empty") {
229+
t.Errorf("expected empty content error, got %v", err)
230+
}
231+
}
232+
233+
func TestFactStore_AddOnlyWhitespace(t *testing.T) {
234+
dir := t.TempDir()
235+
fs := NewFactStore(dir, 5000, 5000)
236+
err := fs.Add("user", " ")
237+
if err == nil {
238+
t.Fatal("expected error for whitespace-only content")
239+
}
240+
}
241+
242+
func TestFactStore_ReplaceEmptyOldText(t *testing.T) {
243+
dir := t.TempDir()
244+
fs := NewFactStore(dir, 5000, 5000)
245+
fs.Add("user", "existing fact")
246+
err := fs.Replace("user", "", "new content")
247+
if err == nil {
248+
t.Fatal("expected error for empty old_text")
249+
}
250+
}
251+
252+
func TestFactStore_ReplaceEmptyContent(t *testing.T) {
253+
dir := t.TempDir()
254+
fs := NewFactStore(dir, 5000, 5000)
255+
fs.Add("user", "existing fact")
256+
err := fs.Replace("user", "existing", "")
257+
if err == nil {
258+
t.Fatal("expected error for empty replacement content")
259+
}
260+
}
261+
262+
func TestFactStore_RemoveEmptyOldText(t *testing.T) {
263+
dir := t.TempDir()
264+
fs := NewFactStore(dir, 5000, 5000)
265+
err := fs.Remove("user", "")
266+
if err == nil {
267+
t.Fatal("expected error for empty old_text")
268+
}
269+
}
270+
271+
func TestFactStore_ReplaceMultipleMatches(t *testing.T) {
272+
dir := t.TempDir()
273+
fs := NewFactStore(dir, 5000, 5000)
274+
fs.Add("user", "uses Go for backend")
275+
fs.Add("user", "Go is fast")
276+
// "Go" matches both — should error
277+
err := fs.Replace("user", "Go", "replacement")
278+
if err == nil {
279+
t.Fatal("expected error for ambiguous old_text matching multiple entries")
280+
}
281+
if !strings.Contains(err.Error(), "entries") {
282+
t.Errorf("expected 'entries' in error, got %v", err)
283+
}
284+
}
285+
286+
func TestFactStore_RemoveMultipleMatches(t *testing.T) {
287+
dir := t.TempDir()
288+
fs := NewFactStore(dir, 5000, 5000)
289+
fs.Add("user", "likes Go")
290+
fs.Add("user", "Go is a language")
291+
err := fs.Remove("user", "Go")
292+
if err == nil {
293+
t.Fatal("expected error for ambiguous old_text matching multiple entries")
294+
}
295+
}
296+
297+
func TestFactStore_AddCapExceeded(t *testing.T) {
298+
dir := t.TempDir()
299+
// Very small cap to force overflow
300+
fs := NewFactStore(dir, 10, 5000)
301+
err := fs.Add("user", "this is way more than ten characters")
302+
if err == nil {
303+
t.Fatal("expected cap error")
304+
}
305+
if !strings.Contains(err.Error(), "cap") {
306+
t.Errorf("expected cap error, got %v", err)
307+
}
308+
}
309+
310+
func TestFactStore_ReplaceCapExceeded(t *testing.T) {
311+
dir := t.TempDir()
312+
fs := NewFactStore(dir, 30, 5000)
313+
fs.Add("user", "short fact")
314+
// Replace with something much longer
315+
err := fs.Replace("user", "short fact", "this is a very long replacement that should overflow the tiny 30 character cap")
316+
if err == nil {
317+
t.Fatal("expected cap error on replace")
318+
}
319+
}

internal/memory/tool_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,15 @@ func TestMemoryToolReturnsJSON(t *testing.T) {
206206
t.Errorf("result should have 'success' and 'message' fields, got keys: %v", parsed)
207207
}
208208
}
209+
210+
func TestMemoryToolDescription(t *testing.T) {
211+
mm := NewMemoryManager(t.TempDir(), nil, DefaultMemoryConfig())
212+
tool := NewMemoryTool(mm)
213+
desc := tool.Description()
214+
if desc == "" {
215+
t.Error("expected non-empty description")
216+
}
217+
if !strings.Contains(desc, "memory") {
218+
t.Errorf("description should mention memory, got %q", desc)
219+
}
220+
}

0 commit comments

Comments
 (0)