Files
NetMesh/application/state/terminalFontSizeSync.test.ts
zhaolei 3c72efcb7f
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
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

366 lines
12 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
createLocalTerminalFontSizeRecord,
parseTerminalFontSizeRecord,
resolveAuthoritativeTerminalFontSizeStorage,
resolveIncomingTerminalFontSize,
resolveTerminalFontSizeStorage,
serializeTerminalFontSizeRecord,
shouldApplyTerminalFontSizeRecord,
shouldBroadcastTerminalFontSizeChange,
type TerminalFontSizeMutationSource,
type TerminalFontSizeRecord,
} from './terminalFontSizeSync.ts';
/**
* Minimal model of the settings ↔ main font-size sync loop that caused #2689.
* Models both IPC rebroadcast and stale localStorage overwrites.
*/
function simulateFontSizeClicks(options: {
shouldBroadcast: (
source: TerminalFontSizeMutationSource,
persistMounted: boolean,
) => { shouldBroadcast: boolean; nextSource: TerminalFontSizeMutationSource };
shouldApply: (current: TerminalFontSizeRecord, incoming: TerminalFontSizeRecord) => boolean;
versioned: boolean;
}): { settingsValues: number[]; mainValues: number[]; storageWrites: string[] } {
let settings: TerminalFontSizeRecord = { fontSize: 16, version: 0, origin: 'legacy' };
let main: TerminalFontSizeRecord = { fontSize: 16, version: 0, origin: 'legacy' };
let settingsSource: TerminalFontSizeMutationSource = 'local';
let mainSource: TerminalFontSizeMutationSource = 'local';
let storage = options.versioned
? serializeTerminalFontSizeRecord(settings)
: String(settings.fontSize);
const settingsValues: number[] = [];
const mainValues: number[] = [];
const storageWrites: string[] = [];
const pendingIpc: Array<{ to: 'settings' | 'main'; record: TerminalFontSizeRecord }> = [];
const writeStorage = (record: TerminalFontSizeRecord) => {
const next = options.versioned
? serializeTerminalFontSizeRecord(record)
: String(record.fontSize);
if (next === storage) return;
storage = next;
storageWrites.push(next);
};
const applyLocal = (window: 'settings' | 'main', fontSize: number) => {
const bump = (
prev: TerminalFontSizeRecord,
origin: string,
): TerminalFontSizeRecord => (
options.versioned
? { fontSize, version: prev.version + 1, origin }
: { fontSize, version: 0, origin: 'legacy' }
);
if (window === 'settings') {
settingsSource = 'local';
settings = bump(settings, 'settings-window');
settingsValues.push(settings.fontSize);
writeStorage(settings);
const decision = options.shouldBroadcast(settingsSource, true);
settingsSource = decision.nextSource;
if (decision.shouldBroadcast) pendingIpc.push({ to: 'main', record: { ...settings } });
return;
}
mainSource = 'local';
main = bump(main, 'main-window');
mainValues.push(main.fontSize);
writeStorage(main);
const decision = options.shouldBroadcast(mainSource, true);
mainSource = decision.nextSource;
if (decision.shouldBroadcast) pendingIpc.push({ to: 'settings', record: { ...main } });
};
const applyIncoming = (window: 'settings' | 'main', record: TerminalFontSizeRecord) => {
if (window === 'settings') {
if (!options.shouldApply(settings, record)) return;
settingsSource = 'incoming';
settings = { ...record };
settingsValues.push(settings.fontSize);
writeStorage(settings);
const decision = options.shouldBroadcast(settingsSource, true);
settingsSource = decision.nextSource;
if (decision.shouldBroadcast) pendingIpc.push({ to: 'main', record: { ...settings } });
return;
}
if (!options.shouldApply(main, record)) return;
mainSource = 'incoming';
main = { ...record };
mainValues.push(main.fontSize);
writeStorage(main);
const decision = options.shouldBroadcast(mainSource, true);
mainSource = decision.nextSource;
if (decision.shouldBroadcast) pendingIpc.push({ to: 'settings', record: { ...main } });
};
const flushIpc = () => {
while (pendingIpc.length > 0) {
const next = pendingIpc.shift()!;
applyIncoming(next.to, next.record);
if (settingsValues.length > 40) break;
}
};
const deliverStorageTo = (window: 'settings' | 'main') => {
const record = parseTerminalFontSizeRecord(storage);
applyIncoming(window, record);
};
// Match reporter steps: + (16→17) then - while a delayed peer echo of 17
// can still race with a later local 15.
applyLocal('settings', 17);
deliverStorageTo('main');
applyLocal('settings', 15);
const delayed = pendingIpc.shift();
flushIpc();
if (delayed) applyIncoming(delayed.to, delayed.record);
deliverStorageTo('settings');
flushIpc();
return { settingsValues, mainValues, storageWrites };
}
test('parseTerminalFontSizeRecord accepts legacy plain numbers and versioned records', () => {
assert.deepEqual(
parseTerminalFontSizeRecord('16'),
{ fontSize: 16, version: 0, origin: 'legacy' },
);
assert.deepEqual(
parseTerminalFontSizeRecord(14),
{ fontSize: 14, version: 0, origin: 'legacy' },
);
assert.deepEqual(
parseTerminalFontSizeRecord({ fontSize: 18, version: 3, origin: 'window-a' }),
{ fontSize: 18, version: 3, origin: 'window-a' },
);
assert.deepEqual(
parseTerminalFontSizeRecord('{"fontSize":15,"version":9,"origin":"window-b"}'),
{ fontSize: 15, version: 9, origin: 'window-b' },
);
assert.deepEqual(
parseTerminalFontSizeRecord('17|12|settings-window'),
{ fontSize: 17, version: 12, origin: 'settings-window' },
);
assert.equal(parseTerminalFontSizeRecord('bad').fontSize, 14);
});
test('shouldApplyTerminalFontSizeRecord ignores stale revisions', () => {
const current = { fontSize: 15, version: 2, origin: 'window-a' };
assert.equal(
shouldApplyTerminalFontSizeRecord(
current,
{ fontSize: 17, version: 1, origin: 'window-b' },
),
false,
);
assert.equal(
shouldApplyTerminalFontSizeRecord(
current,
{ fontSize: 18, version: 3, origin: 'window-b' },
),
true,
);
assert.equal(shouldApplyTerminalFontSizeRecord(current, { ...current }), false);
});
test('simultaneous writers converge through storage in either persistence order', () => {
const run = (order: Array<'settings' | 'main'>) => {
const initial: TerminalFontSizeRecord = {
fontSize: 16,
version: 1,
origin: 'initial-window',
};
let storage = serializeTerminalFontSizeRecord(initial);
let settings = createLocalTerminalFontSizeRecord(
initial,
storage,
17,
'settings-window',
10,
);
let main = createLocalTerminalFontSizeRecord(
initial,
storage,
15,
'main-window',
10,
);
for (const writer of order) {
const current = writer === 'settings' ? settings : main;
const resolution = resolveTerminalFontSizeStorage(current, storage);
if (resolution.shouldAdopt) {
if (writer === 'settings') settings = resolution.record;
else main = resolution.record;
} else if (resolution.shouldPersist) {
storage = resolution.serializedRecord;
}
}
const settingsResolution = resolveTerminalFontSizeStorage(settings, storage);
const mainResolution = resolveTerminalFontSizeStorage(main, storage);
settings = settingsResolution.record;
main = mainResolution.record;
assert.equal(settingsResolution.shouldPersist, false);
assert.equal(mainResolution.shouldPersist, false);
assert.deepEqual(settings, main);
assert.equal(resolveTerminalFontSizeStorage(settings, storage).shouldAdopt, false);
assert.equal(resolveTerminalFontSizeStorage(settings, storage).shouldPersist, false);
return settings;
};
assert.deepEqual(run(['settings', 'main']), run(['main', 'settings']));
});
test('a stale window creates and persists a revision newer than shared storage', () => {
const stored = serializeTerminalFontSizeRecord({
fontSize: 18,
version: 7,
origin: 'main-window',
});
const local = createLocalTerminalFontSizeRecord(
{ fontSize: 15, version: 2, origin: 'settings-window' },
stored,
16,
'settings-window',
5,
);
assert.equal(local.version, 8);
const resolution = resolveTerminalFontSizeStorage(local, stored);
assert.equal(resolution.shouldAdopt, false);
assert.equal(resolution.shouldPersist, true);
assert.deepEqual(parseTerminalFontSizeRecord(resolution.serializedRecord), local);
});
test('a rejected losing record repairs shared storage back to the current winner', () => {
const winner: TerminalFontSizeRecord = {
fontSize: 17,
version: 10,
origin: 'settings-window',
};
const loser: TerminalFontSizeRecord = {
fontSize: 15,
version: 10,
origin: 'main-window',
};
const loserRaw = serializeTerminalFontSizeRecord(loser);
const rejected = resolveIncomingTerminalFontSize(winner, loser, loserRaw);
assert.equal(rejected.shouldUpdate, false);
assert.deepEqual(rejected.record, winner);
assert.equal(
rejected.repairSerializedRecord,
serializeTerminalFontSizeRecord(winner),
);
const stable = resolveIncomingTerminalFontSize(
winner,
loser,
rejected.repairSerializedRecord,
);
assert.equal(stable.shouldUpdate, false);
assert.equal(stable.repairSerializedRecord, null);
});
test('a delayed persistence effect arbitrates from the latest authoritative record', () => {
const renderedWhenEffectWasScheduled: TerminalFontSizeRecord = {
fontSize: 14,
version: 9,
origin: 'old-window',
};
const winner: TerminalFontSizeRecord = {
fontSize: 17,
version: 10,
origin: 'settings-window',
};
const loser: TerminalFontSizeRecord = {
fontSize: 15,
version: 10,
origin: 'main-window',
};
const currentRef = { current: renderedWhenEffectWasScheduled };
currentRef.current = winner;
const resolution = resolveAuthoritativeTerminalFontSizeStorage(
currentRef,
serializeTerminalFontSizeRecord(loser),
);
assert.equal(resolution.shouldAdopt, false);
assert.equal(resolution.shouldPersist, true);
assert.deepEqual(resolution.record, winner);
assert.deepEqual(parseTerminalFontSizeRecord(resolution.serializedRecord), winner);
});
test('shouldBroadcastTerminalFontSizeChange suppresses incoming rebroadcasts', () => {
assert.deepEqual(
shouldBroadcastTerminalFontSizeChange('incoming', true),
{ shouldBroadcast: false, nextSource: 'local' },
);
assert.deepEqual(
shouldBroadcastTerminalFontSizeChange('local', true),
{ shouldBroadcast: true, nextSource: 'local' },
);
assert.deepEqual(
shouldBroadcastTerminalFontSizeChange('local', false),
{ shouldBroadcast: false, nextSource: 'local' },
);
});
test('legacy unversioned always-broadcast font size sync oscillates during rapid +/- clicks', () => {
const alwaysBroadcast = (
_source: TerminalFontSizeMutationSource,
persistMounted: boolean,
) => ({
shouldBroadcast: persistMounted,
nextSource: 'local' as const,
});
const alwaysApply = () => true;
const { settingsValues } = simulateFontSizeClicks({
shouldBroadcast: alwaysBroadcast,
shouldApply: alwaysApply,
versioned: false,
});
const unique = new Set(settingsValues);
assert.ok(
unique.has(17) && unique.has(15) && settingsValues.length > 2,
`expected oscillation between 17 and 15, got ${settingsValues.join(',')}`,
);
});
test('versioned font size sync ignores stale peer echoes during rapid +/- clicks', () => {
const { settingsValues, mainValues } = simulateFontSizeClicks({
shouldBroadcast: shouldBroadcastTerminalFontSizeChange,
shouldApply: shouldApplyTerminalFontSizeRecord,
versioned: true,
});
assert.deepEqual(settingsValues, [17, 15]);
assert.ok(mainValues.includes(15));
assert.equal(mainValues.includes(17) && mainValues[mainValues.length - 1] === 17, false);
});
test('serializeTerminalFontSizeRecord round-trips through parse', () => {
const record = { fontSize: 18, version: 9, origin: 'window|with delimiter' };
const raw = serializeTerminalFontSizeRecord(record);
assert.deepEqual(parseTerminalFontSizeRecord(raw), record);
});
test('serialized records remain readable by the old parseInt storage reader', () => {
const raw = serializeTerminalFontSizeRecord({
fontSize: 18,
version: 9,
origin: 'settings-window',
});
assert.equal(parseInt(raw, 10), 18);
});