mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 04:18:05 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user