1// Package cells_test holds the server's cell arithmetic to the shared2// contract vectors -- the same language-neutral fixtures the TypeScript lane3// answers in analysis/test/vectors.test.ts.4//5// It lives here, outside internal/app, because it reads files and the hostenv6// rule says nothing under internal/app may know what a filesystem is. The7// arithmetic on trial is internal/app/cells; this package is only the reader8// standing between it and the fixtures.9//10// This is what makes a second implementation of a cell system tolerable. The11// TypeScript one at analysis/cellsystems owns the contract; the Go copy12// exists so the server can answer the held-cell question without asking the13// seam, and the two agree because both are held to14// analysis/testdata/cells/*.json rather than to each other's company. The15// hand-derived cases there are marked as such -- the geohash numbers halved16// out on paper, the S2 ones the Go library's own test values -- and the17// corpus grounds are read back against the shipped bundles below, so neither18// lane's descriptor can drift from the declaration a reader really meets.19package cells_test2021import (22 "encoding/json"23 "fmt"24 "math"25 "os"26 "path/filepath"27 "sort"28 "strings"29 "testing"3031 "github.com/FelineStateMachine/atlas/internal/app/cells"32)3334// ground is one entry of grounds.json: what the seam's `Ground` descriptor35// carries, and the two derived facts every consumer re-checks. `lens` is null36// when the application had no lens open, and its two fields are independently37// nullable, which is the whole of the surface ladder's input.38type ground struct {39 Key string `json:"key"`40 Size float64 `json:"tileGridSize"`41 Lens *lensOf `json:"lens"`42 World struct {43 Attrs map[string]string `json:"attrs"`44 } `json:"world"`45 Provenance *struct {46 Volume string `json:"volume"`47 World string `json:"world"`48 Lens string `json:"lens"`49 LensIndex int `json:"lensIndex"`50 } `json:"provenance"`51 Surface [4]float64 `json:"surfaceExtent"`52 Systems []string `json:"systems"`53}5455type lensOf struct {56 Surface *cells.Rect `json:"surface"`57 Bounds *cells.Rect `json:"bounds"`58}5960// extent is the ground every system divides, through the Go ladder.61func (g ground) extent() cells.Extent {62 var surface, bounds *cells.Rect63 if g.Lens != nil {64 surface, bounds = g.Lens.Surface, g.Lens.Bounds65 }66 return cells.SurfaceExtent(surface, bounds, g.Size)67}6869// geohashExtent is the ground GEOHASH divides -- the world square on a plane70// with no earth anchoring, the surface ladder everywhere else (Part 1 of the71// plane ruling; internal/app/cells, GeohashGround).72func (g ground) geohashExtent() cells.Extent {73 var surface, bounds *cells.Rect74 if g.Lens != nil {75 surface, bounds = g.Lens.Surface, g.Lens.Bounds76 }77 return cells.GeohashGround(surface, bounds, g.Size, g.World.Attrs)78}7980// TestGroundsAgreeOnSurfaceAndSystems holds the two derived fields every81// recorded ground carries: the rectangle it hands its systems, and which82// systems are willing to take it. The second is the answer the session's83// validation is made of, so a divergence here is a request the server would84// accept and the navigator would never have offered, or the reverse.85func TestGroundsAgreeOnSurfaceAndSystems(t *testing.T) {86 grounds := loadGrounds(t)87 keys := make([]string, 0, len(grounds))88 for key := range grounds {89 keys = append(keys, key)90 }91 sort.Strings(keys)92 want := []string{93 "bend-or/2026-08-02/0",94 "mars/global/0",95 "test/earth-8192x4096",96 "test/sphere-8192x4096",97 "test/square-1024",98 "test/square-1024-bounds",99 "test/square-1024-no-lens",100 }101 if fmt.Sprint(keys) != fmt.Sprint(want) {102 t.Fatalf("the fixture records grounds %v, want %v", keys, want)103 }104 for key, at := range grounds {105 got := at.extent()106 if !nearAll(got, at.Surface) {107 t.Errorf("%s: surface extent = [%g %g %g %g], recorded %v",108 key, got.MinX, got.MinY, got.MaxX, got.MaxY, at.Surface)109 }110 systems := cells.ApplicableSystems(at.World.Attrs)111 if fmt.Sprint(systems) != fmt.Sprint(at.Systems) {112 t.Errorf("%s: systems = %v, recorded %v", key, systems, at.Systems)113 }114 }115 t.Logf("%d grounds reproduced", len(grounds))116}117118// TestCorpusGroundsMatchTheShippedBundles ties the recorded descriptors back119// to the public corpus: a ground naming a bundle in its provenance must carry120// exactly the declaration that bundle ships, read from the payload rather121// than trusted. This is the fixture's anchor now that the recorded tours are122// gone -- a descriptor edited to make a test pass would stop matching a file123// nobody edits by hand.124func TestCorpusGroundsMatchTheShippedBundles(t *testing.T) {125 corpus := filepath.Join("..", "..", "testdata", "corpus", "bundles")126 tied := 0127 for key, at := range loadGrounds(t) {128 if at.Provenance == nil {129 continue130 }131 from := at.Provenance132 var payload struct {133 Attrs map[string]string `json:"attrs"`134 Lenses []struct {135 Name string `json:"name"`136 Surface *cells.Rect `json:"surface"`137 Bounds *cells.Rect `json:"bounds"`138 } `json:"lenses"`139 }140 readInto(t, filepath.Join(corpus, from.Volume, "worlds", from.World+".payload.json"), &payload)141 if fmt.Sprint(payload.Attrs) != fmt.Sprint(at.World.Attrs) {142 t.Errorf("%s: the recorded attrs %v are not the bundle's %v",143 key, at.World.Attrs, payload.Attrs)144 }145 if from.LensIndex >= len(payload.Lenses) {146 t.Fatalf("%s: the bundle has no lens %d", key, from.LensIndex)147 }148 lens := payload.Lenses[from.LensIndex]149 if lens.Name != from.Lens {150 t.Errorf("%s: lens %d is %q, the ground says %q", key, from.LensIndex, lens.Name, from.Lens)151 }152 if at.Lens == nil ||153 fmt.Sprint(lens.Surface) != fmt.Sprint(at.Lens.Surface) ||154 fmt.Sprint(lens.Bounds) != fmt.Sprint(at.Lens.Bounds) {155 t.Errorf("%s: the recorded lens does not match the bundle's", key)156 }157 var volume struct {158 TileGrid struct {159 Size float64 `json:"size"`160 } `json:"tileGrid"`161 }162 readInto(t, filepath.Join(corpus, from.Volume, "volume.json"), &volume)163 if volume.TileGrid.Size != at.Size {164 t.Errorf("%s: tileGridSize %g, the bundle declares %g", key, at.Size, volume.TileGrid.Size)165 }166 tied++167 }168 if tied < 2 {169 t.Fatalf("only %d grounds are tied to the corpus; bend-or and mars both should be", tied)170 }171}172173// TestVectorsHoldTheGoSide walks every case of every family the server has an174// answer for, and reports what it skipped.175func TestVectorsHoldTheGoSide(t *testing.T) {176 grounds := loadGrounds(t)177 checked, skipped := map[string]int{}, map[string]int{}178179 for _, family := range []string{"surface", "containment", "identity", "hierarchy", "input", "carry"} {180 for _, held := range loadCases(t, family) {181 at, known := grounds[held.Ground]182 if !known {183 t.Fatalf("%s/%s: names a ground the fixture does not record: %q",184 family, held.Name, held.Ground)185 }186 got, answered := answer(t, at, held)187 if !answered {188 skipped[callOf(held)]++189 continue190 }191 if want := string(held.Expect); !sameJSON(t, got, want) {192 t.Errorf("%s/%s: %s%s = %s, the vector records %s",193 family, held.Name, callOf(held), string(held.Args), toJSON(t, got), want)194 }195 checked[callOf(held)]++196 }197 }198199 // The floor is not a target. It is there so that a dispatch that quietly200 // stopped recognizing a call -- a renamed method, a family that moved --201 // fails instead of passing with nothing to say.202 if total(checked) < 80 || checked["s2.contains"] < 10 || checked["s2.descendTarget"] < 5 {203 t.Fatalf("only %d vectors were answered (%d s2.contains, %d s2.descendTarget); the dispatch has come loose",204 total(checked), checked["s2.contains"], checked["s2.descendTarget"])205 }206 t.Logf("%d vectors reproduced", total(checked))207 for _, line := range tally(checked) {208 t.Logf(" held: %s", line)209 }210 t.Logf("%d skipped, every one of them a seam concern", total(skipped))211 for _, line := range tally(skipped) {212 t.Logf(" seam: %s", line)213 }214}215216// TestGeohashHalvingUndoesItself pins the arithmetic fact the grid stands217// on. The halving forward (an address to its218// rectangle) and the halving backward (a point to its address) are one walk,219// and undoing the halving character by character lands exactly on the ground220// -- every division is a halving of a finite float, so the inverse is a221// doubling and the comparison is exact, no epsilon.222func TestGeohashHalvingUndoesItself(t *testing.T) {223 grounds := loadGrounds(t)224 checked := 0225 for key, at := range grounds {226 surface := at.extent()227 for _, hash := range sampleHashes() {228 held := cells.GeohashExtent(surface, hash)229 // The centre of the cell, asked back at the same depth, spells230 // the same address.231 centre := [2]float64{(held.MinX + held.MaxX) / 2, (held.MinY + held.MaxY) / 2}232 if got := cells.GeohashCellAt(surface, centre[0], centre[1], len(hash)); got != hash {233 t.Fatalf("%s: the centre of %q addresses to %q", key, hash, got)234 }235 // And undoing the halvings recovers the ground, exactly.236 if got := grow(held, hash); got != surface {237 t.Fatalf("%s: undoing %q gives %+v, the ground is %+v", key, hash, got, surface)238 }239 checked++240 }241 }242 t.Logf("%d cells halved and un-halved across %d grounds", checked, len(grounds))243}244245// sampleHashes is a deterministic spread of addresses: every depth-1 cell,246// and a diagonal of deeper ones so every character of the alphabet appears at247// every position at least once.248func sampleHashes() []string {249 alphabet := cells.GeohashAlphabet250 out := make([]string, 0, 3*len(alphabet))251 for at := 0; at < len(alphabet); at++ {252 first := alphabet[at]253 second := alphabet[(at+7)%len(alphabet)]254 third := alphabet[(at+19)%len(alphabet)]255 out = append(out,256 string(first),257 string([]byte{first, second}),258 string([]byte{first, second, third}),259 )260 }261 return out262}263264// grow finds the ground whose halving by `hash` is `held`, by undoing the two265// candidate directions of each halving. It is exact: every halving is a266// division by two of a finite float, so the inverse is a doubling.267func grow(held cells.Extent, hash string) cells.Extent {268 // One character, five halvings: undo them in reverse, choosing the side269 // the character's bits name.270 out := held271 for i := len(hash) - 1; i >= 0; i-- {272 value := strings.IndexByte(cells.GeohashAlphabet, hash[i])273 if value < 0 {274 continue275 }276 splitX := (i*5)%2 == 0277 for m := 4; m >= 0; m-- {278 // Walk the masks backwards, alternating from where this279 // character's last halving left off.280 axisX := splitX == (m%2 == 0)281 width := out.MaxX - out.MinX282 height := out.MaxY - out.MinY283 if axisX {284 if value&cells.Masks[m] != 0 {285 out.MinX -= width286 } else {287 out.MaxX += width288 }289 } else {290 if value&cells.Masks[m] != 0 {291 out.MinY -= height292 } else {293 out.MaxY += height294 }295 }296 }297 }298 return out299}300301func callOf(held vector) string {302 if held.System == "" {303 return held.Call304 }305 return held.System + "." + held.Call306}307308func total(counts map[string]int) int {309 sum := 0310 for _, count := range counts {311 sum += count312 }313 return sum314}315316func tally(counts map[string]int) []string {317 out := make([]string, 0, len(counts))318 for call, count := range counts {319 out = append(out, fmt.Sprintf("%3d %s", count, call))320 }321 sort.Strings(out)322 return out323}324325// vector is one recorded call. The arguments are heterogeneous by family, so326// they stay raw until the call is known.327type vector struct {328 Name string `json:"name"`329 Ground string `json:"ground"`330 System string `json:"system"`331 Call string `json:"call"`332 Args json.RawMessage `json:"args"`333 Expect json.RawMessage `json:"expect"`334 Note string `json:"note"`335}336337// answer is the whole dispatch: the calls the server implements, and nothing338// else. The false result means "this call is the seam's", which is a skip339// rather than a failure -- the plan, the ring, the pole closure and the style340// tokens are not arithmetic the application has any use for.341func answer(t *testing.T, at ground, held vector) (any, bool) {342 t.Helper()343 switch held.Call {344 case "surfaceExtent":345 got := at.extent()346 return [4]float64{got.MinX, got.MinY, got.MaxX, got.MaxY}, true347 case "applicableSystems":348 return cells.ApplicableSystems(at.World.Attrs), true349 case "appliesTo":350 return cells.Applicable(at.World.Attrs, held.System), true351 case "geohashCellAt":352 var point [2]float64353 var depth int354 args(t, held, &point, &depth)355 if mapping, anchored := cells.EarthMercator(at.World.Attrs); anchored {356 return cells.GeohashRealCellAt(mapping, point[0], point[1], depth), true357 }358 return cells.GeohashCellAt(at.geohashExtent(), point[0], point[1], depth), true359 case "equivalentCell":360 // The carry is server arithmetic now: cycling the grid system moves361 // the held cell through cells.Equivalent, so the recorded pairs bind362 // the Go side the same way they bind the seam's.363 var from, to, id string364 args(t, held, &from, &to, &id)365 return cells.Equivalent(at.extent(), at.World.Attrs, from, to, id), true366 }367368 // The navigator's field length is a server fact -- grid.go answers it to369 // the template -- so the vector binds the Go side too, per system.370 if held.Call == "inputLength" {371 switch held.System {372 case cells.SystemGeohash:373 return cells.GeohashDepthOf(at.World.Attrs), true374 case cells.SystemS2:375 return cells.S2InputLength, true376 }377 }378379 switch held.System {380 case cells.SystemGeohash:381 mapping, anchored := cells.EarthMercator(at.World.Attrs)382 switch held.Call {383 case "contains":384 var hash string385 var point [2]float64386 args(t, held, &hash, &point)387 if anchored {388 return cells.GeohashRealHeld(mapping, hash)(point[0], point[1]), true389 }390 return cells.GeohashHeld(at.geohashExtent(), hash)(point[0], point[1]), true391 case "descendTarget":392 var hash string393 var point [2]float64394 args(t, held, &hash, &point)395 if anchored {396 depth := cells.GeohashBaseDepth(mapping)397 if hash != "" {398 depth = len(hash) + 1399 }400 return cells.GeohashRealCellAt(mapping, point[0], point[1], depth), true401 }402 return cells.GeohashCellAt(at.geohashExtent(), point[0], point[1], len(hash)+1), true403 }404 case cells.SystemS2:405 mapping, mapped := cells.MappingOf(at.World.Attrs)406 if !mapped {407 t.Fatalf("%s: an S2 case on a ground with no flattening", held.Name)408 }409 switch held.Call {410 case "contains":411 var token string412 var point [2]float64413 args(t, held, &token, &point)414 return cells.S2Held(mapping, token)(point[0], point[1]), true415 case "descendTarget":416 var token string417 var point [2]float64418 args(t, held, &token, &point)419 return cells.S2Descend(mapping, token, point[0], point[1]), true420 case "locate":421 var point [2]float64422 args(t, held, &point)423 cell := cells.S2CellAt(mapping, point[0], point[1])424 if cell == "" {425 return nil, true426 }427 return map[string]string{"label": "S2", "value": cell}, true428 case "level":429 var token string430 args(t, held, &token)431 return cells.S2Level(token), true432 case "parent":433 var token string434 args(t, held, &token)435 return cells.S2Parent(token), true436 case "children":437 var token string438 args(t, held, &token)439 return cells.S2Children(token), true440 case "childIndex":441 // The sibling ordinal is not a function the server has; it is a442 // position in the child list, and asking for it that way holds443 // the list's *order* as well as its members.444 var token string445 args(t, held, &token)446 for at, sibling := range cells.S2Children(cells.S2Parent(token)) {447 if sibling == token {448 return at, true449 }450 }451 t.Errorf("%s: %q is not among its parent's children", held.Name, token)452 return -1, true453 case "parseInput":454 var text string455 args(t, held, &text)456 cell, ok := cells.S2ParseCell(text)457 if !ok {458 return nil, true459 }460 return cell, true461 }462 }463 return nil, false464}465466// args decodes the recorded argument list positionally into the pointers467// given. A case whose shape does not match the call it names is a broken468// fixture, and says so rather than passing.469func args(t *testing.T, held vector, into ...any) {470 t.Helper()471 var raw []json.RawMessage472 if err := json.Unmarshal(held.Args, &raw); err != nil {473 t.Fatalf("%s: args are not a list: %v", held.Name, err)474 }475 if len(raw) != len(into) {476 t.Fatalf("%s: %s takes %d arguments, the vector records %d",477 held.Name, held.Call, len(into), len(raw))478 }479 for at, target := range into {480 if err := json.Unmarshal(raw[at], target); err != nil {481 t.Fatalf("%s: argument %d: %v", held.Name, at, err)482 }483 }484}485486// ---------------------------------------------------------------------------487// Reading the fixtures488// ---------------------------------------------------------------------------489490const vectorsDir = "../../analysis/testdata/cells"491492func loadGrounds(t *testing.T) map[string]ground {493 t.Helper()494 var file struct {495 Grounds map[string]ground `json:"grounds"`496 }497 readInto(t, filepath.Join(vectorsDir, "grounds.json"), &file)498 return file.Grounds499}500501func loadCases(t *testing.T, family string) []vector {502 t.Helper()503 var file struct {504 Cases []vector `json:"cases"`505 }506 readInto(t, filepath.Join(vectorsDir, family+".json"), &file)507 if len(file.Cases) == 0 {508 t.Fatalf("%s.json records no cases", family)509 }510 return file.Cases511}512513func readInto(t *testing.T, path string, into any) {514 t.Helper()515 body, err := os.ReadFile(path)516 if err != nil {517 t.Fatalf("the shared vectors are the oracle and could not be read: %v", err)518 }519 if err := json.Unmarshal(body, into); err != nil {520 t.Fatalf("%s: %v", path, err)521 }522}523524// sameJSON compares an answer with a recorded expectation through JSON, which525// is the form the vectors are written in and the only form both lanes share.526func sameJSON(t *testing.T, got any, want string) bool {527 t.Helper()528 var mine, theirs any529 if err := json.Unmarshal([]byte(toJSON(t, got)), &mine); err != nil {530 t.Fatalf("answer is not JSON: %v", err)531 }532 if err := json.Unmarshal([]byte(want), &theirs); err != nil {533 t.Fatalf("expectation is not JSON: %v", err)534 }535 return equalJSON(mine, theirs)536}537538func toJSON(t *testing.T, value any) string {539 t.Helper()540 body, err := json.Marshal(value)541 if err != nil {542 t.Fatalf("answer will not marshal: %v", err)543 }544 return string(body)545}546547// equalJSON compares decoded JSON with a tolerance on numbers: a recorded548// coordinate is a decimal literal and an answer is a float, and the two agree549// to further places than any cell edge cares about.550func equalJSON(mine, theirs any) bool {551 switch left := mine.(type) {552 case nil:553 return theirs == nil554 case bool:555 right, ok := theirs.(bool)556 return ok && left == right557 case float64:558 right, ok := theirs.(float64)559 return ok && math.Abs(left-right) < 1e-6560 case string:561 right, ok := theirs.(string)562 return ok && left == right563 case []any:564 right, ok := theirs.([]any)565 if !ok || len(left) != len(right) {566 return false567 }568 for at := range left {569 if !equalJSON(left[at], right[at]) {570 return false571 }572 }573 return true574 case map[string]any:575 right, ok := theirs.(map[string]any)576 if !ok || len(left) != len(right) {577 return false578 }579 for key, value := range left {580 other, held := right[key]581 if !held || !equalJSON(value, other) {582 return false583 }584 }585 return true586 }587 return false588}589590func nearAll(got cells.Extent, want [4]float64) bool {591 return near(got.MinX, want[0]) && near(got.MinY, want[1]) &&592 near(got.MaxX, want[2]) && near(got.MaxY, want[3])593}594595func near(got, want float64) bool { return math.Abs(got-want) < 1e-6 }