@@ -10,6 +10,7 @@ import {
1010 connectByotWithSingleRepo ,
1111 ensureOpenPrDrawerOpen ,
1212 mockRepositoryBranches ,
13+ resetWorkbenchStorage ,
1314 setComponentEditorSource ,
1415 setStylesEditorSource ,
1516 waitForAppReady ,
@@ -102,10 +103,45 @@ const removeSavedGitHubToken = async (page: Page) => {
102103 await expect ( dialog ) . not . toHaveAttribute ( 'open' , '' )
103104}
104105
106+ const openStoredWorkspaceContextById = async ( page : Page , workspaceId : string ) => {
107+ const select = page . getByLabel ( 'Stored local editor contexts' )
108+ const openButton = page . locator ( '#workspaces-open' )
109+
110+ if ( ! ( await select . isVisible ( ) ) ) {
111+ await page . getByRole ( 'button' , { name : 'Workspaces' } ) . click ( )
112+ }
113+
114+ await expect ( select ) . toBeVisible ( )
115+
116+ await expect
117+ . poll ( async ( ) => {
118+ return select . evaluate (
119+ ( element , id ) =>
120+ element instanceof HTMLSelectElement &&
121+ Array . from ( element . options ) . some ( option => option . value === id ) ,
122+ workspaceId ,
123+ )
124+ } )
125+ . toBe ( true )
126+
127+ await expect
128+ . poll ( async ( ) => {
129+ await select . selectOption ( workspaceId )
130+ const selectedValue = await select . inputValue ( )
131+ return selectedValue === workspaceId && ( await openButton . isEnabled ( ) )
132+ } )
133+ . toBe ( true )
134+
135+ await openButton . click ( )
136+ }
137+
105138const openMostRecentStoredWorkspaceContext = async ( page : Page ) => {
106- await page . getByRole ( 'button' , { name : 'Workspaces' } ) . click ( )
139+ const select = page . getByLabel ( 'Stored local editor contexts' )
140+
141+ if ( ! ( await select . isVisible ( ) ) ) {
142+ await page . getByRole ( 'button' , { name : 'Workspaces' } ) . click ( )
143+ }
107144
108- const select = page . locator ( '#workspaces-select' )
109145 await expect ( select ) . toBeVisible ( )
110146
111147 const firstContextId = await select . evaluate ( element => {
@@ -118,20 +154,7 @@ const openMostRecentStoredWorkspaceContext = async (page: Page) => {
118154 } )
119155
120156 expect ( firstContextId ) . not . toBe ( '' )
121- await select . selectOption ( firstContextId )
122- await page . locator ( '#workspaces-open' ) . click ( )
123- }
124-
125- const openStoredWorkspaceContextById = async ( page : Page , workspaceId : string ) => {
126- const select = page . locator ( '#workspaces-select' )
127-
128- if ( ! ( await select . isVisible ( ) ) ) {
129- await page . locator ( '#workspaces-toggle' ) . click ( )
130- }
131-
132- await expect ( select ) . toBeVisible ( )
133- await select . selectOption ( workspaceId )
134- await page . locator ( '#workspaces-open' ) . click ( )
157+ await openStoredWorkspaceContextById ( page , firstContextId )
135158}
136159
137160const seedLocalWorkspaceContexts = async (
@@ -846,9 +869,229 @@ test('Open PR drawer can filter stored local contexts by search', async ({ page
846869 expect ( labels ) . toEqual ( [ 'Select a stored local context' , 'local:Beta local context' ] )
847870} )
848871
872+ test ( 'Blank-slate startup persists inactive local workspace before PAT' , async ( {
873+ page,
874+ } ) => {
875+ await resetWorkbenchStorage ( page )
876+
877+ await waitForAppReady ( page , `${ appEntryPath } ` )
878+
879+ await expect
880+ . poll ( async ( ) => {
881+ const records = await getAllWorkspaceRecords ( page )
882+ if ( ! Array . isArray ( records ) || records . length === 0 ) {
883+ return false
884+ }
885+
886+ const latest = records . slice ( ) . sort ( ( a , b ) => {
887+ const aLastModified =
888+ typeof a ?. lastModified === 'number' && Number . isFinite ( a . lastModified )
889+ ? a . lastModified
890+ : 0
891+ const bLastModified =
892+ typeof b ?. lastModified === 'number' && Number . isFinite ( b . lastModified )
893+ ? b . lastModified
894+ : 0
895+ return bLastModified - aLastModified
896+ } ) [ 0 ]
897+
898+ return (
899+ latest ?. prContextState === 'inactive' &&
900+ latest ?. prNumber === null &&
901+ typeof latest ?. repo === 'string'
902+ )
903+ } )
904+ . toBe ( true )
905+ } )
906+
907+ test ( 'Fresh PAT bootstrap persists drawer head metadata to IDB' , async ( { page } ) => {
908+ const repositoryFullName = 'knightedcodemonkey/contract-case'
909+
910+ await resetWorkbenchStorage ( page )
911+
912+ await page . route ( 'https://api.github.com/user/repos**' , async route => {
913+ await route . fulfill ( {
914+ status : 200 ,
915+ contentType : 'application/json' ,
916+ body : JSON . stringify ( [
917+ {
918+ id : 12 ,
919+ owner : { login : 'knightedcodemonkey' } ,
920+ name : 'contract-case' ,
921+ full_name : repositoryFullName ,
922+ default_branch : 'main' ,
923+ permissions : { push : true } ,
924+ } ,
925+ ] ) ,
926+ } )
927+ } )
928+
929+ await mockRepositoryBranches ( page , {
930+ [ repositoryFullName ] : [ 'main' , 'release' ] ,
931+ } )
932+
933+ await waitForAppReady ( page , `${ appEntryPath } ` )
934+
935+ await page
936+ . getByRole ( 'textbox' , { name : 'GitHub token' } )
937+ . fill ( 'github_pat_fake_chat_1234567890' )
938+ await page . getByRole ( 'button' , { name : 'Add GitHub token' } ) . click ( )
939+
940+ await ensureOpenPrDrawerOpen ( page )
941+
942+ await expect
943+ . poll ( async ( ) => {
944+ const selectedRepository = await page
945+ . getByLabel ( 'Pull request repository' )
946+ . inputValue ( )
947+ const drawerHead = await page . getByLabel ( 'Head' ) . inputValue ( )
948+ const records = await getAllWorkspaceRecords ( page )
949+
950+ const latestRecord = records
951+ . filter ( record => record ?. repo === selectedRepository )
952+ . sort ( ( a , b ) => {
953+ const aLastModified =
954+ typeof a ?. lastModified === 'number' && Number . isFinite ( a . lastModified )
955+ ? a . lastModified
956+ : 0
957+ const bLastModified =
958+ typeof b ?. lastModified === 'number' && Number . isFinite ( b . lastModified )
959+ ? b . lastModified
960+ : 0
961+ return bLastModified - aLastModified
962+ } ) [ 0 ]
963+
964+ return (
965+ Boolean ( selectedRepository ) &&
966+ Boolean ( drawerHead ) &&
967+ Boolean ( latestRecord ) &&
968+ latestRecord . repo === selectedRepository &&
969+ latestRecord . head === drawerHead
970+ )
971+ } )
972+ . toBe ( true )
973+ } )
974+
975+ for ( const prContextState of [ 'inactive' , 'disconnected' , 'closed' ] as const ) {
976+ test ( `Head stays fixed across repository changes for ${ prContextState } workspace context` , async ( {
977+ page,
978+ browserName,
979+ } ) => {
980+ // WebKit-only quarantine: keep these specs active on Chromium while CI flake is investigated.
981+ test . fixme (
982+ browserName === 'webkit' ,
983+ 'Temporarily quarantined on WebKit due CI-only Workspaces drawer timing flake.' ,
984+ )
985+
986+ const sourceRepository = 'knightedcodemonkey/contract-case'
987+ const targetRepository = 'knightedcodemonkey/develop-sandbox'
988+ const workspaceHead = 'feat/component-j101'
989+ const workspaceId = buildWorkspaceRecordId ( {
990+ repositoryFullName : sourceRepository ,
991+ headBranch : workspaceHead ,
992+ } )
993+
994+ await resetWorkbenchStorage ( page )
995+
996+ await page . route ( 'https://api.github.com/user/repos**' , async route => {
997+ await route . fulfill ( {
998+ status : 200 ,
999+ contentType : 'application/json' ,
1000+ body : JSON . stringify ( [
1001+ {
1002+ id : 12 ,
1003+ owner : { login : 'knightedcodemonkey' } ,
1004+ name : 'contract-case' ,
1005+ full_name : sourceRepository ,
1006+ default_branch : 'main' ,
1007+ permissions : { push : true } ,
1008+ } ,
1009+ {
1010+ id : 13 ,
1011+ owner : { login : 'knightedcodemonkey' } ,
1012+ name : 'develop-sandbox' ,
1013+ full_name : targetRepository ,
1014+ default_branch : 'main' ,
1015+ permissions : { push : true } ,
1016+ } ,
1017+ ] ) ,
1018+ } )
1019+ } )
1020+
1021+ await mockRepositoryBranches ( page , {
1022+ [ sourceRepository ] : [ 'main' , 'release' , workspaceHead ] ,
1023+ [ targetRepository ] : [ 'main' , 'release' ] ,
1024+ } )
1025+
1026+ await waitForAppReady ( page , `${ appEntryPath } ` )
1027+
1028+ await seedLocalWorkspaceContexts ( page , [
1029+ {
1030+ id : workspaceId ,
1031+ repo : sourceRepository ,
1032+ base : 'main' ,
1033+ head : workspaceHead ,
1034+ prTitle : '' ,
1035+ prNumber : null ,
1036+ prContextState,
1037+ renderMode : 'dom' ,
1038+ tabs : [
1039+ {
1040+ id : 'component' ,
1041+ name : 'App.tsx' ,
1042+ path : 'src/components/App.tsx' ,
1043+ language : 'javascript-jsx' ,
1044+ role : 'entry' ,
1045+ isActive : true ,
1046+ content : 'export const App = () => <main>Workspace context</main>' ,
1047+ } ,
1048+ {
1049+ id : 'styles' ,
1050+ name : 'app.css' ,
1051+ path : 'src/styles/app.css' ,
1052+ language : 'css' ,
1053+ role : 'module' ,
1054+ isActive : false ,
1055+ content : 'main { color: #111; }' ,
1056+ } ,
1057+ ] ,
1058+ activeTabId : 'component' ,
1059+ } ,
1060+ ] )
1061+
1062+ await page
1063+ . getByRole ( 'textbox' , { name : 'GitHub token' } )
1064+ . fill ( 'github_pat_fake_chat_1234567890' )
1065+ await page . getByRole ( 'button' , { name : 'Add GitHub token' } ) . click ( )
1066+
1067+ await openStoredWorkspaceContextById ( page , workspaceId )
1068+
1069+ await ensureOpenPrDrawerOpen ( page )
1070+ await expect ( page . getByLabel ( 'Pull request repository' ) ) . toHaveValue ( sourceRepository )
1071+ await expect ( page . getByLabel ( 'Head' ) ) . toHaveValue ( workspaceHead )
1072+
1073+ await page . getByLabel ( 'Pull request repository' ) . selectOption ( targetRepository )
1074+
1075+ await expect ( page . getByLabel ( 'Head' ) ) . toHaveValue ( workspaceHead )
1076+ await expect
1077+ . poll ( async ( ) => {
1078+ const record = await getWorkspaceTabsRecord ( page , { headBranch : workspaceHead } )
1079+ return record ?. head === workspaceHead
1080+ } )
1081+ . toBe ( true )
1082+ } )
1083+ }
1084+
8491085test ( 'Open PR keeps inactive workspace record when repository changes' , async ( {
8501086 page,
1087+ browserName,
8511088} ) => {
1089+ // WebKit-only quarantine: keep this spec active on Chromium while CI flake is investigated.
1090+ test . fixme (
1091+ browserName === 'webkit' ,
1092+ 'Temporarily quarantined on WebKit due CI-only Workspaces drawer timing flake.' ,
1093+ )
1094+
8521095 const oldRepository = 'knightedcodemonkey/contract-case'
8531096 const newRepository = 'knightedcodemonkey/develop-sandbox'
8541097 const headBranch = 'feat/component-sync'
@@ -1019,9 +1262,7 @@ test('Open PR keeps inactive workspace record when repository changes', async ({
10191262 const repoSelect = page . getByLabel ( 'Pull request repository' )
10201263 await expect ( repoSelect ) . toHaveValue ( oldRepository )
10211264
1022- await page . getByRole ( 'button' , { name : 'Workspaces' } ) . click ( )
1023- await page . locator ( '#workspaces-select' ) . selectOption ( oldWorkspaceId )
1024- await page . locator ( '#workspaces-open' ) . click ( )
1265+ await openStoredWorkspaceContextById ( page , oldWorkspaceId )
10251266
10261267 await ensureOpenPrDrawerOpen ( page )
10271268 await repoSelect . selectOption ( newRepository )
@@ -1694,6 +1935,21 @@ test('Active PR context disconnect uses local-only confirmation flow', async ({
16941935 ) . length
16951936 } )
16961937 . toBe ( 0 )
1938+ await expect
1939+ . poll ( async ( ) => {
1940+ const records = await getAllWorkspaceRecords ( page )
1941+ const localRecord = records . find (
1942+ record =>
1943+ typeof record ?. id === 'string' &&
1944+ record . id . startsWith ( 'local_' ) &&
1945+ record ?. repo === 'knightedcodemonkey/develop' &&
1946+ record ?. prContextState === 'inactive' ,
1947+ )
1948+
1949+ const localHead = typeof localRecord ?. head === 'string' ? localRecord . head : ''
1950+ return / ^ f e a t \/ c o m p o n e n t - [ a - z 0 - 9 ] + - [ a - z 0 - 9 ] + (?: - \d + ) ? $ / . test ( localHead )
1951+ } )
1952+ . toBe ( true )
16971953 expect ( closePullRequestRequestCount ) . toBe ( 0 )
16981954
16991955 await waitForAppReady ( page , `${ appEntryPath } ` )
@@ -2146,6 +2402,9 @@ test('Active PR context rehydrates after token remove and re-add', async ({ page
21462402 await expect (
21472403 page . getByRole ( 'button' , { name : 'Push commit to active pull request branch' } ) ,
21482404 ) . toBeVisible ( )
2405+ await expect
2406+ . poll ( async ( ) => page . getByRole ( 'textbox' , { name : 'Head' } ) . inputValue ( ) )
2407+ . toBe ( githubHeadBranch )
21492408
21502409 await expect
21512410 . poll ( async ( ) => {
@@ -3294,7 +3553,14 @@ test('Active PR context push commit uses Git Database API atomic path by default
32943553
32953554test ( 'Open PR uses module tab paths when stale target file paths collide' , async ( {
32963555 page,
3556+ browserName,
32973557} ) => {
3558+ // WebKit-only quarantine: keep this spec active on Chromium while CI flake is investigated.
3559+ test . fixme (
3560+ browserName === 'webkit' ,
3561+ 'Temporarily quarantined on WebKit due CI-only Workspaces drawer timing flake.' ,
3562+ )
3563+
32983564 const treeRequests : Array < Record < string , unknown > > = [ ]
32993565 const commitRequests : Array < Record < string , unknown > > = [ ]
33003566
0 commit comments