From df98912596d1e999b05957c8d1407e7006d84507 Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Sun, 8 Nov 2026 08:35:04 +0100 Subject: [PATCH] test: keep session creates out of the machine's real /tmp Five gate files created sessions with `workingDir: '/tmp'` against a real server. With the agent skill on by default, every `npm test` then wrote a skill copy into `/tmp/.claude/skills/codeman/`, outside the per-file temp HOME, where a Claude Code started in `/tmp` would load it (the workspace hooks already landed in `/tmp/.claude/settings.local.json` the same way). Each file now uses a temp folder of its own and removes it afterwards. Co-Authored-By: Claude Opus 5.5 --- test/edge-cases.test.ts | 12 +++++++++--- test/operation-lightspeed.test.ts | 13 ++++++++++--- test/quick-start.test.ts | 7 +++++-- test/sse-events.test.ts | 15 +++++++++++---- test/sse-subscription-filter.test.ts | 21 ++++++++++++++------- 5 files changed, 49 insertions(+), 19 deletions(-) diff --git a/test/edge-cases.test.ts b/test/edge-cases.test.ts index a3724705..6e530dbb 100644 --- a/test/edge-cases.test.ts +++ b/test/edge-cases.test.ts @@ -1,10 +1,16 @@ import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest'; import { WebServer } from '../src/web/server.js'; +import { mkdtempSync, rmSync } from 'node:fs'; import { join } from 'node:path'; -import { homedir } from 'node:os'; +import { homedir, tmpdir } from 'node:os'; import { safeRmHomeTree } from './mocks/index.js'; const CASES_DIR = join(homedir(), 'codeman-cases'); +// A folder of this file's own for the sessions below, never the machine's real `/tmp`: +// a create writes the workspace hooks and the agent skill (on by default) into its +// working dir, and those would outlive the run there. +const WORK_DIR = mkdtempSync(join(tmpdir(), 'codeman-edge-cases-')); +afterAll(() => rmSync(WORK_DIR, { recursive: true, force: true })); describe('Edge Cases and Error Handling', () => { let server: WebServer; @@ -279,7 +285,7 @@ describe('Concurrent Session Handling', () => { fetch(`${baseUrl}/api/sessions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ workingDir: '/tmp' }), + body: JSON.stringify({ workingDir: WORK_DIR }), }).then((r) => r.json()) ); @@ -312,7 +318,7 @@ describe('Concurrent Session Handling', () => { const createRes = await fetch(`${baseUrl}/api/sessions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ workingDir: '/tmp' }), + body: JSON.stringify({ workingDir: WORK_DIR }), }); const createData = await createRes.json(); expect(createData.success).toBe(true); diff --git a/test/operation-lightspeed.test.ts b/test/operation-lightspeed.test.ts index 0e93da5c..f954a5ca 100644 --- a/test/operation-lightspeed.test.ts +++ b/test/operation-lightspeed.test.ts @@ -13,9 +13,16 @@ import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'; import { WebServer } from '../src/web/server.js'; import { safeRmHomeTree } from './mocks/index.js'; -import { homedir } from 'node:os'; +import { mkdtempSync, rmSync } from 'node:fs'; +import { homedir, tmpdir } from 'node:os'; import { join } from 'node:path'; +// A folder of this file's own for the sessions below, never the machine's real `/tmp`: +// a create writes the workspace hooks and the agent skill (on by default) into its +// working dir, and those would outlive the run there. +const WORK_DIR = mkdtempSync(join(tmpdir(), 'codeman-lightspeed-')); +afterAll(() => rmSync(WORK_DIR, { recursive: true, force: true })); + // Helper to parse SSE events from raw text function parseSSEEvents(text: string): Array<{ event: string; data: unknown }> { const events: Array<{ event: string; data: unknown }> = []; @@ -86,7 +93,7 @@ async function createSession(baseUrl: string): Promise { const res = await fetch(`${baseUrl}/api/sessions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ workingDir: '/tmp' }), + body: JSON.stringify({ workingDir: WORK_DIR }), }); const data = await res.json(); if (!data.data?.session?.id) { @@ -1226,7 +1233,7 @@ describe('Operation Lightspeed', () => { fetch(`${baseUrl}/api/sessions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ workingDir: '/tmp' }), + body: JSON.stringify({ workingDir: WORK_DIR }), }).then((r) => r.json()) ) ); diff --git a/test/quick-start.test.ts b/test/quick-start.test.ts index 20e1fc56..0166965a 100644 --- a/test/quick-start.test.ts +++ b/test/quick-start.test.ts @@ -174,10 +174,13 @@ describe('Session Management', () => { describe('POST /api/sessions', () => { it('should create a new session', async () => { + // Under the test HOME, never the machine's real `/tmp`: a create writes the workspace + // hooks and the agent skill (on by default) into its working dir. + const workingDir = mkdtempSync(join(TEST_HOME, 'work-')); const response = await fetch(`${baseUrl}/api/sessions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ workingDir: '/tmp' }), + body: JSON.stringify({ workingDir }), }); const data = await response.json(); @@ -185,7 +188,7 @@ describe('Session Management', () => { expect(data.success).toBe(true); expect(data.data.session).toBeDefined(); expect(data.data.session.id).toBeDefined(); - expect(data.data.session.workingDir).toBe('/tmp'); + expect(data.data.session.workingDir).toBe(workingDir); expect(data.data.session.status).toBe('idle'); }); }); diff --git a/test/sse-events.test.ts b/test/sse-events.test.ts index 71fecf37..1addcd67 100644 --- a/test/sse-events.test.ts +++ b/test/sse-events.test.ts @@ -2,9 +2,16 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { WebServer } from '../src/web/server.js'; import { EventEmitter } from 'node:events'; import { safeRmHomeTree } from './mocks/index.js'; -import { homedir } from 'node:os'; +import { mkdtempSync, rmSync } from 'node:fs'; +import { homedir, tmpdir } from 'node:os'; import { join } from 'node:path'; +// A folder of this file's own for the sessions below, never the machine's real `/tmp`: +// a create writes the workspace hooks and the agent skill (on by default) into its +// working dir, and those would outlive the run there. +const WORK_DIR = mkdtempSync(join(tmpdir(), 'codeman-sse-events-')); +afterAll(() => rmSync(WORK_DIR, { recursive: true, force: true })); + // Helper to parse SSE events function parseSSEEvents(text: string): Array<{ event: string; data: unknown }> { const events: Array<{ event: string; data: unknown }> = []; @@ -130,7 +137,7 @@ describe('SSE Events', () => { await fetch(`${baseUrl}/api/sessions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ workingDir: '/tmp' }), + body: JSON.stringify({ workingDir: WORK_DIR }), }); // Wait a bit for the event @@ -168,7 +175,7 @@ describe('SSE Events', () => { await fetch(`${baseUrl}/api/sessions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ workingDir: '/tmp' }), + body: JSON.stringify({ workingDir: WORK_DIR }), }); const response = await fetch(`${baseUrl}/api/status`); @@ -221,7 +228,7 @@ describe('SSE Event Types', () => { const createRes = await fetch(`${baseUrl}/api/sessions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ workingDir: '/tmp' }), + body: JSON.stringify({ workingDir: WORK_DIR }), }); const createData = await createRes.json(); diff --git a/test/sse-subscription-filter.test.ts b/test/sse-subscription-filter.test.ts index e809c91b..f2a011c6 100644 --- a/test/sse-subscription-filter.test.ts +++ b/test/sse-subscription-filter.test.ts @@ -1,9 +1,16 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { WebServer } from '../src/web/server.js'; import { safeRmHomeTree } from './mocks/index.js'; -import { homedir } from 'node:os'; +import { mkdtempSync, rmSync } from 'node:fs'; +import { homedir, tmpdir } from 'node:os'; import { join } from 'node:path'; +// A folder of this file's own for the sessions below, never the machine's real `/tmp`: +// a create writes the workspace hooks and the agent skill (on by default) into its +// working dir, and those would outlive the run there. +const WORK_DIR = mkdtempSync(join(tmpdir(), 'codeman-sse-filter-')); +afterAll(() => rmSync(WORK_DIR, { recursive: true, force: true })); + // Helper to parse SSE events from raw text function parseSSEEvents(text: string): Array<{ event: string; data: unknown }> { const events: Array<{ event: string; data: unknown }> = []; @@ -157,7 +164,7 @@ describe('SSE Subscription Filtering', () => { const createRes = await fetch(`${baseUrl}/api/sessions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ workingDir: '/tmp' }), + body: JSON.stringify({ workingDir: WORK_DIR }), }); const createData = await createRes.json(); const sessionId = createData.data.session.id; @@ -208,7 +215,7 @@ describe('SSE Subscription Filtering', () => { const createRes = await fetch(`${baseUrl}/api/sessions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ workingDir: '/tmp' }), + body: JSON.stringify({ workingDir: WORK_DIR }), }); const createData = await createRes.json(); const sessionId = createData.data.session.id; @@ -263,14 +270,14 @@ describe('SSE Subscription Filtering', () => { const createRes1 = await fetch(`${baseUrl}/api/sessions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ workingDir: '/tmp' }), + body: JSON.stringify({ workingDir: WORK_DIR }), }); const session1 = (await createRes1.json()).data.session; const createRes2 = await fetch(`${baseUrl}/api/sessions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ workingDir: '/tmp' }), + body: JSON.stringify({ workingDir: WORK_DIR }), }); const session2 = (await createRes2.json()).data.session; @@ -330,14 +337,14 @@ describe('SSE Subscription Filtering', () => { const createRes1 = await fetch(`${baseUrl}/api/sessions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ workingDir: '/tmp' }), + body: JSON.stringify({ workingDir: WORK_DIR }), }); const session1 = (await createRes1.json()).data.session; const createRes2 = await fetch(`${baseUrl}/api/sessions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ workingDir: '/tmp' }), + body: JSON.stringify({ workingDir: WORK_DIR }), }); const session2 = (await createRes2.json()).data.session;