fix(agent-hooks): route the first-work branch rename by the repo's execution host (#27080)

* fix(agent-hooks): route the first-work branch rename by the repo's execution host

* fix(agent-hooks): state runtime branch ownership without overclaiming server wiring

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
This commit is contained in:
OrcaWin
2026-10-09 19:00:46 -07:00
committed by GitHub
co-authored by m4air
parent cbc565d804
commit 6f8dc43288
2 changed files with 61 additions and 5 deletions
@@ -21,7 +21,7 @@ const {
gitExecFileAsyncMock: vi.fn(),
resolveLocalGitUsernameMock: vi.fn(async () => 'you'),
getSshGitUsernameMock: vi.fn(async () => 'you'),
getSshGitProviderMock: vi.fn(() => undefined),
getSshGitProviderMock: vi.fn((_connectionId: string): unknown => undefined),
generateBranchNameMock: vi.fn(),
resolveTextGenerationParamsMock: vi.fn(),
prepareLocalEnvMock: vi.fn(async () => ({ ok: true as const })),
@@ -65,6 +65,14 @@ import {
} from './first-work-branch-rename-test-harness'
import { createStructuredAgentSessionLogger } from '../native-chat/agent-session-wire/structured-agent-session-logger'
const REMOTE_REPO_BASE: Repo = {
id: REPO_ID,
path: '/repo',
displayName: 'repo',
badgeColor: '',
addedAt: 0
}
function makeDeps(overrides: Partial<FirstWorkBranchRenameDeps> = {}) {
return makeBranchRenameDeps(vi.fn, overrides)
}
@@ -733,4 +741,44 @@ describe('maybeAutoRenameBranchOnFirstWork', () => {
expect(provider.renameCurrentBranch).toHaveBeenCalledWith('/repo/wt', 'you/fix-auth')
expect(onRenamed).toHaveBeenCalledWith(REPO_ID)
})
it('routes a row that names its SSH owner only as executionHostId to that target', async () => {
const provider = {
exec: vi.fn(gitResponder({ currentBranch: 'you/Nautilus', hasUpstream: false })),
renameCurrentBranch: vi.fn(async () => undefined),
executeCommitMessagePlan: vi.fn()
}
getSshGitProviderMock.mockReturnValue(provider)
const repo: Repo = { ...REMOTE_REPO_BASE, executionHostId: 'ssh:ssh-1' }
const { deps } = makeDeps({ getRepo: () => repo })
await maybeAutoRenameBranchOnFirstWork(workingEvent(), deps)
expect(getSshGitProviderMock).toHaveBeenCalledWith('ssh-1')
expect(gitExecFileAsyncMock).not.toHaveBeenCalled()
expect(provider.renameCurrentBranch).toHaveBeenCalledWith('/repo/wt', 'you/fix-auth')
})
it("never dials a paired server's nested SSH target from this client", async () => {
const provider = {
exec: vi.fn(gitResponder({ currentBranch: 'you/Nautilus', hasUpstream: false })),
renameCurrentBranch: vi.fn(async () => undefined),
executeCommitMessagePlan: vi.fn()
}
getSshGitProviderMock.mockReturnValue(provider)
const repo: Repo = {
...REMOTE_REPO_BASE,
connectionId: 'ssh-1',
executionHostId: 'runtime:env-1'
}
const { deps, onRenamed } = makeDeps({ getRepo: () => repo })
await maybeAutoRenameBranchOnFirstWork(workingEvent(), deps)
expect(getSshGitProviderMock).not.toHaveBeenCalled()
expect(provider.exec).not.toHaveBeenCalled()
expect(gitExecFileAsyncMock).not.toHaveBeenCalled()
expect(generateBranchNameMock).not.toHaveBeenCalled()
expect(onRenamed).not.toHaveBeenCalled()
})
})
@@ -14,7 +14,8 @@ import { getCommitMessageModelDiscoveryHostKey } from '../../shared/commit-messa
import { computeBranchName, getConfiguredBranchPrefix } from '../ipc/worktree-logic'
import { gitExecFileAsync } from '../git/runner'
import { getSshGitUsername, resolveLocalGitUsername } from '../git/git-username'
import { getSshGitProvider } from '../providers/ssh-git-dispatch'
import { resolveGitRouteForHost } from '../providers/execution-host-provider-dispatch'
import { getRepoExecutionHostId } from '../../shared/execution-host'
import {
probeBranchUpstream,
renameCurrentBranch,
@@ -176,10 +177,15 @@ async function runAutoRename(
}
const worktreePath = parsed.worktreePath
const provider = repo.connectionId ? (getSshGitProvider(repo.connectionId) ?? null) : null
if (repo.connectionId && !provider) {
// Why: a runtime row's connectionId names the server's own SSH target; that host owns the branch, never this client.
const route = resolveGitRouteForHost(getRepoExecutionHostId(repo))
if (route.kind === 'runtime') {
return stop(`branch is owned by ${route.hostId}`)
}
if (route.kind === 'ssh' && !route.provider) {
return retry('ssh provider unavailable')
}
const provider = route.kind === 'ssh' ? route.provider : null
const exec: GitExec = provider
? (args) => provider.exec(args, worktreePath)
: (args) => gitExecFileAsync(args, { cwd: worktreePath })
@@ -212,7 +218,9 @@ async function runAutoRename(
}
const settings = deps.getSettings()
const hostKey = getCommitMessageModelDiscoveryHostKey(repo.connectionId ?? null)
const hostKey = getCommitMessageModelDiscoveryHostKey(
route.kind === 'ssh' ? route.connectionId : null
)
const resolvedParams = resolveTextGenerationParams(settings, hostKey, 'branchName', repo)
if (!resolvedParams.ok) {
// Why: a generation-step failure (vs a benign skip) is user-actionable, so surface it on the card.