fix(websearch): use getCcsDir() for test isolation

Ensure all hook-related modules use getCcsDir() from environment.ts
for consistent test isolation. Prevents tests from touching the user's
real ~/.ccs/ directory during test runs.

Changes:
- hook-config.ts: Use getCcsDir() for hookConfigPath
- hook-installer.ts: Use getCcsDir() for HOOK_SOURCE_PATH
- profile-hook-injector.ts: Use getCcsDir() for hook sources
- CLAUDE.md: Add test isolation rules
This commit is contained in:
kaitranntt
2026-01-25 21:03:01 -05:00
parent ce59eb6269
commit b33674b3b2
4 changed files with 71 additions and 21 deletions
+13
View File
@@ -2,6 +2,19 @@
AI-facing guidance for Claude Code when working with this repository.
## Critical Constraints (NEVER VIOLATE)
### Test Isolation (MANDATORY)
**NEVER touch the user's real `~/.ccs/` or `~/.claude/` directories during tests.**
- All code accessing CCS paths MUST use `getCcsDir()` from `src/utils/config-manager.ts`
- This function respects `CCS_HOME` env var for test isolation
- **WRONG:** `path.join(os.homedir(), '.ccs', ...)`
- **CORRECT:** `path.join(getCcsDir(), ...)`
Tests set `process.env.CCS_HOME` to a temp directory. Code using `os.homedir()` directly will modify the user's real files.
## Core Function
CLI wrapper for instant switching between multiple Claude accounts and alternative models (GLM, GLMT, Kimi). See README.md for user documentation.
+10 -5
View File
@@ -11,16 +11,21 @@ import * as path from 'path';
import * as os from 'os';
import { info, warn } from '../ui';
import { getWebSearchConfig } from '../../config/unified-config-loader';
import { getCcsDir } from '../config-manager';
// Path to Claude settings.json
// Path to Claude settings.json (intentionally uses real homedir for global settings)
const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
// CCS hooks directory
const CCS_HOOKS_DIR = path.join(os.homedir(), '.ccs', 'hooks');
// Hook file name
const WEBSEARCH_HOOK = 'websearch-transformer.cjs';
/**
* Get CCS hooks directory (respects CCS_HOME for test isolation)
*/
function getCcsHooksDir(): string {
return path.join(getCcsDir(), 'hooks');
}
// Buffer time added to max provider timeout for hook timeout (seconds)
const HOOK_TIMEOUT_BUFFER = 30;
@@ -31,7 +36,7 @@ const MIN_HOOK_TIMEOUT = 60;
* Get path to WebSearch hook
*/
export function getHookPath(): string {
return path.join(CCS_HOOKS_DIR, WEBSEARCH_HOOK);
return path.join(getCcsHooksDir(), WEBSEARCH_HOOK);
}
/**
+11 -6
View File
@@ -8,21 +8,25 @@
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { info, warn } from '../ui';
import { getWebSearchConfig } from '../../config/unified-config-loader';
import { getHookPath, ensureHookConfig } from './hook-config';
import { removeMigrationMarker } from './profile-hook-injector';
import { getCcsDir } from '../config-manager';
// Re-export from hook-config for backward compatibility
export { getHookPath, getWebSearchHookConfig } from './hook-config';
// CCS hooks directory
const CCS_HOOKS_DIR = path.join(os.homedir(), '.ccs', 'hooks');
// Hook file name
const WEBSEARCH_HOOK = 'websearch-transformer.cjs';
/**
* Get CCS hooks directory (respects CCS_HOME for test isolation)
*/
function getCcsHooksDir(): string {
return path.join(getCcsDir(), 'hooks');
}
/**
* Check if WebSearch hook is installed
*/
@@ -50,8 +54,9 @@ export function installWebSearchHook(): boolean {
}
// Ensure hooks directory exists
if (!fs.existsSync(CCS_HOOKS_DIR)) {
fs.mkdirSync(CCS_HOOKS_DIR, { recursive: true, mode: 0o700 });
const hooksDir = getCcsHooksDir();
if (!fs.existsSync(hooksDir)) {
fs.mkdirSync(hooksDir, { recursive: true, mode: 0o700 });
}
const hookPath = getHookPath();
+37 -10
View File
@@ -9,17 +9,21 @@
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { info, warn } from '../ui';
import { getWebSearchHookConfig, getHookPath } from './hook-config';
import { getWebSearchConfig } from '../../config/unified-config-loader';
import { removeHookConfig } from './hook-config';
import { getCcsDir } from '../config-manager';
// CCS directory
const CCS_DIR = path.join(os.homedir(), '.ccs');
// Valid profile name pattern (alphanumeric, dash, underscore only)
const VALID_PROFILE_NAME = /^[a-zA-Z0-9_-]+$/;
// Migration marker file
const MIGRATION_MARKER = path.join(CCS_DIR, '.hook-migrated');
/**
* Get migration marker path (respects CCS_HOME for test isolation)
*/
function getMigrationMarkerPath(): string {
return path.join(getCcsDir(), '.hook-migrated');
}
/**
* Check if CCS WebSearch hook exists in settings
@@ -44,7 +48,8 @@ function hasCcsHook(settings: Record<string, unknown>): boolean {
* Migrate CCS hook from global settings to profile settings (one-time)
*/
function migrateGlobalHook(): void {
if (fs.existsSync(MIGRATION_MARKER)) {
const markerPath = getMigrationMarkerPath();
if (fs.existsSync(markerPath)) {
return; // Already migrated
}
@@ -53,8 +58,13 @@ function migrateGlobalHook(): void {
if (removed && process.env.CCS_DEBUG) {
console.error(info('Migrated WebSearch hook from global settings'));
}
// Ensure CCS dir exists before creating marker
const ccsDir = getCcsDir();
if (!fs.existsSync(ccsDir)) {
fs.mkdirSync(ccsDir, { recursive: true, mode: 0o700 });
}
// Create marker file
fs.writeFileSync(MIGRATION_MARKER, new Date().toISOString(), 'utf8');
fs.writeFileSync(markerPath, new Date().toISOString(), 'utf8');
} catch (error) {
if (process.env.CCS_DEBUG) {
console.error(warn(`Migration failed: ${(error as Error).message}`));
@@ -70,6 +80,14 @@ function migrateGlobalHook(): void {
*/
export function ensureProfileHooks(profileName: string): boolean {
try {
// Validate profile name to prevent path traversal
if (!VALID_PROFILE_NAME.test(profileName)) {
if (process.env.CCS_DEBUG) {
console.error(warn(`Invalid profile name: ${profileName}`));
}
return false;
}
const wsConfig = getWebSearchConfig();
// Skip if WebSearch is disabled
@@ -80,7 +98,15 @@ export function ensureProfileHooks(profileName: string): boolean {
// One-time migration from global settings
migrateGlobalHook();
const settingsPath = path.join(CCS_DIR, `${profileName}.settings.json`);
// Get CCS directory (respects CCS_HOME for test isolation)
const ccsDir = getCcsDir();
// Ensure CCS dir exists
if (!fs.existsSync(ccsDir)) {
fs.mkdirSync(ccsDir, { recursive: true, mode: 0o700 });
}
const settingsPath = path.join(ccsDir, `${profileName}.settings.json`);
// Read existing settings or create empty
let settings: Record<string, unknown> = {};
@@ -193,8 +219,9 @@ function updateHookTimeoutIfNeeded(
*/
export function removeMigrationMarker(): void {
try {
if (fs.existsSync(MIGRATION_MARKER)) {
fs.unlinkSync(MIGRATION_MARKER);
const markerPath = getMigrationMarkerPath();
if (fs.existsSync(markerPath)) {
fs.unlinkSync(markerPath);
}
} catch {
// Ignore errors