feat(claude): respawn panes on account set/unset + safe --purge (T-480 part 2)
The extension consumer for the per-repo account verbs, making set/unset fully functional. The Claude extension subscribes to accountActionChannel: - set / unset → ClaudeSessionOrchestrator.respawnForWorkspace(cwd): closes the workspace's solo sessions (awaiting real process death, T-437) and re-spawns each on the same id with --resume, so the conversation continues under the newly-bound CLAUDE_CONFIG_DIR (resolved at spawn by agentBootstrap). Team and forked sessions are skipped — re-joining the broker / re-forking on an account swap is out of scope; they adopt the account on their next natural spawn. - remove --purge → deletes the config dir behind isPurgeableAccountDir, a strict guard that only ever removes a ~/.claude-* directory that is a direct child of $HOME. The purge payload now carries the dir (the account is gone from the registry by publish time). login still only publishes its action — spawning the `claude login` terminal pane needs argv+env terminal-pane support and is split to T-485. Covered: respawnForWorkspace (respawn solo, skip fork/other-repo) and the purge guard's accept/reject matrix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -137,4 +137,16 @@ void main() {
|
||||
expect(probeExistingAccountDirs('/no/such/home/${DateTime.now().microsecondsSinceEpoch}'), isEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
group('purge guard (T-480)', () {
|
||||
const home = '/home/u';
|
||||
test('accepts ~/.claude-* direct children only, rejects everything else', () {
|
||||
expect(isPurgeableAccountDir('/home/u/.claude-work', home), isTrue);
|
||||
expect(isPurgeableAccountDir('/home/u/.claude', home), isFalse); // the real config dir
|
||||
expect(isPurgeableAccountDir('/home/u/projects', home), isFalse); // not .claude-*
|
||||
expect(isPurgeableAccountDir('/home/u/sub/.claude-work', home), isFalse); // nested, not a direct child
|
||||
expect(isPurgeableAccountDir('/etc/.claude-work', home), isFalse); // elsewhere on disk
|
||||
expect(isPurgeableAccountDir('/home/u/.claude-work', ''), isFalse); // no home → never delete
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -76,6 +76,20 @@ void main() {
|
||||
expect(created, hasLength(1));
|
||||
});
|
||||
|
||||
test('respawnForWorkspace respawns solo workspace sessions with --resume, skips forks + other repos (T-480)', () async {
|
||||
await orch.spawn(spec('primary')); // solo /repo → respawn
|
||||
await orch.spawn(SpawnSpec(id: 'fork', role: 'fork', sessionId: 'fork-uuid', cwd: '/repo', forkSourceSessionId: 'primary-uuid')); // fork → skip
|
||||
await orch.spawn(SpawnSpec(id: 'other', role: 'other', sessionId: 'other-uuid', cwd: '/elsewhere')); // other repo → skip
|
||||
final before = created.length; // 3
|
||||
|
||||
await orch.respawnForWorkspace('/repo');
|
||||
|
||||
expect(created.first.killed, isTrue, reason: "the primary's original process is killed");
|
||||
expect(created.length, before + 1, reason: 'exactly one session respawned');
|
||||
expect(spawnedArgs.last, contains('--resume'));
|
||||
expect(orch.sessions.map((s) => s.id), unorderedEquals(['primary', 'fork', 'other']));
|
||||
});
|
||||
|
||||
// T-374: spawn() check-then-acts across awaits; without the in-flight
|
||||
// map, two CONCURRENT spawns both passed the registry check and the
|
||||
// loser's live claude process was orphaned.
|
||||
|
||||
@@ -127,7 +127,7 @@ void main() {
|
||||
final r = await run(['remove', 'work'], flags: {'purge': true});
|
||||
expect(r.ok, isTrue, reason: r.error?.message);
|
||||
expect(store.accounts, isEmpty);
|
||||
expect(published.single.data, {'action': 'purge', 'name': 'work'});
|
||||
expect(published.single.data, {'action': 'purge', 'name': 'work', 'dir': '/home/u/.claude-work'});
|
||||
});
|
||||
|
||||
test('login publishes a login action with the dir; unknown account errors', () async {
|
||||
|
||||
Reference in New Issue
Block a user