import assert from 'node:assert/strict'; import { spawn } from 'node:child_process'; import fs from 'node:fs'; import http from 'node:http'; import os from 'node:os'; import path from 'node:path'; import test from 'node:test'; import { assertStateSnapshot, createStateSnapshot, normalizeStoredState, } from '../../src/shared/contracts/state.js'; const root = path.resolve(import.meta.dirname, '../..'); function listen(server) { return new Promise((resolve) => server.listen(0, '127.0.0.1', () => resolve(server.address().port))); } function close(server) { return new Promise((resolve) => server.close(resolve)); } async function freePort() { const server = http.createServer(); const port = await listen(server); await close(server); return port; } async function rawRequest(port, pathname, method = 'GET', body) { const response = await fetch(`http://127.0.0.1:${port}${pathname}`, { method, headers: { 'content-type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body), }); const payload = await response.json(); return { response, payload }; } async function request(port, pathname, method = 'GET', body) { const { response, payload } = await rawRequest(port, pathname, method, body); assert.equal(response.ok, true, JSON.stringify(payload)); return payload; } async function waitForState(port, child, stderr) { for (let attempt = 0; attempt < 100; attempt += 1) { if (child.exitCode !== null) throw new Error(`Harbor exited early: ${stderr()}`); try { return await request(port, '/api/state'); } catch { await new Promise((resolve) => setTimeout(resolve, 20)); } } throw new Error(`Harbor did not start: ${stderr()}`); } test('state v1 normalizes legacy storage and validates the canonical snapshot', () => { const stored = normalizeStoredState({ subscriptionUrl: 'https://provider.example/subscription/test', selectedTag: ' legacy ', servers: [{ tag: ' legacy ', type: 'vless', server: 'vpn.example', server_port: 443 }], }); const snapshot = createStateSnapshot({ storedState: stored, runtime: { running: true, startedAt: '2026-07-11T10:00:00.000Z' }, gatewayAuto: null, appMode: 'gateway', configExists: true, subscriptionHost: 'provider.example/…', now: new Date('2026-07-11T12:00:00.000Z'), }); assert.equal(snapshot.apiVersion, 1); assert.deepEqual(snapshot.selection, { desiredServerId: 'legacy', appliedServerId: 'legacy', }); assert.equal(snapshot.connection.process, 'running'); assert.equal(JSON.stringify(snapshot).includes(stored.subscriptionUrl), false); assert.throws( () => assertStateSnapshot({ ...snapshot, revision: -1 }), /Invalid Harbor state snapshot v1/, ); }); test('GET and domain mutations return one state shape with monotonic revisions', async (t) => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-state-contract-')); const binDir = path.join(dir, 'bin'); const config = { outbounds: [{ type: 'vless', tag: 'test-vpn', server: 'vpn.example.test', server_port: 443, uuid: '00000000-0000-4000-8000-000000000000', tls: { enabled: true }, }], }; fs.mkdirSync(binDir); const singboxPath = path.join(binDir, 'sing-box'); const workingSingbox = `#!/usr/bin/env node if (process.argv[2] === 'check') process.exit(0); if (process.argv[2] === 'version') { console.log('sing-box version 1.12.13'); process.exit(0); } process.on('SIGTERM', () => process.exit(0)); setInterval(() => {}, 60_000); `; fs.writeFileSync(singboxPath, workingSingbox); fs.chmodSync(singboxPath, 0o755); const subscriptionServer = http.createServer((req, res) => { if (req.url === '/unavailable') { res.writeHead(503); return res.end('unavailable'); } res.writeHead(200, { 'content-type': 'application/json', 'subscription-userinfo': 'upload=10; download=20; total=100', }); res.end(JSON.stringify(config)); }); const subscriptionPort = await listen(subscriptionServer); const subscriptionUrl = `http://127.0.0.1:${subscriptionPort}/subscription/test`; fs.writeFileSync(path.join(dir, 'state.json'), JSON.stringify({ subscriptionUrl, selectedTag: 'test-vpn', servers: [{ tag: 'test-vpn', type: 'vless', server: 'vpn.example.test', server_port: 443 }], })); fs.writeFileSync(path.join(dir, 'subscription-cache.json'), JSON.stringify({ url: subscriptionUrl, config, })); const port = await freePort(); const child = spawn(process.execPath, ['src/server/index.js'], { cwd: root, env: { ...process.env, APP_MODE: 'client', DATA_DIR: dir, PORT: String(port), PATH: `${binDir}:${process.env.PATH}`, HARBOR_HOST_NETWORK_STATE: path.join(dir, 'missing-network.json'), }, stdio: ['ignore', 'ignore', 'pipe'], }); let stderr = ''; child.stderr.on('data', (chunk) => { stderr += chunk; }); t.after(async () => { child.kill('SIGTERM'); if (child.exitCode === null) await new Promise((resolve) => child.once('exit', resolve)); await close(subscriptionServer); fs.rmSync(dir, { recursive: true, force: true }); }); const initial = await waitForState(port, child, () => stderr); const version = await request(port, '/api/version'); assert.deepEqual(version, { apiVersion: 1, location: 'mac', components: { macClient: '0.1.0' }, runtime: { singBox: '1.12.13' }, }); assertStateSnapshot(initial); assert.equal(initial.selection.appliedServerId, 'test-vpn'); assert.equal(JSON.stringify(initial).includes(subscriptionUrl), false); const stateKeys = Object.keys(initial).sort(); let revision = initial.revision; const invalidSubscription = await rawRequest( port, '/api/subscription/validate', 'POST', { url: 'not-a-url' }, ); assert.equal(invalidSubscription.response.status, 400); assert.deepEqual( { code: invalidSubscription.payload.error.code, retryable: invalidSubscription.payload.error.retryable, }, { code: 'SUBSCRIPTION_INVALID', retryable: false }, ); assert.equal(typeof invalidSubscription.payload.error.correlationId, 'string'); const providerUnavailable = await rawRequest( port, '/api/subscription/validate', 'POST', { url: `http://127.0.0.1:${subscriptionPort}/unavailable` }, ); assert.equal(providerUnavailable.response.status, 502); assert.equal(providerUnavailable.payload.error.code, 'PROVIDER_UNAVAILABLE'); assert.equal(providerUnavailable.payload.error.retryable, true); const missingServer = await rawRequest( port, '/api/apply', 'POST', { selectedTag: 'missing-server' }, ); assert.equal(missingServer.response.status, 404); assert.equal(missingServer.payload.error.code, 'SERVER_NOT_FOUND'); assert.equal((await request(port, '/api/state')).selection.desiredServerId, 'test-vpn'); async function stateResponse(pathname, method = 'POST', body) { const result = await request(port, pathname, method, body); assert.deepEqual(Object.keys(result.state).sort(), stateKeys); assertStateSnapshot(result.state); return result; } async function mutation(pathname, method = 'POST', body) { const result = await stateResponse(pathname, method, body); assert.ok(result.state.revision > revision, `${pathname} did not increase revision`); revision = result.state.revision; return result; } await stateResponse('/api/subscription/validate', 'POST', { url: subscriptionUrl }); await mutation('/api/subscription/fetch', 'POST', { url: subscriptionUrl }); const applied = await mutation('/api/apply', 'POST', { selectedTag: 'test-vpn' }); assert.deepEqual(applied.state.selection, { desiredServerId: 'test-vpn', appliedServerId: 'test-vpn', }); assert.equal(applied.state.connection.process, 'running'); assert.equal((await mutation('/api/singbox/stop')).state.connection.desired, 'stopped'); assert.equal((await mutation('/api/singbox/restart')).state.connection.desired, 'running'); fs.writeFileSync(singboxPath, `#!/usr/bin/env node if (process.argv[2] === 'check') { require('node:fs').unlinkSync(process.argv[1]); process.exit(0); } `); fs.chmodSync(singboxPath, 0o755); const processFailure = await rawRequest(port, '/api/singbox/restart', 'POST'); assert.equal(processFailure.response.status, 503); assert.equal(processFailure.payload.error.code, 'PROCESS_START_FAILED'); assert.equal(processFailure.payload.error.retryable, true); fs.writeFileSync(singboxPath, workingSingbox); fs.chmodSync(singboxPath, 0o755); await mutation('/api/subscription/refresh'); assert.equal((await mutation('/api/gateway-auto', 'POST', { enabled: false })).state.gatewayAuto.enabled, false); const forgotten = await mutation('/api/subscription', 'DELETE'); assert.equal(forgotten.state.subscription.status, 'missing'); assert.equal(forgotten.state.servers.length, 0); assert.deepEqual((await stateResponse('/api/servers/ping-all')).results, []); const missingConfig = await rawRequest(port, '/api/singbox/restart', 'POST'); assert.equal(missingConfig.response.status, 422); assert.equal(missingConfig.payload.error.code, 'CONFIG_INVALID'); assert.equal(missingConfig.payload.error.retryable, false); });