import { ProjectId, ProviderInstanceId, PullRequestOperationError, ThreadId, type OrchestrationCommand, type OrchestrationProjectShell, type OrchestrationShellSnapshot, type OrchestrationThreadShell, type PullRequestRef, type PullRequestStack, type PullRequestSummary, type ThreadPullRequestLink, type ThreadPullRequestSnapshot,} from "@t3tools/contracts";import { assert, describe, it } from "@effect/vitest";import * as Crypto from "effect/Crypto";import * as Deferred from "effect/Deferred";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as Queue from "effect/Queue";import * as Ref from "effect/Ref";import * as Stream from "effect/Stream";import { TestClock } from "effect/testing"; import { PullRequestService } from "../pullRequest/PullRequestService.ts";import { ServerActivation } from "../serverActivation.ts";import { OrchestrationEngineService, type OrchestrationEngineShape,} from "./Services/OrchestrationEngine.ts";import { ProjectionSnapshotQuery } from "./Services/ProjectionSnapshotQuery.ts";import * as PullRequestSyncReactor from "./PullRequestSyncReactor.ts"; const NOW = "2026-08-28T12:00:00.000Z";const PROJECT_ID = ProjectId.make("sync-project"); type SyncCommand = Extract< OrchestrationCommand, { readonly type: "thread.pull-request-link.sync" }>;type LinkCommand = Extract<OrchestrationCommand, { readonly type: "thread.pull-request.link" }>; const testCrypto = Crypto.make({ randomBytes: (size) => new Uint8Array(size).fill(1), digest: (_algorithm, data) => Effect.succeed(data),}); function makeProject(id: ProjectId = PROJECT_ID): OrchestrationProjectShell { return { id, title: `Project ${id}`, workspaceRoot: "/workspace/project", defaultModelSelection: null, scripts: [], createdAt: "2026-08-01T00:00:00.000Z", updatedAt: NOW, };} function makeThread( id: string, overrides: Partial<OrchestrationThreadShell> = {},): OrchestrationThreadShell { return { id: ThreadId.make(id), projectId: PROJECT_ID, title: id, modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5", }, runtimeMode: "full-access", interactionMode: "default", branch: null, worktreePath: null, pullRequests: [], latestTurn: null, createdAt: "2026-08-01T00:00:00.000Z", updatedAt: "2026-08-20T00:00:00.000Z", archivedAt: null, settledOverride: null, settledAt: null, session: null, latestUserMessageAt: "2026-08-20T00:00:00.000Z", hasPendingApprovals: false, hasPendingUserInput: false, hasActionableProposedPlan: false, ...overrides, };} function makeLink( number: number, snapshot: Partial<ThreadPullRequestSnapshot> | null = null, overrides: Partial<ThreadPullRequestLink> = {},): ThreadPullRequestLink { return { host: "github.com", repository: "owner/repository", number, url: `https://github.com/owner/repository/pull/${number}`, source: "manual", linkedAt: "2026-08-10T00:00:00.000Z", snapshot: snapshot === null ? null : { state: "open", title: "Pull request", headBranch: "feature", baseBranch: "main", isDraft: false, updatedAt: "2026-08-27T00:00:00.000Z", syncedAt: "2026-08-27T00:00:00.000Z", ...snapshot, }, stack: null, ...overrides, };} function makeSnapshot( threads: ReadonlyArray<OrchestrationThreadShell>, snapshotSequence = 1,): OrchestrationShellSnapshot { return { snapshotSequence, projects: [makeProject()], threads, updatedAt: NOW, };} function makeSummary( input: PullRequestRef, overrides: Partial<PullRequestSummary> = {},): PullRequestSummary { return { provider: "github", projectId: input.projectId, repository: input.repository, number: input.number, title: "Pull request", url: `https://github.com/${input.repository}/pull/${input.number}`, state: "open", headBranch: "feature", baseBranch: "main", updatedAt: "2026-08-27T00:00:00.000Z", ...overrides, };} interface HarnessOptions { readonly invalidate?: PullRequestService["Service"]["invalidate"]; readonly snapshot: OrchestrationShellSnapshot; readonly summary?: ( input: PullRequestRef, ) => Effect.Effect<PullRequestSummary, PullRequestOperationError>; readonly stack?: ( input: PullRequestRef, ) => Effect.Effect<PullRequestStack | null, PullRequestOperationError>;} const makeHarness = Effect.fn("makePullRequestSyncHarness")(function* (options: HarnessOptions) { const activation = yield* Deferred.make<void>(); const snapshots = yield* Ref.make(options.snapshot); const snapshotReads = yield* Queue.unbounded<void>(); const syncCommands = yield* Ref.make<ReadonlyArray<SyncCommand>>([]); const linkCommands = yield* Ref.make<ReadonlyArray<LinkCommand>>([]); const summaryCalls = yield* Ref.make<ReadonlyArray<PullRequestRef>>([]); const stackCalls = yield* Ref.make<ReadonlyArray<PullRequestRef>>([]); const summary: PullRequestService["Service"]["summary"] = (input, readOptions) => Effect.gen(function* () { assert.strictEqual(readOptions?.recoverTransientFailure, false); yield* Ref.update(summaryCalls, (calls) => [...calls, input]); return yield* options.summary?.(input) ?? Effect.succeed(makeSummary(input)); }); const stack: PullRequestService["Service"]["stack"] = (input, readOptions) => Effect.gen(function* () { assert.strictEqual(readOptions?.includeDetails, false); yield* Ref.update(stackCalls, (calls) => [...calls, input]); return yield* options.stack?.(input) ?? Effect.succeed(null); }); const dispatch: OrchestrationEngineShape["dispatch"] = (command) => { if (command.type === "thread.pull-request-link.sync") { return Ref.update(syncCommands, (recorded) => [...recorded, command]).pipe( Effect.as({ sequence: 1 }), ); } if (command.type === "thread.pull-request.link") { return Ref.update(linkCommands, (recorded) => [...recorded, command]).pipe( Effect.as({ sequence: 1 }), ); } return Effect.die(new Error(`Unexpected command: ${command.type}`)); }; const dependencies = Layer.mergeAll( Layer.mock(ProjectionSnapshotQuery)({ getShellSnapshot: () => Queue.offer(snapshotReads, undefined).pipe(Effect.andThen(Ref.get(snapshots))), }), Layer.mock(PullRequestService)({ summary, stack, invalidate: options.invalidate ?? (() => Effect.void), }), Layer.mock(OrchestrationEngineService)({ readEvents: () => Stream.empty, dispatch, streamDomainEvents: Stream.empty, latestSequence: Effect.succeed(0), }), Layer.succeed(ServerActivation, Deferred.await(activation)), Layer.succeed(Crypto.Crypto, testCrypto), ); return { activation, snapshots, snapshotReads, syncCommands, linkCommands, summaryCalls, stackCalls, layer: PullRequestSyncReactor.layer.pipe(Layer.provide(dependencies)), };}); type Harness = Effect.Success<ReturnType<typeof makeHarness>>; const startAndSweep = Effect.fn("startPullRequestSyncHarness")(function* (fixture: Harness) { const reactor = yield* PullRequestSyncReactor.PullRequestSyncReactor; yield* reactor.start(); yield* Deferred.succeed(fixture.activation, undefined); yield* Queue.take(fixture.snapshotReads); yield* reactor.drain; return reactor;}); const sweepAgain = Effect.fn("sweepPullRequestSyncHarness")(function* ( fixture: Harness, reactor: PullRequestSyncReactor.PullRequestSyncReactor["Service"],) { yield* TestClock.adjust("1 minute"); yield* Queue.take(fixture.snapshotReads); yield* reactor.drain;}); /** What the reactor would have persisted, so the next sweep sees its own writes. */function applySync( snapshot: OrchestrationShellSnapshot, commands: ReadonlyArray<SyncCommand>,): OrchestrationShellSnapshot { return { ...snapshot, snapshotSequence: snapshot.snapshotSequence + 1, threads: snapshot.threads.map((thread) => ({ ...thread, pullRequests: thread.pullRequests.map((link) => { const command = commands.findLast( (candidate) => candidate.threadId === thread.id && candidate.number === link.number, ); return command === undefined ? link : { ...link, snapshot: command.snapshot, stack: command.stack }; }), })), };} describe("PullRequestSyncReactor", () => { it.effect("retries a failed stack read after the summary becomes terminal", () => Effect.scoped( Effect.gen(function* () { yield* TestClock.setTime(Date.parse(NOW)); let attempts = 0; const nativeStack: PullRequestStack = { id: "stack", number: 7, url: "https://github.com/owner/repository/stacks/7", base: "main", layers: [{ number: 7, headBranch: "feature", state: "merged" }], }; const fixture = yield* makeHarness({ snapshot: makeSnapshot([makeThread("one", { pullRequests: [makeLink(7)] })]), summary: (input) => Effect.succeed(makeSummary(input, { state: "merged", mergedAt: NOW })), stack: () => ++attempts === 1 ? Effect.fail( new PullRequestOperationError({ operation: "stack", detail: "temporary failure", }), ) : Effect.succeed(nativeStack), }); yield* Effect.gen(function* () { const reactor = yield* startAndSweep(fixture); const commands = yield* Ref.get(fixture.syncCommands); yield* Ref.update(fixture.snapshots, (snapshot) => applySync(snapshot, commands)); yield* sweepAgain(fixture, reactor); assert.strictEqual(attempts, 2); assert.deepStrictEqual((yield* Ref.get(fixture.syncCommands)).at(-1)?.stack, { kind: "native", ...nativeStack, }); }).pipe(Effect.provide(fixture.layer)); }), ), ); it.effect("explicit refresh reads a changed stack even when its PR summary is unchanged", () => Effect.scoped( Effect.gen(function* () { yield* TestClock.setTime(Date.parse(NOW)); const fixture = yield* makeHarness({ snapshot: makeSnapshot([makeThread("one", { pullRequests: [makeLink(7, {})] })]), }); yield* Effect.gen(function* () { const reactor = yield* startAndSweep(fixture); assert.strictEqual((yield* Ref.get(fixture.stackCalls)).length, 0); yield* reactor.requestSync({ host: "github.com", repository: "owner/repository", number: 7, }); yield* Queue.take(fixture.snapshotReads); yield* reactor.drain; assert.strictEqual((yield* Ref.get(fixture.stackCalls)).length, 1); }).pipe(Effect.provide(fixture.layer)); }), ), ); it.effect("snapshots an unsynced link once and writes it to the thread", () => Effect.scoped( Effect.gen(function* () { yield* TestClock.setTime(Date.parse(NOW)); const fixture = yield* makeHarness({ snapshot: makeSnapshot([makeThread("one", { pullRequests: [makeLink(42)] })]), summary: (input) => Effect.succeed(makeSummary(input, { title: "Ship it", isDraft: true })), }); yield* Effect.gen(function* () { yield* startAndSweep(fixture); assert.deepStrictEqual(yield* Ref.get(fixture.summaryCalls), [ { projectId: PROJECT_ID, host: "github.com", repository: "owner/repository", number: 42, }, ]); assert.deepStrictEqual( (yield* Ref.get(fixture.syncCommands)).map(({ commandId: _, ...rest }) => rest), [ { type: "thread.pull-request-link.sync", threadId: ThreadId.make("one"), host: "github.com", repository: "owner/repository", number: 42, snapshot: { state: "open", title: "Ship it", headBranch: "feature", baseBranch: "main", isDraft: true, updatedAt: "2026-08-27T00:00:00.000Z", syncedAt: NOW, closedAt: null, mergedAt: null, }, stack: null, }, ], ); assert.strictEqual((yield* Ref.get(fixture.stackCalls)).length, 1); }).pipe(Effect.provide(fixture.layer)); }), ), ); it.effect("asks the host once for a pull request shared by two threads", () => Effect.scoped( Effect.gen(function* () { yield* TestClock.setTime(Date.parse(NOW)); const fixture = yield* makeHarness({ snapshot: makeSnapshot([ makeThread("one", { pullRequests: [makeLink(42)] }), makeThread("two", { pullRequests: [makeLink(42, null, { repository: "Owner/Repository" })], }), ]), }); yield* Effect.gen(function* () { yield* startAndSweep(fixture); assert.strictEqual((yield* Ref.get(fixture.summaryCalls)).length, 1); const commands = yield* Ref.get(fixture.syncCommands); assert.deepStrictEqual( commands .map((command) => [command.threadId, command.repository] as const) .sort((left, right) => left[0].localeCompare(right[0])), [ [ThreadId.make("one"), "owner/repository"], [ThreadId.make("two"), "Owner/Repository"], ], ); }).pipe(Effect.provide(fixture.layer)); }), ), ); it.effect("dispatches nothing when the host snapshot is unchanged", () => Effect.scoped( Effect.gen(function* () { yield* TestClock.setTime(Date.parse(NOW)); const fixture = yield* makeHarness({ snapshot: makeSnapshot([makeThread("one", { pullRequests: [makeLink(42)] })]), }); yield* Effect.gen(function* () { const reactor = yield* startAndSweep(fixture); const firstSweep = yield* Ref.get(fixture.syncCommands); assert.strictEqual(firstSweep.length, 1); yield* Ref.update(fixture.snapshots, (snapshot) => applySync(snapshot, firstSweep)); yield* sweepAgain(fixture, reactor); // Still open on an active thread, so the host was asked again, but nothing changed. assert.strictEqual((yield* Ref.get(fixture.summaryCalls)).length, 2); assert.strictEqual((yield* Ref.get(fixture.stackCalls)).length, 1); assert.strictEqual((yield* Ref.get(fixture.syncCommands)).length, 1); }).pipe(Effect.provide(fixture.layer)); }), ), ); it.effect("refreshes closed links through the reactor's project after reopening elsewhere", () => Effect.scoped( Effect.gen(function* () { yield* TestClock.setTime(Date.parse(NOW)); const stale = yield* Ref.make(true); const fixture = yield* makeHarness({ snapshot: makeSnapshot([ makeThread("first", { pullRequests: [makeLink(42, { state: "closed" })] }), makeThread("second", { projectId: ProjectId.make("second-project"), pullRequests: [makeLink(42, { state: "closed" })], }), ]), invalidate: ({ reference }) => reference?.projectId === makeProject().id && reference.host === "github.com" ? Ref.set(stale, false) : Effect.void, summary: (input) => Ref.get(stale).pipe( Effect.map((cached) => makeSummary(input, { state: cached ? "closed" : "open" })), ), }); yield* Effect.gen(function* () { const reactor = yield* startAndSweep(fixture); assert.strictEqual((yield* Ref.get(fixture.summaryCalls)).length, 1); yield* reactor.requestSync({ host: "github.com", repository: "owner/repository", number: 42, }); yield* Queue.take(fixture.snapshotReads); yield* reactor.drain; const commands = yield* Ref.get(fixture.syncCommands); yield* Ref.update(fixture.snapshots, (snapshot) => applySync(snapshot, commands)); const snapshot = yield* Ref.get(fixture.snapshots); assert.deepStrictEqual( snapshot.threads.map((thread) => thread.pullRequests[0]?.snapshot?.state), ["open", "open"], ); }).pipe(Effect.provide(fixture.layer)); }), ), ); it.effect("stops asking the host once a pull request is merged", () => Effect.scoped( Effect.gen(function* () { yield* TestClock.setTime(Date.parse(NOW)); const fixture = yield* makeHarness({ snapshot: makeSnapshot([ makeThread("merged", { pullRequests: [makeLink(1, { state: "merged" })] }), ]), });Lines 1–500 of 705. Load more
import { ProjectId, ProviderInstanceId, PullRequestOperationError, ThreadId, type OrchestrationCommand, type OrchestrationProjectShell, type OrchestrationShellSnapshot, type OrchestrationThreadShell, type PullRequestRef, type PullRequestStack, type PullRequestSummary, type ThreadPullRequestLink, type ThreadPullRequestSnapshot, } from "@t3tools/contracts"; import { assert, describe, it } from "@effect/vitest"; import * as Crypto from "effect/Crypto"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Queue from "effect/Queue"; import * as Ref from "effect/Ref"; import * as Stream from "effect/Stream"; import { TestClock } from "effect/testing"; import { PullRequestService } from "../pullRequest/PullRequestService.ts"; import { ServerActivation } from "../serverActivation.ts"; import { OrchestrationEngineService, type OrchestrationEngineShape, } from "./Services/OrchestrationEngine.ts"; import { ProjectionSnapshotQuery } from "./Services/ProjectionSnapshotQuery.ts"; import * as PullRequestSyncReactor from "./PullRequestSyncReactor.ts"; const NOW = "2026-08-28T12:00:00.000Z"; const PROJECT_ID = ProjectId.make("sync-project"); type SyncCommand = Extract< OrchestrationCommand, { readonly type: "thread.pull-request-link.sync" } >; type LinkCommand = Extract<OrchestrationCommand, { readonly type: "thread.pull-request.link" }>; const testCrypto = Crypto.make({ randomBytes: (size) => new Uint8Array(size).fill(1), digest: (_algorithm, data) => Effect.succeed(data), }); function makeProject(id: ProjectId = PROJECT_ID): OrchestrationProjectShell { return { id, title: `Project ${id}`, workspaceRoot: "/workspace/project", defaultModelSelection: null, scripts: [], createdAt: "2026-08-01T00:00:00.000Z", updatedAt: NOW, }; } function makeThread( id: string, overrides: Partial<OrchestrationThreadShell> = {}, ): OrchestrationThreadShell { return { id: ThreadId.make(id), projectId: PROJECT_ID, title: id, modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5", }, runtimeMode: "full-access", interactionMode: "default", branch: null, worktreePath: null, pullRequests: [], latestTurn: null, createdAt: "2026-08-01T00:00:00.000Z", updatedAt: "2026-08-20T00:00:00.000Z", archivedAt: null, settledOverride: null, settledAt: null, session: null, latestUserMessageAt: "2026-08-20T00:00:00.000Z", hasPendingApprovals: false, hasPendingUserInput: false, hasActionableProposedPlan: false, ...overrides, }; } function makeLink( number: number, snapshot: Partial<ThreadPullRequestSnapshot> | null = null, overrides: Partial<ThreadPullRequestLink> = {}, ): ThreadPullRequestLink { return { host: "github.com", repository: "owner/repository", number, url: `https://github.com/owner/repository/pull/${number}`, source: "manual", linkedAt: "2026-08-10T00:00:00.000Z", snapshot: snapshot === null ? null : { state: "open", title: "Pull request", headBranch: "feature", baseBranch: "main", isDraft: false, updatedAt: "2026-08-27T00:00:00.000Z", syncedAt: "2026-08-27T00:00:00.000Z", ...snapshot, }, stack: null, ...overrides, }; } function makeSnapshot( threads: ReadonlyArray<OrchestrationThreadShell>, snapshotSequence = 1, ): OrchestrationShellSnapshot { return { snapshotSequence, projects: [makeProject()], threads, updatedAt: NOW, }; } function makeSummary( input: PullRequestRef, overrides: Partial<PullRequestSummary> = {}, ): PullRequestSummary { return { provider: "github", projectId: input.projectId, repository: input.repository, number: input.number, title: "Pull request", url: `https://github.com/${input.repository}/pull/${input.number}`, state: "open", headBranch: "feature", baseBranch: "main", updatedAt: "2026-08-27T00:00:00.000Z", ...overrides, }; } interface HarnessOptions { readonly invalidate?: PullRequestService["Service"]["invalidate"]; readonly snapshot: OrchestrationShellSnapshot; readonly summary?: ( input: PullRequestRef, ) => Effect.Effect<PullRequestSummary, PullRequestOperationError>; readonly stack?: ( input: PullRequestRef, ) => Effect.Effect<PullRequestStack | null, PullRequestOperationError>; } const makeHarness = Effect.fn("makePullRequestSyncHarness")(function* (options: HarnessOptions) { const activation = yield* Deferred.make<void>(); const snapshots = yield* Ref.make(options.snapshot); const snapshotReads = yield* Queue.unbounded<void>(); const syncCommands = yield* Ref.make<ReadonlyArray<SyncCommand>>([]); const linkCommands = yield* Ref.make<ReadonlyArray<LinkCommand>>([]); const summaryCalls = yield* Ref.make<ReadonlyArray<PullRequestRef>>([]); const stackCalls = yield* Ref.make<ReadonlyArray<PullRequestRef>>([]); const summary: PullRequestService["Service"]["summary"] = (input, readOptions) => Effect.gen(function* () { assert.strictEqual(readOptions?.recoverTransientFailure, false); yield* Ref.update(summaryCalls, (calls) => [...calls, input]); return yield* options.summary?.(input) ?? Effect.succeed(makeSummary(input)); }); const stack: PullRequestService["Service"]["stack"] = (input, readOptions) => Effect.gen(function* () { assert.strictEqual(readOptions?.includeDetails, false); yield* Ref.update(stackCalls, (calls) => [...calls, input]); return yield* options.stack?.(input) ?? Effect.succeed(null); }); const dispatch: OrchestrationEngineShape["dispatch"] = (command) => { if (command.type === "thread.pull-request-link.sync") { return Ref.update(syncCommands, (recorded) => [...recorded, command]).pipe( Effect.as({ sequence: 1 }), ); } if (command.type === "thread.pull-request.link") { return Ref.update(linkCommands, (recorded) => [...recorded, command]).pipe( Effect.as({ sequence: 1 }), ); } return Effect.die(new Error(`Unexpected command: ${command.type}`)); }; const dependencies = Layer.mergeAll( Layer.mock(ProjectionSnapshotQuery)({ getShellSnapshot: () => Queue.offer(snapshotReads, undefined).pipe(Effect.andThen(Ref.get(snapshots))), }), Layer.mock(PullRequestService)({ summary, stack, invalidate: options.invalidate ?? (() => Effect.void), }), Layer.mock(OrchestrationEngineService)({ readEvents: () => Stream.empty, dispatch, streamDomainEvents: Stream.empty, latestSequence: Effect.succeed(0), }), Layer.succeed(ServerActivation, Deferred.await(activation)), Layer.succeed(Crypto.Crypto, testCrypto), ); return { activation, snapshots, snapshotReads, syncCommands, linkCommands, summaryCalls, stackCalls, layer: PullRequestSyncReactor.layer.pipe(Layer.provide(dependencies)), }; }); type Harness = Effect.Success<ReturnType<typeof makeHarness>>; const startAndSweep = Effect.fn("startPullRequestSyncHarness")(function* (fixture: Harness) { const reactor = yield* PullRequestSyncReactor.PullRequestSyncReactor; yield* reactor.start(); yield* Deferred.succeed(fixture.activation, undefined); yield* Queue.take(fixture.snapshotReads); yield* reactor.drain; return reactor; }); const sweepAgain = Effect.fn("sweepPullRequestSyncHarness")(function* ( fixture: Harness, reactor: PullRequestSyncReactor.PullRequestSyncReactor["Service"], ) { yield* TestClock.adjust("1 minute"); yield* Queue.take(fixture.snapshotReads); yield* reactor.drain; }); /** What the reactor would have persisted, so the next sweep sees its own writes. */ function applySync( snapshot: OrchestrationShellSnapshot, commands: ReadonlyArray<SyncCommand>, ): OrchestrationShellSnapshot { return { ...snapshot, snapshotSequence: snapshot.snapshotSequence + 1, threads: snapshot.threads.map((thread) => ({ ...thread, pullRequests: thread.pullRequests.map((link) => { const command = commands.findLast( (candidate) => candidate.threadId === thread.id && candidate.number === link.number, ); return command === undefined ? link : { ...link, snapshot: command.snapshot, stack: command.stack }; }), })), }; } describe("PullRequestSyncReactor", () => { it.effect("retries a failed stack read after the summary becomes terminal", () => Effect.scoped( Effect.gen(function* () { yield* TestClock.setTime(Date.parse(NOW)); let attempts = 0; const nativeStack: PullRequestStack = { id: "stack", number: 7, url: "https://github.com/owner/repository/stacks/7", base: "main", layers: [{ number: 7, headBranch: "feature", state: "merged" }], }; const fixture = yield* makeHarness({ snapshot: makeSnapshot([makeThread("one", { pullRequests: [makeLink(7)] })]), summary: (input) => Effect.succeed(makeSummary(input, { state: "merged", mergedAt: NOW })), stack: () => ++attempts === 1 ? Effect.fail( new PullRequestOperationError({ operation: "stack", detail: "temporary failure", }), ) : Effect.succeed(nativeStack), }); yield* Effect.gen(function* () { const reactor = yield* startAndSweep(fixture); const commands = yield* Ref.get(fixture.syncCommands); yield* Ref.update(fixture.snapshots, (snapshot) => applySync(snapshot, commands)); yield* sweepAgain(fixture, reactor); assert.strictEqual(attempts, 2); assert.deepStrictEqual((yield* Ref.get(fixture.syncCommands)).at(-1)?.stack, { kind: "native", ...nativeStack, }); }).pipe(Effect.provide(fixture.layer)); }), ), ); it.effect("explicit refresh reads a changed stack even when its PR summary is unchanged", () => Effect.scoped( Effect.gen(function* () { yield* TestClock.setTime(Date.parse(NOW)); const fixture = yield* makeHarness({ snapshot: makeSnapshot([makeThread("one", { pullRequests: [makeLink(7, {})] })]), }); yield* Effect.gen(function* () { const reactor = yield* startAndSweep(fixture); assert.strictEqual((yield* Ref.get(fixture.stackCalls)).length, 0); yield* reactor.requestSync({ host: "github.com", repository: "owner/repository", number: 7, }); yield* Queue.take(fixture.snapshotReads); yield* reactor.drain; assert.strictEqual((yield* Ref.get(fixture.stackCalls)).length, 1); }).pipe(Effect.provide(fixture.layer)); }), ), ); it.effect("snapshots an unsynced link once and writes it to the thread", () => Effect.scoped( Effect.gen(function* () { yield* TestClock.setTime(Date.parse(NOW)); const fixture = yield* makeHarness({ snapshot: makeSnapshot([makeThread("one", { pullRequests: [makeLink(42)] })]), summary: (input) => Effect.succeed(makeSummary(input, { title: "Ship it", isDraft: true })), }); yield* Effect.gen(function* () { yield* startAndSweep(fixture); assert.deepStrictEqual(yield* Ref.get(fixture.summaryCalls), [ { projectId: PROJECT_ID, host: "github.com", repository: "owner/repository", number: 42, }, ]); assert.deepStrictEqual( (yield* Ref.get(fixture.syncCommands)).map(({ commandId: _, ...rest }) => rest), [ { type: "thread.pull-request-link.sync", threadId: ThreadId.make("one"), host: "github.com", repository: "owner/repository", number: 42, snapshot: { state: "open", title: "Ship it", headBranch: "feature", baseBranch: "main", isDraft: true, updatedAt: "2026-08-27T00:00:00.000Z", syncedAt: NOW, closedAt: null, mergedAt: null, }, stack: null, }, ], ); assert.strictEqual((yield* Ref.get(fixture.stackCalls)).length, 1); }).pipe(Effect.provide(fixture.layer)); }), ), ); it.effect("asks the host once for a pull request shared by two threads", () => Effect.scoped( Effect.gen(function* () { yield* TestClock.setTime(Date.parse(NOW)); const fixture = yield* makeHarness({ snapshot: makeSnapshot([ makeThread("one", { pullRequests: [makeLink(42)] }), makeThread("two", { pullRequests: [makeLink(42, null, { repository: "Owner/Repository" })], }), ]), }); yield* Effect.gen(function* () { yield* startAndSweep(fixture); assert.strictEqual((yield* Ref.get(fixture.summaryCalls)).length, 1); const commands = yield* Ref.get(fixture.syncCommands); assert.deepStrictEqual( commands .map((command) => [command.threadId, command.repository] as const) .sort((left, right) => left[0].localeCompare(right[0])), [ [ThreadId.make("one"), "owner/repository"], [ThreadId.make("two"), "Owner/Repository"], ], ); }).pipe(Effect.provide(fixture.layer)); }), ), ); it.effect("dispatches nothing when the host snapshot is unchanged", () => Effect.scoped( Effect.gen(function* () { yield* TestClock.setTime(Date.parse(NOW)); const fixture = yield* makeHarness({ snapshot: makeSnapshot([makeThread("one", { pullRequests: [makeLink(42)] })]), }); yield* Effect.gen(function* () { const reactor = yield* startAndSweep(fixture); const firstSweep = yield* Ref.get(fixture.syncCommands); assert.strictEqual(firstSweep.length, 1); yield* Ref.update(fixture.snapshots, (snapshot) => applySync(snapshot, firstSweep)); yield* sweepAgain(fixture, reactor); // Still open on an active thread, so the host was asked again, but nothing changed. assert.strictEqual((yield* Ref.get(fixture.summaryCalls)).length, 2); assert.strictEqual((yield* Ref.get(fixture.stackCalls)).length, 1); assert.strictEqual((yield* Ref.get(fixture.syncCommands)).length, 1); }).pipe(Effect.provide(fixture.layer)); }), ), ); it.effect("refreshes closed links through the reactor's project after reopening elsewhere", () => Effect.scoped( Effect.gen(function* () { yield* TestClock.setTime(Date.parse(NOW)); const stale = yield* Ref.make(true); const fixture = yield* makeHarness({ snapshot: makeSnapshot([ makeThread("first", { pullRequests: [makeLink(42, { state: "closed" })] }), makeThread("second", { projectId: ProjectId.make("second-project"), pullRequests: [makeLink(42, { state: "closed" })], }), ]), invalidate: ({ reference }) => reference?.projectId === makeProject().id && reference.host === "github.com" ? Ref.set(stale, false) : Effect.void, summary: (input) => Ref.get(stale).pipe( Effect.map((cached) => makeSummary(input, { state: cached ? "closed" : "open" })), ), }); yield* Effect.gen(function* () { const reactor = yield* startAndSweep(fixture); assert.strictEqual((yield* Ref.get(fixture.summaryCalls)).length, 1); yield* reactor.requestSync({ host: "github.com", repository: "owner/repository", number: 42, }); yield* Queue.take(fixture.snapshotReads); yield* reactor.drain; const commands = yield* Ref.get(fixture.syncCommands); yield* Ref.update(fixture.snapshots, (snapshot) => applySync(snapshot, commands)); const snapshot = yield* Ref.get(fixture.snapshots); assert.deepStrictEqual( snapshot.threads.map((thread) => thread.pullRequests[0]?.snapshot?.state), ["open", "open"], ); }).pipe(Effect.provide(fixture.layer)); }), ), ); it.effect("stops asking the host once a pull request is merged", () => Effect.scoped( Effect.gen(function* () { yield* TestClock.setTime(Date.parse(NOW)); const fixture = yield* makeHarness({ snapshot: makeSnapshot([ makeThread("merged", { pullRequests: [makeLink(1, { state: "merged" })] }), ]), });