mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 04:18:05 +00:00
test(ci): stabilize runner-sensitive isolated tests
- isolate tokens and session-tracker tests in child processes - make child scripts resolve repo modules from the test location - avoid machine-specific paths in the repo
This commit is contained in:
@@ -1,56 +1,111 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
registerSession,
|
||||
unregisterSession,
|
||||
getProxyStatus,
|
||||
} from '../../../src/cliproxy/session-tracker';
|
||||
import { spawnSync } from 'child_process';
|
||||
import { pathToFileURL } from 'url';
|
||||
|
||||
describe('session-tracker target metadata', () => {
|
||||
let tmpDir: string;
|
||||
let originalCcsHome: string | undefined;
|
||||
const port = 28317;
|
||||
const REPO_ROOT = path.resolve(import.meta.dir, '../../..');
|
||||
const SESSION_TRACKER_URL = pathToFileURL(
|
||||
path.join(REPO_ROOT, 'src/cliproxy/session-tracker.ts')
|
||||
).href;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-session-target-test-'));
|
||||
originalCcsHome = process.env.CCS_HOME;
|
||||
process.env.CCS_HOME = tmpDir;
|
||||
});
|
||||
function withScopedSessionTrackerHome<T>(run: (tempHome: string) => T): T {
|
||||
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-session-target-test-'));
|
||||
try {
|
||||
return run(tempHome);
|
||||
} finally {
|
||||
fs.rmSync(tempHome, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
if (originalCcsHome !== undefined) {
|
||||
process.env.CCS_HOME = originalCcsHome;
|
||||
} else {
|
||||
delete process.env.CCS_HOME;
|
||||
function runSessionTrackerScenario(
|
||||
tempHome: string,
|
||||
targets: string[]
|
||||
): {
|
||||
running: boolean;
|
||||
target?: string;
|
||||
sessionCount?: number;
|
||||
} {
|
||||
const script = `
|
||||
import {
|
||||
registerSession,
|
||||
unregisterSession,
|
||||
getProxyStatus,
|
||||
} from ${JSON.stringify(SESSION_TRACKER_URL)};
|
||||
|
||||
const port = 28317;
|
||||
const sessionIds = [];
|
||||
for (const target of ${JSON.stringify(targets)}) {
|
||||
sessionIds.push(registerSession(port, process.pid, undefined, undefined, target));
|
||||
}
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('returns single target when all sessions share same target', () => {
|
||||
const s1 = registerSession(port, process.pid, undefined, undefined, 'droid');
|
||||
const s2 = registerSession(port, process.pid, undefined, undefined, 'droid');
|
||||
|
||||
const status = getProxyStatus(port);
|
||||
expect(status.running).toBe(true);
|
||||
expect(status.target).toBe('droid');
|
||||
expect(status.sessionCount).toBe(2);
|
||||
for (const sessionId of sessionIds) {
|
||||
unregisterSession(sessionId, port);
|
||||
}
|
||||
|
||||
unregisterSession(s1, port);
|
||||
unregisterSession(s2, port);
|
||||
console.log(JSON.stringify({
|
||||
running: status.running,
|
||||
target: status.target ?? null,
|
||||
sessionCount: status.sessionCount ?? null,
|
||||
}));
|
||||
`;
|
||||
|
||||
const scriptPath = path.join(tempHome, `session-target-child-${Date.now()}.mjs`);
|
||||
fs.writeFileSync(scriptPath, script, 'utf8');
|
||||
|
||||
const result = spawnSync('/bin/bash', ['-lc', `bun ${JSON.stringify(scriptPath)}`], {
|
||||
cwd: REPO_ROOT,
|
||||
env: {
|
||||
...process.env,
|
||||
CCS_HOME: tempHome,
|
||||
CCS_DIR: '',
|
||||
},
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
throw new Error(
|
||||
`child session-tracker scenario failed: ${JSON.stringify({
|
||||
command: `bun ${scriptPath}`,
|
||||
status: result.status,
|
||||
signal: result.signal,
|
||||
error: result.error?.message ?? null,
|
||||
stdout: result.stdout,
|
||||
stderr: result.stderr,
|
||||
})}`
|
||||
);
|
||||
}
|
||||
|
||||
const lines = result.stdout
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
return JSON.parse(lines.at(-1) || '{}') as {
|
||||
running: boolean;
|
||||
target?: string;
|
||||
sessionCount?: number;
|
||||
};
|
||||
}
|
||||
|
||||
describe('session-tracker target metadata', () => {
|
||||
it('returns single target when all sessions share same target', () => {
|
||||
withScopedSessionTrackerHome((tempHome) => {
|
||||
const status = runSessionTrackerScenario(tempHome, ['droid', 'droid']);
|
||||
expect(status.running).toBe(true);
|
||||
expect(status.target).toBe('droid');
|
||||
expect(status.sessionCount).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns mixed when active sessions use different targets', () => {
|
||||
const s1 = registerSession(port, process.pid, undefined, undefined, 'claude');
|
||||
const s2 = registerSession(port, process.pid, undefined, undefined, 'droid');
|
||||
|
||||
const status = getProxyStatus(port);
|
||||
expect(status.running).toBe(true);
|
||||
expect(status.target).toBe('mixed');
|
||||
expect(status.sessionCount).toBe(2);
|
||||
|
||||
unregisterSession(s1, port);
|
||||
unregisterSession(s2, port);
|
||||
withScopedSessionTrackerHome((tempHome) => {
|
||||
const status = runSessionTrackerScenario(tempHome, ['claude', 'droid']);
|
||||
expect(status.running).toBe(true);
|
||||
expect(status.target).toBe('mixed');
|
||||
expect(status.sessionCount).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,64 +2,125 @@ import { describe, expect, it } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { handleTokensCommand } from '../../../src/commands/tokens-command';
|
||||
import { getConfigYamlPath, loadUnifiedConfig } from '../../../src/config/unified-config-loader';
|
||||
import { runWithScopedCcsHome, setGlobalConfigDir } from '../../../src/utils/config-manager';
|
||||
import { spawnSync } from 'child_process';
|
||||
import { pathToFileURL } from 'url';
|
||||
import { setGlobalConfigDir } from '../../../src/utils/config-manager';
|
||||
|
||||
async function withScopedTokensHome<T>(run: (tempHome: string) => Promise<T>): Promise<T> {
|
||||
const REPO_ROOT = path.resolve(import.meta.dir, '../../..');
|
||||
const TOKENS_COMMAND_URL = pathToFileURL(
|
||||
path.join(REPO_ROOT, 'src/commands/tokens-command.ts')
|
||||
).href;
|
||||
const UNIFIED_CONFIG_LOADER_URL = pathToFileURL(
|
||||
path.join(REPO_ROOT, 'src/config/unified-config-loader.ts')
|
||||
).href;
|
||||
|
||||
function withScopedTokensHome<T>(run: (tempHome: string) => T): T {
|
||||
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-tokens-rotation-'));
|
||||
setGlobalConfigDir(undefined);
|
||||
|
||||
try {
|
||||
return await runWithScopedCcsHome(tempHome, async () => await run(tempHome));
|
||||
return run(tempHome);
|
||||
} finally {
|
||||
setGlobalConfigDir(undefined);
|
||||
fs.rmSync(tempHome, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function runTokensCommandInChild(tempHome: string, args: string[]) {
|
||||
const script = `
|
||||
import { handleTokensCommand } from ${JSON.stringify(TOKENS_COMMAND_URL)};
|
||||
import { loadUnifiedConfig } from ${JSON.stringify(UNIFIED_CONFIG_LOADER_URL)};
|
||||
|
||||
const exitCode = await handleTokensCommand(${JSON.stringify(args)});
|
||||
const config = loadUnifiedConfig();
|
||||
const managementSecret = config?.cliproxy.auth?.management_secret ?? null;
|
||||
|
||||
console.log(JSON.stringify({
|
||||
exitCode,
|
||||
apiKey: config?.cliproxy.auth?.api_key ?? null,
|
||||
managementSecretLength: typeof managementSecret === 'string' ? managementSecret.length : 0,
|
||||
}));
|
||||
`;
|
||||
|
||||
const scriptPath = path.join(tempHome, `tokens-child-${Date.now()}.mjs`);
|
||||
fs.writeFileSync(scriptPath, script, 'utf8');
|
||||
|
||||
const result = spawnSync('/bin/bash', ['-lc', `bun ${JSON.stringify(scriptPath)}`], {
|
||||
cwd: REPO_ROOT,
|
||||
env: {
|
||||
...process.env,
|
||||
CCS_HOME: tempHome,
|
||||
CCS_DIR: '',
|
||||
NO_COLOR: '1',
|
||||
},
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
throw new Error(
|
||||
`child tokens command failed: ${JSON.stringify({
|
||||
command: `bun ${scriptPath}`,
|
||||
status: result.status,
|
||||
signal: result.signal,
|
||||
error: result.error?.message ?? null,
|
||||
stdout: result.stdout,
|
||||
stderr: result.stderr,
|
||||
})}`
|
||||
);
|
||||
}
|
||||
|
||||
const lines = result.stdout
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
const payload = JSON.parse(lines.at(-1) || '{}') as {
|
||||
exitCode: number;
|
||||
apiKey: string | null;
|
||||
managementSecretLength: number;
|
||||
};
|
||||
|
||||
return { payload, stdout: result.stdout, stderr: result.stderr };
|
||||
}
|
||||
|
||||
describe('tokens command auth rotation', () => {
|
||||
it('applies api-key and regenerated secret in a single invocation', async () => {
|
||||
await withScopedTokensHome(async () => {
|
||||
const exitCode = await handleTokensCommand([
|
||||
it('applies api-key and regenerated secret in a single invocation', () => {
|
||||
withScopedTokensHome((tempHome) => {
|
||||
const { payload } = runTokensCommandInChild(tempHome, [
|
||||
'--api-key',
|
||||
'ccs-custom-key-123',
|
||||
'--regenerate-secret',
|
||||
]);
|
||||
|
||||
const config = loadUnifiedConfig();
|
||||
const managementSecret = config?.cliproxy.auth?.management_secret;
|
||||
const configYamlPath = getConfigYamlPath();
|
||||
const configYamlPath = path.join(tempHome, '.ccs', 'config.yaml');
|
||||
|
||||
const diagnostics = {
|
||||
exitCode,
|
||||
exitCode: payload.exitCode,
|
||||
configYamlPath,
|
||||
configExists: fs.existsSync(configYamlPath),
|
||||
apiKey: config?.cliproxy.auth?.api_key ?? null,
|
||||
managementSecretLength: (managementSecret ?? '').length,
|
||||
apiKey: payload.apiKey,
|
||||
managementSecretLength: payload.managementSecretLength,
|
||||
};
|
||||
|
||||
if (
|
||||
exitCode !== 0 ||
|
||||
config?.cliproxy.auth?.api_key !== 'ccs-custom-key-123' ||
|
||||
typeof managementSecret !== 'string' ||
|
||||
(managementSecret ?? '').length <= 20
|
||||
payload.exitCode !== 0 ||
|
||||
payload.apiKey !== 'ccs-custom-key-123' ||
|
||||
payload.managementSecretLength <= 20
|
||||
) {
|
||||
throw new Error(`tokens rotation diagnostics: ${JSON.stringify(diagnostics)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects conflicting manual and generated secret flags', async () => {
|
||||
await withScopedTokensHome(async () => {
|
||||
const exitCode = await handleTokensCommand([
|
||||
it('rejects conflicting manual and generated secret flags', () => {
|
||||
withScopedTokensHome((tempHome) => {
|
||||
const { payload } = runTokensCommandInChild(tempHome, [
|
||||
'--secret',
|
||||
'manual-secret',
|
||||
'--regenerate-secret',
|
||||
]);
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
expect(fs.existsSync(getConfigYamlPath())).toBe(false);
|
||||
expect(payload.exitCode).toBe(1);
|
||||
expect(fs.existsSync(path.join(tempHome, '.ccs', 'config.yaml'))).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user