feat(claude): Settings → Claude accounts registry list (T-482 part 2)
Completes the account settings surface with the global Accounts list under the Claude category: each registered account shows a sign-in dot, name, and config dir, with re-login and remove affordances plus an inline add-account field. - Sign-in status: accountIsSignedIn, a read-only probe (a .credentials.json, or an oauthAccount marker in .claude.json; under-reports on macOS keychain). - Add: registers ~/.claude-<name> and publishes a login action (T-485 opens the pane). - Remove: registry-remove, guarded while a workspace is bound — matching the CLI `account remove`. --purge dir deletion stays on the CLI flag. All management routes through the AccountRegistry + accountActionChannel, and the list rebuilds live off the settings notifier. en/nl strings + widget/probe tests added. Closes T-482. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -138,6 +138,31 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('sign-in probe (T-482)', () {
|
||||
test('true with a .credentials.json or an oauthAccount marker; false otherwise', () async {
|
||||
final home = await Directory.systemTemp.createTemp('clide-signin-');
|
||||
addTearDown(() => home.deleteSync(recursive: true));
|
||||
|
||||
// .credentials.json present → signed in.
|
||||
final a = Directory('${home.path}/.claude-a')..createSync();
|
||||
File('${a.path}/.credentials.json').writeAsStringSync('{}');
|
||||
expect(accountIsSignedIn(a.path), isTrue);
|
||||
|
||||
// .claude.json with an oauthAccount marker → signed in.
|
||||
final b = Directory('${home.path}/.claude-b')..createSync();
|
||||
File('${b.path}/.claude.json').writeAsStringSync('{"oauthAccount": {"email": "x@y.z"}}');
|
||||
expect(accountIsSignedIn(b.path), isTrue);
|
||||
|
||||
// .claude.json without the marker → not signed in.
|
||||
final c = Directory('${home.path}/.claude-c')..createSync();
|
||||
File('${c.path}/.claude.json').writeAsStringSync('{"theme": "dark"}');
|
||||
expect(accountIsSignedIn(c.path), isFalse);
|
||||
|
||||
// No config dir at all → not signed in.
|
||||
expect(accountIsSignedIn('${home.path}/.claude-missing'), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('purge guard (T-480)', () {
|
||||
const home = '/home/u';
|
||||
test('accepts ~/.claude-* direct children only, rejects everything else', () {
|
||||
|
||||
@@ -95,4 +95,64 @@ void main() {
|
||||
expect(published.single['action'], 'unset');
|
||||
expect(published.single['previous'], 'work');
|
||||
});
|
||||
|
||||
// The global registry list control (T-482 part 2).
|
||||
Future<void> pumpList(WidgetTester tester) => tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
const Align(
|
||||
alignment: Alignment.center,
|
||||
child: SizedBox(width: 460, child: ClaudeAccountsListControl()),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
testWidgets('registry list: empty shows a hint + the add row', (tester) async {
|
||||
await pumpList(tester);
|
||||
await tester.pump();
|
||||
expect(find.textContaining('No accounts registered'), findsOneWidget);
|
||||
expect(find.text('Add account'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('registry list: renders each account name + dir', (tester) async {
|
||||
final reg = AccountRegistry(f.services.settings);
|
||||
await tester.runAsync(() => reg.registerAccount('work', '/home/u/.claude-work'));
|
||||
await pumpList(tester);
|
||||
await tester.pump();
|
||||
expect(find.text('work'), findsOneWidget);
|
||||
expect(find.text('/home/u/.claude-work'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('registry list: typing a name + Add registers it and publishes login', (tester) async {
|
||||
final reg = AccountRegistry(f.services.settings);
|
||||
final published = <Map<String, Object?>>[];
|
||||
final sub = f.services.messages.subscribe(channel: accountActionChannel).listen((m) => published.add(m.data));
|
||||
addTearDown(sub.cancel);
|
||||
|
||||
await pumpList(tester);
|
||||
await tester.pump();
|
||||
await tester.enterText(find.byType(EditableText), 'work');
|
||||
await tester.tap(find.text('Add account'));
|
||||
await tester.pump();
|
||||
expect(reg.accountByName('work'), isNotNull);
|
||||
expect(published.single['action'], 'login');
|
||||
expect(published.single['name'], 'work');
|
||||
});
|
||||
|
||||
testWidgets('registry list: remove deletes an unbound account; a bound one is guarded', (tester) async {
|
||||
final reg = AccountRegistry(f.services.settings);
|
||||
await tester.runAsync(() async {
|
||||
await f.services.settings.setProjectDir(f.tempDir);
|
||||
await reg.registerAccount('work', '/home/u/.claude-work');
|
||||
await reg.registerAccount('personal', '/home/u/.claude-personal');
|
||||
await reg.bindWorkspace(f.tempDir.path, 'work');
|
||||
});
|
||||
|
||||
await pumpList(tester);
|
||||
await tester.pump();
|
||||
await tester.tap(find.bySemanticsLabel('Remove personal'));
|
||||
await tester.pump();
|
||||
expect(reg.accountByName('personal'), isNull);
|
||||
expect(find.bySemanticsLabel(RegExp('bound to a workspace')), findsOneWidget, reason: 'the bound account guards its remove');
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user