@sleep2agi/commhub-server 0.9.0-preview.0 → 0.9.0-preview.100

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (273) hide show
  1. package/README.md +130 -10
  2. package/bin/commhub.ts +92 -11
  3. package/package.json +8 -7
  4. package/src/ack-create-request-transport.test.ts +159 -0
  5. package/src/ack-create-request.test.ts +331 -0
  6. package/src/admin-delete-network-http.test.ts +228 -0
  7. package/src/admin-networks-http.test.ts +88 -0
  8. package/src/agent-access-batch.test.ts +210 -0
  9. package/src/agent-access.ts +487 -0
  10. package/src/agent-acl-groups-http.test.ts +247 -0
  11. package/src/agent-acl-http.test.ts +520 -0
  12. package/src/agent-acl-slice3-http.test.ts +267 -0
  13. package/src/agent-groups-perf.test.ts +190 -0
  14. package/src/agent-notice.ts +48 -0
  15. package/src/alias-filter.test.ts +72 -0
  16. package/src/alias-filter.ts +42 -0
  17. package/src/api-host-supervisors-can-create-nodes.test.ts +350 -0
  18. package/src/api-host-supervisors-fallback.test.ts +271 -0
  19. package/src/api-nodes-shape.test.ts +188 -0
  20. package/src/auth-kdf-migration.test.ts +378 -0
  21. package/src/auth-sessions-http.test.ts +250 -0
  22. package/src/auth-tokens.test.ts +18 -9
  23. package/src/auth-validate.test.ts +5 -1
  24. package/src/auth.ts +554 -38
  25. package/src/avatar-validate.test.ts +163 -0
  26. package/src/avatar-validate.ts +103 -0
  27. package/src/blocked-exit-on-reply.test.ts +122 -0
  28. package/src/boot-side-effect.test.ts +124 -0
  29. package/src/commhub-bin-args.test.ts +85 -0
  30. package/src/config-apply-sec1.test.ts +830 -0
  31. package/src/config-apply-validate.test.ts +352 -0
  32. package/src/config-apply-validate.ts +249 -0
  33. package/src/create-node-blocked-gate.test.ts +356 -0
  34. package/src/create-node-tool-schema.test.ts +39 -0
  35. package/src/create-node-validate.test.ts +275 -0
  36. package/src/create-node-validate.ts +288 -0
  37. package/src/create-node-workdir.test.ts +255 -0
  38. package/src/create-node.test.ts +354 -0
  39. package/src/create-node.ts +349 -0
  40. package/src/cross-tenant-injection.test.ts +110 -0
  41. package/src/dashboard-slash-routing-http.test.ts +107 -0
  42. package/src/db-adapter-guard.test.ts +148 -0
  43. package/src/db-adapter-translate.test.ts +128 -0
  44. package/src/db-adapter.ts +323 -87
  45. package/src/db-timestamp.test.ts +38 -0
  46. package/src/db-timestamp.ts +15 -0
  47. package/src/db.ts +1268 -42
  48. package/src/delivered-without-push.test.ts +140 -0
  49. package/src/department-groups-http.test.ts +209 -0
  50. package/src/department-groups-sync-http.test.ts +328 -0
  51. package/src/department-groups.ts +434 -0
  52. package/src/department-heads-http.test.ts +329 -0
  53. package/src/department-heads.ts +226 -0
  54. package/src/departments-http.test.ts +185 -0
  55. package/src/departments.ts +260 -0
  56. package/src/desktop-message-seam.test.ts +150 -0
  57. package/src/desktop-message.test.ts +368 -0
  58. package/src/dm-file-scope-http.test.ts +178 -0
  59. package/src/ext-token-shared.test.ts +60 -0
  60. package/src/ext-validation-regression.test.ts +131 -0
  61. package/src/external-schedule-edits-http.test.ts +321 -0
  62. package/src/external-schedule-edits-timestamps.test.ts +64 -0
  63. package/src/external-schedule-edits.ts +327 -0
  64. package/src/file-download-authz-dev-open.test.ts +135 -0
  65. package/src/file-download-authz.test.ts +404 -0
  66. package/src/file-download-date-bucket.test.ts +436 -0
  67. package/src/file-network-scope-dev-open.test.ts +97 -0
  68. package/src/file-network-scope.test.ts +732 -0
  69. package/src/group-membership-events-http.test.ts +264 -0
  70. package/src/group-messages-http.test.ts +482 -0
  71. package/src/group-messages.ts +297 -0
  72. package/src/health-redaction.test.ts +642 -0
  73. package/src/http-gzip.test.ts +94 -0
  74. package/src/http-gzip.ts +112 -0
  75. package/src/hub-timestamp.test.ts +38 -0
  76. package/src/hub-timestamp.ts +27 -0
  77. package/src/human-dm.ts +215 -0
  78. package/src/inbox-count-sse.test.ts +310 -0
  79. package/src/inbox-count.ts +29 -0
  80. package/src/index.ts +15 -2128
  81. package/src/lifecycle-guard.ts +50 -0
  82. package/src/list-host-supervisors.test.ts +314 -0
  83. package/src/mcp-context-cost-http.test.ts +119 -0
  84. package/src/mcp-projects-events-http.test.ts +162 -0
  85. package/src/mcp-route-label-http.test.ts +120 -0
  86. package/src/mcp-write-network-resolution.test.ts +429 -0
  87. package/src/member-audit-network-id-http.test.ts +93 -0
  88. package/src/member-presence-http.test.ts +206 -0
  89. package/src/messages-ack-by-agent-http.test.ts +142 -0
  90. package/src/migrate-sqlite-to-pg.test.ts +47 -0
  91. package/src/migrate-sqlite-to-pg.ts +263 -0
  92. package/src/model-auth-notify-http.test.ts +181 -0
  93. package/src/model-auth-notify.ts +116 -0
  94. package/src/network-name-http.test.ts +75 -0
  95. package/src/network-scope.ts +224 -0
  96. package/src/network-token-membership-http.test.ts +139 -0
  97. package/src/node-attrs-validate.ts +88 -0
  98. package/src/node-attrs.test.ts +217 -0
  99. package/src/node-create-request-status-http.test.ts +92 -0
  100. package/src/node-files-transport.test.ts +344 -0
  101. package/src/node-health-dispatch-http.test.ts +279 -0
  102. package/src/node-health-guard.ts +105 -0
  103. package/src/node-health-passthrough.test.ts +115 -0
  104. package/src/node-health-store.ts +72 -0
  105. package/src/node-id-alias.test.ts +188 -0
  106. package/src/node-identity-conflict-http.test.ts +269 -0
  107. package/src/node-identity-conflict.ts +142 -0
  108. package/src/node-identity-mismatch.test.ts +74 -0
  109. package/src/node-lifecycle-controllable.test.ts +64 -0
  110. package/src/node-lifecycle-controllable.ts +51 -0
  111. package/src/node-logs-transport.test.ts +263 -0
  112. package/src/node-permissions-http.test.ts +276 -0
  113. package/src/node-permissions.ts +242 -0
  114. package/src/node-request-retention.test.ts +281 -0
  115. package/src/node-request-retention.ts +120 -0
  116. package/src/node-skills-transport.test.ts +209 -0
  117. package/src/node-token-privilege-http.test.ts +149 -0
  118. package/src/observer-avatar-http.test.ts +431 -0
  119. package/src/observer-push.test.ts +252 -0
  120. package/src/package-version-consistency.test.ts +25 -0
  121. package/src/peer-reply-atomic.test.ts +455 -0
  122. package/src/peer-reply-capability.test.ts +70 -0
  123. package/src/pg-worker.ts +87 -0
  124. package/src/probe-validate.test.ts +221 -0
  125. package/src/probe-validate.ts +104 -0
  126. package/src/probe.test.ts +80 -0
  127. package/src/probe.ts +200 -0
  128. package/src/project-viewer-can-http.test.ts +114 -0
  129. package/src/push.ts +429 -10
  130. package/src/redact-tokens.test.ts +65 -0
  131. package/src/redact-tokens.ts +40 -0
  132. package/src/rename.ts +7 -2
  133. package/src/reply-target-derivation.test.ts +96 -0
  134. package/src/report-status-host-schema.test.ts +211 -0
  135. package/src/report-status-resume-id-handover.test.ts +150 -0
  136. package/src/require-explicit-test-db.ts +9 -0
  137. package/src/requirement-due-reminders-http.test.ts +390 -0
  138. package/src/requirement-due-reminders.ts +310 -0
  139. package/src/requirement-events-http.test.ts +257 -0
  140. package/src/requirement-events.test.ts +45 -0
  141. package/src/requirement-events.ts +144 -0
  142. package/src/requirement-notify.ts +165 -0
  143. package/src/requirement-participant-notify-http.test.ts +309 -0
  144. package/src/requirement-tag-ops-http.test.ts +266 -0
  145. package/src/requirement-tags.ts +89 -0
  146. package/src/requirements-comment-http.test.ts +165 -0
  147. package/src/requirements-completed-at-migration.test.ts +70 -0
  148. package/src/requirements-errors-mcp-http.test.ts +164 -0
  149. package/src/requirements-errors.ts +143 -0
  150. package/src/requirements-etag-http.test.ts +91 -0
  151. package/src/requirements-http.test.ts +709 -0
  152. package/src/requirements-last-event-http.test.ts +344 -0
  153. package/src/requirements-list-mcp-slim-http.test.ts +155 -0
  154. package/src/requirements-list-slim-http.test.ts +319 -0
  155. package/src/requirements-mcp.test.ts +163 -0
  156. package/src/requirements-migrate.ts +225 -0
  157. package/src/requirements-owner-strict-mcp-http.test.ts +145 -0
  158. package/src/requirements-people-http.test.ts +114 -0
  159. package/src/requirements-people-mcp-http.test.ts +225 -0
  160. package/src/requirements-people.ts +127 -0
  161. package/src/requirements-priority-lowest.test.ts +114 -0
  162. package/src/requirements-search-http.test.ts +182 -0
  163. package/src/requirements-search.test.ts +32 -0
  164. package/src/requirements-search.ts +145 -0
  165. package/src/requirements-seq-http.test.ts +193 -0
  166. package/src/requirements-seq-migration.test.ts +63 -0
  167. package/src/requirements-start-date.test.ts +125 -0
  168. package/src/requirements-stats-http.test.ts +288 -0
  169. package/src/requirements-stats.ts +164 -0
  170. package/src/requirements-status-participants-http.test.ts +220 -0
  171. package/src/requirements-upgrade-e2e.test.ts +126 -0
  172. package/src/requirements-upgrade-migration.test.ts +98 -0
  173. package/src/requirements.ts +1761 -0
  174. package/src/resolve-port.test.ts +57 -0
  175. package/src/resolve-port.ts +44 -0
  176. package/src/response-charset.test.ts +130 -0
  177. package/src/rest-explicit-columns-http.test.ts +222 -0
  178. package/src/rest-identity.test.ts +129 -0
  179. package/src/rest-identity.ts +100 -0
  180. package/src/rest-projections.ts +68 -0
  181. package/src/rest-write-network-resolution.test.ts +133 -0
  182. package/src/restricted-files.ts +73 -0
  183. package/src/retention.test.ts +450 -0
  184. package/src/retention.ts +281 -0
  185. package/src/route-callers-http.test.ts +120 -0
  186. package/src/route-timing.test.ts +211 -0
  187. package/src/route-timing.ts +258 -0
  188. package/src/rules-file-duplicate-alias.test.ts +234 -0
  189. package/src/rules-file-session-target.test.ts +207 -0
  190. package/src/rules-file-transport.test.ts +237 -0
  191. package/src/runtime-label.test.ts +22 -0
  192. package/src/runtime-label.ts +21 -0
  193. package/src/scheduled-doorbell-nodeless-session.test.ts +130 -0
  194. package/src/scheduled-reply-unread-http.test.ts +212 -0
  195. package/src/scheduled-run-terminal.test.ts +268 -0
  196. package/src/scheduled-skip-blocking-task.test.ts +120 -0
  197. package/src/scheduled-stuck-http.test.ts +238 -0
  198. package/src/scheduled-stuck.ts +153 -0
  199. package/src/scheduled-tasks-http.test.ts +638 -0
  200. package/src/scheduled-tasks.ts +641 -0
  201. package/src/send-dedup-request-id.test.ts +229 -0
  202. package/src/send-reply-agent-warning.test.ts +202 -0
  203. package/src/send-reply-alias-optional.test.ts +131 -0
  204. package/src/send-reply-attachments.test.ts +458 -0
  205. package/src/send_dedup.test.ts +54 -0
  206. package/src/send_dedup.ts +52 -6
  207. package/src/serve-error.test.ts +38 -0
  208. package/src/serve-error.ts +27 -0
  209. package/src/server.ts +5038 -0
  210. package/src/shared/external-schedule-contract.ts +60 -0
  211. package/src/shared/probe-host-allowlist-drift.test.ts +54 -0
  212. package/src/shared/probe-host-allowlist.ts +128 -0
  213. package/src/shared/reserved-env-drift.test.ts +43 -0
  214. package/src/shared/reserved-env.test.ts +35 -0
  215. package/src/shared/reserved-env.ts +31 -0
  216. package/src/side-thread-command-transport.test.ts +312 -0
  217. package/src/side-thread-command-transport.ts +318 -0
  218. package/src/side-thread-http-integration.test.ts +233 -0
  219. package/src/side-thread-http.ts +504 -0
  220. package/src/side-thread-production.test.ts +72 -0
  221. package/src/side-thread-production.ts +118 -0
  222. package/src/side-thread.test.ts +1067 -0
  223. package/src/side-thread.ts +2499 -0
  224. package/src/skillhub-http.test.ts +108 -0
  225. package/src/sqlite-close-checkpoint.test.ts +78 -0
  226. package/src/stale-sweeper.test.ts +181 -0
  227. package/src/stale-sweeper.ts +114 -0
  228. package/src/start-node.test.ts +109 -0
  229. package/src/status-alias-filter-http.test.ts +106 -0
  230. package/src/status-alias-resolver-http.test.ts +163 -0
  231. package/src/status-alias-resolver.ts +19 -0
  232. package/src/status-node-id-filter-http.test.ts +161 -0
  233. package/src/status-read-cache-http.test.ts +229 -0
  234. package/src/status-read-cache.ts +122 -0
  235. package/src/stop-delete-node.test.ts +1164 -0
  236. package/src/task-access-http.test.ts +379 -0
  237. package/src/task-access.ts +413 -0
  238. package/src/task-auth-origin-http.test.ts +91 -0
  239. package/src/task-auth-origin.test.ts +24 -0
  240. package/src/task-auth-origin.ts +7 -0
  241. package/src/task-consumption.test.ts +482 -0
  242. package/src/task-diagnostic-http.test.ts +149 -0
  243. package/src/task-diagnostic.test.ts +115 -0
  244. package/src/task-diagnostic.ts +118 -0
  245. package/src/task-expiry-notice-http.test.ts +287 -0
  246. package/src/task-expiry-notice.ts +313 -0
  247. package/src/task-idempotency-mcp.test.ts +142 -0
  248. package/src/task-idempotency.test.ts +60 -0
  249. package/src/task-idempotency.ts +75 -0
  250. package/src/task-lifecycle-watcher.test.ts +204 -0
  251. package/src/task-lifecycle-watcher.ts +49 -0
  252. package/src/task-network-resolution-http.test.ts +141 -0
  253. package/src/task-queue-ahead-http.test.ts +309 -0
  254. package/src/task-queue-ahead-scale.test.ts +211 -0
  255. package/src/task-queue-ahead.ts +186 -0
  256. package/src/task-stale-open-http.test.ts +290 -0
  257. package/src/task-stale-open.ts +103 -0
  258. package/src/tasks-pagination-http.test.ts +141 -0
  259. package/src/tool-audience-http.test.ts +195 -0
  260. package/src/tool-audience.ts +109 -0
  261. package/src/tools-list-json-schema.test.ts +67 -0
  262. package/src/tools.ts +5426 -275
  263. package/src/update-provider.test.ts +234 -0
  264. package/src/upload-early-reject-guard.test.ts +155 -0
  265. package/src/uploads-http.test.ts +97 -15
  266. package/src/uploads.test.ts +66 -0
  267. package/src/uploads.ts +138 -5
  268. package/src/user-inbox-by-agent-http.test.ts +161 -0
  269. package/src/user-inbox-netid-notnull.test.ts +122 -0
  270. package/src/user-inbox-read-http.test.ts +223 -0
  271. package/src/user-inbox-schema.test.ts +54 -0
  272. package/src/vault.test.ts +200 -0
  273. package/src/vault.ts +156 -0
@@ -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
+ });