mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 02:17:31 +00:00
* fix(sidebar): keep discovered worktrees hidden when adding a project Adding a project no longer switches it to show worktrees Orca didn't create (Conductor workspaces and other tools' worktrees). They stay behind the sidebar's "Hiding N discovered worktrees" line, the same as every other way a project is added. * fix(sidebar): open a picked linked worktree instead of revealing its siblings When the folder added as a project is itself a linked worktree, its main checkout is hidden. Open the picked worktree instead of switching the whole project to show worktrees Orca didn't create. * fix(repos): register clone and create paths with symlinks resolved Git reports checkout paths with symlinks resolved. Clone and create saved the typed path, so a project cloned under a symlinked folder (e.g. /tmp on macOS) did not recognise its own checkout and hid it as a discovered worktree. Resolve the existing part of the destination before registering, as adding an existing folder already does. * fix(repos): keep finding projects saved under a symlinked clone or create path Projects cloned or created before paths were resolved keep the typed spelling. Match both spellings so retrying the same clone or create reuses the saved project instead of failing on its non-empty folder. * fix(repos): keep Windows clone and create paths as typed Windows Git reports the typed spelling, and resolving would turn mapped drives into UNC paths. Runtime clone/create tests now use resolved temp roots, matching what registration saves. * fix(repos): register Git's checkout path for clone and create Replace the parent-symlink resolution with the path Git reports after clone or git init, as adding an existing folder already does. This also covers a symlink at the target folder itself and WSL paths, leaves plain folders and Windows spellings unchanged, and matches saved projects by both the typed and the resolved spelling. * fix(sidebar): keep opening projects whose own checkout isn't recognised Projects saved through a symlinked path by older builds or paired hosts don't recognise their own checkout. Keep main's behaviour for that case: show the project so the add still opens it. * fix(repos): match resolved paths when importing a folder of repos Clone and create now save Git's resolved checkout path, so a nested import through a symlinked folder must also match that spelling instead of registering a duplicate project. * fix(repos): find saved projects through a symlink before the clone target exists Resolve a missing clone target through its existing ancestors, reusing the worktree-create alias resolver, and read the lookup keys inside the clone lock. Queued clones through a symlinked folder reuse the first clone, and a different repository at a saved project whose folder is gone is refused before cloning, as on main. * fix(repos): match a folder import's resolved path only to local projects The resolved spelling names this machine's folder, so a remote (SSH) project at the same path string must not be treated as the import. * fix(repos): limit a desktop folder import's resolved-path match to this machine Runtime-owned and SSH projects can share the path string; only projects on this machine may match the import's resolved folder. * fix(repos): read a nested import row's connection through the host helper * fix(repos): match only this machine's projects when creating a local project
190 lines
7.6 KiB
TypeScript
190 lines
7.6 KiB
TypeScript
import { openSidebarProjectDialog } from './helpers/sidebar-project-dialog'
|
|
import { execFileSync } from 'node:child_process'
|
|
import { mkdirSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
|
import { mkdtemp } from 'node:fs/promises'
|
|
import os from 'node:os'
|
|
import path from 'node:path'
|
|
import { test, expect } from './helpers/orca-app'
|
|
import { waitForSessionReady } from './helpers/store'
|
|
|
|
const tempRoots: string[] = []
|
|
|
|
async function createCloneFixture(): Promise<{
|
|
sourcePath: string
|
|
destinationParent: string
|
|
}> {
|
|
// Canonicalize macOS /var → /private/var like git does when registering repo.path.
|
|
const rootPath = realpathSync(
|
|
await mkdtemp(path.join(os.tmpdir(), 'orca-e2e-add-project-clone-'))
|
|
)
|
|
tempRoots.push(rootPath)
|
|
|
|
const sourcePath = path.join(rootPath, 'default-checkout-source')
|
|
// Clone through a symlinked parent: git reports the resolved path, which must still be the project's own checkout.
|
|
const destinationParent = path.join(rootPath, 'clones-link')
|
|
|
|
mkdirSync(sourcePath, { recursive: true })
|
|
mkdirSync(path.join(rootPath, 'clones'), { recursive: true })
|
|
symlinkSync(path.join(rootPath, 'clones'), destinationParent, 'junction')
|
|
execFileSync('git', ['init'], { cwd: sourcePath, stdio: 'pipe' })
|
|
execFileSync('git', ['config', 'user.email', 'e2e@test.local'], {
|
|
cwd: sourcePath,
|
|
stdio: 'pipe'
|
|
})
|
|
execFileSync('git', ['config', 'user.name', 'E2E Test'], { cwd: sourcePath, stdio: 'pipe' })
|
|
writeFileSync(path.join(sourcePath, 'README.md'), '# Default checkout source\n')
|
|
execFileSync('git', ['add', 'README.md'], { cwd: sourcePath, stdio: 'pipe' })
|
|
execFileSync('git', ['commit', '-m', 'Initial commit'], { cwd: sourcePath, stdio: 'pipe' })
|
|
execFileSync('git', ['branch', '-M', 'main'], { cwd: sourcePath, stdio: 'pipe' })
|
|
|
|
return { sourcePath, destinationParent }
|
|
}
|
|
|
|
async function createLinkedWorktreeFixture(): Promise<{
|
|
mainPath: string
|
|
siblingPath: string
|
|
}> {
|
|
// Canonicalize the main checkout path like git does on macOS /var symlinks.
|
|
const rootPath = realpathSync(
|
|
await mkdtemp(path.join(os.tmpdir(), 'orca-e2e-add-project-linked-'))
|
|
)
|
|
tempRoots.push(rootPath)
|
|
|
|
const mainPath = path.join(rootPath, 'linked-source')
|
|
const siblingPath = path.join(rootPath, 'linked-feature')
|
|
|
|
mkdirSync(mainPath, { recursive: true })
|
|
execFileSync('git', ['init'], { cwd: mainPath, stdio: 'pipe' })
|
|
execFileSync('git', ['config', 'user.email', 'e2e@test.local'], {
|
|
cwd: mainPath,
|
|
stdio: 'pipe'
|
|
})
|
|
execFileSync('git', ['config', 'user.name', 'E2E Test'], { cwd: mainPath, stdio: 'pipe' })
|
|
writeFileSync(path.join(mainPath, 'README.md'), '# Linked source\n')
|
|
execFileSync('git', ['add', 'README.md'], { cwd: mainPath, stdio: 'pipe' })
|
|
execFileSync('git', ['commit', '-m', 'Initial commit'], { cwd: mainPath, stdio: 'pipe' })
|
|
execFileSync('git', ['branch', '-M', 'main'], { cwd: mainPath, stdio: 'pipe' })
|
|
execFileSync('git', ['worktree', 'add', '-b', 'feature', siblingPath], {
|
|
cwd: mainPath,
|
|
stdio: 'pipe'
|
|
})
|
|
|
|
return { mainPath, siblingPath }
|
|
}
|
|
|
|
test.afterEach(() => {
|
|
for (const root of tempRoots.splice(0)) {
|
|
rmSync(root, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
test.describe('Add project default checkout', () => {
|
|
test('clones a repo and opens the default checkout without the setup-choice modal', async ({
|
|
orcaPage
|
|
}) => {
|
|
await waitForSessionReady(orcaPage)
|
|
const fixture = await createCloneFixture()
|
|
|
|
await openSidebarProjectDialog(orcaPage)
|
|
const addDialog = orcaPage.getByRole('dialog', { name: /Add a project/i })
|
|
await expect(addDialog).toBeVisible()
|
|
await addDialog.getByRole('button', { name: /Clone from URL/i }).click()
|
|
|
|
const cloneDialog = orcaPage.getByRole('dialog', { name: /Clone from URL/i })
|
|
await expect(cloneDialog).toBeVisible()
|
|
await cloneDialog.getByPlaceholder('https://github.com/user/repo.git').fill(fixture.sourcePath)
|
|
await cloneDialog.getByPlaceholder('/path/to/destination').fill(fixture.destinationParent)
|
|
await cloneDialog.getByRole('button', { name: /^Clone$/ }).click()
|
|
|
|
await expect(orcaPage.getByRole('dialog', { name: /Repo added/i })).toBeHidden()
|
|
await expect(orcaPage.getByText('Use existing worktrees')).toBeHidden()
|
|
await expect(orcaPage.getByText('Create a new worktree')).toBeHidden()
|
|
|
|
await expect
|
|
.poll(
|
|
() =>
|
|
orcaPage.evaluate((cloneName) => {
|
|
const state = window.__store?.getState()
|
|
if (!state) {
|
|
return null
|
|
}
|
|
const repo = state.repos.find((candidate) => candidate.displayName === cloneName)
|
|
if (!repo) {
|
|
return null
|
|
}
|
|
const worktrees = state.worktreesByRepo[repo.id] ?? []
|
|
const defaultCheckout = worktrees.find((worktree) => worktree.isMainWorktree)
|
|
const normalizedDefaultCheckoutPath = defaultCheckout?.path.replace(/\\/g, '/') ?? null
|
|
return {
|
|
activeCheckoutIsDefault: state.activeWorktreeId === defaultCheckout?.id,
|
|
defaultCheckoutLooksCloned:
|
|
normalizedDefaultCheckoutPath?.endsWith(`/clones/${cloneName}`) ?? false,
|
|
oldSetupModalOpen: state.activeModal === 'project-added'
|
|
}
|
|
}, path.basename(fixture.sourcePath)),
|
|
{
|
|
timeout: 30_000,
|
|
message: 'cloned repo default checkout was not opened'
|
|
}
|
|
)
|
|
.toEqual({
|
|
activeCheckoutIsDefault: true,
|
|
defaultCheckoutLooksCloned: true,
|
|
oldSetupModalOpen: false
|
|
})
|
|
await expect(orcaPage.getByText(/discovered worktree/)).toHaveCount(0)
|
|
})
|
|
|
|
test('keeps sibling git worktrees hidden when opening the default checkout', async ({
|
|
orcaPage
|
|
}) => {
|
|
await waitForSessionReady(orcaPage)
|
|
const fixture = await createLinkedWorktreeFixture()
|
|
|
|
await orcaPage.evaluate((folderPath) => {
|
|
window.__store?.getState().openModal('confirm-add-project-from-folder', { folderPath })
|
|
}, fixture.mainPath)
|
|
const addProjectDialog = orcaPage.getByRole('dialog', { name: /^Add Project$/i })
|
|
await expect(addProjectDialog).toBeVisible()
|
|
await addProjectDialog.getByRole('button', { name: /^Add Project$/ }).click()
|
|
|
|
await expect(addProjectDialog).toBeHidden()
|
|
await expect(orcaPage.getByRole('dialog', { name: /Repo added/i })).toBeHidden()
|
|
await expect(orcaPage.getByText('Use existing worktrees')).toBeHidden()
|
|
|
|
await expect
|
|
.poll(
|
|
() =>
|
|
orcaPage.evaluate((mainPath) => {
|
|
const state = window.__store?.getState()
|
|
if (!state) {
|
|
return null
|
|
}
|
|
const repo = state.repos.find((candidate) => candidate.path === mainPath)
|
|
if (!repo) {
|
|
return null
|
|
}
|
|
const worktrees = state.worktreesByRepo[repo.id] ?? []
|
|
const defaultCheckout = worktrees.find((worktree) => worktree.isMainWorktree)
|
|
return {
|
|
activeCheckoutIsDefault: state.activeWorktreeId === defaultCheckout?.id,
|
|
linkedRepoVisibility: repo.externalWorktreeVisibility,
|
|
visibleBranches: worktrees.map((worktree) => worktree.branch).sort(),
|
|
visibleCount: worktrees.length
|
|
}
|
|
}, fixture.mainPath),
|
|
{
|
|
timeout: 30_000,
|
|
message: 'default checkout did not open with sibling worktrees hidden'
|
|
}
|
|
)
|
|
.toEqual({
|
|
activeCheckoutIsDefault: true,
|
|
linkedRepoVisibility: undefined,
|
|
visibleBranches: ['refs/heads/main'],
|
|
visibleCount: 1
|
|
})
|
|
await expect(orcaPage.getByText('Hiding 1 discovered worktree')).toBeVisible()
|
|
})
|
|
})
|