import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import test from 'node:test'; import { captureRuntimeCommand, createConnectionService, } from '../../dist/server/features/connection/index.js'; import { createConnectionRuntimeRoute } from '../../dist/server/http/routes/connectionRuntimeRoute.js'; import { createServerApplyRoute } from '../../dist/server/http/routes/serverApplyRoute.js'; 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 rules = [{ type: 'domain_suffix', value: 'example', enabled: true, outbound: 'direct' }]; 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, 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 stateUpdates = 0; let startFailure = overrides.failStart || null; const events = []; const journalEvents = []; const serialize = (operation) => { const run = async () => { active += 1; peak = Math.max(peak, active); try { return await operation(); } finally { active -= 1; } }; const result = tail.then(run, run); tail = result.then(() => undefined, () => undefined); return result; }; 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 restart = async () => { events.push('runtime.restart'); if (overrides.failRestart) throw overrides.failRestart; running = true; }; const service = createConnectionService({ state: { read: () => structuredClone(state), update: updateState }, config: { build: (_subscription, selectedServerId, routeRules) => ({ selectedServerId, routeRules }), read: () => config, write: (value) => { events.push('config.write'); config = JSON.stringify(value); 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 (overrides.failStatus) throw overrides.failStatus; return running; }, start, stop, stopCommand: () => captureRuntimeCommand(stop), 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 }), }; } function domain(value) { const copy = structuredClone(value); delete copy.state.revision; delete copy.peak; return copy; } 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(); 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.appliedRouteRules, rules); 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('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 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_]+$/); } }); 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('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); const running = createHarness(); await running.service.activate('work'); assert.equal(running.snapshot().state.appliedProfileId, 'work'); assert.equal(running.snapshot().state.appliedServerId, 'b'); }); test('gateway-direct changes only local desired state and never claims a remote applied target', async () => { const state = initialState(); state.appliedRouteRules = []; const harness = createHarness({ gatewayDirect: true, state }); await harness.service.apply('work', 'b'); const after = harness.snapshot(); assert.equal(after.state.desiredProfileId, 'work'); assert.equal(after.state.appliedProfileId, 'personal'); assert.deepEqual(after.state.appliedRouteRules, []); assert.equal(harness.events.includes('config.write'), false); assert.equal(harness.events.includes('runtime.start'), false); }); 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('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 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"/); assert.deepEqual(stopped.snapshot().state.appliedRouteRules, rules); }); test('restart in gateway-direct keeps user rules omitted from applied truth', async () => { const state = initialState(); state.appliedRouteRules = []; const harness = createHarness({ gatewayDirect: true, state }); await harness.service.restart(); assert.deepEqual(harness.snapshot().state.appliedRouteRules, []); }); 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('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'; 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 applyRoute = createServerApplyRoute({ connection: { apply: async (...args) => { calls.push(args); return { profileId: 'personal', serverId: 'a', selectedTag: 'Alpha' }; }, }, 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); }, }); 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' }]); 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.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 }, ); });