import test from 'node:test'; import assert from 'node:assert/strict'; import { clearOfflineMutations, enqueueTenderMutation, listOfflineMutations, offlineMutationCount, replayOfflineMutations, } from '../src/services/offlineMutationQueue.js'; const storage = new Map(); globalThis.localStorage = { getItem: key => storage.has(key) ? storage.get(key) : null, setItem: (key, value) => storage.set(key, String(value)), removeItem: key => storage.delete(key), }; test('offline tender mutations are account scoped and merged by tender', async () => { clearOfflineMutations('qa@example.test'); enqueueTenderMutation('qa@example.test', { key: '2026/1', patch: { takipte: true }, snapshot: { ikn: '2026/1' }, version: 2 }); enqueueTenderMutation('qa@example.test', { key: '2026/1', patch: { kanbanStatus: 'teklif_hazirlaniyor' }, snapshot: { ikn: '2026/1' }, version: 2 }); enqueueTenderMutation('other@example.test', { key: '2026/1', patch: { takipte: true }, snapshot: { ikn: '2026/1' }, version: 0 }); assert.equal(offlineMutationCount('qa@example.test'), 1); assert.deepEqual(listOfflineMutations('qa@example.test')[0].patch, { takipte: true, kanbanStatus: 'teklif_hazirlaniyor' }); assert.equal(offlineMutationCount('other@example.test'), 1); clearOfflineMutations('qa@example.test'); clearOfflineMutations('other@example.test'); }); test('replay removes acknowledged rows and retains the first failed row', async () => { clearOfflineMutations('replay@example.test'); enqueueTenderMutation('replay@example.test', { key: 'a', patch: { takipte: true }, snapshot: {}, version: 0 }); enqueueTenderMutation('replay@example.test', { key: 'b', patch: { takipte: true }, snapshot: {}, version: 0 }); const sent = []; const first = await replayOfflineMutations('replay@example.test', async item => { sent.push(item.key); }); assert.deepEqual(sent, ['a', 'b']); assert.equal(first.pending, 0); enqueueTenderMutation('replay@example.test', { key: 'c', patch: { takipte: true }, snapshot: {}, version: 0 }); enqueueTenderMutation('replay@example.test', { key: 'd', patch: { takipte: true }, snapshot: {}, version: 0 }); const second = await replayOfflineMutations('replay@example.test', async item => { if (item.key === 'c') throw new Error('network'); }); assert.equal(second.sent, 0); assert.equal(listOfflineMutations('replay@example.test')[0].key, 'c'); assert.equal(listOfflineMutations('replay@example.test')[0].attempts, 1); clearOfflineMutations('replay@example.test'); }); test('concurrent reconnect signals send each queued mutation once', async () => { clearOfflineMutations('concurrent@example.test'); enqueueTenderMutation('concurrent@example.test', { key: 'a', patch: { takipte: true }, snapshot: {}, version: 0 }); let sends = 0; let release; const gate = new Promise(resolve => { release = resolve; }); const send = async () => { sends += 1; await gate; return { version: 1 }; }; const first = replayOfflineMutations('concurrent@example.test', send); const second = replayOfflineMutations('concurrent@example.test', send); release(); const [a, b] = await Promise.all([first, second]); assert.equal(sends, 1); assert.deepEqual(a, b); assert.equal(offlineMutationCount('concurrent@example.test'), 0); }); test('a mutation added during replay is retained with the acknowledged server version', async () => { clearOfflineMutations('during-replay@example.test'); enqueueTenderMutation('during-replay@example.test', { key: 'a', patch: { takipte: true }, snapshot: {}, version: 3 }); const first = await replayOfflineMutations('during-replay@example.test', async item => { enqueueTenderMutation('during-replay@example.test', { key: 'a', patch: { kanbanStatus: 'teklif_hazirlaniyor' }, snapshot: {}, version: item.version }); return { version: 4 }; }); assert.equal(first.pending, 1); const retained = listOfflineMutations('during-replay@example.test')[0]; assert.equal(retained.version, 4); assert.deepEqual(retained.patch, { takipte: true, kanbanStatus: 'teklif_hazirlaniyor' }); const versions = []; await replayOfflineMutations('during-replay@example.test', async item => { versions.push(item.version); return { version: 5 }; }); assert.deepEqual(versions, [4]); assert.equal(offlineMutationCount('during-replay@example.test'), 0); }); test('a full queue rejects a new tender instead of silently discarding an older change', () => { clearOfflineMutations('full@example.test'); for (let index = 0; index < 100; index += 1) { enqueueTenderMutation('full@example.test', { key: String(index), patch: { takipte: true }, snapshot: {}, version: 0 }); } assert.throws( () => enqueueTenderMutation('full@example.test', { key: 'overflow', patch: { takipte: true }, snapshot: {}, version: 0 }), /kuyruğu dolu/, ); assert.equal(offlineMutationCount('full@example.test'), 100); assert.equal(listOfflineMutations('full@example.test')[0].key, '0'); clearOfflineMutations('full@example.test'); });