@sleep2agi/agent-network-dashboard 0.6.3-preview.50 → 0.6.3-preview.51

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 (237) hide show
  1. package/.next/BUILD_COMMIT +1 -1
  2. package/.next/BUILD_ID +1 -1
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/diagnostics/route-bundle-stats.json +14 -14
  5. package/.next/fallback-build-manifest.json +3 -3
  6. package/.next/prerender-manifest.json +3 -3
  7. package/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/server/app/_global-error.html +1 -1
  10. package/.next/server/app/_global-error.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  14. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  15. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/server/app/_not-found.html +2 -2
  19. package/.next/server/app/_not-found.rsc +2 -2
  20. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  21. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  22. package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  23. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  24. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  25. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  26. package/.next/server/app/admin/page.js.nft.json +1 -1
  27. package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
  28. package/.next/server/app/admin.html +2 -2
  29. package/.next/server/app/admin.rsc +2 -2
  30. package/.next/server/app/admin.segments/_full.segment.rsc +2 -2
  31. package/.next/server/app/admin.segments/_head.segment.rsc +1 -1
  32. package/.next/server/app/admin.segments/_index.segment.rsc +2 -2
  33. package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
  34. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +1 -1
  35. package/.next/server/app/admin.segments/admin.segment.rsc +1 -1
  36. package/.next/server/app/index.html +2 -2
  37. package/.next/server/app/index.rsc +3 -3
  38. package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  39. package/.next/server/app/index.segments/_full.segment.rsc +3 -3
  40. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  41. package/.next/server/app/index.segments/_index.segment.rsc +2 -2
  42. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  43. package/.next/server/app/login/page.js.nft.json +1 -1
  44. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  45. package/.next/server/app/login.html +2 -2
  46. package/.next/server/app/login.rsc +3 -3
  47. package/.next/server/app/login.segments/_full.segment.rsc +3 -3
  48. package/.next/server/app/login.segments/_head.segment.rsc +1 -1
  49. package/.next/server/app/login.segments/_index.segment.rsc +2 -2
  50. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  51. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +2 -2
  52. package/.next/server/app/login.segments/login.segment.rsc +1 -1
  53. package/.next/server/app/logs/page.js.nft.json +1 -1
  54. package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  55. package/.next/server/app/logs.html +2 -2
  56. package/.next/server/app/logs.rsc +2 -2
  57. package/.next/server/app/logs.segments/_full.segment.rsc +2 -2
  58. package/.next/server/app/logs.segments/_head.segment.rsc +1 -1
  59. package/.next/server/app/logs.segments/_index.segment.rsc +2 -2
  60. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  61. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +1 -1
  62. package/.next/server/app/logs.segments/logs.segment.rsc +1 -1
  63. package/.next/server/app/messages/page.js.nft.json +1 -1
  64. package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
  65. package/.next/server/app/messages.html +2 -2
  66. package/.next/server/app/messages.rsc +2 -2
  67. package/.next/server/app/messages.segments/_full.segment.rsc +2 -2
  68. package/.next/server/app/messages.segments/_head.segment.rsc +1 -1
  69. package/.next/server/app/messages.segments/_index.segment.rsc +2 -2
  70. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  71. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +1 -1
  72. package/.next/server/app/messages.segments/messages.segment.rsc +1 -1
  73. package/.next/server/app/node/page.js.nft.json +1 -1
  74. package/.next/server/app/node/page_client-reference-manifest.js +1 -1
  75. package/.next/server/app/node.html +2 -2
  76. package/.next/server/app/node.rsc +2 -2
  77. package/.next/server/app/node.segments/_full.segment.rsc +2 -2
  78. package/.next/server/app/node.segments/_head.segment.rsc +1 -1
  79. package/.next/server/app/node.segments/_index.segment.rsc +2 -2
  80. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  81. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +1 -1
  82. package/.next/server/app/node.segments/node.segment.rsc +1 -1
  83. package/.next/server/app/nodes/[alias]/page.js.nft.json +1 -1
  84. package/.next/server/app/nodes/[alias]/page_client-reference-manifest.js +1 -1
  85. package/.next/server/app/nodes/page.js.nft.json +1 -1
  86. package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
  87. package/.next/server/app/nodes.html +2 -2
  88. package/.next/server/app/nodes.rsc +3 -3
  89. package/.next/server/app/nodes.segments/_full.segment.rsc +3 -3
  90. package/.next/server/app/nodes.segments/_head.segment.rsc +1 -1
  91. package/.next/server/app/nodes.segments/_index.segment.rsc +2 -2
  92. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  93. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +2 -2
  94. package/.next/server/app/nodes.segments/nodes.segment.rsc +1 -1
  95. package/.next/server/app/page.js.nft.json +1 -1
  96. package/.next/server/app/page_client-reference-manifest.js +1 -1
  97. package/.next/server/app/providers/page.js.nft.json +1 -1
  98. package/.next/server/app/providers/page_client-reference-manifest.js +1 -1
  99. package/.next/server/app/providers.html +2 -2
  100. package/.next/server/app/providers.rsc +2 -2
  101. package/.next/server/app/providers.segments/_full.segment.rsc +2 -2
  102. package/.next/server/app/providers.segments/_head.segment.rsc +1 -1
  103. package/.next/server/app/providers.segments/_index.segment.rsc +2 -2
  104. package/.next/server/app/providers.segments/_tree.segment.rsc +2 -2
  105. package/.next/server/app/providers.segments/providers/__PAGE__.segment.rsc +1 -1
  106. package/.next/server/app/providers.segments/providers.segment.rsc +1 -1
  107. package/.next/server/app/server-logs/page.js.nft.json +1 -1
  108. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  109. package/.next/server/app/server-logs.html +2 -2
  110. package/.next/server/app/server-logs.rsc +2 -2
  111. package/.next/server/app/server-logs.segments/_full.segment.rsc +2 -2
  112. package/.next/server/app/server-logs.segments/_head.segment.rsc +1 -1
  113. package/.next/server/app/server-logs.segments/_index.segment.rsc +2 -2
  114. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  115. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +1 -1
  116. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +1 -1
  117. package/.next/server/app/servers/page.js.nft.json +1 -1
  118. package/.next/server/app/servers/page_client-reference-manifest.js +1 -1
  119. package/.next/server/app/servers.html +2 -2
  120. package/.next/server/app/servers.rsc +2 -2
  121. package/.next/server/app/servers.segments/_full.segment.rsc +2 -2
  122. package/.next/server/app/servers.segments/_head.segment.rsc +1 -1
  123. package/.next/server/app/servers.segments/_index.segment.rsc +2 -2
  124. package/.next/server/app/servers.segments/_tree.segment.rsc +2 -2
  125. package/.next/server/app/servers.segments/servers/__PAGE__.segment.rsc +1 -1
  126. package/.next/server/app/servers.segments/servers.segment.rsc +1 -1
  127. package/.next/server/app/settings/networks/page.js.nft.json +1 -1
  128. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  129. package/.next/server/app/settings/networks.html +2 -2
  130. package/.next/server/app/settings/networks.rsc +2 -2
  131. package/.next/server/app/settings/networks.segments/_full.segment.rsc +2 -2
  132. package/.next/server/app/settings/networks.segments/_head.segment.rsc +1 -1
  133. package/.next/server/app/settings/networks.segments/_index.segment.rsc +2 -2
  134. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  135. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +1 -1
  136. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +1 -1
  137. package/.next/server/app/settings/networks.segments/settings.segment.rsc +1 -1
  138. package/.next/server/app/settings/page.js.nft.json +1 -1
  139. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  140. package/.next/server/app/settings/tokens/page.js.nft.json +1 -1
  141. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  142. package/.next/server/app/settings/tokens.html +2 -2
  143. package/.next/server/app/settings/tokens.rsc +2 -2
  144. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +2 -2
  145. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +1 -1
  146. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +2 -2
  147. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  148. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +1 -1
  149. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +1 -1
  150. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +1 -1
  151. package/.next/server/app/settings.html +2 -2
  152. package/.next/server/app/settings.rsc +3 -3
  153. package/.next/server/app/settings.segments/_full.segment.rsc +3 -3
  154. package/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  155. package/.next/server/app/settings.segments/_index.segment.rsc +2 -2
  156. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  157. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +2 -2
  158. package/.next/server/app/settings.segments/settings.segment.rsc +1 -1
  159. package/.next/server/app/tasks/[id]/page.js.nft.json +1 -1
  160. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  161. package/.next/server/app/tasks/page.js.nft.json +1 -1
  162. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  163. package/.next/server/app/tasks.html +2 -2
  164. package/.next/server/app/tasks.rsc +2 -2
  165. package/.next/server/app/tasks.segments/_full.segment.rsc +2 -2
  166. package/.next/server/app/tasks.segments/_head.segment.rsc +1 -1
  167. package/.next/server/app/tasks.segments/_index.segment.rsc +2 -2
  168. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  169. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +1 -1
  170. package/.next/server/app/tasks.segments/tasks.segment.rsc +1 -1
  171. package/.next/server/chunks/ssr/{0ld5_next_dist_00v047f._.js → 0ld5_next_dist_04cce2g._.js} +2 -2
  172. package/.next/server/chunks/ssr/0ld5_next_dist_04cce2g._.js.map +1 -0
  173. package/.next/server/chunks/ssr/[root-of-the-server]__02o~-py._.js +3 -0
  174. package/.next/server/chunks/ssr/[root-of-the-server]__02o~-py._.js.map +1 -0
  175. package/.next/server/chunks/ssr/[root-of-the-server]__06w5i08._.js +1 -1
  176. package/.next/server/chunks/ssr/[root-of-the-server]__06w5i08._.js.map +1 -1
  177. package/.next/server/chunks/ssr/{[root-of-the-server]__00n84ca._.js → [root-of-the-server]__0x9nh~a._.js} +2 -2
  178. package/.next/server/chunks/ssr/[root-of-the-server]__0x9nh~a._.js.map +1 -0
  179. package/.next/server/chunks/ssr/dash-stageb-wt_0q55dv1._.js +0 -0
  180. package/.next/server/chunks/ssr/dash-stageb-wt_0q55dv1._.js.map +1 -0
  181. package/.next/server/chunks/ssr/dash-stageb-wt_13184u1._.js +0 -0
  182. package/.next/server/chunks/ssr/dash-stageb-wt_13184u1._.js.map +1 -0
  183. package/.next/server/chunks/ssr/dash-stageb-wt_app_0qn7hvc._.js +1 -1
  184. package/.next/server/chunks/ssr/dash-stageb-wt_app_0qn7hvc._.js.map +1 -1
  185. package/.next/server/chunks/ssr/dash-stageb-wt_app_0ss0pj-._.js +1 -1
  186. package/.next/server/chunks/ssr/dash-stageb-wt_app_0ss0pj-._.js.map +1 -1
  187. package/.next/server/chunks/ssr/dash-stageb-wt_app_nodes_NodesView_tsx_0patjj7._.js +1 -1
  188. package/.next/server/chunks/ssr/dash-stageb-wt_app_nodes_NodesView_tsx_0patjj7._.js.map +1 -1
  189. package/.next/server/chunks/ssr/{dash-stageb-wt_0hzfl6x._.js → dash-stageb-wt_app_page_tsx_0iyrtvj._.js} +3 -3
  190. package/.next/server/chunks/ssr/dash-stageb-wt_app_page_tsx_0iyrtvj._.js.map +1 -0
  191. package/.next/server/middleware-build-manifest.js +3 -3
  192. package/.next/server/pages/404.html +2 -2
  193. package/.next/server/pages/500.html +1 -1
  194. package/.next/server/server-reference-manifest.js +1 -1
  195. package/.next/server/server-reference-manifest.json +1 -1
  196. package/.next/static/chunks/05~lgfzsv.p~p.js +2 -0
  197. package/.next/static/chunks/092zqb~4un32l.css +1 -0
  198. package/.next/static/chunks/0aaakzcw7tey0.js +0 -0
  199. package/.next/static/chunks/0advoo2tzdcyi.js +1 -0
  200. package/.next/static/chunks/0dkk_l872ocgm.js +1 -0
  201. package/.next/static/chunks/0fz7zk.ek.154.js +1 -0
  202. package/.next/static/chunks/0njt8wek.9aq6.js +1 -0
  203. package/.next/static/chunks/{0hm52b0y~k0~z.js → 0uk67ty_0h_36.js} +1 -1
  204. package/.next/static/chunks/{0ep78sq90x_yc.js → 0z_4d13crd_af.js} +1 -1
  205. package/.next/static/chunks/0zljk8~2jqw93.js +4 -0
  206. package/.next/trace +2 -2
  207. package/.next/trace-build +1 -1
  208. package/app/components/ChatPane.tsx +108 -16
  209. package/app/components/NodeList.tsx +198 -46
  210. package/app/components/NodeListContextMenu.tsx +187 -0
  211. package/app/components/NodeSettingsPanel.tsx +29 -3
  212. package/app/nodes/NodesView.tsx +58 -0
  213. package/package.json +1 -1
  214. package/tests/e2e-9-nodes-team-tag-filter.spec.ts +3 -0
  215. package/tests/e2e-batch-list-actions.spec.ts +234 -0
  216. package/tests/e2e-stage-a-node-list.spec.ts +4 -0
  217. package/tests/e2e-stage-b-chat-pane.spec.ts +103 -0
  218. package/.next/server/chunks/ssr/0ld5_next_dist_00v047f._.js.map +0 -1
  219. package/.next/server/chunks/ssr/[root-of-the-server]__00n84ca._.js.map +0 -1
  220. package/.next/server/chunks/ssr/[root-of-the-server]__0idij1k._.js +0 -3
  221. package/.next/server/chunks/ssr/[root-of-the-server]__0idij1k._.js.map +0 -1
  222. package/.next/server/chunks/ssr/dash-stageb-wt_0hzfl6x._.js.map +0 -1
  223. package/.next/server/chunks/ssr/dash-stageb-wt_app_06.7qt9._.js +0 -0
  224. package/.next/server/chunks/ssr/dash-stageb-wt_app_06.7qt9._.js.map +0 -1
  225. package/.next/server/chunks/ssr/dash-stageb-wt_app_0y~m8-6._.js +0 -0
  226. package/.next/server/chunks/ssr/dash-stageb-wt_app_0y~m8-6._.js.map +0 -1
  227. package/.next/static/chunks/02.5_nifak.uu.js +0 -1
  228. package/.next/static/chunks/03hzj129k~odb.js +0 -1
  229. package/.next/static/chunks/0cey4i.y0a8~j.js +0 -1
  230. package/.next/static/chunks/0mhaf6lrw5nn..css +0 -1
  231. package/.next/static/chunks/0s293geid.j7e.js +0 -4
  232. package/.next/static/chunks/0zj_navpyhltx.js +0 -2
  233. package/.next/static/chunks/14y4x3de1lbug.js +0 -1
  234. package/.next/static/chunks/15ka68xi.w-i-.js +0 -1
  235. /package/.next/static/{vc_aJHtYwefsVcl2_f14Z → JL_QjNKv1KhsCF68ZK28x}/_buildManifest.js +0 -0
  236. /package/.next/static/{vc_aJHtYwefsVcl2_f14Z → JL_QjNKv1KhsCF68ZK28x}/_clientMiddlewareManifest.js +0 -0
  237. /package/.next/static/{vc_aJHtYwefsVcl2_f14Z → JL_QjNKv1KhsCF68ZK28x}/_ssgManifest.js +0 -0
