Skip to content

fix: resolve shared AI-SDK tool-call dedup and error handling gaps (#797)#799

Draft
easonLiangWorldedtech wants to merge 4 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:fix/shared-sdk-error-handle-gap
Draft

fix: resolve shared AI-SDK tool-call dedup and error handling gaps (#797)#799
easonLiangWorldedtech wants to merge 4 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:fix/shared-sdk-error-handle-gap

Conversation

@easonLiangWorldedtech

@easonLiangWorldedtech easonLiangWorldedtech commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Fix shared AI-SDK tool-call streaming/error-handling gaps (#797)

Problem

src/api/transform/ai-sdk.ts's processAiSdkStreamPart and src/api/providers/openai-compatible.ts (the shared base class used by Novita, Moonshot, and Poe) have two confirmed defects:

1. Missing error handling in OpenAICompatibleHandler

  • createMessage() / completePrompt() have no try/catch, unlike 10+ sibling providers that call handleOpenAIError/handleProviderError
  • Errors never get tagged with provider name or .status, so Task.ts's backoffAndAnnounce (error?.status === 429) never fires 429-aware retry/backoff for Novita/Moonshot/Poe

Fix: Wrap both methods in try/catch, throw via handleOpenAIError(error, this.config.providerName)

2. Tool-call dedup keyed only on toolCallId

  • Dedup logic suppresses a second event sharing an already-seen toolCallId without checking toolName/arguments
  • If a backend reuses an id for two different tool calls, the second one is silently dropped

Fix: Change all 9 usages of streamingToolCallIndices to use compound key (id, name)dedupKey = ${event.id}::${event.name}

Changes

File Description
src/api/providers/openai-compatible.ts Add try/catch + handleOpenAIError to createMessage() and completePrompt()
src/core/task/Task.ts 9 places: change streamingToolCallIndices ops from single key to compound (id, name)
src/api/transform/stream.ts ApiStreamToolCallEndChunk: add name?: string field
src/core/assistant-message/NativeToolCallParser.ts finalizeRawChunks() emit includes name: tracked.name

Tests Added

  • openai-compatible.spec.ts: 7 tests covering normal streaming, 429, 500, 400/500 errors with .status and provider name verification
  • NativeToolCallParser.spec.ts: 2 tests verifying compound key dedup for same-id-different-payload collision
  • duplicate-tool-use-ids.spec.ts: 134-line regression test suite

Regression Scope

This logic is shared across every OpenAICompatibleHandler-based provider (Moonshot, Poe, Novita). Fixes apply to all of them.

Related

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of streaming tool calls so tools with the same ID but different names no longer collide.
    • End-of-stream tool-call events now include the tool name, improving correctness and reducing missing/duplicate entries.
    • Provider errors from streaming and non-streaming requests are now surfaced more consistently with clearer status/message details.
  • Tests
    • Added Vitest coverage for OpenAI-compatible message streaming/completion, error propagation, and compound-key (id + name) tool-call deduplication.

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 185d02b2-48cd-4367-95c5-35e2d6d30229

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR wraps OpenAI-compatible provider errors through a shared handleOpenAIError utility, extends tool-call streaming metadata with names, and updates NativeToolCallParser and Task to track streaming tool calls with compound id+name keys. It also adds tests for the new provider, parser, and streaming flows.

Changes

Provider error handling and validation

Layer / File(s) Summary
OpenAI-compatible provider error handling
src/api/providers/openai-compatible.ts
Imports handleOpenAIError and wraps createMessage streaming and completePrompt completion calls in try/catch, rethrowing wrapped provider-tagged errors.
OpenAICompatibleHandler test suite
src/api/providers/__tests__/openai-compatible.spec.ts
Mocks ai and createOpenAICompatible, defines a test handler subclass, and covers streaming output, usage emission, tool-call events, and completion success and failure paths.

Tool-call name tracking and parser coverage

Layer / File(s) Summary
Tool-call name tracking
src/api/transform/stream.ts, src/core/assistant-message/NativeToolCallParser.ts
Adds optional name to tool-call end chunks, emits the name during raw-chunk finalization, and exposes getStreamingToolName() for active streaming tool calls.
NativeToolCallParser coverage
src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts, src/core/assistant-message/__tests__/NativeToolCallParser-additional.spec.ts
Adds finalizeRawChunks coverage for emitted names and duplicate ids, plus broader tests for streaming state, name lookup, MCP behavior, and raw-chunk cleanup paths.

Compound-key tool-call tracking in Task

Layer / File(s) Summary
Compound-key dedup in Task
src/core/task/Task.ts
Changes tool_call_partial handling and raw-chunk finalization to use compound id+name keys for lookup, insertion, and cleanup in streamingToolCallIndices.
Compound-key dedup tests
src/core/task/__tests__/duplicate-tool-use-ids.spec.ts
Adds tests for same-id/different-name acceptance, exact duplicate rejection, per-key index tracking, and targeted cleanup.
Task streaming tool-call integration
src/core/task/__tests__/Task.streaming-tool-calls.spec.ts
Adds an integration suite that exercises streaming raw-chunk handling, partial tool_use creation, finish-reason finalization, and Task-driven tool_call_partial event flows end to end.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant OpenAICompatibleHandler
  participant AISDK as ai
  participant ErrorHandler as handleOpenAIError

  Caller->>OpenAICompatibleHandler: createMessage() / completePrompt()
  OpenAICompatibleHandler->>AISDK: streamText() / generateText()
  AISDK-->>OpenAICompatibleHandler: throws error
  OpenAICompatibleHandler->>ErrorHandler: handleOpenAIError(error, providerName)
  ErrorHandler-->>OpenAICompatibleHandler: wrapped error
  OpenAICompatibleHandler-->>Caller: throws wrapped error
Loading
sequenceDiagram
  participant Stream as tool_call_partial events
  participant Task
  participant Indices as streamingToolCallIndices

  Stream->>Task: tool_call_start(id, name)
  Task->>Indices: insert key = id::name
  Stream->>Task: tool_call_delta(id, name)
  Task->>Indices: lookup key = id::name
  Stream->>Task: tool_call_end(id, name)
  Task->>Indices: lookup and delete key = id::name
Loading

Possibly related issues

Suggested labels: awaiting-review

Suggested reviewers: JamesRobert20, hannesrudolph, navedmerchant, taltas, edelauna

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed, but it does not follow the required template and is missing explicit Test Procedure and checklist sections. Add the required template sections, especially the linked issue, explicit test procedure, and pre-submission checklist items.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main fix: shared AI-SDK tool-call dedup and error handling across providers.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ 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.

@codecov

codecov Bot commented Jul 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 50.00000% with 24 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/core/task/Task.ts 0.00% 22 Missing ⚠️
src/core/assistant-message/NativeToolCallParser.ts 81.81% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/core/assistant-message/NativeToolCallParser.ts (1)

53-63: 🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

Thread (id, name) through streaming tool calls

  • NativeToolCallParser still keys streamingToolCalls by bare id, so reused backend ids can overwrite another in-flight tool call’s accumulated JSON.
  • processFinishReason() also drops name, and Task.ts reads getStreamingToolName(event.id) after finalization, which turns the end-path dedup key into id::undefined.
    Use the compound key in startStreamingToolCall, processStreamingChunk, finalizeStreamingToolCall, and getStreamingToolName, and include name on every tool_call_end.
🤖 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 `@src/core/assistant-message/NativeToolCallParser.ts` around lines 53 - 63,
Thread the tool call name through streaming state so reused backend ids don’t
collide and finalization keeps the correct lookup key. Update
NativeToolCallParser’s startStreamingToolCall, processStreamingChunk,
finalizeStreamingToolCall, and getStreamingToolName to key streamingToolCalls by
the compound (id, name) instead of bare id, and make processFinishReason
preserve name when emitting tool_call_end events. Ensure Task.ts can still
resolve the streamed tool name after finalization by reading the same compound
key path.
🧹 Nitpick comments (1)
src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts (1)

346-411: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Good coverage of finalizeRawChunks(), but the actual argument-accumulation collision isn't tested.

Both new tests only exercise processRawChunk/finalizeRawChunks (index-keyed rawChunkTracker), not startStreamingToolCall/processStreamingChunk/finalizeStreamingToolCall (id-keyed streamingToolCalls). Since the latter map is where the same-id-different-name collision actually corrupts data (see comment on NativeToolCallParser.ts), consider adding a test that calls startStreamingToolCall("dup_id", "tool_a") and startStreamingToolCall("dup_id", "tool_b") before either finalizes, then verifies both tools' accumulated arguments remain distinct and correct.

As per coding guidelines, "Use package-local unit tests for pure logic, parsing, state transitions, validation, serialization, request construction, retry decisions, and error handling."

🤖 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 `@src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts` around
lines 346 - 411, The new coverage is testing raw-chunk finalization, but it
misses the real collision path in streaming state. Add a unit test against
NativeToolCallParser’s streaming flow using startStreamingToolCall,
processStreamingChunk, and finalizeStreamingToolCall to simulate two tools with
the same id and different names. Verify that each streamingToolCalls entry keeps
its own accumulated arguments and that finalization returns distinct, correct
results for both tool names.

Source: Coding guidelines

🤖 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.

Outside diff comments:
In `@src/core/assistant-message/NativeToolCallParser.ts`:
- Around line 53-63: Thread the tool call name through streaming state so reused
backend ids don’t collide and finalization keeps the correct lookup key. Update
NativeToolCallParser’s startStreamingToolCall, processStreamingChunk,
finalizeStreamingToolCall, and getStreamingToolName to key streamingToolCalls by
the compound (id, name) instead of bare id, and make processFinishReason
preserve name when emitting tool_call_end events. Ensure Task.ts can still
resolve the streamed tool name after finalization by reading the same compound
key path.

---

Nitpick comments:
In `@src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts`:
- Around line 346-411: The new coverage is testing raw-chunk finalization, but
it misses the real collision path in streaming state. Add a unit test against
NativeToolCallParser’s streaming flow using startStreamingToolCall,
processStreamingChunk, and finalizeStreamingToolCall to simulate two tools with
the same id and different names. Verify that each streamingToolCalls entry keeps
its own accumulated arguments and that finalization returns distinct, correct
results for both tool names.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 571fc390-4957-406d-a94b-30db466fc4e9

📥 Commits

Reviewing files that changed from the base of the PR and between fa3af3f and 2fc9964.

📒 Files selected for processing (7)
  • src/api/providers/__tests__/openai-compatible.spec.ts
  • src/api/providers/openai-compatible.ts
  • src/api/transform/stream.ts
  • src/core/assistant-message/NativeToolCallParser.ts
  • src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts
  • src/core/task/Task.ts
  • src/core/task/__tests__/duplicate-tool-use-ids.spec.ts

@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: 2

🧹 Nitpick comments (2)
src/api/providers/__tests__/openai-compatible.spec.ts (1)

174-201: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Weak assertion doesn't validate tool-call chunk content.

The test only checks chunks.length at Line 200, never verifying that tool-call-start/tool-call-delta/tool-call-end stream parts are actually converted into the expected chunk types with correct toolCallId/name/argument data. Given this PR's core fix is tool-call name propagation and id+name compound-key dedup, this test should assert on the actual chunk shape to catch regressions in that path.

As per coding guidelines, spec files should cover "state transitions ... and error handling", which this test currently doesn't exercise meaningfully.

♻️ Proposed stronger assertions
 			const stream = handler.createMessage(systemPrompt, messages)
 			const chunks: any[] = []
 			for await (const chunk of stream) {
 				chunks.push(chunk)
 			}
 
-			expect(chunks.length).toBeGreaterThan(0)
+			const toolCallChunks = chunks.filter((chunk) => chunk.type?.startsWith("tool_call"))
+			expect(toolCallChunks.length).toBeGreaterThan(0)
+			const startChunk = toolCallChunks.find((c) => c.type === "tool_call_start")
+			expect(startChunk).toMatchObject({ id: "tc_1", name: "read_file" })
 		})
🤖 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 `@src/api/providers/__tests__/openai-compatible.spec.ts` around lines 174 -
201, The stream test in openai-compatible.spec.ts is too weak because it only
checks that some chunks were produced and never verifies tool-call event
mapping. Update the test around handler.createMessage/mockStreamText to assert
the actual chunk contents for tool-call-start, tool-call-delta, and
tool-call-end, including toolCallId, propagated name, and parsed argument data.
Use the existing mockFullStream and the returned chunks array to validate the
expected chunk shapes instead of just checking chunks.length.

Source: Coding guidelines

src/core/assistant-message/__tests__/NativeToolCallParser-additional.spec.ts (1)

75-86: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Loosened assertion defeats the purpose of the test.

The comment claims MCP tools return "mcp_tool_use" type, but the assertion permits either tool_use or mcp_tool_use, so a regression that returns the wrong type would go undetected.

🧪 Proposed fix to assert the exact expected type
-			expect(result?.type).toMatch(/^(tool_use|mcp_tool_use)$/)
+			expect(result?.type).toBe("mcp_tool_use")
🤖 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 `@src/core/assistant-message/__tests__/NativeToolCallParser-additional.spec.ts`
around lines 75 - 86, The test in NativeToolCallParser-additional.spec.ts is too
permissive because finalizeStreamingToolCall is allowed to return either type,
which masks regressions. Tighten the assertion in the "should return final
McpToolUse for MCP tools on finalize" case to expect the exact MCP-specific type
returned by NativeToolCallParser.finalizeStreamingToolCall for mcp-- prefixed
tool names, and keep the rest of the setup unchanged so the test verifies the
intended behavior precisely.

Source: Coding guidelines

🤖 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
`@src/core/assistant-message/__tests__/NativeToolCallParser-additional.spec.ts`:
- Around line 96-112: The test case is asserting too weakly and its setup
contradicts the scenario it claims to cover. Update the NativeToolCallParser
spec to exercise the actual “not started” path by passing data that does not
trigger a start event in processRawChunk, then assert the exact events returned
by finalizeRawChunks rather than only checking Array.isArray. Use
clearRawChunkState, processRawChunk, and finalizeRawChunks to verify the
intended state transition and rename the test if needed so the title matches the
behavior being validated.

In `@src/core/task/__tests__/Task.streaming-tool-calls.spec.ts`:
- Around line 553-733: The Task streaming-tool-call integration tests only check
that cline is defined, which does not verify any state transition or parsing
behavior. Update the tests in Task.streaming-tool-calls.spec to assert
observable outcomes after attemptApiRequest(0) and iterator.next(), such as
assistantMessageContent, userMessageContent, or streaming-tool-call state, using
the existing Task instance and mock stream generators. For the duplicate
tool_call_start and lifecycle cases, add assertions that the dedup/path handling
actually occurred, or spy on NativeToolCallParser / related internal methods so
the tests validate the raw-chunk routing they are meant to cover.

---

Nitpick comments:
In `@src/api/providers/__tests__/openai-compatible.spec.ts`:
- Around line 174-201: The stream test in openai-compatible.spec.ts is too weak
because it only checks that some chunks were produced and never verifies
tool-call event mapping. Update the test around
handler.createMessage/mockStreamText to assert the actual chunk contents for
tool-call-start, tool-call-delta, and tool-call-end, including toolCallId,
propagated name, and parsed argument data. Use the existing mockFullStream and
the returned chunks array to validate the expected chunk shapes instead of just
checking chunks.length.

In
`@src/core/assistant-message/__tests__/NativeToolCallParser-additional.spec.ts`:
- Around line 75-86: The test in NativeToolCallParser-additional.spec.ts is too
permissive because finalizeStreamingToolCall is allowed to return either type,
which masks regressions. Tighten the assertion in the "should return final
McpToolUse for MCP tools on finalize" case to expect the exact MCP-specific type
returned by NativeToolCallParser.finalizeStreamingToolCall for mcp-- prefixed
tool names, and keep the rest of the setup unchanged so the test verifies the
intended behavior precisely.
🪄 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 Plus

Run ID: f191ed26-f5c7-4454-bb03-95535202595b

📥 Commits

Reviewing files that changed from the base of the PR and between 2fc9964 and 38e274c.

📒 Files selected for processing (3)
  • src/api/providers/__tests__/openai-compatible.spec.ts
  • src/core/assistant-message/__tests__/NativeToolCallParser-additional.spec.ts
  • src/core/task/__tests__/Task.streaming-tool-calls.spec.ts

Comment on lines +96 to +112
it("should not emit events for tools that haven't started", () => {
NativeToolCallParser.clearRawChunkState()

// process a raw chunk without hasStarted (just tracking metadata)
// This simulates the case where only arguments are received without start
const result = NativeToolCallParser.processRawChunk({
index: 1,
id: "toolu_partial",
name: "read_file",
arguments: undefined, // No arguments means no delta event, but still tracks
})

// finalize should not emit for tools that haven't hasStarted
const events = NativeToolCallParser.finalizeRawChunks()
// The behavior depends on implementation - just verify it doesn't crash
expect(Array.isArray(events)).toBe(true)
})

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

Test doesn't verify its own premise and assertion is non-specific.

Since name: "read_file" is passed in the chunk, per processRawChunk's logic a tool_call_start event fires immediately (tracked.name is set on creation, then !tracked.hasStarted && tracked.name triggers the start event) — the tool call has started. The title "should not emit events for tools that haven't started" doesn't match what's actually exercised, and the assertion expect(Array.isArray(events)).toBe(true) will pass regardless of the actual event count, making the test unable to catch regressions.

🧪 Proposed fix to assert actual expected behavior
-		it("should not emit events for tools that haven't started", () => {
+		it("should emit an end event for a tool call that already started (name provided upfront)", () => {
 			NativeToolCallParser.clearRawChunkState()
-
-			// process a raw chunk without hasStarted (just tracking metadata)
-			// This simulates the case where only arguments are received without start
-			const result = NativeToolCallParser.processRawChunk({
+			NativeToolCallParser.processRawChunk({
 				index: 1,
 				id: "toolu_partial",
 				name: "read_file",
-				arguments: undefined, // No arguments means no delta event, but still tracks
+				arguments: undefined,
 			})
-
-			// finalize should not emit for tools that haven't hasStarted
 			const events = NativeToolCallParser.finalizeRawChunks()
-			// The behavior depends on implementation - just verify it doesn't crash
-			expect(Array.isArray(events)).toBe(true)
+			expect(events).toEqual([{ type: "tool_call_end", id: "toolu_partial", name: "read_file" }])
 		})

As per coding guidelines, **/*.{test,spec}.{ts,tsx,js} should test "state transitions" precisely; this test's title and assertion don't match the actual observable state transition.

📝 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
it("should not emit events for tools that haven't started", () => {
NativeToolCallParser.clearRawChunkState()
// process a raw chunk without hasStarted (just tracking metadata)
// This simulates the case where only arguments are received without start
const result = NativeToolCallParser.processRawChunk({
index: 1,
id: "toolu_partial",
name: "read_file",
arguments: undefined, // No arguments means no delta event, but still tracks
})
// finalize should not emit for tools that haven't hasStarted
const events = NativeToolCallParser.finalizeRawChunks()
// The behavior depends on implementation - just verify it doesn't crash
expect(Array.isArray(events)).toBe(true)
})
it("should emit an end event for a tool call that already started (name provided upfront)", () => {
NativeToolCallParser.clearRawChunkState()
NativeToolCallParser.processRawChunk({
index: 1,
id: "toolu_partial",
name: "read_file",
arguments: undefined,
})
const events = NativeToolCallParser.finalizeRawChunks()
expect(events).toEqual([{ type: "tool_call_end", id: "toolu_partial", name: "read_file" }])
})
🤖 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 `@src/core/assistant-message/__tests__/NativeToolCallParser-additional.spec.ts`
around lines 96 - 112, The test case is asserting too weakly and its setup
contradicts the scenario it claims to cover. Update the NativeToolCallParser
spec to exercise the actual “not started” path by passing data that does not
trigger a start event in processRawChunk, then assert the exact events returned
by finalizeRawChunks rather than only checking Array.isArray. Use
clearRawChunkState, processRawChunk, and finalizeRawChunks to verify the
intended state transition and rename the test if needed so the title matches the
behavior being validated.

Source: Coding guidelines

Comment on lines +553 to +733
describe("tool_call_partial chunk handling - Task integration", () => {
it("should handle tool_call_start event from raw chunk processing", async () => {
const cline = new Task({
provider: mockProvider,
apiConfiguration: mockApiConfig,
task: "test task with tool_call_start",
startTask: false,
})

vi.spyOn(cline as any, "getSystemPrompt").mockResolvedValue("mock system prompt")

async function* mockStreamGenerator() {
yield {
type: "tool_call_partial" as const,
index: 0,
id: "toolu_123",
name: "read_file",
arguments: '{"path":"a.ts"}',
}
yield { type: "text" as const, text: "response" }
}

vi.spyOn(cline.api, "createMessage").mockReturnValue(mockStreamGenerator())

const iterator = cline.attemptApiRequest(0)
await iterator.next()

expect(cline).toBeDefined()
})

it("should handle duplicate tool_call_start events", async () => {
const cline = new Task({
provider: mockProvider,
apiConfiguration: mockApiConfig,
task: "test task with duplicate start",
startTask: false,
})

vi.spyOn(cline as any, "getSystemPrompt").mockResolvedValue("mock system prompt")

async function* mockStreamGenerator() {
yield {
type: "tool_call_partial" as const,
index: 0,
id: "toolu_dup123",
name: "read_file",
arguments: '{"path":"test.ts"}',
}
yield {
type: "tool_call_partial" as const,
index: 0,
id: "toolu_dup123",
name: "read_file",
arguments: '{"path":"test.ts"}',
}
yield { type: "text" as const, text: "response" }
}

vi.spyOn(cline.api, "createMessage").mockReturnValue(mockStreamGenerator())

const iterator = cline.attemptApiRequest(0)
await iterator.next()

expect(cline).toBeDefined()
})

it("should handle tool_call_delta event", async () => {
const cline = new Task({
provider: mockProvider,
apiConfiguration: mockApiConfig,
task: "test task with delta",
startTask: false,
})

vi.spyOn(cline as any, "getSystemPrompt").mockResolvedValue("mock system prompt")

async function* mockStreamGenerator() {
yield { type: "tool_call_delta" as const, id: "toolu_delta123", delta: '{"path":"' }
yield { type: "text" as const, text: "response" }
}

vi.spyOn(cline.api, "createMessage").mockReturnValue(mockStreamGenerator())

const iterator = cline.attemptApiRequest(0)
await iterator.next()

expect(cline).toBeDefined()
})

it("should handle tool_call_end event", async () => {
const cline = new Task({
provider: mockProvider,
apiConfiguration: mockApiConfig,
task: "test task with end",
startTask: false,
})

vi.spyOn(cline as any, "getSystemPrompt").mockResolvedValue("mock system prompt")

async function* mockStreamGenerator() {
yield { type: "tool_call_end" as const, id: "toolu_end123" }
yield { type: "text" as const, text: "response" }
}

vi.spyOn(cline.api, "createMessage").mockReturnValue(mockStreamGenerator())

const iterator = cline.attemptApiRequest(0)
await iterator.next()

expect(cline).toBeDefined()
})

it("should handle complete streaming lifecycle: start -> delta -> end", async () => {
const cline = new Task({
provider: mockProvider,
apiConfiguration: mockApiConfig,
task: "test task with full lifecycle",
startTask: false,
})

vi.spyOn(cline as any, "getSystemPrompt").mockResolvedValue("mock system prompt")

async function* mockStreamGenerator() {
yield {
type: "tool_call_partial" as const,
index: 0,
id: "toolu_lifecycle123",
name: "read_file",
arguments: '{"path":"test.ts"}',
}
yield { type: "tool_call_delta" as const, id: "toolu_lifecycle123", delta: "more_args" }
yield { type: "tool_call_end" as const, id: "toolu_lifecycle123" }
yield { type: "text" as const, text: "Done" }
}

vi.spyOn(cline.api, "createMessage").mockReturnValue(mockStreamGenerator())

const iterator = cline.attemptApiRequest(0)
await iterator.next()

expect(cline).toBeDefined()
})

it("should handle multiple sequential tool calls", async () => {
const cline = new Task({
provider: mockProvider,
apiConfiguration: mockApiConfig,
task: "test task with multiple tools",
startTask: false,
})

vi.spyOn(cline as any, "getSystemPrompt").mockResolvedValue("mock system prompt")

async function* mockStreamGenerator() {
yield {
type: "tool_call_partial" as const,
index: 0,
id: "toolu_multi1",
name: "read_file",
arguments: '{"path":"file1.ts"}',
}
yield { type: "tool_call_end" as const, id: "toolu_multi1" }
yield {
type: "tool_call_partial" as const,
index: 1,
id: "toolu_multi2",
name: "write_to_file",
arguments: '{"path":"file2.ts","content":"hello"}',
}
yield { type: "tool_call_end" as const, id: "toolu_multi2" }
yield { type: "text" as const, text: "Done multiple tools" }
}

vi.spyOn(cline.api, "createMessage").mockReturnValue(mockStreamGenerator())

const iterator = cline.attemptApiRequest(0)
await iterator.next()

expect(cline).toBeDefined()
})
})

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 | 🏗️ Heavy lift

Task integration tests only assert expect(cline).toBeDefined(), not actual behavior.

Every test in this describe block (should handle tool_call_start..., duplicate tool_call_start events, tool_call_delta, tool_call_end, complete streaming lifecycle, multiple sequential tool calls) drives the stream and then only checks cline is defined — a condition already true before the stream is processed. None assert on cline.userMessageContent, assistantMessageContent, or internal streaming-tool-call state, so these tests can't catch regressions in the raw-chunk/tool-call routing they claim to cover (especially "should handle duplicate tool_call_start events", which is meant to exercise the compound-key dedup path this PR introduces).

Consider asserting on observable state after iterator.next(), e.g. the resulting assistantMessageContent/userMessageContent entries, or a spy on NativeToolCallParser/dedup-related internals, so these integration tests actually validate the streaming lifecycle instead of merely confirming the code doesn't throw.

As per coding guidelines, **/*.{test,spec}.{ts,tsx,js} should test "state transitions... and error handling" — these tests don't verify any state transition.

🤖 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 `@src/core/task/__tests__/Task.streaming-tool-calls.spec.ts` around lines 553 -
733, The Task streaming-tool-call integration tests only check that cline is
defined, which does not verify any state transition or parsing behavior. Update
the tests in Task.streaming-tool-calls.spec to assert observable outcomes after
attemptApiRequest(0) and iterator.next(), such as assistantMessageContent,
userMessageContent, or streaming-tool-call state, using the existing Task
instance and mock stream generators. For the duplicate tool_call_start and
lifecycle cases, add assertions that the dedup/path handling actually occurred,
or spy on NativeToolCallParser / related internal methods so the tests validate
the raw-chunk routing they are meant to cover.

Source: Coding guidelines

@github-actions github-actions Bot added the awaiting-review PR changes are ready and waiting for maintainer re-review label Jul 4, 2026
@easonLiangWorldedtech easonLiangWorldedtech marked this pull request as draft July 4, 2026 12:04
@github-actions github-actions Bot removed the awaiting-review PR changes are ready and waiting for maintainer re-review label Jul 4, 2026
- Add NativeToolCallParser tests for hasActiveStreamingToolCalls() and getStreamingToolName()

- Add openai-compatible tests for streamText multiple parts and usage handling

- Add Task integration tests for tool_call_start/delta/end lifecycle
@easonLiangWorldedtech easonLiangWorldedtech force-pushed the fix/shared-sdk-error-handle-gap branch from fdd3770 to 26ffe1d Compare July 5, 2026 07:09
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.

2 participants