1package main23import (4 "fmt"5 "strings"6 "testing"7)89// fixtureMergeGrid is one world-sized tile: zoom 0 puts the whole longitude10// range across a single 8192-pixel square, so fixture coordinates project11// somewhere sensible without a tile pyramid behind them.12func fixtureMergeGrid() tileGrid {13 return tileGrid{SourceZoom: 0, FirstTile: 0, TileSize: 8192, Size: 8192}14}1516// fixtureLandmarks builds one point collection of twelve named pins spread17// wide enough for the alignment to stand on, with ids counted from base.18// Both sides of a merge use the same names and places, so the fit is exact19// and every donor pin resolves as matched.20func fixtureLandmarks(base int64) worldCollection {21 collection := worldCollection{22 ID: 1, Title: "Landmarks", Group: "Markers",23 Kind: kindPoint, Icon: "landmark", Visible: true,24 }25 lats := []float64{40, 10, -20}26 lngs := []float64{-120, -60, 20, 90}27 for atLat, lat := range lats {28 for atLng, lng := range lngs {29 n := int64(atLat*len(lngs) + atLng)30 collection.Features = append(collection.Features, feature{31 ID: base + n,32 Title: fmt.Sprintf("Landmark %d", n),33 Lat: lat,34 Lng: lng,35 })36 }37 }38 return collection39}4041// TestMergeWorldHoldsDonorShapeFeatures is the ledger's promise for a merge42// no source pair exercises yet: a donor carrying areas and paths has every43// one of them held on the record, counted in donorFeatures, and none of them44// carried into the winner.45func TestMergeWorldHoldsDonorShapeFeatures(t *testing.T) {46 winnerGame := catalogVolume{47 Slug: "hollowmere", Title: "Hollowmere",48 Worlds: []catalogWorld{{49 ID: 1, Slug: "overworld", UpdatedAt: "2026-01-02",50 Collections: []worldCollection{51 fixtureLandmarks(1),52 {ID: 90, Key: "regions", Title: "Regions", Kind: kindArea,53 Features: []feature{{ID: 100, Title: "Old Town"}}},54 },55 Merged: []mergedSource{{56 Source: "MapGenie", Slug: "mapgenie", Origin: true,57 DonorFeatures: featureCounts{Point: 12, Area: 1},58 }},59 }},60 }61 donorGame := catalogVolume{62 Slug: "hollowmere", Title: "Hollowmere",63 Worlds: []catalogWorld{{64 ID: 2, Slug: "overworld", UpdatedAt: "2026-01-01",65 Collections: []worldCollection{66 fixtureLandmarks(201),67 {ID: 91, Key: "basins", Title: "Basins", Kind: kindArea,68 Features: []feature{69 {ID: 301, Title: "North Basin"},70 {ID: 302, Title: "South Basin"},71 }},72 {ID: 92, Key: "races", Title: "Mill Races", Kind: kindPath,73 Features: []feature{{ID: 303, Title: "Mill Race"}}},74 },75 Merged: []mergedSource{{76 Source: "IGN Wiki", Slug: "ign-wiki", Origin: true,77 DonorFeatures: featureCounts{Point: 12, Path: 1, Area: 2},78 }},79 }},80 }8182 winner := &winnerGame.Worlds[0]83 err := mergeWorld(&winnerGame, winner, &donorGame, &donorGame.Worlds[0], fixtureMergeGrid())84 if err != nil {85 t.Fatal(err)86 }87 if len(winner.Merged) != 2 {88 t.Fatalf("winner carries %d accounts, want origin plus the merge", len(winner.Merged))89 }90 account := winner.Merged[1]91 if account.DonorFeatures != (featureCounts{Point: 12, Path: 1, Area: 2}) {92 t.Errorf("donor features counted as %+v", account.DonorFeatures)93 }94 if len(account.Matched) != 12 || account.Added != 0 || account.AddedShapes != 0 {95 t.Errorf("points resolved as %d matched, %d added, %d added shapes; want 12, 0, 0",96 len(account.Matched), account.Added, account.AddedShapes)97 }98 if len(account.Held) != 3 {99 t.Fatalf("held ledger carries %d entries, want one per donor shape: %+v",100 len(account.Held), account.Held)101 }102 wantHeld := []heldPin{103 {Donor: 301, Title: "North Basin", Reason: heldShapeReason},104 {Donor: 302, Title: "South Basin", Reason: heldShapeReason},105 {Donor: 303, Title: "Mill Race", Reason: heldShapeReason},106 }107 for at, want := range wantHeld {108 if account.Held[at] != want {109 t.Errorf("held[%d] = %+v, want %+v", at, account.Held[at], want)110 }111 }112 // The shapes are held, not carried: the winner keeps exactly the113 // collections it opened with.114 if len(winner.Collections) != 2 {115 t.Errorf("winner grew to %d collections; donor shapes must not ride in", len(winner.Collections))116 }117}118119// gateWorld builds the smallest winner a gate case needs: the given120// collections and one origin account claiming the given counts.121func gateWorld(counts featureCounts, collections ...worldCollection) *catalogWorld {122 return &catalogWorld{123 Slug: "overworld",124 Collections: collections,125 Merged: []mergedSource{{126 Source: "MapGenie", Origin: true, DonorFeatures: counts,127 }},128 }129}130131func TestMergeGateAccountsPerKind(t *testing.T) {132 cases := []struct {133 name string134 merge *mergedSource135 winner *catalogWorld136 want string137 }{138 {139 name: "every donor shape must be held",140 merge: &mergedSource{DonorFeatures: featureCounts{Area: 1}},141 winner: gateWorld(featureCounts{}),142 want: "holds 0 shape features of the 1",143 },144 {145 name: "a held point never settles the shape account",146 merge: &mergedSource{DonorFeatures: featureCounts{Point: 1, Area: 1},147 Held: []heldPin{{Donor: 5, Title: "Well", Reason: "named like another"}}},148 winner: gateWorld(featureCounts{}),149 want: "holds 0 shape features of the 1",150 },151 {152 name: "added shapes are reserved at zero",153 merge: &mergedSource{AddedShapes: 1},154 winner: gateWorld(featureCounts{}),155 want: "no shape feature merges yet",156 },157 {158 name: "one id space across kinds",159 merge: &mergedSource{},160 winner: gateWorld(featureCounts{Point: 1, Area: 1},161 worldCollection{Kind: kindPoint, Features: []feature{{ID: 7, Title: "Gate"}}},162 worldCollection{Kind: kindArea, Features: []feature{{ID: 7, Title: "Gate District"}}},163 ),164 want: "feature id 7 held by both",165 },166 {167 name: "the ledger answers for the points the world holds",168 merge: &mergedSource{},169 winner: gateWorld(featureCounts{},170 worldCollection{Kind: kindPoint, Features: []feature{{ID: 1, Title: "Gate"}}},171 ),172 want: "holds 1 points but its ledger claims 0",173 },174 {175 name: "the ledger answers for the shapes the world holds",176 merge: &mergedSource{},177 winner: gateWorld(featureCounts{},178 worldCollection{Kind: kindArea, Features: []feature{{ID: 9, Title: "Old Town"}}},179 ),180 want: "holds 0 paths and 1 areas but its ledger claims 0 and 0",181 },182 {183 name: "a consistent account of every kind passes",184 merge: &mergedSource{DonorFeatures: featureCounts{Point: 1, Path: 1, Area: 1},185 Held: []heldPin{186 {Donor: 5, Title: "Well", Reason: "named like another"},187 {Donor: 6, Title: "Race", Reason: heldShapeReason},188 {Donor: 7, Title: "Basin", Reason: heldShapeReason},189 }},190 winner: gateWorld(featureCounts{Point: 1, Path: 1, Area: 1},191 worldCollection{Kind: kindPoint, Features: []feature{{ID: 1, Title: "Gate"}}},192 worldCollection{Kind: kindPath, Features: []feature{{ID: 2, Title: "Canal"}}},193 worldCollection{Kind: kindArea, Features: []feature{{ID: 3, Title: "Old Town"}}},194 ),195 want: "",196 },197 }198 for _, c := range cases {199 t.Run(c.name, func(t *testing.T) {200 err := mergeGate(c.merge, c.winner)201 if c.want == "" {202 if err != nil {203 t.Fatalf("gate refused a consistent account: %v", err)204 }205 return206 }207 if err == nil || !strings.Contains(err.Error(), c.want) {208 t.Fatalf("gate said %v, want it to say %q", err, c.want)209 }210 })211 }212}