mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-19 13:33:35 +08:00
fix(agent): avoid concurrent worktree config writes (#572)
Git writes upstream branch config when a worktree starts from origin/main. Use the already-resolved base SHA as the worktree start point so parallel agent worktrees do not race on shared .git/config. Tested: bun test src/utils/__tests__/worktree.test.ts Tested: real gpt-5.5 Sub2API run with four worktree-isolated agents Not-tested: bun run check:server currently fails unrelated WebSocket restart/timeouts Confidence: high Scope-risk: narrow
This commit is contained in:
parent
760295dcd8
commit
a7263cbaf7
117
src/utils/__tests__/worktree.test.ts
Normal file
117
src/utils/__tests__/worktree.test.ts
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
||||||
|
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
||||||
|
import { tmpdir } from 'node:os'
|
||||||
|
import { join } from 'node:path'
|
||||||
|
import {
|
||||||
|
getCwdState,
|
||||||
|
getOriginalCwd,
|
||||||
|
setCwdState,
|
||||||
|
setOriginalCwd,
|
||||||
|
} from '../../bootstrap/state.js'
|
||||||
|
import { resetGitFileWatcher } from '../git/gitFilesystem.js'
|
||||||
|
import { resetSettingsCache } from '../settings/settingsCache.js'
|
||||||
|
import { createAgentWorktree, worktreeBranchName } from '../worktree.js'
|
||||||
|
|
||||||
|
let tempDir: string
|
||||||
|
let repoDir: string
|
||||||
|
let originalCwdState: string
|
||||||
|
let originalCwd: string
|
||||||
|
let originalConfigDir: string | undefined
|
||||||
|
|
||||||
|
function runGit(cwd: string, args: string[], allowFailure = false): {
|
||||||
|
exitCode: number
|
||||||
|
stdout: string
|
||||||
|
stderr: string
|
||||||
|
} {
|
||||||
|
const result = Bun.spawnSync(['git', ...args], {
|
||||||
|
cwd,
|
||||||
|
stdout: 'pipe',
|
||||||
|
stderr: 'pipe',
|
||||||
|
})
|
||||||
|
const stdout = new TextDecoder().decode(result.stdout)
|
||||||
|
const stderr = new TextDecoder().decode(result.stderr)
|
||||||
|
if (!allowFailure && result.exitCode !== 0) {
|
||||||
|
throw new Error(stderr || stdout)
|
||||||
|
}
|
||||||
|
return { exitCode: result.exitCode, stdout, stderr }
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeRepoFile(relativePath: string, content: string): void {
|
||||||
|
const filePath = join(repoDir, relativePath)
|
||||||
|
mkdirSync(join(filePath, '..'), { recursive: true })
|
||||||
|
writeFileSync(filePath, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
function commit(message: string): void {
|
||||||
|
runGit(repoDir, ['add', '.'])
|
||||||
|
runGit(repoDir, ['commit', '-m', message])
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('createAgentWorktree', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
originalCwdState = getCwdState()
|
||||||
|
originalCwd = getOriginalCwd()
|
||||||
|
originalConfigDir = process.env.CLAUDE_CONFIG_DIR
|
||||||
|
|
||||||
|
tempDir = mkdtempSync(join(tmpdir(), 'cc-haha-agent-worktree-'))
|
||||||
|
repoDir = join(tempDir, 'repo')
|
||||||
|
const originDir = join(tempDir, 'origin.git')
|
||||||
|
mkdirSync(repoDir, { recursive: true })
|
||||||
|
|
||||||
|
process.env.CLAUDE_CONFIG_DIR = join(tempDir, 'claude-config')
|
||||||
|
resetSettingsCache()
|
||||||
|
resetGitFileWatcher()
|
||||||
|
|
||||||
|
runGit(repoDir, ['init', '-b', 'main'])
|
||||||
|
runGit(repoDir, ['config', 'user.email', 'worktree-test@example.com'])
|
||||||
|
runGit(repoDir, ['config', 'user.name', 'Worktree Test'])
|
||||||
|
writeRepoFile('README.md', '# worktree test\n')
|
||||||
|
commit('initial')
|
||||||
|
|
||||||
|
runGit(tempDir, ['init', '--bare', originDir])
|
||||||
|
runGit(originDir, ['symbolic-ref', 'HEAD', 'refs/heads/main'])
|
||||||
|
runGit(repoDir, ['remote', 'add', 'origin', originDir])
|
||||||
|
runGit(repoDir, ['push', '-u', 'origin', 'main'])
|
||||||
|
|
||||||
|
setCwdState(repoDir)
|
||||||
|
setOriginalCwd(repoDir)
|
||||||
|
process.chdir(repoDir)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.chdir(originalCwd)
|
||||||
|
setCwdState(originalCwdState)
|
||||||
|
setOriginalCwd(originalCwd)
|
||||||
|
resetGitFileWatcher()
|
||||||
|
resetSettingsCache()
|
||||||
|
if (originalConfigDir === undefined) {
|
||||||
|
delete process.env.CLAUDE_CONFIG_DIR
|
||||||
|
} else {
|
||||||
|
process.env.CLAUDE_CONFIG_DIR = originalConfigDir
|
||||||
|
}
|
||||||
|
rmSync(tempDir, { recursive: true, force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('creates agent worktree branches without upstream tracking config', async () => {
|
||||||
|
const slug = 'agent-race-proof'
|
||||||
|
const { worktreeBranch } = await createAgentWorktree(slug)
|
||||||
|
|
||||||
|
expect(worktreeBranch).toBe(worktreeBranchName(slug))
|
||||||
|
|
||||||
|
const upstreamRemote = runGit(
|
||||||
|
repoDir,
|
||||||
|
['config', '--get', `branch.${worktreeBranch}.remote`],
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
const upstreamMerge = runGit(
|
||||||
|
repoDir,
|
||||||
|
['config', '--get', `branch.${worktreeBranch}.merge`],
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(upstreamRemote.exitCode).not.toBe(0)
|
||||||
|
expect(upstreamRemote.stdout.trim()).toBe('')
|
||||||
|
expect(upstreamMerge.exitCode).not.toBe(0)
|
||||||
|
expect(upstreamMerge.stdout.trim()).toBe('')
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -357,8 +357,10 @@ async function getOrCreateWorktree(
|
|||||||
addArgs.push('--no-checkout')
|
addArgs.push('--no-checkout')
|
||||||
}
|
}
|
||||||
// -B (not -b): reset any orphan branch left behind by a removed worktree dir.
|
// -B (not -b): reset any orphan branch left behind by a removed worktree dir.
|
||||||
// Saves a `git branch -D` subprocess (~15ms spawn overhead) on every create.
|
// Use the resolved SHA rather than origin/<branch>; a remote-tracking start
|
||||||
addArgs.push('-B', worktreeBranch, worktreePath, baseBranch)
|
// point makes git write branch.<name>.remote/merge into shared .git/config,
|
||||||
|
// which races when multiple agent worktrees start together.
|
||||||
|
addArgs.push('-B', worktreeBranch, worktreePath, baseSha)
|
||||||
|
|
||||||
const { code: createCode, stderr: createStderr } =
|
const { code: createCode, stderr: createStderr } =
|
||||||
await execFileNoThrowWithCwd(gitExe(), addArgs, { cwd: repoRoot })
|
await execFileNoThrowWithCwd(gitExe(), addArgs, { cwd: repoRoot })
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user