Skip to content
44 changes: 44 additions & 0 deletions apps/server/src/git/linkCreatedPullRequest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
} from "../orchestration-v2/Orchestrator.ts";
import { v2PullRequestThread } from "../orchestration-v2/testkit/pullRequestFixtures.ts";
import { ProjectionSnapshotQuery } from "../orchestration/Services/ProjectionSnapshotQuery.ts";
import { refreshPushedPullRequests } from "./refreshPushedPullRequests.ts";
import { PullRequestService } from "../pullRequest/PullRequestService.ts";
import { createdPullRequestKey, linkCreatedPullRequest } from "./linkCreatedPullRequest.ts";

const PROJECT_ID = ProjectId.make("project-1");
Expand Down Expand Up @@ -229,3 +231,45 @@ describe("linkCreatedPullRequest", () => {
}),
);
});

it.effect(
"refreshes PR readers after a push from a thread or project, but not a local commit",
() =>
Effect.gen(function* () {
const refreshed: string[] = [];
const dependencies = Layer.mergeAll(
Layer.mock(OrchestratorV2)({
getThreadShell: () => Effect.succeed(v2PullRequestThread(thread)),
}),
Layer.mock(ProjectionSnapshotQuery)({
getProjectShellsWithoutEnrichment: () => Effect.succeed([project]),
}),
Layer.mock(PullRequestService)({
refreshAfterTurn: (id) =>
Effect.sync(() => {
refreshed.push(id);
}),
}),
);
yield* refreshPushedPullRequests(
{ cwd: "/worktree", threadId: THREAD_ID },
{ push: { status: "pushed" } },
).pipe(Effect.provide(dependencies));
yield* refreshPushedPullRequests(
{ cwd: project.workspaceRoot },
{ push: { status: "pushed" } },
).pipe(Effect.provide(dependencies));
yield* refreshPushedPullRequests({ cwd: "/unrelated" }, { push: { status: "pushed" } }).pipe(
Effect.provide(dependencies),
);
yield* refreshPushedPullRequests(
{ cwd: project.workspaceRoot, threadId: THREAD_ID },
{ push: { status: "skipped_not_requested" } },
).pipe(Effect.provide(dependencies));
yield* refreshPushedPullRequests(
{ cwd: "/draft-worktree", projectId: PROJECT_ID },
{ push: { status: "pushed" } },
).pipe(Effect.provide(dependencies));
expect(refreshed).toEqual([PROJECT_ID, PROJECT_ID, PROJECT_ID]);
}),
);
36 changes: 36 additions & 0 deletions apps/server/src/git/refreshPushedPullRequests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { GitRunStackedActionInput, GitRunStackedActionResult } from "@t3tools/contracts";
import * as Effect from "effect/Effect";

import * as OrchestratorV2 from "../orchestration-v2/Orchestrator.ts";
import * as ProjectionSnapshotQuery from "../orchestration/Services/ProjectionSnapshotQuery.ts";
import * as PullRequestService from "../pullRequest/PullRequestService.ts";

export const refreshPushedPullRequests = Effect.fn("refreshPushedPullRequests")(
function* (
input: Pick<GitRunStackedActionInput, "cwd" | "threadId" | "projectId">,
result: Pick<GitRunStackedActionResult, "push">,
) {
if (result.push.status !== "pushed") return;
const pullRequests = yield* PullRequestService.PullRequestService;
if (input.threadId !== undefined) {
const engine = yield* OrchestratorV2.OrchestratorV2;
const thread = yield* engine.getThreadShell(input.threadId);
if (thread !== null) {
yield* pullRequests.refreshAfterTurn(thread.projectId);
return;
}
}
if (input.projectId !== undefined) {
yield* pullRequests.refreshAfterTurn(input.projectId);
return;
}
const snapshots = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery;
const projects = yield* snapshots.getProjectShellsWithoutEnrichment();
yield* Effect.forEach(
projects.filter((project) => project.workspaceRoot === input.cwd),
(project) => pullRequests.refreshAfterTurn(project.id),
{ discard: true },
);
},
Effect.ignore({ log: true }),
);
5 changes: 5 additions & 0 deletions apps/server/src/pullRequest/GitHubPullRequestCli.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { makeChecksRevalidator } from "./gitHubConditionalChecks.ts";
import { runGitHubStackAction, type GitHubStackActionError } from "./githubStackActions.ts";
import * as Context from "effect/Context";
import * as Clock from "effect/Clock";
Expand Down Expand Up @@ -517,6 +518,8 @@ export class GitHubPullRequestCli extends Context.Service<
readonly number: number;
}) => Effect.Effect<ProviderChangeRequestSummary, GitHubPullRequestCliError>;

readonly revalidateChecks: Effect.Success<typeof makeChecksRevalidator>;

