Skip to content

Commit 5c916c9

Browse files
fix: consistent head handling. (#96)
1 parent 2258ab8 commit 5c916c9

6 files changed

Lines changed: 375 additions & 40 deletions

File tree

playwright/github-pr-drawer.spec.ts

Lines changed: 285 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
105138
const 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

137160
const 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+
8491085
test('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 /^feat\/component-[a-z0-9]+-[a-z0-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

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

src/app.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,9 @@ const byotControls = createGitHubByotControls({
514514
githubAiContextState.selectedRepository = selectedRepository
515515
chatDrawerController.setSelectedRepository(selectedRepository)
516516
prDrawerController.setSelectedRepository(selectedRepository)
517+
const isBootstrappingTokenSession =
518+
typeof githubAiContextState.token !== 'string' ||
519+
githubAiContextState.token.trim().length === 0
517520

518521
if (!activeWorkspaceRecordId || activeWorkspaceCreatedAt === null) {
519522
void loadPreferredWorkspaceContext()
@@ -523,6 +526,14 @@ const byotControls = createGitHubByotControls({
523526
.catch(() => {
524527
/* noop */
525528
})
529+
} else if (isBootstrappingTokenSession) {
530+
void loadPreferredWorkspaceContext()
531+
.then(() => {
532+
prDrawerController.syncRepositories()
533+
})
534+
.catch(() => {
535+
/* noop */
536+
})
526537
}
527538
}
528539

@@ -1426,6 +1437,7 @@ bindAppEventsAndStart({
14261437
setCdnLoading,
14271438
},
14281439
workspaceUi: {
1440+
githubPrRepoSelect,
14291441
githubPrBaseBranch,
14301442
githubPrHeadBranch,
14311443
githubPrTitle,

0 commit comments

Comments
 (0)