Files
NetMesh/application/state/windowOpacitySync.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

200 lines
7.2 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
parseWindowOpacityRecord,
serializeWindowOpacityRecord,
shouldApplyWindowOpacityRecord,
shouldBroadcastWindowOpacityChange,
type WindowOpacityMutationSource,
type WindowOpacityRecord,
} from './windowOpacitySync.ts';
/**
* Minimal model of the settings ↔ main opacity sync loop that caused #2018.
* Models both IPC rebroadcast and stale localStorage overwrites.
*/
function simulateOpacityDrag(options: {
shouldBroadcast: (
source: WindowOpacityMutationSource,
persistMounted: boolean,
) => { shouldBroadcast: boolean; nextSource: WindowOpacityMutationSource };
shouldApply: (current: WindowOpacityRecord, incoming: WindowOpacityRecord) => boolean;
versioned: boolean;
}): { settingsValues: number[]; mainValues: number[]; storageWrites: string[] } {
let settings: WindowOpacityRecord = { opacity: 1, version: 0 };
let main: WindowOpacityRecord = { opacity: 1, version: 0 };
let settingsSource: WindowOpacityMutationSource = 'local';
let mainSource: WindowOpacityMutationSource = 'local';
let storage = serializeWindowOpacityRecord(settings);
const settingsValues: number[] = [];
const mainValues: number[] = [];
const storageWrites: string[] = [];
const pendingIpc: Array<{ to: 'settings' | 'main'; record: WindowOpacityRecord }> = [];
const writeStorage = (record: WindowOpacityRecord) => {
const next = options.versioned
? serializeWindowOpacityRecord(record)
: String(record.opacity);
if (next === storage) return;
storage = next;
storageWrites.push(next);
// Cross-window StorageEvent: deliver to the other window.
};
const applyLocal = (window: 'settings' | 'main', opacity: number) => {
const bump = (prev: WindowOpacityRecord): WindowOpacityRecord => (
options.versioned
? { opacity, version: prev.version + 1 }
: { opacity, version: 0 }
);
if (window === 'settings') {
settingsSource = 'local';
settings = bump(settings);
settingsValues.push(settings.opacity);
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);
mainValues.push(main.opacity);
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: WindowOpacityRecord) => {
if (window === 'settings') {
if (!options.shouldApply(settings, record)) return;
settingsSource = 'incoming';
settings = { ...record };
settingsValues.push(settings.opacity);
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.opacity);
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;
}
};
// Replay storage writes onto the peer after each local burst, matching
// Electron's cross-window StorageEvent delivery.
const deliverStorageTo = (window: 'settings' | 'main') => {
const record = options.versioned
? parseWindowOpacityRecord(JSON.parse(storage))
: parseWindowOpacityRecord(storage);
applyIncoming(window, record);
};
applyLocal('settings', 0.5);
deliverStorageTo('main');
applyLocal('settings', 0.65);
// Delayed IPC for the older 0.5 update arrives after 0.65 was already local.
const delayed = pendingIpc.shift();
flushIpc();
if (delayed) applyIncoming(delayed.to, delayed.record);
deliverStorageTo('settings');
flushIpc();
return { settingsValues, mainValues, storageWrites };
}
test('parseWindowOpacityRecord accepts legacy plain numbers and versioned JSON', () => {
assert.deepEqual(parseWindowOpacityRecord('0.85'), { opacity: 0.85, version: 0 });
assert.deepEqual(parseWindowOpacityRecord(0.7), { opacity: 0.7, version: 0 });
assert.deepEqual(
parseWindowOpacityRecord({ opacity: 0.5, version: 3 }),
{ opacity: 0.5, version: 3 },
);
assert.deepEqual(
parseWindowOpacityRecord('{"opacity":0.55,"version":9}'),
{ opacity: 0.55, version: 9 },
);
assert.equal(parseWindowOpacityRecord('bad').opacity, 1);
});
test('shouldApplyWindowOpacityRecord ignores stale revisions', () => {
const current = { opacity: 0.65, version: 2 };
assert.equal(shouldApplyWindowOpacityRecord(current, { opacity: 0.5, version: 1 }), false);
assert.equal(shouldApplyWindowOpacityRecord(current, { opacity: 0.7, version: 3 }), true);
assert.equal(shouldApplyWindowOpacityRecord(current, { opacity: 0.65, version: 2 }), false);
});
test('shouldBroadcastWindowOpacityChange suppresses incoming rebroadcasts', () => {
assert.deepEqual(
shouldBroadcastWindowOpacityChange('incoming', true),
{ shouldBroadcast: false, nextSource: 'local' },
);
assert.deepEqual(
shouldBroadcastWindowOpacityChange('local', true),
{ shouldBroadcast: true, nextSource: 'local' },
);
assert.deepEqual(
shouldBroadcastWindowOpacityChange('local', false),
{ shouldBroadcast: false, nextSource: 'local' },
);
});
test('legacy unversioned always-broadcast opacity sync oscillates during a fast drag', () => {
const alwaysBroadcast = (
_source: WindowOpacityMutationSource,
persistMounted: boolean,
) => ({
shouldBroadcast: persistMounted,
nextSource: 'local' as const,
});
const alwaysApply = () => true;
const { settingsValues } = simulateOpacityDrag({
shouldBroadcast: alwaysBroadcast,
shouldApply: alwaysApply,
versioned: false,
});
const unique = new Set(settingsValues);
assert.ok(
unique.has(0.5) && unique.has(0.65) && settingsValues.length > 2,
`expected oscillation between 0.5 and 0.65, got ${settingsValues.join(',')}`,
);
});
test('versioned opacity sync ignores stale peer echoes during a fast drag', () => {
const { settingsValues, mainValues } = simulateOpacityDrag({
shouldBroadcast: shouldBroadcastWindowOpacityChange,
shouldApply: shouldApplyWindowOpacityRecord,
versioned: true,
});
assert.deepEqual(settingsValues, [0.5, 0.65]);
assert.ok(mainValues.includes(0.65));
assert.equal(mainValues.includes(0.5) && mainValues[mainValues.length - 1] === 0.5, false);
});
test('serializeWindowOpacityRecord round-trips through parse', () => {
const raw = serializeWindowOpacityRecord({ opacity: 0.55, version: 9 });
assert.deepEqual(parseWindowOpacityRecord(JSON.parse(raw)), { opacity: 0.55, version: 9 });
});