1import { ThreadId, type DeviceServiceState } from "@t3tools/contracts";
2import { describe, expect, it } from "vite-plus/test";
3
4import { selectedThreadDevicePreview, threadDevicePreviews } from "./threadDevicePreviews";
5
6const threadId = ThreadId.make("thread");
7const state: DeviceServiceState = {
8 hosts: [
9 {
10 id: "local",
11 kind: "local",
12 label: "Local",
13 platforms: [],
14 hubInstalled: true,
15 agentDeviceInstalled: true,
16 },
17 {
18 id: "remote",
19 kind: "ssh",
20 label: "Mac mini",
21 platforms: [],
22 hubInstalled: true,
23 agentDeviceInstalled: true,
24 },
25 ],
26 hostStatus: "ready",
27 hostStatuses: {},
28 devices: [
29 {
30 hostId: "local",
31 id: "emulator-5554",
32 platform: "android",
33 name: "Pixel 9",
34 version: "Android 16",
35 booted: true,
36 physical: false,
37 },
38 {
39 hostId: "remote",
40 id: "emulator-5554",
41 platform: "android",
42 name: "Pixel 8",
43 version: "Android 15",
44 booted: true,
45 physical: false,
46 },
47 ],
48 sessions: [
49 {
50 threadId,
51 hostId: "local",
52 deviceId: "emulator-5554",
53 platform: "android",
54 openedAt: "2026-09-18T00:00:00Z",
55 },
56 {
57 threadId,
58 hostId: "remote",
59 deviceId: "emulator-5554",
60 platform: "android",
61 openedAt: "2026-09-18T00:01:00Z",
62 },
63 {
64 threadId: ThreadId.make("another-thread"),
65 hostId: "local",
66 deviceId: "iphone",
67 platform: "ios",
68 openedAt: "2026-09-18T00:02:00Z",
69 },
70 ],
71 onboardingCompleted: true,
72 agentAccessEnabled: true,
73 hubBasePath: "/api/device-hub",
74 revision: 1,
75};
76
77describe("thread device previews", () => {
78 it("keeps device names and selection distinct when hosts have the same Android serial", () => {
79 const previews = threadDevicePreviews(state, threadId);
80 expect(previews.map((preview) => preview.name)).toEqual(["Pixel 9", "Pixel 8"]);
81 expect(new Set(previews.map((preview) => preview.key)).size).toBe(2);
82 expect(selectedThreadDevicePreview(previews, previews[1]!.key)?.description).toBe(
83 "Android 15 · Mac mini",
84 );
85 });
86
87 it("selects another open device when the agent closes the selected session", () => {
88 const previews = threadDevicePreviews(state, threadId);
89 const selectedKey = previews[1]!.key;
90 const afterClose = threadDevicePreviews({ ...state, sessions: [state.sessions[0]!] }, threadId);
91 expect(selectedThreadDevicePreview(afterClose, selectedKey)?.name).toBe("Pixel 9");
92 expect(selectedThreadDevicePreview([], selectedKey)).toBeNull();
93 });
94
95 it("can view an open session before device discovery metadata arrives", () => {
96 const previews = threadDevicePreviews({ ...state, hosts: [], devices: [] }, threadId);
97 expect(previews[0]?.name).toBe("Android Emulator");
98 expect(previews[0]?.session.deviceId).toBe("emulator-5554");
99 expect(previews[0]?.description).toBe("");
100 });
101
102 it("offers no devices before state arrives or in a thread without sessions", () => {
103 expect(threadDevicePreviews(null, threadId)).toEqual([]);
104 expect(threadDevicePreviews(state, ThreadId.make("empty-thread"))).toEqual([]);
105 });
106});