Files
paseo/packages/app/src/utils/new-agent-routing.test.ts
T
Mohamed BoudraandGitHub ed07452c80 feat(checkout): merge PR action with real-GitHub e2e test (#814)
* feat(checkout): merge PR action with real-GitHub e2e test

Adds a `checkoutPrMerge` server RPC backed by `gh pr merge` (squash, merge,
or rebase) and surfaces three new actions in the git-actions menu —
"Squash and merge", "Create a merge commit", "Rebase and merge" — with
squash taking the primary slot when a PR is open and mergeable. Also
renames the existing local merge action to "Merge locally" to disambiguate
it from the new GitHub-side merge.

Adds an end-to-end test that creates a real temporary GitHub repo, opens a
PR via the daemon, polls until mergeable, calls `checkoutPrMerge`, and
verifies the merge landed on `main` — no mocks. The test pre-flights the
`delete_repo` scope so it skips cleanly on tokens that can't tear down,
and fails loud if cleanup ever errors so we don't leak repos.

PR status gains an optional `mergeable` field (`MERGEABLE | CONFLICTING |
UNKNOWN`) populated from `gh pr view --json mergeable`. The schema is
backward compatible — old payloads without the field still parse and
default to `UNKNOWN` via `z.catch`.

* Update PR status snapshot expectations

* refactor: collect git/checkout/PR feature into packages/app/src/git/

Move 21 git/checkout/PR files out of components/, hooks/, stores/, screens/,
utils/ flat-peer slop into a single packages/app/src/git/ module with
shrunk filenames (git-diff-pane.tsx -> diff-pane.tsx, use-git-actions.ts ->
use-actions.ts, etc).

Dedupe: drop the duplicate useGitActionHandlers from diff-pane.tsx — the
shared useGitActions hook is the single source. Drop the
buildGitActionsForPane wrapper. Collapse the merge-PR triple fan-out
(mergePrSquash/Merge/RebaseStatus) into one record keyed by
CheckoutPrMergeMethod. Streamline server-side single-use helpers
(dispatchStash/PullRequestMessage adapters, GitHubPullRequestMergeError
wrapper, legacy worktree test helpers).

Schema-additive only: messages.ts exports CheckoutPrMergeMethod and
PullRequestMergeable types; daemon-client.ts uses the typed alias.
Backward-compatible.

Net delta: 43 files, +219/-1212.

* fix(app): use GitHub icon for all merge-PR menu actions

* test: namespace temp GitHub repos under `paseotmp-` and centralize naming

Old prefixes (`paseo-checkout-ship-`, `paseo-e2e-`, `paseo-checkout-pr-merge-`)
collided with the `paseo-*` namespace of real repos, making bulk cleanup
unsafe. Switch every test that creates real GitHub repos to a single
unmistakable `paseotmp-` prefix, owned by one shared helper per test
boundary so no caller can name a repo outside the namespace.

- New `packages/server/src/server/test-utils/temp-github-repo.ts` exports
  `TEMP_GITHUB_REPO_PREFIX` + `createTempGithubRepoName(category)`; both
  server e2e tests (checkout-ship, checkout-pr-merge) consume it.
- `packages/app/e2e/helpers/github-fixtures.ts` locks the prefix internally
  and now requires callers to pass a `category` instead of an arbitrary
  `prefix`. Updated pr-pane and composer-attachments specs accordingly.
2026-05-09 15:39:26 +08:00

94 lines
2.7 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { CheckoutStatusPayload } from "@/git/use-status-query";
import {
parseAgentKey,
resolveNewAgentWorkingDir,
resolveSelectedAgentForNewAgent,
} from "./new-agent-routing";
describe("resolveNewAgentWorkingDir", () => {
it("returns the current cwd for regular checkouts", () => {
expect(resolveNewAgentWorkingDir("/repo/path", null)).toBe("/repo/path");
});
it("falls back to repo root when checkout metadata is unavailable", () => {
expect(resolveNewAgentWorkingDir("/repo/.paseo/worktrees/feature", null)).toBe("/repo");
});
it("supports windows-style paseo worktree paths without checkout metadata", () => {
expect(resolveNewAgentWorkingDir("C:\\Users\\me\\repo\\.paseo\\worktrees\\feature", null)).toBe(
"C:\\Users\\me\\repo",
);
});
it("returns the main repo root for paseo-owned worktrees", () => {
const checkout = {
isPaseoOwnedWorktree: true,
worktreeRoot: "/repo/.paseo/worktrees/feature",
mainRepoRoot: "/repo/main",
} as unknown as CheckoutStatusPayload;
expect(resolveNewAgentWorkingDir("/repo/.paseo/worktrees/feature", checkout)).toBe(
"/repo/main",
);
});
});
describe("parseAgentKey", () => {
it("parses server and agent ids from combined key", () => {
expect(parseAgentKey("srv-1:agent-9")).toEqual({
serverId: "srv-1",
agentId: "agent-9",
});
});
it("uses the last separator to preserve server ids with colons", () => {
expect(parseAgentKey("localhost:6767:agent-9")).toEqual({
serverId: "localhost:6767",
agentId: "agent-9",
});
});
it("returns null for malformed keys", () => {
expect(parseAgentKey("")).toBeNull();
expect(parseAgentKey("only-server")).toBeNull();
expect(parseAgentKey(":agent-1")).toBeNull();
expect(parseAgentKey("srv-1:")).toBeNull();
});
});
describe("resolveSelectedAgentForNewAgent", () => {
it("prefers the agent in the current route", () => {
expect(
resolveSelectedAgentForNewAgent({
pathname: "/h/srv-1/workspace/L3JlcG8?open=agent%3Aagent-2",
selectedAgentId: "srv-9:agent-9",
}),
).toEqual({
serverId: "srv-1",
agentId: "agent-2",
});
});
it("falls back to selected agent key when route has no agent", () => {
expect(
resolveSelectedAgentForNewAgent({
pathname: "/h/srv-1/settings",
selectedAgentId: "srv-1:agent-7",
}),
).toEqual({
serverId: "srv-1",
agentId: "agent-7",
});
});
it("returns null when neither route nor selection has an agent", () => {
expect(
resolveSelectedAgentForNewAgent({
pathname: "/h/srv-1/settings",
}),
).toBeNull();
});
});