@@ -0,0 +1,234 @@
1
+ import { test, expect, Page } from '@playwright/test';
2
+
3
+ const BASE = process.env.TEST_URL || 'http://localhost:3100';
4
+
5
+ // 07-31 batch (通信龙): 3 hard tests for the NodeList/ChatPane batch.
6
+ // Each test asserts an interaction that has no error surface if broken
7
+ // (would ship as "点了没反应" / "打了草稿没了" / "菜单少两项"):
8
+ //
9
+ // ① settings drawer open/close does NOT unmount TaskChatPanel
10
+ // (draft-in-input survives). Witnessed-red by mutating the drawer
11
+ // to KEY-remount the chat (parent-controlled key change → React
12
+ // remount → draft lost).
13
+ // ② keyboard focus discipline: ↓ moves URL when focus is on the
14
+ // rail, ↓ does NOT move URL when focus is in a text input.
15
+ // ③ right-click near list bottom → context menu flips upward
16
+ // (menu.getBoundingClientRect().bottom <= viewport.bottom).
17
+ //
18
+ // Not covered here (already visible in vf.mjs on prod real-data run):
19
+ // - pin/mute badge color swap (unit-level; SPEC §11.3 tokens hardcoded)
20
+ // - view-tasks / search buttons render (trivially visible)
21
+
22
+ // Per-worker login cache — dashboard rate-limit is 10 logins / 15min / IP.
23
+ let cachedCookie: string | undefined;
24
+
25
+ async function login(context: Page['context'] extends () => infer C ? C : never) {
26
+ if (cachedCookie) {
27
+ await context.addCookies([{
28
+ name: 'anet_dashboard_session',
29
+ value: cachedCookie,
30
+ domain: new URL(BASE).hostname,
31
+ path: '/',
32
+ }]);
33
+ return;
34
+ }
35
+ const res = await context.request.post(`${BASE}/api/auth/login`, {
36
+ data: { password: process.env.ANET_DASHBOARD_PASSWORD || 'admin123' },
37
+ });
38
+ const setCookie = res.headers()['set-cookie'] || '';
39
+ const m = setCookie.match(/anet_dashboard_session=([^;]+)/);
40
+ if (m) {
41
+ cachedCookie = decodeURIComponent(m[1]);
42
+ await context.addCookies([{
43
+ name: 'anet_dashboard_session',
44
+ value: cachedCookie,
45
+ domain: new URL(BASE).hostname,
46
+ path: '/',
47
+ }]);
48
+ }
49
+ }
50
+
51
+ const FIXTURE_SESSIONS = {
52
+ sessions: [
53
+ { alias: 'node-alpha', status: 'idle', network_id: 'net_x' },
54
+ { alias: 'node-beta', status: 'idle', network_id: 'net_x' },
55
+ { alias: 'node-gamma', status: 'idle', network_id: 'net_x' },
56
+ ],
57
+ };
58
+ const FIXTURE_NODES = {
59
+ nodes: [
60
+ { alias: 'node-alpha', node_id: 'n_alpha', team: null, tags: [], attrs_revision: 0 },
61
+ { alias: 'node-beta', node_id: 'n_beta', team: null, tags: [], attrs_revision: 0 },
62
+ { alias: 'node-gamma', node_id: 'n_gamma', team: null, tags: [], attrs_revision: 0 },
63
+ ],
64
+ };
65
+ const FIXTURE_HEALTH = {
66
+ ok: true, version: '0.9.0-test', sse_connections: 3,
67
+ sse_sessions: {
68
+ 'net_x:node-alpha': 1, 'net_x:node-beta': 1, 'net_x:node-gamma': 1,
69
+ },
70
+ };
71
+
72
+ async function stubHub(page: Page) {
73
+ await page.route('**/api/hub/status**', r =>
74
+ r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(FIXTURE_SESSIONS) }));
75
+ await page.route('**/api/hub/nodes**', r =>
76
+ r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(FIXTURE_NODES) }));
77
+ await page.route('**/api/hub/networks**', r =>
78
+ r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ networks: [{ network_id: 'net_x', network_name: 'net_x' }] }) }));
79
+ await page.route('**/api/hub/health', r =>
80
+ r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(FIXTURE_HEALTH) }));
81
+ await page.route('**/api/hub/tasks**', r =>
82
+ r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ tasks: [] }) }));
83
+ await page.route('**/api/hub/stats**', r =>
84
+ r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({}) }));
85
+ await page.route('**/api/hub/messages**', r =>
86
+ r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ messages: [] }) }));
87
+ await page.route('**/api/hub/events**', r =>
88
+ r.fulfill({ status: 200, contentType: 'text/event-stream', body: '' }));
89
+ // NodeSettingsPanel loads a per-node config snapshot — return an
90
+ // empty stub so the drawer mounts without console noise.
91
+ await page.route('**/api/anet/node-config**', r =>
92
+ r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ model: '', flags: {}, channels: [], config_revision: 0 }) }));
93
+ }
94
+
95
+ test.describe('Batch — list actions (07-31)', () => {
96
+ test('① settings drawer open/close does NOT unmount TaskChatPanel (draft survives)', async ({ page, context }) => {
97
+ await login(context);
98
+ await stubHub(page);
99
+ await page.goto(`${BASE}/nodes/node-alpha`);
100
+ await expect(page.locator('[data-testid="chat-pane"]').first()).toBeVisible({ timeout: 15000 });
101
+
102
+ // Find the chat's text input. TaskChatPanel renders one primary
103
+ // <textarea> / <input> for message composition — grab the first
104
+ // textbox inside the chat pane.
105
+ const chatInput = page.locator('[data-testid="chat-pane"] textarea, [data-testid="chat-pane"] input[type="text"]').first();
106
+ await expect(chatInput).toBeVisible({ timeout: 10000 });
107
+ // Type a distinctive draft that would be hard to reproduce by accident.
108
+ const DRAFT = 'preserve-across-settings-toggle-abc123';
109
+ await chatInput.fill(DRAFT);
110
+ await expect(chatInput).toHaveValue(DRAFT);
111
+
112
+ // Open settings drawer → close → text MUST still be there.
113
+ await page.locator('[data-testid="chat-pane-settings"]').click();
114
+ await expect(page.locator('[role="dialog"][aria-label*="节点设置"]').first()).toBeVisible({ timeout: 5000 });
115
+ // Close via the backdrop (absolute-positioned inside chat pane).
116
+ await page.keyboard.press('Escape');
117
+ // Wait a tick for React to reconcile.
118
+ await page.waitForTimeout(200);
119
+
120
+ // 🔴 The core assertion — the input still holds the same text.
121
+ // If TaskChatPanel got unmounted+remounted, state resets and this
122
+ // reads as empty (or reset to any autosaved draft, which is
123
+ // different — the point is "same value, no interruption").
124
+ await expect(chatInput).toHaveValue(DRAFT);
125
+ });
126
+
127
+ test('② kbd focus discipline — ↓ moves URL from rail focus, does NOT from input focus', async ({ page, context }) => {
128
+ await login(context);
129
+ await stubHub(page);
130
+ await page.goto(`${BASE}/nodes/node-alpha`);
131
+ await expect(page.locator('[data-testid="chat-pane"]').first()).toBeVisible({ timeout: 15000 });
132
+
133
+ // Wait for rail to actually have entries — the kbd handler
134
+ // bails on sessions.length === 0, so pressing arrow before SWR
135
+ // populates would test nothing.
136
+ await expect(page.locator('[data-node-list-item]').first()).toBeVisible({ timeout: 10000 });
137
+ const railCount = await page.locator('[data-node-list-item]').count();
138
+ expect(railCount).toBeGreaterThanOrEqual(3);
139
+
140
+ // Capture browser console to diagnose whether the handler fires.
141
+ const consoleMsgs: string[] = [];
142
+ page.on('console', msg => consoleMsgs.push(`${msg.type()}: ${msg.text()}`));
143
+
144
+ // Phase 1 — focus a NON-editable element (a rail entry button).
145
+ // TaskChatPanel refocuses its textarea via setTimeout(300) on
146
+ // mount (TaskChatPanel.tsx:745), so we MUST wait past that
147
+ // window before setting our own focus — otherwise the textarea
148
+ // steals it back mid-test. Wait 500ms > 300ms delay, then focus
149
+ // the button and press the key with no interceding await so no
150
+ // subsequent focus event can preempt.
151
+ await page.waitForTimeout(500);
152
+ await page.locator('[data-node-list-item]').first().focus();
153
+ const activeTag = await page.evaluate(() => document.activeElement?.tagName);
154
+ expect(activeTag).toBe('BUTTON');
155
+
156
+ const beforeUrl = new URL(page.url()).pathname;
157
+ expect(beforeUrl).toContain('/nodes/');
158
+ await page.keyboard.press('ArrowDown');
159
+ await page.waitForTimeout(500);
160
+ const afterUrl = new URL(page.url()).pathname;
161
+ expect(afterUrl).not.toBe(beforeUrl);
162
+ expect(afterUrl).toContain('node-beta');
163
+
164
+ // Phase 2 — dispatch same ArrowDown but with document.activeElement
165
+ // pointing at a text control. My handler bails when active.tagName
166
+ // is INPUT/TEXTAREA. Verify URL does NOT change.
167
+ const chatInput = page.locator('[data-testid="chat-pane"] textarea, [data-testid="chat-pane"] input[type="text"]').first();
168
+ await chatInput.focus();
169
+ await page.waitForTimeout(100);
170
+ const inputUrl = new URL(page.url()).pathname;
171
+ // Sanity: confirm the input actually took focus BEFORE dispatching,
172
+ // otherwise this test would spuriously pass (URL wouldn't change
173
+ // because the handler THINKS active is body → moves URL, but I'd
174
+ // observe unchanged URL for a different reason).
175
+ const activeInInput = await page.evaluate(() => document.activeElement?.tagName);
176
+ expect(['INPUT', 'TEXTAREA']).toContain(activeInInput ?? 'NONE');
177
+ await page.evaluate(() => {
178
+ const ev = new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true, cancelable: true });
179
+ window.dispatchEvent(ev);
180
+ });
181
+ await page.waitForTimeout(400);
182
+ const stillInputUrl = new URL(page.url()).pathname;
183
+ expect(stillInputUrl).toBe(inputUrl);
184
+ });
185
+
186
+ test('③ right-click near list bottom → menu flips upward + stays in viewport', async ({ page, context }) => {
187
+ await login(context);
188
+ await stubHub(page);
189
+ // Small viewport height so any entry the user right-clicks is
190
+ // "near the bottom" — a 120px menu rendered downward would
191
+ // overflow. This is the exact scenario the flip logic protects.
192
+ await page.setViewportSize({ width: 1280, height: 300 });
193
+ // Go to a node page (not /nodes) — the rail renders in both, but
194
+ // going through /nodes/<alias> exercises the exact code path
195
+ // tests 1 & 2 already validated the auth on. Then click the rail
196
+ // entry to force the "list-only" shape (chatAlias unchanged in
197
+ // URL is fine — the point is right-click on the last rail row).
198
+ await page.goto(`${BASE}/nodes/node-alpha`);
199
+ await expect(page.locator('[data-node-list-alias="node-gamma"]').first()).toBeVisible({ timeout: 15000 });
200
+
201
+ // Right-click on the last rail entry (node-gamma). Playwright's
202
+ // .click({button:'right'}) fires a native contextmenu with the
203
+ // element's center as clientX/clientY — for the last entry in a
204
+ // 300px-tall viewport, that lands well into the "would overflow
205
+ // downward" zone.
206
+ await page.locator('[data-node-list-alias="node-gamma"]').click({ button: 'right' });
207
+
208
+ const menu = page.locator('[data-testid="node-context-menu"]');
209
+ await expect(menu).toBeVisible({ timeout: 3000 });
210
+ // Wait for the useLayoutEffect + re-render — flip only applies
211
+ // after the second render. If flip did NOT fire, this attribute
212
+ // stays absent → the timeout below reads "expected 'true', got null"
213
+ // and the test reds precisely on the flip contract.
214
+ await expect(menu).toHaveAttribute('data-flipped-y', 'true', { timeout: 2000 });
215
+
216
+ // 🔴 Core assertion — menu is fully inside viewport (bottom edge
217
+ // ≤ viewport bottom). Without the flip, the menu would render
218
+ // from clientY down and have bottom > viewport.height.
219
+ const rect = await menu.evaluate(el => {
220
+ const r = el.getBoundingClientRect();
221
+ return { top: r.top, bottom: r.bottom, left: r.left, right: r.right };
222
+ });
223
+ // Read current viewport (we set it to 1280×300 above).
224
+ const vp = page.viewportSize()!;
225
+ expect(rect.bottom).toBeLessThanOrEqual(vp.height);
226
+ expect(rect.top).toBeGreaterThanOrEqual(0);
227
+ expect(rect.left).toBeGreaterThanOrEqual(0);
228
+ expect(rect.right).toBeLessThanOrEqual(vp.width);
229
+
230
+ // Sibling positive — the flip attribute is set (proves it took
231
+ // the flip branch, not just happened to fit by luck).
232
+ await expect(menu).toHaveAttribute('data-flipped-y', 'true');
233
+ });
234
+ });
@@ -57,6 +57,10 @@ async function login(context: Page['context'] extends () => infer C ? C : never)
57
57
  }
