import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import test from 'node:test'; import { atomicWriteJson, createStateStore, STATE_SCHEMA_VERSION, } from '../../dist/server/services/stateStore.js'; import { createServerId } from '../../dist/shared/serverIdentity.js'; const fixture = (t) => { const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-state-store-')); t.after(() => fs.rmSync(directory, { recursive: true, force: true })); return path.join(directory, 'state.json'); }; test('raw JSON stores stay typed unknown until a migrator validates them', () => { const source = fs.readFileSync( new URL('../../src/server/services/stateStore.ts', import.meta.url), 'utf8', ); assert.match(source, /createJsonStore\(options: RawJsonStoreOptions\): JsonStore/); assert.match(source, /const migrate = options\.migrate \|\| \(\(value: unknown\) => value\)/); assert.doesNotMatch(source, /value as T|migrate = \(value\) => value as/); }); test('data invariant: failure before rename preserves the last successful file', (t) => { const filePath = fixture(t); atomicWriteJson(filePath, { revision: 1 }); assert.throws( () => atomicWriteJson(filePath, { revision: 2 }, { beforeRename: () => { throw new Error('injected failure'); }, }), /injected failure/, ); assert.deepEqual(JSON.parse(fs.readFileSync(filePath, 'utf8')), { revision: 1 }); assert.equal( fs.readdirSync(path.dirname(filePath)).some((name) => name.endsWith('.tmp')), false, ); }); test('schema v2 state migrates built-in .ru into a normal enabled rule', (t) => { const filePath = fixture(t); const legacy = { schemaVersion: 2, revision: 7, selectedTag: 'nl', servers: [{ tag: 'nl', type: 'vless', server: 'nl.example', server_port: 443 }], }; fs.writeFileSync(filePath, JSON.stringify(legacy)); const store = createStateStore(filePath, { now: () => new Date('2026-07-11T12:00:00.000Z'), }); const migrated = store.read(); assert.equal(migrated.schemaVersion, STATE_SCHEMA_VERSION); assert.deepEqual(migrated.routeRules, [ { type: 'domain_suffix', value: 'ru', enabled: true }, ]); const primary = migrated.profiles[0]; assert.equal(primary.label, 'Основной'); assert.equal(primary.desiredServerId, createServerId(legacy.servers[0])); assert.equal(migrated.appliedProfileId, primary.id); assert.equal(migrated.appliedServerId, primary.desiredServerId); assert.equal(Object.hasOwn(migrated, 'selectedServerId'), false); assert.equal(store.migration.fromVersion, 2); assert.deepEqual(JSON.parse(fs.readFileSync(store.migration.backupPath, 'utf8')), legacy); assert.equal(JSON.parse(fs.readFileSync(filePath, 'utf8')).schemaVersion, STATE_SCHEMA_VERSION); }); test('ambiguous legacy selectedTag explicitly requires a new choice', (t) => { const filePath = fixture(t); fs.writeFileSync(filePath, JSON.stringify({ schemaVersion: 3, selectedTag: 'Amsterdam', servers: [ { tag: 'Amsterdam', type: 'vless', server: 'nl-1.example', server_port: 443 }, { tag: 'Amsterdam', type: 'vless', server: 'nl-2.example', server_port: 443 }, ], })); const migrated = createStateStore(filePath).read(); assert.equal(migrated.profiles[0].desiredServerId, ''); assert.equal(migrated.appliedServerId, ''); assert.equal(migrated.profiles[0].servers.length, 2); }); test('schema v4 migration never combines canonical state with a different cache owner', (t) => { const filePath = fixture(t); const stateServer = { tag: 'State server', type: 'vless', server: 'state.example', server_port: 443, }; const cacheServer = { tag: 'Cache server', type: 'vless', server: 'cache.example', server_port: 8443, }; const stateServerId = createServerId(stateServer); fs.writeFileSync(filePath, JSON.stringify({ schemaVersion: 4, revision: 9, subscriptionUrl: 'https://state.example/subscription-a', servers: [stateServer], selectedServerId: stateServerId, appliedServerId: stateServerId, connectionDesired: 'running', })); const migrated = createStateStore(filePath, { legacySubscriptionCache: { url: 'https://cache.example/subscription-b', config: { outbounds: [cacheServer] }, servers: [cacheServer], userInfo: { total: 123 }, fetchedAt: '2026-08-11T12:00:00.000Z', }, }).read(); assert.equal(migrated.profiles.length, 1); assert.equal(migrated.profiles[0].subscriptionUrl, 'https://state.example/subscription-a'); assert.equal(migrated.profiles[0].subscriptionConfig, null); assert.deepEqual(migrated.profiles[0].servers.map(({ id }) => id), [stateServerId]); assert.equal(migrated.profiles[0].desiredServerId, stateServerId); assert.equal(migrated.appliedServerId, stateServerId); assert.equal(migrated.appliedServerSnapshot.id, stateServerId); assert.equal(JSON.stringify(migrated).includes('cache.example'), false); }); test('data invariant: corrupt JSON preserves original bytes and returns explicit recovery state', (t) => { const filePath = fixture(t); fs.writeFileSync(filePath, '{broken'); const store = createStateStore(filePath, { now: () => new Date('2026-07-11T12:00:00.000Z'), }); const recovered = store.read(); assert.equal(recovered.schemaVersion, STATE_SCHEMA_VERSION); assert.equal(recovered.revision, 0); assert.equal(store.recovery.kind, 'corrupt-json'); assert.equal(fs.readFileSync(store.recovery.backupPath, 'utf8'), '{broken'); assert.equal(JSON.parse(fs.readFileSync(filePath, 'utf8')).schemaVersion, STATE_SCHEMA_VERSION); }); test('concurrent updates are serialized without lost values', async (t) => { const store = createStateStore(fixture(t)); store.read(); await Promise.all(Array.from({ length: 50 }, () => Promise.resolve().then(() => ( store.update((state) => ({ ...state, counter: (state.counter || 0) + 1 })) )))); assert.equal(store.read().counter, 50); assert.throws(() => store.update(async (state) => state), /must be synchronous/); });