Files
ccs/tests/unit/docker/cliproxy-host-continuity-assets.test.ts

144 lines
6.1 KiB
TypeScript

import { mkdtempSync, readFileSync, rmSync } from 'fs';
import { spawnSync } from 'child_process';
import { describe, expect, it } from 'bun:test';
import { tmpdir } from 'os';
import { join } from 'path';
const updateScript = readFileSync('docker/host/ccs-cliproxy-safe-update.sh', 'utf8');
const reconcileScript = readFileSync('docker/host/ccs-cliproxy-reconcile.sh', 'utf8');
const updateService = readFileSync('docker/host/systemd/ccs-cliproxy-update.service', 'utf8');
const updateTimer = readFileSync('docker/host/systemd/ccs-cliproxy-update.timer', 'utf8');
const reconcileService = readFileSync('docker/host/systemd/ccs-cliproxy-reconcile.service', 'utf8');
const reconcileTimer = readFileSync('docker/host/systemd/ccs-cliproxy-reconcile.timer', 'utf8');
describe('CLIProxy Docker host continuity assets', () => {
it('stages and validates the replacement before stopping the live proxy', () => {
const stageIndex = updateScript.indexOf('CCS_DIR="$stage_ccs_dir" ccs cliproxy --latest');
const validateIndex = updateScript.indexOf('"$stage_binary" --version');
const stopIndex = updateScript.indexOf('supervisorctl_cmd stop cliproxy', validateIndex);
expect(stageIndex).toBeGreaterThan(-1);
expect(validateIndex).toBeGreaterThan(stageIndex);
expect(stopIndex).toBeGreaterThan(validateIndex);
});
it('serializes updates and reconciliation with the same lock', () => {
expect(updateScript).toContain('/run/lock/ccs-cliproxy-maintenance.lock');
expect(reconcileScript).toContain('/run/lock/ccs-cliproxy-maintenance.lock');
expect(updateScript).toContain('flock -n 9');
expect(reconcileScript).toContain('flock -n 9');
});
it('shares the in-container install lifecycle lock with CLI and dashboard installs', () => {
expect(updateScript).toContain(
"install_lock_target='/root/.ccs/cliproxy/bin/.install-lifecycle-plus'"
);
expect(updateScript).toContain('install_lock_dir="$install_lock_target.lock"');
expect(updateScript).toContain('while ! mkdir "$install_lock_dir"');
expect(updateScript).toContain('touch "$install_lock_dir"');
expect(updateScript).toContain('install_lock_stale_seconds=600');
expect(updateScript).toContain('remove_stale_install_lock');
expect(updateScript).toContain('rmdir "$install_lock_dir"');
expect(updateScript.indexOf('acquire_install_lock')).toBeLessThan(
updateScript.indexOf('supervisorctl_cmd stop cliproxy')
);
});
it('reclaims an orphaned stale lifecycle lock', () => {
const testRoot = mkdtempSync(join(tmpdir(), 'ccs-cliproxy-host-lock-'));
const helpersStart = updateScript.indexOf('remove_stale_install_lock() {');
const helpersEnd = updateScript.indexOf('\nwait_for_proxy() {');
const helpers = updateScript.slice(helpersStart, helpersEnd);
try {
const result = spawnSync(
'bash',
[
'-c',
`set -Eeuo pipefail
install_lock_target="$1/target"
install_lock_dir="$install_lock_target.lock"
install_lock_stale_seconds=600
install_lock_owned=0
mkdir -p "$install_lock_target" "$install_lock_dir"
touch -t 200001010000 "$install_lock_dir"
${helpers}
acquire_install_lock
test "$install_lock_owned" -eq 1
release_install_lock`,
'test-shell',
testRoot,
],
{ encoding: 'utf8' }
);
expect(result.status).toBe(0);
} finally {
rmSync(testRoot, { recursive: true, force: true });
}
});
it('rolls back failed swaps and health-checks both services', () => {
expect(updateScript).toContain('rollback()');
expect(updateScript).toContain('previous-binary');
expect(updateScript).toContain('wait_for_proxy');
expect(reconcileScript).toContain('http://127.0.0.1:3000/');
expect(reconcileScript).toContain('http://127.0.0.1:8317/');
});
it('recreates missing containers and escalates unhealthy recovery', () => {
expect(reconcileScript).toContain('CCS_CLIPROXY_COMPOSE_DIR:-/root/.ccs/docker');
expect(reconcileScript).toContain('$compose_dir/docker-compose.integrated.yml');
expect(reconcileScript).toContain('CCS_CLIPROXY_COMPOSE_PROJECT:-docker');
expect(reconcileScript).toContain('--project-name "$compose_project"');
expect(reconcileScript).toContain('-f "$compose_file" up -d --no-build');
expect(reconcileScript).toContain("2>/dev/null || true");
expect(reconcileScript).toContain('if [ -z "$running_state" ]');
expect(reconcileScript).toContain('restart ccs-dashboard cliproxy');
expect(reconcileScript).toContain('docker restart "$container_name"');
expect(reconcileScript).toContain('up -d --force-recreate --no-build');
expect(reconcileScript.indexOf('docker restart "$container_name"')).toBeGreaterThan(
reconcileScript.indexOf('restart ccs-dashboard cliproxy')
);
});
it('installs executable services on bounded timers', () => {
expect(updateService).toContain('ExecStart=/opt/cliproxy/ccs-cliproxy-safe-update.sh');
expect(updateService).toContain('TimeoutStartSec=10min');
expect(updateTimer).toContain('OnUnitActiveSec=15min');
expect(reconcileService).toContain('ExecStart=/opt/cliproxy/ccs-cliproxy-reconcile.sh');
expect(reconcileTimer).toContain('OnUnitActiveSec=30s');
});
it('forces nonzero signal exits so the EXIT trap rolls back maintenance', () => {
expect(updateScript).toContain("trap 'exit 130' INT");
expect(updateScript).toContain("trap 'exit 143' TERM");
expect(updateScript).toContain("trap 'exit 129' HUP");
const handlerStart = updateScript.indexOf('on_exit() {');
const handlerEnd = updateScript.indexOf('\ntrap on_exit EXIT');
const handlers = updateScript.slice(handlerStart, handlerEnd);
const result = spawnSync(
'bash',
[
'-c',
`set -Eeuo pipefail
maintenance_started=1
stage_root=/tmp/unused
rollback() { printf 'rollback\\n'; }
cleanup() { printf 'cleanup\\n'; }
release_install_lock() { :; }
${handlers}
trap on_exit EXIT
trap 'exit 143' TERM
true
kill -TERM $$`,
],
{ encoding: 'utf8' }
);
expect(result.status).toBe(143);
expect(result.stdout).toContain('rollback');
expect(result.stdout).toContain('cleanup');
});
});