v0.253.0 — errors carry the key of the sentence they are (R-557 slice 2 release B)
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
This commit is contained in:
2026-09-18 11:44:30 +02:00
parent 5270bad76e
commit 7c05b59708
70 changed files with 1853 additions and 377 deletions
@@ -10,6 +10,7 @@ import (
"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
@@ -116,22 +117,40 @@ func TestR553_AlreadyDeployedCarriesKindAndKeepsItsWords(t *testing.T) {
}
}
// The memory refusal is a string built by memoryVerdict from the HOST's real memory, so a unit test
// cannot make it fire. Its kind is therefore pinned where it is attached — at the one line that turns
// that string into an error. Source-level on purpose, like TestDeployAcceptance_DoesNotClaimTheAppIsInstalled:
// the defect this guards is a call written the old way, not a wrong value. The status mapping itself
// is covered by TestR553_Deploy_DecisionSurvivesWordingChange with a translated memory message.
// 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.KindError(ErrNotEnoughMemory, refusal)`) {
t.Error("the memory refusal no longer carries ErrNotEnoughMemory — the API would answer 500 " +
"for a refusal the customer can act on, and translating the sentence would hide it completely")
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)`) {
t.Error("the memory refusal is back to a bare errors.New: its kind is gone (R-553)")
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)")
}
}