mirror of
https://github.com/stablyai/orca.git
synced 2026-11-06 03:57:35 +00:00
refactor(identity): brand host-scoped worktree and repo keys for new code (#29990)
This commit is contained in:
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user