package manifest import ( "os" "path/filepath" "strings" "testing" ) func TestBuild_SkeletonSortedAndKinded(t *testing.T) { root := t.TempDir() dir := "frontend" base := filepath.Join(root, dir) if err := os.MkdirAll(filepath.Join(base, "routes"), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(base, "App.tsx"), []byte("x"), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(base, "index.ts"), []byte("x"), 0o644); err != nil { t.Fatal(err) } m, err := Build(root, dir) if err != nil { t.Fatal(err) } if m.Dir != "frontend" { t.Fatalf("Dir = %q, want frontend", m.Dir) } want := []Item{ {Path: "App.tsx", Kind: "file"}, {Path: "index.ts", Kind: "file"}, {Path: "routes/", Kind: "dir"}, } if len(m.Items) != len(want) { t.Fatalf("items = %+v, want %+v", m.Items, want) } for i := range want { if m.Items[i] != want[i] { t.Fatalf("item %d = %+v, want %+v", i, m.Items[i], want[i]) } } } func TestWriteAndIdempotentRebuild(t *testing.T) { root := t.TempDir() dir := "lib" base := filepath.Join(root, dir) if err := os.MkdirAll(base, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(base, "a.go"), []byte("x"), 0o644); err != nil { t.Fatal(err) } m, err := Build(root, dir) if err != nil { t.Fatal(err) } if err := Write(root, dir, m); err != nil { t.Fatal(err) } if _, err := os.Stat(filepath.Join(base, FileName)); err != nil { t.Fatalf("manifest not written: %v", err) } // The written .ai.json must not appear in a rebuild. m2, err := Build(root, dir) if err != nil { t.Fatal(err) } if len(m2.Items) != 1 || m2.Items[0].Path != "a.go" { t.Fatalf("rebuild not idempotent: %+v", m2.Items) } } func TestKnowledgeDirs_RecursiveExcludingEngine(t *testing.T) { userDir := t.TempDir() // Two top-level knowledge dirs with nested subdirs, plus the engine // workspace which must be excluded along with everything beneath it. for _, p := range []string{ filepath.Join("management", "knowledge"), filepath.Join("management", "roadmap"), filepath.Join("backend"), filepath.Join(".eeco", "state"), } { if err := os.MkdirAll(filepath.Join(userDir, p), 0o755); err != nil { t.Fatal(err) } } got, err := KnowledgeDirs(userDir, ".eeco") if err != nil { t.Fatal(err) } want := []string{ "backend", "management", filepath.Join("management", "knowledge"), filepath.Join("management", "roadmap"), } if len(got) != len(want) { t.Fatalf("dirs = %v, want %v", got, want) } for i := range want { if got[i] != want[i] { t.Fatalf("dir %d = %q, want %q (full: %v)", i, got[i], want[i], got) } } } func TestKnowledgeDirs_MissingUserDirIsNoop(t *testing.T) { got, err := KnowledgeDirs(filepath.Join(t.TempDir(), "absent"), ".eeco") if err != nil { t.Fatalf("err = %v, want nil", err) } if got != nil { t.Fatalf("dirs = %v, want nil", got) } } func TestSubtree_DirPlusNested(t *testing.T) { userDir := t.TempDir() for _, p := range []string{ filepath.Join("management", "knowledge"), filepath.Join("management", "roadmap"), filepath.Join("backend"), } { if err := os.MkdirAll(filepath.Join(userDir, p), 0o755); err != nil { t.Fatal(err) } } got, err := Subtree(userDir, "management") if err != nil { t.Fatal(err) } want := []string{ "management", filepath.Join("management", "knowledge"), filepath.Join("management", "roadmap"), } if len(got) != len(want) { t.Fatalf("subtree = %v, want %v", got, want) } for i := range want { if got[i] != want[i] { t.Fatalf("subtree %d = %q, want %q (full: %v)", i, got[i], want[i], got) } } } func TestBuild_EmptyDirHasEmptyItems(t *testing.T) { root := t.TempDir() dir := "empty" if err := os.MkdirAll(filepath.Join(root, dir), 0o755); err != nil { t.Fatal(err) } m, err := Build(root, dir) if err != nil { t.Fatal(err) } if m.Items == nil { t.Fatal("Items should be a non-nil empty slice") } if len(m.Items) != 0 { t.Fatalf("want empty, got %+v", m.Items) } if err := Write(root, dir, m); err != nil { t.Fatal(err) } b, err := os.ReadFile(filepath.Join(root, dir, FileName)) if err != nil { t.Fatal(err) } if !strings.Contains(string(b), `"items": []`) { t.Fatalf("empty items should marshal as []:\n%s", b) } } func TestKnowledgeDirs_ExcludesGitRepo(t *testing.T) { userDir := t.TempDir() // A real knowledge dir alongside the private workspace-history repo. The // .git tree and everything beneath it must be excluded — a refresh after // `init` must never enumerate (and so never write into) ajhahnde/.git. for _, p := range []string{ "backend", filepath.Join(".git", "objects", "ab"), filepath.Join(".git", "refs", "heads"), } { if err := os.MkdirAll(filepath.Join(userDir, p), 0o755); err != nil { t.Fatal(err) } } got, err := KnowledgeDirs(userDir, ".eeco") if err != nil { t.Fatal(err) } for _, d := range got { if d == vcsDir || strings.HasPrefix(d, vcsDir+string(filepath.Separator)) { t.Fatalf("KnowledgeDirs leaked a .git path: %q (full: %v)", d, got) } } if len(got) != 1 || got[0] != "backend" { t.Fatalf("dirs = %v, want [backend]", got) } } func TestSubtree_ExcludesNestedGit(t *testing.T) { userDir := t.TempDir() for _, p := range []string{ filepath.Join("backend", "api"), filepath.Join("backend", ".git", "objects"), } { if err := os.MkdirAll(filepath.Join(userDir, p), 0o755); err != nil { t.Fatal(err) } } got, err := Subtree(userDir, "backend") if err != nil { t.Fatal(err) } want := []string{"backend", filepath.Join("backend", "api")} if len(got) != len(want) { t.Fatalf("subtree = %v, want %v", got, want) } for i := range want { if got[i] != want[i] { t.Fatalf("subtree %d = %q, want %q (full: %v)", i, got[i], want[i], got) } } } func TestBuild_SkipsGitDir(t *testing.T) { root := t.TempDir() dir := "backend" base := filepath.Join(root, dir) if err := os.MkdirAll(filepath.Join(base, ".git"), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(base, "a.go"), []byte("x"), 0o644); err != nil { t.Fatal(err) } m, err := Build(root, dir) if err != nil { t.Fatal(err) } if len(m.Items) != 1 || m.Items[0].Path != "a.go" { t.Fatalf("Build should skip .git: %+v", m.Items) } }