Skip to content

Commit baf708e

Browse files
feat: workspaces without pat. (#129)
1 parent 49d1ce3 commit baf708e

13 files changed

Lines changed: 475 additions & 94 deletions

File tree

playwright/diagnostics.spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,42 @@ test('typecheck does not report TS2307 for stylesheet side-effect imports', asyn
247247
expect(diagnosticsText).not.toContain("Cannot find module '../styles/app.css'")
248248
})
249249

250+
test('typecheck recognizes css module class maps in React mode', async ({ page }) => {
251+
await waitForInitialRender(page)
252+
253+
await ensurePanelToolsVisible(page, 'component')
254+
await addWorkspaceTab(page, { type: 'style' })
255+
await page.getByRole('button', { name: 'Rename tab module.css' }).click()
256+
const renameInput = page.getByLabel('Rename module.css')
257+
await renameInput.fill('app.module.css')
258+
await renameInput.press('Enter')
259+
260+
await setWorkspaceTabSource(page, {
261+
fileName: 'app.module.css',
262+
kind: 'styles',
263+
source: ['.btn {', ' color: #fff;', '}'].join('\n'),
264+
})
265+
266+
await setComponentEditorSource(
267+
page,
268+
[
269+
"import styles from '../styles/app.module.css'",
270+
'',
271+
'const App = () => <button className={styles.btn}>ok</button>',
272+
'',
273+
].join('\n'),
274+
)
275+
276+
await runTypecheck(page)
277+
await ensureDiagnosticsDrawerOpen(page)
278+
await expect(page.locator('#diagnostics-component')).toContainText(
279+
'No TypeScript errors found.',
280+
)
281+
282+
const diagnosticsText = await page.locator('#diagnostics-component').innerText()
283+
expect(diagnosticsText).not.toContain("Property 'btn' does not exist on type 'string'")
284+
})
285+
250286
test('component diagnostics rows navigate editor to reported line', async ({ page }) => {
251287
await waitForInitialRender(page)
252288

playwright/github-byot-ai.spec.ts

Lines changed: 274 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { ChatRequestBody, ChatRequestMessage } from './helpers/app-test-hel
44
import {
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

1621
test('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

43297
test('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

86340
test('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

181453
test('AI chat drawer opens and closes', async ({ page }) => {

0 commit comments

Comments
 (0)