clone

an offline map explorer where each world, from a game map to a city's open data, ships as one .atlas file

main
atlas / internal / enrich / maturity / maturity_test.go
maturity_test.go 13.7K
1package maturity23import (4 "encoding/json"5 "fmt"6 "strings"7 "testing"89 "github.com/FelineStateMachine/atlas/format/bundle"10 "github.com/FelineStateMachine/atlas/format/semconv"11 "github.com/FelineStateMachine/atlas/internal/enrich"12)1314func points(t *testing.T) Table {15 t.Helper()16 table, err := Points()17 if err != nil {18 t.Fatal(err)19 }20 return table21}2223// build states one world's payloads outright, so a case can say exactly what a24// feature has and ask what that is worth.25type build struct {26 attrs map[string]string27 lenses []map[string]any28 collections []map[string]any29 text map[string]any30 locations []bundle.Location31 merged []enrich.Account32 revision int33}3435func (b build) score(t *testing.T, table Table) *Score {36 t.Helper()37 payload := map[string]any{38 "attrs": b.attrs,39 "lenses": b.lenses,40 "collections": b.collections,41 "merged": b.merged,42 }43 manifest := bundle.Manifest{44 Format: bundle.Format,45 FormatVersion: bundle.FormatVersion,46 Conventions: semconv.Version,47 Volume: bundle.Volume{Slug: "sample", Title: "Sample"},48 Version: bundle.Version{49 Stamp: strings.Repeat("a", 64),50 CreatedAt: "2026-08-01T00:00:00Z",51 Revision: b.revision,52 },53 TileGrid: bundle.TileGrid{SourceZoom: 5, FirstTile: 0, TileSize: 256, Size: 8192},54 Worlds: []bundle.WorldEntry{{55 Slug: "world", Title: "World", UpdatedAt: "2026-08-01T00:00:00Z",56 Points: len(b.locations),57 }},58 }59 text := b.text60 if text == nil {61 text = map[string]any{}62 }63 score, err := ScoreParts(manifest, []maturityParts{{64 Slug: "world",65 Payload: marshal(t, payload),66 Text: marshal(t, text),67 Locations: b.locations,68 }}[0].asParts(), table)69 if err != nil {70 t.Fatal(err)71 }72 return score73}7475// maturityParts is a spelling helper so the table above reads as data.76type maturityParts WorldParts7778func (p maturityParts) asParts() []WorldParts { return []WorldParts{WorldParts(p)} }7980func marshal(t *testing.T, value any) []byte {81 t.Helper()82 data, err := json.Marshal(value)83 if err != nil {84 t.Fatal(err)85 }86 return data87}8889func pointCollection(attrs map[string]string) map[string]any {90 return map[string]any{"id": 1, "title": "Chests", "kind": "point", "attrs": attrs}91}9293func TestAFeatureEarnsForEachQualityItHas(t *testing.T) {94 table := points(t)95 long := strings.Repeat("prose ", 40)9697 cases := []struct {98 what string99 text map[string]any100 extra int101 }{102 {what: "a name alone", text: map[string]any{}},103 {104 what: "and a short description",105 text: map[string]any{"1": map[string]any{"d": "A chest."}},106 extra: table.Feature.Prose,107 },108 {109 what: "and prose past the threshold",110 text: map[string]any{"1": map[string]any{"d": long}},111 extra: table.Feature.Prose + table.Feature.ProseSubstance,112 },113 {114 what: "and true coordinates",115 text: map[string]any{"1": map[string]any{116 "a": map[string]string{semconv.KeyGeoLat: "1", semconv.KeyGeoLon: "2"}}},117 extra: table.Feature.Geo,118 },119 {120 what: "one axis alone is not coordinates",121 text: map[string]any{"1": map[string]any{122 "a": map[string]string{semconv.KeyGeoLat: "1"}}},123 },124 {125 what: "and a membership",126 text: map[string]any{"1": map[string]any{127 "a": map[string]string{semconv.KeyHydroHUC12: "170703010801"}}},128 extra: table.Feature.Membership,129 },130 }131132 base := build{133 collections: []map[string]any{pointCollection(nil)},134 locations: []bundle.Location{{ID: 1, Title: "A chest"}},135 }136 for _, c := range cases {137 t.Run(c.what, func(t *testing.T) {138 with := base139 with.text = c.text140 want := table.Feature.Name + c.extra141 if got := with.score(t, table).Worlds[0].Features; got != want {142 t.Errorf("the feature earned %d, want %d", got, want)143 }144 })145 }146147 t.Run("a nameless feature earns nothing for a name", func(t *testing.T) {148 nameless := base149 nameless.locations = []bundle.Location{{ID: 1, Title: " "}}150 if got := nameless.score(t, table).Worlds[0].Features; got != 0 {151 t.Errorf("a nameless feature earned %d", got)152 }153 })154}155156func TestACollectionThatResolvesArtworkPaysItsFeatures(t *testing.T) {157 table := points(t)158 plain := build{159 collections: []map[string]any{pointCollection(nil)},160 locations: []bundle.Location{{ID: 1, Title: "A chest"}, {ID: 2, Title: "Another"}},161 }162 arted := plain163 arted.collections = []map[string]any{{164 "id": 1, "title": "Chests", "kind": "point", "iconAsset": "chest.svg",165 }}166 before := plain.score(t, table).Worlds[0].Features167 after := arted.score(t, table).Worlds[0].Features168 if after-before != 2*table.Feature.Icon {169 t.Errorf("artwork was worth %d over two features", after-before)170 }171}172173func TestGeometryIsWorthWhatIsActuallyDrawn(t *testing.T) {174 table := points(t)175 shape := func(vertices int) build {176 var coordinates []any177 for index := range vertices {178 coordinates = append(coordinates, []any{float64(index), 0.0})179 }180 return build{collections: []map[string]any{{181 "id": 2, "title": "Zones", "kind": "area",182 "features": []any{map[string]any{183 "id": 10, "title": "A zone",184 "geometry": []any{map[string]any{"coordinates": []any{coordinates}}},185 }},186 }}}187 }188 rectangle := shape(4).score(t, table).Worlds[0].Features189 detailed := shape(1024).score(t, table).Worlds[0].Features190 if detailed <= rectangle {191 t.Errorf("a 1024-vertex boundary (%d) is not worth more than a rectangle (%d)",192 detailed, rectangle)193 }194 // Log scaling: a thousandfold more vertices is worth a handful of points,195 // not a thousandfold more score.196 if detailed-rectangle > 10 {197 t.Errorf("geometry outweighs everything else: %d points for 1020 more vertices",198 detailed-rectangle)199 }200 // And it is monotone.201 previous := 0202 for _, vertices := range []int{2, 4, 8, 64, 512, 4096} {203 got := shape(vertices).score(t, table).Worlds[0].Features204 if got < previous {205 t.Errorf("%d vertices scored %d, less than the shape before it (%d)",206 vertices, got, previous)207 }208 previous = got209 }210}211212func TestCorroborationIsReadOffTheLedger(t *testing.T) {213 table := points(t)214 plain := build{215 collections: []map[string]any{pointCollection(nil)},216 locations: []bundle.Location{{ID: 1, Title: "A chest"}},217 }218 corroborated := plain219 corroborated.merged = []enrich.Account{220 {Source: "MapGenie", Origin: true, DonorFeatures: enrich.Counts{Point: 1}},221 {Source: "IGN Wiki", DonorFeatures: enrich.Counts{Point: 1},222 Matched: []enrich.MatchedPair{{Donor: 99, Winner: 1, DistancePx: 20}}},223 }224 before := plain.score(t, table).Worlds[0].Features225 after := corroborated.score(t, table).Worlds[0].Features226 if after-before != table.Feature.Corroboration {227 t.Errorf("a second reading agreeing was worth %d", after-before)228 }229}230231func TestWorldsEarnForWhatTheyDeclareAndHowDeepTheyGo(t *testing.T) {232 table := points(t)233 bare := build{collections: []map[string]any{pointCollection(nil)}}234 declared := bare235 declared.attrs = map[string]string{236 semconv.KeyGeometrySurface: semconv.SurfaceSphere,237 semconv.KeyGeometryBody: "mars",238 }239 pictured := bare240 pictured.lenses = []map[string]any{{"name": "Default", "minZoom": 0, "maxZoom": 5}}241242 if got := declared.score(t, table).Worlds[0].World - bare.score(t, table).Worlds[0].World; got != 2*table.World.Convention {243 t.Errorf("two declarations were worth %d", got)244 }245 if got := pictured.score(t, table).Worlds[0].World; got != table.World.Lens+6*table.World.LensZoom {246 t.Errorf("a six-level lens was worth %d", got)247 }248}249250func TestCollectionsEarnForTheConventionsTheyDeclare(t *testing.T) {251 table := points(t)252 bare := build{collections: []map[string]any{pointCollection(nil)}}253 speaking := build{collections: []map[string]any{pointCollection(map[string]string{254 semconv.KeyGeometryKind: semconv.GeometryPoint,255 semconv.KeyRenderAs: semconv.RenderAsPin,256 "private.key": "ignored",257 })}}258 got := speaking.score(t, table).Worlds[0].Collections - bare.score(t, table).Worlds[0].Collections259 if got != 2*table.Collection.Convention {260 t.Errorf("two declared conventions were worth %d; a private key must be worth nothing", got)261 }262}263264func TestTheScoreIsUnboundedAndAdditive(t *testing.T) {265 table := points(t)266 measure := func(features int) int {267 locations := make([]bundle.Location, 0, features)268 text := map[string]any{}269 for index := range features {270 id := int64(index + 1)271 locations = append(locations, bundle.Location{ID: id, Title: fmt.Sprintf("Chest %d", index)})272 text[fmt.Sprint(id)] = map[string]any{"d": "A chest."}273 }274 return build{275 collections: []map[string]any{pointCollection(nil)},276 locations: locations,277 text: text,278 }.score(t, table).Total279 }280 one, ten, hundred := measure(1), measure(10), measure(100)281 if ten != 10*one || hundred != 100*one {282 t.Errorf("features do not add up: %d, %d, %d", one, ten, hundred)283 }284 if hundred <= ten {285 t.Error("the score has a ceiling")286 }287}288289func TestSharesAreDiagnosticsAndCannotExceedTheirWhole(t *testing.T) {290 // The reference tooling divided every described feature by the point291 // features alone, so a volume whose shapes were described read 235%.292 table := points(t)293 score := build{294 collections: []map[string]any{295 pointCollection(nil),296 {"id": 2, "title": "Zones", "kind": "area", "features": []any{297 map[string]any{"id": 10, "title": "A zone", "hasText": true},298 map[string]any{"id": 11, "title": "Another zone", "hasText": true},299 }},300 },301 locations: []bundle.Location{{ID: 1, Title: "A chest"}},302 text: map[string]any{303 "1": map[string]any{"d": "A chest."},304 "10": map[string]any{"d": "A zone."},305 "11": map[string]any{"d": "Another zone."},306 },307 }.score(t, table)308309 if score.Axes.Described != 3 || score.Axes.Features != 3 || score.Axes.Points != 1 {310 t.Fatalf("described %d of %d features (%d of them points)",311 score.Axes.Described, score.Axes.Features, score.Axes.Points)312 }313 if got := score.Axes.DescribedShare(); got != "100%" {314 t.Errorf("the described share reads %q, want 100%%", got)315 }316 if got := (Axes{}).DescribedShare(); got != "—" {317 t.Errorf("a share of nothing reads %q", got)318 }319}320321func TestTheGateRewardsGoodDataAndPermitsCorrections(t *testing.T) {322 cases := []struct {323 what string324 before Score325 after Score326 refuse bool327 }{328 {329 what: "more is more",330 before: Score{TableVersion: 1, Total: 100},331 after: Score{TableVersion: 1, Total: 140},332 },333 {334 what: "standing still passes",335 before: Score{TableVersion: 1, Total: 100},336 after: Score{TableVersion: 1, Total: 100},337 },338 {339 what: "losing quality fails",340 before: Score{TableVersion: 1, Total: 100},341 after: Score{TableVersion: 1, Total: 90},342 refuse: true,343 },344 {345 what: "a re-weighting is not a mass failure",346 before: Score{TableVersion: 1, Total: 100},347 after: Score{TableVersion: 2, Total: 40},348 },349 {350 what: "a ledgered correction is permitted",351 before: Score{TableVersion: 1, Total: 100},352 after: Score{TableVersion: 1, Total: 90, Ledger: []LedgerLine{{353 World: "world",354 Account: enrich.Account{Corrections: []enrich.Correction{{355 Reason: "twelve zones claimed a subwatershed they only touched", Points: 12,356 }}},357 }}},358 },359 {360 what: "a correction does not excuse everything",361 before: Score{TableVersion: 1, Total: 100},362 after: Score{TableVersion: 1, Total: 50, Ledger: []LedgerLine{{363 Account: enrich.Account{Corrections: []enrich.Correction{{Reason: "a few", Points: 5}}},364 }}},365 refuse: true,366 },367 }368 for _, c := range cases {369 t.Run(c.what, func(t *testing.T) {370 err := Gate(&c.before, &c.after)371 if c.refuse && err == nil {372 t.Fatal("accepted")373 }374 if !c.refuse && err != nil {375 t.Fatalf("refused: %v", err)376 }377 comparison := Compare(&c.before, &c.after)378 if comparison.Delta != c.after.Total-c.before.Total {379 t.Errorf("delta reads %d", comparison.Delta)380 }381 })382 }383}384385func TestServingIsTheRegistrysOwnOrder(t *testing.T) {386 plain := &Score{Volume: "v", CreatedAt: "2026-08-01T00:00:00Z", Revision: 9, Stamp: "aaa", Path: "a"}387 enriched := &Score{Volume: "v", CreatedAt: "2026-08-01T00:00:00Z", Revision: 109, Stamp: "bbb", Path: "b"}388 newer := &Score{Volume: "v", CreatedAt: "2026-08-02T00:00:00Z", Revision: 9, Stamp: "ccc", Path: "c"}389390 if got := Serving([]*Score{plain, enriched}); got != enriched {391 t.Error("the plain build outranked the enriched build of the same capture")392 }393 if got := Serving([]*Score{enriched, newer}); got != newer {394 t.Error("an older capture outranked a newer one")395 }396}397398func TestAnEnrichedBuildSaysSo(t *testing.T) {399 table := points(t)400 enrichedRevision, err := enrich.BuildRevision(9)401 if err != nil {402 t.Fatal(err)403 }404 plain := build{collections: []map[string]any{pointCollection(nil)}, revision: 9}.score(t, table)405 if plain.Enriched {406 t.Error("a plain build reads as enriched")407 }408 enriched := build{collections: []map[string]any{pointCollection(nil)}, revision: enrichedRevision}.score(t, table)409 if !enriched.Enriched || enriched.EnrichPolicy != enrich.PolicyRevision {410 t.Errorf("an enriched build reads enriched=%t policy %d", enriched.Enriched, enriched.EnrichPolicy)411 }412}413414func TestThePointTableIsVersionedData(t *testing.T) {415 table := points(t)416 if table.Version != 1 || table.Schema != TableSchema {417 t.Errorf("the embedded table is schema %d version %d", table.Schema, table.Version)418 }419 if table.Feature.SubstanceChars <= 0 {420 t.Error("the table declares no substance threshold")421 }422 for _, bad := range []string{423 `{"schema": 99, "version": 1}`,424 `{"schema": 1, "version": 0}`,425 `{"schema": 1, "version": 1, "feature": {"substanceChars": 0}}`,426 } {427 if _, err := ReadTable([]byte(bad)); err == nil {428 t.Errorf("%s was accepted", bad)429 }430 }431 // A score carries the version that produced it, which is what makes two432 // scores comparable or not.433 sample := build{collections: []map[string]any{pointCollection(nil)}}434 if got := sample.score(t, table); got.TableVersion != table.Version {435 t.Errorf("the score says table v%d", got.TableVersion)436 }437}