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 // /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 // /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") } }