@sleep2agi/commhub-server 0.9.0-preview.12 → 0.9.0-preview.121

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 (304) hide show
  1. package/README.md +130 -10
  2. package/bin/commhub.ts +92 -11
  3. package/package.json +7 -7
  4. package/src/ack-create-request-transport.test.ts +182 -0
  5. package/src/ack-create-request.test.ts +50 -7
  6. package/src/admin-delete-network-http.test.ts +228 -0
  7. package/src/admin-networks-http.test.ts +88 -0
  8. package/src/adoption-candidates-http.test.ts +137 -0
  9. package/src/adoption-candidates.ts +158 -0
  10. package/src/agent-access-batch.test.ts +210 -0
  11. package/src/agent-access.ts +487 -0
  12. package/src/agent-acl-groups-http.test.ts +247 -0
  13. package/src/agent-acl-http.test.ts +659 -0
  14. package/src/agent-acl-slice3-http.test.ts +267 -0
  15. package/src/agent-groups-perf.test.ts +223 -0
  16. package/src/agent-notice.ts +48 -0
  17. package/src/agent-team-whoami-http.test.ts +98 -0
  18. package/src/agent-teams-http.test.ts +323 -0
  19. package/src/agent-teams.ts +244 -0
  20. package/src/alias-filter.test.ts +72 -0
  21. package/src/alias-filter.ts +42 -0
  22. package/src/api-host-supervisors-can-create-nodes.test.ts +350 -0
  23. package/src/api-host-supervisors-fallback.test.ts +271 -0
  24. package/src/api-host-supervisors-runtime-readiness.test.ts +195 -0
  25. package/src/api-nodes-shape.test.ts +26 -10
  26. package/src/auth-password-change-cli-tokens-http.test.ts +294 -0
  27. package/src/auth-sessions-http.test.ts +250 -0
  28. package/src/auth-validate.test.ts +5 -1
  29. package/src/auth-weak-password-login.test.ts +176 -0
  30. package/src/auth.ts +598 -49
  31. package/src/avatar-validate.test.ts +163 -0
  32. package/src/avatar-validate.ts +103 -0
  33. package/src/binding-generation-http.test.ts +121 -0
  34. package/src/blocked-exit-on-reply.test.ts +122 -0
  35. package/src/boot-side-effect.test.ts +124 -0
  36. package/src/codex-config-flags.ts +77 -0
  37. package/src/codex-fork-contract.ts +23 -0
  38. package/src/codex-fork-migration.test.ts +38 -0
  39. package/src/commhub-bin-args.test.ts +85 -0
  40. package/src/config-apply-sec1.test.ts +107 -0
  41. package/src/config-apply-validate.test.ts +134 -1
  42. package/src/config-apply-validate.ts +111 -5
  43. package/src/create-node-blocked-gate.test.ts +356 -0
  44. package/src/create-node-tool-schema.test.ts +48 -0
  45. package/src/create-node-unicode-name.test.ts +120 -0
  46. package/src/create-node-validate.test.ts +193 -7
  47. package/src/create-node-validate.ts +217 -15
  48. package/src/create-node-workdir.test.ts +289 -0
  49. package/src/create-node.ts +77 -23
  50. package/src/daemon-provider-snapshots.test.ts +256 -0
  51. package/src/daemon-provider-snapshots.ts +226 -0
  52. package/src/daemon-token-owner-http.test.ts +231 -0
  53. package/src/dashboard-slash-routing-http.test.ts +107 -0
  54. package/src/db-adapter-guard.test.ts +148 -0
  55. package/src/db-adapter-translate.test.ts +128 -0
  56. package/src/db-adapter.ts +323 -87
  57. package/src/db-timestamp.test.ts +38 -0
  58. package/src/db-timestamp.ts +15 -0
  59. package/src/db.ts +903 -35
  60. package/src/delivered-without-push.test.ts +140 -0
  61. package/src/department-groups-http.test.ts +209 -0
  62. package/src/department-groups-sync-http.test.ts +328 -0
  63. package/src/department-groups.ts +434 -0
  64. package/src/department-heads-http.test.ts +330 -0
  65. package/src/department-heads.ts +226 -0
  66. package/src/departments-http.test.ts +185 -0
  67. package/src/departments.ts +260 -0
  68. package/src/desktop-message-seam.test.ts +150 -0
  69. package/src/desktop-message.test.ts +368 -0
  70. package/src/dm-file-scope-http.test.ts +178 -0
  71. package/src/ext-token-shared.test.ts +60 -0
  72. package/src/ext-validation-regression.test.ts +131 -0
  73. package/src/external-schedule-edits-http.test.ts +321 -0
  74. package/src/external-schedule-edits-timestamps.test.ts +64 -0
  75. package/src/external-schedule-edits.ts +327 -0
  76. package/src/file-download-authz-dev-open.test.ts +135 -0
  77. package/src/file-download-authz.test.ts +404 -0
  78. package/src/file-download-date-bucket.test.ts +436 -0
  79. package/src/file-network-scope-dev-open.test.ts +97 -0
  80. package/src/file-network-scope.test.ts +732 -0
  81. package/src/group-membership-events-http.test.ts +264 -0
  82. package/src/group-messages-http.test.ts +482 -0
  83. package/src/group-messages.ts +297 -0
  84. package/src/health-redaction.test.ts +642 -0
  85. package/src/http-gzip.test.ts +94 -0
  86. package/src/http-gzip.ts +112 -0
  87. package/src/hub-timestamp.test.ts +38 -0
  88. package/src/hub-timestamp.ts +27 -0
  89. package/src/human-dm.ts +215 -0
  90. package/src/inbox-count-sse.test.ts +310 -0
  91. package/src/inbox-count.ts +29 -0
  92. package/src/index.ts +15 -2356
  93. package/src/lifecycle-guard.ts +1 -1
  94. package/src/mcp-context-cost-http.test.ts +120 -0
  95. package/src/mcp-projects-events-http.test.ts +162 -0
  96. package/src/mcp-route-label-http.test.ts +120 -0
  97. package/src/mcp-write-network-resolution.test.ts +429 -0
  98. package/src/member-audit-network-id-http.test.ts +93 -0
  99. package/src/member-presence-http.test.ts +210 -0
  100. package/src/messages-ack-by-agent-http.test.ts +142 -0
  101. package/src/migrate-sqlite-to-pg.test.ts +47 -0
  102. package/src/migrate-sqlite-to-pg.ts +263 -0
  103. package/src/model-auth-notify-http.test.ts +181 -0
  104. package/src/model-auth-notify.ts +116 -0
  105. package/src/network-name-http.test.ts +75 -0
  106. package/src/network-scope.ts +276 -0
  107. package/src/network-token-membership-http.test.ts +139 -0
  108. package/src/node-alias-control.ts +12 -0
  109. package/src/node-attrs-validate.ts +88 -0
  110. package/src/node-attrs.test.ts +217 -0
  111. package/src/node-caller-identity-http.test.ts +714 -0
  112. package/src/node-create-request-status-http.test.ts +105 -0
  113. package/src/node-daemon-bindings.test.ts +132 -0
  114. package/src/node-daemon-bindings.ts +130 -0
  115. package/src/node-files-transport.test.ts +344 -0
  116. package/src/node-health-codex-login.test.ts +120 -0
  117. package/src/node-health-dispatch-http.test.ts +279 -0
  118. package/src/node-health-guard.ts +105 -0
  119. package/src/node-health-passthrough.test.ts +115 -0
  120. package/src/node-health-store.ts +101 -0
  121. package/src/node-id-alias.test.ts +190 -0
  122. package/src/node-identity-conflict-http.test.ts +269 -0
  123. package/src/node-identity-conflict.ts +142 -0
  124. package/src/node-identity-mismatch.test.ts +74 -0
  125. package/src/node-lifecycle-controllable.test.ts +64 -0
  126. package/src/node-lifecycle-controllable.ts +53 -0
  127. package/src/node-lifecycle-read-http.test.ts +245 -0
  128. package/src/node-lifecycle-read.ts +121 -0
  129. package/src/node-logs-transport.test.ts +263 -0
  130. package/src/node-permissions-http.test.ts +276 -0
  131. package/src/node-permissions.ts +283 -0
  132. package/src/node-request-retention.test.ts +281 -0
  133. package/src/node-request-retention.ts +120 -0
  134. package/src/node-skills-transport.test.ts +209 -0
  135. package/src/node-token-ownership.ts +36 -0
  136. package/src/node-token-privilege-http.test.ts +158 -0
  137. package/src/observer-avatar-http.test.ts +431 -0
  138. package/src/observer-push.test.ts +252 -0
  139. package/src/package-version-consistency.test.ts +25 -0
  140. package/src/peer-reply-atomic.test.ts +455 -0
  141. package/src/peer-reply-capability.test.ts +70 -0
  142. package/src/pg-worker.ts +87 -0
  143. package/src/project-dir-warn-clear-http.test.ts +132 -0
  144. package/src/project-viewer-can-http.test.ts +114 -0
  145. package/src/push.ts +429 -10
  146. package/src/redact-tokens.test.ts +65 -0
  147. package/src/redact-tokens.ts +40 -0
  148. package/src/rename.ts +22 -2
  149. package/src/reply-target-derivation.test.ts +96 -0
  150. package/src/report-status-handover-capabilities-http.test.ts +124 -0
  151. package/src/report-status-host-schema.test.ts +235 -0
  152. package/src/report-status-resume-id-handover.test.ts +150 -0
  153. package/src/require-explicit-test-db.ts +9 -0
  154. package/src/requirement-due-reminders-http.test.ts +620 -0
  155. package/src/requirement-due-reminders.ts +416 -0
  156. package/src/requirement-events-http.test.ts +268 -0
  157. package/src/requirement-events.test.ts +45 -0
  158. package/src/requirement-events.ts +144 -0
  159. package/src/requirement-notify.ts +165 -0
  160. package/src/requirement-participant-notify-http.test.ts +309 -0
  161. package/src/requirement-tag-ops-http.test.ts +266 -0
  162. package/src/requirement-tags.ts +89 -0
  163. package/src/requirements-abandoned-http.test.ts +236 -0
  164. package/src/requirements-comment-http.test.ts +167 -0
  165. package/src/requirements-completed-at-migration.test.ts +70 -0
  166. package/src/requirements-errors-mcp-http.test.ts +164 -0
  167. package/src/requirements-errors.ts +143 -0
  168. package/src/requirements-etag-http.test.ts +91 -0
  169. package/src/requirements-http.test.ts +709 -0
  170. package/src/requirements-last-event-http.test.ts +349 -0
  171. package/src/requirements-list-mcp-slim-http.test.ts +155 -0
  172. package/src/requirements-list-slim-http.test.ts +319 -0
  173. package/src/requirements-mcp.test.ts +163 -0
  174. package/src/requirements-migrate.ts +259 -0
  175. package/src/requirements-owner-strict-mcp-http.test.ts +145 -0
  176. package/src/requirements-people-http.test.ts +114 -0
  177. package/src/requirements-people-mcp-http.test.ts +225 -0
  178. package/src/requirements-people.ts +127 -0
  179. package/src/requirements-priority-lowest.test.ts +114 -0
  180. package/src/requirements-search-http.test.ts +182 -0
  181. package/src/requirements-search.test.ts +32 -0
  182. package/src/requirements-search.ts +145 -0
  183. package/src/requirements-seq-http.test.ts +193 -0
  184. package/src/requirements-seq-migration.test.ts +63 -0
  185. package/src/requirements-start-date.test.ts +125 -0
  186. package/src/requirements-stats-http.test.ts +288 -0
  187. package/src/requirements-stats.ts +167 -0
  188. package/src/requirements-status-participants-http.test.ts +222 -0
  189. package/src/requirements-upgrade-e2e.test.ts +126 -0
  190. package/src/requirements-upgrade-migration.test.ts +98 -0
  191. package/src/requirements.ts +1806 -0
  192. package/src/resolve-port.test.ts +57 -0
  193. package/src/resolve-port.ts +44 -0
  194. package/src/response-charset.test.ts +130 -0
  195. package/src/rest-explicit-columns-http.test.ts +222 -0
  196. package/src/rest-identity.test.ts +129 -0
  197. package/src/rest-identity.ts +125 -0
  198. package/src/rest-projections.ts +68 -0
  199. package/src/rest-write-network-resolution.test.ts +133 -0
  200. package/src/restricted-files.ts +76 -0
  201. package/src/retention.ts +15 -3
  202. package/src/route-callers-http.test.ts +120 -0
  203. package/src/route-timing.test.ts +211 -0
  204. package/src/route-timing.ts +258 -0
  205. package/src/rules-file-duplicate-alias.test.ts +234 -0
  206. package/src/rules-file-session-target.test.ts +207 -0
  207. package/src/rules-file-transport.test.ts +237 -0
  208. package/src/runtime-label.test.ts +22 -0
  209. package/src/runtime-label.ts +21 -0
  210. package/src/runtime-readiness.ts +90 -0
  211. package/src/schedule-agent-mcp-http.test.ts +381 -0
  212. package/src/schedule-agent.ts +192 -0
  213. package/src/scheduled-doorbell-nodeless-session.test.ts +134 -0
  214. package/src/scheduled-failures-http.test.ts +233 -0
  215. package/src/scheduled-failures.ts +179 -0
  216. package/src/scheduled-offline.test.ts +131 -0
  217. package/src/scheduled-offline.ts +66 -0
  218. package/src/scheduled-reply-unread-http.test.ts +212 -0
  219. package/src/scheduled-run-terminal.test.ts +268 -0
  220. package/src/scheduled-skip-blocking-task.test.ts +120 -0
  221. package/src/scheduled-stuck-http.test.ts +238 -0
  222. package/src/scheduled-stuck.ts +153 -0
  223. package/src/scheduled-tasks-http.test.ts +638 -0
  224. package/src/scheduled-tasks.ts +766 -0
  225. package/src/send-dedup-request-id.test.ts +229 -0
  226. package/src/send-reply-agent-warning.test.ts +202 -0
  227. package/src/send-reply-alias-optional.test.ts +131 -0
  228. package/src/send-reply-attachments.test.ts +458 -0
  229. package/src/send_dedup.test.ts +54 -0
  230. package/src/send_dedup.ts +52 -6
  231. package/src/serve-error.test.ts +38 -0
  232. package/src/serve-error.ts +27 -0
  233. package/src/server.ts +5253 -0
  234. package/src/session-task-on-dispatch.ts +14 -0
  235. package/src/shared/codex-inventory.ts +34 -0
  236. package/src/shared/daemon-provider-snapshot.ts +49 -0
  237. package/src/shared/external-schedule-contract.ts +60 -0
  238. package/src/shared/node-name.test.ts +62 -0
  239. package/src/shared/node-name.ts +161 -0
  240. package/src/shared/provider-application.ts +24 -0
  241. package/src/shared/reserved-env-drift.test.ts +10 -0
  242. package/src/shared/task-content-limit-drift.test.ts +28 -0
  243. package/src/shared/task-content-limit.ts +25 -0
  244. package/src/side-thread-command-transport.test.ts +312 -0
  245. package/src/side-thread-command-transport.ts +318 -0
  246. package/src/side-thread-http-integration.test.ts +233 -0
  247. package/src/side-thread-http.ts +504 -0
  248. package/src/side-thread-production.test.ts +72 -0
  249. package/src/side-thread-production.ts +118 -0
  250. package/src/side-thread.test.ts +1067 -0
  251. package/src/side-thread.ts +2499 -0
  252. package/src/skillhub-http.test.ts +108 -0
  253. package/src/sqlite-close-checkpoint.test.ts +78 -0
  254. package/src/start-node.test.ts +264 -0
  255. package/src/status-alias-filter-http.test.ts +106 -0
  256. package/src/status-alias-resolver-http.test.ts +163 -0
  257. package/src/status-alias-resolver.ts +19 -0
  258. package/src/status-node-id-filter-http.test.ts +161 -0
  259. package/src/status-read-cache-http.test.ts +229 -0
  260. package/src/status-read-cache.ts +122 -0
  261. package/src/stop-delete-node.test.ts +469 -1
  262. package/src/task-access-http.test.ts +402 -0
  263. package/src/task-access.ts +420 -0
  264. package/src/task-auth-origin-http.test.ts +91 -0
  265. package/src/task-auth-origin.test.ts +24 -0
  266. package/src/task-auth-origin.ts +7 -0
  267. package/src/task-consumption.test.ts +482 -0
  268. package/src/task-diagnostic-http.test.ts +149 -0
  269. package/src/task-diagnostic.test.ts +115 -0
  270. package/src/task-diagnostic.ts +118 -0
  271. package/src/task-expiry-notice-http.test.ts +287 -0
  272. package/src/task-expiry-notice.ts +315 -0
  273. package/src/task-idempotency-mcp.test.ts +142 -0
  274. package/src/task-idempotency.test.ts +60 -0
  275. package/src/task-idempotency.ts +75 -0
  276. package/src/task-late-replies.ts +54 -0
  277. package/src/task-late-reply-http.test.ts +152 -0
  278. package/src/task-lifecycle-watcher.test.ts +204 -0
  279. package/src/task-lifecycle-watcher.ts +49 -0
  280. package/src/task-network-resolution-http.test.ts +141 -0
  281. package/src/task-orphan-http.test.ts +287 -0
  282. package/src/task-orphan.ts +119 -0
  283. package/src/task-queue-ahead-http.test.ts +309 -0
  284. package/src/task-queue-ahead-scale.test.ts +236 -0
  285. package/src/task-queue-ahead.ts +186 -0
  286. package/src/task-rejection-log-http.test.ts +160 -0
  287. package/src/task-rejection-log.ts +49 -0
  288. package/src/task-stale-open-http.test.ts +290 -0
  289. package/src/task-stale-open.ts +103 -0
  290. package/src/tasks-pagination-http.test.ts +141 -0
  291. package/src/tool-audience-http.test.ts +222 -0
  292. package/src/tool-audience.ts +116 -0
  293. package/src/tools-list-json-schema.test.ts +67 -0
  294. package/src/tools.ts +3598 -313
  295. package/src/upload-early-reject-guard.test.ts +155 -0
  296. package/src/uploads-http.test.ts +97 -15
  297. package/src/uploads.test.ts +66 -0
  298. package/src/uploads.ts +138 -5
  299. package/src/user-avatar-http.test.ts +74 -0
  300. package/src/user-avatar-migration.test.ts +25 -0
  301. package/src/user-inbox-by-agent-http.test.ts +161 -0
  302. package/src/user-inbox-netid-notnull.test.ts +122 -0
  303. package/src/user-inbox-read-http.test.ts +223 -0
  304. package/src/user-inbox-schema.test.ts +54 -0
