// Unit tests for registry loading — validates the path resolution and // structural invariants of deploy-registry.json. // Run with: node --test test/registry-loader.test.js import assert from 'node:assert/strict'; import { test } from 'node:test'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; // On Bruno the registry is bind-mounted into the container at /app/deploy-registry.json // (process.cwd() = /app). Locally (dev on gal), it lives in coder-core. // Accept an override via env var so CI / test runners can point at any copy. const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const REGISTRY_FILE = process.env.REGISTRY_FILE ?? path.join(ROOT, 'deploy-registry.json') // container bind-mount ?? path.join(ROOT, '../../coder-core/services/kua-deploy/deploy-registry.json'); // local dev fallback // ------------------------------------------------------------------ // Bug #7 regression: DEPLOY_REGISTRY_PATH must resolve to a real file. // In kua-mcp-core the old ROOT was computed via __dirname + "../../../" // which overshoots to "/" inside the container. Here we validate the // kua-deploy-native path (process.cwd() relative) and the mcp-core // path (CODER_CORE_ROOT env-var-backed). // ------------------------------------------------------------------ let registry; test('registry file exists at expected path', () => { assert.ok(fs.existsSync(REGISTRY_FILE), `registry not found at ${REGISTRY_FILE}`); const raw = fs.readFileSync(REGISTRY_FILE, 'utf-8'); registry = JSON.parse(raw); }); test('registry has apps object', () => { assert.ok(registry && typeof registry.apps === 'object', 'registry.apps must be an object'); const count = Object.keys(registry.apps).length; assert.ok(count >= 5, `expected at least 5 registered apps, got ${count}`); }); test('every app has required fields', () => { for (const [name, cfg] of Object.entries(registry.apps)) { assert.ok(typeof cfg.repo_dir === 'string' && cfg.repo_dir.length > 0, `${name}: repo_dir must be a non-empty string`); assert.ok(typeof cfg.deploy_mode === 'string', `${name}: deploy_mode must be present`); assert.ok(cfg.production && typeof cfg.production === 'object', `${name}: production config must be present`); } }); test('no app uses webhook deploy_mode (webhook path is retired)', () => { const webhookApps = Object.entries(registry.apps) .filter(([, cfg]) => cfg.deploy_mode === 'webhook') .map(([name]) => name); assert.deepEqual(webhookApps, [], `These apps still have deploy_mode=webhook (retire them to direct): ${webhookApps.join(', ')}`); }); test('kua-deploy is registered in its own registry', () => { assert.ok('kua-deploy' in registry.apps, 'kua-deploy must be in deploy-registry.json'); assert.equal(registry.apps['kua-deploy'].deploy_mode, 'direct'); });