@@ -78,6 +78,46 @@ async function withWorkspaceEnv<T>(
7878 }
7979}
8080
81+ function successfulBundleStager ( ) : BundleStager {
82+ return {
83+ async stage ( input ) {
84+ await mkdir ( input . outDir , { recursive : true } ) ;
85+ const runner = path . join ( input . outDir , 'runner.mjs' ) ;
86+ const bundle = path . join ( input . outDir , 'agent.bundle.mjs' ) ;
87+ const personaCopy = path . join ( input . outDir , 'persona.json' ) ;
88+ const pkg = path . join ( input . outDir , 'package.json' ) ;
89+ await Promise . all ( [
90+ writeFile ( runner , '' , 'utf8' ) ,
91+ writeFile ( bundle , '' , 'utf8' ) ,
92+ writeFile ( personaCopy , '{}' , 'utf8' ) ,
93+ writeFile ( pkg , '{}' , 'utf8' )
94+ ] ) ;
95+ return {
96+ runnerPath : runner ,
97+ bundlePath : bundle ,
98+ personaCopyPath : personaCopy ,
99+ packageJsonPath : pkg ,
100+ sizeBytes : 1
101+ } ;
102+ }
103+ } ;
104+ }
105+
106+ function successfulDevLauncher ( onLaunch ?: ( ) => void ) : ModeLauncher {
107+ return {
108+ async launch ( ) {
109+ onLaunch ?.( ) ;
110+ return {
111+ id : 'pid-1' ,
112+ async stop ( ) {
113+ /* no-op */
114+ } ,
115+ done : Promise . resolve ( { code : 0 } )
116+ } ;
117+ }
118+ } ;
119+ }
120+
81121test ( 'preflightPersona accepts a valid deploy-shaped persona' , async ( ) => {
82122 const { personaPath, cleanup } = await withTempPersona ( basePersonaJson ( ) ) ;
83123 try {
@@ -189,6 +229,143 @@ test('deploy fails clearly when integration is not connected and --no-connect is
189229 }
190230} ) ;
191231
232+ test ( 'deploy connects each missing persona integration before launch' , async ( ) => {
233+ const { personaPath, cleanup } = await withTempPersona (
234+ basePersonaJson ( { integrations : { github : { } , notion : { } } } )
235+ ) ;
236+ const io = createBufferedIO ( ) ;
237+ const checked : string [ ] = [ ] ;
238+ const connected : string [ ] = [ ] ;
239+ let launched = false ;
240+ const workspaceAuth : WorkspaceAuth = {
241+ async resolveWorkspace ( ) {
242+ return { workspace : 'ws-test' , token : 'tok' } ;
243+ }
244+ } ;
245+ const integrations : IntegrationConnectResolver = {
246+ async isConnected ( { provider } ) {
247+ checked . push ( provider ) ;
248+ return false ;
249+ } ,
250+ async connect ( { provider } ) {
251+ connected . push ( provider ) ;
252+ return { connectionId : `conn-${ provider } ` } ;
253+ }
254+ } ;
255+
256+ try {
257+ const result = await deploy (
258+ { personaPath, mode : 'dev' , io } ,
259+ {
260+ workspaceAuth,
261+ integrations,
262+ bundle : successfulBundleStager ( ) ,
263+ modes : { dev : successfulDevLauncher ( ( ) => { launched = true ; } ) }
264+ }
265+ ) ;
266+
267+ assert . deepEqual ( checked , [ 'github' , 'notion' ] ) ;
268+ assert . deepEqual ( connected , [ 'github' , 'notion' ] ) ;
269+ assert . deepEqual ( result . connectedIntegrations , [ 'github' , 'notion' ] ) ;
270+ assert . equal ( launched , true ) ;
271+ } finally {
272+ await cleanup ( ) ;
273+ }
274+ } ) ;
275+
276+ test ( 'deploy aborts cleanly when one missing integration connect fails' , async ( ) => {
277+ const { personaPath, cleanup } = await withTempPersona (
278+ basePersonaJson ( { integrations : { github : { } , notion : { } } } )
279+ ) ;
280+ const io = createBufferedIO ( ) ;
281+ const connected : string [ ] = [ ] ;
282+ let launched = false ;
283+ const workspaceAuth : WorkspaceAuth = {
284+ async resolveWorkspace ( ) {
285+ return { workspace : 'ws-test' , token : 'tok' } ;
286+ }
287+ } ;
288+ const integrations : IntegrationConnectResolver = {
289+ async isConnected ( ) {
290+ return false ;
291+ } ,
292+ async connect ( { provider } ) {
293+ connected . push ( provider ) ;
294+ if ( provider === 'notion' ) {
295+ throw new Error ( 'notion oauth unavailable' ) ;
296+ }
297+ return { connectionId : `conn-${ provider } ` } ;
298+ }
299+ } ;
300+
301+ try {
302+ await assert . rejects (
303+ deploy (
304+ { personaPath, mode : 'dev' , io } ,
305+ {
306+ workspaceAuth,
307+ integrations,
308+ bundle : successfulBundleStager ( ) ,
309+ modes : { dev : successfulDevLauncher ( ( ) => { launched = true ; } ) }
310+ }
311+ ) ,
312+ / d e p l o y a b o r t e d : 1 i n t e g r a t i o n \( s \) f a i l e d t o c o n n e c t : n o t i o n /
313+ ) ;
314+ assert . deepEqual ( connected , [ 'github' , 'notion' ] ) ;
315+ assert . equal ( launched , false ) ;
316+ assert . ok (
317+ io . messages . find (
318+ ( m ) => m . level === 'error' && m . message . includes ( 'integrations.notion: connect failed: notion oauth unavailable' )
319+ )
320+ ) ;
321+ } finally {
322+ await cleanup ( ) ;
323+ }
324+ } ) ;
325+
326+ test ( 'deploy treats --no-prompt as fail-fast for missing integration connects' , async ( ) => {
327+ const { personaPath, cleanup } = await withTempPersona (
328+ basePersonaJson ( { integrations : { github : { } , notion : { } } } )
329+ ) ;
330+ const io = createBufferedIO ( ) ;
331+ const checked : string [ ] = [ ] ;
332+ let connectCalled = false ;
333+ const workspaceAuth : WorkspaceAuth = {
334+ async resolveWorkspace ( ) {
335+ return { workspace : 'ws-test' , token : 'tok' } ;
336+ }
337+ } ;
338+ const integrations : IntegrationConnectResolver = {
339+ async isConnected ( { provider } ) {
340+ checked . push ( provider ) ;
341+ return false ;
342+ } ,
343+ async connect ( ) {
344+ connectCalled = true ;
345+ throw new Error ( 'connect should not be called when --no-prompt is set' ) ;
346+ }
347+ } ;
348+
349+ try {
350+ await assert . rejects (
351+ deploy (
352+ { personaPath, mode : 'dev' , noPrompt : true , io } ,
353+ { workspaceAuth, integrations }
354+ ) ,
355+ / d e p l o y a b o r t e d : 1 i n t e g r a t i o n \( s \) f a i l e d t o c o n n e c t : g i t h u b /
356+ ) ;
357+ assert . deepEqual ( checked , [ 'github' ] ) ;
358+ assert . equal ( connectCalled , false ) ;
359+ assert . ok (
360+ io . messages . find (
361+ ( m ) => m . level === 'error' && m . message . includes ( '--no-prompt was passed' )
362+ )
363+ ) ;
364+ } finally {
365+ await cleanup ( ) ;
366+ }
367+ } ) ;
368+
192369test ( 'deploy stages a bundle and hands off to the resolved launcher' , async ( ) => {
193370 const { personaPath, dir, cleanup } = await withTempPersona ( basePersonaJson ( ) ) ;
194371 const io = createBufferedIO ( ) ;
0 commit comments