test(e2e): Add a node-flue end-to-end application - #24377
RulaKhaled wants to merge 4 commits into
Conversation
size-limit report 📦
|
8f35f04 to
c9a3c7b
Compare
6232570 to
be1be03
Compare
| const hasOps = (ops: string[]) => (spansOfTrace: { attributes?: Record<string, { value?: unknown }> }[]) => | ||
| ops.every(op => spansOfTrace.some(span => getSpanOp(span) === op)); | ||
|
|
||
| test('captures the invoke_agent / chat / execute_tool hierarchy for a Flue turn', async ({ baseURL }) => { |
There was a problem hiding this comment.
somewhere here we should also test the http.server spans. as this uses hono, it will today likely be unparametrized, which we should codify in the test. once #24371 lands this should automatically be captured as hono spans too then!
The node-integration-test suite drives Flue from a hand-written scenario that calls `__flueBindAgentModule` itself, standing in for what `@flue/vite` does at build time. It cannot show whether a scaffolded app works. This app is what `flue init` produces — plain vite, `'use agent'`, `createAgentRouter` — built and served the way a user runs it, against a real provider. Covers: AI spans, errors captured as issues, a manual span nesting inside a tool, an orchestrion-instrumented `dataloader` span landing in the agent's trace, and both dev and prod. Also asserts the provider's HTTP call nests inside `chat`, which nothing else covers. The loader is called from inside a tool rather than a route, so its span shares the agent's trace instead of sitting in one of its own. No build externals are needed, unlike node-eve: a Flue node build leaves dependencies as bare specifiers, so `dataloader` stays a real module for the transform to hook. The `@flue/*` versions are pinned because the internal registry proxy 403s on releases it has not scanned, and a caret range drifts onto them; the `(latest)` variant is where new versions get exercised. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two lines over the width limit; I formatted the server-utils sources but not the test application. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`runAgentTurn` returned on the `202` and dropped the `streamUrl`, so a turn kept running while the next test started waiting for spans — a leftover trace could satisfy the wrong assertion. It now reads the conversation back until it reports a settlement. Scoping the waits by `gen_ai.conversation.id` would not have worked: Flue generates that id (`conv_01M2G81…`), so it is not the path segment the test chose and the test cannot know it up front. Also names `count_items` in the agent instructions — an earlier edit missed, so the dataloader test was relying on the tool description alone — and drops the `loaders.ts` comment describing the route-based setup that no longer exists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
be1be03 to
707b1f5
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 707b1f5. Configure here.
| const conversation = (await (await fetch(url)).json()) as { settlements?: unknown[] }; | ||
| if (conversation.settlements?.length) { | ||
| return; | ||
| } |
There was a problem hiding this comment.
Reused conversation IDs skip settlement
Medium Severity
runAgentTurn treats any existing settlements as done and the tests pass a fixed conversation id. A Playwright retry (dev allows 3) or a later test:dev run against the same Flue record POSTs a new turn and then returns immediately, so the helper no longer waits for that turn to finish.
Additional Locations (2)
Triggered by project rule: PR Review Guidelines for Cursor Bot
Reviewed by Cursor Bugbot for commit 707b1f5. Configure here.
| ); | ||
| const spansPromise = collectStreamedSpans(APP, spansOfTrace => | ||
| spansOfTrace.some(span => getSpanOp(span) === 'gen_ai.execute_tool'), | ||
| ); |
There was a problem hiding this comment.
Span waits are not unique
Medium Severity
Several collectStreamedSpans predicates match any agent turn (gen_ai.execute_tool, gen_ai.chat plus http.client) instead of the tool or span unique to that test. collectStreamedSpans can resolve the first matching leftover trace, and the dataloader wait can finish before count_items is even in the set.
Additional Locations (2)
Triggered by project rule: PR Review Guidelines for Cursor Bot
Reviewed by Cursor Bugbot for commit 707b1f5. Configure here.
- Drop the explicit `dataloaderIntegration()`; it is in `getTracingIntegrations()` now, so Node registers it by default when spans are enabled. - Always run under orchestrion rather than keeping it as a variant, since that is how the SDK is meant to be set up. Removes the `*:orchestrion` scripts, the `USE_ORCHESTRION` plumbing and the `test.fail()` branch in the dataloader test, which now simply asserts the span lands under `execute_tool`. - Drop the node-eve reference from `vite.config.ts`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| @@ -0,0 +1,3 @@ | |||
| import DataLoader from 'dataloader'; | |||
|
|
|||
| export const itemLoader = new DataLoader<number, number>(async keys => keys.map(key => key * 2)); | |||
There was a problem hiding this comment.
Bug: The module-level singleton itemLoader will cause subsequent calls to the count_items tool to hit the cache, preventing the batch function from running and dataloader spans from being emitted.
Severity: MEDIUM
Suggested Fix
Instead of creating a singleton DataLoader at the module level, create a new DataLoader instance for each tool execution. This aligns with the pattern used in other test applications like node-eve and node-mastra and follows the recommended best practice for using DataLoader in long-lived applications to prevent unintended cache sharing.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location: dev-packages/e2e-tests/test-applications/node-flue/src/loaders.ts#L3
Potential issue: The `itemLoader` in `src/loaders.ts` is created as a module-level
singleton. Due to `DataLoader`'s default caching behavior, once a key is resolved,
subsequent calls to `.load()` with the same key will return a cached result without
re-executing the batch function. This means that if the `count_items` tool is called
more than once within the same server process lifetime, the second and subsequent calls
will not emit the expected dataloader spans. This breaks observability for repeated tool
calls and deviates from the best practice of creating a new `DataLoader` instance per
request to avoid unintended cache sharing.
Also affects:
dev-packages/e2e-tests/test-applications/node-flue/tests/dataloader.test.ts:29~29
Did we get this right? 👍 / 👎 to inform future reviews.


Stacked on #24266 — review that first, this is the e2e app only.
The node-integration-test calls
__flueBindAgentModuleby hand, standing in for what@flue/vitedoes from the'use agent'directive, so it can't show whether a scaffolded app actually works. This isflue initoutput, built and served the way a user runs it, against a real provider.Covered, in both dev and prod and in both the base and orchestrion variants:
invoke_agent/chat/execute_toolwith usage and costexecute_tooldataloaderspan landing in the agent's tracehttp.clientcall nesting insidechatThe loader runs inside a tool rather than a route so its span shares the agent's trace. No build externals are needed unlike
node-eve— a Flue node build leaves dependencies as bare specifiers, sodataloaderstays a real module for the transform to hook.@flue/*is pinned because the internal registry proxy 403s on releases it hasn't scanned.Found: a thrown tool error produced an errored span and no issue at all, since Flue catches the throw and hands it back to the model as a tool result. Fixed in #24265.