import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell";import { EnvironmentId, ThreadId } from "@t3tools/contracts";import type { Atom } from "effect/unstable/reactivity";import { afterEach, describe, expect, it, vi } from "vite-plus/test"; import { createPendingThreadOrder } from "../features/threads/threadOrder";import { appAtomRegistry } from "./atom-registry";import { beginPendingThreadOrder, getPendingThreadOrder, pendingThreadOrderAtom,} from "./thread-order";import { environmentThreadShells } from "./threads"; vi.mock("./atom-registry", async () => { const { AtomRegistry } = await import("effect/unstable/reactivity"); return { appAtomRegistry: AtomRegistry.make() };});vi.mock("./threads", async () => { const { Atom } = await import("effect/unstable/reactivity"); return { environmentThreadShells: { threadShellsAtom: Atom.make([]).pipe(Atom.keepAlive) } };});vi.mock("./server", async () => { const { Atom } = await import("effect/unstable/reactivity"); return { environmentServerConfigsAtom: Atom.make(new Map()).pipe(Atom.keepAlive) };});vi.mock("./use-thread-outbox", async () => { const { Atom } = await import("effect/unstable/reactivity"); return { queuedThreadKeysAtom: Atom.make(new Set<string>()).pipe(Atom.keepAlive) };}); // The mocked shell source is writable so tests can deliver canonical upserts.const shellsAtom = environmentThreadShells.threadShellsAtom as Atom.Writable< readonly EnvironmentThreadShell[], readonly EnvironmentThreadShell[]>; function fixture() { // Only section membership and order fields are read by this coordinator. const rows = ["a", "b"].map( (id, index) => ({ id: ThreadId.make(id), environmentId: EnvironmentId.make("env"), createdAt: `2026-06-01T0${2 - index}:00:00.000Z`, archivedAt: null, pinnedAt: null, activeOrderKey: null, }) as EnvironmentThreadShell, ); appAtomRegistry.set(shellsAtom, rows); const pending = createPendingThreadOrder({ section: "active", ordered: rows, movedId: "env:b", direction: "up", assignments: [ { id: "env:b", orderKey: "aa" }, { id: "env:a", orderKey: "bb" }, ], }); const start = () => beginPendingThreadOrder(pending); const upsert = (id: string, key: string) => { const current = appAtomRegistry.get(shellsAtom); appAtomRegistry.set( shellsAtom, current.map((row) => (row.id === id ? { ...row, activeOrderKey: key } : row)), ); }; return { rows, start, upsert };} afterEach(() => appAtomRegistry.reset()); describe("shared mobile pending move", () => { it("blocks another pickup after receipts and clears on final canonical upsert", () => { const { start, upsert } = fixture(); const move = start(); move.complete(); expect(getPendingThreadOrder()).not.toBeNull(); upsert("b", "aa"); expect(getPendingThreadOrder()).not.toBeNull(); upsert("a", "bb"); expect(getPendingThreadOrder()).toBeNull(); expect(move.isPending()).toBe(false); }); it("waits for receipts when shells arrive first", () => { const { start, upsert } = fixture(); const move = start(); upsert("b", "aa"); upsert("a", "bb"); expect(getPendingThreadOrder()).not.toBeNull(); move.complete(); expect(getPendingThreadOrder()).toBeNull(); }); it.each(["failure", "interruption"])("releases a %s without restoring old canonical keys", () => { const { start, upsert } = fixture(); const move = start(); upsert("b", "aa"); move.cancel(); expect(getPendingThreadOrder()).toBeNull(); expect(appAtomRegistry.get(shellsAtom)[1]?.activeOrderKey).toBe("aa"); const next = start(); move.cancel(); expect(next.isPending()).toBe(true); next.cancel(); }); it("stops remaining writes when a canonical membership change invalidates the move", () => { const { rows, start } = fixture(); const move = start(); appAtomRegistry.set(shellsAtom, rows.slice(1)); expect(move.isPending()).toBe(false); expect(appAtomRegistry.get(pendingThreadOrderAtom)).toBeNull(); move.complete(); appAtomRegistry.set(shellsAtom, rows); expect(getPendingThreadOrder()).toBeNull(); });});
import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell";
import { EnvironmentId, ThreadId } from "@t3tools/contracts";
import type { Atom } from "effect/unstable/reactivity";
import { afterEach, describe, expect, it, vi } from "vite-plus/test";
import { createPendingThreadOrder } from "../features/threads/threadOrder";
import { appAtomRegistry } from "./atom-registry";
import {
beginPendingThreadOrder,
getPendingThreadOrder,
pendingThreadOrderAtom,
} from "./thread-order";
import { environmentThreadShells } from "./threads";
vi.mock("./atom-registry", async () => {
const { AtomRegistry } = await import("effect/unstable/reactivity");
return { appAtomRegistry: AtomRegistry.make() };
});
vi.mock("./threads", async () => {
const { Atom } = await import("effect/unstable/reactivity");
return { environmentThreadShells: { threadShellsAtom: Atom.make([]).pipe(Atom.keepAlive) } };
});
vi.mock("./server", async () => {
const { Atom } = await import("effect/unstable/reactivity");
return { environmentServerConfigsAtom: Atom.make(new Map()).pipe(Atom.keepAlive) };
});
vi.mock("./use-thread-outbox", async () => {
const { Atom } = await import("effect/unstable/reactivity");
return { queuedThreadKeysAtom: Atom.make(new Set<string>()).pipe(Atom.keepAlive) };
});
// The mocked shell source is writable so tests can deliver canonical upserts.
const shellsAtom = environmentThreadShells.threadShellsAtom as Atom.Writable<
readonly EnvironmentThreadShell[],
readonly EnvironmentThreadShell[]
>;
function fixture() {
// Only section membership and order fields are read by this coordinator.
const rows = ["a", "b"].map(
(id, index) =>
({
id: ThreadId.make(id),
environmentId: EnvironmentId.make("env"),
createdAt: `2026-06-01T0${2 - index}:00:00.000Z`,
archivedAt: null,
pinnedAt: null,
activeOrderKey: null,
}) as EnvironmentThreadShell,
);
appAtomRegistry.set(shellsAtom, rows);
const pending = createPendingThreadOrder({
section: "active",
ordered: rows,
movedId: "env:b",
direction: "up",
assignments: [
{ id: "env:b", orderKey: "aa" },
{ id: "env:a", orderKey: "bb" },
],
});
const start = () => beginPendingThreadOrder(pending);
const upsert = (id: string, key: string) => {
const current = appAtomRegistry.get(shellsAtom);
appAtomRegistry.set(
shellsAtom,
current.map((row) => (row.id === id ? { ...row, activeOrderKey: key } : row)),
);
};
return { rows, start, upsert };
}
afterEach(() => appAtomRegistry.reset());
describe("shared mobile pending move", () => {
it("blocks another pickup after receipts and clears on final canonical upsert", () => {
const { start, upsert } = fixture();
const move = start();
move.complete();
expect(getPendingThreadOrder()).not.toBeNull();
upsert("b", "aa");
expect(getPendingThreadOrder()).not.toBeNull();
upsert("a", "bb");
expect(getPendingThreadOrder()).toBeNull();
expect(move.isPending()).toBe(false);
});
it("waits for receipts when shells arrive first", () => {
const { start, upsert } = fixture();
const move = start();
upsert("b", "aa");
upsert("a", "bb");
expect(getPendingThreadOrder()).not.toBeNull();
move.complete();
expect(getPendingThreadOrder()).toBeNull();
});
it.each(["failure", "interruption"])("releases a %s without restoring old canonical keys", () => {
const { start, upsert } = fixture();
const move = start();
upsert("b", "aa");
move.cancel();
expect(getPendingThreadOrder()).toBeNull();
expect(appAtomRegistry.get(shellsAtom)[1]?.activeOrderKey).toBe("aa");
const next = start();
move.cancel();
expect(next.isPending()).toBe(true);
next.cancel();
});
it("stops remaining writes when a canonical membership change invalidates the move", () => {
const { rows, start } = fixture();
const move = start();
appAtomRegistry.set(shellsAtom, rows.slice(1));
expect(move.isPending()).toBe(false);
expect(appAtomRegistry.get(pendingThreadOrderAtom)).toBeNull();
move.complete();
appAtomRegistry.set(shellsAtom, rows);
expect(getPendingThreadOrder()).toBeNull();
});
});