|
4 | 4 | assertNoPendingAttachmentUploads, |
5 | 5 | computeCoveredSettingsFingerprint, |
6 | 6 | computeSyncChangeFingerprint, |
| 7 | + computeStableValueFingerprint, |
7 | 8 | computeSyncPayloadFingerprint, |
8 | 9 | findPendingAttachmentUploads, |
9 | 10 | hasPendingSyncSideEffects, |
@@ -1042,3 +1043,55 @@ describe('sync-helpers computeSyncPayloadFingerprint', () => { |
1042 | 1043 | expect(computeSyncPayloadFingerprint(data)).toBe(computeSyncPayloadFingerprint(data)); |
1043 | 1044 | }); |
1044 | 1045 | }); |
| 1046 | + |
| 1047 | +describe('sync comparison ignores the order of id-keyed lists (#1136)', () => { |
| 1048 | + const attachment = (id: string): Attachment => ({ |
| 1049 | + id, |
| 1050 | + kind: 'file', |
| 1051 | + title: `${id}.pdf`, |
| 1052 | + uri: '', |
| 1053 | + createdAt: '2026-01-01T00:00:00.000Z', |
| 1054 | + updatedAt: '2026-01-01T00:00:00.000Z', |
| 1055 | + }); |
| 1056 | + const task = (id: string, attachments: Attachment[]) => ({ |
| 1057 | + id, |
| 1058 | + title: id, |
| 1059 | + status: 'inbox' as const, |
| 1060 | + tags: [], |
| 1061 | + contexts: [], |
| 1062 | + createdAt: '2026-01-01T00:00:00.000Z', |
| 1063 | + updatedAt: '2026-01-01T00:00:00.000Z', |
| 1064 | + attachments, |
| 1065 | + }); |
| 1066 | + const doc = (tasks: ReturnType<typeof task>[]): AppData => ({ |
| 1067 | + tasks: tasks as unknown as AppData['tasks'], |
| 1068 | + projects: [], |
| 1069 | + sections: [], |
| 1070 | + areas: [], |
| 1071 | + people: [], |
| 1072 | + settings: {}, |
| 1073 | + }); |
| 1074 | + |
| 1075 | + it('treats a merge that listed the same records local-first on each device as equal', () => { |
| 1076 | + const phone = doc([task('t2', [attachment('a2'), attachment('a1')]), task('t1', [])]); |
| 1077 | + const server = doc([task('t1', []), task('t2', [attachment('a1'), attachment('a2')])]); |
| 1078 | + |
| 1079 | + expect(areSyncPayloadsEqual(phone, server)).toBe(true); |
| 1080 | + expect(computeStableValueFingerprint(phone)).toBe(computeStableValueFingerprint(server)); |
| 1081 | + expect(computeSyncPayloadFingerprint(phone)).toBe(computeSyncPayloadFingerprint(server)); |
| 1082 | + }); |
| 1083 | + |
| 1084 | + it('still sees a content difference behind a reorder', () => { |
| 1085 | + const phone = doc([task('t2', [attachment('a2'), attachment('a1')])]); |
| 1086 | + const server = doc([task('t2', [attachment('a1'), { ...attachment('a2'), deletedAt: '2026-01-02T00:00:00.000Z' }])]); |
| 1087 | + |
| 1088 | + expect(areSyncPayloadsEqual(phone, server)).toBe(false); |
| 1089 | + }); |
| 1090 | + |
| 1091 | + it('keeps primitive lists positional', () => { |
| 1092 | + expect(areSyncPayloadsEqual( |
| 1093 | + { ...doc([]), settings: { contexts: ['a', 'b'] } as AppData['settings'] }, |
| 1094 | + { ...doc([]), settings: { contexts: ['b', 'a'] } as AppData['settings'] }, |
| 1095 | + )).toBe(false); |
| 1096 | + }); |
| 1097 | +}); |
0 commit comments