ajhahn.de
← eeco
Go 972 lines
package brief

import (
	"encoding/json"
	"flag"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/ajhahnde/eeco/internal/config"
	"github.com/ajhahnde/eeco/internal/memory"
	"github.com/ajhahnde/eeco/internal/notes"
)

// updateGolden rewrites the golden file instead of comparing it. Run
// `go test ./internal/brief/ -update` after an intentional brief change
// and commit the regenerated golden with the code.
var updateGolden = flag.Bool("update", false, "rewrite golden files under testdata/")

// TestMain pins the workspace owner so config.Load resolves a
// deterministic username across machines. The workspace then lives at
// <root>/tester/.eeco, which the golden fixtures encode.
func TestMain(m *testing.M) {
	os.Setenv("EECO_USERNAME", "tester")
	gdir, err := os.MkdirTemp("", "eeco-global-")
	if err != nil {
		panic(err)
	}
	os.Setenv(config.GlobalConfigEnv, gdir)
	code := m.Run()
	os.RemoveAll(gdir)
	os.Exit(code)
}

func TestRender_Golden(t *testing.T) {
	cfg := sampleRepo(t)
	seedSample(t, cfg)

	got, err := Render(cfg)
	if err != nil {
		t.Fatalf("Render: %v", err)
	}

	golden := filepath.Join("testdata", "brief.golden")
	if *updateGolden {
		if err := os.MkdirAll("testdata", 0o755); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(golden, []byte(got), 0o644); err != nil {
			t.Fatal(err)
		}
		return
	}
	raw, err := os.ReadFile(golden)
	if err != nil {
		t.Fatalf("read golden %s (run with -update to create): %v", golden, err)
	}
	// Git for Windows can rewrite LF to CRLF on checkout when
	// .gitattributes is not honoured; normalise so the golden still
	// matches.
	want := strings.ReplaceAll(string(raw), "\r\n", "\n")
	if want != got {
		t.Errorf("brief differs from %s — re-run with -update if intentional.\n--- want ---\n%s\n--- got ---\n%s", golden, want, got)
	}
}

func TestRender_NoWorkspace(t *testing.T) {
	root := filepath.Join(t.TempDir(), "bare")
	mkdirs(t, root, ".git", "src")
	writeFile(t, filepath.Join(root, "go.mod"), "module bare\n")
	cfg, err := config.Load(root, config.DefaultWorkspace)
	if err != nil {
		t.Fatalf("config.Load: %v", err)
	}
	got, err := Render(cfg)
	if err != nil {
		t.Fatalf("Render: %v", err)
	}
	for _, want := range []string{
		"## Working with eeco",
		"## Project",
		"Workspace not initialised — run `eeco init`",
		"Workspace not initialised — no project memory yet.",
		"Workspace not initialised — no queue yet.",
	} {
		if !strings.Contains(got, want) {
			t.Errorf("no-workspace brief missing %q:\n%s", want, got)
		}
	}
}

func TestRender_NilConfig(t *testing.T) {
	if _, err := Render(nil); err == nil {
		t.Fatal("Render(nil) should return an error")
	}
}

// sampleRepo builds a deterministic initialised repository: a fake .git
// marker, a go.mod (go profile), two tracked top-level directories, and
// a scaffolded eeco workspace. The fake .git makes the tracked-set
// lookup fall back to a directory listing, which keeps the fixture
// independent of a real git checkout.
func sampleRepo(t *testing.T) *config.Config {
	t.Helper()
	root := filepath.Join(t.TempDir(), "sample")
	// EECO_USERNAME=tester (pinned in TestMain) scopes the workspace under
	// <root>/tester/.eeco, so the scaffolded subdirs must live there too
	// for IsInitialized to see an initialised workspace.
	mkdirs(t, root, ".git", "cmd", "docs",
		filepath.Join("tester", config.DefaultWorkspace, "engine"),
		filepath.Join("tester", config.DefaultWorkspace, "memory"),
		filepath.Join("tester", config.DefaultWorkspace, "workflows"),
		filepath.Join("tester", config.DefaultWorkspace, "state"),
		filepath.Join("tester", config.DefaultWorkspace, "docs"),
	)
	writeFile(t, filepath.Join(root, "go.mod"), "module sample\n\ngo 1.24\n")
	cfg, err := config.Load(root, config.DefaultWorkspace)
	if err != nil {
		t.Fatalf("config.Load: %v", err)
	}
	return cfg
}

