ajhahn.de
← eeco
Go 286 lines
package docs

import (
	"os"
	"path/filepath"
	"slices"
	"strings"
	"testing"
)

func TestAllTargets_ContainsVisionAndReadme(t *testing.T) {
	got := AllTargets()
	if len(got) == 0 {
		t.Fatal("AllTargets returned no targets")
	}
	if !slices.Contains(got, TargetVision) {
		t.Errorf("AllTargets missing TargetVision; got %v", got)
	}
	if !slices.Contains(got, TargetReadme) {
		t.Errorf("AllTargets missing TargetReadme; got %v", got)
	}
}

func TestTarget_Filename(t *testing.T) {
	cases := []struct {
		in   Target
		want string
	}{
		{TargetVision, "VISION.md"},
		{TargetReadme, "README.md"},
		{Target("bogus"), ""},
	}
	for _, tc := range cases {
		if got := tc.in.Filename(); got != tc.want {
			t.Errorf("Filename(%q) = %q, want %q", tc.in, got, tc.want)
		}
	}
}

func TestRender_VisionAllCompanionsPresent(t *testing.T) {
	p := Params{
		Project:   "demo",
		Version:   "v1.10.0",
		HasReadme: true,
		HasUsage:  true,
		HasArch:   true,
	}
	got, err := Render(TargetVision, p)
	if err != nil {
		t.Fatalf("Render: %v", err)
	}
	for _, want := range []string{
		"# demo — vision",
		"Scaffolded by `eeco docs new vision` (eeco v1.10.0)",
		"## What it gives you",
		"## Non-goals",
		"## Roadmap",
		"## See also",
		"[README.md](README.md)",
		"[docs/USAGE.md](docs/USAGE.md)",
		"[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)",
	} {
		if !strings.Contains(got, want) {
			t.Errorf("Render missing %q:\n%s", want, got)
		}
	}
}

func TestRender_VisionNoCompanions(t *testing.T) {
	got, err := Render(TargetVision, Params{Project: "lonely", Version: "v0.0.0-dev"})
	if err != nil {
		t.Fatalf("Render: %v", err)
	}
	for _, absent := range []string{
		"[README.md](README.md)",
		"[docs/USAGE.md](docs/USAGE.md)",
		"[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)",
	} {
		if strings.Contains(got, absent) {
			t.Errorf("Render included %q when companion was absent:\n%s", absent, got)
		}
	}
	if !strings.Contains(got, "(no related docs detected") {
		t.Errorf("Render missing empty-See-also placeholder:\n%s", got)
	}
}

func TestRender_Deterministic(t *testing.T) {
	p := Params{Project: "demo", Version: "v1.10.0", HasReadme: true}
	a, err := Render(TargetVision, p)
	if err != nil {
		t.Fatalf("Render a: %v", err)
	}
	b, err := Render(TargetVision, p)
	if err != nil {
		t.Fatalf("Render b: %v", err)
	}
	if a != b {
		t.Errorf("Render is not deterministic across calls with the same Params")
	}
}

func TestRender_UnknownTarget(t *testing.T) {
	if _, err := Render(Target("nope"), Params{}); err == nil {
		t.Fatal("expected error for unknown target")
	}
}

func TestScaffold_WritesVision(t *testing.T) {
	root := t.TempDir()
	p := Params{Project: filepath.Base(root), Version: "v1.10.0", HasReadme: true}
	written, err := Scaffold(TargetVision, root, false, p)
	if err != nil {
		t.Fatalf("Scaffold: %v", err)
	}
	if written != "VISION.md" {
		t.Errorf("written path = %q, want VISION.md", written)
	}
	body, err := os.ReadFile(filepath.Join(root, "VISION.md"))
	if err != nil {
		t.Fatalf("read written file: %v", err)
	}
	if !strings.Contains(string(body), "— vision") {
		t.Errorf("written file missing vision heading:\n%s", body)
	}
}

func TestScaffold_RefusesExisting(t *testing.T) {
	root := t.TempDir()
	full := filepath.Join(root, "VISION.md")
	if err := os.WriteFile(full, []byte("operator content\n"), 0o644); err != nil {
		t.Fatal(err)
	}

	_, err := Scaffold(TargetVision, root, false, Params{Project: "demo"})
	if err == nil {
		t.Fatal("Scaffold should refuse to overwrite an existing file")
	}
	if !strings.Contains(err.Error(), "already exists") || !strings.Contains(err.Error(), "--overwrite") {
		t.Errorf("error should name file + flag, got %q", err)
	}
	// The pre-existing content must be intact.
	body, err := os.ReadFile(full)
	if err != nil {
		t.Fatal(err)
	}
	if string(body) != "operator content\n" {
		t.Errorf("Scaffold mutated the existing file: %q", body)
	}
}

