feat(tools): Tools settings category + re-detect for supporter binaries (T-495)

ToolsSettingsExtension adds a Tools settings category — a path field per
tool (app.tools.<name>) plus a Re-detect action (tools.detect) — and keeps
the live resolver in sync as paths are edited (supporterBinariesFrom). en
+ nl catalogs. Completes the D-104 UI surface.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-29 09:28:46 +02:00
co-authored by Claude Opus 4.8
parent bd4a8332af
commit eac9612a5b
9 changed files with 207 additions and 0 deletions
@@ -0,0 +1,47 @@
/// T-495: ToolsSettingsExtension contributes the Tools settings category and
/// the tools.detect command, and keeps the live resolver in sync with edits.
library;
import 'package:clide/builtin/tools_settings/tools_settings.dart';
import 'package:clide/extension/extension.dart';
import 'package:clide/src/env/supporter_binaries.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../helpers/kernel_fixture.dart';
void main() {
late KernelFixture f;
SupporterBinaries? saved;
setUp(() async {
saved = activeSupporterBinaries;
f = await KernelFixture.create();
f.services.extensions.register(ToolsSettingsExtension());
await f.services.extensions.activate('builtin.tools-settings');
});
tearDown(() {
activeSupporterBinaries = saved; // un-pollute the process-wide resolver
f.dispose();
});
test('contributes the Tools settings category and the tools.detect command', () {
final ext = ToolsSettingsExtension();
final cats = ext.contributions.whereType<SettingsCategoryContribution>();
expect(cats.any((c) => c.category.id == 'tools'), isTrue);
expect(f.services.commands.get('tools.detect'), isNotNull);
});
test('editing a tool path in settings rebuilds the live resolver', () async {
await f.services.settings.set<String>(supporterToolKey('d2'), '/custom/d2');
// The listener rebuilt activeSupporterBinaries from the keys; /custom/d2
// isn't a real file, so it reads as a stale pin — proving the edit took.
expect(activeSupporterBinaries?.isStalePin('d2'), isTrue);
});
test('tools.detect runs, marks first-run done, and refreshes the resolver', () async {
final r = await f.services.commands.execute('tools.detect');
expect(r.ok, isTrue);
expect(f.services.settings.get<bool>('app.tools.detected'), isTrue);
expect(activeSupporterBinaries, isNotNull);
});
}
+13
View File
@@ -105,4 +105,17 @@ void main() {
expect(store['app.tools.detected'], isTrue);
});
});
group('supporterBinariesFrom', () {
test('builds a resolver from the override keys', () {
final store = <String, Object?>{'app.tools.d2': '/x/d2'};
final r = supporterBinariesFrom((k) => store[k], tools: ['d2']);
expect(r.isStalePin('d2'), isTrue); // /x/d2 not a real file → loaded as override
});
test('ignores blank or missing keys', () {
final store = <String, Object?>{'app.tools.d2': ''};
expect(supporterBinariesFrom((k) => store[k], tools: ['claude', 'd2']).isStalePin('d2'), isFalse);
});
});
}