fix(pty): preserve byte-exact reservation paths

This commit is contained in:
Brennan Benson
2026-08-01 05:29:50 -07:00
parent 1a001b9f36
commit a9b9dfbe99
2 changed files with 62 additions and 18 deletions
+46 -12
View File
@@ -9,6 +9,20 @@ import {
} from './stable-pane-id'
const LEAF_ID = '11111111-1111-4111-8111-111111111111'
const PANE_KEY = makePaneKey('tab-1', LEAF_ID)
function reservationKey(
workspaceId: string,
scope: { connectionId?: string; executionRuntime?: string } = {}
) {
return makePaneSpawnReservationKey({
paneKey: PANE_KEY,
providerId: 'provider-1',
workspaceId,
sessionId: 'session-1',
...scope
})
}
describe('stable pane ids', () => {
it('recognizes UUID leaf ids as stable pane ids', () => {
@@ -57,30 +71,50 @@ describe('stable pane ids', () => {
expect(parseLegacyNumericPaneKey('tab:1:12')).toBeNull()
})
it('canonicalizes worktree aliases without collapsing independent PTY scopes', () => {
const paneKey = makePaneKey('tab-1', LEAF_ID)
it.each([
['local', { executionRuntime: 'native' }],
['SSH', { connectionId: 'ssh-1' }]
])('keeps NFC and NFD POSIX workspace paths distinct on %s', (_label, scope) => {
expect(reservationKey('repo-1::/workspaces/café', scope)).not.toBe(
reservationKey('repo-1::/workspaces/cafe\u0301', scope)
)
})
it.each([
['drive', 'repo-1::C:\\Work\\Project\\', 'repo-1::c:/work/project'],
['UNC', 'repo-1::\\\\Server\\Share\\Project\\', 'repo-1:://server/share/project'],
[
'WSL UNC',
'repo-1::\\\\wsl.localhost\\Ubuntu\\home\\Project',
'repo-1:://wsl$/ubuntu/home/Project'
]
])('canonicalizes equivalent Windows %s paths', (_label, left, right) => {
expect(reservationKey(left)).toBe(reservationKey(right))
})
it('canonicalizes raw and worktree-scoped aliases', () => {
expect(reservationKey('repo-1::/workspaces/project')).toBe(
reservationKey('worktree:repo-1::/workspaces/project')
)
})
it('does not collapse independent PTY scopes', () => {
const base = {
paneKey,
paneKey: PANE_KEY,
providerId: 'provider-1',
connectionId: 'ssh-1',
executionRuntime: 'wsl:Ubuntu',
workspaceId: 'worktree:repo-1::C:\\Work\\Project\\',
workspaceId: 'worktree:repo-1::/workspaces/project',
sessionId: 'session-1'
}
const canonical = makePaneSpawnReservationKey(base)
expect(canonical).toBe(
makePaneSpawnReservationKey({
...base,
workspaceId: 'repo-1::c:/work/project'
})
)
for (const distinct of [
{ providerId: 'provider-2' },
{ connectionId: 'ssh-2' },
{ executionRuntime: 'native' },
{ workspaceId: 'repo-1::C:\\Work\\Other' },
{ workspaceId: 'folder:repo-1::C:\\Work\\Project' },
{ workspaceId: 'repo-1::/workspaces/other' },
{ workspaceId: 'folder:repo-1::/workspaces/project' },
{ sessionId: 'session-2' }
]) {
expect(makePaneSpawnReservationKey({ ...base, ...distinct })).not.toBe(canonical)
+16 -6
View File
@@ -1,4 +1,4 @@
import { normalizeRuntimePathForComparison } from './cross-platform-path'
import { isWindowsAbsolutePathLike, normalizeRuntimePathSeparators } from './cross-platform-path'
import { parseWorkspaceKey } from './workspace-scope'
import { splitWorktreeId } from './worktree-id'
@@ -37,22 +37,32 @@ export function makePaneKey(tabId: string, stableLeafId: string): PaneKey {
return `${tabId}:${stableLeafId}` as PaneKey
}
function canonicalPaneReservationWorkspacePath(worktreePath: string): string {
// Why: POSIX and SSH paths can be byte-sensitive; fold only syntactically proven Windows paths.
if (!isWindowsAbsolutePathLike(worktreePath)) {
return worktreePath
}
const normalized = normalizeRuntimePathSeparators(worktreePath)
const trimmed = /^[A-Za-z]:\/$/.test(normalized) ? normalized : normalized.replace(/\/+$/, '')
const wslUnc = trimmed.match(/^\/\/(?:wsl\.localhost|wsl\$)\/([^/]+)(\/[\s\S]*)?$/i)
return wslUnc ? `//wsl/${wslUnc[1].toLowerCase()}${wslUnc[2] ?? ''}` : trimmed.toLowerCase()
}
function canonicalWorkspaceIdentity(workspaceId: string): readonly string[] | null {
const trimmed = workspaceId.trim()
if (!trimmed || trimmed.length > 1024) {
if (!workspaceId.trim() || workspaceId.length > 1024) {
return null
}
const scope = parseWorkspaceKey(trimmed)
const scope = parseWorkspaceKey(workspaceId)
const kind = scope?.type ?? 'worktree'
const unscopedId =
scope?.type === 'worktree'
? scope.worktreeId
: scope?.type === 'folder'
? scope.folderWorkspaceId
: trimmed
: workspaceId
const parsed = splitWorktreeId(unscopedId)
return parsed
? [kind, parsed.repoId, normalizeRuntimePathForComparison(parsed.worktreePath)]
? [kind, parsed.repoId, canonicalPaneReservationWorkspacePath(parsed.worktreePath)]
: [kind, unscopedId]
}