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 '../../src/server/services/stateStore.js'; import { createServerId } from '../../src/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('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 }, ]); assert.equal(migrated.appliedTag, 'nl'); assert.equal(migrated.selectedServerId, createServerId(legacy.servers[0])); assert.equal(migrated.appliedServerId, migrated.selectedServerId); 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.selectedServerId, ''); assert.equal(migrated.appliedServerId, ''); assert.equal(migrated.servers.length, 2); }); 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/); });