import assert from 'node:assert/strict'; import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; import { test } from 'node:test'; import * as Y from 'yjs'; import { checkServerUrl, parseInviteLink, syncSocketUrl } from '../src/sync/api'; import { OfflineStore } from '../src/sync/offlineStore'; import { formatLink, parseLink } from '../src/sync/protocol'; import { applyToYText, safeChange } from '../src/sync/textSync'; import { applyChanges } from '../src/util/textDiff'; test('safeChange never splits a surrogate pair', () => { const cases: [string, string][] = [ ['x😀y', 'x😃y'], ['😀', '😃'], ['a😀', 'a😀b'], ['😀b', 'a😀b'], ['label: 🚀 API', 'label: 🛰 API'], ['plain', 'plainer'], ]; for (const [before, after] of cases) { const change = safeChange(before, after)!; assert.equal(applyChanges(before, [change]), after, `${before} → ${after}`); for (const at of [change.from, change.to]) { const code = before.charCodeAt(at); assert.ok(!(code >= 0xdc00 && code <= 0xdfff), `change edge ${at} lands inside a pair in ${before}`); } } assert.equal(safeChange('same', 'same'), undefined); }); test('applyToYText reaches the text and syncs between copies', () => { const a = new Y.Doc(); const b = new Y.Doc(); a.on('update', (u: Uint8Array) => Y.applyUpdate(b, u)); const text = a.getMap('files'); const t = new Y.Text(); text.set('main.archify', t); applyToYText(t, 'model:\n id: shop\n'); applyToYText(t, '# 😀\nmodel:\n id: shop\n'); applyToYText(t, '# 😃\nmodel:\n id: shop2\n'); assert.equal(b.getMap('files').get('main.archify')!.toString(), '# 😃\nmodel:\n id: shop2\n'); assert.equal(applyToYText(t, '# 😃\nmodel:\n id: shop2\n'), false); }); test('the offline copy keeps every update and survives a torn write', () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-sync-')); try { const doc = new Y.Doc(); const store = new OfflineStore(dir, 'space'); assert.equal(store.exists(), false); doc.on('update', (u: Uint8Array) => store.append(u)); doc.getText('t').insert(0, 'one '); doc.getText('t').insert(4, 'two'); assert.equal(store.pending, 2); // A crash in the middle of the next append. fs.appendFileSync(path.join(dir, 'space.log'), Buffer.from([200, 0, 0, 0, 1, 2])); const again = new Y.Doc(); for (const u of new OfflineStore(dir, 'space').load()) Y.applyUpdate(again, u); assert.equal(again.getText('t').toString(), 'one two'); store.compact(Y.encodeStateAsUpdate(doc)); assert.equal(store.pending, 0); const compacted = new Y.Doc(); for (const u of new OfflineStore(dir, 'space').load()) Y.applyUpdate(compacted, u); assert.equal(compacted.getText('t').toString(), 'one two'); store.remove(); assert.equal(store.exists(), false); } finally { fs.rmSync(dir, { recursive: true, force: true }); } }); test('server addresses: https, or http only on this machine', () => { assert.equal(checkServerUrl('https://archify.example.com/').url, 'https://archify.example.com'); assert.equal(checkServerUrl('http://localhost:8080').url, 'http://localhost:8080'); assert.equal(checkServerUrl('http://127.0.0.1:8080/sync/').url, 'http://127.0.0.1:8080/sync'); assert.ok(checkServerUrl('http://archify.example.com').problem); assert.ok(checkServerUrl('ftp://x').problem); assert.ok(checkServerUrl('nonsense').problem); assert.equal(syncSocketUrl('https://a.example', 'id'), 'wss://a.example/v1/spaces/id/sync?v=1'); }); test('invite links in both forms', () => { const token = 'AbCdEfGhIjKlMnOpQrStUvWxYz0123456789_-abcde'; assert.deepEqual(parseInviteLink(`https://sync.example/join/${token}`), { server: 'https://sync.example', invite: token }); assert.deepEqual(parseInviteLink(`https://example.com/archify/join/${token}`), { server: 'https://example.com/archify', invite: token }); const ext = `vscode://root-at-skic.archify-vscode-ext/join?server=${encodeURIComponent('https://sync.example/')}&invite=${token}`; assert.deepEqual(parseInviteLink(ext), { server: 'https://sync.example', invite: token }); assert.equal(parseInviteLink('https://example.com/other'), undefined); assert.equal(parseInviteLink('not a link'), undefined); }); test('link files round-trip and refuse junk', () => { const link = { server: 'https://sync.example', space: '0198f3a2-1b2c-7d3e-8f40-123456789abc', name: 'Shop' }; assert.deepEqual(parseLink(formatLink(link)), link); assert.equal(parseLink('{"server":"https://x","space":"../../etc"}'), undefined); assert.equal(parseLink('not json'), undefined); });