🐛 fix(vscode): 别的机器上开的会话在本机没有会话文件时,会话 id 行置灰不可双击,不再 resume 报错

This commit is contained in:
cruldra committed 2026-09-24 19:22:29 +08:00
1 parent 2b1e660cc8
commit bf4661e364
20 files changed
+508 -234

No files matched your search

+159
View File
@@ -0,0 +1,159 @@
import type { Issue } from '../gitea/types'
import { promises as fsp, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
annotateMissingSessionIds,
claudeSessionFileExists,
codexSessionFileExists,
listClaudeSessionIds,
listCodexSessionIds,
} from './sessionFiles'
const tempDirs: string[] = []
afterEach(() => {
vi.restoreAllMocks()
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 UUID_A = '019dcb27-58a6-70a1-a5d1-bfc7f3ed9d0a'
const UUID_B = '11111111-2222-4333-8444-555555555555'
const SID_1 = 'aaaaaaaa-1111-4111-8111-111111111111'
const SID_2 = 'bbbbbbbb-2222-4222-8222-222222222222'
const SID_3 = 'cccccccc-3333-4333-8333-333333333333'
function writeRollout(baseDir: string, ymd: [string, string, string], uuid: string): void {
const dayDir = path.join(baseDir, ...ymd)
mkdirSync(dayDir, { recursive: true })
writeFileSync(path.join(dayDir, `rollout-2026-04-27T02-57-26-${uuid}.jsonl`), '{}\n')
}
function writeTranscript(projectsRoot: string, project: string, sid: string): void {
const dir = path.join(projectsRoot, project)
mkdirSync(dir, { recursive: true })
writeFileSync(path.join(dir, `${sid}.jsonl`), '{}\n')
}
describe('listCodexSessionIds', () => {
it('扫年/月/日三层目录,从文件名尾部提取全部会话 id', async () => {
const base = tempDir('codex-sessions-')
writeRollout(base, ['2026', '04', '27'], UUID_A)
writeRollout(base, ['2026', '05', '01'], UUID_B)
const ids = await listCodexSessionIds(base)
expect(ids).toEqual(new Set([UUID_A, UUID_B]))
})
it('忽略不符合 rollout-*-<uuid>.jsonl 的文件', async () => {
const base = tempDir('codex-sessions-')
const dayDir = path.join(base, '2026', '04', '27')
mkdirSync(dayDir, { recursive: true })
writeFileSync(path.join(dayDir, 'notes.txt'), 'x')
writeFileSync(path.join(dayDir, 'rollout-2026-04-27T02-57-26-not-a-uuid.jsonl'), '{}\n')
writeRollout(base, ['2026', '04', '27'], UUID_A)
const ids = await listCodexSessionIds(base)
expect(ids).toEqual(new Set([UUID_A]))
})
it('目录不存在返回空 Set', async () => {
const ids = await listCodexSessionIds(path.join(tempDir('codex-sessions-'), 'nope'))
expect(ids.size).toBe(0)
})
})
describe('listClaudeSessionIds', () => {
it('收集各 project 目录下的 <sid>.jsonl,统一小写', async () => {
const root = tempDir('claude-projects-')
writeTranscript(root, '-Users-a-proj', SID_1.toUpperCase())
writeTranscript(root, '-Users-b-wt', SID_2)
const ids = await listClaudeSessionIds(root)
expect(ids).toEqual(new Set([SID_1, SID_2]))
})
it('不下钻 <sid>/ 子目录,忽略非 jsonl 文件和根目录下的普通文件', async () => {
const root = tempDir('claude-projects-')
writeTranscript(root, '-Users-a-proj', SID_1)
const subagents = path.join(root, '-Users-a-proj', SID_1, 'subagents')
mkdirSync(subagents, { recursive: true })
writeFileSync(path.join(subagents, `${SID_3}.jsonl`), '{}\n')
writeFileSync(path.join(root, '-Users-a-proj', 'notes.txt'), 'x')
writeFileSync(path.join(root, '.DS_Store'), 'x')
const ids = await listClaudeSessionIds(root)
expect(ids).toEqual(new Set([SID_1]))
})
it('根目录不存在返回空 Set', async () => {
const ids = await listClaudeSessionIds(path.join(tempDir('claude-projects-'), 'nope'))
expect(ids.size).toBe(0)
})
})
describe('单个会话文件判定', () => {
it('claudeSessionFileExists:任一 project 目录下有 transcript 即为 true', async () => {
const root = tempDir('claude-projects-')
writeTranscript(root, '-Users-b-wt', SID_1)
expect(await claudeSessionFileExists(SID_1, root)).toBe(true)
expect(await claudeSessionFileExists(SID_2, root)).toBe(false)
})
it('codexSessionFileExists:按 rollout 文件名尾部 uuid 判定', async () => {
const base = tempDir('codex-sessions-')
writeRollout(base, ['2026', '04', '27'], UUID_A)
expect(await codexSessionFileExists(UUID_A.toUpperCase(), base)).toBe(true)
expect(await codexSessionFileExists(UUID_B, base)).toBe(false)
})
})
describe('annotateMissingSessionIds', () => {
const baseIssue = { number: 1, title: 't', column: 'review', htmlUrl: '' } as unknown as Issue
it('claude + codex 混合:只列出本机没有会话文件的 id,全在本机的不加字段', async () => {
const claudeRoot = tempDir('claude-projects-')
const codexRoot = tempDir('codex-sessions-')
writeTranscript(claudeRoot, '-Users-a-proj', SID_1)
writeTranscript(claudeRoot, '-Users-b-wt', SID_2)
writeRollout(codexRoot, ['2026', '04', '27'], UUID_A)
const issues: Issue[] = [
{ ...baseIssue, number: 1, sessionId: SID_1.toUpperCase(), implementSessionId: SID_3, reviewSessionId: UUID_A },
{ ...baseIssue, number: 2, testSessionId: SID_3, prototypeSessionId: SID_2, reviewSessionId: UUID_B },
{ ...baseIssue, number: 3, sessionId: SID_2, reviewSessionId: UUID_A.toUpperCase() },
{ ...baseIssue, number: 4 },
]
const out = await annotateMissingSessionIds(issues, { claudeRoot, codexRoot })
expect(out[0].missingSessionIds).toEqual([SID_3])
expect(out[1].missingSessionIds).toEqual([SID_3, UUID_B])
expect('missingSessionIds' in out[2]).toBe(false)
expect('missingSessionIds' in out[3]).toBe(false)
})
it('没有任何工单带会话 id 时一个目录都不扫', async () => {
const readdir = vi.spyOn(fsp, 'readdir')
const out = await annotateMissingSessionIds([{ ...baseIssue }], {
claudeRoot: tempDir('claude-projects-'),
codexRoot: tempDir('codex-sessions-'),
})
expect(readdir).not.toHaveBeenCalled()
expect('missingSessionIds' in out[0]).toBe(false)
})
it('只带 claude 会话 id 时不扫 codex 目录', async () => {
const claudeRoot = tempDir('claude-projects-')
const codexRoot = tempDir('codex-sessions-')
const readdir = vi.spyOn(fsp, 'readdir')
const out = await annotateMissingSessionIds([{ ...baseIssue, sessionId: SID_1 }], { claudeRoot, codexRoot })
expect(readdir.mock.calls.some(([p]) => p === codexRoot)).toBe(false)
expect(readdir.mock.calls.some(([p]) => p === claudeRoot)).toBe(true)
expect(out[0].missingSessionIds).toEqual([SID_1])
})
})