feat(claude): spawn hosted sessions under the bound account's CLAUDE_CONFIG_DIR (T-484)
The load-bearing piece of the multi-account epic (T-476): a bound workspace's hosted claude now spawns with CLAUDE_CONFIG_DIR set to that account's dir, so it runs under the bound account end-to-end. Every hosted session (primary / secondary / fork / teammate) inherits it — the orchestrator already routes all spawns through agentBootstrap. - New pure resolver claudeConfigDirForWorkspace(cwd, boundConfigDir, env): bound account dir > parent CLAUDE_CONFIG_DIR (respect the launcher) > null (Claude defaults to ~/.claude). The registry is injected as a plain lookup so agent_bootstrap stays Flutter-free (its tests run under `dart test`). - agentBootstrap merges CLAUDE_CONFIG_DIR BEFORE base, so an explicit SpawnSpec.env override still wins (override > binding > parent > unset); the key is omitted entirely when the resolver returns null. - Orchestrator carries an optional AccountRegistry; the claude extension builds it from ctx.settings. Null in tests → no injection (unchanged behaviour). No way to SET a binding yet (that's the CLI T-480 / settings UI T-482), so no changelog entry — the mechanism is in place, the surface lands next. Unit tests cover the resolver's four states and the envDelta precedence. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -83,6 +83,32 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('claudeConfigDirForWorkspace (T-484)', () {
|
||||
test('bound workspace → the account config dir', () {
|
||||
expect(claudeConfigDirForWorkspace(cwd: '/repo/a', boundConfigDir: (_) => '/home/u/.claude-work', env: const {}), '/home/u/.claude-work');
|
||||
});
|
||||
|
||||
test('unbound + parent CLAUDE_CONFIG_DIR set → respects the launcher choice', () {
|
||||
expect(
|
||||
claudeConfigDirForWorkspace(cwd: '/repo/a', boundConfigDir: (_) => null, env: const {'CLAUDE_CONFIG_DIR': '/home/u/.claude-personal'}),
|
||||
'/home/u/.claude-personal',
|
||||
);
|
||||
});
|
||||
|
||||
test('unbound + no parent env → null (Claude defaults to ~/.claude)', () {
|
||||
expect(claudeConfigDirForWorkspace(cwd: '/repo/a', boundConfigDir: (_) => null, env: const {}), isNull);
|
||||
});
|
||||
|
||||
test('binding beats the parent env (binding > parent)', () {
|
||||
expect(claudeConfigDirForWorkspace(cwd: '/repo/a', boundConfigDir: (_) => '/bound', env: const {'CLAUDE_CONFIG_DIR': '/parent'}), '/bound');
|
||||
});
|
||||
|
||||
test('an empty bound dir is ignored (falls through to parent / null)', () {
|
||||
expect(claudeConfigDirForWorkspace(cwd: '/r', boundConfigDir: (_) => '', env: const {'CLAUDE_CONFIG_DIR': '/parent'}), '/parent');
|
||||
expect(claudeConfigDirForWorkspace(cwd: '/r', boundConfigDir: (_) => '', env: const {}), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('agentBootstrap (IO wrapper)', () {
|
||||
test('socket in the delta matches workspaceSocketPath; allow rule in extraArgs', () {
|
||||
final b = agentBootstrap('/some/workspace');
|
||||
@@ -96,5 +122,15 @@ void main() {
|
||||
expect(b.envDelta['FOO'], 'bar');
|
||||
expect(b.envDelta['CLIDE_WORKSPACE'], '/ws');
|
||||
});
|
||||
|
||||
test('injects the bound workspace CLAUDE_CONFIG_DIR (T-484)', () {
|
||||
final b = agentBootstrap('/ws', boundConfigDir: (_) => '/home/u/.claude-work');
|
||||
expect(b.envDelta['CLAUDE_CONFIG_DIR'], '/home/u/.claude-work');
|
||||
});
|
||||
|
||||
test('an explicit base CLAUDE_CONFIG_DIR override beats the workspace binding (T-484)', () {
|
||||
final b = agentBootstrap('/ws', base: {'CLAUDE_CONFIG_DIR': '/override'}, boundConfigDir: (_) => '/bound');
|
||||
expect(b.envDelta['CLAUDE_CONFIG_DIR'], '/override');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user