Skip to content

Commit b280c74

Browse files
authored
fix serialize git worktree mutations and forward teammate PATH (Gitlawb#721)
1 parent 2ff5710 commit b280c74

File tree

4 files changed

+289
-134
lines changed

4 files changed

+289
-134
lines changed

src/utils/swarm/spawnUtils.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { afterEach, beforeEach, expect, test } from 'bun:test'
2+
3+
import { buildInheritedEnvVars } from './spawnUtils.js'
4+
5+
const ORIGINAL_ENV = { ...process.env }
6+
7+
beforeEach(() => {
8+
for (const key of Object.keys(process.env)) {
9+
delete process.env[key]
10+
}
11+
})
12+
13+
afterEach(() => {
14+
for (const key of Object.keys(process.env)) {
15+
delete process.env[key]
16+
}
17+
Object.assign(process.env, ORIGINAL_ENV)
18+
})
19+
20+
test('buildInheritedEnvVars marks spawned teammates as host-managed for provider routing', () => {
21+
const envVars = buildInheritedEnvVars()
22+
23+
expect(envVars).toContain('CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST=1')
24+
})
25+
26+
test('buildInheritedEnvVars forwards PATH for source-built teammate tool lookups', () => {
27+
process.env.PATH = '/custom/bin:/usr/bin'
28+
29+
const envVars = buildInheritedEnvVars()
30+
31+
expect(envVars).toContain('PATH=')
32+
expect(envVars).toContain('/custom/bin\\:/usr/bin')
33+
})

src/utils/swarm/spawnUtils.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,9 @@ const TEAMMATE_ENV_VARS = [
141141
'NODE_EXTRA_CA_CERTS',
142142
'REQUESTS_CA_BUNDLE',
143143
'CURL_CA_BUNDLE',
144+
// Source builds may rely on user shell PATH for rg/node/bun and other tools.
145+
// Forward it so teammates resolve the same toolchain as the parent session.
146+
'PATH',
144147
] as const
145148

146149
/**
@@ -149,7 +152,13 @@ const TEAMMATE_ENV_VARS = [
149152
* plus any provider/config env vars that are set in the current process.
150153
*/
151154
export function buildInheritedEnvVars(): string {
152-
const envVars = ['CLAUDECODE=1', 'CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1']
155+
const envVars = [
156+
'CLAUDECODE=1',
157+
'CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1',
158+
// Teammates should inherit the leader-selected provider route instead of
159+
// replaying persisted ~/.claude or settings.env provider defaults.
160+
'CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST=1',
161+
]
153162

154163
for (const key of TEAMMATE_ENV_VARS) {
155164
const value = process.env[key]

src/utils/worktree.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { afterEach, expect, test } from 'bun:test'
2+
3+
import {
4+
_resetGitWorktreeMutationLocksForTesting,
5+
withGitWorktreeMutationLock,
6+
} from './worktree.js'
7+
8+
afterEach(() => {
9+
_resetGitWorktreeMutationLocksForTesting()
10+
})
11+
12+
test('withGitWorktreeMutationLock serializes mutations for the same repo', async () => {
13+
const order: string[] = []
14+
let releaseFirst!: () => void
15+
const firstGate = new Promise<void>(resolve => {
16+
releaseFirst = resolve
17+
})
18+
19+
const first = withGitWorktreeMutationLock('/repo', async () => {
20+
order.push('first:start')
21+
await firstGate
22+
order.push('first:end')
23+
})
24+
25+
const second = withGitWorktreeMutationLock('/repo', async () => {
26+
order.push('second:start')
27+
order.push('second:end')
28+
})
29+
30+
await Promise.resolve()
31+
await Promise.resolve()
32+
expect(order).toEqual(['first:start'])
33+
34+
releaseFirst()
35+
await Promise.all([first, second])
36+
37+
expect(order).toEqual([
38+
'first:start',
39+
'first:end',
40+
'second:start',
41+
'second:end',
42+
])
43+
})
44+
45+
test('withGitWorktreeMutationLock does not serialize different repos', async () => {
46+
const order: string[] = []
47+
let releaseFirst!: () => void
48+
const firstGate = new Promise<void>(resolve => {
49+
releaseFirst = resolve
50+
})
51+
52+
const first = withGitWorktreeMutationLock('/repo-a', async () => {
53+
order.push('a:start')
54+
await firstGate
55+
order.push('a:end')
56+
})
57+
58+
const second = withGitWorktreeMutationLock('/repo-b', async () => {
59+
order.push('b:start')
60+
order.push('b:end')
61+
})
62+
63+
await Promise.resolve()
64+
await Promise.resolve()
65+
expect(order).toEqual(['a:start', 'b:start', 'b:end'])
66+
67+
releaseFirst()
68+
await Promise.all([first, second])
69+
})

0 commit comments

Comments
 (0)