createSession('Run Session'); $prompt = $service->appendMessage([ 'session_id' => $session->session_id, 'role' => Message::ROLE_USER, 'type' => 'user.prompt', 'content' => 'hello agent', ]); $runId = $dispatcher->dispatchForPrompt($session->session_id, $prompt->message_id); Queue::assertPushed(AgentRunJob::class, function ($job) use ($session, $runId) { return $job->sessionId === $session->session_id && $job->runId === $runId; }); // simulate worker execution (new AgentRunJob($session->session_id, $runId))->handle( app(RunLoop::class), app(OutputSink::class) ); $messages = Message::query() ->where('session_id', $session->session_id) ->orderBy('seq') ->get(); $this->assertTrue($messages->contains(fn ($m) => $m->type === 'run.status' && ($m->payload['status'] ?? null) === 'RUNNING')); $this->assertTrue($messages->contains(fn ($m) => $m->type === 'agent.message' && ($m->payload['run_id'] ?? null) === $runId)); $this->assertTrue($messages->contains(fn ($m) => $m->type === 'run.status' && ($m->payload['status'] ?? null) === 'DONE')); } public function test_second_prompt_dispatches_new_run_after_first_completes(): void { Queue::fake(); $service = app(ChatService::class); $dispatcher = app(RunDispatcher::class); $session = $service->createSession('Sequential Runs'); $firstPrompt = $service->appendMessage([ 'session_id' => $session->session_id, 'role' => Message::ROLE_USER, 'type' => 'user.prompt', 'content' => 'first run', ]); $firstRunId = $dispatcher->dispatchForPrompt($session->session_id, $firstPrompt->message_id); (new AgentRunJob($session->session_id, $firstRunId))->handle( app(RunLoop::class), app(OutputSink::class) ); $secondPrompt = $service->appendMessage([ 'session_id' => $session->session_id, 'role' => Message::ROLE_USER, 'type' => 'user.prompt', 'content' => 'second run', ]); $secondRunId = $dispatcher->dispatchForPrompt($session->session_id, $secondPrompt->message_id); $this->assertNotSame($firstRunId, $secondRunId); Queue::assertPushed(AgentRunJob::class, 2); Queue::assertPushed(AgentRunJob::class, function ($job) use ($secondRunId, $session) { return $job->runId === $secondRunId && $job->sessionId === $session->session_id; }); } public function test_cancel_prevents_agent_reply_and_marks_canceled(): void { Queue::fake(); $service = app(ChatService::class); $dispatcher = app(RunDispatcher::class); $loop = app(RunLoop::class); $session = $service->createSession('Cancel Session'); $prompt = $service->appendMessage([ 'session_id' => $session->session_id, 'role' => Message::ROLE_USER, 'type' => 'user.prompt', 'content' => 'please cancel', ]); $runId = $dispatcher->dispatchForPrompt($session->session_id, $prompt->message_id); $service->appendMessage([ 'session_id' => $session->session_id, 'role' => Message::ROLE_USER, 'type' => 'run.cancel.request', 'payload' => ['run_id' => $runId], ]); $loop->run($session->session_id, $runId); $messages = Message::query() ->where('session_id', $session->session_id) ->get(); $this->assertFalse($messages->contains(fn ($m) => $m->type === 'agent.message' && ($m->payload['run_id'] ?? null) === $runId)); $this->assertTrue($messages->contains(fn ($m) => $m->type === 'run.status' && ($m->payload['status'] ?? null) === 'CANCELED')); } }