ajhahn.de
← eeco
Go 171 lines
package docs

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

func TestRefresh_ReplacesMarkerBlock(t *testing.T) {
	root := t.TempDir()
	p := Params{Project: filepath.Base(root), Version: "v2.8.0", HasUsage: true}
	if _, err := Scaffold(TargetReadme, root, false, p); err != nil {
		t.Fatalf("Scaffold: %v", err)
	}
	full := filepath.Join(root, "README.md")
	before, err := os.ReadFile(full)
	if err != nil {
		t.Fatal(err)
	}
	if !strings.Contains(string(before), "[docs/USAGE.md](docs/USAGE.md)") {
		t.Fatalf("scaffold missing initial USAGE link:\n%s", before)
	}
	if !strings.Contains(string(before), "<!-- eeco:docs:start -->") {
		t.Fatalf("scaffold missing start marker:\n%s", before)
	}

	// Add an operator-edited paragraph below the markers; refresh must
	// preserve it byte-identically.
	const operatorAddition = "\nOperator's free-form prose stays here.\n"
	if err := os.WriteFile(full, append(before, []byte(operatorAddition)...), 0o644); err != nil {
		t.Fatal(err)
	}

	// Refresh with HasArch flipped on — the See also list grows.
	p2 := Params{Project: filepath.Base(root), Version: "v2.8.0", HasUsage: true, HasArch: true}
	rep, err := Refresh(TargetReadme, root, p2)
	if err != nil {
		t.Fatalf("Refresh: %v", err)
	}
	if rep.Action != RefreshReplaced {
		t.Errorf("Action = %q, want %q", rep.Action, RefreshReplaced)
	}

	after, err := os.ReadFile(full)
	if err != nil {
		t.Fatal(err)
	}
	if !strings.Contains(string(after), "[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)") {
		t.Errorf("refreshed body missing new ARCHITECTURE link:\n%s", after)
	}
	if !strings.HasSuffix(string(after), operatorAddition) {
		t.Errorf("refresh discarded operator addition; tail:\n%s", after[max(0, len(after)-200):])
	}
}

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

	rep, err := Refresh(TargetReadme, root, Params{Project: "demo", Version: "v2.8.0", HasUsage: true})
	if err != nil {
		t.Fatalf("Refresh: %v", err)
	}
	if rep.Action != RefreshAppended {
		t.Errorf("Action = %q, want %q", rep.Action, RefreshAppended)
	}

	body, err := os.ReadFile(full)
	if err != nil {
		t.Fatal(err)
	}
	if !strings.HasPrefix(string(body), legacy) {
		t.Errorf("legacy prefix mutated:\n%s", body)
	}
	if !strings.Contains(string(body), "<!-- eeco:docs:start -->") {
		t.Errorf("auto-init missing start marker:\n%s", body)
	}
	if !strings.Contains(string(body), "<!-- eeco:docs:end -->") {
		t.Errorf("auto-init missing end marker:\n%s", body)
	}
	if !strings.Contains(string(body), "[docs/USAGE.md](docs/USAGE.md)") {
		t.Errorf("auto-init missing rendered body:\n%s", body)
	}
}

func TestRefresh_MissingFileRefuses(t *testing.T) {
	root := t.TempDir()
	_, err := Refresh(TargetReadme, root, Params{Project: "demo", Version: "v2.8.0"})
	if err == nil {
		t.Fatal("Refresh on missing file should error")
	}
	if !strings.Contains(err.Error(), "does not exist") {
		t.Errorf("error should hint at missing file, got %q", err)
	}
	if !strings.Contains(err.Error(), "eeco docs new") {
		t.Errorf("error should point to docs new, got %q", err)
	}
}

func TestRefresh_MalformedMarkersRefuse(t *testing.T) {
	root := t.TempDir()
	full := filepath.Join(root, "README.md")
	const bad = "# header\n<!-- eeco:docs:start -->\nbody\n<!-- eeco:docs:start -->\n"
	if err := os.WriteFile(full, []byte(bad), 0o644); err != nil {
		t.Fatal(err)
	}
	before, err := os.ReadFile(full)
	if err != nil {
		t.Fatal(err)
	}

	_, err = Refresh(TargetReadme, root, Params{Project: "demo", Version: "v2.8.0"})
	if err == nil {
		t.Fatal("Refresh on malformed markers should error")
	}
	if !strings.Contains(err.Error(), "nested") {
		t.Errorf("error should name the parse failure, got %q", err)
	}

	after, err := os.ReadFile(full)
	if err != nil {
		t.Fatal(err)
	}
	if string(after) != string(before) {
		t.Errorf("Refresh mutated file on parse error:\nbefore: %q\nafter:  %q", before, after)
	}
}

func TestRefresh_IgnoresFencedMarkers(t *testing.T) {
	root := t.TempDir()
	full := filepath.Join(root, "README.md")
	// Real marker pair after a fenced block that mentions the markers.
	body := "# header\n\n```\n<!-- eeco:docs:start -->\nfenced\n<!-- eeco:docs:end -->\n```\n\n<!-- eeco:docs:start -->\noriginal body\n<!-- eeco:docs:end -->\n\ntrailing operator note\n"
	if err := os.WriteFile(full, []byte(body), 0o644); err != nil {
		t.Fatal(err)
	}

	rep, err := Refresh(TargetReadme, root, Params{Project: "demo", Version: "v2.8.0", HasUsage: true})
	if err != nil {
		t.Fatalf("Refresh: %v", err)
	}
	if rep.Action != RefreshReplaced {
		t.Errorf("Action = %q, want %q", rep.Action, RefreshReplaced)
	}
	after, err := os.ReadFile(full)
	if err != nil {
		t.Fatal(err)
	}
	if !strings.Contains(string(after), "```\n<!-- eeco:docs:start -->\nfenced\n<!-- eeco:docs:end -->\n```") {
		t.Errorf("fenced markers were touched:\n%s", after)
	}
	if !strings.Contains(string(after), "trailing operator note") {
		t.Errorf("trailing operator note discarded:\n%s", after)
	}
	if strings.Contains(string(after), "original body") {
		t.Errorf("real block not replaced:\n%s", after)
	}
}

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