@@ -294,7 +294,7 @@ describe('wirePaymentCapability (via PaymentManagerPrimitive.add)', () => {
294294 // Test 3 – Minimal agent: no known pattern
295295 // =========================================================================
296296 describe ( 'minimal agent with neither get_or_create_agent nor Agent() pattern' , ( ) => {
297- const minimalMain = [ 'def h(e, c): pass' ] . join ( '\n' ) ;
297+ const minimalMain = [ 'from strands import Agent' , '' , ' def h(e, c): pass'] . join ( '\n' ) ;
298298
299299 beforeEach ( ( ) => {
300300 mockExistsSync . mockImplementation ( ( p : string ) => {
@@ -418,10 +418,13 @@ describe('wirePaymentCapability (via PaymentManagerPrimitive.add)', () => {
418418 mockExistsSync . mockImplementation ( ( p : string ) => {
419419 if ( p === TEMPLATE_DIR ) return true ;
420420 if ( p === PAYMENTS_PY_DEST ) return false ;
421- if ( p === MAIN_PY ) return false ; // no main.py → skip patching
421+ if ( p === MAIN_PY ) return true ;
422422 return false ;
423423 } ) ;
424- mockReadFileSync . mockReturnValue ( '' ) ;
424+ // Strands main.py — passes the framework gate; pattern doesn't match
425+ // either get_or_create or Agent() so file write is skipped, but cap dir
426+ // setup still runs.
427+ mockReadFileSync . mockReturnValue ( 'from strands import Agent\n\ndef h(e, c): pass\n' ) ;
425428 } ) ;
426429
427430 it ( 'creates capabilities/payments/ directory with recursive flag' , async ( ) => {
@@ -458,9 +461,11 @@ describe('wirePaymentCapability (via PaymentManagerPrimitive.add)', () => {
458461 mockExistsSync . mockImplementation ( ( p : string ) => {
459462 if ( p === TEMPLATE_DIR ) return true ;
460463 if ( p === PAYMENTS_PY_DEST ) return false ;
461- if ( p === MAIN_PY ) return false ;
464+ if ( p === MAIN_PY ) return true ;
462465 return false ; // init files absent
463466 } ) ;
467+ // Strands main.py to pass the framework gate
468+ mockReadFileSync . mockReturnValue ( 'from strands import Agent\n\ndef h(e, c): pass\n' ) ;
464469 } ) ;
465470
466471 it ( 'creates capabilities/payments/__init__.py when absent' , async ( ) => {
@@ -479,11 +484,12 @@ describe('wirePaymentCapability (via PaymentManagerPrimitive.add)', () => {
479484 mockExistsSync . mockImplementation ( ( p : string ) => {
480485 if ( p === TEMPLATE_DIR ) return true ;
481486 if ( p === PAYMENTS_PY_DEST ) return false ;
482- if ( p === MAIN_PY ) return false ;
487+ if ( p === MAIN_PY ) return true ;
483488 if ( p === CAP_INIT ) return true ; // already exists
484489 if ( p === PARENT_INIT ) return true ; // already exists
485490 return false ;
486491 } ) ;
492+ mockReadFileSync . mockReturnValue ( 'from strands import Agent\n\ndef h(e, c): pass\n' ) ;
487493
488494 await callAdd ( primitive , makeProject ( CODE_LOCATION ) ) ;
489495
@@ -539,24 +545,141 @@ describe('wirePaymentCapability (via PaymentManagerPrimitive.add)', () => {
539545 expect ( between ) . toBe ( '\n' ) ;
540546 } ) ;
541547
542- it ( 'prepends import at top of file when there are no existing imports' , async ( ) => {
543- const noImports = [ 'def handler(event, context):' , ' return {}' ] . join ( '\n' ) ;
548+ // Note: the "no existing imports" case is no longer reachable since
549+ // wirePaymentCapability requires `from strands import` to detect the
550+ // framework before wiring. A main.py with zero imports cannot be a
551+ // Strands template and is correctly skipped by the framework gate
552+ // (covered by the framework-gate tests below).
553+ } ) ;
554+
555+ // =========================================================================
556+ // Test 8 – Framework gate: skip non-Strands runtimes
557+ // =========================================================================
558+ describe ( 'framework gate (non-Strands runtimes)' , ( ) => {
559+ /**
560+ * Each fixture is a snippet from one of the templates we ship. The
561+ * shared expectation is the same for all: when main.py is NOT a Strands
562+ * agent, wirePaymentCapability must NOT touch the filesystem at all
563+ * (no cap dir, no payments.py copy, no main.py rewrite). The success
564+ * result still returns true and lists the runtime name in skippedRuntimes.
565+ */
566+ const fixtures : { framework : string ; main : string } [ ] = [
567+ {
568+ framework : 'LangChain_LangGraph' ,
569+ main : [
570+ 'import os' ,
571+ 'from langchain_core.messages import HumanMessage' ,
572+ 'from langgraph.prebuilt import create_react_agent' ,
573+ 'from bedrock_agentcore.runtime import BedrockAgentCoreApp' ,
574+ '' ,
575+ 'app = BedrockAgentCoreApp()' ,
576+ '' ,
577+ '@app.entrypoint' ,
578+ 'async def invoke(payload, context):' ,
579+ ' pass' ,
580+ ] . join ( '\n' ) ,
581+ } ,
582+ {
583+ framework : 'GoogleADK' ,
584+ main : [
585+ 'import os' ,
586+ 'from google.adk.agents import Agent' ,
587+ 'from google.adk.runners import Runner' ,
588+ 'from bedrock_agentcore.runtime import BedrockAgentCoreApp' ,
589+ '' ,
590+ 'app = BedrockAgentCoreApp()' ,
591+ ] . join ( '\n' ) ,
592+ } ,
593+ {
594+ framework : 'OpenAIAgents' ,
595+ main : [
596+ 'import os' ,
597+ 'from agents import Agent, Runner' ,
598+ 'from bedrock_agentcore.runtime import BedrockAgentCoreApp' ,
599+ '' ,
600+ 'app = BedrockAgentCoreApp()' ,
601+ ] . join ( '\n' ) ,
602+ } ,
603+ {
604+ framework : 'AutoGen' ,
605+ main : [
606+ 'import os' ,
607+ 'from autogen_agentchat.agents import AssistantAgent' ,
608+ 'from bedrock_agentcore.runtime import BedrockAgentCoreApp' ,
609+ '' ,
610+ 'app = BedrockAgentCoreApp()' ,
611+ ] . join ( '\n' ) ,
612+ } ,
613+ ] ;
614+
615+ for ( const fixture of fixtures ) {
616+ it ( `does not wire payments into ${ fixture . framework } main.py` , async ( ) => {
617+ mockExistsSync . mockImplementation ( ( p : string ) => {
618+ if ( p === TEMPLATE_DIR ) return true ;
619+ if ( p === PAYMENTS_PY_DEST ) return false ;
620+ if ( p === MAIN_PY ) return true ;
621+ return false ;
622+ } ) ;
623+ mockReadFileSync . mockReturnValue ( fixture . main ) ;
624+
625+ const result = await callAdd ( primitive , makeProject ( CODE_LOCATION ) ) ;
626+
627+ // add() still succeeds — the manager goes into agentcore.json
628+ expect ( result . success ) . toBe ( true ) ;
629+
630+ // No filesystem mutations to the agent's source tree
631+ expect ( mockMkdirSync ) . not . toHaveBeenCalled ( ) ;
632+ expect ( mockCopyFileSync ) . not . toHaveBeenCalled ( ) ;
633+ const mainPyWrite = mockWriteFileSync . mock . calls . find ( ( c : unknown [ ] ) => ( c [ 0 ] as string ) === MAIN_PY ) ;
634+ expect ( mainPyWrite ) . toBeUndefined ( ) ;
635+ const capInitWrite = mockWriteFileSync . mock . calls . find (
636+ ( c : unknown [ ] ) => ( c [ 0 ] as string ) === CAP_INIT || ( c [ 0 ] as string ) === PARENT_INIT
637+ ) ;
638+ expect ( capInitWrite ) . toBeUndefined ( ) ;
639+
640+ // Runtime name surfaced for the CLI to warn the user
641+ if ( result . success ) {
642+ expect ( result . skippedRuntimes ) . toContain ( 'my-agent' ) ;
643+ }
644+ } ) ;
645+ }
544646
647+ it ( 'skips wiring when main.py is missing entirely (cannot detect framework)' , async ( ) => {
545648 mockExistsSync . mockImplementation ( ( p : string ) => {
546649 if ( p === TEMPLATE_DIR ) return true ;
547650 if ( p === PAYMENTS_PY_DEST ) return false ;
548- if ( p === MAIN_PY ) return true ;
651+ if ( p === MAIN_PY ) return false ;
549652 return false ;
550653 } ) ;
551- mockReadFileSync . mockReturnValue ( noImports ) ;
552654
553- await callAdd ( primitive , makeProject ( CODE_LOCATION ) ) ;
655+ const result = await callAdd ( primitive , makeProject ( CODE_LOCATION ) ) ;
554656
555- const written : string = mockWriteFileSync . mock . calls . find (
556- ( c : unknown [ ] ) => ( c [ 0 ] as string ) === MAIN_PY
557- ) ! [ 1 ] as string ;
657+ expect ( result . success ) . toBe ( true ) ;
658+ expect ( mockMkdirSync ) . not . toHaveBeenCalled ( ) ;
659+ expect ( mockCopyFileSync ) . not . toHaveBeenCalled ( ) ;
660+ if ( result . success ) {
661+ expect ( result . skippedRuntimes ) . toContain ( 'my-agent' ) ;
662+ }
663+ } ) ;
558664
559- expect ( written . startsWith ( 'from capabilities.payments.payments import create_payments_plugin' ) ) . toBe ( true ) ;
665+ it ( 'still wires when "from strands" appears in a Strands-typed main.py' , async ( ) => {
666+ const strandsMain = [ 'from strands import Agent' , '' , '@app.entrypoint' , 'def h(p, c):' , ' pass' ] . join ( '\n' ) ;
667+ mockExistsSync . mockImplementation ( ( p : string ) => {
668+ if ( p === TEMPLATE_DIR ) return true ;
669+ if ( p === PAYMENTS_PY_DEST ) return false ;
670+ if ( p === MAIN_PY ) return true ;
671+ return false ;
672+ } ) ;
673+ mockReadFileSync . mockReturnValue ( strandsMain ) ;
674+
675+ const result = await callAdd ( primitive , makeProject ( CODE_LOCATION ) ) ;
676+
677+ expect ( result . success ) . toBe ( true ) ;
678+ expect ( mockMkdirSync ) . toHaveBeenCalledWith ( CAP_DIR , { recursive : true } ) ;
679+ expect ( mockCopyFileSync ) . toHaveBeenCalledWith ( PAYMENTS_PY_SRC , PAYMENTS_PY_DEST ) ;
680+ if ( result . success ) {
681+ expect ( result . skippedRuntimes ) . toEqual ( [ ] ) ;
682+ }
560683 } ) ;
561684 } ) ;
562685} ) ;
0 commit comments