fd28d70bff
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
112 lines
4.6 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|