58
58
 
59
59
  async function mockCommon(page: Page) {
60
+ // #Vincent auto-select (07-31): these suites exercise the table/filter
61
+ // (manage) view — opt into it explicitly now that /nodes defaults to
62
+ // conversation-first with auto-select.
63
+ await page.addInitScript(() => window.localStorage.setItem('anet-nodes-manage-view', '1'));
60
64
  await page.route('**/api/hub/status**', r =>
61
65
  r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(HUB_STATUS) }));
62
66
  await page.route('**/api/hub/nodes**', r =>
@@ -84,6 +84,15 @@ async function mockCommon(page: Page) {
84
84
  r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ messages: [] }) }));
85
85
  }
86
86
 
87
+
88
+ // #Vincent auto-select (07-31): desktop /nodes now auto-selects the first
89
+ // conversation. Tests that exercise the UNSELECTED table-on-entry state
90
+ // opt into the persisted manage-view mode — the product's explicit doorway
91
+ // to that state — instead of weakening assertions.
92
+ async function manageMode(page: Page) {
93
+ await page.addInitScript(() => window.localStorage.setItem('anet-nodes-manage-view', '1'));
94
+ }
95
+
87
96
  const pane = (page: Page) => page.locator('[data-testid="chat-pane"]');
88
97
  const card = (page: Page, alias: string) => page.locator(`[data-node-card][data-node-alias="${alias}"]`);
