chore: adopt Dart 3.9 toolchain — honest floor + tall-style reformat (T-353)
Raise the declared minimums in pubspec.yaml to what our deps already require: Flutter >=3.35.0 / Dart >=3.9.0 (was 3.19.0 / 3.5.0). alchemist 0.12 needs Flutter 3.32; Dart 3.9 first ships in Flutter 3.35, so 3.35 is the binding floor. Pin the exact build toolchain in .fvmrc (Flutter 3.44.1). Moving to the Dart 3.9 language level switches `dart format` to the new "tall" style and enables two new lints. This commit is the resulting mechanical churn, isolated from any behaviour change: - whole-tree `dart format` reformat (tall style) - `dart fix` for unnecessary_underscores + use_null_aware_elements No runtime behaviour change; `make test` green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -30,12 +30,7 @@ import '../../helpers/widget_harness.dart';
|
||||
/// [Slots.contextPanel] slot into the arrangement so that
|
||||
/// [setVisible]/[setCollapsed] have a state entry to mutate.
|
||||
Future<void> _bootExtension(KernelFixture f) async {
|
||||
f.services.panels.registerSlot(
|
||||
const SlotDefinition(
|
||||
id: Slots.contextPanel,
|
||||
position: SlotPosition.right,
|
||||
),
|
||||
);
|
||||
f.services.panels.registerSlot(const SlotDefinition(id: Slots.contextPanel, position: SlotPosition.right));
|
||||
// Seed the ARRANGEMENT with the context-panel slot (visible:false) so the
|
||||
// extension's setVisible/setCollapsed reveal actually round-trips — in
|
||||
// production the default-layout preset does this; LayoutArrangement.setVisible
|
||||
@@ -44,9 +39,7 @@ Future<void> _bootExtension(KernelFixture f) async {
|
||||
const LayoutPresetContribution(
|
||||
id: 'test.preset',
|
||||
displayName: 'test',
|
||||
slots: [
|
||||
LayoutSlot(slot: Slots.contextPanel, position: SlotPosition.right, visible: false),
|
||||
],
|
||||
slots: [LayoutSlot(slot: Slots.contextPanel, position: SlotPosition.right, visible: false)],
|
||||
),
|
||||
);
|
||||
f.services.extensions.register(DecisionsExtension());
|
||||
@@ -63,19 +56,19 @@ void _select(KernelFixture f, String id) {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
IpcResponse _decisionResponse(String id, {String? filePath}) => IpcResponse.ok(
|
||||
id: '',
|
||||
data: {
|
||||
'id': id,
|
||||
'title': 'Decision $id',
|
||||
'type': 'confirmed',
|
||||
'domain': 'architecture',
|
||||
'status': 'active',
|
||||
'date': '2026-01-01',
|
||||
'body': 'Body of $id.',
|
||||
'refs': <Object?>[],
|
||||
'file_path': filePath ?? 'governance/decisions/architecture.md',
|
||||
},
|
||||
);
|
||||
id: '',
|
||||
data: {
|
||||
'id': id,
|
||||
'title': 'Decision $id',
|
||||
'type': 'confirmed',
|
||||
'domain': 'architecture',
|
||||
'status': 'active',
|
||||
'date': '2026-01-01',
|
||||
'body': 'Body of $id.',
|
||||
'refs': <Object?>[],
|
||||
'file_path': filePath ?? 'governance/decisions/architecture.md',
|
||||
},
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Extension-level unit tests (no Flutter widgets, no IPC)
|
||||
@@ -181,17 +174,15 @@ void main() {
|
||||
|
||||
// After deactivation contributions are removed, so no decisions.detail
|
||||
// tab at all — but the panel activation path must not fire either.
|
||||
f.services.panels.registerSlot(
|
||||
const SlotDefinition(
|
||||
id: Slots.contextPanel,
|
||||
position: SlotPosition.right,
|
||||
),
|
||||
);
|
||||
f.services.panels.registerSlot(const SlotDefinition(id: Slots.contextPanel, position: SlotPosition.right));
|
||||
f.services.messages.publish('builtin.decisions', 'selection', {'id': 'D-99'});
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
expect(f.services.panels.activeTabIn(Slots.contextPanel), isNot('decisions.detail'),
|
||||
reason: 'deactivated extension must not respond to selection messages');
|
||||
expect(
|
||||
f.services.panels.activeTabIn(Slots.contextPanel),
|
||||
isNot('decisions.detail'),
|
||||
reason: 'deactivated extension must not respond to selection messages',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -219,9 +210,7 @@ void main() {
|
||||
tester.view.resetPhysicalSize();
|
||||
tester.view.resetDevicePixelRatio();
|
||||
});
|
||||
await tester.pumpWidget(
|
||||
harness(f, DecisionDetailView(initialId: initialId)),
|
||||
);
|
||||
await tester.pumpWidget(harness(f, DecisionDetailView(initialId: initialId)));
|
||||
await pumpAsync(tester);
|
||||
}
|
||||
|
||||
@@ -334,15 +323,12 @@ void main() {
|
||||
|
||||
testWidgets('IPC error leaves _decision null — shows placeholder', (tester) async {
|
||||
f.ipc.stub(
|
||||
'pql.decisions.read',
|
||||
(_) async => IpcResponse.err(
|
||||
id: '',
|
||||
error: IpcError(
|
||||
code: IpcExitCode.toolError,
|
||||
kind: IpcErrorKind.toolError,
|
||||
message: 'read failed',
|
||||
),
|
||||
));
|
||||
'pql.decisions.read',
|
||||
(_) async => IpcResponse.err(
|
||||
id: '',
|
||||
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'read failed'),
|
||||
),
|
||||
);
|
||||
|
||||
await pumpView(tester, initialId: 'D-99');
|
||||
|
||||
@@ -352,21 +338,22 @@ void main() {
|
||||
|
||||
testWidgets('decision with status open shows status badge', (tester) async {
|
||||
f.ipc.stub(
|
||||
'pql.decisions.read',
|
||||
(args) async => IpcResponse.ok(
|
||||
id: '',
|
||||
data: {
|
||||
'id': 'Q-1',
|
||||
'title': 'Open question',
|
||||
'type': 'question',
|
||||
'domain': 'architecture',
|
||||
'status': 'open',
|
||||
'date': '2026-01-01',
|
||||
'body': '',
|
||||
'refs': <Object?>[],
|
||||
'file_path': 'governance/questions/architecture.md',
|
||||
},
|
||||
));
|
||||
'pql.decisions.read',
|
||||
(args) async => IpcResponse.ok(
|
||||
id: '',
|
||||
data: {
|
||||
'id': 'Q-1',
|
||||
'title': 'Open question',
|
||||
'type': 'question',
|
||||
'domain': 'architecture',
|
||||
'status': 'open',
|
||||
'date': '2026-01-01',
|
||||
'body': '',
|
||||
'refs': <Object?>[],
|
||||
'file_path': 'governance/questions/architecture.md',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await pumpView(tester, initialId: 'Q-1');
|
||||
|
||||
@@ -375,21 +362,22 @@ void main() {
|
||||
|
||||
testWidgets('decision with status resolved shows status badge', (tester) async {
|
||||
f.ipc.stub(
|
||||
'pql.decisions.read',
|
||||
(args) async => IpcResponse.ok(
|
||||
id: '',
|
||||
data: {
|
||||
'id': 'Q-2',
|
||||
'title': 'Resolved question',
|
||||
'type': 'question',
|
||||
'domain': 'architecture',
|
||||
'status': 'resolved',
|
||||
'date': '2026-01-01',
|
||||
'body': '',
|
||||
'refs': <Object?>[],
|
||||
'file_path': 'governance/questions/architecture.md',
|
||||
},
|
||||
));
|
||||
'pql.decisions.read',
|
||||
(args) async => IpcResponse.ok(
|
||||
id: '',
|
||||
data: {
|
||||
'id': 'Q-2',
|
||||
'title': 'Resolved question',
|
||||
'type': 'question',
|
||||
'domain': 'architecture',
|
||||
'status': 'resolved',
|
||||
'date': '2026-01-01',
|
||||
'body': '',
|
||||
'refs': <Object?>[],
|
||||
'file_path': 'governance/questions/architecture.md',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await pumpView(tester, initialId: 'Q-2');
|
||||
|
||||
@@ -398,21 +386,22 @@ void main() {
|
||||
|
||||
testWidgets('decision with unknown status shows status badge in muted color', (tester) async {
|
||||
f.ipc.stub(
|
||||
'pql.decisions.read',
|
||||
(args) async => IpcResponse.ok(
|
||||
id: '',
|
||||
data: {
|
||||
'id': 'D-20',
|
||||
'title': 'Deprecated decision',
|
||||
'type': 'confirmed',
|
||||
'domain': 'architecture',
|
||||
'status': 'deprecated',
|
||||
'date': '2026-01-01',
|
||||
'body': '',
|
||||
'refs': <Object?>[],
|
||||
'file_path': 'governance/decisions/architecture.md',
|
||||
},
|
||||
));
|
||||
'pql.decisions.read',
|
||||
(args) async => IpcResponse.ok(
|
||||
id: '',
|
||||
data: {
|
||||
'id': 'D-20',
|
||||
'title': 'Deprecated decision',
|
||||
'type': 'confirmed',
|
||||
'domain': 'architecture',
|
||||
'status': 'deprecated',
|
||||
'date': '2026-01-01',
|
||||
'body': '',
|
||||
'refs': <Object?>[],
|
||||
'file_path': 'governance/decisions/architecture.md',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await pumpView(tester, initialId: 'D-20');
|
||||
|
||||
@@ -421,23 +410,24 @@ void main() {
|
||||
|
||||
testWidgets('decision with refs using source_id renders ref card', (tester) async {
|
||||
f.ipc.stub(
|
||||
'pql.decisions.read',
|
||||
(args) async => IpcResponse.ok(
|
||||
id: '',
|
||||
data: {
|
||||
'id': 'D-30',
|
||||
'title': 'Decision with source ref',
|
||||
'type': 'confirmed',
|
||||
'domain': 'architecture',
|
||||
'status': 'active',
|
||||
'date': '2026-01-01',
|
||||
'body': '',
|
||||
'refs': [
|
||||
{'source_id': 'D-5', 'ref_type': 'amends'},
|
||||
],
|
||||
'file_path': 'governance/decisions/architecture.md',
|
||||
},
|
||||
));
|
||||
'pql.decisions.read',
|
||||
(args) async => IpcResponse.ok(
|
||||
id: '',
|
||||
data: {
|
||||
'id': 'D-30',
|
||||
'title': 'Decision with source ref',
|
||||
'type': 'confirmed',
|
||||
'domain': 'architecture',
|
||||
'status': 'active',
|
||||
'date': '2026-01-01',
|
||||
'body': '',
|
||||
'refs': [
|
||||
{'source_id': 'D-5', 'ref_type': 'amends'},
|
||||
],
|
||||
'file_path': 'governance/decisions/architecture.md',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await pumpView(tester, initialId: 'D-30');
|
||||
|
||||
@@ -450,23 +440,24 @@ void main() {
|
||||
// the T-prefix routing in _navigateToRecord is only reachable via
|
||||
// ClideMarkdown.onRecordTap (markdown body links).
|
||||
f.ipc.stub(
|
||||
'pql.decisions.read',
|
||||
(args) async => IpcResponse.ok(
|
||||
id: '',
|
||||
data: {
|
||||
'id': 'D-40',
|
||||
'title': 'Decision with ticket ref',
|
||||
'type': 'confirmed',
|
||||
'domain': 'architecture',
|
||||
'status': 'active',
|
||||
'date': '2026-01-01',
|
||||
'body': '',
|
||||
'refs': [
|
||||
{'target_id': 'T-123', 'ref_type': 'tracked-by'},
|
||||
],
|
||||
'file_path': 'governance/decisions/architecture.md',
|
||||
},
|
||||
));
|
||||
'pql.decisions.read',
|
||||
(args) async => IpcResponse.ok(
|
||||
id: '',
|
||||
data: {
|
||||
'id': 'D-40',
|
||||
'title': 'Decision with ticket ref',
|
||||
'type': 'confirmed',
|
||||
'domain': 'architecture',
|
||||
'status': 'active',
|
||||
'date': '2026-01-01',
|
||||
'body': '',
|
||||
'refs': [
|
||||
{'target_id': 'T-123', 'ref_type': 'tracked-by'},
|
||||
],
|
||||
'file_path': 'governance/decisions/architecture.md',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await pumpView(tester, initialId: 'D-40');
|
||||
expect(find.text('T-123'), findsOneWidget);
|
||||
@@ -484,21 +475,22 @@ void main() {
|
||||
|
||||
testWidgets('decision with non-empty body renders body section', (tester) async {
|
||||
f.ipc.stub(
|
||||
'pql.decisions.read',
|
||||
(args) async => IpcResponse.ok(
|
||||
id: '',
|
||||
data: {
|
||||
'id': 'D-50',
|
||||
'title': 'Decision with body',
|
||||
'type': 'confirmed',
|
||||
'domain': 'architecture',
|
||||
'status': 'active',
|
||||
'date': '2026-01-15',
|
||||
'body': 'This is the decision body text.',
|
||||
'refs': <Object?>[],
|
||||
'file_path': 'governance/decisions/architecture.md',
|
||||
},
|
||||
));
|
||||
'pql.decisions.read',
|
||||
(args) async => IpcResponse.ok(
|
||||
id: '',
|
||||
data: {
|
||||
'id': 'D-50',
|
||||
'title': 'Decision with body',
|
||||
'type': 'confirmed',
|
||||
'domain': 'architecture',
|
||||
'status': 'active',
|
||||
'date': '2026-01-15',
|
||||
'body': 'This is the decision body text.',
|
||||
'refs': <Object?>[],
|
||||
'file_path': 'governance/decisions/architecture.md',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await pumpView(tester, initialId: 'D-50');
|
||||
|
||||
@@ -509,20 +501,21 @@ void main() {
|
||||
|
||||
testWidgets('decision without date omits date row', (tester) async {
|
||||
f.ipc.stub(
|
||||
'pql.decisions.read',
|
||||
(args) async => IpcResponse.ok(
|
||||
id: '',
|
||||
data: {
|
||||
'id': 'D-60',
|
||||
'title': 'No date decision',
|
||||
'type': 'confirmed',
|
||||
'domain': 'architecture',
|
||||
'status': 'active',
|
||||
'body': '',
|
||||
'refs': <Object?>[],
|
||||
'file_path': 'governance/decisions/architecture.md',
|
||||
},
|
||||
));
|
||||
'pql.decisions.read',
|
||||
(args) async => IpcResponse.ok(
|
||||
id: '',
|
||||
data: {
|
||||
'id': 'D-60',
|
||||
'title': 'No date decision',
|
||||
'type': 'confirmed',
|
||||
'domain': 'architecture',
|
||||
'status': 'active',
|
||||
'body': '',
|
||||
'refs': <Object?>[],
|
||||
'file_path': 'governance/decisions/architecture.md',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await pumpView(tester, initialId: 'D-60');
|
||||
|
||||
@@ -570,24 +563,14 @@ void main() {
|
||||
testWidgets('back disabled on initial load', (tester) async {
|
||||
await pumpView(tester, initialId: 'D-1');
|
||||
|
||||
expect(
|
||||
find.byWidgetPredicate(
|
||||
(w) => w is Semantics && w.properties.label == 'Back' && w.properties.enabled == false,
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byWidgetPredicate((w) => w is Semantics && w.properties.label == 'Back' && w.properties.enabled == false), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('back enabled after two selections', (tester) async {
|
||||
await pumpView(tester, initialId: 'D-1');
|
||||
await open(tester, 'D-2');
|
||||
|
||||
expect(
|
||||
find.byWidgetPredicate(
|
||||
(w) => w is Semantics && w.properties.label == 'Back' && (w.properties.enabled ?? true),
|
||||
),
|
||||
findsWidgets,
|
||||
);
|
||||
expect(find.byWidgetPredicate((w) => w is Semantics && w.properties.label == 'Back' && (w.properties.enabled ?? true)), findsWidgets);
|
||||
});
|
||||
|
||||
testWidgets('back navigates to previous decision', (tester) async {
|
||||
@@ -596,9 +579,7 @@ void main() {
|
||||
// Title appears in pane header subtitle + body card.
|
||||
expect(find.text('Decision D-2'), findsWidgets);
|
||||
|
||||
final backBtn = find.byWidgetPredicate(
|
||||
(w) => w is Semantics && w.properties.label == 'Back' && (w.properties.enabled ?? true),
|
||||
);
|
||||
final backBtn = find.byWidgetPredicate((w) => w is Semantics && w.properties.label == 'Back' && (w.properties.enabled ?? true));
|
||||
await tester.tap(backBtn.first);
|
||||
await pumpAsync(tester);
|
||||
|
||||
@@ -614,9 +595,7 @@ void main() {
|
||||
addTearDown(sub.cancel);
|
||||
|
||||
// Go back — re-emits on 'load', NOT 'selection'.
|
||||
final backBtn = find.byWidgetPredicate(
|
||||
(w) => w is Semantics && w.properties.label == 'Back' && (w.properties.enabled ?? true),
|
||||
);
|
||||
final backBtn = find.byWidgetPredicate((w) => w is Semantics && w.properties.label == 'Back' && (w.properties.enabled ?? true));
|
||||
await tester.tap(backBtn.first);
|
||||
await pumpAsync(tester);
|
||||
|
||||
@@ -627,12 +606,7 @@ void main() {
|
||||
await pumpView(tester, initialId: 'D-1');
|
||||
await open(tester, 'D-2');
|
||||
|
||||
expect(
|
||||
find.byWidgetPredicate(
|
||||
(w) => w is Semantics && w.properties.label == 'Forward' && w.properties.enabled == false,
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byWidgetPredicate((w) => w is Semantics && w.properties.label == 'Forward' && w.properties.enabled == false), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('forward navigates after back', (tester) async {
|
||||
@@ -640,17 +614,13 @@ void main() {
|
||||
await open(tester, 'D-2');
|
||||
|
||||
// Go back to D-1.
|
||||
final backBtn = find.byWidgetPredicate(
|
||||
(w) => w is Semantics && w.properties.label == 'Back' && (w.properties.enabled ?? true),
|
||||
);
|
||||
final backBtn = find.byWidgetPredicate((w) => w is Semantics && w.properties.label == 'Back' && (w.properties.enabled ?? true));
|
||||
await tester.tap(backBtn.first);
|
||||
await pumpAsync(tester);
|
||||
expect(find.text('Decision D-1'), findsWidgets);
|
||||
|
||||
// Go forward to D-2.
|
||||
final fwdBtn = find.byWidgetPredicate(
|
||||
(w) => w is Semantics && w.properties.label == 'Forward' && (w.properties.enabled ?? true),
|
||||
);
|
||||
final fwdBtn = find.byWidgetPredicate((w) => w is Semantics && w.properties.label == 'Forward' && (w.properties.enabled ?? true));
|
||||
await tester.tap(fwdBtn.first);
|
||||
await pumpAsync(tester);
|
||||
expect(find.text('Decision D-2'), findsWidgets);
|
||||
@@ -661,21 +631,14 @@ void main() {
|
||||
await open(tester, 'D-2');
|
||||
|
||||
// Go back to D-1.
|
||||
final backBtn = find.byWidgetPredicate(
|
||||
(w) => w is Semantics && w.properties.label == 'Back' && (w.properties.enabled ?? true),
|
||||
);
|
||||
final backBtn = find.byWidgetPredicate((w) => w is Semantics && w.properties.label == 'Back' && (w.properties.enabled ?? true));
|
||||
await tester.tap(backBtn.first);
|
||||
await pumpAsync(tester);
|
||||
|
||||
// Open D-3 — truncates D-2 forward history.
|
||||
await open(tester, 'D-3');
|
||||
|
||||
expect(
|
||||
find.byWidgetPredicate(
|
||||
(w) => w is Semantics && w.properties.label == 'Forward' && w.properties.enabled == false,
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byWidgetPredicate((w) => w is Semantics && w.properties.label == 'Forward' && w.properties.enabled == false), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -714,37 +677,23 @@ void main() {
|
||||
|
||||
testWidgets('pin jump affordance not visible before pin set', (tester) async {
|
||||
await pumpView(tester, initialId: 'D-1');
|
||||
expect(
|
||||
find.byWidgetPredicate((w) => w is Semantics && w.properties.label == 'Jump to pin'),
|
||||
findsNothing,
|
||||
);
|
||||
expect(find.byWidgetPredicate((w) => w is Semantics && w.properties.label == 'Jump to pin'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('pin current shows jump-to-pin affordance', (tester) async {
|
||||
await pumpView(tester, initialId: 'D-1');
|
||||
|
||||
await tester.tap(find
|
||||
.byWidgetPredicate(
|
||||
(w) => w is Semantics && w.properties.label == 'Pin',
|
||||
)
|
||||
.first);
|
||||
await tester.tap(find.byWidgetPredicate((w) => w is Semantics && w.properties.label == 'Pin').first);
|
||||
await pumpAsync(tester);
|
||||
|
||||
expect(
|
||||
find.byWidgetPredicate((w) => w is Semantics && w.properties.label == 'Jump to pin'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byWidgetPredicate((w) => w is Semantics && w.properties.label == 'Jump to pin'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('jump to pin loads the pinned decision', (tester) async {
|
||||
await pumpView(tester, initialId: 'D-1');
|
||||
|
||||
// Pin D-1.
|
||||
await tester.tap(find
|
||||
.byWidgetPredicate(
|
||||
(w) => w is Semantics && w.properties.label == 'Pin',
|
||||
)
|
||||
.first);
|
||||
await tester.tap(find.byWidgetPredicate((w) => w is Semantics && w.properties.label == 'Pin').first);
|
||||
await pumpAsync(tester);
|
||||
|
||||
// Navigate to D-2.
|
||||
@@ -753,11 +702,7 @@ void main() {
|
||||
expect(find.text('Decision D-2'), findsWidgets);
|
||||
|
||||
// Jump to pin.
|
||||
await tester.tap(find
|
||||
.byWidgetPredicate(
|
||||
(w) => w is Semantics && w.properties.label == 'Jump to pin',
|
||||
)
|
||||
.first);
|
||||
await tester.tap(find.byWidgetPredicate((w) => w is Semantics && w.properties.label == 'Jump to pin').first);
|
||||
await pumpAsync(tester);
|
||||
|
||||
expect(find.text('Decision D-1'), findsWidgets);
|
||||
@@ -767,28 +712,14 @@ void main() {
|
||||
await pumpView(tester, initialId: 'D-1');
|
||||
|
||||
// Pin D-1 → the jump-to-pin button appears in the navigator.
|
||||
await tester.tap(find
|
||||
.byWidgetPredicate(
|
||||
(w) => w is Semantics && w.properties.label == 'Pin',
|
||||
)
|
||||
.first);
|
||||
await tester.tap(find.byWidgetPredicate((w) => w is Semantics && w.properties.label == 'Pin').first);
|
||||
await pumpAsync(tester);
|
||||
expect(
|
||||
find.byWidgetPredicate((w) => w is Semantics && w.properties.label == 'Jump to pin'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byWidgetPredicate((w) => w is Semantics && w.properties.label == 'Jump to pin'), findsOneWidget);
|
||||
|
||||
// Tapping the toggle again (now 'Unpin') clears the pin.
|
||||
await tester.tap(find
|
||||
.byWidgetPredicate(
|
||||
(w) => w is Semantics && w.properties.label == 'Unpin',
|
||||
)
|
||||
.first);
|
||||
await tester.tap(find.byWidgetPredicate((w) => w is Semantics && w.properties.label == 'Unpin').first);
|
||||
await pumpAsync(tester);
|
||||
expect(
|
||||
find.byWidgetPredicate((w) => w is Semantics && w.properties.label == 'Jump to pin'),
|
||||
findsNothing,
|
||||
);
|
||||
expect(find.byWidgetPredicate((w) => w is Semantics && w.properties.label == 'Jump to pin'), findsNothing);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -818,10 +749,7 @@ void main() {
|
||||
testWidgets('edit pencil not visible when no decision loaded', (tester) async {
|
||||
await pumpView(tester);
|
||||
// Placeholder state — no chrome.
|
||||
expect(
|
||||
find.byWidgetPredicate((w) => w is Semantics && w.properties.label == 'Edit in editor'),
|
||||
findsNothing,
|
||||
);
|
||||
expect(find.byWidgetPredicate((w) => w is Semantics && w.properties.label == 'Edit in editor'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('edit pencil fires editor.open with file_path from decision', (tester) async {
|
||||
@@ -838,16 +766,9 @@ void main() {
|
||||
|
||||
await pumpView(tester, initialId: 'D-1');
|
||||
|
||||
expect(
|
||||
find.byWidgetPredicate((w) => w is Semantics && w.properties.label == 'Edit in editor'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byWidgetPredicate((w) => w is Semantics && w.properties.label == 'Edit in editor'), findsOneWidget);
|
||||
|
||||
await tester.tap(find
|
||||
.byWidgetPredicate(
|
||||
(w) => w is Semantics && w.properties.label == 'Edit in editor',
|
||||
)
|
||||
.first);
|
||||
await tester.tap(find.byWidgetPredicate((w) => w is Semantics && w.properties.label == 'Edit in editor').first);
|
||||
await pumpAsync(tester);
|
||||
|
||||
expect(editorOpenArgs, hasLength(1));
|
||||
@@ -874,10 +795,7 @@ void main() {
|
||||
|
||||
await pumpView(tester, initialId: 'D-70');
|
||||
|
||||
expect(
|
||||
find.byWidgetPredicate((w) => w is Semantics && w.properties.label == 'Edit in editor'),
|
||||
findsNothing,
|
||||
);
|
||||
expect(find.byWidgetPredicate((w) => w is Semantics && w.properties.label == 'Edit in editor'), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -26,28 +26,17 @@ import '../../helpers/widget_harness.dart';
|
||||
IpcResponse _ok(Map<String, Object?> data) => IpcResponse.ok(id: '', data: data);
|
||||
|
||||
IpcResponse _err(String msg) => IpcResponse.err(
|
||||
id: '',
|
||||
error: IpcError(
|
||||
code: IpcExitCode.toolError,
|
||||
kind: IpcErrorKind.toolError,
|
||||
message: msg,
|
||||
),
|
||||
);
|
||||
id: '',
|
||||
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: msg),
|
||||
);
|
||||
|
||||
Map<String, Object?> _decision({
|
||||
required String id,
|
||||
required String title,
|
||||
String type = 'confirmed',
|
||||
String domain = 'architecture',
|
||||
String? status,
|
||||
}) =>
|
||||
{
|
||||
'id': id,
|
||||
'title': title,
|
||||
'type': type,
|
||||
'domain': domain,
|
||||
if (status != null) 'status': status,
|
||||
};
|
||||
Map<String, Object?> _decision({required String id, required String title, String type = 'confirmed', String domain = 'architecture', String? status}) => {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'type': type,
|
||||
'domain': domain,
|
||||
'status': ?status,
|
||||
};
|
||||
|
||||
/// Register both sync and list stubs, returning the provided list of decisions.
|
||||
void _stubDecisions(KernelFixture f, List<Map<String, Object?>> decisions) {
|
||||
@@ -122,11 +111,7 @@ void main() {
|
||||
'pql.decisions.list',
|
||||
(_) async => IpcResponse.err(
|
||||
id: '',
|
||||
error: IpcError(
|
||||
code: IpcExitCode.toolError,
|
||||
kind: IpcErrorKind.toolError,
|
||||
message: 'failed to load decisions',
|
||||
),
|
||||
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'failed to load decisions'),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -155,9 +140,7 @@ void main() {
|
||||
|
||||
group('DecisionsView — grouping into sections', () {
|
||||
testWidgets('confirmed decisions appear in CONFIRMED accordion', (tester) async {
|
||||
_stubDecisions(f, [
|
||||
_decision(id: 'D-1', title: 'Architecture choice'),
|
||||
]);
|
||||
_stubDecisions(f, [_decision(id: 'D-1', title: 'Architecture choice')]);
|
||||
await pumpView(tester);
|
||||
|
||||
// Accordion label renders as 'CONFIRMED · N'.
|
||||
@@ -168,9 +151,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('question decisions appear in QUESTIONS accordion header', (tester) async {
|
||||
_stubDecisions(f, [
|
||||
_decision(id: 'Q-1', title: 'Open question', type: 'question', domain: 'tooling'),
|
||||
]);
|
||||
_stubDecisions(f, [_decision(id: 'Q-1', title: 'Open question', type: 'question', domain: 'tooling')]);
|
||||
await pumpView(tester);
|
||||
|
||||
// The header always renders even when the section is collapsed.
|
||||
@@ -183,9 +164,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('rejected decisions appear in REJECTED accordion header', (tester) async {
|
||||
_stubDecisions(f, [
|
||||
_decision(id: 'R-1', title: 'Rejected idea', type: 'rejected', domain: 'ui'),
|
||||
]);
|
||||
_stubDecisions(f, [_decision(id: 'R-1', title: 'Rejected idea', type: 'rejected', domain: 'ui')]);
|
||||
await pumpView(tester);
|
||||
|
||||
expect(find.textContaining('REJECTED'), findsOneWidget);
|
||||
@@ -212,9 +191,7 @@ void main() {
|
||||
testWidgets('card with resolved status shows resolved badge (via filter)', (tester) async {
|
||||
// Use a confirmed decision with resolved status so the card is
|
||||
// visible without needing to expand the section manually.
|
||||
_stubDecisions(f, [
|
||||
_decision(id: 'D-2', title: 'Resolved decision', type: 'confirmed', domain: 'arch', status: 'resolved'),
|
||||
]);
|
||||
_stubDecisions(f, [_decision(id: 'D-2', title: 'Resolved decision', type: 'confirmed', domain: 'arch', status: 'resolved')]);
|
||||
await pumpView(tester);
|
||||
|
||||
// CONFIRMED is pinned-expanded; card is visible.
|
||||
@@ -222,9 +199,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('domain label is shown on card', (tester) async {
|
||||
_stubDecisions(f, [
|
||||
_decision(id: 'D-5', title: 'Some D', domain: 'architecture'),
|
||||
]);
|
||||
_stubDecisions(f, [_decision(id: 'D-5', title: 'Some D', domain: 'architecture')]);
|
||||
await pumpView(tester);
|
||||
|
||||
expect(find.text('architecture'), findsOneWidget);
|
||||
@@ -233,9 +208,7 @@ void main() {
|
||||
|
||||
group('DecisionsView — card tap publishes selection', () {
|
||||
testWidgets('tapping a confirmed card publishes builtin.decisions/selection', (tester) async {
|
||||
_stubDecisions(f, [
|
||||
_decision(id: 'D-3', title: 'Click me'),
|
||||
]);
|
||||
_stubDecisions(f, [_decision(id: 'D-3', title: 'Click me')]);
|
||||
await pumpView(tester);
|
||||
|
||||
final received = <Message>[];
|
||||
@@ -250,9 +223,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('tapping a question card publishes the correct id', (tester) async {
|
||||
_stubDecisions(f, [
|
||||
_decision(id: 'Q-4', title: 'Q card', type: 'question', domain: 'tooling'),
|
||||
]);
|
||||
_stubDecisions(f, [_decision(id: 'Q-4', title: 'Q card', type: 'question', domain: 'tooling')]);
|
||||
await pumpView(tester);
|
||||
|
||||
// QUESTIONS section is collapsed by default — tap header to expand it.
|
||||
@@ -273,10 +244,7 @@ void main() {
|
||||
|
||||
group('DecisionsView — filter', () {
|
||||
testWidgets('filter box narrows results by id', (tester) async {
|
||||
_stubDecisions(f, [
|
||||
_decision(id: 'D-1', title: 'Architecture choice'),
|
||||
_decision(id: 'D-2', title: 'Build tool selection'),
|
||||
]);
|
||||
_stubDecisions(f, [_decision(id: 'D-1', title: 'Architecture choice'), _decision(id: 'D-2', title: 'Build tool selection')]);
|
||||
await pumpView(tester);
|
||||
|
||||
expect(find.text('D-1'), findsOneWidget);
|
||||
@@ -295,10 +263,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('filter box narrows results by title', (tester) async {
|
||||
_stubDecisions(f, [
|
||||
_decision(id: 'D-1', title: 'Architecture choice'),
|
||||
_decision(id: 'D-2', title: 'Build tool selection'),
|
||||
]);
|
||||
_stubDecisions(f, [_decision(id: 'D-1', title: 'Architecture choice'), _decision(id: 'D-2', title: 'Build tool selection')]);
|
||||
await pumpView(tester);
|
||||
|
||||
final filterBox = find.byWidgetPredicate((w) => w is EditableText);
|
||||
@@ -311,10 +276,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('filter by domain shows matching card', (tester) async {
|
||||
_stubDecisions(f, [
|
||||
_decision(id: 'D-1', title: 'Arch', domain: 'architecture'),
|
||||
_decision(id: 'D-2', title: 'Tool', domain: 'tooling'),
|
||||
]);
|
||||
_stubDecisions(f, [_decision(id: 'D-1', title: 'Arch', domain: 'architecture'), _decision(id: 'D-2', title: 'Tool', domain: 'tooling')]);
|
||||
await pumpView(tester);
|
||||
|
||||
final filterBox = find.byWidgetPredicate((w) => w is EditableText);
|
||||
@@ -327,9 +289,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('filter with no matches yields empty sections', (tester) async {
|
||||
_stubDecisions(f, [
|
||||
_decision(id: 'D-1', title: 'Something'),
|
||||
]);
|
||||
_stubDecisions(f, [_decision(id: 'D-1', title: 'Something')]);
|
||||
await pumpView(tester);
|
||||
|
||||
final filterBox = find.byWidgetPredicate((w) => w is EditableText);
|
||||
@@ -344,9 +304,7 @@ void main() {
|
||||
|
||||
group('DecisionsView — section toggle', () {
|
||||
testWidgets('toggling confirmed section removes confirmed from pinned', (tester) async {
|
||||
_stubDecisions(f, [
|
||||
_decision(id: 'D-1', title: 'Arch choice'),
|
||||
]);
|
||||
_stubDecisions(f, [_decision(id: 'D-1', title: 'Arch choice')]);
|
||||
await pumpView(tester);
|
||||
|
||||
// CONFIRMED is pinned by default and should be expanded (D-1 visible).
|
||||
@@ -362,9 +320,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('toggling a non-pinned section expands it', (tester) async {
|
||||
_stubDecisions(f, [
|
||||
_decision(id: 'R-1', title: 'Rejected one', type: 'rejected', domain: 'ui'),
|
||||
]);
|
||||
_stubDecisions(f, [_decision(id: 'R-1', title: 'Rejected one', type: 'rejected', domain: 'ui')]);
|
||||
await pumpView(tester);
|
||||
|
||||
// REJECTED starts unexpanded (not in _pinned).
|
||||
@@ -381,10 +337,7 @@ void main() {
|
||||
|
||||
group('DecisionsView — focus message', () {
|
||||
testWidgets('receiving a focus message updates _focusedId and rebuilds', (tester) async {
|
||||
_stubDecisions(f, [
|
||||
_decision(id: 'D-1', title: 'First'),
|
||||
_decision(id: 'D-2', title: 'Second'),
|
||||
]);
|
||||
_stubDecisions(f, [_decision(id: 'D-1', title: 'First'), _decision(id: 'D-2', title: 'Second')]);
|
||||
await pumpView(tester);
|
||||
|
||||
// Both cards visible.
|
||||
@@ -400,9 +353,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('focus message with null id is ignored', (tester) async {
|
||||
_stubDecisions(f, [
|
||||
_decision(id: 'D-1', title: 'First'),
|
||||
]);
|
||||
_stubDecisions(f, [_decision(id: 'D-1', title: 'First')]);
|
||||
await pumpView(tester);
|
||||
|
||||
f.services.messages.publish('builtin.decisions', 'focus', {'id': null});
|
||||
@@ -412,9 +363,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('focus message with same id as already focused is ignored', (tester) async {
|
||||
_stubDecisions(f, [
|
||||
_decision(id: 'D-1', title: 'First'),
|
||||
]);
|
||||
_stubDecisions(f, [_decision(id: 'D-1', title: 'First')]);
|
||||
await pumpView(tester);
|
||||
|
||||
f.services.messages.publish('builtin.decisions', 'focus', {'id': 'D-1'});
|
||||
@@ -435,9 +384,7 @@ void main() {
|
||||
f.ipc.stub('pql.decisions.list', (_) async {
|
||||
listCallCount++;
|
||||
return _ok({
|
||||
'decisions': [
|
||||
_decision(id: 'D-$listCallCount', title: 'Call $listCallCount'),
|
||||
],
|
||||
'decisions': [_decision(id: 'D-$listCallCount', title: 'Call $listCallCount')],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -446,9 +393,7 @@ void main() {
|
||||
|
||||
// Tap the refresh icon button — it has tooltip 'Refresh decisions'.
|
||||
// Find ClideTappable widgets and tap the one with the refresh tooltip.
|
||||
final refreshTappable = find.byWidgetPredicate(
|
||||
(w) => w is ClideTappable && w.tooltip == 'Refresh decisions',
|
||||
);
|
||||
final refreshTappable = find.byWidgetPredicate((w) => w is ClideTappable && w.tooltip == 'Refresh decisions');
|
||||
await tester.tap(refreshTappable);
|
||||
await pumpAsync(tester);
|
||||
|
||||
@@ -463,9 +408,7 @@ void main() {
|
||||
f.ipc.stub('pql.decisions.list', (_) async {
|
||||
listCallCount++;
|
||||
return _ok({
|
||||
'decisions': [
|
||||
_decision(id: 'D-$listCallCount', title: 'Version $listCallCount'),
|
||||
],
|
||||
'decisions': [_decision(id: 'D-$listCallCount', title: 'Version $listCallCount')],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -473,12 +416,7 @@ void main() {
|
||||
expect(listCallCount, 1);
|
||||
|
||||
// Emit a files.changed event for a decisions path.
|
||||
f.services.events.emit(DaemonEvent(
|
||||
subsystem: 'files',
|
||||
kind: 'files.changed',
|
||||
data: {'path': 'decisions/architecture.md'},
|
||||
ts: DateTime.now().toUtc(),
|
||||
));
|
||||
f.services.events.emit(DaemonEvent(subsystem: 'files', kind: 'files.changed', data: {'path': 'decisions/architecture.md'}, ts: DateTime.now().toUtc()));
|
||||
await pumpAsync(tester);
|
||||
|
||||
expect(listCallCount, greaterThanOrEqualTo(2));
|
||||
@@ -495,12 +433,7 @@ void main() {
|
||||
await pumpView(tester);
|
||||
final countAfterLoad = listCallCount;
|
||||
|
||||
f.services.events.emit(DaemonEvent(
|
||||
subsystem: 'files',
|
||||
kind: 'files.changed',
|
||||
data: {'path': 'lib/main.dart'},
|
||||
ts: DateTime.now().toUtc(),
|
||||
));
|
||||
f.services.events.emit(DaemonEvent(subsystem: 'files', kind: 'files.changed', data: {'path': 'lib/main.dart'}, ts: DateTime.now().toUtc()));
|
||||
await pumpAsync(tester);
|
||||
|
||||
// Count must not have incremented.
|
||||
|
||||
Reference in New Issue
Block a user