mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-25 10:42:45 +00:00
* refactor(forge): forge-neutral foundation (GitHub-only) Decouple git-hosting from GitHub behind a neutral abstraction (issue #1616), GitHub-only for now; existing GitHub behaviour is unchanged. - Forge manifest, neutral ForgeService contract, forge registry + resolver, and a client forge-module registry. - GitHub code renamed to the neutral shape; PR/Issue attachment wording preserved. - forge.search.response enums parse tolerantly (unknown kind/auth state degrade instead of breaking the client). - createPullRequest reports typed CLI/auth errors instead of a generic message. - forge-resolver host/remote caches are LRU-bounded. - Forge host trust is explicit: only a known cloud host or a CLI-authenticated host is ever talked to; an unauthenticated GitHub Enterprise host fails resolution instead of routing to github.com. - Docs: forge-providers guide, glossary and i18n forge-copy conventions, architecture and rpc-namespacing terminology. - Vitest React Native mocks (unistyles, svg, linking, lucide) consolidated into shared aliased test-stubs. * feat(forge): GitLab adapter, forge-aware UI, pipelines and approvals GitLab adapter over the glab CLI on the neutral contracts: MR status, forge-aware UI, pipeline tree, and N-of-M approvals. - threadIsResolved is part of the neutral timeline item. - Pipeline load failures show an error instead of an empty section. - Manual pipeline jobs render as pending. - Fork/detached MR head pipelines are fetched by MR iid (glab ci get --merge-request). * feat(forge): Gitea family adapter (Gitea, Forgejo, Codeberg) One adapter over the tea CLI serving Gitea, Forgejo, and Codeberg on the neutral contracts. - CI status aggregates commit statuses and Actions runs together. - Gitea's terminal "warning" state maps to failure on server and client. - Gitea Actions check details are reachable from the PR pane by workflowRunId. * refactor(forge): localize compatibility handling * test(forge): expect normalized GitLab facts --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
390 lines
13 KiB
TypeScript
390 lines
13 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
buildPullRequestCheckContextAttachment,
|
|
buildPullRequestCommentContextAttachment,
|
|
buildPullRequestReviewContextAttachment,
|
|
buildPullRequestThreadContextAttachment,
|
|
canAddPullRequestActivityToChat,
|
|
canAddPullRequestCheckLogsToChat,
|
|
type PullRequestContextBuilderInput,
|
|
} from "./context-attachment";
|
|
import type { PrPaneActivity, PrPaneCheck } from "./data";
|
|
import type { PrThreadEntry } from "./timeline";
|
|
|
|
const baseInput = {
|
|
provider: { id: "github", label: "GitHub" },
|
|
forge: "github",
|
|
pullRequest: {
|
|
number: 42,
|
|
title: "Fix flaky build",
|
|
url: "https://github.com/getpaseo/paseo/pull/42",
|
|
},
|
|
} satisfies Omit<PullRequestContextBuilderInput, "activity">;
|
|
|
|
function comment(overrides: Partial<PrPaneActivity> = {}): PrPaneActivity {
|
|
return {
|
|
id: "comment-1",
|
|
provider: "github",
|
|
kind: "comment",
|
|
author: "octocat",
|
|
avatarColor: "#0ea5e9",
|
|
body: "Looks good.",
|
|
age: "3d ago",
|
|
url: "https://github.com/getpaseo/paseo/pull/42#issuecomment-1",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function review(overrides: Partial<PrPaneActivity> = {}): PrPaneActivity {
|
|
return {
|
|
id: "review-1",
|
|
provider: "github",
|
|
kind: "review",
|
|
author: "reviewer",
|
|
avatarColor: "#8b5cf6",
|
|
reviewState: "commented",
|
|
body: "Please simplify this.",
|
|
age: "2d ago",
|
|
url: "https://github.com/getpaseo/paseo/pull/42#pullrequestreview-1",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function check(overrides: Partial<PrPaneCheck> = {}): PrPaneCheck {
|
|
return {
|
|
name: "server-tests",
|
|
provider: "github",
|
|
status: "failure",
|
|
url: "https://github.com/getpaseo/paseo/actions/runs/456/job/789",
|
|
detailRef: { checkRunId: 12345, workflowRunId: 456 },
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("pull request context attachments", () => {
|
|
it("formats a comment as a workspace context attachment", () => {
|
|
expect(
|
|
buildPullRequestCommentContextAttachment({
|
|
...baseInput,
|
|
activity: comment({
|
|
location: {
|
|
path: "packages/app/src/panel.tsx",
|
|
line: 42,
|
|
threadId: "PRRT_1",
|
|
isResolved: false,
|
|
isOutdated: true,
|
|
},
|
|
}),
|
|
}),
|
|
).toEqual({
|
|
kind: "forge.change_request_comment",
|
|
id: "42:comment-1",
|
|
title: "octocat",
|
|
subtitle: "#42 Fix flaky build",
|
|
url: "https://github.com/getpaseo/paseo/pull/42#issuecomment-1",
|
|
text: [
|
|
"GitHub pull request comment",
|
|
"Pull request: #42 Fix flaky build",
|
|
"Pull request URL: https://github.com/getpaseo/paseo/pull/42",
|
|
"URL: https://github.com/getpaseo/paseo/pull/42#issuecomment-1",
|
|
"Author: octocat",
|
|
"Created: 3d ago",
|
|
"Location: packages/app/src/panel.tsx:42 · unresolved · outdated · thread PRRT_1",
|
|
"",
|
|
"Looks good.",
|
|
].join("\n"),
|
|
});
|
|
});
|
|
|
|
it("formats a review with state as a workspace context attachment", () => {
|
|
expect(buildPullRequestReviewContextAttachment({ ...baseInput, activity: review() })).toEqual({
|
|
kind: "forge.change_request_review",
|
|
id: "42:review-1",
|
|
title: "reviewer",
|
|
subtitle: "#42 Fix flaky build",
|
|
url: "https://github.com/getpaseo/paseo/pull/42#pullrequestreview-1",
|
|
text: [
|
|
"GitHub pull request review",
|
|
"Pull request: #42 Fix flaky build",
|
|
"Pull request URL: https://github.com/getpaseo/paseo/pull/42",
|
|
"URL: https://github.com/getpaseo/paseo/pull/42#pullrequestreview-1",
|
|
"Author: reviewer",
|
|
"State: commented",
|
|
"Created: 2d ago",
|
|
"",
|
|
"Please simplify this.",
|
|
].join("\n"),
|
|
});
|
|
});
|
|
|
|
it("labels a GitLab comment as a merge request with the ! prefix", () => {
|
|
expect(
|
|
buildPullRequestCommentContextAttachment({
|
|
...baseInput,
|
|
provider: { id: "gitlab", label: "GitLab" },
|
|
forge: "gitlab",
|
|
pullRequest: {
|
|
number: 14,
|
|
title: "Wire up the timeline",
|
|
url: "https://gitlab.com/acme/app/-/merge_requests/14",
|
|
},
|
|
activity: comment({
|
|
provider: "gitlab",
|
|
url: "https://gitlab.com/acme/app/-/merge_requests/14#note_401",
|
|
}),
|
|
}),
|
|
).toEqual({
|
|
kind: "forge.change_request_comment",
|
|
id: "14:comment-1",
|
|
title: "octocat",
|
|
subtitle: "!14 Wire up the timeline",
|
|
url: "https://gitlab.com/acme/app/-/merge_requests/14#note_401",
|
|
text: [
|
|
"GitLab merge request comment",
|
|
"Merge request: !14 Wire up the timeline",
|
|
"Merge request URL: https://gitlab.com/acme/app/-/merge_requests/14",
|
|
"URL: https://gitlab.com/acme/app/-/merge_requests/14#note_401",
|
|
"Author: octocat",
|
|
"Created: 3d ago",
|
|
"",
|
|
"Looks good.",
|
|
].join("\n"),
|
|
});
|
|
});
|
|
|
|
it("does not add bodyless approvals to chat", () => {
|
|
const activity = review({ reviewState: "approved", body: "" });
|
|
|
|
expect(canAddPullRequestActivityToChat(activity)).toBe(false);
|
|
expect(buildPullRequestReviewContextAttachment({ ...baseInput, activity })).toBeNull();
|
|
});
|
|
|
|
it("adds bodyless changes-requested reviews as metadata-only context", () => {
|
|
const activity = review({ reviewState: "changes_requested", body: "" });
|
|
|
|
expect(canAddPullRequestActivityToChat(activity)).toBe(true);
|
|
expect(buildPullRequestReviewContextAttachment({ ...baseInput, activity })?.text).toBe(
|
|
[
|
|
"GitHub pull request review",
|
|
"Pull request: #42 Fix flaky build",
|
|
"Pull request URL: https://github.com/getpaseo/paseo/pull/42",
|
|
"URL: https://github.com/getpaseo/paseo/pull/42#pullrequestreview-1",
|
|
"Author: reviewer",
|
|
"State: changes_requested",
|
|
"Created: 2d ago",
|
|
].join("\n"),
|
|
);
|
|
});
|
|
|
|
it("shows add-logs only for failed checks", () => {
|
|
expect(canAddPullRequestCheckLogsToChat(check({ status: "failure" }))).toBe(true);
|
|
expect(canAddPullRequestCheckLogsToChat(check({ status: "success" }))).toBe(false);
|
|
expect(canAddPullRequestCheckLogsToChat(check({ status: "pending" }))).toBe(false);
|
|
expect(canAddPullRequestCheckLogsToChat(check({ status: "skipped" }))).toBe(false);
|
|
});
|
|
|
|
it("formats failed check details as a workspace context attachment", () => {
|
|
expect(
|
|
buildPullRequestCheckContextAttachment({
|
|
...baseInput,
|
|
check: check(),
|
|
githubDetails: {
|
|
checkRunId: 12345,
|
|
workflowRunId: 456,
|
|
name: "server-tests",
|
|
status: "completed",
|
|
conclusion: "failure",
|
|
url: "https://github.com/getpaseo/paseo/actions/runs/456/job/789",
|
|
detailsUrl: "https://github.com/getpaseo/paseo/actions/runs/456/job/789",
|
|
output: { title: "Tests failed", summary: "1 failure", text: "Assertion failed" },
|
|
annotations: [
|
|
{
|
|
path: "packages/server/src/index.ts",
|
|
startLine: 10,
|
|
endLine: 12,
|
|
annotationLevel: "failure",
|
|
message: "Expected true",
|
|
},
|
|
],
|
|
failedJobs: [
|
|
{
|
|
jobId: 789,
|
|
name: "test",
|
|
status: "completed",
|
|
conclusion: "failure",
|
|
url: "https://github.com/getpaseo/paseo/actions/runs/456/job/789",
|
|
logTail: "last line",
|
|
logTruncated: false,
|
|
},
|
|
],
|
|
truncated: true,
|
|
},
|
|
}),
|
|
).toEqual({
|
|
kind: "forge.change_request_check",
|
|
id: "42:check-run:12345",
|
|
title: "server-tests",
|
|
subtitle: "#42 Fix flaky build",
|
|
url: "https://github.com/getpaseo/paseo/actions/runs/456/job/789",
|
|
text: [
|
|
"GitHub pull request check",
|
|
"Pull request: #42 Fix flaky build",
|
|
"Pull request URL: https://github.com/getpaseo/paseo/pull/42",
|
|
"Check: server-tests",
|
|
"Status: failure",
|
|
"Conclusion: failure",
|
|
"Check URL: https://github.com/getpaseo/paseo/actions/runs/456/job/789",
|
|
"Details URL: https://github.com/getpaseo/paseo/actions/runs/456/job/789",
|
|
"Output title: Tests failed",
|
|
"Output summary: 1 failure",
|
|
"Output text:",
|
|
"Assertion failed",
|
|
"",
|
|
"Annotations:",
|
|
"- packages/server/src/index.ts:10-12 failure: Expected true",
|
|
"",
|
|
"Failed jobs:",
|
|
"- test: failure",
|
|
" https://github.com/getpaseo/paseo/actions/runs/456/job/789",
|
|
" ```",
|
|
" last line",
|
|
" ```",
|
|
"",
|
|
"Note: Check details were truncated by GitHub/API or local caps.",
|
|
].join("\n"),
|
|
});
|
|
});
|
|
|
|
it("keeps same-named GitHub checks distinct by check run", () => {
|
|
const ubuntu = buildPullRequestCheckContextAttachment({
|
|
...baseInput,
|
|
check: check({ detailRef: { checkRunId: 12345, workflowRunId: 456 } }),
|
|
githubDetails: null,
|
|
});
|
|
const windows = buildPullRequestCheckContextAttachment({
|
|
...baseInput,
|
|
check: check({ detailRef: { checkRunId: 67890, workflowRunId: 456 } }),
|
|
githubDetails: null,
|
|
});
|
|
|
|
expect(ubuntu.id).toBe("42:check-run:12345");
|
|
expect(windows.id).toBe("42:check-run:67890");
|
|
});
|
|
|
|
it("formats metadata-only failed check context when details are unavailable", () => {
|
|
expect(
|
|
buildPullRequestCheckContextAttachment({
|
|
...baseInput,
|
|
check: check({
|
|
name: "status/context",
|
|
url: "https://github.com/getpaseo/paseo/status/context",
|
|
detailRef: undefined,
|
|
}),
|
|
githubDetails: null,
|
|
}),
|
|
).toEqual({
|
|
kind: "forge.change_request_check",
|
|
id: "42:check:status/context",
|
|
title: "status/context",
|
|
subtitle: "#42 Fix flaky build",
|
|
url: "https://github.com/getpaseo/paseo/status/context",
|
|
text: [
|
|
"GitHub pull request check",
|
|
"Pull request: #42 Fix flaky build",
|
|
"Pull request URL: https://github.com/getpaseo/paseo/pull/42",
|
|
"Check: status/context",
|
|
"Status: failure",
|
|
"Check URL: https://github.com/getpaseo/paseo/status/context",
|
|
].join("\n"),
|
|
});
|
|
});
|
|
|
|
it("formats GitLab failed check context as a merge request", () => {
|
|
expect(
|
|
buildPullRequestCheckContextAttachment({
|
|
...baseInput,
|
|
provider: { id: "gitlab", label: "GitLab" },
|
|
forge: "gitlab",
|
|
pullRequest: {
|
|
number: 14,
|
|
title: "Wire up pipelines",
|
|
url: "https://gitlab.com/acme/app/-/merge_requests/14",
|
|
},
|
|
check: check({
|
|
name: "pipeline",
|
|
provider: "gitlab",
|
|
url: "https://gitlab.com/acme/app/-/pipelines/99",
|
|
detailRef: undefined,
|
|
}),
|
|
githubDetails: null,
|
|
}),
|
|
).toEqual({
|
|
kind: "forge.change_request_check",
|
|
id: "14:check:pipeline",
|
|
title: "pipeline",
|
|
subtitle: "!14 Wire up pipelines",
|
|
url: "https://gitlab.com/acme/app/-/pipelines/99",
|
|
text: [
|
|
"GitLab merge request check",
|
|
"Merge request: !14 Wire up pipelines",
|
|
"Merge request URL: https://gitlab.com/acme/app/-/merge_requests/14",
|
|
"Check: pipeline",
|
|
"Status: failure",
|
|
"Check URL: https://gitlab.com/acme/app/-/pipelines/99",
|
|
].join("\n"),
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("buildPullRequestThreadContextAttachment", () => {
|
|
function thread(overrides: Partial<PrThreadEntry> = {}): PrThreadEntry {
|
|
return {
|
|
kind: "thread",
|
|
id: "thread:PRRT_1",
|
|
location: { path: "src/a.ts", line: 12, threadId: "PRRT_1", isResolved: false },
|
|
comments: [
|
|
comment({
|
|
id: "t1",
|
|
body: "This is frozen after initial registration.",
|
|
url: "https://github.com/getpaseo/paseo/pull/42#discussion_r1",
|
|
}),
|
|
comment({ id: "t2", author: "mo", age: "1d ago", body: "Good catch, fixing." }),
|
|
],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
it("bundles the whole thread conversation into one attachment", () => {
|
|
expect(buildPullRequestThreadContextAttachment({ ...baseInput, thread: thread() })).toEqual({
|
|
kind: "forge.change_request_comment",
|
|
id: "42:thread:PRRT_1",
|
|
title: "src/a.ts:12",
|
|
subtitle: "#42 Fix flaky build",
|
|
url: "https://github.com/getpaseo/paseo/pull/42#discussion_r1",
|
|
text: [
|
|
"GitHub pull request review thread",
|
|
"Pull request: #42 Fix flaky build",
|
|
"Pull request URL: https://github.com/getpaseo/paseo/pull/42",
|
|
"URL: https://github.com/getpaseo/paseo/pull/42#discussion_r1",
|
|
"Location: src/a.ts:12",
|
|
"Thread state: unresolved",
|
|
"",
|
|
"octocat (3d ago):\nThis is frozen after initial registration.",
|
|
"",
|
|
"---",
|
|
"",
|
|
"mo (1d ago):\nGood catch, fixing.",
|
|
].join("\n"),
|
|
});
|
|
});
|
|
|
|
it("returns null when no comment has a body", () => {
|
|
expect(
|
|
buildPullRequestThreadContextAttachment({
|
|
...baseInput,
|
|
thread: thread({ comments: [comment({ body: " " })] }),
|
|
}),
|
|
).toBeNull();
|
|
});
|
|
});
|