/** * 操作账本与调度基础结构的单元测试。账本保存依赖、冻结请求和执行证据;队列及堆只决定处理顺序。 * 这里直接操作内存账本,验证状态机与压缩规则,不代表网络或持久存储已经完成。 */ 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'; // 固定的文本元素快照;完整几何和 incarnation 让字段压缩与身份检查有明确基线。 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 }); // 把 create 操作冻结成固定请求;后续状态转换必须保留同一请求 ID 和请求体。 const frozen = ledger => ledger.freeze({ id: 'request', operationIds: ['create'], canvasIds: ['root'], expectedVersions: { root: 1 }, payload: { changes: [] } }); // 先验证大量 FIFO 出入队后的顺序,再验证最小堆以截止时间为主、序号为次序,空容器均返回 undefined。 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 } }; // move 替换整个 geometry 字段而非整份节点;压缩后仍需保留此前 rename 写入的独立 data.title。 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 }]), []); }); // 未知和已受理都保留“可能执行”证据并阻塞依赖删除;只有 applied 才放行,重派发仍使用原请求体。 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'); // create/delete 在这个状态机样本中都没有实际 changes;这里只测依赖、风险和冻结身份。 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); }); // 拒绝缺失依赖及同 ID 不同意图;旧代次落盘完成不能把后来新增的操作一并标记为持久化。 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/); });