280 lines
8.8 KiB
Dart
280 lines
8.8 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/device_identity_store.dart';
|
|
import 'package:xworkmate/runtime/runtime_models.dart';
|
|
import 'package:xworkmate/runtime/secure_config_store.dart';
|
|
|
|
// Mock GatewayRuntime for testing
|
|
class MockGatewayRuntime extends GatewayRuntime {
|
|
factory MockGatewayRuntime() {
|
|
final store = SecureConfigStore();
|
|
return MockGatewayRuntime._(store);
|
|
}
|
|
|
|
MockGatewayRuntime._(SecureConfigStore store)
|
|
: _storeForTest = store,
|
|
super(
|
|
store: store,
|
|
identityStore: DeviceIdentityStore(store),
|
|
);
|
|
|
|
final SecureConfigStore _storeForTest;
|
|
final StreamController<GatewayPushEvent> _eventsController =
|
|
StreamController<GatewayPushEvent>.broadcast();
|
|
GatewayConnectionSnapshot _snapshot = GatewayConnectionSnapshot.initial();
|
|
bool _isConnected = false;
|
|
final List<Map<String, dynamic>> _requests = [];
|
|
|
|
void setConnected(bool connected) {
|
|
_isConnected = connected;
|
|
_snapshot = _snapshot.copyWith(
|
|
status: connected
|
|
? RuntimeConnectionStatus.connected
|
|
: RuntimeConnectionStatus.offline,
|
|
statusText: connected ? 'Connected' : 'Offline',
|
|
);
|
|
notifyListeners();
|
|
|
|
// Emit connection event
|
|
if (connected) {
|
|
_eventsController.add(
|
|
const GatewayPushEvent(
|
|
event: 'gateway/connected',
|
|
payload: <String, dynamic>{},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool get isConnected => _isConnected;
|
|
|
|
@override
|
|
GatewayConnectionSnapshot get snapshot => _snapshot;
|
|
|
|
@override
|
|
Stream<GatewayPushEvent> get events => _eventsController.stream;
|
|
|
|
@override
|
|
Future<dynamic> request(
|
|
String method, {
|
|
Map<String, dynamic>? params,
|
|
Duration timeout = const Duration(seconds: 15),
|
|
}) async {
|
|
_requests.add({'method': method, 'params': params ?? const {}});
|
|
return {'success': true};
|
|
}
|
|
|
|
@override
|
|
Future<void> initialize() async {}
|
|
|
|
@override
|
|
Future<void> connectProfile(
|
|
GatewayConnectionProfile profile, {
|
|
String authTokenOverride = '',
|
|
String authPasswordOverride = '',
|
|
}) async {
|
|
_isConnected = true;
|
|
_snapshot = GatewayConnectionSnapshot.initial(mode: profile.mode).copyWith(
|
|
status: RuntimeConnectionStatus.connected,
|
|
statusText: 'Connected',
|
|
);
|
|
notifyListeners();
|
|
_eventsController.add(
|
|
const GatewayPushEvent(
|
|
event: 'gateway/connected',
|
|
payload: <String, dynamic>{},
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<void> disconnect({bool clearDesiredProfile = true}) async {
|
|
_isConnected = false;
|
|
_snapshot = GatewayConnectionSnapshot.initial(mode: _snapshot.mode).copyWith(
|
|
statusText: 'Offline',
|
|
);
|
|
notifyListeners();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_eventsController.close();
|
|
super.dispose();
|
|
}
|
|
}
|
|
|
|
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));
|
|
});
|
|
});
|
|
}
|