func mkdirs(t *testing.T, root string, subs ...string) {
	t.Helper()
	for _, s := range subs {
		if err := os.MkdirAll(filepath.Join(root, s), 0o755); err != nil {
			t.Fatal(err)
		}
	}
}

func writeFile(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)
	}
}

// seedSample populates cfg's workspace with a deterministic set of
// memory facts and queue items — the shared fixture behind the Markdown
// and JSON brief golden tests, so the two always describe one state.
func seedSample(t *testing.T, cfg *config.Config) {
	t.Helper()
	store, err := memory.Open(cfg)
	if err != nil {
		t.Fatalf("memory.Open: %v", err)
	}
	day := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
	for _, f := range []*memory.Fact{
		{Name: "api-docs", Description: "external API reference", Type: memory.TypeReference, Created: day, LastUsed: day, Ref: "docs/api.md"},
		{Name: "auth-flow", Description: "auth flow lives here", Type: memory.TypeProject, Created: day, LastUsed: day, Ref: "internal/auth/auth.go"},
		{Name: "terse-comments", Description: "keep comments terse", Type: memory.TypeFeedback, Created: day, LastUsed: day},
	} {
		if err := store.Save(f); err != nil {
			t.Fatalf("save %s: %v", f.Name, err)
		}
	}
	writeFile(t, filepath.Join(cfg.Workspace, "state", "queue.md"),
		"- [ ] **finding** — comment-hygiene flagged a tooling string _(sample, 2026-01-01)_\n"+
			"  internal/auth/auth.go:12\n"+
			"- [ ] **handover** — draft handover ready for review _(sample, 2026-01-01)_\n"+
			"  state/parked/handover.md\n")
}

func TestEstimateTokens(t *testing.T) {
	for _, tc := range []struct {
		in, want int
	}{
		{0, 0},
		{4, 1},
		{7, 1},
		{8, 2},
		{4000, 1000},
	} {
		if got := EstimateTokens(tc.in); got != tc.want {
			t.Errorf("EstimateTokens(%d) = %d, want %d", tc.in, got, tc.want)
		}
	}
}

func TestMeasure_PinnedClock(t *testing.T) {
	cfg := sampleRepo(t)
	seedSample(t, cfg)

	// nowFunc is read once at the start of the timed window and once at
	// the end; pin the two reads to a fixed 5ms gap and assert Elapsed
	// exactly — the determinism proof for the timing readout.
	start := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
	calls := 0
	nowFunc = func() time.Time {
		calls++
		if calls == 1 {
			return start
		}
		return start.Add(5 * time.Millisecond)
	}
	t.Cleanup(func() { nowFunc = time.Now })

	_, m, err := Measure(cfg, false)
	if err != nil {
		t.Fatalf("Measure: %v", err)
	}
	if m.Elapsed != 5*time.Millisecond {
		t.Errorf("Elapsed = %s, want 5ms", m.Elapsed)
	}
	if calls != 2 {
		t.Errorf("nowFunc called %d times, want 2 (one per window edge)", calls)
	}
}

func TestMeasure_KnowledgeBytesExact(t *testing.T) {
	cfg := sampleRepo(t)
	store, err := memory.Open(cfg)
	if err != nil {
		t.Fatalf("memory.Open: %v", err)
	}
	day := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
	// Three facts, one disabled — disabled facts are real on-disk bytes
	// the brief omits, so they must still count toward the baseline.
	for _, f := range []*memory.Fact{
		{Name: "alpha", Description: "first fact", Type: memory.TypeProject, Created: day, LastUsed: day, Ref: "a.go"},
		{Name: "beta", Description: "second fact", Type: memory.TypeFeedback, Created: day, LastUsed: day},
		{Name: "gamma", Description: "muted fact", Type: memory.TypeProject, Created: day, LastUsed: day, Disabled: true},
	} {
		if err := store.Save(f); err != nil {
			t.Fatalf("save %s: %v", f.Name, err)
		}
	}
	queuePath := filepath.Join(cfg.Workspace, "state", "queue.md")
	writeFile(t, queuePath, "- [ ] **finding** — weigh this _(sample, 2026-01-01)_\n")

	// Sum the same files independently and assert the helper matches.
	var want int64
	for _, name := range []string{"alpha.md", "beta.md", "gamma.md"} {
		fi, err := os.Stat(filepath.Join(cfg.Workspace, "memory", name))
		if err != nil {
			t.Fatalf("stat %s: %v", name, err)
		}
		want += fi.Size()
	}
	qi, err := os.Stat(queuePath)
	if err != nil {
		t.Fatalf("stat queue: %v", err)
	}
	want += qi.Size()

	_, m, err := Measure(cfg, false)
	if err != nil {
		t.Fatalf("Measure: %v", err)
	}
	if int64(m.KnowledgeBytes) != want {
		t.Errorf("KnowledgeBytes = %d, want %d (3 facts incl. disabled + queue)", m.KnowledgeBytes, want)
	}
	full, err := Render(cfg)
	if err != nil {
		t.Fatalf("Render: %v", err)
	}
	if m.BriefBytes != len(full) {
		t.Errorf("BriefBytes = %d, want %d (len of Render output)", m.BriefBytes, len(full))
	}
}

