60 lines
4.5 KiB
JavaScript
60 lines
4.5 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { createDeque, createMinHeap } from '../../src/lib/canvas-runtime/structures.ts';
|
|
import { createLedger, compressChanges } from '../../src/lib/canvas-runtime/ledger.ts';
|
|
const node = { id: 'a', incarnation: 1, ownerCanvasId: 'root', componentId: 'text', dataVersion: 1, data: { title: 'a', text: '' }, structure: 'node', geometry: { x: 0, y: 0, width: 20, height: 20, rotation: 0 } };
|
|
const intent = (id, changes = [], dependencies = []) => ({ id, operationType: 'document.change', operationSchemaVersion: 1, sequence: 1, origin: 'user', changes, dependencies });
|
|
const frozen = ledger => ledger.freeze({ id: 'request', operationIds: ['create'], canvasIds: ['root'], expectedVersions: { root: 1 }, payload: { changes: [] } });
|
|
|
|
test('FIFO retains order across compaction; retry heap orders deadline then sequence', () => {
|
|
const queue = createDeque(); for (let i = 0; i < 4000; i++) queue.push(i);
|
|
for (let i = 0; i < 4000; i++) assert.equal(queue.pop(), i);
|
|
assert.equal(queue.size, 0); assert.equal(queue.pop(), undefined);
|
|
const heap = createMinHeap((a, b) => a.due - b.due || a.sequence - b.sequence);
|
|
for (const value of [{ due: 5, sequence: 2 }, { due: 2, sequence: 9 }, { due: 5, sequence: 1 }, { due: 1, sequence: 8 }]) heap.push(value);
|
|
assert.deepEqual([heap.pop(), heap.pop(), heap.pop(), heap.pop()].map(item => [item.due, item.sequence]), [[1,8],[2,9],[5,1],[5,2]]);
|
|
assert.equal(heap.pop(), undefined);
|
|
});
|
|
|
|
test('wholly unsent create-data-delete disappears but a position update preserves the title', () => {
|
|
const create = { kind: 'element', id: 'a', before: null, after: node };
|
|
const rename = { kind: 'field', target: { id: 'a', incarnation: 1 }, field: 'data.title', before: 'a', after: 'renamed', beforePresent: true, afterPresent: true };
|
|
const move = { kind: 'field', target: { id: 'a', incarnation: 1 }, field: 'geometry', before: node.geometry, after: { ...node.geometry, x: 100 } };
|
|
const compressed = compressChanges([create, rename, move]);
|
|
assert.equal(compressed.length, 1); assert.equal(compressed[0].after.data.title, 'renamed');
|
|
assert.deepEqual(compressChanges([...compressed, { kind: 'element', id: 'a', before: compressed[0].after, after: null }]), []);
|
|
});
|
|
|
|
test('frozen identity remains exact after unknown/accepted outcomes and deletion is a separate pending operation', () => {
|
|
const ledger = createLedger(); ledger.add(intent('create'), { a: 'absent' });
|
|
const request = frozen(ledger), body = request.body;
|
|
ledger.markDurable(ledger.getSnapshot().generation); ledger.markDispatch('request');
|
|
ledger.result('request', 'unresolved', 'transport_unknown');
|
|
ledger.add(intent('delete', [], ['create']), { a: 'exists' });
|
|
assert.equal(ledger.eligible('delete'), false); assert.equal(ledger.possiblyApplied('request'), true);
|
|
ledger.result('request', 'accepted');
|
|
assert.equal(ledger.eligible('delete'), false); assert.equal(ledger.getSnapshot().requests.get('request').body, body);
|
|
ledger.markDispatch('request'); assert.equal(ledger.getSnapshot().requests.get('request').body, body);
|
|
ledger.result('request', 'applied'); assert.equal(ledger.eligible('delete'), true);
|
|
});
|
|
|
|
test('dependencies are explicit, duplicate identities conflict and late durable completion does not mark newer work durable', () => {
|
|
const ledger = createLedger();
|
|
assert.throws(() => ledger.add(intent('child', [], ['missing']), {}), /dependency_missing/);
|
|
ledger.add(intent('first'), {}); const oldGeneration = ledger.getSnapshot().generation;
|
|
ledger.add(intent('second', [], ['first']), {}); ledger.markDurable(oldGeneration);
|
|
assert.ok(ledger.getSnapshot().durableGeneration < ledger.getSnapshot().generation);
|
|
assert.throws(() => ledger.add(intent('first', [], ['second']), {}), /operation_identity_conflict/);
|
|
assert.equal(ledger.eligible('second'), false);
|
|
});
|
|
|
|
test('recovery retains possible application; explicit terminal zero-write does not pretend dependent writes succeeded', () => {
|
|
const original = createLedger(); original.add(intent('create'), {}); frozen(original); original.markDispatch('request');
|
|
const restored = createLedger(original.getSnapshot());
|
|
assert.equal(restored.possiblyApplied('request'), true);
|
|
restored.result('request', 'not-applied', 'permission');
|
|
assert.equal(restored.possiblyApplied('request'), false);
|
|
assert.equal(restored.getSnapshot().operations.get('create').phase, 'blocked');
|
|
assert.throws(() => restored.result('request', 'applied'), /receipt_conflict/);
|
|
});
|