1package workbench23import (4 "html"5 "io"6 "net/http"7 "net/http/httptest"8 "strconv"9 "strings"10 "testing"1112 "github.com/FelineStateMachine/atlas/internal/enrich/maturity"13)1415// The library every page test reads: two builds of one volume, the newer one16// richer and carrying a ledger, and a second volume with a single build.17func testLibrary(t *testing.T) string {18 t.Helper()19 dir := t.TempDir()20 bundleSpec{21 slug: "hollowmere", title: "Hollowmere",22 createdAt: "2026-01-01T00:00:00Z", stamp: strings.Repeat("a", 64),23 worlds: []worldSpec{{24 slug: "overworld", title: "Overworld",25 features: []featureSpec{{id: 1, title: "Gate"}},26 }},27 }.write(t, dir)28 bundleSpec{29 slug: "hollowmere", title: "Hollowmere",30 createdAt: "2026-02-01T00:00:00Z", stamp: strings.Repeat("b", 64),31 worlds: []worldSpec{{32 slug: "overworld", title: "Overworld", icon: "icons/marker.png",33 features: []featureSpec{34 {id: 1, title: "Gate", description: "The way in, and out."},35 {id: 2, title: "Well"},36 {id: 3, title: "Chapel"},37 },38 merged: []map[string]any{{39 "source": "IGN Wiki",40 "slug": "ign",41 "donorFeatures": map[string]any{"point": 4},42 "added": 1,43 "matched": []map[string]any{{"d": 11, "w": 1, "px": 38, "e": true}},44 "held": []map[string]any{{"d": 12, "t": "Old Well", "why": "name 200px away"}},45 "alignment": "14 names, 3px residual",46 }},47 }},48 }.write(t, dir)49 bundleSpec{50 slug: "driftfen", title: "Driftfen",51 createdAt: "2026-03-01T00:00:00Z", stamp: strings.Repeat("c", 64),52 worlds: []worldSpec{{53 slug: "marsh", title: "Marsh",54 features: []featureSpec{{id: 7, title: "Reed Gate"}},55 }},56 }.write(t, dir)57 return dir58}5960func testWorkbench(t *testing.T, dir string) *Workbench {61 t.Helper()62 held, err := New(Options{63 Targets: Targets{Registry: dir},64 Sources: testSources(),65 Runtime: []byte("/* the vendored runtime */"),66 })67 if err != nil {68 t.Fatal(err)69 }70 return held71}7273func testSources() []Source {74 return []Source{75 {76 Name: "mapgenie", Label: "MapGenie",77 Attribution: "Maps and pin data by MapGenie and its contributors.",78 IDSpace: "native",79 },80 {81 Name: "ign", Label: "IGN Wiki",82 License: "Reproduced under IGN's terms.",83 IDSpace: "native", Crawlable: true, Pair: true,84 TargetHint: "an IGN wikimap as <objectSlug>/<mapSlug>, e.g. cyberpunk-2077/night-city",85 },86 {87 Name: "nasa-trek", Label: "NASA Trek",88 License: "Public domain.",89 Attribution: "Global mosaics from NASA's Solar System Treks.",90 IDSpace: "derived", Crawlable: true,91 TargetHint: "a body, e.g. mars",92 },93 }94}9596func site(t *testing.T, held *Workbench) *httptest.Server {97 t.Helper()98 server := httptest.NewServer(held)99 t.Cleanup(server.Close)100 return server101}102103func get(t *testing.T, server *httptest.Server, path string) (*http.Response, string) {104 t.Helper()105 response, err := server.Client().Get(server.URL + path)106 if err != nil {107 t.Fatal(err)108 }109 t.Cleanup(func() { response.Body.Close() })110 body, err := io.ReadAll(response.Body)111 if err != nil {112 t.Fatal(err)113 }114 // A page is read the way a person reads it: html/template escapes a plus115 // sign and an apostrophe, and a test that asserted on the escapes would be116 // asserting about the encoder rather than about the page.117 return response, html.UnescapeString(string(body))118}119120func wants(t *testing.T, page, body string, wanted ...string) {121 t.Helper()122 for _, want := range wanted {123 if !strings.Contains(body, want) {124 t.Errorf("the %s page misses %q", page, want)125 }126 }127}128129func TestTheLibraryPageHeadlinesEveryVolumeWithItsScore(t *testing.T) {130 dir := testLibrary(t)131 server := site(t, testWorkbench(t, dir))132133 response, body := get(t, server, "/")134 if response.StatusCode != http.StatusOK {135 t.Fatalf("the library page answered %d", response.StatusCode)136 }137 if got := response.Header.Get("Content-Security-Policy"); got != contentSecurityPolicy {138 t.Errorf("the page carries %q as its policy", got)139 }140 wants(t, "library", body, "Hollowmere", "hollowmere", "Driftfen", dir, "IGN Wiki")141142 // The figures are the serving build's, and the serving build is the newer143 // one: the score on the page is the score the enrich lane would compute.144 table, err := maturity.Points()145 if err != nil {146 t.Fatal(err)147 }148 held := &library{dir: dir, table: table}149 volumes, skipped, err := held.volumes()150 if err != nil {151 t.Fatal(err)152 }153 if len(skipped) != 0 {154 t.Fatalf("bundles were skipped: %v", skipped)155 }156 for _, volume := range volumes {157 wants(t, "library", body, strconv.Itoa(volume.Serving().Total))158 }159160 // Hollowmere's newer build is richer, and the movement says so.161 var hollowmere *volume162 for _, volume := range volumes {163 if volume.Slug == "hollowmere" {164 hollowmere = volume165 }166 }167 moved := hollowmere.Movement()168 if moved == nil || moved.Delta <= 0 {169 t.Fatalf("the richer build did not move the score: %+v", moved)170 }171 wants(t, "library", body, "+"+strconv.Itoa(moved.Delta))172}173174func TestTheServingBuildIsTheOneTheRegistryWouldFoldTo(t *testing.T) {175 table, err := maturity.Points()176 if err != nil {177 t.Fatal(err)178 }179 held := &library{dir: testLibrary(t), table: table}180 volumes, _, err := held.volumes()181 if err != nil {182 t.Fatal(err)183 }184 for _, volume := range volumes {185 if want := maturity.Serving(volume.Builds); volume.Serving() != want {186 t.Errorf("%s serves %s; the registry's fold picks %s",187 volume.Slug, volume.Serving().File, want.File)188 }189 }190}191192func TestTheMeasurementPageIsTheScoreThenTheDiagnosticsThenTheLedger(t *testing.T) {193 server := site(t, testWorkbench(t, testLibrary(t)))194195 response, body := get(t, server, "/volume/hollowmere")196 if response.StatusCode != http.StatusOK {197 t.Fatalf("the measurement page answered %d", response.StatusCode)198 }199 wants(t, "measurement", body,200 // the headline201 "points", "against the build before it",202 // the per-world breakdown203 "overworld", "collections",204 // the five axes, carried as diagnostics205 "annotation", "cartography", "structure", "icons", "conventions",206 "nothing gates on them",207 // the whole ledger, not a count of it208 "IGN Wiki", "Old Well", "name 200px away", "14 names, 3px residual",209 // both builds, the newer marked210 "serving")211212 newer := strings.Index(body, strings.Repeat("b", 12))213 older := strings.Index(body, strings.Repeat("a", 12))214 if newer < 0 || older < 0 || newer > older {215 t.Errorf("the builds are not newest first (newer at %d, older at %d)", newer, older)216 }217218 if response, _ := get(t, server, "/volume/nowhere"); response.StatusCode != http.StatusNotFound {219 t.Errorf("an unknown volume answered %d", response.StatusCode)220 }221}222223func TestTheDiffPageIsHeadlinedByTheScoreDelta(t *testing.T) {224 dir := testLibrary(t)225 server := site(t, testWorkbench(t, dir))226227 table, err := maturity.Points()228 if err != nil {229 t.Fatal(err)230 }231 held := &library{dir: dir, table: table}232 volume, _, err := held.volumeBySlug("hollowmere")233 if err != nil {234 t.Fatal(err)235 }236 from, to := volume.Builds[1], volume.Builds[0]237238 response, body := get(t, server, "/volume/hollowmere/diff?a="+from.File+"&b="+to.File)239 if response.StatusCode != http.StatusOK {240 t.Fatalf("the diff page answered %d: %s", response.StatusCode, body)241 }242 moved := maturity.Compare(from, to)243 wants(t, "diff", body,244 "+"+strconv.Itoa(moved.Delta), "richer",245 // what moved, under the headline246 "Score by world", "Axes", "point features", "described share",247 // the features themselves248 "Well", "Chapel",249 // and the ledgers' agreement250 "Matched-pair stability")251252 // The reverse direction is the same comparison read the other way.253 _, reverse := get(t, server, "/volume/hollowmere/diff?a="+to.File+"&b="+from.File)254 wants(t, "reversed diff", reverse, strconv.Itoa(-moved.Delta), "poorer")255256 if response, _ := get(t, server, "/volume/hollowmere/diff?a=nowhere.atlas&b="+to.File); response.StatusCode != http.StatusBadRequest {257 t.Errorf("a diff naming a build that is not there answered %d", response.StatusCode)258 }259}260261func TestTheSourcePagePrintsWhatEachVolumeOwes(t *testing.T) {262 server := site(t, testWorkbench(t, testLibrary(t)))263264 response, body := get(t, server, "/sources")265 if response.StatusCode != http.StatusOK {266 t.Fatalf("the sources page answered %d", response.StatusCode)267 }268 wants(t, "sources", body,269 "MapGenie", "IGN Wiki", "NASA Trek",270 "Maps and pin data by MapGenie and its contributors.",271 "Public domain.", "Reproduced under IGN's terms.",272 "native ids", "derived ids",273 "crawlable", "archived captures only")274}275276func TestTheAssetsAreTheWorkbenchsOwnAndTheRuntimeIsOptional(t *testing.T) {277 server := site(t, testWorkbench(t, testLibrary(t)))278279 response, body := get(t, server, "/assets/workbench.css")280 if response.StatusCode != http.StatusOK {281 t.Fatalf("the stylesheet answered %d", response.StatusCode)282 }283 if got := response.Header.Get("Content-Type"); !strings.HasPrefix(got, "text/css") {284 t.Errorf("the stylesheet arrived as %q", got)285 }286 wants(t, "stylesheet", body, "--accent", ".op-row")287288 if response, _ := get(t, server, "/assets/htmx.js"); response.StatusCode != http.StatusOK {289 t.Errorf("the runtime the wiring handed over answered %d", response.StatusCode)290 }291 if response, _ := get(t, server, "/assets/nothing.js"); response.StatusCode != http.StatusNotFound {292 t.Errorf("an asset nobody has answered %d", response.StatusCode)293 }294295 // A workbench mounted without a runtime is a working workbench.296 bare, err := New(Options{Targets: Targets{Registry: t.TempDir()}})297 if err != nil {298 t.Fatal(err)299 }300 bareSite := site(t, bare)301 if response, _ := get(t, bareSite, "/assets/htmx.js"); response.StatusCode != http.StatusNotFound {302 t.Errorf("a workbench with no runtime served one: %d", response.StatusCode)303 }304 if response, body := get(t, bareSite, "/"); response.StatusCode != http.StatusOK {305 t.Errorf("the library page of an empty registry answered %d: %s", response.StatusCode, body)306 }307}308309// TestTheMeasurementPageCarriesTheMeasuredScore holds the page to the number310// the measurement lane itself reports for the same bytes. The bundle is311// stated, not captured: what each score point is worth is the maturity312// package's own suite's business; this test's business is that the page313// prints that verdict rather than a stale or reformatted one, table version314// included. A stated bundle with prose, an icon and a ledger scores well315// clear of zero, so a page that went blank cannot pass by printing nothing.316func TestTheMeasurementPageCarriesTheMeasuredScore(t *testing.T) {317 dir := t.TempDir()318 path := bundleSpec{319 slug: "hollowmere", title: "Hollowmere",320 worlds: []worldSpec{{321 slug: "overworld", title: "Overworld", icon: "icons/marker.png",322 features: []featureSpec{323 {id: 1, title: "Gate", description: "The way in, and out."},324 {id: 2, title: "Well"},325 },326 merged: []map[string]any{{327 "source": "IGN Wiki", "slug": "ign",328 "donorFeatures": map[string]any{"point": 4},329 "added": 1,330 "matched": []map[string]any{{"d": 11, "w": 1, "px": 38, "e": true}},331 "alignment": "14 names, 3px residual",332 }},333 }},334 }.write(t, dir)335 table, err := maturity.Points()336 if err != nil {337 t.Fatal(err)338 }339 measured, err := maturity.Measure(path, table)340 if err != nil {341 t.Fatal(err)342 }343 if measured.Total <= 0 {344 t.Fatalf("the stated bundle scores %d; the page test needs a number that cannot be mistaken for a blank", measured.Total)345 }346347 server := site(t, testWorkbench(t, dir))348 response, body := get(t, server, "/volume/hollowmere")349 if response.StatusCode != http.StatusOK {350 t.Fatalf("the measurement page answered %d", response.StatusCode)351 }352 wants(t, "measurement", body, "Hollowmere", strconv.Itoa(measured.Total), "point table v"+strconv.Itoa(table.Version), "world")353354 _, library := get(t, server, "/")355 wants(t, "library", library, "Hollowmere", strconv.Itoa(measured.Total))356}