From 5ec72f8723d795d0b02d7e9ebb9f555f0c7e6a2e Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Sun, 2 Aug 2026 23:43:08 -0300 Subject: rebrand to notevi: one CLI over the jj sidecar vr and vrsite become a single binary. vrsite/ folds into a web package in one module (0x4200.cafe/notevi); "notevi web" serves and exports exactly what vrsite did, and "notevi read/grep/note/query" is unchanged. The sidecar is renamed with it: notevi_log, notevi-log.jsonl, and the description "private: notevi log". The pre-rebrand names are still recognized, so an old repository opens and reads; it is renamed in place on the first write, or up front with "notevi migrate DIR...". That rename cannot be a single mv inside jj run. jj only auto-tracks a *new* file in the run working copy below a size limit it does not take from the command line, so writing a whole log under a name the change has never held is silently dropped while jj reports success. ensureLogFile creates the file empty first and lets every later byte be a modification of a tracked file, which snapshots at any size; that also fixes the same latent bug when importing a large legacy vr-log.jsonl. Adds a bem-te-vi mark (favicon and nav brand) and a README. Co-Authored-By: Claude Opus 5 (1M context) --- vrsite/app_test.go | 912 ----------------------------------------------------- 1 file changed, 912 deletions(-) delete mode 100644 vrsite/app_test.go (limited to 'vrsite/app_test.go') diff --git a/vrsite/app_test.go b/vrsite/app_test.go deleted file mode 100644 index 876e253..0000000 --- a/vrsite/app_test.go +++ /dev/null @@ -1,912 +0,0 @@ -package main - -import ( - "crypto/sha256" - "fmt" - "net/http" - "net/http/httptest" - "net/url" - "os" - "os/exec" - "path/filepath" - "regexp" - "strings" - "testing" - "time" - - "0x4200.cafe/vr/sidecar" -) - -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 -} - -// testThemes stands in for whatever themes this machine happens to have, so -// the picker's two files are the same in every test run. -func testThemes(dark, light *Theme) *themeSet { - return themesFrom([]*Theme{dark, light}, dark, light) -} - -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") - } -} - -func TestNoteThreadRecursesThroughRepliesAndMentions(t *testing.T) { - entries := parseLog([]byte(logLines( - `{"op":"note","session":"s","agent":"a","change":"aaa","note_id":1,"text":"root"}`, - `{"op":"note","session":"s","agent":"a","change":"aaa","note_id":2,"reply_to":1,"text":"child"}`, - `{"op":"note","session":"s","agent":"a","change":"aaa","note_id":3,"reply_to":2,"text":"grandchild"}`, - `{"op":"note","session":"s","agent":"a","change":"aaa","note_id":4,"text":"[mentions root](../note/1.html)"}`, - `{"op":"note","session":"s","agent":"a","change":"aaa","note_id":5,"reply_to":3,"text":"[also mentions child](note/2)"}`, - `{"op":"note","session":"s","agent":"a","change":"aaa","note_id":6,"reply_to":5,"text":"arbitrarily deeper"}`, - ))) - s := buildModel("test", entries, "") - root, child := s.NoteByID(1), s.NoteByID(2) - if len(root.Replies) != 1 || root.Replies[0].ID != 2 || len(root.Mentions) != 1 || root.Mentions[0].ID != 4 { - t.Fatalf("root edges = replies %+v, mentions %+v", root.Replies, root.Mentions) - } - if len(child.Mentions) != 1 || child.Mentions[0].ID != 5 { - t.Fatalf("child mentions = %+v", child.Mentions) - } - - thread := s.NoteThread(1) - if thread == nil || len(thread.Children) != 2 || thread.Children[0].Via != "reply" || thread.Children[1].Via != "mention" { - t.Fatalf("root thread = %+v", thread) - } - // 1 → 2 → 3 → 5 → 6 proves the renderer's data is recursive rather - // than a fixed parent/child pair. - deep := thread.Children[0].Children[0].Children[0] - if deep.Note.ID != 5 || len(deep.Children) != 1 || deep.Children[0].Note.ID != 6 { - t.Fatalf("deep reply branch = %+v", deep) - } - // Note 5 also mentions note 2. It is fully rendered under note 3, then left - // as a DAG reference on the second path instead of recursing twice. - join := thread.Children[0].Children[1] - if join.Note.ID != 5 || !join.Reference || join.Via != "mention" { - t.Errorf("DAG join = %+v", join) - } -} - -// 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<