test(delegation): add regression coverage for claudecode stripping

- verify case-insensitive CLAUDECODE removal helper behavior

- cover execClaude env merge, override, no-key, and Windows paths

- cover headless executor spawn env sanitization behavior

Refs #588
This commit is contained in:
Tam Nhu Tran
2026-02-20 23:01:09 +07:00
parent 8e57d59479
commit 80a84edf1f
@@ -0,0 +1,204 @@
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, mock } from 'bun:test';
import { EventEmitter } from 'events';
type SpawnCall = {
command: string;
args: string[];
options: Record<string, unknown> | undefined;
};
const spawnCalls: SpawnCall[] = [];
const originalPlatform = process.platform;
let baselineSigintListeners: Array<(...args: unknown[]) => void> = [];
let baselineSigtermListeners: Array<(...args: unknown[]) => void> = [];
let baselineSighupListeners: Array<(...args: unknown[]) => void> = [];
function createMockChild(): EventEmitter & {
stdout: EventEmitter;
stderr: EventEmitter;
exitCode: number | null;
killed: boolean;
kill: () => boolean;
} {
const child = new EventEmitter() as EventEmitter & {
stdout: EventEmitter;
stderr: EventEmitter;
exitCode: number | null;
killed: boolean;
kill: () => boolean;
};
child.stdout = new EventEmitter();
child.stderr = new EventEmitter();
child.exitCode = null;
child.killed = false;
child.kill = () => {
child.killed = true;
child.exitCode = 1;
return true;
};
return child;
}
mock.module('child_process', () => ({
spawn: (...spawnArgs: unknown[]) => {
const command = String(spawnArgs[0] ?? '');
const maybeArgs = spawnArgs[1];
const args = Array.isArray(maybeArgs) ? (maybeArgs as string[]) : [];
const options = (Array.isArray(maybeArgs) ? spawnArgs[2] : spawnArgs[1]) as
| Record<string, unknown>
| undefined;
spawnCalls.push({ command, args, options });
const child = createMockChild();
setTimeout(() => child.emit('close', 0), 0);
return child;
},
spawnSync: () => ({ status: 0 }),
execSync: () => '',
}));
let execClaude: typeof import('../../../src/utils/shell-executor').execClaude;
let stripClaudeCodeEnv: typeof import('../../../src/utils/shell-executor').stripClaudeCodeEnv;
let HeadlessExecutor: typeof import('../../../src/delegation/headless-executor').HeadlessExecutor;
beforeAll(async () => {
const shellExecutor = await import('../../../src/utils/shell-executor');
execClaude = shellExecutor.execClaude;
stripClaudeCodeEnv = shellExecutor.stripClaudeCodeEnv;
const headless = await import('../../../src/delegation/headless-executor');
HeadlessExecutor = headless.HeadlessExecutor;
});
afterAll(() => {
mock.restore();
});
describe('CLAUDECODE environment stripping', () => {
beforeEach(() => {
spawnCalls.length = 0;
process.env.CCS_QUIET = '1';
baselineSigintListeners = process.listeners('SIGINT');
baselineSigtermListeners = process.listeners('SIGTERM');
baselineSighupListeners = process.listeners('SIGHUP');
});
afterEach(() => {
Object.defineProperty(process, 'platform', { value: originalPlatform });
delete process.env.CLAUDECODE;
delete process.env.claudecode;
delete process.env.CCS_QUIET;
for (const listener of process.listeners('SIGINT')) {
if (!baselineSigintListeners.includes(listener)) {
process.removeListener('SIGINT', listener as (...args: unknown[]) => void);
}
}
for (const listener of process.listeners('SIGTERM')) {
if (!baselineSigtermListeners.includes(listener)) {
process.removeListener('SIGTERM', listener as (...args: unknown[]) => void);
}
}
for (const listener of process.listeners('SIGHUP')) {
if (!baselineSighupListeners.includes(listener)) {
process.removeListener('SIGHUP', listener as (...args: unknown[]) => void);
}
}
});
it('stripClaudeCodeEnv removes CLAUDECODE case-insensitively', () => {
const input: NodeJS.ProcessEnv = {
CLAUDECODE: 'upper',
claudecode: 'lower',
ClAuDeCoDe: 'mixed',
PATH: '/usr/bin',
};
const result = stripClaudeCodeEnv(input);
expect(Object.keys(result).map((k) => k.toUpperCase())).not.toContain('CLAUDECODE');
expect(result.PATH).toBe('/usr/bin');
});
it('execClaude strips CLAUDECODE from merged env (including overrides)', () => {
process.env.CLAUDECODE = 'from-parent';
process.env.claudecode = 'from-parent-lower';
execClaude('claude', ['--version'], {
CCS_PROFILE_TYPE: 'default',
CLAUDECODE: 'from-override',
CCS_WEBSEARCH_SKIP: '1',
});
expect(spawnCalls.length).toBeGreaterThan(0);
const env = spawnCalls[0].options?.env as NodeJS.ProcessEnv;
expect(env).toBeDefined();
expect(Object.keys(env).map((k) => k.toUpperCase())).not.toContain('CLAUDECODE');
expect(env.CCS_WEBSEARCH_SKIP).toBe('1');
});
it('execClaude keeps behavior when CLAUDECODE is absent', () => {
execClaude('claude', ['--help'], { CCS_PROFILE_TYPE: 'default' });
expect(spawnCalls.length).toBeGreaterThan(0);
const env = spawnCalls[0].options?.env as NodeJS.ProcessEnv;
expect(env).toBeDefined();
expect(Object.keys(env).map((k) => k.toUpperCase())).not.toContain('CLAUDECODE');
expect(env.CCS_PROFILE_TYPE).toBe('default');
});
it('execClaude strips CLAUDECODE on Windows shell launch path', () => {
Object.defineProperty(process, 'platform', { value: 'win32' });
process.env.CLAUDECODE = 'set';
execClaude('claude.cmd', ['--version'], { CCS_PROFILE_TYPE: 'default' });
expect(spawnCalls.length).toBeGreaterThan(0);
const env = spawnCalls[0].options?.env as NodeJS.ProcessEnv;
expect(Object.keys(env).map((k) => k.toUpperCase())).not.toContain('CLAUDECODE');
});
it('headless executor spawn path strips CLAUDECODE before spawn', async () => {
process.env.CLAUDECODE = 'nested';
process.env.claudecode = 'nested-lower';
const result = await (
HeadlessExecutor as unknown as {
_spawnAndExecute: (
claudeCli: string,
args: string[],
ctx: {
cwd: string;
profile: string;
timeout: number;
resumeSession: boolean;
sessionId: string | null;
sessionMgr: {
updateSession: (...args: unknown[]) => void;
storeSession: (...args: unknown[]) => void;
cleanupExpired: () => void;
};
}
) => Promise<unknown>;
}
)._spawnAndExecute('claude', ['-p', 'test'], {
cwd: process.cwd(),
profile: 'glm',
timeout: 1000,
resumeSession: false,
sessionId: null,
sessionMgr: {
updateSession: () => {},
storeSession: () => {},
cleanupExpired: () => {},
},
});
expect(result).toBeDefined();
expect(spawnCalls.length).toBeGreaterThan(0);
const env = spawnCalls[0].options?.env as NodeJS.ProcessEnv;
expect(Object.keys(env).map((k) => k.toUpperCase())).not.toContain('CLAUDECODE');
});
});