@@ -4,7 +4,8 @@ import { createState } from "../../state.js";
44import { registerWatchdog } from "../../watchdog.js" ;
55import { buildNudge } from "../../watchdog.js" ;
66import registerAgenticoding from "../../index.js" ;
7- import { createTestPI } from "./helpers.js" ;
7+ import { registerHandoffCommand } from "../../handoff/command.js" ;
8+ import { createTestPI , makeReadonlyUICtx } from "./helpers.js" ;
89
910test ( "watchdog records context usage without user notifications" , async ( ) => {
1011 const pi = createTestPI ( ) ;
@@ -22,7 +23,6 @@ test("watchdog records context usage without user notifications", async () => {
2223 } ,
2324 ) ;
2425
25- assert . equal ( state . lastContextPercent , 70 ) ;
2626 assert . deepEqual ( notifications , [ ] ) ;
2727} ) ;
2828
@@ -44,10 +44,11 @@ test("context injects watchdog reminder before each LLM call", async () => {
4444 assert . equal ( result . messages [ 1 ] . role , "custom" ) ;
4545 assert . equal ( result . messages [ 1 ] . customType , "agenticoding-watchdog" ) ;
4646 assert . equal ( result . messages [ 1 ] . display , false ) ;
47- assert . match ( result . messages [ 1 ] . content , / C o n t e x t a t 7 0 % / ) ;
48- assert . match ( result . messages [ 1 ] . content , / A c t i v e n o t e b o o k t o p i c : o a u t h / ) ;
49- assert . match ( result . messages [ 1 ] . content , / s p a w n i t i n s t e a d o f p o l l u t i n g t h e p a r e n t c o n t e x t / i) ;
50- assert . doesNotMatch ( result . messages [ 1 ] . content , / I f y o u ' r e m i d - j o b a n d s t i l l c l e a r | c o n s i d e r a h a n d o f f a n d d r a f t a c l e a r b r i e f f o r w h a t c o m e s n e x t / i) ;
47+ assert . match ( result . messages [ 1 ] . content , / 7 0 % / ) ;
48+ assert . match ( result . messages [ 1 ] . content , / o a u t h / ) ;
49+ assert . match ( result . messages [ 1 ] . content , / s p a w n / i) ;
50+ assert . match ( result . messages [ 1 ] . content , / p a r e n t c o n t e x t / i) ;
51+ assert . doesNotMatch ( result . messages [ 1 ] . content , / d r a f t a c l e a r b r i e f | w h a t c o m e s n e x t / i) ;
5152} ) ;
5253
5354
@@ -64,7 +65,9 @@ test("context injects a boundary nudge below 30% after an explicit topic change"
6465 ) ;
6566
6667 assert . equal ( result . messages [ 1 ] . display , false ) ;
67- assert . match ( result . messages [ 1 ] . content , / N o t e b o o k t o p i c c h a n g e d f r o m o a u t h t o b i l l i n g / ) ;
68+ assert . match ( result . messages [ 1 ] . content , / o a u t h / i) ;
69+ assert . match ( result . messages [ 1 ] . content , / b i l l i n g / i) ;
70+ assert . match ( result . messages [ 1 ] . content , / t o p i c c h a n g e d / i) ;
6871} ) ;
6972
7073
@@ -82,8 +85,9 @@ test("context injects a no-topic nudge when context is high", async () => {
8285 assert . equal ( result . messages [ 1 ] . role , "custom" ) ;
8386 assert . equal ( result . messages [ 1 ] . customType , "agenticoding-watchdog" ) ;
8487 assert . equal ( result . messages [ 1 ] . display , false ) ;
85- assert . match ( result . messages [ 1 ] . content , / N o a c t i v e n o t e b o o k t o p i c i s s e t / ) ;
86- assert . match ( result . messages [ 1 ] . content , / A s s i g n a f r e s h t o p i c i n t h e n e x t c l e a n c o n t e x t a f t e r h a n d o f f / i) ;
88+ assert . match ( result . messages [ 1 ] . content , / n o a c t i v e n o t e b o o k t o p i c / i) ;
89+ assert . match ( result . messages [ 1 ] . content , / f r e s h t o p i c / i) ;
90+ assert . match ( result . messages [ 1 ] . content , / h a n d o f f / i) ;
8791} ) ;
8892
8993
@@ -98,7 +102,9 @@ test("context consumes a boundary hint after the first injected nudge", async ()
98102 { messages : [ { role : "user" , content : "hi" , timestamp : 1 } ] } ,
99103 { getContextUsage : ( ) => ( { percent : 20 } ) } ,
100104 ) ;
101- assert . match ( first . messages [ 1 ] . content , / N o t e b o o k t o p i c c h a n g e d f r o m o a u t h t o b i l l i n g / ) ;
105+ assert . match ( first . messages [ 1 ] . content , / o a u t h / i) ;
106+ assert . match ( first . messages [ 1 ] . content , / b i l l i n g / i) ;
107+ assert . match ( first . messages [ 1 ] . content , / t o p i c c h a n g e d / i) ;
102108
103109 const second = await handler (
104110 { messages : [ { role : "user" , content : "hi" , timestamp : 2 } ] } ,
@@ -108,11 +114,10 @@ test("context consumes a boundary hint after the first injected nudge", async ()
108114} ) ;
109115
110116
111- test ( "buildNudge no longer emits the old percent-only handoff text" , ( ) => {
112- const old = buildNudge ( { activeNotebookTopic : "oauth" , pendingTopicBoundaryHint : null } , 46 ) ;
113- assert . doesNotMatch ( old , / O n e c o n t e x t , o n e j o b \. | I f y o u ' r e m i d - j o b a n d s t i l l c l e a r | c o n s i d e r a h a n d o f f a n d d r a f t a c l e a r b r i e f / i) ;
114- assert . match ( old , / A c t i v e n o t e b o o k t o p i c : o a u t h / ) ;
115- assert . match ( old , / p r e f e r s p a w n / i) ;
117+ test ( "buildNudge emits topic and spawn guidance" , ( ) => {
118+ const nudge = buildNudge ( { activeNotebookTopic : "oauth" , pendingTopicBoundaryHint : null } , 46 ) ;
119+ assert . match ( nudge , / A c t i v e n o t e b o o k t o p i c : o a u t h / ) ;
120+ assert . match ( nudge , / p r e f e r s p a w n / i) ;
116121} ) ;
117122
118123
@@ -132,13 +137,18 @@ test("buildNudge handles null percent and boundary hints before topic guidance",
132137 assert . match ( noTopic , / N o a c t i v e n o t e b o o k t o p i c i s s e t / ) ;
133138} ) ;
134139
135- test ( "watchdog stays advisory when a requested handoff is not completed " , async ( ) => {
140+ test ( "watchdog stays advisory for a fresh user- requested handoff" , async ( ) => {
136141 const pi = createTestPI ( ) ;
137142 const state = createState ( ) ;
138- state . pendingRequestedHandoff = { direction : "implement auth" , readonlyBypassActive : false , resumeReadonlyAfterHandoff : false , enforcementAttempts : 0 , toolCalled : false } ;
143+ registerHandoffCommand ( pi as any , state ) ;
139144 registerWatchdog ( pi as any , state ) ;
140145 const [ handler ] = pi . handlers . get ( "agent_end" ) ! ;
141146
147+ await pi . commands . get ( "handoff" ) . handler ( "implement auth" , {
148+ ...makeReadonlyUICtx ( ) ,
149+ isIdle : ( ) => true ,
150+ } as any ) ;
151+
142152 const notifications : string [ ] = [ ] ;
143153 await handler (
144154 { } ,
@@ -153,7 +163,82 @@ test("watchdog stays advisory when a requested handoff is not completed", async
153163 ) ;
154164
155165 assert . equal ( state . pendingRequestedHandoff ?. toolCalled , false ) ;
156- assert . equal ( state . pendingRequestedHandoff ?. enforcementAttempts , 1 ) ;
166+ assert . ok ( state . pendingRequestedHandoff , "handoff request should remain active after one turn" ) ;
157167 assert . deepEqual ( notifications , [ ] ) ;
158- assert . deepEqual ( pi . sentUserMessages , [ ] ) ;
168+ } ) ;
169+
170+ test ( "watchdog auto-cancels a user-requested handoff after enough unanswered turns" , async ( ) => {
171+ const pi = createTestPI ( ) ;
172+ const state = createState ( ) ;
173+ registerHandoffCommand ( pi as any , state ) ;
174+ registerWatchdog ( pi as any , state ) ;
175+ const [ handler ] = pi . handlers . get ( "agent_end" ) ! ;
176+
177+ await pi . commands . get ( "handoff" ) . handler ( "implement auth" , {
178+ ...makeReadonlyUICtx ( ) ,
179+ isIdle : ( ) => true ,
180+ } as any ) ;
181+
182+ const notifications : unknown [ ] = [ ] ;
183+ const ctx = {
184+ hasUI : true ,
185+ ui : { notify : ( message : unknown ) => notifications . push ( message ) , setStatus : ( ) => { } } ,
186+ getContextUsage : ( ) => ( { percent : 20 } ) ,
187+ } ;
188+
189+ for ( let i = 0 ; i < 5 ; i ++ ) {
190+ await handler ( { } , ctx ) ;
191+ }
192+
193+ assert . equal ( state . pendingRequestedHandoff , null , "pending handoff should be auto-cancelled" ) ;
194+ assert . ok ( notifications . length > 0 , "user should receive a cancellation notification" ) ;
195+ assert . match ( notifications [ 0 ] as string , / c a n c e l l e d / i, "notification should mention cancellation" ) ;
196+ } ) ;
197+
198+ // ── Readonly-specific injection contracts ─────────────────────────
199+
200+ test ( "context injects a readonly-mode nudge after toggle" , async ( ) => {
201+ const pi = createTestPI ( ) ;
202+ registerAgenticoding ( pi as any ) ;
203+ await pi . commands . get ( "readonly" ) . handler ( "" , makeReadonlyUICtx ( ) as any ) ;
204+ const [ handler ] = pi . handlers . get ( "context" ) ! ;
205+
206+ const result = await handler (
207+ { messages : [ { role : "user" , content : "hi" , timestamp : 1 } ] } ,
208+ { getContextUsage : ( ) => null } ,
209+ ) ;
210+
211+ assert . equal ( result . messages . length , 2 ) ;
212+ assert . equal ( result . messages [ 1 ] . customType , "agenticoding-readonly-nudge" ) ;
213+ assert . match ( result . messages [ 1 ] . content , / r e a d o n l y / i) ;
214+ assert . match ( result . messages [ 1 ] . content , / w r i t e / i) ;
215+ assert . match ( result . messages [ 1 ] . content , / e d i t / i) ;
216+ assert . match ( result . messages [ 1 ] . content , / h a n d o f f / i) ;
217+ assert . match ( result . messages [ 1 ] . content , / b a s h / i) ;
218+ } ) ;
219+
220+ test ( "context injects readonly handoff guidance after explicit user /handoff" , async ( ) => {
221+ const pi = createTestPI ( ) ;
222+ registerAgenticoding ( pi as any ) ;
223+ const [ handler ] = pi . handlers . get ( "context" ) ! ;
224+ await pi . commands . get ( "readonly" ) . handler ( "" , makeReadonlyUICtx ( ) as any ) ;
225+ await handler (
226+ { messages : [ { role : "user" , content : "clear initial readonly nudge" , timestamp : 1 } ] } ,
227+ { getContextUsage : ( ) => null } ,
228+ ) ;
229+ await pi . commands . get ( "handoff" ) . handler ( "continue readonly work" , {
230+ ...makeReadonlyUICtx ( ) ,
231+ isIdle : ( ) => true ,
232+ } as any ) ;
233+
234+ const result = await handler (
235+ { messages : [ { role : "user" , content : "hi" , timestamp : 2 } ] } ,
236+ { getContextUsage : ( ) => ( { percent : 70 } ) } ,
237+ ) ;
238+ const watchdogMessage = result . messages . find ( ( message : any ) => message . customType === "agenticoding-watchdog" ) ;
239+
240+ assert . ok ( watchdogMessage , "requested handoff should inject watchdog guidance" ) ;
241+ assert . match ( watchdogMessage . content , / h a n d o f f / i) ;
242+ assert . match ( watchdogMessage . content , / r e a d o n l y / i) ;
243+ assert . match ( watchdogMessage . content , / r e s u m e / i) ;
159244} ) ;
0 commit comments