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'; 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))); } async function freePort() { const server = http.createServer(); const port = await listen(server); await new Promise((resolve) => server.close(resolve)); return port; } function socketRequest(socketPath, pathname) { return new Promise((resolve, reject) => { const request = http.request({ socketPath, path: pathname }, (response) => { const chunks = []; response.on('data', (chunk) => chunks.push(chunk)); response.on('end', () => { try { resolve({ status: response.statusCode, body: JSON.parse(Buffer.concat(chunks).toString('utf8')) }); } catch (error) { reject(error); } }); }); request.on('error', reject); request.end(); }); } async function httpText(url) { const response = await fetch(url); return { status: response.status, type: response.headers.get('content-type'), body: await response.text() }; } async function waitFor(probe, child, stderr) { for (let attempt = 0; attempt < 100; attempt += 1) { if (child.exitCode !== null) throw new Error(`Compiled Harbor exited early: ${stderr()}`); try { const result = await probe(); if (result) return result; } catch { // The disposable listener is still starting. } await new Promise((resolve) => setTimeout(resolve, 20)); } throw new Error(`Compiled Harbor did not become ready: ${stderr()}`); } async function stop(child) { child.kill('SIGTERM'); if (child.exitCode === null) await new Promise((resolve) => child.once('exit', resolve)); assert.equal(child.exitCode, 0); } test('compiled dispatcher starts and stops control and dataplane contracts', async (t) => { const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-compiled-')); const controlData = path.join(directory, 'control'); const dataplaneData = path.join(directory, 'dataplane'); const socketPath = path.join(directory, 'dataplane.sock'); const port = await freePort(); fs.mkdirSync(controlData); fs.mkdirSync(dataplaneData); const children = []; t.after(async () => { for (const child of children) { if (child.exitCode === null) await stop(child); } fs.rmSync(directory, { recursive: true, force: true }); }); const start = (env) => { const child = spawn(process.execPath, ['dist/server/main.js'], { cwd: root, env: { ...process.env, ...env }, stdio: ['ignore', 'ignore', 'pipe'], }); let stderr = ''; child.stderr.on('data', (chunk) => { stderr += chunk; }); children.push(child); return { child, stderr: () => stderr }; }; const control = start({ APP_COMPONENT: 'control', APP_MODE: 'client', DATA_DIR: controlData, DIST_DIR: '', HARBOR_HOST_NETWORK_STATE: path.join(directory, 'missing-network.json'), PORT: String(port), SING_BOX_CACHE: path.join(controlData, 'cache.db'), }); const state = await waitFor(async () => { const response = await fetch(`http://127.0.0.1:${port}/api/state`); return response.ok ? response.json() : null; }, control.child, control.stderr); assert.equal(state.apiVersion, 1); assert.equal(state.mode, 'client'); const page = await httpText(`http://127.0.0.1:${port}/`); assert.equal(page.status, 200); assert.match(page.type, /^text\/html/); assert.match(page.body, /
<\/div>/); await stop(control.child); const dataplane = start({ APP_COMPONENT: 'dataplane', APP_MODE: 'gateway', DATA_DIR: dataplaneData, DATAPLANE_SOCKET: socketPath, SING_BOX_CACHE: path.join(dataplaneData, 'cache.db'), SING_BOX_CONFIG: path.join(dataplaneData, 'missing-config.json'), }); const status = await waitFor(async () => { const response = await socketRequest(socketPath, '/status'); return response.status === 200 ? response.body : null; }, dataplane.child, dataplane.stderr); assert.equal(status.ready, true); await stop(dataplane.child); }); test('production paths use only the compiled dispatcher', () => { const read = (file) => fs.readFileSync(path.join(root, file), 'utf8'); const packageJson = JSON.parse(read('package.json')); const main = read('src/server/main.ts'); const gatewayEntrypoint = read('entrypoint.sh'); const clientEntrypoint = read('entrypoint.client.sh'); const workflow = read('.gitea/workflows/gateway-build.yml'); const legacyBuild = read('scripts/build-on-107-deploy-111.sh'); const dockerignore = read('.dockerignore'); assert.equal(packageJson.scripts['build:production'], 'npm run build && npm run build:server'); assert.equal(packageJson.scripts.prestart, 'npm run build:production'); assert.equal(packageJson.scripts.start, 'node dist/server/main.js'); assert.match(main, /APP_COMPONENT === 'dataplane'/); assert.match(main, /process\.env\.DIST_DIR \|\|= path\.resolve\('dist'\)/); assert.match(main, /import\('\.\/dataplane\.js'\)/); assert.match(main, /import\('\.\/index\.js'\)/); for (const entrypoint of [gatewayEntrypoint, clientEntrypoint]) { assert.match(entrypoint, /node \/app\/dist\/server\/main\.js/); assert.doesNotMatch(entrypoint, /\/app\/src/); } assert.match(workflow, /npm run build:production/); assert.match(workflow, /NODE_BUILD_IMAGE: mirror\.gcr\.io\/library\/node:20\.19-bookworm/); assert.match(workflow, /command -v npm[^']+command -v git[^']+test -x \/bin\/bash/); assert.doesNotMatch(workflow, /docker image inspect "\$\{\{ env\.NODE_BUILD_IMAGE \}\}"/); assert.match(legacyBuild, /npm run build:production && docker build/); assert.match(dockerignore, /^dist$/m); });