mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 03:43:11 +08:00
merge: integrate QA-003 active-session lifecycle fix
This commit is contained in:
@@ -3474,6 +3474,59 @@ describe('Sessions API', () => {
|
||||
expect((await fetch(`${baseUrl}/api/sessions/${crypto.randomUUID()}/turn-checkpoints`)).status).toBe(404)
|
||||
})
|
||||
|
||||
for (const origin of ['created', 'resumed']) {
|
||||
it(`GET turn-checkpoints keeps a positive ${origin} session known immediately after retention cleanup`, async () => {
|
||||
const { sessionService } = await import('../services/sessionService.js')
|
||||
await fs.writeFile(path.join(tmpDir, 'settings.json'), JSON.stringify({ cleanupPeriodDays: 365 }))
|
||||
resetSettingsCache()
|
||||
let sessionId: string
|
||||
if (origin === 'created') {
|
||||
const created = await fetch(`${baseUrl}/api/sessions`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ workDir: tmpDir }),
|
||||
})
|
||||
expect(created.status).toBe(201)
|
||||
sessionId = (await created.json() as { sessionId: string }).sessionId
|
||||
} else {
|
||||
sessionId = crypto.randomUUID()
|
||||
await writeSessionFile(sanitizePath(tmpDir), sessionId, [
|
||||
{ type: 'session-meta', workDir: tmpDir },
|
||||
{ type: 'user', uuid: 'restored-user', parentUuid: null, message: { role: 'user', content: 'saved before server restart' } },
|
||||
])
|
||||
// Starting the runtime for an existing disk session updates metadata;
|
||||
// this ID was never created by the current SessionService process.
|
||||
await sessionService.appendSessionMetadata(sessionId, { workDir: tmpDir })
|
||||
}
|
||||
const found = await sessionService.findSessionFile(sessionId)
|
||||
expect(found).not.toBeNull()
|
||||
await fs.writeFile(path.join(tmpDir, 'settings.json'), JSON.stringify({ cleanupPeriodDays: 0 }))
|
||||
resetSettingsCache()
|
||||
await fs.unlink(found!.filePath)
|
||||
|
||||
// Clicking the still-open tab requests checkpoints before another prompt
|
||||
// can refresh runtime metadata. No disk evidence must be created here.
|
||||
for (const days of [0, 365]) {
|
||||
await fs.writeFile(path.join(tmpDir, 'settings.json'), JSON.stringify({ cleanupPeriodDays: days }))
|
||||
resetSettingsCache()
|
||||
const response = await fetch(`${baseUrl}/api/sessions/${sessionId}/turn-checkpoints`)
|
||||
expect(response.status).toBe(200)
|
||||
expect(await response.json()).toEqual({ checkpoints: [] })
|
||||
expect(await sessionService.getSessionMessagesWithEvidence(sessionId)).toEqual({
|
||||
messages: [], transcriptEvidenceComplete: false,
|
||||
})
|
||||
expect(await sessionService.findSessionFile(sessionId)).toBeNull()
|
||||
expect((await fetch(`${baseUrl}/api/sessions/${crypto.randomUUID()}/turn-checkpoints`)).status).toBe(404)
|
||||
}
|
||||
|
||||
// Explicit deletion ends this in-process lifetime even when cleanup has
|
||||
// already removed its file; it must not become an immortal empty session.
|
||||
await sessionService.deleteSession(sessionId)
|
||||
expect((await fetch(`${baseUrl}/api/sessions/${sessionId}/turn-checkpoints`)).status).toBe(404)
|
||||
await expect(sessionService.deleteSession(sessionId)).rejects.toMatchObject({ statusCode: 404 })
|
||||
})
|
||||
}
|
||||
|
||||
it('POST /api/sessions should create a session', async () => {
|
||||
const workDir = await fs.mkdtemp(path.join(tmpDir, 'api-session-'))
|
||||
const res = await fetch(`${baseUrl}/api/sessions`, {
|
||||
|
||||
@@ -588,6 +588,10 @@ export class SessionService {
|
||||
// Keep launch state available when retention disables or removes transcripts.
|
||||
// Scope keys by config directory so test/embedded server instances cannot mix state.
|
||||
private readonly memoryLaunchInfo = new Map<string, SessionLaunchInfo>()
|
||||
// Creation or a valid runtime metadata update establishes a session lifetime
|
||||
// independently of its transcript. Cleanup can remove that file before the
|
||||
// next runtime update supplies memoryLaunchInfo.
|
||||
private readonly knownSessionKeys = new Set<string>()
|
||||
private readonly privateTitles = new Map<string, Set<string>>()
|
||||
|
||||
shouldPersistSession(): boolean {
|
||||
@@ -3764,7 +3768,8 @@ export class SessionService {
|
||||
// Retention-zero sessions intentionally have no transcript. The desktop
|
||||
// still asks for turn checkpoints after each live reply; lack of saved
|
||||
// evidence is not a missing session, nor proof of an empty history.
|
||||
if (this.memoryLaunchInfo.has(this.memorySessionKey(sessionId))) {
|
||||
if (this.memoryLaunchInfo.has(this.memorySessionKey(sessionId)) ||
|
||||
this.knownSessionKeys.has(this.memorySessionKey(sessionId))) {
|
||||
return { messages: [], transcriptEvidenceComplete: false }
|
||||
}
|
||||
throw ApiError.notFound(`Session not found: ${sessionId}`)
|
||||
@@ -4071,6 +4076,7 @@ export class SessionService {
|
||||
if (persist && this.shouldPersistSession()) {
|
||||
await fs.writeFile(filePath, JSON.stringify(initialEntry) + '\n' + JSON.stringify(metaEntry) + '\n', 'utf-8')
|
||||
}
|
||||
this.knownSessionKeys.add(this.memorySessionKey(sessionId))
|
||||
this.invalidateSessionListCache()
|
||||
|
||||
return { sessionId, workDir: absWorkDir }
|
||||
@@ -4081,11 +4087,13 @@ export class SessionService {
|
||||
*/
|
||||
async deleteSession(sessionId: string): Promise<void> {
|
||||
const found = await this.findSessionFile(sessionId)
|
||||
if (!found && !this.memoryLaunchInfo.has(this.memorySessionKey(sessionId))) {
|
||||
if (!found && !this.memoryLaunchInfo.has(this.memorySessionKey(sessionId)) &&
|
||||
!this.knownSessionKeys.has(this.memorySessionKey(sessionId))) {
|
||||
throw ApiError.notFound(`Session not found: ${sessionId}`)
|
||||
}
|
||||
|
||||
if (found) await fs.unlink(found.filePath)
|
||||
this.knownSessionKeys.delete(this.memorySessionKey(sessionId))
|
||||
this.memoryLaunchInfo.delete(this.memorySessionKey(sessionId))
|
||||
this.privateTitles.delete(this.memorySessionKey(sessionId))
|
||||
if (found) this.sessionListSummaryCache.delete(found.filePath)
|
||||
@@ -4399,6 +4407,7 @@ export class SessionService {
|
||||
): Promise<void> {
|
||||
const persist = this.shouldPersistSession()
|
||||
const storedInfo = await this.getSessionLaunchInfo(sessionId)
|
||||
if (storedInfo) this.knownSessionKeys.add(this.memorySessionKey(sessionId))
|
||||
const workDir = normalizeDriveRootPathForPlatform(metadata.workDir)
|
||||
const projectDir = this.sanitizePath(workDir)
|
||||
const previousInfo = storedInfo ?? (!persist ? {
|
||||
|
||||
Reference in New Issue
Block a user