main: 增强会话功能,支持归档与消息检索

- 添加会话归档接口及相关服务逻辑,并确保幂等性
- 实现单条消息获取接口,校验消息所属会话
- 增加 SSE 增量推送与实时消息订阅功能
- 提供相关的测试用例覆盖新功能
- 更新接口文档,完善 OpenAPI 规范,新增多项示例
This commit is contained in:
2025-12-14 21:58:05 +08:00
parent 6356baacc0
commit 318571a6d9
7 changed files with 531 additions and 47 deletions

View File

@@ -7,6 +7,7 @@ use App\Models\User;
use App\Services\ChatService;
use Illuminate\Support\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Redis;
use PHPOpenSourceSaver\JWTAuth\Facades\JWTAuth;
use Tests\TestCase;
@@ -218,4 +219,91 @@ class ChatSessionTest extends TestCase
$this->withHeaders($headers)->patchJson("/api/sessions/{$session->session_id}", [])
->assertStatus(422);
}
public function test_archive_is_idempotent_and_blocks_user_prompt(): void
{
$user = User::factory()->create();
$headers = $this->authHeader($user);
$service = app(ChatService::class);
$session = $service->createSession('Archive');
$this->withHeaders($headers)->postJson("/api/sessions/{$session->session_id}/archive")
->assertOk()
->assertJsonFragment(['status' => ChatSessionStatus::CLOSED]);
// repeat archive
$this->withHeaders($headers)->postJson("/api/sessions/{$session->session_id}/archive")
->assertOk()
->assertJsonFragment(['status' => ChatSessionStatus::CLOSED]);
// append should be blocked
$this->withHeaders($headers)->postJson("/api/sessions/{$session->session_id}/messages", [
'role' => 'USER',
'type' => 'user.prompt',
'content' => 'blocked',
])->assertStatus(403);
}
public function test_get_message_respects_session_scope(): void
{
$user = User::factory()->create();
$headers = $this->authHeader($user);
$service = app(ChatService::class);
$s1 = $service->createSession('S1');
$s2 = $service->createSession('S2');
$msg1 = $service->appendMessage([
'session_id' => $s1->session_id,
'role' => 'USER',
'type' => 'user.prompt',
'content' => 'hello',
]);
$this->withHeaders($headers)->getJson("/api/sessions/{$s1->session_id}/messages/{$msg1->message_id}")
->assertOk()
->assertJsonFragment(['message_id' => $msg1->message_id]);
// wrong session should 404
$this->withHeaders($headers)->getJson("/api/sessions/{$s2->session_id}/messages/{$msg1->message_id}")
->assertNotFound();
}
public function test_publish_to_redis_on_append(): void
{
Redis::shouldReceive('publish')->once()->andReturn(1);
$service = app(ChatService::class);
$session = $service->createSession('Redis Pub');
$service->appendMessage([
'session_id' => $session->session_id,
'role' => 'USER',
'type' => 'user.prompt',
'content' => 'hello',
]);
}
public function test_sse_backlog_contains_messages(): void
{
$user = User::factory()->create();
$headers = $this->authHeader($user);
$service = app(ChatService::class);
$session = $service->createSession('SSE Session');
$service->appendMessage([
'session_id' => $session->session_id,
'role' => 'USER',
'type' => 'user.prompt',
'content' => 'hello sse',
]);
$response = $this->withHeaders($headers)->get("/api/sessions/{$session->session_id}/sse?after_seq=0");
$response->assertOk();
$content = $response->baseResponse instanceof \Symfony\Component\HttpFoundation\StreamedResponse
? $response->streamedContent()
: $response->getContent();
$this->assertStringContainsString('id: 1', $content);
$this->assertStringContainsString('event: message', $content);
$this->assertStringContainsString('hello sse', $content);
}
}