Skip to content

fix(ai-client): add missing methods to the no-op chat devtools bridge#752

Open
jan-kubica wants to merge 2 commits into
TanStack:mainfrom
jan-kubica:fix/noop-chat-devtools-bridge-parity
Open

fix(ai-client): add missing methods to the no-op chat devtools bridge#752
jan-kubica wants to merge 2 commits into
TanStack:mainfrom
jan-kubica:fix/noop-chat-devtools-bridge-parity

Conversation

@jan-kubica

@jan-kubica jan-kubica commented Jun 12, 2026

Copy link
Copy Markdown

🎯 Changes

Repro of the issue: https://stackblitz.com/github/jan-kubica/tanstack-ai-noop-bridge-repro?file=index.mjs

If you create a ChatClient without a devtoolsBridgeFactory you get the bundled no-op bridge, and that class is missing three methods the client expects: mountWithTools, notifyToolsChanged and recordStreamId.

So the first sendMessage() errors with:

TypeError: this.devtoolsBridge.mountWithTools is not a function

It throws before addUserMessage, so the message is just gone. The second send works fine, because mountDevtools() flips devtoolsMounted before calling the method that doesn't exist, so it may look like a transient issue.

The parity check at the bottom of devtools-noop.ts was supposed to catch this, but it assigns undefined as never to the missing-keys type, and since never is assignable to anything, that line can't fail no matter how out of sync the no-op gets. I suggest to change the direction: the missing-keys union now has to satisfy an extends never constraint, so a stale no-op fails tsc with Type '"mountWithTools"' does not satisfy the constraint 'never'.

Test plan

  • packages/ai-client: pnpm test:lib (the new chat-client-noop-devtools.test.ts fails on main with the TypeError above, passes here)
  • pnpm test:pr is green locally
  • e2e: pnpm --filter @tanstack/ai-e2e exec playwright test chat-client-default-bridge

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • Bug Fixes

    • Fixed ChatClient throwing an error when using the default configuration without a custom devtools bridge.
    • Fixed issue where the first message was lost when calling sendMessage() or updateOptions() on first use with the default configuration.
  • Tests

    • Added regression tests and end-to-end tests to verify default configuration behavior.

The default NoOpChatDevtoolsBridge was missing mountWithTools,
notifyToolsChanged, and recordStreamId, so the first
ChatClient.sendMessage() threw a TypeError before appending the user
message whenever no devtoolsBridgeFactory was supplied. Subsequent
sends appeared to work because mountDevtools() marks itself done
before calling the missing method.

Rework the compile-time parity check so drift between the real and
no-op bridge surfaces fails the build: the previous form assigned
'undefined as never' to the missing-keys type, which always
typechecks because never is assignable to every type.

Add a unit test that unmocks the suite-wide real-bridge shim so the
shipping default path is exercised.
…ridge

The framework hooks always inject the real devtools bridge factory,
so no existing E2E route exercised the no-op bridge that direct
ChatClient consumers get by default.
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR fixes a runtime error in ChatClient when no devtools bridge factory is supplied: the default NoOpChatDevtoolsBridge was missing required lifecycle and stream-management methods. The fix adds the missing stubs, introduces a compile-time parity check to prevent future surface drift, and provides comprehensive test coverage via unit and E2E tests.

Changes

No-op Devtools Bridge Fix and E2E Validation

Layer / File(s) Summary
No-op Bridge Method Stubs and Parity Checks
packages/ai-client/src/devtools-noop.ts
Added stub implementations for mountWithTools(), notifyToolsChanged(), setCurrentStreamId(), and recordStreamId() to NoOpChatDevtoolsBridge. Refactored compile-time parity checks from separate never assertions into a single AssertBridgeParity helper and unified _bridgeParity tuple-based type assertion that fails the build if chat, generation, or video bridge surfaces drift.
Release Changeset and Unit Tests
.changeset/noop-chat-devtools-bridge-parity.md, packages/ai-client/tests/chat-client-noop-devtools.test.ts
Patch changeset documents the fix preventing the first message from being lost and adding compile-time surface validation. Vitest suite unmocks the no-op devtools module and validates that sendMessage() and updateOptions({ tools }) work without throwing.
E2E Route Page Implementation
testing/e2e/src/routes/chat-client-default-bridge.tsx
New route exercises vanilla ChatClient with the default no-op bridge using deterministic SSE events, tracks messages and errors, and provides a hydration-aware send button to avoid timing issues.
E2E Router Integration and Test Suite
testing/e2e/src/routeTree.gen.ts, testing/e2e/tests/chat-client-default-bridge.spec.ts
Generated router configuration wires /chat-client-default-bridge into type-safe route maps and module augmentation. Playwright E2E test verifies the page loads, sending a message succeeds, user and assistant messages render, and no errors are captured.

