The pre-push coverage gate was failing at 94.84% — below the 95% floor even before the T-511 feature landed (the feature files sit at 97%; the tree without them was at 94.80%). The bulk of the debt was the pql backlinks panel: controller and view carried ~100 lines with zero tests. Covers controller fetch/error/event-refresh/dispose and view empty/error/group/row-navigation states, plus the T-511 stragglers a formatter-conflicted edit dropped (presetLookupRoot) and the env.path production default seams. Gate now passes at 95.26%. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
231 lines
7.9 KiB
Dart
231 lines
7.9 KiB
Dart
/// Tests for `env path …` (D-106, T-511; D-6 CLI parity). Verifies each verb's
|
|
/// store effect, the published mutation payloads, the capture diff, and honest
|
|
/// userErrors — against a fake PathPresetStore so the handler stays
|
|
/// Flutter-free (runs under `dart test`).
|
|
library;
|
|
|
|
import 'package:clide/clide.dart';
|
|
import 'package:clide/src/daemon/env_path_commands.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
class _FakeStore implements PathPresetStore {
|
|
final Map<String, List<String>> byCwd = {};
|
|
|
|
@override
|
|
List<String> dirsFor(String cwd) => List.of(byCwd[cwd] ?? const []);
|
|
|
|
@override
|
|
Future<void> setFor(String cwd, List<String> dirs) async {
|
|
if (dirs.isEmpty) {
|
|
byCwd.remove(cwd);
|
|
} else {
|
|
byCwd[cwd] = List.of(dirs);
|
|
}
|
|
}
|
|
}
|
|
|
|
void main() {
|
|
late _FakeStore store;
|
|
late List<({String publisher, String channel, Map<String, Object?> data})> published;
|
|
late DaemonDispatcher d;
|
|
late Set<String> existingDirs;
|
|
String? loginPath;
|
|
var processPath = '/usr/bin:/bin';
|
|
|
|
void wire({String? cwd = '/repo', bool withStore = true, String? home = '/home/u'}) {
|
|
store = _FakeStore();
|
|
published = [];
|
|
existingDirs = {};
|
|
loginPath = null;
|
|
processPath = '/usr/bin:/bin';
|
|
d = DaemonDispatcher();
|
|
registerEnvPathCommands(
|
|
d,
|
|
() => withStore ? store : null,
|
|
publisher: () =>
|
|
(p, c, data) => published.add((publisher: p, channel: c, data: data)),
|
|
workspaceCwd: () => cwd,
|
|
home: () => home,
|
|
dirExists: (dir) => existingDirs.contains(dir),
|
|
loginPath: () => loginPath,
|
|
processPath: () => processPath,
|
|
);
|
|
}
|
|
|
|
Future<IpcResponse> run(List<String> positional) => d.dispatch(IpcRequest(id: '1', cmd: 'env.path', args: {'positional': positional}));
|
|
|
|
test('registered on the dispatcher → shows in capabilities', () async {
|
|
wire();
|
|
final caps = await d.dispatch(IpcRequest(id: 'c', cmd: 'capabilities', args: const {}));
|
|
expect((caps.data['commands'] as Map).containsKey('env.path'), isTrue);
|
|
});
|
|
|
|
group('set', () {
|
|
test('replaces the preset, de-duplicated, and publishes', () async {
|
|
wire();
|
|
existingDirs.add('/opt/go/bin');
|
|
final r = await run(['set', '/opt/go/bin', '/x', '/opt/go/bin']);
|
|
expect(r.ok, isTrue, reason: r.error?.message);
|
|
expect(r.data['dirs'], ['/opt/go/bin', '/x']);
|
|
expect(r.data['missing'], ['/x'], reason: 'nonexistent dirs warn, never error');
|
|
expect(store.byCwd['/repo'], ['/opt/go/bin', '/x']);
|
|
expect(published.single.channel, envPathChannel);
|
|
expect(published.single.data['action'], 'set');
|
|
expect(published.single.data['dirs'], ['/opt/go/bin', '/x']);
|
|
});
|
|
|
|
test('expands ~/ against HOME and strips trailing slashes', () async {
|
|
wire();
|
|
final r = await run(['set', '~/go/bin/', '~']);
|
|
expect(r.ok, isTrue, reason: r.error?.message);
|
|
expect(r.data['dirs'], ['/home/u/go/bin', '/home/u']);
|
|
});
|
|
|
|
test('relative paths error; empty set points at clear', () async {
|
|
wire();
|
|
final rel = await run(['set', 'go/bin']);
|
|
expect(rel.ok, isFalse);
|
|
expect(rel.error?.message, contains('not an absolute path'));
|
|
final empty = await run(['set']);
|
|
expect(empty.ok, isFalse);
|
|
expect(empty.error?.hint, contains('clear'));
|
|
});
|
|
|
|
test('add and remove apply the same guards as set (relative + separator)', () async {
|
|
wire();
|
|
for (final action in ['set', 'add', 'remove']) {
|
|
final rel = await run([action, 'go/bin']);
|
|
expect(rel.ok, isFalse, reason: '$action relative');
|
|
expect(rel.error?.message, contains('not an absolute path'));
|
|
final sep = await run([action, '/a:']);
|
|
expect(sep.ok, isFalse, reason: '$action separator');
|
|
expect(sep.error?.message, contains('PATH separator'));
|
|
}
|
|
expect(store.byCwd, isEmpty, reason: 'nothing was written');
|
|
});
|
|
|
|
test('~ with no HOME errors instead of storing a broken entry', () async {
|
|
wire(home: null);
|
|
final r = await run(['set', '~/go/bin']);
|
|
expect(r.ok, isFalse);
|
|
expect(r.error?.message, contains('cannot expand'));
|
|
});
|
|
|
|
test('a leading-dash entry is rejected by the schema (T-104 guard)', () async {
|
|
wire();
|
|
final r = await d.dispatch(
|
|
IpcRequest(
|
|
id: '1',
|
|
cmd: 'env.path',
|
|
args: {
|
|
'positional': ['set'],
|
|
'flags': {'dirs': '--evil'},
|
|
},
|
|
),
|
|
);
|
|
expect(r.ok, isFalse);
|
|
});
|
|
});
|
|
|
|
group('add / remove / clear', () {
|
|
test('add appends without duplicating; remove drops; clear empties the key', () async {
|
|
wire();
|
|
await run(['set', '/a']);
|
|
published.clear();
|
|
|
|
var r = await run(['add', '/b', '/a']);
|
|
expect(r.data['dirs'], ['/a', '/b']);
|
|
|
|
r = await run(['remove', '/a']);
|
|
expect(r.data['dirs'], ['/b']);
|
|
|
|
r = await run(['clear']);
|
|
expect(r.ok, isTrue);
|
|
expect(r.data['dirs'], isEmpty);
|
|
expect(store.byCwd.containsKey('/repo'), isFalse, reason: 'empty preset removes the key');
|
|
expect(published.map((p) => p.data['action']), ['add', 'remove', 'clear']);
|
|
});
|
|
|
|
test('add and remove require a <dir>; removing an absent entry errors', () async {
|
|
wire();
|
|
expect((await run(['add'])).ok, isFalse);
|
|
expect((await run(['remove'])).ok, isFalse);
|
|
final r = await run(['remove', '/ghost']);
|
|
expect(r.ok, isFalse);
|
|
expect(r.error?.message, contains('no preset entry matches'));
|
|
});
|
|
});
|
|
|
|
group('list', () {
|
|
test('returns the preset, the missing subset, and the effective PATH preview', () async {
|
|
wire();
|
|
existingDirs.add('/a');
|
|
await run(['set', '/a', '/gone']);
|
|
final r = await run(['list']);
|
|
expect(r.data['root'], '/repo');
|
|
expect(r.data['dirs'], ['/a', '/gone']);
|
|
expect(r.data['missing'], ['/gone']);
|
|
expect(r.data['effectivePath'], startsWith('/a:/gone:'), reason: 'preset prepends the resolved PATH');
|
|
});
|
|
});
|
|
|
|
group('capture', () {
|
|
test('suggests login-shell dirs the process PATH lacks, minus the preset', () async {
|
|
wire();
|
|
loginPath = '/brew/bin:/usr/bin:/opt/go/bin:/bin';
|
|
await run(['set', '/brew/bin']);
|
|
final r = await run(['capture']);
|
|
expect(r.ok, isTrue, reason: r.error?.message);
|
|
expect(r.data['suggested'], ['/opt/go/bin']);
|
|
expect(r.data['loginShellPath'], loginPath);
|
|
expect(r.data['processPath'], '/usr/bin:/bin');
|
|
});
|
|
|
|
test('no login-shell probe → empty suggestions with an honest note', () async {
|
|
wire();
|
|
final r = await run(['capture']);
|
|
expect(r.data['suggested'], isEmpty);
|
|
expect(r.data['note'], contains('unavailable'));
|
|
});
|
|
});
|
|
|
|
test('un-injected production seams (HOME / dir probe / process PATH) work', () async {
|
|
store = _FakeStore();
|
|
d = DaemonDispatcher();
|
|
registerEnvPathCommands(d, () => store, workspaceCwd: () => '/repo');
|
|
final set = await d.dispatch(
|
|
IpcRequest(
|
|
id: '1',
|
|
cmd: 'env.path',
|
|
args: const {
|
|
'positional': ['set', '/definitely-missing-dir'],
|
|
},
|
|
),
|
|
);
|
|
expect(set.ok, isTrue, reason: set.error?.message);
|
|
expect(set.data['missing'], ['/definitely-missing-dir'], reason: 'real Directory probe ran');
|
|
final cap = await d.dispatch(
|
|
IpcRequest(
|
|
id: '2',
|
|
cmd: 'env.path',
|
|
args: const {
|
|
'positional': ['capture'],
|
|
},
|
|
),
|
|
);
|
|
expect(cap.ok, isTrue);
|
|
expect(cap.data['processPath'], isA<String>());
|
|
});
|
|
|
|
test('no workspace / no store / unknown action error clearly', () async {
|
|
wire(cwd: null);
|
|
expect((await run(['list'])).error?.message, contains('no workspace'));
|
|
wire(withStore: false);
|
|
expect((await run(['list'])).error?.message, contains('unavailable'));
|
|
wire();
|
|
final r = await run(['frobnicate']);
|
|
expect(r.ok, isFalse);
|
|
expect(r.error?.hint, contains('list | set | add | remove | clear | capture'));
|
|
});
|
|
}
|