72 lines
5.7 KiB
JavaScript
72 lines
5.7 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';
|
|
// 固定的文本元素快照;完整几何和 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 } };
|
|
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');
|
|
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/);
|
|
});
|