Files
wehub-resource-sync f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

806 lines
25 KiB
Go

package export
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNormalizeRemote(t *testing.T) {
tests := []struct {
name string
raw string
want string
ok bool
}{
{
name: "scp github remote strips git suffix",
raw: "git@github.com:Org/Repo.git",
want: "github.com/Org/Repo",
ok: true,
},
{
name: "https github remote strips git suffix and trailing slash",
raw: "https://github.com/org/repo.git/",
want: "github.com/org/repo",
ok: true,
},
{
name: "ssh URL lowercases host",
raw: "ssh://git@GitHub.com/org/repo",
want: "github.com/org/repo",
ok: true,
},
{
name: "duplicate slashes collapsed",
raw: "https://github.com/org//repo.git",
want: "github.com/org/repo",
ok: true,
},
{
name: "host lowercased path case preserved",
raw: "https://GitHub.com/Org/Repo.git",
want: "github.com/Org/Repo",
ok: true,
},
{
name: "default ssh port omitted",
raw: "ssh://git@example.com:22/acme/app.git",
want: "example.com/acme/app",
ok: true,
},
{
name: "non-default ssh port retained",
raw: "ssh://git@example.com:2222/acme/app.git",
want: "example.com:2222/acme/app",
ok: true,
},
{
name: "non-default https port retained",
raw: "https://example.com:8443/acme/app.git",
want: "example.com:8443/acme/app",
ok: true,
},
{
name: "ipv6 host uses canonical brackets",
raw: "ssh://git@[2001:0db8::1]/acme/app.git",
want: "[2001:db8::1]/acme/app",
ok: true,
},
{
name: "ipv4 host remains unbracketed",
raw: "https://192.0.2.1/acme/app.git",
want: "192.0.2.1/acme/app",
ok: true,
},
{
name: "hostname trailing dot omitted",
raw: "https://GitHub.com./acme/app.git",
want: "github.com/acme/app",
ok: true,
},
{
name: "escaped separators and dot segments normalized",
raw: "https://example.com/acme%2Ftmp%2F..%2Fapp.git",
want: "example.com/acme/app",
ok: true,
},
{
name: "scp query and fragment omitted",
raw: "git@example.com:acme/app.git?token=secret#ref",
want: "example.com/acme/app",
ok: true,
},
{
name: "file remote ignored",
raw: "file:///tmp/repo.git",
ok: false,
},
{
name: "local path remote ignored",
raw: "/srv/git/repo.git",
ok: false,
},
{
name: "windows backslash drive path ignored",
raw: `C:\repo.git`,
ok: false,
},
{
name: "windows slash drive path ignored",
raw: "C:/repo.git",
ok: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, ok := NormalizeGitRemote(tt.raw)
assert.Equal(t, tt.ok, ok)
assert.Equal(t, tt.want, got)
})
}
}
func TestSanitizeGitRemoteForStorageStripsUserInfo(t *testing.T) {
tests := []struct {
name string
raw string
want string
}{
{
name: "https token userinfo",
raw: "https://" + "user:token@" + "Example.com/org/repo.git",
want: "https://Example.com/org/repo.git",
},
{
name: "ssh url userinfo",
raw: "ssh://git@github.com/org/repo.git",
want: "ssh://github.com/org/repo.git",
},
{
name: "scp user prefix",
raw: "git@github.com:Org/Repo.git",
want: "github.com:Org/Repo.git",
},
{
name: "scp token-shaped userinfo",
raw: "user:token@" + "example.com:Org/Repo.git",
want: "example.com:Org/Repo.git",
},
{
name: "scp query credential",
raw: "git@example.com:Org/Repo.git?token=secret",
want: "example.com:Org/Repo.git",
},
{
name: "scp fragment credential",
raw: "git@example.com:Org/Repo.git#token=secret",
want: "example.com:Org/Repo.git",
},
{
name: "no userinfo",
raw: "https://github.com/org/repo.git",
want: "https://github.com/org/repo.git",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, SanitizeGitRemoteForStorage(tt.raw))
})
}
}
func TestSafeProjectDisplayLabelRejectsURLSchemes(t *testing.T) {
tests := []string{
"mailto:alice@example.com",
"urn:example:private-project",
"s3:private-bucket/project",
"custom+ssh:credential-bearing-value",
}
for _, value := range tests {
t.Run(value, func(t *testing.T) {
assert.Empty(t, SafeProjectDisplayLabel(value))
})
}
assert.Equal(t, "release notes", SafeProjectDisplayLabel("release notes"))
assert.Equal(t, "Fix: project identity",
SafeProjectDisplayLabel("Fix: project identity"))
}
func TestProjectIdentitySelectRemotePrefersOriginAndRejectsAmbiguity(t *testing.T) {
name, raw, ok := SelectRemote(map[string]string{
"upstream": "https://github.com/acme/upstream.git",
"origin": "git@github.com:acme/app.git",
})
require.True(t, ok)
assert.Equal(t, "origin", name)
assert.Equal(t, "git@github.com:acme/app.git", raw)
name, raw, ok = SelectRemote(map[string]string{
"zeta": "https://github.com/acme/zeta.git",
"beta": "https://github.com/acme/beta.git",
})
assert.False(t, ok)
assert.Empty(t, name)
assert.Empty(t, raw)
}
func TestProjectIdentityRootPathFallbackNormalizesLocalPath(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("symlink path separators in this contract are POSIX-specific")
}
base := t.TempDir()
realRoot := filepath.Join(base, "real")
linkRoot := filepath.Join(base, "link")
require.NoError(t, mkdirAll(realRoot))
require.NoError(t, symlink(realRoot, linkRoot))
got, ok, err := NormalizeRootPath(linkRoot + string(filepath.Separator))
require.NoError(t, err)
require.True(t, ok)
expectedRoot, ok, err := NormalizeRootPath(realRoot)
require.NoError(t, err)
require.True(t, ok)
assert.Equal(t, expectedRoot, got)
identity := BuildProjectIdentity(ProjectIdentityInput{RootPath: linkRoot + "/"})
require.NotEmpty(t, identity.Key)
assert.Equal(t, "root_path", identity.KeySource)
assert.Equal(t, expectedRoot, identity.RootPath)
}
func TestProjectIdentityStoredRootPathUsesLexicalSymlinkPath(t *testing.T) {
base := t.TempDir()
realRoot := filepath.Join(base, "real")
linkRoot := filepath.Join(base, "link")
require.NoError(t, mkdirAll(realRoot))
if err := symlink(realRoot, linkRoot); err != nil {
t.Skipf("symlink unavailable: %v", err)
}
live := BuildProjectIdentity(ProjectIdentityInput{RootPath: linkRoot})
stored := BuildStoredProjectIdentity(ProjectIdentityInput{RootPath: linkRoot})
require.NotEmpty(t, live.Key)
require.NotEmpty(t, stored.Key)
assert.NotEqual(t, live.Key, stored.Key)
assert.Equal(t, filepath.ToSlash(filepath.Clean(linkRoot)), stored.RootPath)
expectedLive, ok, err := NormalizeRootPath(realRoot)
require.NoError(t, err)
require.True(t, ok)
assert.Equal(t, expectedLive, live.RootPath)
}
func TestStoredProjectReferenceDoesNotFollowRetargetedSymlink(t *testing.T) {
base := t.TempDir()
firstRoot := filepath.Join(base, "first")
secondRoot := filepath.Join(base, "second")
linkRoot := filepath.Join(base, "link")
require.NoError(t, mkdirAll(firstRoot))
require.NoError(t, mkdirAll(secondRoot))
if err := symlink(firstRoot, linkRoot); err != nil {
t.Skipf("symlink unavailable: %v", err)
}
obs := ProjectIdentityObservation{
Project: "fixture", Machine: "fixture-machine",
RootPath: linkRoot, RepositoryPath: linkRoot,
WorktreeRootPath: linkRoot, WorktreeRelationship: WorktreeMain,
}
scope := IdentityScope{ArchiveID: "archive", ArchiveSalt: "salt"}
before := ResolveProjectReferenceFromObservation(obs, scope)
require.NoError(t, os.Remove(linkRoot))
require.NoError(t, symlink(secondRoot, linkRoot))
after := ResolveProjectReferenceFromObservation(obs, scope)
assert.Equal(t, before.Identity, after.Identity)
assert.Equal(t, before.Worktree, after.Worktree)
}
func TestProjectIdentityWindowsDriveRootPathsAreLocal(t *testing.T) {
tests := []struct {
name string
raw string
want string
}{
{
name: "backslash drive path",
raw: `C:\repo\`,
want: "C:/repo",
},
{
name: "slash drive path",
raw: "C:/repo/",
want: "C:/repo",
},
{
name: "parent segments stay within drive root",
raw: "C:/../repo",
want: "C:/repo",
},
{
name: "lowercase drive uppercased",
raw: "c:/repo",
want: "C:/repo",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, ok, err := NormalizeRootPath(tt.raw)
require.NoError(t, err)
require.True(t, ok)
assert.Equal(t, tt.want, got)
stored, ok := NormalizeStoredRootPath(tt.raw)
require.True(t, ok)
assert.Equal(t, tt.want, stored)
identity := BuildStoredProjectIdentity(ProjectIdentityInput{RootPath: tt.raw})
require.NotEmpty(t, identity.Key)
assert.Equal(t, ProjectIdentityKeySourceRootPath, identity.KeySource)
assert.Equal(t, tt.want, identity.RootPath)
assert.True(t, identity.MachineLocal)
})
}
_, ok, err := NormalizeRootPath("host:/srv/repo")
require.NoError(t, err)
assert.False(t, ok)
_, ok = NormalizeStoredRootPath("host:/srv/repo")
assert.False(t, ok)
}
func TestProjectIdentityStoredRootPathAcceptsPOSIXAbsolutePath(t *testing.T) {
got, ok := NormalizeStoredRootPath(`/fixtures\repo/../repo/worktree/`)
require.True(t, ok)
assert.Equal(t, "/fixtures/repo/worktree", got)
identity := BuildStoredProjectIdentity(ProjectIdentityInput{
RootPath: `/fixtures\repo/../repo/worktree/`,
})
require.NotEmpty(t, identity.Key)
assert.Equal(t, ProjectIdentityKeySourceRootPath, identity.KeySource)
assert.Equal(t, "/fixtures/repo/worktree", identity.RootPath)
}
func TestProjectIdentityKeysUseTypedSHA256Inputs(t *testing.T) {
remoteIdentity := BuildProjectIdentity(ProjectIdentityInput{
GitRemote: "git@github.com:Org/Repo.git",
})
assert.Equal(t,
"sha256:"+sha256Hex("git_remote\n"+"github.com/Org/Repo"),
remoteIdentity.Key,
)
assert.Equal(t, "git_remote", remoteIdentity.KeySource)
root, ok, err := NormalizeRootPath(t.TempDir())
require.NoError(t, err)
require.True(t, ok)
rootIdentity := BuildProjectIdentity(ProjectIdentityInput{RootPath: root})
assert.Equal(t, "sha256:"+sha256Hex("root_path\n"+root), rootIdentity.Key)
assert.Equal(t, "root_path", rootIdentity.KeySource)
}
func TestProjectIdentityJSONUsesNormalizedRemoteField(t *testing.T) {
reference := ResolveProjectReference(ProjectIdentityInput{
RootPath: "/work/repo",
WorktreeRootPath: "/work/repo",
WorktreeKind: WorktreeMain,
GitRemote: "git@github.com:Org/Repo.git",
}, IdentityScope{ArchiveID: "archive", ArchiveSalt: "salt", MachineID: "machine"})
require.NotNil(t, reference.Identity)
data, err := json.Marshal(reference.Identity)
require.NoError(t, err)
var got map[string]any
require.NoError(t, json.Unmarshal(data, &got))
assert.Equal(t, "github.com/Org/Repo", got["normalized_remote"])
assert.NotContains(t, got, "git_remote")
assert.NotContains(t, got, "root_path")
assert.NotContains(t, got, "key_source")
assert.NotContains(t, got, "machine_local")
assert.Equal(t, string(ProjectKindGitRemote), got["kind"])
assert.Contains(t, got, "root_key")
assert.Contains(t, got, "repository_key")
}
func TestProjectIdentityBuildProjectsMapResolvesUnknownAndAmbiguous(t *testing.T) {
observations := []ProjectIdentityObservation{
{
Project: "app",
Machine: "laptop",
RootPath: "/tmp/ignored",
GitRemote: "git@github.com:Org/Repo.git",
Key: "stale-derived-value",
},
{
Project: "ambiguous",
Machine: "laptop",
GitRemote: "https://github.com/org/one.git",
},
{
Project: "ambiguous",
Machine: "desktop",
GitRemote: "https://github.com/org/two.git",
},
}
got := BuildProjectsMap([]string{"app", "missing", "ambiguous"}, observations)
require.Equal(t, ProjectResolutionResolved, got["app"].Resolution)
require.NotNil(t, got["app"].Identity)
assert.Equal(t, "github.com/Org/Repo", got["app"].Identity.NormalizedRemote)
assert.Equal(t, ProjectKindGitRemote, got["app"].Identity.Kind)
assert.NotEmpty(t, got["app"].Identity.Key)
assert.Equal(t, ProjectResolutionUnknown, got["missing"].Resolution)
assert.Nil(t, got["missing"].Identity)
assert.Equal(t, ProjectResolutionAmbiguous, got["ambiguous"].Resolution)
assert.Nil(t, got["ambiguous"].Identity)
}
func TestProjectIdentityBuildProjectsMapUsesObservationArchiveScope(t *testing.T) {
got := BuildProjectsMapWithScope([]string{"app"}, []ProjectIdentityObservation{{
SourceArchiveID: "archive-a",
SourceArchiveSalt: "0123456789abcdef",
Project: "app",
Machine: "laptop",
RootPath: "/workspace/app",
GitRemote: "https://github.com/acme/app.git",
}}, IdentityScope{})
require.NotNil(t, got["app"].Identity)
assert.Empty(t, got["app"].Identity.RootKey,
"remote-backed aggregates must not select one machine-local root")
}
func TestProjectMapForWireUsesOpaqueKeysAndSafeLabels(t *testing.T) {
projects := BuildProjectsMapWithScope(
[]string{
"/Users/alice/private/app",
`\\server\share\private-app`,
`\rooted\private-app`,
}, nil,
IdentityScope{ArchiveID: "archive-a", ArchiveSalt: "salt-a"},
)
got := ProjectMapForWire(projects)
require.Len(t, got, 3)
for key, entry := range got {
assert.NotContains(t, key, "/Users/alice")
assert.Empty(t, entry.DisplayLabel)
}
}
func TestProjectLabelKeyIsStableOnlyWithinArchive(t *testing.T) {
build := func(archiveID, salt string) string {
projects := BuildProjectsMapWithScope(
[]string{"/Users/alice/private/app"}, nil,
IdentityScope{ArchiveID: archiveID, ArchiveSalt: salt},
)
for key := range ProjectMapForWire(projects) {
return key
}
return ""
}
first := build("archive-a", "salt-a")
assert.Equal(t, first, build("archive-a", "salt-a"))
assert.NotEqual(t, first, build("archive-b", "salt-b"))
}
func sha256Hex(s string) string {
sum := sha256.Sum256([]byte(s))
return hex.EncodeToString(sum[:])
}
func TestIsAutomountNamespacePath(t *testing.T) {
tests := []struct {
name string
goos string
path string
want bool
}{
{"darwin home root", "darwin", "/home", true},
{"darwin home child", "darwin", "/home/user/repo", true},
{"darwin net child", "darwin", "/net/host/share", true},
{"darwin network servers", "darwin", "/Network/Servers/x", true},
{"darwin prefix collision homework", "darwin", "/homework/repo", false},
{"darwin prefix collision netdata", "darwin", "/netdata", false},
{"darwin regular path", "darwin", "/Users/user/repo", false},
{"linux home is real", "linux", "/home/user/repo", false},
{"windows never matches", "windows", "/home/user", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, IsAutomountNamespacePath(tt.goos, tt.path))
})
}
}
// TestNormalizeStoredRootPathSkipsAutomountNamespace pins that on macOS a
// stored /home/... root path normalizes to its cleaned form without touching
// the filesystem: resolving it through the automounter is both futile (the
// path names a directory on another machine) and expensive (each probe wakes
// automountd/opendirectoryd, and negative results are not cached).
func TestNormalizeStoredRootPathSkipsAutomountNamespace(t *testing.T) {
if runtime.GOOS != "darwin" {
t.Skip("automount namespaces are a darwin-only concern")
}
got, ok := NormalizeStoredRootPath("/home/user/work/repo")
require.True(t, ok)
assert.Equal(t, "/home/user/work/repo", got)
normalized, ok, err := NormalizeRootPath("/home/user/work/repo")
require.NoError(t, err)
require.True(t, ok)
assert.Equal(t, "/home/user/work/repo", normalized)
}
func TestResolveProjectReferenceScrubsPrivateLocalIdentity(t *testing.T) {
got := ResolveProjectReference(ProjectIdentityInput{
DisplayLabel: "app-copy",
RootPath: "/private/work/app-copy",
GitRemote: "https://user:token@example.com/acme/app.git?x=1#frag",
RepositoryPath: "/private/work/app-copy/.git",
WorktreeRootPath: "/private/work/app-copy",
WorktreeKind: WorktreeLinked,
GitBranch: "feature/a",
}, IdentityScope{
ArchiveID: "archive-fixture",
ArchiveSalt: "salt-fixture",
MachineID: "machine-fixture",
})
require.NotNil(t, got.Identity)
assert.Equal(t, "app-copy", got.DisplayLabel)
assert.Equal(t, ProjectResolutionResolved, got.Resolution)
assert.Equal(t, ProjectKindGitRemote, got.Identity.Kind)
assert.Equal(t, "example.com/acme/app", got.Identity.NormalizedRemote)
assert.NotEmpty(t, got.Identity.Key)
assert.NotEmpty(t, got.Identity.RootKey)
assert.NotEmpty(t, got.Identity.RepositoryKey)
assert.Equal(t, WorktreeLinked, got.Worktree.Relationship)
assert.NotEmpty(t, got.Worktree.WorktreeKey)
assert.Equal(t, got.Identity.RepositoryKey, got.Worktree.RepositoryKey)
assert.Equal(t, CheckoutBranch, got.Checkout.State)
assert.Equal(t, "feature/a", got.Checkout.Branch)
encoded := marshalProjectJSON(t, got)
assert.NotContains(t, encoded, "/private/work")
assert.NotContains(t, encoded, "user:token")
}
func TestResolveProjectReferenceUsesMachineScopedRootWithoutRemote(t *testing.T) {
input := ProjectIdentityInput{
DisplayLabel: "local-app",
RootPath: "/work/local-app",
WorktreeRootPath: "/work/local-app",
WorktreeKind: WorktreeMain,
}
first := ResolveProjectReference(input, IdentityScope{
ArchiveID: "archive", ArchiveSalt: "salt", MachineID: "machine-a",
})
second := ResolveProjectReference(input, IdentityScope{
ArchiveID: "archive", ArchiveSalt: "salt", MachineID: "machine-b",
})
require.NotNil(t, first.Identity)
require.NotNil(t, second.Identity)
assert.Equal(t, ProjectKindMachineRoot, first.Identity.Kind)
assert.NotEqual(t, first.Identity.Key, second.Identity.Key)
assert.Empty(t, first.Identity.NormalizedRemote)
assert.Equal(t, CheckoutUnknown, first.Checkout.State)
assert.NotContains(t, marshalProjectJSON(t, first), "/work/local-app")
}
func TestResolveRemoteSelectionFailsClosedWithoutOrigin(t *testing.T) {
selection := ResolveRemoteSelection(map[string]string{
"beta": "https://example.com/acme/beta.git",
"zeta": "https://example.com/acme/zeta.git",
})
assert.Equal(t, ProjectResolutionAmbiguous, selection.Resolution)
assert.Empty(t, selection.Name)
assert.Empty(t, selection.Raw)
assert.Empty(t, selection.Normalized)
}
func TestResolveProjectReferencePropagatesAmbiguousRemoteSelection(t *testing.T) {
got := ResolveProjectReference(ProjectIdentityInput{
DisplayLabel: "app",
RootPath: "/work/app-worktree",
RepositoryPath: "/work/app/.git",
WorktreeRootPath: "/work/app-worktree",
WorktreeKind: WorktreeLinked,
Detached: true,
RemoteSelection: ResolveRemoteSelection(map[string]string{
"one": "https://example.com/acme/one.git",
"two": "https://example.com/acme/two.git",
}),
}, IdentityScope{ArchiveID: "archive", ArchiveSalt: "salt", MachineID: "machine"})
assert.Equal(t, ProjectResolutionAmbiguous, got.Resolution)
assert.Nil(t, got.Identity)
assert.Equal(t, WorktreeLinked, got.Worktree.Relationship)
assert.NotEmpty(t, got.Worktree.WorktreeKey)
assert.NotEmpty(t, got.Worktree.RepositoryKey)
assert.Equal(t, CheckoutDetached, got.Checkout.State)
assert.NotContains(t, marshalProjectJSON(t, got), `"identity"`)
}
func TestBuildProjectsMapPreservesObservedRemoteAmbiguity(t *testing.T) {
got := BuildProjectsMap([]string{"app"}, []ProjectIdentityObservation{{
Project: "app",
Machine: "machine",
RootPath: "/work/app",
RemoteResolution: ProjectResolutionAmbiguous,
}})
assert.Equal(t, ProjectResolutionAmbiguous, got["app"].Resolution)
assert.Nil(t, got["app"].Identity)
}
func TestAggregateIdentityScopeIsOrderIndependentAndArchiveSensitive(t *testing.T) {
first := AggregateIdentityScope([]IdentityScope{
{ArchiveID: "archive-b", ArchiveSalt: "salt-b"},
{ArchiveID: "archive-a", ArchiveSalt: "salt-a"},
})
reordered := AggregateIdentityScope([]IdentityScope{
{ArchiveID: "archive-a", ArchiveSalt: "salt-a"},
{ArchiveID: "archive-b", ArchiveSalt: "salt-b"},
})
changed := AggregateIdentityScope([]IdentityScope{
{ArchiveID: "archive-a", ArchiveSalt: "salt-a"},
{ArchiveID: "archive-c", ArchiveSalt: "salt-c"},
})
assert.Equal(t, first, reordered)
assert.NotEmpty(t, first.ArchiveID)
assert.NotEmpty(t, first.ArchiveSalt)
assert.NotEqual(t, first, changed)
}
func TestResolveProjectReferenceSharesNoRemoteRepositoryAcrossWorktrees(t *testing.T) {
scope := IdentityScope{ArchiveID: "archive", ArchiveSalt: "salt", MachineID: "machine"}
main := ResolveProjectReference(ProjectIdentityInput{
RootPath: "/work/app",
RepositoryPath: "/work/app/.git",
WorktreeRootPath: "/work/app",
WorktreeKind: WorktreeMain,
}, scope)
linked := ResolveProjectReference(ProjectIdentityInput{
RootPath: "/work/app-feature",
RepositoryPath: "/work/app/.git",
WorktreeRootPath: "/work/app-feature",
WorktreeKind: WorktreeLinked,
}, scope)
require.NotNil(t, main.Identity)
require.NotNil(t, linked.Identity)
assert.Equal(t, main.Identity.Key, linked.Identity.Key)
assert.Equal(t, main.Identity.RepositoryKey, linked.Identity.RepositoryKey)
assert.NotEqual(t, main.Identity.RootKey, linked.Identity.RootKey)
assert.NotEqual(t, main.Worktree.WorktreeKey, linked.Worktree.WorktreeKey)
}
func TestResolveProjectReferenceRequiresCompleteLocalScope(t *testing.T) {
input := ProjectIdentityInput{
RootPath: "/work/app",
RepositoryPath: "/work/app/.git",
WorktreeRootPath: "/work/app",
WorktreeKind: WorktreeMain,
}
for name, scope := range map[string]IdentityScope{
"missing archive": {ArchiveSalt: "salt", MachineID: "machine"},
"missing salt": {ArchiveID: "archive", MachineID: "machine"},
"missing machine": {ArchiveID: "archive", ArchiveSalt: "salt"},
} {
t.Run(name, func(t *testing.T) {
got := ResolveProjectReference(input, scope)
assert.Equal(t, ProjectResolutionUnknown, got.Resolution)
assert.Nil(t, got.Identity)
assert.Empty(t, got.Worktree.WorktreeKey)
})
}
}
func TestResolveProjectReferenceRemoteRenameAndSelectionMatrix(t *testing.T) {
scope := IdentityScope{ArchiveID: "archive", ArchiveSalt: "salt", MachineID: "machine"}
tests := []struct {
name string
remotes map[string]string
resolution ProjectResolution
}{
{
name: "origin wins",
remotes: map[string]string{
"origin": "git@example.com:acme/app.git",
"upstream": "https://example.com/acme/upstream.git",
},
resolution: ProjectResolutionResolved,
},
{
name: "single non-origin",
remotes: map[string]string{"upstream": "https://example.com/acme/app.git"},
resolution: ProjectResolutionResolved,
},
{
name: "ambiguous non-origin",
remotes: map[string]string{
"one": "https://example.com/acme/one.git",
"two": "https://example.com/acme/two.git",
},
resolution: ProjectResolutionAmbiguous,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
selection := ResolveRemoteSelection(tt.remotes)
got := ResolveProjectReference(ProjectIdentityInput{
RootPath: "/old/app",
RepositoryPath: "/old/app/.git",
WorktreeRootPath: "/old/app",
WorktreeKind: WorktreeMain,
RemoteSelection: selection,
}, scope)
assert.Equal(t, tt.resolution, got.Resolution)
if tt.resolution != ProjectResolutionResolved {
assert.Nil(t, got.Identity)
return
}
require.NotNil(t, got.Identity)
renamed := ResolveProjectReference(ProjectIdentityInput{
RootPath: "/renamed/app",
RepositoryPath: "/renamed/app/.git",
WorktreeRootPath: "/renamed/app",
WorktreeKind: WorktreeMain,
RemoteSelection: selection,
}, scope)
require.NotNil(t, renamed.Identity)
assert.Equal(t, got.Identity.Key, renamed.Identity.Key)
assert.Equal(t, got.Identity.RepositoryKey, renamed.Identity.RepositoryKey)
assert.NotEqual(t, got.Identity.RootKey, renamed.Identity.RootKey)
})
}
}
func TestResolveProjectReferenceRejectsContradictoryOrPrivateContext(t *testing.T) {
got := ResolveProjectReference(ProjectIdentityInput{
DisplayLabel: "/private/app",
RootPath: "/work/app",
RepositoryPath: "/work/app/.git",
WorktreeRootPath: "/work/app",
WorktreeKind: WorktreeNone,
GitBranch: "https://user:token@example.com/private",
}, IdentityScope{ArchiveID: "archive", ArchiveSalt: "salt", MachineID: "machine"})
assert.Empty(t, got.DisplayLabel)
assert.Empty(t, got.Worktree.WorktreeKey)
assert.Equal(t, CheckoutUnknown, got.Checkout.State)
assert.Empty(t, got.Checkout.Branch)
encoded := marshalProjectJSON(t, got)
assert.NotContains(t, encoded, "/private/app")
assert.NotContains(t, encoded, "token")
}
func TestResolveProjectReferenceMarksDetachedCheckout(t *testing.T) {
got := ResolveProjectReference(ProjectIdentityInput{
DisplayLabel: "app",
RootPath: "/work/app",
GitRemote: "git@example.com:acme/app.git",
Detached: true,
WorktreeKind: WorktreeLinked,
}, IdentityScope{ArchiveID: "archive", ArchiveSalt: "salt", MachineID: "machine"})
assert.Equal(t, CheckoutDetached, got.Checkout.State)
assert.Empty(t, got.Checkout.Branch)
assert.Equal(t, WorktreeLinked, got.Worktree.Relationship)
}
func marshalProjectJSON(t *testing.T, value any) string {
t.Helper()
data, err := json.Marshal(value)
require.NoError(t, err)
return string(data)
}