Skip to content

fix(connectors): one reading of the gate for the hold label, and the selector pinned to it (TASK-160) - #1938

Merged
lilyshen0722 merged 1 commit into
mainfrom
kai/task160-gate-label-and-selector
Sep 27, 2026
Merged

lilyshen0722 merged 1 commit into
mainfrom
kai/task160-gate-label-and-selector

Conversation

@lilyshen0722

Copy link
Copy Markdown
Contributor

Finishes "one reading of config.gates" — the two residues split off #1935/TASK-156 rather than grown inside a gate-pending PR. Both are pre-existing and neither is an authorisation path. Row TASK-160.

Residue (1) — the hold label read its own copy of the gate

cardHoldReason: 'gate_off' restated the ternary at slackBridgeService.ts:209 and telegramBridgeService.ts:288. Both now read !isGatedPodTarget(integration, podId) — the predicate the relay itself uses, so the "why is this held" reason a user sees cannot go stale when gate semantics move. The scope === 'user' test stays: it asks a different question than the gate read does (the label means this person's gate for that pod is off, not this connector owns that pod), and the comment says so.

Residue (2) — the selector is a query and the rule is a predicate

activeHandlersForPod (services/installable/eventHandlers.ts:59) selects connectors with a Mongo $match over config.gates.<podId>.enabled plus membership; connectorRelayPolicy.isRoutedPodTarget reads the same two halves. They had only ever been asserted apart.

The unit tier can drive the aggregate, so the row's agreement test is built rather than replaced by a comment: the existing dispatcher suite already runs real models against mongodb-memory-server and activeHandlersForPod is exported. One fixture matrix — member / non-member × gate on / off / absent — is walked through both the selector and isRoutedPodTarget, and each cell asserts:

  • the gate half on its own (isGatedPodTarget),
  • that the selector and the predicate reach the same verdict,
  • that both equal the concrete expectation, so the two drifting together to "no" reddens as well as a disagreement.

Fixture note the test now carries: install seeds the installed pod's gate ON, so the absent cell unsets the key rather than skipping the write. Absent and false are different inputs, and only absent exercises $exists.

Witnesses

claim witness
the label follows the predicate decisionCardRelay.bridges.test.js gate_off pair (Telegram + Slack) reddens under a predicate change — M1
the old form would NOT have followed M2: same predicate change with Slack's label restated → Telegram's gate_off arm reddens, Slack's survives
the selector's gate half is real M3 (drop [gateEnabledPath]: true) → the two member gate-off/absent cells redden
the selector's membership half is real M4 (broaden createdBy) → the non-member gate-on cell reddens
the predicate's gate half is real M5 (isGatedPodTarget always on) → the four gate-off/absent cells and both label arms redden

Ledger — 6 mutations, baseline and restore 29/29

mutation red
the shared gate predicate changes, labels follow 15 (broad: every consumer of the predicate)
same change, Slack's label restated 14 — Slack's gate_off arm survives; that is the drift this closes
Slack's label restated alone, predicate unchanged 0 — SURVIVOR, deliberate: the old ternary and the predicate agree at the current rule, so the change is resilience, not a live defect. See below.
the selector drops its gate half 3 — the two member gate-off/absent cells + the existing membership arm
the selector broadens its membership half 2 — the non-member gate-on cell + the existing arm
the predicate always says on 6 — the four gate-off/absent cells + both label arms

Disclosures.

  • M2a is a survivor and stays one. Restating the label alone changes no observable behaviour, because the ternary and the predicate agree at the current rule. That is exactly why the row called this drift potential rather than a defect, and why the witness has to be a predicate change (M1/M2) rather than a restatement. Reporting it green is the honest result: this change buys coupling, not behaviour.
  • M1 is broad — 15 arms, because the predicate has four consumers. It is a mutation of the shared rule, not of the label, so the blast radius is expected; M2 is the surgical version.
  • The non-member gate-off/absent cells do not distinguish M4 (they were already non-selected for the membership reason). The non-member gate-on cell is the one that does, which is why it is in the matrix.

Scope

  • Three files: the two bridges (one line each + the comment) and the test suite (+74).
  • No behaviour change at the current gate rule; isGatedPodTarget is untouched.
  • Verified: 8 suites / 185 tests green; .ts lint 0 errors (both bridges 0 diagnostics at head and at HEAD~1); 0 diagnostics on added .js lines (the file's ambient import/extensions count is 9, all requires of the same shape as the new one, and backend .js is not linted).

Gate: Vera.

…selector pinned to it (TASK-160)

Two pre-existing residues from #1935/TASK-156, split off rather than grown there.

(1) `cardHoldReason: 'gate_off'` restated the gate ternary in both bridges
(`telegramBridgeService.ts:288`, `slackBridgeService.ts:209`). Both now read
`!isGatedPodTarget(integration, podId)` — the same predicate the relay uses — so
the "why is this held" reason a user is shown cannot go stale when gate semantics
change. The `scope === 'user'` test stays: it asks a different question than the
gate read does.

(2) `activeHandlersForPod`'s Mongo `$match` and the bridges' rule were only ever
asserted apart. The unit tier can drive the aggregate (mongodb-memory-server, the
existing dispatcher suite), so the agreement test the row allowed for is built
rather than a comment: one fixture matrix — member / non-member × gate on / off /
absent — walked through BOTH the selector and `isRoutedPodTarget`, asserting they
reach the same verdict on every cell, plus the concrete expectation so the two
drifting together to "no" reddens too.

Fixture note worth keeping: `install` seeds the installed pod's gate ON, so the
absent cell unsets the key rather than skipping the write — absent and false are
different inputs and only absent exercises `$exists`.
@lilyshen0722
lilyshen0722 added this pull request to the merge queue Sep 27, 2026
Merged via the queue into main with commit c0f58a9 Sep 27, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant