feat(agent): add voice input capability and standardize tool naming
- Add voice recording with transcribe endpoint (ASR) for multimodal input - Android: add RECORD_AUDIO and INTERNET permissions - Refactor tool naming: frontend tools use 'front.' prefix, backend tools use 'back.' - Migrate calendar tools: create_calendar_event -> back.mutate/list/delete events - Add calendar_event_list.v1 and calendar_operation.v1 UI card types - Update all Flutter and Python tests to match new tool naming conventions - Add record package dependency for voice recording
This commit is contained in:
@@ -1,12 +1,36 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:social_app/core/api/api_exception.dart';
|
||||
import 'package:social_app/features/home/data/voice_recorder.dart';
|
||||
import 'package:social_app/features/home/ui/screens/home_screen.dart';
|
||||
|
||||
class _FakeVoiceRecorder implements VoiceRecorder {
|
||||
bool started = false;
|
||||
String? stoppedPath;
|
||||
|
||||
@override
|
||||
Future<void> start() async {
|
||||
started = true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> stop() async {
|
||||
started = false;
|
||||
stoppedPath = '/tmp/test-audio.wav';
|
||||
return stoppedPath;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {}
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('HomeScreen Widget Tests', () {
|
||||
testWidgets('displays input field', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const MaterialApp(home: HomeScreen()));
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(home: HomeScreen(autoLoadHistory: false)),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(TextField), findsOneWidget);
|
||||
@@ -14,7 +38,9 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('displays header icons', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const MaterialApp(home: HomeScreen()));
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(home: HomeScreen(autoLoadHistory: false)),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byIcon(LucideIcons.settings), findsOneWidget);
|
||||
@@ -25,10 +51,116 @@ void main() {
|
||||
testWidgets('displays send or mic icon based on input', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
await tester.pumpWidget(const MaterialApp(home: HomeScreen()));
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(home: HomeScreen(autoLoadHistory: false)),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byIcon(LucideIcons.mic), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('tap mic starts recording and shows listening state', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final fakeRecorder = _FakeVoiceRecorder();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: HomeScreen(voiceRecorder: fakeRecorder, autoLoadHistory: false),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byIcon(LucideIcons.mic));
|
||||
await tester.pump();
|
||||
|
||||
expect(fakeRecorder.started, true);
|
||||
expect(find.text('正在聆听...'), findsOneWidget);
|
||||
expect(find.byIcon(LucideIcons.square), findsOneWidget);
|
||||
expect(find.byIcon(LucideIcons.send), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('tap send while recording transcribes and auto sends message', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final fakeRecorder = _FakeVoiceRecorder();
|
||||
String? sentTranscript;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: HomeScreen(
|
||||
voiceRecorder: fakeRecorder,
|
||||
autoLoadHistory: false,
|
||||
onTranscribeAudio: (filePath) async {
|
||||
expect(filePath, '/tmp/test-audio.wav');
|
||||
return '语音自动发送';
|
||||
},
|
||||
onAutoSendTranscript: (transcript) async {
|
||||
sentTranscript = transcript;
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byIcon(LucideIcons.mic));
|
||||
await tester.pump();
|
||||
await tester.tap(find.byIcon(LucideIcons.send));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(sentTranscript, '语音自动发送');
|
||||
expect(find.byIcon(LucideIcons.plus), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('tap stop transcribes audio and fills input', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final fakeRecorder = _FakeVoiceRecorder();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: HomeScreen(
|
||||
voiceRecorder: fakeRecorder,
|
||||
autoLoadHistory: false,
|
||||
onTranscribeAudio: (filePath) async {
|
||||
expect(filePath, '/tmp/test-audio.wav');
|
||||
return '语音转文字结果';
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byIcon(LucideIcons.mic));
|
||||
await tester.pump();
|
||||
await tester.tap(find.byIcon(LucideIcons.square));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('语音转文字结果'), findsOneWidget);
|
||||
expect(find.byIcon(LucideIcons.plus), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('tap stop shows readable unauthorized message', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final fakeRecorder = _FakeVoiceRecorder();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: HomeScreen(
|
||||
voiceRecorder: fakeRecorder,
|
||||
autoLoadHistory: false,
|
||||
onTranscribeAudio: (_) async {
|
||||
throw const UnauthorizedException();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byIcon(LucideIcons.mic));
|
||||
await tester.pump();
|
||||
await tester.tap(find.byIcon(LucideIcons.square));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('请重新登录'), findsOneWidget);
|
||||
await tester.pump(const Duration(seconds: 3));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user