1package main23import (4 "strings"5 "testing"67 "github.com/FelineStateMachine/atlas/internal/bundle/bundletest"8)910// diffFixture builds two builds of one game the way a policy change leaves11// them: the newer one loses a pin, keeps one matched pair, moves one, and12// makes one it did not have.13func diffFixture(t *testing.T) (*build, *build, map[string]map[int64]string, map[string]map[int64]string) {14 t.Helper()15 dir := t.TempDir()16 install(t, dir, "hollowmere-a.atlas", bundletest.Spec{17 Slug: "hollowmere", Title: "Hollowmere", CreatedAt: "2026-01-01T00:00:00Z",18 Worlds: []bundletest.WorldSpec{{19 Slug: "overworld",20 Pins: []bundletest.Pin{21 {Title: "Gate"}, {Title: "Well", Silent: true}, {Title: "Shrine"},22 },23 Merged: []map[string]any{{24 "source": "ign-wiki", "donorPins": 4,25 "matched": []map[string]any{26 {"d": 11, "w": 1, "px": 10},27 {"d": 12, "w": 2, "px": 20},28 },29 "alignment": "affine, median 20px",30 }},31 }},32 })33 install(t, dir, "hollowmere-b.atlas", bundletest.Spec{34 Slug: "hollowmere", Title: "Hollowmere", CreatedAt: "2026-02-01T00:00:00Z",35 Worlds: []bundletest.WorldSpec{{36 Slug: "overworld",37 Pins: []bundletest.Pin{38 {Title: "Gate", Description: "The way in."}, {Title: "Well"},39 },40 Merged: []map[string]any{{41 "source": "ign-wiki", "donorPins": 4,42 "matched": []map[string]any{43 {"d": 11, "w": 1, "px": 10},44 {"d": 12, "w": 3, "px": 30},45 {"d": 13, "w": 2, "px": 15},46 },47 "alignment": "affine, median 15px",48 }},49 }},50 })5152 games, _, err := (&library{dir: dir}).volumes()53 if err != nil {54 t.Fatal(err)55 }56 held := games[0]57 older, newer := held.Build("hollowmere-a.atlas"), held.Build("hollowmere-b.atlas")58 pinsA, err := loadPins(older.Path, older.MapSlugs)59 if err != nil {60 t.Fatal(err)61 }62 pinsB, err := loadPins(newer.Path, newer.MapSlugs)63 if err != nil {64 t.Fatal(err)65 }66 return older, newer, pinsA, pinsB67}6869func TestDiffBuilds(t *testing.T) {70 older, newer, pinsA, pinsB := diffFixture(t)71 d := diffBuilds(older, newer, pinsA, pinsB)7273 // The newer build packs two pins where the older packed three: id 3 is74 // gone and nothing arrived, since ids 1 and 2 stand in both.75 if len(d.Added) != 0 {76 t.Errorf("added %v, want none", d.Added)77 }78 if len(d.Removed) != 1 || d.Removed[0].ID != 3 || d.Removed[0].Title != "Shrine" ||79 d.Removed[0].Map != "overworld" {80 t.Errorf("removed %v, want overworld pin 3 Shrine", d.Removed)81 }8283 // Pair (11,1) stands in both ledgers. Pair (12,2) became (12,3), which84 // must read as one lost and one gained -- and (13,2) is a fresh gain.85 if len(d.Pairs) != 1 {86 t.Fatalf("pair lines %v, want one", d.Pairs)87 }88 line := d.Pairs[0]89 if line.Map != "overworld" || line.Source != "ign-wiki" {90 t.Errorf("pair line identity %+v", line)91 }92 if line.Kept != 1 || line.Gained != 2 || line.Lost != 1 {93 t.Errorf("stability kept %d gained %d lost %d, want 1, 2, 1", line.Kept, line.Gained, line.Lost)94 }9596 // Axis deltas read in A-to-B direction.97 byName := make(map[string]axisRow)98 for _, row := range d.Axes {99 byName[row.Name] = row100 }101 if row := byName["pins"]; row.Delta != "-1" || row.Sign != -1 {102 t.Errorf("pins delta %q sign %d", row.Delta, row.Sign)103 }104 // Described goes 2 of 3 to 2 of 2: count flat, share up 34 points.105 if row := byName["described"]; row.Delta != "±0" || row.Sign != 0 {106 t.Errorf("described delta %q sign %d", row.Delta, row.Sign)107 }108 if row := byName["described share"]; row.Delta != "+34 pt" || row.Sign != 1 {109 t.Errorf("described share delta %q sign %d", row.Delta, row.Sign)110 }111 if row := byName["icon coverage"]; row.A != "100%" || row.B != "100%" || row.Sign != 0 {112 t.Errorf("icon coverage row %+v", row)113 }114 if row := byName["unique raster"]; !strings.HasSuffix(row.Delta, "MB") {115 t.Errorf("raster delta %q carries no unit", row.Delta)116 }117}118119func TestDiffPage(t *testing.T) {120 server, _ := testServer(t)121 ts := newTestSite(t, server)122123 status, body := get(t, ts, "/volume/hollowmere/diff?a=hollowmere-jan.atlas&b=hollowmere-feb.atlas")124 if status != 200 {125 t.Fatalf("diff page answered %d", status)126 }127 for _, want := range []string{"what moved", "Matched-pair stability", "described share", "ign-wiki"} {128 if !strings.Contains(body, want) {129 t.Errorf("diff page misses %q", want)130 }131 }132 if status, _ := get(t, ts, "/volume/hollowmere/diff?a=hollowmere-jan.atlas&b=nowhere.atlas"); status != 400 {133 t.Errorf("diff with a missing build answered %d", status)134 }135}