Skip to content

test(e2e): fix flaky multimodal + approval-flow specs (#818) - #819

Merged
tombeckenham merged 1 commit into
mainfrom
818-flaky-e2e-multimodal-+-approval-flow-tests-intermittently-get-empty-responses-chatstream-fatal-from-aimock
Jun 24, 2026
Merged

test(e2e): fix flaky multimodal + approval-flow specs (#818)#819
tombeckenham merged 1 commit into
mainfrom
818-flaky-e2e-multimodal-+-approval-flow-tests-intermittently-get-empty-responses-chatstream-fatal-from-aimock

Conversation

@tombeckenham

@tombeckenham tombeckenham commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

🎯 Changes

Fixes the intermittent E2E failures in the multimodal and approval-flow specs (closes #818). The same commit both passed and failed across re-runs, surfacing as empty assistant responses / chatStream fatal.

Root cause was a test-harness race — not aimock and not the library. End-to-end instrumentation showed every request the harness actually sent succeeded (direct load: 360/360 raw, 480/480 via SDK, 0 fatals). The corruption was in how the test drove the controlled React input.

Multimodal (multimodal-image, multimodal-structured): sendMessageWithImage typed into a controlled input, then attached the image — which auto-sends using that input's value. Under CPU load:

  1. pressSequentially dropped leading characters, so the prompt reached aimock truncated (e.g. "cribe this image") → 404 No fixture matched → the empty chatStream fatal in the report.
  2. React state lagged the committed DOM value, so the auto-send's onChange read empty text and dispatched no request at all.

Approval-flow (rarer): runTest treated the optimistic user-message bump as "run started" and returned before any real stream activity; a stalled run then timed out waiting for an approval that never appeared (eventCount=0, no error).

Fixes (test harness only)

  • tests/helpers.ts — type until the full prompt is committed, then retry the typing + attach until the send actually fires (user bubble renders).
  • src/components/ChatUI.tsx — the image auto-send reads the live input DOM value instead of possibly-stale React state.
  • tests/tools-test/helpers.tsrunTest requires real stream activity (loading on, a tool call, completion, or an assistant message) before returning, and retries the click otherwise.

Verification

  • Multimodal: 200/200, 0 flaky at 20× (4 workers, retries=2); 200/200 at 6 workers / retries=0. (Was ~30% flaky before.)
  • Approval-flow: 450/450 at 25× / 8 workers / retries=0, no double-sends.
  • Full E2E suite: 280 passed, 0 failed.

✅ 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).

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Tests
    • Enhanced image attachment handling test reliability with improved retry logic and DOM value assertions
    • Improved test detection of async operation completion in tool-calling workflows

The multimodal and approval-flow E2E specs intermittently failed with empty
assistant responses (surfaced as `chatStream fatal`). Root cause was a
test-harness race, not aimock or the library — every request the harness
actually sent succeeded.

Multimodal: `sendMessageWithImage` typed into a controlled React input and then
attached the image, which auto-sends using that input's value. Under CPU load
`pressSequentially` dropped leading characters, so the prompt reached aimock
truncated (e.g. "cribe this image") and 404'd as "No fixture matched"; and React
state could lag the committed DOM value so the auto-send fired with empty text
and dispatched no request at all.

- helpers: type until the full prompt is committed, then retry the typing +
  attach until the send actually fires (user bubble renders).
- ChatUI: the image auto-send reads the live input DOM value instead of
  possibly-stale React state.

Approval-flow: `runTest` treated the optimistic user-message bump as "run
started", returning before any real stream activity — a stalled run then timed
out waiting for an approval that never appeared.

- runTest: require real stream activity (loading on, a tool call, completion, or
  an assistant message) before returning, and retry the click otherwise.

Verified: multimodal 200/200 with 0 flaky at 20x (4 workers, retries=2) and at
6 workers/retries=0; approval-flow 450/450 at 25x/8 workers/retries=0; full E2E
suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Three E2E test stability fixes: ChatUI gains a DOM ref so the image-attachment handler reads the committed input value instead of React state. sendMessageWithImage is rewritten with a full retry loop and explicit value assertion. The tools-test runTest helper switches to waitForFunction polling with expanded start-detection conditions.

Changes

E2E Flakiness Fixes: Multimodal and Approval-Flow Tests

Layer / File(s) Summary
ChatUI: DOM ref for committed input value
testing/e2e/src/components/ChatUI.tsx
Adds inputRef = useRef<HTMLInputElement>(null) and attaches it via ref={inputRef} on the chat text input. The image-attachment onChange handler now reads inputRef.current?.value (falling back to React input state) so it captures the committed DOM value rather than potentially stale React state.
sendMessageWithImage: retryable interaction with value assertion
testing/e2e/tests/helpers.ts
Rewrites the helper to click + clear the input, type the full prompt via pressSequentially with a short delay, assert the input value equals the expected text, clear then re-set the attachment file input, and wait for the user message bubble — all inside an expect(...).toPass(...) retry wrapper with a 15s timeout and stepwise intervals.
runTest: polling-based run-started detection
testing/e2e/tests/tools-test/helpers.ts
Replaces the single page.evaluate baseline check with page.waitForFunction (2000ms). Start conditions now include data-is-loading === "true", data-tool-call-count > 0, data-test-complete === "true", or messages count > baseline + 1, preventing the optimistic user message from being misclassified as stream activity.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 Hop, hop, the tests were sly,
Racing inputs, on the fly.
A ref anchors what React forgot,
Retry loops tie the flaky knot.
No more ghost messages — the stream runs true,
Green CI at last, I bid flakes adieu! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.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 Title accurately summarizes the main change: fixing flaky multimodal and approval-flow E2E tests (#818), which directly aligns with the changeset's focus on test harness race condition fixes.
Description check ✅ Passed Description comprehensively covers changes, root causes, fixes, and verification results; includes all required checklist items; clearly marks as dev-only (no changeset needed), exceeding the template requirements.
Linked Issues check ✅ Passed All code changes directly address issue #818's acceptance criteria: fixes identify root cause as test harness races (not aimock), and verification confirms multimodal and approval-flow specs now run green deterministically without flakiness.
Out of Scope Changes check ✅ Passed All changes are tightly scoped to test infrastructure only: E2E helpers, a test component's input handling, and test utility functions—no library code or unrelated modifications introduced.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 818-flaky-e2e-multimodal-+-approval-flow-tests-intermittently-get-empty-responses-chatstream-fatal-from-aimock

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

testing/e2e/tests/helpers.ts

Parsing error: "parserOptions.project" has been provided for @typescript-eslint/parser.
The file was not found in any of the provided project(s): testing/e2e/tests/helpers.ts

testing/e2e/tests/tools-test/helpers.ts

Parsing error: "parserOptions.project" has been provided for @typescript-eslint/parser.
The file was not found in any of the provided project(s): testing/e2e/tests/tools-test/helpers.ts


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.

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

1 package(s) bumped directly, 0 bumped as dependents.

🟩 Patch bumps

Package Version Reason
@tanstack/ai-mcp 0.1.6 → 0.1.7 Changeset

@nx-cloud

nx-cloud Bot commented Jun 24, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 5b82dea

Command Status Duration Result
nx run-many --targets=build --exclude=examples/... ✅ Succeeded 1s View ↗

☁️ Nx Cloud last updated this comment at 2026-06-24 04:37:06 UTC

@pkg-pr-new

pkg-pr-new Bot commented Jun 24, 2026

Copy link
Copy Markdown

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/@tanstack/ai@819

@tanstack/ai-angular

npm i https://pkg.pr.new/@tanstack/ai-angular@819

@tanstack/ai-anthropic

npm i https://pkg.pr.new/@tanstack/ai-anthropic@819

@tanstack/ai-client

npm i https://pkg.pr.new/@tanstack/ai-client@819

@tanstack/ai-code-mode

npm i https://pkg.pr.new/@tanstack/ai-code-mode@819

@tanstack/ai-code-mode-skills

npm i https://pkg.pr.new/@tanstack/ai-code-mode-skills@819

@tanstack/ai-devtools-core

npm i https://pkg.pr.new/@tanstack/ai-devtools-core@819

@tanstack/ai-elevenlabs

npm i https://pkg.pr.new/@tanstack/ai-elevenlabs@819

@tanstack/ai-event-client

npm i https://pkg.pr.new/@tanstack/ai-event-client@819

@tanstack/ai-fal

npm i https://pkg.pr.new/@tanstack/ai-fal@819

@tanstack/ai-gemini

npm i https://pkg.pr.new/@tanstack/ai-gemini@819

@tanstack/ai-grok

npm i https://pkg.pr.new/@tanstack/ai-grok@819

@tanstack/ai-groq

npm i https://pkg.pr.new/@tanstack/ai-groq@819

@tanstack/ai-isolate-cloudflare

npm i https://pkg.pr.new/@tanstack/ai-isolate-cloudflare@819

@tanstack/ai-isolate-node

npm i https://pkg.pr.new/@tanstack/ai-isolate-node@819

@tanstack/ai-isolate-quickjs

npm i https://pkg.pr.new/@tanstack/ai-isolate-quickjs@819

@tanstack/ai-mcp

npm i https://pkg.pr.new/@tanstack/ai-mcp@819

@tanstack/ai-ollama

npm i https://pkg.pr.new/@tanstack/ai-ollama@819

@tanstack/ai-openai

npm i https://pkg.pr.new/@tanstack/ai-openai@819

@tanstack/ai-openrouter

npm i https://pkg.pr.new/@tanstack/ai-openrouter@819

@tanstack/ai-preact

npm i https://pkg.pr.new/@tanstack/ai-preact@819

@tanstack/ai-react

npm i https://pkg.pr.new/@tanstack/ai-react@819

@tanstack/ai-react-ui

npm i https://pkg.pr.new/@tanstack/ai-react-ui@819

@tanstack/ai-solid

npm i https://pkg.pr.new/@tanstack/ai-solid@819

@tanstack/ai-solid-ui

npm i https://pkg.pr.new/@tanstack/ai-solid-ui@819

@tanstack/ai-svelte

npm i https://pkg.pr.new/@tanstack/ai-svelte@819

@tanstack/ai-utils

npm i https://pkg.pr.new/@tanstack/ai-utils@819

@tanstack/ai-vue

npm i https://pkg.pr.new/@tanstack/ai-vue@819

@tanstack/ai-vue-ui

npm i https://pkg.pr.new/@tanstack/ai-vue-ui@819

@tanstack/openai-base

npm i https://pkg.pr.new/@tanstack/openai-base@819

@tanstack/preact-ai-devtools

npm i https://pkg.pr.new/@tanstack/preact-ai-devtools@819

@tanstack/react-ai-devtools

npm i https://pkg.pr.new/@tanstack/react-ai-devtools@819

@tanstack/solid-ai-devtools

npm i https://pkg.pr.new/@tanstack/solid-ai-devtools@819

commit: 5b82dea

@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/helpers.ts`:
- Around line 41-64: The test assertion using await
expect(userMessages.first()).toBeVisible() does not confirm that a new message
was actually sent because the first user message may already be visible from
earlier in the conversation. Instead, capture the initial count of user messages
before attaching the image, then after the attach, verify that the count has
increased by one. This ensures the assertion confirms a new message was added
rather than just checking visibility of an existing message. Store the initial
length before the fileInput.setInputFiles calls and add a subsequent assertion
that the user message count increased.
🪄 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: 237b63ee-3c3e-4702-a17f-fe530be6a8bc

📥 Commits

Reviewing files that changed from the base of the PR and between df40512 and 5b82dea.

📒 Files selected for processing (3)
  • testing/e2e/src/components/ChatUI.tsx
  • testing/e2e/tests/helpers.ts
  • testing/e2e/tests/tools-test/helpers.ts

Comment on lines +41 to +64
const userMessages = page.getByTestId('user-message')

// Attaching the image auto-sends, using the prompt currently in the chat
// input, and the matched aimock fixture keys on the exact user text. A
// *controlled* React input is fragile here under CPU load (CI, parallel
// workers) in two ways: typing char-by-char can drop characters, leaving a
// truncated value like "cribe this image" (which 404s as "No fixture
// matched" → empty `chatStream fatal`); and the attach's onChange can land
// before the typed value is committed, dispatching nothing at all. So drive
// the interaction to its observable outcome — the user bubble rendering —
// retrying both the typing and the attach until the send actually fires with
// the full prompt. A redundant re-attach is harmless: the client ignores a
// second send while the first is still streaming.
await expect(async () => {
await input.click()
await input.fill('')
await input.pressSequentially(text, { delay: 15 })
// Confirm the full prompt is committed before attaching.
expect(await input.inputValue()).toBe(text)
// Reset the selection so re-attaching the same path re-fires onChange.
await fileInput.setInputFiles([])
await fileInput.setInputFiles(imagePath)
await expect(userMessages.first()).toBeVisible({ timeout: 2_000 })
}).toPass({ timeout: 15_000, intervals: [250, 500, 1000] })

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use message-count delta instead of .first() visibility to confirm the send actually fired.

At Line 63, userMessages.first() may already be visible from earlier messages, so this check can pass even when the new image send didn’t occur. Track a baseline count and assert it increases after attach.

Suggested fix
 export async function sendMessageWithImage(
   page: Page,
   text: string,
   imagePath: string,
 ) {
   const input = page.getByTestId('chat-input')
   const fileInput = page.getByTestId('image-attachment-input')
   const userMessages = page.getByTestId('user-message')
+  const baselineUserMessageCount = await userMessages.count()

   await expect(async () => {
     await input.click()
     await input.fill('')
     await input.pressSequentially(text, { delay: 15 })
     // Confirm the full prompt is committed before attaching.
     expect(await input.inputValue()).toBe(text)
     // Reset the selection so re-attaching the same path re-fires onChange.
     await fileInput.setInputFiles([])
     await fileInput.setInputFiles(imagePath)
-    await expect(userMessages.first()).toBeVisible({ timeout: 2_000 })
+    await expect
+      .poll(async () => await userMessages.count(), { timeout: 2_000 })
+      .toBeGreaterThan(baselineUserMessageCount)
   }).toPass({ timeout: 15_000, intervals: [250, 500, 1000] })
 }
🤖 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/helpers.ts` around lines 41 - 64, The test assertion using
await expect(userMessages.first()).toBeVisible() does not confirm that a new
message was actually sent because the first user message may already be visible
from earlier in the conversation. Instead, capture the initial count of user
messages before attaching the image, then after the attach, verify that the
count has increased by one. This ensures the assertion confirms a new message
was added rather than just checking visibility of an existing message. Store the
initial length before the fileInput.setInputFiles calls and add a subsequent
assertion that the user message count increased.

@tombeckenham
tombeckenham requested a review from AlemTuzlak June 24, 2026 04:53
@tombeckenham

Copy link
Copy Markdown
Contributor Author

I'm just going to merge this. Fixing flaky tests only

@tombeckenham
tombeckenham merged commit 614576c into main Jun 24, 2026
10 checks passed
@tombeckenham
tombeckenham deleted the 818-flaky-e2e-multimodal-+-approval-flow-tests-intermittently-get-empty-responses-chatstream-fatal-from-aimock branch June 24, 2026 04:54
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.

Flaky E2E: multimodal + approval-flow tests intermittently get empty responses (chatStream fatal) from aimock

1 participant