89
98
 
@@ -91,6 +100,7 @@ test.describe('Stage B — persistent chat pane + /nodes/[alias] routing', () =>
91
100
  test('clicking a node renders chat inline in the right pane and rewrites the URL', async ({ page, context }) => {
92
101
  await login(context);
93
102
  await mockCommon(page);
103
+ await manageMode(page);
94
104
  await page.goto(`${BASE}/nodes`);
95
105
  await expect(card(page, 'stage-b-1')).toBeVisible({ timeout: 15000 });
96
106
 
@@ -108,6 +118,7 @@ test.describe('Stage B — persistent chat pane + /nodes/[alias] routing', () =>
108
118
  test('clicking another node swaps the SAME pane in place (still exactly one)', async ({ page, context }) => {
109
119
  await login(context);
110
120
  await mockCommon(page);
121
+ await manageMode(page);
111
122
  await page.goto(`${BASE}/nodes`);
112
123
  await expect(card(page, 'stage-b-1')).toBeVisible({ timeout: 15000 });
113
124
 
@@ -188,6 +199,7 @@ test.describe('Stage B — persistent chat pane + /nodes/[alias] routing', () =>
188
199
  test('#Stage C: selected = list+chat (table gone), unselected = table back — BOTH directions asserted', async ({ page, context }) => {
189
200
  await login(context);
190
201
  await mockCommon(page);
202
+ await manageMode(page);
191
203
  await page.goto(`${BASE}/nodes`);
192
204
  // Direction 1 (positive, same source): unselected /nodes renders the table.
193
205
  await expect(card(page, 'stage-b-1')).toBeVisible({ timeout: 15000 });
@@ -209,6 +221,7 @@ test.describe('Stage B — persistent chat pane + /nodes/[alias] routing', () =>
209
221
  test('#Stage C: dense-view user (rail hidden) selecting a node force-shows the rail', async ({ page, context }) => {
210
222
  await login(context);
211
223
  await mockCommon(page);
224
+ await manageMode(page);
212
225
  await page.addInitScript(() => window.localStorage.setItem('anet-nodes-rail', '0'));
213
226
  await page.goto(`${BASE}/nodes`);
214
227
  await expect(card(page, 'stage-b-1')).toBeVisible({ timeout: 15000 });
@@ -314,6 +327,7 @@ test.describe('Stage D — per-column scrolling, page never scrolls', () => {
314
327
  test('unselected + 60 nodes: document still does not scroll; the MIDDLE column scrolls to the last card', async ({ page, context }) => {
315
328
  await login(context);
316
329
  await mockWide(page);
330
+ await manageMode(page);
317
331
  await page.goto(`${BASE}/nodes`);
318
332
  await expect(page.locator('[data-node-card]')).toHaveCount(WIDE_N, { timeout: 15000 });
319
333
 
@@ -436,6 +450,7 @@ test.describe('Stage D ③ — rail-top node search', () => {
436
450
  test('unselected state: rail search filters rail AND table together (Stage A invariant holds)', async ({ page, context }) => {
437
451
  await login(context);
438
452
  await mockSearch(page);
453
+ await manageMode(page);
439
454
  await page.goto(`${BASE}/nodes`);
440
455
  await expect(page.locator('[data-node-card]')).toHaveCount(3, { timeout: 15000 });
441
456
 
@@ -507,3 +522,91 @@ test.describe('Stage D — chat column fixed, message area scrolls inside', () =
507
522
  expect(box!.y + box!.height).toBeLessThanOrEqual(innerH);
508
523
  });
509
524
  });
525
+
526
+ // ─────────────────────────────────────────────────────────────────────
527
+ // Vincent 插单 (07-31): desktop /nodes opens INTO a conversation.
528
+ // Traps pinned: ① replace-not-push (back must LEAVE /nodes, not bounce),
529
+ // ② the manage table keeps an explicit doorway, ③ an explicit ✕ is not
530
+ // answered by an immediate re-select, plus empty-list safety.
531
+ // ─────────────────────────────────────────────────────────────────────
532
+ test.describe('auto-select first conversation on /nodes (desktop)', () => {
533
+ test('entering /nodes lands in the first conversation: URL rewritten, table yields', async ({ page, context }) => {
534
+ await login(context);
535
+ await mockCommon(page);
536
+ await page.goto(`${BASE}/nodes`);
537
+ await expect(pane(page)).toBeVisible({ timeout: 15000 });
538
+ await expect(page).toHaveURL(/\/nodes\/[^/]+$/);
539
+ await expect(page.locator('[data-node-card]')).toHaveCount(0);
540
+ // deterministic first pick under this fixture's sort
541
+ await expect(pane(page)).toHaveAttribute('data-chat-alias', 'stage-b-1');
542
+ });
543
+
544
+ test('browser back LEAVES /nodes (auto-select uses replace, not push)', async ({ page, context }) => {
545
+ await login(context);
546
+ await mockCommon(page);
547
+ await page.goto(`${BASE}/tasks`);
548
+ await page.goto(`${BASE}/nodes`);
549
+ await expect(pane(page)).toBeVisible({ timeout: 15000 });
550
+ await expect(page).toHaveURL(/\/nodes\/[^/]+$/);
551
+
552
+ await page.goBack();
553
+ // With push instead of replace this bounces straight back into
554
+ // /nodes/<alias> — the assertion below goes red (sabotage-verified).
555
+ await expect(page).toHaveURL(`${BASE}/tasks`, { timeout: 10000 });
556
+ });
557
+
558
+ test('explicit ✕ close sticks: no immediate re-select', async ({ page, context }) => {
559
+ await login(context);
560
+ await mockCommon(page);
561
+ await page.goto(`${BASE}/nodes`);
562
+ await expect(pane(page)).toBeVisible({ timeout: 15000 });
563
+
564
+ await page.locator('[data-testid="chat-pane-close"]').click();
565
+ await expect(pane(page)).toHaveCount(0);
566
+ await expect(page).toHaveURL(`${BASE}/nodes`);
567
+ await expect(page.locator('[data-node-card]')).toHaveCount(3);
568
+ await page.waitForTimeout(900);
569
+ await expect(pane(page)).toHaveCount(0); // still closed — ✕ won
570
+ });
571
+
572
+ test('manage-view doorway: toggle to table, persists across reload, toggles back to conversation', async ({ page, context }) => {
573
+ await login(context);
574
+ await mockCommon(page);
575
+ await page.goto(`${BASE}/nodes`);
576
+ await expect(pane(page)).toBeVisible({ timeout: 15000 });
577
+
578
+ await page.locator('[data-testid="manage-view-toggle"]').click();
579
+ await expect(pane(page)).toHaveCount(0);
580
+ await expect(page.locator('[data-node-card]')).toHaveCount(3);
581
+
582
+ await page.goto(`${BASE}/nodes`); // persisted preference: table default, no auto-select
583
+ await expect(page.locator('[data-node-card]')).toHaveCount(3, { timeout: 15000 });
584
+ await page.waitForTimeout(900);
585
+ await expect(pane(page)).toHaveCount(0);
586
+
587
+ await page.locator('[data-testid="manage-view-toggle"]').click(); // 对话视图
588
+ await expect(pane(page)).toBeVisible();
589
+ await expect(page).toHaveURL(/\/nodes\/[^/]+$/);
590
+ });
591
+
592
+ test('empty list: no crash, no loop, URL stays /nodes', async ({ page, context }) => {
593
+ await login(context);
594
+ await page.route('**/api/hub/status**', r =>
595
+ r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ sessions: [] }) }));
596
+ await page.route('**/api/hub/nodes**', r =>
597
+ r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ nodes: [] }) }));
598
+ await page.route('**/api/hub/health', r =>
599
+ r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ ok: true, sse_sessions: {} }) }));
600
+ await page.route('**/api/hub/tasks**', r =>
601
+ r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ tasks: [] }) }));
602
+ await page.route('**/api/hub/stats**', r =>
603
+ r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({}) }));
604
+ await page.route('**/api/hub/messages**', r =>
605
+ r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ messages: [] }) }));
606
+ await page.goto(`${BASE}/nodes`);
607
+ await page.waitForTimeout(2500);
608
+ await expect(page).toHaveURL(`${BASE}/nodes`);
609
+ await expect(pane(page)).toHaveCount(0);
610
+ await expect(page.locator("text=This page couldn’t load")).toHaveCount(0);
611
+ });
612
+ });