@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,120 @@
1
+ // Board #652 — create_node takes Chinese / Unicode node names (the alias); illegal
2
+ // characters are refused with a reason before any row is written; the in-flight
3
+ // uniqueness check still holds. Driven through the REAL /mcp entry (zod schema +
4
+ // handler) on a private port with a temp DB, then get_create_request as the daemon.
5
+
6
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
7
+ import { mkdtempSync, rmSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { tmpdir } from "node:os";
10
+
11
+ const DIR = mkdtempSync(join(tmpdir(), "anet-652-"));
12
+ process.env.COMMHUB_DB ||= join(DIR, "hub.db");
13
+ process.env.HOST = "127.0.0.1";
14
+
15
+ const { db } = await import("./db.js");
16
+ const { register, createNetworkTokenForNode } = await import("./auth.js");
17
+
18
+ const PW = "Unicode652Passw0rd!x";
19
+ let BASE = "";
20
+ let hub: any = null;
21
+ let OWNER = "";
22
+ let OWNER_ID = "";
23
+ let NET = "";
24
+ let DAEMON_TOK = "";
25
+ const DAEMON_ID = "node_t652_daemon";
26
+
27
+ async function rpc(token: string, method: string, params?: unknown): Promise<any> {
28
+ const res = await fetch(`${BASE}/mcp`, {
29
+ method: "POST",
30
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", Accept: "application/json, text/event-stream", "MCP-Protocol-Version": "2025-03-26" },
31
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, ...(params === undefined ? {} : { params }) }),
32
+ });
33
+ const raw = await res.text();
34
+ const lines = raw.split("\n").filter(x => x.startsWith("data:"));
35
+ return lines.length ? JSON.parse(lines.at(-1)!.slice(5).trim()) : JSON.parse(raw);
36
+ }
37
+ async function mcp(token: string, name: string, args: Record<string, unknown>): Promise<any> {
38
+ const out = await rpc(token, "tools/call", { name, arguments: args });
39
+ const text = out?.result?.content?.[0]?.text;
40
+ if (typeof text !== "string") return { _raw: out };
41
+ try { return JSON.parse(text); } catch { return { _text: text, isError: out.result.isError }; }
42
+ }
43
+ const create = (name: string) => mcp(OWNER, "create_node", {
44
+ daemon_node_id: DAEMON_ID, network_id: NET,
45
+ node_spec: { name, runtime: "claude-agent-sdk", model: "claude-sonnet-4-5", flags: {} },
46
+ });
47
+ const rows = (name: string) =>
48
+ db.get<{ n: number }>("SELECT COUNT(*) AS n FROM node_create_requests WHERE daemon_node_id = ?1 AND child_name = ?2", DAEMON_ID, name)?.n ?? -1;
49
+
50
+ beforeAll(async () => {
51
+ const a = register(`t652_owner_${Date.now()}`, PW, undefined, "Owner");
52
+ if (!a.ok) throw new Error("register failed");
53
+ OWNER = a.token!; OWNER_ID = a.user!.user_id; NET = a.network_id!;
54
+ const mod: any = await import("./server.js");
55
+ hub = mod.bootServer({ port: 0, hostname: "127.0.0.1" });
56
+ BASE = `http://127.0.0.1:${hub.port}`;
57
+ const t = createNetworkTokenForNode(OWNER_ID, NET, "t652-daemon", DAEMON_ID);
58
+ if (!t.ok || !t.token) throw new Error("daemon token failed: " + t.error);
59
+ DAEMON_TOK = t.token;
60
+ db.run(
61
+ `INSERT OR REPLACE INTO nodes (node_id, node_name, alias, runtime, model, config_path, channels, server, hostname, network_id, config_revision, config_snapshot)
62
+ VALUES (?1, 't652-daemon', 't652-daemon', 'claude-agent-sdk', 'm', '/tmp/cfg.json', '[]', 'h', 'h', ?2, 0, ?3)`,
63
+ [DAEMON_ID, NET, JSON.stringify({ role: "host_supervisor", daemon_capabilities: { runtimes_supported: ["claude-agent-sdk"] } })],
64
+ );
65
+ });
66
+ afterAll(() => {
67
+ try { hub?.stop?.(true); } catch {}
68
+ rmSync(DIR, { recursive: true, force: true });
69
+ });
70
+
71
+ describe("#652 create_node via /mcp — Unicode names", () => {
72
+ test("「测试」 is accepted; the daemon gets the same alias", async () => {
73
+ const r = await create("测试");
74
+ expect(r.ok).toBe(true);
75
+ expect(String(r.request_id)).toMatch(/^cr_/);
76
+ expect(rows("测试")).toBe(1);
77
+ const g = await mcp(DAEMON_TOK, "get_create_request", { request_id: r.request_id });
78
+ expect(g.ok).toBe(true);
79
+ expect(g.node_spec.name).toBe("测试");
80
+ });
81
+
82
+ test("upper case / digit-first ASCII and 64 CJK characters pass the zod bound and the rule", async () => {
83
+ for (const name of ["研发助手A", "Demo2", "9lives", "测".repeat(64)]) {
84
+ const r = await create(name);
85
+ expect({ name, ok: r.ok, error: r.error }).toEqual({ name, ok: true, error: undefined });
86
+ }
87
+ });
88
+
89
+ test("surrounding whitespace is trimmed before it is stored", async () => {
90
+ const r = await create(" 前后空格 ");
91
+ expect(r.ok).toBe(true);
92
+ expect(rows("前后空格")).toBe(1);
93
+ expect(rows(" 前后空格 ")).toBe(0);
94
+ });
95
+
96
+ test("illegal characters are refused with reason + char + message, and no row is written", async () => {
97
+ for (const [name, reason] of [["a/b", "forbidden_char"], ["a\\b", "forbidden_char"], ["a:b", "forbidden_char"],
98
+ [".hidden", "forbidden_char"], ["..", "forbidden_char"], ["a b", "forbidden_char"], ["a\u0007b", "forbidden_char"],
99
+ ["a\nb", "forbidden_char"], ["a\tb", "forbidden_char"], ["a\u001fb", "forbidden_char"], ["a\u007fb", "forbidden_char"],
100
+ ["a\u2028b", "forbidden_char"], ["a\u2029b", "forbidden_char"],
101
+ ["-x", "leading_dash"], ["测".repeat(65), "too_long"]] as const) {
102
+ const r = await create(name);
103
+ expect({ name, error: r.error, reason: r.reason }).toEqual({ name, error: "node_name_invalid", reason });
104
+ expect(String(r.message ?? "").length).toBeGreaterThan(0);
105
+ expect(rows(name)).toBe(0);
106
+ }
107
+ const slash = await create("a/b");
108
+ expect(slash.char).toBe("/");
109
+ expect(slash.message).toContain("/");
110
+ });
111
+
112
+ test("uniqueness: a second in-flight request for the same Chinese name is a conflict", async () => {
113
+ const first = await create("重复名字");
114
+ expect(first.ok).toBe(true);
115
+ const second = await create("重复名字");
116
+ expect(second.ok).toBe(false);
117
+ expect(second.error).toBe("node_name_conflict");
118
+ expect(rows("重复名字")).toBe(1);
119
+ });
120
+ });
@@ -1,25 +1,39 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import {
3
3
  ValidationError,
4
- validateName, validateRuntime, validateModel, validateFlagValue,
4
+ validateName, validateRuntime, validateModel, validateFlagValue, RUNTIMES,
5
5
  validateEnvRefs, validateChannelsP1, serializeEnvLocal, buildAnetArgs,
6
- MAX_ENV_KEYS_PER_NODE,
6
+ MAX_ENV_KEYS_PER_NODE, validateFlagsForRuntime, validateOpenCodeV2ProviderModel, COPRESENCE_FLAG_RUNTIMES,
7
7
  } from "./create-node-validate.js";
