test(draw): cover the 2.9.0 drawing-card additions to clear the coverage floor

The drawing-card feature batch (icon/image/compare/graph/d2 cards, --stdin,
tool resolution) added widget + wiring code that dipped total coverage under
the 95% floor — surfaced by `make release` (push-check skips coverage). Cover
the gaps: ProblemsController.refresh, every SVG shape-type bbox + the style
vocabulary, _spawnD2 via a real /bin/cat, the icon-show bus path + error
branches, and quad/arc/close marker paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 22:00:56 +02:00
co-authored by Claude Opus 4.8
parent 9abcb0c30c
commit 1d9000988d
11 changed files with 270 additions and 0 deletions
@@ -14,6 +14,8 @@ import 'package:clide/builtin/claude/src/conversation_view.dart';
import 'package:clide/builtin/claude/src/image_thumbnail.dart';
import 'package:clide/builtin/claude/src/transcript_publisher.dart';
import 'package:clide/builtin/claude/src/transcript_reader.dart';
import 'package:clide/widgets/src/draw/drawing_card.dart' show DrawingCard;
import 'package:clide/widgets/src/svg/svg_painter.dart' show SvgView;
import 'package:clide/builtin/claude/src/workflow_run.dart';
import 'package:clide/clide.dart' show IpcResponse;
import 'package:clide/kernel/kernel.dart' show PaneKeyNav;
@@ -493,6 +495,24 @@ void main() {
expect(find.text('48'), findsOneWidget); // largest strip sample
});
testWidgets('an icon entry + card colour are parsed and rendered (T-313)', (tester) async {
await pumpWith(tester, [
_iconMsg([const IconEntry(codepoint: 0xe2a4, name: 'gear', label: 'S', color: '#e2b714')], color: '#888888'),
]);
expect(find.text('S'), findsOneWidget);
});
testWidgets('tapping a data-lightbox drawing opens the zoom lightbox (T-318)', (tester) async {
await pumpWith(tester, [
DrawingMessage(uuid: 'L', timestamp: _t, isSidechain: false, svg: '<svg viewBox="0 0 10 10"><rect width="10" height="10" data-lightbox=""/></svg>'),
]);
await tester.tap(find.byType(DrawingCard));
await tester.pumpAndSettle();
// The lightbox opened — its SvgView renders in the dialog overlay (in
// addition to the card's own).
expect(find.byType(SvgView), findsWidgets);
});
testWidgets('inject() drives a new image card into a live view (T-249)', (tester) async {
final c = await pumpWith(tester, [_user('hi')]);
expect(find.text('image'), findsNothing);
@@ -13,6 +13,7 @@ import 'package:clide/builtin/claude/src/session_orchestrator.dart' show activeS
import 'package:clide/clide.dart';
import 'package:clide/extension/extension.dart';
import 'package:clide/kernel/kernel.dart';
import 'package:clide/src/daemon/icon_commands.dart' show iconShowChannel;
import 'package:clide/src/daemon/image_commands.dart' show imageShowChannel;
import 'package:flutter_test/flutter_test.dart';
@@ -128,6 +129,20 @@ void main() {
// receive the card and the CLI already acked at publish time.
});
test('an icon-show message parses entries, dropped silently with no session (T-313)', () async {
f.services.messages.publish('test', iconShowChannel, {
'entries': [
{'codepoint': 0xe2a4, 'name': 'gear', 'label': 'Settings', 'description': 'd', 'color': '#fff'},
{'name': 'noCodepoint'}, // skipped — no int codepoint
],
'color': '#888',
});
f.services.messages.publish('test', iconShowChannel, {'entries': const []}); // empty → early return
f.services.messages.publish('test', iconShowChannel, {'entries': 'notalist'}); // not a list → early return
await pumpEventQueue();
// No live session — the entry parse ran; nothing to inject into.
});
test('a project switch closes sessions that belong to the old root (T-269)', () async {
f.services.events.emit(const ProjectOpened(path: '/repo-one'));
await pumpEventQueue();
@@ -1,7 +1,11 @@
import 'package:clide/builtin/problems/src/problems_controller.dart';
import 'package:clide/clide.dart';
import 'package:clide/kernel/kernel.dart';
import 'package:clide/src/env/supporter_binaries.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../helpers/fake_ipc.dart';
void main() {
group('supporterToolProblems', () {
test('flags a stale supporter-binary pin', () {
@@ -21,4 +25,89 @@ void main() {
expect(supporterToolProblems(null), isEmpty);
});
});
group('ProblemsController.refresh', () {
late FakeDaemonClient ipc;
SupporterBinaries? saved;
setUp(() {
saved = activeSupporterBinaries;
activeSupporterBinaries = null; // isolate from supporter-tool problems
ipc = FakeDaemonClient(log: Logger(), events: DaemonBus());
});
tearDown(() => activeSupporterBinaries = saved);
IpcResponse ok(Map<String, Object?> data) => IpcResponse.ok(id: '1', data: data);
test('a clean doctor + sync yields no problems', () async {
ipc.stub(
'pql.doctor',
(_) async => ok({
'db': {'exists': true},
'skill': {
'project': {'state': 'ok'},
},
}),
);
ipc.stub('pql.decisions.sync', (_) async => ok({'broken': 0}));
final c = ProblemsController(ipc: ipc);
var notified = 0;
c.addListener(() => notified++);
await c.refresh();
expect(c.problems, isEmpty);
expect(c.loading, isFalse);
expect(c.error, isNull);
expect(notified, greaterThan(0)); // loading toggled + final
});
test('flags a missing db, a stale skill, and broken refs', () async {
ipc.stub(
'pql.doctor',
(_) async => ok({
'db': {'exists': false},
'skill': {
'project': {'state': 'stale'},
},
}),
);
ipc.stub('pql.decisions.sync', (_) async => ok({'broken': 2}));
final c = ProblemsController(ipc: ipc);
await c.refresh();
final msgs = c.problems.map((p) => p.message).join('\n');
expect(c.problems.map((p) => p.source), containsAll(['pql', 'decisions']));
expect(msgs, contains('not found'));
expect(msgs, contains('stale'));
expect(msgs, contains('broken'));
});
test('a missing skill is flagged with the install hint', () async {
ipc.stub(
'pql.doctor',
(_) async => ok({
'db': {'exists': true},
'skill': {
'project': {'state': 'missing'},
},
}),
);
ipc.stub('pql.decisions.sync', (_) async => ok({'broken': 0}));
final c = ProblemsController(ipc: ipc);
await c.refresh();
expect(c.problems.any((p) => p.message.contains('not installed')), isTrue);
});
test('a failed doctor surfaces as a problem', () async {
ipc.stub(
'pql.doctor',
(_) async => IpcResponse.err(
id: '1',
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'boom'),
),
);
ipc.stub('pql.decisions.sync', (_) async => ok({'broken': 0}));
final c = ProblemsController(ipc: ipc);
await c.refresh();
expect(c.problems.any((p) => p.message.contains('doctor failed')), isTrue);
});
});
}
@@ -44,4 +44,17 @@ void main() {
expect(f.services.settings.get<bool>('app.tools.detected'), isTrue);
expect(activeSupporterBinaries, isNotNull);
});
test('exposes its id, title, and version', () {
final ext = ToolsSettingsExtension();
expect(ext.id, 'builtin.tools-settings');
expect(ext.title, isNotEmpty);
expect(ext.version, isNotEmpty);
});
test('deactivate removes the live-sync listener', () async {
await f.services.extensions.deactivate('builtin.tools-settings');
// Listener gone — a settings write no longer rebuilds the resolver, no throw.
await f.services.settings.set<String>(supporterToolKey('d2'), '/x/d2');
});
}