zdd plan 3 task 10: fix round 1: a member's wake event reads as the claims refusal's cause, not its admission wrapper
startAdmitted wraps a refusal before the exec hook in errClaimsAdmission whatever the flag, so a claims-on wake on the closed launch reached members as "wake failed: the claims admission refused before the exec: ...". memberSafeWakeReason now removes that wrapper's text (alone or inside doClaimWake's join), so the event is byte-identical to main's. Every other mapping is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 files changed,
+40
−0
internal/activator/logsink.go+6 −0internal/activator/logsink_test.go+30 −0internal/activator/swap.go+4 −0
diff --git a/internal/activator/logsink.go b/internal/activator/logsink.go| index 08ebaed60..e025bb6df 100644 |
| --- a/internal/activator/logsink.go |
| +++ b/internal/activator/logsink.go |
| @@ -57,6 +57,12 @@ func memberSafeWakeReason(err error) string { |
| err = ErrFenced |
| } |
| s := err.Error() |
| + if errors.Is(err, errClaimsAdmission) { |
| + // claimAdmission's wrapper is the classifier's, not the member's |
| + // (zero-downtime plan 3, Task 10): the event reads exactly as its |
| + // cause did, alone or joined. Every other mapping is unchanged. |
| + s = strings.ReplaceAll(s, claimAdmissionPrefix, "") |
| + } |
| switch { |
| case strings.Contains(s, "address already in use"): |
| // The stale-socket wake trap (#129): every later wake fails until |
diff --git a/internal/activator/logsink_test.go b/internal/activator/logsink_test.go| index e892e6f68..f1649cae0 100644 |
| --- a/internal/activator/logsink_test.go |
| +++ b/internal/activator/logsink_test.go |
| @@ -147,3 +147,33 @@ func TestWakeOutcomesReachTheSink(t *testing.T) { |
| t.Fatalf("no woke event: %+v", sink2.events) |
| } |
| } |
| + |
| +// A claims-on wake on the closed launch fails before its exec hook, and |
| +// startAdmitted wraps that in errClaimsAdmission so the one classifier |
| +// reads it as the admission's (Task 10). The wrapper is internal: the |
| +// member's event renders exactly as the bare cause did on main, whatever |
| +// the flag, alone or joined with FailedAdmission's error as doClaimWake |
| +// returns it. No "claims admission" text reaches a member. |
| +func TestMemberSafeWakeReasonHidesTheClaimsAdmissionWrapper(t *testing.T) { |
| + recovery := errors.New("recovery: release the admission") |
| + for _, tc := range []struct { |
| + name string |
| + wrapped, bare error |
| + }{ |
| + {"the closed launch", claimAdmission(ErrClaimIsolationUnavailable, false), ErrClaimIsolationUnavailable}, |
| + {"the closed launch, wrapped by the launcher", claimAdmission(fmt.Errorf("start main: %w", ErrClaimIsolationUnavailable), false), |
| + fmt.Errorf("start main: %w", ErrClaimIsolationUnavailable)}, |
| + {"joined as doClaimWake returns it", errors.Join(claimAdmission(ErrClaimIsolationUnavailable, false), nil), |
| + errors.Join(ErrClaimIsolationUnavailable, nil)}, |
| + {"joined with a recovery error", errors.Join(claimAdmission(ErrClaimIsolationUnavailable, false), recovery), |
| + errors.Join(ErrClaimIsolationUnavailable, recovery)}, |
| + } { |
| + got, want := memberSafeWakeReason(tc.wrapped), memberSafeWakeReason(tc.bare) |
| + if got != want { |
| + t.Errorf("%s: %q, want main's %q", tc.name, got, want) |
| + } |
| + if strings.Contains(got, "claims admission") { |
| + t.Errorf("%s: the admission wrapper reached a member: %q", tc.name, got) |
| + } |
| + } |
| +} |
diff --git a/internal/activator/swap.go b/internal/activator/swap.go| index 5dba4d72f..6431aa318 100644 |
| --- a/internal/activator/swap.go |
| +++ b/internal/activator/swap.go |
| @@ -191,6 +191,10 @@ func verdictOf(err error) gateVerdict { |
| // failure of the build (spec §7.4). |
| var errClaimsAdmission = errors.New("the claims admission refused before the exec") |
| +// claimAdmissionPrefix is the text claimAdmission puts before its cause's. |
| +// memberSafeWakeReason removes it, so a member's event reads as the cause. |
| +var claimAdmissionPrefix = errClaimsAdmission.Error() + ": " |
| + |
| // claimAdmission wraps err in errClaimsAdmission when the spawn never |
| // reached its exec hook (reached false). An error after the hook ran is the |
| // spawn's own, and is returned as it is. |