func TestMeasure_CompressionMath(t *testing.T) {
	cfg := sampleRepo(t)
	seedSample(t, cfg)

	_, m, err := Measure(cfg, false)
	if err != nil {
		t.Fatalf("Measure: %v", err)
	}
	// The readout's token figures and percentage are pure functions of
	// the measured bytes; recompute them here so the format helper can be
	// trusted to present the same numbers.
	if got := EstimateTokens(m.BriefBytes); got != m.BriefBytes/4 {
		t.Errorf("brief tokens = %d, want %d", got, m.BriefBytes/4)
	}
	if m.KnowledgeBytes <= 0 {
		t.Fatalf("fixture should distil real knowledge bytes, got %d", m.KnowledgeBytes)
	}
	wantPct := max(0, (m.KnowledgeBytes-m.BriefBytes)*100/m.KnowledgeBytes)
	if wantPct > 100 {
		t.Errorf("computed savedPct %d out of range", wantPct)
	}
}

func TestMeasure_BriefVariant(t *testing.T) {
	cfg := sampleRepo(t)
	seedBig(t, cfg)

	text, m, err := Measure(cfg, true)
	if err != nil {
		t.Fatalf("Measure: %v", err)
	}
	wantText, err := RenderBrief(cfg)
	if err != nil {
		t.Fatalf("RenderBrief: %v", err)
	}
	if text != wantText {
		t.Errorf("Measure(cfg,true) text != RenderBrief(cfg)")
	}
	full, _, err := Measure(cfg, false)
	if err != nil {
		t.Fatalf("Measure full: %v", err)
	}
	if m.BriefBytes >= len(full) {
		t.Errorf("brief variant BriefBytes %d should be smaller than full %d", m.BriefBytes, len(full))
	}
}

func TestMeasure_NoWorkspace(t *testing.T) {
	root := filepath.Join(t.TempDir(), "bare")
	mkdirs(t, root, ".git", "src")
	writeFile(t, filepath.Join(root, "go.mod"), "module bare\n")
	cfg, err := config.Load(root, config.DefaultWorkspace)
	if err != nil {
		t.Fatalf("config.Load: %v", err)
	}
	_, m, err := Measure(cfg, false)
	if err != nil {
		t.Fatalf("Measure: %v", err)
	}
	if m.KnowledgeBytes != 0 {
		t.Errorf("uninitialised workspace KnowledgeBytes = %d, want 0", m.KnowledgeBytes)
	}
	if m.BriefBytes <= 0 {
		t.Errorf("BriefBytes = %d, want a non-empty brief even without a workspace", m.BriefBytes)
	}
}

func TestMeasure_NilConfig(t *testing.T) {
	if _, _, err := Measure(nil, false); err == nil {
		t.Fatal("Measure(nil, …) should return an error")
	}
}

func TestMeasure_TextMatchesRender(t *testing.T) {
	cfg := sampleRepo(t)
	seedSample(t, cfg)
	text, _, err := Measure(cfg, false)
	if err != nil {
		t.Fatalf("Measure: %v", err)
	}
	want, err := Render(cfg)
	if err != nil {
		t.Fatalf("Render: %v", err)
	}
	if text != want {
		t.Errorf("Measure(cfg,false) text != Render(cfg) — metrics must never perturb the brief")
	}
}

