xworkmate-app/test/runtime/mode_switcher_test.dart
Haitao Pan e26ffb2116 feat: integrate Codex CLI as built-in code agent
- Add CodexRuntime for process management and JSON-RPC communication
- Add CodexConfigBridge for AI Gateway configuration
- Add ModeSwitcher for OpenClaw Gateway mode switching (local/remote/offline)
- Add AgentRegistry for agent registration and discovery
- Add RuntimeCoordinator for unified coordination
- Add Rust FFI bindings for native integration
- Add comprehensive test coverage

Phase 1-4 features:
- Configuration bridging to AI Gateway
- Mode switching between local/remote/offline
- Agent registration protocol
- Cloud memory sync capability
- Offline fallback support

CI/CD:
- GitHub Actions workflow for Rust FFI build
- Build scripts for macOS universal binary
- Integration with Flutter build process

Co-authored-by: Codex CLI Integration <codex@openai.com>
2026-03-14 00:10:27 +08:00

263 lines
8.4 KiB
Dart

import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:xworkmate/runtime/mode_switcher.dart';
import 'package:xworkmate/runtime/gateway_runtime.dart';
import 'package:xworkmate/runtime/runtime_models.dart';
// Mock GatewayRuntime for testing
class MockGatewayRuntime extends ChangeNotifier implements GatewayRuntime {
final StreamController<GatewayPushEvent> _events = StreamController.broadcast();
GatewayConnectionSnapshot _snapshot = GatewayConnectionSnapshot.initial();
bool _isConnected = false;
final List<Map<String, dynamic>> _requests = [];
void setConnected(bool connected) {
_isConnected = connected;
_snapshot = GatewayConnectionSnapshot(
profile: GatewayConnectionProfile.defaults(),
status: connected ? RuntimeConnectionStatus.connected : RuntimeConnectionStatus.offline,
);
notifyListeners();
// Emit connection event
if (connected) {
_events.add(GatewayPushEvent(event: 'gateway/connected', payload: {}));
}
}
@override
bool get isConnected => _isConnected;
@override
GatewayConnectionSnapshot get snapshot => _snapshot;
@override
Stream<GatewayPushEvent> get events => _events.stream;
@override
Future<Map<String, dynamic>> request(
String method, {
Map<String, dynamic> params = const {},
Duration timeout = const Duration(seconds: 30),
}) async {
_requests.add({'method': method, 'params': params});
return {'success': true};
}
@override
Future<void> initialize() async {}
@override
Future<void> connectProfile(
GatewayConnectionProfile profile, {
String authTokenOverride = '',
String authPasswordOverride = '',
}) async {
_isConnected = true;
_snapshot = GatewayConnectionSnapshot(
profile: profile,
status: RuntimeConnectionStatus.connected,
);
notifyListeners();
_events.add(GatewayPushEvent(event: 'gateway/connected', payload: {}));
}
@override
Future<void> disconnect() async {
_isConnected = false;
_snapshot = GatewayConnectionSnapshot(
profile: _snapshot.profile,
status: RuntimeConnectionStatus.offline,
);
notifyListeners();
}
@override
Future<void> clearLogs() async {}
@override
List<RuntimeLogEntry> get logs => [];
@override
List<RuntimeLogEntry> get logsForTest => [];
@override
void addRuntimeLogForTest({
required String level,
required String category,
required String message,
}) {}
}
void main() {
group('GatewayMode', () {
test('has all expected modes', () {
expect(GatewayMode.values, hasLength(3));
expect(GatewayMode.values, contains(GatewayMode.local));
expect(GatewayMode.values, contains(GatewayMode.remote));
expect(GatewayMode.values, contains(GatewayMode.offline));
});
});
group('ModeSwitcherState', () {
test('has all expected states', () {
expect(ModeSwitcherState.values, hasLength(6));
expect(ModeSwitcherState.values, contains(ModeSwitcherState.disconnected));
expect(ModeSwitcherState.values, contains(ModeSwitcherState.connecting));
expect(ModeSwitcherState.values, contains(ModeSwitcherState.connectedLocal));
expect(ModeSwitcherState.values, contains(ModeSwitcherState.connectedRemote));
expect(ModeSwitcherState.values, contains(ModeSwitcherState.offline));
expect(ModeSwitcherState.values, contains(ModeSwitcherState.error));
});
});
group('ModeCapabilities', () {
test('local mode has correct capabilities', () {
expect(ModeCapabilities.local.hasCloudMemory, isFalse);
expect(ModeCapabilities.local.hasTaskQueue, isFalse);
expect(ModeCapabilities.local.hasMultiAgent, isFalse);
expect(ModeCapabilities.local.hasLocalModels, isTrue);
expect(ModeCapabilities.local.hasCodeAgent, isTrue);
});
test('remote mode has correct capabilities', () {
expect(ModeCapabilities.remote.hasCloudMemory, isTrue);
expect(ModeCapabilities.remote.hasTaskQueue, isTrue);
expect(ModeCapabilities.remote.hasMultiAgent, isTrue);
expect(ModeCapabilities.remote.hasLocalModels, isTrue);
expect(ModeCapabilities.remote.hasCodeAgent, isTrue);
});
test('offline mode has correct capabilities', () {
expect(ModeCapabilities.offline.hasCloudMemory, isFalse);
expect(ModeCapabilities.offline.hasTaskQueue, isFalse);
expect(ModeCapabilities.offline.hasMultiAgent, isFalse);
expect(ModeCapabilities.offline.hasLocalModels, isFalse);
expect(ModeCapabilities.offline.hasCodeAgent, isTrue);
});
test('toMap returns correct values', () {
final map = ModeCapabilities.remote.toMap();
expect(map['hasCloudMemory'], isTrue);
expect(map['hasTaskQueue'], isTrue);
expect(map['hasMultiAgent'], isTrue);
expect(map['hasLocalModels'], isTrue);
expect(map['hasCodeAgent'], isTrue);
});
});
group('ModeSwitchResult', () {
test('success result is created correctly', () {
final result = ModeSwitchResult(
success: true,
mode: GatewayMode.remote,
capabilities: ModeCapabilities.remote.toMap(),
);
expect(result.success, isTrue);
expect(result.mode, equals(GatewayMode.remote));
expect(result.error, isNull);
expect(result.capabilities, isNotNull);
});
test('failure result is created correctly', () {
final result = ModeSwitchResult(
success: false,
mode: GatewayMode.local,
error: 'Connection failed',
);
expect(result.success, isFalse);
expect(result.mode, equals(GatewayMode.local));
expect(result.error, equals('Connection failed'));
expect(result.capabilities, isNull);
});
});
group('ModeSwitcher', () {
late MockGatewayRuntime mockGateway;
late ModeSwitcher modeSwitcher;
setUp(() {
mockGateway = MockGatewayRuntime();
modeSwitcher = ModeSwitcher(mockGateway);
});
test('initial state is disconnected', () {
expect(modeSwitcher.state, equals(ModeSwitcherState.disconnected));
expect(modeSwitcher.currentMode, equals(GatewayMode.offline));
expect(modeSwitcher.lastError, isNull);
});
test('switchToLocal succeeds when gateway connects', () async {
mockGateway.setConnected(true);
final result = await modeSwitcher.switchToLocal();
expect(result.success, isTrue);
expect(result.mode, equals(GatewayMode.local));
expect(modeSwitcher.state, equals(ModeSwitcherState.connectedLocal));
expect(modeSwitcher.capabilities.hasLocalModels, isTrue);
});
test('switchToRemote succeeds when gateway connects', () async {
mockGateway.setConnected(true);
final result = await modeSwitcher.switchToRemote();
expect(result.success, isTrue);
expect(result.mode, equals(GatewayMode.remote));
expect(modeSwitcher.state, equals(ModeSwitcherState.connectedRemote));
expect(modeSwitcher.capabilities.hasCloudMemory, isTrue);
});
test('switchToOffline succeeds', () async {
final result = await modeSwitcher.switchToOffline();
expect(result.success, isTrue);
expect(result.mode, equals(GatewayMode.offline));
expect(modeSwitcher.state, equals(ModeSwitcherState.offline));
expect(modeSwitcher.capabilities.hasCloudMemory, isFalse);
});
test('stateDescription returns correct values', () {
expect(modeSwitcher.stateDescription, equals('Disconnected'));
modeSwitcher.switchToLocal();
// Check after async completes
Future.delayed(Duration(milliseconds: 100), () {
expect(
modeSwitcher.stateDescription,
anyOf(equals('Connected (Local)'), equals('Connecting...')),
);
});
});
test('modeDescription returns correct values', () {
expect(
modeSwitcher.modeDescription,
equals('Offline Mode (Local Codex Only)'),
);
});
test('autoSelect prefers remote by default', () async {
mockGateway.setConnected(true);
final result = await modeSwitcher.autoSelect();
expect(result.success, isTrue);
expect(result.mode, equals(GatewayMode.remote));
});
test('autoSelect falls back to local when remote fails', () async {
// Don't set gateway as connected, remote will fail
final result = await modeSwitcher.autoSelect();
// Should fall back to offline since both remote and local fail
expect(result.mode, equals(GatewayMode.offline));
});
});
}