| 1 | package updatewords |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "os" |
| 6 | "sort" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | ) |
| 10 | |
| 11 | type approvedEntry struct{ ID, File, Text string } |
| 12 | |
| 13 | type approvedRecord struct { |
| 14 | ApprovedBy string `json:"approved_by"` |
| 15 | ApprovedOn string `json:"approved_on"` |
| 16 | Entries []approvedEntry |
| 17 | } |
| 18 | |
| 19 | func loadApproved(t *testing.T, path string) approvedRecord { |
| 20 | t.Helper() |
| 21 | raw, err := os.ReadFile(path) |
| 22 | if err != nil { |
| 23 | t.Fatal(err) |
| 24 | } |
| 25 | var rec approvedRecord |
| 26 | if err := json.Unmarshal(raw, &rec); err != nil { |
| 27 | t.Fatal(err) |
| 28 | } |
| 29 | return rec |
| 30 | } |
| 31 | |
| 32 | // The copy Paul approved lives in four records: plan 2's (2026-10-09, the |
| 33 | // authorize strings), plan 3's (2026-10-11, the settings.console.* strings), |
| 34 | // plan 4's (2026-10-12, the settings.box.* strings), and plan 5's (2026-10-13, |
| 35 | // the settings.cli.* and settings.page.* strings). |
| 36 | // Each record must name exactly its own IDs, each text must be what Text |
| 37 | // returns byte for byte, each entry must name the file that holds it, and |
| 38 | // together the four records must name exactly the IDs of All(), so a new |
| 39 | // draft with no approved record fails. |
| 40 | func TestApprovedCopyIsApplied(t *testing.T) { |
| 41 | const console = "settings.console." |
| 42 | const box = "settings.box." |
| 43 | const surfaces = "settings.cli." |
| 44 | const pagePrefix = "settings.page." |
| 45 | records := []struct { |
| 46 | path string |
| 47 | file string |
| 48 | in func(id string) bool |
| 49 | }{ |
| 50 | {"../../docs/superpowers/plans/2026-10-07-instance-settings-2-copy.json", |
| 51 | "internal/updatewords/words_authorize.go", |
| 52 | func(id string) bool { |
| 53 | return !strings.HasPrefix(id, console) && !strings.HasPrefix(id, box) && |
| 54 | !strings.HasPrefix(id, surfaces) && !strings.HasPrefix(id, pagePrefix) |
| 55 | }}, |
| 56 | {"../../docs/superpowers/plans/2026-10-07-instance-settings-3-copy.json", |
| 57 | "internal/updatewords/words_console.go", |
| 58 | func(id string) bool { return strings.HasPrefix(id, console) }}, |
| 59 | {"../../docs/superpowers/plans/2026-10-07-instance-settings-4-copy.json", |
| 60 | "internal/updatewords/words_box.go", |
| 61 | func(id string) bool { return strings.HasPrefix(id, box) }}, |
| 62 | {"../../docs/superpowers/plans/2026-10-07-instance-settings-5-copy.json", |
| 63 | "internal/updatewords/words_surfaces.go", |
| 64 | func(id string) bool { return strings.HasPrefix(id, surfaces) || strings.HasPrefix(id, pagePrefix) }}, |
| 65 | } |
| 66 | var union []string |
| 67 | docs := "" |
| 68 | for _, r := range records { |
| 69 | rec := loadApproved(t, r.path) |
| 70 | if rec.ApprovedBy == "" || rec.ApprovedOn == "" { |
| 71 | t.Fatalf("%s: the record has no approval", r.path) |
| 72 | } |
| 73 | var got, want []string |
| 74 | seen := map[string]bool{} |
| 75 | for _, e := range rec.Entries { |
| 76 | got = append(got, e.ID) |
| 77 | if seen[e.ID] { |
| 78 | t.Errorf("%s: %s appears twice", r.path, e.ID) |
| 79 | } |
| 80 | seen[e.ID] = true |
| 81 | if e.Text == "" { |
| 82 | t.Errorf("%s: the approved text is empty", e.ID) |
| 83 | continue |
| 84 | } |
| 85 | if e.File != r.file { |
| 86 | t.Errorf("%s: unexpected file %s, want %s", e.ID, e.File, r.file) |
| 87 | } |
| 88 | if e.ID == "settings.authorize.docs" { |
| 89 | docs = e.Text |
| 90 | } |
| 91 | if !has(e.ID) { |
| 92 | continue // reported by the ID comparison below |
| 93 | } |
| 94 | if txt := Text(e.ID); txt != e.Text { |
| 95 | t.Errorf("%s: text is %q, approved %q", e.ID, txt, e.Text) |
| 96 | } |
| 97 | } |
| 98 | for _, d := range All() { |
| 99 | if r.in(d.ID) { |
| 100 | want = append(want, d.ID) |
| 101 | } |
| 102 | } |
| 103 | sort.Strings(got) |
| 104 | sort.Strings(want) |
| 105 | if strings.Join(got, "\n") != strings.Join(want, "\n") { |
| 106 | t.Errorf("%s: record IDs %v, want %v", r.path, got, want) |
| 107 | } |
| 108 | union = append(union, got...) |
| 109 | } |
| 110 | |
| 111 | var all []string |
| 112 | for _, d := range All() { |
| 113 | all = append(all, d.ID) |
| 114 | } |
| 115 | sort.Strings(union) |
| 116 | sort.Strings(all) |
| 117 | if strings.Join(union, "\n") != strings.Join(all, "\n") { |
| 118 | t.Errorf("the approved records name %v, want exactly the IDs of All() %v", union, all) |
| 119 | } |
| 120 | |
| 121 | if docs == "" { |
| 122 | t.Fatal("the record has no settings.authorize.docs entry") |
| 123 | } |
| 124 | page, err := os.ReadFile("../../docs/site/cli.md") |
| 125 | if err != nil { |
| 126 | t.Fatal(err) |
| 127 | } |
| 128 | if !strings.Contains(string(page), docs) { |
| 129 | t.Errorf("docs/site/cli.md does not contain the approved authorize paragraph %q", docs) |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | func has(id string) bool { |
| 134 | for _, d := range All() { |
| 135 | if d.ID == id { |
| 136 | return true |
| 137 | } |
| 138 | } |
| 139 | return false |
| 140 | } |
| 141 | |