package main import ( "crypto/sha256" "fmt" "net/http" "net/http/httptest" "net/url" "os" "os/exec" "path/filepath" "regexp" "strings" "testing" "time" ) func logLines(lines ...string) string { return strings.Join(lines, "\n") + "\n" } const ( noteA = `{"time":"2026-07-20T10:00:00Z","op":"note","session":"s1","agent":"codex","model":"gpt-5.6-sol","reasoning":"max","dir":"/repo","change":"aaa","file":"src/lib.rs","start":10,"end":20,"note_id":1,"type":"struct","text":"Cache is a two-level map"}` noteB = `{"time":"2026-07-25T10:00:00Z","op":"note","session":"s2","agent":"claude","model":"claude-fable-5","reasoning":"xhigh","dir":"/repo","change":"aaa","file":"src/main.rs","note_id":1,"text":"entry point does too much"}` readA = `{"time":"2026-07-20T09:59:00Z","op":"read","session":"s1","agent":"codex","model":"gpt-5.6-sol","dir":"/repo","change":"aaa","file":"src/lib.rs","start":1,"end":30}` grepA = `{"time":"2026-07-20T09:58:00Z","op":"grep","session":"s1","agent":"codex","dir":"/repo","change":"aaa","file":"src/lib.rs","pattern":"Cache","matches":3}` ) func testSite(t *testing.T) *Site { t.Helper() s := buildModel("test", parseLog([]byte(logLines(grepA, readA, noteA, noteB))), "") s.Theme, s.Light = loadTheme("", "", "dark"), loadTheme("", "", "light") return s } func TestBuildModel(t *testing.T) { s := testSite(t) if s.Change != "aaa" { t.Errorf("site change = %q, want aaa", s.Change) } if len(s.Agents) != 2 || s.Agents[0].Bit != 1 || s.Agents[1].Bit != 2 { t.Errorf("agents = %+v", s.Agents) } f := s.Files["src/lib.rs"] if f == nil || f.Reads != 1 || f.Greps != 1 || len(f.Notes) != 1 { t.Fatalf("src/lib.rs = %+v", f) } if f.Cov[15] != 1 || f.Mask != 1 { t.Errorf("coverage = %v mask %d", f.Cov, f.Mask) } if got := s.Notes[0].When; got == "" { t.Error("note has no rendered timestamp") } } // A ninth agent gets no color instead of overflowing the uint8 mask (and the // per-mask CSS it would generate). func TestAgentColorsAreBounded(t *testing.T) { var lines []string for i := 0; i < maxAgents+3; i++ { lines = append(lines, `{"time":"2026-07-20T10:00:00Z","op":"note","agent":"a`+string(rune('a'+i))+`","change":"aaa","note_id":1,"text":"x"}`) } s := buildModel("t", parseLog([]byte(logLines(lines...))), "") if len(s.Agents) != maxAgents+3 { t.Fatalf("agents = %d", len(s.Agents)) } for i, a := range s.Agents { if i < maxAgents && a.Bit == 0 { t.Errorf("agent %d has no bit", i) } if i >= maxAgents && a.Bit != 0 { t.Errorf("agent %d got bit %d, want none", i, a.Bit) } } if n := len(s.masks()); n != 1<