1203 lines · 45.6 KB · TypeScript
RawDownload
import {  DEFAULT_SERVER_SETTINGS,  ProjectId,  ProviderInstanceId,  PullRequestOperationError,  ThreadId,  type OrchestrationCommand,  type OrchestrationProjectShell,  type OrchestrationShellSnapshot,  type OrchestrationThreadShell,  type PullRequestSummary,  type ServerSettings,  type ServerSettingsPatch,} from "@t3tools/contracts";import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings";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 FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as PubSub from "effect/PubSub";import * as Queue from "effect/Queue";import * as Ref from "effect/Ref";import * as Stream from "effect/Stream";import { TestClock } from "effect/testing"; import { GitManager, type GitBranchPullRequest } from "../git/GitManager.ts";import {  PullRequestService,  type PullRequestMergeEvent,} from "../pullRequest/PullRequestService.ts";import { ServerActivation } from "../serverActivation.ts";import { ServerSettingsService } from "../serverSettings.ts";import { OrchestrationCommandInvariantError } from "./Errors.ts";import {  OrchestrationEngineService,  type OrchestrationEngineShape,} from "./Services/OrchestrationEngine.ts";import { ProjectionSnapshotQuery } from "./Services/ProjectionSnapshotQuery.ts";import * as ThreadSettlementReactor from "./ThreadSettlementReactor.ts"; const NOW = "2026-08-28T12:00:00.000Z";const PROJECT_ID = ProjectId.make("settlement-project");const LINKED_PROJECT_ID = ProjectId.make("linked-settlement-project"); type AutoSettleCommand = Extract<OrchestrationCommand, { readonly type: "thread.auto-settle" }>; const testCrypto = Crypto.make({  randomBytes: (size) => new Uint8Array(size).fill(1),  digest: (_algorithm, data) => Effect.succeed(data),}); function makeProject(  id: ProjectId = PROJECT_ID,  workspaceRoot = "/workspace/project",): OrchestrationProjectShell {  return {    id,    title: `Project ${id}`,    workspaceRoot,    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",    pullRequests: [],    branch: null,    worktreePath: null,    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 makeSnapshot(  threads: ReadonlyArray<OrchestrationThreadShell>,  projects: ReadonlyArray<OrchestrationProjectShell> = [makeProject()],): OrchestrationShellSnapshot {  return {    snapshotSequence: 1,    projects,    threads,    updatedAt: NOW,  };} function makePullRequestSummary(input: {  readonly projectId: ProjectId;  readonly repository: string;  readonly number: number;  readonly state: "open" | "closed" | "merged";  readonly updatedAt?: string;}): PullRequestSummary {  return {    provider: "github",    projectId: input.projectId,    repository: input.repository,    number: input.number,    title: "Pull request",    url: `https://example.test/${input.repository}/pull/${input.number}`,    state: input.state,    headBranch: "feature",    baseBranch: "main",    updatedAt: input.updatedAt ?? NOW,    closedAt: input.state === "closed" ? (input.updatedAt ?? NOW) : null,    mergedAt: input.state === "merged" ? (input.updatedAt ?? NOW) : null,  };} function makeBranchPullRequest(  state: GitBranchPullRequest["state"],  updatedAt: string | null = NOW,): GitBranchPullRequest {  return {    number: 42,    title: "Branch pull request",    url: "https://example.test/owner/repository/pull/42",    baseRef: "main",    headRef: "saved-feature",    repositoryKey: "example.test/owner/repository",    state,    updatedAt,    closedAt: state === "closed" ? updatedAt : null,    mergedAt: state === "merged" ? updatedAt : null,  };} interface HarnessOptions {  readonly snapshot: OrchestrationShellSnapshot;  readonly settings?: ServerSettings;  readonly branchPullRequest?: GitManager["Service"]["branchPullRequest"];  readonly pullRequestSummary?: PullRequestService["Service"]["summary"];  readonly existingWorktreePaths?: ReadonlyArray<string>;  readonly onDispatch?: (    command: AutoSettleCommand,  ) => Effect.Effect<void, OrchestrationCommandInvariantError>;} const makeHarness = Effect.fn("makeThreadSettlementHarness")(function* (options: HarnessOptions) {  const activation = yield* Deferred.make<void>();  const snapshots = yield* Ref.make(options.snapshot);  const snapshotReadCount = yield* Ref.make(0);  const snapshotReads = yield* Queue.unbounded<number>();  const settings = yield* Ref.make(options.settings ?? DEFAULT_SERVER_SETTINGS);  const settingsReads = yield* Queue.unbounded<ServerSettings>();  const settingsChanges = yield* PubSub.unbounded<ServerSettings>();  const mergedPullRequests = yield* PubSub.unbounded<PullRequestMergeEvent>();  const commands = yield* Ref.make<ReadonlyArray<AutoSettleCommand>>([]);  const branchCalls = yield* Ref.make<    ReadonlyArray<{ readonly cwd: string; readonly branch: string }>  >([]);  const summaryCalls = yield* Ref.make<    ReadonlyArray<{      readonly projectId: ProjectId;      readonly repository: string;      readonly number: number;    }>  >([]);  const summaryRecovery = yield* Ref.make<ReadonlyArray<boolean | undefined>>([]);  const invalidatedCwds = yield* Ref.make<ReadonlyArray<string>>([]);   const updateSettings = (patch: ServerSettingsPatch) =>    Effect.gen(function* () {      const next = applyServerSettingsPatch(yield* Ref.get(settings), patch);      yield* Ref.set(settings, next);      yield* PubSub.publish(settingsChanges, next);      return next;    });   const branchPullRequest: GitManager["Service"]["branchPullRequest"] = (input, readOptions) =>    Ref.update(branchCalls, (calls) => [...calls, input]).pipe(      Effect.andThen(options.branchPullRequest?.(input, readOptions) ?? Effect.succeed(null)),    );  const pullRequestSummary: PullRequestService["Service"]["summary"] = (input, readOptions) =>    Effect.gen(function* () {      yield* Ref.update(summaryCalls, (calls) => [...calls, input]);      yield* Ref.update(summaryRecovery, (values) => [        ...values,        readOptions?.recoverTransientFailure,      ]);      return yield* (        options.pullRequestSummary?.(input, readOptions) ??          Effect.succeed(            makePullRequestSummary({              ...input,              state: "open",            }),          )      );    });   const dispatch: OrchestrationEngineShape["dispatch"] = (command) => {    if (command.type !== "thread.auto-settle") {      return Effect.die(new Error(`Unexpected command: ${command.type}`));    }    return Ref.update(commands, (recorded) => [...recorded, command]).pipe(      Effect.andThen(options.onDispatch?.(command) ?? Effect.void),      Effect.as({ sequence: 1 }),    );  };   const serverSettings = ServerSettingsService.of({    start: Effect.void,    ready: Effect.void,    getSettings: Ref.get(settings).pipe(Effect.tap((value) => Queue.offer(settingsReads, value))),    updateSettings,    streamChanges: Stream.fromPubSub(settingsChanges),    subscribeChanges: PubSub.subscribe(settingsChanges).pipe(      Effect.map((subscription) => Stream.fromSubscription(subscription)),    ),  });   const dependencies = Layer.mergeAll(    Layer.mock(ProjectionSnapshotQuery)({      getShellSnapshot: () =>        Ref.updateAndGet(snapshotReadCount, (count) => count + 1).pipe(          Effect.tap((count) => Queue.offer(snapshotReads, count)),          Effect.andThen(Ref.get(snapshots)),        ),    }),    Layer.mock(GitManager)({      branchPullRequest,      invalidateStatus: (cwd) => Ref.update(invalidatedCwds, (cwds) => [...cwds, cwd]),    }),    Layer.mock(PullRequestService)({      summary: pullRequestSummary,      subscribeMerges: PubSub.subscribe(mergedPullRequests).pipe(        Effect.map((subscription) => Stream.fromSubscription(subscription)),      ),    }),    Layer.mock(OrchestrationEngineService)({      readEvents: () => Stream.empty,      dispatch,      streamDomainEvents: Stream.empty,      latestSequence: Effect.succeed(0),    }),    Layer.succeed(ServerSettingsService, serverSettings),    Layer.succeed(ServerActivation, Deferred.await(activation)),    Layer.succeed(Crypto.Crypto, testCrypto),    FileSystem.layerNoop({      exists: (path) => Effect.succeed(options.existingWorktreePaths?.includes(path) ?? false),    }),  );   return {    activation,    snapshots,    snapshotReadCount,    snapshotReads,    settingsReads,    commands,    branchCalls,    summaryCalls,    summaryRecovery,    invalidatedCwds,    updateSettings,    publishMerge: PubSub.publish(mergedPullRequests, {      projectId: PROJECT_ID,      repository: "owner/repository",      number: 42,      mergedAt: NOW,    }),    layer: ThreadSettlementReactor.layer.pipe(Layer.provide(dependencies)),  };}); const startHarness = Effect.fn("startThreadSettlementHarness")(function* (  reactor: ThreadSettlementReactor.ThreadSettlementReactor["Service"],  activation: Deferred.Deferred<void>,  snapshotReads: Queue.Queue<number>,) {  yield* reactor.start();  yield* Deferred.succeed(activation, undefined);  yield* Queue.take(snapshotReads);  yield* reactor.drain;}); describe("ThreadSettlementReactor", () => {  it.effect(    "settles all-terminal links from snapshots and keeps open or unsynced links active",    () =>      Effect.scoped(        Effect.gen(function* () {          yield* TestClock.setTime(Date.parse(NOW));          const link = (number: number, state: "open" | "merged" | null) => ({            host: "example.test",            repository: "owner/repository",            number,            url: `https://example.test/owner/repository/pull/${number}`,            source: "manual" as const,            linkedAt: NOW,            stack: null,            snapshot:              state === null                ? null                : {                    state,                    title: "Review",                    headBranch: "feature",                    baseBranch: "main",                    isDraft: false,                    updatedAt: NOW,                    syncedAt: NOW,                    mergedAt: state === "merged" ? NOW : null,                  },          });          const fixture = yield* makeHarness({            snapshot: makeSnapshot([              makeThread("merged", { pullRequests: [link(1, "merged"), link(2, "merged")] }),              makeThread("open", { pullRequests: [link(1, "merged"), link(2, "open")] }),              makeThread("unsynced", { pullRequests: [link(1, "merged"), link(2, null)] }),            ]),            settings: { ...DEFAULT_SERVER_SETTINGS, sidebarAutoSettleOnMerge: true },            branchPullRequest: () => Effect.die("linked threads must not query the branch"),            pullRequestSummary: () => Effect.die("linked threads must use their snapshots"),          });          yield* Effect.gen(function* () {            const reactor = yield* ThreadSettlementReactor.ThreadSettlementReactor;            yield* startHarness(reactor, fixture.activation, fixture.snapshotReads);            assert.deepStrictEqual(              (yield* Ref.get(fixture.commands)).map(({ threadId }) => threadId),              [ThreadId.make("merged")],            );            assert.deepStrictEqual(yield* Ref.get(fixture.branchCalls), []);            assert.deepStrictEqual(yield* Ref.get(fixture.summaryCalls), []);          }).pipe(Effect.provide(fixture.layer));        }),      ),  );  it.effect("uses saved PRs without settling resumed threads or branches with newer PRs", () =>    Effect.scoped(      Effect.gen(function* () {        yield* TestClock.setTime(Date.parse(NOW));        const previous = {          projectId: PROJECT_ID,          repository: "owner/repository",          number: 1,          url: "https://example.test/owner/repository/pull/1",        };        const project = {          ...makeProject(),          repositoryIdentity: {            canonicalKey: "example.test/owner/repository",            rootPath: "/workspace/project",            displayName: "owner/repository",            locator: {              source: "git-remote" as const,              remoteName: "origin",              remoteUrl: "https://example.test/owner/repository.git",            },          },        };        const fixture = yield* makeHarness({          snapshot: makeSnapshot(            [              makeThread("retained-terminal", { branch: "main", branchPullRequest: previous }),              makeThread("reused-manual", { branch: "reused", linkedPullRequest: previous }),              makeThread("reused-detected", { branch: "reused", branchPullRequest: previous }),              makeThread("foreign-branch-pr", { branch: "foreign", linkedPullRequest: previous }),              makeThread("resumed-manual", {                branch: "main",                linkedPullRequest: previous,                latestUserMessageAt: "2026-08-28T00:00:00.000Z",              }),              makeThread("resumed-detected", {                branch: "main",                branchPullRequest: previous,                latestUserMessageAt: "2026-08-28T00:00:00.000Z",              }),            ],            [project],          ),          settings: {            ...DEFAULT_SERVER_SETTINGS,            sidebarAutoSettleAfterDays: null,            sidebarAutoSettleOnMerge: true,          },          branchPullRequest: ({ branch }, options) =>            Effect.succeed(              branch === "reused"                ? makeBranchPullRequest(options?.refresh ? "open" : "merged")                : branch === "foreign"                  ? {                      ...makeBranchPullRequest("open"),                      repositoryKey: "example.test/another/repository",                    }                  : null,            ),          pullRequestSummary: (input) =>            Effect.succeed({              ...makePullRequestSummary({ ...input, state: "merged" }),              mergedAt: "2026-08-27T00:00:00.000Z",            }),        });        yield* Effect.gen(function* () {          const reactor = yield* ThreadSettlementReactor.ThreadSettlementReactor;          yield* startHarness(reactor, fixture.activation, fixture.snapshotReads);          assert.deepStrictEqual(            new Set((yield* Ref.get(fixture.commands)).map((command) => command.threadId)),            new Set([ThreadId.make("retained-terminal"), ThreadId.make("foreign-branch-pr")]),          );        }).pipe(Effect.provide(fixture.layer));      }),    ),  );   it.effect("skips PR work on startup, timer and merge sweeps when settlement is disabled", () =>    Effect.scoped(      Effect.gen(function* () {        yield* TestClock.setTime(Date.parse(NOW));        const fixture = yield* makeHarness({          snapshot: makeSnapshot([            makeThread("branch-thread", { branch: "feature" }),            makeThread("linked-thread", {              linkedPullRequest: {                projectId: PROJECT_ID,                repository: "owner/repository",                number: 42,                url: "https://example.test/owner/repository/pull/42",              },            }),          ]),          settings: {            ...DEFAULT_SERVER_SETTINGS,            sidebarAutoSettleAfterDays: null,            sidebarAutoSettleOnMerge: false,          },        });         yield* Effect.gen(function* () {          const reactor = yield* ThreadSettlementReactor.ThreadSettlementReactor;          yield* reactor.start();          yield* Queue.take(fixture.settingsReads);          yield* Deferred.succeed(fixture.activation, undefined);          yield* Queue.take(fixture.settingsReads);          yield* reactor.drain;          yield* TestClock.adjust("1 minute");          yield* Queue.take(fixture.settingsReads);          yield* reactor.drain;          yield* fixture.publishMerge;          yield* Queue.take(fixture.settingsReads);          yield* reactor.drain;           assert.deepStrictEqual(yield* Ref.get(fixture.branchCalls), []);          assert.deepStrictEqual(yield* Ref.get(fixture.summaryCalls), []);          assert.deepStrictEqual(yield* Ref.get(fixture.invalidatedCwds), []);          assert.deepStrictEqual(yield* Ref.get(fixture.commands), []);          assert.strictEqual(yield* Ref.get(fixture.snapshotReadCount), 0);           yield* fixture.updateSettings({ sidebarAutoSettleAfterDays: 1 });          yield* Queue.take(fixture.snapshotReads);          yield* reactor.drain;          // Inactive threads settle without a PR lookup, so only the dispatches prove work resumed.          assert.deepStrictEqual(            (yield* Ref.get(fixture.commands)).map((command) => command.threadId).toSorted(),            [ThreadId.make("branch-thread"), ThreadId.make("linked-thread")],          );        }).pipe(Effect.provide(fixture.layer));      }),    ),  );   it.effect("starts without clients and skips protected threads before pull request lookup", () =>    Effect.scoped(      Effect.gen(function* () {        yield* TestClock.setTime(Date.parse(NOW));        const linkedPullRequest = {          projectId: LINKED_PROJECT_ID,          repository: "owner/repository",          number: 42,          url: "https://example.test/owner/repository/pull/42",        } as const;        const skipped = [          makeThread("pending-approval", {

Lines 1–500 of 1203.