mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
Merge commit '784118e'
This commit is contained in:
commit
3cd63fabef
@ -1024,6 +1024,7 @@ describe('WebSocket Chat Integration', () => {
|
||||
|
||||
it('should include desktop service diagnostics when CLI startup fails', async () => {
|
||||
const workDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-startup-missing-workdir-'))
|
||||
const canonicalWorkDir = await fs.realpath(workDir)
|
||||
const createRes = await fetch(`${baseUrl}/api/sessions`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@ -1042,7 +1043,7 @@ describe('WebSocket Chat Integration', () => {
|
||||
})
|
||||
expect(error?.message).toContain('Desktop service diagnostics:')
|
||||
expect(error?.message).toContain(`sessionId: ${sessionId}`)
|
||||
expect(error?.message).toContain(`workDir: ${workDir}`)
|
||||
expect(error?.message).toContain(`workDir: ${canonicalWorkDir}`)
|
||||
expect(error?.message).toContain('runtimeOverride: (none)')
|
||||
expect(error?.message).toContain('activeProviderId:')
|
||||
expect(error?.message).toContain('configuredProviders:')
|
||||
|
||||
@ -849,7 +849,8 @@ describe('SessionService', () => {
|
||||
)
|
||||
|
||||
// Verify the file was created
|
||||
const sanitized = sanitizePath(workDir)
|
||||
const canonicalWorkDir = await fs.realpath(workDir)
|
||||
const sanitized = sanitizePath(canonicalWorkDir)
|
||||
const filePath = path.join(tmpDir, 'projects', sanitized, `${sessionId}.jsonl`)
|
||||
const stat = await fs.stat(filePath)
|
||||
expect(stat.isFile()).toBe(true)
|
||||
@ -981,11 +982,12 @@ describe('SessionService', () => {
|
||||
})
|
||||
|
||||
it('should detect placeholder launch info for desktop-created sessions', async () => {
|
||||
const { sessionId } = await service.createSession(os.tmpdir())
|
||||
const workDir = await fs.realpath(os.tmpdir())
|
||||
const { sessionId } = await service.createSession(workDir)
|
||||
|
||||
const launchInfo = await service.getSessionLaunchInfo(sessionId)
|
||||
expect(launchInfo).not.toBeNull()
|
||||
expect(launchInfo!.workDir).toBe(os.tmpdir())
|
||||
expect(launchInfo!.workDir).toBe(workDir)
|
||||
expect(launchInfo!.transcriptMessageCount).toBe(0)
|
||||
expect(launchInfo!.customTitle).toBeNull()
|
||||
})
|
||||
@ -1252,7 +1254,7 @@ describe('Sessions API', () => {
|
||||
isGitRepo: boolean
|
||||
}
|
||||
expect(statusBody.state).toBe('ok')
|
||||
expect(statusBody.workDir).toBe(workDir)
|
||||
expect(statusBody.workDir).toBe(await fs.realpath(workDir))
|
||||
expect(statusBody.isGitRepo).toBe(true)
|
||||
expect(statusBody.changedFiles).toEqual(
|
||||
expect.arrayContaining([
|
||||
@ -1733,6 +1735,54 @@ describe('Sessions API', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('trimSessionMessagesFrom should remove orphan transcript entries beyond the rewind point', async () => {
|
||||
const sessionId = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
|
||||
const firstUserId = crypto.randomUUID()
|
||||
const firstAssistantId = crypto.randomUUID()
|
||||
const secondUserId = crypto.randomUUID()
|
||||
const secondAssistantId = crypto.randomUUID()
|
||||
|
||||
const filePath = await writeSessionFile('-tmp-api-rewind-orphans', sessionId, [
|
||||
makeSnapshotEntry(),
|
||||
makeSessionMetaEntry('/tmp/project-with-hyphen'),
|
||||
{
|
||||
...makeUserEntry('first prompt', firstUserId),
|
||||
sessionId,
|
||||
},
|
||||
{
|
||||
...makeAssistantEntry('first reply', firstUserId),
|
||||
uuid: firstAssistantId,
|
||||
},
|
||||
{
|
||||
...makeUserEntry('second prompt', secondUserId),
|
||||
parentUuid: firstAssistantId,
|
||||
sessionId,
|
||||
},
|
||||
{
|
||||
...makeAssistantEntry('second reply', secondUserId),
|
||||
uuid: secondAssistantId,
|
||||
},
|
||||
{
|
||||
...makeAssistantEntry('late stale reply', secondUserId),
|
||||
uuid: crypto.randomUUID(),
|
||||
},
|
||||
])
|
||||
|
||||
const result = await service.trimSessionMessagesFrom(sessionId, firstUserId)
|
||||
expect(result.removedMessageIds).toContain(firstUserId)
|
||||
expect(result.removedMessageIds).toContain(secondUserId)
|
||||
|
||||
const raw = await fs.readFile(filePath, 'utf-8')
|
||||
expect(raw).toContain('"type":"session-meta"')
|
||||
expect(raw).not.toContain('late stale reply')
|
||||
expect(await service.getSessionMessages(sessionId)).toEqual([])
|
||||
|
||||
const launchInfo = await service.getSessionLaunchInfo(sessionId)
|
||||
expect(launchInfo).not.toBeNull()
|
||||
expect(launchInfo!.workDir).toBe('/tmp/project-with-hyphen')
|
||||
expect(launchInfo!.transcriptMessageCount).toBe(0)
|
||||
})
|
||||
|
||||
it('POST /api/sessions/:id/rewind should target the selected message id instead of a shifted visible index', async () => {
|
||||
const sessionId = 'aaaaaaaa-bbbb-cccc-dddd-ffffffffffff'
|
||||
const firstUserId = crypto.randomUUID()
|
||||
|
||||
@ -133,10 +133,6 @@ export class ConversationService {
|
||||
)
|
||||
}
|
||||
|
||||
if (shouldReplacePlaceholder) {
|
||||
await sessionService.deleteSessionFile(sessionId)
|
||||
}
|
||||
|
||||
if (!fs.existsSync(workDir) || !fs.statSync(workDir).isDirectory()) {
|
||||
throw new ConversationStartupError(
|
||||
`Working directory does not exist or is not a directory: ${workDir}`,
|
||||
@ -144,6 +140,10 @@ export class ConversationService {
|
||||
)
|
||||
}
|
||||
|
||||
if (shouldReplacePlaceholder) {
|
||||
await sessionService.clearSessionTranscript(sessionId, workDir)
|
||||
}
|
||||
|
||||
const args = this.buildSessionCliArgs(
|
||||
sessionId,
|
||||
sdkUrl,
|
||||
@ -540,6 +540,20 @@ export class ConversationService {
|
||||
}
|
||||
}
|
||||
|
||||
async stopSessionAndWait(sessionId: string, timeoutMs = 2_000): Promise<void> {
|
||||
const session = this.sessions.get(sessionId)
|
||||
if (!session) return
|
||||
|
||||
this.sessions.delete(sessionId)
|
||||
session.proc.kill()
|
||||
|
||||
await Promise.race([
|
||||
session.proc.exited.catch(() => undefined),
|
||||
new Promise<void>((resolve) => setTimeout(resolve, timeoutMs)),
|
||||
])
|
||||
await this.waitForProcessOutputDrain(session, timeoutMs)
|
||||
}
|
||||
|
||||
markSessionDeleted(sessionId: string): void {
|
||||
this.deletedSessions.add(sessionId)
|
||||
this.stopSession(sessionId)
|
||||
|
||||
@ -1096,9 +1096,7 @@ export async function executeSessionRewind(
|
||||
target.targetUserMessageId,
|
||||
)
|
||||
|
||||
if (conversationService.hasSession(sessionId)) {
|
||||
conversationService.stopSession(sessionId)
|
||||
}
|
||||
await conversationService.stopSessionAndWait(sessionId)
|
||||
|
||||
if (preview.available && snapshots) {
|
||||
const targetSnapshot = findTargetSnapshot(snapshots, target.targetUserMessageId)
|
||||
|
||||
@ -1197,18 +1197,20 @@ export class SessionService {
|
||||
// expand relative paths — in bundled sidecar mode the server's cwd is
|
||||
// typically '/'. Callers (IM adapters) already send absolute realPath,
|
||||
// but we log here so cwd regressions are caught early.
|
||||
const absWorkDir = path.resolve(resolvedWorkDir)
|
||||
const resolvedPath = path.resolve(resolvedWorkDir)
|
||||
let absWorkDir: string
|
||||
try {
|
||||
absWorkDir = await fs.realpath(resolvedPath)
|
||||
} catch {
|
||||
throw ApiError.badRequest(`Working directory does not exist: ${resolvedPath}`)
|
||||
}
|
||||
console.log(
|
||||
`[SessionService] createSession: requested workDir=${JSON.stringify(
|
||||
workDir,
|
||||
)}, resolved=${absWorkDir} (process.cwd()=${process.cwd()})`,
|
||||
)
|
||||
let stat
|
||||
try {
|
||||
stat = await fs.stat(absWorkDir)
|
||||
} catch {
|
||||
throw ApiError.badRequest(`Working directory does not exist: ${absWorkDir}`)
|
||||
}
|
||||
stat = await fs.stat(absWorkDir)
|
||||
if (!stat.isDirectory()) {
|
||||
throw ApiError.badRequest(`Working directory is not a directory: ${absWorkDir}`)
|
||||
}
|
||||
@ -1378,7 +1380,8 @@ export class SessionService {
|
||||
async clearSessionTranscript(sessionId: string, fallbackWorkDir?: string): Promise<void> {
|
||||
let found = await this.findSessionFile(sessionId)
|
||||
if (!found && fallbackWorkDir) {
|
||||
const absWorkDir = path.resolve(fallbackWorkDir)
|
||||
const resolvedPath = path.resolve(fallbackWorkDir)
|
||||
const absWorkDir = await fs.realpath(resolvedPath).catch(() => resolvedPath)
|
||||
const dirPath = path.join(this.getProjectsDir(), this.sanitizePath(absWorkDir))
|
||||
await fs.mkdir(dirPath, { recursive: true })
|
||||
found = {
|
||||
@ -1462,6 +1465,11 @@ export class SessionService {
|
||||
const removedMessageIds = activeMessages
|
||||
.slice(startIndex)
|
||||
.map((message) => message.id)
|
||||
const remainingMessageIds = new Set(
|
||||
activeMessages
|
||||
.slice(0, startIndex)
|
||||
.map((message) => message.id),
|
||||
)
|
||||
|
||||
if (removedMessageIds.length === 0) {
|
||||
return { removedCount: 0, removedMessageIds: [] }
|
||||
@ -1469,7 +1477,17 @@ export class SessionService {
|
||||
|
||||
const removedIds = new Set(removedMessageIds)
|
||||
const filteredEntries = entries.filter(
|
||||
(entry) => !(typeof entry.uuid === 'string' && removedIds.has(entry.uuid)),
|
||||
(entry) => {
|
||||
if (typeof entry.uuid !== 'string') return true
|
||||
if (removedIds.has(entry.uuid)) return false
|
||||
if (
|
||||
entry.message?.role &&
|
||||
(entry.type === 'user' || entry.type === 'assistant' || entry.type === 'system')
|
||||
) {
|
||||
return remainingMessageIds.has(entry.uuid)
|
||||
}
|
||||
return true
|
||||
},
|
||||
)
|
||||
|
||||
const content =
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user