From 319d7a383fc7be612ae2603c2aaa0c17659402d7 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Wed, 8 Apr 2026 16:35:25 +0800 Subject: [PATCH] test: add automation suite coverage --- .github/workflows/testing.yml | 45 +++++++ AGENTS.md | 13 ++ agent.md | 10 ++ docs/README_TESTING.md | 43 +++++++ go_service/go.mod | 3 + go_service/internal/handler/auth_handler.go | 29 +++++ .../internal/handler/auth_handler_test.go | 22 ++++ go_service/internal/service/auth_service.go | 15 +++ .../internal/service/auth_service_test.go | 13 ++ .../desktop_navigation_flow_test.dart | 4 +- .../desktop_settings_flow_test.dart | 34 ++--- integration_test/home_flow_test.dart | 19 +++ integration_test/login_flow_test.dart | 42 +++++++ .../settings/codex_integration_card.dart | 3 + lib/widgets/section_tabs.dart | 2 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + macos/Podfile.lock | 14 +++ patrol_test/app_test.dart | 5 + patrol_test/camera_test.dart | 5 + patrol_test/permission_test.dart | 5 + pubspec.lock | 64 ++++++++++ pubspec.yaml | 2 + ...ssistant_page_single_agent_flow_suite.dart | 115 +++++++++++++++++ ...gs_page_external_acp_end_to_end_suite.dart | 117 ++++++++++++++++++ test/golden/goldens/assistant_home_shell.png | Bin 0 -> 47977 bytes .../goldens/settings_integrations_shell.png | Bin 0 -> 28492 bytes test/golden/home_golden_test.dart | 21 ++++ test/golden/login_golden_test.dart | 22 ++++ test/helpers/golden_test_bootstrap.dart | 35 ++++++ test/helpers/pump_app.dart | 17 +++ test/helpers/test_keys.dart | 34 +++++ 31 files changed, 738 insertions(+), 17 deletions(-) create mode 100644 .github/workflows/testing.yml create mode 100644 agent.md create mode 100644 docs/README_TESTING.md create mode 100644 go_service/go.mod create mode 100644 go_service/internal/handler/auth_handler.go create mode 100644 go_service/internal/handler/auth_handler_test.go create mode 100644 go_service/internal/service/auth_service.go create mode 100644 go_service/internal/service/auth_service_test.go create mode 100644 integration_test/home_flow_test.dart create mode 100644 integration_test/login_flow_test.dart create mode 100644 patrol_test/app_test.dart create mode 100644 patrol_test/camera_test.dart create mode 100644 patrol_test/permission_test.dart create mode 100644 test/features/assistant_page_single_agent_flow_suite.dart create mode 100644 test/features/settings_page_external_acp_end_to_end_suite.dart create mode 100644 test/golden/goldens/assistant_home_shell.png create mode 100644 test/golden/goldens/settings_integrations_shell.png create mode 100644 test/golden/home_golden_test.dart create mode 100644 test/golden/login_golden_test.dart create mode 100644 test/helpers/golden_test_bootstrap.dart create mode 100644 test/helpers/pump_app.dart create mode 100644 test/helpers/test_keys.dart diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml new file mode 100644 index 00000000..f7f12d09 --- /dev/null +++ b/.github/workflows/testing.yml @@ -0,0 +1,45 @@ +name: testing + +on: + pull_request: + push: + branches: + - main + - 'release/**' + +jobs: + flutter: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: subosito/flutter-action@v2 + with: + channel: stable + - run: flutter pub get + - run: flutter test + - run: flutter test test/golden + - run: flutter test integration_test + + go: + runs-on: ubuntu-latest + defaults: + run: + working-directory: go_service + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.25' + - run: go test ./... + + patrol: + if: startsWith(github.ref, 'refs/heads/release/') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: subosito/flutter-action@v2 + with: + channel: stable + - run: flutter pub get + - run: dart pub global activate patrol_cli + - run: patrol test diff --git a/AGENTS.md b/AGENTS.md index bfa65c3d..8331c7ee 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -119,4 +119,17 @@ A refactor task is complete only when: - Any new macOS or iOS entitlement must be least-privilege, justified by the feature, and covered by tests or manual verification notes. - Auth, secret, network, or entitlement changes require `flutter analyze`, relevant unit/widget tests, and serial device-run integration tests when integration coverage is needed. +## Testing Rules + +- Modify any Flutter UI page, and you must add or update widget tests and golden tests. +- Modify any core business flow, and you must add or update `integration_test`. +- Modify permission, camera, file picker, notification, WebView, or native page interaction behavior, and you must add or update Patrol coverage. +- Modify any Go handler, service, or repository, and you must add or update matching `*_test.go` unit tests. +- All UI tests must use `Key`-based locators first. Avoid fragile text-only or hierarchy-only selectors unless no Key exists yet. +- Release/* branches must run the full chain: `flutter test`, `flutter test test/golden`, `flutter test integration_test`, `patrol test`, and `go test ./...`. +- New features must follow test first, then implementation, then full regression. +- Keep tests split by module. Do not pile every scenario into one file. +- Golden baseline refreshes require UI review confirmation before updating reference images. +- CI failures must be fixed in tests or implementation. Do not skip the failing check in merge workflows. + See [docs/security/secure-development-rules.md](/Users/shenlan/workspaces/cloud-neutral-toolkit/XWorkmate.svc.plus/docs/security/secure-development-rules.md) for the full checklist. diff --git a/agent.md b/agent.md new file mode 100644 index 00000000..3363c4fe --- /dev/null +++ b/agent.md @@ -0,0 +1,10 @@ +# Agent Rules + +- Add or update widget tests and golden tests for any Flutter UI page change. +- Add or update integration tests for any core business flow change. +- Add or update Patrol tests for permission, camera, file picker, notification, WebView, or native page interaction changes. +- Add or update Go `*_test.go` coverage for any handler, service, or repository change. +- Prefer `Key`-based locators for all UI automation. +- Keep tests modular and split by feature. +- Do not update golden baselines without UI review confirmation. +- Fix failing tests or implementation directly; do not skip CI failures. diff --git a/docs/README_TESTING.md b/docs/README_TESTING.md new file mode 100644 index 00000000..22b7c91a --- /dev/null +++ b/docs/README_TESTING.md @@ -0,0 +1,43 @@ +# Testing Guide + +## Flutter + +Run unit and widget tests: + +```bash +flutter test +``` + +Run golden tests: + +```bash +flutter test test/golden +``` + +Run integration tests: + +```bash +flutter test integration_test +``` + +## Patrol + +Run Patrol tests: + +```bash +patrol test +``` + +## Go + +Run Go unit tests: + +```bash +cd go_service +go test ./... +``` + +## CI Coverage + +- Pull requests run Flutter tests, golden tests, integration tests, and Go tests. +- `release/*` branches run Patrol tests in addition to the PR chain. diff --git a/go_service/go.mod b/go_service/go.mod new file mode 100644 index 00000000..79f6e2c3 --- /dev/null +++ b/go_service/go.mod @@ -0,0 +1,3 @@ +module xworkmate/go_service + +go 1.25.0 diff --git a/go_service/internal/handler/auth_handler.go b/go_service/internal/handler/auth_handler.go new file mode 100644 index 00000000..7a8bbfa9 --- /dev/null +++ b/go_service/internal/handler/auth_handler.go @@ -0,0 +1,29 @@ +package handler + +import ( + "encoding/json" + "net/http" + + "xworkmate/go_service/internal/service" +) + +type AuthHandler struct { + service *service.AuthService +} + +func NewAuthHandler(service *service.AuthService) *AuthHandler { + return &AuthHandler{service: service} +} + +func (h *AuthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if h.service == nil { + http.Error(w, "service unavailable", http.StatusServiceUnavailable) + return + } + token := r.Header.Get("Authorization") + if !h.service.ValidateToken(token) { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + _ = json.NewEncoder(w).Encode(map[string]any{"ok": true}) +} diff --git a/go_service/internal/handler/auth_handler_test.go b/go_service/internal/handler/auth_handler_test.go new file mode 100644 index 00000000..4add628a --- /dev/null +++ b/go_service/internal/handler/auth_handler_test.go @@ -0,0 +1,22 @@ +package handler + +import ( + "net/http" + "net/http/httptest" + "testing" + + "xworkmate/go_service/internal/service" +) + +func TestAuthHandlerServeHTTP(t *testing.T) { + h := NewAuthHandler(service.NewAuthService("secret")) + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set("Authorization", "secret") + rr := httptest.NewRecorder() + + h.ServeHTTP(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", rr.Code) + } +} diff --git a/go_service/internal/service/auth_service.go b/go_service/internal/service/auth_service.go new file mode 100644 index 00000000..3fd46aab --- /dev/null +++ b/go_service/internal/service/auth_service.go @@ -0,0 +1,15 @@ +package service + +import "strings" + +type AuthService struct { + expectedToken string +} + +func NewAuthService(expectedToken string) *AuthService { + return &AuthService{expectedToken: strings.TrimSpace(expectedToken)} +} + +func (s *AuthService) ValidateToken(token string) bool { + return strings.TrimSpace(token) != "" && strings.TrimSpace(token) == s.expectedToken +} diff --git a/go_service/internal/service/auth_service_test.go b/go_service/internal/service/auth_service_test.go new file mode 100644 index 00000000..3e636274 --- /dev/null +++ b/go_service/internal/service/auth_service_test.go @@ -0,0 +1,13 @@ +package service + +import "testing" + +func TestAuthServiceValidateToken(t *testing.T) { + svc := NewAuthService("secret") + if !svc.ValidateToken("secret") { + t.Fatal("expected valid token") + } + if svc.ValidateToken("wrong") { + t.Fatal("expected invalid token") + } +} diff --git a/integration_test/desktop_navigation_flow_test.dart b/integration_test/desktop_navigation_flow_test.dart index b16f50dd..f76a7781 100644 --- a/integration_test/desktop_navigation_flow_test.dart +++ b/integration_test/desktop_navigation_flow_test.dart @@ -57,7 +57,9 @@ void main() { ); await _ensureSettingsFocused(tester); expect( - find.byKey(const ValueKey('assistant-focus-active-title-settings')), + find.byKey( + const ValueKey('assistant-focus-active-title-settings'), + ), findsOneWidget, ); diff --git a/integration_test/desktop_settings_flow_test.dart b/integration_test/desktop_settings_flow_test.dart index 9b49c1ec..8f9772c1 100644 --- a/integration_test/desktop_settings_flow_test.dart +++ b/integration_test/desktop_settings_flow_test.dart @@ -46,22 +46,24 @@ void main() { (WidgetTester tester) async { await pumpDesktopApp(tester); - await tester.tap( - find.byKey(const Key('assistant-side-pane-tab-navigation')), - ); - await settleIntegrationUi(tester); - expect( - find.byKey(const Key('assistant-focus-panel-title')), - findsOneWidget, - ); - await _ensureSettingsFocused(tester); - expect( - find.byKey(const ValueKey('assistant-focus-active-title-settings')), - findsOneWidget, - ); + await tester.tap( + find.byKey(const Key('assistant-side-pane-tab-navigation')), + ); + await settleIntegrationUi(tester); + expect( + find.byKey(const Key('assistant-focus-panel-title')), + findsOneWidget, + ); + await _ensureSettingsFocused(tester); + expect( + find.byKey( + const ValueKey('assistant-focus-active-title-settings'), + ), + findsOneWidget, + ); - await tester.pumpWidget(const SizedBox.shrink()); - await settleIntegrationUi(tester); - }, + await tester.pumpWidget(const SizedBox.shrink()); + await settleIntegrationUi(tester); + }, ); } diff --git a/integration_test/home_flow_test.dart b/integration_test/home_flow_test.dart new file mode 100644 index 00000000..9220e6a4 --- /dev/null +++ b/integration_test/home_flow_test.dart @@ -0,0 +1,19 @@ +import 'package:flutter_test/flutter_test.dart'; + +import '../test/helpers/test_keys.dart'; +import 'test_support.dart'; + +void main() { + initializeIntegrationHarness(); + + testWidgets('assistant task flow exposes single agent target', ( + WidgetTester tester, + ) async { + await pumpDesktopApp(tester); + expect(find.byKey(TestKeys.assistantTaskRail), findsOneWidget); + expect(find.byKey(TestKeys.assistantExecutionTargetButton), findsOneWidget); + expect(find.byKey(TestKeys.assistantComposerInput), findsOneWidget); + expect(find.byKey(TestKeys.assistantSubmitButton), findsOneWidget); + expect(find.text('单机智能体'), findsWidgets); + }); +} diff --git a/integration_test/login_flow_test.dart b/integration_test/login_flow_test.dart new file mode 100644 index 00000000..a7ad91e4 --- /dev/null +++ b/integration_test/login_flow_test.dart @@ -0,0 +1,42 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; + +import 'test_support.dart'; + +Map loadDotEnvValues() { + final file = File('.env'); + if (!file.existsSync()) { + return const {}; + } + final values = {}; + for (final rawLine in file.readAsLinesSync()) { + final line = rawLine.trim(); + if (line.isEmpty || line.startsWith('#') || !line.contains('=')) { + continue; + } + final index = line.indexOf('='); + final key = line.substring(0, index).trim(); + var value = line.substring(index + 1).trim(); + if ((value.startsWith("'") && value.endsWith("'")) || + (value.startsWith('"') && value.endsWith('"'))) { + value = value.substring(1, value.length - 1); + } + values[key] = value; + } + return values; +} + +void main() { + initializeIntegrationHarness(); + + testWidgets('loads gateway env values for settings smoke flow', ( + WidgetTester tester, + ) async { + final env = loadDotEnvValues(); + expect(env.containsKey('AI-Gateway-Url'), isTrue); + expect(env.containsKey('AI-Gateway-apiKey'), isTrue); + await pumpDesktopApp(tester); + await settleIntegrationUi(tester); + }); +} diff --git a/lib/features/settings/codex_integration_card.dart b/lib/features/settings/codex_integration_card.dart index 21b34c25..f7e958f5 100644 --- a/lib/features/settings/codex_integration_card.dart +++ b/lib/features/settings/codex_integration_card.dart @@ -120,6 +120,7 @@ class _CodexIntegrationCardState extends State { ), const SizedBox(height: 16), TextField( + key: const ValueKey('codex-cli-path-field'), controller: _pathController, decoration: InputDecoration( labelText: appText('Codex CLI 路径', 'Codex CLI path'), @@ -128,6 +129,7 @@ class _CodexIntegrationCardState extends State { '/opt/homebrew/bin/codex', ), suffixIcon: IconButton( + key: const ValueKey('codex-cli-path-save-button'), onPressed: controller.isCodexBridgeBusy ? null : _savePathOverride, @@ -175,6 +177,7 @@ class _CodexIntegrationCardState extends State { children: [ Expanded( child: FilledButton.icon( + key: const ValueKey('codex-bridge-toggle-button'), onPressed: controller.isCodexBridgeBusy ? null : controller.isCodexBridgeEnabled diff --git a/lib/widgets/section_tabs.dart b/lib/widgets/section_tabs.dart index e5089213..b197cc84 100644 --- a/lib/widgets/section_tabs.dart +++ b/lib/widgets/section_tabs.dart @@ -56,6 +56,7 @@ class SectionTabs extends StatelessWidget { return Padding( padding: const EdgeInsets.only(right: AppSpacing.xxs), child: _SectionTabChip( + key: ValueKey('section-tab-$item'), label: item, selected: selected, padding: padding, @@ -71,6 +72,7 @@ class SectionTabs extends StatelessWidget { class _SectionTabChip extends StatefulWidget { const _SectionTabChip({ + super.key, required this.label, required this.selected, required this.padding, diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index c24cc424..9991ad68 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -10,6 +10,7 @@ import file_selector_macos import irondash_engine_context import mobile_scanner import package_info_plus +import patrol import shared_preferences_foundation import super_native_extensions @@ -19,6 +20,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + PatrolPlugin.register(with: registry.registrar(forPlugin: "PatrolPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SuperNativeExtensionsPlugin.register(with: registry.registrar(forPlugin: "SuperNativeExtensionsPlugin")) } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 967d88ad..d483bc74 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,4 +1,5 @@ PODS: + - CocoaAsyncSocket (7.6.5) - device_info_plus (0.0.1): - FlutterMacOS - file_selector_macos (0.0.1): @@ -10,6 +11,10 @@ PODS: - FlutterMacOS - package_info_plus (0.0.1): - FlutterMacOS + - patrol (0.0.1): + - CocoaAsyncSocket (~> 7.6) + - Flutter + - FlutterMacOS - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS @@ -23,9 +28,14 @@ DEPENDENCIES: - irondash_engine_context (from `Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos`) - mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) + - patrol (from `Flutter/ephemeral/.symlinks/plugins/patrol/darwin`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - super_native_extensions (from `Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos`) +SPEC REPOS: + trunk: + - CocoaAsyncSocket + EXTERNAL SOURCES: device_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos @@ -39,18 +49,22 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos package_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos + patrol: + :path: Flutter/ephemeral/.symlinks/plugins/patrol/darwin shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin super_native_extensions: :path: Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos SPEC CHECKSUMS: + CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76 file_selector_macos: 9e9e068e90ebee155097d00e89ae91edb2374db7 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba mobile_scanner: 0e365ed56cad24f28c0fd858ca04edefb40dfac3 package_info_plus: f0052d280d17aa382b932f399edf32507174e870 + patrol: 5df5d241d7f95f0df12a6906bbf45acb43a1e537 shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 diff --git a/patrol_test/app_test.dart b/patrol_test/app_test.dart new file mode 100644 index 00000000..9a876a31 --- /dev/null +++ b/patrol_test/app_test.dart @@ -0,0 +1,5 @@ +import 'package:patrol/patrol.dart'; + +void main() { + patrolTest('app smoke', ($) async {}); +} diff --git a/patrol_test/camera_test.dart b/patrol_test/camera_test.dart new file mode 100644 index 00000000..642698e4 --- /dev/null +++ b/patrol_test/camera_test.dart @@ -0,0 +1,5 @@ +import 'package:patrol/patrol.dart'; + +void main() { + patrolTest('camera smoke', ($) async {}); +} diff --git a/patrol_test/permission_test.dart b/patrol_test/permission_test.dart new file mode 100644 index 00000000..2889717b --- /dev/null +++ b/patrol_test/permission_test.dart @@ -0,0 +1,5 @@ +import 'package:patrol/patrol.dart'; + +void main() { + patrolTest('permission smoke', ($) async {}); +} diff --git a/pubspec.lock b/pubspec.lock index 9aff188c..bfddbed2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -113,6 +113,22 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.3" + dispose_scope: + dependency: transitive + description: + name: dispose_scope + sha256: "48ec38ca2631c53c4f8fa96b294c801e55c335db5e3fb9f82cede150cfe5a2af" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + equatable: + dependency: transitive + description: + name: equatable + sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" + url: "https://pub.dev" + source: hosted + version: "2.0.8" fake_async: dependency: transitive description: @@ -271,6 +287,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" + golden_toolkit: + dependency: "direct dev" + description: + name: golden_toolkit + sha256: "8f74adab33154fe7b731395782797021f97d2edc52f7bfb85ff4f1b5c4a215f0" + url: "https://pub.dev" + source: hosted + version: "0.15.0" hooks: dependency: transitive description: @@ -332,6 +356,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 + url: "https://pub.dev" + source: hosted + version: "4.11.0" leak_tracker: dependency: transitive description: @@ -507,6 +539,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + patrol: + dependency: "direct dev" + description: + name: patrol + sha256: "32fd0709f3871fa56eb9cd88410e3ca816bfa757122bae806a0f842188acb820" + url: "https://pub.dev" + source: hosted + version: "3.20.0" + patrol_finders: + dependency: transitive + description: + name: patrol_finders + sha256: "4a658d7d560de523f92deb3fa3326c78747ca0bf7e7f4b8788c012463138b628" + url: "https://pub.dev" + source: hosted + version: "2.9.0" + patrol_log: + dependency: transitive + description: + name: patrol_log + sha256: "9fed4143980df1e3bbcfa00d0b443c7d68f04f9132317b7698bbc37f8a5a58c5" + url: "https://pub.dev" + source: hosted + version: "0.5.0" pixel_snap: dependency: transitive description: @@ -603,6 +659,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.1" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" sky_engine: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 540ff38d..44d72978 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,6 +37,8 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter + golden_toolkit: ^0.15.0 + patrol: ^3.13.0 flutter_lints: ^6.0.0 dependency_overrides: diff --git a/test/features/assistant_page_single_agent_flow_suite.dart b/test/features/assistant_page_single_agent_flow_suite.dart new file mode 100644 index 00000000..2051702b --- /dev/null +++ b/test/features/assistant_page_single_agent_flow_suite.dart @@ -0,0 +1,115 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:xworkmate/app/app_controller.dart'; +import 'package:xworkmate/features/assistant/assistant_page.dart'; +import 'package:xworkmate/runtime/runtime_models.dart'; + +import '../test_support.dart'; + +void main() { + testWidgets( + 'AssistantPage single agent can be selected and receive streaming reply', + (WidgetTester tester) async { + final server = await _ChatServer.start(); + addTearDown(server.close); + + final controller = await createTestController(tester); + await controller.saveSettings( + controller.settings.copyWith( + aiGateway: controller.settings.aiGateway.copyWith( + baseUrl: server.baseUri.toString(), + availableModels: const ['codex-chat'], + selectedModels: const ['codex-chat'], + ), + defaultModel: 'codex-chat', + ), + ); + await controller.settingsController.saveAiGatewayApiKey('test-key'); + await controller.setAssistantExecutionTarget( + AssistantExecutionTarget.singleAgent, + ); + + await pumpPage( + tester, + child: AssistantPage(controller: controller, onOpenDetail: (_) {}), + ); + + final targetButton = find.byKey( + const ValueKey('assistant-execution-target-button'), + ); + await tester.tap(targetButton); + await tester.pumpAndSettle(); + await tester.tap(find.text('单机智能体').last); + await tester.pumpAndSettle(); + + expect(find.text('单机智能体'), findsWidgets); + + await tester.enterText( + find.byKey(const ValueKey('assistant-composer-input-area')), + 'hello codex', + ); + await tester.tap( + find.byKey(const ValueKey('assistant-submit-button')), + ); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 200)); + await tester.pumpAndSettle(); + + expect(find.textContaining('CODEX_REPLY'), findsWidgets); + expect(server.requestCount, greaterThanOrEqualTo(1)); + expect(controller.chatMessages.any((m) => m.text.contains('hello codex')), + isTrue); + }, + ); +} + +class _ChatServer { + _ChatServer._(this._server); + + final HttpServer _server; + int requestCount = 0; + + Uri get baseUri => Uri.parse('http://127.0.0.1:${_server.port}'); + + static Future<_ChatServer> start() async { + final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); + final fake = _ChatServer._(server); + unawaited(fake._listen()); + return fake; + } + + Future close() async { + await _server.close(force: true); + } + + Future _listen() async { + await for (final request in _server) { + requestCount += 1; + if (request.uri.path != '/v1/chat/completions') { + request.response.statusCode = HttpStatus.notFound; + await request.response.close(); + continue; + } + final response = { + 'id': 'chatcmpl-test', + 'choices': >[ + { + 'index': 0, + 'delta': {'content': 'CODEX_REPLY'}, + 'finish_reason': 'stop', + }, + ], + }; + request.response.headers.set( + HttpHeaders.contentTypeHeader, + 'text/event-stream; charset=utf-8', + ); + request.response.write('data: ${jsonEncode(response)}\n\n'); + await request.response.close(); + } + } +} diff --git a/test/features/settings_page_external_acp_end_to_end_suite.dart b/test/features/settings_page_external_acp_end_to_end_suite.dart new file mode 100644 index 00000000..f409daa1 --- /dev/null +++ b/test/features/settings_page_external_acp_end_to_end_suite.dart @@ -0,0 +1,117 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:xworkmate/app/app_controller.dart'; +import 'package:xworkmate/features/settings/settings_page.dart'; +import 'package:xworkmate/models/app_models.dart'; +import 'package:xworkmate/runtime/runtime_models_profiles.dart'; + +import '../test_support.dart'; + +void main() { + testWidgets('SettingsPage Codex external ACP can test and save', ( + WidgetTester tester, + ) async { + final server = await _AcpServer.start(); + addTearDown(server.close); + + final controller = await createTestController(tester); + await controller.saveSettings( + controller.settings.copyWith( + externalAcpEndpoints: [ + const ExternalAcpEndpointProfile( + providerKey: 'codex', + label: 'Codex', + badge: 'C', + endpoint: '', + authRef: '', + enabled: true, + ), + ...controller.settings.externalAcpEndpoints.skip(1), + ], + ), + ); + + await pumpPage( + tester, + child: SettingsPage(controller: controller, initialTab: SettingsTab.gateway), + ); + + final endpointField = find.byKey( + const ValueKey('external-acp-endpoint-Codex'), + ); + final testButton = find.byKey( + const ValueKey('external-acp-test-Codex'), + ); + final saveButton = find.byKey( + const ValueKey('external-acp-apply-Codex'), + ); + + expect(endpointField, findsOneWidget); + await tester.enterText(endpointField, server.baseUri.toString()); + await tester.pumpAndSettle(); + + await tester.tap(testButton); + await tester.pumpAndSettle(); + + expect(find.textContaining('连接成功'), findsOneWidget); + + await tester.tap(saveButton); + await tester.pumpAndSettle(); + + final saved = controller.settings.externalAcpEndpointForProviderId('codex'); + expect(saved?.endpoint, server.baseUri.toString()); + expect(server.requestCount, greaterThanOrEqualTo(1)); + }); +} + +class _AcpServer { + _AcpServer._(this._server); + + final HttpServer _server; + int requestCount = 0; + + Uri get baseUri => Uri.parse('http://127.0.0.1:${_server.port}'); + + static Future<_AcpServer> start() async { + final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); + final fake = _AcpServer._(server); + unawaited(fake._listen()); + return fake; + } + + Future close() async { + await _server.close(force: true); + } + + Future _listen() async { + await for (final request in _server) { + requestCount += 1; + if (request.uri.path != '/acp/rpc') { + request.response.statusCode = HttpStatus.notFound; + await request.response.close(); + continue; + } + final body = await utf8.decoder.bind(request).join(); + final decoded = jsonDecode(body) as Map; + final response = { + 'jsonrpc': '2.0', + 'id': decoded['id'], + 'result': { + 'singleAgent': true, + 'multiAgent': true, + 'providers': ['codex'], + }, + }; + request.response.headers.set( + HttpHeaders.contentTypeHeader, + 'text/event-stream; charset=utf-8', + ); + request.response.write('data: ${jsonEncode(response)}\n\n'); + await request.response.close(); + } + } +} diff --git a/test/golden/goldens/assistant_home_shell.png b/test/golden/goldens/assistant_home_shell.png new file mode 100644 index 0000000000000000000000000000000000000000..d019e25a3dba6cb559e84bcc9cae6996da923601 GIT binary patch literal 47977 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 Jp?>G_e*k&VGLrxR literal 0 HcmV?d00001 diff --git a/test/golden/goldens/settings_integrations_shell.png b/test/golden/goldens/settings_integrations_shell.png new file mode 100644 index 0000000000000000000000000000000000000000..0a1d5db19bf987e99e8e8f5a91c0aff272e0fe3d GIT binary patch literal 28492 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 loadGoldenFonts() async { + await loadAppFonts(); +} + +Widget buildGoldenApp(Widget child) { + return MaterialApp( + debugShowCheckedModeBanner: false, + supportedLocales: const [Locale('zh'), Locale('en')], + localizationsDelegates: GlobalMaterialLocalizations.delegates, + theme: AppTheme.light(), + darkTheme: AppTheme.dark(), + home: Scaffold(body: child), + ); +} + +Future pumpGoldenApp( + WidgetTester tester, + Widget child, { + Size size = const Size(1440, 960), +}) async { + tester.view.devicePixelRatio = 1; + tester.view.physicalSize = size; + addTearDown(() { + tester.view.resetPhysicalSize(); + tester.view.resetDevicePixelRatio(); + }); + await tester.pumpWidget(buildGoldenApp(child)); + await tester.pumpAndSettle(); +} diff --git a/test/helpers/pump_app.dart b/test/helpers/pump_app.dart new file mode 100644 index 00000000..69b6a581 --- /dev/null +++ b/test/helpers/pump_app.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:xworkmate/app/app.dart'; + +Future pumpXWorkmateApp( + WidgetTester tester, { + Size size = const Size(1600, 1000), +}) async { + tester.view.devicePixelRatio = 1; + tester.view.physicalSize = size; + addTearDown(() { + tester.view.resetPhysicalSize(); + tester.view.resetDevicePixelRatio(); + }); + await tester.pumpWidget(const XWorkmateApp()); + await tester.pumpAndSettle(); +} diff --git a/test/helpers/test_keys.dart b/test/helpers/test_keys.dart new file mode 100644 index 00000000..0f11a35e --- /dev/null +++ b/test/helpers/test_keys.dart @@ -0,0 +1,34 @@ +import 'package:flutter/widgets.dart'; + +class TestKeys { + const TestKeys._(); + + static const Key settingsGatewayTab = Key('sidebar-settings-tab-gateway'); + static const Key settingsIntegrationsTab = Key('section-tab-ACP 外部接入'); + static const Key settingsGatewayIntegrationTab = Key( + 'section-tab-OpenClaw Gateway', + ); + static const Key settingsExternalAcpProvider = Key('external-acp-card-Codex'); + static const Key settingsExternalAcpEndpoint = Key( + 'external-acp-endpoint-Codex', + ); + static const Key settingsExternalAcpAuth = Key('external-acp-auth-Codex'); + static const Key settingsExternalAcpTest = Key('external-acp-test-Codex'); + static const Key settingsExternalAcpSave = Key('external-acp-apply-Codex'); + + static const Key assistantTaskRail = Key('assistant-task-rail'); + static const Key assistantExecutionTargetButton = Key( + 'assistant-execution-target-button', + ); + static const Key assistantSingleAgentProviderButton = Key( + 'assistant-single-agent-provider-button', + ); + static const Key assistantComposerInput = Key( + 'assistant-composer-input-area', + ); + static const Key assistantSubmitButton = Key('assistant-submit-button'); + static const Key assistantNewTaskButton = Key('assistant-new-task-button'); + static const Key assistantTaskItemMain = ValueKey( + 'assistant-task-item-main', + ); +}