@@ -10,7 +10,7 @@ async function createFeatureCli(options: {
1010 method : string ;
1111 params : Record < string , unknown > ;
1212 result : unknown ;
13- notification ?: { method : string ; params : Record < string , unknown > } ;
13+ notifications ?: Array < { method : string ; params : Record < string , unknown > } > ;
1414} ) : Promise < string > {
1515 const directory = await mkdtemp ( join ( tmpdir ( ) , 'autohand-ts-features-' ) ) ;
1616 temporaryDirectories . push ( directory ) ;
@@ -36,11 +36,11 @@ for await (const line of lines) {
3636 messageCount: 0,
3737 },
3838 }) + '\\n');
39- if ( fixture.notification !== undefined ) {
39+ for (const notification of fixture.notifications ?? [] ) {
4040 process.stdout.write(JSON.stringify({
4141 jsonrpc: '2.0',
42- method: fixture. notification.method,
43- params: fixture. notification.params,
42+ method: notification.method,
43+ params: notification.params,
4444 }) + '\\n');
4545 }
4646 continue;
@@ -80,6 +80,15 @@ async function withSDK<T>(
8080 }
8181}
8282
83+ async function nextNotification ( sdk : AutohandSDK ) {
84+ const events = sdk . events ( ) ;
85+ const next = events . next ( ) ;
86+ await sdk . getState ( ) ;
87+ const event = await next ;
88+ await events . return ( undefined ) ;
89+ return event . value ;
90+ }
91+
8392afterEach ( async ( ) => {
8493 await Promise . all ( temporaryDirectories . splice ( 0 ) . map ( ( directory ) =>
8594 rm ( directory , { recursive : true , force : true } )
@@ -595,3 +604,58 @@ describe('extension RPC features', () => {
595604 ) ) . rejects . toThrow ( / I n v a l i d R P C r e s u l t f o r a u t o h a n d \. s e t C o n t e x t C o m p a c t / ) ;
596605 } ) ;
597606} ) ;
607+
608+ describe ( 'extension notification features' , ( ) => {
609+ it ( 'streams typed auto-mode iteration events from the spawned CLI' , async ( ) => {
610+ const notification = {
611+ method : 'autohand.automode.iteration' ,
612+ params : {
613+ sessionId : 'automode-1' ,
614+ iteration : 3 ,
615+ actions : [ 'edited src/index.ts' , 'ran tests' ] ,
616+ tokensUsed : 1_250 ,
617+ timestamp : '2026-07-20T00:03:00.000Z' ,
618+ } ,
619+ } ;
620+ const sentinel = {
621+ method : 'autohand.error' ,
622+ params : { code : 500 , message : 'sentinel' , recoverable : true , timestamp : 'sentinel' } ,
623+ } ;
624+
625+ await expect ( withSDK ( {
626+ method : 'unused' ,
627+ params : { } ,
628+ result : { } ,
629+ notifications : [ notification , sentinel ] ,
630+ } , nextNotification ) ) . resolves . toEqual ( {
631+ type : 'automode_iteration' ,
632+ ...notification . params ,
633+ } ) ;
634+ } ) ;
635+
636+ it ( 'drops malformed auto-mode iterations without hiding later valid events' , async ( ) => {
637+ const malformed = {
638+ method : 'autohand.automode.iteration' ,
639+ params : {
640+ sessionId : 'automode-1' ,
641+ iteration : 'three' ,
642+ actions : [ ] ,
643+ timestamp : '2026-07-20T00:03:00.000Z' ,
644+ } ,
645+ } ;
646+ const sentinel = {
647+ method : 'autohand.error' ,
648+ params : { code : 500 , message : 'sentinel' , recoverable : true , timestamp : 'sentinel' } ,
649+ } ;
650+
651+ await expect ( withSDK ( {
652+ method : 'unused' ,
653+ params : { } ,
654+ result : { } ,
655+ notifications : [ malformed , sentinel ] ,
656+ } , nextNotification ) ) . resolves . toEqual ( {
657+ type : 'error' ,
658+ ...sentinel . params ,
659+ } ) ;
660+ } ) ;
661+ } ) ;
0 commit comments