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