1041 lines · 31.9 KB · TypeScript
RawDownload
import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell";import { threadSearchMatchKey } from "@t3tools/client-runtime/state/thread-search";import { resolveSnoozePresets } from "@t3tools/client-runtime/state/thread-settled";import {  CommandId,  EnvironmentId,  MessageId,  ProjectId,  ProviderInstanceId,  ThreadId,  TurnId,} from "@t3tools/contracts";import { describe, expect, it } from "vite-plus/test"; import type { PendingNewTask } from "../../state/use-pending-new-tasks";import {  buildThreadListV2Items,  buildThreadListV2ListItems,  resolveThreadListV2ChangeRequestState,  resolveThreadListV2Enabled,  resolveThreadListV2SnoozeMenuSelection,  resolveThreadListV2SnoozeGateExpiryMs,  resolveThreadListV2Status,  resolveThreadListV2SwipeActions,  sortThreadsForListV2,} from "./threadListV2"; const environmentId = EnvironmentId.make("environment-1"); function makeThread(  input: Partial<EnvironmentThreadShell> & Pick<EnvironmentThreadShell, "id" | "title">,): EnvironmentThreadShell {  return {    environmentId,    projectId: ProjectId.make("project-1"),    modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" },    runtimeMode: "full-access",    interactionMode: "default",    branch: null,    worktreePath: null,    latestTurn: null,    createdAt: "2026-06-01T00:00:00.000Z",    updatedAt: "2026-06-01T00:00:00.000Z",    archivedAt: null,    settledOverride: null,    settledAt: null,    session: null,    latestUserMessageAt: null,    hasPendingApprovals: false,    hasPendingUserInput: false,    hasActionableProposedPlan: false,    ...input,  };} const NOW = "2026-06-02T00:00:00.000Z";const linkedPullRequest = {  projectId: ProjectId.make("project-1"),  repository: "pingdotgg/t3code",  number: 42,  url: "https://github.com/pingdotgg/t3code/pull/42",}; describe("resolveThreadListV2ChangeRequestState", () => {  it("preserves the previous state while a linked pull request reloads", () => {    expect(      resolveThreadListV2ChangeRequestState({        linkedPullRequest,        state: null,        updatedAt: null,      }),    ).toBeUndefined();  });   it("clears the previous state after a pull request is unlinked", () => {    expect(      resolveThreadListV2ChangeRequestState({        linkedPullRequest: null,        state: null,        updatedAt: null,      }),    ).toBeNull();  });   it("reports a loaded linked pull request", () => {    expect(      resolveThreadListV2ChangeRequestState({        linkedPullRequest,        state: "merged",        updatedAt: "2026-06-02T00:00:00.000Z",      }),    ).toEqual({      state: "merged",      updatedAt: "2026-06-02T00:00:00.000Z",      linkedPullRequestKey: '["project-1","pingdotgg/t3code",42]',    });  });}); describe("resolveThreadListV2SnoozeMenuSelection", () => {  it("accepts a displayed evening preset while its wake time is still future", () => {    const menuOpenedAt = new Date(2026, 4, 8, 16, 59, 30);    const selectedAt = new Date(2026, 4, 8, 17, 0, 30);    const displayedPresets = resolveSnoozePresets(menuOpenedAt);     const selection = resolveThreadListV2SnoozeMenuSelection({      event: "snooze:evening",      displayedPresets,      now: selectedAt,    });     expect(selection).toEqual({      _tag: "selected",      preset: displayedPresets.find((preset) => preset.id === "evening"),    });  });   it("expires a displayed preset once its wake time has passed", () => {    const displayedPresets = resolveSnoozePresets(new Date(2026, 4, 8, 16, 59, 30));     expect(      resolveThreadListV2SnoozeMenuSelection({        event: "snooze:evening",        displayedPresets,        now: new Date(2026, 4, 8, 18, 0, 1),      }),    ).toEqual({ _tag: "expired" });  });   it("recomputes presets that remain available instead of using old timestamps", () => {    const displayedPresets = resolveSnoozePresets(new Date(2026, 4, 8, 10));    const selectedAt = new Date(2026, 4, 8, 10, 30);    const selection = resolveThreadListV2SnoozeMenuSelection({      event: "snooze:hour",      displayedPresets,      now: selectedAt,    });     expect(selection._tag).toBe("selected");    if (selection._tag === "selected") {      expect(selection.preset.snoozedUntil).toBe(        new Date(selectedAt.getTime() + 60 * 60 * 1_000).toISOString(),      );    }  });}); describe("resolveThreadListV2Enabled", () => {  it("defaults on when the device has never chosen", () => {    expect(      resolveThreadListV2Enabled({ legacyPreference: undefined, preferencesLoaded: true }),    ).toBe(true);  });   it("honors an explicit legacy opt-in", () => {    expect(resolveThreadListV2Enabled({ legacyPreference: true, preferencesLoaded: true })).toBe(      false,    );    expect(resolveThreadListV2Enabled({ legacyPreference: false, preferencesLoaded: true })).toBe(      true,    );  });   it("holds the default while preferences are still loading so the list does not remount", () => {    expect(      resolveThreadListV2Enabled({ legacyPreference: undefined, preferencesLoaded: false }),    ).toBe(true);  });}); describe("resolveThreadListV2Status", () => {  it("prioritizes approval over a running session", () => {    const thread = makeThread({      id: ThreadId.make("t"),      title: "t",      hasPendingApprovals: true,      session: {        threadId: ThreadId.make("t"),        status: "running",        providerName: "Codex",        providerInstanceId: ProviderInstanceId.make("codex"),        runtimeMode: "full-access",        activeTurnId: null,        lastError: null,        updatedAt: NOW,      },    });    expect(resolveThreadListV2Status(thread)).toBe("approval");  });   it("resolves ready for quiescent threads", () => {    expect(resolveThreadListV2Status(makeThread({ id: ThreadId.make("t"), title: "t" }))).toBe(      "ready",    );  });}); describe("resolveThreadListV2SwipeActions", () => {  it("offers settle and snooze for an active snoozable thread", () => {    expect(      resolveThreadListV2SwipeActions({        variant: "card",        settlementSupported: true,        snoozeSupported: true,        snoozable: true,      }),    ).toEqual({ primary: "settle", secondary: "snooze" });  });   it("offers un-settle and snooze for settled history", () => {    expect(      resolveThreadListV2SwipeActions({        variant: "slim",        settlementSupported: true,        snoozeSupported: true,        snoozable: true,      }),    ).toEqual({ primary: "unsettle", secondary: "snooze" });  });   it("omits snooze when the server or thread does not allow it", () => {    expect(      resolveThreadListV2SwipeActions({        variant: "card",        settlementSupported: true,        snoozeSupported: false,        snoozable: true,      }),    ).toEqual({ primary: "settle", secondary: null });    expect(      resolveThreadListV2SwipeActions({        variant: "card",        settlementSupported: true,        snoozeSupported: true,        snoozable: false,      }),    ).toEqual({ primary: "settle", secondary: null });  });   it("falls back to archive only for a pre-lifecycle server", () => {    expect(      resolveThreadListV2SwipeActions({        variant: "card",        settlementSupported: false,        snoozeSupported: false,        snoozable: true,      }),    ).toEqual({ primary: "archive", secondary: null });  });   it("offers wake and no snooze on a snoozed row", () => {    expect(      resolveThreadListV2SwipeActions({        variant: "slim",        settlementSupported: true,        snoozeSupported: true,        snoozable: true,        snoozed: true,      }),    ).toEqual({ primary: "unsnooze", secondary: null });  });}); describe("resolveThreadListV2SnoozeGateExpiryMs", () => {  it("reports when an unadopted turn's grace window lapses", () => {    const thread = makeThread({      id: ThreadId.make("t"),      title: "t",      latestUserMessageAt: "2026-06-02T00:00:30.000Z",    });    expect(resolveThreadListV2SnoozeGateExpiryMs(thread, { now: "2026-06-02T00:01:00.000Z" })).toBe(      Date.parse("2026-06-02T00:02:30.000Z"),    );  });   it("returns null once the thread is snoozable or when only data can unblock it", () => {    expect(      resolveThreadListV2SnoozeGateExpiryMs(        makeThread({ id: ThreadId.make("ready"), title: "Ready" }),        { now: NOW },      ),    ).toBe(null);    expect(      resolveThreadListV2SnoozeGateExpiryMs(        makeThread({          id: ThreadId.make("blocked"),          title: "Blocked",          hasPendingApprovals: true,          latestUserMessageAt: NOW,        }),        { now: NOW },      ),    ).toBe(null);  });}); describe("sortThreadsForListV2", () => {  it("orders by creation time, newest first, ignoring activity", () => {    const sorted = sortThreadsForListV2([      { id: "oldest", createdAt: "2026-06-01T08:00:00.000Z" },      { id: "newest", createdAt: "2026-06-01T12:00:00.000Z" },      { id: "middle", createdAt: "2026-06-01T10:00:00.000Z" },    ]);    expect(sorted.map((thread) => thread.id)).toEqual(["newest", "middle", "oldest"]);  });}); describe("buildThreadListV2Items", () => {  it("ignores the previous pull request state after a different pull request is linked", () => {    const thread = makeThread({      id: ThreadId.make("linked"),      title: "Linked pull request",      linkedPullRequest,    });    const layout = buildThreadListV2Items({      threads: [thread],      environmentId: null,      searchQuery: "",      changeRequestByKey: new Map([        [          `${environmentId}:${thread.id}`,          {            state: "merged" as const,            linkedPullRequestKey: '["project-1","pingdotgg/t3code",41]',          },        ],      ]),      now: NOW,    });     expect(layout.settledCount).toBe(0);    expect(layout.items[0]?.variant).toBe("card");  });   it("settles a thread only when the cached pull request identity matches", () => {    const thread = makeThread({      id: ThreadId.make("linked-merged"),      title: "Linked merged pull request",      linkedPullRequest,    });    const layout = buildThreadListV2Items({      threads: [thread],      environmentId: null,      searchQuery: "",      changeRequestByKey: new Map([        [          `${environmentId}:${thread.id}`,          {            state: "merged" as const,            linkedPullRequestKey: '["project-1","pingdotgg/t3code",42]',          },        ],      ]),      now: NOW,    });     expect(layout.settledCount).toBe(1);    expect(layout.items[0]?.variant).toBe("slim");  });   it("keeps a merged thread active when auto-settle on merge is off", () => {    const merged = makeThread({ id: ThreadId.make("merged"), title: "Merged" });    const layout = buildThreadListV2Items({      threads: [merged],      environmentId: null,      searchQuery: "",      changeRequestByKey: new Map([        [`${environmentId}:${merged.id}`, { state: "merged" as const }],      ]),      autoSettleOnMerge: false,      now: NOW,    });     expect(layout.items.map((item) => item.thread.id)).toEqual(["merged"]);    expect(layout.settledCount).toBe(0);  });   it("hides snoozed threads and counts them — visibility parity with web", () => {    const layout = buildThreadListV2Items({      threads: [        makeThread({ id: ThreadId.make("active"), title: "Active" }),        makeThread({          id: ThreadId.make("snoozed"),          title: "Snoozed",          snoozedUntil: "2026-06-03T09:00:00.000Z",          snoozedAt: "2026-06-01T12:00:00.000Z",        }),        makeThread({          id: ThreadId.make("woken"),          title: "Woken",          // Wake time already passed: back in the active list.          snoozedUntil: "2026-06-01T18:00:00.000Z",          snoozedAt: "2026-06-01T12:00:00.000Z",        }),      ],      environmentId: null,      searchQuery: "",      now: NOW,    });     // Same createdAt → static sort tiebreaks by id; the point is the woken    // thread is BACK in the card block and the snoozed one is gone.    expect(layout.items.map((item) => item.thread.id)).toEqual(["active", "woken"]);    expect(layout.snoozedCount).toBe(1);  });   it("places settled pinned threads in the settled shelf", () => {    const layout = buildThreadListV2Items({      threads: [        makeThread({ id: ThreadId.make("active"), title: "Active" }),        makeThread({          id: ThreadId.make("pinned-settled"),          title: "Pinned while settled",          pinnedAt: "2026-06-01T12:00:00.000Z",          settledOverride: "settled",          settledAt: "2026-06-01T12:00:00.000Z",        }),      ],      environmentId: null,      searchQuery: "",      now: NOW,    });     expect(layout.items.map((item) => item.thread.id)).toEqual(["active", "pinned-settled"]);    expect(layout.items.map((item) => item.pinned)).toEqual([false, false]);    expect(layout.settledCount).toBe(1);  });   it("moves pinned threads to the settled shelf when their pull request merges", () => {    const merged = makeThread({      id: ThreadId.make("pinned-merged"),      title: "Pinned merged pull request",      pinnedAt: "2026-06-01T12:00:00.000Z",    });    const layout = buildThreadListV2Items({      threads: [makeThread({ id: ThreadId.make("active"), title: "Active" }), merged],      environmentId: null,      searchQuery: "",      changeRequestByKey: new Map([[`${environmentId}:${merged.id}`, { state: "merged" }]]),      now: NOW,    });     expect(layout.items.map((item) => item.thread.id)).toEqual(["active", "pinned-merged"]);    expect(layout.items.map((item) => item.variant)).toEqual(["card", "slim"]);    expect(layout.items[1]?.thread.pinnedAt).toBe("2026-06-01T12:00:00.000Z");    expect(layout.settledCount).toBe(1);  });   it("moves inactive pinned threads to the settled shelf", () => {    const inactive = makeThread({      id: ThreadId.make("pinned-inactive"),      title: "Pinned inactive thread",      createdAt: "2026-05-20T00:00:00.000Z",      pinnedAt: "2026-05-21T00:00:00.000Z",      latestTurn: {        turnId: TurnId.make("turn-inactive"),        state: "completed",        requestedAt: "2026-05-21T00:00:00.000Z",        startedAt: "2026-05-21T00:00:01.000Z",        completedAt: "2026-05-21T00:00:02.000Z",        assistantMessageId: null,      },    });    const layout = buildThreadListV2Items({      threads: [inactive],      environmentId: null,      searchQuery: "",      now: NOW,    });     expect(layout.items[0]).toMatchObject({      thread: { id: "pinned-inactive" },      variant: "slim",      pinned: false,    });    expect(layout.settledCount).toBe(1);  });   it("keeps pinned merged threads pinned when auto-settle on merge is off", () => {    const merged = makeThread({      id: ThreadId.make("pinned-merged"),      title: "Pinned merged pull request",      pinnedAt: "2026-06-01T12:00:00.000Z",    });    const layout = buildThreadListV2Items({      threads: [merged],      environmentId: null,      searchQuery: "",      changeRequestByKey: new Map([[`${environmentId}:${merged.id}`, { state: "merged" }]]),      autoSettleOnMerge: false,      now: NOW,    });     expect(layout.items[0]).toMatchObject({      thread: { id: "pinned-merged" },      variant: "card",      pinned: true,    });    expect(layout.settledCount).toBe(0);  });

Lines 1–500 of 1041.