Files
orca/tests/e2e/helpers/golden-source-control.ts
T
Jinjing e84fb46eaf test: add golden E2E tests for source control workflows (#14260)
* test: add golden E2E tests for source control workflows

- Tests core source control interactions: file edit/save, commit staging, and diff viewing
- Integrated into CI/CD pipelines for Linux, macOS, and Windows
- Includes helper utilities for test setup and worktree management

* test(e2e): verify golden commit author and fix test flakiness

- Configure git author name/email at worktree level during setup
- Verify commits are made with correct author details in assertions
- Add explicit timeouts to file visibility waits and git status polling
- Fix test ordering to seed edits after source control is open
- Simplify git status refresh logic to rely on automatic updates

* Add rollback to createGoldenWorktree on setup failure

Cleanup callbacks only register after setup succeeds. When a config
command fails, the half-built worktree and branch leak into later
test runs, causing flakiness. Now we roll back immediately and
re-throw the setup error.

* test(e2e): match explorer rows after the git status badge appears

The golden file-save spec used an exact /^README.md$/ filter. After save,
the explorer row text becomes "README.md M", so reopen clicked nothing.

* test: strengthen golden worktree setup verification

- Track working directory in git call inspection to verify correct execution context
- Verify user.name/email config applies to worktree-specific settings, not repo
- Add exhaustive setup call sequence assertions to catch setup/rollback leaks
2026-08-13 09:56:30 -07:00

184 lines
6.4 KiB
TypeScript

import { execFileSync } from 'node:child_process'
import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import type { Page } from '@stablyai/playwright-test'
import { expect } from './orca-app'
export const GOLDEN_CHANGED_PATH = 'src/index.ts'
export const GOLDEN_REMOVED_LINE = 'export const hello = "world"'
export const GOLDEN_ADDED_LINE = 'export const hello = "golden daily loop"'
export const GOLDEN_GIT_AUTHOR_NAME = 'Orca E2E'
export const GOLDEN_GIT_AUTHOR_EMAIL = 'orca-e2e@example.invalid'
const GOLDEN_PRE_COMMIT_MARKER = '.e2e-pre-commit-ran'
export type GoldenWorktree = {
branchName: string
hooksPath?: string
worktreePath: string
}
export function createGoldenWorktree(repoPath: string, label: string): GoldenWorktree {
const suffix = `${Date.now()}-${Math.random().toString(16).slice(2)}`
const branchName = `e2e-golden-${label}-${suffix}`
const worktreePath = path.join(os.tmpdir(), branchName)
execFileSync('git', ['worktree', 'add', worktreePath, '-b', branchName], {
cwd: repoPath,
stdio: 'pipe'
})
const fixture: GoldenWorktree = { branchName, worktreePath }
// Callers only register cleanup once this returns, so roll back here or the
// half-built worktree and branch leak into every later run.
try {
execFileSync('git', ['config', 'extensions.worktreeConfig', 'true'], {
cwd: worktreePath,
stdio: 'pipe'
})
execFileSync('git', ['config', '--worktree', 'user.name', GOLDEN_GIT_AUTHOR_NAME], {
cwd: worktreePath,
stdio: 'pipe'
})
execFileSync('git', ['config', '--worktree', 'user.email', GOLDEN_GIT_AUTHOR_EMAIL], {
cwd: worktreePath,
stdio: 'pipe'
})
} catch (setupError) {
try {
cleanupGoldenWorktree(repoPath, fixture)
} catch {
// Keep the setup failure as the reported cause.
}
throw setupError
}
return fixture
}
export function cleanupGoldenWorktree(repoPath: string, fixture: GoldenWorktree): void {
try {
execFileSync('git', ['config', '--worktree', '--unset-all', 'core.hooksPath'], {
cwd: fixture.worktreePath,
stdio: 'pipe'
})
} catch {
// The hook setting may not have been installed before setup failed.
}
if (fixture.hooksPath) {
rmSync(fixture.hooksPath, { recursive: true, force: true })
}
try {
execFileSync('git', ['worktree', 'remove', '--force', fixture.worktreePath], {
cwd: repoPath,
stdio: 'pipe'
})
} catch {
rmSync(fixture.worktreePath, { recursive: true, force: true })
execFileSync('git', ['worktree', 'prune'], { cwd: repoPath, stdio: 'pipe' })
}
execFileSync('git', ['branch', '-D', fixture.branchName], { cwd: repoPath, stdio: 'pipe' })
}
export function seedGoldenSourceEdit(worktreePath: string): void {
const changedPath = path.join(worktreePath, GOLDEN_CHANGED_PATH)
const original = readFileSync(changedPath, 'utf8')
if (!original.includes(GOLDEN_REMOVED_LINE)) {
throw new Error(`Golden source fixture is missing: ${GOLDEN_REMOVED_LINE}`)
}
writeFileSync(changedPath, original.replace(GOLDEN_REMOVED_LINE, GOLDEN_ADDED_LINE))
}
export function installPassingNodePreCommitHook(fixture: GoldenWorktree): string {
const hooksPath = mkdtempSync(path.join(os.tmpdir(), 'orca-e2e-git-hooks-'))
const hookPath = path.join(hooksPath, 'pre-commit')
const markerPath = path.join(hooksPath, GOLDEN_PRE_COMMIT_MARKER)
fixture.hooksPath = hooksPath
execFileSync('git', ['config', '--worktree', 'core.hooksPath', hooksPath], {
cwd: fixture.worktreePath,
stdio: 'pipe'
})
writeFileSync(
hookPath,
`#!/bin/sh\nnode -e "const fs = require('node:fs'); const path = require('node:path'); fs.writeFileSync(path.join(path.dirname(process.argv[1]), '${GOLDEN_PRE_COMMIT_MARKER}'), 'ran')" "$0"\n`
)
chmodSync(hookPath, 0o755)
return markerPath
}
export async function activateGoldenWorktree(
page: Page,
repoPath: string,
worktreePath: string
): Promise<void> {
await expect
.poll(
() =>
page.evaluate(
async ({ registeredRepoPath, targetWorktreePath }) => {
const store = window.__store
if (!store) {
throw new Error('window.__store is not available')
}
await store.getState().fetchRepos()
const normalize = (value: string): string => {
const normalized = value
.replace(/\\/g, '/')
.replace(/^\/private(?=\/var\/)/, '')
.replace(/\/+$/, '')
return navigator.userAgent.includes('Windows') ? normalized.toLowerCase() : normalized
}
const repo = store
.getState()
.repos.find((entry) => normalize(entry.path) === normalize(registeredRepoPath))
if (!repo) {
return false
}
await store.getState().fetchWorktrees(repo.id)
const worktree = (store.getState().worktreesByRepo[repo.id] ?? []).find(
(entry) => normalize(entry.path) === normalize(targetWorktreePath)
)
if (!worktree) {
return false
}
store.getState().setActiveRepo(repo.id)
store.getState().setActiveWorktree(worktree.id)
return true
},
{ registeredRepoPath: repoPath, targetWorktreePath: worktreePath }
),
{ timeout: 10_000, message: `Golden worktree did not load: ${worktreePath}` }
)
.toBe(true)
}
export async function openGoldenSourceControl(
page: Page,
repoPath: string,
fixture: GoldenWorktree
): Promise<void> {
await activateGoldenWorktree(page, repoPath, fixture.worktreePath)
await page.evaluate(() => {
const store = window.__store
if (!store) {
throw new Error('window.__store is not available')
}
const state = store.getState()
state.setRightSidebarTab('explorer')
state.setRightSidebarOpen(true)
})
await page.getByRole('button', { name: /Source Control/ }).click()
await expect(page.getByRole('textbox', { name: 'Commit message' })).toBeVisible({
timeout: 10_000
})
await expect
.poll(
() =>
page.evaluate(() => {
const state = window.__store?.getState()
return state?.activeWorktreeId
? Object.hasOwn(state.gitStatusByWorktree, state.activeWorktreeId)
: false
}),
{ timeout: 10_000, message: 'Automatic Git status refresh did not complete' }
)
.toBe(true)
}