@@ -4,6 +4,7 @@ import type { ChatRequestBody, ChatRequestMessage } from './helpers/app-test-hel
44import {
55 appEntryPath ,
66 connectByotWithSingleRepo ,
7+ ensureWorkspacesDrawerClosed ,
78 ensureAiChatDrawerOpen ,
89 ensureOpenPrDrawerOpen ,
910 mockRepositoryBranches ,
@@ -12,6 +13,10 @@ import {
1213 setStylesEditorSource ,
1314 waitForAppReady ,
1415} from './helpers/app-test-helpers.js'
16+ import {
17+ getAllWorkspaceRecords ,
18+ seedLocalWorkspaceContexts ,
19+ } from './github-pr-drawer/github-pr-drawer.helpers.js'
1520
1621test ( 'PR/BYOT controls are visible and chat stays hidden until token connect' , async ( {
1722 page,
@@ -37,7 +42,256 @@ test('PR/BYOT controls are visible and chat stays hidden until token connect', a
3742 await expect ( prToggle ) . toHaveCount ( 1 )
3843 await expect ( prToggle ) . toBeHidden ( )
3944 await expect ( workspacesToggle ) . toHaveCount ( 1 )
40- await expect ( workspacesToggle ) . toBeHidden ( )
45+ await expect ( workspacesToggle ) . toBeVisible ( )
46+ } )
47+
48+ test ( 'Workspaces repository filter is local-only and read-only without PAT' , async ( {
49+ page,
50+ } ) => {
51+ await waitForAppReady ( page )
52+
53+ const workspacesToggle = page . getByRole ( 'button' , {
54+ name : 'Workspaces' ,
55+ exact : true ,
56+ } )
57+ await expect ( workspacesToggle ) . toBeVisible ( )
58+
59+ await workspacesToggle . click ( )
60+
61+ const repositoryFilter = page . getByRole ( 'combobox' , {
62+ name : 'Workspace repository filter' ,
63+ } )
64+ await expect ( repositoryFilter ) . toBeDisabled ( )
65+ await expect ( repositoryFilter ) . toHaveValue ( '__local__' )
66+ await expect ( repositoryFilter . locator ( 'option' ) ) . toHaveCount ( 1 )
67+ await expect ( repositoryFilter . locator ( 'option' ) ) . toHaveText ( [ 'Local' ] )
68+ } )
69+
70+ test ( 'No-PAT startup restores Local workspace from mixed stored contexts' , async ( {
71+ page,
72+ } ) => {
73+ const localWorkspaceId = 'local_no_pat_restore_target'
74+ const localHead = 'feat/local-no-pat-restore'
75+ const localMarker = 'Local restore marker content'
76+ const repositoryMarker = 'Repository restore marker content'
77+
78+ await waitForAppReady ( page )
79+
80+ await page . evaluate ( async ( ) => {
81+ const request = indexedDB . open ( 'knighted-develop-workspaces' )
82+ const db = await new Promise < IDBDatabase > ( ( resolve , reject ) => {
83+ request . onsuccess = ( ) => resolve ( request . result )
84+ request . onerror = ( ) => reject ( request . error )
85+ request . onblocked = ( ) => reject ( new Error ( 'Could not open IndexedDB.' ) )
86+ } )
87+
88+ try {
89+ const tx = db . transaction ( 'prWorkspaces' , 'readwrite' )
90+ const store = tx . objectStore ( 'prWorkspaces' )
91+ const clearRequest = store . clear ( )
92+
93+ await new Promise < void > ( ( resolve , reject ) => {
94+ clearRequest . onsuccess = ( ) => resolve ( )
95+ clearRequest . onerror = ( ) => reject ( clearRequest . error )
96+ } )
97+
98+ await new Promise < void > ( ( resolve , reject ) => {
99+ tx . oncomplete = ( ) => resolve ( )
100+ tx . onerror = ( ) => reject ( tx . error )
101+ tx . onabort = ( ) => reject ( tx . error )
102+ } )
103+ } finally {
104+ db . close ( )
105+ }
106+ } )
107+
108+ await seedLocalWorkspaceContexts ( page , [
109+ {
110+ id : localWorkspaceId ,
111+ repo : '' ,
112+ workspaceScope : 'local' ,
113+ base : 'main' ,
114+ head : localHead ,
115+ prTitle : 'Local restore target' ,
116+ prContextState : 'inactive' ,
117+ prNumber : null ,
118+ tabs : [
119+ {
120+ id : 'entry' ,
121+ name : 'App.tsx' ,
122+ path : 'src/components/App.tsx' ,
123+ language : 'javascript-jsx' ,
124+ role : 'entry' ,
125+ isActive : true ,
126+ content : `export const App = () => <main>${ localMarker } </main>` ,
127+ } ,
128+ ] ,
129+ activeTabId : 'entry' ,
130+ createdAt : Date . now ( ) - 5000 ,
131+ lastModified : Date . now ( ) - 5000 ,
132+ } ,
133+ {
134+ id : 'repo_no_pat_restore_should_not_apply' ,
135+ repo : 'knightedcodemonkey/develop' ,
136+ workspaceScope : 'repository' ,
137+ base : 'main' ,
138+ head : 'feat/repo-should-not-restore-without-pat' ,
139+ prTitle : 'Repository active context' ,
140+ prContextState : 'active' ,
141+ prNumber : 107 ,
142+ tabs : [
143+ {
144+ id : 'entry' ,
145+ name : 'App.tsx' ,
146+ path : 'src/components/App.tsx' ,
147+ language : 'javascript-jsx' ,
148+ role : 'entry' ,
149+ isActive : true ,
150+ content : `export const App = () => <main>${ repositoryMarker } </main>` ,
151+ } ,
152+ ] ,
153+ activeTabId : 'entry' ,
154+ createdAt : Date . now ( ) + 5000 ,
155+ lastModified : Date . now ( ) + 5000 ,
156+ } ,
157+ ] )
158+
159+ await page . reload ( )
160+ await waitForAppReady ( page )
161+
162+ await expect ( page . locator ( '#github-pr-head-branch' ) ) . toHaveValue ( localHead )
163+ await expect (
164+ page . getByRole ( 'textbox' , { name : 'Component source editor' } ) ,
165+ ) . toContainText ( localMarker )
166+ await expect (
167+ page . getByRole ( 'textbox' , { name : 'Component source editor' } ) ,
168+ ) . not . toContainText ( repositoryMarker )
169+
170+ const workspacesToggle = page . getByRole ( 'button' , {
171+ name : 'Workspaces' ,
172+ exact : true ,
173+ } )
174+ await workspacesToggle . click ( )
175+
176+ await expect ( page . locator ( '#workspaces-repository' ) ) . toBeDisabled ( )
177+ await expect ( page . locator ( '#workspaces-select' ) ) . toHaveValue ( localWorkspaceId )
178+ await expect ( page . getByRole ( 'button' , { name : 'Remove' , exact : true } ) ) . toBeDisabled ( )
179+ } )
180+
181+ test ( 'PAT connect after Local-only session preserves Local records and enables repository workflows' , async ( {
182+ page,
183+ } ) => {
184+ const localWorkspaceId = 'local_pat_connect_preserve'
185+ const localHead = 'feat/local-before-pat-connect'
186+
187+ await waitForAppReady ( page )
188+
189+ await page . evaluate ( async ( ) => {
190+ const request = indexedDB . open ( 'knighted-develop-workspaces' )
191+ const db = await new Promise < IDBDatabase > ( ( resolve , reject ) => {
192+ request . onsuccess = ( ) => resolve ( request . result )
193+ request . onerror = ( ) => reject ( request . error )
194+ request . onblocked = ( ) => reject ( new Error ( 'Could not open IndexedDB.' ) )
195+ } )
196+
197+ try {
198+ const tx = db . transaction ( 'prWorkspaces' , 'readwrite' )
199+ const store = tx . objectStore ( 'prWorkspaces' )
200+ const clearRequest = store . clear ( )
201+
202+ await new Promise < void > ( ( resolve , reject ) => {
203+ clearRequest . onsuccess = ( ) => resolve ( )
204+ clearRequest . onerror = ( ) => reject ( clearRequest . error )
205+ } )
206+
207+ await new Promise < void > ( ( resolve , reject ) => {
208+ tx . oncomplete = ( ) => resolve ( )
209+ tx . onerror = ( ) => reject ( tx . error )
210+ tx . onabort = ( ) => reject ( tx . error )
211+ } )
212+ } finally {
213+ db . close ( )
214+ }
215+ } )
216+
217+ await seedLocalWorkspaceContexts ( page , [
218+ {
219+ id : localWorkspaceId ,
220+ repo : '' ,
221+ workspaceScope : 'local' ,
222+ base : 'main' ,
223+ head : localHead ,
224+ prTitle : 'Local only workspace before PAT' ,
225+ prContextState : 'inactive' ,
226+ prNumber : null ,
227+ createdAt : Date . now ( ) - 1000 ,
228+ lastModified : Date . now ( ) - 1000 ,
229+ } ,
230+ ] )
231+
232+ await page . reload ( )
233+ await waitForAppReady ( page )
234+
235+ const workspacesToggle = page . getByRole ( 'button' , {
236+ name : 'Workspaces' ,
237+ exact : true ,
238+ } )
239+ await workspacesToggle . click ( )
240+
241+ const repositoryFilter = page . getByLabel ( 'Workspace repository filter' )
242+ await expect ( repositoryFilter ) . toBeDisabled ( )
243+ await expect ( repositoryFilter ) . toHaveValue ( '__local__' )
244+ await expect ( page . locator ( '#workspaces-select' ) ) . toHaveValue ( localWorkspaceId )
245+
246+ await ensureWorkspacesDrawerClosed ( page )
247+
248+ await page . route ( 'https://api.github.com/user/repos**' , async route => {
249+ await route . fulfill ( {
250+ status : 200 ,
251+ contentType : 'application/json' ,
252+ body : JSON . stringify ( [
253+ {
254+ id : 11 ,
255+ owner : { login : 'knightedcodemonkey' } ,
256+ name : 'develop' ,
257+ full_name : 'knightedcodemonkey/develop' ,
258+ default_branch : 'main' ,
259+ permissions : { push : true } ,
260+ } ,
261+ ] ) ,
262+ } )
263+ } )
264+
265+ await mockRepositoryBranches ( page , {
266+ 'knightedcodemonkey/develop' : [ 'main' , 'release' ] ,
267+ } )
268+
269+ await page
270+ . getByRole ( 'textbox' , { name : 'GitHub token' } )
271+ . fill ( 'github_pat_fake_transition_1234567890' )
272+ await page . getByRole ( 'button' , { name : 'Add GitHub token' } ) . click ( )
273+
274+ await expect ( page . getByRole ( 'button' , { name : 'Open pull request' } ) ) . toBeVisible ( )
275+
276+ await workspacesToggle . click ( )
277+ await expect ( repositoryFilter ) . toBeEnabled ( )
278+ await expect ( repositoryFilter . locator ( 'option' ) ) . toHaveCount ( 2 )
279+ await expect ( repositoryFilter . locator ( 'option' ) ) . toHaveText ( [
280+ 'Local' ,
281+ 'knightedcodemonkey/develop' ,
282+ ] )
283+
284+ await repositoryFilter . selectOption ( 'knightedcodemonkey/develop' )
285+ await expect ( repositoryFilter ) . toHaveValue ( 'knightedcodemonkey/develop' )
286+
287+ const records = await getAllWorkspaceRecords ( page )
288+ const localRecord = records . find ( record => record ?. id === localWorkspaceId )
289+
290+ expect ( localRecord ) . toBeTruthy ( )
291+ expect ( typeof localRecord ?. repo === 'string' ? localRecord . repo : '' ) . toBe ( '' )
292+ expect (
293+ typeof localRecord ?. workspaceScope === 'string' ? localRecord . workspaceScope : '' ,
294+ ) . toBe ( 'local' )
41295} )
42296
43297test ( 'chat becomes available after token connect' , async ( { page } ) => {
@@ -80,7 +334,7 @@ test('BYOT controls render with default app entry', async ({ page }) => {
80334 await expect ( prToggle ) . toHaveCount ( 1 )
81335 await expect ( prToggle ) . toBeHidden ( )
82336 await expect ( workspacesToggle ) . toHaveCount ( 1 )
83- await expect ( workspacesToggle ) . toBeHidden ( )
337+ await expect ( workspacesToggle ) . toBeVisible ( )
84338} )
85339
86340test ( 'GitHub token info panel reflects missing and present token states' , async ( {
@@ -141,9 +395,21 @@ test('deleting saved GitHub token requires confirmation modal', async ({ page })
141395 const tokenDelete = page . getByRole ( 'button' , { name : 'Delete GitHub token' } )
142396 const tokenAdd = page . getByRole ( 'button' , { name : 'Add GitHub token' } )
143397 const tokenInput = page . getByRole ( 'textbox' , { name : 'GitHub token' } )
398+ const workspacesToggle = page . getByRole ( 'button' , {
399+ name : 'Workspaces' ,
400+ exact : true ,
401+ } )
402+ const repositoryFilter = page . getByRole ( 'combobox' , {
403+ name : 'Workspace repository filter' ,
404+ } )
144405
145406 await expect ( tokenDelete ) . toBeVisible ( )
146407
408+ await workspacesToggle . click ( )
409+ await expect ( repositoryFilter ) . toBeEnabled ( )
410+ await repositoryFilter . selectOption ( 'knightedcodemonkey/develop' )
411+ await expect ( repositoryFilter ) . toHaveValue ( 'knightedcodemonkey/develop' )
412+
147413 await tokenDelete . click ( )
148414 await expect ( dialog ) . toHaveAttribute ( 'open' , '' )
149415 await expect ( page . getByText ( 'Remove saved GitHub token?' , { exact : true } ) ) . toHaveText (
@@ -176,6 +442,12 @@ test('deleting saved GitHub token requires confirmation modal', async ({ page })
176442 await expect ( tokenAdd ) . toBeVisible ( )
177443 await expect ( tokenDelete ) . toBeHidden ( )
178444 await expect ( tokenInput ) . toHaveValue ( '' )
445+ await expect ( workspacesToggle ) . toHaveAttribute ( 'aria-expanded' , 'false' )
446+ await expect ( page . getByRole ( 'complementary' , { name : 'Workspaces' } ) ) . toBeHidden ( )
447+
448+ await workspacesToggle . click ( )
449+ await expect ( repositoryFilter ) . toBeDisabled ( )
450+ await expect ( repositoryFilter ) . toHaveValue ( '__local__' )
179451} )
180452
181453test ( 'AI chat drawer opens and closes' , async ( { page } ) => {
0 commit comments