merge: bring in login chain env test
This commit is contained in:
commit
22a5bd9f7a
8
Makefile
8
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
|
||||
|
||||
@ -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);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
75
scripts/check-xworkmate-bridge-service.sh
Executable file
75
scripts/check-xworkmate-bridge-service.sh
Executable 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
|
||||
Loading…
Reference in New Issue
Block a user