clone

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

feat/atlas-next-horizon
atlas / tests / island / harness_test.go
harness_test.go 14.8K
1// Package island holds the clean-room application to its own account of the2// arrangement -- the state island and the display logic around it -- over the3// public corpus at testdata/corpus.4//5// It lives here rather than beside internal/app for one mechanical reason:6// these tests read the corpus off the disk, and the hostenv analyzer forbids7// internal/app -- test files included -- from importing os or path/filepath,8// which is exactly the rule that keeps the handler portable.9//10// What it holds is the island's own contract (internal/app/island.go,11// docs/app.md ยง6): the keys it promises, the values that must agree with the12// page it rides in, and counts that must agree with the corpus payloads the13// page was rendered from. The application is driven through its own HTTP14// surface over an in-memory host. Nothing here reaches inside it.15package island1617import (18 "context"19 "encoding/json"20 "io"21 "net/http"22 "net/http/httptest"23 "net/url"24 "os"25 "path/filepath"26 "strings"27 "testing"2829 "github.com/FelineStateMachine/atlas/format/bundle"30 "github.com/FelineStateMachine/atlas/format/vnext"31 "github.com/FelineStateMachine/atlas/internal/app"32 "github.com/FelineStateMachine/atlas/internal/app/hostenv"33)3435// corpusDir is where the committed corpus lives, relative to this package.36// The corpus is required: a missing bundle is a broken checkout and fails37// loudly, because a suite that skips is a suite that stopped judging.38const corpusDir = "../../testdata/corpus/bundles"3940// ---------------------------------------------------------------------------41// The in-memory host42// ---------------------------------------------------------------------------4344// memoryHost is a host with no machine under it: a fixed library, sessions in45// a map, and no picker. It is what hostenv is for.46type memoryHost struct {47 volumes *memoryVolumes48 sessions hostenv.SessionStore49}5051func (h *memoryHost) Volumes() hostenv.VolumeStore { return h.volumes }52func (h *memoryHost) Sessions() hostenv.SessionStore { return h.sessions }5354func (h *memoryHost) PickFile(context.Context) (io.ReadCloser, string, error) {55 return nil, "", hostenv.ErrNotAvailable56}5758type memoryVolumes struct{ volumes []hostenv.Volume }5960func (s *memoryVolumes) Volumes() []hostenv.Volume { return s.volumes }61func (s *memoryVolumes) Location() string { return "/library" }62func (s *memoryVolumes) Rescan() ([]string, error) { return nil, nil }6364func (s *memoryVolumes) Install(string, io.Reader) (hostenv.Installed, error) {65 return hostenv.Installed{}, os.ErrInvalid66}6768// newApp mounts the application over a fresh in-memory host. No static tree69// is mounted, which is the shape the application is required to work in.70func newApp(t *testing.T, volumes ...hostenv.Volume) http.Handler {71 t.Helper()72 host := &memoryHost{73 volumes: &memoryVolumes{volumes: volumes},74 sessions: hostenv.NewMemorySessions(),75 }76 return app.New(host, app.Options{})77}7879func get(t *testing.T, handler http.Handler, path string) *httptest.ResponseRecorder {80 t.Helper()81 recorder := httptest.NewRecorder()82 handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, path, nil))83 return recorder84}8586func post(t *testing.T, handler http.Handler, path string, form url.Values) *httptest.ResponseRecorder {87 t.Helper()88 request := httptest.NewRequest(http.MethodPost, path, strings.NewReader(form.Encode()))89 request.Header.Set("Content-Type", "application/x-www-form-urlencoded")90 recorder := httptest.NewRecorder()91 handler.ServeHTTP(recorder, request)92 return recorder93}9495// ---------------------------------------------------------------------------96// Corpus volumes97// ---------------------------------------------------------------------------9899// corpusVolume stands one corpus bundle up as a servable volume.100//101// The corpus holds canonicalized extractions rather than .atlas files -- the102// archives are not in the repository -- so the packed locations are repacked103// from their JSON extraction through the format's own codec. What the handler104// sees is therefore exactly what it would see out of an archive: a manifest,105// a world payload, and ATLASLOC bytes.106func corpusVolume(t *testing.T, slug string) hostenv.Volume {107 t.Helper()108 dir := filepath.Join(corpusDir, slug)109 manifestBody, err := os.ReadFile(filepath.Join(dir, "manifest.json"))110 if err != nil {111 t.Fatalf("the corpus is missing %s: %v", slug, err)112 }113 var manifest bundle.Manifest114 if err := json.Unmarshal(manifestBody, &manifest); err != nil {115 t.Fatal(err)116 }117118 entries := map[string][]byte{}119 for _, world := range manifest.Worlds {120 payload, err := os.ReadFile(filepath.Join(dir, "worlds", world.Slug+".payload.json"))121 if err != nil {122 t.Fatalf("corpus %s world %s: %v", slug, world.Slug, err)123 }124 entries[bundle.WorldEntryName(world.Slug, bundle.WorldSuffix)] = payload125126 entries[bundle.WorldEntryName(world.Slug, bundle.PackedSuffix)] =127 repack(t, filepath.Join(dir, "worlds", world.Slug+".locations.json"))128129 if text, err := os.ReadFile(filepath.Join(dir, "worlds", world.Slug+".text.json")); err == nil {130 entries[bundle.WorldEntryName(world.Slug, bundle.TextSuffix)] = text131 }132 }133 return &corpusBundle{manifest: manifest, entries: entries}134}135136func repack(t *testing.T, path string) []byte {137 t.Helper()138 body, err := os.ReadFile(path)139 if err != nil {140 return bundle.PackLocations(nil)141 }142 var held struct {143 Locations []bundle.Location `json:"locations"`144 }145 if err := json.Unmarshal(body, &held); err != nil {146 t.Fatalf("locations %s: %v", path, err)147 }148 return bundle.PackLocations(held.Locations)149}150151// corpusBundleOf builds a servable volume out of nothing, for the one test152// whose fixture is not in the public corpus: a payload written in the test153// itself, empty locations, empty prose.154func corpusBundleOf(slug, title, world string) *corpusBundle {155 const stamp = "13d5657ed9038808e5fe12ef44e769b40297e9d720e53376f430f224128f2dfc"156 return &corpusBundle{157 manifest: bundle.Manifest{158 Format: bundle.Format,159 FormatVersion: bundle.FormatVersion,160 Conventions: 2,161 Volume: bundle.Volume{Slug: slug, Title: title},162 Version: bundle.Version{Stamp: stamp, CreatedAt: "2026-01-01T00:00:00Z"},163 TileGrid: bundle.TileGrid{SourceZoom: 13, FirstTile: 4064, TileSize: 256, Size: 8192},164 Worlds: []bundle.WorldEntry{{Slug: world, Title: title, UpdatedAt: "2026-01-01T00:00:00Z"}},165 },166 entries: map[string][]byte{167 bundle.WorldEntryName(world, bundle.WorldSuffix): []byte(`{"lenses":[],"collections":[]}`),168 bundle.WorldEntryName(world, bundle.PackedSuffix): bundle.PackLocations(nil),169 bundle.WorldEntryName(world, bundle.TextSuffix): []byte(`{}`),170 },171 }172}173174type corpusBundle struct {175 manifest bundle.Manifest176 entries map[string][]byte177}178179func (v *corpusBundle) Info() hostenv.VolumeInfo {180 manifest := v.manifest181 info := hostenv.VolumeInfo{182 Slug: manifest.Volume.Slug, Title: manifest.Volume.Title, Stamp: manifest.Version.Stamp,183 Release: vnext.Release{Title: manifest.Volume.Title, CreatedAt: manifest.Version.CreatedAt, Revision: manifest.Version.Revision, Stamp: manifest.Version.Stamp, Worlds: len(manifest.Worlds)},184 TileGrid: hostenv.TileGrid{SourceZoom: manifest.TileGrid.SourceZoom, FirstTile: manifest.TileGrid.FirstTile, TileSize: manifest.TileGrid.TileSize, Size: manifest.TileGrid.Size},185 }186 for _, world := range manifest.Worlds {187 info.Worlds = append(info.Worlds, hostenv.WorldInfo{Slug: world.Slug, Title: world.Title, Parent: world.Parent, Points: world.Points, Paths: world.Paths, Areas: world.Areas, UpdatedAt: world.UpdatedAt})188 }189 return info190}191192func (v *corpusBundle) semantic() vnext.Volume {193 semantic, _ := vnext.ImportV3Volume(v.manifest, func(name string) ([]byte, error) { return v.Blob(name) })194 return semantic195}196197func (v *corpusBundle) Outline() vnext.Volume {198 semantic := v.semantic()199 for worldIndex := range semantic.Worlds {200 for setIndex := range semantic.Worlds[worldIndex].FeatureSets {201 semantic.Worlds[worldIndex].FeatureSets[setIndex].Features = nil202 }203 }204 return semantic205}206207func (v *corpusBundle) FeaturePage(request vnext.FeaturePageRequest) (vnext.FeaturePageResult, error) {208 for _, world := range v.semantic().Worlds {209 for _, set := range world.FeatureSets {210 if set.ID != request.FeatureSet {211 continue212 }213 return vnext.FeaturePageResult{Features: set.Features}, nil214 }215 }216 return vnext.FeaturePageResult{}, os.ErrNotExist217}218219func (v *corpusBundle) FeatureSetSummaries() ([]vnext.FeatureSetSummary, error) {220 var out []vnext.FeatureSetSummary221 for _, world := range v.semantic().Worlds {222 for _, set := range world.FeatureSets {223 out = append(out, vnext.FeatureSetSummary{FeatureSet: set.ID, Rows: len(set.Features)})224 }225 }226 return out, nil227}228229func (v *corpusBundle) DemandAddressedFeatures() bool { return false }230231func (v *corpusBundle) RasterTile(name string) (vnext.RasterTile, error) {232 data, err := v.Blob(name)233 return vnext.RasterTile{Name: name, Data: data}, err234}235236func (v *corpusBundle) Blob(name string) ([]byte, error) {237 held, ok := v.entries[name]238 if !ok {239 return nil, os.ErrNotExist240 }241 return held, nil242}243244func (v *corpusBundle) Schema() ([]byte, error) { return vnext.StandardSchema().Canonical() }245func (v *corpusBundle) TableBlock(string) ([]byte, error) { return nil, os.ErrNotExist }246247// firstWorld is the world a volume opens on when nobody has been anywhere.248func firstWorld(t *testing.T, volume hostenv.Volume) string {249 t.Helper()250 worlds := volume.Info().Worlds251 if len(worlds) == 0 {252 t.Fatal("the corpus volume holds no worlds")253 }254 return worlds[0].Slug255}256257// ---------------------------------------------------------------------------258// What the corpus itself says259// ---------------------------------------------------------------------------260261// The expectations below are derived from the corpus rather than copied into262// the tests, so a re-extraction that renumbers a feature moves the question263// and the answer together. What is pinned is the relationship: the page and264// the island must agree with the payload they were rendered from.265266// corpusWorld is the slice of a payload the tests read expectations out of.267type corpusWorld struct {268 Slug string `json:"-"`269 Collections []corpusCollection `json:"collections"`270}271272type corpusCollection struct {273 ID json.Number `json:"id"`274 NativeID string `json:"-"`275 Title string `json:"title"`276 Kind string `json:"kind"`277 Group string `json:"group"`278 Attrs map[string]string `json:"attrs"`279 Features []corpusFeature `json:"features"`280}281282type corpusFeature struct {283 ID json.Number `json:"id"`284 Title string `json:"title"`285}286287func readCorpusWorld(t *testing.T, slug, world string) corpusWorld {288 t.Helper()289 body, err := os.ReadFile(filepath.Join(corpusDir, slug, "worlds", world+".payload.json"))290 if err != nil {291 t.Fatalf("the corpus is missing %s/%s: %v", slug, world, err)292 }293 var out corpusWorld294 if err := json.Unmarshal(body, &out); err != nil {295 t.Fatal(err)296 }297 out.Slug = world298 for index := range out.Collections {299 out.Collections[index].NativeID = world + "/layer/" + out.Collections[index].ID.String()300 }301 return out302}303304// pointTotal is how many locations the corpus packs beside a world, which is305// how many points the map draws when nothing is hidden.306func pointTotal(t *testing.T, slug, world string) int {307 t.Helper()308 body, err := os.ReadFile(filepath.Join(corpusDir, slug, "worlds", world+".locations.json"))309 if err != nil {310 t.Fatalf("the corpus packs no locations for %s/%s: %v", slug, world, err)311 }312 var held struct {313 Locations []json.RawMessage `json:"locations"`314 }315 if err := json.Unmarshal(body, &held); err != nil {316 t.Fatal(err)317 }318 return len(held.Locations)319}320321// shapeTotal is how many features the payload's path and area collections322// carry, which is how many shapes the map draws when nothing is hidden.323func shapeTotal(world corpusWorld) int {324 total := 0325 for _, collection := range world.Collections {326 if collection.Kind != "point" {327 total += len(collection.Features)328 }329 }330 return total331}332333// featureNamed finds one shape feature by title, so a test can point at "the334// MPO Boundary" rather than at a number a re-extraction would move.335func featureNamed(t *testing.T, world corpusWorld, title string) string {336 t.Helper()337 for _, collection := range world.Collections {338 for _, feature := range collection.Features {339 if feature.Title == title {340 return world.Slug + "/feature/" + feature.ID.String()341 }342 }343 }344 t.Fatalf("the corpus payload holds no feature titled %q", title)345 return ""346}347348// firstPoint is one point the corpus packs -- the first location of a world,349// its id and its title -- for the tests that need a card about somewhere.350func firstPoint(t *testing.T, slug, world string) (id, title string) {351 t.Helper()352 body, err := os.ReadFile(filepath.Join(corpusDir, slug, "worlds", world+".locations.json"))353 if err != nil {354 t.Fatalf("the corpus packs no locations for %s/%s: %v", slug, world, err)355 }356 var held struct {357 Locations []struct {358 ID json.Number `json:"id"`359 Title string `json:"title"`360 } `json:"locations"`361 }362 if err := json.Unmarshal(body, &held); err != nil {363 t.Fatal(err)364 }365 if len(held.Locations) == 0 {366 t.Fatalf("the corpus packs an empty location list for %s/%s", slug, world)367 }368 return world + "/feature/" + held.Locations[0].ID.String(), held.Locations[0].Title369}370371// collectionNamed finds one collection by title, for the same reason.372func collectionNamed(t *testing.T, world corpusWorld, title string) corpusCollection {373 t.Helper()374 for _, collection := range world.Collections {375 if collection.Title == title {376 return collection377 }378 }379 t.Fatalf("the corpus payload holds no collection titled %q", title)380 return corpusCollection{}381}382383// ---------------------------------------------------------------------------384// Reading the island385// ---------------------------------------------------------------------------386387// readIsland pulls the island's JSON back out of a rendered answer. It is388// deliberately read the way every consumer reads it -- by id, off the389// document -- rather than by asking the application for it a second way.390func readIsland(t *testing.T, page string) map[string]any {391 t.Helper()392 const open = `<script type="application/json" id="atlas-session-island">`393 start := strings.Index(page, open)394 if start < 0 {395 t.Fatal("the page carries no state island")396 }397 body := page[start+len(open):]398 end := strings.Index(body, "</script>")399 if end < 0 {400 t.Fatal("the state island is not closed")401 }402 var out map[string]any403 if err := json.Unmarshal([]byte(body[:end]), &out); err != nil {404 t.Fatalf("the state island is not JSON: %v\n%s", err, body[:end])405 }406 return out407}408409// entryOf is the island's entry, which every test below has questions for.410func entryOf(t *testing.T, page string) map[string]any {411 t.Helper()412 island := readIsland(t, page)413 entry, _ := island["entry"].(map[string]any)414 if entry == nil {415 t.Fatalf("the island carries no entry:\n%v", island)416 }417 return entry418}