Add bnfexpress signed admin client and AI Agent Filament UI

- Modules\Shared\Bnfexpress\BnfexpressAdminClient: HMAC-signed HTTP client
  for bnfexpress's admin API (EV FAQs, agent instructions, chat history),
  with a bnfexpress:smoke-test command and full unit coverage.
- New ai-agent module: Filament pages to manage EV FAQs, publish/roll back
  agent instruction versions, and browse EV chat history + transcripts.
- New manage_ai_agent permission (super_admin/admin).
- Recorded .ai/rules for the client's auth scheme and non-Resource
  Filament page/table testing gotchas.
This commit is contained in:
Nyan Lin Paing
2026-08-30 23:52:21 +07:00
parent 95b369174d
commit b8d31e3dc4
30 changed files with 1362 additions and 1 deletions
@@ -0,0 +1,97 @@
<?php
use App\Models\User;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;
use Livewire\Livewire;
use Modules\AiAgent\Filament\Pages\ManageAgentInstructions;
use Spatie\Permission\Models\Permission;
beforeEach(function () {
config([
'services.bnfexpress.ai_api_url' => 'https://bnfexpress.test',
'services.bnfexpress.client_id' => 'ev_admin',
'services.bnfexpress.client_secret' => 'test-secret',
]);
Permission::findOrCreate('manage_ai_agent', 'web');
$this->admin = User::factory()->create()->givePermissionTo(['manage_ai_agent']);
$this->actingAs($this->admin);
});
test('a user without manage_ai_agent cannot access it', function () {
$this->actingAs(User::factory()->create());
expect(ManageAgentInstructions::canAccess())->toBeFalse();
});
test('it renders the active instruction and version history', function () {
Http::fake([
'bnfexpress.test/admin/agent-instructions/active*' => Http::response(['id' => 2, 'content' => 'You are the EV assistant.']),
'bnfexpress.test/admin/agent-instructions*' => Http::response([
'total' => 1,
'instructions' => [['id' => 2, 'is_active' => true, 'content' => 'You are the EV assistant.', 'created_at' => now()->toIso8601String()]],
]),
]);
Livewire::test(ManageAgentInstructions::class)
->assertOk()
->assertSee('You are the EV assistant.')
->loadTable()
->assertSee('You are the EV assistant.');
});
test('publishing a new version calls publishInstruction and shows a success notification', function () {
Http::fake([
'bnfexpress.test/admin/agent-instructions/active*' => Http::response(['id' => 3, 'content' => 'New instructions.']),
'bnfexpress.test/admin/agent-instructions*' => fn (Request $request) => match ($request->method()) {
'POST' => Http::response(['id' => 3, 'content' => 'New instructions.', 'is_active' => true], 201),
default => Http::response(['total' => 0, 'instructions' => []]),
},
]);
Livewire::test(ManageAgentInstructions::class)
->loadTable()
->callTableAction('publish', data: ['content' => 'New instructions.', 'activate' => true])
->assertNotified('New instruction version published');
Http::assertSent(fn (Request $request) => $request->method() === 'POST'
&& str_contains($request->url(), '/admin/agent-instructions')
&& ! str_contains($request->url(), '/active')
&& $request['content'] === 'New instructions.'
&& $request['activate'] === true);
});
test('a failed publish surfaces the gateway detail message', function () {
Http::fake([
'bnfexpress.test/admin/agent-instructions/active*' => Http::response(['id' => 1, 'content' => 'Old instructions.']),
'bnfexpress.test/admin/agent-instructions*' => fn (Request $request) => match ($request->method()) {
'POST' => Http::response(['detail' => 'Content is required.'], 422),
default => Http::response(['total' => 0, 'instructions' => []]),
},
]);
Livewire::test(ManageAgentInstructions::class)
->loadTable()
->callTableAction('publish', data: ['content' => 'New instructions.', 'activate' => true])
->assertNotified('Failed to publish instruction');
});
test('activate is hidden on the already-active row and visible on others', function () {
Http::fake([
'bnfexpress.test/admin/agent-instructions/active*' => Http::response(['id' => 2, 'content' => 'Current.']),
'bnfexpress.test/admin/agent-instructions*' => Http::response([
'total' => 2,
'instructions' => [
['id' => 2, 'is_active' => true, 'content' => 'Current.', 'created_at' => now()->toIso8601String()],
['id' => 1, 'is_active' => false, 'content' => 'Older.', 'created_at' => now()->toIso8601String()],
],
]),
]);
Livewire::test(ManageAgentInstructions::class)
->loadTable()
->assertTableActionHidden('activate', 2)
->assertTableActionVisible('activate', 1);
});