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'; 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('a 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' }], }; 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(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('corrupt JSON is preserved and replaced with an 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/); });