Sequence Diagram

sequenceDiagram
  participant ChatClient
  participant NoOpChatDevtoolsBridge
  participant SSEFetcher
  participant UI
  ChatClient->>NoOpChatDevtoolsBridge: mountWithTools(initialCount)
  ChatClient->>NoOpChatDevtoolsBridge: recordStreamId(streamId)
  ChatClient->>SSEFetcher: fetch deterministic events
  SSEFetcher-->>ChatClient: user message event
  ChatClient->>NoOpChatDevtoolsBridge: notifyToolsChanged()
  ChatClient->>UI: onMessagesChange callback
  SSEFetcher-->>ChatClient: assistant message event
  ChatClient->>UI: onMessagesChange callback
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • tombeckenham
  • AlemTuzlak

Poem

A rabbit hops through the dev-tools door,
Stub methods now answer the call,
No more missing functions, no more roars—
The bridge stands solid, serving all! 🐰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the primary fix: adding missing methods to the no-op chat devtools bridge to resolve the TypeError on first sendMessage().
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The pull request description follows the required template with all key sections completed, including detailed problem statement, test plan, and checklist items.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@testing/e2e/tests/chat-client-default-bridge.spec.ts`:
- Around line 3-5: Update the header comment above the test that exercises the
no-op bridge (referenced by ChatClient and the framework hooks that pass the
real devtools bridge factory) to explicitly document an aimock policy exception:
state that this spec uses a mock fetcher (a static SSE Response served from the
route file) and therefore never reaches the provider HTTP layer, so aimock is
intentionally not used; include brief rationale and link the exception to the
mock fetcher/SSE Response and the no-op bridge behavior so future readers
understand why aimock is skipped.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a61c5231-635d-4ac9-aa2e-af0854eacc66

📥 Commits

Reviewing files that changed from the base of the PR and between 984ac3c and 0f95b99.

📒 Files selected for processing (6)
  • .changeset/noop-chat-devtools-bridge-parity.md
  • packages/ai-client/src/devtools-noop.ts
  • packages/ai-client/tests/chat-client-noop-devtools.test.ts
  • testing/e2e/src/routeTree.gen.ts
  • testing/e2e/src/routes/chat-client-default-bridge.tsx
  • testing/e2e/tests/chat-client-default-bridge.spec.ts

Comment on lines +3 to +5
// The framework hooks always pass the real devtools bridge factory, so this
// is the only scenario in the suite that exercises the no-op bridge that
// vanilla `ChatClient` consumers get by default.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Document the aimock policy exception in the header comment.

Based on learnings, E2E specs that don't reach the provider HTTP layer should document why aimock is not used. This test uses a mock fetcher (static SSE Response in the route file) that never reaches the provider HTTP layer, qualifying as an exception.

📝 Suggested header comment expansion
+// Policy exception: This test uses a mock fetcher (static SSE Response) that
+// never reaches the provider HTTP layer, so aimock is not needed. The route
+// component's inline fetcher returns deterministic SSE events to test the
+// default no-op bridge behavior without external HTTP dependencies.
+
 // The framework hooks always pass the real devtools bridge factory, so this
 // is the only scenario in the suite that exercises the no-op bridge that
 // vanilla `ChatClient` consumers get by default.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// The framework hooks always pass the real devtools bridge factory, so this
// is the only scenario in the suite that exercises the no-op bridge that
// vanilla `ChatClient` consumers get by default.
// Policy exception: This test uses a mock fetcher (static SSE Response) that
// never reaches the provider HTTP layer, so aimock is not needed. The route
// component's inline fetcher returns deterministic SSE events to test the
// default no-op bridge behavior without external HTTP dependencies.
// The framework hooks always pass the real devtools bridge factory, so this
// is the only scenario in the suite that exercises the no-op bridge that
// vanilla `ChatClient` consumers get by default.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@testing/e2e/tests/chat-client-default-bridge.spec.ts` around lines 3 - 5,
Update the header comment above the test that exercises the no-op bridge
(referenced by ChatClient and the framework hooks that pass the real devtools
bridge factory) to explicitly document an aimock policy exception: state that
this spec uses a mock fetcher (a static SSE Response served from the route file)
and therefore never reaches the provider HTTP layer, so aimock is intentionally
not used; include brief rationale and link the exception to the mock fetcher/SSE
Response and the no-op bridge behavior so future readers understand why aimock
is skipped.

Source: Learnings

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