@sleep2agi/agent-network-dashboard 0.6.3-preview.50 → 0.6.3-preview.52
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.
- package/.next/BUILD_COMMIT +1 -1
- package/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +3 -3
- package/.next/diagnostics/route-bundle-stats.json +50 -50
- package/.next/fallback-build-manifest.json +3 -3
- package/.next/prerender-manifest.json +3 -3
- package/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +2 -2
- package/.next/server/app/_not-found.rsc +13 -13
- package/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
- package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/server/app/_not-found.segments/_index.segment.rsc +8 -8
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/admin/page.js.nft.json +1 -1
- package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
- package/.next/server/app/admin.html +2 -2
- package/.next/server/app/admin.rsc +15 -15
- package/.next/server/app/admin.segments/_full.segment.rsc +15 -15
- package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
- package/.next/server/app/admin.segments/_index.segment.rsc +8 -8
- package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/admin.segments/admin.segment.rsc +3 -3
- package/.next/server/app/index.html +2 -2
- package/.next/server/app/index.rsc +15 -15
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/server/app/index.segments/_index.segment.rsc +8 -8
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/login/page.js.nft.json +1 -1
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/login.html +2 -2
- package/.next/server/app/login.rsc +15 -15
- package/.next/server/app/login.segments/_full.segment.rsc +15 -15
- package/.next/server/app/login.segments/_head.segment.rsc +4 -4
- package/.next/server/app/login.segments/_index.segment.rsc +8 -8
- package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/login.segments/login.segment.rsc +3 -3
- package/.next/server/app/logs/page.js.nft.json +1 -1
- package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
- package/.next/server/app/logs.html +2 -2
- package/.next/server/app/logs.rsc +15 -15
- package/.next/server/app/logs.segments/_full.segment.rsc +15 -15
- package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
- package/.next/server/app/logs.segments/_index.segment.rsc +8 -8
- package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
- package/.next/server/app/messages/page.js.nft.json +1 -1
- package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
- package/.next/server/app/messages.html +2 -2
- package/.next/server/app/messages.rsc +15 -15
- package/.next/server/app/messages.segments/_full.segment.rsc +15 -15
- package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
- package/.next/server/app/messages.segments/_index.segment.rsc +8 -8
- package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
- package/.next/server/app/node/page.js.nft.json +1 -1
- package/.next/server/app/node/page_client-reference-manifest.js +1 -1
- package/.next/server/app/node.html +2 -2
- package/.next/server/app/node.rsc +15 -15
- package/.next/server/app/node.segments/_full.segment.rsc +15 -15
- package/.next/server/app/node.segments/_head.segment.rsc +4 -4
- package/.next/server/app/node.segments/_index.segment.rsc +8 -8
- package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/node.segments/node.segment.rsc +3 -3
- package/.next/server/app/nodes/[alias]/page.js.nft.json +1 -1
- package/.next/server/app/nodes/[alias]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/nodes/page.js.nft.json +1 -1
- package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
- package/.next/server/app/nodes.html +2 -2
- package/.next/server/app/nodes.rsc +15 -15
- package/.next/server/app/nodes.segments/_full.segment.rsc +15 -15
- package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
- package/.next/server/app/nodes.segments/_index.segment.rsc +8 -8
- package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/providers/page.js.nft.json +1 -1
- package/.next/server/app/providers/page_client-reference-manifest.js +1 -1
- package/.next/server/app/providers.html +2 -2
- package/.next/server/app/providers.rsc +15 -15
- package/.next/server/app/providers.segments/_full.segment.rsc +15 -15
- package/.next/server/app/providers.segments/_head.segment.rsc +4 -4
- package/.next/server/app/providers.segments/_index.segment.rsc +8 -8
- package/.next/server/app/providers.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/providers.segments/providers/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/providers.segments/providers.segment.rsc +3 -3
- package/.next/server/app/server-logs/page.js.nft.json +1 -1
- package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
- package/.next/server/app/server-logs.html +2 -2
- package/.next/server/app/server-logs.rsc +15 -15
- package/.next/server/app/server-logs.segments/_full.segment.rsc +15 -15
- package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
- package/.next/server/app/server-logs.segments/_index.segment.rsc +8 -8
- package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
- package/.next/server/app/servers/page.js.nft.json +1 -1
- package/.next/server/app/servers/page_client-reference-manifest.js +1 -1
- package/.next/server/app/servers.html +2 -2
- package/.next/server/app/servers.rsc +15 -15
- package/.next/server/app/servers.segments/_full.segment.rsc +15 -15
- package/.next/server/app/servers.segments/_head.segment.rsc +4 -4
- package/.next/server/app/servers.segments/_index.segment.rsc +8 -8
- package/.next/server/app/servers.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/servers.segments/servers/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/servers.segments/servers.segment.rsc +3 -3
- package/.next/server/app/settings/networks/page.js.nft.json +1 -1
- package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/networks.html +2 -2
- package/.next/server/app/settings/networks.rsc +15 -15
- package/.next/server/app/settings/networks.segments/_full.segment.rsc +15 -15
- package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings/networks.segments/_index.segment.rsc +8 -8
- package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
- package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
- package/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/tokens/page.js.nft.json +1 -1
- package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/tokens.html +2 -2
- package/.next/server/app/settings/tokens.rsc +15 -15
- package/.next/server/app/settings/tokens.segments/_full.segment.rsc +15 -15
- package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings/tokens.segments/_index.segment.rsc +8 -8
- package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
- package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
- package/.next/server/app/settings.html +2 -2
- package/.next/server/app/settings.rsc +15 -15
- package/.next/server/app/settings.segments/_full.segment.rsc +15 -15
- package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings.segments/_index.segment.rsc +8 -8
- package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
- package/.next/server/app/tasks/[id]/page.js.nft.json +1 -1
- package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/tasks/page.js.nft.json +1 -1
- package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
- package/.next/server/app/tasks.html +2 -2
- package/.next/server/app/tasks.rsc +15 -15
- package/.next/server/app/tasks.segments/_full.segment.rsc +15 -15
- package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
- package/.next/server/app/tasks.segments/_index.segment.rsc +8 -8
- package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
- package/.next/server/chunks/ssr/{0ld5_next_dist_00v047f._.js → 0ld5_next_dist_04cce2g._.js} +2 -2
- package/.next/server/chunks/ssr/0ld5_next_dist_04cce2g._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__06w5i08._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__06w5i08._.js.map +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0dcez4x._.js +11 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0dcez4x._.js.map +1 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__0jgqpfv._.js → [root-of-the-server]__0gyqilt._.js} +2 -2
- package/.next/server/chunks/ssr/[root-of-the-server]__0gyqilt._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0j-def.._.js +11 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0j-def.._.js.map +1 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__11wln8.._.js → [root-of-the-server]__0oavw6j._.js} +2 -2
- package/.next/server/chunks/ssr/[root-of-the-server]__0oavw6j._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0rct4.k._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0rct4.k._.js.map +1 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__00n84ca._.js → [root-of-the-server]__0x9nh~a._.js} +2 -2
- package/.next/server/chunks/ssr/[root-of-the-server]__0x9nh~a._.js.map +1 -0
- package/.next/server/chunks/ssr/dash-stageb-wt_0q55dv1._.js +0 -0
- package/.next/server/chunks/ssr/dash-stageb-wt_0q55dv1._.js.map +1 -0
- package/.next/server/chunks/ssr/dash-stageb-wt_13184u1._.js +0 -0
- package/.next/server/chunks/ssr/dash-stageb-wt_13184u1._.js.map +1 -0
- package/.next/server/chunks/ssr/dash-stageb-wt_app_05u4_h~._.js +2 -2
- package/.next/server/chunks/ssr/dash-stageb-wt_app_05u4_h~._.js.map +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0qn7hvc._.js +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0qn7hvc._.js.map +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0ss0pj-._.js +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0ss0pj-._.js.map +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_nodes_NodesView_tsx_0patjj7._.js +2 -2
- package/.next/server/chunks/ssr/dash-stageb-wt_app_nodes_NodesView_tsx_0patjj7._.js.map +1 -1
- package/.next/server/chunks/ssr/{dash-stageb-wt_0hzfl6x._.js → dash-stageb-wt_app_page_tsx_0iyrtvj._.js} +3 -3
- package/.next/server/chunks/ssr/dash-stageb-wt_app_page_tsx_0iyrtvj._.js.map +1 -0
- package/.next/server/middleware-build-manifest.js +3 -3
- package/.next/server/pages/404.html +2 -2
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/0-ot~88nectgp.js +2 -0
- package/.next/static/chunks/00adqh_rd04uf.js +9 -0
- package/.next/static/chunks/051p0pw4z~6ag.js +1 -0
- package/.next/static/chunks/0aaakzcw7tey0.js +0 -0
- package/.next/static/chunks/{0hm52b0y~k0~z.js → 0lf8e-h91s10h.js} +1 -1
- package/.next/static/chunks/0njt8wek.9aq6.js +1 -0
- package/.next/static/chunks/0ohx4jpm9~61..js +1 -0
- package/.next/static/chunks/0pg4pce.q5ne_.js +9 -0
- package/.next/static/chunks/0u5y.gmi~jkw6.js +1 -0
- package/.next/static/chunks/0wf03_efeii.w.css +1 -0
- package/.next/static/chunks/0ws0p1n62l~ub.js +1 -0
- package/.next/static/chunks/0zljk8~2jqw93.js +4 -0
- package/.next/static/chunks/{06q7-vhy~1-y3.js → 0~rx9_~1wupcp.js} +2 -2
- package/.next/static/chunks/{0ep78sq90x_yc.js → 14x094g58kg61.js} +1 -1
- package/.next/trace +2 -2
- package/.next/trace-build +1 -1
- package/app/components/ChatPane.tsx +133 -17
- package/app/components/NodeList.tsx +208 -46
- package/app/components/NodeListContextMenu.tsx +187 -0
- package/app/components/NodeSettingsPanel.tsx +29 -3
- package/app/components/TaskChatPanel.tsx +156 -31
- package/app/messages/page.tsx +22 -22
- package/app/nodes/NodesView.tsx +110 -11
- package/package.json +1 -1
- package/tests/e2e-9-nodes-team-tag-filter.spec.ts +17 -7
- package/tests/e2e-batch-list-actions.spec.ts +234 -0
- package/tests/e2e-messages-spec-visual-align.spec.ts +244 -0
- package/tests/e2e-pin-mute-attr-always-present.spec.ts +171 -0
- package/tests/e2e-stage-a-node-list.spec.ts +18 -7
- package/tests/e2e-stage-b-chat-pane.spec.ts +177 -10
- package/tests/e2e-status-labels.spec.ts +218 -0
- package/.next/server/chunks/ssr/0ld5_next_dist_00v047f._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__00n84ca._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0idij1k._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0idij1k._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0jgqpfv._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0w7k~3f._.js +0 -9
- package/.next/server/chunks/ssr/[root-of-the-server]__0w7k~3f._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__11wln8.._.js.map +0 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_0hzfl6x._.js.map +0 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_06.7qt9._.js +0 -0
- package/.next/server/chunks/ssr/dash-stageb-wt_app_06.7qt9._.js.map +0 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0y~m8-6._.js +0 -0
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0y~m8-6._.js.map +0 -1
- package/.next/static/chunks/02.5_nifak.uu.js +0 -1
- package/.next/static/chunks/03hzj129k~odb.js +0 -1
- package/.next/static/chunks/0cey4i.y0a8~j.js +0 -1
- package/.next/static/chunks/0mhaf6lrw5nn..css +0 -1
- package/.next/static/chunks/0s293geid.j7e.js +0 -4
- package/.next/static/chunks/0zj_navpyhltx.js +0 -2
- package/.next/static/chunks/11zz15q37~bz5.js +0 -7
- package/.next/static/chunks/14y4x3de1lbug.js +0 -1
- package/.next/static/chunks/15ka68xi.w-i-.js +0 -1
- package/.next/static/chunks/17qkc714xy04..js +0 -1
- /package/.next/static/{vc_aJHtYwefsVcl2_f14Z → RiXBfeE2zY5tzXRmF7kBD}/_buildManifest.js +0 -0
- /package/.next/static/{vc_aJHtYwefsVcl2_f14Z → RiXBfeE2zY5tzXRmF7kBD}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{vc_aJHtYwefsVcl2_f14Z → RiXBfeE2zY5tzXRmF7kBD}/_ssgManifest.js +0 -0
|
@@ -40,16 +40,23 @@ const HUB_HEALTH = {
|
|
|
40
40
|
},
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
+
// Login rate limit is 10/15min/IP (app/lib/rate-limit.ts): with 4 spec
|
|
44
|
+
// files in one combined run, per-test logins blow the budget and late
|
|
45
|
+
// tests land on the sign-in page (false red). Login once per worker.
|
|
46
|
+
let cachedSessionCookie: string | null = null;
|
|
43
47
|
async function login(context: Page['context'] extends () => infer C ? C : never) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
if (!cachedSessionCookie) {
|
|
49
|
+
const res = await context.request.post(`${BASE}/api/auth/login`, {
|
|
50
|
+
data: { password: process.env.ANET_DASHBOARD_PASSWORD || 'admin123' },
|
|
51
|
+
});
|
|
52
|
+
const setCookie = res.headers()['set-cookie'] || '';
|
|
53
|
+
const m = setCookie.match(/anet_dashboard_session=([^;]+)/);
|
|
54
|
+
if (m) cachedSessionCookie = decodeURIComponent(m[1]);
|
|
55
|
+
}
|
|
56
|
+
if (cachedSessionCookie) {
|
|
50
57
|
await context.addCookies([{
|
|
51
58
|
name: 'anet_dashboard_session',
|
|
52
|
-
value:
|
|
59
|
+
value: cachedSessionCookie,
|
|
53
60
|
domain: new URL(BASE).hostname,
|
|
54
61
|
path: '/',
|
|
55
62
|
}]);
|
|
@@ -57,6 +64,10 @@ async function login(context: Page['context'] extends () => infer C ? C : never)
|
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
async function mockCommon(page: Page) {
|
|
67
|
+
// #Vincent auto-select (07-31): these suites exercise the table/filter
|
|
68
|
+
// (manage) view — opt into it explicitly now that /nodes defaults to
|
|
69
|
+
// conversation-first with auto-select.
|
|
70
|
+
await page.addInitScript(() => window.localStorage.setItem('anet-nodes-manage-view', '1'));
|
|
60
71
|
await page.route('**/api/hub/status**', r =>
|
|
61
72
|
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(HUB_STATUS) }));
|
|
62
73
|
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
|
|
|
@@ -142,22 +153,86 @@ test.describe('Stage B — persistent chat pane + /nodes/[alias] routing', () =>
|
|
|
142
153
|
await expect(page).toHaveURL(`${BASE}/nodes`);
|
|
143
154
|
});
|
|
144
155
|
|
|
145
|
-
test('< 768px:
|
|
156
|
+
test('< 768px: two-level nav — list fullscreen, tap opens chat as a PAGE (no overlay), URL-addressable', async ({ page, context }) => {
|
|
146
157
|
await login(context);
|
|
147
158
|
await mockCommon(page);
|
|
159
|
+
await page.addInitScript(() => window.localStorage.setItem('anet-nodes-manage-view', '0'));
|
|
148
160
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
149
161
|
await page.goto(`${BASE}/nodes`);
|
|
150
|
-
|
|
162
|
+
// Level 1: conversation list fullscreen; the table does NOT render;
|
|
163
|
+
// and mobile does NOT auto-select (desktop-only behavior — 通信龙:
|
|
164
|
+
// two correct behaviors, don't test one shape with the other's
|
|
165
|
+
// assertions). URL must stay /nodes.
|
|
166
|
+
const rail = page.locator('[data-testid="node-list-rail"]');
|
|
167
|
+
await expect(rail).toBeVisible({ timeout: 15000 });
|
|
168
|
+
await page.waitForTimeout(900);
|
|
169
|
+
await expect(page).toHaveURL(`${BASE}/nodes`);
|
|
170
|
+
const railBox = await rail.boundingBox();
|
|
171
|
+
expect(Math.abs(railBox!.width - 390)).toBeLessThanOrEqual(2);
|
|
172
|
+
await expect(page.locator('[data-node-card]')).toHaveCount(0);
|
|
173
|
+
await expect(page.locator('[data-node-list-item]')).toHaveCount(3);
|
|
151
174
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
await expect(page
|
|
155
|
-
await expect(
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
await expect(
|
|
159
|
-
|
|
175
|
+
// Level 2: tap → chat page (not overlay), back arrow present.
|
|
176
|
+
await page.locator('[data-node-list-item][data-node-list-alias="stage-b-1"]').click();
|
|
177
|
+
await expect(pane(page)).toBeVisible();
|
|
178
|
+
await expect(page.locator('.animate-slide-in')).toHaveCount(0);
|
|
179
|
+
const paneBox = await pane(page).boundingBox();
|
|
180
|
+
expect(Math.abs(paneBox!.width - 390)).toBeLessThanOrEqual(2);
|
|
181
|
+
await expect(rail).toHaveCount(0); // one level at a time
|
|
182
|
+
await expect(page.locator('[data-testid="chat-back"]')).toBeVisible();
|
|
160
183
|
await expect(page).toHaveURL(`${BASE}/nodes/stage-b-1`);
|
|
184
|
+
|
|
185
|
+
// Back arrow returns to the list.
|
|
186
|
+
await page.locator('[data-testid="chat-back"]').click();
|
|
187
|
+
await expect(rail).toBeVisible();
|
|
188
|
+
await expect(page).toHaveURL(`${BASE}/nodes`);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('< 768px: back (arrow AND browser) restores list scroll position within 200px', async ({ page, context }) => {
|
|
192
|
+
await login(context);
|
|
193
|
+
await mockWide(page); // 60 nodes — enough to have a real scroll offset
|
|
194
|
+
await page.addInitScript(() => window.localStorage.setItem('anet-nodes-manage-view', '0'));
|
|
195
|
+
await page.setViewportSize({ width: 390, height: 844 });
|
|
196
|
+
await page.goto(`${BASE}/nodes`);
|
|
197
|
+
const scroll = page.locator('[data-testid="node-list-scroll"]');
|
|
198
|
+
await expect(page.locator('[data-node-list-item]')).toHaveCount(60, { timeout: 15000 });
|
|
199
|
+
|
|
200
|
+
const target = page.locator('[data-node-list-item][data-node-list-alias="stage-d-50"]');
|
|
201
|
+
await target.scrollIntoViewIfNeeded();
|
|
202
|
+
const before = await scroll.evaluate(el => el.scrollTop);
|
|
203
|
+
expect(before).toBeGreaterThan(500); // sanity: we really are deep in the list
|
|
204
|
+
|
|
205
|
+
await target.click();
|
|
206
|
+
await expect(pane(page)).toBeVisible();
|
|
207
|
+
await page.locator('[data-testid="chat-back"]').click();
|
|
208
|
+
await expect(scroll).toBeVisible();
|
|
209
|
+
const afterArrow = await scroll.evaluate(el => el.scrollTop);
|
|
210
|
+
expect(Math.abs(afterArrow - before)).toBeLessThanOrEqual(200);
|
|
211
|
+
|
|
212
|
+
// Browser back behaves the same (判据: 行为一致).
|
|
213
|
+
await target.click();
|
|
214
|
+
await expect(pane(page)).toBeVisible();
|
|
215
|
+
await page.goBack();
|
|
216
|
+
await expect(scroll).toBeVisible();
|
|
217
|
+
const afterBack = await scroll.evaluate(el => el.scrollTop);
|
|
218
|
+
expect(Math.abs(afterBack - before)).toBeLessThanOrEqual(200);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('< 768px: manage mode still reaches the table, with a way back to conversations', async ({ page, context }) => {
|
|
222
|
+
await login(context);
|
|
223
|
+
await mockCommon(page);
|
|
224
|
+
await page.addInitScript(() => window.localStorage.setItem('anet-nodes-manage-view', '1'));
|
|
225
|
+
await page.setViewportSize({ width: 390, height: 844 });
|
|
226
|
+
await page.goto(`${BASE}/nodes`);
|
|
227
|
+
// Fact-side positive: the table is genuinely there in manage mode
|
|
228
|
+
// (guards against "mobile hides table" being green because the table
|
|
229
|
+
// broke everywhere).
|
|
230
|
+
await expect(page.locator('[data-node-card]')).toHaveCount(3, { timeout: 15000 });
|
|
231
|
+
await expect(page.locator('[data-testid="manage-view-toggle-mobile"]')).toBeVisible();
|
|
232
|
+
|
|
233
|
+
await page.locator('[data-testid="manage-view-toggle-mobile"]').click();
|
|
234
|
+
await expect(page.locator('[data-testid="node-list-rail"]')).toBeVisible();
|
|
235
|
+
await expect(page.locator('[data-node-card]')).toHaveCount(0);
|
|
161
236
|
});
|
|
162
237
|
|
|
163
238
|
test('slow-load regression: 1.5s-delayed first /api/hub/status on a deep link → no crash page', async ({ page, context }) => {
|
|
@@ -188,6 +263,7 @@ test.describe('Stage B — persistent chat pane + /nodes/[alias] routing', () =>
|
|
|
188
263
|
test('#Stage C: selected = list+chat (table gone), unselected = table back — BOTH directions asserted', async ({ page, context }) => {
|
|
189
264
|
await login(context);
|
|
190
265
|
await mockCommon(page);
|
|
266
|
+
await manageMode(page);
|
|
191
267
|
await page.goto(`${BASE}/nodes`);
|
|
192
268
|
// Direction 1 (positive, same source): unselected /nodes renders the table.
|
|
193
269
|
await expect(card(page, 'stage-b-1')).toBeVisible({ timeout: 15000 });
|
|
@@ -209,6 +285,7 @@ test.describe('Stage B — persistent chat pane + /nodes/[alias] routing', () =>
|
|
|
209
285
|
test('#Stage C: dense-view user (rail hidden) selecting a node force-shows the rail', async ({ page, context }) => {
|
|
210
286
|
await login(context);
|
|
211
287
|
await mockCommon(page);
|
|
288
|
+
await manageMode(page);
|
|
212
289
|
await page.addInitScript(() => window.localStorage.setItem('anet-nodes-rail', '0'));
|
|
213
290
|
await page.goto(`${BASE}/nodes`);
|
|
214
291
|
await expect(card(page, 'stage-b-1')).toBeVisible({ timeout: 15000 });
|
|
@@ -314,6 +391,7 @@ test.describe('Stage D — per-column scrolling, page never scrolls', () => {
|
|
|
314
391
|
test('unselected + 60 nodes: document still does not scroll; the MIDDLE column scrolls to the last card', async ({ page, context }) => {
|
|
315
392
|
await login(context);
|
|
316
393
|
await mockWide(page);
|
|
394
|
+
await manageMode(page);
|
|
317
395
|
await page.goto(`${BASE}/nodes`);
|
|
318
396
|
await expect(page.locator('[data-node-card]')).toHaveCount(WIDE_N, { timeout: 15000 });
|
|
319
397
|
|
|
@@ -436,6 +514,7 @@ test.describe('Stage D ③ — rail-top node search', () => {
|
|
|
436
514
|
test('unselected state: rail search filters rail AND table together (Stage A invariant holds)', async ({ page, context }) => {
|
|
437
515
|
await login(context);
|
|
438
516
|
await mockSearch(page);
|
|
517
|
+
await manageMode(page);
|
|
439
518
|
await page.goto(`${BASE}/nodes`);
|
|
440
519
|
await expect(page.locator('[data-node-card]')).toHaveCount(3, { timeout: 15000 });
|
|
441
520
|
|
|
@@ -507,3 +586,91 @@ test.describe('Stage D — chat column fixed, message area scrolls inside', () =
|
|
|
507
586
|
expect(box!.y + box!.height).toBeLessThanOrEqual(innerH);
|
|
508
587
|
});
|
|
509
588
|
});
|
|
589
|
+
|
|
590
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
591
|
+
// Vincent 插单 (07-31): desktop /nodes opens INTO a conversation.
|
|
592
|
+
// Traps pinned: ① replace-not-push (back must LEAVE /nodes, not bounce),
|
|
593
|
+
// ② the manage table keeps an explicit doorway, ③ an explicit ✕ is not
|
|
594
|
+
// answered by an immediate re-select, plus empty-list safety.
|
|
595
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
596
|
+
test.describe('auto-select first conversation on /nodes (desktop)', () => {
|
|
597
|
+
test('entering /nodes lands in the first conversation: URL rewritten, table yields', async ({ page, context }) => {
|
|
598
|
+
await login(context);
|
|
599
|
+
await mockCommon(page);
|
|
600
|
+
await page.goto(`${BASE}/nodes`);
|
|
601
|
+
await expect(pane(page)).toBeVisible({ timeout: 15000 });
|
|
602
|
+
await expect(page).toHaveURL(/\/nodes\/[^/]+$/);
|
|
603
|
+
await expect(page.locator('[data-node-card]')).toHaveCount(0);
|
|
604
|
+
// deterministic first pick under this fixture's sort
|
|
605
|
+
await expect(pane(page)).toHaveAttribute('data-chat-alias', 'stage-b-1');
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
test('browser back LEAVES /nodes (auto-select uses replace, not push)', async ({ page, context }) => {
|
|
609
|
+
await login(context);
|
|
610
|
+
await mockCommon(page);
|
|
611
|
+
await page.goto(`${BASE}/tasks`);
|
|
612
|
+
await page.goto(`${BASE}/nodes`);
|
|
613
|
+
await expect(pane(page)).toBeVisible({ timeout: 15000 });
|
|
614
|
+
await expect(page).toHaveURL(/\/nodes\/[^/]+$/);
|
|
615
|
+
|
|
616
|
+
await page.goBack();
|
|
617
|
+
// With push instead of replace this bounces straight back into
|
|
618
|
+
// /nodes/<alias> — the assertion below goes red (sabotage-verified).
|
|
619
|
+
await expect(page).toHaveURL(`${BASE}/tasks`, { timeout: 10000 });
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
test('explicit ✕ close sticks: no immediate re-select', async ({ page, context }) => {
|
|
623
|
+
await login(context);
|
|
624
|
+
await mockCommon(page);
|
|
625
|
+
await page.goto(`${BASE}/nodes`);
|
|
626
|
+
await expect(pane(page)).toBeVisible({ timeout: 15000 });
|
|
627
|
+
|
|
628
|
+
await page.locator('[data-testid="chat-pane-close"]').click();
|
|
629
|
+
await expect(pane(page)).toHaveCount(0);
|
|
630
|
+
await expect(page).toHaveURL(`${BASE}/nodes`);
|
|
631
|
+
await expect(page.locator('[data-node-card]')).toHaveCount(3);
|
|
632
|
+
await page.waitForTimeout(900);
|
|
633
|
+
await expect(pane(page)).toHaveCount(0); // still closed — ✕ won
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
test('manage-view doorway: toggle to table, persists across reload, toggles back to conversation', async ({ page, context }) => {
|
|
637
|
+
await login(context);
|
|
638
|
+
await mockCommon(page);
|
|
639
|
+
await page.goto(`${BASE}/nodes`);
|
|
640
|
+
await expect(pane(page)).toBeVisible({ timeout: 15000 });
|
|
641
|
+
|
|
642
|
+
await page.locator('[data-testid="manage-view-toggle"]').click();
|
|
643
|
+
await expect(pane(page)).toHaveCount(0);
|
|
644
|
+
await expect(page.locator('[data-node-card]')).toHaveCount(3);
|
|
645
|
+
|
|
646
|
+
await page.goto(`${BASE}/nodes`); // persisted preference: table default, no auto-select
|
|
647
|
+
await expect(page.locator('[data-node-card]')).toHaveCount(3, { timeout: 15000 });
|
|
648
|
+
await page.waitForTimeout(900);
|
|
649
|
+
await expect(pane(page)).toHaveCount(0);
|
|
650
|
+
|
|
651
|
+
await page.locator('[data-testid="manage-view-toggle"]').click(); // 对话视图
|
|
652
|
+
await expect(pane(page)).toBeVisible();
|
|
653
|
+
await expect(page).toHaveURL(/\/nodes\/[^/]+$/);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
test('empty list: no crash, no loop, URL stays /nodes', async ({ page, context }) => {
|
|
657
|
+
await login(context);
|
|
658
|
+
await page.route('**/api/hub/status**', r =>
|
|
659
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ sessions: [] }) }));
|
|
660
|
+
await page.route('**/api/hub/nodes**', r =>
|
|
661
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ nodes: [] }) }));
|
|
662
|
+
await page.route('**/api/hub/health', r =>
|
|
663
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ ok: true, sse_sessions: {} }) }));
|
|
664
|
+
await page.route('**/api/hub/tasks**', r =>
|
|
665
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ tasks: [] }) }));
|
|
666
|
+
await page.route('**/api/hub/stats**', r =>
|
|
667
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({}) }));
|
|
668
|
+
await page.route('**/api/hub/messages**', r =>
|
|
669
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ messages: [] }) }));
|
|
670
|
+
await page.goto(`${BASE}/nodes`);
|
|
671
|
+
await page.waitForTimeout(2500);
|
|
672
|
+
await expect(page).toHaveURL(`${BASE}/nodes`);
|
|
673
|
+
await expect(pane(page)).toHaveCount(0);
|
|
674
|
+
await expect(page.locator("text=This page couldn’t load")).toHaveCount(0);
|
|
675
|
+
});
|
|
676
|
+
});
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { test, expect, Page } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
const BASE = process.env.TEST_URL || 'http://localhost:3100';
|
|
4
|
+
|
|
5
|
+
// 通信龙 07-31 GAP 2nd tier: message status → human labels.
|
|
6
|
+
//
|
|
7
|
+
// This spec asserts BOTH:
|
|
8
|
+
// (A) each known status renders the exact expected Chinese label,
|
|
9
|
+
// NOT the raw English word ("已推送", not "delivered")
|
|
10
|
+
// (B) an unknown status (hub adds a new one; front-end hasn't caught
|
|
11
|
+
// up) is rendered fail-loudly — the raw string is visible AND a
|
|
12
|
+
// "未知状态" banner appears in the hover text
|
|
13
|
+
// (C) failed with `result` renders inline reason (first 40 chars);
|
|
14
|
+
// failed without `result` renders "节点未提供原因". Both branches
|
|
15
|
+
// — because a test that only covers "with reason" would let an
|
|
16
|
+
// empty-result bug fall back to "未知错误" silently.
|
|
17
|
+
//
|
|
18
|
+
// Injection strategy: /api/hub/tasks?to_name=... is the message-list
|
|
19
|
+
// endpoint (grepped from TaskChatPanel.tsx:686). We stub it with an
|
|
20
|
+
// array of tasks, one per status, and read the rendered labels off
|
|
21
|
+
// data-status-key / data-status-label attributes. NO mock of the
|
|
22
|
+
// component itself — the render is the wire under test.
|
|
23
|
+
//
|
|
24
|
+
// Note on `chat-pane` gate: TaskChatPanel is inside the ChatPane
|
|
25
|
+
// column, which mounts on /nodes/<alias>. We goto /nodes/node-a to
|
|
26
|
+
// force ChatPane mount, then feed all the varying-status tasks as
|
|
27
|
+
// to_name=node-a so they all render in that chat.
|
|
28
|
+
|
|
29
|
+
let cachedCookie: string | undefined;
|
|
30
|
+
|
|
31
|
+
async function login(context: Page['context'] extends () => infer C ? C : never) {
|
|
32
|
+
if (cachedCookie) {
|
|
33
|
+
await context.addCookies([{
|
|
34
|
+
name: 'anet_dashboard_session',
|
|
35
|
+
value: cachedCookie,
|
|
36
|
+
domain: new URL(BASE).hostname,
|
|
37
|
+
path: '/',
|
|
38
|
+
}]);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const res = await context.request.post(`${BASE}/api/auth/login`, {
|
|
42
|
+
data: { password: process.env.ANET_DASHBOARD_PASSWORD || 'admin123' },
|
|
43
|
+
});
|
|
44
|
+
const setCookie = res.headers()['set-cookie'] || '';
|
|
45
|
+
const m = setCookie.match(/anet_dashboard_session=([^;]+)/);
|
|
46
|
+
if (m) {
|
|
47
|
+
cachedCookie = decodeURIComponent(m[1]);
|
|
48
|
+
await context.addCookies([{
|
|
49
|
+
name: 'anet_dashboard_session',
|
|
50
|
+
value: cachedCookie,
|
|
51
|
+
domain: new URL(BASE).hostname,
|
|
52
|
+
path: '/',
|
|
53
|
+
}]);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// One-line-per-status expectation table. Duplicated deliberately from
|
|
58
|
+
// the component — a test that reads the label out of STATUS_INFO would
|
|
59
|
+
// prove nothing (it'd assert the map equals itself). These strings ARE
|
|
60
|
+
// the contract 通信龙 signed off on; if they change, this test reds.
|
|
61
|
+
const CASES: Array<{ status: string; label: string; hoverContains: string; known: boolean }> = [
|
|
62
|
+
{ status: 'queued', label: '排队中', hoverContains: '已入队,尚未推给节点', known: true },
|
|
63
|
+
{ status: 'delivered', label: '已推送', hoverContains: 'hub 侧已标记推送', known: true },
|
|
64
|
+
{ status: 'acked', label: '已接收', hoverContains: '未必已处理', known: true },
|
|
65
|
+
{ status: 'replied', label: '已回复', hoverContains: '对方已针对此消息回复', known: true },
|
|
66
|
+
{ status: 'failed', label: '失败', hoverContains: '发送失败', known: true },
|
|
67
|
+
{ status: 'created', label: '发送中', hoverContains: '客户端已建消息', known: true },
|
|
68
|
+
{ status: 'closed', label: '已关闭', hoverContains: '会话已被关闭', known: true },
|
|
69
|
+
{ status: 'expired', label: '已超时', hoverContains: '在期限内未收到回复', known: true },
|
|
70
|
+
{ status: 'cancelled', label: '已取消', hoverContains: '任务被主动取消', known: true },
|
|
71
|
+
{ status: 'nonsense_hub_added_a_new_code', label: 'nonsense_hub_added_a_new_code', hoverContains: '未知状态', known: false },
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
function makeFixtureTasks() {
|
|
75
|
+
return CASES.map((c, i) => ({
|
|
76
|
+
task_id: `t_${c.status}_${i}`,
|
|
77
|
+
from_node_id: 'n_a',
|
|
78
|
+
from_name: 'node-a',
|
|
79
|
+
to_node_id: 'n_a',
|
|
80
|
+
to_name: 'node-a',
|
|
81
|
+
priority: 'normal',
|
|
82
|
+
status: c.status,
|
|
83
|
+
content: `case-${c.status}`,
|
|
84
|
+
result: '',
|
|
85
|
+
in_reply_to: null,
|
|
86
|
+
requires_response: 0,
|
|
87
|
+
scope: 'inbox',
|
|
88
|
+
created_at: `2026-07-31T10:0${i}:00`,
|
|
89
|
+
delivered_at: `2026-07-31T10:0${i}:00`,
|
|
90
|
+
started_at: null,
|
|
91
|
+
completed_at: null,
|
|
92
|
+
expires_at: null,
|
|
93
|
+
meta_json: null,
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function stubHub(page: Page, tasks: object[]) {
|
|
98
|
+
await page.route('**/api/hub/status**', r =>
|
|
99
|
+
r.fulfill({ status: 200, contentType: 'application/json',
|
|
100
|
+
body: JSON.stringify({ sessions: [{ alias: 'node-a', status: 'idle', network_id: 'net_x' }] }) }));
|
|
101
|
+
await page.route('**/api/hub/nodes**', r =>
|
|
102
|
+
r.fulfill({ status: 200, contentType: 'application/json',
|
|
103
|
+
body: JSON.stringify({ nodes: [{ alias: 'node-a', node_id: 'n_a', team: null, tags: [], attrs_revision: 0 }] }) }));
|
|
104
|
+
await page.route('**/api/hub/networks**', r =>
|
|
105
|
+
r.fulfill({ status: 200, contentType: 'application/json',
|
|
106
|
+
body: JSON.stringify({ networks: [{ network_id: 'net_x', network_name: 'net_x' }] }) }));
|
|
107
|
+
await page.route('**/api/hub/health', r =>
|
|
108
|
+
r.fulfill({ status: 200, contentType: 'application/json',
|
|
109
|
+
body: JSON.stringify({ ok: true, version: '0.9.0-test', sse_connections: 1, sse_sessions: { 'net_x:node-a': 1 } }) }));
|
|
110
|
+
await page.route('**/api/hub/stats**', r =>
|
|
111
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({}) }));
|
|
112
|
+
await page.route('**/api/hub/messages**', r =>
|
|
113
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ messages: [] }) }));
|
|
114
|
+
await page.route('**/api/hub/events**', r =>
|
|
115
|
+
r.fulfill({ status: 200, contentType: 'text/event-stream', body: '' }));
|
|
116
|
+
// /api/hub/tasks — TaskChatPanel:686 uses this for the message list.
|
|
117
|
+
// Return our fixture regardless of query args (to_name, task_id, etc).
|
|
118
|
+
await page.route('**/api/hub/tasks**', r =>
|
|
119
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ tasks }) }));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
test.describe('Status labels — per-status render + unknown fail-loudly + failed both branches', () => {
|
|
123
|
+
test('every known status renders its exact Chinese label with matching hover text', async ({ page, context }) => {
|
|
124
|
+
await login(context);
|
|
125
|
+
await stubHub(page, makeFixtureTasks());
|
|
126
|
+
await page.goto(`${BASE}/nodes/node-a`);
|
|
127
|
+
await expect(page.locator('[data-testid="chat-pane"]').first()).toBeVisible({ timeout: 15000 });
|
|
128
|
+
|
|
129
|
+
// Premise: at least CASES.length StatusBars rendered — else we'd
|
|
130
|
+
// be asserting into an empty page (see feedback_checker_scope_bug_vacuous_pass).
|
|
131
|
+
const anyStatusBar = page.locator('[data-status-key]');
|
|
132
|
+
await expect(anyStatusBar.first()).toBeVisible({ timeout: 10000 });
|
|
133
|
+
const rendered = await anyStatusBar.count();
|
|
134
|
+
expect(rendered, `expected at least ${CASES.length} StatusBar renders (one per case), got ${rendered}`)
|
|
135
|
+
.toBeGreaterThanOrEqual(CASES.length);
|
|
136
|
+
|
|
137
|
+
// Now assert each case individually. Locate by data-status-key
|
|
138
|
+
// (unique per status in our fixture).
|
|
139
|
+
for (const c of CASES) {
|
|
140
|
+
const bar = page.locator(`[data-status-key="${c.status}"]`).first();
|
|
141
|
+
await expect(bar).toBeVisible();
|
|
142
|
+
|
|
143
|
+
// (A) label text is the exact Chinese string (or raw for unknown).
|
|
144
|
+
const labelAttr = await bar.getAttribute('data-status-label');
|
|
145
|
+
expect(labelAttr, `${c.status} label attr`).toBe(c.label);
|
|
146
|
+
|
|
147
|
+
const visibleText = await bar.locator('span').first().textContent();
|
|
148
|
+
expect(visibleText?.trim(), `${c.status} rendered text`).toContain(c.label);
|
|
149
|
+
|
|
150
|
+
// (A cont.) known/unknown attr matches expectation.
|
|
151
|
+
const knownAttr = await bar.getAttribute('data-status-known');
|
|
152
|
+
expect(knownAttr, `${c.status} known attr`).toBe(c.known ? 'true' : 'false');
|
|
153
|
+
|
|
154
|
+
// (B) hover title contains the semantic-specific string.
|
|
155
|
+
const titleAttr = await bar.locator('span[title]').first().getAttribute('title');
|
|
156
|
+
expect(titleAttr ?? '', `${c.status} hover text`).toContain(c.hoverContains);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('failed WITH result renders inline reason (first 40 chars)', async ({ page, context }) => {
|
|
161
|
+
await login(context);
|
|
162
|
+
const REASON = 'codex-sdk 返回空响应 (in=15806 out=19) — 疑似 vendor 静默限流/配额';
|
|
163
|
+
const tasks = [{
|
|
164
|
+
task_id: 't_failed_with_reason', from_node_id: 'n_a', from_name: 'node-a',
|
|
165
|
+
to_node_id: 'n_a', to_name: 'node-a', priority: 'normal', status: 'failed',
|
|
166
|
+
content: 'failed case', result: REASON, in_reply_to: null,
|
|
167
|
+
requires_response: 0, scope: 'inbox',
|
|
168
|
+
created_at: '2026-07-31T10:00:00', delivered_at: '2026-07-31T10:00:00',
|
|
169
|
+
started_at: null, completed_at: null, expires_at: null, meta_json: null,
|
|
170
|
+
}];
|
|
171
|
+
await stubHub(page, tasks);
|
|
172
|
+
await page.goto(`${BASE}/nodes/node-a`);
|
|
173
|
+
await expect(page.locator('[data-testid="chat-pane"]').first()).toBeVisible({ timeout: 15000 });
|
|
174
|
+
|
|
175
|
+
const bar = page.locator('[data-status-key="failed"]').first();
|
|
176
|
+
await expect(bar).toBeVisible({ timeout: 10000 });
|
|
177
|
+
|
|
178
|
+
// Premise: `result` on the fixture is a specific non-empty string.
|
|
179
|
+
// If our stub fails to reach the component, the reason element
|
|
180
|
+
// would still exist with '节点未提供原因' — that'd look like a
|
|
181
|
+
// "wrong branch chosen" bug rather than "fixture never landed".
|
|
182
|
+
// Asserting the exact first-40 slice catches both.
|
|
183
|
+
const reason = bar.locator('[data-status-reason]');
|
|
184
|
+
await expect(reason).toBeVisible();
|
|
185
|
+
const reasonAttr = await reason.getAttribute('data-status-reason');
|
|
186
|
+
const expected40 = REASON.slice(0, 40);
|
|
187
|
+
expect(reasonAttr, `first-40 slice of "${REASON}"`).toBe(expected40);
|
|
188
|
+
// Visible text mirrors the attribute (truncated via CSS if wider).
|
|
189
|
+
expect(await reason.textContent()).toContain(expected40);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('failed WITHOUT result renders "节点未提供原因" (not silently blank, not "未知错误")', async ({ page, context }) => {
|
|
193
|
+
await login(context);
|
|
194
|
+
const tasks = [{
|
|
195
|
+
task_id: 't_failed_no_reason', from_node_id: 'n_a', from_name: 'node-a',
|
|
196
|
+
to_node_id: 'n_a', to_name: 'node-a', priority: 'normal', status: 'failed',
|
|
197
|
+
content: 'failed no reason', result: '', // ← explicit empty
|
|
198
|
+
in_reply_to: null, requires_response: 0, scope: 'inbox',
|
|
199
|
+
created_at: '2026-07-31T10:00:00', delivered_at: '2026-07-31T10:00:00',
|
|
200
|
+
started_at: null, completed_at: null, expires_at: null, meta_json: null,
|
|
201
|
+
}];
|
|
202
|
+
await stubHub(page, tasks);
|
|
203
|
+
await page.goto(`${BASE}/nodes/node-a`);
|
|
204
|
+
await expect(page.locator('[data-testid="chat-pane"]').first()).toBeVisible({ timeout: 15000 });
|
|
205
|
+
|
|
206
|
+
const bar = page.locator('[data-status-key="failed"]').first();
|
|
207
|
+
await expect(bar).toBeVisible({ timeout: 10000 });
|
|
208
|
+
|
|
209
|
+
const reason = bar.locator('[data-status-reason]');
|
|
210
|
+
await expect(reason).toBeVisible();
|
|
211
|
+
const reasonAttr = await reason.getAttribute('data-status-reason');
|
|
212
|
+
expect(reasonAttr).toBe('节点未提供原因');
|
|
213
|
+
// Specifically NOT the "未知错误" placeholder — that would obscure
|
|
214
|
+
// the distinction between "hub has no reason field" and "reason
|
|
215
|
+
// exists but was bad", which is the whole point of separating them.
|
|
216
|
+
expect(reasonAttr).not.toContain('未知错误');
|
|
217
|
+
});
|
|
218
|
+
});
|