#!/usr/bin/env node 'use strict'; /** * Test Environment Isolation * * Provides isolated ~/.ccs/ directory for tests to prevent * interference with the user's actual CCS configuration. * * Usage: * const { createTestEnvironment } = require('../fixtures/test-environment'); * * describe('my tests', () => { * let testEnv; * * before(() => { * testEnv = createTestEnvironment(); * }); * * after(() => { * testEnv.cleanup(); * }); * * it('test something', () => { * // Tests run with CCS_HOME pointing to temp directory * // Your test code here * }); * }); */ const fs = require('fs'); const path = require('path'); const os = require('os'); let bootstrappedTestHome; const originalHomedir = os.homedir; let homedirPatched = false; function getEffectiveTestHome() { return process.env.CCS_HOME || process.env.HOME || process.env.USERPROFILE || bootstrappedTestHome || originalHomedir(); } function patchHomedirForTests() { if (homedirPatched) { return; } os.homedir = () => getEffectiveTestHome(); homedirPatched = true; } function createIsolatedTestHome(prefix = 'ccs-test-home-') { const testHome = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); fs.mkdirSync(path.join(testHome, '.ccs'), { recursive: true }); fs.mkdirSync(path.join(testHome, '.claude'), { recursive: true }); fs.mkdirSync(path.join(testHome, '.config'), { recursive: true }); fs.mkdirSync(path.join(testHome, '.cache'), { recursive: true }); fs.mkdirSync(path.join(testHome, '.state'), { recursive: true }); return testHome; } function ensureGlobalTestEnvironment() { if (bootstrappedTestHome) { return bootstrappedTestHome; } const testHome = createIsolatedTestHome(); process.env.HOME = testHome; process.env.USERPROFILE = testHome; process.env.CCS_HOME = testHome; process.env.XDG_CONFIG_HOME = path.join(testHome, '.config'); process.env.XDG_CACHE_HOME = path.join(testHome, '.cache'); process.env.XDG_STATE_HOME = path.join(testHome, '.state'); process.env.CCS_TEST_BOOTSTRAP_HOME = testHome; bootstrappedTestHome = testHome; patchHomedirForTests(); return bootstrappedTestHome; } /** * Create an isolated test environment * Sets CCS_HOME to a temporary directory and provides cleanup * * @returns {object} Test environment with paths and cleanup function */ function createTestEnvironment() { // Create unique temp directory for this test run const testHome = createIsolatedTestHome('ccs-test-'); const testCcsDir = path.join(testHome, '.ccs'); // Store original environment const originalHome = process.env.HOME; const originalCcsHome = process.env.CCS_HOME; const originalUserProfile = process.env.USERPROFILE; const originalXdgConfigHome = process.env.XDG_CONFIG_HOME; const originalXdgCacheHome = process.env.XDG_CACHE_HOME; const originalXdgStateHome = process.env.XDG_STATE_HOME; // Keep HOME-family env vars aligned so code that still consults os.homedir() // or XDG defaults stays inside the isolated test sandbox. process.env.HOME = testHome; process.env.USERPROFILE = testHome; process.env.CCS_HOME = testHome; process.env.XDG_CONFIG_HOME = path.join(testHome, '.config'); process.env.XDG_CACHE_HOME = path.join(testHome, '.cache'); process.env.XDG_STATE_HOME = path.join(testHome, '.state'); patchHomedirForTests(); // Return environment object return { /** Path to the test home directory (like ~) */ testHome, /** Path to the test .ccs directory (like ~/.ccs) */ ccsDir: testCcsDir, /** Original HOME value for reference */ originalHome, /** * Get a path within the test CCS directory * @param {...string} parts - Path segments * @returns {string} Full path */ getCcsPath(...parts) { return path.join(testCcsDir, ...parts); }, /** * Create a file in the test CCS directory * @param {string} relativePath - Path relative to .ccs/ * @param {string|object} content - File content (objects are JSON stringified) */ createFile(relativePath, content) { const fullPath = path.join(testCcsDir, relativePath); const dir = path.dirname(fullPath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } const data = typeof content === 'object' ? JSON.stringify(content, null, 2) : content; fs.writeFileSync(fullPath, data); }, /** * Read a file from the test CCS directory * @param {string} relativePath - Path relative to .ccs/ * @param {boolean} [asJson=false] - Parse as JSON * @returns {string|object} File content */ readFile(relativePath, asJson = false) { const fullPath = path.join(testCcsDir, relativePath); const content = fs.readFileSync(fullPath, 'utf8'); return asJson ? JSON.parse(content) : content; }, /** * Check if a file exists in the test CCS directory * @param {string} relativePath - Path relative to .ccs/ * @returns {boolean} */ fileExists(relativePath) { return fs.existsSync(path.join(testCcsDir, relativePath)); }, /** * Clean up the test environment and restore original settings */ cleanup() { // Restore original environment if (originalHome !== undefined) { process.env.HOME = originalHome; } else { delete process.env.HOME; } if (originalUserProfile !== undefined) { process.env.USERPROFILE = originalUserProfile; } else { delete process.env.USERPROFILE; } if (originalCcsHome !== undefined) { process.env.CCS_HOME = originalCcsHome; } else { delete process.env.CCS_HOME; } if (originalXdgConfigHome !== undefined) { process.env.XDG_CONFIG_HOME = originalXdgConfigHome; } else { delete process.env.XDG_CONFIG_HOME; } if (originalXdgCacheHome !== undefined) { process.env.XDG_CACHE_HOME = originalXdgCacheHome; } else { delete process.env.XDG_CACHE_HOME; } if (originalXdgStateHome !== undefined) { process.env.XDG_STATE_HOME = originalXdgStateHome; } else { delete process.env.XDG_STATE_HOME; } // Clean up temp directory try { fs.rmSync(testHome, { recursive: true, force: true }); } catch (err) { // Ignore cleanup errors console.warn(`[test-environment] Cleanup warning: ${err.message}`); } } }; } /** * Get the CCS home directory (respects CCS_HOME env var) * This should be used instead of os.homedir() for CCS paths * * @returns {string} Home directory path */ function getCcsHome() { return process.env.CCS_HOME || os.homedir(); } /** * Get the CCS directory path * * @returns {string} Path to .ccs directory */ function getCcsDir() { return path.join(getCcsHome(), '.ccs'); } module.exports = { createTestEnvironment, ensureGlobalTestEnvironment, getCcsHome, getCcsDir }; if (process.env.CCS_TEST_DISABLE_GLOBAL_BOOTSTRAP !== '1') { ensureGlobalTestEnvironment(); }