mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +00:00
* fix(workspaces): add collision-safe worktree identity * fix(workspaces): read worktree metadata per host and repair ambiguous identities The canonical identity store landed write-only: getWorktreeMetaForHost had no production callers while setWorktreeMetaForHost kept the legacy projection only for the first known owner, so a second host's edits persisted and were never read back. Wire the listing paths through host-qualified reads. An ambiguous alias was also unrecoverable — reads returned undefined and writes threw forever, and the throw escaped the detected-worktree loop, emptying the whole repo's sidebar. Fail open onto the most recently active instance instead. - collapse ambiguous aliases deterministically and persist the repair - reclaim identity rows in the metadata GC so they cannot outlive their locator or resurrect onto a worktree recreated at the same path - drop every host's rows when a locator is removed outright, not just the owner's - honour an explicit instanceId so the stale-lineage rotation guard still works - scope a rename to the moving host; other hosts keep their own locator - prefer the project host setup matching the repo's own execution host, so a repoId registered on two hosts no longer stamps the wrong one durably - reject an unencoded `|` in a host id, the invariant the alias delimiter needs - drop the never-populated hostGeneration from the canonical key * fix(workspaces): close remaining identity review gaps * fix(workspaces): close remaining review gaps * fix(workspaces): address review and CI regressions * test(workspaces): update host-qualified metadata expectations * fix(workspaces): preserve ambiguous identity records * fix(workspaces): snapshot metadata during listing * test(workspaces): mirror listing metadata snapshot in windows fixture * fix(workspaces): preserve identity routing for metadata writes * fix(workspaces): scope stale metadata cleanup by host * fix(workspaces): rekey identities on SSH readoption * fix(workspaces): fail closed for ambiguous board ids * perf(workspaces): snapshot metadata across catalog listing * fix(workspaces): retain neighboring manual order updates * test(workspaces): cover ambiguous board id index * fix(persistence): harden host-qualified worktree metadata * refactor(shared): split project host setup lookup * refactor(workspaces): simplify host-qualified metadata
687 lines
22 KiB
TypeScript
687 lines
22 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
projectHostSetupProjectionFromRepos,
|
|
getProjectHostSetupsForProject,
|
|
isGitHubBackedRepo,
|
|
getProjectIdForProviderIdentity,
|
|
isProjectRemoteIdentityPending
|
|
} from './project-host-setup-projection'
|
|
import { getProjectHostSetupWorktreeMeta } from './project-host-setup-lookup'
|
|
import type { Repo } from './repo-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('keeps timestamps stable when addedAt is 0 across different now values', () => {
|
|
const target = repo({
|
|
id: 'repo-1',
|
|
path: '/Users/alice/orca',
|
|
displayName: 'orca',
|
|
addedAt: 0
|
|
})
|
|
const first = projectHostSetupProjectionFromRepos([target], 1_000)
|
|
const second = projectHostSetupProjectionFromRepos([target], 9_999)
|
|
|
|
expect(first.projects[0]?.createdAt).toBe(0)
|
|
expect(first.projects[0]?.updatedAt).toBe(0)
|
|
expect(first.setups[0]?.createdAt).toBe(0)
|
|
expect(second.projects[0]?.createdAt).toBe(first.projects[0]?.createdAt)
|
|
expect(second.projects[0]?.updatedAt).toBe(first.projects[0]?.updatedAt)
|
|
expect(second.setups[0]?.createdAt).toBe(first.setups[0]?.createdAt)
|
|
expect(second.setups[0]?.updatedAt).toBe(first.setups[0]?.updatedAt)
|
|
})
|
|
|
|
// Why both orders: repo order comes from disk/host enumeration, so it must not decide the
|
|
// project's timestamps. The accumulator carries 0 when the *first* sibling is the unknown one.
|
|
const timestampedSibling = repo({
|
|
id: 'local-repo',
|
|
path: '/Users/alice/orca',
|
|
displayName: 'Orca',
|
|
addedAt: 100,
|
|
upstream: { owner: 'StablyAI', repo: 'Orca' }
|
|
})
|
|
const unknownSibling = repo({
|
|
id: 'remote-repo',
|
|
path: '/home/alice/orca',
|
|
displayName: 'orca',
|
|
addedAt: 0,
|
|
connectionId: 'gpu-vm',
|
|
upstream: { owner: 'stablyai', repo: 'orca' }
|
|
})
|
|
|
|
it('does not wipe a persisted createdAt when a sibling repo has addedAt 0', () => {
|
|
const projection = projectHostSetupProjectionFromRepos([timestampedSibling, unknownSibling])
|
|
|
|
expect(projection.projects).toHaveLength(1)
|
|
expect(projection.projects[0]?.createdAt).toBe(100)
|
|
expect(projection.projects[0]?.updatedAt).toBe(100)
|
|
})
|
|
|
|
it('does not wipe a persisted createdAt when the addedAt 0 sibling comes first', () => {
|
|
const projection = projectHostSetupProjectionFromRepos([unknownSibling, timestampedSibling])
|
|
|
|
expect(projection.projects).toHaveLength(1)
|
|
expect(projection.projects[0]?.createdAt).toBe(100)
|
|
expect(projection.projects[0]?.updatedAt).toBe(100)
|
|
})
|
|
|
|
it('keeps per-repo setup timestamps independent of sibling order', () => {
|
|
for (const repos of [
|
|
[timestampedSibling, unknownSibling],
|
|
[unknownSibling, timestampedSibling]
|
|
]) {
|
|
const { setups } = projectHostSetupProjectionFromRepos(repos)
|
|
const timestamped = setups.find((setup) => setup.repoId === 'local-repo')
|
|
const unknown = setups.find((setup) => setup.repoId === 'remote-repo')
|
|
expect(timestamped?.createdAt).toBe(100)
|
|
expect(timestamped?.updatedAt).toBe(100)
|
|
// Setups are per repo and never merged, so the unknown one keeps its own 0 either way.
|
|
expect(unknown?.createdAt).toBe(0)
|
|
expect(unknown?.updatedAt).toBe(0)
|
|
}
|
|
})
|
|
|
|
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)
|
|
})
|
|
})
|
|
|
|
describe('derived project identity stability', () => {
|
|
const base = { id: 'r', path: '/r', displayName: 'r' } as const
|
|
|
|
// Why pinned: this id is the persisted Project primary key, so folding a host alias here would
|
|
// silently re-key existing projects on upgrade and drop their runtime preference.
|
|
it('keeps a www. remote provider-neutral instead of folding it onto github.com', () => {
|
|
const projection = projectHostSetupProjectionFromRepos([
|
|
repo({
|
|
...base,
|
|
gitRemoteIdentity: {
|
|
canonicalKey: 'www.github.com/acme/app',
|
|
remoteName: 'origin',
|
|
remoteUrl: 'https://www.github.com/acme/app.git'
|
|
}
|
|
})
|
|
])
|
|
expect(projection.projects.map((project) => project.id)).toEqual([
|
|
'git:www.github.com/acme/app'
|
|
])
|
|
})
|
|
|
|
it('still folds the documented ssh.github.com alias onto github.com', () => {
|
|
const projection = projectHostSetupProjectionFromRepos([
|
|
repo({
|
|
...base,
|
|
gitRemoteIdentity: {
|
|
canonicalKey: 'ssh.github.com/acme/app',
|
|
remoteName: 'origin',
|
|
remoteUrl: 'ssh://git@ssh.github.com:443/acme/app.git'
|
|
}
|
|
})
|
|
])
|
|
expect(projection.projects.map((project) => project.id)).toEqual(['github:acme/app'])
|
|
})
|
|
})
|
|
|
|
describe('getProjectHostSetupWorktreeMeta host selection', () => {
|
|
it("picks the setup for the repo's own execution host when a repoId spans two hosts", () => {
|
|
const sshRepo = repo({
|
|
id: 'repo-shared',
|
|
path: '/remote/orca',
|
|
displayName: 'orca',
|
|
connectionId: 'build-box'
|
|
})
|
|
// Local first in the array: a repoId-only match would stamp the wrong host durably.
|
|
const setups = [
|
|
...projectHostSetupProjectionFromRepos([
|
|
repo({ id: 'repo-shared', path: '/local/orca', displayName: 'orca' })
|
|
]).setups,
|
|
...projectHostSetupProjectionFromRepos([sshRepo]).setups
|
|
]
|
|
|
|
expect(getProjectHostSetupWorktreeMeta(setups, sshRepo).hostId).toBe('ssh:build-box')
|
|
})
|
|
})
|