package web import ( "fmt" "go/ast" "go/parser" "go/token" "os" "sort" "strings" "testing" ) // TestNoDirectMetaCopyReadOnPages — the guard for R-560's whole point. // // Catalog copy (`Description`, `AppInfo`, the deploy-field labels, `OptionalConfig`, `Integrations`, // the `DataPaths` labels, the initial-credentials note) is HUNGARIAN in the value the stack manager // caches. A page reaches the household's language only by going through `Metadata.For(lang)` — // `stacks.LocalizeStacks`, `LocalizeStack`, `LocalizeStackPtr` or `MetaFor`. Read `x.Meta.` // straight off a manager value and an English household silently gets Hungarian: no error, no log, // nothing that looks wrong on the page. That is precisely the failure mode this project has shipped // nine times under a comment that read as settled. // // So every direct read of a copy field off a `.Meta` in this package is LISTED BELOW WITH A REASON. // A new one fails this test, and the author then has two honest choices: route it through For(lang), // or add it here saying why it may stay Hungarian. // // WHAT THIS TEST CANNOT DO: it reads the source, so it cannot tell a localised receiver from an // unlocalised one — `found.Meta.AppInfo` looks the same either way. It catches the ARRIVAL of a new // copy read, which is when a human has to think, and that is the whole claim made for it. // // RED-PROOF (2026-09-20): adding `_ = stack.Meta.Description` to handlers.go failed this test // naming handlers.go and Description; removing it went green. var metaCopyReadAllowlist = map[string]string{ // The app page localises `found` in one place at the top of appDetailHandler and every read // below it — these included — is of that localised value. "handlers.go:AppInfo": "appDetailHandler reads it off the LocalizeStackPtr'd `found`", "handlers.go:InitialCreds": "appDetailHandler: the nil check and the note, both off localised `found`", // buildDataPathCards is called with the same localised `found`; the labels it renders are the // English ones when the household is on English. "datapath_card.go:DataPaths": "buildDataPathCards is handed the localised stack by appDetailHandler", } // metaCopyFields are the Metadata fields that carry customer-facing TEXT. Everything else on // Metadata — Slug, Subdomain, OpenPath, BrandColor, Category, Resources, Lifecycle, CatalogSince, // HealthCheck, SMTPMapping, Backup — is configuration and reads the same in every language. // DisplayName is deliberately NOT here: an app's name is not translated (10-localisation.md §11, // operator ruling 7). var metaCopyFields = map[string]bool{ "Description": true, "AppInfo": true, "DeployFields": true, "OptionalConfig": true, "Integrations": true, "DataPaths": true, "InitialCreds": true, } func TestNoDirectMetaCopyReadOnPages(t *testing.T) { entries, err := os.ReadDir(".") if err != nil { t.Fatal(err) } seen := map[string]bool{} var unlisted []string for _, e := range entries { name := e.Name() if e.IsDir() || !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") { continue } fset := token.NewFileSet() f, err := parser.ParseFile(fset, name, nil, 0) // comments are not walked — only real reads count if err != nil { t.Fatalf("%s: %v", name, err) } ast.Inspect(f, func(n ast.Node) bool { outer, ok := n.(*ast.SelectorExpr) if !ok || !metaCopyFields[outer.Sel.Name] { return true } inner, ok := outer.X.(*ast.SelectorExpr) if !ok || inner.Sel.Name != "Meta" { return true } key := name + ":" + outer.Sel.Name seen[key] = true if _, allowed := metaCopyReadAllowlist[key]; !allowed { unlisted = append(unlisted, fmt.Sprintf("%s (%s)", key, fset.Position(outer.Pos()))) } return true }) } sort.Strings(unlisted) for _, u := range unlisted { t.Errorf("catalog COPY read straight off .Meta: %s\n"+ " Route it through stacks.LocalizeStack(s)/LocalizeStackPtr/MetaFor(lang), or add it to\n"+ " metaCopyReadAllowlist in this file with the reason it may stay Hungarian.", u) } // A stale allow-list entry is a lie about what the code does — it must go when its read goes. for key := range metaCopyReadAllowlist { if !seen[key] { t.Errorf("allow-list entry %q no longer matches any read — delete it", key) } } }