Caddy가 commit-digest 공용 asset과 sourcemap을 immutable URL로 제공하고, runtime에 공용 공개 경로를 명시한다.
152 lines
5.0 KiB
JavaScript
152 lines
5.0 KiB
JavaScript
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',
|
|
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,
|
|
},
|
|
],
|
|
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')));
|
|
});
|