1- import { describe , expect , it } from "vitest" ;
2- import { getRenameActionError } from "../../../../tui/console/hooks/useRenameAgentPane.js" ;
1+ import { PassThrough } from "node:stream" ;
2+ import React , { useState } from "react" ;
3+ import { render } from "ink" ;
4+ import { describe , expect , it , vi , beforeEach } from "vitest" ;
5+ import type { AgentInfo , AgentManager } from "@ai-devkit/agent-manager" ;
6+ import type { DaemonEvent } from "@ai-devkit/daemon-client" ;
7+ import type { ActionResult } from "../../../../tui/console/actions/runAction.js" ;
8+ import type { RightPaneMode , TransientMessage } from "../../../../tui/console/types.js" ;
9+
10+ let daemonClient : {
11+ subscribe : ( h : ( e : DaemonEvent ) => void ) => Promise < void > ;
12+ close : ( ) => void ;
13+ } | null = null ;
14+
15+ vi . mock ( "@ai-devkit/daemon-client" , ( ) => ( {
16+ ensureDaemon : ( ) => Promise . resolve ( daemonClient ) ,
17+ DaemonClient : { tryConnect : ( ) => Promise . resolve ( null ) } ,
18+ } ) ) ;
19+
20+ vi . mock ( "../../../../tui/console/actions/runAction.js" , ( ) => ( {
21+ runAction : vi . fn ( ) ,
22+ } ) ) ;
23+
24+ import { runAction } from "../../../../tui/console/actions/runAction.js" ;
25+ import {
26+ getRenameActionError ,
27+ useRenameAgentPane ,
28+ } from "../../../../tui/console/hooks/useRenameAgentPane.js" ;
29+ import {
30+ ConsoleProvider ,
31+ useConsoleAgentContext ,
32+ } from "../../../../tui/console/state/ConsoleContext.js" ;
33+
34+ const runActionMock = vi . mocked ( runAction ) ;
35+
36+ function renderInk ( element : React . ReactElement ) {
37+ const output = new PassThrough ( ) ;
38+ return render ( element , {
39+ stdout : output as unknown as NodeJS . WriteStream ,
40+ interactive : false ,
41+ patchConsole : false ,
42+ } ) ;
43+ }
344
445describe ( "getRenameActionError" , ( ) => {
546 it ( "returns explicit subprocess errors" , ( ) => {
@@ -16,3 +57,197 @@ describe("getRenameActionError", () => {
1657 expect ( getRenameActionError ( { exitCode : 0 } ) ) . toBeNull ( ) ;
1758 } ) ;
1859} ) ;
60+
61+ interface RenameHarness {
62+ submit : ( currentName : string , newName : string ) => void ;
63+ rightPaneMode : ( ) => RightPaneMode ;
64+ transient : ( ) => TransientMessage | null ;
65+ error : ( ) => string | null ;
66+ calls : string [ ] ;
67+ refresh : ( ) => Promise < void > ;
68+ selectAgent : ( name : string ) => void ;
69+ }
70+
71+ function renderRenameHarness ( ) : RenameHarness {
72+ const calls : string [ ] = [ ] ;
73+ const refresh = vi . fn ( async ( ) => {
74+ calls . push ( "refresh" ) ;
75+ } ) ;
76+ const selectAgent = vi . fn ( ( name : string ) => {
77+ calls . push ( `select:${ name } ` ) ;
78+ } ) ;
79+ let submitFn : ( currentName : string , newName : string ) => void = ( ) => { } ;
80+ let mode : RightPaneMode = { type : "preview" } ;
81+ let msg : TransientMessage | null = null ;
82+ let err : string | null = null ;
83+
84+ const Harness : React . FC = ( ) => {
85+ const [ rightPaneMode , setRightPaneMode ] = useState < RightPaneMode > ( { type : "preview" } ) ;
86+ const [ transient , setTransient ] = useState < TransientMessage | null > ( null ) ;
87+ const [ , setFocus ] = useState ( "list" ) ;
88+ const hook = useRenameAgentPane ( {
89+ refresh,
90+ selectAgent,
91+ setFocus,
92+ setRightPaneMode,
93+ setTransient,
94+ } ) ;
95+ submitFn = ( currentName , newName ) => hook . handleRenameSubmit ( currentName , { newName } ) ;
96+ mode = rightPaneMode ;
97+ msg = transient ;
98+ err = hook . renamePaneError ;
99+ return null ;
100+ } ;
101+ renderInk ( React . createElement ( Harness ) ) ;
102+ return {
103+ submit : ( currentName , newName ) => submitFn ( currentName , newName ) ,
104+ rightPaneMode : ( ) => mode ,
105+ transient : ( ) => msg ,
106+ error : ( ) => err ,
107+ calls,
108+ refresh,
109+ selectAgent,
110+ } ;
111+ }
112+
113+ describe ( "useRenameAgentPane" , ( ) => {
114+ beforeEach ( ( ) => {
115+ runActionMock . mockReset ( ) ;
116+ daemonClient = null ;
117+ } ) ;
118+
119+ it ( "refreshes the list then selects the new name only after a successful rename" , async ( ) => {
120+ runActionMock . mockResolvedValue ( { exitCode : 0 } satisfies ActionResult ) ;
121+ const h = renderRenameHarness ( ) ;
122+
123+ h . submit ( "old-name" , "new-name" ) ;
124+
125+ await vi . waitFor ( ( ) => {
126+ expect ( h . calls ) . toEqual ( [ "refresh" , "select:new-name" ] ) ;
127+ } ) ;
128+ expect ( h . rightPaneMode ( ) ) . toEqual ( { type : "preview" } ) ;
129+ expect ( h . transient ( ) ) . toEqual ( {
130+ kind : "info" ,
131+ text : "Renamed old-name to new-name" ,
132+ } ) ;
133+ expect ( h . error ( ) ) . toBeNull ( ) ;
134+ } ) ;
135+
136+ it ( "does not refresh or change selection when the rename fails" , async ( ) => {
137+ runActionMock . mockResolvedValue ( { exitCode : 1 , error : "name already exists" } ) ;
138+ const h = renderRenameHarness ( ) ;
139+
140+ h . submit ( "old-name" , "new-name" ) ;
141+
142+ await vi . waitFor ( ( ) => {
143+ expect ( h . error ( ) ) . toBe ( "name already exists" ) ;
144+ } ) ;
145+ expect ( h . refresh ) . not . toHaveBeenCalled ( ) ;
146+ expect ( h . selectAgent ) . not . toHaveBeenCalled ( ) ;
147+ expect ( h . transient ( ) ) . toBeNull ( ) ;
148+ } ) ;
149+ } ) ;
150+
151+ function agentFixture ( name : string ) : AgentInfo {
152+ return {
153+ name,
154+ status : "running" ,
155+ type : "codex" ,
156+ lastActive : new Date ( "2026-01-01T00:00:00Z" ) ,
157+ } as AgentInfo ;
158+ }
159+
160+ interface ConsoleRenameHarness {
161+ submit : ( currentName : string , newName : string ) => void ;
162+ agents : ( ) => AgentInfo [ ] ;
163+ selected : ( ) => string | null ;
164+ }
165+
166+ function renderConsoleRenameHarness ( manager : AgentManager ) : ConsoleRenameHarness {
167+ let submitFn : ( currentName : string , newName : string ) => void = ( ) => { } ;
168+ let currentAgents : AgentInfo [ ] = [ ] ;
169+ let currentSelected : string | null = "old-name" ;
170+
171+ const Consumer : React . FC = ( ) => {
172+ const { agents, refresh } = useConsoleAgentContext ( ) ;
173+ const [ selected , setSelected ] = useState < string | null > ( "old-name" ) ;
174+ const hook = useRenameAgentPane ( {
175+ refresh,
176+ selectAgent : setSelected ,
177+ setFocus : ( ) => { } ,
178+ setRightPaneMode : ( ) => { } ,
179+ setTransient : ( ) => { } ,
180+ } ) ;
181+ submitFn = ( currentName , newName ) => hook . handleRenameSubmit ( currentName , { newName } ) ;
182+ currentAgents = agents ;
183+ currentSelected = selected ;
184+ return null ;
185+ } ;
186+
187+ renderInk (
188+ React . createElement (
189+ ConsoleProvider ,
190+ { manager, inputFocused : false } ,
191+ React . createElement ( Consumer ) ,
192+ ) ,
193+ ) ;
194+ return {
195+ submit : ( currentName , newName ) => submitFn ( currentName , newName ) ,
196+ agents : ( ) => currentAgents ,
197+ selected : ( ) => currentSelected ,
198+ } ;
199+ }
200+
201+ describe ( "console rename refresh integration" , ( ) => {
202+ beforeEach ( ( ) => {
203+ runActionMock . mockReset ( ) ;
204+ daemonClient = null ;
205+ } ) ;
206+
207+ it ( "refreshes the visible list and reselects the new name without a daemon" , async ( ) => {
208+ const manager = {
209+ listAgents : vi
210+ . fn ( )
211+ . mockResolvedValueOnce ( [ agentFixture ( "old-name" ) ] )
212+ . mockResolvedValue ( [ agentFixture ( "new-name" ) ] ) ,
213+ } as unknown as AgentManager ;
214+ const h = renderConsoleRenameHarness ( manager ) ;
215+
216+ await vi . waitFor ( ( ) => expect ( h . agents ( ) . map ( ( a ) => a . name ) ) . toEqual ( [ "old-name" ] ) ) ;
217+ runActionMock . mockResolvedValue ( { exitCode : 0 } ) ;
218+
219+ h . submit ( "old-name" , "new-name" ) ;
220+
221+ await vi . waitFor ( ( ) => {
222+ expect ( h . agents ( ) . map ( ( a ) => a . name ) ) . toEqual ( [ "new-name" ] ) ;
223+ expect ( h . selected ( ) ) . toBe ( "new-name" ) ;
224+ } ) ;
225+ expect ( manager . listAgents ) . toHaveBeenCalledTimes ( 2 ) ;
226+ } ) ;
227+
228+ it ( "still refreshes immediately when the daemon subscription relaxes polling" , async ( ) => {
229+ daemonClient = {
230+ subscribe : ( ) => Promise . resolve ( ) ,
231+ close : vi . fn ( ) ,
232+ } ;
233+ const manager = {
234+ listAgents : vi
235+ . fn ( )
236+ . mockResolvedValueOnce ( [ agentFixture ( "old-name" ) ] )
237+ . mockResolvedValue ( [ agentFixture ( "new-name" ) ] ) ,
238+ } as unknown as AgentManager ;
239+ const h = renderConsoleRenameHarness ( manager ) ;
240+
241+ await vi . waitFor ( ( ) => expect ( h . agents ( ) . map ( ( a ) => a . name ) ) . toEqual ( [ "old-name" ] ) ) ;
242+ // Give the daemon attach a tick to subscribe and relax the poll to 60s.
243+ await new Promise ( ( resolve ) => setTimeout ( resolve , 10 ) ) ;
244+ runActionMock . mockResolvedValue ( { exitCode : 0 } ) ;
245+
246+ h . submit ( "old-name" , "new-name" ) ;
247+
248+ await vi . waitFor ( ( ) => {
249+ expect ( h . agents ( ) . map ( ( a ) => a . name ) ) . toEqual ( [ "new-name" ] ) ;
250+ expect ( h . selected ( ) ) . toBe ( "new-name" ) ;
251+ } ) ;
252+ } ) ;
253+ } ) ;
0 commit comments