- 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>
263 lines
8.4 KiB
Dart
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));
|
|
});
|
|
});
|
|
}
|