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 "bytes"19 "context"20 "encoding/json"21 "io"22 "net/http"23 "net/http/httptest"24 "net/url"25 "os"26 "path/filepath"27 "strings"28 "testing"2930 "github.com/FelineStateMachine/atlas/format/bundle"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) Manifest() bundle.Manifest { return v.manifest }180181func (v *corpusBundle) Open(name string) (io.ReadCloser, int64, error) {182 held, ok := v.entries[name]183 if !ok {184 return nil, 0, os.ErrNotExist185 }186 return io.NopCloser(bytes.NewReader(held)), int64(len(held)), nil187}188189// firstWorld is the world a volume opens on when nobody has been anywhere.190func firstWorld(t *testing.T, volume hostenv.Volume) string {191 t.Helper()192 worlds := volume.Manifest().Worlds193 if len(worlds) == 0 {194 t.Fatal("the corpus volume holds no worlds")195 }196 return worlds[0].Slug197}198199// ---------------------------------------------------------------------------200// What the corpus itself says201// ---------------------------------------------------------------------------202203// The expectations below are derived from the corpus rather than copied into204// the tests, so a re-extraction that renumbers a feature moves the question205// and the answer together. What is pinned is the relationship: the page and206// the island must agree with the payload they were rendered from.207208// corpusWorld is the slice of a payload the tests read expectations out of.209type corpusWorld struct {210 Collections []corpusCollection `json:"collections"`211}212213type corpusCollection struct {214 ID json.Number `json:"id"`215 Title string `json:"title"`216 Kind string `json:"kind"`217 Group string `json:"group"`218 Attrs map[string]string `json:"attrs"`219 Features []corpusFeature `json:"features"`220}221222type corpusFeature struct {223 ID json.Number `json:"id"`224 Title string `json:"title"`225}226227func readCorpusWorld(t *testing.T, slug, world string) corpusWorld {228 t.Helper()229 body, err := os.ReadFile(filepath.Join(corpusDir, slug, "worlds", world+".payload.json"))230 if err != nil {231 t.Fatalf("the corpus is missing %s/%s: %v", slug, world, err)232 }233 var out corpusWorld234 if err := json.Unmarshal(body, &out); err != nil {235 t.Fatal(err)236 }237 return out238}239240// pointTotal is how many locations the corpus packs beside a world, which is241// how many points the map draws when nothing is hidden.242func pointTotal(t *testing.T, slug, world string) int {243 t.Helper()244 body, err := os.ReadFile(filepath.Join(corpusDir, slug, "worlds", world+".locations.json"))245 if err != nil {246 t.Fatalf("the corpus packs no locations for %s/%s: %v", slug, world, err)247 }248 var held struct {249 Locations []json.RawMessage `json:"locations"`250 }251 if err := json.Unmarshal(body, &held); err != nil {252 t.Fatal(err)253 }254 return len(held.Locations)255}256257// shapeTotal is how many features the payload's path and area collections258// carry, which is how many shapes the map draws when nothing is hidden.259func shapeTotal(world corpusWorld) int {260 total := 0261 for _, collection := range world.Collections {262 if collection.Kind != "point" {263 total += len(collection.Features)264 }265 }266 return total267}268269// featureNamed finds one shape feature by title, so a test can point at "the270// MPO Boundary" rather than at a number a re-extraction would move.271func featureNamed(t *testing.T, world corpusWorld, title string) string {272 t.Helper()273 for _, collection := range world.Collections {274 for _, feature := range collection.Features {275 if feature.Title == title {276 return feature.ID.String()277 }278 }279 }280 t.Fatalf("the corpus payload holds no feature titled %q", title)281 return ""282}283284// firstPoint is one point the corpus packs -- the first location of a world,285// its id and its title -- for the tests that need a card about somewhere.286func firstPoint(t *testing.T, slug, world string) (id, title string) {287 t.Helper()288 body, err := os.ReadFile(filepath.Join(corpusDir, slug, "worlds", world+".locations.json"))289 if err != nil {290 t.Fatalf("the corpus packs no locations for %s/%s: %v", slug, world, err)291 }292 var held struct {293 Locations []struct {294 ID json.Number `json:"id"`295 Title string `json:"title"`296 } `json:"locations"`297 }298 if err := json.Unmarshal(body, &held); err != nil {299 t.Fatal(err)300 }301 if len(held.Locations) == 0 {302 t.Fatalf("the corpus packs an empty location list for %s/%s", slug, world)303 }304 return held.Locations[0].ID.String(), held.Locations[0].Title305}306307// collectionNamed finds one collection by title, for the same reason.308func collectionNamed(t *testing.T, world corpusWorld, title string) corpusCollection {309 t.Helper()310 for _, collection := range world.Collections {311 if collection.Title == title {312 return collection313 }314 }315 t.Fatalf("the corpus payload holds no collection titled %q", title)316 return corpusCollection{}317}318319// ---------------------------------------------------------------------------320// Reading the island321// ---------------------------------------------------------------------------322323// readIsland pulls the island's JSON back out of a rendered answer. It is324// deliberately read the way every consumer reads it -- by id, off the325// document -- rather than by asking the application for it a second way.326func readIsland(t *testing.T, page string) map[string]any {327 t.Helper()328 const open = `<script type="application/json" id="atlas-session-island">`329 start := strings.Index(page, open)330 if start < 0 {331 t.Fatal("the page carries no state island")332 }333 body := page[start+len(open):]334 end := strings.Index(body, "</script>")335 if end < 0 {336 t.Fatal("the state island is not closed")337 }338 var out map[string]any339 if err := json.Unmarshal([]byte(body[:end]), &out); err != nil {340 t.Fatalf("the state island is not JSON: %v\n%s", err, body[:end])341 }342 return out343}344345// entryOf is the island's entry, which every test below has questions for.346func entryOf(t *testing.T, page string) map[string]any {347 t.Helper()348 island := readIsland(t, page)349 entry, _ := island["entry"].(map[string]any)350 if entry == nil {351 t.Fatalf("the island carries no entry:\n%v", island)352 }353 return entry354}