package project import ( "errors" "os" "path/filepath" "strings" "testing" ) func write(t *testing.T, path, content string) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte(content), 0o644); err != nil { t.Fatal(err) } } func names(tasks []Task) string { var s []string for _, t := range tasks { s = append(s, t.Name) } return strings.Join(s, ",") } func TestFindAgentFile(t *testing.T) { dir := t.TempDir() p, _ := Open(dir) if _, _, err := p.FindAgentFile(); !errors.Is(err, ErrAgentNotFound) { t.Fatalf("expected ErrAgentNotFound, got %v", err) } write(t, filepath.Join(dir, "Agent.md"), "x") name, variant, err := p.FindAgentFile() if err != nil || name != "Agent.md" || !variant { t.Fatalf("variant: name=%q variant=%v err=%v", name, variant, err) } write(t, filepath.Join(dir, "agent.md"), "x") name, variant, err = p.FindAgentFile() if err != nil || name != "agent.md" || variant { t.Fatalf("exact: name=%q variant=%v err=%v", name, variant, err) } } func TestAgentSymlinkOutsideRejected(t *testing.T) { dir := t.TempDir() outside := t.TempDir() write(t, filepath.Join(outside, "agent.md"), "EVIL") if err := os.Symlink(filepath.Join(outside, "agent.md"), filepath.Join(dir, "agent.md")); err != nil { t.Skip("symlinks not supported:", err) } p, _ := Open(dir) if _, _, err := p.FindAgentFile(); !errors.Is(err, ErrAgentNotFound) { t.Fatalf("symlink to external agent.md must be rejected, got %v", err) } } func TestDiscoverTasks(t *testing.T) { dir := t.TempDir() outside := t.TempDir() for _, n := range []string{"agent.md", "memoria.md", "README.md", "02-b.md", "01-a.md", "03-c.MD", "notes.txt"} { write(t, filepath.Join(dir, n), "content "+n) } // Las tareas en subdirectorios nunca son instrucciones. write(t, filepath.Join(dir, "subdir", "00-hidden.md"), "no") // Un symlink a un .md externo tampoco. write(t, filepath.Join(outside, "evil.md"), "rm -rf /") symlinkOK := os.Symlink(filepath.Join(outside, "evil.md"), filepath.Join(dir, "04-evil.md")) == nil p, _ := Open(dir) tasks, skipped, missing, err := p.DiscoverTasks("agent.md", nil) if err != nil { t.Fatal(err) } if got := names(tasks); got != "01-a.md,02-b.md,03-c.MD" { t.Errorf("tasks = %s", got) } if symlinkOK && (len(skipped) != 1 || skipped[0] != "04-evil.md") { t.Errorf("skipped = %v", skipped) } if len(missing) != 0 { t.Errorf("missing = %v", missing) } if tasks[0].Content != "content 01-a.md" { t.Errorf("content = %q", tasks[0].Content) } } func TestDiscoverTasksExplicitOrder(t *testing.T) { dir := t.TempDir() for _, n := range []string{"Agent.md", "a.md", "b.md", "c.md"} { write(t, filepath.Join(dir, n), n) } p, _ := Open(dir) tasks, _, missing, err := p.DiscoverTasks("Agent.md", []string{"c.md", "zz.md", "a.md"}) if err != nil { t.Fatal(err) } if got := names(tasks); got != "c.md,a.md,b.md" { t.Errorf("tasks = %s", got) } if len(missing) != 1 || missing[0] != "zz.md" { t.Errorf("missing = %v", missing) } } func TestEnsureMemoryFile(t *testing.T) { dir := t.TempDir() p, _ := Open(dir) created, _, err := p.EnsureMemoryFile() if err != nil || !created { t.Fatalf("created=%v err=%v", created, err) } st, err := os.Stat(p.Path(MemoryFile)) if err != nil { t.Fatal(err) } if st.Mode().Perm() != 0o600 { t.Errorf("perm = %o, want 600", st.Mode().Perm()) } // Segunda llamada: no recrea ni reescribe. if err := os.WriteFile(p.Path(MemoryFile), []byte("keep"), 0o644); err != nil { t.Fatal(err) } if err := os.Chmod(p.Path(MemoryFile), 0o644); err != nil { t.Fatal(err) } created, tooOpen, err := p.EnsureMemoryFile() if err != nil || created || !tooOpen { t.Fatalf("created=%v tooOpen=%v err=%v", created, tooOpen, err) } if data, _ := os.ReadFile(p.Path(MemoryFile)); string(data) != "keep" { t.Error("existing memoria.md must not be modified") } } func TestEnsureWorkDirs(t *testing.T) { dir := t.TempDir() p, _ := Open(dir) if err := p.EnsureWorkDirs(); err != nil { t.Fatal(err) } st, err := os.Stat(filepath.Join(dir, WorkDir, "downloads")) if err != nil || !st.IsDir() || st.Mode().Perm() != 0o700 { t.Fatalf("workdir: %v %v", st, err) } } func TestCheckSecurity(t *testing.T) { dir := t.TempDir() os.Chmod(dir, 0o700) write(t, filepath.Join(dir, "agent.md"), "x") write(t, filepath.Join(dir, "init.conf"), "API_KEY=x") os.Chmod(filepath.Join(dir, "init.conf"), 0o600) os.Chmod(filepath.Join(dir, "agent.md"), 0o644) p, _ := Open(dir) for _, f := range p.CheckSecurity("agent.md", nil, true) { if f.Fatal { t.Errorf("unexpected fatal finding: %s", f.Message) } } os.Chmod(filepath.Join(dir, "agent.md"), 0o666) fatal := false for _, f := range p.CheckSecurity("agent.md", nil, true) { fatal = fatal || f.Fatal && strings.Contains(f.Message, "writable by any user") } if !fatal { t.Error("world-writable agent.md must be fatal") } os.Chmod(filepath.Join(dir, "agent.md"), 0o644) os.Chmod(filepath.Join(dir, "init.conf"), 0o644) warned := false for _, f := range p.CheckSecurity("agent.md", nil, true) { warned = warned || strings.Contains(f.Message, "chmod 600 init.conf") } if !warned { t.Error("readable init.conf with API key must warn") } shared := t.TempDir() os.Chmod(shared, 0o1777) ps, _ := Open(shared) fatal = false for _, f := range ps.CheckSecurity("agent.md", nil, false) { fatal = fatal || f.Fatal } if !fatal { t.Error("PROJECT_ROOT in a shared /tmp-like directory must be fatal") } } func TestLock(t *testing.T) { dir := t.TempDir() p, _ := Open(dir) p.EnsureWorkDirs() unlock, err := p.Lock() if err != nil { t.Fatal(err) } // flock es por descriptor abierto: un segundo Open en el mismo proceso // simula otra instancia. if _, err := p.Lock(); !errors.Is(err, ErrLocked) || !strings.Contains(err.Error(), "pid") { t.Errorf("second lock: %v", err) } unlock() unlock2, err := p.Lock() if err != nil { t.Fatalf("lock after unlock: %v", err) } unlock2() } func TestInstructionSizeLimit(t *testing.T) { dir := t.TempDir() write(t, filepath.Join(dir, "agent.md"), strings.Repeat("x", MaxInstructionSize+1)) write(t, filepath.Join(dir, "01-big.md"), strings.Repeat("x", MaxInstructionSize+1)) write(t, filepath.Join(dir, "02-ok.md"), "ok") p, _ := Open(dir) if _, err := p.ReadInstructionFile("agent.md"); err == nil { t.Error("oversized agent.md must be rejected") } tasks, skipped, _, _ := p.DiscoverTasks("agent.md", nil) if len(tasks) != 1 || len(skipped) != 1 { t.Errorf("tasks=%d skipped=%v", len(tasks), skipped) } }