func TestRenderJSON_Golden(t *testing.T) {
	cfg := sampleRepo(t)
	seedSample(t, cfg)

	got, err := RenderJSON(cfg)
	if err != nil {
		t.Fatalf("RenderJSON: %v", err)
	}
	if !json.Valid([]byte(got)) {
		t.Fatalf("RenderJSON output is not valid JSON:\n%s", got)
	}

	golden := filepath.Join("testdata", "brief.json.golden")
	if *updateGolden {
		if err := os.MkdirAll("testdata", 0o755); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(golden, []byte(got), 0o644); err != nil {
			t.Fatal(err)
		}
		return
	}
	raw, err := os.ReadFile(golden)
	if err != nil {
		t.Fatalf("read golden %s (run with -update to create): %v", golden, err)
	}
	want := strings.ReplaceAll(string(raw), "\r\n", "\n")
	if want != got {
		t.Errorf("JSON brief differs from %s — re-run with -update if intentional.\n--- want ---\n%s\n--- got ---\n%s", golden, want, got)
	}
}

func TestRenderJSON_NoWorkspace(t *testing.T) {
	root := filepath.Join(t.TempDir(), "bare")
	mkdirs(t, root, ".git", "src")
	writeFile(t, filepath.Join(root, "go.mod"), "module bare\n")
	cfg, err := config.Load(root, config.DefaultWorkspace)
	if err != nil {
		t.Fatalf("config.Load: %v", err)
	}
	got, err := RenderJSON(cfg)
	if err != nil {
		t.Fatalf("RenderJSON: %v", err)
	}
	var d Data
	if err := json.Unmarshal([]byte(got), &d); err != nil {
		t.Fatalf("unmarshal: %v\n%s", err, got)
	}
	if d.Initialized {
		t.Error("no-workspace brief should report initialized=false")
	}
	if d.Profile != "go" {
		t.Errorf("profile = %q, want go", d.Profile)
	}
	// Slice fields must marshal as an empty list, never null, so a
	// consumer can iterate without a nil check.
	for _, want := range []string{`"where_to_look": []`, `"knowledge": []`, `"open_decisions": []`} {
		if !strings.Contains(got, want) {
			t.Errorf("no-workspace JSON missing %s:\n%s", want, got)
		}
	}
}

func TestRenderJSON_NilConfig(t *testing.T) {
	if _, err := RenderJSON(nil); err == nil {
		t.Fatal("RenderJSON(nil) should return an error")
	}
}

func TestRenderBrief_Golden(t *testing.T) {
	cfg := sampleRepo(t)
	seedSample(t, cfg)

	got, err := RenderBrief(cfg)
	if err != nil {
		t.Fatalf("RenderBrief: %v", err)
	}

	golden := filepath.Join("testdata", "brief.brief.golden")
	if *updateGolden {
		if err := os.MkdirAll("testdata", 0o755); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(golden, []byte(got), 0o644); err != nil {
			t.Fatal(err)
		}
		return
	}
	raw, err := os.ReadFile(golden)
	if err != nil {
		t.Fatalf("read golden %s (run with -update to create): %v", golden, err)
	}
	want := strings.ReplaceAll(string(raw), "\r\n", "\n")
	if want != got {
		t.Errorf("brief differs from %s — re-run with -update if intentional.\n--- want ---\n%s\n--- got ---\n%s", golden, want, got)
	}
}

func TestRenderBrief_StripsPreambleAndOutro(t *testing.T) {
	cfg := sampleRepo(t)
	seedSample(t, cfg)
	got, err := RenderBrief(cfg)
	if err != nil {
		t.Fatalf("RenderBrief: %v", err)
	}
	for _, dropped := range []string{
		"## Working with eeco",
		"## Recording back",
	} {
		if strings.Contains(got, dropped) {
			t.Errorf("brief should omit %q in --brief mode:\n%s", dropped, got)
		}
	}
	for _, kept := range []string{
		"## Project",
		"## Where to look",
		"## What eeco knows",
		"## Open decisions",
	} {
		if !strings.Contains(got, kept) {
			t.Errorf("brief should keep %q in --brief mode:\n%s", kept, got)
		}
	}
}

func TestRenderJSONBrief_Golden(t *testing.T) {
	cfg := sampleRepo(t)
	seedSample(t, cfg)

	got, err := RenderJSONBrief(cfg)
	if err != nil {
		t.Fatalf("RenderJSONBrief: %v", err)
	}
	if !json.Valid([]byte(got)) {
		t.Fatalf("RenderJSONBrief output is not valid JSON:\n%s", got)
	}

	golden := filepath.Join("testdata", "brief.brief.json.golden")
	if *updateGolden {
		if err := os.MkdirAll("testdata", 0o755); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(golden, []byte(got), 0o644); err != nil {
			t.Fatal(err)
		}
		return
	}
	raw, err := os.ReadFile(golden)
	if err != nil {
		t.Fatalf("read golden %s (run with -update to create): %v", golden, err)
	}
	want := strings.ReplaceAll(string(raw), "\r\n", "\n")
	if want != got {
		t.Errorf("JSON brief differs from %s — re-run with -update if intentional.\n--- want ---\n%s\n--- got ---\n%s", golden, want, got)
	}
}

