192 lines
8.0 KiB
TypeScript
192 lines
8.0 KiB
TypeScript
|
|
import test from 'node:test';
|
||
|
|
import assert from 'node:assert/strict';
|
||
|
|
|
||
|
|
import { isAutoLaunchResultTrustworthy } from './systemSettingsEffects.ts';
|
||
|
|
|
||
|
|
test('isAutoLaunchResultTrustworthy trusts a successful read/write result', () => {
|
||
|
|
assert.equal(isAutoLaunchResultTrustworthy({ success: true }), true);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('isAutoLaunchResultTrustworthy rejects a transient failure result', () => {
|
||
|
|
assert.equal(
|
||
|
|
isAutoLaunchResultTrustworthy({ success: false }),
|
||
|
|
false,
|
||
|
|
'success:false means the OS state is unknown, not confirmed disabled',
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('isAutoLaunchResultTrustworthy defaults to trusting a result with no success field', () => {
|
||
|
|
// Defensive default for any future/alternate caller that omits the field
|
||
|
|
// entirely — only an explicit false should withhold trust.
|
||
|
|
assert.equal(isAutoLaunchResultTrustworthy({}), true);
|
||
|
|
});
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Minimal model of the hydration effect's decision: given a getAutoLaunch()
|
||
|
|
* result, should it overwrite the cached autoLaunchEnabled value? This is
|
||
|
|
* the exact regression scenario from the review — a transient read failure
|
||
|
|
* must not overwrite a cached `true` with a "successful-looking" `false`,
|
||
|
|
* which would otherwise cascade into an unwanted disable write via the
|
||
|
|
* adjacent push effect.
|
||
|
|
*/
|
||
|
|
function simulateHydration(cachedEnabled: boolean, result: { success: boolean; enabled: boolean }): boolean {
|
||
|
|
if (!isAutoLaunchResultTrustworthy(result)) return cachedEnabled;
|
||
|
|
return result.enabled;
|
||
|
|
}
|
||
|
|
|
||
|
|
test('a transient read failure during hydration keeps the cached enabled value', () => {
|
||
|
|
const next = simulateHydration(true, { success: false, enabled: false });
|
||
|
|
assert.equal(next, true, 'must not silently flip a cached true to false on an unrelated read failure');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('a successful read during hydration applies the real OS state, even when it flips the cache', () => {
|
||
|
|
const next = simulateHydration(true, { success: true, enabled: false });
|
||
|
|
assert.equal(next, false, 'a confirmed read must still win over a stale cache');
|
||
|
|
});
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Model of the actual hook: a mount-time hydration request and a
|
||
|
|
* user-triggered write can race. This mirrors the real ordering the review
|
||
|
|
* flagged — hydration starts, the user toggles before it resolves (which
|
||
|
|
* starts a real write), then the stale hydration response arrives.
|
||
|
|
*/
|
||
|
|
function simulateMountToggleRace(
|
||
|
|
initialCachedEnabled: boolean,
|
||
|
|
userToggleTo: boolean,
|
||
|
|
staleHydrationResult: { success: boolean; enabled: boolean },
|
||
|
|
): boolean {
|
||
|
|
let state = initialCachedEnabled;
|
||
|
|
let writeStarted = false;
|
||
|
|
|
||
|
|
// Hydration request is issued here (pending) — resolution happens later.
|
||
|
|
|
||
|
|
// The user toggles before hydration resolves; this is what the real push
|
||
|
|
// effect does: apply optimistically and flag that a real write started.
|
||
|
|
state = userToggleTo;
|
||
|
|
writeStarted = true;
|
||
|
|
|
||
|
|
// The stale hydration response now arrives.
|
||
|
|
if (!writeStarted && isAutoLaunchResultTrustworthy(staleHydrationResult)) {
|
||
|
|
state = staleHydrationResult.enabled;
|
||
|
|
}
|
||
|
|
|
||
|
|
return state;
|
||
|
|
}
|
||
|
|
|
||
|
|
test('a user toggle during in-flight hydration is not clobbered by the stale hydration response', () => {
|
||
|
|
const result = simulateMountToggleRace(false, true, { success: true, enabled: false });
|
||
|
|
|
||
|
|
assert.equal(
|
||
|
|
result,
|
||
|
|
true,
|
||
|
|
'the stale response (enabled:false, read before the user toggled) must not overwrite the user\'s fresh true — ' +
|
||
|
|
'otherwise the adjacent push effect reacts to the overwrite and disables an item the user just enabled',
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('a user toggle to false during in-flight hydration is not clobbered by a stale enabled:true response', () => {
|
||
|
|
const result = simulateMountToggleRace(true, false, { success: true, enabled: true });
|
||
|
|
|
||
|
|
assert.equal(result, false, 'symmetric case: disabling must also win over a stale hydration response');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('hydration still applies normally when it resolves before any write starts', () => {
|
||
|
|
// No race: writeStarted stays false throughout, so trustworthy hydration
|
||
|
|
// results must still be applied — the guard must not disable hydration
|
||
|
|
// unconditionally, only once a real write has actually begun.
|
||
|
|
let state = false;
|
||
|
|
let writeStarted = false;
|
||
|
|
const result = { success: true, enabled: true };
|
||
|
|
|
||
|
|
if (!writeStarted && isAutoLaunchResultTrustworthy(result)) {
|
||
|
|
state = result.enabled;
|
||
|
|
}
|
||
|
|
|
||
|
|
assert.equal(state, true);
|
||
|
|
void writeStarted;
|
||
|
|
});
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Model of the push effect's own overlap guard: rapid double (or triple)
|
||
|
|
* toggles start multiple bridge.setAutoLaunch(...) calls that can resolve
|
||
|
|
* out of order. Each response is only reconciled into state if no newer
|
||
|
|
* request has started since — this is the exact scenario the review
|
||
|
|
* flagged: "each callback closes over its render's autoLaunchEnabled value
|
||
|
|
* ... without checking whether a newer request exists".
|
||
|
|
*/
|
||
|
|
class AutoLaunchWriteSimulator {
|
||
|
|
state: boolean;
|
||
|
|
private generation = 0;
|
||
|
|
|
||
|
|
constructor(initial: boolean) {
|
||
|
|
this.state = initial;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Simulates the user toggling the switch, starting a new write. */
|
||
|
|
startWrite(requestedValue: boolean): { requestGeneration: number; requestedValue: boolean } {
|
||
|
|
this.state = requestedValue;
|
||
|
|
this.generation += 1;
|
||
|
|
return { requestGeneration: this.generation, requestedValue };
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Simulates that write's bridge.setAutoLaunch(...) response arriving. */
|
||
|
|
resolveWrite(request: { requestGeneration: number; requestedValue: boolean }, result: { success: boolean; enabled: boolean }) {
|
||
|
|
if (this.generation !== request.requestGeneration) return; // superseded — ignore
|
||
|
|
if (!isAutoLaunchResultTrustworthy(result)) return;
|
||
|
|
if (result.enabled !== request.requestedValue) this.state = result.enabled;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
test('a stale write response is ignored once a newer request has started', () => {
|
||
|
|
const sim = new AutoLaunchWriteSimulator(false);
|
||
|
|
|
||
|
|
const first = sim.startWrite(true); // user enables
|
||
|
|
const second = sim.startWrite(false); // user immediately disables again, before `first` resolves
|
||
|
|
assert.equal(sim.state, false, 'the latest toggle is reflected optimistically right away');
|
||
|
|
|
||
|
|
// `first`'s response arrives late, reporting a mismatch against ITS OWN
|
||
|
|
// request (e.g. macOS pending approval reported enabled:false for a
|
||
|
|
// requested true) — without the generation guard this would incorrectly
|
||
|
|
// "correct" state to false right as `second` is still in flight, and
|
||
|
|
// with a mismatch reported the OTHER way it could just as easily stomp
|
||
|
|
// a subsequent true.
|
||
|
|
sim.resolveWrite(first, { success: true, enabled: false });
|
||
|
|
assert.equal(sim.state, false, 'the stale response for the superseded first request must not touch state');
|
||
|
|
|
||
|
|
// `second`'s own (current) response arrives and is applied normally.
|
||
|
|
sim.resolveWrite(second, { success: true, enabled: false });
|
||
|
|
assert.equal(sim.state, false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('a stale write response would have incorrectly overridden a later successful write, without the guard', () => {
|
||
|
|
const sim = new AutoLaunchWriteSimulator(false);
|
||
|
|
|
||
|
|
const first = sim.startWrite(true); // request enable — this one will report a mismatch
|
||
|
|
const second = sim.startWrite(true); // user toggles off and back on again quickly; also requests true
|
||
|
|
assert.equal(sim.state, true);
|
||
|
|
|
||
|
|
// second's response arrives first (network/OS timing is not ordered) and
|
||
|
|
// confirms the real, current state.
|
||
|
|
sim.resolveWrite(second, { success: true, enabled: true });
|
||
|
|
assert.equal(sim.state, true);
|
||
|
|
|
||
|
|
// first's stale response finally arrives, reporting a mismatch (its own
|
||
|
|
// request was true but the OS said false at the time) — must not undo
|
||
|
|
// the now-confirmed true state from the newer request.
|
||
|
|
sim.resolveWrite(first, { success: true, enabled: false });
|
||
|
|
assert.equal(
|
||
|
|
sim.state,
|
||
|
|
true,
|
||
|
|
'a delayed response from a superseded request must not overwrite the newer, already-confirmed state',
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('the only in-flight write still reconciles a genuine mismatch normally', () => {
|
||
|
|
const sim = new AutoLaunchWriteSimulator(false);
|
||
|
|
|
||
|
|
const only = sim.startWrite(true);
|
||
|
|
sim.resolveWrite(only, { success: true, enabled: false }); // e.g. blocked by Windows Startup Apps
|
||
|
|
|
||
|
|
assert.equal(sim.state, false, 'with no overlap, a real mismatch must still correct the optimistic toggle');
|
||
|
|
});
|