diff --git a/go-backend/internal/http/handler/control_plane.go b/go-backend/internal/http/handler/control_plane.go index 74378cb..f760793 100644 --- a/go-backend/internal/http/handler/control_plane.go +++ b/go-backend/internal/http/handler/control_plane.go @@ -395,23 +395,13 @@ func (h *Handler) deleteForwardServicesOnNode(forward *forwardRecord, nodeID int candidateTunnelIDs = append(candidateTunnelIDs, allUserTunnelIDs...) bases := buildForwardServiceBaseCandidates(forward.ID, forward.UserID, userTunnelID, candidateTunnelIDs) - var lastErr error - for _, base := range bases { - names := buildForwardControlServiceNames(base, "DeleteService") + return deleteForwardServiceCandidates(bases, func(name string) error { payload := map[string]interface{}{ - "services": names, + "services": []string{name}, } - _, cmdErr := h.sendNodeCommand(nodeID, "DeleteService", payload, false, true) - if cmdErr == nil { - return nil - } - lastErr = cmdErr - } - - if lastErr != nil { - return lastErr - } - return nil + _, err := h.sendNodeCommand(nodeID, "DeleteService", payload, false, false) + return err + }) } func (h *Handler) controlForwardServices(forward *forwardRecord, commandType string, tolerateNotFound bool) error { @@ -517,6 +507,20 @@ func controlForwardServiceCommand(bases []string, commandType string, send func( return false, lastNotFoundErr, nil } +func deleteForwardServiceCandidates(bases []string, send func(name string) error) error { + handled, lastNotFoundErr, err := controlForwardServiceCommand(bases, "DeleteService", send) + if err != nil { + return err + } + if handled { + return nil + } + if lastNotFoundErr != nil { + return nil + } + return nil +} + func shouldSelfHealForwardServiceControl(commandType string) bool { cmd := strings.ToLower(strings.TrimSpace(commandType)) return cmd == "pauseservice" || cmd == "resumeservice" diff --git a/go-backend/internal/http/handler/control_plane_test.go b/go-backend/internal/http/handler/control_plane_test.go index 380f800..1aaf2ee 100644 --- a/go-backend/internal/http/handler/control_plane_test.go +++ b/go-backend/internal/http/handler/control_plane_test.go @@ -122,6 +122,35 @@ func TestControlForwardServiceCommandReturnsLastNotFoundWhenAllMissing(t *testin } } +func TestDeleteForwardServiceCandidatesSkipsNotFoundUntilLegacyMatch(t *testing.T) { + bases := []string{"12_34_56", "12_34_0"} + called := make([]string, 0) + err := deleteForwardServiceCandidates(bases, func(name string) error { + called = append(called, name) + if name == "12_34_0" { + return nil + } + return errors.New("service " + name + " not found") + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + wantCalls := []string{"12_34_56_tcp", "12_34_56_udp", "12_34_56", "12_34_0_tcp", "12_34_0_udp", "12_34_0"} + if !reflect.DeepEqual(called, wantCalls) { + t.Fatalf("expected calls %v, got %v", wantCalls, called) + } +} + +func TestDeleteForwardServiceCandidatesTreatsAllMissingAsSuccess(t *testing.T) { + bases := []string{"12_34_56", "12_34_0"} + err := deleteForwardServiceCandidates(bases, func(name string) error { + return errors.New("service " + name + " not found") + }) + if err != nil { + t.Fatalf("all-missing delete should be tolerated, got %v", err) + } +} + func TestControlForwardServiceCommandReturnsHardError(t *testing.T) { bases := []string{"12_34_56"} handled, lastNotFoundErr, err := controlForwardServiceCommand(bases, "PauseService", func(name string) error { diff --git a/plans/013-forward-delete-notfound-compat-fix.md b/plans/013-forward-delete-notfound-compat-fix.md new file mode 100644 index 0000000..13132fa --- /dev/null +++ b/plans/013-forward-delete-notfound-compat-fix.md @@ -0,0 +1,13 @@ +# 013 Forward Delete NotFound Compatibility Fix + +## Checklist + +- [x] Confirm forward update failure path caused by delete fallback short-circuiting on the first not-found service name. +- [x] Update forward service deletion logic to continue across all candidate runtime names until one is actually deleted or every candidate is exhausted. +- [x] Add regression tests covering mixed not-found and legacy-name delete recovery during forward control/update flows. +- [x] Run focused backend handler tests and record the result. + +## Test Record + +- Command: `cd go-backend && go test ./internal/http/handler/...` +- Result: passed.