Files
ars-backend/tests/Unit/RunLoopToolCallTest.php
Roog 71226c255b 添加新的工具功能和测试覆盖:
- 注册 `LsTool` 和 `BashTool` 工具,支持目录操作和命令执行
- 增强工具调用逻辑,添加日志记录以提升调试能力
- 增加 `ToolRegistry` 和 `RunLoop` 的增量累积与排序优化
- 完善单元测试覆盖新工具的执行与行为验证
2025-12-23 17:26:27 +08:00

162 lines
5.6 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Tests\Unit;
use App\Services\RunLoop;
use ReflectionClass;
use Tests\TestCase;
class RunLoopToolCallTest extends TestCase
{
/**
* 测试流式 tool call 增量正确累积(模拟 OpenAI 流式 API 行为)
*
* OpenAI 流式 API 返回 tool call 时:
* - 第一个 chunk 包含 id、name、index
* - 后续 chunks 只包含 arguments 增量和 index无 id
*/
public function test_accumulate_tool_calls_with_streaming_chunks(): void
{
$buffer = [];
$order = [];
// 模拟 OpenAI 流式返回的 tool call chunks
$chunks = [
// 第一个 chunk包含 id、name、index
[['id' => '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();
}
}