Files
orca/src/shared/project-host-setup-projection.test.ts
T
Yunqian Fanandfanyunqian.1 791577861b fix(project-host-setup): carry identity across hosts (#9413)
Allow setup when the selected project exists only on another host by carrying its validated provider identity with the request instead of reverse-parsing project IDs. Preserve host-qualified provider identity and reject mismatched payloads before linking.

Make linking atomic for local and runtime imports, including clone setup: roll back only newly registered repos and invalidate the same caches as canonical removal. Cover local, runtime, host-qualified identity, mismatch, clone rollback, and renderer routing paths.

Co-authored-by: fanyunqian.1 <fanyunqian.1@bytedance.com>
2026-07-29 18:57:50 -07:00

562 lines
17 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
projectHostSetupProjectionFromRepos,
getProjectHostSetupsForProject,
getProjectHostSetupWorktreeMeta,
isGitHubBackedRepo,
getProjectIdForProviderIdentity,
isProjectRemoteIdentityPending
} from './project-host-setup-projection'
import type { Repo } from './types'
function repo(overrides: Partial<Repo> & Pick<Repo, 'id' | 'path' | 'displayName'>): Repo {
return {
badgeColor: '#737373',
addedAt: 100,
kind: 'git',
...overrides
}
}
describe('project host setup projection', () => {
it('projects a legacy local repo into one project and one ready local setup', () => {
const projection = projectHostSetupProjectionFromRepos(
[repo({ id: 'repo-1', path: '/Users/alice/orca', displayName: 'orca' })],
500
)
expect(projection.projects).toEqual([
{
id: 'repo:repo-1',
displayName: 'orca',
badgeColor: '#737373',
kind: 'git',
sourceRepoIds: ['repo-1'],
createdAt: 100,
updatedAt: 100
}
])
expect(projection.setups).toEqual([
{
id: 'repo-1',
projectId: 'repo:repo-1',
hostId: 'local',
repoId: 'repo-1',
path: '/Users/alice/orca',
displayName: 'orca',
kind: 'git',
setupState: 'ready',
setupMethod: 'legacy-repo',
createdAt: 100,
updatedAt: 100
}
])
})
it('preserves host-local setup fields on SSH repos', () => {
const projection = projectHostSetupProjectionFromRepos([
repo({
id: 'remote-repo',
path: '/home/alice/orca',
displayName: 'orca',
connectionId: 'openclaw 2',
worktreeBasePath: '../worktrees',
gitUsername: 'alice'
})
])
expect(projection.setups[0]).toMatchObject({
id: 'remote-repo',
hostId: 'ssh:openclaw%202',
connectionId: 'openclaw 2',
worktreeBasePath: '../worktrees',
gitUsername: 'alice'
})
})
it('preserves repo-backed setup method metadata', () => {
const projection = projectHostSetupProjectionFromRepos([
repo({
id: 'repo-1',
path: '/Users/alice/orca',
displayName: 'orca',
projectHostSetupMethod: 'cloned'
})
])
expect(projection.setups[0]?.setupMethod).toBe('cloned')
})
it('groups repo checkouts with the same provider identity under one project', () => {
const projection = projectHostSetupProjectionFromRepos([
repo({
id: 'local-repo',
path: '/Users/alice/orca',
displayName: 'Orca',
upstream: { owner: 'StablyAI', repo: 'Orca' }
}),
repo({
id: 'remote-repo',
path: '/home/alice/orca',
displayName: 'orca',
connectionId: 'gpu-vm',
upstream: { owner: 'stablyai', repo: 'orca' }
})
])
expect(projection.projects).toHaveLength(1)
expect(projection.projects[0]).toMatchObject({
id: 'github:stablyai/orca',
sourceRepoIds: ['local-repo', 'remote-repo'],
providerIdentity: { provider: 'github', owner: 'StablyAI', repo: 'Orca' }
})
expect(getProjectHostSetupsForProject(projection.setups, 'github:stablyai/orca')).toHaveLength(
2
)
})
it('keeps same-named github.com and GHES repositories in separate projects', () => {
const projection = projectHostSetupProjectionFromRepos([
repo({
id: 'dotcom-repo',
path: '/work/dotcom',
displayName: 'widgets',
upstream: { owner: 'acme', repo: 'widgets', host: 'github.com' }
}),
repo({
id: 'enterprise-repo',
path: '/work/enterprise',
displayName: 'widgets',
upstream: { owner: 'acme', repo: 'widgets', host: 'github.acme.test' }
})
])
expect(projection.projects).toHaveLength(2)
expect(projection.projects.map((project) => project.id)).toEqual([
'github:acme/widgets',
'github:github.acme.test/acme/widgets'
])
expect(projection.projects[1]?.providerIdentity).toEqual({
provider: 'github',
owner: 'acme',
repo: 'widgets',
host: 'github.acme.test'
})
})
it('groups GHES icon identities only when their hosts match', () => {
const enterpriseIcon = (host: string) => ({
type: 'image' as const,
src: `https://${host}/acme.png?size=64`,
source: 'github' as const,
label: 'acme/widgets'
})
const projection = projectHostSetupProjectionFromRepos([
repo({
id: 'enterprise-local',
path: '/work/local',
displayName: 'widgets',
repoIcon: enterpriseIcon('github.acme.test')
}),
repo({
id: 'enterprise-ssh',
path: '/work/ssh',
displayName: 'widgets',
connectionId: 'builder',
repoIcon: enterpriseIcon('github.acme.test')
}),
repo({
id: 'other-enterprise',
path: '/work/other',
displayName: 'widgets',
repoIcon: enterpriseIcon('github.other.test')
})
])
expect(projection.projects).toHaveLength(2)
expect(projection.projects[0]).toMatchObject({
id: 'github:github.acme.test/acme/widgets',
sourceRepoIds: ['enterprise-local', 'enterprise-ssh']
})
expect(projection.projects[1]?.id).toBe('github:github.other.test/acme/widgets')
})
it('uses GitHub repo icon metadata as a provider identity fallback', () => {
const projection = projectHostSetupProjectionFromRepos([
repo({
id: 'local-repo',
path: '/Users/alice/orca',
displayName: 'Orca',
repoIcon: {
type: 'image',
src: 'https://github.com/stablyai.png?size=64',
source: 'github',
label: 'stablyai/orca'
}
}),
repo({
id: 'remote-repo',
path: '/home/alice/orca',
displayName: 'orca',
connectionId: 'gpu-vm',
repoIcon: {
type: 'image',
src: 'https://github.com/stablyai.png?size=64',
source: 'github',
label: 'StablyAI/Orca'
}
})
])
expect(projection.projects).toHaveLength(1)
expect(projection.projects[0]).toMatchObject({
id: 'github:stablyai/orca',
sourceRepoIds: ['local-repo', 'remote-repo'],
providerIdentity: { provider: 'github', owner: 'stablyai', repo: 'orca' }
})
expect(getProjectHostSetupsForProject(projection.setups, 'github:stablyai/orca')).toHaveLength(
2
)
})
it('uses git remote identity as a provider identity fallback', () => {
const projection = projectHostSetupProjectionFromRepos([
repo({
id: 'canonical-local-repo',
path: '/Users/alice/stably/orca',
displayName: 'orca',
gitRemoteIdentity: {
canonicalKey: 'github.com/stablyai/orca',
remoteName: 'origin',
remoteUrl: 'git@github.com:stablyai/orca.git'
}
}),
repo({
id: 'old-branch-checkout',
path: '/Users/alice/orca/workspaces/orca/re-enable-webgl-for-remote-runtime-terminals',
displayName: 're-enable-webgl-for-remote-runtime-terminals',
repoIcon: {
type: 'image',
src: 'https://github.com/stablyai.png?size=64',
source: 'github',
label: 'stablyai/orca'
}
})
])
expect(projection.projects).toHaveLength(1)
expect(projection.projects[0]).toMatchObject({
id: 'github:stablyai/orca',
displayName: 'orca',
sourceRepoIds: ['canonical-local-repo', 'old-branch-checkout'],
providerIdentity: { provider: 'github', owner: 'stablyai', repo: 'orca' }
})
})
it('uses a GHES canonical remote identity when the remote URL is unavailable', () => {
const projection = projectHostSetupProjectionFromRepos([
repo({
id: 'enterprise-repo',
path: '/work/widgets',
displayName: 'widgets',
gitRemoteIdentity: {
canonicalKey: 'github.acme.test/acme/widgets',
remoteName: 'origin',
remoteUrl: 'not-a-remote'
}
})
])
expect(projection.projects[0]).toMatchObject({
id: 'github:github.acme.test/acme/widgets',
providerIdentity: {
provider: 'github',
owner: 'acme',
repo: 'widgets',
host: 'github.acme.test'
}
})
})
it('preserves GHES HTTP ports but drops SSH transport ports from remote identity', () => {
const projection = projectHostSetupProjectionFromRepos([
repo({
id: 'https-repo',
path: '/work/https',
displayName: 'widgets',
gitRemoteIdentity: {
canonicalKey: 'github.acme.test/acme/widgets',
remoteName: 'origin',
remoteUrl: 'https://github.acme.test:8443/acme/widgets.git'
}
}),
repo({
id: 'ssh-repo',
path: '/work/ssh',
displayName: 'widgets',
gitRemoteIdentity: {
canonicalKey: 'github.acme.test/acme/widgets',
remoteName: 'origin',
remoteUrl: 'ssh://git@github.acme.test:2222/acme/widgets.git'
}
})
])
expect(projection.projects.map((project) => project.providerIdentity?.host)).toEqual([
'github.acme.test:8443',
'github.acme.test'
])
})
it('does not guess that same-named folders are the same project without identity', () => {
const projection = projectHostSetupProjectionFromRepos([
repo({ id: 'local-repo', path: '/Users/alice/app', displayName: 'app' }),
repo({
id: 'remote-repo',
path: '/srv/app',
displayName: 'app',
connectionId: 'work-server'
})
])
expect(projection.projects.map((project) => project.id)).toEqual([
'repo:local-repo',
'repo:remote-repo'
])
})
it('groups same-project records across local, SSH, and runtime hosts by git remote identity', () => {
const projection = projectHostSetupProjectionFromRepos([
repo({
id: 'local-sample-app',
path: '/Users/alice/work/sample-app',
displayName: 'sample-app',
gitRemoteIdentity: {
canonicalKey: 'git.company.test/team/sample-app',
remoteName: 'origin',
remoteUrl: 'git@git.company.test:team/sample-app.git'
}
}),
repo({
id: 'ssh-sample-app',
path: '/home/alice/src/sample-app',
displayName: 'sample-app',
connectionId: 'build server',
gitRemoteIdentity: {
canonicalKey: 'git.company.test/team/sample-app',
remoteName: 'origin',
remoteUrl: 'https://git.company.test/team/sample-app.git'
}
}),
repo({
id: 'runtime-sample-app',
path: '/workspace/sample-app',
displayName: 'sample-app',
executionHostId: 'runtime:dev-container',
gitRemoteIdentity: {
canonicalKey: 'git.company.test/team/sample-app',
remoteName: 'origin',
remoteUrl: 'ssh://git@git.company.test/team/sample-app.git'
}
})
])
expect(projection.projects).toHaveLength(1)
expect(projection.projects[0]).toMatchObject({
id: 'git:git.company.test/team/sample-app',
displayName: 'sample-app',
sourceRepoIds: ['local-sample-app', 'ssh-sample-app', 'runtime-sample-app'],
gitRemoteIdentity: {
canonicalKey: 'git.company.test/team/sample-app',
remoteName: 'origin'
}
})
expect(projection.setups.map((setup) => setup.hostId)).toEqual([
'local',
'ssh:build%20server',
'runtime:dev-container'
])
expect(
getProjectHostSetupsForProject(projection.setups, 'git:git.company.test/team/sample-app')
).toHaveLength(3)
})
it('keeps same-named cross-host records separate when there is no shared repo identity', () => {
const projection = projectHostSetupProjectionFromRepos([
repo({
id: 'local-sample-app',
path: '/Users/alice/work/sample-app',
displayName: 'sample-app'
}),
repo({
id: 'ssh-sample-app',
path: '/srv/unrelated/sample-app',
displayName: 'sample-app',
connectionId: 'staging server'
}),
repo({
id: 'runtime-sample-app',
path: '/workspace/sample-app',
displayName: 'sample-app',
executionHostId: 'runtime:preview'
})
])
// Why: display names are labels, not identity. A future fix needs a
// normalized git remote identity or an explicit user link before merging.
expect(projection.projects).toHaveLength(3)
})
it('does not collapse case-distinct remote paths for self-hosted git remotes', () => {
const projection = projectHostSetupProjectionFromRepos([
repo({
id: 'uppercase-repo',
path: '/Users/alice/work/sample-app',
displayName: 'sample-app',
gitRemoteIdentity: {
canonicalKey: 'git.company.test/Team/Sample-App',
remoteName: 'origin',
remoteUrl: 'git@git.company.test:Team/Sample-App.git'
}
}),
repo({
id: 'lowercase-repo',
path: '/home/alice/src/sample-app',
displayName: 'sample-app',
connectionId: 'build server',
gitRemoteIdentity: {
canonicalKey: 'git.company.test/team/sample-app',
remoteName: 'origin',
remoteUrl: 'git@git.company.test:team/sample-app.git'
}
})
])
expect(projection.projects.map((project) => project.id)).toEqual([
'git:git.company.test/Team/Sample-App',
'git:git.company.test/team/sample-app'
])
})
it('ignores malformed provider identity values', () => {
const projection = projectHostSetupProjectionFromRepos([
repo({
id: 'repo-1',
path: '/Users/alice/orca',
displayName: 'orca',
upstream: { owner: 'stablyai', repo: 42 } as never
})
])
expect(projection.projects[0]?.id).toBe('repo:repo-1')
expect(projection.projects[0]?.providerIdentity).toBeUndefined()
})
it('derives workspace ownership metadata from the repo setup', () => {
const targetRepo = repo({
id: 'remote-repo',
path: '/home/alice/orca',
displayName: 'orca',
connectionId: 'openclaw 2',
upstream: { owner: 'stablyai', repo: 'orca' }
})
const projection = projectHostSetupProjectionFromRepos([targetRepo])
expect(getProjectHostSetupWorktreeMeta(projection.setups, targetRepo)).toEqual({
projectId: 'github:stablyai/orca',
hostId: 'ssh:openclaw%202',
projectHostSetupId: 'remote-repo'
})
})
})
describe('isGitHubBackedRepo', () => {
it('is true when an explicit upstream owner/repo is present', () => {
const target = repo({
id: 'r',
path: '/r',
displayName: 'r',
upstream: { owner: 'stablyai', repo: 'orca' }
})
expect(isGitHubBackedRepo(target)).toBe(true)
})
it('is true when a GitHub-sourced avatar icon encodes the slug', () => {
const target = repo({
id: 'r',
path: '/r',
displayName: 'r',
repoIcon: {
type: 'image',
src: 'https://github.com/stablyai.png?size=64',
source: 'github',
label: 'stablyai/orca'
}
})
expect(isGitHubBackedRepo(target)).toBe(true)
})
it('is false for a non-GitHub icon and no upstream (GitLab/folder)', () => {
const target = repo({
id: 'r',
path: '/r',
displayName: 'r',
repoIcon: { type: 'lucide', name: 'gitlab' }
})
expect(isGitHubBackedRepo(target)).toBe(false)
})
it('is false for a plain local repo with no provider signal', () => {
expect(isGitHubBackedRepo(repo({ id: 'r', path: '/r', displayName: 'r' }))).toBe(false)
})
})
describe('getProjectIdForProviderIdentity', () => {
it('uses the same normalized identity key as project projection', () => {
expect(
getProjectIdForProviderIdentity({ provider: 'github', owner: 'PyTorch', repo: 'PyTorch' })
).toBe('github:pytorch/pytorch')
expect(
getProjectIdForProviderIdentity({
provider: 'github',
owner: 'Acme',
repo: 'Orca',
host: 'GITHUB.ACME.TEST:8443'
})
).toBe('github:github.acme.test:8443/acme/orca')
})
})
describe('isProjectRemoteIdentityPending', () => {
const base = { id: 'r', path: '/r', displayName: 'r' } as const
it('is true while the background remote probe has not answered', () => {
expect(isProjectRemoteIdentityPending(repo({ ...base }))).toBe(true)
expect(isProjectRemoteIdentityPending(repo({ ...base, connectionId: 'builder' }))).toBe(true)
})
it('is false once the probe settles on no usable remote', () => {
expect(isProjectRemoteIdentityPending(repo({ ...base, gitRemoteIdentity: null }))).toBe(false)
})
it('is false once any provider-neutral identity resolves', () => {
expect(
isProjectRemoteIdentityPending(
repo({
...base,
gitRemoteIdentity: {
canonicalKey: 'gitlab.example.com/team/orca',
remoteName: 'origin',
remoteUrl: 'git@gitlab.example.com:team/orca.git'
}
})
)
).toBe(false)
expect(
isProjectRemoteIdentityPending(
repo({ ...base, upstream: { owner: 'stablyai', repo: 'orca' } })
)
).toBe(false)
})
})