func TestRenderJSONBrief_KeepsFrozenKeys(t *testing.T) {
	cfg := sampleRepo(t)
	seedSample(t, cfg)
	got, err := RenderJSONBrief(cfg)
	if err != nil {
		t.Fatalf("RenderJSONBrief: %v", err)
	}
	// Constraint: the nine frozen top-level keys remain present after a
	// brief trim — arrays may be shorter, never absent or null.
	for _, key := range []string{
		`"project"`, `"profile"`, `"gate"`, `"top_level"`, `"initialized"`,
		`"workflows"`, `"where_to_look"`, `"knowledge"`, `"open_decisions"`,
	} {
		if !strings.Contains(got, key) {
			t.Errorf("brief JSON missing frozen key %s:\n%s", key, got)
		}
	}
	// The BriefMode flag is rendering metadata, not project state, so
	// the json:"-" tag must hide it from the JSON brief.
	if strings.Contains(got, "BriefMode") || strings.Contains(got, `"brief_mode"`) {
		t.Errorf("brief JSON leaks the BriefMode flag:\n%s", got)
	}
}

func TestTrimToBrief_CapsLists(t *testing.T) {
	d := Data{
		WhereToLook:   make([]Pointer, 8),
		Knowledge:     make([]KnowledgeFact, 9),
		OpenDecisions: make([]string, 7),
	}
	d.TrimToBrief()
	if !d.BriefMode {
		t.Error("TrimToBrief should set BriefMode")
	}
	if got := len(d.WhereToLook); got != briefCap {
		t.Errorf("WhereToLook len = %d, want %d", got, briefCap)
	}
	if got := len(d.Knowledge); got != briefCap {
		t.Errorf("Knowledge len = %d, want %d", got, briefCap)
	}
	if got := len(d.OpenDecisions); got != briefCap {
		t.Errorf("OpenDecisions len = %d, want %d", got, briefCap)
	}
}

func TestTrimToBrief_LeavesShortListsAlone(t *testing.T) {
	d := Data{
		WhereToLook:   make([]Pointer, 2),
		Knowledge:     make([]KnowledgeFact, 1),
		OpenDecisions: make([]string, 3),
	}
	d.TrimToBrief()
	if len(d.WhereToLook) != 2 || len(d.Knowledge) != 1 || len(d.OpenDecisions) != 3 {
		t.Errorf("TrimToBrief truncated below-cap lists: %+v", d)
	}
}

// seedBig populates cfg's workspace with a dozen ref-carrying project
// facts and a dozen queue items — a fixture large enough that the full
// brief, the brief form, and the lower trim-ladder caps each render to a
// distinct size, so the RenderWithinBudget ladder can be exercised.
func seedBig(t *testing.T, cfg *config.Config) {
	t.Helper()
	store, err := memory.Open(cfg)
	if err != nil {
		t.Fatalf("memory.Open: %v", err)
	}
	day := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
	for i := range 12 {
		f := &memory.Fact{
			Name:        fmt.Sprintf("fact-%02d", i),
			Description: fmt.Sprintf("load-bearing project fact number %02d", i),
			Type:        memory.TypeProject,
			Created:     day,
			LastUsed:    day,
			Ref:         fmt.Sprintf("internal/pkg%02d/file.go", i),
		}
		if err := store.Save(f); err != nil {
			t.Fatalf("save %s: %v", f.Name, err)
		}
	}
	var qb strings.Builder
	for i := range 12 {
		fmt.Fprintf(&qb,
			"- [ ] **finding** — open decision number %02d to weigh _(sample, 2026-01-01)_\n  internal/pkg%02d/file.go:1\n",
			i, i)
	}
	writeFile(t, filepath.Join(cfg.Workspace, "state", "queue.md"), qb.String())
}

