@@ -10,6 +10,7 @@ import {
1010 connectByotWithSingleRepo ,
1111 ensureOpenPrDrawerOpen ,
1212 mockRepositoryBranches ,
13+ resetWorkbenchStorage ,
1314 setComponentEditorSource ,
1415 setStylesEditorSource ,
1516 waitForAppReady ,
@@ -846,6 +847,214 @@ test('Open PR drawer can filter stored local contexts by search', async ({ page
846847 expect ( labels ) . toEqual ( [ 'Select a stored local context' , 'local:Beta local context' ] )
847848} )
848849
850+ test ( 'Blank-slate startup persists inactive local workspace before PAT' , async ( {
851+ page,
852+ } ) => {
853+ await resetWorkbenchStorage ( page )
854+
855+ await waitForAppReady ( page , `${ appEntryPath } ` )
856+
857+ await expect
858+ . poll ( async ( ) => {
859+ const records = await getAllWorkspaceRecords ( page )
860+ if ( ! Array . isArray ( records ) || records . length === 0 ) {
861+ return false
862+ }
863+
864+ const latest = records . slice ( ) . sort ( ( a , b ) => {
865+ const aLastModified =
866+ typeof a ?. lastModified === 'number' && Number . isFinite ( a . lastModified )
867+ ? a . lastModified
868+ : 0
869+ const bLastModified =
870+ typeof b ?. lastModified === 'number' && Number . isFinite ( b . lastModified )
871+ ? b . lastModified
872+ : 0
873+ return bLastModified - aLastModified
874+ } ) [ 0 ]
875+
876+ return (
877+ latest ?. prContextState === 'inactive' &&
878+ latest ?. prNumber === null &&
879+ typeof latest ?. repo === 'string'
880+ )
881+ } )
882+ . toBe ( true )
883+ } )
884+
885+ test ( 'Fresh PAT bootstrap persists drawer head metadata to IDB' , async ( { page } ) => {
886+ const repositoryFullName = 'knightedcodemonkey/contract-case'
887+
888+ await resetWorkbenchStorage ( page )
889+
890+ await page . route ( 'https://api.github.com/user/repos**' , async route => {
891+ await route . fulfill ( {
892+ status : 200 ,
893+ contentType : 'application/json' ,
894+ body : JSON . stringify ( [
895+ {
896+ id : 12 ,
897+ owner : { login : 'knightedcodemonkey' } ,
898+ name : 'contract-case' ,
899+ full_name : repositoryFullName ,
900+ default_branch : 'main' ,
901+ permissions : { push : true } ,
902+ } ,
903+ ] ) ,
904+ } )
905+ } )
906+
907+ await mockRepositoryBranches ( page , {
908+ [ repositoryFullName ] : [ 'main' , 'release' ] ,
909+ } )
910+
911+ await waitForAppReady ( page , `${ appEntryPath } ` )
912+
913+ await page
914+ . getByRole ( 'textbox' , { name : 'GitHub token' } )
915+ . fill ( 'github_pat_fake_chat_1234567890' )
916+ await page . getByRole ( 'button' , { name : 'Add GitHub token' } ) . click ( )
917+
918+ await ensureOpenPrDrawerOpen ( page )
919+
920+ await expect
921+ . poll ( async ( ) => {
922+ const selectedRepository = await page
923+ . getByLabel ( 'Pull request repository' )
924+ . inputValue ( )
925+ const drawerHead = await page . getByLabel ( 'Head' ) . inputValue ( )
926+ const records = await getAllWorkspaceRecords ( page )
927+
928+ const latestRecord = records
929+ . filter ( record => record ?. repo === selectedRepository )
930+ . sort ( ( a , b ) => {
931+ const aLastModified =
932+ typeof a ?. lastModified === 'number' && Number . isFinite ( a . lastModified )
933+ ? a . lastModified
934+ : 0
935+ const bLastModified =
936+ typeof b ?. lastModified === 'number' && Number . isFinite ( b . lastModified )
937+ ? b . lastModified
938+ : 0
939+ return bLastModified - aLastModified
940+ } ) [ 0 ]
941+
942+ return (
943+ Boolean ( selectedRepository ) &&
944+ Boolean ( drawerHead ) &&
945+ Boolean ( latestRecord ) &&
946+ latestRecord . repo === selectedRepository &&
947+ latestRecord . head === drawerHead
948+ )
949+ } )
950+ . toBe ( true )
951+ } )
952+
953+ for ( const prContextState of [ 'inactive' , 'disconnected' , 'closed' ] as const ) {
954+ test ( `Head stays fixed across repository changes for ${ prContextState } workspace context` , async ( {
955+ page,
956+ } ) => {
957+ const sourceRepository = 'knightedcodemonkey/contract-case'
958+ const targetRepository = 'knightedcodemonkey/develop-sandbox'
959+ const workspaceHead = 'feat/component-j101'
960+ const workspaceId = buildWorkspaceRecordId ( {
961+ repositoryFullName : sourceRepository ,
962+ headBranch : workspaceHead ,
963+ } )
964+
965+ await resetWorkbenchStorage ( page )
966+
967+ await page . route ( 'https://api.github.com/user/repos**' , async route => {
968+ await route . fulfill ( {
969+ status : 200 ,
970+ contentType : 'application/json' ,
971+ body : JSON . stringify ( [
972+ {
973+ id : 12 ,
974+ owner : { login : 'knightedcodemonkey' } ,
975+ name : 'contract-case' ,
976+ full_name : sourceRepository ,
977+ default_branch : 'main' ,
978+ permissions : { push : true } ,
979+ } ,
980+ {
981+ id : 13 ,
982+ owner : { login : 'knightedcodemonkey' } ,
983+ name : 'develop-sandbox' ,
984+ full_name : targetRepository ,
985+ default_branch : 'main' ,
986+ permissions : { push : true } ,
987+ } ,
988+ ] ) ,
989+ } )
990+ } )
991+
992+ await mockRepositoryBranches ( page , {
993+ [ sourceRepository ] : [ 'main' , 'release' , workspaceHead ] ,
994+ [ targetRepository ] : [ 'main' , 'release' ] ,
995+ } )
996+
997+ await waitForAppReady ( page , `${ appEntryPath } ` )
998+
999+ await seedLocalWorkspaceContexts ( page , [
1000+ {
1001+ id : workspaceId ,
1002+ repo : sourceRepository ,
1003+ base : 'main' ,
1004+ head : workspaceHead ,
1005+ prTitle : '' ,
1006+ prNumber : null ,
1007+ prContextState,
1008+ renderMode : 'dom' ,
1009+ tabs : [
1010+ {
1011+ id : 'component' ,
1012+ name : 'App.tsx' ,
1013+ path : 'src/components/App.tsx' ,
1014+ language : 'javascript-jsx' ,
1015+ role : 'entry' ,
1016+ isActive : true ,
1017+ content : 'export const App = () => <main>Workspace context</main>' ,
1018+ } ,
1019+ {
1020+ id : 'styles' ,
1021+ name : 'app.css' ,
1022+ path : 'src/styles/app.css' ,
1023+ language : 'css' ,
1024+ role : 'module' ,
1025+ isActive : false ,
1026+ content : 'main { color: #111; }' ,
1027+ } ,
1028+ ] ,
1029+ activeTabId : 'component' ,
1030+ } ,
1031+ ] )
1032+
1033+ await page
1034+ . getByRole ( 'textbox' , { name : 'GitHub token' } )
1035+ . fill ( 'github_pat_fake_chat_1234567890' )
1036+ await page . getByRole ( 'button' , { name : 'Add GitHub token' } ) . click ( )
1037+
1038+ await page . getByRole ( 'button' , { name : 'Workspaces' } ) . click ( )
1039+ await page . locator ( '#workspaces-select' ) . selectOption ( workspaceId )
1040+ await page . locator ( '#workspaces-open' ) . click ( )
1041+
1042+ await ensureOpenPrDrawerOpen ( page )
1043+ await expect ( page . getByLabel ( 'Pull request repository' ) ) . toHaveValue ( sourceRepository )
1044+ await expect ( page . getByLabel ( 'Head' ) ) . toHaveValue ( workspaceHead )
1045+
1046+ await page . getByLabel ( 'Pull request repository' ) . selectOption ( targetRepository )
1047+
1048+ await expect ( page . getByLabel ( 'Head' ) ) . toHaveValue ( workspaceHead )
1049+ await expect
1050+ . poll ( async ( ) => {
1051+ const record = await getWorkspaceTabsRecord ( page , { headBranch : workspaceHead } )
1052+ return record ?. head === workspaceHead
1053+ } )
1054+ . toBe ( true )
1055+ } )
1056+ }
1057+
8491058test ( 'Open PR keeps inactive workspace record when repository changes' , async ( {
8501059 page,
8511060} ) => {
@@ -1694,6 +1903,21 @@ test('Active PR context disconnect uses local-only confirmation flow', async ({
16941903 ) . length
16951904 } )
16961905 . toBe ( 0 )
1906+ await expect
1907+ . poll ( async ( ) => {
1908+ const records = await getAllWorkspaceRecords ( page )
1909+ const localRecord = records . find (
1910+ record =>
1911+ typeof record ?. id === 'string' &&
1912+ record . id . startsWith ( 'local_' ) &&
1913+ record ?. repo === 'knightedcodemonkey/develop' &&
1914+ record ?. prContextState === 'inactive' ,
1915+ )
1916+
1917+ const localHead = typeof localRecord ?. head === 'string' ? localRecord . head : ''
1918+ return / ^ f e a t \/ c o m p o n e n t - [ a - z 0 - 9 ] { 4 } $ / . test ( localHead )
1919+ } )
1920+ . toBe ( true )
16971921 expect ( closePullRequestRequestCount ) . toBe ( 0 )
16981922
16991923 await waitForAppReady ( page , `${ appEntryPath } ` )
@@ -2146,6 +2370,9 @@ test('Active PR context rehydrates after token remove and re-add', async ({ page
21462370 await expect (
21472371 page . getByRole ( 'button' , { name : 'Push commit to active pull request branch' } ) ,
21482372 ) . toBeVisible ( )
2373+ await expect
2374+ . poll ( async ( ) => page . getByRole ( 'textbox' , { name : 'Head' } ) . inputValue ( ) )
2375+ . toBe ( githubHeadBranch )
21492376
21502377 await expect
21512378 . poll ( async ( ) => {
0 commit comments