import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:kuaishai2/features/device/models/device_state.dart'; import 'package:kuaishai2/features/device/services/device_log.dart'; import 'package:kuaishai2/features/device/services/device_message.dart'; import 'package:kuaishai2/features/device/services/json_protocol.dart'; import 'package:kuaishai2/features/device/services/task_payload.dart'; import 'package:kuaishai2/features/programs/models/program.dart'; import 'package:kuaishai2/features/programs/models/step.dart'; void main() { group('DeviceMessageType', () { test('wire names match protocol', () { expect(DeviceMessageType.deviceInfo.wireName, 'device_info'); expect(DeviceMessageType.createTask.wireName, 'create_task'); expect(DeviceMessageType.lightControl.wireName, 'light_control'); expect(DeviceMessageType.control.wireName, 'control'); }); test('fromWire round-trip', () { for (final t in DeviceMessageType.values) { expect(DeviceMessageType.fromWire(t.wireName), t); } expect(DeviceMessageType.fromWire(null), isNull); expect(DeviceMessageType.fromWire('nope'), isNull); }); }); group('DeviceMessage', () { test('toJson 与数据模型一致', () { final msg = DeviceMessage( messageId: 'abc-1', type: DeviceMessageType.deviceInfo, data: const { 'door_status': 'open', 'task_status': 'running', 'light_status': 'on', }, ); final json = msg.toJson(); expect(json['message_id'], 'abc-1'); expect(json['type'], 'device_info'); expect(json['ack'], isNull); expect(json['need_ack'], isFalse); expect((json['data'] as Map)['door_status'], 'open'); }); test('encode/decode 闭环', () { final src = DeviceMessage( messageId: 'm-001', type: DeviceMessageType.lightControl, data: const {'status': 'on'}, ack: 'reply-001', needAck: true, ); final raw = src.encode(); final parsed = DeviceMessage.decode(raw); expect(parsed, isNotNull); expect(parsed!.messageId, 'm-001'); expect(parsed.type, DeviceMessageType.lightControl); expect(parsed.ack, 'reply-001'); expect(parsed.needAck, isTrue); expect(parsed.data['status'], 'on'); }); test('decode 非法 JSON 返回 null', () { expect(DeviceMessage.decode('not-json'), isNull); expect(DeviceMessage.decode('{"foo":1}'), isNull); expect(DeviceMessage.decode(jsonEncode({ 'message_id': '', 'type': 'device_info', 'data': {}, })), isNull); }); test('ackFor 工厂生成应答消息', () { final original = DeviceMessage( messageId: 'orig-1', type: DeviceMessageType.createTask, data: const {'k': 'v'}, ); final ack = DeviceMessage.ackFor(original, {'result': 'ok'}); expect(ack.ack, 'orig-1'); expect(ack.type, DeviceMessageType.createTask); expect(ack.data['result'], 'ok'); }); }); group('MessageIdGenerator', () { test('连续生成的 ID 互不相同', () { final gen = MessageIdGenerator(); final ids = List.generate(100, (_) => gen.next()).toSet(); expect(ids.length, 100); }); }); group('JsonProtocolService', () { test('encode + tryDecode 闭环', () { final p = JsonProtocolService(); final msg = DeviceMessage( messageId: 'p-1', type: DeviceMessageType.control, data: const {'status': 'stop'}, ); final bytes = p.encode(msg); // 4 字节长度前缀 + 至少 1 字节 JSON + 1 字节换行 expect(bytes.length, greaterThan(5)); final (decoded, consumed) = p.tryDecode(bytes); expect(consumed, bytes.length); expect(decoded, isNotNull); expect(decoded!.type, DeviceMessageType.control); expect(decoded.data['status'], 'stop'); }); test('不足一帧时返回 null', () { final p = JsonProtocolService(); final partial = Uint8List.fromList([0x00, 0x00, 0x00]); // 只有 3 字节长度前缀 final (decoded, consumed) = p.tryDecode(partial); expect(decoded, isNull); expect(consumed, 0); }); test('分片到达也能解析', () { final p = JsonProtocolService(); final msg = DeviceMessage( messageId: 'frag-1', type: DeviceMessageType.createTask, data: const {'x': 1}, ); final bytes = p.encode(msg); // 拆成 1 字节 / 剩余两段 final first = bytes.sublist(0, 1); final second = bytes.sublist(1, 4); final third = bytes.sublist(4); expect(p.tryDecode(first).$1, isNull); final (m1, _) = p.tryDecode(second); expect(m1, isNull); final (m2, consumed) = p.tryDecode(third); expect(m2, isNotNull); expect(m2!.messageId, 'frag-1'); // consumed = 4 字节长度前缀 + json 长度 + 1 字节换行 expect(consumed, bytes.length); }); test('长度字段异常时丢弃首字节重新对齐', () { final p = JsonProtocolService(); // 长度 = 0xFFFFFF 不可能 final corrupted = Uint8List.fromList([0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x01, 0x7B, 0x7D, 0x0A]); final (decoded, _) = p.tryDecode(corrupted); // 长度异常 -> 丢弃 1 字节 + 下一帧有效 // 第二帧 length=1, json='{', 缺少换行也允许 expect(decoded, isNull); }); }); group('TaskPayload → DeviceMessage', () { test('构造 create_task 消息字段与文档一致', () { final program = Program( id: 1, code: 'P001', name: 'Demo', createdAt: '2026-06-04', temperature: 50, airflowTime: 60, ); final steps = [ Step( id: 1, programId: 1, stepNo: 1, position: 'A1', name: '混合', mixTime: 60, magnetTime: 30, volume: 100, speed: 5, ), ]; final payload = TaskPayload.fromProgram(program, steps); final msg = payload.toMessage('mid-1', needAck: true); expect(msg.type, DeviceMessageType.createTask); expect(msg.needAck, isTrue); expect(msg.data['temperature'], 50); expect(msg.data['airflowtime'], 60); final stepJson = (msg.data['steps'] as List).first as Map; expect(stepJson['no'], 1); expect(stepJson['slot'], 1); // A1 -> 1 expect(stepJson['mixtime'], 60); expect(stepJson['pulltime'], 30); expect(stepJson['volume'], 100); expect(stepJson['speed'], 5); }); }); group('DeviceState model', () { test('copyWith 不会修改未传字段', () { const s = DeviceState( lightStatus: DeviceLightStatus.on, doorStatus: DeviceDoorStatus.open, taskStatus: DeviceTaskStatus.running, lastInfoAt: null, ); final s2 = s.copyWith(lightStatus: DeviceLightStatus.off); expect(s2.lightStatus, DeviceLightStatus.off); expect(s2.doorStatus, DeviceDoorStatus.open); expect(s2.taskStatus, DeviceTaskStatus.running); }); test('infoStale 在 10 秒后为 true', () { final past = DateTime.now().subtract(const Duration(seconds: 30)); final s = DeviceState(lastInfoAt: past); expect(s.infoStale, isTrue); }); }); group('DeviceLog', () { test('summarizeData 截断长字符串与多余键', () { final data = { 'a': 1, 'b': 'short', 'c': 'x' * 200, 'd': [1, 2, 3], 'e': true, 'f': null, 'g': 'extra', 'h': 'extra2', 'i': 'extra3', }; final summary = DeviceLog.summarizeData(data, maxKeys: 4); expect(summary.contains('a=1'), isTrue); expect(summary.contains('b="short"'), isTrue); expect(summary.contains('…'), isTrue); // 长字符串被截断 expect(summary.contains('…'), isTrue); expect(summary.contains(', ...'), isTrue); // 多余键被截断 }); test('summarizeData 空 Map / null 友好处理', () { expect(DeviceLog.summarizeData(null), '{}'); expect(DeviceLog.summarizeData({}), '{}'); }); test('info / warn / severe 不抛异常', () { // 仅验证调用不抛异常;dart:developer.log 在测试环境无可见输出 DeviceLog.info('hello'); DeviceLog.warn('warn', error: StateError('x')); DeviceLog.severe('severe', error: 'err'); }); }); }