HandshakeTestPanel cross-checks the component counts a handshake returns against the virtual server's own aggregate, and shows an amber "Component counts don't match the virtual server's aggregate. This can mean a federated source is unreachable or filtering has diverged" banner on a mismatch.
PR #76 fixed the main population mismatch by excluding disabled components from the aggregate (VirtualServerDetailsPanel.tsx:295-302), since the handshake's list calls only ever see enabled ones. Two narrower paths can still produce a spurious banner.
1. A single failed query silently contributes 0. The aggregate is summed over whichever of the three /servers/{id}/{tools,resources,prompts} queries returned data. If the resources query 403s while tools succeeds, the aggregate records resources: 0 against a handshake reporting 3, and the banner fires.
2. The fallback population can't be filtered. When all three queries come back empty or errored, allComponents falls back to buildComponentItems(server) (:287). Those items are built from the associatedTools / associatedResources / associatedPrompts name lists and carry no enabled field (components/gateways/utils.ts:59-86), so the enabled === false filter can never fire and disabled components get counted again.
Severity: low. The consequence is a false advisory banner. The handshake result, meta line, capability flags and raw response are all unaffected, and nothing destructive follows. Path 1 needs a permission or backend failure on one of the three component queries; path 2 needs all three to fail together.
Suggested fix: pass aggregatedCounts only when all three queries have succeeded, and derive it from fetchedComponents rather than allComponents. useQuery already exposes a per-call error (hooks/useQuery.ts:27), so this is a destructure plus a guard at the call site (:497). HandshakeTestPanel already handles an absent aggregate: hasComparison goes false and the comparison column and banner drop out.
Note that gating on the existing componentsLoading (:344) fixes neither path, since isLoading goes false on error.
Trade-off: a virtual server with genuinely zero components would lose the "matches server's total" confirmation, since an empty fetched population is indistinguishable from a failed one at the call site. Acceptable, or worth tracking success explicitly.
HandshakeTestPanelcross-checks the component counts a handshake returns against the virtual server's own aggregate, and shows an amber "Component counts don't match the virtual server's aggregate. This can mean a federated source is unreachable or filtering has diverged" banner on a mismatch.PR #76 fixed the main population mismatch by excluding disabled components from the aggregate (
VirtualServerDetailsPanel.tsx:295-302), since the handshake'slistcalls only ever see enabled ones. Two narrower paths can still produce a spurious banner.1. A single failed query silently contributes 0. The aggregate is summed over whichever of the three
/servers/{id}/{tools,resources,prompts}queries returned data. If the resources query 403s while tools succeeds, the aggregate recordsresources: 0against a handshake reporting 3, and the banner fires.2. The fallback population can't be filtered. When all three queries come back empty or errored,
allComponentsfalls back tobuildComponentItems(server)(:287). Those items are built from theassociatedTools/associatedResources/associatedPromptsname lists and carry noenabledfield (components/gateways/utils.ts:59-86), so theenabled === falsefilter can never fire and disabled components get counted again.Severity: low. The consequence is a false advisory banner. The handshake result, meta line, capability flags and raw response are all unaffected, and nothing destructive follows. Path 1 needs a permission or backend failure on one of the three component queries; path 2 needs all three to fail together.
Suggested fix: pass
aggregatedCountsonly when all three queries have succeeded, and derive it fromfetchedComponentsrather thanallComponents.useQueryalready exposes a per-callerror(hooks/useQuery.ts:27), so this is a destructure plus a guard at the call site (:497).HandshakeTestPanelalready handles an absent aggregate:hasComparisongoes false and the comparison column and banner drop out.Note that gating on the existing
componentsLoading(:344) fixes neither path, sinceisLoadinggoes false on error.Trade-off: a virtual server with genuinely zero components would lose the "matches server's total" confirmation, since an empty fetched population is indistinguishable from a failed one at the call site. Acceptable, or worth tracking success explicitly.