Files
archify-vscode-ext/test/sync.test.ts
T
root-at-skic c99f3e5fe8 0.10.0: Archify Sync, optional live collaboration on a model
Several people edit one model at once through a server you run yourself
(archify-vscode-ext-sync); the files stay ordinary files on each disk.
Sign-in through the browser, offline-first with later merge, teammates'
cursors in the Source view and text editors, and a Shared section on the
model page with people, versions, Compare and Restore.

Without a server nothing changes: the sync code is a separate bundle that
loads only when a model is shared, after a sign-in, or when a sync command
or invite link is used.
2026-11-06 22:55:01 +02:00

102 lines
4.6 KiB
TypeScript

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<Y.Text>('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<Y.Text>('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);
});