@@ -0,0 +1,210 @@
1
+ // visibleAgents() batched into one query. The old implementation (kept here verbatim as the
2
+ // reference) ran one nodes ∪ sessions lookup per node_id grant — ~300 queries per call for a
3
+ // restricted member with 300 grants, on every restricted /api/status poll. The new one must return
4
+ // IDENTICAL output (same members, same order) on randomized fixtures.
5
+
6
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
7
+ import { mkdtempSync, rmSync } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+
11
+ const dir = mkdtempSync(join(tmpdir(), "anet-agent-access-batch-"));
12
+ process.env.COMMHUB_DB = join(dir, "hub.db");
13
+
14
+ let db: any;
15
+ let visibleAgents: (userId: string, networkId: string) => any;
16
+ let listAgentGrants: (networkId: string, userId: string) => any[];
17
+ let CHUNK = 0;
18
+
19
+ function visibleAgentsReference(userId: string, networkId: string) {
20
+ const aliases = new Set<string>();
21
+ const nodeIds = new Set<string>();
22
+ const messageAliases = new Set<string>();
23
+ const messageNodeIds = new Set<string>();
24
+ // Group grants (#2131) expanded exactly as agent-access.ts groupGrantNodes() does, appended after
25
+ // the direct grants — the order the old loop saw them in.
26
+ const groupNodes = db.all(
27
+ `SELECT m.node_id, g.can_message
28
+ FROM network_member_group_grants g
29
+ JOIN agent_groups ag ON ag.group_id = g.group_id AND ag.network_id = g.network_id
30
+ JOIN agent_group_members m ON m.group_id = g.group_id
31
+ JOIN nodes n ON n.node_id = m.node_id AND n.network_id = g.network_id
32
+ WHERE g.network_id = ?1 AND g.user_id = ?2`,
33
+ networkId, userId,
34
+ ).map((row: any) => ({ node_id: row.node_id, alias: null, can_message: row.can_message === 1 }));
35
+ for (const grant of [...listAgentGrants(networkId, userId), ...groupNodes]) {
36
+ const grantAliases: string[] = [];
37
+ if (grant.node_id) {
38
+ nodeIds.add(grant.node_id);
39
+ if (grant.can_message) messageNodeIds.add(grant.node_id);
40
+ for (const row of db.all(
41
+ `SELECT alias FROM nodes WHERE node_id = ?1 AND network_id = ?2
42
+ UNION SELECT alias FROM sessions WHERE node_id = ?1 AND network_id = ?2`,
43
+ grant.node_id, networkId,
44
+ )) {
45
+ if (row.alias) grantAliases.push(row.alias);
46
+ }
47
+ } else if (grant.alias) {
48
+ grantAliases.push(grant.alias);
49
+ }
50
+ for (const alias of grantAliases) {
51
+ aliases.add(alias);
52
+ if (grant.can_message) messageAliases.add(alias);
53
+ }
54
+ }
55
+ return { aliases: [...aliases], nodeIds: [...nodeIds], messageAliases: [...messageAliases], messageNodeIds: [...messageNodeIds] };
56
+ }
57
+
58
+ // Deterministic PRNG so a failure is reproducible from the printed seed.
59
+ function rng(seed: number) {
60
+ let s = seed >>> 0;
61
+ return () => { s = (s * 1664525 + 1013904223) >>> 0; return s / 2 ** 32; };
62
+ }
63
+
64
+ let seq = 0;
65
+ function seedFixture(seed: number, opts: { nodes: number; grants: number; groups?: number }) {
66
+ const r = rng(seed);
67
+ const pick = <T,>(xs: T[]) => xs[Math.floor(r() * xs.length)];
68
+ const tag = `s${seed}_${seq++}`;
69
+ db.exec("BEGIN");
70
+ const net = `net_${tag}`, other = `net_${tag}_other`, user = `u_${tag}`;
71
+ db.run(`INSERT INTO users (user_id, username, password_hash) VALUES (?1, ?1, 'x')`, [user]);
72
+ const nodeIds: string[] = [];
73
+ const legacyAliases: string[] = [];
74
+ for (let i = 0; i < opts.nodes; i++) {
75
+ const nodeId = `node_${tag}_${i}`;
76
+ nodeIds.push(nodeId);
77
+ const alias = `agent-${tag}-${i}`;
78
+ const shape = r();
79
+ // nodes row (sometimes missing, sometimes NULL alias)
80
+ if (shape > 0.1) db.run(`INSERT INTO nodes (node_id, node_name, alias, network_id) VALUES (?1, ?2, ?3, ?4)`, [nodeId, alias, shape > 0.15 ? alias : null, net]);
81
+ // sessions rows: same alias, a renamed alias (rename window), several, or none
82
+ const sessions = r() < 0.2 ? 0 : 1 + Math.floor(r() * 3);
83
+ for (let k = 0; k < sessions; k++) {
84
+ const sAlias = k === 0 && r() < 0.6 ? alias : `${alias}-renamed-${k}`;
85
+ db.run(`INSERT INTO sessions (resume_id, alias, node_id, status, network_id) VALUES (?1, ?2, ?3, 'idle', ?4)`, [`r_${tag}_${i}_${k}`, sAlias, nodeId, net]);
86
+ }
87
+ // same node_id in another network must never leak in
88
+ if (r() < 0.3) db.run(`INSERT INTO sessions (resume_id, alias, node_id, status, network_id) VALUES (?1, ?2, ?3, 'idle', ?4)`, [`r_${tag}_${i}_x`, `${alias}-elsewhere`, nodeId, other]);
89
+ }
90
+ for (let i = 0; i < Math.ceil(opts.nodes / 5); i++) {
91
+ const alias = `legacy-${tag}-${i}`;
92
+ legacyAliases.push(alias);
93
+ db.run(`INSERT INTO sessions (resume_id, alias, status, network_id) VALUES (?1, ?2, 'idle', ?3)`, [`rl_${tag}_${i}`, alias, net]);
94
+ }
95
+ const usedAliases = new Set<string>();
96
+ // distinct nodes in a shuffled order (the grant table has one row per node per member)
97
+ const order = nodeIds.map(n => [r(), n] as const).sort((a, b) => a[0] - b[0]).map(([, n]) => n);
98
+ for (let i = 0; i < opts.grants; i++) {
99
+ const byNode = r() < 0.8;
100
+ const can = r() < 0.5 ? 1 : 0;
101
+ if (byNode) {
102
+ const nodeId = r() < 0.05 ? `node_${tag}_ghost_${i}` : order.pop();
103
+ if (!nodeId) continue;
104
+ db.run(`INSERT INTO network_member_agent_grants (network_id, user_id, node_id, alias, can_message) VALUES (?1, ?2, ?3, NULL, ?4)`, [net, user, nodeId, can]);
105
+ } else {
106
+ const alias = pick(legacyAliases);
107
+ if (usedAliases.has(alias)) continue;
108
+ usedAliases.add(alias);
109
+ db.run(`INSERT INTO network_member_agent_grants (network_id, user_id, node_id, alias, can_message) VALUES (?1, ?2, NULL, ?3, ?4)`, [net, user, alias, can]);
110
+ }
111
+ }
112
+ // Groups (#2131): some granted, some not; members include nodes also granted directly (with a
113
+ // different can_message), ghost nodes with no nodes row, and a node that lives in another network.
114
+ const groups = opts.groups ?? 0;
115
+ for (let gi = 0; gi < groups; gi++) {
116
+ const groupId = `grp_${tag}_${gi}`;
117
+ const groupNet = r() < 0.1 ? other : net;
118
+ db.run(`INSERT INTO agent_groups (group_id, network_id, name) VALUES (?1, ?2, ?3)`, [groupId, groupNet, `group ${tag} ${gi}`]);
119
+ const size = 1 + Math.floor(r() * 8);
120
+ const members = new Set<string>();
121
+ for (let k = 0; k < size; k++) members.add(r() < 0.05 ? `node_${tag}_ghostmember_${gi}_${k}` : pick(nodeIds));
122
+ if (r() < 0.2) {
123
+ const foreign = `node_${tag}_foreign_${gi}`;
124
+ db.run(`INSERT INTO nodes (node_id, node_name, alias, network_id) VALUES (?1, ?1, ?2, ?3)`, [foreign, `foreign-${tag}-${gi}`, other]);
125
+ members.add(foreign);
126
+ }
127
+ for (const m of members) db.run(`INSERT INTO agent_group_members (group_id, node_id) VALUES (?1, ?2)`, [groupId, m]);
128
+ if (r() < 0.7) db.run(`INSERT INTO network_member_group_grants (network_id, user_id, group_id, can_message) VALUES (?1, ?2, ?3, ?4)`, [net, user, groupId, r() < 0.5 ? 1 : 0]);
129
+ }
130
+ db.exec("COMMIT");
131
+ return { net, user };
132
+ }
133
+
134
+ beforeAll(async () => {
135
+ ({ db } = await import("./db.js"));
136
+ ({ visibleAgents, listAgentGrants, VISIBLE_AGENTS_CHUNK: CHUNK } = await import("./agent-access.js") as any);
137
+ });
138
+
139
+ afterAll(() => {
140
+ try { rmSync(dir, { recursive: true, force: true }); } catch {}
141
+ });
142
+
143
+ describe("visibleAgents batched = reference, on randomized fixtures", () => {
144
+ test("200 random fixtures: identical output (members and order)", () => {
145
+ for (let seed = 1; seed <= 200; seed++) {
146
+ const { net, user } = seedFixture(seed, { nodes: 5 + (seed % 40), grants: seed % 7 === 0 ? 0 : 3 + (seed % 50), groups: seed % 3 === 0 ? 0 : 1 + (seed % 12) });
147
+ const got = visibleAgents(user, net);
148
+ const want = visibleAgentsReference(user, net);
149
+ if (JSON.stringify(got) !== JSON.stringify(want)) {
150
+ throw new Error(`seed ${seed} differs:\nnew ${JSON.stringify(got)}\nold ${JSON.stringify(want)}`);
151
+ }
152
+ }
153
+ }, 120_000);
154
+
155
+ test("the fixtures exercise every shape (rename window, NULL alias, ghost node, legacy alias, other network, groups)", () => {
156
+ const { net, user } = seedFixture(4242, { nodes: 60, grants: 80, groups: 15 });
157
+ const groupNodes = db.get<{ n: number }>("SELECT COUNT(*) AS n FROM network_member_group_grants WHERE network_id = ?1 AND user_id = ?2", net, user)!.n;
158
+ expect(groupNodes).toBeGreaterThan(0);
159
+ const got = visibleAgents(user, net);
160
+ expect(got.aliases.some((a: string) => a.includes("-renamed-"))).toBe(true);
161
+ expect(got.aliases.some((a: string) => a.startsWith("legacy-"))).toBe(true);
162
+ expect(got.aliases.some((a: string) => a.endsWith("-elsewhere"))).toBe(false);
163
+ expect(got.messageAliases.length).toBeLessThan(got.aliases.length);
164
+ expect(JSON.stringify(got)).toBe(JSON.stringify(visibleAgentsReference(user, net)));
165
+ });
166
+
167
+ test("groups only (no direct grants): identical output, group nodes visible", () => {
168
+ const { net, user } = seedFixture(9090, { nodes: 80, grants: 0, groups: 25 });
169
+ const got = visibleAgents(user, net);
170
+ expect(got.nodeIds.length).toBeGreaterThan(0);
171
+ expect(JSON.stringify(got)).toBe(JSON.stringify(visibleAgentsReference(user, net)));
172
+ });
173
+
174
+ test("no grants → all empty; unknown user → all empty", () => {
175
+ const { net } = seedFixture(7, { nodes: 10, grants: 0 });
176
+ expect(visibleAgents("nobody", net)).toEqual({ aliases: [], nodeIds: [], messageAliases: [], messageNodeIds: [] });
177
+ });
178
+
179
+ test("query-count guard: a fixed number of queries per call, not one per grant (#2133)", () => {
180
+ const { net, user } = seedFixture(301, { nodes: 400, grants: 420, groups: 30 });
181
+ const grants = listAgentGrants(net, user).length;
182
+ expect(grants).toBeGreaterThanOrEqual(300);
183
+ const calls = { all: 0, get: 0 };
184
+ const all = db.all.bind(db), get = db.get.bind(db);
185
+ db.all = (...a: any[]) => { calls.all++; return all(...a); };
186
+ db.get = (...a: any[]) => { calls.get++; return get(...a); };
187
+ try { visibleAgents(user, net); } finally { db.all = all; db.get = get; }
188
+ // direct grants + group grants + ceil(nodes / chunk) alias reads
189
+ expect(calls.all + calls.get).toBeLessThanOrEqual(2 + Math.ceil(grants / CHUNK));
190
+ expect(calls.all + calls.get).toBeLessThanOrEqual(3);
191
+ }, 60_000);
192
+
193
+ test("more granted nodes than one chunk: same output across the chunk boundary", () => {
194
+ const { net, user } = seedFixture(777, { nodes: CHUNK + 120, grants: CHUNK + 200 });
195
+ expect(listAgentGrants(net, user).filter((g: any) => g.node_id).length).toBeGreaterThan(CHUNK);
196
+ expect(JSON.stringify(visibleAgents(user, net))).toBe(JSON.stringify(visibleAgentsReference(user, net)));
197
+ }, 120_000);
198
+
199
+ test("timing at 300 grants (printed; new must be faster)", () => {
200
+ const { net, user } = seedFixture(300, { nodes: 400, grants: 420 });
201
+ const grants = listAgentGrants(net, user).length;
202
+ expect(grants).toBeGreaterThanOrEqual(300);
203
+ const time = (fn: () => unknown, n = 50) => { fn(); const t0 = performance.now(); for (let i = 0; i < n; i++) fn(); return (performance.now() - t0) / n; };
204
+ const oldMs = time(() => visibleAgentsReference(user, net));
205
+ const newMs = time(() => visibleAgents(user, net));
206
+ console.log(`visibleAgents @ ${grants} grants: old ${oldMs.toFixed(2)} ms/call, new ${newMs.toFixed(2)} ms/call (${(oldMs / newMs).toFixed(1)}x)`);
207
+ expect(JSON.stringify(visibleAgents(user, net))).toBe(JSON.stringify(visibleAgentsReference(user, net)));
208
+ expect(newMs).toBeLessThan(oldMs);
209
+ }, 120_000);
210
+ });