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); });});
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);
});
});