@sleep2agi/commhub-server 0.9.0-preview.0 → 0.9.0-preview.100
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/README.md +130 -10
- package/bin/commhub.ts +92 -11
- package/package.json +8 -7
- package/src/ack-create-request-transport.test.ts +159 -0
- package/src/ack-create-request.test.ts +331 -0
- package/src/admin-delete-network-http.test.ts +228 -0
- package/src/admin-networks-http.test.ts +88 -0
- package/src/agent-access-batch.test.ts +210 -0
- package/src/agent-access.ts +487 -0
- package/src/agent-acl-groups-http.test.ts +247 -0
- package/src/agent-acl-http.test.ts +520 -0
- package/src/agent-acl-slice3-http.test.ts +267 -0
- package/src/agent-groups-perf.test.ts +190 -0
- package/src/agent-notice.ts +48 -0
- package/src/alias-filter.test.ts +72 -0
- package/src/alias-filter.ts +42 -0
- package/src/api-host-supervisors-can-create-nodes.test.ts +350 -0
- package/src/api-host-supervisors-fallback.test.ts +271 -0
- package/src/api-nodes-shape.test.ts +188 -0
- package/src/auth-kdf-migration.test.ts +378 -0
- package/src/auth-sessions-http.test.ts +250 -0
- package/src/auth-tokens.test.ts +18 -9
- package/src/auth-validate.test.ts +5 -1
- package/src/auth.ts +554 -38
- package/src/avatar-validate.test.ts +163 -0
- package/src/avatar-validate.ts +103 -0
- package/src/blocked-exit-on-reply.test.ts +122 -0
- package/src/boot-side-effect.test.ts +124 -0
- package/src/commhub-bin-args.test.ts +85 -0
- package/src/config-apply-sec1.test.ts +830 -0
- package/src/config-apply-validate.test.ts +352 -0
- package/src/config-apply-validate.ts +249 -0
- package/src/create-node-blocked-gate.test.ts +356 -0
- package/src/create-node-tool-schema.test.ts +39 -0
- package/src/create-node-validate.test.ts +275 -0
- package/src/create-node-validate.ts +288 -0
- package/src/create-node-workdir.test.ts +255 -0
- package/src/create-node.test.ts +354 -0
- package/src/create-node.ts +349 -0
- package/src/cross-tenant-injection.test.ts +110 -0
- package/src/dashboard-slash-routing-http.test.ts +107 -0
- package/src/db-adapter-guard.test.ts +148 -0
- package/src/db-adapter-translate.test.ts +128 -0
- package/src/db-adapter.ts +323 -87
- package/src/db-timestamp.test.ts +38 -0
- package/src/db-timestamp.ts +15 -0
- package/src/db.ts +1268 -42
- package/src/delivered-without-push.test.ts +140 -0
- package/src/department-groups-http.test.ts +209 -0
- package/src/department-groups-sync-http.test.ts +328 -0
- package/src/department-groups.ts +434 -0
- package/src/department-heads-http.test.ts +329 -0
- package/src/department-heads.ts +226 -0
- package/src/departments-http.test.ts +185 -0
- package/src/departments.ts +260 -0
- package/src/desktop-message-seam.test.ts +150 -0
- package/src/desktop-message.test.ts +368 -0
- package/src/dm-file-scope-http.test.ts +178 -0
- package/src/ext-token-shared.test.ts +60 -0
- package/src/ext-validation-regression.test.ts +131 -0
- package/src/external-schedule-edits-http.test.ts +321 -0
- package/src/external-schedule-edits-timestamps.test.ts +64 -0
- package/src/external-schedule-edits.ts +327 -0
- package/src/file-download-authz-dev-open.test.ts +135 -0
- package/src/file-download-authz.test.ts +404 -0
- package/src/file-download-date-bucket.test.ts +436 -0
- package/src/file-network-scope-dev-open.test.ts +97 -0
- package/src/file-network-scope.test.ts +732 -0
- package/src/group-membership-events-http.test.ts +264 -0
- package/src/group-messages-http.test.ts +482 -0
- package/src/group-messages.ts +297 -0
- package/src/health-redaction.test.ts +642 -0
- package/src/http-gzip.test.ts +94 -0
- package/src/http-gzip.ts +112 -0
- package/src/hub-timestamp.test.ts +38 -0
- package/src/hub-timestamp.ts +27 -0
- package/src/human-dm.ts +215 -0
- package/src/inbox-count-sse.test.ts +310 -0
- package/src/inbox-count.ts +29 -0
- package/src/index.ts +15 -2128
- package/src/lifecycle-guard.ts +50 -0
- package/src/list-host-supervisors.test.ts +314 -0
- package/src/mcp-context-cost-http.test.ts +119 -0
- package/src/mcp-projects-events-http.test.ts +162 -0
- package/src/mcp-route-label-http.test.ts +120 -0
- package/src/mcp-write-network-resolution.test.ts +429 -0
- package/src/member-audit-network-id-http.test.ts +93 -0
- package/src/member-presence-http.test.ts +206 -0
- package/src/messages-ack-by-agent-http.test.ts +142 -0
- package/src/migrate-sqlite-to-pg.test.ts +47 -0
- package/src/migrate-sqlite-to-pg.ts +263 -0
- package/src/model-auth-notify-http.test.ts +181 -0
- package/src/model-auth-notify.ts +116 -0
- package/src/network-name-http.test.ts +75 -0
- package/src/network-scope.ts +224 -0
- package/src/network-token-membership-http.test.ts +139 -0
- package/src/node-attrs-validate.ts +88 -0
- package/src/node-attrs.test.ts +217 -0
- package/src/node-create-request-status-http.test.ts +92 -0
- package/src/node-files-transport.test.ts +344 -0
- package/src/node-health-dispatch-http.test.ts +279 -0
- package/src/node-health-guard.ts +105 -0
- package/src/node-health-passthrough.test.ts +115 -0
- package/src/node-health-store.ts +72 -0
- package/src/node-id-alias.test.ts +188 -0
- package/src/node-identity-conflict-http.test.ts +269 -0
- package/src/node-identity-conflict.ts +142 -0
- package/src/node-identity-mismatch.test.ts +74 -0
- package/src/node-lifecycle-controllable.test.ts +64 -0
- package/src/node-lifecycle-controllable.ts +51 -0
- package/src/node-logs-transport.test.ts +263 -0
- package/src/node-permissions-http.test.ts +276 -0
- package/src/node-permissions.ts +242 -0
- package/src/node-request-retention.test.ts +281 -0
- package/src/node-request-retention.ts +120 -0
- package/src/node-skills-transport.test.ts +209 -0
- package/src/node-token-privilege-http.test.ts +149 -0
- package/src/observer-avatar-http.test.ts +431 -0
- package/src/observer-push.test.ts +252 -0
- package/src/package-version-consistency.test.ts +25 -0
- package/src/peer-reply-atomic.test.ts +455 -0
- package/src/peer-reply-capability.test.ts +70 -0
- package/src/pg-worker.ts +87 -0
- package/src/probe-validate.test.ts +221 -0
- package/src/probe-validate.ts +104 -0
- package/src/probe.test.ts +80 -0
- package/src/probe.ts +200 -0
- package/src/project-viewer-can-http.test.ts +114 -0
- package/src/push.ts +429 -10
- package/src/redact-tokens.test.ts +65 -0
- package/src/redact-tokens.ts +40 -0
- package/src/rename.ts +7 -2
- package/src/reply-target-derivation.test.ts +96 -0
- package/src/report-status-host-schema.test.ts +211 -0
- package/src/report-status-resume-id-handover.test.ts +150 -0
- package/src/require-explicit-test-db.ts +9 -0
- package/src/requirement-due-reminders-http.test.ts +390 -0
- package/src/requirement-due-reminders.ts +310 -0
- package/src/requirement-events-http.test.ts +257 -0
- package/src/requirement-events.test.ts +45 -0
- package/src/requirement-events.ts +144 -0
- package/src/requirement-notify.ts +165 -0
- package/src/requirement-participant-notify-http.test.ts +309 -0
- package/src/requirement-tag-ops-http.test.ts +266 -0
- package/src/requirement-tags.ts +89 -0
- package/src/requirements-comment-http.test.ts +165 -0
- package/src/requirements-completed-at-migration.test.ts +70 -0
- package/src/requirements-errors-mcp-http.test.ts +164 -0
- package/src/requirements-errors.ts +143 -0
- package/src/requirements-etag-http.test.ts +91 -0
- package/src/requirements-http.test.ts +709 -0
- package/src/requirements-last-event-http.test.ts +344 -0
- package/src/requirements-list-mcp-slim-http.test.ts +155 -0
- package/src/requirements-list-slim-http.test.ts +319 -0
- package/src/requirements-mcp.test.ts +163 -0
- package/src/requirements-migrate.ts +225 -0
- package/src/requirements-owner-strict-mcp-http.test.ts +145 -0
- package/src/requirements-people-http.test.ts +114 -0
- package/src/requirements-people-mcp-http.test.ts +225 -0
- package/src/requirements-people.ts +127 -0
- package/src/requirements-priority-lowest.test.ts +114 -0
- package/src/requirements-search-http.test.ts +182 -0
- package/src/requirements-search.test.ts +32 -0
- package/src/requirements-search.ts +145 -0
- package/src/requirements-seq-http.test.ts +193 -0
- package/src/requirements-seq-migration.test.ts +63 -0
- package/src/requirements-start-date.test.ts +125 -0
- package/src/requirements-stats-http.test.ts +288 -0
- package/src/requirements-stats.ts +164 -0
- package/src/requirements-status-participants-http.test.ts +220 -0
- package/src/requirements-upgrade-e2e.test.ts +126 -0
- package/src/requirements-upgrade-migration.test.ts +98 -0
- package/src/requirements.ts +1761 -0
- package/src/resolve-port.test.ts +57 -0
- package/src/resolve-port.ts +44 -0
- package/src/response-charset.test.ts +130 -0
- package/src/rest-explicit-columns-http.test.ts +222 -0
- package/src/rest-identity.test.ts +129 -0
- package/src/rest-identity.ts +100 -0
- package/src/rest-projections.ts +68 -0
- package/src/rest-write-network-resolution.test.ts +133 -0
- package/src/restricted-files.ts +73 -0
- package/src/retention.test.ts +450 -0
- package/src/retention.ts +281 -0
- package/src/route-callers-http.test.ts +120 -0
- package/src/route-timing.test.ts +211 -0
- package/src/route-timing.ts +258 -0
- package/src/rules-file-duplicate-alias.test.ts +234 -0
- package/src/rules-file-session-target.test.ts +207 -0
- package/src/rules-file-transport.test.ts +237 -0
- package/src/runtime-label.test.ts +22 -0
- package/src/runtime-label.ts +21 -0
- package/src/scheduled-doorbell-nodeless-session.test.ts +130 -0
- package/src/scheduled-reply-unread-http.test.ts +212 -0
- package/src/scheduled-run-terminal.test.ts +268 -0
- package/src/scheduled-skip-blocking-task.test.ts +120 -0
- package/src/scheduled-stuck-http.test.ts +238 -0
- package/src/scheduled-stuck.ts +153 -0
- package/src/scheduled-tasks-http.test.ts +638 -0
- package/src/scheduled-tasks.ts +641 -0
- package/src/send-dedup-request-id.test.ts +229 -0
- package/src/send-reply-agent-warning.test.ts +202 -0
- package/src/send-reply-alias-optional.test.ts +131 -0
- package/src/send-reply-attachments.test.ts +458 -0
- package/src/send_dedup.test.ts +54 -0
- package/src/send_dedup.ts +52 -6
- package/src/serve-error.test.ts +38 -0
- package/src/serve-error.ts +27 -0
- package/src/server.ts +5038 -0
- package/src/shared/external-schedule-contract.ts +60 -0
- package/src/shared/probe-host-allowlist-drift.test.ts +54 -0
- package/src/shared/probe-host-allowlist.ts +128 -0
- package/src/shared/reserved-env-drift.test.ts +43 -0
- package/src/shared/reserved-env.test.ts +35 -0
- package/src/shared/reserved-env.ts +31 -0
- package/src/side-thread-command-transport.test.ts +312 -0
- package/src/side-thread-command-transport.ts +318 -0
- package/src/side-thread-http-integration.test.ts +233 -0
- package/src/side-thread-http.ts +504 -0
- package/src/side-thread-production.test.ts +72 -0
- package/src/side-thread-production.ts +118 -0
- package/src/side-thread.test.ts +1067 -0
- package/src/side-thread.ts +2499 -0
- package/src/skillhub-http.test.ts +108 -0
- package/src/sqlite-close-checkpoint.test.ts +78 -0
- package/src/stale-sweeper.test.ts +181 -0
- package/src/stale-sweeper.ts +114 -0
- package/src/start-node.test.ts +109 -0
- package/src/status-alias-filter-http.test.ts +106 -0
- package/src/status-alias-resolver-http.test.ts +163 -0
- package/src/status-alias-resolver.ts +19 -0
- package/src/status-node-id-filter-http.test.ts +161 -0
- package/src/status-read-cache-http.test.ts +229 -0
- package/src/status-read-cache.ts +122 -0
- package/src/stop-delete-node.test.ts +1164 -0
- package/src/task-access-http.test.ts +379 -0
- package/src/task-access.ts +413 -0
- package/src/task-auth-origin-http.test.ts +91 -0
- package/src/task-auth-origin.test.ts +24 -0
- package/src/task-auth-origin.ts +7 -0
- package/src/task-consumption.test.ts +482 -0
- package/src/task-diagnostic-http.test.ts +149 -0
- package/src/task-diagnostic.test.ts +115 -0
- package/src/task-diagnostic.ts +118 -0
- package/src/task-expiry-notice-http.test.ts +287 -0
- package/src/task-expiry-notice.ts +313 -0
- package/src/task-idempotency-mcp.test.ts +142 -0
- package/src/task-idempotency.test.ts +60 -0
- package/src/task-idempotency.ts +75 -0
- package/src/task-lifecycle-watcher.test.ts +204 -0
- package/src/task-lifecycle-watcher.ts +49 -0
- package/src/task-network-resolution-http.test.ts +141 -0
- package/src/task-queue-ahead-http.test.ts +309 -0
- package/src/task-queue-ahead-scale.test.ts +211 -0
- package/src/task-queue-ahead.ts +186 -0
- package/src/task-stale-open-http.test.ts +290 -0
- package/src/task-stale-open.ts +103 -0
- package/src/tasks-pagination-http.test.ts +141 -0
- package/src/tool-audience-http.test.ts +195 -0
- package/src/tool-audience.ts +109 -0
- package/src/tools-list-json-schema.test.ts +67 -0
- package/src/tools.ts +5426 -275
- package/src/update-provider.test.ts +234 -0
- package/src/upload-early-reject-guard.test.ts +155 -0
- package/src/uploads-http.test.ts +97 -15
- package/src/uploads.test.ts +66 -0
- package/src/uploads.ts +138 -5
- package/src/user-inbox-by-agent-http.test.ts +161 -0
- package/src/user-inbox-netid-notnull.test.ts +122 -0
- package/src/user-inbox-read-http.test.ts +223 -0
- package/src/user-inbox-schema.test.ts +54 -0
- package/src/vault.test.ts +200 -0
- package/src/vault.ts +156 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// GET /api/status?alias=<name> — one agent's rows instead of the whole network.
|
|
2
|
+
// The app's chat info / node detail read the FULL projection for a single agent; unfiltered that
|
|
3
|
+
// was every session (533 KB raw / 94 KB gzip on the production network) per open and per 10 s poll.
|
|
4
|
+
// Old hubs ignore the parameter, so the app keeps picking its row by alias; this suite pins that
|
|
5
|
+
// the filter narrows, never widens (network scope still applies), and that no/blank alias is the
|
|
6
|
+
// old response.
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test, beforeAll, afterAll } from "bun:test";
|
|
9
|
+
import { mkdtempSync, rmSync } from "fs";
|
|
10
|
+
import { join } from "path";
|
|
11
|
+
import { tmpdir } from "os";
|
|
12
|
+
import { register } from "./auth.js";
|
|
13
|
+
import { db } from "./db.js";
|
|
14
|
+
|
|
15
|
+
const DIR = mkdtempSync(join(tmpdir(), "anet-status-alias-"));
|
|
16
|
+
let BASE = "";
|
|
17
|
+
let hub: any = null;
|
|
18
|
+
const PW = "StatusAliasPassw0rd!";
|
|
19
|
+
let aToken = "", aId = "", NET_A = "";
|
|
20
|
+
let bToken = "", bId = "", NET_B = "";
|
|
21
|
+
|
|
22
|
+
async function get(token: string, path: string) {
|
|
23
|
+
const res = await fetch(`${BASE}${path}`, { headers: { Authorization: `Bearer ${token}` } });
|
|
24
|
+
return { status: res.status, body: await res.json().catch(() => null) as any };
|
|
25
|
+
}
|
|
26
|
+
function seed(alias: string, node: string, net: string, owner: string, minutesAgo: number) {
|
|
27
|
+
db.run(`INSERT INTO nodes (node_id, node_name, alias, network_id, owner_user_id) VALUES (?1, ?2, ?2, ?3, ?4)`, [node, alias, net, owner]);
|
|
28
|
+
db.run(
|
|
29
|
+
`INSERT INTO sessions (resume_id, alias, node_id, status, network_id, task, updated_at, last_seen_at) VALUES (?1, ?2, ?3, 'idle', ?4, ?5, datetime('now', ?6), datetime('now'))`,
|
|
30
|
+
[`r_${node}`, alias, node, net, `task of ${alias}`, `-${minutesAgo} minutes`],
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
const aliases = (rows: any[]) => rows.map(r => r.alias);
|
|
34
|
+
|
|
35
|
+
beforeAll(async () => {
|
|
36
|
+
process.env.COMMHUB_DB ||= join(DIR, "hub.db");
|
|
37
|
+
process.env.COMMHUB_UPLOADS_DIR = join(DIR, "uploads");
|
|
38
|
+
process.env.HOST = "127.0.0.1";
|
|
39
|
+
const a = register(`sa_owner_a_${Date.now()}`, PW);
|
|
40
|
+
aToken = a.token!; aId = a.user!.user_id; NET_A = a.network_id!;
|
|
41
|
+
const b = register(`sa_owner_b_${Date.now()}`, PW);
|
|
42
|
+
bToken = b.token!; bId = b.user!.user_id; NET_B = b.network_id!;
|
|
43
|
+
seed("sa-one", "node_sa_one", NET_A, aId, 1);
|
|
44
|
+
seed("sa-two", "node_sa_two", NET_A, aId, 2);
|
|
45
|
+
seed("sa-three", "node_sa_three", NET_A, aId, 3);
|
|
46
|
+
// Same alias in someone else's network: the filter must not reach it.
|
|
47
|
+
seed("sa-two", "node_sa_two_b", NET_B, bId, 0);
|
|
48
|
+
const mod: any = await import("./server.js");
|
|
49
|
+
hub = mod.bootServer({ port: 0, hostname: "127.0.0.1" });
|
|
50
|
+
BASE = `http://127.0.0.1:${hub.port}`;
|
|
51
|
+
}, 30_000);
|
|
52
|
+
|
|
53
|
+
afterAll(() => {
|
|
54
|
+
try { hub?.stop?.(true); } catch {}
|
|
55
|
+
try { rmSync(DIR, { recursive: true, force: true }); } catch {}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("/api/status?alias=", () => {
|
|
59
|
+
test("no alias: every session in scope, newest first (unchanged)", async () => {
|
|
60
|
+
const r = await get(aToken, `/api/status?network_id=${NET_A}`);
|
|
61
|
+
expect(r.status).toBe(200);
|
|
62
|
+
expect(aliases(r.body.sessions)).toEqual(["sa-one", "sa-two", "sa-three"]);
|
|
63
|
+
expect(r.body.summary.total).toBe(3);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("alias narrows the full projection to that agent", async () => {
|
|
67
|
+
const r = await get(aToken, `/api/status?network_id=${NET_A}&alias=sa-two`);
|
|
68
|
+
expect(r.status).toBe(200);
|
|
69
|
+
expect(aliases(r.body.sessions)).toEqual(["sa-two"]);
|
|
70
|
+
expect(r.body.sessions[0].node_id).toBe("node_sa_two");
|
|
71
|
+
// still the full projection (capability bits / host block), not the light one
|
|
72
|
+
expect(typeof r.body.sessions[0].rules_file_capable).toBe("boolean");
|
|
73
|
+
expect(r.body.sessions[0].host).toBeDefined();
|
|
74
|
+
expect(r.body.summary.total).toBe(1);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("alias narrows the light projection too", async () => {
|
|
78
|
+
const r = await get(aToken, `/api/status?light=1&network_id=${NET_A}&alias=sa-three`);
|
|
79
|
+
expect(aliases(r.body.sessions)).toEqual(["sa-three"]);
|
|
80
|
+
expect(r.body.sessions[0].host).toBeUndefined();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("the filter never widens scope: same alias in another network stays invisible", async () => {
|
|
84
|
+
const scoped = await get(aToken, `/api/status?network_id=${NET_A}&alias=sa-two`);
|
|
85
|
+
expect(scoped.body.sessions.map((s: any) => s.node_id)).toEqual(["node_sa_two"]);
|
|
86
|
+
// Unscoped: whatever the caller's scope already shows (the first registered user is an admin
|
|
87
|
+
// here, so that is both networks) — the filter only picks rows out of it, never adds any.
|
|
88
|
+
const all = await get(aToken, `/api/status`);
|
|
89
|
+
const unscoped = await get(aToken, `/api/status?alias=sa-two`);
|
|
90
|
+
expect(unscoped.body.sessions.map((s: any) => s.node_id)).toEqual(all.body.sessions.filter((s: any) => s.alias === "sa-two").map((s: any) => s.node_id));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("a non-admin member only ever gets their own network's row for a shared alias", async () => {
|
|
94
|
+
const r = await get(bToken, `/api/status?alias=sa-two`);
|
|
95
|
+
expect(r.body.sessions.map((s: any) => s.node_id)).toEqual(["node_sa_two_b"]);
|
|
96
|
+
const other = await get(bToken, `/api/status?alias=sa-one`);
|
|
97
|
+
expect(other.body.sessions).toEqual([]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("blank alias is ignored; unknown alias is an empty list, not an error", async () => {
|
|
101
|
+
expect(aliases((await get(aToken, `/api/status?network_id=${NET_A}&alias=%20`)).body.sessions)).toEqual(["sa-one", "sa-two", "sa-three"]);
|
|
102
|
+
const none = await get(aToken, `/api/status?network_id=${NET_A}&alias=nobody`);
|
|
103
|
+
expect(none.status).toBe(200);
|
|
104
|
+
expect(none.body.sessions).toEqual([]);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// 旧版 agent-node(< 2.5.0-preview.93)找自己别名的全量 /api/status 读法 → light 投影 + node_id。
|
|
2
|
+
//
|
|
3
|
+
// 背景(2026-10-01,app「连接较慢」#431):生产上约 2.3 次/秒的全量状态读(~93 KB gzip)几乎都来自旧节点的
|
|
4
|
+
// CurrentAliasResolver,它只读 sessions[].node_id / .alias,且 2.5 s 硬超时。经 RELAY frp 隧道到国内机器时常常
|
|
5
|
+
// 读不完就中止,Hub 已经推进隧道的字节被丢掉(约占隧道 60%)。这里钉住:
|
|
6
|
+
// - 签名只认「节点令牌 + Accept 恰好 application/json + 只有 network_id」(statusAliasResolverRead);
|
|
7
|
+
// - 命中时回 light + node_id,旧解析器照旧解析出别名(含改名之后);
|
|
8
|
+
// - 不命中的(用户令牌 / CLI 不带 Accept / 浏览器 */* / light / node_id / alias / full=1 / 多值 Accept)字节不变;
|
|
9
|
+
// - 量体积:同一批行,完整投影 vs 这条读法的 gzip 字节。
|
|
10
|
+
|
|
11
|
+
import { describe, expect, test, beforeAll, afterAll } from "bun:test";
|
|
12
|
+
import { mkdtempSync, rmSync } from "fs";
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
import { tmpdir } from "os";
|
|
15
|
+
import { gzipSync } from "zlib";
|
|
16
|
+
import { register, createNetworkTokenForNode } from "./auth.js";
|
|
17
|
+
import { db } from "./db.js";
|
|
18
|
+
import { statusAliasResolverRead } from "./status-alias-resolver.js";
|
|
19
|
+
|
|
20
|
+
const DIR = mkdtempSync(join(tmpdir(), "anet-status-alias-resolver-"));
|
|
21
|
+
let BASE = "";
|
|
22
|
+
let hub: any = null;
|
|
23
|
+
const PW = "StatusAliasResolverPassw0rd!";
|
|
24
|
+
let userToken = "", userId = "", NET = "";
|
|
25
|
+
let nodeToken = "";
|
|
26
|
+
const ROWS = 120;
|
|
27
|
+
|
|
28
|
+
function seed(i: number) {
|
|
29
|
+
const node = `node_sar_${i}`, alias = `sar-${i}`;
|
|
30
|
+
db.run(`INSERT INTO nodes (node_id, node_name, alias, network_id, owner_user_id) VALUES (?1, ?2, ?2, ?3, ?4)`, [node, alias, NET, userId]);
|
|
31
|
+
db.run(
|
|
32
|
+
`INSERT INTO sessions (resume_id, alias, node_id, status, network_id, task, output, project_dir, config_path, hostname, ip, server, agent, version,
|
|
33
|
+
cpu_load_1min, cpu_cores, mem_total_gb, mem_used_gb, mem_avail_gb, disk_total_gb, disk_used_gb, disk_avail_gb,
|
|
34
|
+
process_rss_bytes, process_rss_mb, process_cpu_pct, process_uptime_seconds, process_in_flight_count, updated_at, last_seen_at, registered_at)
|
|
35
|
+
VALUES (?1, ?2, ?3, 'idle', ?4, ?5, ?6, ?7, ?8, ?9, '10.0.0.' || ?10, ?9, 'agent-node:codex-app-server', '2.5.0-preview.88',
|
|
36
|
+
'1.5', '8', '61.4', '26.9', '34.5', '491.8', '442.2', '29.4', 121868288, 116.2, 0.2, 292866, 0,
|
|
37
|
+
datetime('now', '-' || ?10 || ' seconds'), datetime('now'), datetime('now', '-30 days'))`,
|
|
38
|
+
[`r_${node}`, alias, node, NET, `示例任务 ${i}:`.repeat(12), `示例输出 ${i} `.repeat(20), `/home/user/project-${i}`,
|
|
39
|
+
`/home/user/project-${i}/.anet/nodes/${alias}/config.json`, `sample-host-${i % 4}`, i],
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function req(path: string, headers: Record<string, string>) {
|
|
44
|
+
const res = await fetch(`${BASE}${path}`, { headers: { "Accept-Encoding": "gzip", ...headers } });
|
|
45
|
+
const text = await res.text();
|
|
46
|
+
return { status: res.status, text, body: JSON.parse(text) as any, projection: res.headers.get("x-status-projection") };
|
|
47
|
+
}
|
|
48
|
+
const asNode = (accept?: string) => ({ Authorization: `Bearer ${nodeToken}`, ...(accept !== undefined ? { Accept: accept } : {}) });
|
|
49
|
+
const gz = (s: string) => gzipSync(Buffer.from(s)).length;
|
|
50
|
+
|
|
51
|
+
// The exact call agent-node 2.5.0-preview.32 … .90 makes (dist/cli.js CurrentAliasResolver.fetchCanonicalAlias).
|
|
52
|
+
async function oldResolver(nodeId: string, token: string, networkId: string): Promise<string | null> {
|
|
53
|
+
try {
|
|
54
|
+
const headers: Record<string, string> = { Accept: "application/json" };
|
|
55
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
56
|
+
const url = `${BASE}/api/status${networkId ? `?network_id=${encodeURIComponent(networkId)}` : ""}`;
|
|
57
|
+
const ctl = new AbortController();
|
|
58
|
+
const timer = setTimeout(() => ctl.abort(), 2500);
|
|
59
|
+
try {
|
|
60
|
+
const res = await fetch(url, { headers, signal: ctl.signal });
|
|
61
|
+
if (!res.ok) return null;
|
|
62
|
+
const body = (await res.json()) as { sessions?: Array<{ node_id?: string; alias?: string }> };
|
|
63
|
+
return body.sessions?.find((s) => s.node_id === nodeId)?.alias ?? null;
|
|
64
|
+
} finally { clearTimeout(timer); }
|
|
65
|
+
} catch { return null; }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
beforeAll(async () => {
|
|
69
|
+
process.env.COMMHUB_DB ||= join(DIR, "hub.db");
|
|
70
|
+
process.env.COMMHUB_UPLOADS_DIR = join(DIR, "uploads");
|
|
71
|
+
process.env.HOST = "127.0.0.1";
|
|
72
|
+
const a = register(`sar_owner_${Date.now()}`, PW);
|
|
73
|
+
userToken = a.token!; userId = a.user!.user_id; NET = a.network_id!;
|
|
74
|
+
for (let i = 0; i < ROWS; i++) seed(i);
|
|
75
|
+
nodeToken = createNetworkTokenForNode(userId, NET, "sar-7", "node_sar_7").token!;
|
|
76
|
+
const mod: any = await import("./server.js");
|
|
77
|
+
hub = mod.bootServer({ port: 0, hostname: "127.0.0.1" });
|
|
78
|
+
BASE = `http://127.0.0.1:${hub.port}`;
|
|
79
|
+
}, 30_000);
|
|
80
|
+
|
|
81
|
+
afterAll(() => {
|
|
82
|
+
try { hub?.stop?.(true); } catch {}
|
|
83
|
+
try { rmSync(DIR, { recursive: true, force: true }); } catch {}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("statusAliasResolverRead (signature)", () => {
|
|
87
|
+
const p = (q: string) => new URLSearchParams(q);
|
|
88
|
+
test("matches only node token + Accept exactly application/json + at most network_id", () => {
|
|
89
|
+
expect(statusAliasResolverRead(p(""), "ntok_x", "application/json")).toBe(true);
|
|
90
|
+
expect(statusAliasResolverRead(p("network_id=net_1"), "ntok_x", "application/json")).toBe(true);
|
|
91
|
+
expect(statusAliasResolverRead(p("network_id=net_1"), "ntok_x", " Application/JSON ")).toBe(true);
|
|
92
|
+
expect(statusAliasResolverRead(p("network_id=net_1"), "utok_x", "application/json")).toBe(false);
|
|
93
|
+
expect(statusAliasResolverRead(p("network_id=net_1"), "legacy-global-token", "application/json")).toBe(false);
|
|
94
|
+
expect(statusAliasResolverRead(p("network_id=net_1"), "ntok_x", null)).toBe(false);
|
|
95
|
+
expect(statusAliasResolverRead(p("network_id=net_1"), "ntok_x", "*/*")).toBe(false);
|
|
96
|
+
expect(statusAliasResolverRead(p("network_id=net_1"), "ntok_x", "application/json, text/plain, */*")).toBe(false);
|
|
97
|
+
for (const q of ["light=1", "node_id=n", "alias=a", "full=1", "network_id=n&full=1", "network_id=n&light=0"]) {
|
|
98
|
+
expect(statusAliasResolverRead(p(q), "ntok_x", "application/json")).toBe(false);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("GET /api/status for the old alias resolver", () => {
|
|
104
|
+
test("old resolver still resolves its alias from the light body", async () => {
|
|
105
|
+
expect(await oldResolver("node_sar_7", nodeToken, NET)).toBe("sar-7");
|
|
106
|
+
expect(await oldResolver("node_sar_7", nodeToken, "")).toBe("sar-7");
|
|
107
|
+
// other rows are resolvable too (the resolver can be asked about a renamed peer id) and an unknown id is null
|
|
108
|
+
expect(await oldResolver("node_sar_42", nodeToken, NET)).toBe("sar-42");
|
|
109
|
+
expect(await oldResolver("node_nobody", nodeToken, NET)).toBeNull();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("…and follows a rename (the reason the resolver exists)", async () => {
|
|
113
|
+
db.run("UPDATE sessions SET alias = 'sar-7-renamed' WHERE node_id = 'node_sar_7'");
|
|
114
|
+
try {
|
|
115
|
+
expect(await oldResolver("node_sar_7", nodeToken, NET)).toBe("sar-7-renamed");
|
|
116
|
+
} finally {
|
|
117
|
+
db.run("UPDATE sessions SET alias = 'sar-7' WHERE node_id = 'node_sar_7'");
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("the body is the light projection plus node_id, marked by a header", async () => {
|
|
122
|
+
const r = await req(`/api/status?network_id=${NET}`, asNode("application/json"));
|
|
123
|
+
expect(r.status).toBe(200);
|
|
124
|
+
expect(r.projection).toBe("alias-resolver");
|
|
125
|
+
expect(r.body.sessions).toHaveLength(ROWS);
|
|
126
|
+
expect(Object.keys(r.body.sessions[0]).sort()).toEqual(["agent", "alias", "network_id", "node_id", "runtime", "server", "status", "task", "updated_at"]);
|
|
127
|
+
expect(r.body.summary.total).toBe(ROWS);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("everything else is byte-identical to the full projection", async () => {
|
|
131
|
+
const full = await req(`/api/status?network_id=${NET}&full=1`, asNode("application/json"));
|
|
132
|
+
expect(full.projection).toBeNull();
|
|
133
|
+
expect(full.body.sessions[0].host).toBeDefined();
|
|
134
|
+
const same = [
|
|
135
|
+
await req(`/api/status?network_id=${NET}`, { Authorization: `Bearer ${userToken}`, Accept: "application/json" }), // user token (app / dashboard)
|
|
136
|
+
await req(`/api/status?network_id=${NET}`, asNode()), // anet CLI: authHeaders() only
|
|
137
|
+
await req(`/api/status?network_id=${NET}`, asNode("*/*")), // browser / undici / bun default
|
|
138
|
+
await req(`/api/status?network_id=${NET}`, asNode("application/json, text/plain, */*")),
|
|
139
|
+
];
|
|
140
|
+
for (const r of same) {
|
|
141
|
+
expect(r.projection).toBeNull();
|
|
142
|
+
expect(r.body.sessions[0].host).toBeDefined();
|
|
143
|
+
expect(r.text).toBe(full.text);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("light / node_id reads keep their own exact shape", async () => {
|
|
148
|
+
const light = await req(`/api/status?network_id=${NET}&light=1`, asNode("application/json"));
|
|
149
|
+
expect(light.projection).toBeNull();
|
|
150
|
+
expect(light.body.sessions[0].node_id).toBeUndefined();
|
|
151
|
+
const one = await req(`/api/status?network_id=${NET}&node_id=node_sar_7&light=1`, asNode("application/json"));
|
|
152
|
+
expect(one.body.sessions).toHaveLength(1);
|
|
153
|
+
expect(one.body.sessions[0].node_id).toBe("node_sar_7");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("size: full projection vs the alias-resolver body (gzip)", async () => {
|
|
157
|
+
const full = await req(`/api/status?network_id=${NET}&full=1`, asNode("application/json"));
|
|
158
|
+
const slim = await req(`/api/status?network_id=${NET}`, asNode("application/json"));
|
|
159
|
+
const f = gz(full.text), s = gz(slim.text);
|
|
160
|
+
console.log(`[size] ${ROWS} rows: full ${full.text.length} B raw / ${f} B gzip → alias-resolver ${slim.text.length} B raw / ${s} B gzip (${Math.round(100 - (100 * s) / f)}% smaller)`);
|
|
161
|
+
expect(s).toBeLessThan(f / 2);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// GET /api/status 的「旧版 agent-node 找自己别名」读法识别(纯函数,可单测)。
|
|
2
|
+
//
|
|
3
|
+
// agent-node < 2.5.0-preview.93 的 CurrentAliasResolver 每 30 s 读一次全网完整 /api/status,只用
|
|
4
|
+
// sessions[].node_id / .alias,并且 2.5 s 硬超时。它的请求有一个别的客户端都不发的签名(2026-10-01 逐个核过
|
|
5
|
+
// 线上在用的 agent-node .32–.93 包、agent-network CLI 2.2.21 / 2.3.0-preview.76–.120 源码、dashboard、
|
|
6
|
+
// claude-code 节点的 .anet/node-server.js):
|
|
7
|
+
// - 节点令牌(ntok_);
|
|
8
|
+
// - Accept 恰好是 `application/json`(CLI 的 authHeaders 只带 Authorization;浏览器 / undici / bun 默认 `*/*`);
|
|
9
|
+
// - 查询串里除了 network_id 什么都没有(不带 light / node_id / alias / full)。
|
|
10
|
+
// 命中 ⇒ 回 light 投影 + node_id(它读的就这两列)。`?full=1` 永远拿完整投影。
|
|
11
|
+
|
|
12
|
+
const ALLOWED_PARAMS = new Set(["network_id"]);
|
|
13
|
+
|
|
14
|
+
export function statusAliasResolverRead(params: URLSearchParams, token: string, accept: string | null): boolean {
|
|
15
|
+
if (!token.startsWith("ntok_")) return false;
|
|
16
|
+
if ((accept ?? "").trim().toLowerCase() !== "application/json") return false;
|
|
17
|
+
for (const key of params.keys()) if (!ALLOWED_PARAMS.has(key)) return false;
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// GET /api/status?node_id=<id> — one node's rows instead of the whole network.
|
|
2
|
+
// Every agent-node resolves its own current alias every 30 s (CurrentAliasResolver). Unfiltered that
|
|
3
|
+
// is the whole network's full projection (~91 KB gzip per call on production, ~90 nodes ⇒ ~3 req/s)
|
|
4
|
+
// to read one row. This suite pins that the filter narrows, never widens (it is exactly the caller's
|
|
5
|
+
// unfiltered rows with that node_id, for every kind of caller), that it combines with light=1 (which
|
|
6
|
+
// then carries node_id so the caller can check the row is its own), that an unknown id is an empty
|
|
7
|
+
// list rather than an error, that no/blank node_id is byte-identical to the old response, and that
|
|
8
|
+
// /health advertises it.
|
|
9
|
+
|
|
10
|
+
import { describe, expect, test, beforeAll, afterAll } from "bun:test";
|
|
11
|
+
import { mkdtempSync, rmSync } from "fs";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
import { tmpdir } from "os";
|
|
14
|
+
import { register, createNetworkTokenForNode } from "./auth.js";
|
|
15
|
+
import { db } from "./db.js";
|
|
16
|
+
|
|
17
|
+
const DIR = mkdtempSync(join(tmpdir(), "anet-status-node-id-"));
|
|
18
|
+
let BASE = "";
|
|
19
|
+
let hub: any = null;
|
|
20
|
+
const PW = "StatusNodeIdPassw0rd!";
|
|
21
|
+
let aToken = "", aId = "", NET_A = "";
|
|
22
|
+
let bToken = "", bId = "", NET_B = "";
|
|
23
|
+
let nodeToken = "";
|
|
24
|
+
|
|
25
|
+
async function raw(token: string | null, path: string) {
|
|
26
|
+
const res = await fetch(`${BASE}${path}`, { headers: token ? { Authorization: `Bearer ${token}` } : {} });
|
|
27
|
+
return { status: res.status, text: await res.text() };
|
|
28
|
+
}
|
|
29
|
+
async function get(token: string, path: string) {
|
|
30
|
+
const r = await raw(token, path);
|
|
31
|
+
return { status: r.status, body: JSON.parse(r.text) as any };
|
|
32
|
+
}
|
|
33
|
+
function seed(alias: string, node: string, net: string, owner: string, minutesAgo: number) {
|
|
34
|
+
db.run(`INSERT INTO nodes (node_id, node_name, alias, network_id, owner_user_id) VALUES (?1, ?2, ?2, ?3, ?4)`, [node, alias, net, owner]);
|
|
35
|
+
db.run(
|
|
36
|
+
`INSERT INTO sessions (resume_id, alias, node_id, status, network_id, task, updated_at, last_seen_at) VALUES (?1, ?2, ?3, 'idle', ?4, ?5, datetime('now', ?6), datetime('now'))`,
|
|
37
|
+
[`r_${node}`, alias, node, net, `task of ${alias}`, `-${minutesAgo} minutes`],
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const nodeIds = (rows: any[]) => rows.map(r => r.node_id);
|
|
41
|
+
|
|
42
|
+
beforeAll(async () => {
|
|
43
|
+
process.env.COMMHUB_DB ||= join(DIR, "hub.db");
|
|
44
|
+
process.env.COMMHUB_UPLOADS_DIR = join(DIR, "uploads");
|
|
45
|
+
process.env.HOST = "127.0.0.1";
|
|
46
|
+
const a = register(`sn_owner_a_${Date.now()}`, PW);
|
|
47
|
+
aToken = a.token!; aId = a.user!.user_id; NET_A = a.network_id!;
|
|
48
|
+
const b = register(`sn_owner_b_${Date.now()}`, PW);
|
|
49
|
+
bToken = b.token!; bId = b.user!.user_id; NET_B = b.network_id!;
|
|
50
|
+
seed("sn-one", "node_sn_one", NET_A, aId, 1);
|
|
51
|
+
seed("sn-two", "node_sn_two", NET_A, aId, 2);
|
|
52
|
+
seed("sn-three", "node_sn_three", NET_A, aId, 3);
|
|
53
|
+
seed("sn-b-only", "node_sn_b_only", NET_B, bId, 0);
|
|
54
|
+
const minted = createNetworkTokenForNode(aId, NET_A, "sn-two", "node_sn_two");
|
|
55
|
+
nodeToken = minted.token!;
|
|
56
|
+
const mod: any = await import("./server.js");
|
|
57
|
+
hub = mod.bootServer({ port: 0, hostname: "127.0.0.1" });
|
|
58
|
+
BASE = `http://127.0.0.1:${hub.port}`;
|
|
59
|
+
}, 30_000);
|
|
60
|
+
|
|
61
|
+
afterAll(() => {
|
|
62
|
+
try { hub?.stop?.(true); } catch {}
|
|
63
|
+
try { rmSync(DIR, { recursive: true, force: true }); } catch {}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("/api/status?node_id=", () => {
|
|
67
|
+
test("node_id narrows the full projection to that node", async () => {
|
|
68
|
+
const r = await get(aToken, `/api/status?network_id=${NET_A}&node_id=node_sn_two`);
|
|
69
|
+
expect(r.status).toBe(200);
|
|
70
|
+
expect(nodeIds(r.body.sessions)).toEqual(["node_sn_two"]);
|
|
71
|
+
expect(r.body.sessions[0].alias).toBe("sn-two");
|
|
72
|
+
expect(typeof r.body.sessions[0].rules_file_capable).toBe("boolean");
|
|
73
|
+
expect(r.body.sessions[0].host).toBeDefined();
|
|
74
|
+
expect(r.body.summary.total).toBe(1);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("combines with light=1: light projection, one row, and it carries node_id", async () => {
|
|
78
|
+
const r = await get(aToken, `/api/status?light=1&network_id=${NET_A}&node_id=node_sn_three`);
|
|
79
|
+
expect(r.status).toBe(200);
|
|
80
|
+
expect(r.body.sessions).toHaveLength(1);
|
|
81
|
+
expect(r.body.sessions[0].alias).toBe("sn-three");
|
|
82
|
+
expect(r.body.sessions[0].node_id).toBe("node_sn_three");
|
|
83
|
+
expect(r.body.sessions[0].host).toBeUndefined();
|
|
84
|
+
expect(Object.keys(r.body.sessions[0]).sort()).toEqual(
|
|
85
|
+
["agent", "alias", "network_id", "node_id", "runtime", "server", "status", "task", "updated_at"],
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("unknown id is an empty list with 200, not a 404", async () => {
|
|
90
|
+
for (const path of [`/api/status?network_id=${NET_A}&node_id=node_nobody`, `/api/status?light=1&node_id=node_nobody`]) {
|
|
91
|
+
const r = await get(aToken, path);
|
|
92
|
+
expect(r.status).toBe(200);
|
|
93
|
+
expect(r.body.ok).toBe(true);
|
|
94
|
+
expect(r.body.sessions).toEqual([]);
|
|
95
|
+
expect(r.body.summary.total).toBe(0);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("absent or blank node_id: response bytes identical to the old read (full and light)", async () => {
|
|
100
|
+
for (const base of [`/api/status?network_id=${NET_A}`, `/api/status?light=1&network_id=${NET_A}`, `/api/status`]) {
|
|
101
|
+
const plain = await raw(aToken, base);
|
|
102
|
+
const blank = await raw(aToken, `${base}${base.includes("?") ? "&" : "?"}node_id=%20`);
|
|
103
|
+
expect(plain.status).toBe(200);
|
|
104
|
+
expect(blank.text).toBe(plain.text);
|
|
105
|
+
}
|
|
106
|
+
// the light rows without the param never gained a node_id field
|
|
107
|
+
const light = await get(aToken, `/api/status?light=1&network_id=${NET_A}`);
|
|
108
|
+
expect(light.body.sessions.length).toBe(3);
|
|
109
|
+
for (const s of light.body.sessions) expect("node_id" in s).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("never widens: for every caller, filtered == that caller's unfiltered rows with that node_id", async () => {
|
|
113
|
+
const callers = [aToken, bToken, nodeToken];
|
|
114
|
+
const ids = ["node_sn_one", "node_sn_two", "node_sn_three", "node_sn_b_only", "node_nobody"];
|
|
115
|
+
for (const tok of callers) {
|
|
116
|
+
const full = await get(tok, `/api/status`);
|
|
117
|
+
expect(full.status).toBe(200);
|
|
118
|
+
for (const id of ids) {
|
|
119
|
+
const one = await get(tok, `/api/status?node_id=${id}`);
|
|
120
|
+
expect(one.status).toBe(full.status);
|
|
121
|
+
expect(one.body.sessions).toEqual(full.body.sessions.filter((s: any) => s.node_id === id));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("a member of another network gets nothing for a node outside their network", async () => {
|
|
127
|
+
const r = await get(bToken, `/api/status?node_id=node_sn_one`);
|
|
128
|
+
expect(r.status).toBe(200);
|
|
129
|
+
expect(r.body.sessions).toEqual([]);
|
|
130
|
+
const own = await get(bToken, `/api/status?node_id=node_sn_b_only`);
|
|
131
|
+
expect(nodeIds(own.body.sessions)).toEqual(["node_sn_b_only"]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("a node token reads its own row (light) and nothing from another network", async () => {
|
|
135
|
+
const own = await get(nodeToken, `/api/status?light=1&node_id=node_sn_two`);
|
|
136
|
+
expect(own.status).toBe(200);
|
|
137
|
+
expect(own.body.sessions.map((s: any) => [s.node_id, s.alias])).toEqual([["node_sn_two", "sn-two"]]);
|
|
138
|
+
const foreign = await get(nodeToken, `/api/status?light=1&node_id=node_sn_b_only`);
|
|
139
|
+
expect(foreign.status).toBe(200);
|
|
140
|
+
expect(foreign.body.sessions).toEqual([]);
|
|
141
|
+
// explicit other network_id is refused exactly as without the filter
|
|
142
|
+
const cross = await raw(nodeToken, `/api/status?network_id=${NET_B}&node_id=node_sn_b_only`);
|
|
143
|
+
const crossNoFilter = await raw(nodeToken, `/api/status?network_id=${NET_B}`);
|
|
144
|
+
expect(cross.status).toBe(crossNoFilter.status);
|
|
145
|
+
if (cross.status !== 200) expect(cross.text).toBe(crossNoFilter.text);
|
|
146
|
+
else expect(JSON.parse(cross.text).sessions).toEqual([]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("unauthenticated read is refused exactly as without the filter", async () => {
|
|
150
|
+
const withFilter = await raw(null, `/api/status?node_id=node_sn_one`);
|
|
151
|
+
const without = await raw(null, `/api/status`);
|
|
152
|
+
expect(withFilter.status).toBe(without.status);
|
|
153
|
+
expect(withFilter.text).toBe(without.text);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("/health advertises status_node_id", async () => {
|
|
157
|
+
const r = await raw(null, `/health`);
|
|
158
|
+
expect(r.status).toBe(200);
|
|
159
|
+
expect(JSON.parse(r.text).capabilities).toContain("status_node_id");
|
|
160
|
+
});
|
|
161
|
+
});
|