New workspace create page (#710)

* New workspace page with agent catalog, composer modal, and terminal integration

* fix lint

* better blank state

* fix: resolve typecheck errors in new-workspace flow

- widen activateAndRevealWorktree's issueCommand to accept direct
  command shape used by NewWorkspacePage, not just the main-process
  runner-script variant
- use a ref object in pty-connection tests to dodge TS narrowing the
  captured callback to never across the mock closure boundary

* fix: bound worktree name auto-suffix loop to prevent OOM in tests

The auto-suffix loop in createLocalWorktree had no termination cap.
When tests (or a misconfigured env) mocked getBranchConflictKind /
getPRForBranch to always return a collision, the loop ran forever and
the vitest worker crashed with "Ineffective mark-compacts near heap
limit".

Cap the search at 100 suffixes, and if all fail, fall back to the
original specific error messages (branch already exists / PR already
owns the name) instead of a generic failure.

Update the two tests that asserted the old throw-on-first-conflict
behavior to verify the new auto-suffix path end-to-end.
This commit is contained in:
Neil
2026-04-16 15:41:40 -07:00
committed by GitHub
parent c8a8ef1b5c
commit d2a53c8fcc
52 changed files with 5699 additions and 1052 deletions
+1 -71
View File
@@ -29,7 +29,7 @@ vi.mock('../git/runner', () => ({
gitExecFileAsync: gitExecFileAsyncMock
}))
import { getPRForBranch, getPRChecks, _resetOwnerRepoCache } from './client'
import { getPRForBranch, _resetOwnerRepoCache } from './client'
describe('getPRForBranch', () => {
beforeEach(() => {
@@ -257,73 +257,3 @@ describe('getPRForBranch', () => {
expect(pr).toBeNull()
})
})
describe('getPRChecks', () => {
beforeEach(() => {
execFileAsyncMock.mockReset()
ghExecFileAsyncMock.mockReset()
getOwnerRepoMock.mockReset()
gitExecFileAsyncMock.mockReset()
acquireMock.mockReset()
releaseMock.mockReset()
acquireMock.mockResolvedValue(undefined)
_resetOwnerRepoCache()
})
it('queries check-runs by PR head SHA when GitHub remote metadata is available', async () => {
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
ghExecFileAsyncMock.mockResolvedValueOnce({
stdout: JSON.stringify({
check_runs: [
{
name: 'build',
status: 'completed',
conclusion: 'success',
html_url: 'https://github.com/acme/widgets/actions/runs/1',
details_url: null
}
]
})
})
const checks = await getPRChecks('/repo-root', 42, 'head-oid')
expect(ghExecFileAsyncMock).toHaveBeenCalledWith(
['api', '--cache', '60s', 'repos/acme/widgets/commits/head-oid/check-runs?per_page=100'],
{ cwd: '/repo-root' }
)
expect(checks).toEqual([
{
name: 'build',
status: 'completed',
conclusion: 'success',
url: 'https://github.com/acme/widgets/actions/runs/1'
}
])
})
it('falls back to gh pr checks when the cached head SHA no longer resolves', async () => {
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
ghExecFileAsyncMock
.mockRejectedValueOnce(new Error('gh: No commit found for SHA: stale-head (HTTP 422)'))
.mockResolvedValueOnce({
stdout: JSON.stringify([{ name: 'lint', state: 'PASS', link: 'https://example.com/lint' }])
})
const checks = await getPRChecks('/repo-root', 42, 'stale-head')
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(
2,
['pr', 'checks', '42', '--json', 'name,state,link'],
{ cwd: '/repo-root' }
)
expect(checks).toEqual([
{
name: 'lint',
status: 'completed',
conclusion: 'success',
url: 'https://example.com/lint'
}
])
})
})