Refactor VPN proxy components and update related behavior
Build and Deploy Gateway / build-and-push (push) Successful in 20s
Build and Deploy Gateway / deploy (push) Successful in 13s

This commit is contained in:
2026-08-11 01:27:46 +03:00
parent c89e56942a
commit aa9c959368
58 changed files with 4234 additions and 2755 deletions
+214 -380
View File
@@ -12,130 +12,113 @@ import { HarborError } from '../../dist/shared/errors.js';
const serverA = { id: 'a', label: 'Alpha', host: 'a.example', port: 1, protocol: 'vless' };
const serverB = { id: 'b', label: 'Beta', host: 'b.example', port: 2, protocol: 'vless' };
const duplicateA = { id: 'a2', label: 'Alpha', host: 'a2.example', port: 3, protocol: 'vless' };
const rules = [{ type: 'domain_suffix', value: 'example', enabled: true }];
function createHarness(overrides = {}) {
let state = structuredClone(overrides.state ?? {
const profile = (id, label, desiredServerId = 'a', servers = [serverA, serverB]) => ({
id,
label,
subscriptionUrl: `https://${id}.example/sub`,
subscriptionConfig: { outbounds: [] },
servers,
userInfo: {},
fetchedAt: '2026-08-08T10:00:00.000Z',
desiredServerId,
lastRefreshAttemptAt: null,
lastRefreshErrorCode: null,
});
function initialState() {
return {
revision: 5,
servers: [serverA, serverB],
selectedServerId: serverA.id,
appliedServerId: serverA.id,
profiles: [profile('personal', 'Личный'), profile('work', 'Работа', 'b')],
desiredProfileId: 'personal',
appliedProfileId: 'personal',
appliedServerId: 'a',
appliedServerSnapshot: serverA,
routeRules: rules,
appliedRouteRules: rules,
routeRulesRevision: 1,
connectionDesired: 'running',
});
};
}
function createHarness(overrides = {}) {
let state = structuredClone(overrides.state ?? initialState());
let config = Object.hasOwn(overrides, 'config') ? overrides.config : 'old-config';
let running = overrides.running ?? true;
let tail = Promise.resolve();
let active = 0;
let peak = 0;
let stateUpdateIndex = 0;
let stateUpdates = 0;
let startFailure = overrides.failStart || null;
const events = [];
const failures = {
...overrides.failures,
stateUpdates: [...(overrides.failures?.stateUpdates || [])],
runtimeStarts: [...(overrides.failures?.runtimeStarts || [])],
runtimeStops: [...(overrides.failures?.runtimeStops || [])],
runtimeRestarts: [...(overrides.failures?.runtimeRestarts || [])],
};
function applyState(mutator) {
const failure = failures.stateUpdates[stateUpdateIndex++];
if (failure instanceof Error) throw failure;
const revision = state.revision + 1;
state = { ...structuredClone(mutator(structuredClone(state))), revision };
events.push(stateUpdateIndex === 1 ? 'state.desired' : stateUpdateIndex === 2 ? 'state.applied' : 'state.restore');
if (failure?.after) throw failure.after;
return structuredClone(state);
}
const serialize = (operation) => {
const run = async () => {
active += 1;
peak = Math.max(peak, active);
try {
return await operation();
} finally {
active -= 1;
}
try { return await operation(); } finally { active -= 1; }
};
const result = tail.then(run, run);
tail = result.then(() => undefined, () => undefined);
return result;
};
const stopRuntime = async () => {
events.push('runtime.stop');
const failure = failures.runtimeStops.shift();
if (failure) {
const updateState = (mutator) => {
stateUpdates += 1;
const next = structuredClone(mutator(structuredClone(state)));
state = { ...next, revision: state.revision + 1 };
events.push('state.update');
if (overrides.failStateAt === stateUpdates) throw new Error('state failed');
return structuredClone(state);
};
const start = async () => {
events.push('runtime.start');
if (startFailure) {
const failure = startFailure;
startFailure = null;
running = false;
throw failure;
}
if (overrides.startGate) await overrides.startGate();
running = true;
};
const stop = async () => {
events.push('runtime.stop');
if (overrides.failStop) throw overrides.failStop;
running = false;
};
const restartRuntime = async () => {
const restart = async () => {
events.push('runtime.restart');
const failure = failures.runtimeRestarts.shift();
if (failure) {
if (failure.code !== 'CONFIG_INVALID') running = false;
throw failure;
}
if (overrides.failRestart) throw overrides.failRestart;
running = true;
};
const service = createConnectionService({
state: {
read: () => structuredClone(state),
update: applyState,
},
subscription: {
readConfig: () => overrides.missingSubscription ? null : { outbounds: [] },
},
state: { read: () => structuredClone(state), update: updateState },
config: {
exists: () => config !== null,
build: (_subscription, selectedServerId, routeRules) => ({ selectedServerId, routeRules }),
read: () => config,
write: (value) => {
events.push('config.write');
if (failures.configWrite) throw failures.configWrite;
config = JSON.stringify(value);
if (failures.configWriteAfter) throw failures.configWriteAfter;
},
restore: (value) => {
events.push('config.restore');
if (failures.configRestore) throw failures.configRestore;
config = value;
},
remove: () => {
events.push('config.remove');
if (failures.configRemove) throw failures.configRemove;
config = null;
if (overrides.failConfigWrite) throw overrides.failConfigWrite;
},
restore: (value) => { events.push('config.restore'); config = value; },
remove: () => { events.push('config.remove'); config = null; },
},
runtime: {
isRunning: async () => {
if (failures.runtimeStatus) throw failures.runtimeStatus;
if (overrides.failStatus) throw overrides.failStatus;
return running;
},
start: async () => {
events.push('runtime.start');
const failure = failures.runtimeStarts.shift();
if (failure) {
running = false;
throw failure;
}
if (overrides.startGate) await overrides.startGate();
running = true;
},
stop: stopRuntime,
stopCommand: () => captureRuntimeCommand(stopRuntime),
restartCommand: () => captureRuntimeCommand(
restartRuntime,
{ preMutationErrorCodes: ['CONFIG_INVALID'] },
),
start,
stop,
stopCommand: () => captureRuntimeCommand(stop),
restartCommand: () => captureRuntimeCommand(restart, { preMutationErrorCodes: ['CONFIG_INVALID'] }),
},
route: { isGatewayDirect: () => overrides.gatewayDirect === true },
serialize,
now: () => new Date('2026-08-08T12:00:00.000Z'),
});
@@ -143,344 +126,195 @@ function createHarness(overrides = {}) {
return {
service,
events,
enqueue: serialize,
setState: (value) => { state = structuredClone(value); },
serialize,
snapshot: () => structuredClone({ state, config, running, peak }),
};
}
function assertDomainRestored(actual, expected) {
const actualDomain = structuredClone(actual);
const expectedDomain = structuredClone(expected);
delete actualDomain.state.revision;
delete expectedDomain.state.revision;
delete actualDomain.peak;
delete expectedDomain.peak;
assert.deepEqual(actualDomain, expectedDomain);
assert.ok(actual.state.revision >= expected.state.revision);
function domain(value) {
const copy = structuredClone(value);
delete copy.state.revision;
delete copy.peak;
return copy;
}
test('apply resolves ID inside the queue and commits desired, config, runtime, then applied state', async () => {
const harness = createHarness({ state: {
revision: 5,
servers: [serverA, serverB, duplicateA],
selectedServerId: serverA.id,
appliedServerId: serverA.id,
routeRules: rules,
appliedRouteRules: rules,
} });
assert.deepEqual(await harness.service.apply(' b ', 'Alpha'), { serverId: 'b', selectedTag: 'Beta' });
const snapshot = harness.snapshot();
assert.equal(snapshot.state.selectedServerId, 'b');
assert.equal(snapshot.state.appliedServerId, 'b');
assert.equal(snapshot.state.connectionDesired, 'running');
assert.equal(snapshot.state.appliedAt, '2026-08-08T12:00:00.000Z');
assert.deepEqual(snapshot.state.appliedRouteRules, rules);
assert.deepEqual(harness.events, ['state.desired', 'config.write', 'runtime.start', 'state.applied']);
await assert.rejects(harness.service.apply('', 'Alpha'), (error) => error.code === 'SERVER_NOT_FOUND');
await assert.rejects(harness.service.apply('missing', 'Beta'), (error) => error.code === 'SERVER_NOT_FOUND');
});
test('apply accepts one legacy label and rejects missing config before mutation', async () => {
const unique = createHarness();
assert.deepEqual(await unique.service.apply('', ' Beta '), { serverId: 'b', selectedTag: 'Beta' });
const missing = createHarness({ missingSubscription: true });
const before = missing.snapshot();
await assert.rejects(missing.service.apply('b', ''), (error) => error.code === 'CONFIG_INVALID');
assertDomainRestored(missing.snapshot(), before);
assert.deepEqual(missing.events, []);
});
test('apply restores previous running domain/config/runtime after mutation failures', async () => {
const failureCases = [
{ failures: { stateUpdates: [new Error('desired')] }, expected: 'desired' },
{ failures: { configWriteAfter: new Error('config') }, expected: 'config' },
{ failures: { runtimeStarts: [new Error('start')] }, expected: 'start' },
{ failures: { stateUpdates: [null, { after: new Error('applied') }] }, expected: 'applied' },
];
for (const { failures, expected } of failureCases) {
const harness = createHarness({ failures });
const before = harness.snapshot();
await assert.rejects(harness.service.apply('b', ''), new RegExp(expected));
assertDomainRestored(harness.snapshot(), before);
}
});
test('apply compensates a desired state update that persists and then throws', async () => {
const failure = new Error('desired persisted then failed');
const harness = createHarness({ failures: { stateUpdates: [{ after: failure }] } });
const before = harness.snapshot();
await assert.rejects(harness.service.apply('b', ''), (error) => error === failure);
test('apply validates the profile pair and publishes applied identity only after runtime succeeds', async () => {
const harness = createHarness();
assert.deepEqual(await harness.service.apply('work', 'a', '', 5), {
profileId: 'work', serverId: 'a', selectedTag: 'Alpha',
});
const after = harness.snapshot();
assertDomainRestored(after, before);
assert.ok(after.state.revision > before.state.revision);
assert.equal(after.state.desiredProfileId, 'work');
assert.equal(after.state.profiles.find(({ id }) => id === 'work').desiredServerId, 'a');
assert.equal(after.state.appliedProfileId, 'work');
assert.equal(after.state.appliedServerId, 'a');
assert.deepEqual(after.state.appliedServerSnapshot, serverA);
assert.deepEqual(harness.events, ['config.write', 'runtime.start', 'state.update']);
await assert.rejects(harness.service.apply('personal', 'missing'), (error) => error.code === 'SERVER_NOT_FOUND');
await assert.rejects(harness.service.apply('missing', 'a'), (error) => error.code === 'PROFILE_NOT_FOUND');
await assert.rejects(harness.service.apply('personal', 'a', '', 4), (error) => error.code === 'STATE_CONFLICT');
});
test('apply restores previous stopped state and prior config absence', async () => {
const failure = new Error('start');
const harness = createHarness({ config: null, running: false, failures: { runtimeStarts: [failure] } });
const before = harness.snapshot();
await assert.rejects(harness.service.apply('b', ''), (error) => error === failure);
assertDomainRestored(harness.snapshot(), before);
assert.ok(harness.events.includes('config.remove'));
assert.ok(harness.events.includes('runtime.stop'));
});
test('apply rollback continues after restore failures and marks runtime rollback failure', async () => {
const original = new Error('apply failed');
const configFailure = new Error('config restore failed');
const nonRuntime = createHarness({
failures: { runtimeStarts: [original], configRestore: configFailure },
});
await assert.rejects(nonRuntime.service.apply('b', ''), (error) => {
assert.ok(error instanceof AggregateError);
assert.deepEqual(error.errors, [original, configFailure]);
return true;
});
assert.ok(nonRuntime.events.filter((event) => event === 'runtime.start').length >= 2);
assert.ok(nonRuntime.events.filter((event) => event.startsWith('state.')).length >= 2);
const rollbackFailure = new Error('runtime rollback failed');
const runtimeBroken = createHarness({ failures: { runtimeStarts: [original, rollbackFailure] } });
await assert.rejects(runtimeBroken.service.apply('b', ''), (error) => {
assert.equal(error.code, 'PROCESS_START_FAILED');
assert.deepEqual(error.cause.errors, [original, rollbackFailure]);
return true;
});
assert.ok(runtimeBroken.events.filter((event) => event.startsWith('state.')).length >= 2);
});
test('queued apply resolves latest state and concurrent valid applies remain serial', async () => {
let release;
const blocker = new Promise((resolve) => { release = resolve; });
const stale = createHarness();
const blocked = stale.enqueue(() => blocker);
const apply = stale.service.apply('b', '');
stale.setState({ ...stale.snapshot().state, servers: [serverA] });
release();
await blocked;
await assert.rejects(apply, (error) => error.code === 'SERVER_NOT_FOUND');
const serial = createHarness();
const [first, second] = await Promise.all([
serial.service.apply('a', ''),
serial.service.apply('b', ''),
]);
assert.equal(first.serverId, 'a');
assert.equal(second.serverId, 'b');
assert.equal(serial.snapshot().state.appliedServerId, 'b');
assert.equal(serial.snapshot().peak, 1);
});
test('server apply route preserves defaults, operation kind, response, and single ownership', async () => {
const calls = [];
const sent = [];
const route = createServerApplyRoute({
connection: {
apply: async (serverId, selectedTag) => {
calls.push([serverId, selectedTag]);
return { serverId: 'resolved', selectedTag: 'Resolved' };
},
},
readBody: async () => ({}),
withOperation: async (kind, operation) => {
calls.push(kind);
return operation();
},
sendState: async (_res, extra) => { sent.push(extra); },
});
const response = {};
assert.equal(await route.handle({ method: 'GET', url: '/api/apply' }, response), false);
assert.equal(await route.handle({ method: 'POST', url: '/api/apply' }, response), true);
assert.deepEqual(calls, ['apply-server', ['', '']]);
assert.deepEqual(sent, [{ serverId: 'resolved', selectedTag: 'Resolved' }]);
const source = readFileSync(new URL('../../src/server/index.ts', import.meta.url), 'utf8');
assert.match(source, /createServerApplyRoute\(\{/);
assert.doesNotMatch(source, /function applySelectedServer|req\.url === ['"]\/api\/apply['"]/);
});
test('stop always cleans runtime and commits only desired stopped state', async () => {
for (const running of [true, false]) {
const harness = createHarness({ running });
test('failed candidate config or runtime restores the old profile, config and running state', async () => {
for (const failure of [
{ failConfigWrite: new Error('write failed') },
{ failStart: new Error('start failed') },
{ failStateAt: 1 },
]) {
const harness = createHarness(failure);
const before = harness.snapshot();
await harness.service.stop();
const after = harness.snapshot();
assert.equal(after.running, false);
assert.equal(after.state.connectionDesired, 'stopped');
assert.equal(after.state.selectedServerId, before.state.selectedServerId);
assert.equal(after.state.appliedServerId, before.state.appliedServerId);
assert.deepEqual(after.state.appliedRouteRules, before.state.appliedRouteRules);
assert.equal(harness.events.filter((event) => event === 'runtime.stop').length, 1);
await assert.rejects(harness.service.apply('work', 'b'));
assert.deepEqual(domain(harness.snapshot()), domain(before));
}
});
test('restart validates config inside the queue and commits applied runtime fields', async () => {
const missing = createHarness({ config: null });
const beforeMissing = missing.snapshot();
await assert.rejects(missing.service.restart(), (error) => error.code === 'CONFIG_INVALID');
assertDomainRestored(missing.snapshot(), beforeMissing);
assert.deepEqual(missing.events, []);
for (const running of [true, false]) {
const harness = createHarness({ running, state: {
revision: 1,
servers: [serverA, serverB],
selectedServerId: serverB.id,
appliedServerId: serverA.id,
routeRules: rules,
appliedRouteRules: [],
appliedAt: 'preserved',
connectionDesired: 'stopped',
} });
await harness.service.restart();
const after = harness.snapshot();
assert.equal(after.running, true);
assert.equal(after.state.appliedServerId, serverB.id);
assert.equal(after.state.connectionDesired, 'running');
assert.deepEqual(after.state.appliedRouteRules, rules);
assert.equal(after.state.appliedAt, 'preserved');
}
test('desired and applied targets stay old while candidate runtime is still starting', async () => {
let entered;
let release;
const started = new Promise((resolve) => { entered = resolve; });
const gate = new Promise((resolve) => { release = resolve; });
const harness = createHarness({ startGate: async () => { entered(); await gate; } });
const switching = harness.service.apply('work', 'b');
await started;
assert.equal(harness.snapshot().state.desiredProfileId, 'personal');
assert.equal(harness.snapshot().state.appliedProfileId, 'personal');
release();
await switching;
});
test('stop and restart compensate runtime and pre/post-write state failures', async () => {
const statusFailure = new Error('status failed');
for (const command of ['stop', 'restart']) {
const status = createHarness({ failures: { runtimeStatus: statusFailure } });
await status.service[command]();
assert.ok(status.events.includes(`runtime.${command}`));
}
test('activate only changes desired while stopped and transactionally switches while running', async () => {
const stopped = createHarness({ running: false });
await stopped.service.activate('work');
assert.equal(stopped.snapshot().state.desiredProfileId, 'work');
assert.equal(stopped.snapshot().state.appliedProfileId, 'personal');
assert.equal(stopped.events.includes('config.write'), false);
for (const stateFailure of [new Error('state before'), { after: new Error('state after') }]) {
const stopHarness = createHarness({ failures: { stateUpdates: [stateFailure] } });
const stopBefore = stopHarness.snapshot();
await assert.rejects(stopHarness.service.stop(), /state/);
assertDomainRestored(stopHarness.snapshot(), stopBefore);
const restartHarness = createHarness({
running: false,
failures: { stateUpdates: [stateFailure] },
});
const restartBefore = restartHarness.snapshot();
await assert.rejects(restartHarness.service.restart(), /state/);
assertDomainRestored(restartHarness.snapshot(), restartBefore);
}
const stopFailure = new Error('stop failed');
const stopped = createHarness({ failures: { runtimeStops: [stopFailure] } });
const stoppedBefore = stopped.snapshot();
await assert.rejects(stopped.service.stop(), (error) => error === stopFailure);
assertDomainRestored(stopped.snapshot(), stoppedBefore);
const restartFailure = new Error('restart failed');
const restarted = createHarness({ running: false, failures: { runtimeRestarts: [restartFailure] } });
const restartedBefore = restarted.snapshot();
await assert.rejects(restarted.service.restart(), (error) => error === restartFailure);
assertDomainRestored(restarted.snapshot(), restartedBefore);
const running = createHarness();
await running.service.activate('work');
assert.equal(running.snapshot().state.appliedProfileId, 'work');
assert.equal(running.snapshot().state.appliedServerId, 'b');
});
test('restart preserves CONFIG_INVALID when validation fails before runtime mutation', async () => {
const invalid = new HarborError('CONFIG_INVALID');
const harness = createHarness({ failures: { runtimeRestarts: [invalid] } });
const before = harness.snapshot();
await assert.rejects(harness.service.restart(), (error) => error === invalid);
assertDomainRestored(harness.snapshot(), before);
assert.equal(harness.events.filter((event) => event === 'runtime.restart').length, 1);
test('gateway-direct changes only local desired state and never claims a remote applied target', async () => {
const harness = createHarness({ gatewayDirect: true });
await harness.service.apply('work', 'b');
const after = harness.snapshot();
assert.equal(after.state.desiredProfileId, 'work');
assert.equal(after.state.appliedProfileId, 'personal');
assert.equal(harness.events.includes('config.write'), false);
assert.equal(harness.events.includes('runtime.start'), false);
});
test('runtime command adapter makes local and remote mutation phase explicit', async () => {
const localFailure = new HarborError('CONFIG_INVALID');
assert.deepEqual(
await captureRuntimeCommand(
async () => { throw localFailure; },
{ preMutationErrorCodes: ['CONFIG_INVALID'] },
),
{ ok: false, mutationStarted: false, error: localFailure },
);
const remoteFailure = new HarborError('PROCESS_START_FAILED');
assert.deepEqual(
await captureRuntimeCommand(async () => { throw remoteFailure; }),
{ ok: false, mutationStarted: true, error: remoteFailure },
);
test('stop clears applied identity but preserves the desired pair', async () => {
const harness = createHarness();
await harness.service.stop();
const after = harness.snapshot();
assert.equal(after.running, false);
assert.equal(after.state.connectionDesired, 'stopped');
assert.equal(after.state.desiredProfileId, 'personal');
assert.equal(after.state.profiles[0].desiredServerId, 'a');
assert.equal(after.state.appliedProfileId, '');
assert.equal(after.state.appliedServerId, '');
assert.equal(after.state.appliedServerSnapshot, null);
});
test('runtime command rollback continues through state errors and maps runtime restore failure', async () => {
const original = new Error('stop state failed');
const stateRestore = new Error('state restore failed');
const aggregate = createHarness({
failures: { stateUpdates: [{ after: original }, stateRestore] },
});
await assert.rejects(aggregate.service.stop(), (error) => {
assert.ok(error instanceof AggregateError);
assert.deepEqual(error.errors, [original, stateRestore]);
return true;
});
assert.equal(aggregate.snapshot().running, true);
test('restart uses applied pair while running and desired pair while stopped', async () => {
const running = createHarness();
await running.service.restart();
assert.match(running.snapshot().config, /"selectedServerId":"a"/);
const stopFailure = new Error('stop failed');
const runtimeRestore = new Error('start rollback failed');
const broken = createHarness({
failures: { runtimeStops: [stopFailure], runtimeStarts: [runtimeRestore] },
});
await assert.rejects(broken.service.stop(), (error) => {
assert.equal(error.code, 'PROCESS_START_FAILED');
assert.deepEqual(error.cause.errors, [stopFailure, runtimeRestore]);
return true;
});
const stoppedState = initialState();
stoppedState.desiredProfileId = 'work';
stoppedState.connectionDesired = 'stopped';
const stopped = createHarness({ running: false, state: stoppedState });
await stopped.service.restart();
assert.equal(stopped.snapshot().state.appliedProfileId, 'work');
assert.equal(stopped.snapshot().state.appliedServerId, 'b');
assert.match(stopped.snapshot().config, /"selectedServerId":"b"/);
});
test('apply, stop, and restart share one deterministic connection queue', async () => {
const applyThenStop = createHarness();
await Promise.all([
applyThenStop.service.apply('b', ''),
applyThenStop.service.stop(),
]);
assert.equal(applyThenStop.snapshot().state.connectionDesired, 'stopped');
assert.equal(applyThenStop.snapshot().peak, 1);
const stopThenRestart = createHarness();
await Promise.all([
stopThenRestart.service.stop(),
stopThenRestart.service.restart(),
]);
assert.equal(stopThenRestart.snapshot().state.connectionDesired, 'running');
assert.equal(stopThenRestart.snapshot().running, true);
assert.equal(stopThenRestart.snapshot().peak, 1);
test('running restart preserves a pending desired profile while restoring the applied pair', async () => {
const state = initialState();
state.desiredProfileId = 'work';
const harness = createHarness({ state });
await harness.service.restart();
assert.equal(harness.snapshot().state.desiredProfileId, 'work');
assert.equal(harness.snapshot().state.appliedProfileId, 'personal');
});
test('connection runtime route preserves operation kinds, response extras, and single ownership', async () => {
test('restart fails closed when runtime status is unknown', async () => {
const state = initialState();
state.desiredProfileId = 'work';
const failure = new Error('status unavailable');
const harness = createHarness({ state, failStatus: failure });
const before = harness.snapshot();
await assert.rejects(harness.service.restart(), (error) => error === failure);
assert.deepEqual(domain(harness.snapshot()), domain(before));
});
test('all connection mutations share one deterministic queue', async () => {
let release;
const gate = new Promise((resolve) => { release = resolve; });
const harness = createHarness({ startGate: () => gate });
const apply = harness.service.apply('work', 'b');
const stop = harness.service.stop();
release();
await Promise.all([apply, stop]);
assert.equal(harness.snapshot().peak, 1);
assert.equal(harness.snapshot().state.connectionDesired, 'stopped');
});
test('pair-aware apply and runtime routes preserve one HTTP owner', async () => {
const calls = [];
const sent = [];
const route = createConnectionRuntimeRoute({
const applyRoute = createServerApplyRoute({
connection: {
stop: async () => { calls.push('service.stop'); },
restart: async () => { calls.push('service.restart'); },
apply: async (...args) => {
calls.push(args);
return { profileId: 'personal', serverId: 'a', selectedTag: 'Alpha' };
},
},
withOperation: async (kind, operation) => {
calls.push(kind);
return operation();
readBody: async () => ({ profileId: 'personal', serverId: 'a', expectedRevision: 7 }),
withOperation: async (kind, operation, options) => {
calls.push([kind, options]);
return operation(8);
},
sendState: async (_res, extra) => { sent.push(extra); },
});
const response = {};
assert.equal(await applyRoute.handle({ method: 'POST', url: '/api/apply' }, {}), true);
assert.deepEqual(calls, [
['apply-server', { expectedRevision: 7, profileId: 'personal', serverId: 'a' }],
['personal', 'a', '', 8],
]);
assert.deepEqual(sent, [{ profileId: 'personal', serverId: 'a', selectedTag: 'Alpha' }]);
assert.equal(await route.handle({ method: 'GET', url: '/api/singbox/stop' }, response), false);
assert.equal(await route.handle({ method: 'POST', url: '/api/singbox/stop' }, response), true);
assert.equal(await route.handle({ method: 'POST', url: '/api/singbox/restart' }, response), true);
assert.deepEqual(calls, ['stop', 'service.stop', 'start', 'service.restart']);
assert.deepEqual(sent, [{ singboxRunning: false }, { singboxRunning: true }]);
const runtimeCalls = [];
const runtimeRoute = createConnectionRuntimeRoute({
connection: {
stop: async () => { runtimeCalls.push('service.stop'); },
restart: async () => { runtimeCalls.push('service.restart'); },
},
withOperation: async (kind, operation) => { runtimeCalls.push(kind); return operation(); },
sendState: async () => {},
});
await runtimeRoute.handle({ method: 'POST', url: '/api/singbox/stop' }, {});
await runtimeRoute.handle({ method: 'POST', url: '/api/singbox/restart' }, {});
assert.deepEqual(runtimeCalls, ['stop', 'service.stop', 'start', 'service.restart']);
const source = readFileSync(new URL('../../src/server/index.ts', import.meta.url), 'utf8');
assert.match(source, /createConnectionRuntimeRoute\(\{/);
assert.doesNotMatch(source, /req\.url === ['"]\/api\/singbox\/(?:stop|restart)['"]/);
assert.doesNotMatch(source, /function applySelectedServer|req\.url === ['"]\/api\/apply['"]/);
});
test('runtime command adapter distinguishes failures before and after mutation', async () => {
const invalid = new HarborError('CONFIG_INVALID');
assert.deepEqual(
await captureRuntimeCommand(async () => { throw invalid; }, { preMutationErrorCodes: ['CONFIG_INVALID'] }),
{ ok: false, mutationStarted: false, error: invalid },
);
const failed = new Error('remote failed');
assert.deepEqual(
await captureRuntimeCommand(async () => { throw failed; }),
{ ok: false, mutationStarted: true, error: failed },
);
});