Files
clide/test/builtin/cli_install/cli_install_extension_test.dart
T
jpmschweitzerandClaude Opus 4.8 51cfa15c78 distinguish a dev-tree clide from a packaged install (T-256)
CliInstaller.inspect() gains a devTree state: when `clide` on PATH resolves
to a dev-tree build artifact (native/<plat>/clide, the Makefile's
CLIDE_CLI_BIN output) it's classified separately from a packaged install
rather than silently treated as "installed". needsInstall stays false for a
dev build (it's intentional on a checkout, not a reinstall prompt), and the
launch-time check surfaces it as an info note. install() flags fromDevTree +
notes it in the result message when the copied source is a dev build.

Closes the last open child of the T-208 "give Claude hands" initiative.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 21:53:01 +02:00

151 lines
5.1 KiB
Dart

/// T-212: the cli-install extension wires the Flutter-free [CliInstaller] to
/// a palette/CLI command and proactive launch-time notifications.
library;
import 'dart:io';
import 'package:clide/builtin/cli_install/cli_install.dart';
import 'package:clide/clide.dart';
import 'package:clide/kernel/kernel.dart';
import 'package:clide/kernel/src/cli_install.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../helpers/kernel_fixture.dart';
void main() {
late KernelFixture f;
late Directory tmp;
setUp(() async {
f = await KernelFixture.create();
tmp = Directory.systemTemp.createTempSync('clide_cli_ext_');
});
tearDown(() async {
await f.dispose();
tmp.deleteSync(recursive: true);
});
File touchExec(String path, {String contents = '#!/bin/sh\n'}) {
final file = File(path)
..createSync(recursive: true)
..writeAsStringSync(contents);
if (!Platform.isWindows) Process.runSync('chmod', ['755', path]);
return file;
}
/// Register + activate the extension with an injected installer.
Future<void> boot(CliInstaller installer) async {
f.services.extensions.register(CliInstallExtension(installer: installer));
await f.services.extensions.activate('builtin.cli-install');
}
test('identifies itself', () {
final ext = CliInstallExtension();
expect(ext.id, 'builtin.cli-install');
expect(ext.title, 'CLI Install');
expect(ext.version, '0.1.0');
});
test('contributes the install command', () async {
await boot(CliInstaller(
resolvedExecutable: '${tmp.path}/gui/clide',
env: {'PATH': '${tmp.path}/bin'},
bundledClientCandidates: const [],
installDir: '${tmp.path}/bin',
));
expect(f.services.commands.get('clide.installCli'), isNotNull);
});
test('warns on activation when clide is missing from PATH', () async {
await boot(CliInstaller(
resolvedExecutable: '${tmp.path}/gui/clide',
env: {'PATH': '${tmp.path}/empty'},
bundledClientCandidates: const [],
installDir: '${tmp.path}/bin',
));
final notes = f.services.notify.active;
expect(notes, isNotEmpty);
expect(notes.first.level, NotificationLevel.warning);
expect(notes.first.title, 'clide CLI not installed');
});
test('warns on activation when clide is a stale GUI symlink', () async {
final gui = touchExec('${tmp.path}/gui/clide').path;
final binDir = Directory('${tmp.path}/bin')..createSync();
Link('${binDir.path}/clide').createSync(gui); // PATH clide → the GUI
await boot(CliInstaller(
resolvedExecutable: gui,
env: {'PATH': binDir.path},
bundledClientCandidates: const [],
installDir: binDir.path,
));
final notes = f.services.notify.active;
expect(notes, isNotEmpty);
expect(notes.first.level, NotificationLevel.warning);
expect(notes.first.title, 'clide CLI is stale');
});
test('notes a dev-tree build on activation (info, not a warning) (T-256)', () async {
final dev = touchExec('${tmp.path}/native/linux-x64/clide').path;
final binDir = Directory('${tmp.path}/bin')..createSync();
Link('${binDir.path}/clide').createSync(dev);
await boot(CliInstaller(
resolvedExecutable: '${tmp.path}/gui/clide',
env: {'PATH': binDir.path},
bundledClientCandidates: const [],
installDir: binDir.path,
));
final notes = f.services.notify.active;
expect(notes, isNotEmpty);
expect(notes.first.level, NotificationLevel.info);
expect(notes.first.title, 'clide CLI: dev build');
});
test('does not warn when clide is already installed', () async {
final binDir = Directory('${tmp.path}/bin')..createSync();
touchExec('${binDir.path}/clide');
await boot(CliInstaller(
resolvedExecutable: '${tmp.path}/gui/clide',
env: {'PATH': binDir.path},
bundledClientCandidates: const [],
installDir: binDir.path,
));
expect(f.services.notify.active, isEmpty);
});
test('running the command installs the client and reports success', () async {
final src = touchExec('${tmp.path}/bundle/clide-cli');
final binDir = '${tmp.path}/bin';
await boot(CliInstaller(
resolvedExecutable: '${tmp.path}/gui/clide',
env: {'PATH': binDir},
bundledClientCandidates: [src.path],
installDir: binDir,
));
final r = await f.services.commands.execute('clide.installCli');
expect(r.ok, isTrue);
expect(r.data['installed'], '$binDir/clide');
expect(File('$binDir/clide').existsSync(), isTrue);
expect(
f.services.notify.active.any((n) => n.level == NotificationLevel.success && n.title == 'clide CLI installed'),
isTrue,
);
});
test('running the command surfaces a tool error when nothing to install', () async {
await boot(CliInstaller(
resolvedExecutable: '${tmp.path}/gui/clide',
env: {'PATH': '${tmp.path}/bin'},
bundledClientCandidates: const [],
installDir: '${tmp.path}/bin',
));
final r = await f.services.commands.execute('clide.installCli');
expect(r.ok, isFalse);
expect(r.error!.kind, IpcErrorKind.toolError);
expect(
f.services.notify.active.any((n) => n.level == NotificationLevel.error),
isTrue,
);
});
}