Update Harbor client and gateway integration workflows
This commit is contained in:
@@ -124,6 +124,8 @@ function createHarness(overrides = {}) {
|
||||
clearInterval: (timer) => { timer.clearCalls += 1; },
|
||||
},
|
||||
onRefreshError: overrides.onRefreshError || (() => {}),
|
||||
onEvent: overrides.onEvent,
|
||||
failover: overrides.failover,
|
||||
now: () => new Date('2026-08-08T12:30:00.000Z'),
|
||||
});
|
||||
|
||||
@@ -305,6 +307,36 @@ test('inactive delete is state-only; applied delete requires one stop-and-delete
|
||||
assert.equal(after.config, null);
|
||||
});
|
||||
|
||||
test('failed active delete restores the previous reserve selector', async () => {
|
||||
const state = defaultState();
|
||||
state.failoverPolicy = {
|
||||
enabled: true,
|
||||
paused: false,
|
||||
primary: { profileId: 'work', serverId: 'shared' },
|
||||
reserve: { profileId: 'personal', serverId: 'shared' },
|
||||
};
|
||||
state.appliedFailoverPolicy = {
|
||||
primary: state.failoverPolicy.primary,
|
||||
reserve: state.failoverPolicy.reserve,
|
||||
};
|
||||
const harness = createHarness({
|
||||
state,
|
||||
failStateAt: 1,
|
||||
failover: {
|
||||
reconcile: async () => {},
|
||||
restoreAppliedActivation: async (previousState) => {
|
||||
assert.equal(previousState.appliedProfileId, 'personal');
|
||||
harness.calls.push('selector.reserve');
|
||||
},
|
||||
},
|
||||
});
|
||||
await assert.rejects(harness.service.deleteProfile('personal', 'stop-and-delete'), /state failed/);
|
||||
assert.equal(harness.snapshot().running, true);
|
||||
assert.equal(harness.snapshot().config, 'old-config');
|
||||
assert.equal(harness.snapshot().state.appliedProfileId, 'personal');
|
||||
assert.ok(harness.calls.indexOf('selector.reserve') > harness.calls.indexOf('runtime.start'));
|
||||
});
|
||||
|
||||
test('deleting a pending desired profile leaves the running applied route untouched', async () => {
|
||||
const state = defaultState();
|
||||
state.desiredProfileId = 'work';
|
||||
@@ -317,6 +349,90 @@ test('deleting a pending desired profile leaves the running applied route untouc
|
||||
assert.equal(harness.calls.includes('gateway.set'), false);
|
||||
});
|
||||
|
||||
test('deleting a desired failover target disables only the pending policy', async () => {
|
||||
const state = defaultState();
|
||||
state.failoverPolicy = {
|
||||
enabled: true,
|
||||
paused: false,
|
||||
primary: { profileId: 'work', serverId: 'shared' },
|
||||
reserve: { profileId: 'personal', serverId: 'shared' },
|
||||
};
|
||||
const harness = createHarness({ state });
|
||||
await harness.service.deleteProfile('work', 'delete', 3);
|
||||
const after = harness.snapshot();
|
||||
assert.equal(after.running, true);
|
||||
assert.equal(after.config, 'old-config');
|
||||
assert.equal(after.state.failoverPolicy.enabled, false);
|
||||
assert.deepEqual(after.state.failoverPolicy.primary, { profileId: '', serverId: '' });
|
||||
assert.deepEqual(after.state.failoverPolicy.reserve, { profileId: 'personal', serverId: 'shared' });
|
||||
});
|
||||
|
||||
test('refresh pauses failover when a loaded channel target disappears', async () => {
|
||||
const state = defaultState();
|
||||
state.failoverPolicy = {
|
||||
enabled: true,
|
||||
paused: false,
|
||||
primary: { profileId: 'personal', serverId: 'shared' },
|
||||
reserve: { profileId: 'work', serverId: 'shared' },
|
||||
};
|
||||
state.appliedFailoverPolicy = {
|
||||
primary: state.failoverPolicy.primary,
|
||||
reserve: state.failoverPolicy.reserve,
|
||||
primaryConfigFingerprint: 'a'.repeat(64),
|
||||
reserveConfigFingerprint: 'b'.repeat(64),
|
||||
};
|
||||
const events = [];
|
||||
const harness = createHarness({ state, onEvent: (event) => events.push(event) });
|
||||
await harness.service.refreshProfile('personal');
|
||||
assert.equal(harness.snapshot().state.failoverPolicy.paused, true);
|
||||
assert.equal(harness.snapshot().running, true);
|
||||
assert.equal(harness.snapshot().config, 'old-config');
|
||||
assert.ok(events.some(({ type }) => type === 'failover.paused'));
|
||||
});
|
||||
|
||||
test('scheduled refresh failures use one stable key for the same failure streak', async () => {
|
||||
const failure = Object.assign(new Error('down'), { code: 'PROVIDER_UNAVAILABLE' });
|
||||
const events = [];
|
||||
const harness = createHarness({
|
||||
fetchSubscription: async () => { throw failure; },
|
||||
onEvent: (event) => events.push(event),
|
||||
});
|
||||
await assert.rejects(harness.service.refreshProfile('personal', undefined, 'scheduled'));
|
||||
await assert.rejects(harness.service.refreshProfile('personal', undefined, 'scheduled'));
|
||||
const keys = events.filter(({ type }) => type === 'subscription.refresh_failed').map(({ dedupeKey }) => dedupeKey);
|
||||
assert.equal(keys.length, 2);
|
||||
assert.equal(keys[0], keys[1]);
|
||||
});
|
||||
|
||||
test('scheduled success logs only content changes or recovery while manual refresh always logs', async () => {
|
||||
const unchanged = (id) => ({
|
||||
config: { profile: id },
|
||||
servers: [oldServer],
|
||||
userInfo: { total: 100 },
|
||||
fetchedAt: '2026-08-08T12:00:00.000Z',
|
||||
});
|
||||
const events = [];
|
||||
const harness = createHarness({
|
||||
fetchSubscription: async () => unchanged('work'),
|
||||
onEvent: (event) => events.push(event),
|
||||
});
|
||||
await harness.service.refreshProfile('work', undefined, 'scheduled');
|
||||
assert.equal(events.length, 0);
|
||||
await harness.service.refreshProfile('work');
|
||||
assert.deepEqual(events.map(({ type }) => type), ['subscription.refreshed']);
|
||||
|
||||
const recoveryState = defaultState();
|
||||
recoveryState.profiles[1].lastRefreshErrorCode = 'PROVIDER_UNAVAILABLE';
|
||||
const recoveryEvents = [];
|
||||
const recovery = createHarness({
|
||||
state: recoveryState,
|
||||
fetchSubscription: async () => unchanged('work'),
|
||||
onEvent: (event) => recoveryEvents.push(event),
|
||||
});
|
||||
await recovery.service.refreshProfile('work', undefined, 'scheduled');
|
||||
assert.deepEqual(recoveryEvents.map(({ type }) => type), ['subscription.refreshed']);
|
||||
});
|
||||
|
||||
test('auto refresh iterates profiles once, reports scoped failures, and stops idempotently', async () => {
|
||||
const errors = [];
|
||||
const failure = Object.assign(new Error('down'), { code: 'PROVIDER_UNAVAILABLE' });
|
||||
|
||||
Reference in New Issue
Block a user