func TestRenderWithinBudget_NoCapReturnsFull(t *testing.T) {
	cfg := sampleRepo(t)
	seedSample(t, cfg)

	got, rep, err := RenderWithinBudget(cfg, 0, false)
	if err != nil {
		t.Fatalf("RenderWithinBudget: %v", err)
	}
	full, err := Render(cfg)
	if err != nil {
		t.Fatalf("Render: %v", err)
	}
	if got != full {
		t.Errorf("no-cap budget brief should equal Render output\n--- got ---\n%s\n--- want ---\n%s", got, full)
	}
	if rep.Tier != "full" || !rep.Met || rep.Bytes != len(got) {
		t.Errorf("report = %+v, want {full %d true}", rep, len(got))
	}
}

func TestRenderWithinBudget_NoCapSkipFullReturnsBrief(t *testing.T) {
	cfg := sampleRepo(t)
	seedSample(t, cfg)

	got, rep, err := RenderWithinBudget(cfg, 0, true)
	if err != nil {
		t.Fatalf("RenderWithinBudget: %v", err)
	}
	brief, err := RenderBrief(cfg)
	if err != nil {
		t.Fatalf("RenderBrief: %v", err)
	}
	if got != brief {
		t.Errorf("no-cap skipFull brief should equal RenderBrief output")
	}
	if rep.Tier != "brief" || !rep.Met {
		t.Errorf("report = %+v, want brief/met", rep)
	}
}

func TestRenderWithinBudget_FullFitsLargeBudget(t *testing.T) {
	cfg := sampleRepo(t)
	seedBig(t, cfg)

	full, err := Render(cfg)
	if err != nil {
		t.Fatalf("Render: %v", err)
	}
	got, rep, err := RenderWithinBudget(cfg, len(full)+1024, false)
	if err != nil {
		t.Fatalf("RenderWithinBudget: %v", err)
	}
	if got != full {
		t.Errorf("a budget above the full size should return the full brief")
	}
	if rep.Tier != "full" || !rep.Met {
		t.Errorf("report = %+v, want full/met", rep)
	}
}

func TestRenderWithinBudget_StepsDown(t *testing.T) {
	cfg := sampleRepo(t)
	seedBig(t, cfg)

	full, err := Render(cfg)
	if err != nil {
		t.Fatalf("Render: %v", err)
	}
	brief, err := RenderBrief(cfg)
	if err != nil {
		t.Fatalf("RenderBrief: %v", err)
	}
	if len(brief) >= len(full) {
		t.Fatalf("fixture not discriminating: brief %d not smaller than full %d", len(brief), len(full))
	}

	validTier := func(s string) bool {
		if s == "full" || s == "brief" {
			return true
		}
		return strings.HasPrefix(s, "brief (cap ")
	}

	// Walk the budget down from above-full to below-the-smallest-tier.
	// Whenever the budget is met the brief must fit, the tier name must
	// be from the known set, and a tighter budget must never yield a
	// larger brief than a looser one.
	prevLen := len(full) + 1
	for budget := len(full) + 64; budget >= 1; budget -= 32 {
		got, rep, err := RenderWithinBudget(cfg, budget, false)
		if err != nil {
			t.Fatalf("RenderWithinBudget(%d): %v", budget, err)
		}
		if !validTier(rep.Tier) {
			t.Errorf("budget %d: unexpected tier %q", budget, rep.Tier)
		}
		if rep.Bytes != len(got) {
			t.Errorf("budget %d: report Bytes %d != len(got) %d", budget, rep.Bytes, len(got))
		}
		if rep.Met && len(got) > budget {
			t.Errorf("budget %d: Met but brief is %d bytes", budget, len(got))
		}
		if len(got) > prevLen {
			t.Errorf("budget %d: brief grew to %d bytes as the budget tightened (prev %d)", budget, len(got), prevLen)
		}
		prevLen = len(got)
	}
}

func TestRenderWithinBudget_ImpossibleBudget(t *testing.T) {
	cfg := sampleRepo(t)
	seedBig(t, cfg)

	got, rep, err := RenderWithinBudget(cfg, 1, false)
	if err != nil {
		t.Fatalf("RenderWithinBudget: %v", err)
	}
	if rep.Met {
		t.Error("a 1-byte budget cannot be met")
	}
	if rep.Tier != "brief (cap 0)" {
		t.Errorf("Tier = %q, want \"brief (cap 0)\"", rep.Tier)
	}
	if got == "" {
		t.Error("the smallest brief should still be returned, not an empty string")
	}
	// The smallest tier drops every per-section list but keeps the
	// fixed scaffolding, so a real brief is still written.
	if !strings.Contains(got, "## Project") {
		t.Errorf("cap-0 brief missing the Project section:\n%s", got)
	}
}

