289 lines · 9.7 KB · TypeScript
RawDownload
import { describe, expect, it } from "vite-plus/test";import {  ProviderInstanceId,  ThreadId,  ProjectId,  TurnId,  type OrchestrationThreadShell,  type ThreadPullRequestLink,} from "@t3tools/contracts";import { type SettlementPullRequest, resolveAutoSettlementAt } from "./ThreadSettlementPolicy.ts"; const NOW = "2026-08-28T12:00:00.000Z";const makeThread = (  overrides: Partial<OrchestrationThreadShell> = {},): OrchestrationThreadShell => ({  id: ThreadId.make("thread-1"),  projectId: ProjectId.make("project-1"),  title: "Thread",  modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5" },  runtimeMode: "full-access",  interactionMode: "default",  pullRequests: [],  branch: "feature",  worktreePath: "/repo",  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,}); const decide = (  thread: OrchestrationThreadShell,  pullRequest: SettlementPullRequest | null = null,  settings: { days?: number | null; merge?: boolean } = {},) =>  resolveAutoSettlementAt({    thread,    pullRequest,    now: NOW,    autoSettleAfterDays: settings.days === undefined ? 3 : settings.days,    autoSettleOnMerge: settings.merge ?? true,  }) !== null; describe("resolveAutoSettlementAt", () => {  it("returns the last activity time for persisted settlement", () => {    expect(      resolveAutoSettlementAt({        thread: makeThread({          latestTurn: {            turnId: TurnId.make("turn-terminal"),            state: "completed",            requestedAt: "2026-08-19T00:00:00.000Z",            startedAt: "2026-08-19T00:01:00.000Z",            completedAt: "2026-08-21T00:00:00.000Z",            assistantMessageId: null,          },        }),        pullRequest: null,        now: NOW,        autoSettleAfterDays: 3,        autoSettleOnMerge: true,      }),    ).toBe("2026-08-21T00:00:00.000Z");  });   it("uses creation time for PR settlement when the thread has no activity", () => {    expect(      resolveAutoSettlementAt({        thread: makeThread({          latestUserMessageAt: null,          latestTurn: null,          updatedAt: "2026-08-27T00:00:00.000Z",        }),        pullRequest: { state: "closed", closedAt: NOW },        now: NOW,        autoSettleAfterDays: null,        autoSettleOnMerge: true,      }),    ).toBe("2026-08-01T00:00:00.000Z");  });   it("settles inactive threads and leaves never-used threads active", () => {    expect(decide(makeThread())).toBe(true);    expect(decide(makeThread({ latestUserMessageAt: null }))).toBe(false);    expect(decide(makeThread(), null, { days: null })).toBe(false);  });   it("keeps a thread active at the exact inactivity boundary", () => {    expect(decide(makeThread({ latestUserMessageAt: "2026-08-25T12:00:00.000Z" }))).toBe(false);  });   it("settles inactive threads with open pull requests", () => {    expect(decide(makeThread(), { state: "open", updatedAt: NOW })).toBe(true);  });   it("settles closed requests and honors the merge setting", () => {    expect(decide(makeThread(), { state: "closed", closedAt: NOW }, { merge: false })).toBe(true);    expect(decide(makeThread(), { state: "merged", mergedAt: NOW }, { merge: false })).toBe(true);    expect(      decide(makeThread(), { state: "merged", mergedAt: NOW }, { merge: false, days: null }),    ).toBe(false);  });   it("does not settle again after user activity newer than the PR", () => {    expect(      decide(        makeThread({ latestUserMessageAt: "2026-08-27T00:00:00.000Z" }),        { state: "merged", mergedAt: "2026-08-26T00:00:00.000Z" },        { days: null },      ),    ).toBe(false);  });   it.each(["closed", "merged"] as const)(    "ignores metadata edits after resumed work for %s requests",    (state) => {      expect(        decide(          makeThread({ latestUserMessageAt: "2026-08-27T00:00:00.000Z" }),          {            state,            closedAt: "2026-08-26T00:00:00.000Z",            mergedAt: "2026-08-26T00:00:00.000Z",            updatedAt: NOW,          },          { days: null },        ),      ).toBe(false);      expect(decide(makeThread(), { state, updatedAt: NOW }, { days: null })).toBe(false);    },  );   it("does not inherit a terminal pull request older than the thread", () => {    expect(      decide(        makeThread({ createdAt: "2026-08-20T00:00:00.000Z", latestUserMessageAt: null }),        { state: "closed", closedAt: "2026-08-19T00:00:00.000Z" },        { days: null },      ),    ).toBe(false);  });   it("requires a comparable PR timestamp for immediate settlement", () => {    const recentThread = makeThread({ latestUserMessageAt: "2026-08-27T00:00:00.000Z" });    expect(decide(recentThread, { state: "closed", closedAt: null })).toBe(false);    expect(decide(recentThread, { state: "merged", mergedAt: "unknown" })).toBe(false);    expect(decide(makeThread(), { state: "closed", closedAt: null })).toBe(true);  });   it("uses user request time instead of completion time as the PR anchor", () => {    const thread = makeThread({      latestTurn: {        turnId: TurnId.make("turn-1"),        state: "completed",        requestedAt: "2026-08-25T00:00:00.000Z",        startedAt: "2026-08-25T00:01:00.000Z",        completedAt: "2026-08-27T00:00:00.000Z",        assistantMessageId: null,      },    });    expect(decide(thread, { state: "merged", mergedAt: "2026-08-26T00:00:00.000Z" })).toBe(true);  });   it("blocks pins, snooze, pending work, live sessions, and queued starts", () => {    expect(decide(makeThread({ settledOverride: "active" }))).toBe(false);    expect(decide(makeThread({ snoozedUntil: "2026-08-29T00:00:00.000Z" }))).toBe(false);    expect(decide(makeThread({ hasPendingApprovals: true }))).toBe(false);    expect(decide(makeThread({ hasPendingUserInput: true }))).toBe(false);    expect(decide(makeThread({ backgroundLiveness: "working" }))).toBe(false);    expect(decide(makeThread({ backgroundLiveness: "monitoring" }))).toBe(false);    expect(      decide(        makeThread({          session: {            threadId: ThreadId.make("thread-1"),            status: "running",            providerName: "codex",            runtimeMode: "full-access",            activeTurnId: TurnId.make("turn-1"),            lastError: null,            updatedAt: NOW,          },        }),      ),    ).toBe(false);    expect(      decide(makeThread({ latestUserMessageAt: "2026-08-28T11:59:00.000Z", latestTurn: null })),    ).toBe(false);  });   it("allows a fresh completion to wake snooze before settlement", () => {    expect(      decide(        makeThread({          snoozedAt: "2026-08-19T00:00:00.000Z",          snoozedUntil: "2026-08-29T00:00:00.000Z",          latestTurn: {            turnId: TurnId.make("turn-woke"),            state: "completed",            requestedAt: "2026-08-18T00:00:00.000Z",            startedAt: "2026-08-18T00:01:00.000Z",            completedAt: "2026-08-20T00:00:00.000Z",            assistantMessageId: null,          },        }),      ),    ).toBe(true);  });}); function linkedRequest(  number: number,  snapshot: ThreadPullRequestLink["snapshot"],): ThreadPullRequestLink {  return {    host: "github.com",    repository: "org/repo",    number,    url: `https://github.com/org/repo/pull/${number}`,    source: "manual",    linkedAt: NOW,    stack: null,    snapshot,  };} const terminalSnapshot = (  state: "closed" | "merged",  terminalAt: string,  updatedAt = terminalAt,) => ({  state,  title: "Change",  headBranch: "feature",  baseBranch: "main",  isDraft: false,  closedAt: terminalAt,  mergedAt: state === "merged" ? terminalAt : null,  updatedAt,  syncedAt: NOW,}); describe("linked request settlement", () => {  it.each(["closed", "merged"] as const)(    "uses the latest actual %s transition despite later comments on another PR",    (state) => {      const old = linkedRequest(1, terminalSnapshot(state, "2026-08-19T00:00:00.000Z", NOW));      const recent = linkedRequest(2, terminalSnapshot(state, "2026-08-21T00:00:00.000Z"));      expect(decide(makeThread({ pullRequests: [old, recent] }), null, { days: null })).toBe(true);      expect(decide(makeThread({ pullRequests: [recent, old] }), null, { days: null })).toBe(true);      expect(decide(makeThread({ pullRequests: [old] }), null, { days: null })).toBe(false);    },  );   it("keeps unknown and open links active even after the inactivity window", () => {    const merged = linkedRequest(1, terminalSnapshot("merged", NOW));    const unknown = linkedRequest(2, null);    const open = linkedRequest(3, {      ...terminalSnapshot("closed", NOW),      state: "open",      closedAt: null,    });    expect(decide(makeThread({ pullRequests: [merged, unknown] }))).toBe(false);    expect(decide(makeThread({ pullRequests: [merged, open] }))).toBe(false);    expect(      decide(makeThread({ pullRequests: [merged, { ...unknown, source: "stack-dismissed" }] })),    ).toBe(true);  });   it("honors merge settings and ignores missing terminal timestamps", () => {    const merged = linkedRequest(1, terminalSnapshot("merged", NOW));    expect(decide(makeThread({ pullRequests: [merged] }), null, { days: null, merge: false })).toBe(      false,    );    const missing = linkedRequest(2, { ...terminalSnapshot("merged", NOW), mergedAt: null });    expect(decide(makeThread({ pullRequests: [missing] }), null, { days: null })).toBe(false);    expect(decide(makeThread({ pullRequests: [missing, merged] }), null, { days: null })).toBe(      true,    );  });});