@vibelet/cli 0.1.37 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (323) hide show
  1. package/README.md +80 -0
  2. package/bin/cloudflared-quick-tunnel.mjs +11 -0
  3. package/bin/cloudflared-resolver.mjs +171 -0
  4. package/bin/vibelet-runtime-policy.mjs +36 -0
  5. package/bin/vibelet.cjs +12 -0
  6. package/bin/vibelet.mjs +1062 -0
  7. package/dist/index.cjs +126 -0
  8. package/package.json +25 -24
  9. package/app.json +0 -5
  10. package/dist/advertised-hosts.d.ts +0 -34
  11. package/dist/advertised-hosts.d.ts.map +0 -1
  12. package/dist/advertised-hosts.js +0 -176
  13. package/dist/advertised-hosts.js.map +0 -1
  14. package/dist/advertised-hosts.test.d.ts +0 -2
  15. package/dist/advertised-hosts.test.d.ts.map +0 -1
  16. package/dist/advertised-hosts.test.js +0 -96
  17. package/dist/advertised-hosts.test.js.map +0 -1
  18. package/dist/audit.d.ts +0 -30
  19. package/dist/audit.d.ts.map +0 -1
  20. package/dist/audit.js +0 -73
  21. package/dist/audit.js.map +0 -1
  22. package/dist/audit.test.d.ts +0 -2
  23. package/dist/audit.test.d.ts.map +0 -1
  24. package/dist/audit.test.js +0 -33
  25. package/dist/audit.test.js.map +0 -1
  26. package/dist/auth.d.ts +0 -6
  27. package/dist/auth.d.ts.map +0 -1
  28. package/dist/auth.js +0 -27
  29. package/dist/auth.js.map +0 -1
  30. package/dist/claude-hooks.d.ts +0 -58
  31. package/dist/claude-hooks.d.ts.map +0 -1
  32. package/dist/claude-hooks.js +0 -129
  33. package/dist/claude-hooks.js.map +0 -1
  34. package/dist/cli-version.d.ts +0 -3
  35. package/dist/cli-version.d.ts.map +0 -1
  36. package/dist/cli-version.js +0 -35
  37. package/dist/cli-version.js.map +0 -1
  38. package/dist/cli-version.test.d.ts +0 -2
  39. package/dist/cli-version.test.d.ts.map +0 -1
  40. package/dist/cli-version.test.js +0 -38
  41. package/dist/cli-version.test.js.map +0 -1
  42. package/dist/config.d.ts +0 -30
  43. package/dist/config.d.ts.map +0 -1
  44. package/dist/config.js +0 -327
  45. package/dist/config.js.map +0 -1
  46. package/dist/config.test.d.ts +0 -2
  47. package/dist/config.test.d.ts.map +0 -1
  48. package/dist/config.test.js +0 -184
  49. package/dist/config.test.js.map +0 -1
  50. package/dist/dev-auth.test.d.ts +0 -2
  51. package/dist/dev-auth.test.d.ts.map +0 -1
  52. package/dist/dev-auth.test.js +0 -154
  53. package/dist/dev-auth.test.js.map +0 -1
  54. package/dist/dev-script.test.d.ts +0 -2
  55. package/dist/dev-script.test.d.ts.map +0 -1
  56. package/dist/dev-script.test.js +0 -412
  57. package/dist/dev-script.test.js.map +0 -1
  58. package/dist/drivers/claude.d.ts +0 -34
  59. package/dist/drivers/claude.d.ts.map +0 -1
  60. package/dist/drivers/claude.js +0 -413
  61. package/dist/drivers/claude.js.map +0 -1
  62. package/dist/drivers/claude.test.d.ts +0 -2
  63. package/dist/drivers/claude.test.d.ts.map +0 -1
  64. package/dist/drivers/claude.test.js +0 -951
  65. package/dist/drivers/claude.test.js.map +0 -1
  66. package/dist/drivers/codex.d.ts +0 -38
  67. package/dist/drivers/codex.d.ts.map +0 -1
  68. package/dist/drivers/codex.js +0 -771
  69. package/dist/drivers/codex.js.map +0 -1
  70. package/dist/drivers/codex.test.d.ts +0 -2
  71. package/dist/drivers/codex.test.d.ts.map +0 -1
  72. package/dist/drivers/codex.test.js +0 -939
  73. package/dist/drivers/codex.test.js.map +0 -1
  74. package/dist/drivers/types.d.ts +0 -14
  75. package/dist/drivers/types.d.ts.map +0 -1
  76. package/dist/drivers/types.js +0 -2
  77. package/dist/drivers/types.js.map +0 -1
  78. package/dist/e2e.test.d.ts +0 -2
  79. package/dist/e2e.test.d.ts.map +0 -1
  80. package/dist/e2e.test.js +0 -111
  81. package/dist/e2e.test.js.map +0 -1
  82. package/dist/identity.d.ts +0 -10
  83. package/dist/identity.d.ts.map +0 -1
  84. package/dist/identity.js +0 -66
  85. package/dist/identity.js.map +0 -1
  86. package/dist/identity.test.d.ts +0 -2
  87. package/dist/identity.test.d.ts.map +0 -1
  88. package/dist/identity.test.js +0 -25
  89. package/dist/identity.test.js.map +0 -1
  90. package/dist/index-entry.test.d.ts +0 -2
  91. package/dist/index-entry.test.d.ts.map +0 -1
  92. package/dist/index-entry.test.js +0 -272
  93. package/dist/index-entry.test.js.map +0 -1
  94. package/dist/index.d.ts +0 -2
  95. package/dist/index.d.ts.map +0 -1
  96. package/dist/index.js +0 -707
  97. package/dist/index.js.map +0 -1
  98. package/dist/logger.d.ts +0 -31
  99. package/dist/logger.d.ts.map +0 -1
  100. package/dist/logger.js +0 -75
  101. package/dist/logger.js.map +0 -1
  102. package/dist/metrics.d.ts +0 -52
  103. package/dist/metrics.d.ts.map +0 -1
  104. package/dist/metrics.js +0 -89
  105. package/dist/metrics.js.map +0 -1
  106. package/dist/pairing-store.d.ts +0 -29
  107. package/dist/pairing-store.d.ts.map +0 -1
  108. package/dist/pairing-store.js +0 -131
  109. package/dist/pairing-store.js.map +0 -1
  110. package/dist/pairing-store.test.d.ts +0 -2
  111. package/dist/pairing-store.test.d.ts.map +0 -1
  112. package/dist/pairing-store.test.js +0 -47
  113. package/dist/pairing-store.test.js.map +0 -1
  114. package/dist/paths.d.ts +0 -16
  115. package/dist/paths.d.ts.map +0 -1
  116. package/dist/paths.js +0 -18
  117. package/dist/paths.js.map +0 -1
  118. package/dist/perf-compare.d.ts +0 -13
  119. package/dist/perf-compare.d.ts.map +0 -1
  120. package/dist/perf-compare.js +0 -125
  121. package/dist/perf-compare.js.map +0 -1
  122. package/dist/port-conflict.d.ts +0 -9
  123. package/dist/port-conflict.d.ts.map +0 -1
  124. package/dist/port-conflict.js +0 -33
  125. package/dist/port-conflict.js.map +0 -1
  126. package/dist/port-conflict.test.d.ts +0 -2
  127. package/dist/port-conflict.test.d.ts.map +0 -1
  128. package/dist/port-conflict.test.js +0 -38
  129. package/dist/port-conflict.test.js.map +0 -1
  130. package/dist/process-scanner.d.ts +0 -43
  131. package/dist/process-scanner.d.ts.map +0 -1
  132. package/dist/process-scanner.js +0 -453
  133. package/dist/process-scanner.js.map +0 -1
  134. package/dist/process-scanner.perf.test.d.ts +0 -2
  135. package/dist/process-scanner.perf.test.d.ts.map +0 -1
  136. package/dist/process-scanner.perf.test.js +0 -186
  137. package/dist/process-scanner.perf.test.js.map +0 -1
  138. package/dist/process-scanner.test.d.ts +0 -2
  139. package/dist/process-scanner.test.d.ts.map +0 -1
  140. package/dist/process-scanner.test.js +0 -399
  141. package/dist/process-scanner.test.js.map +0 -1
  142. package/dist/push-protocol.d.ts +0 -15
  143. package/dist/push-protocol.d.ts.map +0 -1
  144. package/dist/push-protocol.js +0 -23
  145. package/dist/push-protocol.js.map +0 -1
  146. package/dist/push-protocol.test.d.ts +0 -2
  147. package/dist/push-protocol.test.d.ts.map +0 -1
  148. package/dist/push-protocol.test.js +0 -57
  149. package/dist/push-protocol.test.js.map +0 -1
  150. package/dist/push-store.d.ts +0 -22
  151. package/dist/push-store.d.ts.map +0 -1
  152. package/dist/push-store.js +0 -103
  153. package/dist/push-store.js.map +0 -1
  154. package/dist/push-store.test.d.ts +0 -2
  155. package/dist/push-store.test.d.ts.map +0 -1
  156. package/dist/push-store.test.js +0 -79
  157. package/dist/push-store.test.js.map +0 -1
  158. package/dist/push.d.ts +0 -65
  159. package/dist/push.d.ts.map +0 -1
  160. package/dist/push.js +0 -202
  161. package/dist/push.js.map +0 -1
  162. package/dist/push.test.d.ts +0 -2
  163. package/dist/push.test.d.ts.map +0 -1
  164. package/dist/push.test.js +0 -199
  165. package/dist/push.test.js.map +0 -1
  166. package/dist/safe-stdio.d.ts +0 -3
  167. package/dist/safe-stdio.d.ts.map +0 -1
  168. package/dist/safe-stdio.js +0 -46
  169. package/dist/safe-stdio.js.map +0 -1
  170. package/dist/scanner.d.ts +0 -30
  171. package/dist/scanner.d.ts.map +0 -1
  172. package/dist/scanner.js +0 -859
  173. package/dist/scanner.js.map +0 -1
  174. package/dist/scanner.perf.test.d.ts +0 -2
  175. package/dist/scanner.perf.test.d.ts.map +0 -1
  176. package/dist/scanner.perf.test.js +0 -320
  177. package/dist/scanner.perf.test.js.map +0 -1
  178. package/dist/scanner.test.d.ts +0 -2
  179. package/dist/scanner.test.d.ts.map +0 -1
  180. package/dist/scanner.test.js +0 -948
  181. package/dist/scanner.test.js.map +0 -1
  182. package/dist/session-inventory.d.ts +0 -63
  183. package/dist/session-inventory.d.ts.map +0 -1
  184. package/dist/session-inventory.js +0 -525
  185. package/dist/session-inventory.js.map +0 -1
  186. package/dist/session-inventory.perf.test.d.ts +0 -2
  187. package/dist/session-inventory.perf.test.d.ts.map +0 -1
  188. package/dist/session-inventory.perf.test.js +0 -220
  189. package/dist/session-inventory.perf.test.js.map +0 -1
  190. package/dist/session-inventory.test.d.ts +0 -2
  191. package/dist/session-inventory.test.d.ts.map +0 -1
  192. package/dist/session-inventory.test.js +0 -712
  193. package/dist/session-inventory.test.js.map +0 -1
  194. package/dist/session-manager.d.ts +0 -75
  195. package/dist/session-manager.d.ts.map +0 -1
  196. package/dist/session-manager.js +0 -1515
  197. package/dist/session-manager.js.map +0 -1
  198. package/dist/session-manager.test.d.ts +0 -2
  199. package/dist/session-manager.test.d.ts.map +0 -1
  200. package/dist/session-manager.test.js +0 -2861
  201. package/dist/session-manager.test.js.map +0 -1
  202. package/dist/session-store.d.ts +0 -42
  203. package/dist/session-store.d.ts.map +0 -1
  204. package/dist/session-store.js +0 -163
  205. package/dist/session-store.js.map +0 -1
  206. package/dist/session-store.test.d.ts +0 -2
  207. package/dist/session-store.test.d.ts.map +0 -1
  208. package/dist/session-store.test.js +0 -236
  209. package/dist/session-store.test.js.map +0 -1
  210. package/dist/session-title.d.ts +0 -6
  211. package/dist/session-title.d.ts.map +0 -1
  212. package/dist/session-title.js +0 -105
  213. package/dist/session-title.js.map +0 -1
  214. package/dist/session-title.perf.test.d.ts +0 -2
  215. package/dist/session-title.perf.test.d.ts.map +0 -1
  216. package/dist/session-title.perf.test.js +0 -99
  217. package/dist/session-title.perf.test.js.map +0 -1
  218. package/dist/session-title.test.d.ts +0 -2
  219. package/dist/session-title.test.d.ts.map +0 -1
  220. package/dist/session-title.test.js +0 -199
  221. package/dist/session-title.test.js.map +0 -1
  222. package/dist/shutdown-endpoint.test.d.ts +0 -2
  223. package/dist/shutdown-endpoint.test.d.ts.map +0 -1
  224. package/dist/shutdown-endpoint.test.js +0 -93
  225. package/dist/shutdown-endpoint.test.js.map +0 -1
  226. package/dist/storage-housekeeping.d.ts +0 -28
  227. package/dist/storage-housekeeping.d.ts.map +0 -1
  228. package/dist/storage-housekeeping.js +0 -76
  229. package/dist/storage-housekeeping.js.map +0 -1
  230. package/dist/storage-housekeeping.test.d.ts +0 -2
  231. package/dist/storage-housekeeping.test.d.ts.map +0 -1
  232. package/dist/storage-housekeeping.test.js +0 -65
  233. package/dist/storage-housekeeping.test.js.map +0 -1
  234. package/dist/test-daemon-harness.d.ts +0 -31
  235. package/dist/test-daemon-harness.d.ts.map +0 -1
  236. package/dist/test-daemon-harness.js +0 -337
  237. package/dist/test-daemon-harness.js.map +0 -1
  238. package/dist/token-auth.test.d.ts +0 -2
  239. package/dist/token-auth.test.d.ts.map +0 -1
  240. package/dist/token-auth.test.js +0 -52
  241. package/dist/token-auth.test.js.map +0 -1
  242. package/dist/utils.d.ts +0 -4
  243. package/dist/utils.d.ts.map +0 -1
  244. package/dist/utils.js +0 -40
  245. package/dist/utils.js.map +0 -1
  246. package/dist/utils.test.d.ts +0 -2
  247. package/dist/utils.test.d.ts.map +0 -1
  248. package/dist/utils.test.js +0 -54
  249. package/dist/utils.test.js.map +0 -1
  250. package/dist/ws-data.d.ts +0 -4
  251. package/dist/ws-data.d.ts.map +0 -1
  252. package/dist/ws-data.js +0 -20
  253. package/dist/ws-data.js.map +0 -1
  254. package/dist/ws-data.test.d.ts +0 -2
  255. package/dist/ws-data.test.d.ts.map +0 -1
  256. package/dist/ws-data.test.js +0 -17
  257. package/dist/ws-data.test.js.map +0 -1
  258. package/perf-reporter.mjs +0 -138
  259. package/scripts/build-release.mjs +0 -41
  260. package/scripts/dev.mjs +0 -537
  261. package/src/advertised-hosts.test.ts +0 -125
  262. package/src/advertised-hosts.ts +0 -225
  263. package/src/audit.test.ts +0 -38
  264. package/src/audit.ts +0 -117
  265. package/src/auth.ts +0 -31
  266. package/src/claude-hooks.ts +0 -195
  267. package/src/cli-version.test.ts +0 -36
  268. package/src/cli-version.ts +0 -46
  269. package/src/config.test.ts +0 -254
  270. package/src/config.ts +0 -324
  271. package/src/dev-auth.test.ts +0 -183
  272. package/src/dev-script.test.ts +0 -511
  273. package/src/drivers/claude.test.ts +0 -1186
  274. package/src/drivers/claude.ts +0 -443
  275. package/src/drivers/codex.test.ts +0 -1096
  276. package/src/drivers/codex.ts +0 -879
  277. package/src/drivers/types.ts +0 -15
  278. package/src/e2e.test.ts +0 -139
  279. package/src/identity.test.ts +0 -26
  280. package/src/identity.ts +0 -82
  281. package/src/index-entry.test.ts +0 -336
  282. package/src/index.ts +0 -781
  283. package/src/logger.ts +0 -112
  284. package/src/metrics.ts +0 -117
  285. package/src/pairing-store.test.ts +0 -53
  286. package/src/pairing-store.ts +0 -154
  287. package/src/paths.ts +0 -19
  288. package/src/perf-compare.ts +0 -164
  289. package/src/port-conflict.test.ts +0 -45
  290. package/src/port-conflict.ts +0 -44
  291. package/src/process-scanner.perf.test.ts +0 -222
  292. package/src/process-scanner.test.ts +0 -575
  293. package/src/process-scanner.ts +0 -514
  294. package/src/push-protocol.test.ts +0 -74
  295. package/src/push-protocol.ts +0 -36
  296. package/src/push-store.test.ts +0 -89
  297. package/src/push-store.ts +0 -126
  298. package/src/push.test.ts +0 -234
  299. package/src/push.ts +0 -318
  300. package/src/safe-stdio.ts +0 -51
  301. package/src/scanner.perf.test.ts +0 -359
  302. package/src/scanner.test.ts +0 -1045
  303. package/src/scanner.ts +0 -924
  304. package/src/session-inventory.perf.test.ts +0 -250
  305. package/src/session-inventory.test.ts +0 -1002
  306. package/src/session-inventory.ts +0 -721
  307. package/src/session-manager.test.ts +0 -3430
  308. package/src/session-manager.ts +0 -1775
  309. package/src/session-store.test.ts +0 -276
  310. package/src/session-store.ts +0 -202
  311. package/src/session-title.perf.test.ts +0 -118
  312. package/src/session-title.test.ts +0 -286
  313. package/src/session-title.ts +0 -108
  314. package/src/shutdown-endpoint.test.ts +0 -95
  315. package/src/storage-housekeeping.test.ts +0 -78
  316. package/src/storage-housekeeping.ts +0 -111
  317. package/src/test-daemon-harness.ts +0 -410
  318. package/src/token-auth.test.ts +0 -67
  319. package/src/utils.test.ts +0 -65
  320. package/src/utils.ts +0 -47
  321. package/src/ws-data.test.ts +0 -20
  322. package/src/ws-data.ts +0 -26
  323. package/tsconfig.json +0 -12
