R-569: stack-lifecycle handlers pick their status by error KIND, not English words
stop/start/restart/update, remove and delete matched "protected", "not found", "not deployed", "still running", "not orphaned" in err.Error(). New sentinels in internal/stacks (stack_errors.go) carried by the producers in manager.go/delete.go via util.KindErrorf (message bytes unchanged); api.stackOpStatusFor maps them. Tests: reworded-message table per family, wiring check over all three handlers, producers keep kind + words. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/util"
|
||||
)
|
||||
|
||||
// R-569 — the stop/start/restart/update, remove and delete handlers pick their status by the
|
||||
// refusal's KIND. Every row passes a REWORDED message: the words the handlers used to match
|
||||
// ("protected", "not found", "not deployed", "still running", "not orphaned") are gone, only the
|
||||
// kind remains. RED-PROOF (REPORT): restore any strings.Contains chain and its family's reworded rows
|
||||
// answer 500.
|
||||
func TestR569_StackOpStatus_SurvivesRewording(t *testing.T) {
|
||||
cases := []struct {
|
||||
family, name string
|
||||
err error
|
||||
want int
|
||||
}{
|
||||
// action family (start/stop/restart/update)
|
||||
{"action", "unknown app, reworded", util.KindErrorf(stacks.ErrStackNotFound, "no app called %q", "x"), http.StatusNotFound},
|
||||
{"action", "unknown app, wrapped by the caller", fmt.Errorf("restarting: %w",
|
||||
util.KindErrorf(stacks.ErrStackNotFound, "no app called %q", "x")), http.StatusNotFound},
|
||||
{"action", "protected, reworded", util.KindErrorf(stacks.ErrProtectedStack, "%q is infrastructure", "traefik"), http.StatusForbidden},
|
||||
{"action", "update refusal", &stacks.UpdateRefusal{Reason: "busy", Message: "foglalt"}, http.StatusConflict},
|
||||
{"action", "update refusal: unknown app", &stacks.UpdateRefusal{Reason: "not_found", Message: "nincs ilyen"}, http.StatusNotFound},
|
||||
// remove family
|
||||
{"remove", "not deployed, reworded", util.KindErrorf(stacks.ErrNotDeployed, "%q has nothing installed", "x"), http.StatusConflict},
|
||||
{"remove", "still running, reworded", util.KindErrorf(stacks.ErrStillRunning, "%q is up — stop it first", "x"), http.StatusConflict},
|
||||
{"remove", "protected, reworded", util.KindErrorf(stacks.ErrProtectedStack, "cannot take %q away", "traefik"), http.StatusForbidden},
|
||||
// delete family
|
||||
{"delete", "not orphaned, reworded", util.KindErrorf(stacks.ErrNotOrphaned, "%q still belongs to the catalog", "x"), http.StatusConflict},
|
||||
{"delete", "still running, reworded", util.KindErrorf(stacks.ErrStillRunning, "%q is up", "x"), http.StatusConflict},
|
||||
{"delete", "unknown, reworded", util.KindErrorf(stacks.ErrStackNotFound, "nothing named %q", "x"), http.StatusNotFound},
|
||||
// the negative half: the old WORDS without a kind are not a refusal
|
||||
{"any", "text that merely contains the old words", errors.New("docker: image not found; protected; still running"), http.StatusInternalServerError},
|
||||
{"any", "anything else", errors.New("compose exploded"), http.StatusInternalServerError},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := stackOpStatusFor(c.err); got != c.want {
|
||||
t.Errorf("%s / %s: stackOpStatusFor(%q) = %d, want %d", c.family, c.name, c.err.Error(), got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The seam must be WIRED in all three handlers, and none may decide by reading the error text again.
|
||||
func TestR569_HandlersUseTheKind(t *testing.T) {
|
||||
src, err := os.ReadFile("router.go")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body := string(src)
|
||||
for _, fn := range []string{"func (r *Router) actionStack(", "func (r *Router) removeStack(", "func (r *Router) deleteStack("} {
|
||||
i := strings.Index(body, fn)
|
||||
if i < 0 {
|
||||
t.Fatalf("%s not found — this test no longer reads what it thinks it reads", fn)
|
||||
}
|
||||
h := body[i:]
|
||||
if j := strings.Index(h[10:], "\nfunc "); j > 0 {
|
||||
h = h[:j+10]
|
||||
}
|
||||
if !strings.Contains(h, "stackOpStatusFor(err)") {
|
||||
t.Errorf("%s no longer asks stackOpStatusFor for the status code", fn)
|
||||
}
|
||||
for _, line := range strings.Split(h, "\n") {
|
||||
if strings.HasPrefix(strings.TrimSpace(line), "//") {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(line, "strings.Contains(err.Error()") {
|
||||
t.Errorf("%s decides by reading the error text again (R-569): %s", fn, strings.TrimSpace(line))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user