import { appendFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import * as os from 'node:os' import * as path from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { watchForCodexSession } from './codexSessionWatcher' const tempDirs: string[] = [] afterEach(() => { while (tempDirs.length > 0) { const dir = tempDirs.pop() if (dir) rmSync(dir, { recursive: true, force: true }) } }) function tempDir(prefix: string): string { const dir = mkdtempSync(path.join(os.tmpdir(), prefix)) tempDirs.push(dir) return dir } const MAIN_ID = '01a0e3bb-61e3-7c31-adf3-2d2a746a3282' const SUB_ID = '01a0e3bc-2a65-7490-84a6-e963505d9bff' const OTHER_ID = '01a0e3be-4665-7991-b89e-f6881609c36b' function metaLine(id: string, cwd: string, source: unknown): string { return `${JSON.stringify({ timestamp: '2026-09-27T16:38:24.015Z', type: 'session_meta', payload: { id, cwd, source } })}\n` } function rolloutPath(baseDir: string, id: string): string { const day = path.join(baseDir, '2026', '09', '28') mkdirSync(day, { recursive: true }) return path.join(day, `rollout-2026-09-28T00-38-23-${id}.jsonl`) } const tick = (ms = 50): Promise => new Promise(r => setTimeout(r, ms)) describe('watchForCodexSession', () => { it('只认本目录的主会话,跳过别的目录和子助手会话', async () => { const baseDir = tempDir('codex-sessions-') const cwd = tempDir('worktree-') const ac = new AbortController() const found = watchForCodexSession({ baseDir, cwd, signal: ac.signal }) await tick() writeFileSync(rolloutPath(baseDir, OTHER_ID), metaLine(OTHER_ID, '/somewhere/else', 'cli')) writeFileSync(rolloutPath(baseDir, SUB_ID), metaLine(SUB_ID, cwd, { subagent: { thread_spawn: { parent_thread_id: MAIN_ID } } })) await tick() writeFileSync(rolloutPath(baseDir, MAIN_ID), metaLine(MAIN_ID, cwd, 'cli')) await expect(found).resolves.toBe(MAIN_ID) }) it('没有时间上限,文件晚出现也能认到', async () => { const baseDir = tempDir('codex-sessions-') const cwd = tempDir('worktree-') const ac = new AbortController() const found = watchForCodexSession({ baseDir, cwd, signal: ac.signal }) await tick(300) writeFileSync(rolloutPath(baseDir, MAIN_ID), metaLine(MAIN_ID, cwd, 'cli')) await expect(found).resolves.toBe(MAIN_ID) }) it('第一行还没写完时等它写完再认', async () => { const baseDir = tempDir('codex-sessions-') const cwd = tempDir('worktree-') const ac = new AbortController() const found = watchForCodexSession({ baseDir, cwd, signal: ac.signal }) await tick() const file = rolloutPath(baseDir, MAIN_ID) const line = metaLine(MAIN_ID, cwd, 'cli') writeFileSync(file, line.slice(0, 20)) await tick() appendFileSync(file, line.slice(20)) await expect(found).resolves.toBe(MAIN_ID) }) it('中止后返回 null', async () => { const baseDir = tempDir('codex-sessions-') const cwd = tempDir('worktree-') const ac = new AbortController() const found = watchForCodexSession({ baseDir, cwd, signal: ac.signal }) await tick() ac.abort() await expect(found).resolves.toBeNull() }) })