Migrate Harbor state and traffic history to SQLite
Build and Deploy Gateway / build-and-push (push) Successful in 1m22s
Build and Deploy Gateway / deploy (push) Successful in 16s

This commit is contained in:
2026-09-10 19:21:21 +03:00
parent ab14fc979e
commit 1ae23d848b
48 changed files with 1703 additions and 446 deletions
+21 -19
View File
@@ -5,6 +5,13 @@ import http from 'node:http';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { DatabaseSync } from 'node:sqlite';
function readState(directory) {
const db = new DatabaseSync(path.join(directory, 'harbor.sqlite'), { readOnly: true });
try { return JSON.parse(db.prepare("SELECT value FROM documents WHERE key = 'state'").get().value); }
finally { db.close(); }
}
import {
assertStateSnapshot,
@@ -99,7 +106,7 @@ test('state v1 projects legacy storage through the canonical profile snapshot',
);
});
test('startup discards a rejected cached subscription and returns to first-run', async (t) => {
test('startup rejects an invalid legacy subscription without changing migration originals', async (t) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-rejected-cache-'));
const port = await freePort();
const subscriptionUrl = 'https://provider.example/disabled';
@@ -141,16 +148,11 @@ test('startup discards a rejected cached subscription and returns to first-run',
fs.rmSync(dir, { recursive: true, force: true });
});
const state = await waitForState(port, child, () => stderr);
assert.equal(state.subscription.status, 'missing');
assert.equal(state.hasSubscription, false);
assert.deepEqual(state.servers, []);
assert.ok(state.route.localRules.some((rule) => (
rule.type === 'domain_suffix' && rule.value === 'example.org' && rule.enabled
)));
assert.equal(fs.existsSync(path.join(dir, 'subscription-cache.json')), false);
assert.equal(fs.existsSync(path.join(dir, 'sing-box-config.json')), false);
assert.equal(child.exitCode, null);
await assert.rejects(waitForState(port, child, () => stderr), /Harbor exited early/);
assert.notEqual(child.exitCode, 0);
assert.equal(fs.existsSync(path.join(dir, 'subscription-cache.json')), true);
assert.equal(fs.readFileSync(path.join(dir, 'sing-box-config.json'), 'utf8'), '{}');
assert.equal(JSON.parse(fs.readFileSync(path.join(dir, 'state.json'), 'utf8')).subscriptionUrl, subscriptionUrl);
});
test('data invariant: canonical profile API mutates one snapshot and preserves migrated profile data', async (t) => {
@@ -305,15 +307,15 @@ setInterval(() => {}, 60_000);
assert.equal(initial.route.localRulesRevision, 0);
assert.equal(initial.route.localRulesPendingRestart, false);
assert.equal(JSON.stringify(initial).includes(subscriptionUrl), false);
const migratedState = JSON.parse(fs.readFileSync(path.join(dir, 'state.json'), 'utf8'));
const migratedState = readState(dir);
assert.equal(migratedState.schemaVersion, STATE_SCHEMA_VERSION);
assert.equal(migratedState.profiles.length, 1);
assert.equal(migratedState.profiles[0].subscriptionUrl, subscriptionUrl);
assert.equal(migratedState.profiles[0].desiredServerId, testServerId);
assert.equal(migratedState.profiles[0].subscriptionConfig.outbounds[0].tag, testServerId);
assert.equal(Object.hasOwn(migratedState, 'subscriptionUrl'), false);
assert.equal(fs.existsSync(path.join(dir, 'subscription-cache.json')), false);
assert.ok(fs.readdirSync(dir).some((name) => name.startsWith('subscription-cache.json.backup-v1-')));
assert.equal(fs.existsSync(path.join(dir, 'subscription-cache.json')), true);
assert.deepEqual(JSON.parse(fs.readFileSync(path.join(dir, 'subscription-cache.json'), 'utf8')), { url: subscriptionUrl, config });
const stateKeys = Object.keys(initial).sort();
assert.deepEqual(stateKeys, [
'apiVersion',
@@ -373,7 +375,7 @@ setInterval(() => {}, 60_000);
const primaryProfileId = initial.profiles[0].id;
const preservedPrimary = structuredClone(
JSON.parse(fs.readFileSync(path.join(dir, 'state.json'), 'utf8')).profiles[0],
readState(dir).profiles[0],
);
const preservedConfig = fs.readFileSync(path.join(dir, 'sing-box-config.json'), 'utf8');
for (const [pathname, expectedCode] of [
@@ -394,9 +396,9 @@ setInterval(() => {}, 60_000);
},
);
assert.equal(failedAdd.payload.error.code, expectedCode);
const storedAfterFailure = JSON.parse(fs.readFileSync(path.join(dir, 'state.json'), 'utf8'));
const storedAfterFailure = readState(dir);
assert.deepEqual(storedAfterFailure.profiles, [preservedPrimary]);
assert.equal(fs.existsSync(path.join(dir, 'subscription-cache.json')), false);
assert.equal(fs.existsSync(path.join(dir, 'subscription-cache.json')), true);
assert.equal(
fs.readFileSync(path.join(dir, 'sing-box-config.json'), 'utf8'),
preservedConfig,
@@ -475,7 +477,7 @@ setInterval(() => {}, 60_000);
assert.equal(added.state.selection.desiredProfileId, primaryProfileId);
assert.equal(added.state.profiles.find(({ id }) => id === workProfileId).desiredServerId, '');
assert.equal(
JSON.parse(fs.readFileSync(path.join(dir, 'state.json'), 'utf8'))
readState(dir)
.profiles.find(({ id }) => id === workProfileId).subscriptionConfig.outbounds[0].tag,
testServerId,
);
@@ -517,7 +519,7 @@ setInterval(() => {}, 60_000);
);
assert.equal(failedRefresh.response.status, 400);
assert.equal(failedRefresh.payload.error.code, 'SUBSCRIPTION_INVALID');
const storedAfterFailedRefresh = JSON.parse(fs.readFileSync(path.join(dir, 'state.json'), 'utf8'));
const storedAfterFailedRefresh = readState(dir);
const staleWorkProfile = storedAfterFailedRefresh.profiles.find(({ id }) => id === workProfileId);
assert.equal(staleWorkProfile.desiredServerId, testServerId);
assert.equal(staleWorkProfile.lastRefreshErrorCode, 'SUBSCRIPTION_INVALID');