createMock(PendingToolConfirmationMapper::class); $mapper->expects($this->never())->method('findActiveForAction'); $service = new ToolConfirmationService($mapper); $this->assertFalse($service->consume('nuqloud_files_delete', [ 'path' => 'private.txt', ], [ 'user_id' => 'alice', 'bot_id' => 7, 'room_token' => 'room-a', 'user_message' => 'not yet', ])); } public function testConsumesMatchingPendingActionAfterShortAffirmativeReply(): void { $confirmation = new PendingToolConfirmation(); $confirmation->setId(42); $mapper = $this->createMock(PendingToolConfirmationMapper::class); $mapper->expects($this->once()) ->method('findActiveForAction') ->with( 'alice', 7, 'room-a', 'nuqloud_talk_create_conversation', hash('sha256', '{"name":"Project room","visibility":"private"}'), 101, $this->isType('int') ) ->willReturn($confirmation); $mapper->expects($this->once())->method('claim')->with($confirmation, $this->isType('int'))->willReturn(true); $service = new ToolConfirmationService($mapper); $this->assertTrue($service->consume('nuqloud_talk_create_conversation', [ 'name' => 'Project room', 'visibility' => 'private', ], [ 'user_id' => 'alice', 'bot_id' => 7, 'room_token' => 'room-a', 'talk_message_id' => 101, 'user_message' => 'yes create', ])); } public function testConsumesNaturalAffirmativeReply(): void { $confirmation = new PendingToolConfirmation(); $confirmation->setId(42); $mapper = $this->createMock(PendingToolConfirmationMapper::class); $mapper->expects($this->once())->method('findActiveForAction')->willReturn($confirmation); $mapper->expects($this->once())->method('claim')->with($confirmation, $this->isType('int'))->willReturn(true); $service = new ToolConfirmationService($mapper); $this->assertTrue($service->consume('nuqloud_files_delete', [ 'path' => 'private.txt', ], [ 'user_id' => 'alice', 'bot_id' => 7, 'room_token' => 'room-a', 'talk_message_id' => 101, 'user_message' => 'Affirmative, please delete it.', ])); } public function testRejectsContradictoryAffirmativeReply(): void { $mapper = $this->createMock(PendingToolConfirmationMapper::class); $mapper->expects($this->never())->method('findActiveForAction'); $service = new ToolConfirmationService($mapper); $this->assertFalse($service->consume('nuqloud_files_delete', [ 'path' => 'private.txt', ], [ 'user_id' => 'alice', 'bot_id' => 7, 'room_token' => 'room-a', 'talk_message_id' => 101, 'user_message' => "Yes, but don't delete it.", ])); } public function testDoesNotConsumeConfirmationWithoutTalkMessageId(): void { $mapper = $this->createMock(PendingToolConfirmationMapper::class); $mapper->expects($this->never())->method('claim'); $service = new ToolConfirmationService($mapper); $this->assertFalse($service->consume('nuqloud_talk_create_conversation', [ 'name' => 'Project room', ], [ 'user_id' => 'alice', 'bot_id' => 7, 'room_token' => 'room-a', 'user_message' => 'yes', ])); } public function testRequestStoresOnlyActionHashAndBoundIdentity(): void { $mapper = $this->createMock(PendingToolConfirmationMapper::class); $mapper->expects($this->once())->method('insert')->with($this->callback(function ($confirmation): bool { return $confirmation->getUserId() === 'alice' && $confirmation->getBotId() === 7 && $confirmation->getRoomToken() === 'room-a' && $confirmation->getToolName() === 'nuqloud_files_delete' && strlen($confirmation->getArgumentsHash()) === 64 && $confirmation->getExpiresAt() > time(); })); $service = new ToolConfirmationService($mapper); $result = $service->request('nuqloud_files_delete', ['path' => 'private.txt'], [ 'user_id' => 'alice', 'bot_id' => 7, 'room_token' => 'room-a', ]); $this->assertSame(600, $result['expires_in_seconds']); } }