Files
paseo/packages/app/src/screens/new-workspace-picker-item.test.ts
T
Mohamed BoudraandGitHub fb657ebc20 Default new worktrees to the upstream branch (#2848)
* fix(workspaces): default new worktrees to upstream refs

Use the daemon-resolved upstream as the untouched starting ref so unpushed local commits do not leak into new workspaces. Preserve exact base identity for fork remotes while keeping local-first behavior across version drift.

* fix(workspaces): preserve exact base refs in git actions

Resolve accepted display aliases back to the stored ref before comparisons and updates. Fail closed when a non-origin remote base has no explicit mutable merge target.

* fix(workspaces): reject conflicting qualified base refs

Display aliases remain accepted for compatibility, but a qualified ref carries remote identity and must not be canonicalized to a different stored upstream.

* test(server): make upstream fixtures portable

Keep the checkout-status fixture aligned with the optional upstream field, and select main explicitly when cloning a bare test remote whose default HEAD varies by platform.

* fix(workspaces): preserve exact upstream refs

Qualified remote refs now fail closed when pruned instead of switching lineage. Metadata accepts Git-valid upstream names rather than applying a narrower character whitelist.

* fix(workspaces): preserve qualified local base refs

Local tracking branches are exact upstream identities too. Keep heuristics only for legacy unqualified metadata so origin cannot replace a stored refs/heads base.
2026-08-04 09:01:38 +02:00

343 lines
9.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from "vitest";
import type { ForgeSearchItem } from "@getpaseo/protocol/messages";
import {
type BranchPickerDetail,
branchPickerOptionId,
buildBranchPickerItems,
buildPickerOptionData,
defaultBasePickerItem,
pickerItemToCheckoutRequest,
type PickerItem,
} from "./new-workspace-picker-item";
const prItem: ForgeSearchItem = {
kind: "change_request",
number: 42,
title: "Add picker",
url: "https://example.com/pull/42",
state: "open",
body: null,
labels: [],
baseRefName: "main",
headRefName: "feature/picker",
};
describe("branchPickerOptionId", () => {
it("distinguishes a local origin-prefixed branch from the origin ref", () => {
expect(branchPickerOptionId("refs/heads/origin/main")).not.toBe(
branchPickerOptionId("refs/remotes/origin/main"),
);
});
});
describe("pickerItemToCheckoutRequest", () => {
it("returns undefined for no selection (null)", () => {
expect(pickerItemToCheckoutRequest(null)).toBeUndefined();
});
it("maps a branch row to branch-off with its exact ref", () => {
const item: PickerItem = {
kind: "branch",
name: "dev",
refName: "refs/heads/dev",
accessibilityLabel: "dev, local branch",
};
expect(pickerItemToCheckoutRequest(item)).toEqual({
action: "branch-off",
refName: "refs/heads/dev",
});
});
it("maps a github-pr row to checkout using the head ref and pr number", () => {
const item: PickerItem = {
kind: "github-pr",
item: prItem,
};
expect(pickerItemToCheckoutRequest(item)).toEqual({
action: "checkout",
refName: "feature/picker",
checkoutSource: { kind: "change_request", forge: "github", number: 42 },
githubPrNumber: 42,
});
});
it("handles a github-pr with a null baseRef", () => {
const item: PickerItem = {
kind: "github-pr",
item: {
...prItem,
number: 7,
title: "Orphan branch",
baseRefName: null,
headRefName: "orphan",
},
};
expect(pickerItemToCheckoutRequest(item)).toEqual({
action: "checkout",
refName: "orphan",
checkoutSource: { kind: "change_request", forge: "github", number: 7 },
githubPrNumber: 7,
});
});
it("does not send the legacy githubPrNumber for non-GitHub change requests", () => {
const item: PickerItem = {
kind: "github-pr",
item: {
...prItem,
forge: "gitlab",
number: 21,
projectPath: "acme/repo",
url: "https://gitlab.example.com/acme/repo/-/merge_requests/21",
},
};
expect(pickerItemToCheckoutRequest(item)).toEqual({
action: "checkout",
refName: "feature/picker",
checkoutSource: {
kind: "change_request",
forge: "gitlab",
number: 21,
projectPath: "acme/repo",
},
});
});
});
describe("buildBranchPickerItems", () => {
it("keeps a single origin row when local and origin match", () => {
expect(
buildBranchPickerItems([
{
name: "main",
committerDate: 10,
hasLocal: true,
hasRemote: true,
localAhead: 0,
localBehind: 0,
},
]),
).toEqual([
{
kind: "branch",
name: "main",
refName: "refs/remotes/origin/main",
accessibilityLabel: "main, origin branch",
committerDate: 10,
},
]);
});
it("puts the origin row first and disambiguates the local row when the refs differ", () => {
expect(
buildBranchPickerItems([
{
name: "main",
committerDate: 10,
hasLocal: true,
hasRemote: true,
localAhead: 3,
localBehind: 2,
},
]),
).toEqual([
{
kind: "branch",
name: "main",
refName: "refs/remotes/origin/main",
accessibilityLabel: "main, origin branch",
committerDate: 10,
},
{
kind: "branch",
name: "main (local)",
refName: "refs/heads/main",
divergenceLabel: "+3 −2",
accessibilityLabel: "main, local branch, 3 commits ahead and 2 behind origin main",
committerDate: 10,
},
]);
});
it("uses an exact origin ref for remote-only branches", () => {
expect(
buildBranchPickerItems([
{
name: "release",
committerDate: 5,
hasLocal: false,
hasRemote: true,
},
]),
).toEqual([
{
kind: "branch",
name: "release",
refName: "refs/remotes/origin/release",
accessibilityLabel: "release, origin branch",
committerDate: 5,
},
]);
});
it("keeps the plain name for a local-only branch", () => {
expect(
buildBranchPickerItems([
{
name: "scratch",
committerDate: 5,
hasLocal: true,
hasRemote: false,
},
]),
).toEqual([
{
kind: "branch",
name: "scratch",
refName: "refs/heads/scratch",
accessibilityLabel: "scratch, local branch",
committerDate: 5,
},
]);
});
it("shows both refs when their divergence is unavailable", () => {
expect(
buildBranchPickerItems([
{
name: "main",
committerDate: 10,
hasLocal: true,
hasRemote: true,
},
]),
).toEqual([
{
kind: "branch",
name: "main",
refName: "refs/remotes/origin/main",
accessibilityLabel: "main, origin branch",
committerDate: 10,
},
{
kind: "branch",
name: "main (local)",
refName: "refs/heads/main",
accessibilityLabel: "main, local branch",
committerDate: 10,
},
]);
});
it("keeps the legacy unqualified row when provenance is unavailable", () => {
expect(buildBranchPickerItems([{ name: "legacy", committerDate: 1 }])).toEqual([
{
kind: "branch",
name: "legacy",
refName: "legacy",
accessibilityLabel: "legacy, branch",
committerDate: 1,
},
]);
});
});
const mainRow: BranchPickerDetail = {
name: "main",
committerDate: 10,
hasLocal: true,
hasRemote: true,
localAhead: 2,
localBehind: 0,
};
describe("buildPickerOptionData", () => {
it("marks origin and keeps an ahead local main explicit", () => {
const baseItem = defaultBasePickerItem({
currentBranch: "main",
upstreamRef: "refs/remotes/origin/main",
});
const data = buildPickerOptionData({ branchDetails: [mainRow], prItems: [], baseItem });
expect(data.options.map((option) => option.label)).toEqual(["main", "main (local)"]);
expect(data.selectedOptionId).toBe(branchPickerOptionId("refs/remotes/origin/main"));
});
it("adds and disambiguates a fork upstream absent from branch suggestions", () => {
const baseItem = defaultBasePickerItem({
currentBranch: "main",
upstreamRef: "refs/remotes/upstream/main",
});
const data = buildPickerOptionData({
branchDetails: [{ ...mainRow, localAhead: 0, localBehind: 0 }],
prItems: [],
baseItem,
});
expect(data.options.map((option) => option.label)).toEqual(["main (upstream)", "main"]);
expect(data.selectedOptionId).toBe(branchPickerOptionId("refs/remotes/upstream/main"));
});
it("marks a visible row on an old daemon when local and origin are in sync", () => {
const baseItem = defaultBasePickerItem({ currentBranch: "main" });
const data = buildPickerOptionData({
branchDetails: [{ ...mainRow, localAhead: 0, localBehind: 0 }],
prItems: [],
baseItem,
});
expect(data.options.map((option) => option.label)).toEqual(["main (local)", "main"]);
expect(data.selectedOptionId).toBe(branchPickerOptionId("refs/heads/main"));
});
it("keeps an explicit local selection marked", () => {
const baseItem: PickerItem = {
kind: "branch",
name: "main (local)",
refName: "refs/heads/main",
accessibilityLabel: "main, local branch",
};
const data = buildPickerOptionData({
branchDetails: [mainRow],
prItems: [],
baseItem,
});
const selected = data.itemById.get(data.selectedOptionId) ?? null;
expect(pickerItemToCheckoutRequest(selected)).toEqual({
action: "branch-off",
refName: "refs/heads/main",
});
});
});
describe("defaultBasePickerItem", () => {
it("uses origin when the current branch tracks origin", () => {
expect(
defaultBasePickerItem({
currentBranch: "main",
upstreamRef: "refs/remotes/origin/main",
}),
).toMatchObject({ refName: "refs/remotes/origin/main", name: "main" });
});
it("uses the exact non-origin upstream", () => {
expect(
defaultBasePickerItem({
currentBranch: "main",
upstreamRef: "refs/remotes/upstream/main",
}),
).toMatchObject({ refName: "refs/remotes/upstream/main", name: "main" });
});
it("keeps old-daemon behavior local", () => {
expect(defaultBasePickerItem({ currentBranch: "main" })).toMatchObject({
refName: "refs/heads/main",
name: "main",
});
});
it("has no default for detached HEAD", () => {
expect(defaultBasePickerItem({ currentBranch: null })).toBeNull();
});
});