🐛 fix(vscode): 别的机器上开的会话在本机没有会话文件时,会话 id 行置灰不可双击,不再 resume 报错
This commit is contained in:
1 parent
2b1e660cc8
commit
bf4661e364
20 files changed
+508
-234
No files matched your search
@@ -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])
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user