Files
iasysadmin/internal/project/project_test.go
T
2026-09-27 16:04:02 -06:00

234 lines
6.5 KiB
Go

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)
}
}