Files
stablyai--orca/tests/e2e/folder-setup-shallow-priority.spec.ts
2026-07-13 13:05:33 +08:00

308 lines
10 KiB
TypeScript

import { execFileSync } from 'node:child_process'
import { mkdirSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
import { mkdtemp } from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import type { ElectronApplication, Locator } from '@stablyai/playwright-test'
import { test, expect } from './helpers/orca-app'
import { waitForSessionReady } from './helpers/store'
const tempRoots: string[] = []
const IMPORT_AS_GROUP_BUTTON_NAME = 'Yes, import as group'
function initializeGitRepo(repoPath: string): void {
mkdirSync(repoPath, { recursive: true })
execFileSync('git', ['init'], { cwd: repoPath, stdio: 'pipe' })
execFileSync('git', ['config', 'user.email', 'e2e@test.local'], {
cwd: repoPath,
stdio: 'pipe'
})
execFileSync('git', ['config', 'user.name', 'E2E Test'], { cwd: repoPath, stdio: 'pipe' })
writeFileSync(path.join(repoPath, 'README.md'), `# ${path.basename(repoPath)}\n`)
execFileSync('git', ['add', 'README.md'], { cwd: repoPath, stdio: 'pipe' })
execFileSync('git', ['commit', '-m', 'Initial commit'], { cwd: repoPath, stdio: 'pipe' })
}
async function createShallowPriorityTruncationFixture(): Promise<{
parentPath: string
webClientPath: string
groupName: string
}> {
// Why: realpathSync so the paths the test asserts on match the store's
// repo.path / projectGroup.parentPath on macOS, where os.tmpdir() (/var/...)
// symlinks to /private/var/... and the app canonicalizes paths on import.
const parentPath = realpathSync(
await mkdtemp(path.join(os.tmpdir(), 'orca-e2e-shallow-priority-'))
)
tempRoots.push(parentPath)
const archivePath = path.join(parentPath, 'archive')
const webClientPath = path.join(parentPath, 'z-web-client')
for (let index = 1; index <= 101; index += 1) {
const repoName = `archived-service-${String(index).padStart(3, '0')}`
// Why: the scan only needs repo markers here; a real git init for 101
// throwaway repos makes this regression much slower without adding signal.
mkdirSync(path.join(archivePath, repoName, '.git'), { recursive: true })
}
initializeGitRepo(webClientPath)
return {
parentPath,
webClientPath,
groupName: path.basename(parentPath)
}
}
async function createCancellableScanFixture(): Promise<{
parentPath: string
apiPath: string
webPath: string
groupName: string
}> {
// Why: realpathSync so the paths the test asserts on match the store's
// canonicalized repo.path / projectGroup.parentPath on macOS (os.tmpdir()
// /var/... symlinks to /private/var/...).
const parentPath = realpathSync(
await mkdtemp(path.join(os.tmpdir(), 'orca-e2e-cancellable-scan-'))
)
tempRoots.push(parentPath)
const apiPath = path.join(parentPath, 'api')
const webPath = path.join(parentPath, 'web')
initializeGitRepo(apiPath)
initializeGitRepo(webPath)
return {
parentPath,
apiPath,
webPath,
groupName: path.basename(parentPath)
}
}
async function chooseFolderInNativeDialog(
electronApp: ElectronApplication,
folderPath: string
): Promise<void> {
await electronApp.evaluate(({ dialog }, selectedPath) => {
dialog.showOpenDialog = async () => ({
canceled: false,
filePaths: [selectedPath],
bookmarks: []
})
}, folderPath)
}
async function installCancellableNestedScanMock(
electronApp: ElectronApplication,
scan: {
selectedPath: string
selectedPathKind: 'non_git_folder'
repos: { path: string; displayName: string; depth: number }[]
truncated: boolean
timedOut: boolean
stopped: boolean
durationMs: number
maxDepth: number
maxRepos: number
timeoutMs: number | null
}
): Promise<void> {
await electronApp.evaluate(({ ipcMain }, mockedScan) => {
const pendingScans = new Map<string, () => void>()
ipcMain.removeHandler('projectGroups:scanNested')
ipcMain.removeHandler('projectGroups:cancelNestedScan')
ipcMain.handle('projectGroups:scanNested', async (event, rawArgs: unknown) => {
const args = rawArgs as { scanId?: string }
const scanId = args.scanId ?? 'missing-scan-id'
return await new Promise((resolve) => {
pendingScans.set(scanId, () =>
resolve({
...mockedScan,
stopped: true,
durationMs: mockedScan.durationMs + 1
})
)
event.sender.send('projectGroups:scanNestedProgress', {
scanId,
scan: { ...mockedScan, stopped: false }
})
})
})
ipcMain.handle('projectGroups:cancelNestedScan', (_event, rawArgs: unknown) => {
const args = rawArgs as { scanId?: string }
const scanId = args.scanId ?? ''
const resolveScan = pendingScans.get(scanId)
if (!resolveScan) {
return false
}
pendingScans.delete(scanId)
resolveScan()
return true
})
}, scan)
}
function getImportAsGroupButton(importDialog: Locator): Locator {
// Why: this test should fail on import-dialog copy drift instead of falling
// back to a retired accessible label.
return importDialog.getByRole('button', {
name: IMPORT_AS_GROUP_BUTTON_NAME,
exact: true
})
}
test.afterEach(() => {
for (const root of tempRoots.splice(0)) {
rmSync(root, { recursive: true, force: true })
}
})
test('prioritizes shallow sibling repositories in a bounded nested scan', async ({
electronApp,
orcaPage
}) => {
await waitForSessionReady(orcaPage)
const fixture = await createShallowPriorityTruncationFixture()
await chooseFolderInNativeDialog(electronApp, fixture.parentPath)
await orcaPage
.getByRole('button', { name: /Add Project/i })
.first()
.click()
const dialog = orcaPage.getByRole('dialog', { name: /Add a project/i })
await expect(dialog).toBeVisible()
await dialog.getByRole('button', { name: /Browse folder/i }).click()
const importDialog = orcaPage.getByRole('dialog', {
name: /Import repositories from folder/i
})
await expect(importDialog.getByText(/Found 100 repositories in/)).toBeVisible()
await expect(importDialog.getByText('Showing partial scan results.')).toBeVisible()
await expect(importDialog.getByText('z-web-client', { exact: true }).first()).toBeVisible()
await importDialog.getByLabel('Deselect all').click()
await importDialog
.locator('label')
.filter({ hasText: 'z-web-client' })
.locator('input[type="checkbox"]')
.check()
await getImportAsGroupButton(importDialog).click()
await expect
.poll(
() =>
orcaPage.evaluate(
(args) => {
const state = window.__store?.getState()
if (!state) {
return null
}
const group = state.projectGroups.find((entry) => entry.parentPath === args.parentPath)
const importedRepo = state.repos.find((repo) => repo.path === args.webClientPath)
return {
groupName: group?.name ?? null,
groupParentPath: group?.parentPath ?? null,
importedRepoPath: importedRepo?.path ?? null,
importedRepoName: importedRepo?.displayName ?? null,
repoInCreatedGroup: group !== undefined && importedRepo?.projectGroupId === group.id,
importedArchiveCount: state.repos.filter((repo) =>
repo.path.includes(`${args.pathSeparator}archive${args.pathSeparator}`)
).length
}
},
{ ...fixture, pathSeparator: path.sep }
),
{
timeout: 20_000,
message: 'shallow repo from truncated nested scan was not imported into the group'
}
)
.toEqual({
groupName: fixture.groupName,
groupParentPath: fixture.parentPath,
importedRepoPath: fixture.webClientPath,
importedRepoName: 'z-web-client',
repoInCreatedGroup: true,
importedArchiveCount: 0
})
await orcaPage.evaluate(() => {
const state = window.__store?.getState()
state?.closeModal()
state?.setGroupBy('repo')
})
await expect(orcaPage.getByText(fixture.groupName)).toBeVisible()
await expect(orcaPage.getByText('z-web-client').first()).toBeVisible()
})
test('can stop a nested repo scan and import repositories found so far', async ({
electronApp,
orcaPage
}) => {
await waitForSessionReady(orcaPage)
const fixture = await createCancellableScanFixture()
await installCancellableNestedScanMock(electronApp, {
selectedPath: fixture.parentPath,
selectedPathKind: 'non_git_folder',
repos: [{ path: fixture.apiPath, displayName: 'api', depth: 1 }],
truncated: false,
timedOut: false,
stopped: false,
durationMs: 25,
maxDepth: 3,
maxRepos: 100,
timeoutMs: null
})
await chooseFolderInNativeDialog(electronApp, fixture.parentPath)
await orcaPage
.getByRole('button', { name: /Add Project/i })
.first()
.click()
const dialog = orcaPage.getByRole('dialog', { name: /Add a project/i })
await dialog.getByRole('button', { name: /Browse folder/i }).click()
const importDialog = orcaPage.getByRole('dialog', {
name: /Import repositories from folder/i
})
await expect(importDialog.getByText(/Scanning\.\.\.\s*Found 1 repository in/)).toBeVisible()
await expect(getImportAsGroupButton(importDialog)).toBeDisabled()
await importDialog.getByRole('button', { name: /Stop scan/i }).click()
await expect(importDialog.getByText('Scan stopped early.')).toBeVisible()
await expect(importDialog.getByText(/Found 1 repository in/)).toBeVisible()
await expect(getImportAsGroupButton(importDialog)).toBeEnabled()
await getImportAsGroupButton(importDialog).click()
await expect
.poll(
() =>
orcaPage.evaluate((args) => {
const state = window.__store?.getState()
if (!state) {
return null
}
const group = state.projectGroups.find((entry) => entry.parentPath === args.parentPath)
const apiRepo = state.repos.find((repo) => repo.path === args.apiPath)
const webRepo = state.repos.find((repo) => repo.path === args.webPath)
return {
groupName: group?.name ?? null,
importedApiPath: apiRepo?.path ?? null,
apiInCreatedGroup: group !== undefined && apiRepo?.projectGroupId === group.id,
importedWebPath: webRepo?.path ?? null
}
}, fixture),
{
timeout: 20_000,
message: 'stopped nested scan partial result was not imported'
}
)
.toEqual({
groupName: fixture.groupName,
importedApiPath: fixture.apiPath,
apiInCreatedGroup: true,
importedWebPath: null
})
})