func TestScaffold_OverwriteReplaces(t *testing.T) {
	root := t.TempDir()
	full := filepath.Join(root, "VISION.md")
	if err := os.WriteFile(full, []byte("stale content\n"), 0o644); err != nil {
		t.Fatal(err)
	}

	if _, err := Scaffold(TargetVision, root, true, Params{Project: "demo", Version: "v1.10.0"}); err != nil {
		t.Fatalf("Scaffold --overwrite: %v", err)
	}
	body, err := os.ReadFile(full)
	if err != nil {
		t.Fatal(err)
	}
	if !strings.Contains(string(body), "demo — vision") {
		t.Errorf("overwrite did not render new template:\n%s", body)
	}
}

func TestScaffold_UnknownTarget(t *testing.T) {
	root := t.TempDir()
	if _, err := Scaffold(Target("nope"), root, false, Params{}); err == nil {
		t.Fatal("expected error for unknown target")
	}
}

func TestRender_ReadmeAllCompanionsPresent(t *testing.T) {
	p := Params{
		Project:   "demo",
		Version:   "v2.6.0",
		HasReadme: true,
		HasUsage:  true,
		HasArch:   true,
	}
	got, err := Render(TargetReadme, p)
	if err != nil {
		t.Fatalf("Render: %v", err)
	}
	for _, want := range []string{
		"# demo",
		"Scaffolded by `eeco docs new readme` (eeco v2.6.0)",
		"## Quick start",
		"## How it works",
		"## See also",
		"[docs/USAGE.md](docs/USAGE.md)",
		"[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)",
	} {
		if !strings.Contains(got, want) {
			t.Errorf("Render missing %q:\n%s", want, got)
		}
	}
	if strings.Contains(got, "[README.md](README.md)") {
		t.Errorf("readme template must not self-link to README.md:\n%s", got)
	}
}

func TestRender_ReadmeNoCompanions(t *testing.T) {
	got, err := Render(TargetReadme, Params{Project: "lonely", Version: "v0.0.0-dev"})
	if err != nil {
		t.Fatalf("Render: %v", err)
	}
	for _, absent := range []string{
		"[docs/USAGE.md](docs/USAGE.md)",
		"[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)",
	} {
		if strings.Contains(got, absent) {
			t.Errorf("Render included %q when companion was absent:\n%s", absent, got)
		}
	}
	if !strings.Contains(got, "(no related docs detected") {
		t.Errorf("Render missing empty-See-also placeholder:\n%s", got)
	}
}

func TestScaffold_WritesReadme(t *testing.T) {
	root := t.TempDir()
	p := Params{Project: filepath.Base(root), Version: "v2.6.0", HasUsage: true}
	written, err := Scaffold(TargetReadme, root, false, p)
	if err != nil {
		t.Fatalf("Scaffold: %v", err)
	}
	if written != "README.md" {
		t.Errorf("written path = %q, want README.md", written)
	}
	body, err := os.ReadFile(filepath.Join(root, "README.md"))
	if err != nil {
		t.Fatalf("read written file: %v", err)
	}
	if !strings.Contains(string(body), "## Quick start") {
		t.Errorf("written file missing Quick start heading:\n%s", body)
	}
}

func TestScaffold_ReadmeRefusesExisting(t *testing.T) {
	root := t.TempDir()
	full := filepath.Join(root, "README.md")
	if err := os.WriteFile(full, []byte("operator content\n"), 0o644); err != nil {
		t.Fatal(err)
	}

	_, err := Scaffold(TargetReadme, root, false, Params{Project: "demo"})
	if err == nil {
		t.Fatal("Scaffold should refuse to overwrite an existing README.md")
	}
	if !strings.Contains(err.Error(), "already exists") || !strings.Contains(err.Error(), "--overwrite") {
		t.Errorf("error should name file + flag, got %q", err)
	}
	body, err := os.ReadFile(full)
	if err != nil {
		t.Fatal(err)
	}
	if string(body) != "operator content\n" {
		t.Errorf("Scaffold mutated the existing file: %q", body)
	}
}

func TestScaffold_ReadmeOverwriteReplaces(t *testing.T) {
	root := t.TempDir()
	full := filepath.Join(root, "README.md")
	if err := os.WriteFile(full, []byte("stale content\n"), 0o644); err != nil {
		t.Fatal(err)
	}

	if _, err := Scaffold(TargetReadme, root, true, Params{Project: "demo", Version: "v2.6.0"}); err != nil {
		t.Fatalf("Scaffold --overwrite: %v", err)
	}
	body, err := os.ReadFile(full)
	if err != nil {
		t.Fatal(err)
	}
	if !strings.Contains(string(body), "## Quick start") {
		t.Errorf("overwrite did not render new template:\n%s", body)
	}
}