Update Harbor client and gateway integration workflows
This commit is contained in:
@@ -52,6 +52,7 @@ function createHarness(overrides = {}) {
|
||||
let stateUpdates = 0;
|
||||
let startFailure = overrides.failStart || null;
|
||||
const events = [];
|
||||
const journalEvents = [];
|
||||
|
||||
const serialize = (operation) => {
|
||||
const run = async () => {
|
||||
@@ -119,13 +120,16 @@ function createHarness(overrides = {}) {
|
||||
restartCommand: () => captureRuntimeCommand(restart, { preMutationErrorCodes: ['CONFIG_INVALID'] }),
|
||||
},
|
||||
route: { isGatewayDirect: () => overrides.gatewayDirect === true },
|
||||
failover: overrides.failover,
|
||||
serialize,
|
||||
now: () => new Date('2026-08-08T12:00:00.000Z'),
|
||||
onEvent: (event) => journalEvents.push(event),
|
||||
});
|
||||
|
||||
return {
|
||||
service,
|
||||
events,
|
||||
journalEvents,
|
||||
serialize,
|
||||
snapshot: () => structuredClone({ state, config, running, peak }),
|
||||
};
|
||||
@@ -167,6 +171,8 @@ test('failed candidate config or runtime restores the old profile, config and ru
|
||||
const before = harness.snapshot();
|
||||
await assert.rejects(harness.service.apply('work', 'b'));
|
||||
assert.deepEqual(domain(harness.snapshot()), domain(before));
|
||||
assert.equal(harness.journalEvents.at(-1).type, 'connection.failed');
|
||||
assert.match(harness.journalEvents.at(-1).data.errorCode, /^[A-Z0-9_]+$/);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -256,6 +262,193 @@ test('running restart preserves a pending desired profile while restoring the ap
|
||||
assert.equal(harness.snapshot().state.appliedProfileId, 'personal');
|
||||
});
|
||||
|
||||
test('running dual restart rebuilds only the applied failover pair', async () => {
|
||||
const state = initialState();
|
||||
state.failoverPolicy = { enabled: true };
|
||||
state.appliedFailoverPolicy = {
|
||||
primary: { profileId: 'personal', serverId: 'a' },
|
||||
reserve: { profileId: 'work', serverId: 'b' },
|
||||
};
|
||||
const sources = [];
|
||||
const prepared = [];
|
||||
const harness = createHarness({
|
||||
state,
|
||||
failover: {
|
||||
build: (_state, source) => {
|
||||
sources.push(source);
|
||||
return {
|
||||
config: { source },
|
||||
applied: state.appliedFailoverPolicy,
|
||||
primaryProfile: state.profiles[0],
|
||||
primaryServer: serverA,
|
||||
};
|
||||
},
|
||||
prepareActivation: async (role) => { prepared.push(role); harness.events.push(`selector.${role}`); },
|
||||
restoreAppliedActivation: async () => {},
|
||||
reconcile: async () => {},
|
||||
},
|
||||
});
|
||||
await harness.service.restart();
|
||||
assert.deepEqual(sources, ['applied']);
|
||||
assert.deepEqual(prepared, ['primary']);
|
||||
assert.deepEqual(harness.events.slice(0, 4), ['config.write', 'runtime.restart', 'selector.primary', 'state.update']);
|
||||
assert.match(harness.snapshot().config, /"source":"applied"/);
|
||||
});
|
||||
|
||||
test('running single-channel keeps server edits pending after failover is enabled', async () => {
|
||||
const state = initialState();
|
||||
state.failoverPolicy = { enabled: true };
|
||||
state.appliedFailoverPolicy = null;
|
||||
const harness = createHarness({ state });
|
||||
await harness.service.apply('work', 'b');
|
||||
assert.equal(harness.snapshot().state.desiredProfileId, 'work');
|
||||
assert.equal(harness.snapshot().state.appliedProfileId, 'personal');
|
||||
assert.equal(harness.events.includes('config.write'), false);
|
||||
assert.equal(harness.events.includes('runtime.start'), false);
|
||||
});
|
||||
|
||||
test('running dual restart preserves reserve and commits only after selector read-back', async () => {
|
||||
const state = initialState();
|
||||
state.failoverPolicy = { enabled: true };
|
||||
state.appliedFailoverPolicy = {
|
||||
primary: { profileId: 'personal', serverId: 'a' },
|
||||
reserve: { profileId: 'work', serverId: 'b' },
|
||||
};
|
||||
state.appliedProfileId = 'work';
|
||||
state.appliedServerId = 'b';
|
||||
state.appliedServerSnapshot = serverB;
|
||||
const harness = createHarness({
|
||||
state,
|
||||
failover: {
|
||||
build: () => ({
|
||||
config: { dual: true },
|
||||
applied: state.appliedFailoverPolicy,
|
||||
primaryProfile: state.profiles[0],
|
||||
primaryServer: serverA,
|
||||
}),
|
||||
prepareActivation: async (role) => { harness.events.push(`selector.${role}`); },
|
||||
restoreAppliedActivation: async () => { harness.events.push('selector.primary'); },
|
||||
reconcile: async () => {},
|
||||
},
|
||||
});
|
||||
await harness.service.restart();
|
||||
assert.equal(harness.snapshot().state.appliedProfileId, 'work');
|
||||
assert.equal(harness.snapshot().state.appliedServerId, 'b');
|
||||
assert.deepEqual(harness.events.slice(0, 4), ['config.write', 'runtime.restart', 'selector.reserve', 'state.update']);
|
||||
});
|
||||
|
||||
test('selector activation failure rolls runtime and config back before applied truth changes', async () => {
|
||||
const state = initialState();
|
||||
state.failoverPolicy = { enabled: true };
|
||||
state.appliedFailoverPolicy = {
|
||||
primary: { profileId: 'personal', serverId: 'a' },
|
||||
reserve: { profileId: 'work', serverId: 'b' },
|
||||
};
|
||||
const harness = createHarness({
|
||||
state,
|
||||
failover: {
|
||||
build: () => ({
|
||||
config: { dual: true },
|
||||
applied: state.appliedFailoverPolicy,
|
||||
primaryProfile: state.profiles[0],
|
||||
primaryServer: serverA,
|
||||
}),
|
||||
prepareActivation: async (role) => {
|
||||
harness.events.push(`selector.${role}`);
|
||||
if (harness.events.filter((event) => event.startsWith('selector.')).length === 1) {
|
||||
throw new Error('selector failed');
|
||||
}
|
||||
},
|
||||
restoreAppliedActivation: async () => { harness.events.push('selector.primary'); },
|
||||
reconcile: async () => {},
|
||||
},
|
||||
});
|
||||
const before = harness.snapshot();
|
||||
await assert.rejects(harness.service.restart(), /selector failed/);
|
||||
assert.deepEqual(domain(harness.snapshot()), domain(before));
|
||||
assert.equal(harness.events.includes('state.update'), false);
|
||||
assert.deepEqual(harness.events.filter((event) => event.startsWith('selector.')), ['selector.primary', 'selector.primary']);
|
||||
});
|
||||
|
||||
test('state commit rollback restores the previously selected reserve role', async () => {
|
||||
const state = initialState();
|
||||
state.failoverPolicy = { enabled: true };
|
||||
state.appliedFailoverPolicy = {
|
||||
primary: { profileId: 'personal', serverId: 'a' },
|
||||
reserve: { profileId: 'work', serverId: 'b' },
|
||||
};
|
||||
state.appliedProfileId = 'work';
|
||||
state.appliedServerId = 'b';
|
||||
state.appliedServerSnapshot = serverB;
|
||||
const harness = createHarness({
|
||||
state,
|
||||
failStateAt: 1,
|
||||
failover: {
|
||||
build: () => ({
|
||||
config: { dual: true },
|
||||
applied: state.appliedFailoverPolicy,
|
||||
primaryProfile: state.profiles[0],
|
||||
primaryServer: serverA,
|
||||
}),
|
||||
prepareActivation: async (role) => { harness.events.push(`selector.${role}`); },
|
||||
restoreAppliedActivation: async () => { harness.events.push('selector.reserve'); },
|
||||
reconcile: async () => {},
|
||||
},
|
||||
});
|
||||
const before = harness.snapshot();
|
||||
await assert.rejects(harness.service.restart(), /state failed/);
|
||||
assert.deepEqual(domain(harness.snapshot()), domain(before));
|
||||
assert.deepEqual(harness.events.filter((event) => event.startsWith('selector.')), ['selector.reserve', 'selector.reserve']);
|
||||
});
|
||||
|
||||
test('failed stop restores the selected reserve role before restoring state', async () => {
|
||||
const state = initialState();
|
||||
state.failoverPolicy = { enabled: true };
|
||||
state.appliedFailoverPolicy = {
|
||||
primary: { profileId: 'personal', serverId: 'a' },
|
||||
reserve: { profileId: 'work', serverId: 'b' },
|
||||
};
|
||||
state.appliedProfileId = 'work';
|
||||
state.appliedServerId = 'b';
|
||||
state.appliedServerSnapshot = serverB;
|
||||
const harness = createHarness({
|
||||
state,
|
||||
failStateAt: 1,
|
||||
failover: {
|
||||
restoreAppliedActivation: async () => { harness.events.push('selector.reserve'); },
|
||||
reconcile: async () => {},
|
||||
},
|
||||
});
|
||||
const before = harness.snapshot();
|
||||
await assert.rejects(harness.service.stop(), /state failed/);
|
||||
assert.deepEqual(domain(harness.snapshot()), domain(before));
|
||||
assert.deepEqual(harness.events.slice(0, 4), ['runtime.stop', 'state.update', 'runtime.start', 'selector.reserve']);
|
||||
});
|
||||
|
||||
test('disabled passive dual rollback restores the selected reserve role', async () => {
|
||||
const state = initialState();
|
||||
state.failoverPolicy = { enabled: false };
|
||||
state.appliedFailoverPolicy = {
|
||||
primary: { profileId: 'personal', serverId: 'a' },
|
||||
reserve: { profileId: 'work', serverId: 'b' },
|
||||
};
|
||||
state.appliedProfileId = 'work';
|
||||
state.appliedServerId = 'b';
|
||||
state.appliedServerSnapshot = serverB;
|
||||
const harness = createHarness({
|
||||
state,
|
||||
failStateAt: 1,
|
||||
failover: {
|
||||
restoreAppliedActivation: async () => { harness.events.push('selector.reserve'); },
|
||||
reconcile: async () => {},
|
||||
},
|
||||
});
|
||||
const before = harness.snapshot();
|
||||
await assert.rejects(harness.service.restart(), /state failed/);
|
||||
assert.deepEqual(domain(harness.snapshot()), domain(before));
|
||||
assert.equal(harness.events.includes('selector.reserve'), true);
|
||||
});
|
||||
|
||||
test('restart fails closed when runtime status is unknown', async () => {
|
||||
const state = initialState();
|
||||
state.desiredProfileId = 'work';
|
||||
|
||||
Reference in New Issue
Block a user