Initial import

This commit is contained in:
how2ice
2026-04-21 03:33:23 +09:00
commit 9e4b70f1f1
495 changed files with 94680 additions and 0 deletions

View File

@@ -0,0 +1,242 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { EventEmitter } from 'node:events';
import { writeFile } from 'node:fs/promises';
import { env } from '../config/env.js';
import {
buildHealthCheckUrls,
buildRestartFailureMessage,
buildServerCommandApiRestartUrl,
listServerCommands,
resolveDockerSocketPath,
restartServerCommand,
} from './server-command-service.js';
test('buildRestartFailureMessage includes exit info and stderr output', () => {
const message = buildRestartFailureMessage(
'TEST',
Object.assign(new Error('Command failed'), {
code: 1,
stderr: 'no such service: app',
stdout: '',
}),
);
assert.match(message, /TEST 재기동에 실패했습니다\./);
assert.match(message, /exit:1/);
assert.match(message, /no such service: app/);
});
test('listServerCommands uses app as the default test restart service', async () => {
const commands = await listServerCommands();
const testCommand = commands.find((item) => item.key === 'test');
assert.ok(testCommand);
assert.equal(testCommand.serviceName, 'app');
});
test('listServerCommands resolves restart script from main project when project root fallback is needed', async () => {
const commands = await listServerCommands();
const testCommand = commands.find((item) => item.key === 'test');
assert.ok(testCommand);
assert.match(testCommand.commandScript, /\/etc\/commands\/server-command\/restart-test\.sh$/);
assert.notEqual(testCommand.commandScript, '/etc/commands/server-command/restart-test.sh');
});
test('test and release restart scripts fall back to Docker socket when docker CLI is unavailable', () => {
const commandsRoot = new URL('../../../../commands/server-command/', import.meta.url);
const testScript = fs.readFileSync(new URL('restart-test.sh', commandsRoot), 'utf8');
const relScript = fs.readFileSync(new URL('restart-rel.sh', commandsRoot), 'utf8');
const workServerScript = fs.readFileSync(new URL('restart-work-server.sh', commandsRoot), 'utf8');
const socketRestartScript = fs.readFileSync(new URL('restart-via-docker-socket.mjs', commandsRoot), 'utf8');
assert.match(testScript, /command -v docker >/);
assert.match(testScript, /docker compose -f "\$SERVER_COMMAND_COMPOSE_FILE" restart "\$SERVER_COMMAND_SERVICE"/);
assert.match(testScript, /docker compose -f "\$SERVER_COMMAND_COMPOSE_FILE" up -d --no-deps "\$SERVER_COMMAND_SERVICE"/);
assert.match(testScript, /restart-via-docker-socket\.mjs/);
assert.match(testScript, /SERVER_COMMAND_CONTAINER_NAME="\$\{SERVER_COMMAND_CONTAINER_NAME:-ai-code-app-app-1\}"/);
assert.match(relScript, /command -v docker >/);
assert.match(relScript, /docker compose -f "\$SERVER_COMMAND_COMPOSE_FILE" restart "\$SERVER_COMMAND_SERVICE"/);
assert.match(relScript, /docker compose -f "\$SERVER_COMMAND_COMPOSE_FILE" up -d --no-deps "\$SERVER_COMMAND_SERVICE"/);
assert.match(relScript, /restart-via-docker-socket\.mjs/);
assert.match(relScript, /SERVER_COMMAND_CONTAINER_NAME="\$\{SERVER_COMMAND_CONTAINER_NAME:-ai-code-app-release\}"/);
assert.match(
workServerScript,
/docker compose -f etc\/servers\/work-server\/docker-compose\.yml up -d --build --force-recreate --no-deps work-server/,
);
assert.match(socketRestartScript, /\/containers\/\$\{encodeURIComponent\(containerName\)\}\/restart\?t=30/);
});
test('work-server package dev script does not use watch mode and rebuilds before start', async () => {
const packageJsonPath = new URL('../../package.json', import.meta.url);
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) as {
scripts?: Record<string, string>;
};
assert.equal(packageJson.scripts?.dev, 'npm run build && npm run start');
assert.doesNotMatch(String(packageJson.scripts?.dev ?? ''), /\bwatch\b/i);
});
test('buildServerCommandApiRestartUrl replaces key placeholders on configured command api endpoint', () => {
assert.equal(
buildServerCommandApiRestartUrl('http://127.0.0.1:3200/', '/commands/{key}/restart', 'work-server'),
'http://127.0.0.1:3200/commands/work-server/restart',
);
});
test('buildServerCommandApiRestartUrl avoids duplicating an existing base path prefix', () => {
assert.equal(
buildServerCommandApiRestartUrl(
'http://127.0.0.1:3211/api',
'/api/server-commands/{key}/actions/restart',
'test',
),
'http://127.0.0.1:3211/api/server-commands/test/actions/restart',
);
});
test('buildHealthCheckUrls adds localhost fallbacks for command-runner', () => {
assert.deepEqual(buildHealthCheckUrls('command-runner', 'http://host.docker.internal:3211/health'), [
'http://host.docker.internal:3211/health',
'http://127.0.0.1:3211/health',
'http://localhost:3211/health',
]);
assert.deepEqual(buildHealthCheckUrls('work-server', 'http://host.docker.internal:3100/health'), [
'http://host.docker.internal:3100/health',
]);
});
test('resolveDockerSocketPath prefers explicit socket path and falls back to unix DOCKER_HOST', () => {
assert.equal(
resolveDockerSocketPath({
SERVER_COMMAND_DOCKER_SOCKET: '/custom/docker.sock',
DOCKER_HOST: 'unix:///run/user/1000/docker.sock',
}),
'/custom/docker.sock',
);
assert.equal(
resolveDockerSocketPath({
DOCKER_HOST: 'unix:///run/user/1000/docker.sock',
}),
'/run/user/1000/docker.sock',
);
});
test('restartServerCommand delegates to configured command api when base url is provided', async (t) => {
const originalBaseUrl = env.SERVER_COMMAND_API_BASE_URL;
const originalAccessToken = env.SERVER_COMMAND_API_ACCESS_TOKEN;
const originalPathTemplate = env.SERVER_COMMAND_API_RESTART_PATH_TEMPLATE;
env.SERVER_COMMAND_API_BASE_URL = 'http://127.0.0.1:3200/api';
env.SERVER_COMMAND_API_ACCESS_TOKEN = 'local-command-token';
env.SERVER_COMMAND_API_RESTART_PATH_TEMPLATE = '/commands/{key}/restart';
const fetchMock = t.mock.method(globalThis, 'fetch', async (input: string | URL | Request, init?: RequestInit) => {
assert.equal(String(input), 'http://127.0.0.1:3200/api/commands/test/restart');
assert.equal(init?.method, 'POST');
const headers = new Headers(init?.headers);
assert.equal(headers.get('X-Access-Token'), 'local-command-token');
return new Response(
JSON.stringify({
restartState: 'accepted',
item: {
key: 'test',
label: 'TEST',
composeStatus: 'restarting',
},
commandOutput: 'restart accepted',
}),
{
status: 200,
headers: {
'content-type': 'application/json',
},
},
);
});
try {
const result = await restartServerCommand('test');
assert.equal(fetchMock.mock.callCount(), 1);
assert.equal(result.restartState, 'accepted');
assert.equal(result.commandOutput, 'restart accepted');
assert.equal(result.server.key, 'test');
assert.equal(result.server.composeStatus, 'restarting');
} finally {
env.SERVER_COMMAND_API_BASE_URL = originalBaseUrl;
env.SERVER_COMMAND_API_ACCESS_TOKEN = originalAccessToken;
env.SERVER_COMMAND_API_RESTART_PATH_TEMPLATE = originalPathTemplate;
}
});
test('restartServerCommand surfaces deferred restart script failures for work-server', async (t) => {
const originalBaseUrl = env.SERVER_COMMAND_API_BASE_URL;
env.SERVER_COMMAND_API_BASE_URL = '';
const childProcessModule = (await import('node:child_process')) as { spawn: (...args: unknown[]) => unknown };
const spawnMock = t.mock.method(
childProcessModule,
'spawn',
(...spawnArgs: unknown[]) => {
const args = Array.isArray(spawnArgs[1]) ? (spawnArgs[1] as string[]) : [];
const shellCommand = String(args[1] ?? '');
const logPath = shellCommand.match(/>"([^"]+\.log)"/)?.[1] ?? '';
const statusPath = shellCommand.match(/>"([^"]+\.status)"/)?.[1] ?? '';
queueMicrotask(() => {
void writeFile(statusPath, '1', 'utf8');
void writeFile(logPath, 'docker compose failed', 'utf8');
});
const child = new EventEmitter() as EventEmitter & { unref(): void };
child.unref = () => undefined;
queueMicrotask(() => {
child.emit('spawn');
});
return child;
},
);
try {
await assert.rejects(() => restartServerCommand('work-server'), /WORK-SERVER 재기동에 실패했습니다\./);
assert.equal(spawnMock.mock.callCount(), 1);
} finally {
env.SERVER_COMMAND_API_BASE_URL = originalBaseUrl;
}
});
test('listServerCommands marks command-runner online when localhost fallback responds', async (t) => {
const fetchMock = t.mock.method(globalThis, 'fetch', async (input: string | URL | Request) => {
const url = String(input);
if (url === 'http://host.docker.internal:3211/health') {
throw new Error('fetch failed');
}
if (url === 'http://127.0.0.1:3211/health') {
return new Response(JSON.stringify({ ok: true, service: 'server-command-runner' }), {
status: 200,
headers: {
'content-type': 'application/json',
},
});
}
return new Response('ok', { status: 200 });
});
const commands = await listServerCommands();
const runnerCommand = commands.find((item) => item.key === 'command-runner');
assert.ok(runnerCommand);
assert.equal(fetchMock.mock.calls.some((call) => String(call.arguments[0]) === 'http://host.docker.internal:3211/health'), true);
assert.equal(fetchMock.mock.calls.some((call) => String(call.arguments[0]) === 'http://127.0.0.1:3211/health'), true);
assert.equal(runnerCommand.availability, 'online');
assert.equal(runnerCommand.httpStatus, 200);
assert.match(String(runnerCommand.errorMessage ?? ''), /fallback health check succeeded via http:\/\/127\.0\.0\.1:3211\/health/);
});