mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
fix: preserve Windows drive-root project identity
Normalize realpath results for Windows drive roots so D:\ does not round-trip to drive-relative D: and resolve back into the current repository. Also tighten Sidebar hidden-project matching so a drive root does not match every child project on the same drive. Tested: bun test src/server/__tests__/windows-drive-path.test.ts src/server/__tests__/sessions.test.ts src/server/__tests__/workspace-service.test.ts Tested: cd desktop && bun run test -- --run src/components/layout/Sidebar.test.tsx Tested: cd desktop && bun run build:windows-x64 Tested: bun run check:desktop Not-tested: bun run check:server (failed on existing/environment failures including the pre-existing untracked FileReadTool test) Confidence: high Scope-risk: narrow
This commit is contained in:
parent
9746a6893b
commit
c373f8cf45
@ -799,6 +799,33 @@ describe('Sidebar', () => {
|
||||
expect(screen.getByRole('button', { name: /Drive Project Session/ })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not restore a hidden Windows drive root when creating a child project session', async () => {
|
||||
window.localStorage.setItem(PROJECT_HIDDEN_STORAGE_KEY, JSON.stringify(['D:\\']))
|
||||
createSession.mockResolvedValue('child-new')
|
||||
const now = new Date().toISOString()
|
||||
useSessionStore.setState({
|
||||
sessions: [
|
||||
makeSession('child-1', 'Child Session', 'D:\\workspace\\code\\cc-haha', now),
|
||||
],
|
||||
})
|
||||
useTabStore.setState({
|
||||
tabs: [{ sessionId: 'child-1', title: 'Child Session', type: 'session', status: 'idle' }],
|
||||
activeTabId: 'child-1',
|
||||
})
|
||||
|
||||
render(<Sidebar />)
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'New Session' }))
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createSession).toHaveBeenCalledWith('D:\\workspace\\code\\cc-haha')
|
||||
})
|
||||
expect(JSON.parse(window.localStorage.getItem(PROJECT_HIDDEN_STORAGE_KEY) ?? '[]')).toEqual(['D:\\'])
|
||||
expect(desktopUiPreferencesApiMock.updateSidebarPreferences).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('right-aligns running status, worktree marker, and update time on session rows', () => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(new Date('2026-05-19T12:00:00.000Z'))
|
||||
|
||||
@ -1657,17 +1657,21 @@ function normalizeProjectKeyList(values: unknown): string[] {
|
||||
}
|
||||
|
||||
function normalizeProjectPathForComparison(value: string): string {
|
||||
const normalized = value.replace(/\\/g, '/').replace(/\/+$/g, '')
|
||||
return normalized || value
|
||||
const normalized = value.replace(/\\/g, '/').replace(/\/+$/g, '') || value
|
||||
return isWindows ? normalized.toLowerCase() : normalized
|
||||
}
|
||||
|
||||
function isDriveRootComparisonPath(value: string): boolean {
|
||||
return /^[a-z]:$/i.test(value)
|
||||
}
|
||||
|
||||
function projectPathMatches(projectKey: string, workDir: string): boolean {
|
||||
const normalizedProjectKey = normalizeProjectPathForComparison(projectKey)
|
||||
const normalizedWorkDir = normalizeProjectPathForComparison(workDir)
|
||||
|
||||
return normalizedProjectKey === normalizedWorkDir
|
||||
|| normalizedWorkDir.startsWith(`${normalizedProjectKey}/`)
|
||||
|| normalizedProjectKey.startsWith(`${normalizedWorkDir}/`)
|
||||
if (normalizedProjectKey === normalizedWorkDir) return true
|
||||
if (isDriveRootComparisonPath(normalizedProjectKey)) return false
|
||||
return normalizedWorkDir.startsWith(`${normalizedProjectKey}/`)
|
||||
}
|
||||
|
||||
function hasSidebarProjectPreferences(preferences: SidebarProjectPreferences): boolean {
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
import { describe, expect, it } from 'bun:test'
|
||||
import * as fs from 'node:fs/promises'
|
||||
import * as os from 'node:os'
|
||||
import * as path from 'node:path'
|
||||
import {
|
||||
isSameOrInsidePathForPlatform,
|
||||
normalizeDriveRootPathForPlatform,
|
||||
} from '../services/windowsDrivePath.js'
|
||||
import { getRepositoryContext } from '../services/repositoryLaunchService.js'
|
||||
import { SessionService } from '../services/sessionService.js'
|
||||
import { sanitizePath } from '../../utils/sessionStoragePortable.js'
|
||||
|
||||
describe('Windows drive root path handling', () => {
|
||||
it('normalizes drive-relative root inputs to absolute drive roots on Windows', () => {
|
||||
@ -27,4 +32,46 @@ describe('Windows drive root path handling', () => {
|
||||
expect(isSameOrInsidePathForPlatform('D:\\project-extra', 'D:\\project', 'win32')).toBe(false)
|
||||
expect(isSameOrInsidePathForPlatform('E:\\child', 'D:\\', 'win32')).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps realpathed drive roots from resolving to the current drive directory', async () => {
|
||||
if (process.platform !== 'win32') return
|
||||
|
||||
const originalConfigDir = process.env.CLAUDE_CONFIG_DIR
|
||||
const configDir = await fs.mkdtemp(path.join(os.tmpdir(), 'drive-root-session-'))
|
||||
const driveRoot = path.parse(process.cwd()).root
|
||||
const sessionId = crypto.randomUUID()
|
||||
|
||||
try {
|
||||
process.env.CLAUDE_CONFIG_DIR = configDir
|
||||
const projectDir = path.join(configDir, 'projects', sanitizePath(driveRoot))
|
||||
await fs.mkdir(projectDir, { recursive: true })
|
||||
await fs.writeFile(
|
||||
path.join(projectDir, `${sessionId}.jsonl`),
|
||||
JSON.stringify({
|
||||
type: 'session-meta',
|
||||
isMeta: true,
|
||||
workDir: driveRoot,
|
||||
timestamp: new Date().toISOString(),
|
||||
}) + '\n',
|
||||
'utf-8',
|
||||
)
|
||||
|
||||
const service = new SessionService()
|
||||
const { sessions } = await service.listSessions({ limit: 5 })
|
||||
const session = sessions.find((item) => item.id === sessionId)
|
||||
expect(session?.workDir).toBe(driveRoot)
|
||||
expect(session?.projectRoot).toBe(driveRoot)
|
||||
|
||||
const context = await getRepositoryContext(driveRoot)
|
||||
expect(context.workDir).toBe(driveRoot)
|
||||
expect(context.repoRoot).not.toBe(process.cwd())
|
||||
} finally {
|
||||
if (originalConfigDir === undefined) {
|
||||
delete process.env.CLAUDE_CONFIG_DIR
|
||||
} else {
|
||||
process.env.CLAUDE_CONFIG_DIR = originalConfigDir
|
||||
}
|
||||
await fs.rm(configDir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@ -172,7 +172,7 @@ async function resolveDirectory(workDir: string): Promise<string> {
|
||||
const resolved = path.resolve(normalizeDriveRootPathForPlatform(workDir))
|
||||
let realPath: string
|
||||
try {
|
||||
realPath = await fs.realpath(resolved)
|
||||
realPath = normalizeDriveRootPathForPlatform(await fs.realpath(resolved))
|
||||
} catch {
|
||||
throw repositoryBadRequest(
|
||||
REPOSITORY_ERROR.workdirMissing,
|
||||
|
||||
@ -366,7 +366,7 @@ export class SessionService {
|
||||
|
||||
private async canonicalizeProjectPath(projectPath: string): Promise<string> {
|
||||
try {
|
||||
return (await fs.realpath(projectPath)).normalize('NFC')
|
||||
return normalizeDriveRootPathForPlatform(await fs.realpath(projectPath)).normalize('NFC')
|
||||
} catch {
|
||||
return projectPath.normalize('NFC')
|
||||
}
|
||||
|
||||
@ -1030,7 +1030,7 @@ export class WorkspaceService {
|
||||
return {
|
||||
kind: 'ok',
|
||||
workspaceRoot: workDir,
|
||||
canonicalWorkspaceRoot: await fs.realpath(workDir),
|
||||
canonicalWorkspaceRoot: normalizeDriveRootPathForPlatform(await fs.realpath(workDir)),
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user