Files
orca/src/main/runtime/rpc/methods/git.test.ts
T
Trevin ChowandJinjing a325ac467c feat: expandable commits and actions in the git history panel (#5419)
* feat: expandable commits and actions in the git history panel

Expand a commit row in the Commits panel to see its changed files inline; click a file to open that file's commit diff. Author and date surface on expand, so the dense row itself stays subject-only.

Right-click a commit for: open in the in-app browser, copy hash, copy message, and explain changes (spawns the default agent seeded with the commit context).

Open-in-browser resolves the provider commit URL in the main process via a new remoteCommitUrl resolver (GitHub/GitLab/Bitbucket), mirroring the existing remoteFileUrl chain end-to-end (repo, IPC, SSH provider, runtime RPC, preload) so it works for local and SSH/remote workspaces.

Layout: subject-first single-line rows with a tighter graph, refs moved inline, and local/remote ref pills deduped when they point at the same commit.

* fix: address git history review feedback

* fix: address PR review feedback on the git history panel

- Trim commit SHA before building the remote URL so whitespace input returns null instead of an invalid %20 URL.
- Gate commit-row expansion on the file loader (onLoadCommitFiles) so a row can't expand into a perpetual loading state.
- Harden the explain prompt: treat the commit subject and diff as untrusted data and run git show --no-ext-diff.
- Keep ambiguous multi-segment remote refs instead of mis-deduping them against a local branch.
- Use standard 10-char i18n keys for the new commit-history strings and translate them into es/ja/ko/zh.

* refactor: extract commit-history actions into useGitHistoryCommitActions hook

Moves the commit load/open/context-menu action callbacks (and the per-commit compare cache) out of SourceControl.tsx — which already carries a max-lines disable — into a focused hook, addressing the PR review nitpick. Behavior is unchanged.

* Refine git history row rendering, ref deduplication, and OID validation

- Prevent deduplication of remote branch badges in the history view
  when multiple remotes exist or when a ref is explicitly preserved.
- Render GitHistoryRow as an accessible button with dynamic ARIA labels
  for expansion states.
- Add double-click handler on commit files to open them permanently.
- Validate commit SHAs as full 40-character Git object IDs before
  requesting remote commit URLs.

---------

Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
2026-06-15 19:28:45 -07:00

536 lines
18 KiB
TypeScript

/* eslint-disable max-lines -- Why: git RPC methods share one dispatcher fixture, and keeping the contract cases together makes method coverage easy to audit. */
import { describe, expect, it, vi } from 'vitest'
import { RpcDispatcher } from '../dispatcher'
import type { RpcRequest } from '../core'
import type { OrcaRuntimeService } from '../../orca-runtime'
import { GIT_METHODS } from './git'
function makeRequest(method: string, params?: unknown): RpcRequest {
return { id: 'req-1', authToken: 'tok', method, params }
}
describe('git RPC methods', () => {
it('returns status for a selected worktree', async () => {
const runtime = {
getRuntimeId: () => 'test-runtime',
getRuntimeGitStatus: vi.fn().mockResolvedValue({
entries: [],
conflictOperation: 'unknown',
branch: 'main',
head: 'abc'
})
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: GIT_METHODS })
const response = await dispatcher.dispatch(makeRequest('git.status', { worktree: 'id:wt-1' }))
expect(runtime.getRuntimeGitStatus).toHaveBeenCalledWith('id:wt-1')
expect(response).toMatchObject({
ok: true,
result: { entries: [], branch: 'main' }
})
})
it('forwards includeIgnored for status requests', async () => {
const runtime = {
getRuntimeId: () => 'test-runtime',
getRuntimeGitStatus: vi.fn().mockResolvedValue({
entries: [],
conflictOperation: 'unknown',
ignoredPaths: ['dist/']
})
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: GIT_METHODS })
const response = await dispatcher.dispatch(
makeRequest('git.status', { worktree: 'id:wt-1', includeIgnored: true })
)
expect(runtime.getRuntimeGitStatus).toHaveBeenCalledWith('id:wt-1', {
includeIgnored: true
})
expect(response).toMatchObject({
ok: true,
result: { ignoredPaths: ['dist/'] }
})
})
it('returns ignored paths for selected explorer rows', async () => {
const runtime = {
getRuntimeId: () => 'test-runtime',
checkRuntimeGitIgnoredPaths: vi.fn().mockResolvedValue(['dist/bundle.js'])
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: GIT_METHODS })
const response = await dispatcher.dispatch(
makeRequest('git.checkIgnored', {
worktree: 'id:wt-1',
paths: ['dist/bundle.js', 'src/index.ts']
})
)
expect(runtime.checkRuntimeGitIgnoredPaths).toHaveBeenCalledWith('id:wt-1', [
'dist/bundle.js',
'src/index.ts'
])
expect(response).toMatchObject({
ok: true,
result: ['dist/bundle.js']
})
})
it('returns a worktree file diff', async () => {
const runtime = {
getRuntimeId: () => 'test-runtime',
getRuntimeGitDiff: vi.fn().mockResolvedValue({
kind: 'text',
originalContent: '',
modifiedContent: 'hello',
originalIsBinary: false,
modifiedIsBinary: false
})
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: GIT_METHODS })
const response = await dispatcher.dispatch(
makeRequest('git.diff', {
worktree: 'id:wt-1',
filePath: 'src/index.ts',
staged: false,
compareAgainstHead: true
})
)
expect(runtime.getRuntimeGitDiff).toHaveBeenCalledWith('id:wt-1', 'src/index.ts', false, true)
expect(response).toMatchObject({
ok: true,
result: { kind: 'text', modifiedContent: 'hello' }
})
})
it('returns bounded git history for a selected worktree', async () => {
const history = {
items: [],
hasIncomingChanges: false,
hasOutgoingChanges: false,
hasMore: false,
limit: 50
}
const runtime = {
getRuntimeId: () => 'test-runtime',
getRuntimeGitHistory: vi.fn().mockResolvedValue(history)
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: GIT_METHODS })
const response = await dispatcher.dispatch(
makeRequest('git.history', {
worktree: 'id:wt-1',
limit: 25,
baseRef: 'origin/main'
})
)
expect(runtime.getRuntimeGitHistory).toHaveBeenCalledWith('id:wt-1', {
limit: 25,
baseRef: 'origin/main'
})
expect(response).toMatchObject({ ok: true, result: history })
})
it('routes common mutations to the runtime', async () => {
const runtime = {
getRuntimeId: () => 'test-runtime',
stageRuntimeGitPath: vi.fn().mockResolvedValue({ ok: true }),
bulkUnstageRuntimeGitPaths: vi.fn().mockResolvedValue({ ok: true }),
discardRuntimeGitPath: vi.fn().mockResolvedValue({ ok: true }),
bulkDiscardRuntimeGitPaths: vi.fn().mockResolvedValue({ ok: true })
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: GIT_METHODS })
await dispatcher.dispatch(
makeRequest('git.stage', { worktree: 'id:wt-1', filePath: 'src/a.ts' })
)
await dispatcher.dispatch(
makeRequest('git.bulkUnstage', { worktree: 'id:wt-1', filePaths: ['src/a.ts', 'b.ts'] })
)
await dispatcher.dispatch(
makeRequest('git.discard', { worktree: 'id:wt-1', filePath: 'src/a.ts' })
)
await dispatcher.dispatch(
makeRequest('git.bulkDiscard', { worktree: 'id:wt-1', filePaths: ['src/a.ts', 'b.ts'] })
)
expect(runtime.stageRuntimeGitPath).toHaveBeenCalledWith('id:wt-1', 'src/a.ts')
expect(runtime.bulkUnstageRuntimeGitPaths).toHaveBeenCalledWith('id:wt-1', ['src/a.ts', 'b.ts'])
expect(runtime.discardRuntimeGitPath).toHaveBeenCalledWith('id:wt-1', 'src/a.ts')
expect(runtime.bulkDiscardRuntimeGitPaths).toHaveBeenCalledWith('id:wt-1', ['src/a.ts', 'b.ts'])
})
it('rejects empty bulk mutation paths before calling the runtime', async () => {
const runtime = {
getRuntimeId: () => 'test-runtime',
bulkDiscardRuntimeGitPaths: vi.fn()
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: GIT_METHODS })
const response = await dispatcher.dispatch(
makeRequest('git.bulkDiscard', { worktree: 'id:wt-1', filePaths: [''] })
)
expect(response.ok).toBe(false)
expect(response).toMatchObject({
error: expect.objectContaining({ code: 'invalid_argument' })
})
expect(runtime.bulkDiscardRuntimeGitPaths).not.toHaveBeenCalled()
})
it('routes remote operations to the runtime', async () => {
const runtime = {
getRuntimeId: () => 'test-runtime',
commitRuntimeGit: vi.fn().mockResolvedValue({ success: true }),
generateRuntimeCommitMessage: vi
.fn()
.mockResolvedValue({ success: true, message: 'feat: test' }),
discoverRuntimeCommitMessageModels: vi.fn().mockResolvedValue({
success: true,
models: [{ id: 'auto', label: 'Auto' }],
defaultModelId: 'auto'
}),
cancelRuntimeGenerateCommitMessage: vi.fn().mockResolvedValue({ ok: true }),
abortRuntimeGitMerge: vi.fn().mockResolvedValue({ ok: true }),
abortRuntimeGitRebase: vi.fn().mockResolvedValue({ ok: true }),
pushRuntimeGit: vi.fn().mockResolvedValue({ ok: true }),
getRuntimeGitRemoteFileUrl: vi.fn().mockResolvedValue('https://example.com/file#L3'),
getRuntimeGitRemoteCommitUrl: vi.fn().mockResolvedValue('https://example.com/commit/abc')
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: GIT_METHODS })
const commitOid = '0123456789abcdef0123456789abcdef01234567'
await dispatcher.dispatch(
makeRequest('git.commit', { worktree: 'id:wt-1', message: 'feat: test' })
)
await dispatcher.dispatch(makeRequest('git.generateCommitMessage', { worktree: 'id:wt-1' }))
await dispatcher.dispatch(
makeRequest('git.discoverCommitMessageModels', {
worktree: 'id:wt-1',
agentId: 'cursor',
agentCmdOverrides: { cursor: 'cursor-agent' }
})
)
await dispatcher.dispatch(
makeRequest('git.cancelGenerateCommitMessage', { worktree: 'id:wt-1' })
)
await dispatcher.dispatch(makeRequest('git.abortMerge', { worktree: 'id:wt-1' }))
await dispatcher.dispatch(makeRequest('git.abortRebase', { worktree: 'id:wt-1' }))
await dispatcher.dispatch(
makeRequest('git.push', {
worktree: 'id:wt-1',
publish: true,
pushTarget: { remoteName: 'origin', branchName: 'feature' }
})
)
const response = await dispatcher.dispatch(
makeRequest('git.remoteFileUrl', {
worktree: 'id:wt-1',
relativePath: 'src/a.ts',
line: 3
})
)
const commitUrlResponse = await dispatcher.dispatch(
makeRequest('git.remoteCommitUrl', {
worktree: 'id:wt-1',
sha: commitOid
})
)
expect(runtime.commitRuntimeGit).toHaveBeenCalledWith('id:wt-1', 'feat: test')
expect(runtime.generateRuntimeCommitMessage).toHaveBeenCalledWith('id:wt-1')
expect(runtime.discoverRuntimeCommitMessageModels).toHaveBeenCalledWith('id:wt-1', 'cursor', {
agentCmdOverrides: { cursor: 'cursor-agent' }
})
expect(runtime.cancelRuntimeGenerateCommitMessage).toHaveBeenCalledWith('id:wt-1')
expect(runtime.abortRuntimeGitMerge).toHaveBeenCalledWith('id:wt-1')
expect(runtime.abortRuntimeGitRebase).toHaveBeenCalledWith('id:wt-1')
expect(runtime.pushRuntimeGit).toHaveBeenCalledWith(
'id:wt-1',
true,
{ remoteName: 'origin', branchName: 'feature' },
undefined
)
expect(response).toMatchObject({ ok: true, result: 'https://example.com/file#L3' })
expect(runtime.getRuntimeGitRemoteCommitUrl).toHaveBeenCalledWith('id:wt-1', commitOid)
expect(commitUrlResponse).toMatchObject({ ok: true, result: 'https://example.com/commit/abc' })
})
it('rejects remote commit URL requests without a full git object id', async () => {
const runtime = {
getRuntimeId: () => 'test-runtime',
getRuntimeGitRemoteCommitUrl: vi.fn()
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: GIT_METHODS })
const response = await dispatcher.dispatch(
makeRequest('git.remoteCommitUrl', {
worktree: 'id:wt-1',
sha: 'abc123'
})
)
expect(response.ok).toBe(false)
expect(runtime.getRuntimeGitRemoteCommitUrl).not.toHaveBeenCalled()
})
it('forwards force-with-lease push mode to the runtime', async () => {
const runtime = {
getRuntimeId: () => 'test-runtime',
pushRuntimeGit: vi.fn().mockResolvedValue({ ok: true })
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: GIT_METHODS })
await dispatcher.dispatch(
makeRequest('git.push', {
worktree: 'id:wt-1',
forceWithLease: true
})
)
expect(runtime.pushRuntimeGit).toHaveBeenCalledWith('id:wt-1', undefined, undefined, true)
})
it('forwards rebase-from-base to the runtime', async () => {
const runtime = {
getRuntimeId: () => 'test-runtime',
rebaseRuntimeGitFromBase: vi.fn().mockResolvedValue({ ok: true })
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: GIT_METHODS })
await dispatcher.dispatch(
makeRequest('git.rebaseFromBase', {
worktree: 'id:wt-1',
baseRef: 'origin/main'
})
)
expect(runtime.rebaseRuntimeGitFromBase).toHaveBeenCalledWith('id:wt-1', 'origin/main')
})
it('forwards fetch push target to the runtime', async () => {
const runtime = {
getRuntimeId: () => 'test-runtime',
fetchRuntimeGit: vi.fn().mockResolvedValue({ ok: true })
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: GIT_METHODS })
const pushTarget = { remoteName: 'fork', branchName: 'feature' }
await dispatcher.dispatch(
makeRequest('git.fetch', {
worktree: 'id:wt-1',
pushTarget
})
)
expect(runtime.fetchRuntimeGit).toHaveBeenCalledWith('id:wt-1', pushTarget)
})
it('forwards fast-forward push target to the runtime', async () => {
const runtime = {
getRuntimeId: () => 'test-runtime',
fastForwardRuntimeGit: vi.fn().mockResolvedValue({ ok: true })
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: GIT_METHODS })
const pushTarget = { remoteName: 'fork', branchName: 'feature' }
await dispatcher.dispatch(
makeRequest('git.fastForward', {
worktree: 'id:wt-1',
pushTarget
})
)
expect(runtime.fastForwardRuntimeGit).toHaveBeenCalledWith('id:wt-1', pushTarget)
})
it('forwards commit-message settings to the runtime', async () => {
const commitMessageAi = {
enabled: true,
agentId: 'codex',
selectedModelByAgent: { codex: 'gpt-5.3-codex-spark' },
selectedModelByAgentByHost: { 'ssh:conn-1': { cursor: 'remote-model' } },
discoveredModelsByAgent: {
cursor: [{ id: 'local-model', label: 'Local Model' }]
},
discoveredModelsByAgentByHost: {
'ssh:conn-1': {
cursor: [{ id: 'remote-model', label: 'Remote Model' }]
}
},
selectedThinkingByModel: { 'gpt-5.3-codex-spark': 'medium' },
customPrompt: '',
customAgentCommand: ''
}
const agentCmdOverrides = { codex: 'codex --profile work' }
const runtime = {
getRuntimeId: () => 'test-runtime',
generateRuntimeCommitMessage: vi.fn().mockResolvedValue({ success: true, message: 'test' })
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: GIT_METHODS })
await dispatcher.dispatch(
makeRequest('git.generateCommitMessage', {
worktree: 'id:wt-1',
commitMessageAi,
agentCmdOverrides,
enableGitHubAttribution: true,
commitMessageDiscoveryHostKey: 'runtime:env-1'
})
)
expect(runtime.generateRuntimeCommitMessage).toHaveBeenCalledWith('id:wt-1', {
commitMessageAi,
agentCmdOverrides,
enableGitHubAttribution: true,
commitMessageDiscoveryHostKey: 'runtime:env-1'
})
})
it('forwards one-shot commit-message params to the runtime', async () => {
const sourceControlAiResolvedParams = {
agentId: 'codex',
model: 'gpt-5.5',
thinkingLevel: 'high',
customPrompt: 'Use Conventional Commits.'
}
const runtime = {
getRuntimeId: () => 'test-runtime',
generateRuntimeCommitMessage: vi.fn().mockResolvedValue({ success: true, message: 'test' })
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: GIT_METHODS })
await dispatcher.dispatch(
makeRequest('git.generateCommitMessage', {
worktree: 'id:wt-1',
sourceControlAiResolvedParams
})
)
expect(runtime.generateRuntimeCommitMessage).toHaveBeenCalledWith('id:wt-1', {
sourceControlAiResolvedParams
})
})
it('forwards one-shot pull-request generation params to the runtime', async () => {
const sourceControlAiResolvedParams = {
agentId: 'codex',
model: 'gpt-5.5',
thinkingLevel: 'high',
commandInputTemplate: '{basePrompt}\n\nUse release-note style.'
}
const runtime = {
getRuntimeId: () => 'test-runtime',
generateRuntimePullRequestFields: vi
.fn()
.mockResolvedValue({ success: true, fields: { title: 'Test', body: '', draft: false } })
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: GIT_METHODS })
await dispatcher.dispatch(
makeRequest('git.generatePullRequestFields', {
worktree: 'id:wt-1',
base: 'main',
title: '',
body: '',
draft: false,
sourceControlAiResolvedParams
})
)
expect(runtime.generateRuntimePullRequestFields).toHaveBeenCalledWith(
'id:wt-1',
{ base: 'main', title: '', body: '', draft: false },
{ sourceControlAiResolvedParams }
)
})
it('rejects malformed commit-message settings before calling the runtime', async () => {
const runtime = {
getRuntimeId: () => 'test-runtime',
generateRuntimeCommitMessage: vi.fn()
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: GIT_METHODS })
const response = await dispatcher.dispatch(
makeRequest('git.generateCommitMessage', {
worktree: 'id:wt-1',
commitMessageAi: {
enabled: true,
agentId: 'codex'
}
})
)
expect(response.ok).toBe(false)
expect(response).toMatchObject({
error: expect.objectContaining({ code: 'invalid_argument' })
})
expect(runtime.generateRuntimeCommitMessage).not.toHaveBeenCalled()
})
it('rejects branch diff revisions that are not full object ids', async () => {
const runtime = {
getRuntimeId: () => 'test-runtime',
getRuntimeGitBranchDiff: vi.fn()
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: GIT_METHODS })
const response = await dispatcher.dispatch(
makeRequest('git.branchDiff', {
worktree: 'id:wt-1',
filePath: 'src/a.ts',
compare: {
headOid: '--output=/tmp/orca-test',
mergeBase: 'a'.repeat(40)
}
})
)
expect(response.ok).toBe(false)
expect(runtime.getRuntimeGitBranchDiff).not.toHaveBeenCalled()
})
it('rejects branch compare refs that look like git options', async () => {
const runtime = {
getRuntimeId: () => 'test-runtime',
getRuntimeGitBranchCompare: vi.fn()
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: GIT_METHODS })
const response = await dispatcher.dispatch(
makeRequest('git.branchCompare', {
worktree: 'id:wt-1',
baseRef: '--output=/tmp/orca-test'
})
)
expect(response.ok).toBe(false)
expect(runtime.getRuntimeGitBranchCompare).not.toHaveBeenCalled()
})
it('rejects git history limits above the runtime cap', async () => {
const runtime = {
getRuntimeId: () => 'test-runtime',
getRuntimeGitHistory: vi.fn()
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: GIT_METHODS })
const response = await dispatcher.dispatch(
makeRequest('git.history', {
worktree: 'id:wt-1',
limit: 201
})
)
expect(response.ok).toBe(false)
expect(runtime.getRuntimeGitHistory).not.toHaveBeenCalled()
})
})