v0.262.1: the busy refusal is a CONFLICT, not a server error (R-633)
gates / gates (push) Successful in 24s

Caught by the live proof on 9202, not by a test. The v0.262.0 guard fired exactly right and
answered HTTP 500: router.go maps remove errors by grepping the error TEXT for "not deployed" /
"still running" / "not found" / "protected", and the busy sentence contains none of them.

A 500 tells the UI something broke; this is "wait a moment". Now a typed *stacks.RemoveBusyError
matched with errors.As and answered 409, carrying both the Hungarian bytes and the bundle key.

Its test asserts the sentence contains none of the words the text mapping greps for, so the type is
load-bearing rather than decorative. Red-proof seen failing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-22 21:48:01 +02:00
parent b793638484
commit 3d41758be0
4 changed files with 75 additions and 3 deletions
@@ -1,6 +1,7 @@
package stacks
import (
"errors"
"os"
"path/filepath"
"strings"
@@ -156,3 +157,35 @@ func TestClearUpdateState(t *testing.T) {
}
m.ClearUpdateState("does-not-exist") // must not panic
}
// ── B — the busy refusal is a CONFLICT, by type (R-633) ──────────────────────────────────────────
//
// RED-PROOF: return `util.MsgError(KeyRemoveBusy)` instead of `&RemoveBusyError{}` and this fails —
// which is what the first live run did, answering **500** while refusing correctly. The status
// mapping greps the error TEXT for "not deployed"/"still running"; the busy sentence has neither.
func TestRemoveBusyError_IsTypedAndCarriesBothForms(t *testing.T) {
var err error = &RemoveBusyError{Why: "a backup or restore is running (single-flight held)"}
var busy *RemoveBusyError
if !errors.As(err, &busy) {
t.Fatal("the API must be able to recognise this by TYPE, not by matching prose")
}
if busy.Why == "" {
t.Error("the guard's own words must survive for the log")
}
if err.Error() != MsgRemoveBusyHU {
t.Errorf("the default-language bytes must be the bundle's Hungarian, got %q", err.Error())
}
if busy.Key() != KeyRemoveBusy {
t.Errorf("the key must travel so the API can localise it, got %q", busy.Key())
}
// The whole point: none of the words the status mapping greps for appear in this sentence, so a
// text-matching handler WOULD answer 500. This assertion is why the type exists.
for _, w := range []string{"not deployed", "still running", "not found", "protected"} {
if strings.Contains(err.Error(), w) {
t.Errorf("the sentence happens to contain %q — the text mapping would catch it and this "+
"test would stop proving anything", w)
}
}
}