orca/src/main/providers/ssh-git-provider.test.ts

1371 lines
44 KiB
TypeScript

/* eslint-disable max-lines -- Why: this suite covers the SSH git provider's one-RPC-per-method contract; splitting it would duplicate the shared mux fixture. */
import { describe, expect, it, vi, beforeEach } from 'vitest'
import { SshGitProvider } from './ssh-git-provider'
type MockMultiplexer = {
request: ReturnType<typeof vi.fn>
notify: ReturnType<typeof vi.fn>
onNotification: ReturnType<typeof vi.fn>
onNotificationByMethod: ReturnType<typeof vi.fn>
dispose: ReturnType<typeof vi.fn>
isDisposed: ReturnType<typeof vi.fn>
}
function createMockMux(): MockMultiplexer {
return {
request: vi.fn().mockResolvedValue(undefined),
notify: vi.fn(),
onNotification: vi.fn(),
onNotificationByMethod: vi.fn().mockReturnValue(vi.fn()),
dispose: vi.fn(),
isDisposed: vi.fn().mockReturnValue(false)
}
}
async function waitForRequestCount(mock: ReturnType<typeof vi.fn>, count: number): Promise<void> {
for (let i = 0; i < 20; i++) {
if (mock.mock.calls.length >= count) {
return
}
await new Promise<void>((resolve) => setTimeout(resolve, 0))
}
}
function deferredValue<T>(value: T): { promise: Promise<T>; resolve: () => void } {
let resolve!: (value: T) => void
const promise = new Promise<T>((innerResolve) => {
resolve = innerResolve
})
return { promise, resolve: () => resolve(value) }
}
describe('SshGitProvider', () => {
let mux: MockMultiplexer
let provider: SshGitProvider
beforeEach(() => {
mux = createMockMux()
provider = new SshGitProvider('conn-1', mux as never)
})
it('returns the connectionId', () => {
expect(provider.getConnectionId()).toBe('conn-1')
})
it('getStatus sends git.status request', async () => {
const statusResult = { entries: [], conflictOperation: 'unknown' }
mux.request.mockResolvedValue(statusResult)
const result = await provider.getStatus('/home/user/repo')
expect(mux.request).toHaveBeenCalledWith('git.status', { worktreePath: '/home/user/repo' })
expect(result).toEqual(statusResult)
})
it('getStatus forwards includeIgnored only when requested', async () => {
const statusResult = { entries: [], conflictOperation: 'unknown', ignoredPaths: ['dist/'] }
mux.request.mockResolvedValue(statusResult)
await provider.getStatus('/home/user/repo', { includeIgnored: true })
await provider.getStatus('/home/user/repo', { includeIgnored: false })
expect(mux.request).toHaveBeenNthCalledWith(1, 'git.status', {
worktreePath: '/home/user/repo',
includeIgnored: true
})
expect(mux.request).toHaveBeenNthCalledWith(2, 'git.status', {
worktreePath: '/home/user/repo'
})
})
it('getStatus forwards upstream-negative-cache bypass only when requested', async () => {
const statusResult = { entries: [], conflictOperation: 'unknown' }
mux.request.mockResolvedValue(statusResult)
await provider.getStatus('/home/user/repo', { bypassEffectiveUpstreamNegativeCache: true })
await provider.getStatus('/home/user/repo', { bypassEffectiveUpstreamNegativeCache: false })
expect(mux.request).toHaveBeenNthCalledWith(1, 'git.status', {
worktreePath: '/home/user/repo',
bypassEffectiveUpstreamNegativeCache: true
})
expect(mux.request).toHaveBeenNthCalledWith(2, 'git.status', {
worktreePath: '/home/user/repo'
})
})
it('getSubmoduleStatus sends git.submoduleStatus request', async () => {
const statusResult = { entries: [], conflictOperation: 'unknown' }
mux.request.mockResolvedValue(statusResult)
const result = await provider.getSubmoduleStatus('/home/user/repo', 'vendor/lib')
expect(mux.request).toHaveBeenCalledWith('git.submoduleStatus', {
worktreePath: '/home/user/repo',
submodulePath: 'vendor/lib',
area: 'unstaged'
})
expect(result).toEqual(statusResult)
})
it('getSubmoduleStatus forwards the requested source-control area', async () => {
const statusResult = { entries: [], conflictOperation: 'unknown' }
mux.request.mockResolvedValue(statusResult)
await provider.getSubmoduleStatus('/home/user/repo', 'vendor/lib', 'staged')
expect(mux.request).toHaveBeenCalledWith('git.submoduleStatus', {
worktreePath: '/home/user/repo',
submodulePath: 'vendor/lib',
area: 'staged'
})
})
it('reports an actionable reconnect message when the relay lacks submodule status', async () => {
const methodNotFound = new Error('Method not found: git.submoduleStatus') as Error & {
code?: number
}
methodNotFound.code = -32601
mux.request.mockRejectedValueOnce(methodNotFound)
await expect(provider.getSubmoduleStatus('/home/user/repo', 'vendor/lib')).rejects.toThrow(
'SSH submodule diff support is unavailable on this relay. Reconnect the SSH target to update Orca on the host, then try again.'
)
})
it('rethrows non-method-not-found submodule status errors unchanged', async () => {
mux.request.mockRejectedValueOnce(new Error('fatal: not a submodule'))
await expect(provider.getSubmoduleStatus('/home/user/repo', 'vendor/lib')).rejects.toThrow(
'fatal: not a submodule'
)
})
it('checkIgnoredPaths sends git.checkIgnored request', async () => {
mux.request.mockResolvedValue(['dist/bundle.js'])
const result = await provider.checkIgnoredPaths('/home/user/repo', ['dist/bundle.js'])
expect(mux.request).toHaveBeenCalledWith('git.checkIgnored', {
worktreePath: '/home/user/repo',
paths: ['dist/bundle.js']
})
expect(result).toEqual(['dist/bundle.js'])
})
it('clone sends git.clone request and forwards matching progress notifications', async () => {
const unsubscribe = vi.fn()
const onProgress = vi.fn()
mux.onNotificationByMethod.mockReturnValue(unsubscribe)
mux.request.mockImplementationOnce(async (_method, params) => {
const progressHandler = mux.onNotificationByMethod.mock.calls[0][1]
progressHandler({
progressId: params.progressId,
phase: 'Receiving objects',
percent: 42
})
progressHandler({
progressId: 'other-clone',
phase: 'Receiving objects',
percent: 99
})
return { stdout: '', stderr: '' }
})
await provider.clone(['clone', '--progress', '--', 'url', 'repo'], '/home/user', {
timeoutMs: 1000,
onProgress
})
expect(mux.request).toHaveBeenCalledWith(
'git.clone',
expect.objectContaining({
args: ['clone', '--progress', '--', 'url', 'repo'],
cwd: '/home/user',
progressId: expect.stringMatching(/^clone-/)
}),
{ signal: undefined, timeoutMs: 1000 }
)
expect(mux.onNotificationByMethod).toHaveBeenCalledWith(
'git.cloneProgress',
expect.any(Function)
)
expect(onProgress).toHaveBeenCalledWith({ phase: 'Receiving objects', percent: 42 })
expect(onProgress).toHaveBeenCalledTimes(1)
expect(unsubscribe).toHaveBeenCalledTimes(1)
})
it('reports an actionable reconnect message when the relay does not support cloning', async () => {
const methodNotFound = new Error('Method not found: git.clone') as Error & { code?: number }
methodNotFound.code = -32601
mux.request.mockRejectedValueOnce(methodNotFound)
await expect(
provider.clone(['clone', '--progress', '--', 'url', 'repo'], '/home/user')
).rejects.toThrow(
'SSH clone support is unavailable on this relay. Reconnect the SSH target to update Orca on the host, then try again.'
)
})
it('getHistory sends git.history request', async () => {
const historyResult = {
items: [],
hasIncomingChanges: false,
hasOutgoingChanges: false,
hasMore: false,
limit: 50
}
mux.request.mockResolvedValue(historyResult)
const result = await provider.getHistory('/home/user/repo', {
limit: 25,
baseRef: 'origin/main'
})
expect(mux.request).toHaveBeenCalledWith('git.history', {
worktreePath: '/home/user/repo',
limit: 25,
baseRef: 'origin/main'
})
expect(result).toEqual(historyResult)
})
it('commit sends git.commit request', async () => {
const commitResult = { success: true }
mux.request.mockResolvedValue(commitResult)
const result = await provider.commit('/home/user/repo', 'feat: add source control commit')
expect(mux.request).toHaveBeenCalledWith('git.commit', {
worktreePath: '/home/user/repo',
message: 'feat: add source control commit'
})
expect(result).toEqual(commitResult)
})
it('execNonInteractive delegates fixed binary commands to the relay', async () => {
const execResult = {
stdout: '10.0.0\n',
stderr: '',
exitCode: 0,
timedOut: false
}
mux.request.mockResolvedValue(execResult)
const result = await provider.execNonInteractive('pnpm', ['--version'], '/home/user/repo', 8000)
expect(mux.request).toHaveBeenCalledWith('agent.execNonInteractive', {
binary: 'pnpm',
args: ['--version'],
cwd: '/home/user/repo',
stdin: null,
timeoutMs: 8000
})
expect(result).toEqual(execResult)
})
it('execNonInteractive forwards environment variables to the relay', async () => {
const execResult = {
stdout: '',
stderr: '',
exitCode: 0,
timedOut: false
}
mux.request.mockResolvedValue(execResult)
await provider.execNonInteractive(
'/bin/bash',
['-lc', 'echo "$ORCA_WORKTREE_PATH"'],
'/home/user/repo',
120_000,
undefined,
{
ORCA_ROOT_PATH: '/home/user/repo',
ORCA_WORKTREE_PATH: '/home/user/repo-feature'
}
)
expect(mux.request).toHaveBeenCalledWith('agent.execNonInteractive', {
binary: '/bin/bash',
args: ['-lc', 'echo "$ORCA_WORKTREE_PATH"'],
cwd: '/home/user/repo',
stdin: null,
timeoutMs: 120_000,
env: {
ORCA_ROOT_PATH: '/home/user/repo',
ORCA_WORKTREE_PATH: '/home/user/repo-feature'
}
})
})
it('cancelNonInteractiveExec sends best-effort relay cancellation', async () => {
await provider.cancelNonInteractiveExec('/home/user/repo')
expect(mux.request).toHaveBeenCalledWith('agent.cancelExec', { cwd: '/home/user/repo' })
})
it('exec forwards abort and timeout options to the relay request', async () => {
const controller = new AbortController()
mux.request.mockResolvedValue({ stdout: '', stderr: '' })
await provider.exec(
['clone', '--progress', '--', 'git@example.com:repo.git', 'repo'],
'/home/user',
{
signal: controller.signal,
timeoutMs: 60_000
}
)
expect(mux.request).toHaveBeenCalledWith(
'git.exec',
{
args: ['clone', '--progress', '--', 'git@example.com:repo.git', 'repo'],
cwd: '/home/user'
},
{
signal: controller.signal,
timeoutMs: 60_000
}
)
})
it('getStagedCommitContext reads branch, staged summary, and staged patch remotely', async () => {
mux.request.mockImplementation(async (method, payload) => {
expect(method).toBe('git.exec')
if (payload.args[1] === '--show-current') {
return { stdout: 'feature/ai-commit\n' }
}
if (payload.args[2] === '--name-status') {
return { stdout: 'M\tREADME.md\n' }
}
if (payload.args[2] === '--patch') {
return { stdout: 'diff --git a/README.md b/README.md\n+hello' }
}
throw new Error(`unexpected args: ${payload.args.join(' ')}`)
})
const result = await provider.getStagedCommitContext('/home/user/repo')
expect(result).toEqual({
branch: 'feature/ai-commit',
stagedSummary: 'M\tREADME.md',
stagedPatch: 'diff --git a/README.md b/README.md\n+hello'
})
expect(mux.request).toHaveBeenCalledWith('git.exec', {
args: ['diff', '--cached', '--patch', '--minimal', '--no-color', '--no-ext-diff'],
cwd: '/home/user/repo'
})
})
it('getStagedCommitContext returns null when nothing is staged', async () => {
mux.request.mockImplementation(async (_method, payload) => {
if (payload.args[1] === '--show-current') {
return { stdout: 'main\n' }
}
return { stdout: '' }
})
await expect(provider.getStagedCommitContext('/home/user/repo')).resolves.toBeNull()
expect(mux.request).toHaveBeenCalledTimes(2)
})
it('getStagedCommitContext falls back when the remote staged patch overflows', async () => {
mux.request.mockImplementation(async (_method, payload) => {
if (payload.args[1] === '--show-current') {
return { stdout: 'feature/ai-commit\n' }
}
if (payload.args[2] === '--name-status') {
return { stdout: 'A\thuge.jsonl\n' }
}
throw Object.assign(new Error('git stdout exceeded maxBuffer.'), { code: 'ENOBUFS' })
})
await expect(provider.getStagedCommitContext('/home/user/repo')).resolves.toEqual({
branch: 'feature/ai-commit',
stagedSummary: 'A\thuge.jsonl',
stagedPatch: ''
})
})
it('getStagedCommitContext rethrows remote patch failures that are not buffer overflows', async () => {
mux.request.mockImplementation(async (_method, payload) => {
if (payload.args[1] === '--show-current') {
return { stdout: 'feature/ai-commit\n' }
}
if (payload.args[2] === '--name-status') {
return { stdout: 'M\tREADME.md\n' }
}
throw new Error('fatal: bad revision')
})
await expect(provider.getStagedCommitContext('/home/user/repo')).rejects.toThrow(
'fatal: bad revision'
)
})
it('executeCommitMessagePlan delegates the prepared plan to the relay', async () => {
const execResult = {
stdout: 'Update docs',
stderr: '',
exitCode: 0,
timedOut: false
}
mux.request.mockResolvedValue(execResult)
const result = await provider.executeCommitMessagePlan(
{
binary: 'codex',
args: ['exec', 'PROMPT'],
stdinPayload: null,
label: 'Codex'
},
'/home/user/repo',
60_000
)
expect(mux.request).toHaveBeenCalledWith('agent.execNonInteractive', {
binary: 'codex',
args: ['exec', 'PROMPT'],
cwd: '/home/user/repo',
stdin: null,
timeoutMs: 60_000,
operation: 'commit-message'
})
expect(result).toEqual(execResult)
})
it('keeps SSH commit-message and pull-request execution lanes separate', async () => {
const completeRequests: (() => void)[] = []
mux.request.mockImplementation((method) => {
if (method === 'agent.cancelExec') {
return Promise.resolve({ canceled: true })
}
return new Promise((resolve) => {
completeRequests.push(() =>
resolve({
stdout: '',
stderr: '',
exitCode: 0,
timedOut: false
})
)
})
})
const plan = {
binary: 'codex',
args: ['exec', 'PROMPT'],
stdinPayload: null,
label: 'Codex'
}
const commit = provider.executeCommitMessagePlan(plan, '/home/user/repo', 60_000)
const pullRequest = provider.executeCommitMessagePlan(
plan,
'/home/user/repo',
60_000,
'pull-request-fields'
)
await waitForRequestCount(mux.request, 2)
expect(mux.request).toHaveBeenNthCalledWith(1, 'agent.execNonInteractive', {
binary: 'codex',
args: ['exec', 'PROMPT'],
cwd: '/home/user/repo',
stdin: null,
timeoutMs: 60_000,
operation: 'commit-message'
})
expect(mux.request).toHaveBeenNthCalledWith(2, 'agent.execNonInteractive', {
binary: 'codex',
args: ['exec', 'PROMPT'],
cwd: '/home/user/repo',
stdin: null,
timeoutMs: 60_000,
operation: 'pull-request-fields'
})
await provider.cancelGenerateCommitMessage('/home/user/repo')
await provider.cancelGenerateCommitMessage('/home/user/repo', 'pull-request-fields')
expect(mux.request).toHaveBeenNthCalledWith(3, 'agent.cancelExec', {
cwd: '/home/user/repo',
operation: 'commit-message'
})
expect(mux.request).toHaveBeenNthCalledWith(4, 'agent.cancelExec', {
cwd: '/home/user/repo',
operation: 'pull-request-fields'
})
completeRequests.shift()?.()
completeRequests.shift()?.()
await Promise.all([commit, pullRequest])
})
it('serializes non-interactive relay execs for the same cwd and operation', async () => {
const completeRequests: (() => void)[] = []
mux.request.mockImplementation(
() =>
new Promise((resolve) => {
completeRequests.push(() =>
resolve({
stdout: '',
stderr: '',
exitCode: 0,
timedOut: false
})
)
})
)
const first = provider.execNonInteractive('pnpm', ['store', 'prune'], '/home/user/repo', 8000)
const second = provider.execNonInteractive('pnpm', ['install'], '/home/user/repo', 8000)
await waitForRequestCount(mux.request, 1)
expect(mux.request).toHaveBeenCalledTimes(1)
completeRequests.shift()?.()
await first
await waitForRequestCount(mux.request, 2)
expect(mux.request).toHaveBeenNthCalledWith(2, 'agent.execNonInteractive', {
binary: 'pnpm',
args: ['install'],
cwd: '/home/user/repo',
stdin: null,
timeoutMs: 8000
})
completeRequests.shift()?.()
await second
})
it('cancels a queued non-interactive exec without canceling the active relay child', async () => {
const completeRequests: (() => void)[] = []
mux.request.mockImplementation(
() =>
new Promise((resolve) => {
completeRequests.push(() =>
resolve({
stdout: '',
stderr: '',
exitCode: 0,
timedOut: false
})
)
})
)
const first = provider.execNonInteractive('pnpm', ['store', 'prune'], '/home/user/repo', 8000)
const second = provider.execNonInteractive('pnpm', ['install'], '/home/user/repo', 8000)
await waitForRequestCount(mux.request, 1)
await provider.cancelNonInteractiveExec('/home/user/repo')
expect(mux.request).toHaveBeenCalledTimes(1)
expect(mux.request).not.toHaveBeenCalledWith('agent.cancelExec', { cwd: '/home/user/repo' })
completeRequests.shift()?.()
await first
const secondResult = await second
expect(mux.request).toHaveBeenCalledTimes(1)
expect(secondResult).toMatchObject({ canceled: true })
})
it('uses an exec abort signal to cancel the matching active relay child with queued work present', async () => {
const completeRequests: (() => void)[] = []
mux.request.mockImplementation((method) => {
if (method === 'agent.cancelExec') {
return Promise.resolve({ canceled: true })
}
return new Promise((resolve) => {
completeRequests.push(() =>
resolve({
stdout: '',
stderr: '',
exitCode: 0,
timedOut: false
})
)
})
})
const controller = new AbortController()
const first = provider.execNonInteractive(
'pnpm',
['store', 'prune'],
'/home/user/repo',
8000,
controller.signal
)
const second = provider.execNonInteractive('pnpm', ['install'], '/home/user/repo', 8000)
await waitForRequestCount(mux.request, 1)
controller.abort()
await waitForRequestCount(mux.request, 2)
expect(mux.request).toHaveBeenNthCalledWith(2, 'agent.cancelExec', { cwd: '/home/user/repo' })
completeRequests.shift()?.()
await first
await waitForRequestCount(mux.request, 3)
expect(mux.request).toHaveBeenNthCalledWith(3, 'agent.execNonInteractive', {
binary: 'pnpm',
args: ['install'],
cwd: '/home/user/repo',
stdin: null,
timeoutMs: 8000
})
completeRequests.shift()?.()
await second
})
it('cancelGenerateCommitMessage sends best-effort relay cancellation', async () => {
await provider.cancelGenerateCommitMessage('/home/user/repo')
expect(mux.request).toHaveBeenCalledWith('agent.cancelExec', {
cwd: '/home/user/repo',
operation: 'commit-message'
})
})
it('getDiff sends git.diff request', async () => {
const diffResult = { kind: 'text', originalContent: '', modifiedContent: 'hello' }
mux.request.mockResolvedValue(diffResult)
const result = await provider.getDiff('/home/user/repo', 'src/index.ts', true)
expect(mux.request).toHaveBeenCalledWith('git.diff', {
worktreePath: '/home/user/repo',
filePath: 'src/index.ts',
staged: true
})
expect(result).toEqual(diffResult)
})
it('stageFile sends git.stage request', async () => {
await provider.stageFile('/home/user/repo', 'src/file.ts')
expect(mux.request).toHaveBeenCalledWith('git.stage', {
worktreePath: '/home/user/repo',
filePath: 'src/file.ts'
})
})
it('unstageFile sends git.unstage request', async () => {
await provider.unstageFile('/home/user/repo', 'src/file.ts')
expect(mux.request).toHaveBeenCalledWith('git.unstage', {
worktreePath: '/home/user/repo',
filePath: 'src/file.ts'
})
})
it('bulkStageFiles sends git.bulkStage request', async () => {
await provider.bulkStageFiles('/home/user/repo', ['a.ts', 'b.ts'])
expect(mux.request).toHaveBeenCalledWith('git.bulkStage', {
worktreePath: '/home/user/repo',
filePaths: ['a.ts', 'b.ts']
})
})
it('bulkUnstageFiles sends git.bulkUnstage request', async () => {
await provider.bulkUnstageFiles('/home/user/repo', ['a.ts', 'b.ts'])
expect(mux.request).toHaveBeenCalledWith('git.bulkUnstage', {
worktreePath: '/home/user/repo',
filePaths: ['a.ts', 'b.ts']
})
})
it('discardChanges sends git.discard request', async () => {
await provider.discardChanges('/home/user/repo', 'src/file.ts')
expect(mux.request).toHaveBeenCalledWith('git.discard', {
worktreePath: '/home/user/repo',
filePath: 'src/file.ts'
})
})
it('bulkDiscardChanges sends git.bulkDiscard request', async () => {
await provider.bulkDiscardChanges('/home/user/repo', ['a.ts', 'b.ts'])
expect(mux.request).toHaveBeenCalledWith('git.bulkDiscard', {
worktreePath: '/home/user/repo',
filePaths: ['a.ts', 'b.ts']
})
})
it('detectConflictOperation sends git.conflictOperation request', async () => {
mux.request.mockResolvedValue('rebase')
const result = await provider.detectConflictOperation('/home/user/repo')
expect(mux.request).toHaveBeenCalledWith('git.conflictOperation', {
worktreePath: '/home/user/repo'
})
expect(result).toBe('rebase')
})
it('getBranchCompare sends git.branchCompare request', async () => {
const compareResult = { summary: { ahead: 2, behind: 0 }, entries: [] }
mux.request.mockResolvedValue(compareResult)
const result = await provider.getBranchCompare('/home/user/repo', 'main')
expect(mux.request).toHaveBeenCalledWith('git.branchCompare', {
worktreePath: '/home/user/repo',
baseRef: 'main'
})
expect(result).toEqual(compareResult)
})
it('getUpstreamStatus sends git.upstreamStatus request', async () => {
const upstreamResult = { hasUpstream: true, upstreamName: 'origin/main', ahead: 1, behind: 0 }
mux.request.mockResolvedValue(upstreamResult)
const result = await provider.getUpstreamStatus('/home/user/repo')
expect(mux.request).toHaveBeenCalledWith('git.upstreamStatus', {
worktreePath: '/home/user/repo'
})
expect(result).toEqual(upstreamResult)
})
it('getUpstreamStatus forwards an explicit push target', async () => {
const upstreamResult = { hasUpstream: true, upstreamName: 'fork/feature', ahead: 0, behind: 1 }
mux.request.mockResolvedValue(upstreamResult)
const pushTarget = { remoteName: 'fork', branchName: 'feature' }
const result = await provider.getUpstreamStatus('/home/user/repo', pushTarget)
expect(mux.request).toHaveBeenCalledWith('git.upstreamStatus', {
worktreePath: '/home/user/repo',
pushTarget
})
expect(result).toEqual(upstreamResult)
})
it('pushBranch sends git.push request and forwards publish mode and target', async () => {
await provider.pushBranch('/home/user/repo', true, {
remoteName: 'pr-fork-orca',
branchName: 'contributor/fix'
})
expect(mux.request).toHaveBeenCalledWith('git.push', {
worktreePath: '/home/user/repo',
publish: true,
pushTarget: {
remoteName: 'pr-fork-orca',
branchName: 'contributor/fix'
}
})
})
it('pushBranch forwards force-with-lease mode', async () => {
await provider.pushBranch('/home/user/repo', false, undefined, { forceWithLease: true })
expect(mux.request).toHaveBeenCalledWith('git.push', {
worktreePath: '/home/user/repo',
publish: false,
pushTarget: undefined,
forceWithLease: true
})
})
it('pullBranch sends git.pull request', async () => {
await provider.pullBranch('/home/user/repo')
expect(mux.request).toHaveBeenCalledWith('git.pull', {
worktreePath: '/home/user/repo'
})
})
it('pullBranch forwards an explicit push target', async () => {
const pushTarget = { remoteName: 'fork', branchName: 'feature' }
await provider.pullBranch('/home/user/repo', pushTarget)
expect(mux.request).toHaveBeenCalledWith('git.pull', {
worktreePath: '/home/user/repo',
pushTarget
})
})
it('fastForwardBranch sends git.fastForward request', async () => {
await provider.fastForwardBranch('/home/user/repo')
expect(mux.request).toHaveBeenCalledWith('git.fastForward', {
worktreePath: '/home/user/repo'
})
})
it('fastForwardBranch forwards an explicit push target', async () => {
const pushTarget = { remoteName: 'fork', branchName: 'feature' }
await provider.fastForwardBranch('/home/user/repo', pushTarget)
expect(mux.request).toHaveBeenCalledWith('git.fastForward', {
worktreePath: '/home/user/repo',
pushTarget
})
})
it('rebaseFromBase sends git.rebaseFromBase request', async () => {
await provider.rebaseFromBase('/home/user/repo', 'upstream/main')
expect(mux.request).toHaveBeenCalledWith('git.rebaseFromBase', {
worktreePath: '/home/user/repo',
baseRef: 'upstream/main'
})
})
it('fetchRemote sends git.fetch request', async () => {
await provider.fetchRemote('/home/user/repo')
expect(mux.request).toHaveBeenCalledWith('git.fetch', {
worktreePath: '/home/user/repo'
})
})
it('fetchRemote forwards an explicit push target', async () => {
const pushTarget = { remoteName: 'fork', branchName: 'feature' }
await provider.fetchRemote('/home/user/repo', pushTarget)
expect(mux.request).toHaveBeenCalledWith('git.fetch', {
worktreePath: '/home/user/repo',
pushTarget
})
})
it('syncForkDefaultBranch sends git.forkSync request', async () => {
const syncResult = {
status: 'synced',
originRemote: 'origin',
upstreamRemote: 'upstream',
branchName: 'main',
ahead: 0,
behind: 2
}
mux.request.mockResolvedValue(syncResult)
const expectedUpstream = { owner: 'stablyai', repo: 'orca' }
const result = await provider.syncForkDefaultBranch('/home/user/repo', expectedUpstream)
expect(mux.request).toHaveBeenCalledWith('git.forkSync', {
worktreePath: '/home/user/repo',
expectedUpstream
})
expect(result).toEqual(syncResult)
})
it('fetchRemoteTrackingRef sends git.fetchRemoteTrackingRef request', async () => {
await provider.fetchRemoteTrackingRef(
'/home/user/repo',
'origin',
'main',
'refs/remotes/origin/main',
{ skipAutoMaintenance: true }
)
expect(mux.request).toHaveBeenCalledWith('git.fetchRemoteTrackingRef', {
worktreePath: '/home/user/repo',
remote: 'origin',
branch: 'main',
ref: 'refs/remotes/origin/main',
skipAutoMaintenance: true
})
})
it('fetchGitLabMergeRequestHead sends git.fetchGitLabMergeRequestHead request', async () => {
await provider.fetchGitLabMergeRequestHead('/home/user/repo', 'origin', 42)
expect(mux.request).toHaveBeenCalledWith('git.fetchGitLabMergeRequestHead', {
worktreePath: '/home/user/repo',
remote: 'origin',
mrIid: 42
})
})
it('getBranchDiff sends git.branchDiff request', async () => {
const diffs = [{ kind: 'text', originalContent: '', modifiedContent: 'new' }]
mux.request.mockResolvedValue(diffs)
const result = await provider.getBranchDiff('/home/user/repo', 'main')
expect(mux.request).toHaveBeenCalledWith('git.branchDiff', {
worktreePath: '/home/user/repo',
baseRef: 'main'
})
expect(result).toEqual(diffs)
})
it('coalesces concurrent identical diff RPCs while in flight', async () => {
const diff = {
kind: 'text',
originalContent: 'old',
modifiedContent: 'new',
originalIsBinary: false,
modifiedIsBinary: false
}
const pendingDiff = deferredValue(diff)
mux.request.mockReturnValue(pendingDiff.promise)
const reads = Array.from({ length: 8 }, () =>
provider.getDiff('/home/user/repo', 'src/file.ts', false, true)
)
await waitForRequestCount(mux.request, 1)
expect(mux.request).toHaveBeenCalledTimes(1)
pendingDiff.resolve()
await expect(Promise.all(reads)).resolves.toEqual(Array(8).fill(diff))
mux.request.mockReset()
const branchDiffs = [diff]
const pendingBranchDiff = deferredValue(branchDiffs)
mux.request.mockReturnValue(pendingBranchDiff.promise)
const branchReads = Array.from({ length: 8 }, () =>
provider.getBranchDiff('/home/user/repo', 'main', {
includePatch: true,
filePath: 'src/file.ts'
})
)
await waitForRequestCount(mux.request, 1)
expect(mux.request).toHaveBeenCalledTimes(1)
pendingBranchDiff.resolve()
await expect(Promise.all(branchReads)).resolves.toEqual(Array(8).fill(branchDiffs))
mux.request.mockReset()
const pendingCommitDiff = deferredValue(diff)
mux.request.mockReturnValue(pendingCommitDiff.promise)
const commitReads = Array.from({ length: 8 }, () =>
provider.getCommitDiff('/home/user/repo', {
commitOid: 'c'.repeat(40),
parentOid: 'b'.repeat(40),
filePath: 'src/file.ts'
})
)
await waitForRequestCount(mux.request, 1)
expect(mux.request).toHaveBeenCalledTimes(1)
pendingCommitDiff.resolve()
await expect(Promise.all(commitReads)).resolves.toEqual(Array(8).fill(diff))
})
it('retries diff RPCs after an in-flight rejection settles', async () => {
const failure = new Error('transient relay failure')
mux.request.mockRejectedValueOnce(failure)
const firstBurst = Array.from({ length: 8 }, () =>
provider.getDiff('/home/user/repo', 'src/file.ts', false, true)
)
await expect(Promise.all(firstBurst)).rejects.toThrow('transient relay failure')
expect(mux.request).toHaveBeenCalledTimes(1)
const diff = {
kind: 'text',
originalContent: 'old',
modifiedContent: 'new',
originalIsBinary: false,
modifiedIsBinary: false
}
mux.request.mockResolvedValueOnce(diff)
await expect(provider.getDiff('/home/user/repo', 'src/file.ts', false, true)).resolves.toBe(
diff
)
expect(mux.request).toHaveBeenCalledTimes(2)
})
it('clears pending diff RPCs when status runs', async () => {
const diff = {
kind: 'text',
originalContent: 'old',
modifiedContent: 'new',
originalIsBinary: false,
modifiedIsBinary: false
}
const pendingDiff = deferredValue(diff)
mux.request.mockReturnValueOnce(pendingDiff.promise)
const first = provider.getDiff('/home/user/repo', 'src/file.ts', false, true)
await waitForRequestCount(mux.request, 1)
mux.request.mockResolvedValueOnce({ entries: [], conflictOperation: 'unknown' })
await provider.getStatus('/home/user/repo')
mux.request.mockResolvedValueOnce(diff)
const second = provider.getDiff('/home/user/repo', 'src/file.ts', false, true)
pendingDiff.resolve()
await expect(Promise.all([first, second])).resolves.toEqual([diff, diff])
expect(mux.request).toHaveBeenCalledTimes(3)
})
it('clears pending diff RPCs when submodule status runs', async () => {
const diff = {
kind: 'text',
originalContent: 'old',
modifiedContent: 'new',
originalIsBinary: false,
modifiedIsBinary: false
}
const pendingDiff = deferredValue(diff)
mux.request.mockReturnValueOnce(pendingDiff.promise)
const first = provider.getDiff('/home/user/repo', 'src/file.ts', false, true)
await waitForRequestCount(mux.request, 1)
mux.request.mockResolvedValueOnce({ entries: [], conflictOperation: 'unknown' })
await provider.getSubmoduleStatus('/home/user/repo', 'vendor/lib')
mux.request.mockResolvedValueOnce(diff)
const second = provider.getDiff('/home/user/repo', 'src/file.ts', false, true)
pendingDiff.resolve()
await expect(Promise.all([first, second])).resolves.toEqual([diff, diff])
expect(mux.request).toHaveBeenCalledTimes(3)
})
it('clears pending diff RPCs when a mutation runs', async () => {
const diff = {
kind: 'text',
originalContent: 'old',
modifiedContent: 'new',
originalIsBinary: false,
modifiedIsBinary: false
}
const pendingDiff = deferredValue(diff)
mux.request.mockReturnValueOnce(pendingDiff.promise)
const first = provider.getDiff('/home/user/repo', 'src/file.ts', false, true)
await waitForRequestCount(mux.request, 1)
mux.request.mockResolvedValueOnce(undefined)
await provider.stageFile('/home/user/repo', 'src/file.ts')
mux.request.mockResolvedValueOnce(diff)
const second = provider.getDiff('/home/user/repo', 'src/file.ts', false, true)
pendingDiff.resolve()
await expect(Promise.all([first, second])).resolves.toEqual([diff, diff])
expect(mux.request).toHaveBeenCalledTimes(3)
})
it('clears pending branch diff RPCs when a ref-moving provider operation runs', async () => {
const diff = {
kind: 'text',
originalContent: 'old',
modifiedContent: 'new',
originalIsBinary: false,
modifiedIsBinary: false
}
const pendingDiff = deferredValue([diff])
mux.request.mockReturnValueOnce(pendingDiff.promise)
const first = provider.getBranchDiff('/home/user/repo', 'origin/main', {
includePatch: true,
filePath: 'src/file.ts'
})
await waitForRequestCount(mux.request, 1)
mux.request.mockResolvedValueOnce(undefined)
await provider.fetchRemoteTrackingRef(
'/home/user/repo',
'origin',
'main',
'refs/remotes/origin/main'
)
mux.request.mockResolvedValueOnce([diff])
const second = provider.getBranchDiff('/home/user/repo', 'origin/main', {
includePatch: true,
filePath: 'src/file.ts'
})
pendingDiff.resolve()
await expect(Promise.all([first, second])).resolves.toEqual([[diff], [diff]])
expect(mux.request).toHaveBeenCalledTimes(3)
})
it('coalesces logically identical branch and commit diff RPC args regardless of property order', async () => {
const diff = {
kind: 'text',
originalContent: 'old',
modifiedContent: 'new',
originalIsBinary: false,
modifiedIsBinary: false
}
mux.request.mockResolvedValue([diff])
await Promise.all([
provider.getBranchDiff('/home/user/repo', 'main', {
includePatch: true,
filePath: 'src/file.ts',
oldPath: 'src/old-file.ts'
}),
provider.getBranchDiff('/home/user/repo', 'main', {
oldPath: 'src/old-file.ts',
filePath: 'src/file.ts',
includePatch: true
})
])
expect(mux.request).toHaveBeenCalledTimes(1)
mux.request.mockReset()
mux.request.mockResolvedValue(diff)
await Promise.all([
provider.getCommitDiff('/home/user/repo', {
commitOid: 'c'.repeat(40),
parentOid: 'b'.repeat(40),
filePath: 'src/file.ts',
oldPath: 'src/old-file.ts'
}),
provider.getCommitDiff('/home/user/repo', {
oldPath: 'src/old-file.ts',
filePath: 'src/file.ts',
parentOid: 'b'.repeat(40),
commitOid: 'c'.repeat(40)
})
])
expect(mux.request).toHaveBeenCalledTimes(1)
})
it('keeps distinct diff RPC keys independent', async () => {
const diff = {
kind: 'text',
originalContent: 'old',
modifiedContent: 'new',
originalIsBinary: false,
modifiedIsBinary: false
}
mux.request.mockResolvedValue(diff)
await Promise.all([
provider.getDiff('/home/user/repo', 'src/file.ts', false, false),
provider.getDiff('/home/user/repo', 'src/file.ts', true, false),
provider.getDiff('/home/user/repo', 'src/file.ts', false, true),
provider.getBranchDiff('/home/user/repo', 'main', {
includePatch: true,
filePath: 'src/file.ts'
}),
provider.getBranchDiff('/home/user/repo', 'main', {
includePatch: false,
filePath: 'src/file.ts'
}),
provider.getBranchDiff('/home/user/repo', 'main', {
includePatch: true,
filePath: 'src/file.ts',
oldPath: 'src/old-file.ts'
}),
provider.getBranchDiff('/home/user/repo', 'develop', {
includePatch: true,
filePath: 'src/file.ts'
}),
provider.getCommitDiff('/home/user/repo', {
commitOid: 'c'.repeat(40),
parentOid: 'b'.repeat(40),
filePath: 'src/file.ts'
}),
provider.getCommitDiff('/home/user/repo', {
commitOid: 'c'.repeat(40),
parentOid: 'a'.repeat(40),
filePath: 'src/file.ts'
}),
provider.getCommitDiff('/home/user/repo', {
commitOid: 'c'.repeat(40),
parentOid: 'b'.repeat(40),
filePath: 'src/file.ts',
oldPath: 'src/old-file.ts'
})
])
expect(mux.request).toHaveBeenCalledTimes(10)
})
it('listWorktrees sends git.listWorktrees request', async () => {
const worktrees = [
{
path: '/home/user/repo',
head: 'abc123',
branch: 'main',
isBare: false,
isMainWorktree: true
}
]
mux.request.mockResolvedValue(worktrees)
const controller = new AbortController()
const result = await provider.listWorktrees('/home/user/repo', { signal: controller.signal })
expect(mux.request).toHaveBeenCalledWith(
'git.listWorktrees',
{ repoPath: '/home/user/repo' },
{ signal: controller.signal }
)
expect(result).toEqual(worktrees)
})
it('addWorktree sends git.addWorktree request', async () => {
await provider.addWorktree('/home/user/repo', 'feature', '/home/user/feat', {
base: 'main',
noCheckout: true
})
expect(mux.request).toHaveBeenCalledWith('git.addWorktree', {
repoPath: '/home/user/repo',
branchName: 'feature',
targetDir: '/home/user/feat',
base: 'main',
noCheckout: true
})
})
it('removeWorktree sends git.removeWorktree request', async () => {
await provider.removeWorktree('/home/user/feat', true)
expect(mux.request).toHaveBeenCalledWith('git.removeWorktree', {
worktreePath: '/home/user/feat',
force: true
})
})
it('worktreeIsClean sends git.worktreeIsClean request', async () => {
const cleanResult = { clean: false, stdout: '?? scratch.txt\n' }
mux.request.mockResolvedValue(cleanResult)
const result = await provider.worktreeIsClean('/home/user/feat')
expect(mux.request).toHaveBeenCalledWith('git.worktreeIsClean', {
worktreePath: '/home/user/feat'
})
expect(result).toEqual(cleanResult)
})
it('worktreeIsClean can ignore untracked files', async () => {
const cleanResult = { clean: true }
mux.request.mockResolvedValue(cleanResult)
const result = await provider.worktreeIsClean('/home/user/feat', { includeUntracked: false })
expect(mux.request).toHaveBeenCalledWith('git.worktreeIsClean', {
worktreePath: '/home/user/feat',
includeUntracked: false
})
expect(result).toEqual(cleanResult)
})
it('worktreeIsClean filters untracked stdout when old relays ignore the option', async () => {
mux.request.mockResolvedValue({ clean: false, stdout: '?? scratch.txt\n' })
const result = await provider.worktreeIsClean('/home/user/feat', { includeUntracked: false })
expect(result).toEqual({ clean: true })
})
it('worktreeIsClean keeps dirty results without stdout dirty for tracked-only checks', async () => {
mux.request.mockResolvedValue({ clean: false })
const result = await provider.worktreeIsClean('/home/user/feat', { includeUntracked: false })
expect(result).toEqual({ clean: false })
})
it('refreshLocalBaseRefForWorktreeCreate sends the narrow refresh request', async () => {
await provider.refreshLocalBaseRefForWorktreeCreate({
repoPath: '/home/user/repo',
fullRef: 'refs/heads/main',
remoteTrackingRef: 'refs/remotes/origin/main',
ownerWorktreePath: '/home/user/repo'
})
expect(mux.request).toHaveBeenCalledWith('git.refreshLocalBaseRefForWorktreeCreate', {
repoPath: '/home/user/repo',
fullRef: 'refs/heads/main',
remoteTrackingRef: 'refs/remotes/origin/main',
ownerWorktreePath: '/home/user/repo'
})
})
it('worktreeIsClean falls back to git.status for old relays', async () => {
const methodNotFound = Object.assign(new Error('Method not found: git.worktreeIsClean'), {
code: -32601
})
mux.request.mockRejectedValueOnce(methodNotFound).mockResolvedValueOnce({
entries: [{ path: 'scratch.txt', status: 'untracked', area: 'untracked' }],
conflictOperation: 'unknown'
})
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
try {
const result = await provider.worktreeIsClean('/home/user/feat')
expect(mux.request).toHaveBeenNthCalledWith(1, 'git.worktreeIsClean', {
worktreePath: '/home/user/feat'
})
expect(mux.request).toHaveBeenNthCalledWith(2, 'git.status', {
worktreePath: '/home/user/feat'
})
expect(result).toEqual({ clean: false, stdout: 'untracked untracked: scratch.txt' })
} finally {
warnSpy.mockRestore()
}
})
it('worktreeIsClean filters untracked entries in old-relay fallback', async () => {
const methodNotFound = Object.assign(new Error('Method not found: git.worktreeIsClean'), {
code: -32601
})
mux.request.mockRejectedValueOnce(methodNotFound).mockResolvedValueOnce({
entries: [{ path: 'scratch.txt', status: 'untracked', area: 'untracked' }],
conflictOperation: 'unknown'
})
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
try {
await expect(
provider.worktreeIsClean('/home/user/feat', { includeUntracked: false })
).resolves.toEqual({ clean: true })
expect(mux.request).toHaveBeenNthCalledWith(2, 'git.status', {
worktreePath: '/home/user/feat'
})
} finally {
warnSpy.mockRestore()
}
})
it('renameCurrentBranch sends the narrow branch-rename request', async () => {
await provider.renameCurrentBranch('/home/user/feat', 'you/fix-auth')
expect(mux.request).toHaveBeenCalledWith('git.renameCurrentBranch', {
worktreePath: '/home/user/feat',
newBranch: 'you/fix-auth'
})
})
it('forceDeletePreservedBranch sends the preserved-branch delete request', async () => {
await provider.forceDeletePreservedBranch('/home/user/repo', 'you/fix-auth', 'abc123')
expect(mux.request).toHaveBeenCalledWith('git.forceDeletePreservedBranch', {
repoPath: '/home/user/repo',
branchName: 'you/fix-auth',
expectedHead: 'abc123'
})
})
it('forceDeletePreservedBranch maps old relays to the reconnect message', async () => {
const methodNotFound = Object.assign(
new Error('Method not found: git.forceDeletePreservedBranch'),
{ code: -32601 }
)
mux.request.mockRejectedValueOnce(methodNotFound)
await expect(
provider.forceDeletePreservedBranch('/home/user/repo', 'you/fix-auth', 'abc123')
).rejects.toThrow(
'This SSH host is running an older Orca relay that cannot delete preserved branches. Reconnect to deploy the latest relay, then try again.'
)
})
it('forceDeletePreservedBranch rethrows non-method-not-found errors', async () => {
const error = new Error('remote update-ref failed')
mux.request.mockRejectedValueOnce(error)
await expect(
provider.forceDeletePreservedBranch('/home/user/repo', 'you/fix-auth', 'abc123')
).rejects.toBe(error)
})
it('isGitRepo always returns true for remote paths', () => {
expect(provider.isGitRepo('/any/path')).toBe(true)
})
})