mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-20 13:53:32 +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 () => {
|
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 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`, {
|
const createRes = await fetch(`${baseUrl}/api/sessions`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@ -1042,7 +1043,7 @@ describe('WebSocket Chat Integration', () => {
|
|||||||
})
|
})
|
||||||
expect(error?.message).toContain('Desktop service diagnostics:')
|
expect(error?.message).toContain('Desktop service diagnostics:')
|
||||||
expect(error?.message).toContain(`sessionId: ${sessionId}`)
|
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('runtimeOverride: (none)')
|
||||||
expect(error?.message).toContain('activeProviderId:')
|
expect(error?.message).toContain('activeProviderId:')
|
||||||
expect(error?.message).toContain('configuredProviders:')
|
expect(error?.message).toContain('configuredProviders:')
|
||||||
|
|||||||
@ -849,7 +849,8 @@ describe('SessionService', () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Verify the file was created
|
// 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 filePath = path.join(tmpDir, 'projects', sanitized, `${sessionId}.jsonl`)
|
||||||
const stat = await fs.stat(filePath)
|
const stat = await fs.stat(filePath)
|
||||||
expect(stat.isFile()).toBe(true)
|
expect(stat.isFile()).toBe(true)
|
||||||
@ -981,11 +982,12 @@ describe('SessionService', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should detect placeholder launch info for desktop-created sessions', async () => {
|
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)
|
const launchInfo = await service.getSessionLaunchInfo(sessionId)
|
||||||
expect(launchInfo).not.toBeNull()
|
expect(launchInfo).not.toBeNull()
|
||||||
expect(launchInfo!.workDir).toBe(os.tmpdir())
|
expect(launchInfo!.workDir).toBe(workDir)
|
||||||
expect(launchInfo!.transcriptMessageCount).toBe(0)
|
expect(launchInfo!.transcriptMessageCount).toBe(0)
|
||||||
expect(launchInfo!.customTitle).toBeNull()
|
expect(launchInfo!.customTitle).toBeNull()
|
||||||
})
|
})
|
||||||
@ -1252,7 +1254,7 @@ describe('Sessions API', () => {
|
|||||||
isGitRepo: boolean
|
isGitRepo: boolean
|
||||||
}
|
}
|
||||||
expect(statusBody.state).toBe('ok')
|
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.isGitRepo).toBe(true)
|
||||||
expect(statusBody.changedFiles).toEqual(
|
expect(statusBody.changedFiles).toEqual(
|
||||||
expect.arrayContaining([
|
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 () => {
|
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 sessionId = 'aaaaaaaa-bbbb-cccc-dddd-ffffffffffff'
|
||||||
const firstUserId = crypto.randomUUID()
|
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()) {
|
if (!fs.existsSync(workDir) || !fs.statSync(workDir).isDirectory()) {
|
||||||
throw new ConversationStartupError(
|
throw new ConversationStartupError(
|
||||||
`Working directory does not exist or is not a directory: ${workDir}`,
|
`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(
|
const args = this.buildSessionCliArgs(
|
||||||
sessionId,
|
sessionId,
|
||||||
sdkUrl,
|
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 {
|
markSessionDeleted(sessionId: string): void {
|
||||||
this.deletedSessions.add(sessionId)
|
this.deletedSessions.add(sessionId)
|
||||||
this.stopSession(sessionId)
|
this.stopSession(sessionId)
|
||||||
|
|||||||
@ -1096,9 +1096,7 @@ export async function executeSessionRewind(
|
|||||||
target.targetUserMessageId,
|
target.targetUserMessageId,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (conversationService.hasSession(sessionId)) {
|
await conversationService.stopSessionAndWait(sessionId)
|
||||||
conversationService.stopSession(sessionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preview.available && snapshots) {
|
if (preview.available && snapshots) {
|
||||||
const targetSnapshot = findTargetSnapshot(snapshots, target.targetUserMessageId)
|
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
|
// expand relative paths — in bundled sidecar mode the server's cwd is
|
||||||
// typically '/'. Callers (IM adapters) already send absolute realPath,
|
// typically '/'. Callers (IM adapters) already send absolute realPath,
|
||||||
// but we log here so cwd regressions are caught early.
|
// 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(
|
console.log(
|
||||||
`[SessionService] createSession: requested workDir=${JSON.stringify(
|
`[SessionService] createSession: requested workDir=${JSON.stringify(
|
||||||
workDir,
|
workDir,
|
||||||
)}, resolved=${absWorkDir} (process.cwd()=${process.cwd()})`,
|
)}, resolved=${absWorkDir} (process.cwd()=${process.cwd()})`,
|
||||||
)
|
)
|
||||||
let stat
|
let stat
|
||||||
try {
|
stat = await fs.stat(absWorkDir)
|
||||||
stat = await fs.stat(absWorkDir)
|
|
||||||
} catch {
|
|
||||||
throw ApiError.badRequest(`Working directory does not exist: ${absWorkDir}`)
|
|
||||||
}
|
|
||||||
if (!stat.isDirectory()) {
|
if (!stat.isDirectory()) {
|
||||||
throw ApiError.badRequest(`Working directory is not a directory: ${absWorkDir}`)
|
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> {
|
async clearSessionTranscript(sessionId: string, fallbackWorkDir?: string): Promise<void> {
|
||||||
let found = await this.findSessionFile(sessionId)
|
let found = await this.findSessionFile(sessionId)
|
||||||
if (!found && fallbackWorkDir) {
|
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))
|
const dirPath = path.join(this.getProjectsDir(), this.sanitizePath(absWorkDir))
|
||||||
await fs.mkdir(dirPath, { recursive: true })
|
await fs.mkdir(dirPath, { recursive: true })
|
||||||
found = {
|
found = {
|
||||||
@ -1462,6 +1465,11 @@ export class SessionService {
|
|||||||
const removedMessageIds = activeMessages
|
const removedMessageIds = activeMessages
|
||||||
.slice(startIndex)
|
.slice(startIndex)
|
||||||
.map((message) => message.id)
|
.map((message) => message.id)
|
||||||
|
const remainingMessageIds = new Set(
|
||||||
|
activeMessages
|
||||||
|
.slice(0, startIndex)
|
||||||
|
.map((message) => message.id),
|
||||||
|
)
|
||||||
|
|
||||||
if (removedMessageIds.length === 0) {
|
if (removedMessageIds.length === 0) {
|
||||||
return { removedCount: 0, removedMessageIds: [] }
|
return { removedCount: 0, removedMessageIds: [] }
|
||||||
@ -1469,7 +1477,17 @@ export class SessionService {
|
|||||||
|
|
||||||
const removedIds = new Set(removedMessageIds)
|
const removedIds = new Set(removedMessageIds)
|
||||||
const filteredEntries = entries.filter(
|
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 =
|
const content =
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user