@malloy-publisher/server 0.0.235 → 0.0.237

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 (319) hide show
  1. package/dist/app/api-doc.yaml +83 -27
  2. package/dist/app/assets/{EnvironmentPage-BsAnavYN.js → EnvironmentPage-DaP6CpgK.js} +1 -1
  3. package/dist/app/assets/{HomePage-CADE138j.js → HomePage-C9yNw3iu.js} +1 -1
  4. package/dist/app/assets/{LightMode-Cfh7KzN8.js → LightMode-Be4-G-dX.js} +1 -1
  5. package/dist/app/assets/{MainPage-CO3pRlnV.js → MainPage-J_rU2SgI.js} +2 -2
  6. package/dist/app/assets/{MaterializationsPage-p9YjkRXZ.js → MaterializationsPage-DHgPI4MO.js} +1 -1
  7. package/dist/app/assets/ModelPage-BzXfjTu5.js +1 -0
  8. package/dist/app/assets/{PackagePage-e4kN75YR.js → PackagePage-ByoSTLJ4.js} +1 -1
  9. package/dist/app/assets/RouteError-CrSeKN2r.js +1 -0
  10. package/dist/app/assets/{ThemeEditorPage-CciagFTq.js → ThemeEditorPage-Btxnmhtk.js} +1 -1
  11. package/dist/app/assets/{WorkbookPage-DNWmkCXa.js → WorkbookPage-D6JLPnWO.js} +1 -1
  12. package/dist/app/assets/{core-Rj_4rRnA.es-BZyvITuO.js → core-BSRgt1mG.es-Bc_D2Lq4.js} +1 -1
  13. package/dist/app/assets/{index-CH2AcDzc.js → index-BRijWj6K.js} +15 -15
  14. package/dist/app/assets/{index-DQa463gC.js → index-BX4D94xw.js} +1 -1
  15. package/dist/app/assets/{index-VBbcc8s6.js → index-Cvy0xtb7.js} +1 -1
  16. package/dist/app/assets/{index-DQpV7MyA.js → index-DMAhIJNC.js} +1 -1
  17. package/dist/app/index.html +1 -1
  18. package/dist/package_load_worker.mjs +59 -20
  19. package/dist/server.mjs +1342 -616
  20. package/package.json +8 -2
  21. package/.eslintrc.json +0 -44
  22. package/.prettierignore +0 -1
  23. package/.prettierrc +0 -4
  24. package/build.ts +0 -97
  25. package/dist/app/assets/ModelPage-C1OSTv-x.js +0 -1
  26. package/dist/app/assets/RouteError-CzbfOkng.js +0 -1
  27. package/eslint.config.mjs +0 -76
  28. package/k6-tests/README.md +0 -144
  29. package/k6-tests/bun.lock +0 -797
  30. package/k6-tests/load-test/load-test-crud-connections.ts +0 -263
  31. package/k6-tests/load-test/load-test-crud-packages.ts +0 -461
  32. package/k6-tests/load-test/load-test-crud-projects.ts +0 -160
  33. package/k6-tests/load-test/load-test-crud.ts +0 -142
  34. package/k6-tests/load-test/load-test.ts +0 -784
  35. package/k6-tests/package.json +0 -27
  36. package/k6-tests/scripts/run-with-otel.sh +0 -50
  37. package/k6-tests/smoke-test/smoke-test.ts +0 -158
  38. package/k6-tests/tsconfig.json +0 -29
  39. package/k6-tests/utils/client_factory.ts +0 -128
  40. package/k6-tests/utils/common.ts +0 -489
  41. package/k6-tests/utils/logger.ts +0 -62
  42. package/k6-tests/utils/types.d.ts +0 -35
  43. package/malloy_mcp.dxt +0 -0
  44. package/publisher.config.example.bigquery.json +0 -33
  45. package/publisher.config.example.duckdb.json +0 -23
  46. package/publisher.config.json +0 -23
  47. package/scripts/bake-duckdb-extensions.js +0 -110
  48. package/src/config.spec.ts +0 -1535
  49. package/src/config.theme.spec.ts +0 -178
  50. package/src/config.ts +0 -1142
  51. package/src/constants.ts +0 -105
  52. package/src/controller/compile.controller.ts +0 -40
  53. package/src/controller/connection.controller.spec.ts +0 -849
  54. package/src/controller/connection.controller.ts +0 -901
  55. package/src/controller/database.controller.ts +0 -24
  56. package/src/controller/materialization.controller.spec.ts +0 -293
  57. package/src/controller/materialization.controller.ts +0 -319
  58. package/src/controller/model.controller.ts +0 -164
  59. package/src/controller/package.controller.spec.ts +0 -273
  60. package/src/controller/package.controller.ts +0 -306
  61. package/src/controller/query.controller.ts +0 -152
  62. package/src/controller/theme.controller.ts +0 -83
  63. package/src/controller/watch-mode.controller.ts +0 -243
  64. package/src/data_styles.ts +0 -71
  65. package/src/default-publisher.config.json +0 -23
  66. package/src/ducklake_version.spec.ts +0 -163
  67. package/src/ducklake_version.ts +0 -153
  68. package/src/errors.spec.ts +0 -119
  69. package/src/errors.ts +0 -237
  70. package/src/filter_deprecation.spec.ts +0 -64
  71. package/src/filter_deprecation.ts +0 -42
  72. package/src/health.spec.ts +0 -123
  73. package/src/health.ts +0 -233
  74. package/src/heap_check.spec.ts +0 -144
  75. package/src/heap_check.ts +0 -144
  76. package/src/instrumentation.ts +0 -214
  77. package/src/json_utils.spec.ts +0 -51
  78. package/src/json_utils.ts +0 -33
  79. package/src/logger.spec.ts +0 -210
  80. package/src/logger.ts +0 -251
  81. package/src/malloy_pin_prereqs.spec.ts +0 -25
  82. package/src/materialization_metrics.spec.ts +0 -169
  83. package/src/materialization_metrics.ts +0 -346
  84. package/src/mcp/error_messages.spec.ts +0 -35
  85. package/src/mcp/error_messages.ts +0 -183
  86. package/src/mcp/handler_utils.spec.ts +0 -108
  87. package/src/mcp/handler_utils.ts +0 -251
  88. package/src/mcp/mcp_constants.ts +0 -6
  89. package/src/mcp/query_envelope.spec.ts +0 -229
  90. package/src/mcp/query_envelope.ts +0 -240
  91. package/src/mcp/server.protocol.spec.ts +0 -250
  92. package/src/mcp/server.ts +0 -87
  93. package/src/mcp/skills/build_skills_bundle.spec.ts +0 -51
  94. package/src/mcp/skills/build_skills_bundle.ts +0 -176
  95. package/src/mcp/skills/skills_bundle.json +0 -1
  96. package/src/mcp/skills/skills_bundle.spec.ts +0 -171
  97. package/src/mcp/tool_response.spec.ts +0 -108
  98. package/src/mcp/tool_response.ts +0 -138
  99. package/src/mcp/tools/compile_tool.spec.ts +0 -315
  100. package/src/mcp/tools/compile_tool.ts +0 -208
  101. package/src/mcp/tools/docs_search/build_docs_index.ts +0 -132
  102. package/src/mcp/tools/docs_search/malloy_docs_index.json +0 -1
  103. package/src/mcp/tools/docs_search_tool.spec.ts +0 -32
  104. package/src/mcp/tools/docs_search_tool.ts +0 -128
  105. package/src/mcp/tools/embedding_index.spec.ts +0 -1236
  106. package/src/mcp/tools/embedding_index.ts +0 -808
  107. package/src/mcp/tools/execute_query_tool.spec.ts +0 -294
  108. package/src/mcp/tools/execute_query_tool.ts +0 -316
  109. package/src/mcp/tools/get_context_eval.ts +0 -275
  110. package/src/mcp/tools/get_context_tool.spec.ts +0 -592
  111. package/src/mcp/tools/get_context_tool.ts +0 -585
  112. package/src/mcp/tools/reload_package_tool.spec.ts +0 -232
  113. package/src/mcp/tools/reload_package_tool.ts +0 -132
  114. package/src/mcp_config.spec.ts +0 -919
  115. package/src/mcp_config.ts +0 -425
  116. package/src/oom_guards.integration.spec.ts +0 -269
  117. package/src/package_load/package_load_pool.spec.ts +0 -252
  118. package/src/package_load/package_load_pool.ts +0 -931
  119. package/src/package_load/package_load_worker.ts +0 -1034
  120. package/src/package_load/protocol.ts +0 -364
  121. package/src/package_load/rpc_wait_accountant.spec.ts +0 -109
  122. package/src/package_load/rpc_wait_accountant.ts +0 -76
  123. package/src/package_load_metrics.spec.ts +0 -114
  124. package/src/package_load_metrics.ts +0 -127
  125. package/src/path_safety.spec.ts +0 -158
  126. package/src/path_safety.ts +0 -146
  127. package/src/pg_helpers.spec.ts +0 -223
  128. package/src/pg_helpers.ts +0 -52
  129. package/src/query_cap_metrics.spec.ts +0 -89
  130. package/src/query_cap_metrics.ts +0 -116
  131. package/src/query_concurrency.spec.ts +0 -247
  132. package/src/query_concurrency.ts +0 -237
  133. package/src/query_metadata_metrics.ts +0 -49
  134. package/src/query_param_utils.ts +0 -29
  135. package/src/query_timeout.spec.ts +0 -224
  136. package/src/query_timeout.ts +0 -179
  137. package/src/runtime/publisher.js +0 -328
  138. package/src/server-old.ts +0 -988
  139. package/src/server.ts +0 -2059
  140. package/src/service/annotations.spec.ts +0 -118
  141. package/src/service/annotations.ts +0 -91
  142. package/src/service/authorize.spec.ts +0 -154
  143. package/src/service/authorize.ts +0 -498
  144. package/src/service/authorize_integration.spec.ts +0 -2041
  145. package/src/service/build_plan.spec.ts +0 -685
  146. package/src/service/build_plan.ts +0 -797
  147. package/src/service/compile_authorize.spec.ts +0 -85
  148. package/src/service/compile_fragment_techniques.spec.ts +0 -156
  149. package/src/service/connection.spec.ts +0 -2360
  150. package/src/service/connection.ts +0 -2187
  151. package/src/service/connection_config.spec.ts +0 -734
  152. package/src/service/connection_config.ts +0 -899
  153. package/src/service/connection_federation.spec.ts +0 -184
  154. package/src/service/connection_fingerprint.spec.ts +0 -102
  155. package/src/service/connection_service.spec.ts +0 -711
  156. package/src/service/connection_service.ts +0 -291
  157. package/src/service/cron_evaluator.spec.ts +0 -79
  158. package/src/service/cron_evaluator.ts +0 -105
  159. package/src/service/db_utils.spec.ts +0 -1085
  160. package/src/service/db_utils.ts +0 -1502
  161. package/src/service/duckdb_instance_isolation.spec.ts +0 -137
  162. package/src/service/ducklake_lazy_attach.spec.ts +0 -110
  163. package/src/service/embedding_provider.spec.ts +0 -329
  164. package/src/service/embedding_provider.ts +0 -236
  165. package/src/service/environment.ts +0 -2191
  166. package/src/service/environment_admission.spec.ts +0 -344
  167. package/src/service/environment_compile.spec.ts +0 -197
  168. package/src/service/environment_store.spec.ts +0 -2274
  169. package/src/service/environment_store.ts +0 -2434
  170. package/src/service/environment_store_anchoring.spec.ts +0 -107
  171. package/src/service/environment_store_clone.spec.ts +0 -350
  172. package/src/service/explore_visibility.spec.ts +0 -434
  173. package/src/service/exports_probe.spec.ts +0 -107
  174. package/src/service/extension_fetch_policy.spec.ts +0 -256
  175. package/src/service/filter.spec.ts +0 -458
  176. package/src/service/filter.ts +0 -341
  177. package/src/service/filter_bypass.spec.ts +0 -418
  178. package/src/service/filter_integration.spec.ts +0 -1004
  179. package/src/service/freshness.spec.ts +0 -183
  180. package/src/service/freshness.ts +0 -112
  181. package/src/service/gcs_s3_utils.ts +0 -427
  182. package/src/service/given.ts +0 -105
  183. package/src/service/givens_integration.spec.ts +0 -219
  184. package/src/service/manifest_loader.spec.ts +0 -189
  185. package/src/service/manifest_loader.ts +0 -156
  186. package/src/service/materialization_build_session.spec.ts +0 -435
  187. package/src/service/materialization_build_session.ts +0 -681
  188. package/src/service/materialization_config_validation.spec.ts +0 -99
  189. package/src/service/materialization_config_validation.ts +0 -120
  190. package/src/service/materialization_eligibility.spec.ts +0 -158
  191. package/src/service/materialization_eligibility.ts +0 -305
  192. package/src/service/materialization_schedule_surface.spec.ts +0 -256
  193. package/src/service/materialization_scheduler.spec.ts +0 -289
  194. package/src/service/materialization_scheduler.ts +0 -304
  195. package/src/service/materialization_serve_transform.spec.ts +0 -1003
  196. package/src/service/materialization_serve_transform.ts +0 -779
  197. package/src/service/materialization_service.spec.ts +0 -2373
  198. package/src/service/materialization_service.ts +0 -2342
  199. package/src/service/materialization_test_fixtures.ts +0 -249
  200. package/src/service/model.spec.ts +0 -1157
  201. package/src/service/model.ts +0 -3370
  202. package/src/service/model_limits.spec.ts +0 -209
  203. package/src/service/model_limits.ts +0 -131
  204. package/src/service/model_storage_serve.spec.ts +0 -193
  205. package/src/service/model_storage_serve_joins.spec.ts +0 -193
  206. package/src/service/package.spec.ts +0 -612
  207. package/src/service/package.ts +0 -1799
  208. package/src/service/package_manifest.spec.ts +0 -230
  209. package/src/service/package_manifest.ts +0 -241
  210. package/src/service/package_memory_governor.spec.ts +0 -173
  211. package/src/service/package_memory_governor.ts +0 -233
  212. package/src/service/package_quote_bound_tables.spec.ts +0 -171
  213. package/src/service/package_race.spec.ts +0 -208
  214. package/src/service/package_reload_safety.spec.ts +0 -193
  215. package/src/service/package_rollback.spec.ts +0 -190
  216. package/src/service/package_worker_path.spec.ts +0 -645
  217. package/src/service/path_injection.spec.ts +0 -39
  218. package/src/service/persist_annotation_validation.spec.ts +0 -89
  219. package/src/service/persist_annotation_validation.ts +0 -59
  220. package/src/service/persistence_policy.spec.ts +0 -374
  221. package/src/service/proxy.spec.ts +0 -414
  222. package/src/service/proxy.ts +0 -248
  223. package/src/service/query_boundary.spec.ts +0 -470
  224. package/src/service/query_metadata.spec.ts +0 -408
  225. package/src/service/query_metadata.ts +0 -492
  226. package/src/service/query_metadata_identity.spec.ts +0 -149
  227. package/src/service/query_text.spec.ts +0 -79
  228. package/src/service/query_text.ts +0 -43
  229. package/src/service/quoting.spec.ts +0 -135
  230. package/src/service/quoting.ts +0 -78
  231. package/src/service/resolve_environment.ts +0 -15
  232. package/src/service/restricted_mode.spec.ts +0 -299
  233. package/src/service/source_extraction.ts +0 -225
  234. package/src/service/theme_store.ts +0 -199
  235. package/src/storage/DatabaseInterface.ts +0 -198
  236. package/src/storage/StorageManager.ts +0 -113
  237. package/src/storage/duckdb/ConnectionRepository.ts +0 -143
  238. package/src/storage/duckdb/DuckDBConnection.ts +0 -123
  239. package/src/storage/duckdb/DuckDBRepository.ts +0 -208
  240. package/src/storage/duckdb/EnvironmentRepository.ts +0 -148
  241. package/src/storage/duckdb/MaterializationRepository.spec.ts +0 -191
  242. package/src/storage/duckdb/MaterializationRepository.ts +0 -336
  243. package/src/storage/duckdb/PackageRepository.ts +0 -135
  244. package/src/storage/duckdb/schema.ts +0 -229
  245. package/src/stream_helpers.spec.ts +0 -280
  246. package/src/stream_helpers.ts +0 -162
  247. package/src/telemetry.ts +0 -20
  248. package/src/test_helpers/metrics_harness.ts +0 -166
  249. package/src/theme_key_parity.spec.ts +0 -57
  250. package/src/utils.ts +0 -29
  251. package/tests/fixtures/authorize-compile/model.malloy +0 -9
  252. package/tests/fixtures/authorize-compile/publisher.json +0 -4
  253. package/tests/fixtures/html-pages-nopublic/model.malloy +0 -1
  254. package/tests/fixtures/html-pages-nopublic/publisher.json +0 -5
  255. package/tests/fixtures/html-pages-test/data.csv +0 -3
  256. package/tests/fixtures/html-pages-test/public/assets/app.css +0 -3
  257. package/tests/fixtures/html-pages-test/public/barehtml.html +0 -4
  258. package/tests/fixtures/html-pages-test/public/bodymeta.html +0 -8
  259. package/tests/fixtures/html-pages-test/public/data.json +0 -1
  260. package/tests/fixtures/html-pages-test/public/fitcomment.html +0 -11
  261. package/tests/fixtures/html-pages-test/public/index.html +0 -9
  262. package/tests/fixtures/html-pages-test/public/nohead.html +0 -8
  263. package/tests/fixtures/html-pages-test/public/notfit.html +0 -13
  264. package/tests/fixtures/html-pages-test/public/slides.html +0 -12
  265. package/tests/fixtures/html-pages-test/public/sub/page2.html +0 -9
  266. package/tests/fixtures/html-pages-test/public/unterminated.html +0 -10
  267. package/tests/fixtures/html-pages-test/publisher.json +0 -5
  268. package/tests/fixtures/html-pages-test/report.malloy +0 -1
  269. package/tests/fixtures/persist-multi-level/data/orders.csv +0 -5
  270. package/tests/fixtures/persist-multi-level/multi_level.malloy +0 -18
  271. package/tests/fixtures/persist-multi-level/publisher.json +0 -5
  272. package/tests/fixtures/persist-schedule-test/data/orders.csv +0 -5
  273. package/tests/fixtures/persist-schedule-test/persist_schedule_test.malloy +0 -11
  274. package/tests/fixtures/persist-schedule-test/publisher.json +0 -7
  275. package/tests/fixtures/persist-schedule-warn-test/data/orders.csv +0 -5
  276. package/tests/fixtures/persist-schedule-warn-test/persist_warn_test.malloy +0 -11
  277. package/tests/fixtures/persist-schedule-warn-test/publisher.json +0 -6
  278. package/tests/fixtures/persist-test/data/orders.csv +0 -5
  279. package/tests/fixtures/persist-test/persist_test.malloy +0 -11
  280. package/tests/fixtures/persist-test/publisher.json +0 -5
  281. package/tests/fixtures/publisher.config.json +0 -15
  282. package/tests/fixtures/query-givens/data/orders.csv +0 -7
  283. package/tests/fixtures/query-givens/model.malloy +0 -34
  284. package/tests/fixtures/query-givens/publisher.json +0 -5
  285. package/tests/fixtures/xlsx/database.xlsx +0 -0
  286. package/tests/harness/mcp_test_setup.ts +0 -332
  287. package/tests/harness/rest_e2e.ts +0 -68
  288. package/tests/integration/authorize/compile_authorize_http.integration.spec.ts +0 -92
  289. package/tests/integration/concurrent_package/concurrent_package.integration.spec.ts +0 -280
  290. package/tests/integration/duckdb_storage/duckdb_storage.integration.spec.ts +0 -138
  291. package/tests/integration/first_boot/readiness_line.integration.spec.ts +0 -177
  292. package/tests/integration/html_pages/html_pages.integration.spec.ts +0 -440
  293. package/tests/integration/legacy_routes/legacy_routes.integration.spec.ts +0 -259
  294. package/tests/integration/materialization/environment_list.integration.spec.ts +0 -133
  295. package/tests/integration/materialization/freshness_gate.integration.spec.ts +0 -292
  296. package/tests/integration/materialization/manifest_binding.integration.spec.ts +0 -430
  297. package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +0 -441
  298. package/tests/integration/materialization/orchestrated_rebind.integration.spec.ts +0 -150
  299. package/tests/integration/materialization/reference_manifest.integration.spec.ts +0 -251
  300. package/tests/integration/materialization/schedule_edit.integration.spec.ts +0 -126
  301. package/tests/integration/materialization/scheduler.integration.spec.ts +0 -200
  302. package/tests/integration/materialization/scheduler_recovery.integration.spec.ts +0 -172
  303. package/tests/integration/materialization/scheduler_transitions.integration.spec.ts +0 -256
  304. package/tests/integration/materialization/scheduler_wiring.integration.spec.ts +0 -143
  305. package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +0 -438
  306. package/tests/integration/mcp/mcp_get_context_semantic.integration.spec.ts +0 -235
  307. package/tests/integration/mcp/mcp_transport.integration.spec.ts +0 -98
  308. package/tests/integration/query_givens/query_givens.integration.spec.ts +0 -146
  309. package/tests/integration/query_givens/query_givens_authorize.integration.spec.ts +0 -121
  310. package/tests/integration/sdk_givens/sdk_givens.integration.spec.ts +0 -110
  311. package/tests/integration/watch-mode/watch_mode.integration.spec.ts +0 -421
  312. package/tests/unit/controller/theme.controller.spec.ts +0 -141
  313. package/tests/unit/duckdb/attached_databases.test.ts +0 -1243
  314. package/tests/unit/duckdb/duckdb_connection.test.ts +0 -181
  315. package/tests/unit/duckdb/legacy_schema_migration.test.ts +0 -197
  316. package/tests/unit/duckdb/repositories.test.ts +0 -208
  317. package/tests/unit/ducklake/ducklake.test.ts +0 -952
  318. package/tests/unit/service/theme_store.spec.ts +0 -139
  319. package/tsconfig.json +0 -32