@@ -1,1002 +0,0 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import type { SessionInfo } from '@vibelet/shared';
4
- import {
5
- __resetExternalInventoryStateForTests,
6
- __waitForExternalInventoryRefreshForTests,
7
- buildSessionInventoryPage,
8
- getExternalInventoryHealth,
9
- listSessionPage,
10
- onExternalInventoryBackfill,
11
- type InventorySources,
12
- } from './session-inventory.js';
13
- import type { RunningSessionCandidate } from './process-scanner.js';
14
- import { metrics } from './metrics.js';
15
-
16
- function createDeferred<T>() {
17
- let resolve!: (value: T | PromiseLike<T>) => void;
18
- let reject!: (reason?: unknown) => void;
19
- const promise = new Promise<T>((res, rej) => {
20
- resolve = res;
21
- reject = rej;
22
- });
23
- return { promise, resolve, reject };
24
- }
25
-
26
- function idleSession(overrides: Partial<SessionInfo>): SessionInfo {
27
- return {
28
- sessionId: 'session-1',
29
- agent: 'codex',
30
- cwd: '/repo',
31
- title: 'Session',
32
- createdAt: '2026-03-01T00:00:00.000Z',
33
- lastActivityAt: '2026-03-20T00:00:00.000Z',
34
- sources: ['scanner'],
35
- runtime: {
36
- state: 'idle',
37
- confidence: 'high',
38
- resumeMode: 'resumeSession',
39
- },
40
- ...overrides,
41
- };
42
- }
43
-
44
- function sources(overrides: Partial<InventorySources>): InventorySources {
45
- return {
46
- activeSessions: [],
47
- sessionRecords: [],
48
- scannedSessions: [],
49
- runningSessions: [],
50
- scannedAt: '2026-03-20T12:00:00.000Z',
51
- deletedSessionIds: new Set<string>(),
52
- ...overrides,
53
- };
54
- }
55
-
56
- test.beforeEach(() => {
57
- __resetExternalInventoryStateForTests();
58
- });
59
-
60
- test('returns active sessions first and paginates only idle sessions', () => {
61
- const page = buildSessionInventoryPage(
62
- sources({
63
- activeSessions: [
64
- {
65
- sessionId: 'active-1',
66
- agent: 'codex',
67
- cwd: '/repo',
68
- title: 'Active',
69
- createdAt: '2026-03-20T09:00:00.000Z',
70
- lastActivityAt: '2026-03-20T12:00:00.000Z',
71
- },
72
- ],
73
- scannedSessions: [
74
- idleSession({
75
- sessionId: 'idle-new',
76
- title: 'Idle New',
77
- lastActivityAt: '2026-03-20T11:00:00.000Z',
78
- }),
79
- idleSession({
80
- sessionId: 'idle-old',
81
- title: 'Idle Old',
82
- lastActivityAt: '2026-03-19T11:00:00.000Z',
83
- }),
84
- ],
85
- }),
86
- 1,
87
- );
88
-
89
- assert.equal(page.sessions.length, 2);
90
- assert.equal(page.sessions[0].sessionId, 'active-1');
91
- assert.equal(page.sessions[0].runtime.state, 'daemonActive');
92
- assert.equal(page.sessions[1].sessionId, 'idle-new');
93
- assert.deepEqual(page.nextCursor, {
94
- lastActivityAt: '2026-03-20T11:00:00.000Z',
95
- sessionId: 'idle-new',
96
- });
97
- });
98
-
99
- test('listSessionPage returns fast-path records before external backfill completes', async () => {
100
- const scanned = createDeferred<SessionInfo[]>();
101
- const running = createDeferred<RunningSessionCandidate[]>();
102
-
103
- const page = await listSessionPage({
104
- limit: 50,
105
- activeSessions: [],
106
- sessionRecords: [
107
- {
108
- sessionId: 'record-only',
109
- agent: 'codex',
110
- cwd: '/repo',
111
- approvalMode: 'normal',
112
- title: 'Record Only',
113
- createdAt: '2026-03-01T00:00:00.000Z',
114
- lastActivityAt: '2026-03-20T10:00:00.000Z',
115
- },
116
- ],
117
- loaders: {
118
- listScannedSessions: async () => scanned.promise,
119
- scanRunningSessions: async () => running.promise,
120
- },
121
- });
122
-
123
- assert.equal(page.sessions.length, 1);
124
- assert.equal(page.sessions[0].sessionId, 'record-only');
125
- assert.equal(page.sessions[0].runtime.state, 'idle');
126
- assert.deepEqual(page.sessions[0].sources, ['record']);
127
- assert.equal(getExternalInventoryHealth().backfillInFlight, true);
128
-
129
- scanned.resolve([
130
- idleSession({
131
- sessionId: 'external-idle',
132
- title: 'External Idle',
133
- lastActivityAt: '2026-03-20T11:00:00.000Z',
134
- }),
135
- ]);
136
- running.resolve([]);
137
- await __waitForExternalInventoryRefreshForTests();
138
- const health = getExternalInventoryHealth();
139
- const metricsSnapshot = metrics.snapshot();
140
-
141
- assert.equal(health.backfillInFlight, false);
142
- assert.equal(health.cachedSessions, 1);
143
- assert.equal(health.runningSessions, 0);
144
- assert.equal(typeof health.lastBackfillDurationMs, 'number');
145
- assert.equal(typeof health.lastBackfillCompletedAt, 'string');
146
- assert.equal(typeof health.lastBackfillAppliedAt, 'string');
147
- assert.ok(metricsSnapshot.timers['session.inventory.fast_path_latency_ms']);
148
- assert.ok(metricsSnapshot.timers['session.inventory.backfill_latency_ms']);
149
- assert.ok(metricsSnapshot.counters['session.inventory.backfill_applied']);
150
-
151
- const refreshed = await listSessionPage({
152
- limit: 50,
153
- activeSessions: [],
154
- sessionRecords: [
155
- {
156
- sessionId: 'record-only',
157
- agent: 'codex',
158
- cwd: '/repo',
159
- approvalMode: 'normal',
160
- title: 'Record Only',
161
- createdAt: '2026-03-01T00:00:00.000Z',
162
- lastActivityAt: '2026-03-20T10:00:00.000Z',
163
- },
164
- ],
165
- });
166
-
167
- assert.deepEqual(
168
- refreshed.sessions.map((session) => session.sessionId),
169
- ['external-idle', 'record-only'],
170
- );
171
- });
172
-
173
- test('listSessionPage notifies listeners when external backfill changes inventory', async () => {
174
- const scanned = createDeferred<SessionInfo[]>();
175
- const running = createDeferred<RunningSessionCandidate[]>();
176
- let notifications = 0;
177
- const unsubscribe = onExternalInventoryBackfill(() => {
178
- notifications += 1;
179
- });
180
-
181
- await listSessionPage({
182
- limit: 50,
183
- activeSessions: [],
184
- sessionRecords: [],
185
- loaders: {
186
- listScannedSessions: async () => scanned.promise,
187
- scanRunningSessions: async () => running.promise,
188
- },
189
- });
190
-
191
- scanned.resolve([
192
- idleSession({
193
- sessionId: 'external-idle',
194
- title: 'External Idle',
195
- lastActivityAt: '2026-03-20T11:00:00.000Z',
196
- }),
197
- ]);
198
- running.resolve([]);
199
- await __waitForExternalInventoryRefreshForTests();
200
- unsubscribe();
201
-
202
- assert.equal(notifications, 1);
203
- });
204
-
205
- test('uses max lastActivityAt across sources and keeps later pages idle-only', () => {
206
- const firstPage = buildSessionInventoryPage(
207
- sources({
208
- sessionRecords: [
209
- {
210
- sessionId: 'shared',
211
- agent: 'codex',
212
- cwd: '/repo',
213
- approvalMode: 'normal',
214
- title: 'Shared Record',
215
- createdAt: '2026-03-01T00:00:00.000Z',
216
- lastActivityAt: '2026-03-02T00:00:00.000Z',
217
- },
218
- ],
219
- scannedSessions: [
220
- idleSession({
221
- sessionId: 'shared',
222
- title: 'Shared Scanner',
223
- lastActivityAt: '2026-03-20T10:00:00.000Z',
224
- }),
225
- idleSession({
226
- sessionId: 'later-idle',
227
- title: 'Later Idle',
228
- lastActivityAt: '2026-03-19T10:00:00.000Z',
229
- }),
230
- ],
231
- runningSessions: [
232
- {
233
- agent: 'claude',
234
- pid: 9001,
235
- cwd: '/repo',
236
- command: 'claude --continue',
237
- sessionId: 'claude-live',
238
- confidence: 'medium',
239
- isResponding: true,
240
- },
241
- ],
242
- }),
243
- 1,
244
- );
245
-
246
- assert.equal(firstPage.sessions[0].sessionId, 'claude-live');
247
- assert.equal(firstPage.sessions[0].runtime.state, 'externalRunning');
248
- assert.equal(firstPage.sessions[0].runtime.isResponding, true);
249
- assert.equal(firstPage.sessions[1].sessionId, 'shared');
250
- assert.equal(firstPage.sessions[1].lastActivityAt, '2026-03-20T10:00:00.000Z');
251
- assert.deepEqual(firstPage.sessions[1].sources.sort(), ['record', 'scanner']);
252
-
253
- const secondPage = buildSessionInventoryPage(
254
- sources({
255
- sessionRecords: [
256
- {
257
- sessionId: 'shared',
258
- agent: 'codex',
259
- cwd: '/repo',
260
- approvalMode: 'normal',
261
- title: 'Shared Record',
262
- createdAt: '2026-03-01T00:00:00.000Z',
263
- lastActivityAt: '2026-03-02T00:00:00.000Z',
264
- },
265
- ],
266
- scannedSessions: [
267
- idleSession({
268
- sessionId: 'shared',
269
- title: 'Shared Scanner',
270
- lastActivityAt: '2026-03-20T10:00:00.000Z',
271
- }),
272
- idleSession({
273
- sessionId: 'later-idle',
274
- title: 'Later Idle',
275
- lastActivityAt: '2026-03-19T10:00:00.000Z',
276
- }),
277
- ],
278
- runningSessions: [
279
- {
280
- agent: 'claude',
281
- pid: 9001,
282
- cwd: '/repo',
283
- command: 'claude --continue',
284
- sessionId: 'claude-live',
285
- confidence: 'medium',
286
- },
287
- ],
288
- }),
289
- 1,
290
- firstPage.nextCursor,
291
- );
292
-
293
- assert.equal(secondPage.sessions.length, 1);
294
- assert.equal(secondPage.sessions[0].sessionId, 'later-idle');
295
- assert.equal(secondPage.sessions[0].runtime.state, 'idle');
296
- });
297
-
298
- test('externalRunning sessions preserve parsed approval mode', () => {
299
- const page = buildSessionInventoryPage(
300
- sources({
301
- runningSessions: [
302
- {
303
- agent: 'claude',
304
- pid: 9001,
305
- cwd: '/repo',
306
- command: 'claude --continue',
307
- sessionId: 'claude-live',
308
- confidence: 'medium',
309
- approvalMode: 'acceptEdits',
310
- },
311
- ],
312
- }),
313
- 50,
314
- );
315
-
316
- assert.equal(page.sessions[0].runtime.state, 'externalRunning');
317
- assert.equal(page.sessions[0].approvalMode, undefined);
318
- assert.equal(page.sessions[0].observedApprovalMode, 'acceptEdits');
319
- });
320
-
321
- test('record approval override survives scanner and running observed modes', () => {
322
- const page = buildSessionInventoryPage(
323
- sources({
324
- sessionRecords: [
325
- {
326
- sessionId: 'remote-shared',
327
- agent: 'claude',
328
- cwd: '/repo',
329
- approvalMode: 'plan',
330
- title: 'Remote Shared',
331
- createdAt: '2026-03-01T00:00:00.000Z',
332
- lastActivityAt: '2026-03-02T00:00:00.000Z',
333
- },
334
- ],
335
- scannedSessions: [
336
- idleSession({
337
- sessionId: 'remote-shared',
338
- agent: 'claude',
339
- observedApprovalMode: 'acceptEdits',
340
- title: 'Remote Shared',
341
- }),
342
- ],
343
- runningSessions: [
344
- {
345
- agent: 'claude',
346
- pid: 9002,
347
- cwd: '/repo',
348
- command: 'claude --resume remote-shared',
349
- sessionId: 'remote-shared',
350
- confidence: 'high',
351
- approvalMode: 'autoApprove',
352
- },
353
- ],
354
- }),
355
- 50,
356
- );
357
-
358
- assert.equal(page.sessions[0].approvalMode, 'plan');
359
- assert.equal(page.sessions[0].observedApprovalMode, 'autoApprove');
360
- });
361
-
362
- test('listSessionPage keeps search requests on the blocking path', async () => {
363
- const scanned = createDeferred<SessionInfo[]>();
364
- const contentMatches = createDeferred<Set<string>>();
365
- let resolved = false;
366
-
367
- const pending = listSessionPage({
368
- limit: 50,
369
- search: 'keyword',
370
- activeSessions: [],
371
- sessionRecords: [],
372
- loaders: {
373
- listScannedSessions: async () => scanned.promise,
374
- scanRunningSessions: async () => [],
375
- searchSessionContent: async () => contentMatches.promise,
376
- },
377
- }).then((page) => {
378
- resolved = true;
379
- return page;
380
- });
381
-
382
- await Promise.resolve();
383
- assert.equal(resolved, false);
384
-
385
- scanned.resolve([
386
- idleSession({
387
- sessionId: 'matched-search',
388
- title: 'No metadata match here',
389
- }),
390
- ]);
391
- contentMatches.resolve(new Set(['matched-search']));
392
- const page = await pending;
393
-
394
- assert.equal(resolved, true);
395
- assert.equal(page.sessions.length, 1);
396
- assert.equal(page.sessions[0].sessionId, 'matched-search');
397
- });
398
-
399
- // ===== Filtering =====
400
-
401
- test('filters by agent type', () => {
402
- const page = buildSessionInventoryPage(
403
- sources({
404
- scannedSessions: [
405
- idleSession({ sessionId: 'codex-1', agent: 'codex', title: 'Codex' }),
406
- idleSession({ sessionId: 'claude-1', agent: 'claude', title: 'Claude' }),
407
- ],
408
- }),
409
- 10,
410
- undefined,
411
- 'claude',
412
- );
413
-
414
- assert.equal(page.sessions.length, 1);
415
- assert.equal(page.sessions[0].agent, 'claude');
416
- });
417
-
418
- test('filters by cwd', () => {
419
- const page = buildSessionInventoryPage(
420
- sources({
421
- scannedSessions: [
422
- idleSession({ sessionId: 's1', cwd: '/repo-a', title: 'A' }),
423
- idleSession({ sessionId: 's2', cwd: '/repo-b', title: 'B' }),
424
- ],
425
- }),
426
- 10,
427
- undefined,
428
- undefined,
429
- '/repo-b',
430
- );
431
-
432
- assert.equal(page.sessions.length, 1);
433
- assert.equal(page.sessions[0].cwd, '/repo-b');
434
- });
435
-
436
- test('filters by search term matching title', () => {
437
- const page = buildSessionInventoryPage(
438
- sources({
439
- scannedSessions: [
440
- idleSession({ sessionId: 's1', title: 'Fix login bug' }),
441
- idleSession({ sessionId: 's2', title: 'Add dashboard' }),
442
- ],
443
- }),
444
- 10,
445
- undefined,
446
- undefined,
447
- undefined,
448
- 'login',
449
- );
450
-
451
- assert.equal(page.sessions.length, 1);
452
- assert.equal(page.sessions[0].sessionId, 's1');
453
- });
454
-
455
- test('filters by search term matching cwd', () => {
456
- const page = buildSessionInventoryPage(
457
- sources({
458
- scannedSessions: [
459
- idleSession({ sessionId: 's1', cwd: '/home/user/frontend', title: 'A' }),
460
- idleSession({ sessionId: 's2', cwd: '/home/user/backend', title: 'B' }),
461
- ],
462
- }),
463
- 10,
464
- undefined,
465
- undefined,
466
- undefined,
467
- 'frontend',
468
- );
469
-
470
- assert.equal(page.sessions.length, 1);
471
- assert.equal(page.sessions[0].sessionId, 's1');
472
- });
473
-
474
- test('search with contentMatchIds includes sessions matching content', () => {
475
- const page = buildSessionInventoryPage(
476
- sources({
477
- scannedSessions: [
478
- idleSession({ sessionId: 's1', title: 'No match' }),
479
- idleSession({ sessionId: 's2', title: 'Also no match' }),
480
- ],
481
- }),
482
- 10,
483
- undefined,
484
- undefined,
485
- undefined,
486
- 'special-query',
487
- new Set(['s2']),
488
- );
489
-
490
- assert.equal(page.sessions.length, 1);
491
- assert.equal(page.sessions[0].sessionId, 's2');
492
- });
493
-
494
- test('search disables pagination — returns all matching sessions', () => {
495
- const sessions = Array.from({ length: 80 }, (_, i) =>
496
- idleSession({
497
- sessionId: `s${i}`,
498
- title: `Session ${i} with keyword`,
499
- lastActivityAt: `2026-03-${String(20 - Math.floor(i / 4)).padStart(2, '0')}T${String(i % 24).padStart(2, '0')}:00:00.000Z`,
500
- }),
501
- );
502
- const page = buildSessionInventoryPage(
503
- sources({ scannedSessions: sessions }),
504
- 50,
505
- undefined,
506
- undefined,
507
- undefined,
508
- 'keyword',
509
- );
510
-
511
- assert.equal(page.sessions.length, 80);
512
- assert.equal(page.nextCursor, undefined);
513
- });
514
-
515
- test('without search, pagination limits results to pageSize', () => {
516
- const sessions = Array.from({ length: 80 }, (_, i) =>
517
- idleSession({
518
- sessionId: `s${i}`,
519
- title: `Session ${i}`,
520
- lastActivityAt: `2026-03-${String(20 - Math.floor(i / 4)).padStart(2, '0')}T${String(i % 24).padStart(2, '0')}:00:00.000Z`,
521
- }),
522
- );
523
- const page = buildSessionInventoryPage(
524
- sources({ scannedSessions: sessions }),
525
- 50,
526
- );
527
-
528
- assert.equal(page.sessions.length, 50);
529
- assert.ok(page.nextCursor);
530
- });
531
-
532
- test('search matches title OR content — union of both', () => {
533
- const page = buildSessionInventoryPage(
534
- sources({
535
- scannedSessions: [
536
- idleSession({ sessionId: 'title-match', title: 'Fix login bug' }),
537
- idleSession({ sessionId: 'content-match', title: 'Unrelated title' }),
538
- idleSession({ sessionId: 'no-match', title: 'Something else' }),
539
- ],
540
- }),
541
- 50,
542
- undefined,
543
- undefined,
544
- undefined,
545
- 'login',
546
- new Set(['content-match']),
547
- );
548
-
549
- assert.equal(page.sessions.length, 2);
550
- const ids = page.sessions.map(s => s.sessionId).sort();
551
- assert.deepEqual(ids, ['content-match', 'title-match']);
552
- });
553
-
554
- test('search is case insensitive for title and cwd', () => {
555
- const page = buildSessionInventoryPage(
556
- sources({
557
- scannedSessions: [
558
- idleSession({ sessionId: 's1', title: 'Fix Login Bug', cwd: '/home/User/Project' }),
559
- idleSession({ sessionId: 's2', title: 'other', cwd: '/tmp' }),
560
- ],
561
- }),
562
- 50,
563
- undefined,
564
- undefined,
565
- undefined,
566
- 'LOGIN',
567
- );
568
-
569
- assert.equal(page.sessions.length, 1);
570
- assert.equal(page.sessions[0].sessionId, 's1');
571
- });
572
-
573
- test('search matches Chinese characters in title', () => {
574
- const page = buildSessionInventoryPage(
575
- sources({
576
- scannedSessions: [
577
- idleSession({ sessionId: 's1', title: '修复登录问题' }),
578
- idleSession({ sessionId: 's2', title: '添加仪表盘功能' }),
579
- ],
580
- }),
581
- 50,
582
- undefined,
583
- undefined,
584
- undefined,
585
- '登录',
586
- );
587
-
588
- assert.equal(page.sessions.length, 1);
589
- assert.equal(page.sessions[0].sessionId, 's1');
590
- });
591
-
592
- test('search with no matches returns empty page', () => {
593
- const page = buildSessionInventoryPage(
594
- sources({
595
- scannedSessions: [
596
- idleSession({ sessionId: 's1', title: 'Fix bug' }),
597
- idleSession({ sessionId: 's2', title: 'Add feature' }),
598
- ],
599
- }),
600
- 50,
601
- undefined,
602
- undefined,
603
- undefined,
604
- 'nonexistent-term',
605
- new Set(),
606
- );
607
-
608
- assert.equal(page.sessions.length, 0);
609
- assert.equal(page.nextCursor, undefined);
610
- });
611
-
612
- test('search with contentMatchIds includes sessions not matched by meta', () => {
613
- const contentIds = new Set(['s1', 's3']);
614
- const page = buildSessionInventoryPage(
615
- sources({
616
- scannedSessions: [
617
- idleSession({ sessionId: 's1', title: 'No meta match', cwd: '/a' }),
618
- idleSession({ sessionId: 's2', title: 'No meta match', cwd: '/b' }),
619
- idleSession({ sessionId: 's3', title: 'No meta match', cwd: '/c' }),
620
- ],
621
- }),
622
- 50,
623
- undefined,
624
- undefined,
625
- undefined,
626
- 'xyz',
627
- contentIds,
628
- );
629
-
630
- assert.equal(page.sessions.length, 2);
631
- const ids = page.sessions.map(s => s.sessionId).sort();
632
- assert.deepEqual(ids, ['s1', 's3']);
633
- });
634
-
635
- test('search combined with agent filter narrows results', () => {
636
- const page = buildSessionInventoryPage(
637
- sources({
638
- scannedSessions: [
639
- idleSession({ sessionId: 's1', agent: 'claude', title: 'Fix login' }),
640
- idleSession({ sessionId: 's2', agent: 'codex', title: 'Fix login' }),
641
- ],
642
- }),
643
- 50,
644
- undefined,
645
- 'claude',
646
- undefined,
647
- 'login',
648
- );
649
-
650
- assert.equal(page.sessions.length, 1);
651
- assert.equal(page.sessions[0].agent, 'claude');
652
- });
653
-
654
- // ===== Additional coverage tests =====
655
-
656
- test('active sessions are sorted by runtime state rank then by lastActivityAt', () => {
657
- const page = buildSessionInventoryPage(
658
- sources({
659
- runningSessions: [
660
- {
661
- agent: 'codex', pid: 1, cwd: '/repo', command: 'codex',
662
- sessionId: 'running-1', confidence: 'medium',
663
- },
664
- ],
665
- activeSessions: [
666
- {
667
- sessionId: 'daemon-1', agent: 'codex', cwd: '/repo', title: 'Daemon',
668
- createdAt: '2026-03-20T09:00:00.000Z', lastActivityAt: '2026-03-20T10:00:00.000Z',
669
- },
670
- ],
671
- }),
672
- 10,
673
- );
674
-
675
- // daemonActive (rank 3) should come before externalRunning (rank 2)
676
- assert.equal(page.sessions[0].runtime.state, 'daemonActive');
677
- assert.equal(page.sessions[1].runtime.state, 'externalRunning');
678
- });
679
-
680
- test('cursor that does not match any session resets to beginning', () => {
681
- const page = buildSessionInventoryPage(
682
- sources({
683
- scannedSessions: [
684
- idleSession({ sessionId: 's1', lastActivityAt: '2026-03-20T10:00:00.000Z' }),
685
- idleSession({ sessionId: 's2', lastActivityAt: '2026-03-19T10:00:00.000Z' }),
686
- ],
687
- }),
688
- 10,
689
- { lastActivityAt: 'nonexistent', sessionId: 'nonexistent' },
690
- );
691
-
692
- // Should fall back to beginning
693
- assert.equal(page.sessions.length, 2);
694
- assert.equal(page.sessions[0].sessionId, 's1');
695
- });
696
-
697
- test('idle sessions are sorted by lastActivityAt descending', () => {
698
- const page = buildSessionInventoryPage(
699
- sources({
700
- scannedSessions: [
701
- idleSession({ sessionId: 'old', lastActivityAt: '2026-03-01T00:00:00.000Z' }),
702
- idleSession({ sessionId: 'new', lastActivityAt: '2026-03-20T00:00:00.000Z' }),
703
- idleSession({ sessionId: 'mid', lastActivityAt: '2026-03-10T00:00:00.000Z' }),
704
- ],
705
- }),
706
- 10,
707
- );
708
-
709
- assert.equal(page.sessions[0].sessionId, 'new');
710
- assert.equal(page.sessions[1].sessionId, 'mid');
711
- assert.equal(page.sessions[2].sessionId, 'old');
712
- });
713
-
714
- test('runtime state: externalRunning beats idle in sort order', () => {
715
- const page = buildSessionInventoryPage(
716
- sources({
717
- scannedSessions: [
718
- idleSession({ sessionId: 'idle-1', lastActivityAt: '2026-03-20T12:00:00.000Z' }),
719
- ],
720
- runningSessions: [
721
- {
722
- agent: 'codex', pid: 1, cwd: '/repo', command: 'codex',
723
- sessionId: 'running-1', confidence: 'low',
724
- },
725
- ],
726
- }),
727
- 10,
728
- );
729
-
730
- assert.equal(page.sessions[0].sessionId, 'running-1');
731
- assert.equal(page.sessions[0].runtime.state, 'externalRunning');
732
- assert.equal(page.sessions[1].sessionId, 'idle-1');
733
- });
734
-
735
- test('deleted sessions are filtered even when scanner finds them again', () => {
736
- const page = buildSessionInventoryPage(
737
- sources({
738
- sessionRecords: [
739
- {
740
- sessionId: 'deleted-1',
741
- agent: 'codex',
742
- cwd: '/repo',
743
- approvalMode: 'normal',
744
- title: 'Deleted record',
745
- createdAt: '2026-03-01T00:00:00.000Z',
746
- lastActivityAt: '2026-03-02T00:00:00.000Z',
747
- managed: true,
748
- },
749
- ],
750
- scannedSessions: [
751
- idleSession({
752
- sessionId: 'deleted-1',
753
- title: 'Deleted scanner',
754
- lastActivityAt: '2026-03-20T10:00:00.000Z',
755
- }),
756
- idleSession({
757
- sessionId: 'kept-1',
758
- title: 'Kept session',
759
- lastActivityAt: '2026-03-19T10:00:00.000Z',
760
- }),
761
- ],
762
- deletedSessionIds: new Set(['deleted-1']),
763
- }),
764
- 10,
765
- );
766
-
767
- assert.equal(page.sessions.length, 1);
768
- assert.equal(page.sessions[0].sessionId, 'kept-1');
769
- });
770
-
771
- test('pickRuntime chooses higher confidence when same state', () => {
772
- const page = buildSessionInventoryPage(
773
- sources({
774
- runningSessions: [
775
- {
776
- agent: 'codex', pid: 1, cwd: '/repo', command: 'codex',
777
- sessionId: 'shared', confidence: 'low',
778
- },
779
- {
780
- agent: 'codex', pid: 2, cwd: '/repo', command: 'codex resume shared',
781
- sessionId: 'shared', confidence: 'high',
782
- },
783
- ],
784
- }),
785
- 10,
786
- );
787
-
788
- assert.equal(page.sessions.length, 1);
789
- assert.equal(page.sessions[0].runtime.confidence, 'high');
790
- });
791
-
792
- // ===== Merge edge cases =====
793
-
794
- test('merges same session from record and scanner, picks higher runtime state', () => {
795
- const page = buildSessionInventoryPage(
796
- sources({
797
- sessionRecords: [
798
- {
799
- sessionId: 'shared',
800
- agent: 'codex',
801
- cwd: '/repo',
802
- approvalMode: 'normal',
803
- title: 'Record title',
804
- createdAt: '2026-03-01T00:00:00.000Z',
805
- lastActivityAt: '2026-03-02T00:00:00.000Z',
806
- },
807
- ],
808
- activeSessions: [
809
- {
810
- sessionId: 'shared',
811
- agent: 'codex',
812
- cwd: '/repo',
813
- title: 'Active title',
814
- createdAt: '2026-03-01T00:00:00.000Z',
815
- lastActivityAt: '2026-03-20T12:00:00.000Z',
816
- },
817
- ],
818
- }),
819
- 10,
820
- );
821
-
822
- assert.equal(page.sessions.length, 1);
823
- assert.equal(page.sessions[0].runtime.state, 'daemonActive');
824
- assert.equal(page.sessions[0].title, 'Active title');
825
- });
826
-
827
- test('returns empty page when no sessions match filters', () => {
828
- const page = buildSessionInventoryPage(
829
- sources({
830
- scannedSessions: [
831
- idleSession({ sessionId: 's1', agent: 'codex' }),
832
- ],
833
- }),
834
- 10,
835
- undefined,
836
- 'claude',
837
- );
838
-
839
- assert.equal(page.sessions.length, 0);
840
- assert.equal(page.nextCursor, undefined);
841
- });
842
-
843
- test('uses min createdAt across merged sources', () => {
844
- const page = buildSessionInventoryPage(
845
- sources({
846
- sessionRecords: [
847
- {
848
- sessionId: 'shared',
849
- agent: 'codex',
850
- cwd: '/repo',
851
- title: 'Title',
852
- createdAt: '2026-03-01T00:00:00.000Z',
853
- lastActivityAt: '2026-03-10T00:00:00.000Z',
854
- },
855
- ],
856
- scannedSessions: [
857
- idleSession({
858
- sessionId: 'shared',
859
- createdAt: '2026-03-05T00:00:00.000Z',
860
- lastActivityAt: '2026-03-20T00:00:00.000Z',
861
- }),
862
- ],
863
- }),
864
- 10,
865
- );
866
-
867
- assert.equal(page.sessions[0].createdAt, '2026-03-01T00:00:00.000Z');
868
- assert.equal(page.sessions[0].lastActivityAt, '2026-03-20T00:00:00.000Z');
869
- });
870
-
871
- // ===== Title merging =====
872
-
873
- test('mergeInto does not let process scanner override daemonActive lastActivityAt', () => {
874
- const fiveMinsAgo = '2026-03-20T11:55:00.000Z';
875
- const scannedAt = '2026-03-20T12:00:00.000Z'; // now
876
-
877
- const page = buildSessionInventoryPage(
878
- sources({
879
- activeSessions: [
880
- {
881
- sessionId: 'shared-session',
882
- agent: 'codex',
883
- cwd: '/repo',
884
- title: 'Daemon session',
885
- createdAt: '2026-03-01T00:00:00.000Z',
886
- lastActivityAt: fiveMinsAgo,
887
- },
888
- ],
889
- runningSessions: [
890
- {
891
- agent: 'codex',
892
- pid: 42,
893
- cwd: '/repo',
894
- command: 'codex --continue',
895
- sessionId: 'shared-session',
896
- confidence: 'medium',
897
- },
898
- ],
899
- scannedAt,
900
- }),
901
- 10,
902
- );
903
-
904
- assert.equal(page.sessions.length, 1);
905
- assert.equal(page.sessions[0].sessionId, 'shared-session');
906
- // The daemon's lastActivityAt (5 mins ago) should be preserved,
907
- // NOT overwritten by the process scanner's scannedAt (now).
908
- assert.equal(
909
- page.sessions[0].lastActivityAt,
910
- fiveMinsAgo,
911
- 'lastActivityAt should be the daemon timestamp, not the scanner scannedAt',
912
- );
913
- });
914
-
915
- test('managed idle sessions are sorted with active sessions, not idle', () => {
916
- const page = buildSessionInventoryPage(
917
- sources({
918
- sessionRecords: [
919
- {
920
- sessionId: 'managed-idle',
921
- agent: 'claude',
922
- cwd: '/repo',
923
- title: 'Managed Session',
924
- createdAt: '2026-03-01T00:00:00.000Z',
925
- lastActivityAt: '2026-03-20T10:00:00.000Z',
926
- managed: true,
927
- },
928
- ],
929
- scannedSessions: [
930
- idleSession({
931
- sessionId: 'scanned-idle',
932
- title: 'Scanned Session',
933
- lastActivityAt: '2026-03-20T11:00:00.000Z',
934
- }),
935
- ],
936
- }),
937
- 1,
938
- );
939
-
940
- // managed idle should appear first (in active section), scanned idle in idle section
941
- assert.equal(page.sessions[0].sessionId, 'managed-idle');
942
- assert.equal(page.sessions[0].managed, true);
943
- assert.equal(page.sessions[1].sessionId, 'scanned-idle');
944
- });
945
-
946
- test('managed flag is preserved when merging record and scanner sources', () => {
947
- const page = buildSessionInventoryPage(
948
- sources({
949
- sessionRecords: [
950
- {
951
- sessionId: 'shared',
952
- agent: 'codex',
953
- cwd: '/repo',
954
- title: 'Record',
955
- createdAt: '2026-03-01T00:00:00.000Z',
956
- lastActivityAt: '2026-03-02T00:00:00.000Z',
957
- managed: true,
958
- },
959
- ],
960
- scannedSessions: [
961
- idleSession({
962
- sessionId: 'shared',
963
- title: 'Scanned',
964
- lastActivityAt: '2026-03-20T10:00:00.000Z',
965
- }),
966
- ],
967
- }),
968
- 10,
969
- );
970
-
971
- assert.equal(page.sessions.length, 1);
972
- assert.equal(page.sessions[0].managed, true);
973
- });
974
-
975
- test('replaces id-like fallback titles with a real scanned title', () => {
976
- const sessionId = '019d03e3-2672-7011-9c6d-5ba083b71111';
977
- const page = buildSessionInventoryPage(
978
- sources({
979
- sessionRecords: [
980
- {
981
- sessionId,
982
- agent: 'codex',
983
- cwd: '/repo',
984
- approvalMode: 'normal',
985
- title: sessionId,
986
- createdAt: '2026-03-01T00:00:00.000Z',
987
- lastActivityAt: '2026-03-02T00:00:00.000Z',
988
- },
989
- ],
990
- scannedSessions: [
991
- idleSession({
992
- sessionId,
993
- title: 'Recovered title',
994
- lastActivityAt: '2026-03-20T10:00:00.000Z',
995
- }),
996
- ],
997
- }),
998
- 10,
999
- );
1000
-
1001
- assert.equal(page.sessions[0].title, 'Recovered title');
1002
- });