func TestRenderWithinBudget_SkipFullExcludesFull(t *testing.T) {
	cfg := sampleRepo(t)
	seedBig(t, cfg)

	full, err := Render(cfg)
	if err != nil {
		t.Fatalf("Render: %v", err)
	}
	got, rep, err := RenderWithinBudget(cfg, len(full)+1024, true)
	if err != nil {
		t.Fatalf("RenderWithinBudget: %v", err)
	}
	if got == full {
		t.Error("skipFull should never return the full brief, even under a large budget")
	}
	if rep.Tier != "brief" {
		t.Errorf("Tier = %q, want brief", rep.Tier)
	}
}

func TestRenderWithinBudget_NilConfig(t *testing.T) {
	if _, _, err := RenderWithinBudget(nil, 0, false); err == nil {
		t.Fatal("RenderWithinBudget(nil, …) should return an error")
	}
}

// seedNotes drops a deterministic set of three notes into cfg's
// workspace, oldest first; List returns them newest first, which the
// with-notes golden encodes.
func seedNotes(t *testing.T, cfg *config.Config) {
	t.Helper()
	notesDir := filepath.Join(cfg.Workspace, "notes")
	fixtures := []struct {
		when time.Time
		text string
	}{
		{time.Date(2026, 1, 1, 11, 15, 0, 0, time.UTC), "Sketch refactor for module split"},
		{time.Date(2026, 1, 2, 14, 30, 0, 0, time.UTC), "Open question about hook chain"},
		{time.Date(2026, 1, 3, 9, 0, 0, 0, time.UTC), "Investigate cache eviction"},
	}
	for _, n := range fixtures {
		if _, err := notes.Add(notesDir, n.text, n.when); err != nil {
			t.Fatalf("notes.Add: %v", err)
		}
	}
}

func TestRender_DefaultExcludesNotes(t *testing.T) {
	cfg := sampleRepo(t)
	seedSample(t, cfg)
	seedNotes(t, cfg)
	// cfg.BriefIncludeNotes is the zero value (false): the section must
	// stay out of the Markdown brief even though notes live on disk, so
	// bare `eeco go` is byte-identical to the notes-free output.
	got, err := Render(cfg)
	if err != nil {
		t.Fatalf("Render: %v", err)
	}
	if strings.Contains(got, "## Recent notes") {
		t.Errorf("default-off brief should not include the Recent notes section:\n%s", got)
	}
}

func TestRender_WithNotes_Golden(t *testing.T) {
	cfg := sampleRepo(t)
	seedSample(t, cfg)
	seedNotes(t, cfg)
	cfg.BriefIncludeNotes = true

	got, err := Render(cfg)
	if err != nil {
		t.Fatalf("Render: %v", err)
	}

	golden := filepath.Join("testdata", "brief.with_notes.golden")
	if *updateGolden {
		if err := os.MkdirAll("testdata", 0o755); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(golden, []byte(got), 0o644); err != nil {
			t.Fatal(err)
		}
		return
	}
	raw, err := os.ReadFile(golden)
	if err != nil {
		t.Fatalf("read golden %s (run with -update to create): %v", golden, err)
	}
	want := strings.ReplaceAll(string(raw), "\r\n", "\n")
	if want != got {
		t.Errorf("with-notes brief differs from %s — re-run with -update if intentional.\n--- want ---\n%s\n--- got ---\n%s", golden, want, got)
	}
}

func TestRender_WithNotesEnabledNoNotesOnDisk(t *testing.T) {
	cfg := sampleRepo(t)
	seedSample(t, cfg)
	cfg.BriefIncludeNotes = true

	got, err := Render(cfg)
	if err != nil {
		t.Fatalf("Render: %v", err)
	}
	if !strings.Contains(got, "## Recent notes") {
		t.Errorf("enabled brief should still emit the section header when no notes exist:\n%s", got)
	}
	if !strings.Contains(got, "No notes recorded yet") {
		t.Errorf("enabled brief with no notes should print the empty-state message:\n%s", got)
	}
}

