merge: consolidate CI and thread binding branch work
This commit is contained in:
parent
31ad01646c
commit
06117e9f0c
25
.github/workflows/build-and-release.yml
vendored
25
.github/workflows/build-and-release.yml
vendored
@ -4,13 +4,12 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
- "release/**"
|
||||||
tags:
|
tags:
|
||||||
- "v*"
|
- "v*"
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- "README.md"
|
- "README.md"
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
paths:
|
||||||
- "lib/**"
|
- "lib/**"
|
||||||
- "assets/**"
|
- "assets/**"
|
||||||
@ -39,6 +38,7 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
prepare:
|
prepare:
|
||||||
|
if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main' || (github.event_name == 'pull_request' && github.base_ref == 'main') }}
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
needs:
|
needs:
|
||||||
- verify
|
- verify
|
||||||
@ -85,11 +85,28 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: bash ./scripts/ci/setup_platform_deps.sh linux
|
run: bash ./scripts/ci/setup_platform_deps.sh linux
|
||||||
|
|
||||||
- name: Run analysis and tests
|
- name: Run Flutter verification suite
|
||||||
shell: bash
|
shell: bash
|
||||||
run: bash ./scripts/ci/run_code_analysis.sh
|
run: bash ./scripts/ci/run_flutter_ci_suite.sh
|
||||||
|
|
||||||
|
patrol:
|
||||||
|
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/heads/release/') }}
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout source
|
||||||
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||||
|
|
||||||
|
- name: Set up Flutter SDK
|
||||||
|
uses: ./.github/actions/setup-flutter-sdk
|
||||||
|
with:
|
||||||
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
|
|
||||||
|
- name: Run Patrol suite
|
||||||
|
shell: bash
|
||||||
|
run: bash ./scripts/ci/run_patrol_suite.sh
|
||||||
|
|
||||||
build:
|
build:
|
||||||
|
if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main' || (github.event_name == 'pull_request' && github.base_ref == 'main') }}
|
||||||
name: Build ${{ matrix.platform }} ${{ matrix.package }}
|
name: Build ${{ matrix.platform }} ${{ matrix.package }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
|||||||
33
.github/workflows/testing.yml
vendored
33
.github/workflows/testing.yml
vendored
@ -1,33 +0,0 @@
|
|||||||
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
|
|
||||||
|
|
||||||
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
|
|
||||||
@ -45,6 +45,41 @@ import 'app_controller_desktop_thread_storage.dart';
|
|||||||
import 'app_controller_desktop_skill_permissions.dart';
|
import 'app_controller_desktop_skill_permissions.dart';
|
||||||
import 'app_controller_desktop_runtime_helpers.dart';
|
import 'app_controller_desktop_runtime_helpers.dart';
|
||||||
|
|
||||||
|
class DesktopThreadBindingSnapshotInternal {
|
||||||
|
const DesktopThreadBindingSnapshotInternal({
|
||||||
|
required this.executionTarget,
|
||||||
|
required this.singleAgentProvider,
|
||||||
|
required this.record,
|
||||||
|
});
|
||||||
|
|
||||||
|
final AssistantExecutionTarget executionTarget;
|
||||||
|
final SingleAgentProvider singleAgentProvider;
|
||||||
|
final TaskThread? record;
|
||||||
|
}
|
||||||
|
|
||||||
|
DesktopThreadBindingSnapshotInternal
|
||||||
|
resolveDesktopThreadBindingSnapshotInternal({
|
||||||
|
required AssistantExecutionTarget defaultExecutionTarget,
|
||||||
|
AssistantExecutionTarget? executionTargetOverride,
|
||||||
|
TaskThread? latestRecord,
|
||||||
|
}) {
|
||||||
|
final resolvedExecutionTarget =
|
||||||
|
executionTargetOverride ??
|
||||||
|
(latestRecord == null
|
||||||
|
? defaultExecutionTarget
|
||||||
|
: assistantExecutionTargetFromExecutionMode(
|
||||||
|
latestRecord.executionBinding.executionMode,
|
||||||
|
));
|
||||||
|
final resolvedProvider = SingleAgentProviderCopy.fromJsonValue(
|
||||||
|
latestRecord?.executionBinding.providerId ?? '',
|
||||||
|
);
|
||||||
|
return DesktopThreadBindingSnapshotInternal(
|
||||||
|
executionTarget: resolvedExecutionTarget,
|
||||||
|
singleAgentProvider: resolvedProvider,
|
||||||
|
record: latestRecord,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
extension AppControllerDesktopThreadBinding on AppController {
|
extension AppControllerDesktopThreadBinding on AppController {
|
||||||
String managedLocalThreadWorkspaceSuffixInternal(String sessionKey) =>
|
String managedLocalThreadWorkspaceSuffixInternal(String sessionKey) =>
|
||||||
'/.xworkmate/threads/${threadWorkspaceDirectoryNameInternal(sessionKey)}';
|
'/.xworkmate/threads/${threadWorkspaceDirectoryNameInternal(sessionKey)}';
|
||||||
@ -161,20 +196,6 @@ extension AppControllerDesktopThreadBinding on AppController {
|
|||||||
required ThreadOwnerScope ownerScope,
|
required ThreadOwnerScope ownerScope,
|
||||||
WorkspaceBinding? existingBinding,
|
WorkspaceBinding? existingBinding,
|
||||||
}) {
|
}) {
|
||||||
final preservesRemoteSingleAgentBinding =
|
|
||||||
existingBinding != null &&
|
|
||||||
existingBinding.workspaceKind == WorkspaceKind.remoteFs &&
|
|
||||||
existingBinding.workspacePath.trim().isNotEmpty &&
|
|
||||||
!isOwnerScopedRemoteWorkspacePathInternal(
|
|
||||||
existingBinding.workspacePath,
|
|
||||||
);
|
|
||||||
if (preservesRemoteSingleAgentBinding) {
|
|
||||||
return existingBinding.copyWith(
|
|
||||||
displayPath: existingBinding.displayPath.trim().isEmpty
|
|
||||||
? existingBinding.workspacePath
|
|
||||||
: null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (executionTarget == AssistantExecutionTarget.singleAgent) {
|
if (executionTarget == AssistantExecutionTarget.singleAgent) {
|
||||||
if (existingBinding != null &&
|
if (existingBinding != null &&
|
||||||
existingBinding.workspaceKind == WorkspaceKind.localFs &&
|
existingBinding.workspaceKind == WorkspaceKind.localFs &&
|
||||||
@ -264,39 +285,34 @@ extension AppControllerDesktopThreadBinding on AppController {
|
|||||||
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
|
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
|
||||||
sessionKey,
|
sessionKey,
|
||||||
);
|
);
|
||||||
final existing = assistantThreadRecordsInternal[normalizedSessionKey];
|
|
||||||
final resolvedExecutionTarget =
|
|
||||||
executionTarget ??
|
|
||||||
(existing == null
|
|
||||||
? null
|
|
||||||
: assistantExecutionTargetFromExecutionMode(
|
|
||||||
existing.executionBinding.executionMode,
|
|
||||||
)) ??
|
|
||||||
assistantExecutionTargetForSession(normalizedSessionKey);
|
|
||||||
final ownerScope = await ensureDesktopThreadOwnerScopeInternal(
|
final ownerScope = await ensureDesktopThreadOwnerScopeInternal(
|
||||||
normalizedSessionKey,
|
normalizedSessionKey,
|
||||||
);
|
);
|
||||||
|
final latestRecord = assistantThreadRecordsInternal[normalizedSessionKey];
|
||||||
|
final snapshot = resolveDesktopThreadBindingSnapshotInternal(
|
||||||
|
defaultExecutionTarget: settings.assistantExecutionTarget,
|
||||||
|
executionTargetOverride: executionTarget,
|
||||||
|
latestRecord: latestRecord,
|
||||||
|
);
|
||||||
final workspaceBinding = buildDesktopWorkspaceBindingInternal(
|
final workspaceBinding = buildDesktopWorkspaceBindingInternal(
|
||||||
normalizedSessionKey,
|
normalizedSessionKey,
|
||||||
executionTarget: resolvedExecutionTarget,
|
executionTarget: snapshot.executionTarget,
|
||||||
ownerScope: ownerScope,
|
ownerScope: ownerScope,
|
||||||
existingBinding: existing?.workspaceBinding,
|
existingBinding: snapshot.record?.workspaceBinding,
|
||||||
);
|
);
|
||||||
upsertTaskThreadInternal(
|
upsertTaskThreadInternal(
|
||||||
normalizedSessionKey,
|
normalizedSessionKey,
|
||||||
ownerScope: ownerScope,
|
ownerScope: ownerScope,
|
||||||
workspaceBinding: workspaceBinding,
|
workspaceBinding: workspaceBinding,
|
||||||
executionBinding: buildDesktopExecutionBindingInternal(
|
executionBinding: buildDesktopExecutionBindingInternal(
|
||||||
executionTarget: resolvedExecutionTarget,
|
executionTarget: snapshot.executionTarget,
|
||||||
singleAgentProvider: settings.sanitizeSingleAgentProviderSelection(
|
singleAgentProvider: settings.sanitizeSingleAgentProviderSelection(
|
||||||
SingleAgentProviderCopy.fromJsonValue(
|
snapshot.singleAgentProvider,
|
||||||
existing?.executionBinding.providerId ?? '',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
existingBinding: existing?.executionBinding,
|
existingBinding: snapshot.record?.executionBinding,
|
||||||
),
|
),
|
||||||
lifecycleState:
|
lifecycleState:
|
||||||
(existing?.lifecycleState ??
|
(snapshot.record?.lifecycleState ??
|
||||||
const ThreadLifecycleState(
|
const ThreadLifecycleState(
|
||||||
archived: false,
|
archived: false,
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
|
|||||||
@ -47,6 +47,45 @@ import 'app_controller_desktop_runtime_helpers.dart';
|
|||||||
import 'app_controller_desktop_thread_sessions_collaboration_impl.dart';
|
import 'app_controller_desktop_thread_sessions_collaboration_impl.dart';
|
||||||
|
|
||||||
// ignore_for_file: invalid_use_of_visible_for_testing_member, invalid_use_of_protected_member
|
// ignore_for_file: invalid_use_of_visible_for_testing_member, invalid_use_of_protected_member
|
||||||
|
|
||||||
|
AssistantThreadConnectionState resolveGatewayThreadConnectionStateInternal({
|
||||||
|
required AssistantExecutionTarget target,
|
||||||
|
required GatewayConnectionSnapshot connection,
|
||||||
|
required GatewayConnectionProfile targetProfile,
|
||||||
|
}) {
|
||||||
|
final expectedMode = target == AssistantExecutionTarget.local
|
||||||
|
? RuntimeConnectionMode.local
|
||||||
|
: RuntimeConnectionMode.remote;
|
||||||
|
final matchesTarget = connection.mode == expectedMode;
|
||||||
|
final targetAddress =
|
||||||
|
targetProfile.host.trim().isNotEmpty && targetProfile.port > 0
|
||||||
|
? '${targetProfile.host.trim()}:${targetProfile.port}'
|
||||||
|
: appText('未连接目标', 'No target');
|
||||||
|
final rawStatus = matchesTarget
|
||||||
|
? connection.status
|
||||||
|
: RuntimeConnectionStatus.offline;
|
||||||
|
final pairingRequired = matchesTarget && connection.pairingRequired;
|
||||||
|
final gatewayTokenMissing = matchesTarget && connection.gatewayTokenMissing;
|
||||||
|
final status = pairingRequired || gatewayTokenMissing
|
||||||
|
? RuntimeConnectionStatus.error
|
||||||
|
: rawStatus;
|
||||||
|
final primaryLabel = pairingRequired
|
||||||
|
? appText('需配对', 'Pairing Required')
|
||||||
|
: gatewayTokenMissing
|
||||||
|
? appText('缺少令牌', 'Missing Token')
|
||||||
|
: status.label;
|
||||||
|
return AssistantThreadConnectionState(
|
||||||
|
executionTarget: target,
|
||||||
|
status: status,
|
||||||
|
primaryLabel: primaryLabel,
|
||||||
|
detailLabel: targetAddress,
|
||||||
|
ready: status == RuntimeConnectionStatus.connected,
|
||||||
|
pairingRequired: pairingRequired,
|
||||||
|
gatewayTokenMissing: gatewayTokenMissing,
|
||||||
|
lastError: matchesTarget ? connection.lastError?.trim() : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
extension AppControllerDesktopThreadSessions on AppController {
|
extension AppControllerDesktopThreadSessions on AppController {
|
||||||
TaskThread? taskThreadForSessionInternal(String sessionKey) {
|
TaskThread? taskThreadForSessionInternal(String sessionKey) {
|
||||||
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
|
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
|
||||||
@ -394,41 +433,10 @@ extension AppControllerDesktopThreadSessions on AppController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final expectedMode = target == AssistantExecutionTarget.local
|
return resolveGatewayThreadConnectionStateInternal(
|
||||||
? RuntimeConnectionMode.local
|
target: target,
|
||||||
: RuntimeConnectionMode.remote;
|
connection: connection,
|
||||||
final matchesTarget = connection.mode == expectedMode;
|
targetProfile: gatewayProfileForAssistantExecutionTargetInternal(target),
|
||||||
final fallbackProfile = gatewayProfileForAssistantExecutionTargetInternal(
|
|
||||||
target,
|
|
||||||
);
|
|
||||||
final fallbackAddress = gatewayAddressLabelInternal(fallbackProfile);
|
|
||||||
final detail = matchesTarget
|
|
||||||
? (connection.remoteAddress?.trim().isNotEmpty == true
|
|
||||||
? connection.remoteAddress!.trim()
|
|
||||||
: fallbackAddress)
|
|
||||||
: fallbackAddress;
|
|
||||||
final rawStatus = matchesTarget
|
|
||||||
? connection.status
|
|
||||||
: RuntimeConnectionStatus.offline;
|
|
||||||
final pairingRequired = matchesTarget && connection.pairingRequired;
|
|
||||||
final gatewayTokenMissing = matchesTarget && connection.gatewayTokenMissing;
|
|
||||||
final status = pairingRequired || gatewayTokenMissing
|
|
||||||
? RuntimeConnectionStatus.error
|
|
||||||
: rawStatus;
|
|
||||||
final primaryLabel = pairingRequired
|
|
||||||
? appText('需配对', 'Pairing Required')
|
|
||||||
: gatewayTokenMissing
|
|
||||||
? appText('缺少令牌', 'Missing Token')
|
|
||||||
: status.label;
|
|
||||||
return AssistantThreadConnectionState(
|
|
||||||
executionTarget: target,
|
|
||||||
status: status,
|
|
||||||
primaryLabel: primaryLabel,
|
|
||||||
detailLabel: detail,
|
|
||||||
ready: status == RuntimeConnectionStatus.connected,
|
|
||||||
pairingRequired: pairingRequired,
|
|
||||||
gatewayTokenMissing: gatewayTokenMissing,
|
|
||||||
lastError: matchesTarget ? connection.lastError?.trim() : null,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -256,6 +256,9 @@ class MobileShellStateInternal extends State<MobileShell> {
|
|||||||
?.trim()
|
?.trim()
|
||||||
.isNotEmpty ??
|
.isNotEmpty ??
|
||||||
false;
|
false;
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!accountSignedIn) {
|
if (!accountSignedIn) {
|
||||||
await openGatewaySetupCodeEntryInternal();
|
await openGatewaySetupCodeEntryInternal();
|
||||||
messenger?.showSnackBar(
|
messenger?.showSnackBar(
|
||||||
|
|||||||
@ -2,7 +2,7 @@ name: xworkmate
|
|||||||
description: "XWorkmate desktop-first AI workspace shell."
|
description: "XWorkmate desktop-first AI workspace shell."
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 1.1.0+4
|
version: 1.0.0-beta.2+4
|
||||||
build-date: 2026-03-28
|
build-date: 2026-03-28
|
||||||
build-id: f153d7b
|
build-id: f153d7b
|
||||||
|
|
||||||
|
|||||||
8
scripts/ci/run_flutter_ci_suite.sh
Normal file
8
scripts/ci/run_flutter_ci_suite.sh
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
flutter pub get
|
||||||
|
flutter analyze
|
||||||
|
flutter test
|
||||||
|
flutter test test/golden
|
||||||
|
flutter test integration_test
|
||||||
6
scripts/ci/run_patrol_suite.sh
Normal file
6
scripts/ci/run_patrol_suite.sh
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
flutter pub get
|
||||||
|
dart pub global activate patrol_cli
|
||||||
|
patrol test
|
||||||
133
test/app_controller_desktop_thread_binding_test.dart
Normal file
133
test/app_controller_desktop_thread_binding_test.dart
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:xworkmate/app/app_controller_desktop_thread_binding.dart';
|
||||||
|
import 'package:xworkmate/app/app_controller_desktop_thread_sessions.dart';
|
||||||
|
import 'package:xworkmate/runtime/runtime_models.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('resolveDesktopThreadBindingSnapshotInternal', () {
|
||||||
|
const localOwner = ThreadOwnerScope(
|
||||||
|
realm: ThreadRealm.local,
|
||||||
|
subjectType: ThreadSubjectType.user,
|
||||||
|
subjectId: 'u1',
|
||||||
|
displayName: 'User',
|
||||||
|
);
|
||||||
|
|
||||||
|
TaskThread buildThread({
|
||||||
|
required String threadId,
|
||||||
|
required ThreadExecutionMode mode,
|
||||||
|
required String providerId,
|
||||||
|
}) {
|
||||||
|
return TaskThread(
|
||||||
|
threadId: threadId,
|
||||||
|
ownerScope: localOwner,
|
||||||
|
workspaceBinding: const WorkspaceBinding(
|
||||||
|
workspaceId: 'ws-1',
|
||||||
|
workspaceKind: WorkspaceKind.localFs,
|
||||||
|
workspacePath: '/tmp/ws',
|
||||||
|
displayPath: '/tmp/ws',
|
||||||
|
writable: true,
|
||||||
|
),
|
||||||
|
executionBinding: ExecutionBinding(
|
||||||
|
executionMode: mode,
|
||||||
|
executorId: providerId,
|
||||||
|
providerId: providerId,
|
||||||
|
endpointId: '',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('prefers the latest record after async binding resumes', () {
|
||||||
|
final latestRecord = buildThread(
|
||||||
|
threadId: 'thread-1',
|
||||||
|
mode: ThreadExecutionMode.localAgent,
|
||||||
|
providerId: SingleAgentProvider.opencode.providerId,
|
||||||
|
);
|
||||||
|
|
||||||
|
final snapshot = resolveDesktopThreadBindingSnapshotInternal(
|
||||||
|
defaultExecutionTarget: AssistantExecutionTarget.local,
|
||||||
|
latestRecord: latestRecord,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(snapshot.executionTarget, AssistantExecutionTarget.singleAgent);
|
||||||
|
expect(snapshot.singleAgentProvider, SingleAgentProvider.opencode);
|
||||||
|
expect(snapshot.record, same(latestRecord));
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'keeps an explicit execution override while preserving latest provider',
|
||||||
|
() {
|
||||||
|
final latestRecord = buildThread(
|
||||||
|
threadId: 'thread-2',
|
||||||
|
mode: ThreadExecutionMode.localAgent,
|
||||||
|
providerId: SingleAgentProvider.opencode.providerId,
|
||||||
|
);
|
||||||
|
|
||||||
|
final snapshot = resolveDesktopThreadBindingSnapshotInternal(
|
||||||
|
defaultExecutionTarget: AssistantExecutionTarget.local,
|
||||||
|
executionTargetOverride: AssistantExecutionTarget.remote,
|
||||||
|
latestRecord: latestRecord,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(snapshot.executionTarget, AssistantExecutionTarget.remote);
|
||||||
|
expect(snapshot.singleAgentProvider, SingleAgentProvider.opencode);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('does not recover provider from stale fallback-only records', () {
|
||||||
|
final staleRecord = buildThread(
|
||||||
|
threadId: 'thread-3',
|
||||||
|
mode: ThreadExecutionMode.gatewayRemote,
|
||||||
|
providerId: SingleAgentProvider.codex.providerId,
|
||||||
|
);
|
||||||
|
|
||||||
|
final snapshot = resolveDesktopThreadBindingSnapshotInternal(
|
||||||
|
defaultExecutionTarget: AssistantExecutionTarget.remote,
|
||||||
|
latestRecord: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(snapshot.executionTarget, AssistantExecutionTarget.remote);
|
||||||
|
expect(snapshot.singleAgentProvider, SingleAgentProvider.auto);
|
||||||
|
expect(snapshot.record, isNull);
|
||||||
|
expect(staleRecord.executionBinding.providerId, isNotEmpty);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('resolveGatewayThreadConnectionStateInternal', () {
|
||||||
|
test('uses the thread target profile as the only address source', () {
|
||||||
|
final state = resolveGatewayThreadConnectionStateInternal(
|
||||||
|
target: AssistantExecutionTarget.remote,
|
||||||
|
connection:
|
||||||
|
GatewayConnectionSnapshot.initial(
|
||||||
|
mode: RuntimeConnectionMode.remote,
|
||||||
|
).copyWith(
|
||||||
|
status: RuntimeConnectionStatus.connected,
|
||||||
|
remoteAddress: '127.0.0.1:18789',
|
||||||
|
),
|
||||||
|
targetProfile: GatewayConnectionProfile.defaultsRemote(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(state.status, RuntimeConnectionStatus.connected);
|
||||||
|
expect(state.detailLabel, 'openclaw.svc.plus:443');
|
||||||
|
expect(state.ready, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('marks mismatched local snapshot as offline for remote threads', () {
|
||||||
|
final state = resolveGatewayThreadConnectionStateInternal(
|
||||||
|
target: AssistantExecutionTarget.remote,
|
||||||
|
connection:
|
||||||
|
GatewayConnectionSnapshot.initial(
|
||||||
|
mode: RuntimeConnectionMode.local,
|
||||||
|
).copyWith(
|
||||||
|
status: RuntimeConnectionStatus.connected,
|
||||||
|
remoteAddress: '127.0.0.1:18789',
|
||||||
|
),
|
||||||
|
targetProfile: GatewayConnectionProfile.defaultsRemote(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(state.status, RuntimeConnectionStatus.offline);
|
||||||
|
expect(state.detailLabel, 'openclaw.svc.plus:443');
|
||||||
|
expect(state.ready, isFalse);
|
||||||
|
expect(state.lastError, isNull);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user