Track applied route rules separately from pending edits
All checks were successful
Build and Deploy Gateway / build-and-push (push) Successful in 16s
Build and Deploy Gateway / deploy (push) Successful in 7s

This commit is contained in:
2026-07-11 23:07:20 +03:00
parent 65bf88bf41
commit f135ade43b
9 changed files with 215 additions and 49 deletions

View File

@@ -172,10 +172,13 @@ setInterval(() => {}, 60_000);
assert.deepEqual(initial.route.localRules, [
{ type: 'domain_suffix', value: 'ru', enabled: true },
]);
assert.deepEqual(initial.route.activeLocalRules, initial.route.localRules);
assert.equal(initial.route.localRulesRevision, 0);
assert.equal(initial.route.localRulesPendingRestart, false);
assert.equal(JSON.stringify(initial).includes(subscriptionUrl), false);
const stateKeys = Object.keys(initial).sort();
let revision = initial.revision;
let rulesRevision = initial.route.localRulesRevision;
const invalidSubscription = await rawRequest(
port,
@@ -238,21 +241,22 @@ setInterval(() => {}, 60_000);
assert.equal((await mutation('/api/singbox/stop')).state.connection.desired, 'stopped');
assert.equal((await mutation('/api/singbox/restart')).state.connection.desired, 'running');
const rulesRevision = revision;
let routed = await mutation('/api/route-rules', 'PUT', {
expectedRevision: rulesRevision,
expectedRulesRevision: rulesRevision,
rules: [
{ type: 'domain_suffix', value: 'ru', enabled: false },
{ type: 'domain', value: 'https://Example.com/private?q=1', enabled: true },
{ type: 'domain_suffix', value: '*.Example.org', enabled: true },
],
});
rulesRevision = routed.state.route.localRulesRevision;
assert.deepEqual(routed.state.route.localRules, [
{ type: 'domain_suffix', value: 'ru', enabled: false },
{ type: 'domain', value: 'example.com', enabled: true },
{ type: 'domain_suffix', value: 'example.org', enabled: true },
]);
assert.equal(routed.state.route.localRulesPendingRestart, false);
assert.deepEqual(routed.state.route.activeLocalRules, routed.state.route.localRules);
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' },
@@ -261,19 +265,22 @@ setInterval(() => {}, 60_000);
await mutation('/api/singbox/stop');
routed = await mutation('/api/route-rules', 'PUT', {
expectedRevision: revision,
expectedRulesRevision: rulesRevision,
rules: [
...routed.state.route.localRules,
{ type: 'domain_keyword', value: 'media', enabled: true },
],
});
rulesRevision = routed.state.route.localRulesRevision;
assert.equal(routed.state.connection.process, 'stopped');
assert.equal(routed.state.route.localRulesPendingRestart, true);
assert.deepEqual(routed.state.route.activeLocalRules, []);
const restartedRules = await mutation('/api/singbox/restart');
assert.equal(restartedRules.state.route.localRulesPendingRestart, false);
assert.deepEqual(restartedRules.state.route.activeLocalRules, routed.state.route.localRules);
const invalidRules = await rawRequest(port, '/api/route-rules', 'PUT', {
expectedRevision: revision,
expectedRulesRevision: rulesRevision,
rules: [{ type: 'domain_regex', value: '.*' }],
});
assert.equal(invalidRules.response.status, 400);
@@ -281,13 +288,19 @@ setInterval(() => {}, 60_000);
assert.equal((await request(port, '/api/state')).revision, revision);
const staleRules = await rawRequest(port, '/api/route-rules', 'PUT', {
expectedRevision: rulesRevision,
expectedRulesRevision: 0,
rules: [],
});
assert.equal(staleRules.response.status, 409);
assert.equal(staleRules.payload.error.code, 'STATE_CONFLICT');
assert.deepEqual((await request(port, '/api/state')).route.localRules, routed.state.route.localRules);
const legacyNoop = await rawRequest(port, '/api/route-rules', 'PUT', {
expectedRevision: revision,
rules: routed.state.route.localRules,
});
assert.equal(legacyNoop.response.status, 200);
const workingConfig = fs.readFileSync(path.join(dir, 'sing-box-config.json'), 'utf8');
fs.writeFileSync(singboxPath, `#!/usr/bin/env node
const fs = require('node:fs');
@@ -301,7 +314,7 @@ setInterval(() => {}, 60_000);
`);
fs.chmodSync(singboxPath, 0o755);
const failedRules = await rawRequest(port, '/api/route-rules', 'PUT', {
expectedRevision: revision,
expectedRulesRevision: rulesRevision,
rules: [{ type: 'domain', value: 'broken.example' }],
});
assert.equal(failedRules.response.status, 422);

View File

@@ -14,6 +14,11 @@ test('rule editor add latency stays constant and dirty exits are guarded', () =>
assert.match(component, /addEventListener\('beforeunload', warnBeforeUnload\)/);
assert.match(component, /requestCloseLocalRules\(\)/);
assert.match(component, /localRulesPendingRestart/);
assert.match(component, /activeLocalRules/);
assert.match(component, /localRulesRevision/);
assert.match(component, /Не сохранено/);
assert.match(component, /Ждёт перезапуска/);
assert.match(component, /Перезапустить VPN/);
});
test('critical confirmations share one accessible blocking popup', () => {