From 2dc11ea48fabcfb03facbab365db0212963dc0eb Mon Sep 17 00:00:00 2001 From: Oleg Miagkov Date: Sun, 12 Jul 2026 10:21:17 +0400 Subject: [PATCH 1/2] fix(frontend): isolate test data dirs and remove load-sensitive sleeps Full parallel vitest runs failed 1-10 random tests per run (the failing set changed every run; each test passed in isolation). Three independent race sources: 1. setup.ts rm -rf'd the shared /tmp/auto-code-ui-tests in beforeEach while parallel workers were using it; the computed per-test testId subdir was dead code. TEST_DATA_DIR is now a unique per-test dir under a per-worker base (pid + VITEST_POOL_ID), and afterEach cleans only that dir - the shared parent is never deleted. 2. The five security-*-e2e specs all shared cwd/test-data and unlinked each other's fixture files in beforeAll/afterAll when scheduled into different workers. Each spec now uses its own tmpdir-scoped dir. 3. Fixed sleeps lost under 16-worker load: project-store.test.ts waited a flat 50ms for ProjectStore's fire-and-forget initializeAsync(), so addProject minted a fresh UUID instead of loading the pre-seeded project; window-security.test.ts waited a flat 100ms for app.whenReady() before reading captured BrowserWindow options. ProjectStore now exposes whenReady() (the init promise) which the tests await, and window-security polls until options are captured. Verified: 3 consecutive full npm run test runs green (161 files, 3898 passed / 6 skipped each). Co-Authored-By: Claude Fable 5 Signed-off-by: Oleg Miagkov --- apps/frontend/src/__tests__/setup.ts | 41 ++++++++--------- .../src/main/__tests__/project-store.test.ts | 46 +++++++++---------- .../main/__tests__/window-security.test.ts | 43 ++++++++++------- apps/frontend/src/main/project-store.ts | 12 ++++- .../src/security-allowlist-e2e.spec.ts | 6 ++- .../src/security-audit-log-e2e.spec.ts | 6 ++- apps/frontend/src/security-export-e2e.spec.ts | 6 ++- .../src/security-level-presets-e2e.spec.ts | 6 ++- .../src/security-warnings-e2e.spec.ts | 6 ++- 9 files changed, 97 insertions(+), 75 deletions(-) diff --git a/apps/frontend/src/__tests__/setup.ts b/apps/frontend/src/__tests__/setup.ts index f891d4b8c..f384621ed 100644 --- a/apps/frontend/src/__tests__/setup.ts +++ b/apps/frontend/src/__tests__/setup.ts @@ -2,7 +2,8 @@ * Test setup file for Vitest */ import { vi, beforeEach, afterEach } from 'vitest'; -import { mkdirSync, rmSync, existsSync } from 'fs'; +import { mkdirSync, rmSync } from 'fs'; +import { tmpdir } from 'os'; import path from 'path'; // Mock localStorage for tests that need it @@ -57,39 +58,35 @@ if (typeof global.requestAnimationFrame === 'undefined') { }); } -// Test data directory for isolated file operations -export const TEST_DATA_DIR = '/tmp/auto-code-ui-tests'; +// Base directory scoped to this worker process so parallel vitest workers +// never touch each other's files (pid covers the forks pool, VITEST_POOL_ID +// covers the threads pool where workers share a pid) +const WORKER_DATA_DIR = path.join( + tmpdir(), + 'auto-code-ui-tests', + `worker-${process.pid}-${process.env.VITEST_POOL_ID ?? '0'}` +); + +// Test data directory for isolated file operations - reassigned to a unique +// directory before each test, and only that directory is ever cleaned up +export let TEST_DATA_DIR = WORKER_DATA_DIR; // Create fresh test directory before each test beforeEach(() => { // Clear localStorage localStorageMock.clear(); - // Use a unique subdirectory per test to avoid race conditions in parallel tests + // Unique subdirectory per test; never delete the shared parent const testId = `test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; - const _testDir = path.join(TEST_DATA_DIR, testId); - - try { - if (existsSync(TEST_DATA_DIR)) { - rmSync(TEST_DATA_DIR, { recursive: true, force: true }); - } - } catch { - // Ignore errors if directory is in use by another parallel test - // Each test uses unique subdirectory anyway - } - - try { - mkdirSync(TEST_DATA_DIR, { recursive: true }); - mkdirSync(path.join(TEST_DATA_DIR, 'store'), { recursive: true }); - } catch { - // Ignore errors if directory already exists from another parallel test - } + TEST_DATA_DIR = path.join(WORKER_DATA_DIR, testId); + mkdirSync(path.join(TEST_DATA_DIR, 'store'), { recursive: true }); }); -// Clean up test directory after each test +// Clean up this test's directory after each test afterEach(() => { vi.clearAllMocks(); vi.resetModules(); + rmSync(TEST_DATA_DIR, { recursive: true, force: true }); }); // Mock window.electronAPI for renderer tests diff --git a/apps/frontend/src/main/__tests__/project-store.test.ts b/apps/frontend/src/main/__tests__/project-store.test.ts index bc638eef4..7138178c7 100644 --- a/apps/frontend/src/main/__tests__/project-store.test.ts +++ b/apps/frontend/src/main/__tests__/project-store.test.ts @@ -48,15 +48,11 @@ async function waitForFile(filePath: string, timeout = 2000): Promise { /** * Wait for the ProjectStore's async initialization to complete. * The constructor fires initializeAsync() in the background (fire-and-forget), - * which can race with subsequent method calls on macOS. This helper - * yields enough event-loop ticks for the async init (mkdir + readFile) to finish. + * which can race with subsequent method calls. Awaiting whenReady() is + * deterministic regardless of I/O load, unlike fixed sleeps. */ -async function waitForStoreInit(): Promise { - // Two async I/O ops in initializeAsync (mkdir + readFile) plus microtask overhead. - // Yielding a few times ensures they complete before we proceed. - for (let i = 0; i < 5; i++) { - await new Promise(r => setTimeout(r, 10)); - } +async function waitForStoreInit(store: { whenReady(): Promise }): Promise { + await store.whenReady(); } /** @@ -186,7 +182,7 @@ describe('ProjectStore', () => { it('should persist project to disk', async () => { const { ProjectStore } = await import('../project-store'); const store = new ProjectStore(); - await waitForStoreInit(); + await waitForStoreInit(store); store.addProject(TEST_PROJECT_PATH); @@ -225,7 +221,7 @@ describe('ProjectStore', () => { it('should persist removal to disk', async () => { const { ProjectStore } = await import('../project-store'); const store = new ProjectStore(); - await waitForStoreInit(); + await waitForStoreInit(store); const project = store.addProject(TEST_PROJECT_PATH); // Wait for addProject's async save to complete before removing @@ -313,7 +309,7 @@ describe('ProjectStore', () => { it('should update settings and return updated project', async () => { const { ProjectStore } = await import('../project-store'); const store = new ProjectStore(); - await waitForStoreInit(); + await waitForStoreInit(store); const project = store.addProject(TEST_PROJECT_PATH); const updated = store.updateProjectSettings(project.id, { @@ -329,7 +325,7 @@ describe('ProjectStore', () => { it('should update updatedAt timestamp', async () => { const { ProjectStore } = await import('../project-store'); const store = new ProjectStore(); - await waitForStoreInit(); + await waitForStoreInit(store); const project = store.addProject(TEST_PROJECT_PATH); const originalUpdatedAt = project.updatedAt; @@ -345,7 +341,7 @@ describe('ProjectStore', () => { it('should persist settings changes', async () => { const { ProjectStore } = await import('../project-store'); const store = new ProjectStore(); - await waitForStoreInit(); + await waitForStoreInit(store); const project = store.addProject(TEST_PROJECT_PATH); // Wait for addProject's async save to flush @@ -680,7 +676,7 @@ describe('ProjectStore', () => { const store = new ProjectStore(); // Wait for async initialization to load the file from disk - await waitForStoreInit(); + await waitForStoreInit(store); const projects = store.getProjects(); @@ -698,7 +694,7 @@ describe('ProjectStore', () => { const store = new ProjectStore(); // Wait for async initialization to attempt loading the corrupted file - await waitForStoreInit(); + await waitForStoreInit(store); const projects = store.getProjects(); @@ -725,7 +721,7 @@ describe('ProjectStore', () => { const { ProjectStore } = await import('../project-store'); const store = new ProjectStore(); - await waitForStoreInit(); + await waitForStoreInit(store); const project = store.addProject(TEST_PROJECT_PATH); const result = await store.archiveTasks(project.id, ['001-test-task'], '1.0.0'); @@ -775,7 +771,7 @@ describe('ProjectStore', () => { const { ProjectStore } = await import('../project-store'); const store = new ProjectStore(); - await waitForStoreInit(); + await waitForStoreInit(store); const project = store.addProject(TEST_PROJECT_PATH); const result = await store.archiveTasks(project.id, ['002-multi-location'], '2.0.0'); @@ -825,7 +821,7 @@ describe('ProjectStore', () => { const { ProjectStore } = await import('../project-store'); const store = new ProjectStore(); - await waitForStoreInit(); + await waitForStoreInit(store); const project = store.addProject(TEST_PROJECT_PATH); const result = await store.archiveTasks(project.id, ['003-worktree-only'], '1.0.0'); @@ -843,7 +839,7 @@ describe('ProjectStore', () => { it('should skip non-existent task gracefully', async () => { const { ProjectStore } = await import('../project-store'); const store = new ProjectStore(); - await waitForStoreInit(); + await waitForStoreInit(store); // Create .auto-claude directory so project is recognized mkdirSync(path.join(TEST_PROJECT_PATH, '.auto-claude'), { recursive: true }); @@ -866,7 +862,7 @@ describe('ProjectStore', () => { const { ProjectStore } = await import('../project-store'); const store = new ProjectStore(); - await waitForStoreInit(); + await waitForStoreInit(store); const project = store.addProject(TEST_PROJECT_PATH); @@ -891,7 +887,7 @@ describe('ProjectStore', () => { it('should return false for non-existent project', async () => { const { ProjectStore } = await import('../project-store'); const store = new ProjectStore(); - await waitForStoreInit(); + await waitForStoreInit(store); const result = await store.archiveTasks('nonexistent-project-id', ['some-task']); @@ -937,7 +933,7 @@ describe('ProjectStore', () => { const { ProjectStore } = await import('../project-store'); const store = new ProjectStore(); - await waitForStoreInit(); + await waitForStoreInit(store); const project = store.addProject(TEST_PROJECT_PATH); const result = await store.unarchiveTasks(project.id, ['004-unarchive-test']); @@ -984,7 +980,7 @@ describe('ProjectStore', () => { const { ProjectStore } = await import('../project-store'); const store = new ProjectStore(); - await waitForStoreInit(); + await waitForStoreInit(store); const project = store.addProject(TEST_PROJECT_PATH); expect(project.id).toBe(projectId); @@ -1025,7 +1021,7 @@ describe('ProjectStore', () => { const { ProjectStore } = await import('../project-store'); const store = new ProjectStore(); - await waitForStoreInit(); + await waitForStoreInit(store); const project = store.addProject(TEST_PROJECT_PATH); expect(project.id).toBe(projectId); @@ -1095,7 +1091,7 @@ describe('ProjectStore', () => { const { ProjectStore } = await import('../project-store'); const store = new ProjectStore(); - await waitForStoreInit(); + await waitForStoreInit(store); const project = store.addProject(TEST_PROJECT_PATH); const tasks = await store.getTasks(project.id); diff --git a/apps/frontend/src/main/__tests__/window-security.test.ts b/apps/frontend/src/main/__tests__/window-security.test.ts index dd118ad9f..37a07d954 100644 --- a/apps/frontend/src/main/__tests__/window-security.test.ts +++ b/apps/frontend/src/main/__tests__/window-security.test.ts @@ -212,6 +212,28 @@ vi.mock("../notification-service", () => ({ }, })); +type CapturedWindowOptions = { webPreferences?: Record } | null; + +/** + * Poll until the BrowserWindow constructor has been invoked (app.whenReady() + * and createWindow() run asynchronously after the module import). Polling is + * deterministic under parallel test load, unlike a fixed sleep. + */ +async function waitForCapturedOptions(timeout = 5000): Promise { + const start = Date.now(); + const getOptions = () => + ( + BrowserWindow as unknown as { getCapturedOptions: () => Record } + ).getCapturedOptions() as CapturedWindowOptions; + + while (Date.now() - start < timeout) { + const options = getOptions(); + if (options !== null) return options; + await new Promise((resolve) => setTimeout(resolve, 10)); + } + return getOptions(); +} + describe("BrowserWindow Security Configuration", () => { beforeEach(() => { // Reset captured options before each test @@ -225,13 +247,8 @@ describe("BrowserWindow Security Configuration", () => { // This must happen after all mocks are set up await import("../index"); - // Wait a bit for app.whenReady() to resolve and createWindow() to be called - await new Promise((resolve) => setTimeout(resolve, 100)); - - // Get the captured options from BrowserWindow constructor - const capturedOptions = ( - BrowserWindow as unknown as { getCapturedOptions: () => Record } - ).getCapturedOptions() as { webPreferences?: Record } | null; + // Wait for app.whenReady() to resolve and createWindow() to be called + const capturedOptions = await waitForCapturedOptions(); // Verify options were captured expect(capturedOptions).not.toBeNull(); @@ -249,11 +266,7 @@ describe("BrowserWindow Security Configuration", () => { await import("../index"); // Wait for window creation - await new Promise((resolve) => setTimeout(resolve, 100)); - - const capturedOptions = ( - BrowserWindow as unknown as { getCapturedOptions: () => Record } - ).getCapturedOptions() as { webPreferences?: Record } | null; + const capturedOptions = await waitForCapturedOptions(); expect(capturedOptions).not.toBeNull(); expect(capturedOptions?.webPreferences).toBeDefined(); @@ -266,11 +279,7 @@ describe("BrowserWindow Security Configuration", () => { await import("../index"); // Wait for window creation - await new Promise((resolve) => setTimeout(resolve, 100)); - - const capturedOptions = ( - BrowserWindow as unknown as { getCapturedOptions: () => Record } - ).getCapturedOptions() as { webPreferences?: Record } | null; + const capturedOptions = await waitForCapturedOptions(); expect(capturedOptions).not.toBeNull(); const webPreferences = capturedOptions?.webPreferences; diff --git a/apps/frontend/src/main/project-store.ts b/apps/frontend/src/main/project-store.ts index cc85552b9..62a492ac0 100644 --- a/apps/frontend/src/main/project-store.ts +++ b/apps/frontend/src/main/project-store.ts @@ -46,6 +46,7 @@ export class ProjectStore { */ private consecutiveFailures = 0; private static readonly MAX_FAILURES_BEFORE_WARNING = 3; + private readonly initPromise: Promise; constructor() { // Store in app's userData directory @@ -57,11 +58,20 @@ export class ProjectStore { this.data = { projects: [], settings: {} }; // Start async initialization in background - this.initializeAsync().catch(error => { + this.initPromise = this.initializeAsync().catch(error => { console.error('[ProjectStore] Failed to initialize store:', error); }); } + /** + * Resolves once the background initialization (mkdir + load from disk) + * has finished. Callers that need the on-disk data loaded should await + * this before reading or mutating the store. + */ + whenReady(): Promise { + return this.initPromise; + } + /** * Async initialization - ensures directory exists and loads data */ diff --git a/apps/frontend/src/security-allowlist-e2e.spec.ts b/apps/frontend/src/security-allowlist-e2e.spec.ts index 341b5727a..5194c61f4 100644 --- a/apps/frontend/src/security-allowlist-e2e.spec.ts +++ b/apps/frontend/src/security-allowlist-e2e.spec.ts @@ -13,11 +13,13 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { promises as fs } from 'fs'; +import { tmpdir } from 'os'; import path from 'path'; import type { SecurityProfile, CommandAllowlistEntry } from './shared/types/security'; -// Test data paths -const TEST_DATA_DIR = path.join(process.cwd(), 'test-data'); +// Test data paths - unique per spec file and process so parallel vitest +// workers running the other security e2e specs never share this directory +const TEST_DATA_DIR = path.join(tmpdir(), 'auto-code-ui-tests', `security-allowlist-e2e-${process.pid}`); const TEST_PROFILE_PATH = path.join(TEST_DATA_DIR, '.auto-claude-security.json'); /** diff --git a/apps/frontend/src/security-audit-log-e2e.spec.ts b/apps/frontend/src/security-audit-log-e2e.spec.ts index 70e5a3a33..86b44bb34 100644 --- a/apps/frontend/src/security-audit-log-e2e.spec.ts +++ b/apps/frontend/src/security-audit-log-e2e.spec.ts @@ -12,11 +12,13 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { promises as fs } from 'fs'; +import { tmpdir } from 'os'; import path from 'path'; import type { SecurityAuditLog } from './shared/types/security'; -// Test data paths -const TEST_DATA_DIR = path.join(process.cwd(), 'test-data'); +// Test data paths - unique per spec file and process so parallel vitest +// workers running the other security e2e specs never share this directory +const TEST_DATA_DIR = path.join(tmpdir(), 'auto-code-ui-tests', `security-audit-log-e2e-${process.pid}`); const TEST_AUDIT_LOG_PATH = path.join(TEST_DATA_DIR, '.auto-claude-audit.json'); /** diff --git a/apps/frontend/src/security-export-e2e.spec.ts b/apps/frontend/src/security-export-e2e.spec.ts index 891a2c332..550c2743c 100644 --- a/apps/frontend/src/security-export-e2e.spec.ts +++ b/apps/frontend/src/security-export-e2e.spec.ts @@ -13,11 +13,13 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { promises as fs } from 'fs'; +import { tmpdir } from 'os'; import path from 'path'; import type { SecurityProfile, SecurityExport, SecurityAuditLog } from './shared/types/security'; -// Test data paths -const TEST_DATA_DIR = path.join(process.cwd(), 'test-data'); +// Test data paths - unique per spec file and process so parallel vitest +// workers running the other security e2e specs never share this directory +const TEST_DATA_DIR = path.join(tmpdir(), 'auto-code-ui-tests', `security-export-e2e-${process.pid}`); const TEST_PROFILE_PATH = path.join(TEST_DATA_DIR, '.auto-claude-security.json'); const TEST_AUDIT_LOG_PATH = path.join(TEST_DATA_DIR, '.auto-claude-audit.log'); const EXPORT_OUTPUT_DIR = path.join(TEST_DATA_DIR, 'exports'); diff --git a/apps/frontend/src/security-level-presets-e2e.spec.ts b/apps/frontend/src/security-level-presets-e2e.spec.ts index dfc159b9e..ea2a4a61a 100644 --- a/apps/frontend/src/security-level-presets-e2e.spec.ts +++ b/apps/frontend/src/security-level-presets-e2e.spec.ts @@ -12,11 +12,13 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { promises as fs } from 'fs'; +import { tmpdir } from 'os'; import path from 'path'; import type { SecurityProfile, SecurityLevel, CommandAllowlistEntry } from './shared/types/security'; -// Test data paths -const TEST_DATA_DIR = path.join(process.cwd(), 'test-data'); +// Test data paths - unique per spec file and process so parallel vitest +// workers running the other security e2e specs never share this directory +const TEST_DATA_DIR = path.join(tmpdir(), 'auto-code-ui-tests', `security-level-presets-e2e-${process.pid}`); const TEST_PROFILE_PATH = path.join(TEST_DATA_DIR, '.auto-claude-security.json'); // Security level preset configurations (matching SecuritySettings.tsx) diff --git a/apps/frontend/src/security-warnings-e2e.spec.ts b/apps/frontend/src/security-warnings-e2e.spec.ts index 4f7f4f653..07152b116 100644 --- a/apps/frontend/src/security-warnings-e2e.spec.ts +++ b/apps/frontend/src/security-warnings-e2e.spec.ts @@ -12,11 +12,13 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { promises as fs } from 'fs'; +import { tmpdir } from 'os'; import path from 'path'; import type { SecurityProfile, CommandAllowlistEntry, SecurityAuditLog } from './shared/types/security'; -// Test data paths -const TEST_DATA_DIR = path.join(process.cwd(), 'test-data'); +// Test data paths - unique per spec file and process so parallel vitest +// workers running the other security e2e specs never share this directory +const TEST_DATA_DIR = path.join(tmpdir(), 'auto-code-ui-tests', `security-warnings-e2e-${process.pid}`); const TEST_PROFILE_PATH = path.join(TEST_DATA_DIR, '.auto-claude-security.json'); const TEST_AUDIT_LOG_PATH = path.join(TEST_DATA_DIR, '.auto-claude-audit.json'); From a718d3b00203b03168a632b4bf562412765db172 Mon Sep 17 00:00:00 2001 From: Oleg Miagkov Date: Sun, 12 Jul 2026 10:33:49 +0400 Subject: [PATCH 2/2] fix(frontend): use mkdtemp for test data dirs (CodeQL js/insecure-temporary-file) Predictable names under the shared os.tmpdir() triggered 61 CWE-377 alerts: another local user could pre-create or symlink the path. mkdtempSync creates an unpredictable directory with 0700 permissions, which also keeps the per-worker/per-file isolation this PR introduced. Co-Authored-By: Claude Fable 5 Signed-off-by: Oleg Miagkov --- apps/frontend/src/__tests__/setup.ts | 12 ++++-------- apps/frontend/src/security-allowlist-e2e.spec.ts | 9 +++++---- apps/frontend/src/security-audit-log-e2e.spec.ts | 9 +++++---- apps/frontend/src/security-export-e2e.spec.ts | 9 +++++---- apps/frontend/src/security-level-presets-e2e.spec.ts | 9 +++++---- apps/frontend/src/security-warnings-e2e.spec.ts | 9 +++++---- 6 files changed, 29 insertions(+), 28 deletions(-) diff --git a/apps/frontend/src/__tests__/setup.ts b/apps/frontend/src/__tests__/setup.ts index f384621ed..8836a40a4 100644 --- a/apps/frontend/src/__tests__/setup.ts +++ b/apps/frontend/src/__tests__/setup.ts @@ -2,7 +2,7 @@ * Test setup file for Vitest */ import { vi, beforeEach, afterEach } from 'vitest'; -import { mkdirSync, rmSync } from 'fs'; +import { mkdirSync, mkdtempSync, rmSync } from 'fs'; import { tmpdir } from 'os'; import path from 'path'; @@ -59,13 +59,9 @@ if (typeof global.requestAnimationFrame === 'undefined') { } // Base directory scoped to this worker process so parallel vitest workers -// never touch each other's files (pid covers the forks pool, VITEST_POOL_ID -// covers the threads pool where workers share a pid) -const WORKER_DATA_DIR = path.join( - tmpdir(), - 'auto-code-ui-tests', - `worker-${process.pid}-${process.env.VITEST_POOL_ID ?? '0'}` -); +// never touch each other's files. mkdtemp creates an unpredictable 0700 +// directory, so no other local user can pre-create or tamper with it. +const WORKER_DATA_DIR = mkdtempSync(path.join(tmpdir(), 'auto-code-ui-tests-')); // Test data directory for isolated file operations - reassigned to a unique // directory before each test, and only that directory is ever cleaned up diff --git a/apps/frontend/src/security-allowlist-e2e.spec.ts b/apps/frontend/src/security-allowlist-e2e.spec.ts index 5194c61f4..90bc81f69 100644 --- a/apps/frontend/src/security-allowlist-e2e.spec.ts +++ b/apps/frontend/src/security-allowlist-e2e.spec.ts @@ -12,14 +12,15 @@ */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; -import { promises as fs } from 'fs'; +import { mkdtempSync, promises as fs } from 'fs'; import { tmpdir } from 'os'; import path from 'path'; import type { SecurityProfile, CommandAllowlistEntry } from './shared/types/security'; -// Test data paths - unique per spec file and process so parallel vitest -// workers running the other security e2e specs never share this directory -const TEST_DATA_DIR = path.join(tmpdir(), 'auto-code-ui-tests', `security-allowlist-e2e-${process.pid}`); +// Test data paths - mkdtemp gives this spec file its own unpredictable +// 0700 directory, so parallel vitest workers running the other security +// e2e specs never share it (and no other local user can pre-create it) +const TEST_DATA_DIR = mkdtempSync(path.join(tmpdir(), 'security-allowlist-e2e-')); const TEST_PROFILE_PATH = path.join(TEST_DATA_DIR, '.auto-claude-security.json'); /** diff --git a/apps/frontend/src/security-audit-log-e2e.spec.ts b/apps/frontend/src/security-audit-log-e2e.spec.ts index 86b44bb34..924dd20d5 100644 --- a/apps/frontend/src/security-audit-log-e2e.spec.ts +++ b/apps/frontend/src/security-audit-log-e2e.spec.ts @@ -11,14 +11,15 @@ */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; -import { promises as fs } from 'fs'; +import { mkdtempSync, promises as fs } from 'fs'; import { tmpdir } from 'os'; import path from 'path'; import type { SecurityAuditLog } from './shared/types/security'; -// Test data paths - unique per spec file and process so parallel vitest -// workers running the other security e2e specs never share this directory -const TEST_DATA_DIR = path.join(tmpdir(), 'auto-code-ui-tests', `security-audit-log-e2e-${process.pid}`); +// Test data paths - mkdtemp gives this spec file its own unpredictable +// 0700 directory, so parallel vitest workers running the other security +// e2e specs never share it (and no other local user can pre-create it) +const TEST_DATA_DIR = mkdtempSync(path.join(tmpdir(), 'security-audit-log-e2e-')); const TEST_AUDIT_LOG_PATH = path.join(TEST_DATA_DIR, '.auto-claude-audit.json'); /** diff --git a/apps/frontend/src/security-export-e2e.spec.ts b/apps/frontend/src/security-export-e2e.spec.ts index 550c2743c..328ba3a1b 100644 --- a/apps/frontend/src/security-export-e2e.spec.ts +++ b/apps/frontend/src/security-export-e2e.spec.ts @@ -12,14 +12,15 @@ */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; -import { promises as fs } from 'fs'; +import { mkdtempSync, promises as fs } from 'fs'; import { tmpdir } from 'os'; import path from 'path'; import type { SecurityProfile, SecurityExport, SecurityAuditLog } from './shared/types/security'; -// Test data paths - unique per spec file and process so parallel vitest -// workers running the other security e2e specs never share this directory -const TEST_DATA_DIR = path.join(tmpdir(), 'auto-code-ui-tests', `security-export-e2e-${process.pid}`); +// Test data paths - mkdtemp gives this spec file its own unpredictable +// 0700 directory, so parallel vitest workers running the other security +// e2e specs never share it (and no other local user can pre-create it) +const TEST_DATA_DIR = mkdtempSync(path.join(tmpdir(), 'security-export-e2e-')); const TEST_PROFILE_PATH = path.join(TEST_DATA_DIR, '.auto-claude-security.json'); const TEST_AUDIT_LOG_PATH = path.join(TEST_DATA_DIR, '.auto-claude-audit.log'); const EXPORT_OUTPUT_DIR = path.join(TEST_DATA_DIR, 'exports'); diff --git a/apps/frontend/src/security-level-presets-e2e.spec.ts b/apps/frontend/src/security-level-presets-e2e.spec.ts index ea2a4a61a..1543f76a9 100644 --- a/apps/frontend/src/security-level-presets-e2e.spec.ts +++ b/apps/frontend/src/security-level-presets-e2e.spec.ts @@ -11,14 +11,15 @@ */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; -import { promises as fs } from 'fs'; +import { mkdtempSync, promises as fs } from 'fs'; import { tmpdir } from 'os'; import path from 'path'; import type { SecurityProfile, SecurityLevel, CommandAllowlistEntry } from './shared/types/security'; -// Test data paths - unique per spec file and process so parallel vitest -// workers running the other security e2e specs never share this directory -const TEST_DATA_DIR = path.join(tmpdir(), 'auto-code-ui-tests', `security-level-presets-e2e-${process.pid}`); +// Test data paths - mkdtemp gives this spec file its own unpredictable +// 0700 directory, so parallel vitest workers running the other security +// e2e specs never share it (and no other local user can pre-create it) +const TEST_DATA_DIR = mkdtempSync(path.join(tmpdir(), 'security-level-presets-e2e-')); const TEST_PROFILE_PATH = path.join(TEST_DATA_DIR, '.auto-claude-security.json'); // Security level preset configurations (matching SecuritySettings.tsx) diff --git a/apps/frontend/src/security-warnings-e2e.spec.ts b/apps/frontend/src/security-warnings-e2e.spec.ts index 07152b116..42b9f1f98 100644 --- a/apps/frontend/src/security-warnings-e2e.spec.ts +++ b/apps/frontend/src/security-warnings-e2e.spec.ts @@ -11,14 +11,15 @@ */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; -import { promises as fs } from 'fs'; +import { mkdtempSync, promises as fs } from 'fs'; import { tmpdir } from 'os'; import path from 'path'; import type { SecurityProfile, CommandAllowlistEntry, SecurityAuditLog } from './shared/types/security'; -// Test data paths - unique per spec file and process so parallel vitest -// workers running the other security e2e specs never share this directory -const TEST_DATA_DIR = path.join(tmpdir(), 'auto-code-ui-tests', `security-warnings-e2e-${process.pid}`); +// Test data paths - mkdtemp gives this spec file its own unpredictable +// 0700 directory, so parallel vitest workers running the other security +// e2e specs never share it (and no other local user can pre-create it) +const TEST_DATA_DIR = mkdtempSync(path.join(tmpdir(), 'security-warnings-e2e-')); const TEST_PROFILE_PATH = path.join(TEST_DATA_DIR, '.auto-claude-security.json'); const TEST_AUDIT_LOG_PATH = path.join(TEST_DATA_DIR, '.auto-claude-audit.json');