refactor(identity): brand host-scoped worktree and repo keys for new code (#29990)

This commit is contained in:
Neil
2026-10-16 23:19:42 -07:00
committed by GitHub
parent 37c314bcc9
commit 2d33cd965f
8 changed files with 109 additions and 12 deletions
@@ -45,7 +45,7 @@ export function useSettingsRepoScrollEffects(
const { neededRepos } = terminal
useEffect(() => {
const repoHostIdentitySet = new Set(repos.map(getRepoHostIdentity))
const repoHostIdentitySet = new Set<string>(repos.map(getRepoHostIdentity))
setRepoHooksMap((previous) => {
const next = Object.fromEntries(
Object.entries(previous).filter(([identity]) => repoHostIdentitySet.has(identity))
@@ -111,7 +111,7 @@ export function useWorkspaceSpaceManagerPanel() {
dismissDeleteWorktreeFailureToast(target)
}
void window.api.workspaceSpace.requestWorktreeRefresh?.()
const deletedIdentities = new Set(
const deletedIdentities = new Set<string>(
deletedTargets.map((target) =>
composeWorktreeHostIdentity(target.executionHostId ?? undefined, target.id)
)
@@ -0,0 +1,14 @@
import { describe, expect, expectTypeOf, it } from 'vitest'
import { getRepoHostIdentity, type HostScopedRepoKey } from './repo-host-identity'
// Why: frozen bytes (D25); a repo's host comes from executionHostId, then connectionId.
describe('getRepoHostIdentity golden bytes', () => {
it('composes the repo key from the repo host', () => {
expect(getRepoHostIdentity({ id: 'r', connectionId: null })).toBe('local\0r')
expect(getRepoHostIdentity({ id: 'r', connectionId: 'box 1' })).toBe('ssh:box%201\0r')
expect(
getRepoHostIdentity({ id: 'r', connectionId: null, executionHostId: 'runtime:env' })
).toBe('runtime:env\0r')
expectTypeOf(getRepoHostIdentity({ id: 'r' })).toEqualTypeOf<HostScopedRepoKey>()
})
})
@@ -5,13 +5,16 @@ import {
getSettingsFocusedExecutionHostId,
type ExecutionHostId
} from '../../../../shared/execution-host'
import { getRepoHostIdentityForParts } from '../../../../shared/repo-host-identity'
import {
getRepoHostIdentityForParts,
type HostScopedRepoKey
} from '../../../../shared/repo-host-identity'
export { getRepoHostIdentityForParts }
export { getRepoHostIdentityForParts, type HostScopedRepoKey }
type RepoIdentityParts = Pick<Repo, 'id' | 'connectionId' | 'executionHostId'>
export function getRepoHostIdentity(repo: RepoIdentityParts): string {
export function getRepoHostIdentity(repo: RepoIdentityParts): HostScopedRepoKey {
return getRepoHostIdentityForParts(repo.id, getRepoExecutionHostId(repo))
}
+31
View File
@@ -0,0 +1,31 @@
import { describe, expect, expectTypeOf, it } from 'vitest'
import { toRuntimeExecutionHostId, toSshExecutionHostId } from './execution-host'
import { getRepoHostIdentityForParts, type HostScopedRepoKey } from './repo-host-identity'
// Why: these keys index persisted and cross-module repo state, so the bytes are frozen (D25).
describe('repo host identity golden bytes', () => {
it.each([
['repo-1', 'local', 'local\0repo-1'],
[
'repo-1',
toSshExecutionHostId('deploy@10.0.0.4:2222'),
'ssh:deploy%4010.0.0.4%3A2222\0repo-1'
],
['repo-1', toRuntimeExecutionHostId('env 1/a'), 'runtime:env%201%2Fa\0repo-1'],
['repo|1::x', 'local', 'local\0repo|1::x'],
['repo-1', '', '\0repo-1']
] as const)('composes %s on %s', (repoId, hostId, expected) => {
expect(getRepoHostIdentityForParts(repoId, hostId)).toBe(expected)
})
it('brands the key so a plain string cannot pose as one', () => {
const key = getRepoHostIdentityForParts('repo-1', 'local')
expectTypeOf(key).toEqualTypeOf<HostScopedRepoKey>()
expectTypeOf<HostScopedRepoKey>().toExtend<string>()
expectTypeOf<string>().not.toExtend<HostScopedRepoKey>()
const raw: string = key
// @ts-expect-error a raw string is not a host-scoped key
const forged: HostScopedRepoKey = 'local\0repo-1'
expect(raw).toBe(forged)
})
})
+8 -2
View File
@@ -1,5 +1,11 @@
export function getRepoHostIdentityForParts(repoId: string, hostId: string): string {
declare const hostScopedRepoKeyBrand: unique symbol
// Why: for new code only; existing raw-id stores stay as they are (D7).
export type HostScopedRepoKey = string & { readonly [hostScopedRepoKeyBrand]: true }
export function getRepoHostIdentityForParts(repoId: string, hostId: string): HostScopedRepoKey {
// Why: host ids and repo ids can contain punctuation; NUL keeps the composite
// key collision-free without escaping user/provider-owned strings.
return `${hostId}\0${repoId}`
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: this is the only constructor of the brand; the bytes are the unbranded composite, frozen by golden tests (D25).
return `${hostId}\0${repoId}` as HostScopedRepoKey
}
@@ -1,10 +1,11 @@
import { describe, expect, it } from 'vitest'
import { describe, expect, expectTypeOf, it } from 'vitest'
import { toRuntimeExecutionHostId, toSshExecutionHostId } from '../execution-host'
import {
composeWorktreeHostIdentity,
getExecutionHostIdFromWorktreeHostIdentity,
getWorktreeHostIdentity,
getWorktreeIdFromHostIdentity
getWorktreeIdFromHostIdentity,
type HostScopedWorktreeKey
} from './host-qualified-identity'
const WORKTREE_ID = 'repo-1::/work/orca'
@@ -85,3 +86,37 @@ describe('worktree host identity', () => {
).toBe(toSshExecutionHostId('build-box'))
})
})
// Why: worktreeIdentityAliases and lastVisitedAtByWorktreeId persist these keys,
// so the bytes are frozen (D25).
describe('worktree host identity golden bytes', () => {
it.each([
['local', 'repo::/p', 'local|repo::/p'],
[undefined, 'repo::/p', '|repo::/p'],
[
toSshExecutionHostId('deploy@10.0.0.4:2222'),
'repo::/p',
'ssh:deploy%4010.0.0.4%3A2222|repo::/p'
],
[toSshExecutionHostId('we|rd'), 'repo::/p', 'ssh:we%7Crd|repo::/p'],
[toRuntimeExecutionHostId('env 1/a'), 'repo::/p', 'runtime:env%201%2Fa|repo::/p'],
['local', 'repo::/work/a|b', 'local|repo::/work/a|b']
] as const)('composes %s + %s as %s', (hostId, worktreeId, expected) => {
const key = composeWorktreeHostIdentity(hostId, worktreeId)
expect(key).toBe(expected)
expect(getWorktreeHostIdentity({ id: worktreeId, hostId })).toBe(expected)
expect(getWorktreeIdFromHostIdentity(key)).toBe(worktreeId)
expect(getExecutionHostIdFromWorktreeHostIdentity(key)).toBe(hostId)
})
it('brands the key so a plain string cannot pose as one', () => {
const key = composeWorktreeHostIdentity('local', WORKTREE_ID)
expectTypeOf(key).toEqualTypeOf<HostScopedWorktreeKey>()
expectTypeOf<HostScopedWorktreeKey>().toExtend<string>()
expectTypeOf<string>().not.toExtend<HostScopedWorktreeKey>()
const raw: string = key
// @ts-expect-error a raw string is not a host-scoped key
const forged: HostScopedWorktreeKey = WORKTREE_ID
expect([raw, forged]).toEqual([key, WORKTREE_ID])
})
})
+11 -3
View File
@@ -17,6 +17,11 @@ import type { Worktree } from './types'
*/
const HOST_SEPARATOR = '|'
declare const hostScopedWorktreeKeyBrand: unique symbol
// Why: for new code only; existing raw-id stores stay as they are (D7).
export type HostScopedWorktreeKey = string & { readonly [hostScopedWorktreeKeyBrand]: true }
/**
* Stable key for one workspace on one host (STA-4343).
*
@@ -27,15 +32,18 @@ const HOST_SEPARATOR = '|'
* An unqualified row gets its own bucket rather than being folded into a host:
* it may well BE one of them, but nothing here can prove which.
*/
export function getWorktreeHostIdentity(worktree: Pick<Worktree, 'id' | 'hostId'>): string {
export function getWorktreeHostIdentity(
worktree: Pick<Worktree, 'id' | 'hostId'>
): HostScopedWorktreeKey {
return composeWorktreeHostIdentity(worktree.hostId, worktree.id)
}
export function composeWorktreeHostIdentity(
hostId: ExecutionHostId | undefined,
worktreeId: string
): string {
return `${hostId ?? ''}${HOST_SEPARATOR}${worktreeId}`
): HostScopedWorktreeKey {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: this is the only constructor of the brand; the bytes are the unbranded composite, frozen by golden tests (D25).
return `${hostId ?? ''}${HOST_SEPARATOR}${worktreeId}` as HostScopedWorktreeKey
}
/**