Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions apps/server/src/pullRequest/GiteaConversation.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { describe, expect, it } from "@effect/vitest";
import * as Schema from "effect/Schema";

import {
editableCommentId,
Expand All @@ -23,10 +24,10 @@ describe("GiteaConversation", () => {

it("groups supported Gitea reactions and names the signed-in viewer separately", () => {
const rows: ReadonlyArray<typeof RawGiteaReaction.Type> = [
{ reaction: "+1", user: { login: "Reader" } },
{ reaction: "+1", user: { login: "teammate" } },
{ reaction: "heart", user: { login: "friend" } },
{ reaction: "party", user: { login: "ignored" } },
{ content: "+1", user: { login: "Reader" } },
{ content: "+1", user: { login: "teammate" } },
{ content: "heart", user: { login: "friend" } },
{ content: "party", user: { login: "ignored" } },
];

expect(reactionsForViewer(rows, "reader")).toEqual([
Expand All @@ -35,6 +36,19 @@ describe("GiteaConversation", () => {
]);
});

it("decodes and groups the native Gitea reaction response shape", () => {
const decodeReaction = Schema.decodeUnknownSync(RawGiteaReaction);
const row = decodeReaction({
content: "+1",
created_at: "2026-09-05T00:00:00Z",
user: { id: 7, login: "kalvens", full_name: "Kalven" },
});

expect(reactionsForViewer([row], "Kalvens")).toEqual([
{ content: "thumbs-up", count: 1, actors: [], viewerHasReacted: true },
]);
});

it("uses Gitea's reaction spelling on writes", () => {
expect(nativeReactionContent("thumbs-up")).toBe("+1");
expect(nativeReactionContent("heart")).toBe("heart");
Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/pullRequest/GiteaConversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const RawReactionUser = Schema.Struct({

/** The shape returned by Gitea's issue and issue-comment reaction endpoints. */
export const RawGiteaReaction = Schema.Struct({
reaction: Schema.optional(Schema.String),
content: Schema.String,
user: Schema.optional(Schema.NullOr(RawReactionUser)),
});

Expand Down Expand Up @@ -71,7 +71,7 @@ export function reactionsForViewer(
{ count: number; actors: Array<string>; viewerHasReacted: boolean }
>();
for (const row of rows) {
const content = row.reaction === undefined ? undefined : reactionContent.get(row.reaction);
const content = reactionContent.get(row.content);
if (content === undefined) continue;
const group = groups.get(content) ?? { count: 0, actors: [], viewerHasReacted: false };
group.count += 1;
Expand Down
38 changes: 33 additions & 5 deletions apps/server/src/pullRequest/GiteaPullRequestApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1547,13 +1547,17 @@ layer("GiteaPullRequestApi", (it) => {
.mockReturnValueOnce(
Effect.succeed(
response([
{ reaction: "+1", user: { login: "reader" } },
{ reaction: "+1", user: { login: "teammate" } },
{
content: "+1",
created_at: "2026-09-05T00:00:00Z",
user: { login: "reader" },
},
{ content: "+1", user: { login: "teammate" } },
]),
),
)
.mockReturnValueOnce(
Effect.succeed(response([{ reaction: "heart", user: { login: "friend" } }])),
Effect.succeed(response([{ content: "heart", user: { login: "friend" } }])),
)
.mockReturnValueOnce(Effect.succeed(response([])));
const api = yield* GiteaPullRequestApi.make;
Expand Down Expand Up @@ -1582,17 +1586,41 @@ layer("GiteaPullRequestApi", (it) => {
}),
);

it.effect("treats a native null reaction list as empty without dropping other subjects", () =>
Effect.gen(function* () {
mockedRequest
.mockReturnValueOnce(Effect.succeed(response({ features: [] })))
.mockReturnValueOnce(Effect.succeed(response(null)))
.mockReturnValueOnce(
Effect.succeed(response([{ content: "heart", user: { login: "friend" } }])),
);
const api = yield* GiteaPullRequestApi.make;
const reactions = yield* api.listConversationReactions({
host: "forge.example.test",
repository: "acme/web",
number: 7,
viewer: "Reader",
subjectIds: ["issue:12"],
});

expect(reactions.pullRequest).toEqual([]);
expect(reactions.bySubjectId.get("issue:12")).toEqual([
{ content: "heart", count: 1, actors: ["friend"], viewerHasReacted: false },
]);
}),
);

it.effect("follows a reaction list when Gitea caps a requested page below its limit", () =>
Effect.gen(function* () {
mockedRequest.mockImplementation((input) => {
if (input.path === "/settings/api") return Effect.succeed(response({ features: [] }));
if (input.path === "/repos/acme/web/issues/7/reactions?page=1&limit=50")
return Effect.succeed(
response([{ reaction: "heart", user: { login: "one" } }], { "x-total-count": "2" }),
response([{ content: "heart", user: { login: "one" } }], { "x-total-count": "2" }),
);
if (input.path === "/repos/acme/web/issues/7/reactions?page=2&limit=50")
return Effect.succeed(
response([{ reaction: "eyes", user: { login: "two" } }], { "x-total-count": "2" }),
response([{ content: "eyes", user: { login: "two" } }], { "x-total-count": "2" }),
);
return Effect.die(`unexpected request: ${input.path}`);
});
Expand Down
21 changes: 19 additions & 2 deletions apps/server/src/pullRequest/GiteaPullRequestApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -845,9 +845,23 @@ export const make = Effect.gen(function* () {
host: string;
repository: string;
path: string;
nullAsEmpty?: boolean;
}) {
const response = yield* request({ ...input, method: "GET" });
const rows = yield* decode(input.operation, Schema.Array(Schema.Unknown), response);
const response = yield* request({
operation: input.operation,
host: input.host,
repository: input.repository,
path: input.path,
method: "GET",
});
const decoded = yield* decode(
input.operation,
input.nullAsEmpty
? Schema.NullOr(Schema.Array(Schema.Unknown))
: Schema.Array(Schema.Unknown),
response,
);
const rows = decoded ?? [];
return { rows, headers: response.headers } satisfies UnknownPage;
});

Expand Down Expand Up @@ -1022,6 +1036,7 @@ export const make = Effect.gen(function* () {
path: string;
limit: number;
requirePaginationEvidence?: boolean;
nullAsEmpty?: boolean;
}) {
const rows: Array<unknown> = [];
let path = input.path;
Expand All @@ -1032,6 +1047,7 @@ export const make = Effect.gen(function* () {
host: input.host,
repository: input.repository,
path,
...(input.nullAsEmpty === undefined ? {} : { nullAsEmpty: input.nullAsEmpty }),
});
rowsSeen += result.rows.length;
const remaining = Math.max(0, input.limit - rows.length);
Expand Down Expand Up @@ -1681,6 +1697,7 @@ export const make = Effect.gen(function* () {
},
),
limit: PAGE_SIZE * MAX_PAGINATION_PAGES,
nullAsEmpty: true,
}).pipe(
Effect.map((result) => ({
subjectId: entry.subjectId,
Expand Down