Skip to content
Source
Sign inSign up

useThreadVisitedMigration.test.ts

103 lines3.8 KB

1import { scopeThreadRef, scopedThreadKey } from "@t3tools/client-runtime/environment";
2import { runAtomCommand } from "@t3tools/client-runtime/state/runtime";
3import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell";
4import { EnvironmentId } from "@t3tools/contracts";
5import { AsyncResult, Atom, AtomRegistry } from "effect/reactivity";
6import { describe, expect, it, vi } from "vite-plus/test";
7​
8import { environmentThreadShells } from "../state/threads";
9import { makeThreadFixture } from "../test-fixtures";
10import { startThreadVisitedMigration } from "./useThreadVisitedMigration";
11​
12const state = vi.hoisted(() => ({
13 localVisits: {} as Record<string, string>,
14}));
15​
16vi.mock("@t3tools/client-runtime/state/runtime", () => ({ runAtomCommand: vi.fn() }));
17vi.mock("../uiStateStore", () => ({
18 useUiStateStore: { getState: () => ({ threadLastVisitedAtById: state.localVisits }) },
19}));
20vi.mock("../state/threads", async () => {
21 const { Atom } = await import("effect/reactivity");
22 return {
23 environmentThreadShells: {
24 threadShellsAtom: Atom.make<ReadonlyArray<EnvironmentThreadShell>>([]),
25 },
26 threadEnvironment: { visit: {} },
27 };
28});
29​
30const localVisit = "2026-10-27T10:00:00.000Z";
31​
32function setup(threads: ReadonlyArray<EnvironmentThreadShell>, local = localVisit) {
33 state.localVisits = Object.fromEntries(
34 threads.map((thread) => [
35 scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)),
36 local,
37 ]),
38 );
39 vi.mocked(runAtomCommand).mockReset().mockResolvedValue(AsyncResult.success(undefined));
40 const tasks: Array<() => void> = [];
41 const registry = AtomRegistry.make({
42 scheduleTask: (task) => {
43 tasks.push(task);
44 return () => {};
45 },
46 });
47 const shells = environmentThreadShells.threadShellsAtom;
48 if (!Atom.isWritable(shells)) throw new Error("Expected writable test shell state");
49 registry.set(shells, threads);
50 const stop = startThreadVisitedMigration(registry);
51 return {
52 setThreads(next: ReadonlyArray<EnvironmentThreadShell>) {
53 registry.set(shells, next);
54 for (let task = tasks.shift(); task !== undefined; task = tasks.shift()) task();
55 },
56 [Symbol.dispose]() {
57 stop();
58 registry.dispose();
59 },
60 };
62​
63describe("startThreadVisitedMigration", () => {
64 it("preserves mark unread when a client with a newer local visit reloads", () => {
65 using _migration = setup([makeThreadFixture({ lastVisitedAt: "2026-10-27T09:59:59.999Z" })]);
66​
67 expect(runAtomCommand).not.toHaveBeenCalled();
68 });
69​
70 it("seeds unset visits once per environment and thread", () => {
71 const thread = makeThreadFixture({ lastVisitedAt: null });
72 const remote = { ...thread, environmentId: EnvironmentId.make("remote") };
73 using migration = setup([thread, remote]);
74​
75 expect(vi.mocked(runAtomCommand).mock.calls.map((call) => call[2])).toEqual([
76 {
77 environmentId: thread.environmentId,
78 input: { threadId: thread.id, visitedAt: localVisit },
79 },
80 {
81 environmentId: remote.environmentId,
82 input: { threadId: remote.id, visitedAt: localVisit },
83 },
84 ]);
85 migration.setThreads([{ ...thread }, { ...remote }]);
86 expect(runAtomCommand).toHaveBeenCalledTimes(2);
87 });
88​
89 it("waits for older servers to support visits before migrating", () => {
90 const thread = makeThreadFixture();
91 expect(thread.lastVisitedAt).toBeUndefined();
92 using migration = setup([thread]);
93 expect(runAtomCommand).not.toHaveBeenCalled();
94​
95 migration.setThreads([{ ...thread, lastVisitedAt: null }]);
96 expect(runAtomCommand).toHaveBeenCalledTimes(1);
97 });
98​
99 it.each(["", "not-a-date"])("ignores invalid local visits: %s", (local) => {
100 using _migration = setup([makeThreadFixture({ lastVisitedAt: null })], local);
101 expect(runAtomCommand).not.toHaveBeenCalled();
102 });
103});