'Bearer '.JWTAuth::fromUser($user)]; } public function test_append_messages_sequential_seq_and_last_seq(): void { $service = app(ChatService::class); $session = $service->createSession('Test Session'); $messages = collect(range(1, 20))->map(function ($i) use ($service, $session) { return $service->appendMessage([ 'session_id' => $session->session_id, 'role' => 'USER', 'type' => 'user.prompt', 'content' => 'msg '.$i, ]); }); $seqs = $messages->pluck('seq')->all(); $this->assertEquals(range(1, 20), $seqs); $session->refresh(); $this->assertEquals(20, $session->last_seq); } public function test_dedupe_returns_existing_message(): void { $service = app(ChatService::class); $session = $service->createSession('Test Session'); $first = $service->appendMessage([ 'session_id' => $session->session_id, 'role' => 'USER', 'type' => 'user.prompt', 'content' => 'hello', 'dedupe_key' => 'k1', ]); $second = $service->appendMessage([ 'session_id' => $session->session_id, 'role' => 'USER', 'type' => 'user.prompt', 'content' => 'hello again', 'dedupe_key' => 'k1', ]); $this->assertEquals($first->message_id, $second->message_id); $this->assertEquals($first->seq, $second->seq); $this->assertCount(1, $session->messages()->get()); } public function test_closed_session_blocks_append_except_whitelisted(): void { $service = app(ChatService::class); $session = $service->createSession('Test Session'); $session->update(['status' => ChatSessionStatus::CLOSED]); $this->expectExceptionMessage('Session is closed'); $service->appendMessage([ 'session_id' => $session->session_id, 'role' => 'USER', 'type' => 'user.prompt', 'content' => 'blocked', ]); } public function test_http_append_and_list_messages(): void { $user = User::factory()->create(); $headers = $this->authHeader($user); $createSession = $this->withHeaders($headers)->postJson('/api/sessions', [ 'session_name' => 'API Session', ])->assertCreated(); $sessionId = $createSession->json('data.session_id'); $append = $this->withHeaders($headers)->postJson("/api/sessions/{$sessionId}/messages", [ 'role' => 'USER', 'type' => 'user.prompt', 'content' => 'hello api', ])->assertCreated(); $this->assertEquals(1, $append->json('data.seq')); $list = $this->withHeaders($headers)->getJson("/api/sessions/{$sessionId}/messages?after_seq=0&limit=10") ->assertOk(); $this->assertEquals(1, $list->json('data.0.seq')); $this->assertEquals('hello api', $list->json('data.0.content')); } public function test_session_list_sorted_and_last_message_preview(): void { $user = User::factory()->create(); $headers = $this->authHeader($user); $service = app(ChatService::class); Carbon::setTestNow('2025-01-01 00:00:00'); $s1 = $service->createSession('First'); $service->appendMessage([ 'session_id' => $s1->session_id, 'role' => 'USER', 'type' => 'user.prompt', 'content' => 'hello first', ]); Carbon::setTestNow('2025-01-01 00:00:10'); $s2 = $service->createSession('Second'); $service->appendMessage([ 'session_id' => $s2->session_id, 'role' => 'USER', 'type' => 'user.prompt', 'content' => 'hello second', ]); Carbon::setTestNow(); $resp = $this->withHeaders($headers)->getJson('/api/sessions?per_page=10') ->assertOk(); $this->assertEquals($s2->session_id, $resp->json('data.0.session_id')); $this->assertEquals('hello second', $resp->json('data.0.last_message_preview')); $this->assertEquals('hello first', $resp->json('data.1.last_message_preview')); $this->assertNotNull($resp->json('data.0.last_message_at')); } public function test_session_list_filters_status_and_query(): void { $user = User::factory()->create(); $headers = $this->authHeader($user); $service = app(ChatService::class); $open = $service->createSession('Alpha'); $locked = $service->createSession('Beta'); $locked->update(['status' => ChatSessionStatus::LOCKED]); $closed = $service->createSession('Gamma'); $closed->update(['status' => ChatSessionStatus::CLOSED]); $respStatus = $this->withHeaders($headers)->getJson('/api/sessions?status=LOCKED') ->assertOk(); $this->assertCount(1, $respStatus->json('data')); $this->assertEquals($locked->session_id, $respStatus->json('data.0.session_id')); $respQuery = $this->withHeaders($headers)->getJson('/api/sessions?q=Alpha') ->assertOk(); $this->assertCount(1, $respQuery->json('data')); $this->assertEquals($open->session_id, $respQuery->json('data.0.session_id')); } public function test_patch_updates_session_name(): void { $user = User::factory()->create(); $headers = $this->authHeader($user); $service = app(ChatService::class); $session = $service->createSession('Old Name'); $resp = $this->withHeaders($headers)->patchJson("/api/sessions/{$session->session_id}", [ 'session_name' => 'New Name', ])->assertOk(); $this->assertEquals('New Name', $resp->json('data.session_name')); } public function test_patch_updates_status_transitions(): void { $user = User::factory()->create(); $headers = $this->authHeader($user); $service = app(ChatService::class); $session = $service->createSession('Status Session'); $this->withHeaders($headers)->patchJson("/api/sessions/{$session->session_id}", [ 'status' => ChatSessionStatus::LOCKED, ])->assertOk()->assertJsonFragment(['status' => ChatSessionStatus::LOCKED]); $this->withHeaders($headers)->patchJson("/api/sessions/{$session->session_id}", [ 'status' => ChatSessionStatus::OPEN, ])->assertOk()->assertJsonFragment(['status' => ChatSessionStatus::OPEN]); } public function test_closed_session_cannot_reopen(): void { $user = User::factory()->create(); $headers = $this->authHeader($user); $service = app(ChatService::class); $session = $service->createSession('Closed Session'); $session->update(['status' => ChatSessionStatus::CLOSED]); $this->withHeaders($headers)->patchJson("/api/sessions/{$session->session_id}", [ 'status' => ChatSessionStatus::OPEN, ])->assertStatus(403); } public function test_empty_patch_rejected(): void { $user = User::factory()->create(); $headers = $this->authHeader($user); $service = app(ChatService::class); $session = $service->createSession('Patch Empty'); $this->withHeaders($headers)->patchJson("/api/sessions/{$session->session_id}", []) ->assertStatus(422); } }