211 lines
6.5 KiB
Dart
211 lines
6.5 KiB
Dart
@TestOn('vm')
|
|
library;
|
|
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:xworkmate/app/app_controller.dart';
|
|
import 'package:xworkmate/features/ai_gateway/ai_gateway_page.dart';
|
|
import 'package:xworkmate/features/settings/settings_page.dart';
|
|
import 'package:xworkmate/models/app_models.dart';
|
|
import 'package:xworkmate/runtime/codex_runtime.dart';
|
|
import 'package:xworkmate/runtime/device_identity_store.dart';
|
|
import 'package:xworkmate/runtime/gateway_runtime.dart';
|
|
import 'package:xworkmate/runtime/runtime_coordinator.dart';
|
|
import 'package:xworkmate/runtime/runtime_models.dart';
|
|
import 'package:xworkmate/runtime/secure_config_store.dart';
|
|
import 'package:xworkmate/theme/app_theme.dart';
|
|
|
|
import '../test_support.dart';
|
|
|
|
class _FakeGatewayRuntime extends GatewayRuntime {
|
|
_FakeGatewayRuntime()
|
|
: super(
|
|
store: SecureConfigStore(),
|
|
identityStore: DeviceIdentityStore(SecureConfigStore()),
|
|
);
|
|
|
|
@override
|
|
Future<void> connectProfile(
|
|
GatewayConnectionProfile profile, {
|
|
String authTokenOverride = '',
|
|
String authPasswordOverride = '',
|
|
}) async {}
|
|
|
|
@override
|
|
Future<void> disconnect({bool clearDesiredProfile = true}) async {}
|
|
|
|
@override
|
|
Future<dynamic> request(
|
|
String method, {
|
|
Map<String, dynamic>? params,
|
|
Duration timeout = const Duration(seconds: 30),
|
|
}) async {
|
|
return <String, dynamic>{};
|
|
}
|
|
}
|
|
|
|
class _FakeCodexRuntime extends CodexRuntime {
|
|
@override
|
|
Future<String?> findCodexBinary() async => null;
|
|
|
|
@override
|
|
Future<void> stop() async {}
|
|
}
|
|
|
|
class _AiGatewayPageTestController extends AppController {
|
|
_AiGatewayPageTestController({
|
|
required super.store,
|
|
required super.runtimeCoordinator,
|
|
});
|
|
|
|
@override
|
|
Future<void> refreshMultiAgentMounts({bool sync = false}) async {}
|
|
}
|
|
|
|
void main() {
|
|
testWidgets('AiGatewayPage edit settings opens detail context', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final controller = await createTestController(tester);
|
|
|
|
await pumpPage(
|
|
tester,
|
|
child: AiGatewayPage(controller: controller, onOpenDetail: (_) {}),
|
|
);
|
|
|
|
await tester.tap(find.text('编辑设置'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(controller.destination, WorkspaceDestination.settings);
|
|
expect(controller.settingsDetail, SettingsDetailPage.aiGatewayIntegration);
|
|
expect(
|
|
controller.settingsNavigationContext?.aiGatewayTab,
|
|
AiGatewayTab.models,
|
|
);
|
|
});
|
|
|
|
testWidgets(
|
|
'Settings external agents detail shows Codex bridge runtime states',
|
|
(WidgetTester tester) async {
|
|
late AppController controller;
|
|
late Directory testRoot;
|
|
await tester.runAsync(() async {
|
|
SharedPreferences.setMockInitialValues(<String, Object>{});
|
|
testRoot = await Directory.systemTemp.createTemp(
|
|
'xworkmate-ai-gateway-page-',
|
|
);
|
|
final store = SecureConfigStore(
|
|
enableSecureStorage: false,
|
|
databasePathResolver: () async => '${testRoot.path}/settings.sqlite3',
|
|
fallbackDirectoryPathResolver: () async => testRoot.path,
|
|
);
|
|
controller = _AiGatewayPageTestController(
|
|
store: store,
|
|
runtimeCoordinator: RuntimeCoordinator(
|
|
gateway: _FakeGatewayRuntime(),
|
|
codex: _FakeCodexRuntime(),
|
|
),
|
|
);
|
|
await _waitFor(() => !controller.initializing);
|
|
});
|
|
addTearDown(() => controller.dispose());
|
|
addTearDown(() async {
|
|
if (await testRoot.exists()) {
|
|
await testRoot.delete(recursive: true);
|
|
}
|
|
});
|
|
|
|
tester.view.devicePixelRatio = 1;
|
|
tester.view.physicalSize = const Size(1600, 1000);
|
|
addTearDown(() {
|
|
tester.view.resetPhysicalSize();
|
|
tester.view.resetDevicePixelRatio();
|
|
});
|
|
|
|
controller.openSettings(
|
|
detail: SettingsDetailPage.externalAgents,
|
|
navigationContext: SettingsNavigationContext(
|
|
rootLabel: 'AI Gateway',
|
|
destination: WorkspaceDestination.aiGateway,
|
|
sectionLabel: AiGatewayTab.tools.label,
|
|
aiGatewayTab: AiGatewayTab.tools,
|
|
),
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
locale: const Locale('zh'),
|
|
supportedLocales: const [Locale('zh'), Locale('en')],
|
|
localizationsDelegates: GlobalMaterialLocalizations.delegates,
|
|
theme: AppTheme.light(),
|
|
darkTheme: AppTheme.dark(),
|
|
home: Scaffold(
|
|
body: SettingsPage(
|
|
controller: controller,
|
|
initialTab: controller.settingsTab,
|
|
initialDetail: controller.settingsDetail,
|
|
navigationContext: controller.settingsNavigationContext,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
expect(find.text('External Codex CLI'), findsOneWidget);
|
|
expect(find.text('Built-in Codex (Experimental)'), findsOneWidget);
|
|
expect(find.text('未检测到'), findsOneWidget);
|
|
|
|
final builtInChip = find.widgetWithText(
|
|
ChoiceChip,
|
|
'Built-in Codex (Experimental)',
|
|
);
|
|
await tester.ensureVisible(builtInChip);
|
|
await tester.tap(builtInChip);
|
|
await tester.pumpAndSettle();
|
|
expect(
|
|
controller.settings.codeAgentRuntimeMode,
|
|
CodeAgentRuntimeMode.builtIn,
|
|
);
|
|
|
|
late Directory tempDir;
|
|
late File codexBinary;
|
|
await tester.runAsync(() async {
|
|
tempDir = await Directory.systemTemp.createTemp(
|
|
'codex-ai-gateway-page-',
|
|
);
|
|
codexBinary = File('${tempDir.path}/codex');
|
|
await codexBinary.writeAsString('#!/bin/sh\nexit 0\n');
|
|
await controller.saveSettings(
|
|
controller.settings.copyWith(
|
|
codeAgentRuntimeMode: CodeAgentRuntimeMode.externalCli,
|
|
codexCliPath: codexBinary.path,
|
|
),
|
|
);
|
|
});
|
|
addTearDown(() async {
|
|
if (await tempDir.exists()) {
|
|
await tempDir.delete(recursive: true);
|
|
}
|
|
});
|
|
await tester.pump(const Duration(milliseconds: 200));
|
|
|
|
expect(find.text('已就绪'), findsOneWidget);
|
|
expect(find.text(codexBinary.path), findsAtLeastNWidgets(1));
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<void> _waitFor(bool Function() predicate) async {
|
|
final deadline = DateTime.now().add(const Duration(seconds: 10));
|
|
while (!predicate()) {
|
|
if (DateTime.now().isAfter(deadline)) {
|
|
fail('condition not met before timeout');
|
|
}
|
|
await Future<void>.delayed(const Duration(milliseconds: 20));
|
|
}
|
|
}
|