halkyon/halkyon_learn_flutter/test/feature/assistant/connect_claude_test.dart
Tizian 「ティツィアーン」 8dde0fa42a
feat(assistant): a question opens in the AI assistant chosen, Proton's Lumo by default
Questions asked from the app went to Claude alone. Settings › Assistant
now chooses where they open (decided 2026-10-14): Lumo, the default, by
its guest chat with the question in the address's fragment (Lumo's
`prefill`, which needs no account and reaches no server; the address for
one's own account loses the question when nobody is signed in), ChatGPT,
Claude, Mistral, Perplexity, Duck.ai, Grok, Kagi Assistant, HuggingChat,
or an address of one's own with %s. Each link fills the question in for
the person to send (Perplexity answers at once, Grok asks first); Gemini,
which has no such link, and Copilot, whose stopped working, are left out.
The field names the assistant ("Ask Lumo…"). A device that sent questions
to Claude goes on asking an assistant, now Lumo. Connecting Claude to read
studysets stays as it was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 14:35:35 +02:00

167 lines
5.4 KiB
Dart

import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/src/core/device_settings/model/device_settings.dart';
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
import 'package:halkyon_learn_flutter/src/feature/assistant/model/connected_assistant.dart';
import 'package:halkyon_learn_flutter/src/feature/assistant/widget/connect_claude_screen.dart';
import 'package:halkyon_learn_flutter/src/feature/settings/widget/settings_screen.dart';
import 'package:material_ui/material_ui.dart';
import '../../support/fake_assistant_gateway.dart';
import '../../support/pump_app.dart';
import '../../support/test_dependencies.dart';
void main() {
late FakeAssistantGateway gateway;
setUp(() {
gateway = FakeAssistantGateway(
initially: [
ConnectedAssistant(
id: 1,
name: 'Claude',
host: 'claude.ai',
connectedAt: DateTime.utc(2026, 10, 9),
lastUsedAt: DateTime.utc(2026, 10, 10),
),
],
);
});
Future<void> showApp(
WidgetTester tester, {
String location = RoutePaths.connectAssistant,
bool on = true,
}) async {
tester.view.physicalSize = const Size(400, 1000);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
await pumpApp(
tester,
testDependencies(
assistantGateway: gateway,
deviceSettings: DeviceSettings(assistant: on),
),
location: location,
);
await tester.pumpAndSettle();
}
/// Brings [finder] in Settings into view.
Future<void> scrollTo(WidgetTester tester, Finder finder) async {
await tester.scrollUntilVisible(
finder,
200,
scrollable: find
.descendant(
of: find.byType(SettingsScreen),
matching: find.byType(Scrollable),
)
.first,
);
await tester.ensureVisible(finder);
await tester.pumpAndSettle();
}
testWidgets('Settings offers to connect Claude only while asking Claude is '
'on', (tester) async {
await showApp(tester, location: RoutePaths.settings, on: false);
await scrollTo(tester, find.text('Ask an AI assistant or search the web'));
expect(find.text('Connect Claude'), findsNothing);
await tester.tap(find.text('Ask an AI assistant or search the web'));
await tester.pumpAndSettle();
await scrollTo(tester, find.text('Connect Claude'));
await tester.tap(find.text('Connect Claude'));
await tester.pumpAndSettle();
expect(find.byType(ConnectClaudeScreen), findsOneWidget);
});
testWidgets("shows the connector's address to add in Claude, to copy", (
tester,
) async {
final copied = <Object?>[];
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform,
(call) async {
if (call.method == 'Clipboard.setData') {
copied.add((call.arguments as Map)['text']);
}
return null;
},
);
addTearDown(
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform,
null,
),
);
await showApp(tester);
expect(find.text('https://learn.example.org/mcp'), findsOneWidget);
expect(find.textContaining('learn.example.org through'), findsOneWidget);
await tester.tap(find.byTooltip('Copy'));
await tester.pumpAndSettle();
expect(copied, ['https://learn.example.org/mcp']);
expect(find.text('Copied'), findsOneWidget);
});
testWidgets('shows a code on asking, and a new one replaces it', (
tester,
) async {
await showApp(tester);
await tester.tap(find.text('Show a code'));
await tester.pumpAndSettle();
expect(find.text('K7QX-M2P0'), findsOneWidget);
expect(find.textContaining('Valid until'), findsOneWidget);
await tester.tap(find.text('New code'));
await tester.pumpAndSettle();
expect(find.text('K7QX-M2P0'), findsNothing);
expect(find.text('K7QX-M2P1'), findsOneWidget);
});
testWidgets('lists what is connected; disconnecting asks first', (
tester,
) async {
await showApp(tester);
expect(find.text('Claude'), findsOneWidget);
expect(find.textContaining('claude.ai'), findsOneWidget);
await tester.tap(find.widgetWithText(TextButton, 'Disconnect'));
await tester.pumpAndSettle();
expect(find.text('Disconnect Claude?'), findsOneWidget);
await tester.tap(find.widgetWithText(FilledButton, 'Disconnect'));
await tester.pumpAndSettle();
expect(gateway.connected, isEmpty);
expect(find.text('Nothing connected yet.'), findsOneWidget);
});
testWidgets('an instance without a connector says so, and shows no steps', (
tester,
) async {
gateway = FakeAssistantGateway(url: null);
await showApp(tester);
expect(find.textContaining("doesn't offer a connector"), findsOneWidget);
expect(find.text('Show a code'), findsNothing);
});
testWidgets('an instance out of reach says so, and asks again on Try '
'again', (tester) async {
gateway.unreachable = true;
await showApp(tester);
expect(find.textContaining("can't be reached"), findsOneWidget);
gateway.unreachable = false;
await tester.tap(find.text('Try again'));
await tester.pumpAndSettle();
expect(find.textContaining("can't be reached"), findsNothing);
expect(find.text('https://learn.example.org/mcp'), findsOneWidget);
});
}