diff --git a/scripts/regression.js b/scripts/regression.js index e53467b..cd7dac8 100644 --- a/scripts/regression.js +++ b/scripts/regression.js @@ -3,6 +3,7 @@ const fs = require('fs'); const os = require('os'); const path = require('path'); +const net = require('net'); const { spawn, spawnSync } = require('child_process'); const WebSocket = require('ws'); @@ -19,6 +20,18 @@ function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } +function getFreePort() { + return new Promise((resolve, reject) => { + const server = net.createServer(); + server.on('error', reject); + server.listen(0, '127.0.0.1', () => { + const addr = server.address(); + const port = addr && typeof addr === 'object' ? addr.port : null; + server.close(() => resolve(port)); + }); + }); +} + function assert(condition, message) { if (!condition) { throw new Error(message); @@ -303,7 +316,7 @@ async function main() { createFakeClaudeHistory(homeDir); const codexFixture = createFakeCodexHistory(homeDir); - const port = 9102; + const port = await getFreePort(); const password = 'Regression!234'; await withServer({ @@ -429,6 +442,24 @@ async function main() { assert(claudeSpawnLine && claudeSpawnLine.includes('--input-format stream-json'), 'Claude image message should switch stdin to stream-json'); const storedClaudeSession = JSON.parse(fs.readFileSync(path.join(sessionsDir, `${claudeImageSession.sessionId}.json`), 'utf8')); assert(Array.isArray(storedClaudeSession.messages?.[0]?.attachments) && storedClaudeSession.messages[0].attachments.length === 1, 'Claude message should persist attachment metadata'); + assert(storedClaudeSession.claudeSessionId, 'Claude session id should be persisted after first run'); + const claudeSessionIdBeforeMode = storedClaudeSession.claudeSessionId; + + // Mode switching must not clear Claude runtime session id (resume should keep context). + ws.send(JSON.stringify({ type: 'set_mode', sessionId: claudeImageSession.sessionId, mode: 'plan' })); + await nextMessage(messages, ws, (msg) => msg.type === 'mode_changed' && msg.mode === 'plan'); + const storedClaudeAfterMode = JSON.parse(fs.readFileSync(path.join(sessionsDir, `${claudeImageSession.sessionId}.json`), 'utf8')); + assert(storedClaudeAfterMode.claudeSessionId === claudeSessionIdBeforeMode, 'Claude session id should survive mode switch'); + + ws.send(JSON.stringify({ type: 'message', text: 'second claude prompt', sessionId: claudeImageSession.sessionId, mode: 'plan', agent: 'claude' })); + await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === claudeImageSession.sessionId); + const claudeSpawns = fs.readFileSync(path.join(logsDir, 'process.log'), 'utf8') + .trim() + .split('\n') + .filter((line) => line.includes(`"event":"process_spawn"`) && line.includes(claudeImageSession.sessionId.slice(0, 8))); + const lastClaudeSpawn = claudeSpawns[claudeSpawns.length - 1] || ''; + assert(lastClaudeSpawn.includes(`--resume ${claudeSessionIdBeforeMode}`), 'Claude mode switch should keep --resume session id'); + assert(lastClaudeSpawn.includes('--permission-mode plan'), 'Claude plan mode should set --permission-mode plan'); ws.send(JSON.stringify({ type: 'list_native_sessions' })); const nativeSessions = await nextMessage(messages, ws, (msg) => msg.type === 'native_sessions'); diff --git a/server.js b/server.js index cd1120d..6a0bb19 100644 --- a/server.js +++ b/server.js @@ -2066,9 +2066,7 @@ function handleSlashCommand(ws, text, sessionId, fallbackAgent) { const mode = modeInput.toLowerCase(); if (session) { session.permissionMode = mode; - // Claude CLI permission mode changes should reset runtime resume id. - // For Codex, keep thread id so mode switching does not drop context. - if (isClaudeSession(session)) clearRuntimeSessionId(session); + // Mode switching should not reset runtime context (Claude/Codex both resume). session.updated = new Date().toISOString(); saveSession(session); } @@ -2339,8 +2337,7 @@ function handleRenameSession(ws, sessionId, title) { const session = loadSession(sessionId); if (session) { session.permissionMode = mode; - // Same rule as /mode: don't clear Codex thread id on mode changes. - if (isClaudeSession(session)) clearRuntimeSessionId(session); + // Same rule as /mode: don't clear runtime context on mode changes. session.updated = new Date().toISOString(); saveSession(session); }