473 lines · 15.5 KB · TypeScript
RawDownload
import {  ProjectId,  ProviderInstanceId,  ThreadId,  TurnId,  type OrchestrationThreadShell,} from "@t3tools/contracts";import { describe, expect, it } from "vite-plus/test"; import {  canSettle,  changeRequestAutoSettles,  effectiveSettled,  hasQueuedTurnStart,  threadLastActivityAt,  type ChangeRequestStateLike,} from "./threadSettled.ts"; const NOW = "2026-04-10T00:00:00.000Z";const FRESH = "2026-04-09T00:00:00.000Z";const STALE = "2026-04-06T23:59:59.999Z"; describe("changeRequestAutoSettles", () => {  it.each([    ["open", true, false],    ["merged", true, true],    ["merged", false, false],    ["closed", false, true],    [null, false, false],  ] as const)("state=%s autoSettleOnMerge=%s returns %s", (state, autoSettleOnMerge, expected) => {    expect(changeRequestAutoSettles(state, autoSettleOnMerge)).toBe(expected);  });}); function makeShell(input: {  readonly settledOverride?: "settled" | "active" | null;  readonly activityAt: string | null;  readonly sessionStatus?: "starting" | "running";  readonly pending?: "approval" | "user-input";}): OrchestrationThreadShell {  const threadId = ThreadId.make("thread-1");  return {    id: threadId,    projectId: ProjectId.make("project-1"),    title: "Thread",    modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" },    runtimeMode: "full-access",    interactionMode: "default",    branch: null,    worktreePath: null,    latestTurn:      input.activityAt === null        ? null        : {            turnId: TurnId.make("turn-1"),            state: "completed",            requestedAt: input.activityAt,            startedAt: null,            completedAt: null,            assistantMessageId: null,          },    createdAt: "2026-04-01T00:00:00.000Z",    updatedAt: NOW,    archivedAt: null,    settledOverride: input.settledOverride ?? null,    settledAt: input.settledOverride === "settled" ? NOW : null,    session:      input.sessionStatus === undefined        ? null        : {            threadId,            status: input.sessionStatus,            providerName: "Codex",            runtimeMode: "full-access",            activeTurnId: null,            lastError: null,            updatedAt: NOW,          },    latestUserMessageAt: null,    hasPendingApprovals: input.pending === "approval",    hasPendingUserInput: input.pending === "user-input",    hasActionableProposedPlan: false,  };} describe("threadLastActivityAt", () => {  it("returns the latest real user or turn activity and ignores thread/session updates", () => {    const shell = makeShell({ activityAt: null, sessionStatus: "running" });    const withActivity: OrchestrationThreadShell = {      ...shell,      latestUserMessageAt: "2026-04-04T00:00:00.000Z",      latestTurn: {        turnId: TurnId.make("turn-1"),        state: "completed",        requestedAt: "2026-04-03T00:00:00.000Z",        startedAt: "2026-04-05T00:00:00.000Z",        completedAt: "2026-04-06T00:00:00.000Z",        assistantMessageId: null,      },    };     expect(threadLastActivityAt(withActivity)).toBe("2026-04-06T00:00:00.000Z");    expect(threadLastActivityAt(shell)).toBeNull();  });}); describe("effectiveSettled", () => {  const overrideCases = [null, "settled", "active"] as const;  const changeRequestStates = [undefined, "open", "merged"] as const;  const inactivityCases = [    ["fresh", FRESH],    ["stale", STALE],    ["no-activity", null],  ] as const;  const runningCases = [false, true] as const;  const pendingCases = [undefined, "approval", "user-input"] as const;  const truthTable = overrideCases.flatMap((settledOverride) =>    changeRequestStates.flatMap((changeRequestState) =>      inactivityCases.flatMap(([inactivity, activityAt]) =>        runningCases.flatMap((running) =>          pendingCases.map((pending) => ({            settledOverride,            changeRequestState,            inactivity,            activityAt,            running,            pending,            // Settled iff nothing blocks (pending work / live session) AND            // the override says settled, or (with no override) a merged PR            // or staleness auto-settles. The "active" pin suppresses both            // auto signals, and an open PR suppresses the inactivity path:            // a thread with a PR out for review is never done, however quiet.            expected:              pending === undefined &&              !running &&              (settledOverride === "settled" ||                (settledOverride === null &&                  (changeRequestState === "merged" ||                    (changeRequestState !== "open" && inactivity === "stale")))),          })),        ),      ),    ),  );   it.each(truthTable)(    "override=$settledOverride pr=$changeRequestState inactivity=$inactivity running=$running pending=$pending",    ({ settledOverride, changeRequestState, activityAt, running, pending, expected }) => {      const shell = makeShell({        settledOverride,        activityAt,        ...(running ? { sessionStatus: "running" as const } : {}),        ...(pending === undefined ? {} : { pending }),      });      const changeRequestOptions =        changeRequestState === undefined          ? {}          : { changeRequestState: changeRequestState as ChangeRequestStateLike };       expect(        effectiveSettled(shell, {          now: NOW,          autoSettleAfterDays: 3,          ...changeRequestOptions,        }),      ).toBe(expected);    },  );   it("treats closed change requests like merged ones", () => {    const shell = makeShell({ activityAt: null });    expect(      effectiveSettled(shell, {        now: NOW,        autoSettleAfterDays: null,        changeRequestState: "closed",      }),    ).toBe(true);  });   it("settles immediately when a change request merges or closes", () => {    const recentlyActive = makeShell({ activityAt: "2026-04-09T23:59:59.999Z" });    for (const changeRequestState of ["merged", "closed"] as const) {      expect(        effectiveSettled(recentlyActive, {          now: NOW,          autoSettleAfterDays: null,          changeRequestState,        }),      ).toBe(true);    }  });   it("can keep a merged change request active", () => {    const recentlyActive = makeShell({ activityAt: "2026-04-09T23:59:59.999Z" });    expect(      effectiveSettled(recentlyActive, {        now: NOW,        autoSettleAfterDays: null,        autoSettleOnMerge: false,        changeRequestState: "merged",      }),    ).toBe(false);     expect(      effectiveSettled(recentlyActive, {        now: NOW,        autoSettleAfterDays: null,        autoSettleOnMerge: false,        changeRequestState: "closed",      }),    ).toBe(true);  });   it("never auto-settles a stale thread with an open change request", () => {    const stale = makeShell({ activityAt: STALE });    expect(      effectiveSettled(stale, {        now: NOW,        autoSettleAfterDays: 3,        changeRequestState: "open",      }),    ).toBe(false);    // An explicit user settle still wins: open PR only blocks the auto path.    const settled = makeShell({ settledOverride: "settled", activityAt: STALE });    expect(      effectiveSettled(settled, {        now: NOW,        autoSettleAfterDays: 3,        changeRequestState: "open",      }),    ).toBe(true);  });   it("keeps an explicitly un-settled merged-PR thread active", () => {    const shell = makeShell({      settledOverride: "active",      activityAt: "2026-04-09T23:59:59.999Z",    });    expect(      effectiveSettled(shell, {        now: NOW,        autoSettleAfterDays: null,        changeRequestState: "merged",      }),    ).toBe(false);  });   it("never settles a starting session, even with a settled override", () => {    const shell = makeShell({      settledOverride: "settled",      activityAt: STALE,      sessionStatus: "starting",    });    expect(      effectiveSettled(shell, {        now: NOW,        autoSettleAfterDays: 3,        changeRequestState: "merged",      }),    ).toBe(false);  });   it("keeps a new turn active from queued through starting and running", () => {    const requestedAt = "2026-04-09T12:00:00.000Z";    const transitionNow = "2026-04-09T12:00:30.000Z";    const base = makeShell({      settledOverride: null,      activityAt: STALE,    });    const queued: OrchestrationThreadShell = {      ...base,      latestUserMessageAt: requestedAt,      latestTurn: null,      session: null,    };    const starting: OrchestrationThreadShell = {      ...queued,      session: {        threadId: queued.id,        status: "starting",        providerName: "Codex",        runtimeMode: "full-access",        activeTurnId: null,        lastError: null,        updatedAt: requestedAt,      },    };    const running: OrchestrationThreadShell = {      ...starting,      session: {        ...starting.session!,        status: "running",        activeTurnId: TurnId.make("turn-new"),      },    };     for (const shell of [queued, starting, running]) {      expect(        effectiveSettled(shell, {          now: transitionNow,          autoSettleAfterDays: 3,          changeRequestState: "merged",        }),      ).toBe(false);    }  });   it("uses a strict inactivity boundary and honors a null threshold", () => {    const boundary = makeShell({      activityAt: "2026-04-07T00:00:00.000Z",    });    const stale = makeShell({ activityAt: STALE });     expect(effectiveSettled(boundary, { now: NOW, autoSettleAfterDays: 3 })).toBe(false);    expect(effectiveSettled(stale, { now: NOW, autoSettleAfterDays: null })).toBe(false);  });}); describe("hasQueuedTurnStart", () => {  const QUEUED_AT = "2026-04-09T12:00:00.000Z";  // Within the adoption grace window of the queued message.  const JUST_AFTER = { now: "2026-04-09T12:00:30.000Z" };   it("flags a user message no turn has picked up, within the grace window", () => {    const noTurn = { latestUserMessageAt: QUEUED_AT, latestTurn: null, session: null };    expect(hasQueuedTurnStart(noTurn, JUST_AFTER)).toBe(true);     const staleTurn = {      ...makeShell({ activityAt: FRESH }),      latestUserMessageAt: QUEUED_AT,    };    expect(hasQueuedTurnStart(staleTurn, JUST_AFTER)).toBe(true);  });   it("expires after the grace window: an unadopted message is a failed start, not queued work", () => {    const noTurn = { latestUserMessageAt: QUEUED_AT, latestTurn: null, session: null };    expect(hasQueuedTurnStart(noTurn, { now: "2026-04-09T12:03:00.000Z" })).toBe(false);    // Historical shells (e.g. from servers that never carried latestTurn)    // must never read as queued.    expect(hasQueuedTurnStart(noTurn, { now: NOW })).toBe(false);  });   it("clears once a turn adopts the message or the start fails", () => {    const adopted = {      ...makeShell({ activityAt: QUEUED_AT }),      latestUserMessageAt: QUEUED_AT,    };    expect(hasQueuedTurnStart(adopted, JUST_AFTER)).toBe(false);     const failed = makeShell({ activityAt: FRESH });    const failedShell = {      ...failed,      latestUserMessageAt: QUEUED_AT,      session: {        threadId: failed.id,        status: "error" as const,        providerName: "Codex",        runtimeMode: "full-access" as const,        activeTurnId: null,        lastError: "boom",        updatedAt: NOW,      },    };    expect(hasQueuedTurnStart(failedShell, JUST_AFTER)).toBe(false);  });   it("is quiet without user messages", () => {    expect(hasQueuedTurnStart(makeShell({ activityAt: FRESH }), JUST_AFTER)).toBe(false);  });   it("bounds the grace window in both directions: a future-stamped message is skew, not queued work", () => {    // Message timestamps originate on other devices; a clock an hour ahead    // must not hold the queued state for the whole skew.    const skewed = {      latestUserMessageAt: "2026-04-09T13:00:00.000Z",      latestTurn: null,      session: null,    };    expect(hasQueuedTurnStart(skewed, { now: "2026-04-09T12:00:00.000Z" })).toBe(false);    // A small negative age (within the grace window) still reads as queued.    const slightlyAhead = {      latestUserMessageAt: "2026-04-09T12:00:30.000Z",      latestTurn: null,      session: null,    };    expect(hasQueuedTurnStart(slightlyAhead, { now: "2026-04-09T12:00:00.000Z" })).toBe(true);  });}); describe("canSettle", () => {  it("blocks every state effectiveSettled refuses to classify as settled", () => {    expect(canSettle(makeShell({ activityAt: FRESH }), { now: NOW })).toBe(true);    expect(      canSettle(makeShell({ activityAt: FRESH, sessionStatus: "starting" }), { now: NOW }),    ).toBe(false);    expect(      canSettle(makeShell({ activityAt: FRESH, sessionStatus: "running" }), { now: NOW }),    ).toBe(false);    expect(canSettle(makeShell({ activityAt: FRESH, pending: "approval" }), { now: NOW })).toBe(      false,    );    expect(canSettle(makeShell({ activityAt: FRESH, pending: "user-input" }), { now: NOW })).toBe(      false,    );  });   it("blocks settling a queued turn start, only within the grace window", () => {    const queued = {      ...makeShell({ activityAt: FRESH }),      latestUserMessageAt: "2026-04-09T12:00:00.000Z",    };    const justAfter = "2026-04-09T12:00:30.000Z";    expect(canSettle(queued, { now: justAfter })).toBe(false);    // effectiveSettled must agree: queued work never auto-settles either,    // even with a merged PR.    expect(      effectiveSettled(queued, {        now: justAfter,        autoSettleAfterDays: 3,        changeRequestState: "merged",      }),    ).toBe(false);    // Past the window the message is a failed/stale start: settleable again.    expect(canSettle(queued, { now: NOW })).toBe(true);  });   it("lets a server-accepted settle overrule the clock-derived queued blocker", () => {    // The settle action ran with wall-clock `now` (past the grace window);    // the list partition re-evaluates with a minute-floored `now` that is    // still INSIDE the window. settledAt >= message time proves the server    // already adjudicated this exact message, so the row must not snap back    // to active until the coarser clock catches up.    const messageAt = "2026-04-09T12:00:00.000Z";    const flooredNow = "2026-04-09T12:01:00.000Z";    const base = makeShell({ settledOverride: "settled", activityAt: null });    const settledAfterMessage = {      ...base,      latestUserMessageAt: messageAt,      settledAt: "2026-04-09T12:02:10.000Z",    };    expect(hasQueuedTurnStart(settledAfterMessage, { now: flooredNow })).toBe(true);    expect(effectiveSettled(settledAfterMessage, { now: flooredNow, autoSettleAfterDays: 3 })).toBe(      true,    );     // A message NEWER than settledAt is genuinely new work: still blocked    // until the server's auto-unsettle lands.    const messageAfterSettle = {      ...base,      latestUserMessageAt: "2026-04-09T12:03:00.000Z",      settledAt: "2026-04-09T12:02:10.000Z",    };    expect(      effectiveSettled(messageAfterSettle, {        now: "2026-04-09T12:03:30.000Z",        autoSettleAfterDays: 3,      }),    ).toBe(false);  });   it("agrees with effectiveSettled's blockers for explicitly settled shells", () => {    // Anything canSettle rejects must render as active even when the user    // settled it earlier.    const blocked = makeShell({      settledOverride: "settled",      activityAt: FRESH,      pending: "user-input",    });    expect(canSettle(blocked, { now: NOW })).toBe(false);    expect(effectiveSettled(blocked, { now: NOW, autoSettleAfterDays: 3 })).toBe(false);  });});