mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-15 20:20:09 +00:00
194 lines
6.6 KiB
TypeScript
194 lines
6.6 KiB
TypeScript
import { describe, expect, it } from 'bun:test';
|
|
import { DockerExecutor } from '../../../src/docker/docker-executor';
|
|
import type { DockerCommandResult } from '../../../src/docker/docker-types';
|
|
|
|
const fakeAssets = {
|
|
dockerDir: '/tmp/ccs-docker',
|
|
composeFile: '/tmp/ccs-docker/docker-compose.integrated.yml',
|
|
dockerfile: '/tmp/ccs-docker/Dockerfile.integrated',
|
|
supervisordConfig: '/tmp/ccs-docker/supervisord.conf',
|
|
entrypoint: '/tmp/ccs-docker/entrypoint-integrated.sh',
|
|
};
|
|
|
|
type SyncCall = {
|
|
command: string;
|
|
args: string[];
|
|
options?: {
|
|
cwd?: string;
|
|
env?: NodeJS.ProcessEnv;
|
|
remote?: boolean;
|
|
timeoutMs?: number;
|
|
};
|
|
};
|
|
|
|
function okResult(remote = false): DockerCommandResult {
|
|
return {
|
|
command: '',
|
|
exitCode: 0,
|
|
stdout: '',
|
|
stderr: '',
|
|
remote,
|
|
};
|
|
}
|
|
|
|
describe('docker executor', () => {
|
|
it('passes compose env and bundled compose file when bringing the stack up locally', async () => {
|
|
const calls: SyncCall[] = [];
|
|
const executor = new DockerExecutor({
|
|
assets: fakeAssets,
|
|
getInstalledCcsVersion: () => '7.59.0',
|
|
resolveLocalComposePrefix: () => ['docker', 'compose'],
|
|
runSync: (command, args, options) => {
|
|
calls.push({ command, args, options });
|
|
return okResult(options?.remote ?? false);
|
|
},
|
|
});
|
|
|
|
await executor.up({ port: 4000, proxyPort: 9317 });
|
|
|
|
expect(calls).toHaveLength(1);
|
|
expect(calls[0].command).toBe('docker');
|
|
expect(calls[0].args).toEqual(['compose', '-f', fakeAssets.composeFile, 'up', '-d', '--build']);
|
|
expect(calls[0].options?.cwd).toBe(fakeAssets.dockerDir);
|
|
expect(calls[0].options?.env?.CCS_NPM_VERSION).toBe('7.59.0');
|
|
expect(calls[0].options?.env?.CCS_DASHBOARD_PORT).toBe('4000');
|
|
expect(calls[0].options?.env?.CCS_CLIPROXY_PORT).toBe('9317');
|
|
expect(calls[0].options?.env?.CCS_DOCKER_BIND_HOST).toBe('127.0.0.1');
|
|
expect(calls[0].options?.env?.CCS_DOCKER_LEGACY_KEY_GRACE_DAYS).toBe('');
|
|
expect(calls[0].options?.env?.CCS_DOCKER_RESTORE_LEGACY_API_KEY).toBe('');
|
|
expect(calls[0].options?.timeoutMs).toBe(300_000);
|
|
});
|
|
|
|
it('stages bundled assets before remote compose startup', async () => {
|
|
const calls: SyncCall[] = [];
|
|
const executor = new DockerExecutor({
|
|
assets: fakeAssets,
|
|
getInstalledCcsVersion: () => '7.59.0',
|
|
runSync: (command, args, options) => {
|
|
calls.push({ command, args, options });
|
|
return okResult(options?.remote ?? false);
|
|
},
|
|
});
|
|
|
|
await executor.up({ host: 'docker', port: 3000, proxyPort: 8317 });
|
|
|
|
expect(calls).toHaveLength(3);
|
|
expect(calls[0]).toEqual({
|
|
command: 'ssh',
|
|
args: ['docker', 'mkdir -p ~/.ccs/docker'],
|
|
options: { remote: true, timeoutMs: 30_000 },
|
|
});
|
|
expect(calls[1].command).toBe('scp');
|
|
expect(calls[1].args).toEqual([
|
|
fakeAssets.composeFile,
|
|
fakeAssets.dockerfile,
|
|
fakeAssets.supervisordConfig,
|
|
fakeAssets.entrypoint,
|
|
'docker:~/.ccs/docker/',
|
|
]);
|
|
expect(calls[1].options).toEqual({ remote: true, timeoutMs: 30_000 });
|
|
expect(calls[2].command).toBe('ssh');
|
|
expect(calls[2].args[0]).toBe('docker');
|
|
expect(calls[2].args[1]).toContain("export CCS_NPM_VERSION='7.59.0'");
|
|
expect(calls[2].args[1]).toContain("export CCS_DASHBOARD_PORT='3000'");
|
|
expect(calls[2].args[1]).toContain("export CCS_CLIPROXY_PORT='8317'");
|
|
expect(calls[2].args[1]).toContain("export CCS_DOCKER_BIND_HOST='127.0.0.1'");
|
|
expect(calls[2].args[1]).toContain("export CCS_DOCKER_LEGACY_KEY_GRACE_DAYS=''");
|
|
expect(calls[2].args[1]).toContain("export CCS_DOCKER_RESTORE_LEGACY_API_KEY=''");
|
|
expect(calls[2].args[1]).toContain('docker-compose version >/dev/null 2>&1');
|
|
expect(calls[2].options?.timeoutMs).toBe(300_000);
|
|
});
|
|
|
|
it('uses npm install latest rather than npm update during in-container updates', async () => {
|
|
const calls: SyncCall[] = [];
|
|
const executor = new DockerExecutor({
|
|
assets: fakeAssets,
|
|
runSync: (command, args, options) => {
|
|
calls.push({ command, args, options });
|
|
return okResult(options?.remote ?? false);
|
|
},
|
|
});
|
|
|
|
await executor.update({});
|
|
|
|
expect(calls).toHaveLength(1);
|
|
expect(calls[0].command).toBe('docker');
|
|
expect(calls[0].args[0]).toBe('exec');
|
|
expect(calls[0].args[1]).toBe('ccs-cliproxy');
|
|
expect(calls[0].args[4]).toContain('npm install -g @kaitranntt/ccs@latest --force');
|
|
expect(calls[0].args[4]).toContain('ccs cliproxy --latest');
|
|
expect(calls[0].args[4]).toContain(
|
|
'supervisorctl -c /etc/supervisord.conf restart ccs-dashboard cliproxy'
|
|
);
|
|
});
|
|
|
|
it('preserves supervisorctl failures in status results for CLI rendering', async () => {
|
|
let callCount = 0;
|
|
const executor = new DockerExecutor({
|
|
assets: fakeAssets,
|
|
resolveLocalComposePrefix: () => ['docker', 'compose'],
|
|
runSync: (_command, args, options) => {
|
|
callCount++;
|
|
if (callCount === 1) {
|
|
expect(args).toEqual(['compose', '-f', fakeAssets.composeFile, 'ps']);
|
|
return {
|
|
command: '',
|
|
exitCode: 0,
|
|
stdout: 'NAME STATUS',
|
|
stderr: '',
|
|
remote: options?.remote ?? false,
|
|
};
|
|
}
|
|
|
|
expect(args).toEqual([
|
|
'exec',
|
|
'ccs-cliproxy',
|
|
'supervisorctl',
|
|
'-c',
|
|
'/etc/supervisord.conf',
|
|
'status',
|
|
]);
|
|
return {
|
|
command: '',
|
|
exitCode: 7,
|
|
stdout: '',
|
|
stderr: 'unix:///var/run/supervisor.sock no such file',
|
|
remote: options?.remote ?? false,
|
|
};
|
|
},
|
|
});
|
|
|
|
const status = await executor.status({});
|
|
|
|
expect(status.compose.exitCode).toBe(0);
|
|
expect(status.supervisor?.exitCode).toBe(7);
|
|
expect(status.supervisor?.stderr).toContain('supervisor.sock');
|
|
});
|
|
|
|
it('surfaces a clear timeout message for blocked sync commands', () => {
|
|
const executor = new DockerExecutor({
|
|
assets: fakeAssets,
|
|
});
|
|
|
|
const result = (
|
|
executor as unknown as {
|
|
runSync: (
|
|
command: string,
|
|
args: string[],
|
|
options?: { remote?: boolean; timeoutMs?: number }
|
|
) => DockerCommandResult;
|
|
}
|
|
).runSync(process.execPath, ['-e', 'setTimeout(() => {}, 1000)'], {
|
|
remote: true,
|
|
timeoutMs: 25,
|
|
});
|
|
|
|
expect(result.exitCode).toBe(1);
|
|
expect(result.stderr).toContain(
|
|
'Command timed out after 25ms while running a remote Docker command.'
|
|
);
|
|
expect(result.stderr).toContain(`Command: ${process.execPath}`);
|
|
expect(result.stderr).toContain('Check SSH reachability and the remote Docker host');
|
|
});
|
|
});
|