mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 20:27:15 +01:00
Fix wire-format key mismatch with Python (deletedHlc -> deleted_hlc)
Record.toJson() was writing the delete-clock field as deletedHlc while the Python package's Record.to_dict() writes deleted_hlc -- an undetected cross-language wire divergence: a tombstoned record synced Python<->Dart would silently lose its delete clock on the receiving side. Also widen syncLog's decode error handling from FormatException alone to also catch TypeError, matching the Python side's (ValueError, KeyError, TypeError) so a valid-JSON-but-wrong-shape push from an incompatible writer is skipped per-device instead of aborting the whole sync. Adds a fixture test shared verbatim with the Python package's test suite so this class of divergence fails loudly going forward. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017McuA1nAi6jBT1EaS5BrVf
This commit is contained in:
parent
4b66703280
commit
6b14e6d36d
@ -38,7 +38,9 @@ class Record {
|
||||
(name, field) => MapEntry(name, [field.$1, field.$2.toStr()]),
|
||||
),
|
||||
'deleted': deleted,
|
||||
'deletedHlc': deletedHlc?.toStr(),
|
||||
// Wire key is snake_case to match crdt_sync's Python `Record.to_dict` --
|
||||
// this is a cross-language wire format, not a Dart-idiomatic API.
|
||||
'deleted_hlc': deletedHlc?.toStr(),
|
||||
};
|
||||
|
||||
factory Record.fromJson(Map<String, dynamic> json) {
|
||||
@ -47,7 +49,7 @@ class Record {
|
||||
final pair = value as List<dynamic>;
|
||||
return MapEntry(name, (pair[0], Hlc.fromStr(pair[1] as String)));
|
||||
});
|
||||
final deletedHlcStr = json['deletedHlc'] as String?;
|
||||
final deletedHlcStr = json['deleted_hlc'] as String?;
|
||||
return Record(
|
||||
id: json['id'] as String,
|
||||
fields: fields,
|
||||
|
||||
@ -12,9 +12,12 @@ const _defaultFilename = 'log.json';
|
||||
/// to `<pathPrefix>/<deviceId>/<filename>`.
|
||||
///
|
||||
/// [encode] serializes a merged log for pushing. [decode] parses a remote
|
||||
/// device's pushed text back into a log; throwing [FormatException] is
|
||||
/// treated as a corrupt/unparsable push, and that device is skipped for
|
||||
/// this tick rather than aborting the whole sync.
|
||||
/// device's pushed text back into a log; throwing [FormatException] (bad
|
||||
/// JSON syntax) or [TypeError] (valid JSON, wrong shape -- e.g. a missing
|
||||
/// or mistyped field during [Record.fromJson]) is treated as a
|
||||
/// corrupt/unparsable push, and that device is skipped for this tick
|
||||
/// rather than aborting the whole sync. Mirrors the Python `sync_log`'s
|
||||
/// `(ValueError, KeyError, TypeError)` catch for the same reason.
|
||||
Future<Log> syncLog({
|
||||
required GitHubClient client,
|
||||
required String deviceId,
|
||||
@ -36,6 +39,8 @@ Future<Log> syncLog({
|
||||
mergedLog = mergeLogs(mergedLog, decode(text));
|
||||
} on FormatException {
|
||||
continue;
|
||||
} on TypeError {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -138,4 +138,35 @@ void main() {
|
||||
expect(Record.fromJson(record.toJson()), equals(record));
|
||||
});
|
||||
});
|
||||
|
||||
group('cross-language wire format', () {
|
||||
// Pins the exact wire shape shared with crdt-sync's Python Record.
|
||||
// These values are duplicated verbatim in the Python package's
|
||||
// `test_record.py` (`TestCrossLanguageWireFormat`). If this test and
|
||||
// that one both pass, the two languages agree on the wire format; if
|
||||
// only one changes, the two suites diverge and at least one of them
|
||||
// fails -- catching exactly the kind of key-naming mismatch
|
||||
// (`deleted_hlc` vs `deletedHlc`) that neither language's own
|
||||
// round-trip test can see on its own.
|
||||
test('matches the fixture shared with the Python package', () {
|
||||
final record = Record(
|
||||
id: 'abc123',
|
||||
fields: {
|
||||
'text': ('hello', Hlc(wallTimeMs: 100, counter: 0, nodeId: 'pc')),
|
||||
},
|
||||
deleted: true,
|
||||
deletedHlc: Hlc(wallTimeMs: 1000, counter: 0, nodeId: 'node-a'),
|
||||
);
|
||||
final expected = {
|
||||
'id': 'abc123',
|
||||
'fields': {
|
||||
'text': ['hello', '1970-01-01T00:00:00.100Z-0000-pc'],
|
||||
},
|
||||
'deleted': true,
|
||||
'deleted_hlc': '1970-01-01T00:00:01.000Z-0000-node-a',
|
||||
};
|
||||
expect(record.toJson(), equals(expected));
|
||||
expect(Record.fromJson(expected), equals(record));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -146,6 +146,31 @@ void main() {
|
||||
expect(putCalls, hasLength(1));
|
||||
});
|
||||
|
||||
test('skips a device whose pushed JSON has the wrong shape', () async {
|
||||
// Valid JSON that isn't a record map (e.g. from an incompatible
|
||||
// writer) must be skipped like corrupt JSON, not crash the whole
|
||||
// sync -- this is what the `on TypeError` catch in `syncLog` is
|
||||
// for, not just JSON syntax errors caught by `FormatException`.
|
||||
final (:client, :putCalls) = _client(
|
||||
contentResponses: {
|
||||
'devices': _directoryOf(['phone']),
|
||||
'devices/phone/log.json': _fileContaining('{"a": 5}'),
|
||||
},
|
||||
);
|
||||
|
||||
final merged = await syncLog(
|
||||
client: client,
|
||||
deviceId: 'pc',
|
||||
pathPrefix: 'devices',
|
||||
localLog: {},
|
||||
encode: _encode,
|
||||
decode: _decode,
|
||||
);
|
||||
|
||||
expect(merged, isEmpty);
|
||||
expect(putCalls, hasLength(1));
|
||||
});
|
||||
|
||||
test('merges in a remote device\'s entries', () async {
|
||||
final remoteLog = <String, Record>{
|
||||
'b': Record(id: 'b', fields: {'text': ('from phone', _make(100))}),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user