Introduce stable server IDs for subscription state
All checks were successful
Build and Deploy Gateway / build-and-push (push) Successful in 14s
Build and Deploy Gateway / deploy (push) Successful in 7s

This commit is contained in:
2026-07-12 11:59:22 +03:00
parent 005c7a101b
commit 267afc5c7e
16 changed files with 323 additions and 145 deletions

View File

@@ -11,6 +11,7 @@ import {
createStateSnapshot,
normalizeStoredState,
} from '../../src/shared/contracts/state.js';
import { createServerId } from '../../src/shared/serverIdentity.js';
import { HARBOR_VERSIONS } from '../../src/shared/versions.js';
const root = path.resolve(import.meta.dirname, '../..');
@@ -59,10 +60,12 @@ async function waitForState(port, child, stderr) {
}
test('state v1 normalizes legacy storage and validates the canonical snapshot', () => {
const legacyServer = { tag: ' legacy ', type: 'vless', server: 'vpn.example', server_port: 443 };
const legacyServerId = createServerId(legacyServer);
const stored = normalizeStoredState({
subscriptionUrl: 'https://provider.example/subscription/test',
selectedTag: ' legacy ',
servers: [{ tag: ' legacy ', type: 'vless', server: 'vpn.example', server_port: 443 }],
servers: [legacyServer],
});
const snapshot = createStateSnapshot({
storedState: stored,
@@ -76,8 +79,8 @@ test('state v1 normalizes legacy storage and validates the canonical snapshot',
assert.equal(snapshot.apiVersion, 1);
assert.deepEqual(snapshot.selection, {
desiredServerId: 'legacy',
appliedServerId: 'legacy',
desiredServerId: legacyServerId,
appliedServerId: legacyServerId,
});
assert.equal(snapshot.connection.process, 'running');
assert.equal(JSON.stringify(snapshot).includes(stored.subscriptionUrl), false);
@@ -100,6 +103,7 @@ test('GET and domain mutations return one state shape with monotonic revisions',
tls: { enabled: true },
}],
};
const testServerId = createServerId(config.outbounds[0]);
fs.mkdirSync(binDir);
const singboxPath = path.join(binDir, 'sing-box');
const workingSingbox = `#!/usr/bin/env node
@@ -190,7 +194,7 @@ setInterval(() => {}, 60_000);
runtime: { singBox: '1.12.13' },
});
assertStateSnapshot(initial);
assert.equal(initial.selection.appliedServerId, 'test-vpn');
assert.equal(initial.selection.appliedServerId, testServerId);
assert.deepEqual(initial.route.localRules, [
{ type: 'domain_suffix', value: 'ru', enabled: true },
]);
@@ -266,7 +270,7 @@ setInterval(() => {}, 60_000);
);
assert.equal(missingServer.response.status, 404);
assert.equal(missingServer.payload.error.code, 'SERVER_NOT_FOUND');
assert.equal((await request(port, '/api/state')).selection.desiredServerId, 'test-vpn');
assert.equal((await request(port, '/api/state')).selection.desiredServerId, testServerId);
async function stateResponse(pathname, method = 'POST', body) {
const result = await request(port, pathname, method, body);
@@ -285,10 +289,14 @@ setInterval(() => {}, 60_000);
const fetchesBeforeImport = providerFetchCount;
await mutation('/api/subscription/fetch', 'POST', { url: subscriptionUrl });
assert.equal(providerFetchCount, fetchesBeforeImport + 1);
const applied = await mutation('/api/apply', 'POST', { selectedTag: 'test-vpn' });
assert.equal(
JSON.parse(fs.readFileSync(path.join(dir, 'subscription-cache.json'))).config.outbounds[0].tag,
'test-vpn',
);
const applied = await mutation('/api/apply', 'POST', { serverId: testServerId });
assert.deepEqual(applied.state.selection, {
desiredServerId: 'test-vpn',
appliedServerId: 'test-vpn',
desiredServerId: testServerId,
appliedServerId: testServerId,
});
assert.equal(applied.state.connection.process, 'running');
const cacheBeforeFailedRefresh = fs.readFileSync(path.join(dir, 'subscription-cache.json'), 'utf8');
@@ -323,7 +331,7 @@ setInterval(() => {}, 60_000);
assert.deepEqual(JSON.parse(fs.readFileSync(path.join(dir, 'sing-box-config.json'))).route.rules.slice(0, 3), [
{ domain: ['example.com'], outbound: 'direct' },
{ domain_suffix: ['example.org'], outbound: 'direct' },
{ inbound: ['mixed-in'], outbound: 'test-vpn' },
{ inbound: ['mixed-in'], outbound: testServerId },
]);
await mutation('/api/singbox/stop');