refactor(server): extract checkout status projection (#838)
This commit is contained in:
@@ -0,0 +1,38 @@
|
|||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
|
import { CheckoutPrStatusSchema } from "../../shared/messages.js";
|
||||||
|
import { normalizeCheckoutPrStatusPayload } from "./status-projection.js";
|
||||||
|
|
||||||
|
describe("checkout status projection", () => {
|
||||||
|
test("includes repository identity fields on the PR status wire payload", () => {
|
||||||
|
const payload = normalizeCheckoutPrStatusPayload({
|
||||||
|
number: 123,
|
||||||
|
repoOwner: "internal-owner",
|
||||||
|
repoName: "internal-repo",
|
||||||
|
url: "https://github.com/getpaseo/paseo/pull/123",
|
||||||
|
title: "Ship PR pane",
|
||||||
|
state: "open",
|
||||||
|
baseRefName: "main",
|
||||||
|
headRefName: "feature/pr-pane",
|
||||||
|
isMerged: false,
|
||||||
|
isDraft: true,
|
||||||
|
mergeable: "MERGEABLE",
|
||||||
|
checks: [
|
||||||
|
{
|
||||||
|
name: "typecheck",
|
||||||
|
status: "success",
|
||||||
|
url: "https://github.com/getpaseo/paseo/actions/runs/1",
|
||||||
|
workflow: "CI",
|
||||||
|
duration: "1m 20s",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
checksStatus: "success",
|
||||||
|
reviewDecision: "approved",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(payload).toHaveProperty("repoOwner", "internal-owner");
|
||||||
|
expect(payload).toHaveProperty("repoName", "internal-repo");
|
||||||
|
expect(payload).toHaveProperty("mergeable", "MERGEABLE");
|
||||||
|
expect(CheckoutPrStatusSchema.parse(payload)).toEqual(payload);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import type {
|
||||||
|
CheckoutPrStatusResponse,
|
||||||
|
CheckoutStatusResponse,
|
||||||
|
SessionOutboundMessage,
|
||||||
|
} from "../../shared/messages.js";
|
||||||
|
import type { WorkspaceGitRuntimeSnapshot } from "../workspace-git-service.js";
|
||||||
|
|
||||||
|
type CheckoutPrStatusPayload = Extract<
|
||||||
|
SessionOutboundMessage,
|
||||||
|
{ type: "checkout_pr_status_response" }
|
||||||
|
>["payload"];
|
||||||
|
type CheckoutPrStatusPayloadStatus = NonNullable<CheckoutPrStatusPayload["status"]>;
|
||||||
|
|
||||||
|
export function buildCheckoutStatusPayloadFromSnapshot({
|
||||||
|
cwd,
|
||||||
|
requestId,
|
||||||
|
snapshot,
|
||||||
|
}: {
|
||||||
|
cwd: string;
|
||||||
|
requestId: string;
|
||||||
|
snapshot: WorkspaceGitRuntimeSnapshot;
|
||||||
|
}): CheckoutStatusResponse["payload"] {
|
||||||
|
if (!snapshot.git.isGit) {
|
||||||
|
return {
|
||||||
|
cwd,
|
||||||
|
isGit: false,
|
||||||
|
repoRoot: null,
|
||||||
|
currentBranch: null,
|
||||||
|
isDirty: null,
|
||||||
|
baseRef: null,
|
||||||
|
aheadBehind: null,
|
||||||
|
aheadOfOrigin: null,
|
||||||
|
behindOfOrigin: null,
|
||||||
|
hasRemote: false,
|
||||||
|
remoteUrl: null,
|
||||||
|
isPaseoOwnedWorktree: false,
|
||||||
|
error: null,
|
||||||
|
requestId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.git.repoRoot === null || snapshot.git.isDirty === null) {
|
||||||
|
throw new Error("Workspace git snapshot is missing required checkout status fields");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.git.isPaseoOwnedWorktree) {
|
||||||
|
if (snapshot.git.mainRepoRoot === null || snapshot.git.baseRef === null) {
|
||||||
|
throw new Error("Workspace git snapshot is missing required worktree status fields");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
cwd,
|
||||||
|
isGit: true,
|
||||||
|
repoRoot: snapshot.git.repoRoot,
|
||||||
|
mainRepoRoot: snapshot.git.mainRepoRoot,
|
||||||
|
currentBranch: snapshot.git.currentBranch ?? null,
|
||||||
|
isDirty: snapshot.git.isDirty,
|
||||||
|
baseRef: snapshot.git.baseRef,
|
||||||
|
aheadBehind: snapshot.git.aheadBehind ?? null,
|
||||||
|
aheadOfOrigin: snapshot.git.aheadOfOrigin ?? null,
|
||||||
|
behindOfOrigin: snapshot.git.behindOfOrigin ?? null,
|
||||||
|
hasRemote: snapshot.git.hasRemote,
|
||||||
|
remoteUrl: snapshot.git.remoteUrl,
|
||||||
|
isPaseoOwnedWorktree: true,
|
||||||
|
error: null,
|
||||||
|
requestId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
cwd,
|
||||||
|
isGit: true,
|
||||||
|
repoRoot: snapshot.git.repoRoot,
|
||||||
|
mainRepoRoot: snapshot.git.mainRepoRoot,
|
||||||
|
currentBranch: snapshot.git.currentBranch ?? null,
|
||||||
|
isDirty: snapshot.git.isDirty,
|
||||||
|
baseRef: snapshot.git.baseRef ?? null,
|
||||||
|
aheadBehind: snapshot.git.aheadBehind ?? null,
|
||||||
|
aheadOfOrigin: snapshot.git.aheadOfOrigin ?? null,
|
||||||
|
behindOfOrigin: snapshot.git.behindOfOrigin ?? null,
|
||||||
|
hasRemote: snapshot.git.hasRemote,
|
||||||
|
remoteUrl: snapshot.git.remoteUrl,
|
||||||
|
isPaseoOwnedWorktree: false,
|
||||||
|
error: null,
|
||||||
|
requestId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCheckoutPrStatusPayloadFromSnapshot({
|
||||||
|
cwd,
|
||||||
|
requestId,
|
||||||
|
snapshot,
|
||||||
|
}: {
|
||||||
|
cwd: string;
|
||||||
|
requestId: string;
|
||||||
|
snapshot: WorkspaceGitRuntimeSnapshot;
|
||||||
|
}): CheckoutPrStatusResponse["payload"] {
|
||||||
|
return {
|
||||||
|
cwd,
|
||||||
|
status: normalizeCheckoutPrStatusPayload(snapshot.github.pullRequest),
|
||||||
|
githubFeaturesEnabled: snapshot.github.featuresEnabled,
|
||||||
|
error: snapshot.github.error
|
||||||
|
? {
|
||||||
|
code: "UNKNOWN",
|
||||||
|
message: snapshot.github.error.message,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
requestId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeCheckoutPrStatusPayload(
|
||||||
|
status: WorkspaceGitRuntimeSnapshot["github"]["pullRequest"],
|
||||||
|
): CheckoutPrStatusPayloadStatus | null {
|
||||||
|
if (!status) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
number: status.number,
|
||||||
|
url: status.url,
|
||||||
|
title: status.title,
|
||||||
|
state: status.state,
|
||||||
|
repoOwner: status.repoOwner,
|
||||||
|
repoName: status.repoName,
|
||||||
|
baseRefName: status.baseRefName,
|
||||||
|
headRefName: status.headRefName,
|
||||||
|
isMerged: status.isMerged,
|
||||||
|
isDraft: status.isDraft ?? false,
|
||||||
|
mergeable: status.mergeable ?? "UNKNOWN",
|
||||||
|
checks: status.checks ?? [],
|
||||||
|
checksStatus: status.checksStatus,
|
||||||
|
reviewDecision: status.reviewDecision,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -6,10 +6,9 @@ import { join } from "path";
|
|||||||
import pino from "pino";
|
import pino from "pino";
|
||||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
import { CheckoutPrStatusSchema } from "../shared/messages.js";
|
|
||||||
import type { WorkspaceDescriptorPayload } from "../shared/messages.js";
|
import type { WorkspaceDescriptorPayload } from "../shared/messages.js";
|
||||||
import { decodeFileTransferFrame, FileTransferOpcode } from "../shared/binary-frames/index.js";
|
import { decodeFileTransferFrame, FileTransferOpcode } from "../shared/binary-frames/index.js";
|
||||||
import { normalizeCheckoutPrStatusPayload, Session } from "./session.js";
|
import { Session } from "./session.js";
|
||||||
import { StructuredAgentFallbackError } from "./agent/agent-response-loop.js";
|
import { StructuredAgentFallbackError } from "./agent/agent-response-loop.js";
|
||||||
import type {
|
import type {
|
||||||
AgentClient,
|
AgentClient,
|
||||||
@@ -1191,40 +1190,6 @@ describe("session agent import", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("session PR status payload normalization", () => {
|
|
||||||
test("includes repository identity fields on the wire", () => {
|
|
||||||
const payload = normalizeCheckoutPrStatusPayload({
|
|
||||||
number: 123,
|
|
||||||
repoOwner: "internal-owner",
|
|
||||||
repoName: "internal-repo",
|
|
||||||
url: "https://github.com/getpaseo/paseo/pull/123",
|
|
||||||
title: "Ship PR pane",
|
|
||||||
state: "open",
|
|
||||||
baseRefName: "main",
|
|
||||||
headRefName: "feature/pr-pane",
|
|
||||||
isMerged: false,
|
|
||||||
isDraft: true,
|
|
||||||
mergeable: "MERGEABLE",
|
|
||||||
checks: [
|
|
||||||
{
|
|
||||||
name: "typecheck",
|
|
||||||
status: "success",
|
|
||||||
url: "https://github.com/getpaseo/paseo/actions/runs/1",
|
|
||||||
workflow: "CI",
|
|
||||||
duration: "1m 20s",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
checksStatus: "success",
|
|
||||||
reviewDecision: "approved",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(payload).toHaveProperty("repoOwner", "internal-owner");
|
|
||||||
expect(payload).toHaveProperty("repoName", "internal-repo");
|
|
||||||
expect(payload).toHaveProperty("mergeable", "MERGEABLE");
|
|
||||||
expect(CheckoutPrStatusSchema.parse(payload)).toEqual(payload);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("session provider refresh cwd routing", () => {
|
describe("session provider refresh cwd routing", () => {
|
||||||
test("routes no-cwd provider snapshot refreshes through settings refresh", async () => {
|
test("routes no-cwd provider snapshot refreshes through settings refresh", async () => {
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ import {
|
|||||||
type FileExplorerRequest,
|
type FileExplorerRequest,
|
||||||
type FileDownloadTokenRequest,
|
type FileDownloadTokenRequest,
|
||||||
type GitSetupOptions,
|
type GitSetupOptions,
|
||||||
type CheckoutPrStatusResponse,
|
|
||||||
type CheckoutStatusResponse,
|
|
||||||
type StartWorkspaceScriptRequest,
|
type StartWorkspaceScriptRequest,
|
||||||
type CloseItemsRequest,
|
type CloseItemsRequest,
|
||||||
type SubscribeCheckoutDiffRequest,
|
type SubscribeCheckoutDiffRequest,
|
||||||
@@ -192,6 +190,10 @@ import { expandTilde } from "../utils/path.js";
|
|||||||
import { searchHomeDirectories, searchWorkspaceEntries } from "../utils/directory-suggestions.js";
|
import { searchHomeDirectories, searchWorkspaceEntries } from "../utils/directory-suggestions.js";
|
||||||
import { toCheckoutError } from "./checkout-git-utils.js";
|
import { toCheckoutError } from "./checkout-git-utils.js";
|
||||||
import { CheckoutDiffManager } from "./checkout-diff-manager.js";
|
import { CheckoutDiffManager } from "./checkout-diff-manager.js";
|
||||||
|
import {
|
||||||
|
buildCheckoutPrStatusPayloadFromSnapshot,
|
||||||
|
buildCheckoutStatusPayloadFromSnapshot,
|
||||||
|
} from "./checkout/status-projection.js";
|
||||||
import type { LocalSpeechModelId } from "./speech/providers/local/models.js";
|
import type { LocalSpeechModelId } from "./speech/providers/local/models.js";
|
||||||
import { toResolver, type Resolvable } from "./speech/provider-resolver.js";
|
import { toResolver, type Resolvable } from "./speech/provider-resolver.js";
|
||||||
import type { SpeechReadinessSnapshot, SpeechReadinessState } from "./speech/speech-runtime.js";
|
import type { SpeechReadinessSnapshot, SpeechReadinessState } from "./speech/speech-runtime.js";
|
||||||
@@ -636,11 +638,6 @@ export type SessionLifecycleIntent =
|
|||||||
reason?: string;
|
reason?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CheckoutPrStatusPayload = Extract<
|
|
||||||
SessionOutboundMessage,
|
|
||||||
{ type: "checkout_pr_status_response" }
|
|
||||||
>["payload"];
|
|
||||||
type CheckoutPrStatusPayloadStatus = NonNullable<CheckoutPrStatusPayload["status"]>;
|
|
||||||
type PullRequestTimelinePayload = Extract<
|
type PullRequestTimelinePayload = Extract<
|
||||||
SessionOutboundMessage,
|
SessionOutboundMessage,
|
||||||
{ type: "pull_request_timeline_response" }
|
{ type: "pull_request_timeline_response" }
|
||||||
@@ -4489,7 +4486,7 @@ export class Session {
|
|||||||
const snapshot = await this.workspaceGitService.getSnapshot(resolvedCwd);
|
const snapshot = await this.workspaceGitService.getSnapshot(resolvedCwd);
|
||||||
this.emit({
|
this.emit({
|
||||||
type: "checkout_status_response",
|
type: "checkout_status_response",
|
||||||
payload: this.buildCheckoutStatusPayloadFromSnapshot({
|
payload: buildCheckoutStatusPayloadFromSnapshot({
|
||||||
cwd,
|
cwd,
|
||||||
requestId,
|
requestId,
|
||||||
snapshot,
|
snapshot,
|
||||||
@@ -4838,116 +4835,18 @@ export class Session {
|
|||||||
this.checkoutDiffSubscriptions.delete(msg.subscriptionId);
|
this.checkoutDiffSubscriptions.delete(msg.subscriptionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildCheckoutStatusPayloadFromSnapshot({
|
|
||||||
cwd,
|
|
||||||
requestId,
|
|
||||||
snapshot,
|
|
||||||
}: {
|
|
||||||
cwd: string;
|
|
||||||
requestId: string;
|
|
||||||
snapshot: WorkspaceGitRuntimeSnapshot;
|
|
||||||
}): CheckoutStatusResponse["payload"] {
|
|
||||||
if (!snapshot.git.isGit) {
|
|
||||||
return {
|
|
||||||
cwd,
|
|
||||||
isGit: false,
|
|
||||||
repoRoot: null,
|
|
||||||
currentBranch: null,
|
|
||||||
isDirty: null,
|
|
||||||
baseRef: null,
|
|
||||||
aheadBehind: null,
|
|
||||||
aheadOfOrigin: null,
|
|
||||||
behindOfOrigin: null,
|
|
||||||
hasRemote: false,
|
|
||||||
remoteUrl: null,
|
|
||||||
isPaseoOwnedWorktree: false,
|
|
||||||
error: null,
|
|
||||||
requestId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapshot.git.repoRoot === null || snapshot.git.isDirty === null) {
|
|
||||||
throw new Error("Workspace git snapshot is missing required checkout status fields");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapshot.git.isPaseoOwnedWorktree) {
|
|
||||||
if (snapshot.git.mainRepoRoot === null || snapshot.git.baseRef === null) {
|
|
||||||
throw new Error("Workspace git snapshot is missing required worktree status fields");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
cwd,
|
|
||||||
isGit: true,
|
|
||||||
repoRoot: snapshot.git.repoRoot,
|
|
||||||
mainRepoRoot: snapshot.git.mainRepoRoot,
|
|
||||||
currentBranch: snapshot.git.currentBranch ?? null,
|
|
||||||
isDirty: snapshot.git.isDirty,
|
|
||||||
baseRef: snapshot.git.baseRef,
|
|
||||||
aheadBehind: snapshot.git.aheadBehind ?? null,
|
|
||||||
aheadOfOrigin: snapshot.git.aheadOfOrigin ?? null,
|
|
||||||
behindOfOrigin: snapshot.git.behindOfOrigin ?? null,
|
|
||||||
hasRemote: snapshot.git.hasRemote,
|
|
||||||
remoteUrl: snapshot.git.remoteUrl,
|
|
||||||
isPaseoOwnedWorktree: true,
|
|
||||||
error: null,
|
|
||||||
requestId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
cwd,
|
|
||||||
isGit: true,
|
|
||||||
repoRoot: snapshot.git.repoRoot,
|
|
||||||
mainRepoRoot: snapshot.git.mainRepoRoot,
|
|
||||||
currentBranch: snapshot.git.currentBranch ?? null,
|
|
||||||
isDirty: snapshot.git.isDirty,
|
|
||||||
baseRef: snapshot.git.baseRef ?? null,
|
|
||||||
aheadBehind: snapshot.git.aheadBehind ?? null,
|
|
||||||
aheadOfOrigin: snapshot.git.aheadOfOrigin ?? null,
|
|
||||||
behindOfOrigin: snapshot.git.behindOfOrigin ?? null,
|
|
||||||
hasRemote: snapshot.git.hasRemote,
|
|
||||||
remoteUrl: snapshot.git.remoteUrl,
|
|
||||||
isPaseoOwnedWorktree: false,
|
|
||||||
error: null,
|
|
||||||
requestId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private buildCheckoutPrStatusPayloadFromSnapshot({
|
|
||||||
cwd,
|
|
||||||
requestId,
|
|
||||||
snapshot,
|
|
||||||
}: {
|
|
||||||
cwd: string;
|
|
||||||
requestId: string;
|
|
||||||
snapshot: WorkspaceGitRuntimeSnapshot;
|
|
||||||
}): CheckoutPrStatusResponse["payload"] {
|
|
||||||
return {
|
|
||||||
cwd,
|
|
||||||
status: normalizeCheckoutPrStatusPayload(snapshot.github.pullRequest),
|
|
||||||
githubFeaturesEnabled: snapshot.github.featuresEnabled,
|
|
||||||
error: snapshot.github.error
|
|
||||||
? {
|
|
||||||
code: "UNKNOWN",
|
|
||||||
message: snapshot.github.error.message,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
requestId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private emitCheckoutStatusUpdate(cwd: string, snapshot: WorkspaceGitRuntimeSnapshot): void {
|
private emitCheckoutStatusUpdate(cwd: string, snapshot: WorkspaceGitRuntimeSnapshot): void {
|
||||||
try {
|
try {
|
||||||
const requestId = `subscription:${cwd}`;
|
const requestId = `subscription:${cwd}`;
|
||||||
this.emit({
|
this.emit({
|
||||||
type: "checkout_status_update",
|
type: "checkout_status_update",
|
||||||
payload: {
|
payload: {
|
||||||
...this.buildCheckoutStatusPayloadFromSnapshot({
|
...buildCheckoutStatusPayloadFromSnapshot({
|
||||||
cwd,
|
cwd,
|
||||||
requestId,
|
requestId,
|
||||||
snapshot,
|
snapshot,
|
||||||
}),
|
}),
|
||||||
prStatus: this.buildCheckoutPrStatusPayloadFromSnapshot({
|
prStatus: buildCheckoutPrStatusPayloadFromSnapshot({
|
||||||
cwd,
|
cwd,
|
||||||
requestId,
|
requestId,
|
||||||
snapshot,
|
snapshot,
|
||||||
@@ -5382,18 +5281,11 @@ export class Session {
|
|||||||
const snapshot = await this.workspaceGitService.getSnapshot(cwd);
|
const snapshot = await this.workspaceGitService.getSnapshot(cwd);
|
||||||
this.emit({
|
this.emit({
|
||||||
type: "checkout_pr_status_response",
|
type: "checkout_pr_status_response",
|
||||||
payload: {
|
payload: buildCheckoutPrStatusPayloadFromSnapshot({
|
||||||
cwd,
|
cwd,
|
||||||
status: normalizeCheckoutPrStatusPayload(snapshot.github.pullRequest),
|
|
||||||
githubFeaturesEnabled: snapshot.github.featuresEnabled,
|
|
||||||
error: snapshot.github.error
|
|
||||||
? {
|
|
||||||
code: "UNKNOWN",
|
|
||||||
message: snapshot.github.error.message,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
requestId,
|
requestId,
|
||||||
},
|
snapshot,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.emit({
|
this.emit({
|
||||||
@@ -8895,30 +8787,6 @@ export class Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeCheckoutPrStatusPayload(
|
|
||||||
status: WorkspaceGitRuntimeSnapshot["github"]["pullRequest"],
|
|
||||||
): CheckoutPrStatusPayloadStatus | null {
|
|
||||||
if (!status) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
number: status.number,
|
|
||||||
url: status.url,
|
|
||||||
title: status.title,
|
|
||||||
state: status.state,
|
|
||||||
repoOwner: status.repoOwner,
|
|
||||||
repoName: status.repoName,
|
|
||||||
baseRefName: status.baseRefName,
|
|
||||||
headRefName: status.headRefName,
|
|
||||||
isMerged: status.isMerged,
|
|
||||||
isDraft: status.isDraft ?? false,
|
|
||||||
mergeable: status.mergeable ?? "UNKNOWN",
|
|
||||||
checks: status.checks ?? [],
|
|
||||||
checksStatus: status.checksStatus,
|
|
||||||
reviewDecision: status.reviewDecision,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function isValidPullRequestTimelineIdentity(options: {
|
function isValidPullRequestTimelineIdentity(options: {
|
||||||
prNumber: number;
|
prNumber: number;
|
||||||
repoOwner: string;
|
repoOwner: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user