authorgravatar for 62337003+Bil0000@users.noreply.github.comBilal Bakr <62337003+Bil0000@users.noreply.github.com> 2026-08-29 18:50:43+03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-29 08:50:43-07:00
logebb9b9fda03d33a81cad3ce7f4d1106adba3044b
treed9efb25f41d1f559c0b0b04a8121b775f64499c4
parentfc262f1a28d8305951c751f2486da6ca72e6c1d1
signature Signed by PGP key B5690EEEBB952194

fix(client-runtime): refresh edited pull request comments (#8094)


2 files changed, 152 insertions(+), 5 deletions(-)

packages/client-runtime/src/state/pullRequests.test.ts created+142
......@@ -0,0 +1,142 @@
1import { EnvironmentId, ProjectId, WS_METHODS } from "@t3tools/contracts";
2import { expect, it } from "@effect/vitest";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";
6import * as Stream from "effect/Stream";
7import * as SubscriptionRef from "effect/SubscriptionRef";
8import { AsyncResult, Atom, AtomRegistry } from "effect/unstable/reactivity";
9
10import {
11 AVAILABLE_CONNECTION_STATE,
12 PrimaryConnectionTarget,
13 type PreparedConnection,
14 type SupervisorConnectionState,
15} from "../connection/model.ts";
16import * as EnvironmentRegistry from "../connection/registry.ts";
17import * as EnvironmentSupervisor from "../connection/supervisor.ts";
18import type { WsRpcProtocolClient } from "../rpc/protocol.ts";
19import type { RpcSession } from "../rpc/session.ts";
20import { createPullRequestEnvironmentAtoms } from "./pullRequests.ts";
21import { PullRequestDiffLoader } from "./pullRequestDiffHttp.ts";
22import { executeAtomQuery } from "./runtime.ts";
23
24const TARGET = new PrimaryConnectionTarget({
25 environmentId: EnvironmentId.make("environment-1"),
26 label: "Test environment",
27 httpBaseUrl: "https://environment.example.test",
28 wsBaseUrl: "wss://environment.example.test",
29});
30
31function session(client: WsRpcProtocolClient): RpcSession {
32 return {
33 client,
34 initialConfig: Effect.never,
35 ready: Effect.void,
36 probe: Effect.void,
37 closed: Effect.never,
38 };
39}
40
41it.effect("refreshes pull request activity after a comment is updated", () =>
42 Effect.scoped(
43 Effect.gen(function* () {
44 let commentBody = "old comment";
45 const client = {
46 [WS_METHODS.pullRequestsActivity]: () =>
47 Effect.succeed({
48 author: null,
49 reviewers: [],
50 comments: [
51 {
52 id: "comment-1",
53 kind: "issue-comment",
54 author: null,
55 body: commentBody,
56 createdAt: "2026-08-24T00:00:00Z",
57 url: null,
58 path: null,
59 reviewState: null,
60 reactions: [],
61 },
62 ],
63 commentCount: 1,
64 commentsTruncated: false,
65 reviewThreads: [],
66 commits: [],
67 reactions: [],
68 }),
69 [WS_METHODS.pullRequestsUpdateComment]: (input: { readonly body: string }) =>
70 Effect.sync(() => {
71 commentBody = input.body;
72 }),
73 } as unknown as WsRpcProtocolClient;
74 const connectionState: SupervisorConnectionState = {
75 ...AVAILABLE_CONNECTION_STATE,
76 desired: true,
77 network: "online",
78 phase: "connected",
79 attempt: 1,
80 generation: 1,
81 };
82 const supervisor = EnvironmentSupervisor.EnvironmentSupervisor.of({
83 target: TARGET,
84 state: yield* SubscriptionRef.make(connectionState),
85 session: yield* SubscriptionRef.make(Option.some(session(client))),
86 prepared: yield* SubscriptionRef.make(Option.none<PreparedConnection>()),
87 connect: Effect.void,
88 disconnect: Effect.void,
89 retryNow: Effect.void,
90 } satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]);
91 const environmentRegistry = EnvironmentRegistry.EnvironmentRegistry.of({
92 run: (_environmentId, effect) =>
93 Effect.provideService(effect, EnvironmentSupervisor.EnvironmentSupervisor, supervisor),
94 runStream: (_environmentId, stream) =>
95 Stream.provideService(stream, EnvironmentSupervisor.EnvironmentSupervisor, supervisor),
96 followStream: (_environmentId, stream) =>
97 Stream.provideService(stream, EnvironmentSupervisor.EnvironmentSupervisor, supervisor),
98 } as EnvironmentRegistry.EnvironmentRegistry["Service"]);
99 const runtime = Atom.runtime(
100 Layer.merge(
101 Layer.succeed(EnvironmentRegistry.EnvironmentRegistry, environmentRegistry),
102 Layer.succeed(
103 PullRequestDiffLoader,
104 PullRequestDiffLoader.of({ load: () => Effect.die("unused") }),
105 ),
106 ),
107 );
108 const atoms = createPullRequestEnvironmentAtoms(runtime);
109 const registry = yield* Effect.acquireRelease(Effect.sync(AtomRegistry.make), (registry) =>
110 Effect.sync(() => registry.dispose()),
111 );
112 const reference = {
113 projectId: ProjectId.make("project-1"),
114 repository: "acme/web",
115 number: 1,
116 } as const;
117 const activity = atoms.activity({ environmentId: TARGET.environmentId, input: reference });
118 const unmount = registry.mount(activity);
119 yield* Effect.addFinalizer(() => Effect.sync(unmount));
120
121 const initial = yield* Effect.promise(() => executeAtomQuery(registry, activity));
122 expect(AsyncResult.isSuccess(initial)).toBe(true);
123 if (!AsyncResult.isSuccess(initial)) {
124 return yield* Effect.die("activity did not load");
125 }
126 expect(initial.value.comments[0]?.body).toBe("old comment");
127
128 const update = yield* Effect.promise(() =>
129 atoms.updateComment.run(registry, {
130 environmentId: TARGET.environmentId,
131 input: { ...reference, commentId: "comment-1", kind: "issue-comment", body: "updated" },
132 }),
133 );
134
135 expect(AsyncResult.isSuccess(update)).toBe(true);
136 expect(
137 (yield* AtomRegistry.getResult(registry, activity, { suspendOnWaiting: true })).comments[0]
138 ?.body,
139 ).toBe("updated");
140 }),
141 ),
142);
packages/client-runtime/src/state/pullRequests.ts+10-5
......@@ -65,6 +65,11 @@ export function createPullRequestEnvironmentAtoms<R, E>(
6565 mode: "serial",
6666 key: ({ environmentId }: { readonly environmentId: string }) => environmentId,
6767 } as const;
68 const activity = createEnvironmentRpcQueryAtomFamily(runtime, {
69 label: "environment-data:pull-requests:activity",
70 tag: WS_METHODS.pullRequestsActivity,
71 staleTimeMs: 15_000,
72 });
6873 return {
6974 list: createEnvironmentRpcQueryAtomFamily(runtime, {
7075 label: "environment-data:pull-requests:list",
......@@ -87,11 +92,7 @@ export function createPullRequestEnvironmentAtoms<R, E>(
8792 tag: WS_METHODS.pullRequestsDetail,
8893 staleTimeMs: 15_000,
8994 }),
90 activity: createEnvironmentRpcQueryAtomFamily(runtime, {
91 label: "environment-data:pull-requests:activity",
92 tag: WS_METHODS.pullRequestsActivity,
93 staleTimeMs: 15_000,
94 }),
95 activity,
9596 threadComments: createEnvironmentRpcCommand(runtime, {
9697 label: "environment-data:pull-requests:thread-comments",
9798 tag: WS_METHODS.pullRequestsThreadComments,
......@@ -160,6 +161,10 @@ export function createPullRequestEnvironmentAtoms<R, E>(
160161 tag: WS_METHODS.pullRequestsUpdateComment,
161162 scheduler: commandScheduler,
162163 concurrency: serialPerEnvironment,
164 onSuccess: ({ environmentId, input: { projectId, repository, number } }, registry) =>
165 Effect.sync(() =>
166 registry.refresh(activity({ environmentId, input: { projectId, repository, number } })),
167 ),
163168 }),
164169 submitReview: createEnvironmentRpcCommand(runtime, {
165170 label: "environment-data:pull-requests:submit-review",