986 lines · 30.4 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,  RunId,  ThreadId,} from "@t3tools/contracts";import { describe, expect, it } from "vite-plus/test"; import type { PendingNewTask } from "../../state/use-pending-new-tasks";import { makeThreadShellFixture } from "../../test-fixtures";import {  buildThreadListV2Items,  buildThreadListV2ListItems,  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 makeThreadShellFixture({    environmentId,    ...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("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 runtime", () => {    const thread = makeThread({      id: ThreadId.make("t"),      title: "t",      hasPendingApprovals: true,      runtime: {        status: "running",        activeRunId: RunId.make("run-t"),        providerName: "Codex",        providerInstanceId: ProviderInstanceId.make("codex"),        lastError: null,        updatedAt: NOW,      },    });    expect(resolveThreadListV2Status(thread)).toBe("approval");  });   it("reports waiting when presentation parks runtime idle for background tasks", () => {    expect(      resolveThreadListV2Status(        makeThread({          id: ThreadId.make("t"),          title: "t",          pendingBackgroundTasks: [{ taskId: "bg-1", description: "Run Codex review" }],          runtime: {            status: "idle",            activeRunId: null,            providerInstanceId: ProviderInstanceId.make("codex"),            providerName: "Codex",            lastError: null,            updatedAt: NOW,          },        }),      ),    ).toBe("waiting");  });   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("places a persisted settled thread in the settled shelf", () => {    const thread = makeThread({      id: ThreadId.make("linked-merged"),      title: "Linked merged pull request",      linkedPullRequest,      settledOverride: "settled",      settledAt: NOW,    });    const layout = buildThreadListV2Items({      threads: [thread],      environmentId: null,      searchQuery: "",      now: NOW,    });     expect(layout.settledCount).toBe(1);    expect(layout.items[0]?.variant).toBe("slim");  });   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("keeps active pinned threads in the pinned block", () => {    const pinned = makeThread({      id: ThreadId.make("pinned"),      title: "Pinned thread",      pinnedAt: "2026-06-01T12:00:00.000Z",    });    const layout = buildThreadListV2Items({      threads: [pinned],      environmentId: null,      searchQuery: "",      now: NOW,    });     expect(layout.items[0]).toMatchObject({      thread: { id: "pinned" },      variant: "card",      pinned: true,    });    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("moves a settled pinned thread into the settled shelf — parity with web (#7969)", () => {    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",          // Stale settled state (the decider clears it on pin): the pin wins.          settledOverride: "settled",          settledAt: "2026-06-01T12:00:00.000Z",        }),      ],      environmentId: null,      searchQuery: "",      now: NOW,    });     // Since #7969 a settled thread leaves the active block even while pinned;    // the pin re-applies when the thread is un-settled.    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("snooze hides a pinned thread and wake restores it to the pinned block", () => {    const snoozedInput = {      threads: [        makeThread({ id: ThreadId.make("active"), title: "Active" }),        makeThread({          id: ThreadId.make("pinned-snoozed"),          title: "Pinned and snoozed",          pinnedAt: "2026-06-01T12:00:00.000Z",          snoozedUntil: "2026-06-03T09:00:00.000Z",          snoozedAt: "2026-06-01T11:00:00.000Z",        }),      ],      environmentId: null,      searchQuery: "",    };     // Before the wake time: the snooze wins; the pin holds underneath.    const whileSnoozed = buildThreadListV2Items({ ...snoozedInput, now: NOW });    expect(whileSnoozed.items.map((item) => item.thread.id)).toEqual(["active"]);    expect(whileSnoozed.snoozedCount).toBe(1);     // After the wake time: the thread returns pinned, back on top.    const afterWake = buildThreadListV2Items({ ...snoozedInput, now: "2026-06-03T10:00:00.000Z" });    expect(afterWake.items.map((item) => item.thread.id)).toEqual(["pinned-snoozed", "active"]);    expect(afterWake.items[0]?.pinned).toBe(true);    expect(afterWake.snoozedCount).toBe(0);  });   it("classifies snooze with the second-precise clock and reports the next wake", () => {    const layout = buildThreadListV2Items({      threads: [        makeThread({          id: ThreadId.make("just-woke"),          title: "Just woke",          // Woke 30s ago: hidden under the minute-floored clock, visible          // under the precise one.          snoozedUntil: "2026-06-02T00:00:30.000Z",          snoozedAt: "2026-06-01T12:00:00.000Z",        }),        makeThread({          id: ThreadId.make("still-snoozed"),          title: "Still snoozed",          snoozedUntil: "2026-06-02T09:00:00.000Z",          snoozedAt: "2026-06-01T12:00:00.000Z",        }),      ],      environmentId: null,      searchQuery: "",      now: "2026-06-02T00:01:07.500Z",    });     expect(layout.items.map((item) => item.thread.id)).toEqual(["just-woke"]);    expect(layout.snoozedCount).toBe(1);    expect(layout.nextSnoozeWakeAt).toBe("2026-06-02T09:00:00.000Z");  });   it("builds snoozed rows between active and settled when the shelf is expanded", () => {    const layout = buildThreadListV2Items({      threads: [        makeThread({ id: ThreadId.make("active"), title: "Active" }),        makeThread({          id: ThreadId.make("settled"),          title: "Settled",          settledOverride: "settled",          settledAt: NOW,        }),        makeThread({          id: ThreadId.make("later"),          title: "Wakes later",          snoozedUntil: "2026-06-03T09:00:00.000Z",          snoozedAt: "2026-06-01T12:00:00.000Z",        }),        makeThread({          id: ThreadId.make("sooner"),          title: "Wakes sooner",          snoozedUntil: "2026-06-02T09:00:00.000Z",          snoozedAt: "2026-06-01T12:00:00.000Z",        }),      ],      environmentId: null,      searchQuery: "",

Lines 1–500 of 986.