Skip to content

Commit 978a323

Browse files
fix: consistent head handling.
1 parent 2258ab8 commit 978a323

6 files changed

Lines changed: 301 additions & 20 deletions

File tree

playwright/github-pr-drawer.spec.ts

Lines changed: 227 additions & 0 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,
@@ -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+
8491058
test('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 /^feat\/component-[a-z0-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 () => {

src/app.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1426,6 +1426,7 @@ bindAppEventsAndStart({
14261426
setCdnLoading,
14271427
},
14281428
workspaceUi: {
1429+
githubPrRepoSelect,
14291430
githubPrBaseBranch,
14301431
githubPrHeadBranch,
14311432
githubPrTitle,

src/modules/app-core/app-bindings-startup.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ const bindAppEventsAndStart = ({
6868
setCdnLoading,
6969
} = sourceActions
7070
const {
71+
githubPrRepoSelect,
7172
githubPrBaseBranch,
7273
githubPrHeadBranch,
7374
githubPrTitle,
@@ -340,7 +341,12 @@ const bindAppEventsAndStart = ({
340341
})
341342
})
342343

343-
for (const element of [githubPrBaseBranch, githubPrHeadBranch, githubPrTitle]) {
344+
for (const element of [
345+
githubPrRepoSelect,
346+
githubPrBaseBranch,
347+
githubPrHeadBranch,
348+
githubPrTitle,
349+
]) {
344350
bindWorkspaceMetadataPersistence(element)
345351
}
346352

@@ -514,6 +520,9 @@ const bindAppEventsAndStart = ({
514520
}
515521

516522
setHasCompletedInitialWorkspaceBootstrap(true)
523+
void flushWorkspaceSave().catch(() => {
524+
/* Save failures are already surfaced through saver onError. */
525+
})
517526
prDrawerController.syncRepositories()
518527
await renderPreview()
519528
})

src/modules/app-core/workspace-pr-session-handoff-controller.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ export const createWorkspacePrSessionHandoffController = ({
4141

4242
let lastKnownPrContextMeta = null
4343

44+
const createFreshLocalHeadBranch = () => {
45+
const entropy = Math.random().toString(36).slice(2, 6)
46+
return `feat/component-${entropy}`
47+
}
48+
4449
const createFreshLocalEntryTab = () => {
4550
const now = Date.now()
4651

@@ -65,14 +70,15 @@ export const createWorkspacePrSessionHandoffController = ({
6570
const startFreshLocalWorkspace = async ({ statusMessage } = {}) => {
6671
const now = Date.now()
6772
const localWorkspaceId = `local_${now}`
73+
const freshLocalHeadBranch = createFreshLocalHeadBranch()
6874
let didPersistFreshWorkspace = false
6975

7076
setWorkspacePrContextState('inactive')
7177
setWorkspacePrNumber(null)
7278
lastKnownPrContextMeta = null
7379

7480
if (githubPrHeadBranch) {
75-
githubPrHeadBranch.value = ''
81+
githubPrHeadBranch.value = freshLocalHeadBranch
7682
}
7783

7884
if (githubPrTitle) {
@@ -125,7 +131,7 @@ export const createWorkspacePrSessionHandoffController = ({
125131
id: localWorkspaceId,
126132
repo: getCurrentSelectedRepositoryFullName(),
127133
base: '',
128-
head: '',
134+
head: freshLocalHeadBranch,
129135
prTitle: '',
130136
prNumber: null,
131137
prContextState: 'inactive',

src/modules/github/pr/drawer/controller/create-controller.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { formatActivePrReference } from '../../context.js'
1212
import {
1313
createDefaultBranchName,
1414
createSelectOption,
15-
isAutoGeneratedHeadBranch,
1615
mergeBranchOptions,
1716
toBranchCacheKey,
1817
} from '../branches.js'
@@ -237,7 +236,6 @@ export const createGitHubPrDrawer = ({
237236
verifyActivePullRequestContext: contextHandlers.verifyActivePullRequestContext,
238237
toSafeText,
239238
sanitizeBranchPart,
240-
isAutoGeneratedHeadBranch,
241239
createDefaultBranchName,
242240
createSelectOption,
243241
mergeBranchOptions,
@@ -348,6 +346,7 @@ export const createGitHubPrDrawer = ({
348346
repositoryFullName,
349347
activeContext,
350348
})
349+
syncFormForRepository({ resetAll: true })
351350
uiHandlers.setSubmitButtonLabel()
352351
uiHandlers.emitActivePrContextChange()
353352
return Boolean(getCurrentActivePrContext())

0 commit comments

Comments
 (0)