@sleep2agi/commhub-server 0.9.0-preview.13 → 0.9.0-preview.130

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 (317) hide show
  1. package/README.md +130 -10
  2. package/bin/commhub.ts +99 -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/clone-node.test.ts +248 -0
  37. package/src/clone-node.ts +289 -0
  38. package/src/codex-config-flags.ts +77 -0
  39. package/src/codex-fork-contract.ts +23 -0
  40. package/src/codex-fork-migration.test.ts +38 -0
  41. package/src/commhub-bin-args.test.ts +85 -0
  42. package/src/config-apply-sec1.test.ts +107 -0
  43. package/src/config-apply-validate.test.ts +134 -1
  44. package/src/config-apply-validate.ts +111 -5
  45. package/src/create-node-blocked-gate.test.ts +356 -0
  46. package/src/create-node-network-id.test.ts +79 -0
  47. package/src/create-node-tool-schema.test.ts +48 -0
  48. package/src/create-node-unicode-name.test.ts +120 -0
  49. package/src/create-node-validate.test.ts +193 -7
  50. package/src/create-node-validate.ts +227 -15
  51. package/src/create-node-workdir.test.ts +289 -0
  52. package/src/create-node.ts +77 -23
  53. package/src/daemon-provider-snapshots.test.ts +421 -0
  54. package/src/daemon-provider-snapshots.ts +327 -0
  55. package/src/daemon-token-owner-http.test.ts +236 -0
  56. package/src/dashboard-slash-routing-http.test.ts +107 -0
  57. package/src/db-adapter-guard.test.ts +148 -0
  58. package/src/db-adapter-translate.test.ts +128 -0
  59. package/src/db-adapter.ts +323 -87
  60. package/src/db-timestamp.test.ts +38 -0
  61. package/src/db-timestamp.ts +15 -0
  62. package/src/db.ts +930 -35
  63. package/src/delivered-without-push.test.ts +140 -0
  64. package/src/department-groups-http.test.ts +209 -0
  65. package/src/department-groups-sync-http.test.ts +328 -0
  66. package/src/department-groups.ts +434 -0
  67. package/src/department-heads-http.test.ts +330 -0
  68. package/src/department-heads.ts +226 -0
  69. package/src/departments-http.test.ts +185 -0
  70. package/src/departments.ts +260 -0
  71. package/src/desktop-message-seam.test.ts +150 -0
  72. package/src/desktop-message.test.ts +368 -0
  73. package/src/dm-file-scope-http.test.ts +178 -0
  74. package/src/ext-token-shared.test.ts +60 -0
  75. package/src/ext-validation-regression.test.ts +131 -0
  76. package/src/external-schedule-edits-http.test.ts +321 -0
  77. package/src/external-schedule-edits-timestamps.test.ts +64 -0
  78. package/src/external-schedule-edits.ts +327 -0
  79. package/src/file-download-authz-dev-open.test.ts +135 -0
  80. package/src/file-download-authz.test.ts +404 -0
  81. package/src/file-download-date-bucket.test.ts +436 -0
  82. package/src/file-network-scope-dev-open.test.ts +97 -0
  83. package/src/file-network-scope.test.ts +732 -0
  84. package/src/group-membership-events-http.test.ts +264 -0
  85. package/src/group-messages-http.test.ts +482 -0
  86. package/src/group-messages.ts +297 -0
  87. package/src/health-redaction.test.ts +642 -0
  88. package/src/http-gzip.test.ts +94 -0
  89. package/src/http-gzip.ts +112 -0
  90. package/src/hub-provider-config.test.ts +139 -0
  91. package/src/hub-provider-config.ts +105 -0
  92. package/src/hub-timestamp.test.ts +38 -0
  93. package/src/hub-timestamp.ts +27 -0
  94. package/src/human-dm.ts +215 -0
  95. package/src/inbox-caller.ts +49 -0
  96. package/src/inbox-count-sse.test.ts +310 -0
  97. package/src/inbox-count.ts +29 -0
  98. package/src/index.ts +15 -2362
  99. package/src/lifecycle-guard.ts +1 -1
  100. package/src/mcp-context-cost-http.test.ts +123 -0
  101. package/src/mcp-projects-events-http.test.ts +162 -0
  102. package/src/mcp-route-label-http.test.ts +120 -0
  103. package/src/mcp-write-network-resolution.test.ts +432 -0
  104. package/src/member-audit-network-id-http.test.ts +93 -0
  105. package/src/member-presence-http.test.ts +210 -0
  106. package/src/messages-ack-by-agent-http.test.ts +142 -0
  107. package/src/migrate-sqlite-to-pg.test.ts +47 -0
  108. package/src/migrate-sqlite-to-pg.ts +263 -0
  109. package/src/model-auth-notify-http.test.ts +181 -0
  110. package/src/model-auth-notify.ts +116 -0
  111. package/src/network-name-http.test.ts +75 -0
  112. package/src/network-scope.ts +276 -0
  113. package/src/network-token-membership-http.test.ts +139 -0
  114. package/src/node-alias-control.ts +12 -0
  115. package/src/node-attrs-validate.ts +88 -0
  116. package/src/node-attrs.test.ts +217 -0
  117. package/src/node-caller-identity-http.test.ts +781 -0
  118. package/src/node-create-request-status-http.test.ts +105 -0
  119. package/src/node-daemon-bindings.test.ts +132 -0
  120. package/src/node-daemon-bindings.ts +130 -0
  121. package/src/node-files-transport.test.ts +344 -0
  122. package/src/node-health-codex-login.test.ts +120 -0
  123. package/src/node-health-dispatch-http.test.ts +279 -0
  124. package/src/node-health-guard.ts +105 -0
  125. package/src/node-health-passthrough.test.ts +115 -0
  126. package/src/node-health-store.ts +101 -0
  127. package/src/node-id-alias.test.ts +190 -0
  128. package/src/node-identity-conflict-http.test.ts +269 -0
  129. package/src/node-identity-conflict.ts +142 -0
  130. package/src/node-identity-mismatch.test.ts +74 -0
  131. package/src/node-lifecycle-controllable.test.ts +64 -0
  132. package/src/node-lifecycle-controllable.ts +53 -0
  133. package/src/node-lifecycle-read-http.test.ts +245 -0
  134. package/src/node-lifecycle-read.ts +121 -0
  135. package/src/node-logs-transport.test.ts +263 -0
  136. package/src/node-permissions-http.test.ts +276 -0
  137. package/src/node-permissions.ts +285 -0
  138. package/src/node-request-retention.test.ts +281 -0
  139. package/src/node-request-retention.ts +120 -0
  140. package/src/node-skills-transport.test.ts +209 -0
  141. package/src/node-token-ownership.ts +51 -0
  142. package/src/node-token-privilege-http.test.ts +158 -0
  143. package/src/observer-avatar-http.test.ts +431 -0
  144. package/src/observer-push.test.ts +252 -0
  145. package/src/opencode-v2-catalog.test.ts +268 -0
  146. package/src/opencode-v2-catalog.ts +352 -0
  147. package/src/package-version-consistency.test.ts +25 -0
  148. package/src/peer-reply-atomic.test.ts +455 -0
  149. package/src/peer-reply-capability.test.ts +70 -0
  150. package/src/pg-worker.ts +87 -0
  151. package/src/project-dir-warn-clear-http.test.ts +132 -0
  152. package/src/project-viewer-can-http.test.ts +114 -0
  153. package/src/push.ts +429 -10
  154. package/src/redact-tokens.test.ts +65 -0
  155. package/src/redact-tokens.ts +40 -0
  156. package/src/rename.ts +22 -2
  157. package/src/reply-target-derivation.test.ts +96 -0
  158. package/src/report-status-handover-capabilities-http.test.ts +124 -0
  159. package/src/report-status-host-schema.test.ts +235 -0
  160. package/src/report-status-resume-id-handover.test.ts +150 -0
  161. package/src/require-explicit-test-db.ts +9 -0
  162. package/src/requirement-due-reminders-http.test.ts +620 -0
  163. package/src/requirement-due-reminders.ts +416 -0
  164. package/src/requirement-events-http.test.ts +268 -0
  165. package/src/requirement-events.test.ts +45 -0
  166. package/src/requirement-events.ts +144 -0
  167. package/src/requirement-notify.ts +165 -0
  168. package/src/requirement-participant-notify-http.test.ts +309 -0
  169. package/src/requirement-tag-ops-http.test.ts +266 -0
  170. package/src/requirement-tags.ts +89 -0
  171. package/src/requirements-abandoned-http.test.ts +236 -0
  172. package/src/requirements-comment-http.test.ts +167 -0
  173. package/src/requirements-completed-at-migration.test.ts +70 -0
  174. package/src/requirements-errors-mcp-http.test.ts +164 -0
  175. package/src/requirements-errors.ts +143 -0
  176. package/src/requirements-etag-http.test.ts +91 -0
  177. package/src/requirements-http.test.ts +709 -0
  178. package/src/requirements-last-event-http.test.ts +349 -0
  179. package/src/requirements-list-mcp-slim-http.test.ts +155 -0
  180. package/src/requirements-list-slim-http.test.ts +319 -0
  181. package/src/requirements-mcp.test.ts +163 -0
  182. package/src/requirements-migrate.ts +259 -0
  183. package/src/requirements-owner-strict-mcp-http.test.ts +145 -0
  184. package/src/requirements-people-http.test.ts +114 -0
  185. package/src/requirements-people-mcp-http.test.ts +225 -0
  186. package/src/requirements-people.ts +127 -0
  187. package/src/requirements-priority-lowest.test.ts +114 -0
  188. package/src/requirements-search-http.test.ts +182 -0
  189. package/src/requirements-search.test.ts +32 -0
  190. package/src/requirements-search.ts +145 -0
  191. package/src/requirements-seq-http.test.ts +193 -0
  192. package/src/requirements-seq-migration.test.ts +63 -0
  193. package/src/requirements-start-date.test.ts +125 -0
  194. package/src/requirements-stats-http.test.ts +288 -0
  195. package/src/requirements-stats.ts +167 -0
  196. package/src/requirements-status-participants-http.test.ts +222 -0
  197. package/src/requirements-upgrade-e2e.test.ts +126 -0
  198. package/src/requirements-upgrade-migration.test.ts +98 -0
  199. package/src/requirements.ts +1806 -0
  200. package/src/resolve-port.test.ts +57 -0
  201. package/src/resolve-port.ts +44 -0
  202. package/src/response-charset.test.ts +130 -0
  203. package/src/rest-explicit-columns-http.test.ts +222 -0
  204. package/src/rest-identity.test.ts +129 -0
  205. package/src/rest-identity.ts +125 -0
  206. package/src/rest-projections.ts +68 -0
  207. package/src/rest-write-network-resolution.test.ts +133 -0
  208. package/src/restricted-files.ts +76 -0
  209. package/src/retention.ts +15 -3
  210. package/src/route-callers-http.test.ts +120 -0
  211. package/src/route-timing.test.ts +211 -0
  212. package/src/route-timing.ts +258 -0
  213. package/src/rules-file-duplicate-alias.test.ts +234 -0
  214. package/src/rules-file-session-target.test.ts +207 -0
  215. package/src/rules-file-transport.test.ts +237 -0
  216. package/src/runtime-label.test.ts +22 -0
  217. package/src/runtime-label.ts +21 -0
  218. package/src/runtime-readiness.ts +90 -0
  219. package/src/schedule-agent-mcp-http.test.ts +381 -0
  220. package/src/schedule-agent.ts +192 -0
  221. package/src/scheduled-doorbell-nodeless-session.test.ts +134 -0
  222. package/src/scheduled-failures-http.test.ts +233 -0
  223. package/src/scheduled-failures.ts +179 -0
  224. package/src/scheduled-offline.test.ts +131 -0
  225. package/src/scheduled-offline.ts +66 -0
  226. package/src/scheduled-reply-unread-http.test.ts +212 -0
  227. package/src/scheduled-run-terminal.test.ts +268 -0
  228. package/src/scheduled-skip-blocking-task.test.ts +120 -0
  229. package/src/scheduled-stuck-http.test.ts +238 -0
  230. package/src/scheduled-stuck.ts +153 -0
  231. package/src/scheduled-tasks-http.test.ts +638 -0
  232. package/src/scheduled-tasks.ts +766 -0
  233. package/src/scoped-secrets.ts +124 -0
  234. package/src/send-dedup-request-id.test.ts +229 -0
  235. package/src/send-reply-agent-warning.test.ts +202 -0
  236. package/src/send-reply-alias-optional.test.ts +131 -0
  237. package/src/send-reply-attachments.test.ts +458 -0
  238. package/src/send_dedup.test.ts +54 -0
  239. package/src/send_dedup.ts +52 -6
  240. package/src/serve-error.test.ts +38 -0
  241. package/src/serve-error.ts +27 -0
  242. package/src/server.ts +5257 -0
  243. package/src/session-task-on-dispatch.ts +14 -0
  244. package/src/session-token-ownership-http.test.ts +122 -0
  245. package/src/shared/codex-inventory.ts +41 -0
  246. package/src/shared/daemon-provider-login.ts +31 -0
  247. package/src/shared/daemon-provider-snapshot.ts +52 -0
  248. package/src/shared/external-schedule-contract.ts +60 -0
  249. package/src/shared/node-name.test.ts +62 -0
  250. package/src/shared/node-name.ts +161 -0
  251. package/src/shared/provider-application.ts +29 -0
  252. package/src/shared/provider-collection.ts +14 -0
  253. package/src/shared/provider-initialization.ts +8 -0
  254. package/src/shared/reserved-env-drift.test.ts +10 -0
  255. package/src/shared/scoped-secret-contract.ts +5 -0
  256. package/src/shared/task-content-limit-drift.test.ts +28 -0
  257. package/src/shared/task-content-limit.ts +25 -0
  258. package/src/side-thread-command-transport.test.ts +312 -0
  259. package/src/side-thread-command-transport.ts +318 -0
  260. package/src/side-thread-http-integration.test.ts +233 -0
  261. package/src/side-thread-http.ts +504 -0
  262. package/src/side-thread-production.test.ts +72 -0
  263. package/src/side-thread-production.ts +118 -0
  264. package/src/side-thread.test.ts +1067 -0
  265. package/src/side-thread.ts +2499 -0
  266. package/src/skillhub-http.test.ts +108 -0
  267. package/src/sqlite-close-checkpoint.test.ts +78 -0
  268. package/src/start-node.test.ts +264 -0
  269. package/src/status-alias-filter-http.test.ts +106 -0
  270. package/src/status-alias-resolver-http.test.ts +163 -0
  271. package/src/status-alias-resolver.ts +19 -0
  272. package/src/status-node-id-filter-http.test.ts +161 -0
  273. package/src/status-read-cache-http.test.ts +229 -0
  274. package/src/status-read-cache.ts +122 -0
  275. package/src/stop-delete-node.test.ts +509 -1
  276. package/src/task-access-http.test.ts +402 -0
  277. package/src/task-access.ts +420 -0
  278. package/src/task-auth-origin-http.test.ts +91 -0
  279. package/src/task-auth-origin.test.ts +24 -0
  280. package/src/task-auth-origin.ts +7 -0
  281. package/src/task-consumption.test.ts +482 -0
  282. package/src/task-diagnostic-http.test.ts +149 -0
  283. package/src/task-diagnostic.test.ts +115 -0
  284. package/src/task-diagnostic.ts +118 -0
  285. package/src/task-expiry-notice-http.test.ts +287 -0
  286. package/src/task-expiry-notice.ts +315 -0
  287. package/src/task-idempotency-mcp.test.ts +142 -0
  288. package/src/task-idempotency.test.ts +60 -0
  289. package/src/task-idempotency.ts +75 -0
  290. package/src/task-late-replies.ts +54 -0
  291. package/src/task-late-reply-http.test.ts +152 -0
  292. package/src/task-lifecycle-watcher.test.ts +204 -0
  293. package/src/task-lifecycle-watcher.ts +49 -0
  294. package/src/task-network-resolution-http.test.ts +141 -0
  295. package/src/task-orphan-http.test.ts +287 -0
  296. package/src/task-orphan.ts +119 -0
  297. package/src/task-queue-ahead-http.test.ts +309 -0
  298. package/src/task-queue-ahead-scale.test.ts +236 -0
  299. package/src/task-queue-ahead.ts +186 -0
  300. package/src/task-rejection-log-http.test.ts +160 -0
  301. package/src/task-rejection-log.ts +49 -0
  302. package/src/task-stale-open-http.test.ts +290 -0
  303. package/src/task-stale-open.ts +103 -0
  304. package/src/tasks-pagination-http.test.ts +141 -0
  305. package/src/tool-audience-http.test.ts +220 -0
  306. package/src/tool-audience.ts +118 -0
  307. package/src/tools-list-json-schema.test.ts +67 -0
  308. package/src/tools.ts +3927 -323
  309. package/src/upload-early-reject-guard.test.ts +155 -0
  310. package/src/uploads-http.test.ts +97 -15
  311. package/src/uploads.ts +103 -6
  312. package/src/user-avatar-http.test.ts +74 -0
  313. package/src/user-avatar-migration.test.ts +25 -0
  314. package/src/user-inbox-by-agent-http.test.ts +161 -0
  315. package/src/user-inbox-netid-notnull.test.ts +122 -0
  316. package/src/user-inbox-read-http.test.ts +223 -0
  317. package/src/user-inbox-schema.test.ts +54 -0