8
8
 
9
9
  const okSecret = (k: string, _net: string, key: string) => k === key ? `value-of-${k}` : undefined;
10
10
  const allow = (k: string) => new Set<string>([k, "ANTHROPIC_API_KEY", "OPENAI_API_KEY"]);
11
11
 
12
- describe("validateName (§4.2.2)", () => {
13
- test("good names", () => {
14
- for (const n of ["a", "demo-bot", "node_1", "z2", "abc-def-ghi"]) {
12
+ describe("validateName (§4.2.2, #652 Unicode names)", () => {
13
+ test("good names — incl. Chinese, upper case, digits first", () => {
14
+ for (const n of ["a", "demo-bot", "node_1", "z2", "abc-def-ghi", "测试", "研发助手A", "Demo", "1demo", "测".repeat(64)]) {
15
15
  expect(() => validateName(n)).not.toThrow();
16
16
  }
17
17
  });
18
- test("rejects shell-injection-like names", () => {
19
- for (const n of [";rm -rf /", "demo bot", "Demo", "1demo", "-demo", "demo;rm", "demo$INJECT", "x".repeat(65)]) {
18
+ test("returns the trimmed name", () => {
19
+ expect(validateName(" 测试 ")).toBe("测试");
20
+ });
21
+ test("rejects shell-injection-like and path-like names with a reason", () => {
22
+ for (const n of [";rm -rf /", "demo bot", "-demo", "demo;rm", "demo$INJECT", "x".repeat(65), "测".repeat(65),
23
+ "a/b", "a\\b", "a:b", ".hidden", "..", "a\u0000b", "a`id`", "", " "]) {
20
24
  expect(() => validateName(n)).toThrow(ValidationError);
21
25
  }
22
26
  });
27
+ test("error carries reason + offending char + a readable message", () => {
28
+ try { validateName("a/b"); throw new Error("no throw"); }
29
+ catch (e: any) {
30
+ expect(e).toBeInstanceOf(ValidationError);
31
+ expect(e.code).toBe("node_name_invalid");
32
+ expect(e.detail.reason).toBe("forbidden_char");
33
+ expect(e.detail.char).toBe("/");
34
+ expect(String(e.detail.message)).toContain("/");
35
+ }
36
+ });
23
37
  });
24
38
 
25
39
  describe("validateRuntime / validateModel (§4.2.2)", () => {
@@ -35,6 +49,15 @@ describe("validateRuntime / validateModel (§4.2.2)", () => {
35
49
  expect(() => validateModel("gpt-4o")).not.toThrow();
36
50
  expect(() => validateModel("bad model")).toThrow(ValidationError);
37
51
  expect(() => validateModel("x;rm")).toThrow(ValidationError);
52
+ // provider/model(OpenCode 共存;桌面向导 0.2.61 起发这种形状,Vincent 2026-09-08 撞到 model_invalid)
53
+ expect(() => validateModel("opencode/mimo-v2.5-free")).not.toThrow();
54
+ expect(() => validateModel("anthropic/claude-sonnet-4")).not.toThrow();
55
+ expect(() => validateModel("a/b/c")).toThrow(ValidationError); // 只许一个斜杠
56
+ expect(() => validateModel("/model")).toThrow(ValidationError); // 不能以斜杠开头
57
+ expect(() => validateModel("provider/")).toThrow(ValidationError); // 不能以斜杠结尾
58
+ expect(() => validateModel("../x")).toThrow(ValidationError); // 纯点段(路径穿越形状)
59
+ expect(() => validateModel("x/.")).toThrow(ValidationError);
60
+ expect(() => validateModel("open code/m")).toThrow(ValidationError);
38
61
  expect(() => validateModel("")).toThrow(ValidationError);
39
62
  });
40
63
  });
@@ -60,6 +83,30 @@ describe("validateFlagValue (§4.2.2)", () => {
60
83
  expect(() => validateFlagValue("permissionMode", "plan")).not.toThrow();
61
84
  expect(() => validateFlagValue("permissionMode", "anything-else")).toThrow(ValidationError);
62
85
  });
86
+ // #605 —— agent-node 按毫秒读 flags.timeout(默认 300000)。这里曾收 1..86400(秒的形状):
87
+ // 600 原样到子节点 = 0.6 秒超时;600000(10 分钟)反被拒。现在与 update_node_config 同一把尺子。
88
+ test("timeout is milliseconds: 0 or 1000..3_600_000 (#605)", () => {
89
+ for (const v of [0, 1000, 300_000, 600_000, 3_600_000]) {
90
+ expect(() => validateFlagValue("timeout", v)).not.toThrow();
91
+ }
92
+ for (const v of [1, 600, 999, 86_400 * 1000 + 1, 3_600_001, -1, 1500.5, "600000", null]) {
93
+ expect(() => validateFlagValue("timeout", v)).toThrow(ValidationError);
94
+ }
95
+ try { validateFlagValue("timeout", 600); throw new Error("unreachable"); }
96
+ catch (e) {
97
+ expect(e).toBeInstanceOf(ValidationError);
98
+ expect((e as ValidationError).code).toBe("flag_value_invalid");
99
+ expect(String((e as ValidationError).detail?.reason)).toContain("milliseconds");
100
+ }
101
+ });
102
+ test("timeout range matches update_node_config's (#605)", async () => {
103
+ const { validatePatch } = await import("./config-apply-validate.js");
104
+ for (const v of [0, 1, 600, 999, 1000, 600_000, 3_600_000, 3_600_001, -1, 1.5]) {
105
+ let createOk = true;
106
+ try { validateFlagValue("timeout", v); } catch { createOk = false; }
107
+ expect({ v, ok: createOk }).toEqual({ v, ok: validatePatch(undefined, { timeout: v }) === null });
108
+ }
109
+ });
63
110
  });
64
111
 
65
112
  describe("validateEnvRefs (§4.4.7 + B1 G7/G8 sub-cases)", () => {
@@ -169,6 +216,41 @@ describe("buildAnetArgs (§4.2.2 F2 — fully validated argv)", () => {
169
216
  expect(args).toContain("--permission-mode");
170
217
  expect(args[args.indexOf("--permission-mode") + 1]).toBe("plan");
171
218
  });
219
+ test("OpenCode V2 requires provider/model only after unsafe opt-in", () => {
220
+ let unsafe = "";
221
+ try {
222
+ buildAnetArgs({ name: "v2", runtime: "opencode-cli", flags: { opencodeGeneration: "v2" } });
223
+ } catch (e) { unsafe = (e as ValidationError).code; }
224
+ expect(unsafe).toBe("opencode_v2_requires_unsafe_opt_in");
225
+ let missing = "";
226
+ try {
227
+ buildAnetArgs({ name: "v2", runtime: "opencode-cli", flags: { opencodeGeneration: "v2", opencodeUnsafeTools: true } });
228
+ } catch (e) { missing = (e as ValidationError).code; }
229
+ expect(missing).toBe("opencode_v2_requires_provider_model");
230
+ let bare = "";
231
+ try {
232
+ buildAnetArgs({ name: "v2", runtime: "opencode-cli", model: "opencode", flags: { opencodeGeneration: "v2", opencodeUnsafeTools: true } });
233
+ } catch (e) { bare = (e as ValidationError).code; }
234
+ expect(bare).toBe("opencode_v2_requires_provider_model");
235
+ const args = buildAnetArgs({
236
+ name: "v2", runtime: "opencode-cli", model: "stub/model",
237
+ flags: { opencodeGeneration: "v2", opencodeUnsafeTools: true },
238
+ });
239
+ expect(args).toContain("stub/model");
240
+ expect(() => validateOpenCodeV2ProviderModel("opencode-cli", undefined, { opencodeGeneration: "v2" })).not.toThrow();
241
+ expect(() => validateOpenCodeV2ProviderModel("claude-agent-sdk", undefined, { opencodeGeneration: "v2", opencodeUnsafeTools: true })).not.toThrow();
242
+ expect(buildAnetArgs({ name: "x", runtime: "opencode-cli" })).toEqual(["node", "create", "x", "--runtime", "opencode-cli"]);
243
+ });
244
+ test("omitted model is allowed and does not emit --model", () => {
245
+ const args = buildAnetArgs({
246
+ name: "x", runtime: "codex-sdk",
247
+ flags: { permissionMode: "plan" },
248
+ });
249
+ expect(args).toEqual(["node", "create", "x", "--runtime", "codex-sdk", "--permission-mode", "plan"]);
250
+ });
251
+ test("empty model is still rejected", () => {
252
+ expect(() => buildAnetArgs({ name: "x", runtime: "codex-sdk", model: "" })).toThrow(ValidationError);
253
+ });
172
254
  test("rejects bad name with shell metachar", () => {
173
255
  expect(() => buildAnetArgs({ name: ";rm -rf /", runtime: "claude-agent-sdk", model: "x" })).toThrow(ValidationError);
174
256
  });
@@ -185,3 +267,107 @@ describe("buildAnetArgs (§4.2.2 F2 — fully validated argv)", () => {
185
267
  } as any)).toThrow(ValidationError);
186
268
  });
187
269
  });
270
+
271
+ // ─── runtime_invalid 要说清「允许哪些」和「还有什么路」 ────────────────
272
+ //
273
+ // 2026-08-28 实测:从 Dashboard 建一个 codex-app-server 节点,用户拿到的是
274
+ // {"ok":false,"error":"runtime_invalid","value":"codex-app-server"}
275
+ // 而目标机器的 daemon 日志里一行都没有 —— 请求被 hub 拦在最前面,
276
+ // 用户无从知道①哪些允许②是不是打错了③有没有别的办法。
277
+ // 🔴 2026-08-28:这一节原本拿 `codex-app-server` / `opencode-cli` 当"非法 runtime"的例子。
278
+ // #1298 把 hub 的 RUNTIMES 从 3 个放开到 7 个之后,那两个变成了合法值,这两条测试因此红了 ——
279
+ // **测试正确地抓到了行为变更**。修法是换例子,不是削弱断言:
280
+ // 意图仍然是「非法 runtime 要给可操作的报错」,只是例子换成永远不会合法的字符串。
281
+ describe("validateRuntime — 报错要可操作", () => {
282
+ function thrown(v: unknown): any {
283
+ try { validateRuntime(v); } catch (e: any) { return e; }
284
+ throw new Error("expected validateRuntime to throw");
285
+ }
286
+
287
+ test("非法 runtime 的报错带上允许集合", () => {
288
+ const e = thrown("definitely-not-a-runtime");
289
+ expect(e.code).toBe("runtime_invalid");
290
+ expect(Array.isArray(e.detail?.allowed)).toBe(true);
291
+ // 🔴 断言它等于 RUNTIMES 本身,而不是等于一份手抄的清单 ——
292
+ // 手抄的那份会漂,漂了之后报错会理直气壮地告诉用户一组错的名字。
293
+ expect(e.detail.allowed).toEqual([...RUNTIMES]);
294
+ });
295
+
296
+ test("报错带上一条出路提示", () => {
297
+ const e = thrown("another-bogus-runtime");
298
+ expect(typeof e.detail?.hint).toBe("string");
299
+ expect(e.detail.hint.length).toBeGreaterThan(10);
300
+ // 提示必须点名那条真实存在的路,否则它只是安慰话
301
+ expect(e.detail.hint).toContain("anet node create");
302
+ });
303
+
304
+ test("仍然回报用户传进来的那个值(便于识别是不是打错了)", () => {
305
+ const e = thrown("codex-app-servr"); // 故意少一个 e
306
+ expect(e.detail.value).toBe("codex-app-servr");
307
+ });
308
+
309
+ test("🔴 合法 runtime 一个都不能被这次改动误伤", () => {
310
+ for (const r of RUNTIMES) {
311
+ expect(() => validateRuntime(r)).not.toThrow();
312
+ }
313
+ // 正控:非字符串必须仍然被拒 —— 否则上面那条循环全过也说明不了什么
314
+ expect(() => validateRuntime(123 as any)).toThrow();
315
+ expect(() => validateRuntime(undefined as any)).toThrow();
316
+ });
317
+ });
318
+
319
+ // #1298 —— daemon 远程创建放开到 7 个 runtime(Vincent 2026-08-28 定,
320
+ // 理由是兜底排错:节点不动了,人要能进去跟 Codex / Claude Code 对话)。
321
+ describe("#1298 RUNTIMES —— 七个都放行,且与 CLI 侧一致", () => {
322
+ test("三个共存 runtime 不再被拒", () => {
323
+ for (const r of ["codex-app-server", "grok-build-cli", "opencode-cli"]) {
324
+ expect(() => validateRuntime(r)).not.toThrow();
325
+ }
326
+ });
327
+ test("原有四个仍然放行(放开不该顺手改坏别的)", () => {
328
+ for (const r of ["claude-agent-sdk", "claude-code-cli", "codex-sdk", "grok-build-acp"]) {
329
+ expect(() => validateRuntime(r)).not.toThrow();
330
+ }
331
+ });
332
+ test("🔴 恰好七个 —— 多一个少一个都要有人解释", () => {
333
+ expect(RUNTIMES.length).toBe(7);
334
+ // 正控:不在名单里的仍然被拒,证明上面不是"什么都放行"
335
+ expect(() => validateRuntime("definitely-not-a-runtime")).toThrow();
336
+ expect(() => validateRuntime("")).toThrow();
337
+ });
338
+ });
339
+
340
+ // #584 —— app「Codex(TUI 共存)」经 daemon 建出来的是无头节点:create_node 没有任何字段能表达「要共存」。
341
+ describe("#584 flags.copresence —— 建节点时的共存开关", () => {
342
+ test("is a known flag key and must be boolean", () => {
343
+ expect(() => validateFlagValue("copresence", true)).not.toThrow();
344
+ expect(() => validateFlagValue("copresence", false)).not.toThrow();
345
+ for (const bad of ["true", 1, null, {}]) {
346
+ try { validateFlagValue("copresence", bad); throw new Error("did not throw"); }
347
+ catch (e) { expect((e as ValidationError).code).toBe("flag_value_invalid"); }
348
+ }
349
+ });
350
+ test("only codex-app-server accepts it — elsewhere it would be swallowed silently", () => {
351
+ expect(() => validateFlagsForRuntime("codex-app-server", { copresence: true })).not.toThrow();
352
+ expect(() => validateFlagsForRuntime("claude-agent-sdk", { permissionMode: "default" })).not.toThrow();
353
+ expect(() => validateFlagsForRuntime("claude-agent-sdk", undefined)).not.toThrow();
354
+ for (const rt of ["claude-agent-sdk", "claude-code-cli", "codex-sdk", "grok-build-acp", "grok-build-cli", "opencode-cli"]) {
355
+ try { validateFlagsForRuntime(rt, { copresence: true }); throw new Error(`did not throw for ${rt}`); }
356
+ catch (e) {
357
+ expect((e as ValidationError).code).toBe("flag_not_applicable_to_runtime");
358
+ expect((e as ValidationError).detail).toMatchObject({ field: "copresence", runtime: rt, applicable: [...COPRESENCE_FLAG_RUNTIMES] });
359
+ }
360
+ }
361
+ });
362
+ test("buildAnetArgs emits the CLI's boolean `--copresence` (no value), and nothing for false", () => {
363
+ expect(buildAnetArgs({ name: "cx", runtime: "codex-app-server", flags: { copresence: true } }))
364
+ .toEqual(["node", "create", "cx", "--runtime", "codex-app-server", "--copresence"]);
365
+ expect(buildAnetArgs({ name: "cx", runtime: "codex-app-server", flags: { copresence: false } }))
366
+ .toEqual(["node", "create", "cx", "--runtime", "codex-app-server"]);
367
+ expect(() => buildAnetArgs({ name: "cx", runtime: "codex-sdk", flags: { copresence: true } })).toThrow(ValidationError);
368
+ });
369
+ test("the old request shape (no flag) is unchanged", () => {
370
+ expect(buildAnetArgs({ name: "cx", runtime: "codex-app-server", flags: { permissionMode: "default" } }))
371
+ .toEqual(["node", "create", "cx", "--runtime", "codex-app-server", "--permission-mode", "default"]);
372
+ });
373
+ });