@@ -1,1535 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
- import fs from "fs";
3
- import path from "path";
4
- import {
5
- getPersistCollisionEnforce,
6
- getPublisherConfig,
7
- getPublisherConfigDir,
8
- type PublisherConfig,
9
- } from "./config";
10
- import { PUBLISHER_CONFIG_NAME } from "./constants";
11
-
12
- describe("Config Environment Variable Substitution", () => {
13
- const testServerRoot = path.join(process.cwd(), "test-temp-config");
14
- const configPath = path.join(testServerRoot, PUBLISHER_CONFIG_NAME);
15
-
16
- beforeEach(() => {
17
- // Create test directory
18
- if (!fs.existsSync(testServerRoot)) {
19
- fs.mkdirSync(testServerRoot, { recursive: true });
20
- }
21
- });
22
-
23
- afterEach(() => {
24
- // Clean up test files and environment variables
25
- if (fs.existsSync(configPath)) {
26
- fs.unlinkSync(configPath);
27
- }
28
- if (fs.existsSync(testServerRoot)) {
29
- fs.rmdirSync(testServerRoot, { recursive: true });
30
- }
31
-
32
- // Clean up all test environment variables
33
- const testEnvVars = [
34
- "TEST_VAR",
35
- "BUCKET_NAME",
36
- "DB_HOST",
37
- "DB_PORT",
38
- "DB_NAME",
39
- "API_KEY",
40
- "API_HOST",
41
- "KEY_NAME",
42
- "VALUE_VAR",
43
- "EMPTY_VAR",
44
- "BUCKET_1",
45
- "BUCKET_2",
46
- "NORMAL_VALUE",
47
- "GCS_BUCKET",
48
- "PROJECT_ID",
49
- "CONNECTION_STRING",
50
- "PACKAGE_NAME",
51
- "DEFINED_VAR",
52
- "DEV_BUCKET",
53
- "PROD_BUCKET",
54
- "DB_CONNECTION",
55
- "ENV",
56
- "DATA_BUCKET",
57
- "TAG1",
58
- "TAG2",
59
- "CONFIG_VALUE",
60
- ];
61
-
62
- testEnvVars.forEach((varName) => {
63
- delete process.env[varName];
64
- });
65
- });
66
-
67
- describe("Scenario 1: ${VAR} present in config but value not available", () => {
68
- it("should throw error when environment variable is not defined", () => {
69
- // The correct behavior: throw an error when a required variable is missing
70
- const locationWithVar = "./path/${UNDEFINED_VAR}/end" as const;
71
-
72
- const config: PublisherConfig = {
73
- frozenConfig: false,
74
- environments: [
75
- {
76
- name: "test-project",
77
- packages: [
78
- {
79
- name: "test-package",
80
- location: locationWithVar,
81
- },
82
- ],
83
- },
84
- ],
85
- };
86
-
87
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
88
-
89
- // Should throw an error about the missing environment variable
90
- expect(() => getPublisherConfig(testServerRoot)).toThrow(
91
- "Environment variable '${UNDEFINED_VAR}' is not set in configuration file",
92
- );
93
- });
94
-
95
- it("should throw error for undefined variables in gs:// URLs", () => {
96
- const locationWithVar = "gs://${BUCKET_NAME}/packages" as const;
97
-
98
- const config: PublisherConfig = {
99
- frozenConfig: false,
100
- environments: [
101
- {
102
- name: "test-project",
103
- packages: [
104
- {
105
- name: "test-package",
106
- location: locationWithVar,
107
- },
108
- ],
109
- },
110
- ],
111
- };
112
-
113
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
114
-
115
- // Should throw an error about the missing environment variable
116
- expect(() => getPublisherConfig(testServerRoot)).toThrow(
117
- "Environment variable '${BUCKET_NAME}' is not set in configuration file",
118
- );
119
- });
120
- });
121
-
122
- describe("Scenario 2: ${VAR} present in config and value is available", () => {
123
- it("should substitute environment variable in package location with gs:// path", () => {
124
- process.env.BUCKET_NAME = "my-test-bucket";
125
-
126
- const config: PublisherConfig = {
127
- frozenConfig: false,
128
- environments: [
129
- {
130
- name: "test-project",
131
- packages: [
132
- {
133
- name: "test-package",
134
- location: "gs://${BUCKET_NAME}/packages",
135
- },
136
- ],
137
- },
138
- ],
139
- };
140
-
141
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
142
-
143
- const result = getPublisherConfig(testServerRoot);
144
-
145
- expect(result.environments[0].packages[0].location).toBe(
146
- "gs://my-test-bucket/packages",
147
- );
148
- });
149
-
150
- it("should substitute environment variable in filesystem path", () => {
151
- process.env.PROJECT_ID = "analytics-2024";
152
-
153
- const config: PublisherConfig = {
154
- frozenConfig: false,
155
- environments: [
156
- {
157
- name: "test-project",
158
- packages: [
159
- {
160
- name: "test-package",
161
- location: "./environments/${PROJECT_ID}/models",
162
- },
163
- ],
164
- },
165
- ],
166
- };
167
-
168
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
169
-
170
- const result = getPublisherConfig(testServerRoot);
171
-
172
- expect(result.environments[0].packages[0].location).toBe(
173
- "./environments/analytics-2024/models",
174
- );
175
- });
176
-
177
- it("should substitute multiple environment variables in single value", () => {
178
- process.env.BUCKET_NAME = "data-warehouse";
179
- process.env.PROJECT_ID = "prod-analytics";
180
-
181
- const config: PublisherConfig = {
182
- frozenConfig: false,
183
- environments: [
184
- {
185
- name: "test-project",
186
- packages: [
187
- {
188
- name: "test-package",
189
- location: "gs://${BUCKET_NAME}/${PROJECT_ID}/models",
190
- },
191
- ],
192
- },
193
- ],
194
- };
195
-
196
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
197
-
198
- const result = getPublisherConfig(testServerRoot);
199
-
200
- expect(result.environments[0].packages[0].location).toBe(
201
- "gs://data-warehouse/prod-analytics/models",
202
- );
203
- });
204
-
205
- it("should substitute variables across multiple packages", () => {
206
- process.env.BUCKET_1 = "bucket-one";
207
- process.env.BUCKET_2 = "bucket-two";
208
-
209
- const config: PublisherConfig = {
210
- frozenConfig: false,
211
- environments: [
212
- {
213
- name: "test-project",
214
- packages: [
215
- {
216
- name: "package-1",
217
- location: "gs://${BUCKET_1}/path",
218
- },
219
- {
220
- name: "package-2",
221
- location: "gs://${BUCKET_2}/path",
222
- },
223
- ],
224
- },
225
- ],
226
- };
227
-
228
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
229
-
230
- const result = getPublisherConfig(testServerRoot);
231
-
232
- expect(result.environments[0].packages[0].location).toBe(
233
- "gs://bucket-one/path",
234
- );
235
- expect(result.environments[0].packages[1].location).toBe(
236
- "gs://bucket-two/path",
237
- );
238
- });
239
-
240
- it("should substitute variables in connection names", () => {
241
- process.env.DB_HOST = "localhost";
242
-
243
- const config: PublisherConfig = {
244
- frozenConfig: false,
245
- environments: [
246
- {
247
- name: "test-project",
248
- packages: [],
249
- connections: [
250
- {
251
- name: "db-${DB_HOST}",
252
- type: "postgres",
253
- },
254
- ],
255
- },
256
- ],
257
- };
258
-
259
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
260
-
261
- const result = getPublisherConfig(testServerRoot);
262
-
263
- expect(result.environments[0].connections?.[0].name).toBe(
264
- "db-localhost",
265
- );
266
- });
267
-
268
- it("should substitute variables in nested configuration objects", () => {
269
- process.env.API_KEY = "secret-key-123";
270
- process.env.API_HOST = "api.example.com";
271
-
272
- const config = {
273
- frozenConfig: false,
274
- environments: [
275
- {
276
- name: "test-project",
277
- packages: [],
278
- settings: {
279
- apiEndpoint: "https://${API_HOST}/v1",
280
- credentials: {
281
- apiKey: "${API_KEY}",
282
- },
283
- },
284
- },
285
- ],
286
- };
287
-
288
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
289
-
290
- const result = getPublisherConfig(testServerRoot);
291
- const projectWithSettings = result
292
- .environments[0] as (typeof result.environments)[0] & {
293
- settings: {
294
- apiEndpoint: string;
295
- credentials: {
296
- apiKey: string;
297
- };
298
- };
299
- };
300
-
301
- expect(projectWithSettings.settings.apiEndpoint).toBe(
302
- "https://api.example.com/v1",
303
- );
304
- expect(projectWithSettings.settings.credentials.apiKey).toBe(
305
- "secret-key-123",
306
- );
307
- });
308
-
309
- it("should handle mixed substitution with literal text", () => {
310
- process.env.PROJECT_ID = "my-project";
311
-
312
- const config: PublisherConfig = {
313
- frozenConfig: false,
314
- environments: [
315
- {
316
- name: "test-project",
317
- packages: [
318
- {
319
- name: "test-package",
320
- location:
321
- "gs://bucket/prefix-${PROJECT_ID}-suffix/models",
322
- },
323
- ],
324
- },
325
- ],
326
- };
327
-
328
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
329
-
330
- const result = getPublisherConfig(testServerRoot);
331
-
332
- expect(result.environments[0].packages[0].location).toBe(
333
- "gs://bucket/prefix-my-project-suffix/models",
334
- );
335
- });
336
-
337
- it("should substitute variables across multiple environments", () => {
338
- process.env.DEV_BUCKET = "dev-data";
339
- process.env.PROD_BUCKET = "prod-data";
340
-
341
- const config: PublisherConfig = {
342
- frozenConfig: false,
343
- environments: [
344
- {
345
- name: "development",
346
- packages: [
347
- {
348
- name: "dev-package",
349
- location: "gs://${DEV_BUCKET}/models",
350
- },
351
- ],
352
- },
353
- {
354
- name: "production",
355
- packages: [
356
- {
357
- name: "prod-package",
358
- location: "gs://${PROD_BUCKET}/models",
359
- },
360
- ],
361
- },
362
- ],
363
- };
364
-
365
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
366
-
367
- const result = getPublisherConfig(testServerRoot);
368
-
369
- expect(result.environments[0].packages[0].location).toBe(
370
- "gs://dev-data/models",
371
- );
372
- expect(result.environments[1].packages[0].location).toBe(
373
- "gs://prod-data/models",
374
- );
375
-
376
- delete process.env.DEV_BUCKET;
377
- delete process.env.PROD_BUCKET;
378
- });
379
- });
380
-
381
- describe("Scenario 3: ${VAR} present in config as a key (not as a value)", () => {
382
- it("should NOT substitute environment variables in object keys", () => {
383
- process.env.KEY_NAME = "myKey";
384
-
385
- const config = {
386
- frozenConfig: false,
387
- environments: [
388
- {
389
- name: "test-project",
390
- packages: [],
391
- "${KEY_NAME}": "some-value",
392
- },
393
- ],
394
- };
395
-
396
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
397
-
398
- const result = getPublisherConfig(testServerRoot);
399
-
400
- // The key should remain as-is (not substituted)
401
- expect(result.environments[0]).toHaveProperty("${KEY_NAME}");
402
- expect(
403
- (result.environments[0] as Record<string, unknown>)["${KEY_NAME}"],
404
- ).toBe("some-value");
405
- });
406
-
407
- it("should preserve keys with variable syntax while substituting values", () => {
408
- process.env.NORMAL_VALUE = "substituted-value";
409
-
410
- const config = {
411
- frozenConfig: false,
412
- environments: [
413
- {
414
- name: "test-project",
415
- packages: [],
416
- "${DYNAMIC_KEY}": "value1",
417
- normal_key: "${NORMAL_VALUE}",
418
- },
419
- ],
420
- };
421
-
422
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
423
-
424
- const result = getPublisherConfig(testServerRoot);
425
-
426
- // Key should not be substituted
427
- expect(result.environments[0]).toHaveProperty("${DYNAMIC_KEY}");
428
- expect(
429
- (result.environments[0] as Record<string, unknown>)[
430
- "${DYNAMIC_KEY}"
431
- ],
432
- ).toBe("value1");
433
-
434
- // Value should be substituted
435
- expect(
436
- (result.environments[0] as Record<string, unknown>)["normal_key"],
437
- ).toBe("substituted-value");
438
- });
439
-
440
- it("should handle mixed scenario: variable in key and different variable in value", () => {
441
- process.env.VALUE_VAR = "actual-value";
442
-
443
- const config = {
444
- frozenConfig: false,
445
- environments: [
446
- {
447
- name: "test-project",
448
- packages: [],
449
- "${KEY_VAR}": "${VALUE_VAR}",
450
- },
451
- ],
452
- };
453
-
454
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
455
-
456
- const result = getPublisherConfig(testServerRoot);
457
-
458
- // Key remains with variable syntax
459
- expect(result.environments[0]).toHaveProperty("${KEY_VAR}");
460
-
461
- // Value is substituted
462
- expect(
463
- (result.environments[0] as Record<string, unknown>)["${KEY_VAR}"],
464
- ).toBe("actual-value");
465
- });
466
-
467
- it("should substitute variables in package names since they are values", () => {
468
- process.env.PACKAGE_NAME = "my-package";
469
- process.env.BUCKET_NAME = "my-bucket";
470
-
471
- const config: PublisherConfig = {
472
- frozenConfig: false,
473
- environments: [
474
- {
475
- name: "test-project",
476
- packages: [
477
- {
478
- name: "${PACKAGE_NAME}",
479
- location: "gs://${BUCKET_NAME}/models",
480
- },
481
- ],
482
- },
483
- ],
484
- };
485
-
486
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
487
-
488
- const result = getPublisherConfig(testServerRoot);
489
-
490
- // Package name is a property VALUE, so it WILL be substituted
491
- expect(result.environments[0].packages[0].name).toBe("my-package");
492
-
493
- // Location should also be substituted
494
- expect(result.environments[0].packages[0].location).toBe(
495
- "gs://my-bucket/models",
496
- );
497
-
498
- delete process.env.PACKAGE_NAME;
499
- });
500
-
501
- it("should handle nested objects with variable syntax in keys", () => {
502
- process.env.CONFIG_VALUE = "test-value";
503
-
504
- const config = {
505
- frozenConfig: false,
506
- environments: [
507
- {
508
- name: "test-project",
509
- packages: [],
510
- metadata: {
511
- "${DYNAMIC_PROP}": {
512
- setting: "${CONFIG_VALUE}",
513
- },
514
- },
515
- },
516
- ],
517
- };
518
-
519
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
520
-
521
- const result = getPublisherConfig(testServerRoot);
522
- const projectWithMetadata = result
523
- .environments[0] as (typeof result.environments)[0] & {
524
- metadata: Record<string, { setting: string }>;
525
- };
526
-
527
- // Key should remain unchanged
528
- expect(projectWithMetadata.metadata).toHaveProperty("${DYNAMIC_PROP}");
529
-
530
- // Nested value should be substituted
531
- expect(projectWithMetadata.metadata["${DYNAMIC_PROP}"].setting).toBe(
532
- "test-value",
533
- );
534
-
535
- delete process.env.CONFIG_VALUE;
536
- });
537
- });
538
-
539
- describe("Edge cases and special scenarios", () => {
540
- it("should handle empty string environment variable", () => {
541
- process.env.EMPTY_VAR = "";
542
-
543
- const config: PublisherConfig = {
544
- frozenConfig: false,
545
- environments: [
546
- {
547
- name: "test-project",
548
- packages: [
549
- {
550
- name: "test-package",
551
- location: "gs://bucket/${EMPTY_VAR}/path",
552
- },
553
- ],
554
- },
555
- ],
556
- };
557
-
558
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
559
-
560
- const result = getPublisherConfig(testServerRoot);
561
-
562
- expect(result.environments[0].packages[0].location).toBe(
563
- "gs://bucket//path",
564
- );
565
- });
566
-
567
- it("should handle non-string values without modification", () => {
568
- const config: PublisherConfig = {
569
- frozenConfig: true,
570
- environments: [
571
- {
572
- name: "test-project",
573
- packages: [],
574
- },
575
- ],
576
- };
577
-
578
- // Add non-standard properties for testing
579
- const configWithExtras = {
580
- ...config,
581
- environments: [
582
- {
583
- ...config.environments[0],
584
- count: 42,
585
- enabled: true,
586
- ratio: 3.14,
587
- metadata: null,
588
- tags: ["tag1", "tag2"],
589
- },
590
- ],
591
- };
592
-
593
- fs.writeFileSync(
594
- configPath,
595
- JSON.stringify(configWithExtras, null, 2),
596
- );
597
-
598
- const result = getPublisherConfig(testServerRoot);
599
- const projectWithExtras = result
600
- .environments[0] as (typeof result.environments)[0] & {
601
- count: number;
602
- enabled: boolean;
603
- ratio: number;
604
- metadata: null;
605
- tags: string[];
606
- };
607
-
608
- expect(result.frozenConfig).toBe(true);
609
- expect(projectWithExtras.count).toBe(42);
610
- expect(projectWithExtras.enabled).toBe(true);
611
- expect(projectWithExtras.ratio).toBe(3.14);
612
- expect(projectWithExtras.metadata).toBe(null);
613
- expect(projectWithExtras.tags).toEqual(["tag1", "tag2"]);
614
- });
615
-
616
- it("should handle config with no environment variables", () => {
617
- const config: PublisherConfig = {
618
- frozenConfig: false,
619
- environments: [
620
- {
621
- name: "test-project",
622
- packages: [
623
- {
624
- name: "test-package",
625
- location: "./packages/test",
626
- },
627
- ],
628
- },
629
- ],
630
- };
631
-
632
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
633
-
634
- const result = getPublisherConfig(testServerRoot);
635
-
636
- expect(result).toEqual(config);
637
- });
638
-
639
- it("should handle empty environments array", () => {
640
- const config: PublisherConfig = {
641
- frozenConfig: false,
642
- environments: [],
643
- };
644
-
645
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
646
-
647
- const result = getPublisherConfig(testServerRoot);
648
-
649
- expect(result.environments).toEqual([]);
650
- });
651
-
652
- it("should return default config when file does not exist", () => {
653
- // Don't create config file
654
- const result = getPublisherConfig(testServerRoot);
655
-
656
- expect(result).toEqual({
657
- frozenConfig: false,
658
- environments: [],
659
- });
660
- });
661
-
662
- it("should handle whitespace around variable names", () => {
663
- process.env.BUCKET_NAME = "test-bucket";
664
-
665
- const config: PublisherConfig = {
666
- frozenConfig: false,
667
- environments: [
668
- {
669
- name: "test-project",
670
- packages: [
671
- {
672
- name: "test-package",
673
- location: "gs://${ BUCKET_NAME }/path",
674
- },
675
- ],
676
- },
677
- ],
678
- };
679
-
680
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
681
-
682
- const result = getPublisherConfig(testServerRoot);
683
-
684
- // Whitespace around variable name means it won't match the pattern
685
- // Due to bug, the unmatched variable causes duplication
686
- expect(result.environments[0].packages[0].location).toBe(
687
- "gs://${ BUCKET_NAME }/path",
688
- );
689
- });
690
-
691
- it("should handle multiple environments with mixed variable usage", () => {
692
- process.env.PROD_BUCKET = "production-data";
693
-
694
- const config: PublisherConfig = {
695
- frozenConfig: false,
696
- environments: [
697
- {
698
- name: "development",
699
- packages: [
700
- {
701
- name: "dev-package",
702
- location: "./packages/dev",
703
- },
704
- ],
705
- },
706
- {
707
- name: "production",
708
- packages: [
709
- {
710
- name: "prod-package",
711
- location: "gs://${PROD_BUCKET}/models",
712
- },
713
- ],
714
- },
715
- ],
716
- };
717
-
718
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
719
-
720
- const result = getPublisherConfig(testServerRoot);
721
-
722
- expect(result.environments[0].packages[0].location).toBe(
723
- "./packages/dev",
724
- );
725
- expect(result.environments[1].packages[0].location).toBe(
726
- "gs://production-data/models",
727
- );
728
-
729
- delete process.env.PROD_BUCKET;
730
- });
731
-
732
- it("should preserve variable syntax if not matching pattern", () => {
733
- const config: PublisherConfig = {
734
- frozenConfig: false,
735
- environments: [
736
- {
737
- name: "test-project",
738
- packages: [
739
- {
740
- name: "test-package",
741
- location: "gs://bucket/${lowercase_var}/path",
742
- },
743
- ],
744
- },
745
- ],
746
- };
747
-
748
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
749
-
750
- const result = getPublisherConfig(testServerRoot);
751
-
752
- // Variable won't match pattern, so won't be substituted
753
- expect(result.environments[0].packages[0].location).toBe(
754
- "gs://bucket/${lowercase_var}/path",
755
- );
756
- });
757
-
758
- it("should handle arrays of strings with variables", () => {
759
- process.env.TAG1 = "analytics";
760
- process.env.TAG2 = "production";
761
-
762
- const config = {
763
- frozenConfig: false,
764
- environments: [
765
- {
766
- name: "test-project",
767
- packages: [],
768
- tags: ["${TAG1}", "${TAG2}", "static-tag"],
769
- },
770
- ],
771
- };
772
-
773
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
774
-
775
- const result = getPublisherConfig(testServerRoot);
776
- const projectWithTags = result
777
- .environments[0] as (typeof result.environments)[0] & {
778
- tags: string[];
779
- };
780
-
781
- expect(projectWithTags.tags).toEqual([
782
- "analytics",
783
- "production",
784
- "static-tag",
785
- ]);
786
-
787
- delete process.env.TAG1;
788
- delete process.env.TAG2;
789
- });
790
- });
791
-
792
- describe("Real-world configuration scenarios", () => {
793
- it("should handle typical multi-environment setup", () => {
794
- process.env.ENV = "staging";
795
- process.env.DATA_BUCKET = "company-data-staging";
796
- process.env.PROJECT_ID = "company-project-staging";
797
-
798
- const config: PublisherConfig = {
799
- frozenConfig: false,
800
- environments: [
801
- {
802
- name: "${ENV}",
803
- packages: [
804
- {
805
- name: "analytics",
806
- location: "gs://${DATA_BUCKET}/${PROJECT_ID}/analytics",
807
- },
808
- {
809
- name: "reporting",
810
- location: "gs://${DATA_BUCKET}/${PROJECT_ID}/reporting",
811
- },
812
- ],
813
- },
814
- ],
815
- };
816
-
817
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
818
-
819
- const result = getPublisherConfig(testServerRoot);
820
-
821
- expect(result.environments[0].name).toBe("staging");
822
- expect(result.environments[0].packages[0].location).toBe(
823
- "gs://company-data-staging/company-project-staging/analytics",
824
- );
825
- expect(result.environments[0].packages[1].location).toBe(
826
- "gs://company-data-staging/company-project-staging/reporting",
827
- );
828
-
829
- delete process.env.ENV;
830
- delete process.env.DATA_BUCKET;
831
- });
832
-
833
- it("should handle connection configurations with environment variables", () => {
834
- process.env.DB_CONNECTION = "production-db";
835
-
836
- const config: PublisherConfig = {
837
- frozenConfig: false,
838
- environments: [
839
- {
840
- name: "production",
841
- packages: [],
842
- connections: [
843
- {
844
- name: "${DB_CONNECTION}",
845
- type: "bigquery",
846
- },
847
- ],
848
- },
849
- ],
850
- };
851
-
852
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
853
-
854
- const result = getPublisherConfig(testServerRoot);
855
-
856
- expect(result.environments[0].connections?.[0].name).toBe(
857
- "production-db",
858
- );
859
-
860
- delete process.env.DB_CONNECTION;
861
- });
862
- });
863
- });
864
-
865
- // TODO: Remove this during projects cleanup
866
- describe("Config legacy 'projects' key back-compat", () => {
867
- const testServerRoot = path.join(process.cwd(), "test-temp-legacy-config");
868
- const configPath = path.join(testServerRoot, PUBLISHER_CONFIG_NAME);
869
-
870
- beforeEach(() => {
871
- if (!fs.existsSync(testServerRoot)) {
872
- fs.mkdirSync(testServerRoot, { recursive: true });
873
- }
874
- });
875
-
876
- afterEach(() => {
877
- if (fs.existsSync(configPath)) {
878
- fs.unlinkSync(configPath);
879
- }
880
- if (fs.existsSync(testServerRoot)) {
881
- fs.rmdirSync(testServerRoot, { recursive: true });
882
- }
883
- });
884
-
885
- it("reads from legacy 'projects' key when 'environments' is absent", async () => {
886
- // Pre-rename on-disk shape: top-level key is `projects`, not
887
- // `environments`. Without back-compat this silently parses as empty.
888
- const legacyConfig = {
889
- frozenConfig: false,
890
- projects: [
891
- {
892
- name: "legacy-env",
893
- packages: [
894
- {
895
- name: "p1",
896
- location: "./packages/p1",
897
- },
898
- ],
899
- },
900
- ],
901
- };
902
-
903
- fs.writeFileSync(configPath, JSON.stringify(legacyConfig, null, 2));
904
-
905
- // Spy on logger.warn so we can assert the deprecation message fired.
906
- const { logger } = await import("./logger");
907
- const originalWarn = logger.warn;
908
- const warnings: string[] = [];
909
- logger.warn = ((msg: unknown, ..._rest: unknown[]) => {
910
- warnings.push(typeof msg === "string" ? msg : String(msg));
911
- return logger;
912
- }) as typeof logger.warn;
913
-
914
- try {
915
- const result = getPublisherConfig(testServerRoot);
916
-
917
- expect(result.environments.length).toBe(1);
918
- expect(result.environments[0].name).toBe("legacy-env");
919
- expect(result.environments[0].packages[0].name).toBe("p1");
920
-
921
- expect(
922
- warnings.some((w) => w.includes('uses deprecated "projects" key')),
923
- ).toBe(true);
924
- } finally {
925
- logger.warn = originalWarn;
926
- }
927
- });
928
-
929
- it("prefers the new 'environments' key when both are present", async () => {
930
- const config = {
931
- frozenConfig: false,
932
- environments: [
933
- {
934
- name: "new-env",
935
- packages: [{ name: "p1", location: "./packages/p1" }],
936
- },
937
- ],
938
- projects: [
939
- {
940
- name: "should-be-ignored",
941
- packages: [{ name: "p2", location: "./packages/p2" }],
942
- },
943
- ],
944
- };
945
-
946
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
947
-
948
- const { logger } = await import("./logger");
949
- const originalWarn = logger.warn;
950
- const warnings: string[] = [];
951
- logger.warn = ((msg: unknown, ..._rest: unknown[]) => {
952
- warnings.push(typeof msg === "string" ? msg : String(msg));
953
- return logger;
954
- }) as typeof logger.warn;
955
-
956
- try {
957
- const result = getPublisherConfig(testServerRoot);
958
-
959
- expect(result.environments.length).toBe(1);
960
- expect(result.environments[0].name).toBe("new-env");
961
-
962
- // No deprecation warning should fire when `environments` is present.
963
- expect(
964
- warnings.some((w) => w.includes('uses deprecated "projects" key')),
965
- ).toBe(false);
966
- } finally {
967
- logger.warn = originalWarn;
968
- }
969
- });
970
- });
971
-
972
- describe("Committed example configs", () => {
973
- const serverDir = path.resolve(__dirname, "..");
974
-
975
- it.each([
976
- ["publisher.config.json", false],
977
- ["publisher.config.example.duckdb.json", false],
978
- ["publisher.config.example.bigquery.json", true],
979
- ])(
980
- "%s parses as a valid PublisherConfig",
981
- (filename, expectsBigQueryConnection) => {
982
- const filePath = path.join(serverDir, filename);
983
- expect(fs.existsSync(filePath)).toBe(true);
984
-
985
- const raw = fs.readFileSync(filePath, "utf-8");
986
- const parsed = JSON.parse(raw) as PublisherConfig;
987
-
988
- expect(parsed).toHaveProperty("environments");
989
- expect(Array.isArray(parsed.environments)).toBe(true);
990
- expect(parsed.environments.length).toBeGreaterThan(0);
991
-
992
- const env = parsed.environments[0];
993
- expect(env.name).toBeTruthy();
994
- expect(Array.isArray(env.packages)).toBe(true);
995
- expect(env.packages.length).toBeGreaterThan(0);
996
-
997
- if (expectsBigQueryConnection) {
998
- expect(
999
- (env.connections ?? []).some((c) => c.type === "bigquery"),
1000
- ).toBe(true);
1001
- expect(
1002
- env.packages.some((p) => p.name === "bigquery-hackernews"),
1003
- ).toBe(true);
1004
- } else {
1005
- expect(
1006
- env.packages.some((p) => p.name === "bigquery-hackernews"),
1007
- ).toBe(false);
1008
- }
1009
- },
1010
- );
1011
- });
1012
-
1013
- describe("Config path resolution (--config and bundled default)", () => {
1014
- const emptyServerRoot = path.join(process.cwd(), "test-empty-server-root");
1015
- const customConfigPath = path.join(
1016
- process.cwd(),
1017
- "test-custom-publisher.config.json",
1018
- );
1019
-
1020
- beforeEach(() => {
1021
- if (!fs.existsSync(emptyServerRoot)) {
1022
- fs.mkdirSync(emptyServerRoot, { recursive: true });
1023
- }
1024
- delete process.env.PUBLISHER_CONFIG_PATH;
1025
- delete process.env.PUBLISHER_USE_BUNDLED_DEFAULT;
1026
- });
1027
-
1028
- afterEach(() => {
1029
- delete process.env.PUBLISHER_CONFIG_PATH;
1030
- delete process.env.PUBLISHER_USE_BUNDLED_DEFAULT;
1031
- if (fs.existsSync(customConfigPath)) {
1032
- fs.unlinkSync(customConfigPath);
1033
- }
1034
- if (fs.existsSync(emptyServerRoot)) {
1035
- fs.rmSync(emptyServerRoot, { recursive: true, force: true });
1036
- }
1037
- });
1038
-
1039
- it("falls back to the bundled default config when serverRoot has no publisher.config.json and bundled-default opt-in is set", () => {
1040
- // The bundled default is opt-in (server.ts sets the env var when
1041
- // the user passed neither --server_root nor --config) so embedded
1042
- // callers don't get a surprise filesystem read.
1043
- process.env.PUBLISHER_USE_BUNDLED_DEFAULT = "true";
1044
- const result = getPublisherConfig(emptyServerRoot);
1045
-
1046
- expect(result.environments.length).toBeGreaterThan(0);
1047
- const env = result.environments[0];
1048
- expect(env.name).toBe("examples");
1049
- expect(env.packages.some((p) => p.name === "storefront")).toBe(true);
1050
- expect(env.packages.some((p) => p.name === "bigquery-hackernews")).toBe(
1051
- false,
1052
- );
1053
- });
1054
-
1055
- it("does NOT fall back to the bundled default when opt-in flag is unset (programmatic construction)", () => {
1056
- // No PUBLISHER_USE_BUNDLED_DEFAULT, no PUBLISHER_CONFIG_PATH, no
1057
- // file in emptyServerRoot — result should be the original empty
1058
- // shape, preserving prior behavior for embeds and tests.
1059
- const result = getPublisherConfig(emptyServerRoot);
1060
- expect(result.environments).toEqual([]);
1061
- });
1062
-
1063
- it("honors PUBLISHER_CONFIG_PATH (set by --config) over the server root", () => {
1064
- const customConfig = {
1065
- frozenConfig: false,
1066
- environments: [
1067
- { name: "custom-env", packages: [{ name: "foo", location: "/x" }] },
1068
- ],
1069
- };
1070
- fs.writeFileSync(customConfigPath, JSON.stringify(customConfig));
1071
- process.env.PUBLISHER_CONFIG_PATH = customConfigPath;
1072
-
1073
- const result = getPublisherConfig(emptyServerRoot);
1074
- expect(result.environments[0].name).toBe("custom-env");
1075
- });
1076
-
1077
- it("returns empty environments (not the bundled default) when --config points at a missing file", () => {
1078
- process.env.PUBLISHER_CONFIG_PATH = path.join(
1079
- process.cwd(),
1080
- "does-not-exist.json",
1081
- );
1082
- // Even with bundled-default opt-in, --config with a missing target
1083
- // is a user error and we don't paper over it.
1084
- process.env.PUBLISHER_USE_BUNDLED_DEFAULT = "true";
1085
- const result = getPublisherConfig(emptyServerRoot);
1086
- expect(result.environments).toEqual([]);
1087
- });
1088
- });
1089
-
1090
- describe("getMemoryGovernorConfig", () => {
1091
- const GOVERNOR_ENV_VARS = [
1092
- "PUBLISHER_MAX_MEMORY_BYTES",
1093
- "PUBLISHER_MEMORY_HIGH_WATER_FRACTION",
1094
- "PUBLISHER_MEMORY_LOW_WATER_FRACTION",
1095
- "PUBLISHER_MEMORY_CHECK_INTERVAL_MS",
1096
- "PUBLISHER_MEMORY_BACKPRESSURE",
1097
- ];
1098
-
1099
- beforeEach(() => {
1100
- for (const v of GOVERNOR_ENV_VARS) delete process.env[v];
1101
- });
1102
- afterEach(() => {
1103
- for (const v of GOVERNOR_ENV_VARS) delete process.env[v];
1104
- });
1105
-
1106
- it("returns null when PUBLISHER_MAX_MEMORY_BYTES is unset", async () => {
1107
- const { getMemoryGovernorConfig } = await import("./config");
1108
- expect(getMemoryGovernorConfig()).toBeNull();
1109
- });
1110
-
1111
- it("parses defaults when only PUBLISHER_MAX_MEMORY_BYTES is set", async () => {
1112
- process.env.PUBLISHER_MAX_MEMORY_BYTES = String(2 * 1024 * 1024 * 1024);
1113
- const { getMemoryGovernorConfig } = await import("./config");
1114
- const cfg = getMemoryGovernorConfig();
1115
- expect(cfg).not.toBeNull();
1116
- expect(cfg!.maxMemoryBytes).toBe(2 * 1024 * 1024 * 1024);
1117
- expect(cfg!.backpressureEnabled).toBe(true);
1118
- expect(cfg!.highWaterFraction).toBeGreaterThan(cfg!.lowWaterFraction);
1119
- });
1120
-
1121
- it("honours fraction and interval overrides", async () => {
1122
- process.env.PUBLISHER_MAX_MEMORY_BYTES = "1000000000";
1123
- process.env.PUBLISHER_MEMORY_HIGH_WATER_FRACTION = "0.85";
1124
- process.env.PUBLISHER_MEMORY_LOW_WATER_FRACTION = "0.7";
1125
- process.env.PUBLISHER_MEMORY_CHECK_INTERVAL_MS = "10000";
1126
- process.env.PUBLISHER_MEMORY_BACKPRESSURE = "false";
1127
- const { getMemoryGovernorConfig } = await import("./config");
1128
- const cfg = getMemoryGovernorConfig();
1129
- expect(cfg).not.toBeNull();
1130
- expect(cfg!.highWaterFraction).toBe(0.85);
1131
- expect(cfg!.lowWaterFraction).toBe(0.7);
1132
- expect(cfg!.checkIntervalMs).toBe(10000);
1133
- expect(cfg!.backpressureEnabled).toBe(false);
1134
- });
1135
-
1136
- it("treats PUBLISHER_MAX_MEMORY_BYTES=0 as disabled (returns null)", async () => {
1137
- process.env.PUBLISHER_MAX_MEMORY_BYTES = "0";
1138
- const { getMemoryGovernorConfig } = await import("./config");
1139
- expect(getMemoryGovernorConfig()).toBeNull();
1140
- });
1141
-
1142
- it("rejects a negative PUBLISHER_MAX_MEMORY_BYTES", async () => {
1143
- process.env.PUBLISHER_MAX_MEMORY_BYTES = "-1";
1144
- const { getMemoryGovernorConfig } = await import("./config");
1145
- expect(() => getMemoryGovernorConfig()).toThrow();
1146
- });
1147
-
1148
- it("rejects low >= high", async () => {
1149
- process.env.PUBLISHER_MAX_MEMORY_BYTES = "1000000000";
1150
- process.env.PUBLISHER_MEMORY_HIGH_WATER_FRACTION = "0.7";
1151
- process.env.PUBLISHER_MEMORY_LOW_WATER_FRACTION = "0.8";
1152
- const { getMemoryGovernorConfig } = await import("./config");
1153
- expect(() => getMemoryGovernorConfig()).toThrow();
1154
- });
1155
-
1156
- it("rejects an out-of-range fraction", async () => {
1157
- process.env.PUBLISHER_MAX_MEMORY_BYTES = "1000000000";
1158
- process.env.PUBLISHER_MEMORY_HIGH_WATER_FRACTION = "1.5";
1159
- const { getMemoryGovernorConfig } = await import("./config");
1160
- expect(() => getMemoryGovernorConfig()).toThrow();
1161
- });
1162
-
1163
- it("rejects a check interval below the safety floor", async () => {
1164
- process.env.PUBLISHER_MAX_MEMORY_BYTES = "1000000000";
1165
- process.env.PUBLISHER_MEMORY_CHECK_INTERVAL_MS = "10";
1166
- const { getMemoryGovernorConfig } = await import("./config");
1167
- expect(() => getMemoryGovernorConfig()).toThrow();
1168
- });
1169
- });
1170
-
1171
- describe("getMaxQueryRows", () => {
1172
- beforeEach(() => {
1173
- delete process.env.PUBLISHER_MAX_QUERY_ROWS;
1174
- });
1175
- afterEach(() => {
1176
- delete process.env.PUBLISHER_MAX_QUERY_ROWS;
1177
- });
1178
-
1179
- it("returns DEFAULT_MAX_QUERY_ROWS when the env var is unset", async () => {
1180
- const { getMaxQueryRows } = await import("./config");
1181
- const { DEFAULT_MAX_QUERY_ROWS } = await import("./constants");
1182
- expect(getMaxQueryRows()).toBe(DEFAULT_MAX_QUERY_ROWS);
1183
- });
1184
-
1185
- it("returns DEFAULT_MAX_QUERY_ROWS when the env var is empty", async () => {
1186
- process.env.PUBLISHER_MAX_QUERY_ROWS = "";
1187
- const { getMaxQueryRows } = await import("./config");
1188
- const { DEFAULT_MAX_QUERY_ROWS } = await import("./constants");
1189
- expect(getMaxQueryRows()).toBe(DEFAULT_MAX_QUERY_ROWS);
1190
- });
1191
-
1192
- it("returns the override when the env var is set", async () => {
1193
- process.env.PUBLISHER_MAX_QUERY_ROWS = "5000";
1194
- const { getMaxQueryRows } = await import("./config");
1195
- expect(getMaxQueryRows()).toBe(5000);
1196
- });
1197
-
1198
- it("accepts 0 (used to opt out of row cap)", async () => {
1199
- process.env.PUBLISHER_MAX_QUERY_ROWS = "0";
1200
- const { getMaxQueryRows } = await import("./config");
1201
- expect(getMaxQueryRows()).toBe(0);
1202
- });
1203
-
1204
- it("rejects a negative override", async () => {
1205
- process.env.PUBLISHER_MAX_QUERY_ROWS = "-1";
1206
- const { getMaxQueryRows } = await import("./config");
1207
- expect(() => getMaxQueryRows()).toThrow();
1208
- });
1209
-
1210
- it("rejects a non-integer override", async () => {
1211
- process.env.PUBLISHER_MAX_QUERY_ROWS = "1.5";
1212
- const { getMaxQueryRows } = await import("./config");
1213
- expect(() => getMaxQueryRows()).toThrow();
1214
- });
1215
-
1216
- it("rejects a non-numeric override", async () => {
1217
- process.env.PUBLISHER_MAX_QUERY_ROWS = "lots";
1218
- const { getMaxQueryRows } = await import("./config");
1219
- expect(() => getMaxQueryRows()).toThrow();
1220
- });
1221
- });
1222
-
1223
- describe("getMaxResponseBytes", () => {
1224
- beforeEach(() => {
1225
- delete process.env.PUBLISHER_MAX_RESPONSE_BYTES;
1226
- });
1227
- afterEach(() => {
1228
- delete process.env.PUBLISHER_MAX_RESPONSE_BYTES;
1229
- });
1230
-
1231
- it("returns DEFAULT_MAX_RESPONSE_BYTES when the env var is unset", async () => {
1232
- const { getMaxResponseBytes } = await import("./config");
1233
- const { DEFAULT_MAX_RESPONSE_BYTES } = await import("./constants");
1234
- expect(getMaxResponseBytes()).toBe(DEFAULT_MAX_RESPONSE_BYTES);
1235
- });
1236
-
1237
- it("returns DEFAULT_MAX_RESPONSE_BYTES when the env var is empty", async () => {
1238
- process.env.PUBLISHER_MAX_RESPONSE_BYTES = "";
1239
- const { getMaxResponseBytes } = await import("./config");
1240
- const { DEFAULT_MAX_RESPONSE_BYTES } = await import("./constants");
1241
- expect(getMaxResponseBytes()).toBe(DEFAULT_MAX_RESPONSE_BYTES);
1242
- });
1243
-
1244
- it("returns the override when the env var is set", async () => {
1245
- process.env.PUBLISHER_MAX_RESPONSE_BYTES = "1048576";
1246
- const { getMaxResponseBytes } = await import("./config");
1247
- expect(getMaxResponseBytes()).toBe(1048576);
1248
- });
1249
-
1250
- it("accepts 0 (used to opt out of byte cap)", async () => {
1251
- process.env.PUBLISHER_MAX_RESPONSE_BYTES = "0";
1252
- const { getMaxResponseBytes } = await import("./config");
1253
- expect(getMaxResponseBytes()).toBe(0);
1254
- });
1255
-
1256
- it("rejects a negative override", async () => {
1257
- process.env.PUBLISHER_MAX_RESPONSE_BYTES = "-1";
1258
- const { getMaxResponseBytes } = await import("./config");
1259
- expect(() => getMaxResponseBytes()).toThrow();
1260
- });
1261
-
1262
- it("rejects a non-integer override", async () => {
1263
- process.env.PUBLISHER_MAX_RESPONSE_BYTES = "1.5";
1264
- const { getMaxResponseBytes } = await import("./config");
1265
- expect(() => getMaxResponseBytes()).toThrow();
1266
- });
1267
-
1268
- it("rejects a non-numeric override", async () => {
1269
- process.env.PUBLISHER_MAX_RESPONSE_BYTES = "big";
1270
- const { getMaxResponseBytes } = await import("./config");
1271
- expect(() => getMaxResponseBytes()).toThrow();
1272
- });
1273
- });
1274
-
1275
- describe("getDefaultQueryRowLimit", () => {
1276
- beforeEach(() => {
1277
- delete process.env.PUBLISHER_DEFAULT_QUERY_ROW_LIMIT;
1278
- });
1279
- afterEach(() => {
1280
- delete process.env.PUBLISHER_DEFAULT_QUERY_ROW_LIMIT;
1281
- });
1282
-
1283
- it("returns DEFAULT_QUERY_ROW_LIMIT when the env var is unset", async () => {
1284
- const { getDefaultQueryRowLimit } = await import("./config");
1285
- const { DEFAULT_QUERY_ROW_LIMIT } = await import("./constants");
1286
- expect(getDefaultQueryRowLimit()).toBe(DEFAULT_QUERY_ROW_LIMIT);
1287
- });
1288
-
1289
- it("returns DEFAULT_QUERY_ROW_LIMIT when the env var is empty", async () => {
1290
- process.env.PUBLISHER_DEFAULT_QUERY_ROW_LIMIT = "";
1291
- const { getDefaultQueryRowLimit } = await import("./config");
1292
- const { DEFAULT_QUERY_ROW_LIMIT } = await import("./constants");
1293
- expect(getDefaultQueryRowLimit()).toBe(DEFAULT_QUERY_ROW_LIMIT);
1294
- });
1295
-
1296
- it("returns the override when the env var is set", async () => {
1297
- process.env.PUBLISHER_DEFAULT_QUERY_ROW_LIMIT = "250";
1298
- const { getDefaultQueryRowLimit } = await import("./config");
1299
- expect(getDefaultQueryRowLimit()).toBe(250);
1300
- });
1301
-
1302
- it("rejects 0 (a default of 'zero rows' is always a misconfiguration)", async () => {
1303
- process.env.PUBLISHER_DEFAULT_QUERY_ROW_LIMIT = "0";
1304
- const { getDefaultQueryRowLimit } = await import("./config");
1305
- expect(() => getDefaultQueryRowLimit()).toThrow();
1306
- });
1307
-
1308
- it("rejects a negative override", async () => {
1309
- process.env.PUBLISHER_DEFAULT_QUERY_ROW_LIMIT = "-1";
1310
- const { getDefaultQueryRowLimit } = await import("./config");
1311
- expect(() => getDefaultQueryRowLimit()).toThrow();
1312
- });
1313
-
1314
- it("rejects a non-integer override", async () => {
1315
- process.env.PUBLISHER_DEFAULT_QUERY_ROW_LIMIT = "1.5";
1316
- const { getDefaultQueryRowLimit } = await import("./config");
1317
- expect(() => getDefaultQueryRowLimit()).toThrow();
1318
- });
1319
-
1320
- it("rejects a non-numeric override", async () => {
1321
- process.env.PUBLISHER_DEFAULT_QUERY_ROW_LIMIT = "lots";
1322
- const { getDefaultQueryRowLimit } = await import("./config");
1323
- expect(() => getDefaultQueryRowLimit()).toThrow();
1324
- });
1325
- });
1326
-
1327
- describe("getQueryTimeoutMs", () => {
1328
- beforeEach(() => {
1329
- delete process.env.PUBLISHER_QUERY_TIMEOUT_MS;
1330
- });
1331
- afterEach(() => {
1332
- delete process.env.PUBLISHER_QUERY_TIMEOUT_MS;
1333
- });
1334
-
1335
- it("returns DEFAULT_QUERY_TIMEOUT_MS when the env var is unset", async () => {
1336
- const { getQueryTimeoutMs } = await import("./config");
1337
- const { DEFAULT_QUERY_TIMEOUT_MS } = await import("./constants");
1338
- expect(getQueryTimeoutMs()).toBe(DEFAULT_QUERY_TIMEOUT_MS);
1339
- });
1340
-
1341
- it("returns the override when the env var is set", async () => {
1342
- process.env.PUBLISHER_QUERY_TIMEOUT_MS = "30000";
1343
- const { getQueryTimeoutMs } = await import("./config");
1344
- expect(getQueryTimeoutMs()).toBe(30000);
1345
- });
1346
-
1347
- it("accepts 0 (used to opt out of the timeout)", async () => {
1348
- process.env.PUBLISHER_QUERY_TIMEOUT_MS = "0";
1349
- const { getQueryTimeoutMs } = await import("./config");
1350
- expect(getQueryTimeoutMs()).toBe(0);
1351
- });
1352
-
1353
- it("rejects a negative override", async () => {
1354
- process.env.PUBLISHER_QUERY_TIMEOUT_MS = "-1";
1355
- const { getQueryTimeoutMs } = await import("./config");
1356
- expect(() => getQueryTimeoutMs()).toThrow();
1357
- });
1358
-
1359
- it("rejects a non-integer override", async () => {
1360
- process.env.PUBLISHER_QUERY_TIMEOUT_MS = "1.5";
1361
- const { getQueryTimeoutMs } = await import("./config");
1362
- expect(() => getQueryTimeoutMs()).toThrow();
1363
- });
1364
-
1365
- it("rejects a non-numeric override", async () => {
1366
- process.env.PUBLISHER_QUERY_TIMEOUT_MS = "slow";
1367
- const { getQueryTimeoutMs } = await import("./config");
1368
- expect(() => getQueryTimeoutMs()).toThrow();
1369
- });
1370
- });
1371
-
1372
- describe("getMaxConcurrentQueries", () => {
1373
- beforeEach(() => {
1374
- delete process.env.PUBLISHER_MAX_CONCURRENT_QUERIES;
1375
- });
1376
- afterEach(() => {
1377
- delete process.env.PUBLISHER_MAX_CONCURRENT_QUERIES;
1378
- });
1379
-
1380
- it("returns DEFAULT_MAX_CONCURRENT_QUERIES when the env var is unset", async () => {
1381
- const { getMaxConcurrentQueries } = await import("./config");
1382
- const { DEFAULT_MAX_CONCURRENT_QUERIES } = await import("./constants");
1383
- expect(getMaxConcurrentQueries()).toBe(DEFAULT_MAX_CONCURRENT_QUERIES);
1384
- });
1385
-
1386
- it("returns the override when the env var is set", async () => {
1387
- process.env.PUBLISHER_MAX_CONCURRENT_QUERIES = "8";
1388
- const { getMaxConcurrentQueries } = await import("./config");
1389
- expect(getMaxConcurrentQueries()).toBe(8);
1390
- });
1391
-
1392
- it("accepts 0 (used to opt out of the cap)", async () => {
1393
- process.env.PUBLISHER_MAX_CONCURRENT_QUERIES = "0";
1394
- const { getMaxConcurrentQueries } = await import("./config");
1395
- expect(getMaxConcurrentQueries()).toBe(0);
1396
- });
1397
-
1398
- it("rejects a negative override", async () => {
1399
- process.env.PUBLISHER_MAX_CONCURRENT_QUERIES = "-1";
1400
- const { getMaxConcurrentQueries } = await import("./config");
1401
- expect(() => getMaxConcurrentQueries()).toThrow();
1402
- });
1403
-
1404
- it("rejects a non-integer override", async () => {
1405
- process.env.PUBLISHER_MAX_CONCURRENT_QUERIES = "1.5";
1406
- const { getMaxConcurrentQueries } = await import("./config");
1407
- expect(() => getMaxConcurrentQueries()).toThrow();
1408
- });
1409
-
1410
- it("rejects a non-numeric override", async () => {
1411
- process.env.PUBLISHER_MAX_CONCURRENT_QUERIES = "many";
1412
- const { getMaxConcurrentQueries } = await import("./config");
1413
- expect(() => getMaxConcurrentQueries()).toThrow();
1414
- });
1415
- });
1416
-
1417
- describe("getPublisherConfigDir", () => {
1418
- const testRoot = path.join(process.cwd(), "test-temp-config-dir");
1419
- const elsewhere = path.join(testRoot, "elsewhere");
1420
- const savedConfigPath = process.env.PUBLISHER_CONFIG_PATH;
1421
-
1422
- beforeEach(() => {
1423
- fs.mkdirSync(elsewhere, { recursive: true });
1424
- // Both knobs feed resolvePublisherConfigPath, and every spec file shares
1425
- // one process, so clear both rather than inherit whatever ran before.
1426
- delete process.env.PUBLISHER_CONFIG_PATH;
1427
- delete process.env.PUBLISHER_USE_BUNDLED_DEFAULT;
1428
- });
1429
-
1430
- afterEach(() => {
1431
- fs.rmSync(testRoot, { recursive: true, force: true });
1432
- // Clear both, not just the one this describe saves: every spec file shares
1433
- // one process, so a knob left set here follows whatever runs next.
1434
- delete process.env.PUBLISHER_USE_BUNDLED_DEFAULT;
1435
- if (savedConfigPath === undefined) {
1436
- delete process.env.PUBLISHER_CONFIG_PATH;
1437
- } else {
1438
- process.env.PUBLISHER_CONFIG_PATH = savedConfigPath;
1439
- }
1440
- });
1441
-
1442
- it("returns the server root when the config sits there", () => {
1443
- fs.writeFileSync(
1444
- path.join(testRoot, PUBLISHER_CONFIG_NAME),
1445
- JSON.stringify({ environments: [] }),
1446
- );
1447
- expect(getPublisherConfigDir(testRoot)).toBe(testRoot);
1448
- });
1449
-
1450
- it("follows PUBLISHER_CONFIG_PATH to a config outside the server root", () => {
1451
- // The --config case: a relative package location must anchor next to
1452
- // the config that declares it, not at whatever cwd the server booted in.
1453
- const configPath = path.join(elsewhere, PUBLISHER_CONFIG_NAME);
1454
- fs.writeFileSync(configPath, JSON.stringify({ environments: [] }));
1455
- process.env.PUBLISHER_CONFIG_PATH = configPath;
1456
- expect(getPublisherConfigDir(testRoot)).toBe(elsewhere);
1457
- });
1458
-
1459
- it("returns null when no config resolves, so callers fall back to the server root", () => {
1460
- expect(getPublisherConfigDir(testRoot)).toBeNull();
1461
- });
1462
-
1463
- it("returns an absolute anchor even when --config is relative", () => {
1464
- // An anchor that is itself relative would re-resolve against the cwd of
1465
- // whoever reads it, which is the bug this whole change is fixing.
1466
- const rel = path.relative(
1467
- process.cwd(),
1468
- path.join(elsewhere, PUBLISHER_CONFIG_NAME),
1469
- );
1470
- fs.writeFileSync(
1471
- path.join(elsewhere, PUBLISHER_CONFIG_NAME),
1472
- JSON.stringify({ environments: [] }),
1473
- );
1474
- process.env.PUBLISHER_CONFIG_PATH = rel;
1475
- const anchor = getPublisherConfigDir(testRoot);
1476
- expect(anchor).not.toBeNull();
1477
- expect(path.isAbsolute(anchor as string)).toBe(true);
1478
- expect(anchor).toBe(elsewhere);
1479
- });
1480
-
1481
- it("returns null when --config names a directory instead of a file", () => {
1482
- // existsSync is true for a directory, so without a file check the anchor
1483
- // would silently become that directory's PARENT, for a config that never
1484
- // loads (getPublisherConfig catches the read error and returns empty).
1485
- process.env.PUBLISHER_CONFIG_PATH = elsewhere;
1486
- expect(getPublisherConfigDir(testRoot)).toBeNull();
1487
- });
1488
-
1489
- it("returns null for the bundled default rather than anchoring inside the install", () => {
1490
- // Zero-arg `npx @malloy-publisher/server` resolves the config that ships
1491
- // inside the package. Anchoring a user's relative location under
1492
- // node_modules would be meaningless, so the caller falls back to the
1493
- // server root, which is what this did before the anchor moved.
1494
- process.env.PUBLISHER_USE_BUNDLED_DEFAULT = "true";
1495
- expect(getPublisherConfigDir(testRoot)).toBeNull();
1496
- });
1497
- });
1498
-
1499
- describe("PERSIST_COLLISION_ENFORCE", () => {
1500
- // The flag decides whether a persist-target collision blocks a publish, so
1501
- // mis-parsing it leaves the check warn-only — failing open in exactly the
1502
- // direction it exists to prevent. An ad-hoc `=== "true"` did that for `1`,
1503
- // `yes` and `on`.
1504
- const prev = process.env.PERSIST_COLLISION_ENFORCE;
1505
- afterEach(() => {
1506
- if (prev === undefined) delete process.env.PERSIST_COLLISION_ENFORCE;
1507
- else process.env.PERSIST_COLLISION_ENFORCE = prev;
1508
- });
1509
-
1510
- it("defaults to warn-only when unset or empty", () => {
1511
- delete process.env.PERSIST_COLLISION_ENFORCE;
1512
- expect(getPersistCollisionEnforce()).toBe(false);
1513
- process.env.PERSIST_COLLISION_ENFORCE = " ";
1514
- expect(getPersistCollisionEnforce()).toBe(false);
1515
- });
1516
-
1517
- it("enforces for every spelling of true an operator might use", () => {
1518
- for (const raw of ["true", "TRUE", " True ", "1", "yes", "on"]) {
1519
- process.env.PERSIST_COLLISION_ENFORCE = raw;
1520
- expect(getPersistCollisionEnforce()).toBe(true);
1521
- }
1522
- });
1523
-
1524
- it("stays warn-only for every spelling of false", () => {
1525
- for (const raw of ["false", "FALSE", "0", "no", "off"]) {
1526
- process.env.PERSIST_COLLISION_ENFORCE = raw;
1527
- expect(getPersistCollisionEnforce()).toBe(false);
1528
- }
1529
- });
1530
-
1531
- it("throws on a value that is neither, rather than guessing", () => {
1532
- process.env.PERSIST_COLLISION_ENFORCE = "enabled";
1533
- expect(() => getPersistCollisionEnforce()).toThrow(/expected a boolean/i);
1534
- });
1535
- });