mirror of
https://github.com/stablyai/orca.git
synced 2026-11-05 19:47:27 +00:00
* [comment audit] Shorten daemon, hook, and emulator comments [skip ci] * [comment audit] Shorten VM, conversation, SQLite, and theme-import comments [skip ci] * [comment audit] Tighten Git execution and source-control comment contracts [skip ci] * [comment audit] Clarify repository probes and maintenance comments [skip ci] * [comment audit] Shorten status, diff-cache, and checkout comments [skip ci] * [comment audit] Clarify worktree lifecycle and WSL routing comments [skip ci] * [comment audit] Shorten forge identity and review-refresh comments [skip ci] * [comment audit] Tighten hosted-review client comment contracts [skip ci] * [comment audit] Clarify GitLab compatibility and review comments [skip ci] * [comment audit] Tighten hook ownership and watchdog rationale [skip ci] * [comment audit] Condense host and integration compatibility comments [skip ci] * [comment audit] Clarify IPC authority and remote-routing rationale [skip ci] * [comment audit] Finalize file-import and containment comment cleanup * [comment audit] Fix stale Jira type imports blocking full CI --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
319 lines
11 KiB
TypeScript
319 lines
11 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const { gitExecFileAsyncMock } = vi.hoisted(() => ({
|
|
gitExecFileAsyncMock:
|
|
vi.fn<(args: string[], opts: { cwd: string }) => Promise<{ stdout: string }>>()
|
|
}))
|
|
|
|
vi.mock('./runner', () => ({
|
|
gitExecFileAsync: gitExecFileAsyncMock,
|
|
translateWslOutputPaths: (output: string) => output
|
|
}))
|
|
|
|
import {
|
|
LOCAL_BASE_REF_REFRESH_WAIT_MS,
|
|
refreshLocalBaseRefForWorktreeCreate
|
|
} from './worktree-base-refresh'
|
|
|
|
const INDEX_LOCK_ERROR = Object.assign(new Error('Command failed: git merge --ff-only'), {
|
|
stderr:
|
|
"fatal: Unable to create '/repo/.git/index.lock': File exists.\n\nAnother git process seems to be running in this repository"
|
|
})
|
|
const OWNER_WORKTREE_LIST = 'worktree /repo\nHEAD old-main\nbranch refs/heads/main\n'
|
|
|
|
type GitFake = {
|
|
owner?: boolean
|
|
mutate: () => Promise<{ stdout: string }>
|
|
/** Whether local already contains the target when a failed move is re-checked. */
|
|
localContainsTarget?: () => boolean
|
|
ownerHead?: () => string
|
|
}
|
|
|
|
/** The git subcommand, past leading `-c key=value` and `--flag` globals. */
|
|
function commandOf(args: readonly string[]): string {
|
|
for (let index = 0; index < args.length; index += 1) {
|
|
if (args[index] === '-c') {
|
|
index += 1
|
|
} else if (!args[index].startsWith('-')) {
|
|
return args[index]
|
|
}
|
|
}
|
|
return ''
|
|
}
|
|
|
|
function installGitFake(fake: GitFake): void {
|
|
let localMain = 'old-main'
|
|
gitExecFileAsyncMock.mockImplementation(async (args: string[]) => {
|
|
const command = commandOf(args)
|
|
if (command === 'rev-parse') {
|
|
return { stdout: args[2] === 'refs/heads/main^{commit}' ? `${localMain}\n` : 'remote-main\n' }
|
|
}
|
|
if (command === 'rev-list') {
|
|
return { stdout: '0\t3\n' }
|
|
}
|
|
if (command === 'status') {
|
|
return { stdout: '' }
|
|
}
|
|
if (command === 'worktree') {
|
|
return { stdout: fake.owner ? OWNER_WORKTREE_LIST : '' }
|
|
}
|
|
if (command === 'symbolic-ref') {
|
|
return { stdout: `${fake.ownerHead?.() ?? 'refs/heads/main'}\n` }
|
|
}
|
|
if (command === 'merge-base') {
|
|
if (fake.localContainsTarget?.()) {
|
|
return { stdout: '' }
|
|
}
|
|
throw Object.assign(new Error('not an ancestor'), { code: 1 })
|
|
}
|
|
if (command === 'merge' || command === 'update-ref') {
|
|
const result = await fake.mutate()
|
|
localMain = 'remote-main'
|
|
return result
|
|
}
|
|
throw new Error(`unexpected git ${args.join(' ')}`)
|
|
})
|
|
}
|
|
|
|
function refresh(repoPath = '/repo') {
|
|
return refreshLocalBaseRefForWorktreeCreate(repoPath, 'origin/main', 'refs/remotes/origin/main')
|
|
}
|
|
|
|
function mutationCalls(): string[][] {
|
|
return gitExecFileAsyncMock.mock.calls
|
|
.map(([args]) => args)
|
|
.filter((args) => ['merge', 'update-ref', 'reset'].includes(commandOf(args)))
|
|
}
|
|
|
|
describe('refreshLocalBaseRefForWorktreeCreate lock contention', () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers()
|
|
gitExecFileAsyncMock.mockReset()
|
|
})
|
|
afterEach(() => vi.useRealTimers())
|
|
|
|
it('retries a fast-forward that lost the index.lock race and reports updated', async () => {
|
|
const mutate = vi
|
|
.fn()
|
|
.mockRejectedValueOnce(INDEX_LOCK_ERROR)
|
|
.mockResolvedValueOnce({ stdout: '' })
|
|
installGitFake({ owner: true, mutate })
|
|
|
|
const pending = refresh()
|
|
await vi.runAllTimersAsync()
|
|
|
|
await expect(pending).resolves.toEqual({
|
|
baseRef: 'origin/main',
|
|
localBranch: 'main',
|
|
status: 'updated',
|
|
ownerWorktreePath: '/repo'
|
|
})
|
|
expect(mutationCalls().map(commandOf)).toEqual(['merge', 'merge'])
|
|
expect(mutationCalls().map((args) => args.at(-1))).toEqual(['remote-main', 'remote-main'])
|
|
})
|
|
|
|
it('re-confirms the owner has the branch checked out before each retry', async () => {
|
|
let symbolicRefReads = 0
|
|
installGitFake({
|
|
owner: true,
|
|
mutate: () => Promise.reject(INDEX_LOCK_ERROR),
|
|
ownerHead: () => (++symbolicRefReads >= 2 ? 'refs/heads/develop' : 'refs/heads/main')
|
|
})
|
|
|
|
const pending = refresh()
|
|
await vi.runAllTimersAsync()
|
|
|
|
await expect(pending).resolves.toMatchObject({ status: 'skipped_error' })
|
|
expect(mutationCalls()).toHaveLength(1)
|
|
})
|
|
|
|
it('reports updated without a warning when the lock never clears but local already reached the target', async () => {
|
|
installGitFake({
|
|
owner: true,
|
|
mutate: () => Promise.reject(INDEX_LOCK_ERROR),
|
|
localContainsTarget: () => true
|
|
})
|
|
|
|
const pending = refresh()
|
|
await vi.runAllTimersAsync()
|
|
|
|
await expect(pending).resolves.toMatchObject({ status: 'updated' })
|
|
expect(mutationCalls()).toHaveLength(4)
|
|
})
|
|
|
|
it('reports skipped_error when the lock never clears and local is still behind', async () => {
|
|
installGitFake({ owner: true, mutate: () => Promise.reject(INDEX_LOCK_ERROR) })
|
|
|
|
const pending = refresh()
|
|
await vi.runAllTimersAsync()
|
|
|
|
await expect(pending).resolves.toMatchObject({ status: 'skipped_error' })
|
|
expect(mutationCalls()).toHaveLength(4)
|
|
})
|
|
|
|
it('does not retry a failure that is not lock contention', async () => {
|
|
const casMismatch = Object.assign(new Error('Command failed: git update-ref'), {
|
|
stderr: "fatal: cannot lock ref 'refs/heads/main': is at other-oid but expected old-main"
|
|
})
|
|
installGitFake({ mutate: () => Promise.reject(casMismatch) })
|
|
|
|
const pending = refresh()
|
|
await vi.runAllTimersAsync()
|
|
|
|
await expect(pending).resolves.toMatchObject({ status: 'skipped_error' })
|
|
expect(mutationCalls()).toEqual([
|
|
[
|
|
'update-ref',
|
|
'-m',
|
|
'orca: fast-forward to refs/remotes/origin/main',
|
|
'refs/heads/main',
|
|
'remote-main',
|
|
'old-main'
|
|
]
|
|
])
|
|
})
|
|
|
|
it('treats a lost update-ref race as success when someone else moved local to the target', async () => {
|
|
const casMismatch = Object.assign(new Error('Command failed: git update-ref'), {
|
|
stderr: "fatal: cannot lock ref 'refs/heads/main': is at remote-main but expected old-main"
|
|
})
|
|
installGitFake({ mutate: () => Promise.reject(casMismatch), localContainsTarget: () => true })
|
|
|
|
await expect(refresh()).resolves.toEqual({
|
|
baseRef: 'origin/main',
|
|
localBranch: 'main',
|
|
status: 'updated'
|
|
})
|
|
expect(mutationCalls()).toHaveLength(1)
|
|
})
|
|
|
|
it('skips the owner check and the move when local is already current', async () => {
|
|
installGitFake({ owner: true, mutate: () => Promise.resolve({ stdout: '' }) })
|
|
const base = gitExecFileAsyncMock.getMockImplementation()!
|
|
gitExecFileAsyncMock.mockImplementation(async (args: string[], opts: { cwd: string }) =>
|
|
args[0] === 'rev-parse' ? { stdout: 'remote-main\n' } : base(args, opts)
|
|
)
|
|
|
|
await expect(refresh()).resolves.toBeUndefined()
|
|
expect(gitExecFileAsyncMock.mock.calls.map(([args]) => args[0])).toEqual([
|
|
'rev-parse',
|
|
'rev-parse'
|
|
])
|
|
})
|
|
|
|
it("runs the shared refresh without one create's abort signal or timeout", async () => {
|
|
installGitFake({ owner: true, mutate: () => Promise.resolve({ stdout: '' }) })
|
|
|
|
await refreshLocalBaseRefForWorktreeCreate(
|
|
'/repo',
|
|
'origin/main',
|
|
'refs/remotes/origin/main',
|
|
undefined,
|
|
{ wslDistro: 'Ubuntu', signal: new AbortController().signal, timeout: 8000 }
|
|
)
|
|
|
|
for (const [, options] of gitExecFileAsyncMock.mock.calls) {
|
|
expect(options).toEqual({ cwd: expect.any(String), wslDistro: 'Ubuntu' })
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('refreshLocalBaseRefForWorktreeCreate runs one refresh at a time per branch', () => {
|
|
beforeEach(() => {
|
|
gitExecFileAsyncMock.mockReset()
|
|
})
|
|
afterEach(() => vi.useRealTimers())
|
|
|
|
function installRepoFake(repoPath: string) {
|
|
let localOid = 'old-main'
|
|
let finishMerge!: () => void
|
|
const mergeStarted = new Promise<void>((resolve) => {
|
|
gitExecFileAsyncMock.mockImplementation(async (args: string[], opts: { cwd: string }) => {
|
|
const command = commandOf(args)
|
|
if (opts.cwd.replace(/\/+$/, '') !== repoPath) {
|
|
return { stdout: command === 'rev-parse' ? 'current\n' : '' }
|
|
}
|
|
if (command === 'rev-parse') {
|
|
return { stdout: args[2]?.startsWith('refs/heads/') ? `${localOid}\n` : 'remote-main\n' }
|
|
}
|
|
if (command === 'rev-list') {
|
|
return { stdout: '0\t3\n' }
|
|
}
|
|
if (command === 'worktree') {
|
|
return { stdout: `worktree ${repoPath}\nHEAD ${localOid}\nbranch refs/heads/main\n` }
|
|
}
|
|
if (command === 'symbolic-ref') {
|
|
return { stdout: 'refs/heads/main\n' }
|
|
}
|
|
if (command === 'merge') {
|
|
resolve()
|
|
await new Promise<void>((release) => {
|
|
finishMerge = release
|
|
})
|
|
localOid = 'remote-main'
|
|
}
|
|
return { stdout: '' }
|
|
})
|
|
})
|
|
return { mergeStarted, finishMerge: () => finishMerge() }
|
|
}
|
|
|
|
it('folds creates that arrive mid-refresh into one follow-up that finds local current', async () => {
|
|
const repo = installRepoFake('/repo')
|
|
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
|
|
const first = refresh('/repo')
|
|
const joiners = [refresh('/repo/'), refresh('/repo')]
|
|
await repo.mergeStarted
|
|
const callsWhileFirstMerges = gitExecFileAsyncMock.mock.calls.length
|
|
repo.finishMerge()
|
|
|
|
await expect(first).resolves.toMatchObject({ status: 'updated' })
|
|
await expect(Promise.all(joiners)).resolves.toEqual([undefined, undefined])
|
|
const commands = gitExecFileAsyncMock.mock.calls.map(([args]) => commandOf(args))
|
|
// Joiners share a follow-up inspection only after the first run releases the checkout.
|
|
expect(commands.slice(0, callsWhileFirstMerges).filter((c) => c === 'rev-list')).toHaveLength(1)
|
|
expect(commands.slice(callsWhileFirstMerges)).toEqual(['rev-parse', 'rev-parse', 'rev-parse'])
|
|
expect(mutationCalls()).toHaveLength(1)
|
|
expect(warn).not.toHaveBeenCalled()
|
|
warn.mockRestore()
|
|
})
|
|
|
|
it('does not queue refreshes of different repos behind each other', async () => {
|
|
const repo = installRepoFake('/repo')
|
|
|
|
const first = refresh('/repo')
|
|
await repo.mergeStarted
|
|
await expect(refresh('/other')).resolves.toBeUndefined()
|
|
repo.finishMerge()
|
|
await expect(first).resolves.toMatchObject({ status: 'updated' })
|
|
})
|
|
|
|
it('stops waiting on a wedged refresh after the bound without starting a second move', async () => {
|
|
vi.useFakeTimers()
|
|
const repo = installRepoFake('/repo')
|
|
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
let secondResult: unknown = 'pending'
|
|
|
|
const first = refresh('/repo')
|
|
await repo.mergeStarted
|
|
await vi.advanceTimersByTimeAsync(10_000)
|
|
const second = refresh('/repo').then((result) => (secondResult = result))
|
|
|
|
await vi.advanceTimersByTimeAsync(LOCAL_BASE_REF_REFRESH_WAIT_MS - 1)
|
|
expect(secondResult).toBe('pending')
|
|
await vi.advanceTimersByTimeAsync(1)
|
|
await second
|
|
expect(secondResult).toBeUndefined()
|
|
await expect(first).resolves.toBeUndefined()
|
|
expect(mutationCalls()).toHaveLength(1)
|
|
expect(warn).toHaveBeenCalledWith(expect.stringContaining('stopped waiting'))
|
|
|
|
repo.finishMerge()
|
|
await vi.runAllTimersAsync()
|
|
await vi.waitFor(() => expect(gitExecFileAsyncMock.mock.calls.at(-1)?.[0][0]).toBe('rev-parse'))
|
|
expect(mutationCalls()).toHaveLength(1)
|
|
warn.mockRestore()
|
|
})
|
|
})
|