import test from 'node:test'; import assert from 'node:assert/strict'; import { validateComposeModel } from '../runtime/validate-compose-model.mjs'; const safeRuntime = { environment: { CORE_SOURCE_MODE: 'clone', NODE_OPTIONS: '--max-old-space-size=1536', TURN_DAEMON_NODE_OPTIONS: '--max-old-space-size=3072', RAYON_NUM_THREADS: '2', GATEWAY_IMAGE_UPLOAD_URL: 'https://sam-image.hided.net', GATEWAY_IMAGE_UPLOAD_SECRET_FILE: '/run/secrets/image_upload_core2026_secret', GATEWAY_SHARED_ICON_PUBLIC_URL: 'https://sam-image.hided.net/icons', GATEWAY_USER_ICON_PUBLIC_URL: 'https://sam-image.hided.net/icons', GAME_IMAGE_UPLOAD_URL: 'https://sam-image.hided.net', GAME_IMAGE_UPLOAD_SECRET_FILE: '/run/secrets/image_upload_core2026_secret', GAME_CONTENT_IMAGE_PUBLIC_URL: 'https://sam-image.hided.net/uploads/core2026', IMAGE_SYNC_URL: 'https://sam-image.hided.net', IMAGE_SYNC_SECRET_FILE: '/run/secrets/image_sync_core2026_secret', VITE_IMAGE_PUBLIC_URL: 'https://sam-image.hided.net', VITE_GATEWAY_USER_ICON_BASE_URL: 'https://sam-image.hided.net/icons', WEB_PUSH_ENABLED: 'false', WEB_PUSH_VAPID_PRIVATE_KEY_FILE: '/run/secrets/web_push_vapid_private_key', FRONTEND_SERVE_MODE: 'static', FRONTEND_SHARED_ASSET_PUBLIC_PATH: '/gateway/profile-assets', RELEASE_BUILDER_URL: 'http://builder:15100', GATEWAY_ACTIVE_RELEASE_GIT_REF: 'refs/sammo/active-gateway', }, volumes: [ { type: 'bind', source: '/secrets/image_upload_core2026_secret', target: '/run/secrets/image_upload_core2026_secret', read_only: true, }, { type: 'volume', source: 'frontend-artifacts', target: '/srv/frontend-artifacts', read_only: false, }, { type: 'bind', source: '/secrets/image_sync_core2026_secret', target: '/run/secrets/image_sync_core2026_secret', read_only: true, }, ], secrets: [ { source: 'web_push_vapid_private_key', target: '/run/secrets/web_push_vapid_private_key', }, ], restart: 'unless-stopped', mem_limit: String(4 * 1024 * 1024 * 1024), memswap_limit: String(4 * 1024 * 1024 * 1024), cpus: 4, pids_limit: 256, }; const safeBuilder = { environment: { NODE_OPTIONS: '--max-old-space-size=3072', RAYON_NUM_THREADS: '2', }, restart: 'unless-stopped', mem_limit: String(4 * 1024 * 1024 * 1024), memswap_limit: String(4 * 1024 * 1024 * 1024), cpus: 4, pids_limit: 256, }; const safeCaddy = { restart: 'unless-stopped', volumes: [ { type: 'volume', source: 'frontend-artifacts', target: '/srv/frontend-artifacts', read_only: true, }, ], }; const safeServices = () => ({ runtime: structuredClone(safeRuntime), builder: structuredClone(safeBuilder), caddy: structuredClone(safeCaddy), }); test('accepts a bounded production runtime', () => { assert.deepEqual(validateComposeModel({ services: safeServices() }, 'production'), []); }); test('accepts a literal, non-restarting development model', () => { const services = safeServices(); services.postgres = { restart: 'no' }; services.redis = { restart: 'no' }; services.builder.restart = 'no'; services.caddy.restart = 'no'; services.runtime = { ...safeRuntime, restart: 'no', environment: { ...safeRuntime.environment, CORE_SOURCE_MODE: 'bind', RUNTIME_MODE: 'development', }, }; assert.deepEqual(validateComposeModel({ services }, 'development'), []); }); test('accepts a bounded, non-restarting production smoke model', () => { const services = safeServices(); services.postgres = { restart: 'no' }; services.redis = { restart: 'no' }; services.builder.restart = 'no'; services.caddy.restart = 'no'; services.runtime = { ...safeRuntime, restart: 'no' }; assert.deepEqual(validateComposeModel({ services }, 'smoke'), []); }); test('rejects an unbounded or production-mode development runtime', () => { const services = safeServices(); services.postgres = { restart: 'unless-stopped' }; services.redis = { restart: 'unless-stopped' }; services.runtime = { environment: { CORE_SOURCE_MODE: 'bind', RUNTIME_MODE: 'production' }, restart: 'unless-stopped', mem_limit: 0, memswap_limit: 0, cpus: 0, pids_limit: 0, }; const errors = validateComposeModel({ services }, 'development'); assert.ok(errors.some((error) => error.includes('memory limit must be positive'))); assert.ok(errors.some((error) => error.includes('literal development'))); assert.ok(errors.some((error) => error.includes('restart policy must be no'))); }); test('rejects missing or excessive build memory and parallelism limits', () => { const runtime = { ...safeRuntime, environment: { CORE_SOURCE_MODE: 'clone', NODE_OPTIONS: '--max-old-space-size=4096', TURN_DAEMON_NODE_OPTIONS: '--max-old-space-size=8192', RAYON_NUM_THREADS: '8', }, }; const services = safeServices(); services.runtime = runtime; const errors = validateComposeModel({ services }, 'production'); assert.ok(errors.some((error) => error.includes('Node heap limit'))); assert.ok(errors.some((error) => error.includes('turn daemon Node heap limit'))); assert.ok(errors.some((error) => error.includes('Rayon thread count'))); }); test('requires Web Push activation to keep the private key in a mounted secret', () => { const services = safeServices(); services.runtime.environment.WEB_PUSH_ENABLED = 'true'; services.runtime.secrets = []; const errors = validateComposeModel({ services }, 'production'); assert.ok(errors.some((error) => error.includes('VAPID private key'))); assert.ok(errors.some((error) => error.includes('WEB_PUSH_VAPID_SUBJECT'))); assert.ok(errors.some((error) => error.includes('WEB_PUSH_VAPID_PUBLIC_KEY'))); });