`clide image show --file meta.json` reads a {path,label,description,caption}
payload so an image can carry a title and a longer description, not just a
one-line caption. ImageMessage + the image card render the richer metadata;
the bare `image show <path> [--caption]` form is unchanged. Honest userError
on a malformed/missing payload. Text annotation only (option a) — visual
marker overlays stay a follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
151 lines
6.0 KiB
Dart
151 lines
6.0 KiB
Dart
/// Tests for `image.show` — the CLI that drives an image card into the Claude
|
|
/// conversation log (T-249, D-6 parity). Verifies format validation, path
|
|
/// resolution, the published MessageBus 'image' payload, and honest failure
|
|
/// when the file is missing or there is no live UI.
|
|
library;
|
|
|
|
import 'package:clide/clide.dart';
|
|
import 'package:clide/src/daemon/image_commands.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
void main() {
|
|
late List<({String publisher, String channel, Map<String, Object?> data})> published;
|
|
late DaemonDispatcher d;
|
|
|
|
// [found] is the set of paths the fake resolver treats as existing on disk;
|
|
// it echoes them back prefixed with /abs to stand in for an absolute path.
|
|
void wire({bool liveUi = true, Set<String> found = const {'docs/diagram.png'}, bool withResolver = true, Map<String, String> files = const {}}) {
|
|
published = [];
|
|
d = DaemonDispatcher();
|
|
registerImageCommands(
|
|
d,
|
|
() {
|
|
if (!liveUi) return null;
|
|
return (publisher, channel, data) => published.add((publisher: publisher, channel: channel, data: data));
|
|
},
|
|
resolve: withResolver ? (path) => found.contains(path) ? '/abs/$path' : null : null,
|
|
readFile: (path) async => files[path],
|
|
);
|
|
}
|
|
|
|
Future<IpcResponse> show(List<String> positional, {Map<String, Object?>? flags}) =>
|
|
d.dispatch(IpcRequest(id: '1', cmd: 'image.show', args: {'positional': positional, 'flags': ?flags}));
|
|
|
|
test('resolves a workspace-relative path and publishes an image message', () async {
|
|
wire();
|
|
final r = await show(['docs/diagram.png']);
|
|
expect(r.ok, isTrue, reason: r.error?.message);
|
|
expect(r.data['shown'], isTrue);
|
|
expect(r.data['path'], '/abs/docs/diagram.png');
|
|
expect(published.single.publisher, 'cli');
|
|
expect(published.single.channel, 'image');
|
|
expect(published.single.data, {'path': '/abs/docs/diagram.png'});
|
|
});
|
|
|
|
test('--caption rides along in the payload', () async {
|
|
wire();
|
|
final r = await show(['docs/diagram.png'], flags: {'caption': 'before the fix'});
|
|
expect(r.ok, isTrue, reason: r.error?.message);
|
|
expect(published.single.data, {'path': '/abs/docs/diagram.png', 'caption': 'before the fix'});
|
|
});
|
|
|
|
test('--file metadata payload carries label + description (T-316)', () async {
|
|
wire(found: {'docs/shot.png'}, files: {'meta.json': '{"path":"docs/shot.png","label":"HUD v3","description":"cramped status row","caption":"before"}'});
|
|
final r = await show([], flags: {'file': 'meta.json'});
|
|
expect(r.ok, isTrue, reason: r.error?.message);
|
|
expect(published.single.data, {'path': '/abs/docs/shot.png', 'caption': 'before', 'label': 'HUD v3', 'description': 'cramped status row'});
|
|
});
|
|
|
|
test('a malformed --file payload is an honest userError, nothing published', () async {
|
|
wire(files: {'meta.json': 'not json'});
|
|
final r = await show([], flags: {'file': 'meta.json'});
|
|
expect(r.error?.kind, IpcErrorKind.userError);
|
|
expect(published, isEmpty);
|
|
});
|
|
|
|
test('a missing --file is notFound', () async {
|
|
wire();
|
|
final r = await show([], flags: {'file': 'gone.json'});
|
|
expect(r.error?.kind, IpcErrorKind.notFound);
|
|
});
|
|
|
|
test('--fullscreen rides along in the payload (T-252)', () async {
|
|
wire();
|
|
final r = await show(['docs/diagram.png'], flags: {'fullscreen': true});
|
|
expect(r.ok, isTrue, reason: r.error?.message);
|
|
expect(r.data['fullscreen'], isTrue);
|
|
expect(published.single.data, {'path': '/abs/docs/diagram.png', 'fullscreen': true});
|
|
});
|
|
|
|
test('accepts the documented formats case-insensitively', () async {
|
|
for (final name in ['a.PNG', 'b.jpg', 'c.jpeg', 'd.gif', 'e.webp', 'f.bmp']) {
|
|
wire(found: {name});
|
|
final r = await show([name]);
|
|
expect(r.ok, isTrue, reason: '$name: ${r.error?.message}');
|
|
}
|
|
});
|
|
|
|
test('unsupported format → userError, nothing published', () async {
|
|
wire(found: {'notes.txt'});
|
|
final r = await show(['notes.txt']);
|
|
expect(r.ok, isFalse);
|
|
expect(r.error?.kind, IpcErrorKind.userError);
|
|
expect(published, isEmpty);
|
|
});
|
|
|
|
test('missing path → userError', () async {
|
|
wire();
|
|
final r = await show([]);
|
|
expect(r.ok, isFalse);
|
|
expect(r.error?.kind, IpcErrorKind.userError);
|
|
expect(published, isEmpty);
|
|
});
|
|
|
|
test('a file that does not resolve → notFound, nothing published', () async {
|
|
wire(found: const {});
|
|
final r = await show(['docs/diagram.png']);
|
|
expect(r.ok, isFalse);
|
|
expect(r.error?.kind, IpcErrorKind.notFound);
|
|
expect(published, isEmpty);
|
|
});
|
|
|
|
test('no live UI (null publisher) → toolError, not a hang', () async {
|
|
wire(liveUi: false);
|
|
final r = await show(['docs/diagram.png']);
|
|
expect(r.ok, isFalse);
|
|
expect(r.error?.kind, IpcErrorKind.toolError);
|
|
});
|
|
|
|
test('a leading-dash path is rejected by the schema (argv-injection guard)', () async {
|
|
wire();
|
|
final r = await show(['-rf.png']);
|
|
expect(r.ok, isFalse);
|
|
expect(r.error?.kind, IpcErrorKind.userError);
|
|
expect(published, isEmpty);
|
|
});
|
|
|
|
test('null resolver passes the path through unverified (headless)', () async {
|
|
wire(withResolver: false);
|
|
final r = await show(['docs/diagram.png']);
|
|
expect(r.ok, isTrue, reason: r.error?.message);
|
|
expect(published.single.data, {'path': 'docs/diagram.png'});
|
|
});
|
|
|
|
test('image.show appears in the capabilities discovery surface (T-248)', () async {
|
|
wire();
|
|
final r = await d.dispatch(IpcRequest(id: '1', cmd: 'capabilities', args: const {}));
|
|
expect(r.ok, isTrue);
|
|
final commands = r.data['commands'] as Map<String, Object?>;
|
|
expect(commands.containsKey('image.show'), isTrue);
|
|
final spec = commands['image.show'] as Map<String, Object?>;
|
|
expect(spec['subsystem'], 'image');
|
|
expect(spec['verb'], 'show');
|
|
expect(spec['positional'], ['path']);
|
|
final args = spec['args'] as Map<String, Object?>;
|
|
// path is no longer required — it may come from a --file payload (T-316).
|
|
expect((args['path'] as Map)['required'], isNot(true));
|
|
expect(args.containsKey('caption'), isTrue);
|
|
expect(args.containsKey('file'), isTrue);
|
|
});
|
|
}
|