mirror of
https://github.com/stablyai/orca.git
synced 2026-09-28 08:02:43 +00:00
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:
@@ -0,0 +1,288 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const {
|
||||
execFileAsyncMock,
|
||||
ghExecFileAsyncMock,
|
||||
getOwnerRepoMock,
|
||||
gitExecFileAsyncMock,
|
||||
acquireMock,
|
||||
releaseMock
|
||||
} = vi.hoisted(() => ({
|
||||
execFileAsyncMock: vi.fn(),
|
||||
ghExecFileAsyncMock: vi.fn(),
|
||||
getOwnerRepoMock: vi.fn(),
|
||||
gitExecFileAsyncMock: vi.fn(),
|
||||
acquireMock: vi.fn(),
|
||||
releaseMock: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('./gh-utils', () => ({
|
||||
execFileAsync: execFileAsyncMock,
|
||||
ghExecFileAsync: ghExecFileAsyncMock,
|
||||
getOwnerRepo: getOwnerRepoMock,
|
||||
acquire: acquireMock,
|
||||
release: releaseMock,
|
||||
_resetOwnerRepoCache: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('../git/runner', () => ({
|
||||
gitExecFileAsync: gitExecFileAsyncMock
|
||||
}))
|
||||
|
||||
import { listWorkItems, _resetOwnerRepoCache } from './client'
|
||||
|
||||
describe('listWorkItems', () => {
|
||||
beforeEach(() => {
|
||||
execFileAsyncMock.mockReset()
|
||||
ghExecFileAsyncMock.mockReset()
|
||||
getOwnerRepoMock.mockReset()
|
||||
gitExecFileAsyncMock.mockReset()
|
||||
acquireMock.mockReset()
|
||||
releaseMock.mockReset()
|
||||
acquireMock.mockResolvedValue(undefined)
|
||||
_resetOwnerRepoCache()
|
||||
})
|
||||
|
||||
it('runs both issue and PR GitHub searches for a mixed query and merges the results by recency', async () => {
|
||||
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
|
||||
ghExecFileAsyncMock
|
||||
.mockResolvedValueOnce({
|
||||
stdout: JSON.stringify([
|
||||
{
|
||||
number: 12,
|
||||
title: 'Fix bug',
|
||||
state: 'OPEN',
|
||||
url: 'https://github.com/acme/widgets/issues/12',
|
||||
labels: [],
|
||||
updatedAt: '2026-03-29T00:00:00Z',
|
||||
author: { login: 'octocat' }
|
||||
}
|
||||
])
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
stdout: JSON.stringify([
|
||||
{
|
||||
number: 42,
|
||||
title: 'Add feature',
|
||||
state: 'OPEN',
|
||||
url: 'https://github.com/acme/widgets/pull/42',
|
||||
labels: [],
|
||||
updatedAt: '2026-03-28T00:00:00Z',
|
||||
author: { login: 'octocat' },
|
||||
isDraft: false,
|
||||
headRefName: 'feature/add-feature',
|
||||
baseRefName: 'main'
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
const items = await listWorkItems('/repo-root', 10, 'assignee:@me')
|
||||
|
||||
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
[
|
||||
'issue',
|
||||
'list',
|
||||
'--limit',
|
||||
'10',
|
||||
'--json',
|
||||
'number,title,state,url,labels,updatedAt,author',
|
||||
'--repo',
|
||||
'acme/widgets',
|
||||
'--assignee',
|
||||
'@me'
|
||||
],
|
||||
{ cwd: '/repo-root' }
|
||||
)
|
||||
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
[
|
||||
'pr',
|
||||
'list',
|
||||
'--limit',
|
||||
'10',
|
||||
'--json',
|
||||
'number,title,state,url,labels,updatedAt,author,isDraft,headRefName,baseRefName',
|
||||
'--repo',
|
||||
'acme/widgets',
|
||||
'--assignee',
|
||||
'@me'
|
||||
],
|
||||
{ cwd: '/repo-root' }
|
||||
)
|
||||
expect(items).toEqual([
|
||||
{
|
||||
id: 'issue:12',
|
||||
type: 'issue',
|
||||
number: 12,
|
||||
title: 'Fix bug',
|
||||
state: 'open',
|
||||
url: 'https://github.com/acme/widgets/issues/12',
|
||||
labels: [],
|
||||
updatedAt: '2026-03-29T00:00:00Z',
|
||||
author: 'octocat'
|
||||
},
|
||||
{
|
||||
id: 'pr:42',
|
||||
type: 'pr',
|
||||
number: 42,
|
||||
title: 'Add feature',
|
||||
state: 'open',
|
||||
url: 'https://github.com/acme/widgets/pull/42',
|
||||
labels: [],
|
||||
updatedAt: '2026-03-28T00:00:00Z',
|
||||
author: 'octocat',
|
||||
branchName: 'feature/add-feature',
|
||||
baseRefName: 'main'
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('routes draft queries to PR search only', async () => {
|
||||
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
|
||||
ghExecFileAsyncMock.mockResolvedValueOnce({
|
||||
stdout: JSON.stringify([
|
||||
{
|
||||
number: 7,
|
||||
title: 'Draft work',
|
||||
state: 'OPEN',
|
||||
url: 'https://github.com/acme/widgets/pull/7',
|
||||
labels: [],
|
||||
updatedAt: '2026-03-30T00:00:00Z',
|
||||
author: { login: 'octocat' },
|
||||
isDraft: true,
|
||||
headRefName: 'draft/work',
|
||||
baseRefName: 'main'
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
const items = await listWorkItems('/repo-root', 10, 'is:pr is:draft')
|
||||
|
||||
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(1)
|
||||
expect(ghExecFileAsyncMock).toHaveBeenCalledWith(
|
||||
[
|
||||
'pr',
|
||||
'list',
|
||||
'--limit',
|
||||
'10',
|
||||
'--json',
|
||||
'number,title,state,url,labels,updatedAt,author,isDraft,headRefName,baseRefName',
|
||||
'--repo',
|
||||
'acme/widgets',
|
||||
'--state',
|
||||
'open',
|
||||
'--draft'
|
||||
],
|
||||
{ cwd: '/repo-root' }
|
||||
)
|
||||
expect(items).toEqual([
|
||||
{
|
||||
id: 'pr:7',
|
||||
type: 'pr',
|
||||
number: 7,
|
||||
title: 'Draft work',
|
||||
state: 'draft',
|
||||
url: 'https://github.com/acme/widgets/pull/7',
|
||||
labels: [],
|
||||
updatedAt: '2026-03-30T00:00:00Z',
|
||||
author: 'octocat',
|
||||
branchName: 'draft/work',
|
||||
baseRefName: 'main'
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('returns open issues and PRs for the all-open preset query', async () => {
|
||||
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
|
||||
ghExecFileAsyncMock
|
||||
.mockResolvedValueOnce({
|
||||
stdout: JSON.stringify([
|
||||
{
|
||||
number: 1,
|
||||
title: 'Open issue',
|
||||
state: 'OPEN',
|
||||
url: 'https://github.com/acme/widgets/issues/1',
|
||||
labels: [],
|
||||
updatedAt: '2026-03-31T00:00:00Z',
|
||||
author: { login: 'octocat' }
|
||||
}
|
||||
])
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
stdout: JSON.stringify([
|
||||
{
|
||||
number: 2,
|
||||
title: 'Open PR',
|
||||
state: 'OPEN',
|
||||
url: 'https://github.com/acme/widgets/pull/2',
|
||||
labels: [],
|
||||
updatedAt: '2026-03-30T00:00:00Z',
|
||||
author: { login: 'octocat' },
|
||||
isDraft: false,
|
||||
headRefName: 'feature/open-pr',
|
||||
baseRefName: 'main'
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
const items = await listWorkItems('/repo-root', 10, 'is:open')
|
||||
|
||||
expect(ghExecFileAsyncMock).toHaveBeenCalledWith(
|
||||
[
|
||||
'issue',
|
||||
'list',
|
||||
'--limit',
|
||||
'10',
|
||||
'--json',
|
||||
'number,title,state,url,labels,updatedAt,author',
|
||||
'--repo',
|
||||
'acme/widgets',
|
||||
'--state',
|
||||
'open'
|
||||
],
|
||||
{ cwd: '/repo-root' }
|
||||
)
|
||||
expect(ghExecFileAsyncMock).toHaveBeenCalledWith(
|
||||
[
|
||||
'pr',
|
||||
'list',
|
||||
'--limit',
|
||||
'10',
|
||||
'--json',
|
||||
'number,title,state,url,labels,updatedAt,author,isDraft,headRefName,baseRefName',
|
||||
'--repo',
|
||||
'acme/widgets',
|
||||
'--state',
|
||||
'open'
|
||||
],
|
||||
{ cwd: '/repo-root' }
|
||||
)
|
||||
expect(items).toEqual([
|
||||
{
|
||||
id: 'issue:1',
|
||||
type: 'issue',
|
||||
number: 1,
|
||||
title: 'Open issue',
|
||||
state: 'open',
|
||||
url: 'https://github.com/acme/widgets/issues/1',
|
||||
labels: [],
|
||||
updatedAt: '2026-03-31T00:00:00Z',
|
||||
author: 'octocat'
|
||||
},
|
||||
{
|
||||
id: 'pr:2',
|
||||
type: 'pr',
|
||||
number: 2,
|
||||
title: 'Open PR',
|
||||
state: 'open',
|
||||
url: 'https://github.com/acme/widgets/pull/2',
|
||||
labels: [],
|
||||
updatedAt: '2026-03-30T00:00:00Z',
|
||||
author: 'octocat',
|
||||
branchName: 'feature/open-pr',
|
||||
baseRefName: 'main'
|
||||
}
|
||||
])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user