package movementtest import ( "encoding/json" "os" "path/filepath" "strings" "testing" movement "amadan.net/carlos/movement/internal/movement" ) // The copy in this app is reviewed by Paul, string by string, and applied // verbatim. This test is what stops it drifting afterwards. // // Without it, "applied verbatim" is a claim about one afternoon. A later // session fixing a typo, tightening a sentence or making a heading // "consistent" would be quietly overwriting somebody's reviewed words, and // nothing would notice. copy-review/result.json is the record of what was // approved, so it is the thing to compare against. // The durable record of what has been approved. NOT result.json: the review // server deletes that when a new round starts, so a test pointed at it loses // its guard the moment somebody opens another round — which is exactly when // copy is most likely to move. approved.json is written from a finished // round and outlives the next one. const reviewResult = "../../copy-review/approved.json" type reviewedString struct { ID string `json:"id"` Text string `json:"text"` } type reviewOutcome struct { Action string `json:"action"` Strings []reviewedString `json:"strings"` } // notInTheTable are ids that went through review but deliberately do not // appear in the copy table. Each one needs a reason here, so that dropping a // reviewed string is a decision on the record rather than an omission. var notInTheTable = map[string]string{ // Paul, after submitting: "Lose the JS required message ... that's an // irrelevant implementation detail." The form still needs JavaScript; the // page no longer explains why. "form.nojs": "removed on request after review", // These three are web addresses rather than words on a page. They are // reviewed copy, but they live in the router. "slug.never": "routing, not display copy", "slug.curious": "routing, not display copy", "slug.enthusiast": "routing, not display copy", } // One submitted string opened "This was entirely AI-generated. Human editor // leaving as-is:" and it was queried, because it reads like a note to whoever // is applying the copy. It is not. Paul confirmed on 2026-09-02 that it is // the joke: the people who said they are all in on AI get the model's output // verbatim. // // On 2026-09-03 he moved it: the disclosure now sits AFTER the text it // describes, reworded, as its own string awaiting its own review. So the // message shipped is the submitted text minus that prefix, which is the one // place this file allows a difference — and it names it rather than // tolerating a mismatch quietly. // changedAfterReview records strings the user directed a change to after // approving them. Each needs a date and what they asked for, so that a // difference from the review record is always a decision somebody made and // never drift nobody noticed. var changedAfterReview = map[string]string{ "stance.enthusiast.message": "2026-09-03: Paul moved the AI disclosure " + "below the text it describes, so the prefix left this string and " + "became stance.enthusiast.disclosure, which awaits its own review.", } // pendingAgain is computed from the copy table itself rather than listed by // hand: anything the app still marks as a placeholder is mid-round, whatever // it was approved as previously. var pendingAgain = func() map[string]bool { m := map[string]bool{} for _, id := range movement.Placeholders() { m[id] = true } return m }() func TestEveryStringMatchesTheApprovedReview(t *testing.T) { raw, err := os.ReadFile(filepath.FromSlash(reviewResult)) if err != nil { t.Fatalf("no review record to check against: %v", err) } var out reviewOutcome if err := json.Unmarshal(raw, &out); err != nil { t.Fatalf("unreadable review record: %v", err) } if out.Action != "approve" { t.Fatalf("the review record says %q, not approve — nothing should have shipped", out.Action) } checked := 0 for _, s := range out.Strings { if why, skipped := notInTheTable[s.ID]; skipped { t.Logf("%s: not in the table (%s)", s.ID, why) continue } // A string that has been re-marked as a placeholder is awaiting a new // round, so the old approved text is no longer what it should equal. // Production still refuses to serve it, which is the real guard. if pendingAgain[s.ID] { t.Logf("%s: re-opened for review, not compared", s.ID) continue } if why, changed := changedAfterReview[s.ID]; changed { // Still checked, but against what the change left behind: the // submitted text with the moved prefix removed. const prefix = "This was entirely AI-generated. Human editor leaving as-is:" rest, found := strings.CutPrefix(s.Text, prefix) if !found { t.Errorf("%s is recorded as changed after review (%s) but no "+ "longer matches the shape that change expected", s.ID, why) continue } if got := movement.T(s.ID); got != strings.TrimLeft(rest, "\n ") { t.Errorf("%s differs from the approved text by more than the "+ "change on record (%s).\n approved: %q\n shipped : %q", s.ID, why, s.Text, got) } checked++ continue } want := s.Text got := movement.T(s.ID) if got != want { t.Errorf("%s does not match what was approved.\n approved: %q\n shipped : %q", s.ID, want, got) } checked++ } if checked == 0 { t.Fatal("the review record matched nothing; the ids have drifted apart") } t.Logf("%d reviewed strings match", checked) } // A string in the table that nobody reviewed is the failure this whole gate // exists to prevent, so it is worth catching directly rather than trusting // that the next person remembers. func TestNoStringShippedWithoutReview(t *testing.T) { raw, err := os.ReadFile(filepath.FromSlash(reviewResult)) if err != nil { t.Fatalf("no review record: %v", err) } var out reviewOutcome if err := json.Unmarshal(raw, &out); err != nil { t.Fatal(err) } reviewed := map[string]bool{} for _, s := range out.Strings { reviewed[s.ID] = true } // A string still marked as a placeholder has not shipped: the app // refuses to serve the canonical host while any remain, which is the // mechanism that lets design work run ahead of the next copy round. pending := map[string]bool{} for _, id := range movement.Placeholders() { pending[id] = true } for _, id := range movement.CopyIDs() { if !reviewed[id] && !pending[id] { t.Errorf("%q is in the copy table, was never reviewed, and is not "+ "marked as a placeholder — so it would ship unreviewed", id) } } }