@@ -4,7 +4,7 @@ import { act, type ReactNode } from 'react'
44import { QueryClient , QueryClientProvider } from '@tanstack/react-query'
55import { createRoot , type Root } from 'react-dom/client'
66import { afterEach , beforeEach , expect , it , vi } from 'vitest'
7- import { TaskPill } from '@/app/workspace/[workspaceId]/home/components/message-content/components/task-pill/task-pill '
7+ import { WatchActivity } from '@/app/workspace/[workspaceId]/home/components/message-content/components/watch-activity/watch-activity '
88import { mothershipTaskKeys } from '@/hooks/queries/mothership-tasks'
99
1010const { request } = vi . hoisted ( ( ) => ( { request : vi . fn ( ) } ) )
@@ -13,12 +13,6 @@ vi.mock('@/lib/mothership/tools/client/resource-display', () => ({
1313 id === 'workflow' ? 'Alfred' : undefined ,
1414} ) )
1515vi . mock ( '@/lib/api/client/request' , ( ) => ( { requestJson : request } ) )
16- vi . mock ( '@sim/emcn' , ( ) => ( {
17- Check : ( ) => null ,
18- Clock : ( ) => null ,
19- X : ( ) => null ,
20- cn : ( ...values : string [ ] ) => values . join ( ' ' ) ,
21- } ) )
2216const task = {
2317 taskId : '22222222-2222-4222-8222-222222222222' ,
2418 kind : 'timer' ,
@@ -55,27 +49,34 @@ afterEach(() => {
5549 client . clear ( )
5650} )
5751
58- it ( 'updates a pill in an earlier message without needing an event in that turn' , async ( ) => {
52+ it ( 'updates the same pending tool row in an earlier message without needing an event in that turn' , async ( ) => {
5953 render (
6054 < QueryClientProvider client = { client } >
61- < TaskPill task = { task } />
55+ < WatchActivity task = { task } />
6256 </ QueryClientProvider >
6357 )
6458 await act ( async ( ) => vi . waitFor ( ( ) => expect ( request ) . toHaveBeenCalledTimes ( 1 ) ) )
59+ const row = statusElement ( )
60+ expect ( row . parentElement ?. getAttribute ( 'aria-busy' ) ) . toBe ( 'true' )
61+ expect ( row . querySelector ( '[class*=shimmer]' ) ) . not . toBeNull ( )
62+ expect ( row . textContent ) . toBe ( 'Waiting for timer' )
6563 request . mockResolvedValue ( { taskId : task . taskId , status : 'completed' , summary : 'Timer elapsed' } )
6664 await act ( ( ) => client . invalidateQueries ( { queryKey : mothershipTaskKeys . detail ( task . taskId ) } ) )
6765 await act ( async ( ) =>
6866 vi . waitFor ( ( ) => {
69- expect ( statusElement ( ) . textContent ) . toMatch ( / B a c k g r o u n d w a t c h . * C o m p l e t e d t i m e r / )
67+ expect ( statusElement ( ) . textContent ) . toBe ( 'Timer finished' )
7068 } )
7169 )
72- expect ( statusElement ( ) . title ) . toContain ( 'Timer elapsed' )
70+ expect ( statusElement ( ) ) . toBe ( row )
71+ expect ( row . parentElement ?. getAttribute ( 'aria-busy' ) ) . toBe ( 'false' )
72+ expect ( row . querySelector ( '[class*=shimmer]' ) ) . toBeNull ( )
73+ expect ( row . parentElement ?. title ) . toContain ( 'Timer elapsed' )
7374} )
7475
7576it ( 'never replaces a live terminal event with a cached pending status' , async ( ) => {
7677 const view = render (
7778 < QueryClientProvider client = { client } >
78- < TaskPill task = { task } />
79+ < WatchActivity task = { task } />
7980 </ QueryClientProvider >
8081 )
8182 await act ( async ( ) =>
@@ -85,18 +86,18 @@ it('never replaces a live terminal event with a cached pending status', async ()
8586 )
8687 view . rerender (
8788 < QueryClientProvider client = { client } >
88- < TaskPill task = { { ...task , status : 'stopped' } } />
89+ < WatchActivity task = { { ...task , status : 'stopped' } } />
8990 </ QueryClientProvider >
9091 )
91- expect ( statusElement ( ) . textContent ) . toMatch ( / S t o p p e d t i m e r / )
92+ expect ( statusElement ( ) . textContent ) . toBe ( 'Timer stopped' )
9293} )
9394
9495it ( 'shows completed workflow watches as status without duplicate run ids or action controls' , ( ) => {
9596 const executionId = '733e3428-f081-4504-bc5e-25be3b515304'
9697 const summary = `Workflow run ${ executionId } of "Alfred" completed`
9798 render (
9899 < QueryClientProvider client = { client } >
99- < TaskPill
100+ < WatchActivity
100101 task = { {
101102 ...task ,
102103 kind : 'workflow_run' ,
@@ -108,8 +109,8 @@ it('shows completed workflow watches as status without duplicate run ids or acti
108109 </ QueryClientProvider >
109110 )
110111 const status = statusElement ( )
111- expect ( status . textContent ) . toBe ( 'Background watch · Completed workflow run · Alfred' )
112- expect ( status . title ) . toContain ( summary )
112+ expect ( status . textContent ) . toBe ( 'Completed workflow run: Alfred' )
113+ expect ( status . parentElement ?. title ) . toContain ( summary )
113114 expect ( status . textContent ) . not . toContain ( executionId )
114115 expect ( container . querySelector ( 'button' ) ) . toBeNull ( )
115116 expect ( request ) . not . toHaveBeenCalled ( )
@@ -120,10 +121,36 @@ it.each(['failed', 'expired', 'stopped'] as const)(
120121 ( status ) => {
121122 render (
122123 < QueryClientProvider client = { client } >
123- < TaskPill task = { { ...task , kind : 'workflow_run' , status, summary : 'Run did not finish' } } />
124+ < WatchActivity
125+ task = { { ...task , kind : 'workflow_run' , status, summary : 'Run did not finish' } }
126+ />
124127 </ QueryClientProvider >
125128 )
126129 expect ( statusElement ( ) . textContent ) . not . toContain ( 'Completed' )
127- expect ( statusElement ( ) . title ) . toContain ( 'Run did not finish' )
130+ expect ( statusElement ( ) . parentElement ?. title ) . toContain ( 'Run did not finish' )
128131 }
129132)
133+
134+ it ( 'renders a timer as a normal pending tool row without separate status chrome' , ( ) => {
135+ const firesAt = '2026-09-18T17:32:00Z'
136+ const time = new Date ( firesAt ) . toLocaleTimeString ( [ ] , { hour : '2-digit' , minute : '2-digit' } )
137+ render (
138+ < QueryClientProvider client = { client } >
139+ < WatchActivity task = { { ...task , target : { firesAt } } } />
140+ </ QueryClientProvider >
141+ )
142+ expect ( statusElement ( ) . textContent ) . toBe ( `Waiting until ${ time } ` )
143+ expect ( statusElement ( ) . querySelector ( '[class*=shimmer]' ) ) . not . toBeNull ( )
144+ expect ( container . textContent ) . not . toContain ( 'Background watch' )
145+ expect ( container . querySelector ( 'svg' ) ) . toBeNull ( )
146+ } )
147+
148+ it ( 'keeps pending workflow watches active even after the originating turn ends' , ( ) => {
149+ render (
150+ < QueryClientProvider client = { client } >
151+ < WatchActivity task = { { ...task , kind : 'workflow_run' , target : { workflowId : 'workflow' } } } />
152+ </ QueryClientProvider >
153+ )
154+ expect ( statusElement ( ) . textContent ) . toBe ( 'Waiting for workflow run: Alfred' )
155+ expect ( statusElement ( ) . querySelector ( '[class*=shimmer]' ) ) . not . toBeNull ( )
156+ } )
0 commit comments