readonly getPullRequestDetail: (input: {
readonly cwd: string;
readonly repository: string;
Expand Down Expand Up @@ -1065,6 +1068,7 @@ function actionArgs(
export const make = Effect.gen(function* () {
const github = yield* GitHubCli.GitHubCli;
const graphQlBudget = yield* GitHubGraphQlBudget.GitHubGraphQlBudget;
const revalidateChecks = yield* makeChecksRevalidator;
const routingIdentities = new Map<
string,
{
Expand Down Expand Up @@ -1729,6 +1733,7 @@ export const make = Effect.gen(function* () {

return GitHubPullRequestCli.of({
withVerifiedCredential,
revalidateChecks,
getRoutingIdentity,
getViewerLogin: (input) =>
getRoutingIdentity(input).pipe(Effect.map((identity) => identity.viewer)),
Expand Down
17 changes: 17 additions & 0 deletions apps/server/src/pullRequest/GitHubPullRequestProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ it.effect("maps credential verification failures without relabeling operation fa
const provider = yield* make.pipe(
Effect.provide(
Layer.mock(GitHubPullRequestCli.GitHubPullRequestCli)({
revalidateChecks: (_input, read) => read,
withVerifiedCredential: (_input, use) =>
verificationFails
? Effect.fail(
Expand Down Expand Up @@ -74,6 +75,7 @@ it.effect("refreshes checks without permissions or comparison reads", () =>
const provider = yield* make.pipe(
Effect.provide(
Layer.mock(GitHubPullRequestCli.GitHubPullRequestCli)({
revalidateChecks: (_input, read) => read,
getPullRequestDetail: () =>
Effect.sync(() => {
reads++;
Expand Down Expand Up @@ -116,6 +118,7 @@ it.effect("uses one narrow read for a linked pull request summary", () =>
const provider = yield* make.pipe(
Effect.provide(
Layer.mock(GitHubPullRequestCli.GitHubPullRequestCli)({
revalidateChecks: (_input, read) => read,
getPullRequestSummary: () =>
Effect.sync(() => {
summaryReads += 1;
Expand Down Expand Up @@ -165,6 +168,7 @@ it.effect("declares host-native stacks and passes the one the CLI reads through"
const provider = yield* make.pipe(
Effect.provide(
Layer.mock(GitHubPullRequestCli.GitHubPullRequestCli)({
revalidateChecks: (_input, read) => read,
getPullRequestStack: (input) => Effect.succeed(input.number === 7 ? stack : null),
}),
),
Expand All @@ -184,6 +188,7 @@ it.effect("reports a failed stack read against its own operation", () =>
const provider = yield* make.pipe(
Effect.provide(
Layer.mock(GitHubPullRequestCli.GitHubPullRequestCli)({
revalidateChecks: (_input, read) => read,
getPullRequestStack: () =>
Effect.fail(
new GitHubPullRequestCli.GitHubPullRequestReadError({
Expand Down Expand Up @@ -337,6 +342,7 @@ describe("gitHubViewerPermissions", () => {
}).pipe(
Effect.provide(
Layer.mock(GitHubPullRequestCli.GitHubPullRequestCli)({
revalidateChecks: (_input, read) => read,
getPullRequestDetail: () =>
Effect.succeed({
...coreFields,
Expand Down Expand Up @@ -430,6 +436,7 @@ describe("gitHubViewerPermissions", () => {
}).pipe(
Effect.provide(
Layer.mock(GitHubPullRequestCli.GitHubPullRequestCli)({
revalidateChecks: (_input, read) => read,
getPullRequestDetail: () =>
Effect.succeed({
...coreFields,
Expand Down Expand Up @@ -576,6 +583,7 @@ it.effect("does not classify same-repository gates as fork workflow approvals",
}).pipe(
Effect.provide(
Layer.mock(GitHubPullRequestCli.GitHubPullRequestCli)({
revalidateChecks: (_input, read) => read,
getPullRequestDetail: () => Effect.succeed({ ...openDetail, isCrossRepository: false }),
getPullRequestBaseComparison: () => Effect.succeed({ behindBy: 0, viewerCanUpdate: true }),
listWorkflowRunsRequiringApproval: () =>
Expand Down Expand Up @@ -615,6 +623,7 @@ it.effect("keeps an unsafe workflow approval scope visible as unknown", () =>
}).pipe(
Effect.provide(
Layer.mock(GitHubPullRequestCli.GitHubPullRequestCli)({
revalidateChecks: (_input, read) => read,
getPullRequestDetail: () => Effect.succeed(openDetail),
getPullRequestBaseComparison: () => Effect.succeed({ behindBy: 0, viewerCanUpdate: true }),
listWorkflowRunsRequiringApproval: () =>
Expand Down Expand Up @@ -658,6 +667,7 @@ it.effect("propagates workflow discovery rate limits", () =>
}).pipe(
Effect.provide(
Layer.mock(GitHubPullRequestCli.GitHubPullRequestCli)({
revalidateChecks: (_input, read) => read,
getPullRequestDetail: () => Effect.succeed(openDetail),
getPullRequestBaseComparison: () => Effect.succeed({ behindBy: 0, viewerCanUpdate: true }),
listWorkflowRunsRequiringApproval: () =>
Expand Down Expand Up @@ -700,6 +710,7 @@ describe("getViewerPermissions", () => {
}).pipe(
Effect.provide(
Layer.mock(GitHubPullRequestCli.GitHubPullRequestCli)({
revalidateChecks: (_input, read) => read,
getPullRequestDetail: () => Effect.die("Unexpected detail read"),
getPullRequestBaseComparison: () => Effect.die("Unexpected comparison read"),
getViewerAccess: () =>
Expand All @@ -725,6 +736,7 @@ describe("getViewerPermissions", () => {
}>,
) =>
Layer.mock(GitHubPullRequestCli.GitHubPullRequestCli)({
revalidateChecks: (_input, read) => read,
getPullRequestDetail: () => Effect.succeed(openDetail),
getPullRequestBaseComparison: () => comparison,
getViewerAccess: () =>
Expand Down Expand Up @@ -771,6 +783,7 @@ describe("getViewerPermissions", () => {
}).pipe(
Effect.provide(
Layer.mock(GitHubPullRequestCli.GitHubPullRequestCli)({
revalidateChecks: (_input, read) => read,
getPullRequestDetail: () => Effect.succeed(openDetail),
getPullRequestBaseComparison: (input) =>
Effect.sync(() => {
Expand Down Expand Up @@ -810,6 +823,7 @@ describe("getViewerPermissions", () => {
}).pipe(
Effect.provide(
Layer.mock(GitHubPullRequestCli.GitHubPullRequestCli)({
revalidateChecks: (_input, read) => read,
getPullRequestDetail: () => Effect.succeed(openDetail),
getPullRequestBaseComparison: () =>
Effect.fail(
Expand Down Expand Up @@ -879,6 +893,7 @@ describe("getChangeRequest commits", () => {

const layerWith = (commits: GitHubReviewThreadComments["commits"]) =>
Layer.mock(GitHubPullRequestCli.GitHubPullRequestCli)({
revalidateChecks: (_input, read) => read,
getPullRequestActivity: () =>
Effect.succeed({
author: baseDetail.author,
Expand Down Expand Up @@ -963,6 +978,7 @@ describe("getChangeRequestActivity dismissed reviews", () => {
};
const layerFor = (body: string) =>
Layer.mock(GitHubPullRequestCli.GitHubPullRequestCli)({
revalidateChecks: (_input, read) => read,
getPullRequestActivity: () =>
Effect.succeed({ author: null, comments: [dismissedReview(body)], commits: [] }),
listReviewThreadComments: () => Effect.succeed(threadComments),
Expand Down Expand Up @@ -1045,6 +1061,7 @@ describe("editing", () => {
}).pipe(
Effect.provide(
Layer.mock(GitHubPullRequestCli.GitHubPullRequestCli)({
revalidateChecks: (_input, read) => read,
updatePullRequest: (input) => Effect.sync(() => void rewrites.push(input)),
updateComment: (input) => Effect.sync(() => void rewrites.push(input)),
}),
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/pullRequest/GitHubPullRequestProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ export const make = Effect.gen(function* () {
cli.getPullRequestPreview(input).pipe(Effect.mapError(fail("getChangeRequestPreview"))),

getChangeRequestChecks: (input) =>
readChecks(input).pipe(
cli.revalidateChecks(input, readChecks(input)).pipe(
Effect.map(({ state, checks }) => ({ state, checks })),
Effect.mapError(fail("getChangeRequestChecks")),
),
Expand Down
8 changes: 5 additions & 3 deletions apps/server/src/pullRequest/PullRequestService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1844,14 +1844,16 @@ for (const [provider, host] of [
number: 1,
allowStale: false,
};
yield* Effect.all([service[read](reference), service[read](reference)], { concurrency: 2 });
const request: Effect.Effect<unknown, PullRequestService.PullRequestError> =
service[read](reference);
yield* Effect.all([request, request], { concurrency: 2 });
assert.strictEqual(calls, 1);
yield* TestClock.adjust("45 seconds");
limited = true;
yield* Effect.flip(service[read](reference));
assert.strictEqual((yield* Effect.exit(request))._tag, "Failure");
assert.strictEqual(calls, 2);
yield* TestClock.adjust("45 seconds");
yield* Effect.flip(service[read](reference));
assert.strictEqual((yield* Effect.exit(request))._tag, "Failure");
assert.strictEqual(calls, 2);
yield* TestClock.adjust("75 seconds");
limited = false;
Expand Down
Loading
Loading