The three temporary_file_disappears_before_stat subtests raced a polling
goroutine against a 16MB write, hoping the removal landed between write
and stat. On fast CI runners the window is routinely missed, failing
Coverage jobs on main and every open PR. Replace the race with an
injectable markdownStatUpload seam that removes the file before
delegating to os.Stat.
Normalize message projections across read shortcuts, preserve mixed user JSON, expand forwarded records, mask ciphertext, and accept media-download message ID aliases while retaining the Cobra/Schema required contract.
Drop the unreachable defensive tag-skip branch in transportEnvelopeSchema
(every transport.Event field carries a non-empty JSON tag) and add a unit
test for the validatePersonalEventOutputMode success path so the changed
code coverage gate reaches 100%.
pluginDescriptorConflictsWithDistribution and the identity-owner seeding
both treated conference as distribution-owned, so the whole plugin server
was skipped before the replaceable-fallback merge in addPluginCommandsSafe
could run. Skip replaceablePluginFallbacks names in both early gates while
keeping reserved-command protection and plugin-vs-plugin ownership intact.
The macOS/Windows coverage gates only execute tests matching
^(TestAllShortcuts|TestCrossPlatformCoverage), so the 401 refresh-retry
tests added for this change were invisible to them, leaving 12 changed
statements uncovered (92.73% < 100%). Rename the 12 existing tests into
the TestCrossPlatformCoverage prefix and add a fetchTicketAttempt edge
test covering transport failures, retryable statuses, and missing
endpoint/ticket payload fields.
The Coverage gate flagged 16 uncovered changed statements (90.6% < 100%):
- drop the unreachable handler error / nil response branches in
runPortalTicketAttempt: makeHandler never fails, matching the pre-port
portal loop on main
- cover portalStageError nil Error/Unwrap, the reconnect min/max clamp,
and the acked backoff reset via an end-to-end reconnect test
- cover personalRetryLogError fallback when a token failure carries no
structured HTTP status
- cover ClassifyRefreshFailure nil/net.Error/redirect branches, the nil
HTTPStatusError message, and oauthExchangeDisplayError fallback
- cover the personal stream source ForceRefreshToken wiring end to end
Local gate now reports changed code coverage 100.0% (165 statements).
Ported from 342d44efe (backup/event-token-lazy-resolution-pre-rewrite) and
adapted to the current in-place single 401 refresh+retry design:
- portal source: classify ticket/dial/read/ack failures via portalStageError
and reconnect with backoff on retryable stages only (DisableReconnect for
tests and one-shot callers); stage errors never leak response bodies
- personal/portal: transient token provider or refresh failures (network,
408/429/5xx) go through the reconnect loop instead of killing the source;
terminal failures (400/401/403) remain fatal
- personalRetryLogError: token resolution/refresh errors log only the
structured HTTP status, never provider error details
Unlike the original commit, a rejected token is still retried once in place
after a successful refresh, and a second 401 stays fatal (single-refresh
guard agreed in review).
Restored from the pre-rewrite branch head 342d44efe (backed up as
backup/event-token-lazy-resolution-pre-rewrite); the auth-layer changes
apply verbatim on the rebased branch.
- Add ClassifyRefreshFailure with structured HTTPStatusError so refresh
failures split into transient (network, timeout, 408/429/5xx) and
terminal (400/401/403) classes; unknown errors stay fatal.
- GetTokenSnapshot no longer marks a profile expired on transient
refresh failures, so long-running sources can retry after backoff.
- postJSON returns HTTPStatusError keeping the response body out of the
error string; the OAuth callback page HTML-escapes the sanitized
exchange error instead of echoing raw server output.
- isInvalidGrantError also matches the preserved response body.
A 401 whose error body failed mid-read (e.g. unexpected EOF) was wrapped
as retryable by the body-read path, letting the outer reconnect loop
re-enter fetchTicket and refresh again on every iteration, bypassing the
single refresh-retry guard.
Classify non-2xx responses by status first; the body is only drained
best-effort since it is never used for error reporting here. 401 stays
fatal regardless of body state, while 2xx body-read failures remain
retryable transport errors.
Portal and personal ticket requests now perform a single controlled
refresh + retry inside the production chain when the server rejects the
resolved access token with HTTP 401:
- Add optional ForceRefreshToken callback to PortalTicketConfig and
PersonalConfig. It receives the exact rejected token so the app-level
compare-and-refresh (ForceRefreshRejectedToken) can dedupe concurrent
rotations, and returns the fresh token.
- requestPortalTicket / fetchTicket retry the ticket request once with
the rotated token directly instead of surfacing an error and hoping an
outer loop retries; a second 401 stays fatal to prevent refresh loops.
- Refresh failures keep both the original 401 and the refresh error via
errors.Join; empty rotated tokens fail fast before hitting the server.
- Wire forceRefreshRejectedAccessToken into event consume (portal) and
personal stream sources; resolveSourceAccessToken strict semantics are
unchanged (provider errors still propagate, no static-token fallback).
- Tests: full DingtalkSource.Start -> startPortalTicket chain
(401 -> refresh -> ticket ok -> WebSocket event), rotated-token reuse,
refresh failure, nil-callback compatibility, second-401 fatality, and
app-level wiring.
Wait for the accept loop to stop before waiting for connection handlers, and track accepted connections before publishing handlers. This removes the WaitGroup Add/Wait race caught by PR #667 CI and follows up the event bus introduced in #589.