Skip to content
Closed
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
334 changes: 334 additions & 0 deletions apps/server/src/pullRequest/PullRequestService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3643,6 +3643,340 @@ it.effect("answers a known pull request immediately while the host refreshes", (
}),
);

it.effect("an invalidated detail read sees an external change a plain re-read holds back", () =>
Effect.gen(function* () {
const gate = yield* Deferred.make<void>();
let calls = 0;
let hostTitle = "old title";
let hostChecks: ReadonlyArray<{ readonly name: string; readonly status: "success" }> = [];
const reference = { projectId: "p1" as ProjectId, repository: "acme/web", number: 1 };
const service = yield* makeService({
projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })],
providers: [
fakeProvider("github", {
getChangeRequest: () =>
Effect.gen(function* () {
calls += 1;
// The plain re-read's background refresh stalls here, so the hold stays old
// while the invalidated poll read runs past it.
if (calls === 2) yield* Deferred.await(gate);
return {
...hostedChangeRequest("polled body", 4),
title: hostTitle,
checks: hostChecks.map((check) => ({
name: check.name,
status: check.status,
description: null,
url: null,
})),
};
}),
}),
],
});

const first = yield* service.detail(reference);
assert.strictEqual(first.title, "old title");
assert.deepStrictEqual(first.checks, []);

hostTitle = "new title";
hostChecks = [{ name: "ci", status: "success" }];
yield* TestClock.adjust("16 seconds");

// The old poll path: a plain re-read answers from the hold while refreshing behind it.
const second = yield* service.detail(reference);
assert.strictEqual(second.title, "old title");
assert.deepStrictEqual(second.checks, []);
yield* Effect.yieldNow;
assert.strictEqual(calls, 2);

// The poll path the panel now takes: invalidate first so the re-read misses the hold,
// even with that background refresh still in flight.
yield* service.invalidate({ reference });
const polled = yield* service.detail(reference);
assert.strictEqual(polled.title, "new title");
assert.deepStrictEqual(
polled.checks.map((check) => check.name),
["ci"],
);

yield* Deferred.succeed(gate, undefined);
yield* Effect.yieldNow;
}),
);

it.effect("a detail-scoped invalidate refreshes detail without stranding the held diff", () =>
Effect.gen(function* () {
const diffGate = yield* Deferred.make<void>();
let detailCalls = 0;
let diffCalls = 0;
let hostTitle = "old title";
let hostPatch = "old patch";
const reference = { projectId: "p1" as ProjectId, repository: "acme/web", number: 1 };
const service = yield* makeService({
projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })],
providers: [
fakeProvider("github", {
getChangeRequest: () =>
Effect.sync(() => {
detailCalls += 1;
return { ...hostedChangeRequest("polled body", 4), title: hostTitle };
}),
getDiff: () =>
Effect.gen(function* () {
diffCalls += 1;
// The stale-while-revalidate background refresh stalls here, so a held diff
// must answer from its snapshot rather than wait on the host.
if (diffCalls === 2) yield* Deferred.await(diffGate);
return { patch: hostPatch, truncated: false, nextCursor: null };
}),
}),
],
});

const firstDetail = yield* service.detail(reference);
assert.strictEqual(firstDetail.title, "old title");
const firstDiff = yield* service.diff(reference);
assert.strictEqual(firstDiff.patch, "old patch");
assert.strictEqual(diffCalls, 1);

hostTitle = "new title";
hostPatch = "new patch";
// Past the diff cache TTL but inside the stale-while-revalidate window, so a held diff
// answers from its snapshot while refreshing behind it.
yield* TestClock.adjust("61 seconds");

// The poll path: detail misses the hold while the diff key — and its hold — is untouched.
yield* service.invalidate({ reference, scope: "detail" });
const polledDetail = yield* service.detail(reference);
assert.strictEqual(polledDetail.title, "new title");
assert.strictEqual(detailCalls, 2);

const polledDiff = yield* service.diff(reference);
assert.strictEqual(polledDiff.patch, "old patch");
yield* Effect.yieldNow;
assert.strictEqual(diffCalls, 2);

yield* Deferred.succeed(diffGate, undefined);
yield* Effect.yieldNow;
}),
);

it.effect("recently read scopes retain held responses when epoch maps reach capacity", () =>
Effect.gen(function* () {
const reference = { projectId: "p1" as ProjectId, repository: "acme/web", number: 1 };
let failHost = false;
const failure = new PullRequestProviderError({
provider: "github",
operation: "read",
reason: "failed",
detail: "HTTP 503",
});
const service = yield* makeService({
projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })],
providers: [
fakeProvider("github", {
getChangeRequest: () =>
failHost ? Effect.fail(failure) : Effect.succeed(hostedChangeRequest("held body", 4)),
getDiff: () =>
failHost
? Effect.fail(failure)
: Effect.succeed({ patch: "held patch", truncated: false, nextCursor: null }),
}),
],
});
const heldDetail = yield* service.detail(reference);
yield* service.diff(reference);
for (let number = 2; number <= 2_048; number += 1) {
yield* service.invalidate({ reference: { ...reference, number } });
}
// Reading the active scope keeps its generations newer than the untouched scopes.
yield* service.detail(reference);
yield* service.diff(reference);
yield* service.invalidate({ reference: { ...reference, number: 2_049 } });
failHost = true;
yield* TestClock.adjust("61 seconds");

assert.deepStrictEqual(yield* service.detail(reference), heldDetail);
assert.strictEqual((yield* service.diff(reference)).patch, "held patch");
}),
);

