Skip to content

[Bug] ClusterTap's Exclude does not guard onMemberEvent, and onUnknownEvent is silent #1502

Description

@pathosDev

Found while adding ClusterStatsPublished in #842.

What is claimed

src/devtools/taps/ClusterTap.ts routes unmatched cluster events through .otherwise into onMemberEvent, whose parameter is an Exclude<ClusterEvent, …> and which dereferences event.member. Two comments in that file state the consequence — onShardRegistrationEvent's says a fall-through "would have typed event.member on something that has none" — and the #842 triage repeated it: adding a non-member-carrying event without an explicit arm is supposed to be a bun run typecheck failure.

What actually happens

Measured on the finished #842 tree:

  • Deleting the ClusterStatsPublished arm and removing the type from onMemberEvent's Exclude: bunx tsc --noEmit exits 0.
  • tests/unit/devtools/ClusterTap.test.ts under that mutation: 7 pass, 0 fail.
  • The same, done to MemberConfigurationMismatch instead: also exit 0. So it is the family's property, not one event's.

Mechanism: Exclude<T, U> removes union members assignable to U. ClusterStatsPublished carries leader: Option<Member>, so it is structurally assignable to LeaderChanged — which is already in the exclude list — and it silently disappears from the parameter type. event.member therefore never has to be explained. Any future event with a field named leader: Option<Member>, or with a superset of any excluded class's fields, hides the same way.

At runtime the fall-through reaches onUnknownEvent(), which is {} — an empty method. So no test can distinguish "handled by an explicit no-op arm" from "fell through the unknown-event path" either.

Net: nothing stops a new ClusterEvent from being routed into a member-event handler it does not belong in, and nothing reports that it happened.

Suggested directions (not decided)

  1. Make onUnknownEvent observable — a debug line naming event.constructor.name, or a counter. That is what makes the wrong path detectable, and it is what the path's own comment already implies it should do ("the wrong report").
  2. Replace the structural Exclude with an explicit union of the ten member-carrying classes, so a new event has to be named somewhere rather than vanishing by assignability. (The ten are structurally identical, so this is a naming device, not a type-safety one — but it forces the edit.)
  3. Add a test that enumerates the ClusterEvent union and asserts every member is either in MEMBER_EVENT_NAMES or has an explicit arm. That is the guard the triage assumed existed; there is no event-inventory test under tests/unit/cluster/ today.

Interim state

#842 keeps the explicit arm, corrects the JSDoc to say the compiler is not the guard here, and pins the mechanism in ClusterTap.test.ts as a compile-checked assignment (const asLeaderChanged: LeaderChanged = new ClusterStatsPublished(…)) that stops compiling the day the shape changes.


Found during the #887 configuration wave, while working on #842.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions