mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-14 22:17:20 +00:00
Merge pull request #707 from kaitranntt/kai/fix/706-proxy-version-install-restart
fix: restart CLIProxy after dashboard version install
This commit is contained in:
11 files changed
+674
-70
No files matched your search
@@ -0,0 +1,99 @@
|
||||
import { afterEach, describe, expect, it, mock } from 'bun:test';
|
||||
|
||||
describe('installCliproxyVersion', () => {
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
it('attempts to stop the proxy even when there is no tracked running session', async () => {
|
||||
const calls = {
|
||||
stopProxy: 0,
|
||||
waitForPortFree: 0,
|
||||
deleteBinary: 0,
|
||||
ensureBinary: 0,
|
||||
};
|
||||
|
||||
mock.module('../../../src/utils/ui', () => ({
|
||||
info: (message: string) => message,
|
||||
warn: (message: string) => message,
|
||||
}));
|
||||
|
||||
mock.module('../../../src/cliproxy/config-generator', () => ({
|
||||
getBinDir: () => '/tmp/ccs-bin',
|
||||
CLIPROXY_DEFAULT_PORT: 8317,
|
||||
}));
|
||||
|
||||
mock.module('../../../src/cliproxy/platform-detector', () => ({
|
||||
DEFAULT_BACKEND: 'plus',
|
||||
CLIPROXY_MAX_STABLE_VERSION: '9.9.999-0',
|
||||
BACKEND_CONFIG: {
|
||||
plus: {
|
||||
fallbackVersion: '6.6.80',
|
||||
repo: 'router-for-me/CLIProxyAPIPlus',
|
||||
},
|
||||
original: {
|
||||
fallbackVersion: '0.0.0',
|
||||
repo: 'router-for-me/CLIProxyAPI',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
mock.module('../../../src/cliproxy/services/proxy-lifecycle-service', () => ({
|
||||
stopProxy: async () => {
|
||||
calls.stopProxy += 1;
|
||||
return { stopped: false, error: 'No active CLIProxy session found' };
|
||||
},
|
||||
}));
|
||||
|
||||
mock.module('../../../src/utils/port-utils', () => ({
|
||||
waitForPortFree: async () => {
|
||||
calls.waitForPortFree += 1;
|
||||
return true;
|
||||
},
|
||||
}));
|
||||
|
||||
mock.module('../../../src/config/unified-config-loader', () => ({
|
||||
loadOrCreateUnifiedConfig: () => ({
|
||||
cliproxy: { backend: 'plus' },
|
||||
}),
|
||||
}));
|
||||
|
||||
mock.module('../../../src/cliproxy/binary', () => ({
|
||||
checkForUpdates: async () => ({
|
||||
hasUpdate: false,
|
||||
currentVersion: '6.6.80',
|
||||
latestVersion: '6.6.80',
|
||||
fromCache: false,
|
||||
checkedAt: Date.now(),
|
||||
}),
|
||||
deleteBinary: () => {
|
||||
calls.deleteBinary += 1;
|
||||
},
|
||||
getBinaryPath: () => '/tmp/ccs-bin/plus/cliproxy',
|
||||
isBinaryInstalled: () => false,
|
||||
getBinaryInfo: async () => null,
|
||||
getPinnedVersion: () => null,
|
||||
savePinnedVersion: () => {},
|
||||
clearPinnedVersion: () => {},
|
||||
isVersionPinned: () => false,
|
||||
getVersionPinPath: () => '/tmp/ccs-bin/plus/.version-pin',
|
||||
readInstalledVersion: () => '6.6.80',
|
||||
ensureBinary: async () => {
|
||||
calls.ensureBinary += 1;
|
||||
return '/tmp/ccs-bin/plus/cliproxy';
|
||||
},
|
||||
migrateVersionPin: () => {},
|
||||
}));
|
||||
|
||||
const binaryManager = await import(
|
||||
`../../../src/cliproxy/binary-manager?binary-manager-install=${Date.now()}`
|
||||
);
|
||||
|
||||
await binaryManager.installCliproxyVersion('6.7.1', false, 'plus');
|
||||
|
||||
expect(calls.stopProxy).toBe(1);
|
||||
expect(calls.waitForPortFree).toBe(0);
|
||||
expect(calls.deleteBinary).toBe(0);
|
||||
expect(calls.ensureBinary).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import {
|
||||
compareCliproxyVersions,
|
||||
isCliproxyVersionExperimental,
|
||||
isCliproxyVersionInRange,
|
||||
} from '../../../ui/src/lib/cliproxy-version-risk';
|
||||
|
||||
describe('cliproxy-version-risk helpers', () => {
|
||||
it('compares versions while ignoring release suffixes', () => {
|
||||
expect(compareCliproxyVersions('6.6.88', '6.6.81-0')).toBe(1);
|
||||
expect(compareCliproxyVersions('6.6.81-0', '6.6.81')).toBe(0);
|
||||
expect(compareCliproxyVersions('6.6.80', '6.6.81')).toBe(-1);
|
||||
});
|
||||
|
||||
it('detects experimental versions against max stable', () => {
|
||||
expect(isCliproxyVersionExperimental('10.0.0', '9.9.999-0')).toBe(true);
|
||||
expect(isCliproxyVersionExperimental('6.6.88', '9.9.999-0')).toBe(false);
|
||||
});
|
||||
|
||||
it('detects versions inside the faulty range', () => {
|
||||
expect(isCliproxyVersionInRange('6.6.81', '6.6.81-0', '6.6.88-0')).toBe(true);
|
||||
expect(isCliproxyVersionInRange('6.6.88', '6.6.81-0', '6.6.88-0')).toBe(true);
|
||||
expect(isCliproxyVersionInRange('6.6.89', '6.6.81-0', '6.6.88-0')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import type { CLIProxyBackend } from '../../../src/cliproxy/types';
|
||||
import {
|
||||
installDashboardCliproxyVersion,
|
||||
type DashboardCliproxyInstallResult,
|
||||
} from '../../../src/web-server/services/cliproxy-dashboard-install-service';
|
||||
|
||||
function createDeps(
|
||||
overrides: {
|
||||
sessionRunning?: boolean;
|
||||
remoteRunning?: boolean;
|
||||
startResult?: { started: boolean; alreadyRunning: boolean; port: number; error?: string };
|
||||
} = {}
|
||||
) {
|
||||
const calls = {
|
||||
isCliproxyRunning: 0,
|
||||
installCliproxyVersion: 0,
|
||||
ensureCliproxyService: 0,
|
||||
};
|
||||
|
||||
const deps = {
|
||||
getProxyStatus: () => ({ running: overrides.sessionRunning ?? false }),
|
||||
isCliproxyRunning: async () => {
|
||||
calls.isCliproxyRunning += 1;
|
||||
return overrides.remoteRunning ?? false;
|
||||
},
|
||||
installCliproxyVersion: async (
|
||||
_version: string,
|
||||
_verbose?: boolean,
|
||||
_backend?: CLIProxyBackend
|
||||
) => {
|
||||
calls.installCliproxyVersion += 1;
|
||||
},
|
||||
ensureCliproxyService: async () => {
|
||||
calls.ensureCliproxyService += 1;
|
||||
return (
|
||||
overrides.startResult ?? {
|
||||
started: true,
|
||||
alreadyRunning: false,
|
||||
port: 8317,
|
||||
}
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
return { deps, calls };
|
||||
}
|
||||
|
||||
describe('installDashboardCliproxyVersion', () => {
|
||||
it('restarts the proxy after install when it was already running', async () => {
|
||||
const { deps, calls } = createDeps({ sessionRunning: true });
|
||||
|
||||
const result = await installDashboardCliproxyVersion('6.7.1', 'plus', deps);
|
||||
|
||||
expect(result).toEqual<DashboardCliproxyInstallResult>({
|
||||
success: true,
|
||||
restarted: true,
|
||||
port: 8317,
|
||||
message: 'Successfully installed CLIProxy Plus v6.7.1 and restarted it on port 8317',
|
||||
});
|
||||
expect(calls.isCliproxyRunning).toBe(0);
|
||||
expect(calls.installCliproxyVersion).toBe(1);
|
||||
expect(calls.ensureCliproxyService).toBe(1);
|
||||
});
|
||||
|
||||
it('keeps the proxy stopped after install when it was not running beforehand', async () => {
|
||||
const { deps, calls } = createDeps({ sessionRunning: false, remoteRunning: false });
|
||||
|
||||
const result = await installDashboardCliproxyVersion('6.7.1', 'plus', deps);
|
||||
|
||||
expect(result).toEqual<DashboardCliproxyInstallResult>({
|
||||
success: true,
|
||||
restarted: false,
|
||||
message: 'Successfully installed CLIProxy Plus v6.7.1',
|
||||
});
|
||||
expect(calls.isCliproxyRunning).toBe(1);
|
||||
expect(calls.installCliproxyVersion).toBe(1);
|
||||
expect(calls.ensureCliproxyService).toBe(0);
|
||||
});
|
||||
|
||||
it('reports a restart failure after a successful install when the proxy had been running', async () => {
|
||||
const { deps, calls } = createDeps({
|
||||
sessionRunning: false,
|
||||
remoteRunning: true,
|
||||
startResult: {
|
||||
started: false,
|
||||
alreadyRunning: false,
|
||||
port: 8317,
|
||||
error: 'Port 8317 is blocked by another process',
|
||||
},
|
||||
});
|
||||
|
||||
const result = await installDashboardCliproxyVersion('6.7.1', 'original', deps);
|
||||
|
||||
expect(result).toEqual<DashboardCliproxyInstallResult>({
|
||||
success: false,
|
||||
restarted: false,
|
||||
error: 'Port 8317 is blocked by another process',
|
||||
message: 'Installed CLIProxy v6.7.1, but failed to restart it',
|
||||
});
|
||||
expect(calls.isCliproxyRunning).toBe(1);
|
||||
expect(calls.installCliproxyVersion).toBe(1);
|
||||
expect(calls.ensureCliproxyService).toBe(1);
|
||||
});
|
||||
|
||||
it('uses a fallback restart error when the start result omits one', async () => {
|
||||
const { deps } = createDeps({
|
||||
remoteRunning: true,
|
||||
startResult: {
|
||||
started: false,
|
||||
alreadyRunning: false,
|
||||
port: 8317,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await installDashboardCliproxyVersion('6.7.1', 'plus', deps);
|
||||
|
||||
expect(result).toEqual<DashboardCliproxyInstallResult>({
|
||||
success: false,
|
||||
restarted: false,
|
||||
error: 'Installed CLIProxy Plus v6.7.1, but restart failed',
|
||||
message: 'Installed CLIProxy Plus v6.7.1, but failed to restart it',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,168 @@
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||
import express from 'express';
|
||||
import type { Server } from 'http';
|
||||
|
||||
const installSpy = {
|
||||
calls: 0,
|
||||
};
|
||||
|
||||
mock.module('../../../src/config/unified-config-loader', () => ({
|
||||
loadOrCreateUnifiedConfig: () => ({
|
||||
cliproxy: { backend: 'plus' },
|
||||
}),
|
||||
}));
|
||||
|
||||
mock.module('../../../src/cliproxy/binary-manager', () => ({
|
||||
checkCliproxyUpdate: async () => ({
|
||||
hasUpdate: false,
|
||||
currentVersion: '6.6.80',
|
||||
latestVersion: '6.6.89',
|
||||
fromCache: false,
|
||||
checkedAt: Date.now(),
|
||||
backend: 'plus',
|
||||
backendLabel: 'CLIProxy Plus',
|
||||
isStable: true,
|
||||
maxStableVersion: '9.9.999-0',
|
||||
}),
|
||||
getInstalledCliproxyVersion: () => '6.6.80',
|
||||
installCliproxyVersion: async () => {},
|
||||
}));
|
||||
|
||||
mock.module('../../../src/cliproxy/binary/version-checker', () => ({
|
||||
fetchAllVersions: async () => ({
|
||||
versions: ['6.6.89', '6.6.88', '6.6.81', '6.6.80'],
|
||||
latestStable: '6.6.89',
|
||||
latest: '6.6.89',
|
||||
fromCache: false,
|
||||
checkedAt: Date.now(),
|
||||
}),
|
||||
isNewerVersion: (version: string, maxStable: string) => {
|
||||
const normalize = (value: string) => value.replace(/-\d+$/, '').split('.').map(Number);
|
||||
const versionParts = normalize(version);
|
||||
const maxStableParts = normalize(maxStable);
|
||||
|
||||
for (let index = 0; index < 3; index += 1) {
|
||||
const versionPart = versionParts[index] || 0;
|
||||
const maxStablePart = maxStableParts[index] || 0;
|
||||
|
||||
if (versionPart > maxStablePart) return true;
|
||||
if (versionPart < maxStablePart) return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
isVersionFaulty: (version: string) =>
|
||||
['6.6.81', '6.6.82', '6.6.83', '6.6.84', '6.6.85', '6.6.86', '6.6.87', '6.6.88'].includes(
|
||||
version
|
||||
),
|
||||
}));
|
||||
|
||||
mock.module('../../../src/web-server/services/cliproxy-dashboard-install-service', () => ({
|
||||
installDashboardCliproxyVersion: async () => {
|
||||
installSpy.calls += 1;
|
||||
return {
|
||||
success: true,
|
||||
restarted: true,
|
||||
port: 8317,
|
||||
message: 'installed',
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
let cliproxyStatsRoutes: typeof import('../../../src/web-server/routes/cliproxy-stats-routes').default;
|
||||
let server: Server;
|
||||
let baseUrl = '';
|
||||
|
||||
beforeAll(async () => {
|
||||
cliproxyStatsRoutes = (await import('../../../src/web-server/routes/cliproxy-stats-routes'))
|
||||
.default;
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use('/api/cliproxy', cliproxyStatsRoutes);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server = app.listen(0, '127.0.0.1');
|
||||
const onError = (error: Error) => reject(error);
|
||||
server.once('error', onError);
|
||||
server.once('listening', () => {
|
||||
server.off('error', onError);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
const address = server.address();
|
||||
if (!address || typeof address === 'string') {
|
||||
throw new Error('Unable to resolve test server port');
|
||||
}
|
||||
baseUrl = `http://127.0.0.1:${address.port}`;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
installSpy.calls = 0;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
describe('cliproxy-stats-routes install contract', () => {
|
||||
it('returns faultyRange in the versions response', async () => {
|
||||
const response = await fetch(`${baseUrl}/api/cliproxy/versions`);
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const body = (await response.json()) as {
|
||||
faultyRange: { min: string; max: string };
|
||||
currentVersion: string;
|
||||
};
|
||||
expect(body.currentVersion).toBe('6.6.80');
|
||||
expect(body.faultyRange).toEqual({ min: '6.6.81-0', max: '6.6.88-0' });
|
||||
});
|
||||
|
||||
it('returns faulty confirmation metadata without calling the installer', async () => {
|
||||
const response = await fetch(`${baseUrl}/api/cliproxy/install`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ version: '6.6.81' }),
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const body = (await response.json()) as {
|
||||
success: boolean;
|
||||
requiresConfirmation: boolean;
|
||||
isFaulty: boolean;
|
||||
isExperimental: boolean;
|
||||
message: string;
|
||||
};
|
||||
expect(body.success).toBe(false);
|
||||
expect(body.requiresConfirmation).toBe(true);
|
||||
expect(body.isFaulty).toBe(true);
|
||||
expect(body.isExperimental).toBe(false);
|
||||
expect(body.message).toContain('known bugs');
|
||||
expect(installSpy.calls).toBe(0);
|
||||
});
|
||||
|
||||
it('returns experimental confirmation metadata without calling the installer', async () => {
|
||||
const response = await fetch(`${baseUrl}/api/cliproxy/install`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ version: '10.0.0' }),
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const body = (await response.json()) as {
|
||||
success: boolean;
|
||||
requiresConfirmation: boolean;
|
||||
isFaulty: boolean;
|
||||
isExperimental: boolean;
|
||||
message: string;
|
||||
};
|
||||
expect(body.success).toBe(false);
|
||||
expect(body.requiresConfirmation).toBe(true);
|
||||
expect(body.isFaulty).toBe(false);
|
||||
expect(body.isExperimental).toBe(true);
|
||||
expect(body.message).toContain('experimental');
|
||||
expect(installSpy.calls).toBe(0);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user