添加新的工具功能和测试覆盖:
- 注册 `LsTool` 和 `BashTool` 工具,支持目录操作和命令执行 - 增强工具调用逻辑,添加日志记录以提升调试能力 - 增加 `ToolRegistry` 和 `RunLoop` 的增量累积与排序优化 - 完善单元测试覆盖新工具的执行与行为验证
This commit is contained in:
161
tests/Unit/RunLoopToolCallTest.php
Normal file
161
tests/Unit/RunLoopToolCallTest.php
Normal file
@@ -0,0 +1,161 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user