'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(ManageSuggestions::canAccess())->toBeFalse(); }); test('it renders and lists suggestions from the client', function () { Http::fake(['bnfexpress.test/*' => Http::response([ ['id' => 1, 'text_display' => 'How do I charge my EV?', 'lang' => 'en', 'intent' => null, 'weight' => 0, 'source' => 'manual', 'updated_at' => now()->toIso8601String()], ])]); Livewire::test(ManageSuggestions::class) ->assertOk() ->loadTable() ->assertSee('How do I charge my EV?'); Http::assertSent(fn (Request $request) => str_contains($request->url(), '/admin/suggestions') && $request->method() === 'GET'); }); test('creating a suggestion calls createSuggestion and shows a success notification', function () { Http::fake(['bnfexpress.test/*' => fn (Request $request) => match ($request->method()) { 'POST' => Http::response(['id' => 1], 201), default => Http::response([]), }]); Livewire::test(ManageSuggestions::class) ->loadTable() ->callTableAction('create', data: ['text_display' => 'New phrase', 'lang' => 'en', 'intent' => null, 'weight' => 0, 'source' => 'admin']) ->assertNotified('Suggestion created'); Http::assertSent(fn (Request $request) => $request->method() === 'POST' && $request->url() === 'https://bnfexpress.test/admin/suggestions' && $request['text_display'] === 'New phrase'); }); test('a failed create surfaces the gateway detail message', function () { Http::fake(['bnfexpress.test/*' => fn (Request $request) => match ($request->method()) { 'POST' => Http::response(['detail' => 'text_display is required.'], 422), default => Http::response([]), }]); Livewire::test(ManageSuggestions::class) ->loadTable() ->callTableAction('create', data: ['text_display' => 'New phrase', 'lang' => 'en', 'intent' => null, 'weight' => 0, 'source' => 'admin']) ->assertNotified('Failed to create suggestion'); }); test('createMany splits pasted lines into batch items and shows the created/skipped result', function () { Http::fake(['bnfexpress.test/*' => fn (Request $request) => match (true) { str_contains($request->url(), '/admin/suggestions/batch') => Http::response(['created' => 2, 'skipped' => 0, 'trie_rebuilt' => true]), default => Http::response([]), }]); Livewire::test(ManageSuggestions::class) ->loadTable() ->callTableAction('createMany', data: ['items_raw' => "First phrase\nSecond phrase\n\n", 'lang' => 'en', 'intent' => null]) ->assertNotified('2 created, 0 skipped'); Http::assertSent(fn (Request $request) => str_contains($request->url(), '/admin/suggestions/batch') && $request['items'] === [ ['text' => 'First phrase', 'lang' => 'en'], ['text' => 'Second phrase', 'lang' => 'en'], ]); }); test('deleting a suggestion calls deleteSuggestion and shows a success notification', function () { Http::fake(['bnfexpress.test/*' => fn (Request $request) => match ($request->method()) { 'DELETE' => Http::response([]), default => Http::response([ ['id' => 1, 'text_display' => 'To delete', 'lang' => 'en', 'intent' => null, 'weight' => 0, 'source' => 'manual', 'updated_at' => now()->toIso8601String()], ]), }]); Livewire::test(ManageSuggestions::class) ->loadTable() ->callTableAction('delete', 1) ->assertNotified('Suggestion deleted'); Http::assertSent(fn (Request $request) => $request->method() === 'DELETE' && str_contains($request->url(), '/admin/suggestions/1')); }); test('deleteSelected bulk action calls batchDeleteSuggestions with just the selected ids', function () { Http::fake(['bnfexpress.test/*' => fn (Request $request) => match (true) { $request->method() === 'DELETE' && str_contains($request->url(), '/admin/suggestions/batch') => Http::response(['deleted' => 2, 'skipped' => 0]), default => Http::response([ ['id' => 1, 'text_display' => 'A', 'lang' => 'en', 'intent' => null, 'weight' => 0, 'source' => 'manual', 'updated_at' => now()->toIso8601String()], ['id' => 2, 'text_display' => 'B', 'lang' => 'en', 'intent' => null, 'weight' => 0, 'source' => 'manual', 'updated_at' => now()->toIso8601String()], ]), }]); Livewire::test(ManageSuggestions::class) ->loadTable() ->callTableBulkAction('deleteSelected', [1, 2]) ->assertNotified('2 deleted, 0 skipped'); Http::assertSent(fn (Request $request) => $request->method() === 'DELETE' && str_contains($request->url(), '/admin/suggestions/batch') && $request['ids'] === [1, 2]); }); test('sync sets job state and polling chains a reload-index call on finished', function () { Http::fake(['bnfexpress.test/*' => fn (Request $request) => match (true) { str_contains($request->url(), '/sync-chroma/job-1') => Http::response(['status' => 'finished', 'result' => ['synced' => 3]]), str_contains($request->url(), '/sync-chroma') => Http::response(['job_id' => 'job-1'], 202), str_contains($request->url(), '/reload-index') => Http::response(['trie_rebuilt' => true]), default => Http::response([]), }]); $test = Livewire::test(ManageSuggestions::class) ->loadTable() ->callTableAction('sync') ->assertSet('syncJobId', 'job-1') ->assertSet('syncStatus', 'queued'); $test->call('pollSyncStatus') ->assertSet('syncJobId', null) ->assertSet('syncStatus', 'finished') ->assertNotified('Sync completed and index reloaded'); Http::assertSent(fn (Request $request) => str_contains($request->url(), '/admin/suggestions/reload-index')); }); test('a failed sync job notifies danger and stops polling', function () { Http::fake(['bnfexpress.test/*' => fn (Request $request) => match (true) { str_contains($request->url(), '/sync-chroma/job-1') => Http::response(['status' => 'failed', 'result' => null]), str_contains($request->url(), '/sync-chroma') => Http::response(['job_id' => 'job-1'], 202), default => Http::response([]), }]); Livewire::test(ManageSuggestions::class) ->loadTable() ->callTableAction('sync') ->call('pollSyncStatus') ->assertSet('syncJobId', null) ->assertNotified('Sync failed'); }); test('reloadIndex calls reloadSuggestionIndex directly', function () { Http::fake(['bnfexpress.test/*' => Http::response(['trie_rebuilt' => true])]); Livewire::test(ManageSuggestions::class) ->loadTable() ->callTableAction('reloadIndex') ->assertNotified('Index reloaded'); Http::assertSent(fn (Request $request) => str_contains($request->url(), '/admin/suggestions/reload-index')); }); test('syncEmbedding and deleteEmbedding row actions call the right per-id endpoint', function () { Http::fake(['bnfexpress.test/*' => fn (Request $request) => match (true) { str_contains($request->url(), '/1/sync-chroma') => Http::response(['synced' => true]), str_contains($request->url(), '/1/chroma') => Http::response([]), default => Http::response([ ['id' => 1, 'text_display' => 'A', 'lang' => 'en', 'intent' => null, 'weight' => 0, 'source' => 'manual', 'updated_at' => now()->toIso8601String()], ]), }]); $test = Livewire::test(ManageSuggestions::class)->loadTable(); $test->callTableAction('syncEmbedding', 1)->assertNotified('Embedding synced'); $test->callTableAction('deleteEmbedding', 1)->assertNotified('Embedding deleted'); Http::assertSent(fn (Request $request) => str_contains($request->url(), '/admin/suggestions/1/sync-chroma') && $request->method() === 'POST'); Http::assertSent(fn (Request $request) => str_contains($request->url(), '/admin/suggestions/1/chroma') && $request->method() === 'DELETE'); });