Spike result (probed claude 2.1.175 over stream-json): there is NO set_effort/set_thinking_effort control subtype — both are rejected. The lever is the `--effort <level>` spawn flag (low/medium/high/xhigh/max; settings.json effortLevel is the persisted default). So changing effort restarts the process: respawn-with-resume keeps the conversation and carries the flag — the same continuity /clear and /resume already rely on. - SpawnSpec.effort → orchestrator appends `--effort <level>`. - claude_pane: /effort <level> validates and respawns (toast explains the restart); bare /effort opens a picker; the pane re-applies its effort on every later respawn. Invalid level → local notice listing levels. - ModelPickerCard generalised minimally (title + isCurrent predicate) so the effort picker reuses it; effort needs exact matching because `high` is a substring of `xhigh` and alias-containment would mis-mark it. - SessionStatus.effort + StreamJsonSession.noteEffort: the wire never reports effort, so the spawner records what it set; status/sidebar read it from the normal status stream. - Routing: effort moves from the TUI-only catalog to kClideOwnedCommands. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
109 lines
4.5 KiB
Dart
109 lines
4.5 KiB
Dart
/// Widget tests for [ModelPickerCard] — the bare `/model` interaction-zone
|
|
/// picker (T-408): rendering, current-model marking, number-key / arrow+Enter
|
|
/// selection, and Esc cancel.
|
|
library;
|
|
|
|
import 'package:clide/builtin/claude/src/model_picker_card.dart';
|
|
import 'package:clide/builtin/claude/src/stream_json_session.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import '../../helpers/kernel_fixture.dart';
|
|
import '../../helpers/widget_harness.dart';
|
|
|
|
const models = [
|
|
ModelOption(value: 'default', displayName: 'Default', description: 'recommended'),
|
|
ModelOption(value: 'sonnet', displayName: 'Sonnet', description: 'fast'),
|
|
ModelOption(value: 'opus', displayName: 'Opus', description: 'most capable'),
|
|
];
|
|
|
|
void main() {
|
|
late KernelFixture f;
|
|
|
|
setUp(() async => f = await KernelFixture.create());
|
|
tearDown(() async => f.dispose());
|
|
|
|
group('modelOptionIsCurrent', () {
|
|
test('matches by exact value or alias containment, never for default', () {
|
|
const sonnet = ModelOption(value: 'sonnet', displayName: 'Sonnet');
|
|
expect(modelOptionIsCurrent(sonnet, 'sonnet'), isTrue);
|
|
expect(modelOptionIsCurrent(sonnet, 'claude-sonnet-4-6'), isTrue);
|
|
expect(modelOptionIsCurrent(sonnet, 'claude-opus-4-8'), isFalse);
|
|
expect(modelOptionIsCurrent(sonnet, null), isFalse);
|
|
expect(modelOptionIsCurrent(const ModelOption(value: 'default', displayName: 'Default'), 'claude-opus-4-8'), isFalse);
|
|
});
|
|
});
|
|
|
|
testWidgets('renders every model with the current one marked', (tester) async {
|
|
await tester.pumpWidget(harness(f, ModelPickerCard(models: models, currentModel: 'claude-sonnet-4-6', onPick: (_) {}, onCancel: () {})));
|
|
expect(find.textContaining('Default'), findsOneWidget);
|
|
expect(find.textContaining('● Sonnet'), findsOneWidget); // current
|
|
expect(find.textContaining('○ Opus'), findsOneWidget);
|
|
expect(find.textContaining('most capable'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('tapping an entry picks its value', (tester) async {
|
|
String? picked;
|
|
await tester.pumpWidget(harness(f, ModelPickerCard(models: models, currentModel: null, onPick: (v) => picked = v, onCancel: () {})));
|
|
await tester.tap(find.textContaining('Opus'));
|
|
await tester.pump();
|
|
expect(picked, 'opus');
|
|
});
|
|
|
|
testWidgets('a number key picks directly', (tester) async {
|
|
String? picked;
|
|
await tester.pumpWidget(harness(f, ModelPickerCard(models: models, currentModel: null, onPick: (v) => picked = v, onCancel: () {})));
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.digit2);
|
|
await tester.pump();
|
|
expect(picked, 'sonnet');
|
|
});
|
|
|
|
testWidgets('arrows move the highlight and Enter picks it', (tester) async {
|
|
String? picked;
|
|
await tester.pumpWidget(harness(f, ModelPickerCard(models: models, currentModel: 'claude-sonnet-4-6', onPick: (v) => picked = v, onCancel: () {})));
|
|
// Highlight starts on the current model (sonnet, index 1).
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); // → opus
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
|
await tester.pump();
|
|
expect(picked, 'opus');
|
|
});
|
|
|
|
testWidgets('Esc cancels without picking', (tester) async {
|
|
String? picked;
|
|
var cancelled = false;
|
|
await tester.pumpWidget(harness(f, ModelPickerCard(models: models, currentModel: null, onPick: (v) => picked = v, onCancel: () => cancelled = true)));
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
|
await tester.pump();
|
|
expect(cancelled, isTrue);
|
|
expect(picked, isNull);
|
|
});
|
|
|
|
testWidgets('an out-of-range number key is ignored', (tester) async {
|
|
String? picked;
|
|
await tester.pumpWidget(harness(f, ModelPickerCard(models: models, currentModel: null, onPick: (v) => picked = v, onCancel: () {})));
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.digit9);
|
|
await tester.pump();
|
|
expect(picked, isNull);
|
|
});
|
|
|
|
testWidgets('effort reuse: custom title + exact-match marking (T-412)', (tester) async {
|
|
// Exact-match isCurrent: `high` must not be marked when effort is `xhigh`.
|
|
await tester.pumpWidget(
|
|
harness(
|
|
f,
|
|
ModelPickerCard(
|
|
title: 'effort',
|
|
models: kEffortLevels,
|
|
currentModel: 'xhigh',
|
|
isCurrent: (o, c) => c != null && o.value == c,
|
|
onPick: (_) {},
|
|
onCancel: () {},
|
|
),
|
|
),
|
|
);
|
|
expect(find.text('effort'), findsOneWidget); // the custom header
|
|
expect(find.textContaining('● xhigh'), findsOneWidget);
|
|
expect(find.textContaining('○ high'), findsOneWidget); // NOT containment-marked
|
|
});
|
|
}
|