@TestOn('vm') library; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:xworkmate/app/app_controller.dart'; import 'package:xworkmate/app/ui_feature_manifest.dart'; import 'package:xworkmate/features/settings/settings_page.dart'; import 'package:xworkmate/models/app_models.dart'; import 'package:xworkmate/runtime/desktop_platform_service.dart'; import 'package:xworkmate/runtime/runtime_models.dart'; import 'package:xworkmate/runtime/skill_directory_access.dart'; import '../test_support.dart'; class _DesktopServiceStub implements DesktopPlatformService { @override DesktopIntegrationState get state => DesktopIntegrationState.fromJson(const { 'isSupported': true, 'environment': 'kde', 'mode': 'proxy', 'trayAvailable': true, 'trayEnabled': true, 'autostartEnabled': false, 'networkManagerAvailable': true, 'systemProxy': { 'enabled': true, 'host': '127.0.0.1', 'port': 7890, 'backend': 'kioslaverc', 'lastAppliedMode': 'proxy', }, 'tunnel': { 'available': true, 'connected': false, 'connectionName': 'XWorkmate Tunnel', 'backend': 'nmcli', 'lastError': '', }, 'statusMessage': '', }); @override bool get isSupported => state.isSupported; @override Future initialize(LinuxDesktopConfig config) async {} @override Future syncConfig(LinuxDesktopConfig config) async {} @override Future refresh() async {} @override Future setMode(VpnMode mode) async {} @override Future connectTunnel() async {} @override Future disconnectTunnel() async {} @override Future setLaunchAtLogin(bool enabled) async {} @override void dispose() {} } Future _createControllerWithSkillAccessService( WidgetTester tester, SkillDirectoryAccessService skillDirectoryAccessService, ) async { final controller = AppController( store: createIsolatedTestStore(enableSecureStorage: false), skillDirectoryAccessService: skillDirectoryAccessService, ); addTearDown(controller.dispose); await tester.pump(const Duration(milliseconds: 100)); await tester.pumpAndSettle(); return controller; } class _FakeSkillDirectoryAccessService implements SkillDirectoryAccessService { _FakeSkillDirectoryAccessService({ required this.userHomeDirectory, this.multiDirectoryResponse = const [], }); final String userHomeDirectory; final List multiDirectoryResponse; @override bool get isSupported => true; @override Future resolveUserHomeDirectory() async { return userHomeDirectory; } @override Future> authorizeDirectories({ List suggestedPaths = const [], }) async { return multiDirectoryResponse; } @override Future authorizeDirectory({ String suggestedPath = '', }) async { final normalized = normalizeAuthorizedSkillDirectoryPath(suggestedPath); if (normalized.isEmpty) { return null; } return AuthorizedSkillDirectory( path: normalized, bookmark: 'bookmark-${normalized.hashCode}', ); } @override Future openDirectory( AuthorizedSkillDirectory directory, ) async { final normalized = normalizeAuthorizedSkillDirectoryPath(directory.path); if (normalized.isEmpty) { return null; } return SkillDirectoryAccessHandle(path: normalized, onClose: () async {}); } } void main() { testWidgets('SettingsPage theme chips update controller theme mode', ( WidgetTester tester, ) async { final controller = await createTestController(tester); await pumpPage( tester, child: SettingsPage(controller: controller), platform: TargetPlatform.macOS, ); await tester.tap(find.text('外观')); await tester.pumpAndSettle(); await tester.tap(find.text('深色')); await tester.pumpAndSettle(); expect(controller.themeMode, ThemeMode.dark); await tester.tap(find.text('浅色')); await tester.pumpAndSettle(); expect(controller.themeMode, ThemeMode.light); }); testWidgets('SettingsPage hides account access controls by default', ( WidgetTester tester, ) async { final controller = await createTestController(tester); await pumpPage( tester, child: SettingsPage(controller: controller), platform: TargetPlatform.macOS, ); expect(find.text('账号访问'), findsNothing); expect(find.text('Account Access'), findsNothing); expect(find.text('账号本地模式'), findsNothing); expect(find.text('Account local mode'), findsNothing); }); testWidgets('SettingsPage can expose account access when feature enabled', ( WidgetTester tester, ) async { final manifest = UiFeatureManifest.fallback().copyWithFeature( platform: UiFeaturePlatform.desktop, module: 'settings', feature: 'account_access', enabled: true, releaseTier: UiFeatureReleaseTier.experimental, ); final controller = await createTestController( tester, uiFeatureManifest: manifest, ); await pumpPage( tester, child: SettingsPage(controller: controller), platform: TargetPlatform.macOS, ); expect(find.text('账号访问'), findsOneWidget); expect(find.text('账号本地模式'), findsOneWidget); }); testWidgets('SettingsPage integration tab exposes unified gateway controls', ( WidgetTester tester, ) async { final controller = await createTestController(tester); await pumpPage( tester, child: SettingsPage(controller: controller), platform: TargetPlatform.macOS, ); await tester.tap(find.text('集成')); await tester.pumpAndSettle(); expect(find.text('OpenClaw Gateway'), findsWidgets); expect(find.text('LLM 接入点'), findsOneWidget); expect(find.text('ACP 外部接入'), findsOneWidget); expect(find.text('Vault Server'), findsNothing); expect(find.byKey(const ValueKey('ai-gateway-url-field')), findsNothing); expect(find.byKey(const ValueKey('gateway-mode-field')), findsNothing); expect(find.text('认证诊断'), findsNothing); expect(find.byKey(const ValueKey('gateway-test-button')), findsOneWidget); expect(find.byKey(const ValueKey('gateway-save-button')), findsOneWidget); expect(find.byKey(const ValueKey('gateway-apply-button')), findsOneWidget); expect( find.byKey(const ValueKey('gateway-profile-chip-0')), findsOneWidget, ); expect( find.byKey(const ValueKey('gateway-profile-chip-1')), findsOneWidget, ); expect( find.byKey(const ValueKey('gateway-profile-chip-2')), findsOneWidget, ); expect( find.byKey(const ValueKey('gateway-profile-chip-3')), findsOneWidget, ); expect( find.byKey(const ValueKey('gateway-profile-chip-4')), findsOneWidget, ); expect( find.descendant( of: find.byKey(const ValueKey('gateway-profile-chip-2')), matching: find.text('连接源 1(空)'), ), findsOneWidget, ); expect(find.text('自定义连接源 1(空)'), findsNothing); expect( find.byKey(const ValueKey('gateway-device-security-card')), findsOneWidget, ); }); testWidgets('SettingsPage can expose vault section when feature enabled', ( WidgetTester tester, ) async { final manifest = UiFeatureManifest.fallback().copyWithFeature( platform: UiFeaturePlatform.desktop, module: 'settings', feature: 'vault_server', enabled: true, releaseTier: UiFeatureReleaseTier.experimental, ); final controller = await createTestController( tester, uiFeatureManifest: manifest, ); await pumpPage( tester, child: SettingsPage(controller: controller), platform: TargetPlatform.macOS, ); await tester.tap(find.text('集成')); await tester.pumpAndSettle(); expect(find.text('Vault Server'), findsOneWidget); }); testWidgets('SettingsPage integration tab exposes ACP provider endpoints', ( WidgetTester tester, ) async { final controller = await createTestController(tester); await pumpPage( tester, child: SettingsPage(controller: controller), platform: TargetPlatform.macOS, ); await tester.tap(find.text('集成')); await tester.pumpAndSettle(); await tester.tap(find.text('ACP 外部接入').first); await tester.pumpAndSettle(); expect(find.text('外部 ACP Server Endpoint'), findsOneWidget); expect(find.text('Codex'), findsWidgets); expect(find.text('OpenCode'), findsWidgets); expect(find.text('Claude'), findsNothing); expect(find.text('Gemini'), findsNothing); expect( find.byKey(const ValueKey('external-acp-provider-add-button')), findsOneWidget, ); expect(find.text('添加更多自定义配置'), findsOneWidget); expect(find.textContaining('ws://127.0.0.1:9001'), findsWidgets); expect(find.text('标志'), findsNothing); expect(find.text('Badge'), findsNothing); expect( find.byKey(const ValueKey('settings-global-save-button')), findsOneWidget, ); expect( find.byKey(const ValueKey('settings-global-apply-button')), findsOneWidget, ); }); testWidgets('SettingsPage ACP wizard adds a custom provider card', ( WidgetTester tester, ) async { final controller = await createTestController(tester); await pumpPage( tester, child: SettingsPage(controller: controller), platform: TargetPlatform.macOS, ); await tester.tap(find.text('集成')); await tester.pumpAndSettle(); await tester.tap(find.text('ACP 外部接入').first); await tester.pumpAndSettle(); await tester.tap( find.byKey(const ValueKey('external-acp-provider-add-button')), ); await tester.pumpAndSettle(); await tester.enterText( find.byKey(const ValueKey('external-acp-wizard-name-field')), 'Lab Agent', ); await tester.enterText( find.byKey(const ValueKey('external-acp-wizard-endpoint-field')), 'wss://lab.example.com/acp', ); await tester.tap( find.byKey(const ValueKey('external-acp-wizard-confirm-button')), ); await tester.pumpAndSettle(); expect(find.text('Lab Agent'), findsWidgets); expect(find.text('wss://lab.example.com/acp'), findsWidgets); }); testWidgets('SettingsPage skills authorization tab keeps only preset roots', ( WidgetTester tester, ) async { final controller = await _createControllerWithSkillAccessService( tester, _FakeSkillDirectoryAccessService(userHomeDirectory: '/Users/tester'), ); await pumpPage( tester, child: SettingsPage(controller: controller), platform: TargetPlatform.macOS, ); await tester.tap(find.text('集成')); await tester.pumpAndSettle(); await tester.tap(find.text('SKILLS 目录授权')); await tester.pumpAndSettle(); expect(find.text('~/.agents/skills'), findsOneWidget); expect(find.text('/Users/tester/.agents/skills'), findsOneWidget); expect(find.text('~/.codex/skills'), findsOneWidget); expect(find.text('/Users/tester/.codex/skills'), findsOneWidget); expect(find.text('~/.workbuddy/skills'), findsOneWidget); expect(find.text('/Users/tester/.workbuddy/skills'), findsOneWidget); }); testWidgets('SettingsPage can batch add custom skills directories', ( WidgetTester tester, ) async { final controller = await _createControllerWithSkillAccessService( tester, _FakeSkillDirectoryAccessService( userHomeDirectory: '/Users/tester', multiDirectoryResponse: const [ AuthorizedSkillDirectory( path: '/Users/tester/custom-a', bookmark: 'bookmark-a', ), AuthorizedSkillDirectory( path: '/Users/tester/custom-b', bookmark: 'bookmark-b', ), ], ), ); await pumpPage( tester, child: SettingsPage(controller: controller), platform: TargetPlatform.macOS, ); await tester.tap(find.text('集成')); await tester.pumpAndSettle(); await tester.tap(find.text('SKILLS 目录授权')); await tester.pumpAndSettle(); await tester.tap( find.byKey(const ValueKey('skill-directory-batch-add-button')), ); await tester.pumpAndSettle(); await tester.enterText( find.byKey(const ValueKey('skill-directory-path-input')), ''' paths: - /Users/tester/custom-a - "/Users/tester/custom-b" ''', ); await tester.pumpAndSettle(); await tester.tap( find.byKey(const ValueKey('skill-directory-direct-add-button')), ); await tester.pump(); for ( var attempt = 0; attempt < 10 && controller.authorizedSkillDirectories.length < 2; attempt += 1 ) { await tester.pump(const Duration(milliseconds: 100)); } expect( controller.authorizedSkillDirectories.map((item) => item.path), containsAll(const [ '/Users/tester/custom-a', '/Users/tester/custom-b', ]), ); expect(find.text('custom-a'), findsOneWidget); expect(find.text('custom-b'), findsOneWidget); }); testWidgets('SettingsPage skills authorization dialog can use picker flow', ( WidgetTester tester, ) async { final controller = await _createControllerWithSkillAccessService( tester, _FakeSkillDirectoryAccessService( userHomeDirectory: '/Users/tester', multiDirectoryResponse: const [ AuthorizedSkillDirectory( path: '/Users/tester/custom-picker', bookmark: 'bookmark-picker', ), ], ), ); await pumpPage( tester, child: SettingsPage(controller: controller), platform: TargetPlatform.macOS, ); await tester.tap(find.text('集成')); await tester.pumpAndSettle(); await tester.tap(find.text('SKILLS 目录授权')); await tester.pumpAndSettle(); await tester.tap( find.byKey(const ValueKey('skill-directory-batch-add-button')), ); await tester.pumpAndSettle(); await tester.enterText( find.byKey(const ValueKey('skill-directory-path-input')), '/Users/tester/custom-picker', ); await tester.pumpAndSettle(); await tester.tap( find.byKey(const ValueKey('skill-directory-picker-button')), ); await tester.pump(); for ( var attempt = 0; attempt < 10 && controller.authorizedSkillDirectories.isEmpty; attempt += 1 ) { await tester.pump(const Duration(milliseconds: 100)); } expect( controller.authorizedSkillDirectories.map((item) => item.path), contains('/Users/tester/custom-picker'), ); }); testWidgets( 'SettingsPage batch add normalizes pasted SKILL.md paths to skill package directories', (WidgetTester tester) async { final controller = await _createControllerWithSkillAccessService( tester, _FakeSkillDirectoryAccessService(userHomeDirectory: '/Users/tester'), ); await pumpPage( tester, child: SettingsPage(controller: controller), platform: TargetPlatform.macOS, ); await tester.tap(find.text('集成')); await tester.pumpAndSettle(); await tester.tap(find.text('SKILLS 目录授权')); await tester.pumpAndSettle(); await tester.tap( find.byKey(const ValueKey('skill-directory-batch-add-button')), ); await tester.pumpAndSettle(); await tester.enterText( find.byKey(const ValueKey('skill-directory-path-input')), '/Users/tester/workspaces/ai-workflow-craft/skills/docx/SKILL.md', ); await tester.pumpAndSettle(); await tester.tap( find.byKey(const ValueKey('skill-directory-direct-add-button')), ); await tester.pump(); for ( var attempt = 0; attempt < 10 && controller.authorizedSkillDirectories.isEmpty; attempt += 1 ) { await tester.pump(const Duration(milliseconds: 100)); } expect( controller.authorizedSkillDirectories.map((item) => item.path), const [ '/Users/tester/workspaces/ai-workflow-craft/skills/docx', ], ); expect(find.text('docx'), findsOneWidget); }, ); testWidgets('SettingsPage gateway sections can collapse individually', ( WidgetTester tester, ) async { final controller = await createTestController(tester); await pumpPage( tester, child: SettingsPage(controller: controller), platform: TargetPlatform.macOS, ); await tester.tap(find.text('集成')); await tester.pumpAndSettle(); await tester.tap(find.byTooltip('折叠').first); await tester.pumpAndSettle(); expect(find.byKey(const ValueKey('gateway-host-field')), findsNothing); expect(find.byKey(const ValueKey('gateway-test-button')), findsNothing); expect( find.byKey(const ValueKey('gateway-device-security-card')), findsNothing, ); await tester.tap(find.byTooltip('展开').first); await tester.pumpAndSettle(); expect(find.byKey(const ValueKey('gateway-host-field')), findsOneWidget); expect(find.byKey(const ValueKey('gateway-test-button')), findsOneWidget); expect( find.byKey(const ValueKey('gateway-device-security-card')), findsOneWidget, ); }); testWidgets('SettingsPage shows Linux desktop integration controls', ( WidgetTester tester, ) async { final controller = await createTestController( tester, desktopPlatformService: _DesktopServiceStub(), ); await pumpPage( tester, child: SettingsPage(controller: controller), platform: TargetPlatform.macOS, ); expect( find.byKey(const ValueKey('linux-desktop-integration-card')), findsOneWidget, ); expect(find.text('Linux 桌面集成'), findsOneWidget); expect(find.text('切换到代理'), findsOneWidget); expect(find.text('连接隧道'), findsOneWidget); }); testWidgets('SettingsPage multi-agent tab keeps header readable', ( WidgetTester tester, ) async { final manifest = UiFeatureManifest.fallback().copyWithFeature( platform: UiFeaturePlatform.desktop, module: 'settings', feature: 'agents', enabled: true, releaseTier: UiFeatureReleaseTier.stable, ); final controller = await createTestController( tester, uiFeatureManifest: manifest, ); await pumpPage( tester, child: const SizedBox(width: 1100, height: 900, child: Placeholder()), platform: TargetPlatform.macOS, ); await pumpPage( tester, child: SizedBox( width: 1100, height: 900, child: SettingsPage(controller: controller), ), platform: TargetPlatform.macOS, ); await tester.tap(find.text('多 Agent')); await tester.pumpAndSettle(); final titleFinder = find.text('多 Agent 协作'); expect(titleFinder, findsOneWidget); expect(tester.getSize(titleFinder).width, greaterThan(80)); expect(find.text('启用协作模式'), findsOneWidget); expect(find.text('协作框架'), findsOneWidget); expect(find.textContaining('Lead Engineer'), findsWidgets); expect(find.textContaining('ollama launch codex'), findsOneWidget); expect(tester.takeException(), isNull); }); testWidgets('SettingsPage hides gateway setup code editor by default', ( WidgetTester tester, ) async { final controller = await createTestController(tester); await pumpPage( tester, child: SettingsPage(controller: controller), platform: TargetPlatform.macOS, ); await tester.tap(find.text('集成')); await tester.pumpAndSettle(); await tester.tap(find.byKey(const ValueKey('gateway-profile-chip-1'))); await tester.pumpAndSettle(); expect(find.text('配置码'), findsNothing); expect( find.byKey(const ValueKey('gateway-setup-code-field')), findsNothing, ); expect(find.byKey(const ValueKey('gateway-host-field')), findsOneWidget); }); testWidgets('SettingsPage diagnostics tab filters and clears runtime logs', ( WidgetTester tester, ) async { final controller = await createTestController(tester); controller.runtime.addRuntimeLogForTest( level: 'info', category: 'connect', message: 'connected remote gateway', ); controller.runtime.addRuntimeLogForTest( level: 'warn', category: 'pairing', message: 'pairing required', ); await pumpPage(tester, child: SettingsPage(controller: controller)); await tester.tap(find.text('诊断')); await tester.pumpAndSettle(); expect(find.byKey(const ValueKey('runtime-log-card')), findsOneWidget); expect(find.textContaining('connected remote gateway'), findsOneWidget); expect(find.textContaining('pairing required'), findsOneWidget); await tester.enterText( find.byKey(const ValueKey('runtime-log-filter')), 'pairing', ); await tester.pump(const Duration(milliseconds: 200)); expect(find.textContaining('connected remote gateway'), findsNothing); expect(find.textContaining('pairing required'), findsOneWidget); await tester.tap(find.text('清空')); await tester.pump(const Duration(milliseconds: 200)); expect(controller.runtimeLogs, isEmpty); }); testWidgets('SettingsPage hides tabs disabled by feature manifest', ( WidgetTester tester, ) async { final manifest = UiFeatureManifest.fallback() .copyWithFeature( platform: UiFeaturePlatform.desktop, module: 'settings', feature: 'diagnostics', enabled: false, ) .copyWithFeature( platform: UiFeaturePlatform.desktop, module: 'settings', feature: 'experimental', enabled: false, ); final controller = await createTestController( tester, uiFeatureManifest: manifest, ); await pumpPage( tester, child: SettingsPage(controller: controller), platform: TargetPlatform.macOS, ); expect(find.text('诊断'), findsNothing); expect(find.text('实验特性'), findsNothing); }); testWidgets( 'SettingsPage clears local assistant state with double confirmation', (WidgetTester tester) async { final controller = await createTestController(tester); await pumpPage(tester, child: SettingsPage(controller: controller)); await tester.tap(find.text('诊断')); await tester.pump(const Duration(milliseconds: 300)); expect( find.byKey(const ValueKey('assistant-local-state-card')), findsOneWidget, ); await tester.tap( find.byKey(const ValueKey('assistant-local-state-clear-button')), ); await tester.pump(const Duration(milliseconds: 300)); final confirmButtonFinder = find.widgetWithText(FilledButton, '确认清理'); final confirmButtonBefore = tester.widget( confirmButtonFinder, ); expect(confirmButtonBefore.onPressed, isNull); await tester.tap( find.byKey(const ValueKey('assistant-local-state-clear-confirm')), ); await tester.pump(const Duration(milliseconds: 300)); final confirmButtonAfter = tester.widget( confirmButtonFinder, ); expect(confirmButtonAfter.onPressed, isNotNull); }, ); testWidgets('SettingsPage detail mode returns to overview', ( WidgetTester tester, ) async { final controller = await createTestController(tester); controller.openSettings( detail: SettingsDetailPage.gatewayConnection, navigationContext: SettingsNavigationContext( rootLabel: '设置', destination: WorkspaceDestination.settings, sectionLabel: SettingsTab.gateway.label, settingsTab: SettingsTab.gateway, ), ); await pumpPage( tester, child: SettingsPage( controller: controller, initialTab: controller.settingsTab, initialDetail: controller.settingsDetail, navigationContext: controller.settingsNavigationContext, ), ); expect(find.text('Gateway 连接参数'), findsWidgets); expect(find.text('返回概览'), findsOneWidget); await tester.tap(find.text('返回概览')); await tester.pumpAndSettle(); expect(controller.settingsDetail, isNull); expect(find.text('搜索设置'), findsOneWidget); }); testWidgets('SettingsPage expands optional LLM endpoints with add button', ( WidgetTester tester, ) async { final controller = await createTestController(tester); controller.openSettings( detail: SettingsDetailPage.aiGatewayIntegration, navigationContext: SettingsNavigationContext( rootLabel: '设置', destination: WorkspaceDestination.settings, sectionLabel: SettingsTab.gateway.label, settingsTab: SettingsTab.gateway, ), ); await pumpPage( tester, child: SettingsPage( controller: controller, initialTab: controller.settingsTab, initialDetail: controller.settingsDetail, navigationContext: controller.settingsNavigationContext, ), ); expect(find.byKey(const ValueKey('llm-endpoint-chip-0')), findsOneWidget); expect(find.byKey(const ValueKey('llm-endpoint-chip-1')), findsNothing); await tester.tap(find.byKey(const ValueKey('llm-endpoint-add-button'))); await tester.pumpAndSettle(); expect( find.descendant( of: find.byKey(const ValueKey('llm-endpoint-chip-0')), matching: find.textContaining('主 LLM API'), ), findsOneWidget, ); expect( find.descendant( of: find.byKey(const ValueKey('llm-endpoint-chip-1')), matching: find.textContaining('Ollama 本地'), ), findsOneWidget, ); expect(find.text('连接源详情'), findsOneWidget); expect(find.textContaining('自定义连接源'), findsNothing); expect(find.byKey(const ValueKey('llm-endpoint-chip-1')), findsOneWidget); expect( find.byKey(const ValueKey('llm-endpoint-panel-ollamaLocal')), findsOneWidget, ); }); }