@@ -0,0 +1,107 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { mkdtempSync, rmSync } from "fs";
3
+ import { join } from "path";
4
+ import { tmpdir } from "os";
5
+ import { isInteractiveDashboardTask } from "../../agent-node/src/inbox-dispatch";
6
+ import { shouldCreateScheduledGoal } from "../../agent-node/src/goals/routing";
7
+ import { createNetworkTokenForNode, register } from "./auth.js";
8
+ import { db } from "./db.js";
9
+
10
+ const PRIVATE_DB_DIR = mkdtempSync(join(tmpdir(), "anet-dashboard-slash-http-"));
11
+ const TARGET = `dashboard-slash-target-${process.pid}`;
12
+ const TARGET_NODE_ID = `node_dashboard_slash_target_${process.pid}`;
13
+ const TARGET_RESUME_ID = `resume_dashboard_slash_target_${process.pid}`;
14
+ let server: any;
15
+ let base = "";
16
+ let userToken = "";
17
+ let nodeToken = "";
18
+ let networkId = "";
19
+ let requestSequence = 0;
20
+
21
+ // 30s 而不是 bun 默认的 5s:这个 hook 跑 register()(KDF)+bootServer(),
22
+ // 本机空载基线 ~620ms,而 2026-08-17 与 2026-08-31 两次 CI 上它被 5s 上限打死
23
+ // (0 pass / 1 fail / Ran 1 test —— 见 #928、#1627)。5247ms 是**被截断的下界**,
24
+ // 不是测量值,所以余量按基线的 ~48x 取,而不是按那个数取。
25
+ beforeAll(async () => {
26
+ process.env.COMMHUB_DB = process.env.COMMHUB_DB || join(PRIVATE_DB_DIR, "hub.db");
27
+ const username = `dashboard_slash_${Date.now()}`;
28
+ const registered = register(username, "DashboardSlash123!", undefined, "seed");
29
+ expect(registered.ok).toBe(true);
30
+ userToken = registered.token!;
31
+ networkId = registered.network_id!;
32
+ const userId = db.get<{ user_id: string }>("SELECT user_id FROM users WHERE username = ?1", [username])!.user_id;
33
+ const minted = createNetworkTokenForNode(userId, networkId, "dashboard-slash-node");
34
+ expect(minted.ok).toBe(true);
35
+ nodeToken = minted.token!;
36
+ db.run(
37
+ `INSERT INTO sessions (resume_id, alias, status, node_id, network_id, updated_at, last_seen_at)
38
+ VALUES (?1, ?2, 'idle', ?3, ?4, datetime('now'), datetime('now'))`,
39
+ [TARGET_RESUME_ID, TARGET, TARGET_NODE_ID, networkId],
40
+ );
41
+ db.run(
42
+ `INSERT INTO nodes (node_id, node_name, alias, network_id, created_at, updated_at, lifecycle_state)
43
+ VALUES (?1, ?2, ?2, ?3, datetime('now'), datetime('now'), 'active')`,
44
+ [TARGET_NODE_ID, TARGET, networkId],
45
+ );
46
+ const mod: any = await import("./server.js");
47
+ server = mod.bootServer({ port: 0, hostname: "127.0.0.1" });
48
+ base = `http://127.0.0.1:${server.port}`;
49
+ }, 30_000);
50
+
51
+ afterAll(() => {
52
+ try { server?.stop?.(true); } catch {}
53
+ try { rmSync(PRIVATE_DB_DIR, { recursive: true, force: true }); } catch {}
54
+ });
55
+
56
+ async function postAndRead(token: string, from: string, task: string) {
57
+ requestSequence += 1;
58
+ const clientRequestId = `dreq_${requestSequence.toString(16).padStart(32, "0")}`;
59
+ const response = await fetch(`${base}/api/task`, {
60
+ method: "POST",
61
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
62
+ body: JSON.stringify({
63
+ alias: TARGET,
64
+ from,
65
+ network_id: networkId,
66
+ task,
67
+ meta: { source: "dashboard-chat", client_request_id: clientRequestId, auth_origin: "user" },
68
+ }),
69
+ });
70
+ const body = await response.json() as any;
71
+ expect([200, 202]).toContain(response.status);
72
+ const taskId = body.task_id ?? body.message_id;
73
+ const row = db.get<{ content: string; meta_json: string }>(
74
+ "SELECT content, meta_json FROM inbox WHERE id = ?1",
75
+ [taskId],
76
+ );
77
+ expect(row).toBeTruthy();
78
+ return { type: "task", content: row!.content, meta_json: row!.meta_json };
79
+ }
80
+
81
+ describe("real Hub → authenticated Dashboard slash routing", () => {
82
+ test("Dashboard /goal and /loop remain exact native payloads for every runtime", async () => {
83
+ for (const command of ["/goal 更新一下文档", "/loop 5m native loop"] as const) {
84
+ const row = await postAndRead(userToken, "dashboard-user", command);
85
+ expect(row.content).toBe(command);
86
+ expect(isInteractiveDashboardTask(row)).toBe(true);
87
+ for (const runtime of ["codex-app-server", "codex", "claude", "grok", "opencode"] as const) {
88
+ expect(shouldCreateScheduledGoal(row.content, runtime, true)).toBe(false);
89
+ }
90
+ }
91
+ });
92
+
93
+ test("Dashboard /agoal and /aloop use the Agent Network scheduler", async () => {
94
+ for (const command of ["/agoal 5m update docs", "/aloop 5m update docs"] as const) {
95
+ const row = await postAndRead(userToken, "dashboard-user", command);
96
+ expect(isInteractiveDashboardTask(row)).toBe(true);
97
+ expect(shouldCreateScheduledGoal(row.content, "codex-app-server", true)).toBe(true);
98
+ }
99
+ });
100
+
101
+ test("node token cannot forge Dashboard pass-through and keeps legacy compatibility", async () => {
102
+ const row = await postAndRead(nodeToken, "dashboard-slash-node", "/loop 5m legacy automation");
103
+ expect(JSON.parse(row.meta_json).auth_origin).toBe("node");
104
+ expect(isInteractiveDashboardTask(row)).toBe(false);
105
+ expect(shouldCreateScheduledGoal(row.content, "codex-app-server", false)).toBe(true);
106
+ });
107
+ });
@@ -0,0 +1,148 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ assertSafeTestDatabaseEnv,
4
+ redactPgUrl,
5
+ resolveDatabaseTarget,
6
+ } from "./db-adapter";
7
+
8
+ const BASE_ENV: NodeJS.ProcessEnv = { HOME: "/nonexistent" };
9
+
10
+ describe("#435 inherited DATABASE_URL guard", () => {
11
+ for (const databaseUrl of [
12
+ "postgres://user:pw@prod.example:5432/commhub",
13
+ "postgresql://user:pw@prod.example:5432/commhub",
14
+ "sqlite:///tmp/not-an-escape",
15
+ ]) {
16
+ test(`NODE_ENV=test rejects ${databaseUrl.split(":", 1)[0]} DATABASE_URL`, () => {
17
+ expect(() => assertSafeTestDatabaseEnv({
18
+ ...BASE_ENV,
19
+ NODE_ENV: "test",
20
+ DATABASE_URL: databaseUrl,
21
+ })).toThrow(/REFUSING to honor inherited DATABASE_URL/);
22
+ });
23
+ }
24
+
25
+ test("test without DATABASE_URL leaves the existing SQLite guard in charge", () => {
26
+ expect(() => assertSafeTestDatabaseEnv({ ...BASE_ENV, NODE_ENV: "test" })).not.toThrow();
27
+ expect(() => resolveDatabaseTarget({ ...BASE_ENV, NODE_ENV: "test" }))
28
+ .toThrow(/REFUSING to open the default SQLite database/);
29
+ });
30
+
31
+ test("DATABASE_URL refusal wins even when COMMHUB_DB is also set", () => {
32
+ expect(() => resolveDatabaseTarget({
33
+ ...BASE_ENV,
34
+ NODE_ENV: "test",
35
+ DATABASE_URL: "postgres://user:pw@prod.example:5432/commhub",
36
+ COMMHUB_DB: "/tmp/isolated.db",
37
+ })).toThrow(/REFUSING to honor inherited DATABASE_URL/);
38
+ });
39
+
40
+ test("production postgres branch remains unchanged without constructing it", () => {
41
+ const url = "postgres://user:pw@prod.example:5432/commhub";
42
+ expect(resolveDatabaseTarget({
43
+ ...BASE_ENV,
44
+ NODE_ENV: "production",
45
+ DATABASE_URL: url,
46
+ })).toEqual({ kind: "postgres", url });
47
+ });
48
+
49
+ test("unset NODE_ENV still selects postgres", () => {
50
+ const url = "postgresql://user:pw@prod.example:5432/commhub";
51
+ expect(resolveDatabaseTarget({ ...BASE_ENV, DATABASE_URL: url }))
52
+ .toEqual({ kind: "postgres", url });
53
+ });
54
+
55
+ test("explicit SQLite targets remain unchanged in test and production", () => {
56
+ for (const NODE_ENV of ["test", "production"]) {
57
+ expect(resolveDatabaseTarget({
58
+ ...BASE_ENV,
59
+ NODE_ENV,
60
+ COMMHUB_DB: "/tmp/isolated.db",
61
+ })).toEqual({ kind: "sqlite", path: "/tmp/isolated.db" });
62
+ }
63
+ });
64
+
65
+ test("non-server callers cannot silently fall through to the production default SQLite path", () => {
66
+ for (const NODE_ENV of [undefined, "development", "production"]) {
67
+ expect(() => resolveDatabaseTarget({ ...BASE_ENV, NODE_ENV }))
68
+ .toThrow(/explicit COMMHUB_DB|COMMHUB_SERVER=1/);
69
+ }
70
+ });
71
+
72
+ test("the explicit server boot capability permits the canonical default SQLite path", () => {
73
+ expect(resolveDatabaseTarget({
74
+ ...BASE_ENV,
75
+ NODE_ENV: "production",
76
+ COMMHUB_SERVER: "1",
77
+ })).toEqual({ kind: "sqlite", path: "/nonexistent/.commhub/commhub.db" });
78
+ });
79
+
80
+ test("lookalike server capability values fail closed", () => {
81
+ for (const COMMHUB_SERVER of ["", "true", "yes", "01", " 1"] ) {
82
+ expect(() => resolveDatabaseTarget({ ...BASE_ENV, COMMHUB_SERVER }))
83
+ .toThrow(/COMMHUB_SERVER=1/);
84
+ }
85
+ });
86
+ });
87
+
88
+ describe("RFC-039 §9 COMMHUB_TEST_PG_URL (test-only PostgreSQL)", () => {
89
+ const TEST = { ...BASE_ENV, NODE_ENV: "test" };
90
+ const ok = (url: string) => expect(resolveDatabaseTarget({ ...TEST, COMMHUB_TEST_PG_URL: url })).toEqual({ kind: "postgres", url });
91
+ const refused = (url: string, why: RegExp) => {
92
+ let message = "";
93
+ try { resolveDatabaseTarget({ ...TEST, COMMHUB_TEST_PG_URL: url }); } catch (e: any) { message = String(e?.message ?? e); }
94
+ expect(message).toMatch(/REFUSING COMMHUB_TEST_PG_URL/);
95
+ expect(message).toMatch(why);
96
+ // Credentials never reach an error message.
97
+ expect(message).not.toContain("s3cret");
98
+ expect(message).not.toContain("tester");
99
+ };
100
+
101
+ test("loopback host + anet_*_test database is accepted under NODE_ENV=test", () => {
102
+ ok("postgres://tester:s3cret@127.0.0.1:5432/anet_sched_test");
103
+ ok("postgresql://tester:s3cret@localhost/anet_side_thread_test");
104
+ ok("postgres://tester:s3cret@[::1]:6543/anet_x_test?connect_timeout=2&sslmode=disable");
105
+ });
106
+
107
+ test("ignored outside NODE_ENV=test", () => {
108
+ expect(resolveDatabaseTarget({ ...BASE_ENV, NODE_ENV: "production", COMMHUB_DB: "/tmp/i.db", COMMHUB_TEST_PG_URL: "postgres://tester:s3cret@127.0.0.1/anet_x_test" }))
109
+ .toEqual({ kind: "sqlite", path: "/tmp/i.db" });
110
+ });
111
+
112
+ test("inherited DATABASE_URL still refuses even when COMMHUB_TEST_PG_URL is set", () => {
113
+ expect(() => resolveDatabaseTarget({
114
+ ...TEST,
115
+ DATABASE_URL: "postgres://user:pw@prod.example:5432/commhub",
116
+ COMMHUB_TEST_PG_URL: "postgres://tester:s3cret@127.0.0.1/anet_x_test",
117
+ })).toThrow(/REFUSING to honor inherited DATABASE_URL/);
118
+ });
119
+
120
+ test("refuses a value that is not a URL or not postgres", () => {
121
+ refused("not a url", /not a URL/);
122
+ refused("mysql://tester:s3cret@127.0.0.1/anet_x_test", /scheme/);
123
+ });
124
+
125
+ test("refuses every host that is not literally loopback", () => {
126
+ for (const host of ["10.0.0.5", "db.internal", "localhost.evil.example", "127.0.0.2", "127.0.0.1,evil.example", "LOCALHOST", "%2Ftmp"]) {
127
+ refused(`postgres://tester:s3cret@${host}/anet_x_test`, /host must be/);
128
+ }
129
+ });
130
+
131
+ test("refuses database names outside /^anet_[a-z0-9_]*_test$/", () => {
132
+ for (const name of ["commhub", "anet_x", "anet_x_test2", "ANET_X_TEST", "anet-x_test", "anet_x_test/extra", ""]) {
133
+ refused(`postgres://tester:s3cret@127.0.0.1/${name}`, /database name/);
134
+ }
135
+ });
136
+
137
+ test("refuses connection-steering query parameters", () => {
138
+ refused("postgres://tester:s3cret@127.0.0.1/anet_x_test?host=db.internal", /"host" is not allowed/);
139
+ refused("postgres://tester:s3cret@127.0.0.1/anet_x_test?options=-csearch_path%3Dprod", /"options" is not allowed/);
140
+ });
141
+
142
+ test("redactPgUrl drops user and password", () => {
143
+ const out = redactPgUrl("postgres://tester:s3cret@127.0.0.1:5432/anet_x_test");
144
+ expect(out).not.toContain("tester");
145
+ expect(out).not.toContain("s3cret");
146
+ expect(out).toContain("127.0.0.1:5432/anet_x_test");
147
+ });
148
+ });
@@ -0,0 +1,128 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { Database } from "bun:sqlite";
3
+ import { PG_EXPERIMENTAL_ENV, PgAdapter, SQLiteAdapter, splitSqlStatements, sqliteToPostgres } from "./db-adapter";
4
+
5
+ const UTC_NOW = "to_char((NOW()) AT TIME ZONE 'UTC', 'YYYY-MM-DD HH24:MI:SS')";
6
+
7
+ describe("RFC-039 S2a sqliteToPostgres", () => {
8
+ test("datetime('now') stays SQLite-format UTC text", () => {
9
+ expect(sqliteToPostgres("SELECT datetime('now')")).toBe(`SELECT ${UTC_NOW}`);
10
+ });
11
+
12
+ test("DEFAULT (datetime('now')) keeps the column TEXT", () => {
13
+ const out = sqliteToPostgres("created_at TEXT NOT NULL DEFAULT (datetime('now'))");
14
+ expect(out).toBe(`created_at TEXT NOT NULL DEFAULT (${UTC_NOW})`);
15
+ expect(out).not.toContain("TIMESTAMP");
16
+ });
17
+
18
+ test("strftime with milliseconds keeps SQLite's %f shape", () => {
19
+ expect(sqliteToPostgres("VALUES (strftime('%Y-%m-%d %H:%M:%f', 'now'))")).toBe(
20
+ "VALUES (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD HH24:MI:SS.MS'))",
21
+ );
22
+ });
23
+
24
+ test("literal offset", () => {
25
+ expect(sqliteToPostgres("x < datetime('now', '+14 days')")).toBe(
26
+ "x < to_char((NOW() + INTERVAL '14 days') AT TIME ZONE 'UTC', 'YYYY-MM-DD HH24:MI:SS')",
27
+ );
28
+ });
29
+
30
+ test("parameter offset is cast so PG can infer its type", () => {
31
+ expect(sqliteToPostgres("x < datetime('now', ?2)")).toBe(
32
+ "x < to_char((NOW() + CAST($2 AS TEXT)::INTERVAL) AT TIME ZONE 'UTC', 'YYYY-MM-DD HH24:MI:SS')",
33
+ );
34
+ });
35
+
36
+ test("INTEGER → BIGINT (epoch-ms columns), identifiers containing integer untouched", () => {
37
+ expect(sqliteToPostgres("created_at INTEGER NOT NULL, integer_ref TEXT")).toBe("created_at BIGINT NOT NULL, integer_ref TEXT");
38
+ expect(sqliteToPostgres("CAST(x AS INTEGER)")).toBe("CAST(x AS BIGINT)");
39
+ });
40
+
41
+ test("REAL → DOUBLE PRECISION (SQLite REAL is 8-byte), identifiers containing real untouched", () => {
42
+ expect(sqliteToPostgres("mem_used_gb REAL, realm TEXT")).toBe("mem_used_gb DOUBLE PRECISION, realm TEXT");
43
+ expect(sqliteToPostgres("CAST(x AS REAL)")).toBe("CAST(x AS DOUBLE PRECISION)");
44
+ });
45
+
46
+ test("BLOB → BYTEA, identifiers containing blob untouched", () => {
47
+ expect(sqliteToPostgres("iv BLOB NOT NULL, blob_ref TEXT")).toBe("iv BYTEA NOT NULL, blob_ref TEXT");
48
+ });
49
+
50
+ test("placeholders and AUTOINCREMENT unchanged in behaviour", () => {
51
+ expect(sqliteToPostgres("SELECT * FROM t WHERE a = ?1 AND b = ?2")).toBe("SELECT * FROM t WHERE a = $1 AND b = $2");
52
+ expect(sqliteToPostgres("a = ? AND b = ?")).toBe("a = $1 AND b = $2");
53
+ expect(sqliteToPostgres("id INTEGER PRIMARY KEY AUTOINCREMENT")).toBe("id BIGSERIAL PRIMARY KEY");
54
+ });
55
+ });
56
+
57
+ describe("RFC-039 S2a splitSqlStatements", () => {
58
+ test("a `;` inside a -- comment does not split (db.ts first schema block)", () => {
59
+ expect(splitSqlStatements("CREATE TABLE a (x TEXT);\n-- note; more words\nCREATE TABLE b (y TEXT);")).toEqual([
60
+ "CREATE TABLE a (x TEXT)",
61
+ "CREATE TABLE b (y TEXT)",
62
+ ]);
63
+ });
64
+
65
+ test("block comments are dropped", () => {
66
+ expect(splitSqlStatements("SELECT 1 /* a; b */ ; SELECT 2")).toEqual(["SELECT 1", "SELECT 2"]);
67
+ });
68
+
69
+ test("quoted strings and identifiers keep their `;` and doubled quotes", () => {
70
+ expect(splitSqlStatements(`INSERT INTO t VALUES ('a;b', 'it''s;'); SELECT "c;d" FROM t`)).toEqual([
71
+ `INSERT INTO t VALUES ('a;b', 'it''s;')`,
72
+ `SELECT "c;d" FROM t`,
73
+ ]);
74
+ });
75
+
76
+ test("a plpgsql $$ body stays one statement", () => {
77
+ const fn = "CREATE FUNCTION f() RETURNS trigger AS $$\nBEGIN\n INSERT INTO t VALUES (1);\n RETURN NEW;\nEND;\n$$ LANGUAGE plpgsql";
78
+ expect(splitSqlStatements(`${fn};\nDROP TRIGGER IF EXISTS x ON t;`)).toEqual([fn, "DROP TRIGGER IF EXISTS x ON t"]);
79
+ });
80
+
81
+ test("tagged dollar quotes; $1 placeholders are not dollar quotes", () => {
82
+ expect(splitSqlStatements("SELECT $body$a;b$body$; UPDATE t SET a = $1 WHERE b = $2")).toEqual([
83
+ "SELECT $body$a;b$body$",
84
+ "UPDATE t SET a = $1 WHERE b = $2",
85
+ ]);
86
+ });
87
+
88
+ test("empty statements are dropped", () => {
89
+ expect(splitSqlStatements(" ; ;\n SELECT 1 ;; ")).toEqual(["SELECT 1"]);
90
+ });
91
+ });
92
+
93
+ describe("RFC-039 transactional feature gates", () => {
94
+ // A PgAdapter without its constructor: no worker, no connection.
95
+ function pgWith(atomic: boolean) {
96
+ const pg = Object.create(PgAdapter.prototype);
97
+ Object.defineProperty(pg, "atomicTransactions", { value: atomic });
98
+ return pg as PgAdapter;
99
+ }
100
+ function withFlag<T>(value: string | undefined, fn: () => T): T {
101
+ const saved = process.env[PG_EXPERIMENTAL_ENV];
102
+ if (value === undefined) delete process.env[PG_EXPERIMENTAL_ENV];
103
+ else process.env[PG_EXPERIMENTAL_ENV] = value;
104
+ try { return fn(); } finally {
105
+ if (saved === undefined) delete process.env[PG_EXPERIMENTAL_ENV];
106
+ else process.env[PG_EXPERIMENTAL_ENV] = saved;
107
+ }
108
+ }
109
+
110
+ test("SQLite: open, flag irrelevant", () => {
111
+ const sqlite = new SQLiteAdapter(new Database(":memory:"));
112
+ expect(withFlag(undefined, () => sqlite.transactionalFeaturesRefusal)).toBeNull();
113
+ expect(withFlag("1", () => sqlite.transactionalFeaturesRefusal)).toBeNull();
114
+ sqlite.close();
115
+ });
116
+
117
+ test("PostgreSQL with real transactions: closed by default, message names the variable", () => {
118
+ const refusal = withFlag(undefined, () => pgWith(true).transactionalFeaturesRefusal);
119
+ expect(refusal).toContain(`${PG_EXPERIMENTAL_ENV}=1`);
120
+ expect(refusal).toContain("experimental");
121
+ expect(withFlag("true", () => pgWith(true).transactionalFeaturesRefusal)).not.toBeNull();
122
+ });
123
+
124
+ test("PostgreSQL: opens only with atomic transactions AND the opt-in", () => {
125
+ expect(withFlag("1", () => pgWith(true).transactionalFeaturesRefusal)).toBeNull();
126
+ expect(withFlag("1", () => pgWith(false).transactionalFeaturesRefusal)).toContain("not atomic");
127
+ });
128
+ });