From 2cc92e8bb80ba7239bb66b8cd24e0cb4c6ff0627 Mon Sep 17 00:00:00 2001 From: Sergey Galuza Date: Sat, 11 Apr 2026 07:16:50 +0200 Subject: [PATCH 1/4] test(ci): isolate CCS-managed env vars from host session When tests run inside a CCS-managed Claude session, host env vars (CLAUDE_CONFIG_DIR, CCS_PROFILE_TYPE) leak into subprocess spawns and fixture setup, causing 9 test failures: - persist-command-handler: reads real symlinked settings.json instead of temp fixture because CLAUDE_CONFIG_DIR overrides scoped CCS_HOME - websearch-transformer: hook silently exits via shouldSkipHook() because CCS_PROFILE_TYPE=account triggers native_account_profile skip - claudecode-env-stripping: normalizeSharedPluginMetadataPaths receives leaked CLAUDE_CONFIG_DIR instead of undefined for default profiles Fix: clear CCS-managed env vars in beforeEach and neutralize CCS_PROFILE_TYPE in subprocess env blocks. Built [OnSteroids](https://onsteroids.ai) --- tests/unit/commands/persist-command-handler.test.ts | 1 + tests/unit/hooks/websearch-transformer.test.ts | 6 ++++++ tests/unit/utils/claudecode-env-stripping.test.ts | 5 +++++ 3 files changed, 12 insertions(+) diff --git a/tests/unit/commands/persist-command-handler.test.ts b/tests/unit/commands/persist-command-handler.test.ts index 6235be55..8f235714 100644 --- a/tests/unit/commands/persist-command-handler.test.ts +++ b/tests/unit/commands/persist-command-handler.test.ts @@ -73,6 +73,7 @@ function stubProcessExit(): void { beforeEach(async () => { tempRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'ccs-persist-handler-test-')); originalClaudeConfigDir = process.env.CLAUDE_CONFIG_DIR; + delete process.env.CLAUDE_CONFIG_DIR; originalProcessExit = process.exit; originalFsOpen = fs.promises.open; originalFsRename = fs.promises.rename; diff --git a/tests/unit/hooks/websearch-transformer.test.ts b/tests/unit/hooks/websearch-transformer.test.ts index 80bb74d9..42e7e3be 100644 --- a/tests/unit/hooks/websearch-transformer.test.ts +++ b/tests/unit/hooks/websearch-transformer.test.ts @@ -101,6 +101,7 @@ function runHookWithMockedFetch(mode: 'success' | 'empty' | 'non-result' | 'fail }), env: { ...process.env, + CCS_PROFILE_TYPE: '', CCS_WEBSEARCH_ENABLED: '1', CCS_WEBSEARCH_SKIP: '0', CCS_WEBSEARCH_BRAVE: '0', @@ -352,6 +353,7 @@ describe('websearch-transformer hook helpers', () => { }), env: { ...process.env, + CCS_PROFILE_TYPE: '', CCS_HOME: ccsHome, CCS_WEBSEARCH_TRACE: '1', CCS_WEBSEARCH_TRACE_LAUNCH_ID: 'hook-trace-test', @@ -428,6 +430,7 @@ describe('websearch-transformer hook helpers', () => { }), env: { ...process.env, + CCS_PROFILE_TYPE: '', CCS_HOME: ccsHome, CCS_WEBSEARCH_TRACE: '1', CCS_WEBSEARCH_TRACE_FILE: disallowedTracePath, @@ -509,6 +512,7 @@ global.fetch = async (url) => { }), env: { ...process.env, + CCS_PROFILE_TYPE: '', CCS_HOME: ccsHome, CCS_WEBSEARCH_TRACE: '1', CCS_WEBSEARCH_TRACE_LAUNCH_ID: 'quota-fallback-test', @@ -623,6 +627,7 @@ global.fetch = async (url) => { }), env: { ...process.env, + CCS_PROFILE_TYPE: '', CCS_HOME: ccsHome, CCS_WEBSEARCH_TRACE: '1', CCS_WEBSEARCH_TRACE_LAUNCH_ID: 'cooldown-skip-test', @@ -723,6 +728,7 @@ global.fetch = async (url) => { }), env: { ...process.env, + CCS_PROFILE_TYPE: '', CCS_HOME: ccsHome, CCS_WEBSEARCH_TRACE: '1', CCS_WEBSEARCH_TRACE_LAUNCH_ID: 'transient-retry-test', diff --git a/tests/unit/utils/claudecode-env-stripping.test.ts b/tests/unit/utils/claudecode-env-stripping.test.ts index b00908fe..4c3e3dd0 100644 --- a/tests/unit/utils/claudecode-env-stripping.test.ts +++ b/tests/unit/utils/claudecode-env-stripping.test.ts @@ -30,6 +30,7 @@ let baselineSighupListeners: Array<(...args: unknown[]) => void> = []; let originalCcsHome: string | undefined; let originalCcsClaudePath: string | undefined; let originalDisableAutoUpdater: string | undefined; +let originalClaudeConfigDir: string | undefined; const realSpawn = childProcess.spawn.bind(childProcess); const realSpawnSync = childProcess.spawnSync.bind(childProcess); const realExecSync = childProcess.execSync.bind(childProcess); @@ -154,7 +155,9 @@ describe('CLAUDECODE environment stripping', () => { originalCcsHome = process.env.CCS_HOME; originalCcsClaudePath = process.env.CCS_CLAUDE_PATH; originalDisableAutoUpdater = process.env.DISABLE_AUTOUPDATER; + originalClaudeConfigDir = process.env.CLAUDE_CONFIG_DIR; delete process.env.DISABLE_AUTOUPDATER; + delete process.env.CLAUDE_CONFIG_DIR; baselineSigintListeners = process.listeners('SIGINT'); baselineSigtermListeners = process.listeners('SIGTERM'); baselineSighupListeners = process.listeners('SIGHUP'); @@ -175,6 +178,8 @@ describe('CLAUDECODE environment stripping', () => { } else { delete process.env.DISABLE_AUTOUPDATER; } + if (originalClaudeConfigDir !== undefined) process.env.CLAUDE_CONFIG_DIR = originalClaudeConfigDir; + else delete process.env.CLAUDE_CONFIG_DIR; for (const listener of process.listeners('SIGINT')) { if (!baselineSigintListeners.includes(listener)) { From 1a7fc4a3f280e647c07c4b1a4e9eeae2d3ae3d3c Mon Sep 17 00:00:00 2001 From: Sergey Galuza Date: Sat, 11 Apr 2026 17:18:51 +0200 Subject: [PATCH 2/4] test(ci): improve test isolation readability per review feedback - Extract NEUTRAL_PROFILE_TYPE constant with JSDoc explaining why CCS_PROFILE_TYPE must be neutralised in subprocess env blocks - Replace 6 bare '' occurrences with the named constant (DRY) - Group save/delete operations in beforeEach with section comments Built [OnSteroids](https://onsteroids.ai) --- tests/unit/hooks/websearch-transformer.test.ts | 18 ++++++++++++------ .../utils/claudecode-env-stripping.test.ts | 5 +++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/tests/unit/hooks/websearch-transformer.test.ts b/tests/unit/hooks/websearch-transformer.test.ts index 42e7e3be..6cc83de4 100644 --- a/tests/unit/hooks/websearch-transformer.test.ts +++ b/tests/unit/hooks/websearch-transformer.test.ts @@ -12,6 +12,12 @@ import { tmpdir } from 'node:os'; import { spawnSync } from 'node:child_process'; const hookPath = join(process.cwd(), 'lib', 'hooks', 'websearch-transformer.cjs'); + +/** + * Neutralise CCS_PROFILE_TYPE so shouldSkipHook() does not short-circuit + * when tests run inside a CCS-managed Claude session (where it is 'account'). + */ +const NEUTRAL_PROFILE_TYPE = ''; type HookOutput = { hookSpecificOutput: { additionalContext: string; @@ -101,7 +107,7 @@ function runHookWithMockedFetch(mode: 'success' | 'empty' | 'non-result' | 'fail }), env: { ...process.env, - CCS_PROFILE_TYPE: '', + CCS_PROFILE_TYPE: NEUTRAL_PROFILE_TYPE, CCS_WEBSEARCH_ENABLED: '1', CCS_WEBSEARCH_SKIP: '0', CCS_WEBSEARCH_BRAVE: '0', @@ -353,7 +359,7 @@ describe('websearch-transformer hook helpers', () => { }), env: { ...process.env, - CCS_PROFILE_TYPE: '', + CCS_PROFILE_TYPE: NEUTRAL_PROFILE_TYPE, CCS_HOME: ccsHome, CCS_WEBSEARCH_TRACE: '1', CCS_WEBSEARCH_TRACE_LAUNCH_ID: 'hook-trace-test', @@ -430,7 +436,7 @@ describe('websearch-transformer hook helpers', () => { }), env: { ...process.env, - CCS_PROFILE_TYPE: '', + CCS_PROFILE_TYPE: NEUTRAL_PROFILE_TYPE, CCS_HOME: ccsHome, CCS_WEBSEARCH_TRACE: '1', CCS_WEBSEARCH_TRACE_FILE: disallowedTracePath, @@ -512,7 +518,7 @@ global.fetch = async (url) => { }), env: { ...process.env, - CCS_PROFILE_TYPE: '', + CCS_PROFILE_TYPE: NEUTRAL_PROFILE_TYPE, CCS_HOME: ccsHome, CCS_WEBSEARCH_TRACE: '1', CCS_WEBSEARCH_TRACE_LAUNCH_ID: 'quota-fallback-test', @@ -627,7 +633,7 @@ global.fetch = async (url) => { }), env: { ...process.env, - CCS_PROFILE_TYPE: '', + CCS_PROFILE_TYPE: NEUTRAL_PROFILE_TYPE, CCS_HOME: ccsHome, CCS_WEBSEARCH_TRACE: '1', CCS_WEBSEARCH_TRACE_LAUNCH_ID: 'cooldown-skip-test', @@ -728,7 +734,7 @@ global.fetch = async (url) => { }), env: { ...process.env, - CCS_PROFILE_TYPE: '', + CCS_PROFILE_TYPE: NEUTRAL_PROFILE_TYPE, CCS_HOME: ccsHome, CCS_WEBSEARCH_TRACE: '1', CCS_WEBSEARCH_TRACE_LAUNCH_ID: 'transient-retry-test', diff --git a/tests/unit/utils/claudecode-env-stripping.test.ts b/tests/unit/utils/claudecode-env-stripping.test.ts index 4c3e3dd0..117f2608 100644 --- a/tests/unit/utils/claudecode-env-stripping.test.ts +++ b/tests/unit/utils/claudecode-env-stripping.test.ts @@ -152,12 +152,17 @@ describe('CLAUDECODE environment stripping', () => { beforeEach(() => { spawnCalls.length = 0; process.env.CCS_QUIET = '1'; + + // Save original env values for restoration in afterEach originalCcsHome = process.env.CCS_HOME; originalCcsClaudePath = process.env.CCS_CLAUDE_PATH; originalDisableAutoUpdater = process.env.DISABLE_AUTOUPDATER; originalClaudeConfigDir = process.env.CLAUDE_CONFIG_DIR; + + // Clear CCS-managed env vars that leak from host sessions delete process.env.DISABLE_AUTOUPDATER; delete process.env.CLAUDE_CONFIG_DIR; + baselineSigintListeners = process.listeners('SIGINT'); baselineSigtermListeners = process.listeners('SIGTERM'); baselineSighupListeners = process.listeners('SIGHUP'); From 79251d7af8b22a3a7fdb11ac6a357dd245df4590 Mon Sep 17 00:00:00 2001 From: Sergey Galuza Date: Sat, 11 Apr 2026 17:29:15 +0200 Subject: [PATCH 3/4] test(ci): add env isolation comment to persist-command-handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Align with claudecode-env-stripping.test.ts style — explain why CLAUDE_CONFIG_DIR is cleared in beforeEach for consistency across test files. Built [OnSteroids](https://onsteroids.ai) --- tests/unit/commands/persist-command-handler.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/commands/persist-command-handler.test.ts b/tests/unit/commands/persist-command-handler.test.ts index 8f235714..80a6dcd6 100644 --- a/tests/unit/commands/persist-command-handler.test.ts +++ b/tests/unit/commands/persist-command-handler.test.ts @@ -73,6 +73,7 @@ function stubProcessExit(): void { beforeEach(async () => { tempRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'ccs-persist-handler-test-')); originalClaudeConfigDir = process.env.CLAUDE_CONFIG_DIR; + // Clear CLAUDE_CONFIG_DIR so scoped CCS_HOME takes effect (leaks from host CCS session) delete process.env.CLAUDE_CONFIG_DIR; originalProcessExit = process.exit; originalFsOpen = fs.promises.open; From ef564cafdf4213d07914f8e2cf9a348a6fb272b3 Mon Sep 17 00:00:00 2001 From: Sergey Galuza Date: Sat, 11 Apr 2026 21:08:08 +0200 Subject: [PATCH 4/4] ci: retrigger validation