for (const afterTurn of [false, true]) {
it.effect(
`evicting invalidation epochs never revives held responses (after turn: ${afterTurn})`,
() =>
Effect.gen(function* () {
const reference = { projectId: "p1" as ProjectId, repository: "acme/web", number: 1 };
let version = "old";
const service = yield* makeService({
projects: [
project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }),
],
providers: [
fakeProvider("github", {
getChangeRequest: () =>
Effect.succeed({ ...hostedChangeRequest("body", 4), title: version }),
getDiff: (input) =>
Effect.succeed({
patch: `${version}:${input.cursor ?? "first"}`,
truncated: false,
nextCursor: input.cursor ? null : "page-2",
}),
}),
],
});

assert.strictEqual((yield* service.detail(reference)).title, "old");
assert.strictEqual((yield* service.diff(reference)).patch, "old:first");
assert.strictEqual(
(yield* service.diff({ ...reference, cursor: "page-2" })).patch,
"old:page-2",
);
if (afterTurn) {
yield* service.refreshAfterTurn;
yield* service.detail(reference);
yield* service.diff(reference);
yield* service.diff({ ...reference, cursor: "page-2" });
}
version = "new";
yield* service.invalidate({ reference });
// Fill the bounded epoch maps with other scopes without evicting the held responses.
for (let number = 2; number <= 2_049; number += 1) {
yield* service.invalidate({ reference: { ...reference, number } });
}

assert.strictEqual((yield* service.detail(reference)).title, "new");
assert.strictEqual((yield* service.diff(reference)).patch, "new:first");
assert.strictEqual(
(yield* service.diff({ ...reference, cursor: "page-2" })).patch,
"new:page-2",
);
}),
);
}

it.effect("a changed updatedAt reloads every held diff page after detail-only invalidation", () =>
Effect.gen(function* () {
const reference = { projectId: "p1" as ProjectId, repository: "acme/web", number: 1 };
const calls: Array<string | undefined> = [];
let updatedAt = "2026-07-02T00:00:00Z";
let patchVersion = "old";
const service = yield* makeService({
projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })],
providers: [
fakeProvider("github", {
getChangeRequest: () => Effect.succeed({ ...hostedChangeRequest("body", 4), updatedAt }),
getDiff: (input) =>
Effect.sync(() => {
calls.push(input.cursor);
return {
patch: `${patchVersion}:${input.cursor ?? "first"}`,
truncated: false,
nextCursor: input.cursor ? null : "page-2",
};
}),
}),
],
});

yield* service.detail(reference);
assert.strictEqual((yield* service.diff(reference)).patch, "old:first");
assert.strictEqual(
(yield* service.diff({ ...reference, cursor: "page-2" })).patch,
"old:page-2",
);

patchVersion = "new";
yield* service.invalidate({ reference, scope: "detail" });
yield* service.detail(reference);
assert.strictEqual((yield* service.diff(reference)).patch, "old:first");
assert.strictEqual(
(yield* service.diff({ ...reference, cursor: "page-2" })).patch,
"old:page-2",
);
assert.deepStrictEqual(calls, [undefined, "page-2"]);

updatedAt = "2026-07-03T00:00:00Z";
yield* service.invalidate({ reference, scope: "detail" });
assert.strictEqual((yield* service.detail(reference)).updatedAt, updatedAt);
assert.strictEqual((yield* service.diff(reference)).patch, "new:first");
assert.strictEqual(
(yield* service.diff({ ...reference, cursor: "page-2" })).patch,
"new:page-2",
);
assert.deepStrictEqual(calls, [undefined, "page-2", undefined, "page-2"]);
}),
);

it.effect("full invalidation reloads every held diff page after a failed detail refresh", () =>
Effect.gen(function* () {
const reference = { projectId: "p1" as ProjectId, repository: "acme/web", number: 1 };
const calls: Array<string | undefined> = [];
let revision = "old";
let failDetail = false;
const service = yield* makeService({
projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })],
providers: [
fakeProvider("github", {
getChangeRequest: () =>
failDetail
? Effect.fail(
new PullRequestProviderError({
provider: "github",
operation: "getChangeRequest",
reason: "failed",
detail: "HTTP 503",
}),
)
: Effect.succeed(hostedChangeRequest("body", 4)),
getDiff: (input) =>
Effect.sync(() => {
calls.push(input.cursor);
return {
patch: `${revision}:${input.cursor ?? "first"}`,
truncated: false,
nextCursor: input.cursor ? null : "page-2",
};
}),
}),
],
});
yield* service.detail(reference);
assert.strictEqual((yield* service.diff(reference)).patch, "old:first");
assert.strictEqual(
(yield* service.diff({ ...reference, cursor: "page-2" })).patch,
"old:page-2",
);

revision = "new";
yield* service.invalidate({ reference, scope: "detail" });
assert.strictEqual((yield* service.diff(reference)).patch, "old:first");
assert.strictEqual(
(yield* service.diff({ ...reference, cursor: "page-2" })).patch,
"old:page-2",
);
assert.deepStrictEqual(calls, [undefined, "page-2"]);

failDetail = true;
yield* Effect.flip(service.detail(reference));
assert.strictEqual((yield* service.diff(reference)).patch, "old:first");
assert.strictEqual(
(yield* service.diff({ ...reference, cursor: "page-2" })).patch,
"old:page-2",
);
assert.deepStrictEqual(calls, [undefined, "page-2"]);

yield* service.invalidate({ reference });
assert.strictEqual((yield* service.diff(reference)).patch, "new:first");
assert.strictEqual(
(yield* service.diff({ ...reference, cursor: "page-2" })).patch,
"new:page-2",
);
assert.deepStrictEqual(calls, [undefined, "page-2", undefined, "page-2"]);
}),
);

it.effect("does not ask the host again for a linked summary it already holds", () =>
Effect.gen(function* () {
let calls = 0;
Expand Down
Loading
Loading