@iflow-mcp/ejmockler-brutalist
Version:
Deploy Claude, Codex & Gemini CLI agents to demolish your work before users do. Real file analysis. Brutal honesty. Now with conversation continuation & intelligent pagination.
193 lines • 6.54 kB
JavaScript
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { logger } from '../logger.js';
import crypto from 'crypto';
/**
* Provides isolated test environments with unique workspaces,
* cache namespaces, and environment variables
*/
export class TestIsolation {
static activeWorkspaces = new Set();
static originalEnv = { ...process.env };
testId;
workspacePath;
envOverrides = {};
cacheNamespace;
constructor(testName) {
// Generate unique test ID
this.testId = `${testName.replace(/[^a-z0-9]/gi, '_')}_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
this.cacheNamespace = `test_cache_${this.testId}`;
}
/**
* Create an isolated workspace directory for the test
*/
async createWorkspace() {
if (this.workspacePath) {
return this.workspacePath;
}
const tmpDir = os.tmpdir();
this.workspacePath = path.join(tmpDir, 'brutalist-test', this.testId);
// Create directory recursively
await fs.promises.mkdir(this.workspacePath, { recursive: true });
TestIsolation.activeWorkspaces.add(this.workspacePath);
logger.debug(`TestIsolation: Created workspace ${this.workspacePath}`);
return this.workspacePath;
}
/**
* Get the cache namespace for this test
*/
getCacheNamespace() {
return this.cacheNamespace;
}
/**
* Set isolated environment variables for the test
*/
setEnv(overrides) {
this.envOverrides = { ...this.envOverrides, ...overrides };
// Apply overrides to process.env
for (const [key, value] of Object.entries(overrides)) {
process.env[key] = value;
}
}
/**
* Create a test file in the workspace
*/
async createFile(relativePath, content) {
const workspace = await this.createWorkspace();
const filePath = path.join(workspace, relativePath);
// Create directory if needed
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
// Write file
await fs.promises.writeFile(filePath, content, 'utf-8');
return filePath;
}
/**
* Create a test directory structure
*/
async createDirectory(relativePath) {
const workspace = await this.createWorkspace();
const dirPath = path.join(workspace, relativePath);
await fs.promises.mkdir(dirPath, { recursive: true });
return dirPath;
}
/**
* Read a file from the workspace
*/
async readFile(relativePath) {
if (!this.workspacePath) {
throw new Error('Workspace not created');
}
const filePath = path.join(this.workspacePath, relativePath);
return fs.promises.readFile(filePath, 'utf-8');
}
/**
* Check if a file exists in the workspace
*/
async fileExists(relativePath) {
if (!this.workspacePath) {
return false;
}
const filePath = path.join(this.workspacePath, relativePath);
try {
await fs.promises.access(filePath);
return true;
}
catch {
return false;
}
}
/**
* Clean up the test workspace and restore environment
*/
async cleanup() {
// Restore original environment variables
for (const key of Object.keys(this.envOverrides)) {
if (TestIsolation.originalEnv[key] !== undefined) {
process.env[key] = TestIsolation.originalEnv[key];
}
else {
delete process.env[key];
}
}
this.envOverrides = {};
// Remove workspace directory
if (this.workspacePath) {
try {
await fs.promises.rm(this.workspacePath, { recursive: true, force: true });
TestIsolation.activeWorkspaces.delete(this.workspacePath);
logger.debug(`TestIsolation: Cleaned up workspace ${this.workspacePath}`);
}
catch (error) {
logger.error(`Failed to clean up workspace ${this.workspacePath}:`, error);
}
this.workspacePath = undefined;
}
}
/**
* Clean up all active workspaces (for global cleanup)
*/
static async cleanupAll() {
const cleanupPromises = Array.from(TestIsolation.activeWorkspaces).map(async (workspace) => {
try {
await fs.promises.rm(workspace, { recursive: true, force: true });
logger.debug(`TestIsolation: Cleaned up orphaned workspace ${workspace}`);
}
catch (error) {
logger.error(`Failed to clean up orphaned workspace ${workspace}:`, error);
}
});
await Promise.all(cleanupPromises);
TestIsolation.activeWorkspaces.clear();
// Restore original environment
process.env = { ...TestIsolation.originalEnv };
}
/**
* Assert no workspaces are leaked
*/
static assertNoLeakedWorkspaces() {
if (TestIsolation.activeWorkspaces.size > 0) {
const leaked = Array.from(TestIsolation.activeWorkspaces);
throw new Error(`Test leaked ${leaked.length} workspaces:\n ${leaked.join('\n ')}`);
}
}
/**
* Get diagnostic information
*/
getDiagnostics() {
const lines = ['TestIsolation diagnostics:'];
lines.push(` Test ID: ${this.testId}`);
lines.push(` Workspace: ${this.workspacePath || 'not created'}`);
lines.push(` Cache namespace: ${this.cacheNamespace}`);
lines.push(` Env overrides: ${Object.keys(this.envOverrides).join(', ') || 'none'}`);
return lines.join('\n');
}
}
/**
* Jest test helpers for isolation
*/
export function setupTestIsolation(testName) {
const isolation = new TestIsolation(testName);
// Register cleanup in afterEach
afterEach(async () => {
await isolation.cleanup();
});
return isolation;
}
/**
* Global test setup for isolation
*/
export async function globalTestSetup() {
// Clean up any leftover workspaces from previous runs
await TestIsolation.cleanupAll();
}
/**
* Global test teardown for isolation
*/
export async function globalTestTeardown() {
// Clean up all workspaces
await TestIsolation.cleanupAll();
// Assert no leaks
TestIsolation.assertNoLeakedWorkspaces();
}
//# sourceMappingURL=test-isolation.js.map