mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-06 15:39:41 +02:00
chore: bump version to 0.1437
This commit is contained in:
@@ -1,6 +1,15 @@
|
||||
/**
|
||||
* Cleanup fixture for E2E tests
|
||||
* Tracks and cleans up all resources (sessions, cases, screens)
|
||||
*
|
||||
* CRITICAL SAFETY: This fixture protects user screens by:
|
||||
* 1. Capturing pre-existing screens at MODULE LOAD time - these are NEVER killed
|
||||
* 2. Tracking screens created during tests - these ARE cleaned up
|
||||
* 3. Protecting current process screen ($CLAUDEMAN_SCREEN_NAME)
|
||||
*
|
||||
* KEY INSIGHT: The cleanup is based on WHEN screens were created, not naming patterns.
|
||||
* Screens that existed before this module loaded are user screens and protected.
|
||||
* Screens created after are test screens and will be cleaned up.
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
@@ -8,6 +17,73 @@ import { existsSync, rmSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
/**
|
||||
* Capture pre-existing screens at MODULE LOAD time (before any tests run).
|
||||
* These screens existed before tests started and must NEVER be killed.
|
||||
* This is the ONLY reliable way to distinguish user screens from test screens.
|
||||
*/
|
||||
const PRE_EXISTING_SCREENS: Set<string> = new Set();
|
||||
const CURRENT_SCREEN_NAME = process.env.CLAUDEMAN_SCREEN_NAME || '';
|
||||
|
||||
// Capture pre-existing screens immediately when this module loads
|
||||
try {
|
||||
const output = execSync('screen -ls 2>/dev/null || true', {
|
||||
encoding: 'utf-8',
|
||||
timeout: 5000,
|
||||
});
|
||||
for (const line of output.split('\n')) {
|
||||
const match = line.match(/\d+\.([^\s]+)/);
|
||||
if (match) {
|
||||
PRE_EXISTING_SCREENS.add(match[1]);
|
||||
}
|
||||
}
|
||||
if (PRE_EXISTING_SCREENS.size > 0) {
|
||||
console.log(`[CleanupTracker] Protected ${PRE_EXISTING_SCREENS.size} pre-existing user screens`);
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors during capture
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a screen name matches user-created patterns (w1-*, s1-*, etc.)
|
||||
*/
|
||||
function isUserScreenPattern(screenName: string): boolean {
|
||||
// w1-, w2-, s1-, s2-, etc. prefixes are user session patterns
|
||||
return /^[ws]\d+-/.test(screenName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a screen name looks like a test screen (contains 'test' or 'e2e')
|
||||
*/
|
||||
function isE2ETestScreen(screenName: string): boolean {
|
||||
return screenName.includes('test') || screenName.includes('e2e');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a screen is protected (should NEVER be killed)
|
||||
*
|
||||
* CRITICAL: Protection is based on WHEN the screen was created, not naming.
|
||||
* - Screens in PRE_EXISTING_SCREENS existed before tests started = USER screens
|
||||
* - Current process screen is always protected
|
||||
* - User patterns (w1-*, s1-*) are protected as extra safety
|
||||
*/
|
||||
function isProtectedScreen(screenName: string): boolean {
|
||||
// Pre-existing screens are ALWAYS protected - this is the primary safeguard
|
||||
if (PRE_EXISTING_SCREENS.has(screenName)) {
|
||||
return true;
|
||||
}
|
||||
// Current process's screen is protected
|
||||
if (CURRENT_SCREEN_NAME && screenName === CURRENT_SCREEN_NAME) {
|
||||
return true;
|
||||
}
|
||||
// User-created screen patterns (w1-*, s1-*) are protected as extra safety
|
||||
if (isUserScreenPattern(screenName)) {
|
||||
return true;
|
||||
}
|
||||
// Everything else can be cleaned up (it was created after tests started)
|
||||
return false;
|
||||
}
|
||||
|
||||
export class CleanupTracker {
|
||||
private sessions: Set<string> = new Set();
|
||||
private cases: Set<string> = new Set();
|
||||
@@ -21,23 +97,30 @@ export class CleanupTracker {
|
||||
}
|
||||
|
||||
/**
|
||||
* Track a session for cleanup
|
||||
* Track a session for cleanup.
|
||||
* Sessions are deleted via API which handles screen cleanup.
|
||||
*/
|
||||
trackSession(sessionId: string): void {
|
||||
this.sessions.add(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Track a case for cleanup
|
||||
* Track a case for cleanup.
|
||||
* SAFETY: Only e2e-test-* cases will be deleted during cleanup.
|
||||
*/
|
||||
trackCase(caseName: string): void {
|
||||
this.cases.add(caseName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Track a screen for cleanup
|
||||
* Track a screen for cleanup.
|
||||
* SAFETY: Protected screens (pre-existing) will be skipped during actual cleanup.
|
||||
*/
|
||||
trackScreen(screenName: string): void {
|
||||
// Still warn but allow tracking - actual protection happens at kill time
|
||||
if (isProtectedScreen(screenName)) {
|
||||
console.warn(`[CleanupTracker] WARNING: Tracking protected screen ${screenName} - will be skipped during cleanup`);
|
||||
}
|
||||
this.screens.add(screenName);
|
||||
}
|
||||
|
||||
@@ -57,8 +140,13 @@ export class CleanupTracker {
|
||||
}
|
||||
this.sessions.clear();
|
||||
|
||||
// Delete case directories
|
||||
// Delete case directories - ONLY e2e-test-* cases
|
||||
for (const caseName of this.cases) {
|
||||
// SAFETY: Double-check case name before deletion
|
||||
if (!caseName.startsWith('e2e-test-')) {
|
||||
console.warn(`[CleanupTracker] BLOCKED deletion of non-e2e-test case: ${caseName}`);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const casePath = join(this.casesDir, caseName);
|
||||
if (existsSync(casePath)) {
|
||||
@@ -79,9 +167,23 @@ export class CleanupTracker {
|
||||
|
||||
/**
|
||||
* Kill a specific screen session
|
||||
* SAFETY: Refuses to kill protected or non-e2e-test screens
|
||||
*/
|
||||
private killScreen(screenName: string): void {
|
||||
// CRITICAL SAFETY CHECK: Never kill protected screens
|
||||
if (isProtectedScreen(screenName)) {
|
||||
console.warn(`[CleanupTracker] BLOCKED attempt to kill protected screen: ${screenName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Double-check: only kill e2e-test screens
|
||||
if (!isE2ETestScreen(screenName)) {
|
||||
console.warn(`[CleanupTracker] BLOCKED attempt to kill non-e2e-test screen: ${screenName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`[CleanupTracker] Killing e2e-test screen: ${screenName}`);
|
||||
execSync(`screen -S ${screenName} -X quit 2>/dev/null`, {
|
||||
stdio: 'pipe',
|
||||
timeout: 5000,
|
||||
@@ -92,28 +194,35 @@ export class CleanupTracker {
|
||||
}
|
||||
|
||||
/**
|
||||
* Force cleanup ALL e2e-test-* cases and claudeman-* screens
|
||||
* Use this in afterAll to ensure no orphans remain
|
||||
* Force cleanup ALL test-created resources.
|
||||
* This cleans up:
|
||||
* - All tracked sessions (via API)
|
||||
* - All e2e-test-* case directories
|
||||
* - ALL claudeman screens created AFTER this module loaded (not in PRE_EXISTING_SCREENS)
|
||||
*/
|
||||
async forceCleanupAll(): Promise<void> {
|
||||
// First, clean tracked resources
|
||||
await this.cleanup();
|
||||
|
||||
// Force kill ALL e2e-test sessions via API
|
||||
// Delete ONLY e2e-test sessions via API
|
||||
// CRITICAL: NEVER delete sessions based on screen name - only explicit e2e-test markers
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/api/sessions`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.sessions) {
|
||||
for (const session of data.sessions) {
|
||||
if (session.name?.startsWith('e2e-test-') || session.workingDir?.includes('e2e-test-')) {
|
||||
try {
|
||||
await fetch(`${this.baseUrl}/api/sessions/${session.id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
const sessions = Array.isArray(data) ? data : data.sessions || [];
|
||||
for (const session of sessions) {
|
||||
// ONLY delete sessions with explicit e2e-test markers
|
||||
// Never use screen name matching - it's not reliable
|
||||
const isTestSession = session.name?.includes('e2e-test') ||
|
||||
session.workingDir?.includes('e2e-test');
|
||||
if (isTestSession) {
|
||||
try {
|
||||
await fetch(`${this.baseUrl}/api/sessions/${session.id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,7 +250,9 @@ export class CleanupTracker {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
|
||||
// Kill all e2e-test claudeman screens
|
||||
// Kill ONLY screens that explicitly contain 'test' in the name
|
||||
// CRITICAL: Never kill screens based on timing/pre-existing checks alone
|
||||
// This is the only safe approach - rely on explicit test naming
|
||||
try {
|
||||
const screenList = execSync('screen -ls 2>/dev/null || true', {
|
||||
encoding: 'utf-8',
|
||||
@@ -150,11 +261,14 @@ export class CleanupTracker {
|
||||
|
||||
const screenLines = screenList.split('\n');
|
||||
for (const line of screenLines) {
|
||||
// Match claudeman-e2e-test-* screens
|
||||
const match = line.match(/\d+\.(claudeman-e2e-test-[^\s]+)/);
|
||||
// Match any claudeman-* screen
|
||||
const match = line.match(/\d+\.(claudeman-[^\s]+)/);
|
||||
if (match) {
|
||||
const screenName = match[1];
|
||||
this.killScreen(screenName);
|
||||
// ONLY kill screens with explicit 'test' in the name
|
||||
if (screenName.includes('test') && !isProtectedScreen(screenName)) {
|
||||
this.killScreen(screenName);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
@@ -203,3 +317,6 @@ export class CleanupTracker {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export safety utilities for use in other test files
|
||||
export { PRE_EXISTING_SCREENS, isProtectedScreen, isE2ETestScreen, isUserScreenPattern };
|
||||
|
||||
@@ -91,9 +91,10 @@ describe('Ralph Loop Wizard E2E', () => {
|
||||
}
|
||||
}, E2E_TIMEOUTS.TEST);
|
||||
|
||||
it('should start Ralph Loop with claudeman-ios using initprompt.md', async () => {
|
||||
it('should start Ralph Loop with test case using initprompt.md', async () => {
|
||||
let browser: BrowserFixture | null = null;
|
||||
const caseName = 'claudeman-ios'; // Use existing case
|
||||
// CRITICAL: Use e2e-test- prefix to ensure cleanup safety
|
||||
const caseName = generateCaseName('ralph-wizard');
|
||||
const casePath = join(homedir(), 'claudeman-cases', caseName);
|
||||
|
||||
try {
|
||||
@@ -103,20 +104,14 @@ describe('Ralph Loop Wizard E2E', () => {
|
||||
if (!cleanup) {
|
||||
cleanup = new CleanupTracker(serverFixture.baseUrl);
|
||||
}
|
||||
cleanup.trackCase(caseName);
|
||||
|
||||
// Read the initprompt.md from the case
|
||||
const initPromptPath = join(casePath, 'initprompt.md');
|
||||
let taskDescription = 'Test task for Ralph Loop';
|
||||
if (existsSync(initPromptPath)) {
|
||||
taskDescription = readFileSync(initPromptPath, 'utf-8').trim();
|
||||
console.log('[RalphLoopE2E] Using initprompt.md:', taskDescription.slice(0, 100) + '...');
|
||||
} else {
|
||||
console.log('[RalphLoopE2E] initprompt.md not found, using default task');
|
||||
}
|
||||
// Use a test task description (not from a real user case)
|
||||
const taskDescription = 'Test task for Ralph Loop E2E: implement a simple hello world function';
|
||||
console.log('[RalphLoopE2E] Using test task:', taskDescription);
|
||||
|
||||
// Check if @fix_plan.md exists (for reuse)
|
||||
const fixPlanPath = join(casePath, '@fix_plan.md');
|
||||
const hasExistingPlan = existsSync(fixPlanPath);
|
||||
// No existing fix plan for test case
|
||||
const hasExistingPlan = false;
|
||||
console.log('[RalphLoopE2E] Existing @fix_plan.md:', hasExistingPlan);
|
||||
|
||||
browser = await createBrowserFixture();
|
||||
@@ -129,6 +124,29 @@ describe('Ralph Loop Wizard E2E', () => {
|
||||
await clickElement(page, '.btn-ralph');
|
||||
await page.waitForSelector('#ralphWizardModal.active', { timeout: E2E_TIMEOUTS.ELEMENT_VISIBLE });
|
||||
|
||||
// For test case, we need to create it first via API, then select it
|
||||
// Create the test case via quick-start API first
|
||||
const createRes = await fetch(`${serverFixture.baseUrl}/api/quick-start`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ caseName, mode: 'claude' }),
|
||||
});
|
||||
const createData = await createRes.json();
|
||||
if (createData.success && createData.sessionId) {
|
||||
cleanup.trackSession(createData.sessionId);
|
||||
// Delete the session but keep the case (we'll use it for Ralph wizard)
|
||||
await fetch(`${serverFixture.baseUrl}/api/sessions/${createData.sessionId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
// Refresh page to pick up the new case
|
||||
await page.reload();
|
||||
await page.waitForSelector('.btn-ralph', { timeout: E2E_TIMEOUTS.ELEMENT_VISIBLE });
|
||||
await clickElement(page, '.btn-ralph');
|
||||
await page.waitForSelector('#ralphWizardModal.active', { timeout: E2E_TIMEOUTS.ELEMENT_VISIBLE });
|
||||
|
||||
// Select case in dropdown
|
||||
const caseSelect = await page.$('#ralphCaseSelect');
|
||||
if (caseSelect) {
|
||||
@@ -201,7 +219,7 @@ describe('Ralph Loop Wizard E2E', () => {
|
||||
expect(Array.isArray(sessions)).toBe(true);
|
||||
expect(sessions.length).toBeGreaterThan(0);
|
||||
|
||||
// Find the NEWEST session for claudeman-ios (sort by createdAt descending)
|
||||
// Find the NEWEST session for our e2e-test case (sort by createdAt descending)
|
||||
const matchingSessions = sessions
|
||||
.filter((s: any) => s.workingDir?.includes(caseName))
|
||||
.sort((a: any, b: any) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||
@@ -213,9 +231,11 @@ describe('Ralph Loop Wizard E2E', () => {
|
||||
expect(session).toBeDefined();
|
||||
cleanup.trackSession(session.id);
|
||||
|
||||
// Verify screen was created
|
||||
// Verify screen was created - note: screen name won't contain 'e2e-test' since it uses sessionId
|
||||
// This is fine - the CleanupTracker will refuse to track it but that's OK since
|
||||
// the session deletion via API will kill the screen anyway
|
||||
const screenName = `claudeman-${session.id.slice(0, 8)}`;
|
||||
cleanup.trackScreen(screenName);
|
||||
// Note: Don't track screen directly - let session deletion handle it
|
||||
const screenList = execSync('screen -ls 2>/dev/null || true', {
|
||||
encoding: 'utf-8',
|
||||
timeout: 5000,
|
||||
|
||||
+103
-2
@@ -6,7 +6,13 @@
|
||||
* - Tracked resource cleanup (only kills what tests create)
|
||||
* - Global beforeAll/afterAll hooks
|
||||
*
|
||||
* SAFETY: This setup ONLY cleans up resources that the test suite itself creates.
|
||||
* CRITICAL SAFETY GUARANTEES:
|
||||
* 1. Pre-existing screens (captured at MODULE LOAD) are NEVER killed
|
||||
* 2. Current process screen ($CLAUDEMAN_SCREEN_NAME) is NEVER killed
|
||||
* 3. Only screens explicitly registered via registerTestScreen() can be killed
|
||||
* 4. All screen names must pass validation before being accepted
|
||||
*
|
||||
* This setup ONLY cleans up resources that the test suite itself creates.
|
||||
* It will NEVER kill Claude processes or screens that weren't spawned by tests.
|
||||
* This makes it safe to run tests from within a Claudeman-managed session.
|
||||
*/
|
||||
@@ -27,12 +33,97 @@ const activeTestClaudePids = new Set<number>();
|
||||
let currentScreenCount = 0;
|
||||
const screenWaiters: Array<() => void> = [];
|
||||
|
||||
/**
|
||||
* CRITICAL: Pre-existing screens captured at MODULE LOAD time.
|
||||
* These screens existed before any test code ran and must NEVER be killed.
|
||||
* This is captured immediately when the module loads, not in beforeAll.
|
||||
*/
|
||||
const preExistingScreensAtModuleLoad = new Set<string>();
|
||||
|
||||
/** Current process's screen name - NEVER kill this */
|
||||
const CURRENT_PROCESS_SCREEN = process.env.CLAUDEMAN_SCREEN_NAME || '';
|
||||
|
||||
// Capture pre-existing screens IMMEDIATELY when this module loads
|
||||
// This happens before any test runs, providing maximum protection
|
||||
try {
|
||||
const output = execSync('screen -ls 2>/dev/null || true', { encoding: 'utf-8', timeout: 5000 });
|
||||
for (const line of output.split('\n')) {
|
||||
const match = line.match(/\d+\.([^\s]+)/);
|
||||
if (match) {
|
||||
preExistingScreensAtModuleLoad.add(match[1]);
|
||||
}
|
||||
}
|
||||
if (preExistingScreensAtModuleLoad.size > 0 || CURRENT_PROCESS_SCREEN) {
|
||||
console.log(`[Test Setup] MODULE LOAD: Protected ${preExistingScreensAtModuleLoad.size} pre-existing screens`);
|
||||
if (CURRENT_PROCESS_SCREEN) {
|
||||
console.log(`[Test Setup] MODULE LOAD: Current process screen: ${CURRENT_PROCESS_SCREEN}`);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors during capture
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a screen name matches user-created patterns (w1-*, s1-*)
|
||||
*/
|
||||
function isUserScreenPattern(screenName: string): boolean {
|
||||
return /^[ws]\d+-/.test(screenName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a screen name looks like a test screen (contains 'test')
|
||||
*/
|
||||
function isTestScreen(screenName: string): boolean {
|
||||
return screenName.toLowerCase().includes('test');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a screen is protected and must NEVER be killed.
|
||||
*
|
||||
* CRITICAL: Protection is based on WHEN the screen was created:
|
||||
* - Screens in preExistingScreensAtModuleLoad existed before tests = USER screens
|
||||
* - Current process screen is always protected
|
||||
* - User patterns (w1-*, s1-*) are protected as extra safety
|
||||
*/
|
||||
function isScreenProtected(screenName: string): boolean {
|
||||
// Pre-existing screens from module load are ALWAYS protected
|
||||
if (preExistingScreensAtModuleLoad.has(screenName)) {
|
||||
return true;
|
||||
}
|
||||
// Current process's screen is protected
|
||||
if (CURRENT_PROCESS_SCREEN && screenName === CURRENT_PROCESS_SCREEN) {
|
||||
return true;
|
||||
}
|
||||
// Also protect screens from preExistingScreens set (captured in beforeAll)
|
||||
if (preExistingScreens.has(screenName)) {
|
||||
return true;
|
||||
}
|
||||
// Protect user-created screen patterns (w1-*, s1-*) as extra safety
|
||||
if (isUserScreenPattern(screenName)) {
|
||||
return true;
|
||||
}
|
||||
// Everything else can be cleaned up
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill only the screens that tests have registered via registerTestScreen()
|
||||
* SAFETY: Only kills screens with 'test' in the name AND not protected
|
||||
*/
|
||||
function killTrackedTestScreens(): void {
|
||||
for (const screenName of activeTestScreens) {
|
||||
// CRITICAL: Only kill screens with explicit 'test' marker
|
||||
if (!screenName.includes('test')) {
|
||||
console.warn(`[Test Setup] SKIPPING: Screen ${screenName} doesn't contain 'test' - not killing`);
|
||||
continue;
|
||||
}
|
||||
// Double-check protection before killing
|
||||
if (isScreenProtected(screenName)) {
|
||||
console.warn(`[Test Setup] BLOCKED: Refusing to kill protected screen: ${screenName}`);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
console.log(`[Test Setup] Killing test screen: ${screenName}`);
|
||||
execSync(`screen -S ${screenName} -X quit 2>/dev/null || true`, { encoding: 'utf-8' });
|
||||
} catch {
|
||||
// Ignore errors
|
||||
@@ -100,9 +191,15 @@ export function releaseScreenSlot(): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a screen session for tracking
|
||||
* Register a screen session for tracking.
|
||||
* SAFETY: Protected screens will be skipped at cleanup time.
|
||||
*/
|
||||
export function registerTestScreen(screenName: string): void {
|
||||
// Warn if registering a protected screen, but allow it
|
||||
// (protection happens at kill time, not registration time)
|
||||
if (isScreenProtected(screenName)) {
|
||||
console.warn(`[Test Setup] WARNING: Registering protected screen ${screenName} - will be skipped during cleanup`);
|
||||
}
|
||||
activeTestScreens.add(screenName);
|
||||
}
|
||||
|
||||
@@ -250,4 +347,8 @@ export {
|
||||
killTrackedTestScreens,
|
||||
killTrackedTestClaudeProcesses,
|
||||
MAX_CONCURRENT_SCREENS,
|
||||
isScreenProtected,
|
||||
isTestScreen,
|
||||
isUserScreenPattern,
|
||||
preExistingScreensAtModuleLoad,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user