@@ -873,8 +873,9 @@ describe('EntryShell onboarding OpenDesign AMR runtime', () => {
873873 expect ( props . onAgentChange ) . not . toHaveBeenCalled ( ) ;
874874 } ) ;
875875
876- it ( 'requires a successful Local Agent test before persisting and completing setup' , async ( ) => {
877- globalThis . fetch = vi . fn ( async ( input , init ) => {
876+ it ( 'tests Local Agent on Continue, stays on failure, and retries on the next click' , async ( ) => {
877+ let testCalls = 0 ;
878+ const fetchMock = vi . fn ( async ( input , init ) => {
878879 const url = String ( input ) ;
879880 if ( url . endsWith ( '/api/integrations/vela/status' ) ) {
880881 return jsonResponse ( {
@@ -885,64 +886,56 @@ describe('EntryShell onboarding OpenDesign AMR runtime', () => {
885886 } ) ;
886887 }
887888 if ( url . endsWith ( '/api/test/connection' ) && init ?. method === 'POST' ) {
888- return jsonResponse ( {
889- ok : true ,
890- kind : 'success' ,
891- latencyMs : 12 ,
892- model : 'sonnet' ,
893- sample : 'pong' ,
894- agentName : 'Claude Code' ,
895- } ) ;
889+ testCalls += 1 ;
890+ return testCalls === 1
891+ ? jsonResponse ( {
892+ ok : false ,
893+ kind : 'agent_spawn_failed' ,
894+ latencyMs : 12 ,
895+ model : 'sonnet' ,
896+ agentName : 'Claude Code' ,
897+ detail : 'process exited before responding' ,
898+ } )
899+ : jsonResponse ( {
900+ ok : true ,
901+ kind : 'success' ,
902+ latencyMs : 12 ,
903+ model : 'sonnet' ,
904+ sample : 'pong' ,
905+ agentName : 'Claude Code' ,
906+ } ) ;
896907 }
897908 throw new Error ( `unexpected fetch: ${ url } ` ) ;
898- } ) as typeof fetch ;
909+ } ) ;
910+ globalThis . fetch = fetchMock as typeof fetch ;
899911 const props = renderOnboarding ( {
900912 config : baseConfig ( {
901913 agentId : 'claude-code' ,
902914 agentModels : { 'claude-code' : { model : 'sonnet' } } ,
903915 } ) ,
904916 } ) ;
905917
906- fireEvent . click (
907- await screen . findByRole ( 'button' , { name : / C o n t i n u e \( s i g n e d i n \) / i } ) ,
908- ) ;
909- fireEvent . click ( await screen . findByRole ( 'radio' , { name : / L o c a l A g e n t / i } ) ) ;
910- fireEvent . click ( screen . getByRole ( 'button' , { name : / ^ C o n t i n u e $ / i } ) ) ;
911-
912- expect ( await screen . findByRole ( 'heading' , { name : 'Local Agent' } ) ) . toBeTruthy ( ) ;
918+ await openLocalRuntimeSetup ( ) ;
913919 const continueButton = screen . getByRole ( 'button' , { name : / ^ C o n t i n u e $ / i } ) ;
914- expect ( continueButton . getAttribute ( 'aria-disabled' ) ) . toBe ( 'true' ) ;
915- fireEvent . click ( screen . getByRole ( 'button' , { name : / ^ T e s t $ / i } ) ) ;
916- expect ( await screen . findByText ( / C l a u d e C o d e r e p l i e d i n 1 2 m s / i) ) . toBeTruthy ( ) ;
917920 expect ( continueButton . getAttribute ( 'aria-disabled' ) ) . toBeNull ( ) ;
921+
918922 fireEvent . click ( continueButton ) ;
923+ expect ( await screen . findByText ( / C o u l d n o t s t a r t C l a u d e C o d e / i) ) . toBeTruthy ( ) ;
924+ expect ( props . onCompleteOnboarding ) . not . toHaveBeenCalled ( ) ;
919925
926+ fireEvent . click ( continueButton ) ;
920927 await waitFor ( ( ) => {
928+ expect ( testCalls ) . toBe ( 2 ) ;
921929 expect ( props . onCompleteOnboarding ) . toHaveBeenCalledTimes ( 1 ) ;
922930 } ) ;
923931 expect ( props . onConfigPersist ) . toHaveBeenCalledWith (
924932 expect . objectContaining ( { mode : 'daemon' , agentId : 'claude-code' } ) ,
925933 ) ;
926- expect (
927- findTrackedEvent < Record < string , unknown > > (
928- 'ui_click' ,
929- ( payload ) => payload . element === 'local_coding_agent' ,
930- ) ,
931- ) . toMatchObject ( {
932- area : 'model_source' ,
933- step_name : 'model_source' ,
934- runtime_type : 'local_cli' ,
935- } ) ;
936934 expect ( latestTrackedEvent ( 'onboarding_complete_result' ) ) . toMatchObject ( {
937935 result : 'completed' ,
938936 exit_step_name : 'runtime_setup' ,
939937 runtime_type : 'local_cli' ,
940938 } ) ;
941- expect (
942- trackedEvents ( 'page_view' ) . filter ( ( [ , payload ] ) =>
943- ( payload as Record < string , unknown > ) . area === 'runtime_setup' ,
944- ) ,
945- ) . toHaveLength ( 1 ) ;
946939 } ) ;
947940
948941 it ( 'does not auto-select OpenDesign AMR when the AMR runtime is unavailable' , async ( ) => {
@@ -1577,6 +1570,72 @@ describe('EntryShell onboarding OpenDesign AMR runtime', () => {
15771570 expect ( props . onApiModelChange ) . not . toHaveBeenCalledWith ( 'upstream-first' ) ;
15781571 } ) ;
15791572
1573+ it ( 'tests BYOK on Continue, stays on rate limit, and retries on the next click' , async ( ) => {
1574+ let testCalls = 0 ;
1575+ globalThis . fetch = vi . fn ( async ( input , init ) => {
1576+ const url = String ( input ) ;
1577+ if ( url . endsWith ( '/api/integrations/vela/status' ) ) {
1578+ return jsonResponse ( {
1579+ loggedIn : true ,
1580+ profile : 'prod' ,
1581+ configPath : '/x' ,
1582+ user : { id : 'u' , email : 'user@example.com' } ,
1583+ } ) ;
1584+ }
1585+ if ( url . endsWith ( '/api/provider/models' ) && init ?. method === 'POST' ) {
1586+ return jsonResponse ( {
1587+ ok : true ,
1588+ kind : 'success' ,
1589+ latencyMs : 10 ,
1590+ models : [ { id : 'gpt-test' , label : 'GPT Test' } ] ,
1591+ } ) ;
1592+ }
1593+ if ( url . endsWith ( '/api/test/connection' ) && init ?. method === 'POST' ) {
1594+ testCalls += 1 ;
1595+ return testCalls === 1
1596+ ? jsonResponse ( {
1597+ ok : false ,
1598+ kind : 'rate_limited' ,
1599+ latencyMs : 12 ,
1600+ model : 'gpt-test' ,
1601+ status : 429 ,
1602+ } )
1603+ : jsonResponse ( {
1604+ ok : true ,
1605+ kind : 'success' ,
1606+ latencyMs : 12 ,
1607+ model : 'gpt-test' ,
1608+ sample : 'Connected' ,
1609+ } ) ;
1610+ }
1611+ throw new Error ( `unexpected fetch: ${ url } ` ) ;
1612+ } ) as typeof fetch ;
1613+ const props = renderOnboarding ( {
1614+ config : baseConfig ( {
1615+ mode : 'api' ,
1616+ apiProtocol : 'openai' ,
1617+ apiKey : 'test-api-key' ,
1618+ baseUrl : 'https://api.openai.com/v1' ,
1619+ model : 'gpt-test' ,
1620+ apiProviderBaseUrl : 'https://api.openai.com/v1' ,
1621+ } ) ,
1622+ } ) ;
1623+
1624+ await openByokRuntimeSetup ( ) ;
1625+ const continueButton = screen . getByRole ( 'button' , { name : / ^ C o n t i n u e $ / i } ) ;
1626+ expect ( continueButton . getAttribute ( 'aria-disabled' ) ) . toBeNull ( ) ;
1627+
1628+ fireEvent . click ( continueButton ) ;
1629+ expect ( await screen . findByText ( / r a t e - l i m i t e d t h e t e s t / i) ) . toBeTruthy ( ) ;
1630+ expect ( props . onCompleteOnboarding ) . not . toHaveBeenCalled ( ) ;
1631+
1632+ fireEvent . click ( continueButton ) ;
1633+ await waitFor ( ( ) => {
1634+ expect ( testCalls ) . toBe ( 2 ) ;
1635+ expect ( props . onCompleteOnboarding ) . toHaveBeenCalledTimes ( 1 ) ;
1636+ } ) ;
1637+ } ) ;
1638+
15801639 it ( 'persists the BYOK config before finishing onboarding' , async ( ) => {
15811640 globalThis . fetch = vi . fn ( async ( input , init ) => {
15821641 const url = String ( input ) ;
0 commit comments