@@ -295,6 +295,230 @@ describe("Prompts", () => {
295295 expect ( screen . getByRole ( "menuitem" , { name : "Delete" } ) ) . toBeInTheDocument ( ) ;
296296 } ) ;
297297
298+ it ( "deletes the sole prompt in a group and closes the drawer after confirmation" , async ( ) => {
299+ const user = userEvent . setup ( ) ;
300+ let remaining : Prompt [ ] = [
301+ createMockPrompt ( {
302+ id : "prompt-1" ,
303+ name : "summarize" ,
304+ displayName : "Summarize document" ,
305+ gatewayId : "gw-github" ,
306+ gatewaySlug : "gh-repo-tasks" ,
307+ } ) ,
308+ ] ;
309+
310+ const deleteSpy = vi . fn ( ( ) => {
311+ remaining = remaining . filter ( ( p ) => p . id !== "prompt-1" ) ;
312+ return HttpResponse . json ( { status : "success" } ) ;
313+ } ) ;
314+ server . use (
315+ http . get ( "/prompts" , ( ) => HttpResponse . json ( remaining ) ) ,
316+ http . delete ( "/prompts/prompt-1" , deleteSpy ) ,
317+ ) ;
318+
319+ renderWithProviders ( < Prompts /> ) ;
320+
321+ await waitFor ( ( ) => {
322+ expect ( screen . getByText ( "gh-repo-tasks" ) ) . toBeInTheDocument ( ) ;
323+ } ) ;
324+
325+ // Open the group's details panel and its Definition tab.
326+ await user . click ( screen . getByRole ( "button" , { name : "More options for gh-repo-tasks" } ) ) ;
327+ await user . click ( await screen . findByRole ( "menuitem" , { name : "View details" } ) ) ;
328+ await user . click ( await screen . findByRole ( "tab" , { name : / d e f i n i t i o n / i } ) ) ;
329+
330+ // Trigger delete from the row overflow menu.
331+ await user . click ( await screen . findByRole ( "button" , { name : / m o r e o p t i o n s f o r s u m m a r i z e / i } ) ) ;
332+ await user . click ( await screen . findByRole ( "menuitem" , { name : "Delete" } ) ) ;
333+
334+ // Confirm dialog names the prompt and requires an explicit confirmation.
335+ const dialog = await screen . findByRole ( "dialog" ) ;
336+ expect ( within ( dialog ) . getByText ( "Delete prompt" ) ) . toBeInTheDocument ( ) ;
337+ expect (
338+ within ( dialog ) . getByText ( / A r e y o u s u r e y o u w a n t t o d e l e t e " S u m m a r i z e d o c u m e n t " / ) ,
339+ ) . toBeInTheDocument ( ) ;
340+
341+ await user . click ( within ( dialog ) . getByRole ( "button" , { name : / ^ d e l e t e $ / i } ) ) ;
342+
343+ await waitFor ( ( ) => {
344+ expect ( deleteSpy ) . toHaveBeenCalledTimes ( 1 ) ;
345+ } ) ;
346+
347+ // The group's only prompt is gone, so its grid card (and the card-only
348+ // "More options" trigger) disappears.
349+ await waitFor ( ( ) => {
350+ expect (
351+ screen . queryByRole ( "button" , { name : "More options for gh-repo-tasks" } ) ,
352+ ) . not . toBeInTheDocument ( ) ;
353+ } ) ;
354+ } ) ;
355+
356+ it ( "keeps the drawer and other rows when deleting one prompt from a multi-prompt group" , async ( ) => {
357+ const user = userEvent . setup ( ) ;
358+ let remaining : Prompt [ ] = [
359+ createMockPrompt ( {
360+ id : "prompt-1" ,
361+ name : "summarize" ,
362+ displayName : "Summarize document" ,
363+ gatewayId : "gw-github" ,
364+ gatewaySlug : "gh-repo-tasks" ,
365+ } ) ,
366+ createMockPrompt ( {
367+ id : "prompt-2" ,
368+ name : "translate" ,
369+ displayName : "Translate document" ,
370+ gatewayId : "gw-github" ,
371+ gatewaySlug : "gh-repo-tasks" ,
372+ } ) ,
373+ ] ;
374+
375+ server . use (
376+ http . get ( "/prompts" , ( ) => HttpResponse . json ( remaining ) ) ,
377+ http . delete ( "/prompts/prompt-1" , ( ) => {
378+ remaining = remaining . filter ( ( p ) => p . id !== "prompt-1" ) ;
379+ return HttpResponse . json ( { status : "success" } ) ;
380+ } ) ,
381+ ) ;
382+
383+ renderWithProviders ( < Prompts /> ) ;
384+
385+ await waitFor ( ( ) => {
386+ expect ( screen . getByText ( "gh-repo-tasks" ) ) . toBeInTheDocument ( ) ;
387+ } ) ;
388+
389+ await user . click ( screen . getByRole ( "button" , { name : "More options for gh-repo-tasks" } ) ) ;
390+ await user . click ( await screen . findByRole ( "menuitem" , { name : "View details" } ) ) ;
391+ await user . click ( await screen . findByRole ( "tab" , { name : / d e f i n i t i o n / i } ) ) ;
392+
393+ await user . click ( await screen . findByRole ( "button" , { name : / m o r e o p t i o n s f o r s u m m a r i z e / i } ) ) ;
394+ await user . click ( await screen . findByRole ( "menuitem" , { name : "Delete" } ) ) ;
395+
396+ const dialog = await screen . findByRole ( "dialog" ) ;
397+ await user . click ( within ( dialog ) . getByRole ( "button" , { name : / ^ d e l e t e $ / i } ) ) ;
398+
399+ // The deleted row is gone but the drawer stays open with the sibling prompt.
400+ await waitFor ( ( ) => {
401+ expect (
402+ screen . queryByRole ( "button" , { name : / m o r e o p t i o n s f o r s u m m a r i z e / i } ) ,
403+ ) . not . toBeInTheDocument ( ) ;
404+ } ) ;
405+ expect ( screen . getByRole ( "button" , { name : / m o r e o p t i o n s f o r t r a n s l a t e / i } ) ) . toBeInTheDocument ( ) ;
406+ } ) ;
407+
408+ it ( "reconciles with the server and restores the row when the DELETE fails and the prompt survives" , async ( ) => {
409+ const user = userEvent . setup ( ) ;
410+ const prompts : Prompt [ ] = [
411+ createMockPrompt ( {
412+ id : "prompt-1" ,
413+ name : "summarize" ,
414+ displayName : "Summarize document" ,
415+ gatewayId : "gw-github" ,
416+ gatewaySlug : "gh-repo-tasks" ,
417+ } ) ,
418+ createMockPrompt ( {
419+ id : "prompt-2" ,
420+ name : "translate" ,
421+ displayName : "Translate document" ,
422+ gatewayId : "gw-github" ,
423+ gatewaySlug : "gh-repo-tasks" ,
424+ } ) ,
425+ ] ;
426+
427+ // The DELETE fails and the prompt is NOT removed server-side (a pre-commit
428+ // failure), so the post-failure refetch still returns it and the row
429+ // reappears.
430+ server . use (
431+ http . get ( "/prompts" , ( ) => HttpResponse . json ( prompts ) ) ,
432+ http . delete ( "/prompts/prompt-1" , ( ) =>
433+ HttpResponse . json ( { detail : "Prompt is in use" } , { status : 409 } ) ,
434+ ) ,
435+ ) ;
436+
437+ renderWithProviders ( < Prompts /> ) ;
438+
439+ await waitFor ( ( ) => {
440+ expect ( screen . getByText ( "gh-repo-tasks" ) ) . toBeInTheDocument ( ) ;
441+ } ) ;
442+
443+ await user . click ( screen . getByRole ( "button" , { name : "More options for gh-repo-tasks" } ) ) ;
444+ await user . click ( await screen . findByRole ( "menuitem" , { name : "View details" } ) ) ;
445+ await user . click ( await screen . findByRole ( "tab" , { name : / d e f i n i t i o n / i } ) ) ;
446+
447+ await user . click ( await screen . findByRole ( "button" , { name : / m o r e o p t i o n s f o r s u m m a r i z e / i } ) ) ;
448+ await user . click ( await screen . findByRole ( "menuitem" , { name : "Delete" } ) ) ;
449+
450+ const dialog = await screen . findByRole ( "dialog" ) ;
451+ await user . click ( within ( dialog ) . getByRole ( "button" , { name : / ^ d e l e t e $ / i } ) ) ;
452+
453+ // The optimistic removal is reconciled against server truth, so the row
454+ // reappears because the prompt still exists.
455+ await waitFor ( ( ) => {
456+ expect (
457+ screen . getByRole ( "button" , { name : / m o r e o p t i o n s f o r s u m m a r i z e / i } ) ,
458+ ) . toBeInTheDocument ( ) ;
459+ } ) ;
460+ } ) ;
461+
462+ it ( "does not resurrect a prompt when the DELETE reports an error but the row is already gone" , async ( ) => {
463+ const user = userEvent . setup ( ) ;
464+ // Simulates a post-commit failure: PromptService.delete_prompt() commits
465+ // the removal, then a later step (notification/cache invalidation) fails
466+ // and the endpoint returns an error. The server no longer has the prompt,
467+ // so reconciling must NOT restore it.
468+ let remaining : Prompt [ ] = [
469+ createMockPrompt ( {
470+ id : "prompt-1" ,
471+ name : "summarize" ,
472+ displayName : "Summarize document" ,
473+ gatewayId : "gw-github" ,
474+ gatewaySlug : "gh-repo-tasks" ,
475+ } ) ,
476+ createMockPrompt ( {
477+ id : "prompt-2" ,
478+ name : "translate" ,
479+ displayName : "Translate document" ,
480+ gatewayId : "gw-github" ,
481+ gatewaySlug : "gh-repo-tasks" ,
482+ } ) ,
483+ ] ;
484+
485+ server . use (
486+ http . get ( "/prompts" , ( ) => HttpResponse . json ( remaining ) ) ,
487+ http . delete ( "/prompts/prompt-1" , ( ) => {
488+ // Row is committed as deleted, but the request still errors out.
489+ remaining = remaining . filter ( ( p ) => p . id !== "prompt-1" ) ;
490+ return HttpResponse . json ( { detail : "Post-commit hook failed" } , { status : 500 } ) ;
491+ } ) ,
492+ ) ;
493+
494+ renderWithProviders ( < Prompts /> ) ;
495+
496+ await waitFor ( ( ) => {
497+ expect ( screen . getByText ( "gh-repo-tasks" ) ) . toBeInTheDocument ( ) ;
498+ } ) ;
499+
500+ await user . click ( screen . getByRole ( "button" , { name : "More options for gh-repo-tasks" } ) ) ;
501+ await user . click ( await screen . findByRole ( "menuitem" , { name : "View details" } ) ) ;
502+ await user . click ( await screen . findByRole ( "tab" , { name : / d e f i n i t i o n / i } ) ) ;
503+
504+ await user . click ( await screen . findByRole ( "button" , { name : / m o r e o p t i o n s f o r s u m m a r i z e / i } ) ) ;
505+ await user . click ( await screen . findByRole ( "menuitem" , { name : "Delete" } ) ) ;
506+
507+ const dialog = await screen . findByRole ( "dialog" ) ;
508+ await user . click ( within ( dialog ) . getByRole ( "button" , { name : / ^ d e l e t e $ / i } ) ) ;
509+
510+ // The drawer stays open with the surviving sibling, and the deleted prompt
511+ // is not brought back by the failure path.
512+ await waitFor ( ( ) => {
513+ expect (
514+ screen . getByRole ( "button" , { name : / m o r e o p t i o n s f o r t r a n s l a t e / i } ) ,
515+ ) . toBeInTheDocument ( ) ;
516+ } ) ;
517+ expect (
518+ screen . queryByRole ( "button" , { name : / m o r e o p t i o n s f o r s u m m a r i z e / i } ) ,
519+ ) . not . toBeInTheDocument ( ) ;
520+ } ) ;
521+
298522 it ( "collapses gateway-less prompts into a single REST prompts card" , async ( ) => {
299523 const prompts : Prompt [ ] = [
300524 createMockPrompt ( {
0 commit comments