ajhahn.de
← eeco
Go 257 lines
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)
	}
}