'tooluse_ABC123', 'name' => 'ls', 'arguments' => '', 'index' => 0]], // 后续 chunks:只有 arguments 增量和 index(无 id) [['id' => null, 'name' => null, 'arguments' => '{"direc', 'index' => 0]], [['id' => null, 'name' => null, 'arguments' => 'tory', 'index' => 0]], [['id' => null, 'name' => null, 'arguments' => '": "/h', 'index' => 0]], [['id' => null, 'name' => null, 'arguments' => 'ome"}', 'index' => 0]], ]; foreach ($chunks as $toolCalls) { $this->invokeAccumulateToolCalls($buffer, $order, $toolCalls); } // 验证只有一个 tool call $this->assertCount(1, $buffer); $this->assertArrayHasKey(0, $buffer); // 验证 id 和 name 正确保留 $this->assertSame('tooluse_ABC123', $buffer[0]['id']); $this->assertSame('ls', $buffer[0]['name']); // 验证 arguments 正确拼接 $this->assertSame('{"directory": "/home"}', $buffer[0]['arguments']); } /** * 测试多个并行 tool calls 的累积 */ public function test_accumulate_multiple_parallel_tool_calls(): void { $buffer = []; $order = []; // 模拟两个并行的 tool calls $chunks = [ // 第一个 tool call 的第一个 chunk [['id' => 'call_1', 'name' => 'ls', 'arguments' => '', 'index' => 0]], // 第二个 tool call 的第一个 chunk [['id' => 'call_2', 'name' => 'cat', 'arguments' => '', 'index' => 1]], // 第一个 tool call 的 arguments [['id' => null, 'name' => null, 'arguments' => '{"path": "/home"}', 'index' => 0]], // 第二个 tool call 的 arguments [['id' => null, 'name' => null, 'arguments' => '{"file": "test.txt"}', 'index' => 1]], ]; foreach ($chunks as $toolCalls) { $this->invokeAccumulateToolCalls($buffer, $order, $toolCalls); } // 验证有两个 tool calls $this->assertCount(2, $buffer); // 验证第一个 tool call $this->assertSame('call_1', $buffer[0]['id']); $this->assertSame('ls', $buffer[0]['name']); $this->assertSame('{"path": "/home"}', $buffer[0]['arguments']); // 验证第二个 tool call $this->assertSame('call_2', $buffer[1]['id']); $this->assertSame('cat', $buffer[1]['name']); $this->assertSame('{"file": "test.txt"}', $buffer[1]['arguments']); } /** * 测试 finalizeToolCalls 正确整理结果 */ public function test_finalize_tool_calls(): void { $buffer = [ 0 => ['id' => 'call_1', 'name' => 'ls', 'arguments' => '{"path": "/"}', 'index' => 0], 1 => ['id' => 'call_2', 'name' => 'cat', 'arguments' => '{"file": "a.txt"}', 'index' => 1], ]; $order = [0 => 0, 1 => 1]; $result = $this->invokeFinalizeToolCalls($buffer, $order, 'tool_calls'); $this->assertCount(2, $result); $this->assertSame('call_1', $result[0]['id']); $this->assertSame('ls', $result[0]['name']); $this->assertSame('{"path": "/"}', $result[0]['arguments']); $this->assertSame('tool_calls', $result[0]['finish_reason']); $this->assertSame('call_2', $result[1]['id']); $this->assertSame('cat', $result[1]['name']); } /** * 测试空 buffer 返回空数组 */ public function test_finalize_empty_buffer(): void { $result = $this->invokeFinalizeToolCalls([], [], 'stop'); $this->assertSame([], $result); } /** * 测试缺少 index 时默认使用 0 */ public function test_accumulate_without_index_defaults_to_zero(): void { $buffer = []; $order = []; $this->invokeAccumulateToolCalls($buffer, $order, [ ['id' => 'call_1', 'name' => 'test', 'arguments' => '{}'], ]); $this->assertCount(1, $buffer); $this->assertArrayHasKey(0, $buffer); $this->assertSame('call_1', $buffer[0]['id']); } private function invokeAccumulateToolCalls(array &$buffer, array &$order, array $toolCalls): void { $runLoop = $this->createRunLoopMock(); $method = (new ReflectionClass(RunLoop::class))->getMethod('accumulateToolCalls'); $method->setAccessible(true); $method->invokeArgs($runLoop, [&$buffer, &$order, $toolCalls]); } private function invokeFinalizeToolCalls(array $buffer, array $order, ?string $doneReason): array { $runLoop = $this->createRunLoopMock(); $method = (new ReflectionClass(RunLoop::class))->getMethod('finalizeToolCalls'); $method->setAccessible(true); return $method->invokeArgs($runLoop, [$buffer, $order, $doneReason]); } private function createRunLoopMock(): RunLoop { return $this->getMockBuilder(RunLoop::class) ->disableOriginalConstructor() ->getMock(); } }