7c05b59708
gates / gates (push) Successful in 24s
179 Hungarian sentences were built deep inside a package with fmt.Errorf and printed by whoever caught them: too late to translate where they are shown, too early where they are made. Every one now carries its key across that gap. ZERO Hungarian error literals remain. util.MsgError does three things at once, each earned: - Error() is the Hungarian, byte for byte, so every un-converted printer is unchanged; - errors.Is answers for the kind AND for a wrapped cause (KindErrorf dropped the cause); - an error ARGUMENT renders recursively, so "formázás sikertelen: %w" translates whole. A foreign error — restic, docker, ssh, the stdlib — prints verbatim. It is not ours. 76 display sites go through errText, and TestNoErrErrorInPageOutput convicts any that do not. memoryVerdict returns an error rather than a sentence, so the deploy's 409 and the household's language come from one value; UpdateRefusal gained a Cause to carry it. Plurals, one rule, stated once: a key with .one/.other takes its COUNT first. Not a per-call-site flag — the producer somebody forgot would read "3 app is not running". The guard caught a real key collision (alert.deadapp.one) the day the rule landed. TWO DEFECTS FOUND IN MY OWN TOOLING, recorded rather than quietly fixed. The bulk converter silently dropped multi-line concatenations, damaging 7 producers — and the parity gate could not see it, because every surviving fragment WAS a real base literal while the CALL had lost text; two behaviour tests caught it. And the counting script was case-sensitive, so it said "0 left" while five remained. MinAgent: 0.131.0 (unchanged). No hub release needed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
157 lines
6.2 KiB
Go
157 lines
6.2 KiB
Go
package stacks
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/util"
|
|
)
|
|
|
|
// r553Manager builds a real Manager over a temp stacks dir holding one app fixture, so DeployStack's
|
|
// own validation runs — no stub, no modelled predicate.
|
|
func r553Manager(t *testing.T, felhomYml string) *Manager {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
cfg := &config.Config{}
|
|
cfg.Paths.StacksDir = filepath.Join(dir, "stacks")
|
|
cfg.Paths.SystemDataPath = filepath.Join(dir, "system")
|
|
cfg.Stacks.ComposeCommand = "docker compose" // no detection; nothing is executed in this test
|
|
appDir := filepath.Join(cfg.Paths.StacksDir, "r553app")
|
|
if err := os.MkdirAll(appDir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(appDir, "docker-compose.yml"), []byte("services:\n app:\n image: busybox\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(appDir, ".felhom.yml"), []byte(felhomYml), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m, err := NewManager(cfg, log.New(io.Discard, "", 0))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := m.ScanStacks(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := m.GetStack("r553app"); !ok {
|
|
t.Fatal("fixture invalid: the app was not scanned, so DeployStack would refuse for the wrong reason")
|
|
}
|
|
return m
|
|
}
|
|
|
|
// R-553 — the deploy refusals carry their KIND, and their Hungarian sentences are byte-for-byte what
|
|
// they were before the kinds existed (a localisation-adjacent change may not move one byte of copy).
|
|
//
|
|
// RED-PROOF (REPORT): drop the kind from either producer in deploy.go (back to plain fmt.Errorf) and
|
|
// the errors.Is assertion fails, while the message assertion still passes — which is exactly the
|
|
// silent state this row exists to prevent.
|
|
func TestR553_DeployProducersCarryKindAndKeepTheirWords(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
yml string
|
|
values map[string]string
|
|
kind error
|
|
wantMsg string
|
|
}{
|
|
{
|
|
name: "required field left empty",
|
|
yml: "display_name: R553\ndeploy_fields:\n - env_var: DATA_DIR\n label: Adatmappa\n type: text\n required: true\n",
|
|
values: map[string]string{},
|
|
kind: ErrRequiredField,
|
|
wantMsg: `a(z) "Adatmappa" (DATA_DIR) mező kitöltése kötelező`,
|
|
},
|
|
{
|
|
name: "password field left empty",
|
|
yml: "display_name: R553\ndeploy_fields:\n - env_var: ADMIN_PW\n label: Jelszó\n type: password\n",
|
|
values: map[string]string{},
|
|
kind: ErrRequiredField,
|
|
wantMsg: `a(z) "Jelszó" mező kitöltése kötelező — használja a Generálás gombot vagy írjon be egy jelszót`,
|
|
},
|
|
{
|
|
name: "path field naming something that is not there",
|
|
yml: "display_name: R553\ndeploy_fields:\n - env_var: MEDIA\n label: Média\n type: path\n",
|
|
values: map[string]string{"MEDIA": "/definitely/not/here/r553"},
|
|
kind: ErrPathMissing,
|
|
wantMsg: `path "/definitely/not/here/r553" does not exist for field "Média"`,
|
|
},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
m := r553Manager(t, c.yml)
|
|
_, err := m.DeployStack(DeployRequest{StackName: "r553app", Values: c.values})
|
|
if err == nil {
|
|
t.Fatal("deploy was admitted — the fixture no longer exercises the refusal")
|
|
}
|
|
if !errors.Is(err, c.kind) {
|
|
t.Errorf("refusal carries no kind: %v (want errors.Is … %v)", err, c.kind)
|
|
}
|
|
if err.Error() != c.wantMsg {
|
|
t.Errorf("the customer's sentence CHANGED:\n got %q\nwant %q", err.Error(), c.wantMsg)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// The already-deployed refusal, on the same Manager: its words are unchanged and it carries its kind
|
|
// (the API answers 409 from that kind).
|
|
func TestR553_AlreadyDeployedCarriesKindAndKeepsItsWords(t *testing.T) {
|
|
m := r553Manager(t, "display_name: R553\n")
|
|
m.mu.Lock()
|
|
m.stacks["r553app"].Deployed = true
|
|
m.mu.Unlock()
|
|
_, err := m.DeployStack(DeployRequest{StackName: "r553app"})
|
|
if err == nil {
|
|
t.Fatal("a deployed stack was admitted for a second deploy")
|
|
}
|
|
if !errors.Is(err, ErrAlreadyDeployed) {
|
|
t.Errorf("refusal carries no kind: %v", err)
|
|
}
|
|
if want := `stack "r553app" is already deployed; use update instead`; err.Error() != want {
|
|
t.Errorf("message CHANGED:\n got %q\nwant %q", err.Error(), want)
|
|
}
|
|
}
|
|
|
|
// The memory refusal is built by memoryVerdict from the HOST's real memory, so a unit test cannot
|
|
// make it fire. Two halves therefore, and v0.253.0 (R-557) turned the weaker one into the stronger:
|
|
//
|
|
// - BEHAVIOUR: the value memoryVerdict now returns is an ERROR it builds itself, so the test can
|
|
// build the same value and check it — the kind is reachable by errors.Is AND the sentence is the
|
|
// one the customer used to read, byte for byte, in Hungarian. That is no longer a source check.
|
|
// - SOURCE: memoryVerdict still hands that error back rather than a sentence. The defect this
|
|
// guards is a call written the old way, and only the source can show that.
|
|
func TestR553_MemoryRefusalIsWrappedWithItsKind(t *testing.T) {
|
|
// The exact value the producer returns, for a 900 MB request that does not fit.
|
|
e := util.MsgErrorf(ErrNotEnoughMemory, "err.stacks.not_enough_memory", 900, 100, 2048, 1436, 512)
|
|
|
|
if !errors.Is(e, ErrNotEnoughMemory) {
|
|
t.Error("the memory refusal does not carry ErrNotEnoughMemory — the API would answer 500 " +
|
|
"for a refusal the customer can act on")
|
|
}
|
|
const wantHU = "Nincs elég memória az alkalmazás telepítéséhez. Szükséges: 900 MB, Elérhető: 100 MB " +
|
|
"(összesen: 2048 MB, ebből 1436 MB használt, 512 MB rendszer számára fenntartva)"
|
|
if e.Error() != wantHU {
|
|
t.Errorf("the Hungarian refusal moved:\n got %q\nwant %q", e.Error(), wantHU)
|
|
}
|
|
if en := util.ErrText("en", e); en == wantHU || en == "" {
|
|
t.Errorf("the refusal has no English of its own: %q", en)
|
|
}
|
|
|
|
src, err := os.ReadFile("deploy.go")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body := string(src)
|
|
if !strings.Contains(body, `util.MsgErrorf(ErrNotEnoughMemory, "err.stacks.not_enough_memory"`) {
|
|
t.Error("memoryVerdict no longer builds its refusal with ErrNotEnoughMemory and its key")
|
|
}
|
|
if strings.Contains(body, `errors.New(refusal)`) || strings.Contains(body, `fmt.Errorf(refusal)`) {
|
|
t.Error("the memory refusal is back to a bare error: its kind is gone (R-553)")
|
|
}
|
|
}
|