merge: bring in login chain env test

This commit is contained in:
Haitao Pan 2026-04-10 10:18:52 +08:00
commit 22a5bd9f7a
3 changed files with 322 additions and 11 deletions

View File

@ -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

View File

@ -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<String, String> 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, <String>[
'XWORKMATE_TEST_ACCOUNT_BASE_URL',
'ACCOUNTS_SVC_PLUS_URL',
'XWORKMATE_ACCOUNT_BASE_URL',
], fallback: 'https://accounts.svc.plus');
final accountIdentifier = _readEnv(env, <String>[
'XWORKMATE_TEST_ACCOUNT_IDENTIFIER',
'XWORKMATE_TEST_LOGIN_NAME',
'XWORKMATE_LOGIN_NAME',
'LOGIN_NAME',
]);
final accountPassword = _readEnv(env, <String>[
'XWORKMATE_TEST_ACCOUNT_PASSWORD',
'XWORKMATE_TEST_LOGIN_PASSWORD',
'XWORKMATE_LOGIN_PASSWORD',
'LOGIN_PASSWORD',
]);
if (accountIdentifier.isEmpty || accountPassword.isEmpty) {
return null;
}
final expectedRemoteHost = _readEnv(env, <String>[
'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, <String>[
'XWORKMATE_TEST_ENABLE_GATEWAY_CONNECTION_CHECK',
'XWORKMATE_TEST_GATEWAY_CONNECT',
]),
);
}
}
Map<String, String> _loadMergedEnv() {
final fileValues = _loadDotEnvValues();
return <String, String>{...fileValues, ...Platform.environment};
}
Map<String, String> _loadDotEnvValues() {
final file = File('.env');
if (!file.existsSync()) {
return const <String, String>{};
@ -22,21 +85,188 @@ Map<String, String> 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<String, String> env,
List<String> keys, {
String fallback = '',
}) {
for (final key in keys) {
final value = env[key]?.trim() ?? '';
if (value.isNotEmpty) {
return value;
}
}
return fallback;
}
bool _readBoolEnv(Map<String, String> env, List<String> keys) {
final value = _readEnv(env, keys).toLowerCase();
return value == '1' || value == 'true' || value == 'yes' || value == 'on';
}
Future<void> _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<void> _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<void> _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<Text>(
find.byKey(const ValueKey('account-session-status')),
);
final syncStatus = tester.widget<Text>(
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<Text>(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<TextField>(
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<String>('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);
},
);
}

View File

@ -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