Files
felhom-controller/controller/internal/web/meta_copy_allowlist_test.go
T
admin fd28d70bff v0.308.0: controller batch 2026-10-12 — clear-address warning (R-922), secret sweep over every page (R-255), read_failed (R-266), no NVMe heat band (R-333), share globe (R-577), Hungarian dates and numbers (R-562), lost-drive option pinned (R-521)
Operator rulings 2026-10-12, 09 §3 decisions 196, 204-207, 223, 227. MinAgent 0.131.0 unchanged.
R-255's sweep found one leak, fixed here: an optional-config secret_input was rendered into its
settings page; it is now revealed on demand. Hungarian parity fixtures re-captured once for R-562.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-10-12 15:13:59 +02:00

112 lines
4.6 KiB
Go

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.<copy>`
// 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",
// R-255 (v0.308.0): appAutoFieldRevealHandler reads only EnvVar and Type of each optional field, to
// AUTHORISE a reveal; no label or help text reaches a page from here.
"handlers.go:OptionalConfig": "appAutoFieldRevealHandler: EnvVar/Type authorisation only, no copy rendered",
}
// 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)
}
}
}