* test(imessage): mock core turn dispatch for media policy coverage
PR #110095 missed this iMessage media-policy test file.
* test(imessage): mock core turn dispatch for last-route coverage
* test(whatsapp): mock core turn dispatch for broadcast coverage
* test(imessage): drop type-dead terminal-admission branch in media harness
* fix(qa-lab): close transport before gateway teardown
* test(qa-lab): bind teardown order to production plan
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(whatsapp): resolve abortPromise when stop signal already fired
waitForClose() races on abortPromise, but a pre-aborted gateway stop
signal never settled that promise because the abort listener was skipped.
Resolve abortPromise immediately when abortSignal.aborted is already true.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(whatsapp): resolve tsgo TS18048 and test mock in pre-aborted abortPromise fix
- Extract params.abortSignal to local const so tsgo can narrow the type
through else-if control flow, fixing TS18048: possibly undefined
- Remove setupAbortController/ownerAcquireAbortController.abort() from
the constructor's pre-aborted branch so openConnection() still works
when the stop signal was already fired at construction time
- Use createSocketWithTransportEmitter() mock in the new test so
shutdown() cleanup has sock.end() and ws.removeListener() available
* fix(whatsapp): preserve pre-aborted setup cancellation
* fix(whatsapp): unify connection abort handling
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(discord): adopt durable ingress drain for gateway messages
Gateway MESSAGE_CREATE events dispatched with only an in-memory/persistent
5-minute dedupe; a crash between receive and dispatch lost the message (live
sessions replay via RESUME, but session state is memory-only so a cold
restart re-IDENTIFYs — stated platform limitation: the drain closes the
receive->dispatch window, not cold downtime). Raw API messages now journal
durably (event_id = message snowflake, lane per channel/thread) before
normalization; dispatch runs through the core drain with deferred claims,
fan-out adoption, gated settlement, and 30d/5k tombstones. The persistent
guard is deleted after its RESUME duplicate-delivery parity test. Edits stay
outside the queue: only MessageCreateListener is registered, MESSAGE_UPDATE
never dispatches, so snowflake tombstones cannot swallow edit turns; a future
edit dispatcher must namespace its event ids.
Autoreview blocked by codex sandbox network in the build stage; full manual
review performed. Part of the #109657 fleet adoption program (wave 2).
* style(discord): keep drain-internal types and dispatcher unexported
* refactor(discord): move message dispatcher to its own module for the dead-export gate
* fix(discord): shutdown dispatch releases claims instead of tombstoning
Landing autoreview reproduced a real loss race: a dispatch entering after
dispatcher shutdown (or with an aborted signal) returned completed, so the
drain tombstoned a message that never ran and a restarted gateway skipped it
forever. Aborted-before-dispatch now returns failed-retryable so the claim
releases for replay. Test type imports of drain-internal types converted to
factory-derived types so the dead-export gate and typecheck agree.
* fix(msteams): bound probe token acquisition to request deadline
probeMSTeams() at extensions/msteams/src/probe.ts:75 and :89 awaited
tokenProvider.getAccessToken(...) for the Bot Framework and Microsoft
Graph token endpoints with no surrounding deadline. The Microsoft
Teams SDK does not carry an inherent timeout on these calls, so a
stalled Azure AD token endpoint pinned the probe indefinitely.
Wrap both awaits with withMSTeamsRequestDeadline (default
MSTEAMS_REQUEST_TIMEOUT_MS = 30_000), matching the pattern already
used by six other MS Teams call sites: attachments/bot-framework.ts:252,
attachments/graph.ts:258, monitor-handler/message-handler.ts:594/654/685/692,
attachments/download.ts:167, team-identity.ts:37.
The probe was the one missing site. No new helper, no SDK change.
The existing outer catch at probe.ts:138 and inner catch at probe.ts:110
convert the timeout into a ProbeMSTeamsResult with ok: false and a
structured error field.
Added probe.timeout.test.ts: real probeMSTeams() with vi.mock
injected never-resolving getBotToken/getGraphToken; asserts the call
returns within the 30s bound instead of hanging to the proof budget.
* test(msteams): drive probe timeout test with vi.useFakeTimers
The original probe.timeout.test.ts waited 90 seconds of wall-clock per
focused run (3 stalled cases racing against a real setTimeout budget).
Per ClawSweeper P2 (automation), this material deterministic CI cost can
slow or time out test shards.
Drive the withTimeout race (from @openclaw/fs-safe/dist/timing.js, uses
setTimeout + clearTimeout) via vi.useFakeTimers() so each stalled case
resolves in milliseconds. Add one new case that spies on withTimeout's
timeoutMs argument to assert the production default deadline is exactly
MSTEAMS_REQUEST_TIMEOUT_MS = 30_000, so the production contract is not
silently weakened by the fake-timer change.
Per-case wall-clock: 25ms / 3ms / 2ms / 1ms / 2ms (was: 30s / 30s / 30s /
2ms / n/a).
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(msteams): bound remaining token acquisition
* test(msteams): keep credential fixture unchanged
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(test): converge Slack harness state across module reloads
Five reaction tests in monitor.tool-result.test.ts failed whenever a sibling
file's vi.resetModules() ran earlier in the same non-isolated worker: the
cached globalThis __slackClient kept routing reactions to the OLD test-helpers
module's mocks while tests asserted on the recreated module's fresh mocks —
reactions 'never fired' (replies survived via freshly injected runtime).
slackTestState is now a globalThis-backed singleton so every module
incarnation and the cached client share one state object. Also: reaction
assertions wait on the mock (reactions apply via detached debounce/queue
work), and stale Bolt handler registrations clear per test/stop so
waitForSlackEvent cannot match a previous provider's handler. Proven with six
consecutive full extensions/slack runs (was ~50% failure).
* style(slack): bracket global test-state access for underscore lint
* fix(perplexity): send Search API date filters with official field names
* test(perplexity): cover date filter request body
* test(perplexity): simplify request capture
* style(perplexity): format request test
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(discord): avoid Activity OAuth stalls after API rejection
* test(discord): preserve status on cancel failure
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(discord): honor caller abortSignal during 429 retry backoff
* test(discord): prove 429 backoff abort through a real loopback server
* test(discord): make retry abort proof deterministic
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(slack): add user-identity config surface (session token/cookie)
Introduce an account-level identity discriminator ("bot" default | "user") for
the Slack channel, plus sessionToken/sessionCookie secret fields for the
browser-session (user) path. Schema enforces structural coupling only
(identity="user" rejects appToken/signingSecret/relay/non-default mode);
credential presence is deferred to runtime like bot/app tokens. Account entries
inherit the top-level identity. Adds env fallback (SLACK_SESSION_TOKEN/COOKIE,
default account only), secret-contract registration, provenance *Source fields,
and UI hints. Config surface only; resolver/transport are later phases.
* refactor(slack): identity-aware operation-token resolver + dedup
Route Slack read/write token selection through a single identity-aware
resolveSlackOperationToken: identity="user" selects the browser-session token
for both operations (companion cookie attached later at client construction),
identity="bot" keeps the exact userToken/botToken + userTokenReadOnly behavior.
Delete the duplicated token-selection gate in action-runtime and route direct
sends/DM opens through the shared resolver with identity-neutral error wording.
Behavior-neutral for bot identity; net non-test LOC -2.
* refactor(slack): retarget user identity to xoxp + companion app
Replace the abandoned browser-session (xoxc/xoxd) approach for identity="user"
with the official path: authenticate as the human via the existing userToken
(xoxp) while a companion app (appToken/socket or signingSecret/http) carries
events. Remove sessionToken/sessionCookie config, env vars, secret-contract
entries, resolver fields, and their tests. Invert the schema coupling so
identity="user" permits the companion-app transport credentials, and drop the
now-vestigial user-identity structural guard entirely (identity is a permissive
discriminator; credential presence is a runtime concern). Resolver returns the
user token for user-identity reads and writes. Behavior-neutral for bot
identity.
* feat(slack): user-identity provider transport (xoxp + companion app)
Wire the identity="user" event transport. The companion Slack app runs Socket
Mode (appToken) or HTTP (signingSecret) while Bolt authenticates as the human
via the user token (no bot token). Startup auth.test runs on the user token and
its user_id becomes the self/mention target and self-send dedupe id (Bolt's
lazy per-event authorize populates context.botUserId=user_id, so the existing
self-event middleware drops the human's own messages). Identity-aware token
requirements and diagnostics; the bot-token identity warning is suppressed for
user identity. Behavior-neutral for identity="bot".
* feat(slack): user-identity setup, doctor, and docs
Add the setup wizard, doctor/status, and documentation for identity="user".
The wizard collects a user token plus the companion-app transport credential
(app token for socket, signing secret for http) and only persists identity when
"user" so bot setups keep byte-identical generated config; doctor validates the
user token via auth.test and warns on a missing transport credential; account
inspection reports user-token status. Message actions are gated on the active
identity's credential so user accounts expose actions. Docs add the exact
companion-app recipe (user scopes, on-behalf-of-user message events, socket/http
examples, the DM/group-DM-only-via-user-scope note, and self-send dedupe).
Behavior-neutral for identity="bot".
* test(slack): isolate user-identity provider tests to fix parallel flake
The new user-identity provider tests shared the globalThis-stored Slack test
client/handlers across files under parallel isolate:false, so their reaction
closures could reference a prior file's state and intermittently fail sibling
monitor tests (monitor.tool-result reactions). Track started monitors and
abort/await them all in afterEach (even on failed assertions), reset the shared
auth.test/mock state, and dispose the global Slack test runtime per file via a
new disposeSlackTestRuntime helper. Full Slack suite green across repeated
parallel runs.
* fix(slack): regenerate channel config metadata after rebase
* fix(slack): resolve setup consistent-return and doctor dead-export lint
* style: format install script test
* docs(slack): regenerate docs map for user-identity section
* test(slack): use renamed createSlackBoltApp token param in interop test
The rebase folded main's new wrapReceiver test into the renamed
createSlackBoltApp signature (botToken -> token); update the call site.
Keep the OpenAI OAuth token endpoint usable behind fake-IP proxy DNS while retaining exact-host SSRF protection for redirects and unrelated hosts.
Co-authored-by: Abner Shang <75654486+abnershang@users.noreply.github.com>