[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
211
domain/convergentSync/convergentSync.property.test.ts
Normal file
211
domain/convergentSync/convergentSync.property.test.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import fc from 'fast-check';
|
||||
|
||||
import {
|
||||
applyConvergentMutations,
|
||||
createConvergentSyncState,
|
||||
materializeConvergentSyncState,
|
||||
mergeConvergentSyncStates,
|
||||
serializeConvergentSyncState,
|
||||
type ConvergentMutation,
|
||||
type JsonValue,
|
||||
} from './index.ts';
|
||||
|
||||
const jsonValueArbitrary: fc.Arbitrary<JsonValue> = fc.oneof(
|
||||
fc.string({ maxLength: 20 }),
|
||||
fc.integer({ min: -1_000, max: 1_000 }),
|
||||
fc.boolean(),
|
||||
fc.array(fc.integer({ min: 0, max: 20 }), { maxLength: 5 }),
|
||||
fc.record({ enabled: fc.boolean(), label: fc.string({ maxLength: 10 }) }),
|
||||
);
|
||||
|
||||
const settingPathArbitrary: fc.Arbitrary<string[]> = fc.constantFrom(
|
||||
'theme',
|
||||
'terminalRoot',
|
||||
'fontSize',
|
||||
'palette',
|
||||
).map((value) => {
|
||||
if (value === 'theme') return ['theme'];
|
||||
if (value === 'terminalRoot') return ['terminal'];
|
||||
return ['terminal', value];
|
||||
});
|
||||
|
||||
const mutationArbitrary: fc.Arbitrary<ConvergentMutation> = fc.oneof(
|
||||
fc.record({
|
||||
kind: fc.constant<'setting-set'>('setting-set'),
|
||||
path: settingPathArbitrary,
|
||||
value: jsonValueArbitrary,
|
||||
}),
|
||||
fc.record({
|
||||
kind: fc.constant<'setting-delete'>('setting-delete'),
|
||||
path: settingPathArbitrary,
|
||||
}),
|
||||
fc.record({
|
||||
kind: fc.constant<'entity-field-set'>('entity-field-set'),
|
||||
collection: fc.constant('hosts'),
|
||||
entityId: fc.constantFrom('host-0', 'host-1', 'host-2'),
|
||||
field: fc.constantFrom('label', 'hostname', 'tags'),
|
||||
value: jsonValueArbitrary,
|
||||
}),
|
||||
fc.record({
|
||||
kind: fc.constant<'entity-delete'>('entity-delete'),
|
||||
collection: fc.constant('hosts'),
|
||||
entityId: fc.constantFrom('host-0', 'host-1', 'host-2'),
|
||||
}),
|
||||
fc.record({
|
||||
kind: fc.constant<'string-entry-add'>('string-entry-add'),
|
||||
collection: fc.constant('customGroups'),
|
||||
value: fc.constantFrom('alpha', 'beta', 'gamma'),
|
||||
position: fc.integer({ min: 0, max: 10 }),
|
||||
}),
|
||||
fc.record({
|
||||
kind: fc.constant<'string-entry-delete'>('string-entry-delete'),
|
||||
collection: fc.constant('customGroups'),
|
||||
value: fc.constantFrom('alpha', 'beta', 'gamma'),
|
||||
}),
|
||||
);
|
||||
|
||||
const mutationListArbitrary = fc.array(mutationArbitrary, { maxLength: 16 });
|
||||
|
||||
function replica(deviceId: string, mutations: ConvergentMutation[], time: number) {
|
||||
return applyConvergentMutations(
|
||||
createConvergentSyncState(),
|
||||
deviceId,
|
||||
mutations,
|
||||
time,
|
||||
);
|
||||
}
|
||||
|
||||
test('merge is commutative', () => {
|
||||
fc.assert(fc.property(
|
||||
mutationListArbitrary,
|
||||
mutationListArbitrary,
|
||||
(leftMutations, rightMutations) => {
|
||||
const left = replica('device-a', leftMutations, 100);
|
||||
const right = replica('device-b', rightMutations, 100);
|
||||
const leftRight = mergeConvergentSyncStates(left, right);
|
||||
const rightLeft = mergeConvergentSyncStates(right, left);
|
||||
assert.equal(
|
||||
serializeConvergentSyncState(leftRight),
|
||||
serializeConvergentSyncState(rightLeft),
|
||||
);
|
||||
assert.deepEqual(
|
||||
materializeConvergentSyncState(leftRight),
|
||||
materializeConvergentSyncState(rightLeft),
|
||||
);
|
||||
},
|
||||
), { numRuns: 150 });
|
||||
});
|
||||
|
||||
test('merge is associative', () => {
|
||||
fc.assert(fc.property(
|
||||
mutationListArbitrary,
|
||||
mutationListArbitrary,
|
||||
mutationListArbitrary,
|
||||
(aMutations, bMutations, cMutations) => {
|
||||
const a = replica('device-a', aMutations, 100);
|
||||
const b = replica('device-b', bMutations, 100);
|
||||
const c = replica('device-c', cMutations, 100);
|
||||
const leftGrouped = mergeConvergentSyncStates(
|
||||
mergeConvergentSyncStates(a, b),
|
||||
c,
|
||||
);
|
||||
const rightGrouped = mergeConvergentSyncStates(
|
||||
a,
|
||||
mergeConvergentSyncStates(b, c),
|
||||
);
|
||||
assert.equal(
|
||||
serializeConvergentSyncState(leftGrouped),
|
||||
serializeConvergentSyncState(rightGrouped),
|
||||
);
|
||||
},
|
||||
), { numRuns: 120 });
|
||||
});
|
||||
|
||||
test('merge is idempotent', () => {
|
||||
fc.assert(fc.property(mutationListArbitrary, (mutations) => {
|
||||
const state = replica('device-a', mutations, 100);
|
||||
assert.equal(
|
||||
serializeConvergentSyncState(mergeConvergentSyncStates(state, state)),
|
||||
serializeConvergentSyncState(state),
|
||||
);
|
||||
}), { numRuns: 150 });
|
||||
});
|
||||
|
||||
test('causal parent deletion removes every generated descendant', () => {
|
||||
fc.assert(fc.property(
|
||||
fc.array(
|
||||
fc.tuple(
|
||||
fc.constantFrom('fontSize', 'fontFamily', 'palette', 'cursor'),
|
||||
jsonValueArbitrary,
|
||||
),
|
||||
{ minLength: 1, maxLength: 12 },
|
||||
),
|
||||
(leaves) => {
|
||||
const populated = applyConvergentMutations(
|
||||
createConvergentSyncState(),
|
||||
'device-a',
|
||||
leaves.map(([leaf, value]) => ({
|
||||
kind: 'setting-set' as const,
|
||||
path: ['terminal', leaf],
|
||||
value,
|
||||
})),
|
||||
100,
|
||||
);
|
||||
const deleted = applyConvergentMutations(populated, 'device-a', [{
|
||||
kind: 'setting-delete',
|
||||
path: ['terminal'],
|
||||
}], 101);
|
||||
|
||||
assert.equal(
|
||||
Object.hasOwn(materializeConvergentSyncState(deleted).settings, 'terminal'),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
Object.hasOwn(
|
||||
materializeConvergentSyncState(
|
||||
mergeConvergentSyncStates(populated, deleted),
|
||||
).settings,
|
||||
'terminal',
|
||||
),
|
||||
false,
|
||||
);
|
||||
},
|
||||
), { numRuns: 100 });
|
||||
});
|
||||
|
||||
test('2-20 offline replicas converge across reordering, partitions, and duplicates', () => {
|
||||
fc.assert(fc.property(
|
||||
fc.array(mutationListArbitrary, { minLength: 2, maxLength: 20 }),
|
||||
fc.array(fc.integer(), { minLength: 20, maxLength: 20 }),
|
||||
(replicaMutations, orderKeys) => {
|
||||
const replicas = replicaMutations.map((mutations, index) =>
|
||||
replica(`device-${index.toString().padStart(2, '0')}`, mutations, 100 + index),
|
||||
);
|
||||
const baseline = replicas.reduce(mergeConvergentSyncStates);
|
||||
const order = replicas
|
||||
.map((state, index) => ({ state, key: orderKeys[index] ?? index }))
|
||||
.sort((left, right) => left.key - right.key)
|
||||
.map((item) => item.state);
|
||||
const reordered = order.reduce(mergeConvergentSyncStates);
|
||||
const split = Math.max(1, Math.floor(order.length / 2));
|
||||
const leftPartition = order.slice(0, split).reduce(mergeConvergentSyncStates);
|
||||
const rightPartition = order.slice(split).reduce(mergeConvergentSyncStates);
|
||||
const partitioned = mergeConvergentSyncStates(
|
||||
mergeConvergentSyncStates(leftPartition, leftPartition),
|
||||
mergeConvergentSyncStates(rightPartition, rightPartition),
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
serializeConvergentSyncState(reordered),
|
||||
serializeConvergentSyncState(baseline),
|
||||
);
|
||||
assert.equal(
|
||||
serializeConvergentSyncState(partitioned),
|
||||
serializeConvergentSyncState(baseline),
|
||||
);
|
||||
},
|
||||
), { numRuns: 60 });
|
||||
});
|
||||
Reference in New Issue
Block a user