'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(ManageSuggestionMisses::canAccess())->toBeFalse(); }); test('it renders and lists misses from the client', function () { Http::fake(['bnfexpress.test/*' => Http::response([ ['id' => 1, 'text_norm' => 'ev charging cost', 'lang' => 'en', 'syllables' => 3, 'was_used' => false, 'created_at' => now()->toIso8601String()], ])]); Livewire::test(ManageSuggestionMisses::class) ->assertOk() ->loadTable() ->assertSee('ev charging cost'); Http::assertSent(fn (Request $request) => str_contains($request->url(), '/admin/suggestion-misses') && $request->method() === 'GET'); }); test('dismiss calls dismissSuggestionMiss 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_norm' => 'to dismiss', 'lang' => 'en', 'syllables' => 2, 'was_used' => false, 'created_at' => now()->toIso8601String()], ]), }]); Livewire::test(ManageSuggestionMisses::class) ->loadTable() ->callTableAction('dismiss', 1) ->assertNotified('Miss dismissed'); Http::assertSent(fn (Request $request) => $request->method() === 'DELETE' && str_contains($request->url(), '/admin/suggestion-misses/1')); }); test('a failed dismiss surfaces the gateway detail message', function () { Http::fake(['bnfexpress.test/*' => fn (Request $request) => match ($request->method()) { 'DELETE' => Http::response(['detail' => 'Suggestion miss not found.'], 404), default => Http::response([ ['id' => 1, 'text_norm' => 'to dismiss', 'lang' => 'en', 'syllables' => 2, 'was_used' => false, 'created_at' => now()->toIso8601String()], ]), }]); Livewire::test(ManageSuggestionMisses::class) ->loadTable() ->callTableAction('dismiss', 1) ->assertNotified('Failed to dismiss miss'); }); test('promote bulk action prefills the bulk-add modal with the selected misses\' text and language', function () { Http::fake(['bnfexpress.test/*' => Http::response([ ['id' => 1, 'text_norm' => 'ev charging cost', 'lang' => 'my', 'syllables' => 3, 'was_used' => false, 'created_at' => now()->toIso8601String()], ['id' => 2, 'text_norm' => 'nearest charger', 'lang' => 'my', 'syllables' => 2, 'was_used' => false, 'created_at' => now()->toIso8601String()], ])]); Livewire::test(ManageSuggestionMisses::class) ->loadTable() ->mountTableBulkAction('promote', [1, 2]) ->assertTableBulkActionDataSet([ 'items_raw' => "ev charging cost\nnearest charger", 'lang' => 'my', ]); }); test('promote bulk action calls batchCreateSuggestions with the (possibly edited) phrases and shows the 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]), str_contains($request->url(), '/admin/suggestion-misses/batch') => Http::response(['deleted' => 2, 'skipped' => 0]), default => Http::response([ ['id' => 1, 'text_norm' => 'a', 'lang' => 'en', 'syllables' => 1, 'was_used' => false, 'created_at' => now()->toIso8601String()], ['id' => 2, 'text_norm' => 'b', 'lang' => 'en', 'syllables' => 1, 'was_used' => false, 'created_at' => now()->toIso8601String()], ]), }]); Livewire::test(ManageSuggestionMisses::class) ->loadTable() ->callTableBulkAction('promote', [1, 2], data: ['items_raw' => "a\nb\nc", 'lang' => 'my', 'intent' => null]) ->assertNotified('2 promoted, 0 skipped'); Http::assertSent(fn (Request $request) => str_contains($request->url(), '/admin/suggestions/batch') && $request['items'] === [ ['text' => 'a', 'lang' => 'my'], ['text' => 'b', 'lang' => 'my'], ['text' => 'c', 'lang' => 'my'], ]); Http::assertSent(fn (Request $request) => $request->method() === 'DELETE' && str_contains($request->url(), '/admin/suggestion-misses/batch') && $request['miss_ids'] === [1, 2]); }); test('promoting deletes the original misses after a successful promote', function () { Http::fake(['bnfexpress.test/*' => fn (Request $request) => match (true) { str_contains($request->url(), '/admin/suggestions/batch') => Http::response(['created' => 1, 'skipped' => 0, 'trie_rebuilt' => true]), str_contains($request->url(), '/admin/suggestion-misses/batch') => Http::response(['deleted' => 1, 'skipped' => 0]), default => Http::response([ ['id' => 1, 'text_norm' => 'ev charging cost', 'lang' => 'en', 'syllables' => 3, 'was_used' => false, 'created_at' => now()->toIso8601String()], ]), }]); Livewire::test(ManageSuggestionMisses::class) ->loadTable() ->callTableBulkAction('promote', [1], data: ['items_raw' => 'ev charging cost', 'lang' => 'en', 'intent' => null]) ->assertNotified('1 promoted, 0 skipped'); Http::assertSent(fn (Request $request) => $request->method() === 'DELETE' && str_contains($request->url(), '/admin/suggestion-misses/batch') && $request['miss_ids'] === [1]); }); test('a failed cleanup delete after a successful promote surfaces a separate warning notification', function () { Http::fake(['bnfexpress.test/*' => fn (Request $request) => match (true) { str_contains($request->url(), '/admin/suggestions/batch') => Http::response(['created' => 1, 'skipped' => 0, 'trie_rebuilt' => true]), str_contains($request->url(), '/admin/suggestion-misses/batch') => Http::response(['detail' => 'boom'], 500), default => Http::response([ ['id' => 1, 'text_norm' => 'ev charging cost', 'lang' => 'en', 'syllables' => 3, 'was_used' => false, 'created_at' => now()->toIso8601String()], ]), }]); // assertNotified() pulls (and clears) every queued notification on its // first call, so only one chained call can see anything — check the // warning here; the success title is covered by the other promote tests. Livewire::test(ManageSuggestionMisses::class) ->loadTable() ->callTableBulkAction('promote', [1], data: ['items_raw' => 'ev charging cost', 'lang' => 'en', 'intent' => null]) ->assertNotified('Promoted, but failed to remove the original misses'); });