bootstrap clide-hosted Claude sessions (Epic B)
Per D-83 the clide-hosted stream-json session is the primary dogfood target — the process clide spawns, so the one clide can fully equip. Adds a Flutter-free agent_bootstrap module and injects it centrally in ClaudeSessionOrchestrator.spawn so every hosted session (primary, secondary, fork, teammate) is handed: - CLIDE_SOCK (the per-workspace socket, D-70) + CLIDE_WORKSPACE in its env, and `clide` prepended to PATH when not already resolvable, so `clide …` works with zero manual socket discovery (T-215); - a system-prompt context note (--append-system-prompt) telling it it is inside clide and how to drive the IDE via `clide …`, plus the D-6 parity contract; the note merges with the team preamble into one --append-system-prompt (T-216); - a Bash(clide:*) allow rule (--allowedTools) so clide calls aren't prompted (T-217). The PATH resolver no-ops when clide is already on PATH (installed case, T-211) and falls back to ~/.local/bin and the dev native/<os>-<arch> tree; it degrades gracefully when none is found. Pure helpers are unit-tested; the fork-args test is made position-independent. Closes T-214, T-215, T-216, T-217 (under T-208 "Give Claude hands"). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
/// Tests for the clide-hosted agent bootstrap (Epic B / T-215..T-217, D-83).
|
||||
library;
|
||||
|
||||
import 'package:clide/builtin/claude/src/agent_bootstrap.dart';
|
||||
import 'package:clide/src/ipc/paths.dart' show workspaceSocketPath;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('clideContextNote (T-216)', () {
|
||||
test('names clide, the clide CLI, the env vars, and the parity contract', () {
|
||||
final note = clideContextNote('/home/dev/proj');
|
||||
expect(note, contains('inside clide'));
|
||||
expect(note, contains('clide <subsystem> <verb>'));
|
||||
expect(note, contains('CLIDE_WORKSPACE'));
|
||||
expect(note, contains('CLIDE_SOCK'));
|
||||
expect(note, contains('/home/dev/proj'));
|
||||
expect(note, contains('D-6'));
|
||||
});
|
||||
|
||||
test('lists only subsystems that dispatch today; not the unfinished status snapshot', () {
|
||||
final note = clideContextNote('/repo');
|
||||
expect(note, contains('git status'));
|
||||
expect(note, contains('editor active'));
|
||||
// `clide status` (T-221) and live pane reflection (Epic C) are not wired
|
||||
// yet — the note must not point the agent at a command that returns
|
||||
// nothing, mirroring the T-213 "never mislead an agent" guard.
|
||||
expect(note, isNot(contains('clide status')));
|
||||
});
|
||||
});
|
||||
|
||||
group('clideBashAllowRule (T-217)', () {
|
||||
test('is the command-scoped Bash rule and rides on --allowedTools', () {
|
||||
expect(clideBashAllowRule, 'Bash(clide:*)');
|
||||
expect(clideAllowedToolsArgs, ['--allowedTools', 'Bash(clide:*)']);
|
||||
});
|
||||
});
|
||||
|
||||
group('agentEnvDelta (T-215)', () {
|
||||
test('always exports CLIDE_SOCK + CLIDE_WORKSPACE', () {
|
||||
final d = agentEnvDelta(
|
||||
workspaceRoot: '/repo',
|
||||
socketPath: '/run/clide/abc.sock',
|
||||
currentPath: '/usr/bin',
|
||||
clideCliDir: null,
|
||||
);
|
||||
expect(d['CLIDE_WORKSPACE'], '/repo');
|
||||
expect(d['CLIDE_SOCK'], '/run/clide/abc.sock');
|
||||
});
|
||||
|
||||
test('leaves PATH untouched when clide is already resolvable (clideCliDir null)', () {
|
||||
final d = agentEnvDelta(
|
||||
workspaceRoot: '/repo',
|
||||
socketPath: '/s.sock',
|
||||
currentPath: '/usr/bin',
|
||||
clideCliDir: null,
|
||||
);
|
||||
expect(d.containsKey('PATH'), isFalse);
|
||||
});
|
||||
|
||||
test('prepends the cli dir to PATH when given', () {
|
||||
final d = agentEnvDelta(
|
||||
workspaceRoot: '/repo',
|
||||
socketPath: '/s.sock',
|
||||
currentPath: '/usr/bin:/bin',
|
||||
clideCliDir: '/home/dev/.local/bin',
|
||||
);
|
||||
expect(d['PATH'], '/home/dev/.local/bin:/usr/bin:/bin');
|
||||
});
|
||||
|
||||
test('sets PATH to just the cli dir when there is no current PATH', () {
|
||||
final d = agentEnvDelta(
|
||||
workspaceRoot: '/repo',
|
||||
socketPath: '/s.sock',
|
||||
currentPath: null,
|
||||
clideCliDir: '/opt/clide/bin',
|
||||
);
|
||||
expect(d['PATH'], '/opt/clide/bin');
|
||||
});
|
||||
});
|
||||
|
||||
group('resolveClideCliDir (T-215)', () {
|
||||
test('returns null when clide already resolves on PATH (no PATH change needed)', () {
|
||||
final dir = resolveClideCliDir(
|
||||
currentPath: '/usr/bin:/home/dev/.local/bin',
|
||||
candidateDirs: const ['/repo/native/linux-x64'],
|
||||
isExecutableFile: (p) => p == '/home/dev/.local/bin/clide',
|
||||
);
|
||||
expect(dir, isNull);
|
||||
});
|
||||
|
||||
test('returns the first candidate holding an executable clide when not on PATH', () {
|
||||
final dir = resolveClideCliDir(
|
||||
currentPath: '/usr/bin',
|
||||
candidateDirs: const ['/nope', '/repo/native/linux-x64', '/also'],
|
||||
isExecutableFile: (p) => p == '/repo/native/linux-x64/clide',
|
||||
);
|
||||
expect(dir, '/repo/native/linux-x64');
|
||||
});
|
||||
|
||||
test('returns null when nothing holds clide', () {
|
||||
final dir = resolveClideCliDir(
|
||||
currentPath: '/usr/bin',
|
||||
candidateDirs: const ['/a', '/b'],
|
||||
isExecutableFile: (_) => false,
|
||||
);
|
||||
expect(dir, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('agentBootstrap (IO wrapper)', () {
|
||||
test('socket in the delta matches workspaceSocketPath; allow rule in extraArgs', () {
|
||||
final b = agentBootstrap('/some/workspace');
|
||||
expect(b.envDelta['CLIDE_SOCK'], workspaceSocketPath('/some/workspace'));
|
||||
expect(b.envDelta['CLIDE_WORKSPACE'], '/some/workspace');
|
||||
expect(b.extraArgs, ['--allowedTools', 'Bash(clide:*)']);
|
||||
});
|
||||
|
||||
test('merges over the provided base env', () {
|
||||
final b = agentBootstrap('/ws', base: {'FOO': 'bar'});
|
||||
expect(b.envDelta['FOO'], 'bar');
|
||||
expect(b.envDelta['CLIDE_WORKSPACE'], '/ws');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -230,8 +230,12 @@ void main() {
|
||||
const sourceId = 'bbbb2222-2222-4222-8222-222222222222';
|
||||
await orch.spawn(forkSpec('fork-1', sourceId));
|
||||
expect(capturedArgs, isNotNull);
|
||||
expect(capturedArgs![0], '--resume');
|
||||
expect(capturedArgs![1], sourceId);
|
||||
// The Epic B bootstrap (T-215..T-217) prepends --append-system-prompt
|
||||
// + --allowedTools, so --resume <source> is no longer at index 0 — but
|
||||
// they stay adjacent and --fork-session is present, no --session-id.
|
||||
final resumeAt = capturedArgs!.indexOf('--resume');
|
||||
expect(resumeAt, greaterThanOrEqualTo(0));
|
||||
expect(capturedArgs![resumeAt + 1], sourceId);
|
||||
expect(capturedArgs!, contains('--fork-session'));
|
||||
expect(capturedArgs!, isNot(contains('--session-id')));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user