Handle stale sync state in client UI
This commit is contained in:
98
test/web/harbor-state.test.js
Normal file
98
test/web/harbor-state.test.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
classifySyncError,
|
||||
harborReducer,
|
||||
initialHarborState,
|
||||
} from '../../src/web/state/harborReducer.js';
|
||||
|
||||
const snapshot = (revision, desiredServerId = '', serverIds = ['one', 'two']) => ({
|
||||
apiVersion: 1,
|
||||
revision,
|
||||
selection: { desiredServerId },
|
||||
servers: serverIds.map((id) => ({ id })),
|
||||
});
|
||||
|
||||
const receive = (state, value) => harborReducer(state, {
|
||||
type: 'sync-succeeded',
|
||||
snapshot: value,
|
||||
receivedAt: `2026-07-11T12:00:0${value.revision}.000Z`,
|
||||
});
|
||||
|
||||
function deferred() {
|
||||
let resolve;
|
||||
const promise = new Promise((done) => { resolve = done; });
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
test('an older polling promise cannot replace a newer mutation snapshot', async () => {
|
||||
let state = receive(initialHarborState, snapshot(1, 'one'));
|
||||
const poll = deferred();
|
||||
const mutation = deferred();
|
||||
const apply = (promise) => promise.then((value) => { state = receive(state, value); });
|
||||
const pollApplied = apply(poll.promise);
|
||||
const mutationApplied = apply(mutation.promise);
|
||||
|
||||
mutation.resolve(snapshot(3, 'two'));
|
||||
await mutationApplied;
|
||||
poll.resolve(snapshot(2, 'one'));
|
||||
await pollApplied;
|
||||
|
||||
assert.equal(state.snapshot.revision, 3);
|
||||
assert.equal(state.snapshot.selection.desiredServerId, 'two');
|
||||
});
|
||||
|
||||
test('an equal revision keeps the current snapshot identity', () => {
|
||||
const state = receive(initialHarborState, snapshot(4, 'one'));
|
||||
const next = receive(state, snapshot(4, 'two'));
|
||||
|
||||
assert.equal(next.snapshot, state.snapshot);
|
||||
assert.equal(next.snapshot.selection.desiredServerId, 'one');
|
||||
});
|
||||
|
||||
test('pending selection survives polling until canonical state acknowledges it', () => {
|
||||
let state = receive(initialHarborState, snapshot(1, 'one'));
|
||||
state = harborReducer(state, { type: 'select-server', serverId: 'two' });
|
||||
state = receive(state, snapshot(2, 'one'));
|
||||
assert.equal(state.pendingServerId, 'two');
|
||||
|
||||
state = receive(state, snapshot(3, 'two'));
|
||||
assert.equal(state.pendingServerId, '');
|
||||
});
|
||||
|
||||
test('pending selection is cleared when its server disappears', () => {
|
||||
let state = receive(initialHarborState, snapshot(1, 'one'));
|
||||
state = harborReducer(state, { type: 'select-server', serverId: 'two' });
|
||||
|
||||
assert.equal(receive(state, snapshot(2, 'one', ['one'])).pendingServerId, '');
|
||||
});
|
||||
|
||||
test('initial 500 and connection refusal become retryable boot failures', () => {
|
||||
const serverError = Object.assign(new Error('Internal Server Error'), { status: 500 });
|
||||
const refused = new TypeError('fetch failed');
|
||||
|
||||
assert.equal(classifySyncError(serverError), 'control-unreachable');
|
||||
assert.equal(classifySyncError(refused), 'control-unreachable');
|
||||
const failed = harborReducer(initialHarborState, { type: 'sync-failed', error: serverError });
|
||||
assert.equal(failed.transport.bootStatus, 'control-unreachable');
|
||||
assert.equal(failed.snapshot, null);
|
||||
});
|
||||
|
||||
test('ready state becomes stale after three failures and recovers on success', () => {
|
||||
const known = snapshot(1, 'one');
|
||||
let state = receive(initialHarborState, known);
|
||||
const error = new TypeError('fetch failed');
|
||||
|
||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||
state = harborReducer(state, { type: 'sync-failed', error });
|
||||
}
|
||||
assert.equal(state.snapshot, known);
|
||||
assert.equal(state.transport.stale, true);
|
||||
assert.equal(state.transport.consecutiveFailures, 3);
|
||||
|
||||
state = receive(state, snapshot(2, 'two'));
|
||||
assert.equal(state.transport.stale, false);
|
||||
assert.equal(state.transport.consecutiveFailures, 0);
|
||||
assert.equal(state.snapshot.selection.desiredServerId, 'two');
|
||||
});
|
||||
Reference in New Issue
Block a user