fix: verify Cursor chat store before reopen (#1037)

* test: reproduce issue #841

* test: cover Cursor chat store discovery

* fix: verify Cursor chat store before resume (closes #841)

* test: preserve non-Cursor resume behavior

* test: cover conservative Cursor resume gating

* fix: gate Cursor reopen until store verification

* test: cover legacy Cursor drawer fallback

* fix: scan unique legacy Cursor store drawer

* test: preserve raw Cursor workspace path hashing

* fix: hash raw Cursor workspace path

* test: pin Cursor probe owner and machine

* fix: probe Cursor store on recorded owner

* test: normalize Cursor probe owner home

* fix: normalize Cursor probe owner home
This commit is contained in:
SSU-WEI HUANG
2026-07-16 12:34:41 +08:00
committed by GitHub
parent adb6f41858
commit 520c3f511a
27 changed files with 955 additions and 34 deletions
+77 -1
View File
@@ -1,11 +1,12 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { mkdtempSync, rmSync, mkdirSync, realpathSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { homedir, tmpdir } from 'node:os'
import { join } from 'node:path'
const ioMock = vi.hoisted(() => vi.fn())
const listOpencodeModelsForCwdMock = vi.hoisted(() => vi.fn())
const listGrokModelsForCwdMock = vi.hoisted(() => vi.fn())
const inspectCursorChatStoreMock = vi.hoisted(() => vi.fn())
vi.mock('socket.io-client', () => ({
io: ioMock
@@ -23,6 +24,10 @@ vi.mock('../modules/common/grokModels', () => ({
listGrokModelsForCwd: listGrokModelsForCwdMock
}))
vi.mock('@/cursor/cursorChatStoreStatus', () => ({
inspectCursorChatStore: inspectCursorChatStoreMock
}))
import { ApiMachineClient, normalizeWindowsDriveRoot } from './apiMachine'
import type { Machine } from './types'
@@ -74,6 +79,77 @@ async function callListGrokModels(client: ApiMachineClient, machineId: string, c
return JSON.parse(raw) as unknown
}
async function callCursorChatStoreStatus(
client: ApiMachineClient,
machineId: string,
params: { workspacePath: string; cursorSessionId: string; homeDir?: string }
): Promise<unknown> {
const manager = (client as unknown as { rpcHandlerManager: { handleRequest: (req: { method: string; params: string }) => Promise<string> } }).rpcHandlerManager
const raw = await manager.handleRequest({
method: `${machineId}:cursor-chat-store-status`,
params: JSON.stringify(params)
})
return JSON.parse(raw) as unknown
}
describe('ApiMachineClient cursor-chat-store-status handler', () => {
beforeEach(() => {
inspectCursorChatStoreMock.mockReset()
inspectCursorChatStoreMock.mockResolvedValue({ onDisk: false, store: null })
})
it('inspects stores under the recorded session owner home', async () => {
const machine = makeMachine('cursor-store-machine')
const client = new ApiMachineClient('cli-token', machine)
try {
await callCursorChatStoreStatus(client, machine.id, {
workspacePath: '/work/project',
cursorSessionId: 'cursor-session',
homeDir: ' /home/recorded-owner '
})
expect(inspectCursorChatStoreMock).toHaveBeenCalledWith({
home: '/home/recorded-owner',
workspacePath: '/work/project',
cursorSessionId: 'cursor-session'
})
} finally {
client.shutdown()
}
})
it('falls back to the CLI process home for old or whitespace-only homeDir metadata', async () => {
const machine = makeMachine('cursor-store-fallback-machine')
const client = new ApiMachineClient('cli-token', machine)
try {
await callCursorChatStoreStatus(client, machine.id, {
workspacePath: '/work/project',
cursorSessionId: 'cursor-session-old'
})
await callCursorChatStoreStatus(client, machine.id, {
workspacePath: '/work/project',
cursorSessionId: 'cursor-session-empty',
homeDir: ' '
})
expect(inspectCursorChatStoreMock).toHaveBeenNthCalledWith(1, {
home: homedir(),
workspacePath: '/work/project',
cursorSessionId: 'cursor-session-old'
})
expect(inspectCursorChatStoreMock).toHaveBeenNthCalledWith(2, {
home: homedir(),
workspacePath: '/work/project',
cursorSessionId: 'cursor-session-empty'
})
} finally {
client.shutdown()
}
})
})
describe('ApiMachineClient listOpencodeModelsForCwd handler', () => {
let workspaceRoot: string
+21
View File
@@ -31,6 +31,9 @@ import type { SpawnSessionOptions, SpawnSessionResult } from '../modules/common/
import { applyVersionedAck } from './versionedUpdate'
import { buildSocketIoExtraHeaderOptions } from './hubExtraHeaders'
import { collectMachineHealth } from '@/utils/machineHealth'
import { inspectCursorChatStore } from '@/cursor/cursorChatStoreStatus'
import { homedir } from 'node:os'
import type { CursorChatStoreStatus } from '@hapi/protocol/apiTypes'
type MachineRpcHandlers = {
spawnSession: (options: SpawnSessionOptions) => Promise<SpawnSessionResult>
@@ -46,6 +49,12 @@ interface ListMachineDirectoryRequest {
path: string
}
interface CursorChatStoreStatusRequest {
workspacePath: string
cursorSessionId: string
homeDir?: string
}
export function normalizeWindowsDriveRoot(path: string): string {
return /^[A-Za-z]:$/.test(path) ? `${path}\\` : path
}
@@ -128,6 +137,18 @@ export class ApiMachineClient {
return { exists }
})
this.rpcHandlerManager.registerHandler<CursorChatStoreStatusRequest, CursorChatStoreStatus>(
RPC_METHODS.CursorChatStoreStatus,
async (params) => {
const recordedHome = typeof params?.homeDir === 'string' ? params.homeDir.trim() : ''
return await inspectCursorChatStore({
home: recordedHome || homedir(),
workspacePath: typeof params?.workspacePath === 'string' ? params.workspacePath : '',
cursorSessionId: typeof params?.cursorSessionId === 'string' ? params.cursorSessionId : ''
})
}
)
this.rpcHandlerManager.registerHandler<ListMachineDirectoryRequest, MachineListDirectoryResponse>(RPC_METHODS.ListMachineDirectory, async (params) => {
if (!this.normalizedWorkspaceRoots?.length) {
return { success: false, error: 'Workspace browsing is not enabled for this machine' }