mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
* feat(app): add pull request panel context actions Attach pull request comments, reviews, and failed check details to chat context. The PR panel now lives in its own module so the activity, query, and context policies have one home. * fix(server): mark capped check details as truncated * fix(app): preserve concurrent PR panel attachments * fix(server): preserve annotation truncation state * fix(app): keep matrix check attachments distinct * fix(app): streamline pull request context attachments * fix(app): simplify pull request feedback rendering * feat(app): group pull request review threads * fix(app): tidy pull request panel boundaries * fix: use worktree cwd for dev services * fix(app): keep git query keys UI-free * feat(app): collapse resolved comments and add chat actions to headers * fix: address PR feedback and Playwright flakes * fix(app): hide check chat action without attachment scope * fix(server): dedupe inline review timeline comments
438 lines
13 KiB
TypeScript
438 lines
13 KiB
TypeScript
import { describe, expect, test } from "vitest";
|
|
import { z } from "zod";
|
|
import {
|
|
CheckoutPrStatusResponseSchema,
|
|
PullRequestTimelineRequestSchema,
|
|
PullRequestTimelineResponseSchema,
|
|
SessionInboundMessageSchema,
|
|
SessionOutboundMessageSchema,
|
|
} from "./messages.js";
|
|
|
|
describe("pull request timeline message schemas", () => {
|
|
test("requires request identity fields", () => {
|
|
expect(() =>
|
|
PullRequestTimelineRequestSchema.parse({
|
|
type: "pull_request_timeline_request",
|
|
}),
|
|
).toThrow();
|
|
});
|
|
|
|
test("parses request fields", () => {
|
|
const parsed = PullRequestTimelineRequestSchema.parse({
|
|
type: "pull_request_timeline_request",
|
|
cwd: "/tmp/repo",
|
|
prNumber: 42,
|
|
repoOwner: "getpaseo",
|
|
repoName: "paseo",
|
|
requestId: "request-1",
|
|
});
|
|
|
|
expect(parsed).toEqual({
|
|
type: "pull_request_timeline_request",
|
|
cwd: "/tmp/repo",
|
|
prNumber: 42,
|
|
repoOwner: "getpaseo",
|
|
repoName: "paseo",
|
|
requestId: "request-1",
|
|
});
|
|
});
|
|
|
|
test("parses request through the inbound message union", () => {
|
|
const parsed = SessionInboundMessageSchema.parse({
|
|
type: "pull_request_timeline_request",
|
|
cwd: "/tmp/repo",
|
|
prNumber: 42,
|
|
repoOwner: "getpaseo",
|
|
repoName: "paseo",
|
|
requestId: "request-1",
|
|
});
|
|
|
|
expect(parsed).toEqual({
|
|
type: "pull_request_timeline_request",
|
|
cwd: "/tmp/repo",
|
|
prNumber: 42,
|
|
repoOwner: "getpaseo",
|
|
repoName: "paseo",
|
|
requestId: "request-1",
|
|
});
|
|
});
|
|
|
|
test("defaults optional response payload and malformed timeline item fields", () => {
|
|
const parsed = PullRequestTimelineResponseSchema.parse({
|
|
type: "pull_request_timeline_response",
|
|
payload: {
|
|
items: [{}, { kind: "review" }],
|
|
},
|
|
});
|
|
|
|
expect(parsed).toEqual({
|
|
type: "pull_request_timeline_response",
|
|
payload: {
|
|
cwd: "",
|
|
prNumber: null,
|
|
items: [
|
|
{
|
|
id: "",
|
|
kind: "comment",
|
|
author: "unknown",
|
|
body: "",
|
|
createdAt: 0,
|
|
url: "",
|
|
},
|
|
{
|
|
id: "",
|
|
kind: "review",
|
|
author: "unknown",
|
|
body: "",
|
|
createdAt: 0,
|
|
url: "",
|
|
reviewState: "commented",
|
|
},
|
|
],
|
|
truncated: false,
|
|
error: null,
|
|
requestId: "",
|
|
githubFeaturesEnabled: true,
|
|
},
|
|
});
|
|
});
|
|
|
|
test("normalizes unknown timeline item kinds to comments", () => {
|
|
const parsed = PullRequestTimelineResponseSchema.parse({
|
|
type: "pull_request_timeline_response",
|
|
payload: {
|
|
items: [
|
|
{
|
|
id: "future-1",
|
|
kind: "unknown_future_kind",
|
|
author: "octocat",
|
|
body: "Future daemon item",
|
|
createdAt: 1710000000000,
|
|
url: "https://github.com/getpaseo/paseo/pull/42#future-1",
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
expect(parsed.payload.items).toEqual([
|
|
{
|
|
id: "future-1",
|
|
kind: "comment",
|
|
author: "octocat",
|
|
body: "Future daemon item",
|
|
createdAt: 1710000000000,
|
|
url: "https://github.com/getpaseo/paseo/pull/42#future-1",
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("parses optional author avatar, author URL, and inline comment location fields", () => {
|
|
const parsed = PullRequestTimelineResponseSchema.parse({
|
|
type: "pull_request_timeline_response",
|
|
payload: {
|
|
items: [
|
|
{
|
|
id: "review-1",
|
|
kind: "review",
|
|
author: "octocat",
|
|
authorUrl: "https://github.com/octocat",
|
|
avatarUrl: "https://avatars.githubusercontent.com/u/1?v=4",
|
|
body: "Looks good",
|
|
createdAt: 1710000000000,
|
|
url: "https://github.com/getpaseo/paseo/pull/42#pullrequestreview-1",
|
|
reviewState: "approved",
|
|
},
|
|
{
|
|
id: "thread-comment-1",
|
|
kind: "comment",
|
|
author: "hubot",
|
|
authorUrl: "https://github.com/hubot",
|
|
avatarUrl: "https://avatars.githubusercontent.com/u/2?v=4",
|
|
body: "Inline note",
|
|
createdAt: 1710000001000,
|
|
url: "https://github.com/getpaseo/paseo/pull/42#discussion_r1",
|
|
location: {
|
|
path: "packages/app/src/git/pull-request-panel/data.ts",
|
|
line: 24,
|
|
startLine: 20,
|
|
threadId: "PRRT_1",
|
|
isResolved: true,
|
|
isOutdated: false,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
expect(parsed.payload.items).toEqual([
|
|
{
|
|
id: "review-1",
|
|
kind: "review",
|
|
author: "octocat",
|
|
authorUrl: "https://github.com/octocat",
|
|
avatarUrl: "https://avatars.githubusercontent.com/u/1?v=4",
|
|
body: "Looks good",
|
|
createdAt: 1710000000000,
|
|
url: "https://github.com/getpaseo/paseo/pull/42#pullrequestreview-1",
|
|
reviewState: "approved",
|
|
},
|
|
{
|
|
id: "thread-comment-1",
|
|
kind: "comment",
|
|
author: "hubot",
|
|
authorUrl: "https://github.com/hubot",
|
|
avatarUrl: "https://avatars.githubusercontent.com/u/2?v=4",
|
|
body: "Inline note",
|
|
createdAt: 1710000001000,
|
|
url: "https://github.com/getpaseo/paseo/pull/42#discussion_r1",
|
|
location: {
|
|
path: "packages/app/src/git/pull-request-panel/data.ts",
|
|
line: 24,
|
|
startLine: 20,
|
|
threadId: "PRRT_1",
|
|
isResolved: true,
|
|
isOutdated: false,
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("parses response through the outbound message union", () => {
|
|
const parsed = SessionOutboundMessageSchema.parse({
|
|
type: "pull_request_timeline_response",
|
|
payload: {
|
|
cwd: "/tmp/repo",
|
|
prNumber: 42,
|
|
items: [
|
|
{
|
|
id: "review-1",
|
|
kind: "review",
|
|
author: "octocat",
|
|
body: "Looks good",
|
|
createdAt: 1710000000000,
|
|
url: "https://github.com/getpaseo/paseo/pull/42#pullrequestreview-1",
|
|
reviewState: "approved",
|
|
},
|
|
{
|
|
id: "comment-1",
|
|
kind: "comment",
|
|
author: "hubot",
|
|
body: "Left a note",
|
|
createdAt: 1710000001000,
|
|
url: "https://github.com/getpaseo/paseo/pull/42#issuecomment-1",
|
|
},
|
|
],
|
|
truncated: true,
|
|
error: { kind: "unknown", message: "rate limited" },
|
|
requestId: "request-1",
|
|
githubFeaturesEnabled: true,
|
|
},
|
|
});
|
|
|
|
expect(parsed.type).toBe("pull_request_timeline_response");
|
|
});
|
|
|
|
test("an old client timeline schema parses new daemon avatar and inline location fields", () => {
|
|
const oldClientPullRequestTimelineResponseSchema = z.object({
|
|
type: z.literal("pull_request_timeline_response"),
|
|
payload: z.object({
|
|
cwd: z.string(),
|
|
prNumber: z.number().nullable(),
|
|
items: z.array(
|
|
z.discriminatedUnion("kind", [
|
|
z.object({
|
|
id: z.string(),
|
|
kind: z.literal("review"),
|
|
author: z.string(),
|
|
body: z.string(),
|
|
createdAt: z.number(),
|
|
url: z.string(),
|
|
reviewState: z.enum(["approved", "changes_requested", "commented"]),
|
|
}),
|
|
z.object({
|
|
id: z.string(),
|
|
kind: z.literal("comment"),
|
|
author: z.string(),
|
|
body: z.string(),
|
|
createdAt: z.number(),
|
|
url: z.string(),
|
|
}),
|
|
]),
|
|
),
|
|
truncated: z.boolean(),
|
|
error: z.unknown().nullable(),
|
|
requestId: z.string(),
|
|
githubFeaturesEnabled: z.boolean(),
|
|
}),
|
|
});
|
|
const newDaemonPayload = PullRequestTimelineResponseSchema.parse({
|
|
type: "pull_request_timeline_response",
|
|
payload: {
|
|
cwd: "/tmp/repo",
|
|
prNumber: 42,
|
|
items: [
|
|
{
|
|
id: "comment-1",
|
|
kind: "comment",
|
|
author: "octocat",
|
|
authorUrl: "https://github.com/octocat",
|
|
avatarUrl: "https://avatars.githubusercontent.com/u/1?v=4",
|
|
body: "Inline note",
|
|
createdAt: 1710000000000,
|
|
url: "https://github.com/getpaseo/paseo/pull/42#discussion_r1",
|
|
location: {
|
|
path: "src/file.ts",
|
|
line: 12,
|
|
threadId: "PRRT_1",
|
|
},
|
|
},
|
|
],
|
|
truncated: false,
|
|
error: null,
|
|
requestId: "request-compat",
|
|
githubFeaturesEnabled: true,
|
|
},
|
|
});
|
|
|
|
expect(oldClientPullRequestTimelineResponseSchema.parse(newDaemonPayload)).toEqual({
|
|
type: "pull_request_timeline_response",
|
|
payload: {
|
|
cwd: "/tmp/repo",
|
|
prNumber: 42,
|
|
items: [
|
|
{
|
|
id: "comment-1",
|
|
kind: "comment",
|
|
author: "octocat",
|
|
body: "Inline note",
|
|
createdAt: 1710000000000,
|
|
url: "https://github.com/getpaseo/paseo/pull/42#discussion_r1",
|
|
},
|
|
],
|
|
truncated: false,
|
|
error: null,
|
|
requestId: "request-compat",
|
|
githubFeaturesEnabled: true,
|
|
},
|
|
});
|
|
});
|
|
|
|
test("normalizes future error kinds to unknown", () => {
|
|
const parsed = PullRequestTimelineResponseSchema.parse({
|
|
type: "pull_request_timeline_response",
|
|
payload: {
|
|
error: { kind: "future_kind", message: "x" },
|
|
},
|
|
});
|
|
|
|
expect(parsed.payload.error).toEqual({ kind: "unknown", message: "x" });
|
|
});
|
|
|
|
test.each(["rate_limited", "", undefined])(
|
|
"normalizes malformed error kind %j to unknown",
|
|
(kind) => {
|
|
const parsed = PullRequestTimelineResponseSchema.parse({
|
|
type: "pull_request_timeline_response",
|
|
payload: {
|
|
error: { kind },
|
|
},
|
|
});
|
|
|
|
expect(parsed.payload.error).toEqual({ kind: "unknown", message: "" });
|
|
},
|
|
);
|
|
});
|
|
|
|
describe("checkout PR status compatibility", () => {
|
|
test("an old client schema parses a new daemon checkout PR status response", () => {
|
|
const oldClientCheckoutPrStatusResponseSchema = z.object({
|
|
type: z.literal("checkout_pr_status_response"),
|
|
payload: z.object({
|
|
cwd: z.string(),
|
|
status: z
|
|
.object({
|
|
url: z.string(),
|
|
title: z.string(),
|
|
state: z.string(),
|
|
baseRefName: z.string(),
|
|
headRefName: z.string(),
|
|
isMerged: z.boolean(),
|
|
checks: z
|
|
.array(
|
|
z.object({
|
|
name: z.string(),
|
|
status: z.string(),
|
|
url: z.string().nullable(),
|
|
}),
|
|
)
|
|
.optional()
|
|
.default([]),
|
|
checksStatus: z.string().optional(),
|
|
reviewDecision: z.string().nullable().optional(),
|
|
})
|
|
.nullable(),
|
|
githubFeaturesEnabled: z.boolean(),
|
|
error: z.unknown().nullable(),
|
|
requestId: z.string(),
|
|
}),
|
|
});
|
|
const newDaemonPayload = CheckoutPrStatusResponseSchema.parse({
|
|
type: "checkout_pr_status_response",
|
|
payload: {
|
|
cwd: "/tmp/repo",
|
|
status: {
|
|
number: 42,
|
|
url: "https://github.com/getpaseo/paseo/pull/42",
|
|
title: "Wire real PR pane data",
|
|
state: "OPEN",
|
|
baseRefName: "main",
|
|
headRefName: "feature/pr-pane",
|
|
isMerged: false,
|
|
isDraft: true,
|
|
checks: [
|
|
{
|
|
name: "server-tests",
|
|
status: "success",
|
|
url: "https://github.com/getpaseo/paseo/actions/runs/123",
|
|
workflow: "Server CI",
|
|
duration: "2m 14s",
|
|
},
|
|
],
|
|
checksStatus: "success",
|
|
reviewDecision: "APPROVED",
|
|
},
|
|
githubFeaturesEnabled: true,
|
|
error: null,
|
|
requestId: "request-compat",
|
|
},
|
|
});
|
|
|
|
expect(oldClientCheckoutPrStatusResponseSchema.parse(newDaemonPayload)).toEqual({
|
|
type: "checkout_pr_status_response",
|
|
payload: {
|
|
cwd: "/tmp/repo",
|
|
status: {
|
|
url: "https://github.com/getpaseo/paseo/pull/42",
|
|
title: "Wire real PR pane data",
|
|
state: "OPEN",
|
|
baseRefName: "main",
|
|
headRefName: "feature/pr-pane",
|
|
isMerged: false,
|
|
checks: [
|
|
{
|
|
name: "server-tests",
|
|
status: "success",
|
|
url: "https://github.com/getpaseo/paseo/actions/runs/123",
|
|
},
|
|
],
|
|
checksStatus: "success",
|
|
reviewDecision: "APPROVED",
|
|
},
|
|
githubFeaturesEnabled: true,
|
|
error: null,
|
|
requestId: "request-compat",
|
|
},
|
|
});
|
|
});
|
|
});
|