From 71590da541e32a6596924a316fcb40f2ce9fad1f Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Fri, 10 Apr 2026 10:18:46 +0800 Subject: [PATCH] test: add real env login chain checks --- Makefile | 8 +- integration_test/login_flow_test.dart | 250 +++++++++++++++++++++- scripts/check-xworkmate-bridge-service.sh | 75 +++++++ 3 files changed, 322 insertions(+), 11 deletions(-) create mode 100755 scripts/check-xworkmate-bridge-service.sh diff --git a/Makefile b/Makefile index b1679fae..9dedac7e 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ APP_BUILD_NUMBER := $(if $(APP_BUILD_NUMBER_RAW),$(APP_BUILD_NUMBER_RAW),1) APP_DART_DEFINE_VERSION ?= --dart-define=XWORKMATE_DISPLAY_VERSION=$(APP_VERSION) APP_DART_DEFINE_BUILD ?= --dart-define=XWORKMATE_BUILD_NUMBER=$(APP_BUILD_NUMBER) -.PHONY: help deps analyze test test-all test-flutter test-golden test-integration test-integration-macos test-patrol test-go test-ci check format run open-macos-xcode sync-version build-linux build-macos build-ios-sim package-deb package-rpm package-linux package-mac install-mac clean build-go-core render-release-docs check-export-compliance +.PHONY: help deps analyze test test-all test-flutter test-golden test-integration test-integration-macos test-patrol test-go test-ci check format run open-macos-xcode sync-version build-linux build-macos build-ios-sim package-deb package-rpm package-linux package-mac install-mac clean build-go-core render-release-docs check-export-compliance test-real-env-login-chain inspect-xworkmate-bridge-service help: ## Show available targets @grep -E '^[a-zA-Z0-9_.-]+:.*?## ' Makefile | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "%-18s %s\n", $$1, $$2}' @@ -43,6 +43,12 @@ test-integration-macos: ## Run macOS integration tests serially for the desktop $(FLUTTER) test integration_test/desktop_navigation_flow_test.dart -d macos $(FLUTTER) test integration_test/desktop_settings_flow_test.dart -d macos +test-real-env-login-chain: ## Run the real-env login/sync integration chain on macOS + $(FLUTTER) test integration_test/login_flow_test.dart -d macos + +inspect-xworkmate-bridge-service: ## Read-only SSH inspection for xworkmate-bridge.svc.plus service + bash scripts/check-xworkmate-bridge-service.sh + test-patrol: ## Run Patrol end-to-end tests dart pub global activate patrol_cli patrol test diff --git a/integration_test/login_flow_test.dart b/integration_test/login_flow_test.dart index a7ad91e4..108d0f24 100644 --- a/integration_test/login_flow_test.dart +++ b/integration_test/login_flow_test.dart @@ -1,10 +1,73 @@ import 'dart:io'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import '../test/helpers/test_keys.dart'; import 'test_support.dart'; -Map loadDotEnvValues() { +class _RealEnvConfig { + const _RealEnvConfig({ + required this.accountBaseUrl, + required this.accountIdentifier, + required this.accountPassword, + required this.expectedRemoteHost, + required this.enableGatewayConnectionCheck, + }); + + final String accountBaseUrl; + final String accountIdentifier; + final String accountPassword; + final String expectedRemoteHost; + final bool enableGatewayConnectionCheck; + + static _RealEnvConfig? load() { + final env = _loadMergedEnv(); + final accountBaseUrl = _readEnv(env, [ + 'XWORKMATE_TEST_ACCOUNT_BASE_URL', + 'ACCOUNTS_SVC_PLUS_URL', + 'XWORKMATE_ACCOUNT_BASE_URL', + ], fallback: 'https://accounts.svc.plus'); + final accountIdentifier = _readEnv(env, [ + 'XWORKMATE_TEST_ACCOUNT_IDENTIFIER', + 'XWORKMATE_TEST_LOGIN_NAME', + 'XWORKMATE_LOGIN_NAME', + 'LOGIN_NAME', + ]); + final accountPassword = _readEnv(env, [ + 'XWORKMATE_TEST_ACCOUNT_PASSWORD', + 'XWORKMATE_TEST_LOGIN_PASSWORD', + 'XWORKMATE_LOGIN_PASSWORD', + 'LOGIN_PASSWORD', + ]); + if (accountIdentifier.isEmpty || accountPassword.isEmpty) { + return null; + } + + final expectedRemoteHost = _readEnv(env, [ + 'XWORKMATE_TEST_EXPECT_REMOTE_HOST', + 'XWORKMATE_TEST_GATEWAY_REMOTE_HOST', + 'OPENCLAW_REMOTE_HOST', + ], fallback: 'openclaw.svc.plus'); + return _RealEnvConfig( + accountBaseUrl: accountBaseUrl, + accountIdentifier: accountIdentifier, + accountPassword: accountPassword, + expectedRemoteHost: expectedRemoteHost, + enableGatewayConnectionCheck: _readBoolEnv(env, [ + 'XWORKMATE_TEST_ENABLE_GATEWAY_CONNECTION_CHECK', + 'XWORKMATE_TEST_GATEWAY_CONNECT', + ]), + ); + } +} + +Map _loadMergedEnv() { + final fileValues = _loadDotEnvValues(); + return {...fileValues, ...Platform.environment}; +} + +Map _loadDotEnvValues() { final file = File('.env'); if (!file.existsSync()) { return const {}; @@ -22,21 +85,188 @@ Map loadDotEnvValues() { (value.startsWith('"') && value.endsWith('"'))) { value = value.substring(1, value.length - 1); } - values[key] = value; + if (key.isNotEmpty) { + values[key] = value; + } } return values; } +String _readEnv( + Map env, + List keys, { + String fallback = '', +}) { + for (final key in keys) { + final value = env[key]?.trim() ?? ''; + if (value.isNotEmpty) { + return value; + } + } + return fallback; +} + +bool _readBoolEnv(Map env, List keys) { + final value = _readEnv(env, keys).toLowerCase(); + return value == '1' || value == 'true' || value == 'yes' || value == 'on'; +} + +Future _waitForCondition( + WidgetTester tester, + bool Function() predicate, { + Duration timeout = const Duration(seconds: 20), + Duration step = const Duration(milliseconds: 250), + String label = 'condition', +}) async { + final maxIterations = timeout.inMilliseconds ~/ step.inMilliseconds; + for (var i = 0; i < maxIterations; i += 1) { + await tester.pump(step); + if (predicate()) { + return; + } + } + throw TestFailure('Timed out waiting for $label'); +} + +Future _openIntegrationsSettings(WidgetTester tester) async { + await tester.tap(find.byKey(TestKeys.sidebarFooterSettings)); + await settleIntegrationUi(tester); + await tester.tap(find.byKey(TestKeys.settingsIntegrationsTab)); + await settleIntegrationUi(tester); +} + +Future _openGatewaySettings(WidgetTester tester) async { + await tester.tap(find.byKey(TestKeys.settingsGatewayTab)); + await settleIntegrationUi(tester); +} + 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); + setUp(() async { + await resetIntegrationPreferences(); }); + + testWidgets( + 'real env login chain signs in, syncs remote defaults, and exposes remote gateway profile', + (WidgetTester tester) async { + final config = _RealEnvConfig.load(); + if (config == null) { + print( + 'Skipping real env login chain test: set ' + 'XWORKMATE_TEST_ACCOUNT_IDENTIFIER/XWORKMATE_TEST_ACCOUNT_PASSWORD ' + 'or LOGIN_NAME/LOGIN_PASSWORD in the environment or .env.', + ); + return; + } + + await pumpDesktopApp(tester); + await waitForIntegrationFinder( + tester, + find.byKey(TestKeys.assistantConversationShell), + ); + await _openIntegrationsSettings(tester); + + await tester.enterText( + find.byKey(const ValueKey('account-base-url-field')), + config.accountBaseUrl, + ); + await tester.enterText( + find.byKey(const ValueKey('account-username-field')), + config.accountIdentifier, + ); + await tester.enterText( + find.byKey(const ValueKey('account-password-field')), + config.accountPassword, + ); + await settleIntegrationUi(tester); + + await tester.tap(find.byKey(const ValueKey('account-login-button'))); + await settleIntegrationUi(tester); + + await _waitForCondition( + tester, + () => + find + .byKey(const ValueKey('account-sync-button')) + .evaluate() + .isNotEmpty || + find + .byKey(const ValueKey('account-verify-mfa-button')) + .evaluate() + .isNotEmpty, + label: 'account sign-in state', + ); + + expect( + find.byKey(const ValueKey('account-verify-mfa-button')), + findsNothing, + reason: 'This real-env chain currently expects a non-MFA test account.', + ); + expect(find.byKey(const ValueKey('account-sync-button')), findsOneWidget); + + final sessionStatus = tester.widget( + find.byKey(const ValueKey('account-session-status')), + ); + final syncStatus = tester.widget( + find.byKey(const ValueKey('account-sync-status')), + ); + expect(sessionStatus.data ?? '', contains('Signed in')); + expect(syncStatus.data ?? '', contains('ready')); + + final lastSyncFinder = find.byKey( + const ValueKey('acp-bridge-cloud-last-sync'), + ); + if (lastSyncFinder.evaluate().isNotEmpty) { + final lastSync = tester.widget(lastSyncFinder); + expect(lastSync.data ?? '', isNot(contains('Not synced yet'))); + } + + await _openGatewaySettings(tester); + await tester.tap(find.byKey(const ValueKey('gateway-profile-chip-1'))); + await settleIntegrationUi(tester); + + final gatewayHostField = tester.widget( + find.byKey(const ValueKey('gateway-host-field')), + ); + final resolvedGatewayHost = + gatewayHostField.controller?.text.trim() ?? ''; + expect(resolvedGatewayHost, isNotEmpty); + expect(resolvedGatewayHost, contains(config.expectedRemoteHost)); + + if (config.enableGatewayConnectionCheck) { + await tester.tap(find.byKey(const ValueKey('gateway-test-button'))); + await settleIntegrationUi(tester); + await _waitForCondition( + tester, + () => + find + .textContaining('Connection succeeded') + .evaluate() + .isNotEmpty || + find.textContaining('连ζŽ₯成功').evaluate().isNotEmpty || + find.textContaining('pairing required').evaluate().isNotEmpty || + find.textContaining('PAIRING_REQUIRED').evaluate().isNotEmpty, + timeout: const Duration(seconds: 30), + label: 'gateway test result', + ); + } + + await tester.tap( + find.byKey(const ValueKey('workspace-breadcrumb-0')), + ); + await settleIntegrationUi(tester); + await waitForIntegrationFinder( + tester, + find.byKey(TestKeys.assistantConversationShell), + ); + + await switchNewConversationExecutionTargetForIntegration( + tester, + find.byKey(TestKeys.assistantExecutionTargetMenuItemRemote), + ); + + expect(find.textContaining(config.expectedRemoteHost), findsWidgets); + }, + ); } diff --git a/scripts/check-xworkmate-bridge-service.sh b/scripts/check-xworkmate-bridge-service.sh new file mode 100755 index 00000000..331808ff --- /dev/null +++ b/scripts/check-xworkmate-bridge-service.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ -f .env ]]; then + # shellcheck disable=SC1091 + set -a && source ./.env && set +a +fi + +SSH_TARGET="${XWORKMATE_TEST_SSH_TARGET:-root@p-xhttp-contabo.svc.plus}" +BRIDGE_SERVICE="${XWORKMATE_TEST_BRIDGE_SERVICE:-xworkmate-bridge.svc.plus}" +SSH_BIN="${SSH_BIN:-ssh}" +SSH_CONNECT_TIMEOUT="${XWORKMATE_TEST_SSH_CONNECT_TIMEOUT:-8}" +SSH_EXTRA_OPTS="${XWORKMATE_TEST_SSH_OPTS:-}" +JOURNAL_LINES="${XWORKMATE_TEST_BRIDGE_JOURNAL_LINES:-80}" + +echo "==> Inspecting ${BRIDGE_SERVICE} on ${SSH_TARGET}" + +# shellcheck disable=SC2086 +"${SSH_BIN}" \ + -o BatchMode=yes \ + -o ConnectTimeout="${SSH_CONNECT_TIMEOUT}" \ + ${SSH_EXTRA_OPTS} \ + "${SSH_TARGET}" bash -s -- "${BRIDGE_SERVICE}" "${JOURNAL_LINES}" <<'REMOTE' +set -euo pipefail + +service_name="${1}" +journal_lines="${2}" + +echo "## Access" +echo "host=$(hostname -f 2>/dev/null || hostname)" +echo "time=$(date -Is)" +echo "kernel=$(uname -srmo)" +echo + +echo "## System" +systemctl is-system-running || true +echo + +echo "## Service Summary" +systemctl show "${service_name}" \ + --property=Id \ + --property=Description \ + --property=LoadState \ + --property=ActiveState \ + --property=SubState \ + --property=UnitFileState \ + --property=FragmentPath \ + --property=ExecMainPID \ + --property=ExecMainStartTimestamp \ + --property=MemoryCurrent \ + --property=TasksCurrent \ + --property=User \ + --property=Group || true +echo + +echo "## Service Status" +systemctl status "${service_name}" --no-pager --full || true +echo + +echo "## Recent Journal" +journalctl -u "${service_name}" -n "${journal_lines}" --no-pager || true +echo + +echo "## Listening Ports" +ss -ltnp | grep -E 'LISTEN|4317|4318|8080|8787|18789' || true +echo + +echo "## Process Snapshot" +main_pid="$(systemctl show "${service_name}" --property=ExecMainPID --value 2>/dev/null || true)" +if [[ -n "${main_pid}" && "${main_pid}" != "0" ]]; then + ps -p "${main_pid}" -o pid,ppid,user,%cpu,%mem,etime,command || true +else + echo "main process not running" +fi +REMOTE