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 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-11-08 08:35:04 +01:00
1 parent ddcdd5d974
commit df98912596
5 files changed
+49 -19

No files matched your search

+9 -3
View File
@@ -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);
+10 -3
View File
@@ -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<string> {
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())
)
);
+5 -2
View File
@@ -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');
});
});
+11 -4
View File
@@ -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();
+14 -7
View File
@@ -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;