func TestRenderJSON_WithNotesEnabledKeepsFrozenKeys(t *testing.T) {
	cfg := sampleRepo(t)
	seedSample(t, cfg)
	seedNotes(t, cfg)
	cfg.BriefIncludeNotes = true

	got, err := RenderJSON(cfg)
	if err != nil {
		t.Fatalf("RenderJSON: %v", err)
	}
	if !json.Valid([]byte(got)) {
		t.Fatalf("RenderJSON output is not valid JSON:\n%s", got)
	}
	// The nine frozen top-level keys must remain — adding the notes
	// surface must not leak a tenth key into the public JSON contract.
	for _, key := range []string{
		`"project"`, `"profile"`, `"gate"`, `"top_level"`, `"initialized"`,
		`"workflows"`, `"where_to_look"`, `"knowledge"`, `"open_decisions"`,
	} {
		if !strings.Contains(got, key) {
			t.Errorf("JSON brief missing frozen key %s:\n%s", key, got)
		}
	}
	for _, leak := range []string{`"notes"`, `"Notes"`, `"IncludeNotes"`, `"include_notes"`} {
		if strings.Contains(got, leak) {
			t.Errorf("JSON brief leaked %s — notes belong to the Markdown channel only:\n%s", leak, got)
		}
	}
}

func TestTrimToCap_CapsNotes(t *testing.T) {
	d := Data{
		Notes: make([]notes.Note, 8),
	}
	d.trimToCap(briefCap)
	if got := len(d.Notes); got != briefCap {
		t.Errorf("Notes len = %d, want %d", got, briefCap)
	}
	d.Notes = make([]notes.Note, briefCap)
	d.trimToCap(briefCap)
	if got := len(d.Notes); got != briefCap {
		t.Errorf("Notes len = %d, want %d (no-op at cap)", got, briefCap)
	}
	d.Notes = make([]notes.Note, 2)
	d.trimToCap(briefCap)
	if got := len(d.Notes); got != 2 {
		t.Errorf("Notes len = %d, want 2 (below-cap left alone)", got)
	}
}

func TestCollect_NotesCappedAtBriefCap(t *testing.T) {
	cfg := sampleRepo(t)
	notesDir := filepath.Join(cfg.Workspace, "notes")
	// Twelve notes, distinct UTC minutes so the sort is deterministic.
	for i := range 12 {
		when := time.Date(2026, 1, 1, 10, i, 0, 0, time.UTC)
		if _, err := notes.Add(notesDir, fmt.Sprintf("note number %02d", i), when); err != nil {
			t.Fatalf("notes.Add: %v", err)
		}
	}
	cfg.BriefIncludeNotes = true
	d, err := Collect(cfg)
	if err != nil {
		t.Fatalf("Collect: %v", err)
	}
	if got := len(d.Notes); got != briefCap {
		t.Errorf("Notes len = %d, want %d (Collect caps before the trim ladder runs)", got, briefCap)
	}
}

func TestCollect_DisabledFactsHidden(t *testing.T) {
	cfg := sampleRepo(t)
	store, err := memory.Open(cfg)
	if err != nil {
		t.Fatalf("memory.Open: %v", err)
	}
	day := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
	enabled := &memory.Fact{
		Name: "active-feedback", Description: "active feedback",
		Type: memory.TypeFeedback, Created: day, LastUsed: day,
	}
	disabled := &memory.Fact{
		Name: "muted-feedback", Description: "muted feedback",
		Type: memory.TypeFeedback, Created: day, LastUsed: day,
		Disabled: true,
	}
	disabledWithRef := &memory.Fact{
		Name: "muted-pointer", Description: "muted pointer",
		Type: memory.TypeProject, Created: day, LastUsed: day,
		Ref: "internal/secret.go", Disabled: true,
	}
	for _, f := range []*memory.Fact{enabled, disabled, disabledWithRef} {
		if err := store.Save(f); err != nil {
			t.Fatalf("save %s: %v", f.Name, err)
		}
	}
	d, err := Collect(cfg)
	if err != nil {
		t.Fatalf("Collect: %v", err)
	}
	for _, k := range d.Knowledge {
		if k.Name == "muted-feedback" {
			t.Errorf("disabled fact %q leaked into Knowledge", k.Name)
		}
	}
	for _, p := range d.WhereToLook {
		if p.Ref == "internal/secret.go" {
			t.Errorf("disabled fact ref %q leaked into WhereToLook", p.Ref)
		}
	}
	// Enabled feedback fact must still appear.
	var found bool
	for _, k := range d.Knowledge {
		if k.Name == "active-feedback" {
			found = true
			break
		}
	}
	if !found {
		t.Error("enabled feedback fact missing from Knowledge")
	}
}