@antelopejs/dms 0.15.1 → 0.16.1

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 (295) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +36 -26
  3. package/bin/control/base.mjs +9 -4
  4. package/bin/control/browser-client.mjs +24 -5
  5. package/bin/control/browser-wire.mjs +41 -0
  6. package/bin/control/browser-wire.test.mjs +96 -0
  7. package/bin/control/browser.mjs +78 -27
  8. package/bin/control/commands-stack.mjs +58 -8
  9. package/bin/control/commands-verify.mjs +47 -1
  10. package/bin/control/doctor.mjs +49 -4
  11. package/bin/control/failure.mjs +9 -0
  12. package/bin/control/journey.mjs +158 -30
  13. package/bin/control/journey.test.mjs +61 -0
  14. package/bin/control/json.mjs +229 -0
  15. package/bin/control/json.test.mjs +130 -0
  16. package/bin/control/mail.mjs +57 -7
  17. package/bin/control/mail.test.mjs +55 -0
  18. package/bin/control/mongo.mjs +205 -27
  19. package/bin/control/mongo.test.mjs +298 -0
  20. package/bin/control/options.mjs +33 -0
  21. package/bin/control/processes.mjs +24 -2
  22. package/bin/control/renderer.mjs +39 -0
  23. package/bin/control/renderer.test.mjs +324 -0
  24. package/bin/control/run-state.mjs +119 -6
  25. package/bin/control/run-state.test.mjs +171 -0
  26. package/bin/control/session.mjs +30 -14
  27. package/bin/control/stack.mjs +95 -24
  28. package/bin/control/tab.mjs +117 -18
  29. package/bin/control/verify/behaviour.mjs +44 -6
  30. package/bin/control/verify/behaviour.test.mjs +130 -22
  31. package/bin/control/verify/decode.mjs +220 -0
  32. package/bin/control/verify/decode.test.mjs +245 -0
  33. package/bin/control/verify/doors.mjs +1 -1
  34. package/bin/control/verify/doors.test.mjs +40 -0
  35. package/bin/control/verify/emit.mjs +135 -9
  36. package/bin/control/verify/formal.mjs +61 -10
  37. package/bin/control/verify/formal.test.mjs +19 -3
  38. package/bin/control/verify/judgement.mjs +22 -3
  39. package/bin/control/verify/map.mjs +25 -5
  40. package/bin/control/verify/publish.mjs +20 -14
  41. package/bin/control/verify/seeded.mjs +65 -16
  42. package/bin/control/verify/skill.mjs +27 -6
  43. package/bin/control/verify/types.mjs +29 -0
  44. package/bin/control/verify/verdict.mjs +72 -7
  45. package/bin/dms-control.mjs +8 -28
  46. package/checks/api-error-presentation.d.mts +61 -0
  47. package/dist/attachments/access.js +1 -1
  48. package/dist/attachments/access.js.map +1 -1
  49. package/dist/attachments/fields.d.ts +2 -2
  50. package/dist/attachments/fields.js +26 -13
  51. package/dist/attachments/fields.js.map +1 -1
  52. package/dist/attachments/index.d.ts +1 -0
  53. package/dist/attachments/index.js +1 -0
  54. package/dist/attachments/index.js.map +1 -1
  55. package/dist/attachments/read.js.map +1 -1
  56. package/dist/attachments/save.d.ts +1 -1
  57. package/dist/attachments/save.js +49 -17
  58. package/dist/attachments/save.js.map +1 -1
  59. package/dist/attachments/table-view.d.ts +1 -2
  60. package/dist/attachments/table-view.js +23 -13
  61. package/dist/attachments/table-view.js.map +1 -1
  62. package/dist/automation/events.d.ts +10 -10
  63. package/dist/automation/index.js +3 -4
  64. package/dist/automation/index.js.map +1 -1
  65. package/dist/automation/send-notification.d.ts +2 -2
  66. package/dist/automation/send-notification.js +4 -6
  67. package/dist/automation/send-notification.js.map +1 -1
  68. package/dist/crons/cleanup-user-invites.js.map +1 -1
  69. package/dist/crons/sweep-expired-sessions.js.map +1 -1
  70. package/dist/crons/sweep-stale-exports.js.map +1 -1
  71. package/dist/db/guard-row.d.ts +2 -3
  72. package/dist/db/guard-row.js +1 -2
  73. package/dist/db/guard-row.js.map +1 -1
  74. package/dist/db/models/attachments.model.d.ts +7 -5
  75. package/dist/db/models/setupToken.model.d.ts +7 -5
  76. package/dist/db/models/signInAttempts.model.d.ts +7 -5
  77. package/dist/db/models/systemState.model.d.ts +7 -5
  78. package/dist/db/models/userEmailChanges.model.d.ts +7 -5
  79. package/dist/db/models/userKnownDevices.model.d.ts +7 -5
  80. package/dist/db/models/userNotificationPreferences.model.d.ts +7 -5
  81. package/dist/db/models/userNotifications.model.d.ts +9 -6
  82. package/dist/db/models/userNotifications.model.js.map +1 -1
  83. package/dist/dev/module-update-notifications.js.map +1 -1
  84. package/dist/hooks/internal/sign-in-monitor.js +6 -6
  85. package/dist/hooks/internal/sign-in-monitor.js.map +1 -1
  86. package/dist/implementations/dms/page/controller.d.ts +2 -2
  87. package/dist/implementations/dms/page/controller.js +3 -1
  88. package/dist/implementations/dms/page/controller.js.map +1 -1
  89. package/dist/implementations/dms/page/dynamic-menu.js +3 -2
  90. package/dist/implementations/dms/page/dynamic-menu.js.map +1 -1
  91. package/dist/implementations/dms/page/notifications.d.ts +6 -2
  92. package/dist/implementations/dms/page/notifications.js +1 -0
  93. package/dist/implementations/dms/page/notifications.js.map +1 -1
  94. package/dist/implementations/dms/page/preview-pages.js +6 -3
  95. package/dist/implementations/dms/page/preview-pages.js.map +1 -1
  96. package/dist/implementations/dms/page/registry.js +10 -4
  97. package/dist/implementations/dms/page/registry.js.map +1 -1
  98. package/dist/implementations/dms/permissions.js +12 -12
  99. package/dist/implementations/dms/permissions.js.map +1 -1
  100. package/dist/implementations/dms-base/bulk-selection.d.ts +3 -4
  101. package/dist/implementations/dms-base/bulk-selection.js +18 -6
  102. package/dist/implementations/dms-base/bulk-selection.js.map +1 -1
  103. package/dist/implementations/dms-base/filter-tokens.js.map +1 -1
  104. package/dist/implementations/dms-base/footer-summary.js +44 -27
  105. package/dist/implementations/dms-base/footer-summary.js.map +1 -1
  106. package/dist/implementations/dms-base/row-rule-operand.d.ts +6 -0
  107. package/dist/implementations/dms-base/row-rule-operand.js +41 -0
  108. package/dist/implementations/dms-base/row-rule-operand.js.map +1 -0
  109. package/dist/implementations/dms-base/search-route.d.ts +9 -13
  110. package/dist/implementations/dms-base/search-route.js +7 -6
  111. package/dist/implementations/dms-base/search-route.js.map +1 -1
  112. package/dist/implementations/dms-base/services.d.ts +1 -0
  113. package/dist/implementations/dms-base/services.js +1 -0
  114. package/dist/implementations/dms-base/services.js.map +1 -1
  115. package/dist/implementations/dms-base/table-view.d.ts +5 -6
  116. package/dist/implementations/dms-base/table-view.js +17 -6
  117. package/dist/implementations/dms-base/table-view.js.map +1 -1
  118. package/dist/implementations/dms-html-render/index.js +16 -6
  119. package/dist/implementations/dms-html-render/index.js.map +1 -1
  120. package/dist/implementations/dms-notifications/delivery-guard.d.ts +3 -2
  121. package/dist/implementations/dms-notifications/delivery-guard.js.map +1 -1
  122. package/dist/implementations/dms-notifications/index.js.map +1 -1
  123. package/dist/index.js.map +1 -1
  124. package/dist/pages/public/preview/block-types.d.ts +1 -2
  125. package/dist/pages/public/preview/block-types.js.map +1 -1
  126. package/dist/pages/settings/overview/account-summary.js +4 -1
  127. package/dist/pages/settings/overview/account-summary.js.map +1 -1
  128. package/dist/pages/settings/users/account-activity-feed.js +3 -2
  129. package/dist/pages/settings/users/account-activity-feed.js.map +1 -1
  130. package/dist/pages/settings/users/account-credentials.d.ts +1 -1
  131. package/dist/pages/settings/users/account-credentials.js +1 -2
  132. package/dist/pages/settings/users/account-credentials.js.map +1 -1
  133. package/dist/pages/settings/users/account-data-store.d.ts +1 -1
  134. package/dist/pages/settings/users/account-data-store.js +1 -3
  135. package/dist/pages/settings/users/account-data-store.js.map +1 -1
  136. package/dist/pages/settings/users/email-change.d.ts +2 -2
  137. package/dist/pages/settings/users/email-change.js +2 -3
  138. package/dist/pages/settings/users/email-change.js.map +1 -1
  139. package/dist/pages/settings/users/invite-extension-routes.d.ts +2 -2
  140. package/dist/pages/settings/users/invite-extension-routes.js +11 -4
  141. package/dist/pages/settings/users/invite-extension-routes.js.map +1 -1
  142. package/dist/pages/settings/users/invites.d.ts +2 -2
  143. package/dist/pages/settings/users/member-invite-batch.d.ts +2 -2
  144. package/dist/pages/settings/users/member-invite-batch.js +7 -3
  145. package/dist/pages/settings/users/member-invite-batch.js.map +1 -1
  146. package/dist/pages/settings/users/member-roles-field.js +1 -4
  147. package/dist/pages/settings/users/member-roles-field.js.map +1 -1
  148. package/dist/pages/settings/users/members.d.ts +2 -3
  149. package/dist/pages/settings/users/members.js.map +1 -1
  150. package/dist/pages/settings/users/notifications.d.ts +2 -3
  151. package/dist/pages/settings/users/notifications.js +2 -1
  152. package/dist/pages/settings/users/notifications.js.map +1 -1
  153. package/dist/pages/settings/users/profile-helpers.d.ts +0 -1
  154. package/dist/pages/settings/users/profile-helpers.js +0 -17
  155. package/dist/pages/settings/users/profile-helpers.js.map +1 -1
  156. package/dist/pages/settings/users/profile.d.ts +3 -4
  157. package/dist/pages/settings/users/profile.js +12 -12
  158. package/dist/pages/settings/users/profile.js.map +1 -1
  159. package/dist/pages/settings/users/region.d.ts +3 -4
  160. package/dist/pages/settings/users/region.js.map +1 -1
  161. package/dist/pages/settings/users/regional-preferences.d.ts +8 -1
  162. package/dist/pages/settings/users/regional-preferences.js +10 -6
  163. package/dist/pages/settings/users/regional-preferences.js.map +1 -1
  164. package/dist/pages/settings/users/role-preview.js +3 -2
  165. package/dist/pages/settings/users/role-preview.js.map +1 -1
  166. package/dist/pages/settings/users/roles.d.ts +4 -5
  167. package/dist/pages/settings/users/roles.js.map +1 -1
  168. package/dist/pages/settings/users/security.d.ts +9 -12
  169. package/dist/pages/settings/users/security.js +11 -9
  170. package/dist/pages/settings/users/security.js.map +1 -1
  171. package/dist/pages/settings/users/two-factor-operations.d.ts +7 -7
  172. package/dist/pages/settings/users/two-factor-operations.js +5 -7
  173. package/dist/pages/settings/users/two-factor-operations.js.map +1 -1
  174. package/dist/realtime/broker.js.map +1 -1
  175. package/dist/realtime/heartbeat.js.map +1 -1
  176. package/dist/realtime/redis-broker.js.map +1 -1
  177. package/dist/realtime/sessions.js.map +1 -1
  178. package/dist/realtime/sse.js +1 -2
  179. package/dist/realtime/sse.js.map +1 -1
  180. package/dist/routes/auth/forgot-password.d.ts +1 -1
  181. package/dist/routes/auth/forgot-password.js +1 -3
  182. package/dist/routes/auth/forgot-password.js.map +1 -1
  183. package/dist/routes/auth/login.d.ts +1 -1
  184. package/dist/routes/auth/login.js +1 -2
  185. package/dist/routes/auth/login.js.map +1 -1
  186. package/dist/routes/auth/logout.d.ts +1 -1
  187. package/dist/routes/auth/logout.js +1 -2
  188. package/dist/routes/auth/logout.js.map +1 -1
  189. package/dist/routes/auth/oauth/providers.js +30 -6
  190. package/dist/routes/auth/oauth/providers.js.map +1 -1
  191. package/dist/routes/auth/oauth/state.js +8 -5
  192. package/dist/routes/auth/oauth/state.js.map +1 -1
  193. package/dist/routes/auth/oauth/token-exchange.js +8 -1
  194. package/dist/routes/auth/oauth/token-exchange.js.map +1 -1
  195. package/dist/routes/auth/refresh.d.ts +1 -1
  196. package/dist/routes/auth/refresh.js +1 -2
  197. package/dist/routes/auth/refresh.js.map +1 -1
  198. package/dist/routes/auth/request-2fa-email.d.ts +1 -1
  199. package/dist/routes/auth/request-2fa-email.js +1 -2
  200. package/dist/routes/auth/request-2fa-email.js.map +1 -1
  201. package/dist/routes/auth/reset-password.d.ts +1 -1
  202. package/dist/routes/auth/reset-password.js +1 -3
  203. package/dist/routes/auth/reset-password.js.map +1 -1
  204. package/dist/routes/auth/session-handoff.d.ts +1 -1
  205. package/dist/routes/auth/session-handoff.js +1 -2
  206. package/dist/routes/auth/session-handoff.js.map +1 -1
  207. package/dist/routes/auth/signup.d.ts +1 -1
  208. package/dist/routes/auth/signup.js +1 -2
  209. package/dist/routes/auth/signup.js.map +1 -1
  210. package/dist/routes/auth/switch-tenant.d.ts +2 -3
  211. package/dist/routes/auth/switch-tenant.js +3 -2
  212. package/dist/routes/auth/switch-tenant.js.map +1 -1
  213. package/dist/routes/auth/validate-forgot-password-token.d.ts +1 -1
  214. package/dist/routes/auth/validate-forgot-password-token.js +1 -3
  215. package/dist/routes/auth/validate-forgot-password-token.js.map +1 -1
  216. package/dist/routes/auth/validate-invite-token.d.ts +1 -1
  217. package/dist/routes/auth/validate-invite-token.js +1 -3
  218. package/dist/routes/auth/validate-invite-token.js.map +1 -1
  219. package/dist/routes/auth/verify-2fa.d.ts +1 -1
  220. package/dist/routes/auth/verify-2fa.js +1 -2
  221. package/dist/routes/auth/verify-2fa.js.map +1 -1
  222. package/dist/routes/auth/verify-email.d.ts +1 -1
  223. package/dist/routes/auth/verify-email.js +1 -2
  224. package/dist/routes/auth/verify-email.js.map +1 -1
  225. package/dist/routes/auth.d.ts +14 -15
  226. package/dist/routes/auth.js +15 -13
  227. package/dist/routes/auth.js.map +1 -1
  228. package/dist/routes/files.d.ts +1 -2
  229. package/dist/routes/files.js.map +1 -1
  230. package/dist/routes/onboarding.d.ts +1 -2
  231. package/dist/routes/onboarding.js.map +1 -1
  232. package/dist/routes/realtime.js +5 -6
  233. package/dist/routes/realtime.js.map +1 -1
  234. package/dist/routes/store.d.ts +6 -7
  235. package/dist/routes/store.js +19 -13
  236. package/dist/routes/store.js.map +1 -1
  237. package/dist/store/catalog.d.ts +13 -5
  238. package/dist/store/catalog.js +19 -9
  239. package/dist/store/catalog.js.map +1 -1
  240. package/dist/store/compatibility.d.ts +1 -0
  241. package/dist/store/compatibility.js +24 -8
  242. package/dist/store/compatibility.js.map +1 -1
  243. package/dist/store/frontend-compatibility.js +15 -3
  244. package/dist/store/frontend-compatibility.js.map +1 -1
  245. package/dist/store/interfaces.js +35 -13
  246. package/dist/store/interfaces.js.map +1 -1
  247. package/dist/store/internal/frontend/frontend-modules.js +7 -3
  248. package/dist/store/internal/frontend/frontend-modules.js.map +1 -1
  249. package/dist/store/internal/frontend/frontend-ready.d.ts +2 -1
  250. package/dist/store/internal/frontend/frontend-ready.js +6 -4
  251. package/dist/store/internal/frontend/frontend-ready.js.map +1 -1
  252. package/dist/store/internal/frontend/manifest.js +6 -7
  253. package/dist/store/internal/frontend/manifest.js.map +1 -1
  254. package/dist/store/operations.js.map +1 -1
  255. package/dist/store/registry.js.map +1 -1
  256. package/dist/store/runtime.d.ts +13 -0
  257. package/dist/store/runtime.js +9 -1
  258. package/dist/store/runtime.js.map +1 -1
  259. package/dist/utils/admin-invite-email.js +3 -1
  260. package/dist/utils/admin-invite-email.js.map +1 -1
  261. package/dist/utils/email-subjects.js +3 -1
  262. package/dist/utils/email-subjects.js.map +1 -1
  263. package/dist/utils/export-jobs-run.js.map +1 -1
  264. package/dist/utils/export-jobs.js +3 -9
  265. package/dist/utils/export-jobs.js.map +1 -1
  266. package/dist/utils/index.d.ts +1 -0
  267. package/dist/utils/index.js +1 -0
  268. package/dist/utils/index.js.map +1 -1
  269. package/dist/utils/own-value.d.ts +1 -0
  270. package/dist/utils/own-value.js +7 -0
  271. package/dist/utils/own-value.js.map +1 -0
  272. package/dist/utils/row-action-rule-evaluator.js +31 -35
  273. package/dist/utils/row-action-rule-evaluator.js.map +1 -1
  274. package/dist/utils/sign-in-country.js +4 -2
  275. package/dist/utils/sign-in-country.js.map +1 -1
  276. package/dist/utils/tenant-export-archive.js +9 -14
  277. package/dist/utils/tenant-export-archive.js.map +1 -1
  278. package/dist/utils/user-agent.js +2 -1
  279. package/dist/utils/user-agent.js.map +1 -1
  280. package/dist/validation/auth.schema.d.ts +10 -10
  281. package/dist/validation/regional-preferences.schema.d.ts +2 -2
  282. package/dist/validation/role-editor.schema.d.ts +1 -1
  283. package/frontend-vue/build/backend-boundary.ts +49 -18
  284. package/frontend-vue/dms.frontend.ts +8 -12
  285. package/frontend-vue/layers/dms-auth/server/accounts.ts +44 -3
  286. package/frontend-vue/layers/dms-auth/server/actions.ts +17 -4
  287. package/frontend-vue/layers/dms-auth/server/oauth.ts +42 -4
  288. package/frontend-vue/layers/dms-core/app/types/watch.ts +1 -2
  289. package/frontend-vue/layers/dms-ui/app/build/composables/form/fieldValueRule.ts +57 -6
  290. package/frontend-vue/layers/dms-ui/app/build/composables/form/registerFormFunctions.ts +7 -9
  291. package/frontend-vue/layers/dms-ui/app/entries/composables/form/registerFormFunctions.ts +1 -0
  292. package/frontend-vue/lazy-plugin.ts +18 -0
  293. package/frontend-vue/package.json +2 -2
  294. package/package.json +19 -13
  295. package/skills/dms-project/SKILL.md +4 -3
@@ -0,0 +1,298 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import test from "node:test";
6
+ import { arrayOf, decode, isNumber, isString, objectOf } from "./json.mjs";
7
+ import { dropDatabase, dropRunDatabases, mongodBinary } from "./mongo.mjs";
8
+
9
+ const isDriverLog = objectOf({
10
+ label: isString,
11
+ built: isNumber,
12
+ closed: isNumber,
13
+ dropped: arrayOf(isString),
14
+ });
15
+
16
+ /**
17
+ * @param {import("node:test").TestContext} t
18
+ * @param {Record<string, string>} packages Package name to the source of its `index.js`
19
+ * @returns {string} The folder whose `node_modules` holds them
20
+ */
21
+ function project(t, packages) {
22
+ const folder = mkdtempSync(join(tmpdir(), "dms-control-mongo-test-"));
23
+ t.after(() => rmSync(folder, { recursive: true, force: true }));
24
+ writeFileSync(join(folder, "package.json"), "{}");
25
+ for (const [name, source] of Object.entries(packages)) {
26
+ const root = join(folder, "node_modules", name);
27
+ mkdirSync(root, { recursive: true });
28
+ writeFileSync(join(root, "package.json"), `{"name":"${name}"}`);
29
+ writeFileSync(join(root, "index.js"), source);
30
+ }
31
+ return folder;
32
+ }
33
+
34
+ /**
35
+ * A driver whose client lists databases and records what is done to it in `globalThis.fakeMongo`, labelled by `label`.
36
+ * @param {{ label?: string, names?: string, listing?: string, client?: string, exported?: string }} [shape] `exported` is the expression the module exports as `MongoClient`
37
+ * @returns {string}
38
+ */
39
+ function fakeDriver({
40
+ label = "driver",
41
+ names = '["verify_run1_a", "verify_run1_b", "other", "verify_run2_a"]',
42
+ listing = `{ databases: ${names}.map((name) => ({ name })), ok: 1 }`,
43
+ client = "",
44
+ exported = "MongoClient",
45
+ } = {}) {
46
+ return `
47
+ const state = { label: "${label}", built: 0, closed: 0, dropped: [] };
48
+ Object.defineProperty(globalThis, "fakeMongo", { value: state, configurable: true });
49
+ class MongoClient {
50
+ constructor() { state.built += 1; }
51
+ async connect() {}
52
+ async close() { state.closed += 1; }
53
+ db(name) {
54
+ return {
55
+ admin: () => ({ listDatabases: async () => (${listing}) }),
56
+ dropDatabase: async () => { state.dropped.push(name); },
57
+ };
58
+ }
59
+ }
60
+ ${client}
61
+ module.exports = { MongoClient: ${exported} };
62
+ `;
63
+ }
64
+
65
+ /**
66
+ * @returns {{ label: string, built: number, closed: number, dropped: string[] }} What was done to the client built last
67
+ */
68
+ function driverLog() {
69
+ /** @type {unknown} */
70
+ const recorded = Object.getOwnPropertyDescriptor(
71
+ globalThis,
72
+ "fakeMongo",
73
+ )?.value;
74
+ return decode(isDriverLog, recorded, "fakeMongo", "the driver log");
75
+ }
76
+
77
+ await test("the run's databases are dropped and every other one is kept", async (t) => {
78
+ const folder = project(t, { mongodb: fakeDriver() });
79
+ const lines = await dropRunDatabases("mongodb://x", "verify_run1", [folder]);
80
+ assert.deepEqual(lines, [
81
+ "dropped database verify_run1_a",
82
+ "dropped database verify_run1_b",
83
+ ]);
84
+ assert.deepEqual(driverLog().dropped, ["verify_run1_a", "verify_run1_b"]);
85
+ assert.equal(driverLog().closed, 1);
86
+ });
87
+
88
+ await test("one database is dropped by name", async (t) => {
89
+ const folder = project(t, { mongodb: fakeDriver() });
90
+ await dropDatabase("mongodb://x", "verify_run1_a", [folder]);
91
+ assert.deepEqual(driverLog().dropped, ["verify_run1_a"]);
92
+ assert.equal(driverLog().closed, 1);
93
+ });
94
+
95
+ await test("the driver beside mongodb-memory-server-core wins over an earlier folder's own", async (t) => {
96
+ const first = project(t, { mongodb: fakeDriver({ label: "first" }) });
97
+ const second = project(t, {
98
+ mongodb: fakeDriver({ label: "second" }),
99
+ "mongodb-memory-server-core": "module.exports = {};",
100
+ });
101
+ await dropDatabase("mongodb://x", "verify_run1_a", [first, second]);
102
+ assert.equal(driverLog().label, "second");
103
+ });
104
+
105
+ await test("a list of databases with a nameless entry is refused and the client still closes", async (t) => {
106
+ const folder = project(t, {
107
+ mongodb: fakeDriver({ listing: "{ databases: [{ id: 1 }] }" }),
108
+ });
109
+ await assert.rejects(
110
+ dropRunDatabases("mongodb://x", "verify_run1", [folder]),
111
+ {
112
+ message: "listDatabases is malformed: expected a list of databases",
113
+ },
114
+ );
115
+ assert.equal(driverLog().closed, 1);
116
+ assert.deepEqual(driverLog().dropped, []);
117
+ });
118
+
119
+ await test("a driver without MongoClient is refused", async (t) => {
120
+ const folder = project(t, {
121
+ mongodb: "module.exports = { MongoClient: 1 };",
122
+ });
123
+ await assert.rejects(dropDatabase("mongodb://x", "a", [folder]), {
124
+ message: "The MongoDB driver exports no MongoClient",
125
+ });
126
+ });
127
+
128
+ await test("an arrow function is no MongoClient and is never called", async (t) => {
129
+ const folder = project(t, {
130
+ mongodb: fakeDriver({ exported: "(url) => { state.built += 1; }" }),
131
+ });
132
+ await assert.rejects(dropDatabase("mongodb://x", "a", [folder]), {
133
+ message: "The MongoDB driver exports no MongoClient",
134
+ });
135
+ assert.equal(driverLog().built, 0);
136
+ });
137
+
138
+ await test("a class is a MongoClient and is built once", async (t) => {
139
+ const folder = project(t, { mongodb: fakeDriver() });
140
+ await dropDatabase("mongodb://x", "a", [folder]);
141
+ assert.equal(driverLog().built, 1);
142
+ assert.deepEqual(driverLog().dropped, ["a"]);
143
+ });
144
+
145
+ await test("a bound class is a MongoClient and is built once", async (t) => {
146
+ const folder = project(t, {
147
+ mongodb: fakeDriver({ exported: "MongoClient.bind(null)" }),
148
+ });
149
+ await dropDatabase("mongodb://x", "a", [folder]);
150
+ assert.equal(driverLog().built, 1);
151
+ assert.deepEqual(driverLog().dropped, ["a"]);
152
+ });
153
+
154
+ for (const member of ["connect", "close", "db"]) {
155
+ await test(`a client whose ${member} is no function is refused before it is used`, async (t) => {
156
+ const folder = project(t, {
157
+ mongodb: fakeDriver({ client: `MongoClient.prototype.${member} = 1;` }),
158
+ });
159
+ await assert.rejects(dropDatabase("mongodb://x", "a", [folder]), {
160
+ message: "MongoClient built something that is no client",
161
+ });
162
+ assert.deepEqual(driverLog().dropped, []);
163
+ });
164
+ }
165
+
166
+ await test("a client without close is refused before it is used", async (t) => {
167
+ const folder = project(t, {
168
+ mongodb: fakeDriver({ client: "delete MongoClient.prototype.close;" }),
169
+ });
170
+ await assert.rejects(dropDatabase("mongodb://x", "a", [folder]), {
171
+ message: "MongoClient built something that is no client",
172
+ });
173
+ assert.deepEqual(driverLog().dropped, []);
174
+ });
175
+
176
+ const NO_DATABASES = [
177
+ ["nothing", "{}"],
178
+ ["no admin", "{ dropDatabase: async () => {} }"],
179
+ ["no dropDatabase", "{ admin: () => ({}) }"],
180
+ [
181
+ "an admin that is no function",
182
+ "{ admin: 1, dropDatabase: async () => {} }",
183
+ ],
184
+ [
185
+ "a dropDatabase that is no function",
186
+ "{ admin: () => ({}), dropDatabase: 1 }",
187
+ ],
188
+ ];
189
+
190
+ for (const [kind, database] of NO_DATABASES) {
191
+ await test(`a driver whose db() answers ${kind} is refused`, async (t) => {
192
+ const folder = project(t, {
193
+ mongodb: fakeDriver({
194
+ client: `MongoClient.prototype.db = () => (${database});`,
195
+ }),
196
+ });
197
+ await assert.rejects(dropDatabase("mongodb://x", "a", [folder]), {
198
+ message: "MongoClient.db(a) returned no database",
199
+ });
200
+ assert.equal(driverLog().closed, 1);
201
+ });
202
+ }
203
+
204
+ const NO_LIST_DATABASES = [
205
+ ["no listDatabases", "{}"],
206
+ ["a listDatabases that is no function", "{ listDatabases: 1 }"],
207
+ ];
208
+
209
+ for (const [kind, admin] of NO_LIST_DATABASES) {
210
+ await test(`an admin handle with ${kind} is refused and drops nothing`, async (t) => {
211
+ const folder = project(t, {
212
+ mongodb: fakeDriver({
213
+ client: `MongoClient.prototype.db = () => ({ admin: () => (${admin}), dropDatabase: async () => { state.dropped.push("any"); } });`,
214
+ }),
215
+ });
216
+ await assert.rejects(
217
+ dropRunDatabases("mongodb://x", "verify_run1", [folder]),
218
+ {
219
+ message: "The admin handle of the driver has no listDatabases",
220
+ },
221
+ );
222
+ assert.equal(driverLog().closed, 1);
223
+ assert.deepEqual(driverLog().dropped, []);
224
+ });
225
+ }
226
+
227
+ await test("a missing driver names the devDependency to add", async (t) => {
228
+ const folder = project(t, {});
229
+ await assert.rejects(dropDatabase("mongodb://x", "a", [folder]), {
230
+ message:
231
+ "dms-control needs mongodb: add it to the playground's devDependencies.",
232
+ });
233
+ });
234
+
235
+ /**
236
+ * @param {import("node:test").TestContext} t
237
+ * @param {string} resolver The source of the `getPath` the package exports
238
+ * @returns {string}
239
+ */
240
+ function memoryServer(t, resolver) {
241
+ return project(t, {
242
+ "mongodb-memory-server-core": `
243
+ module.exports = { MongoBinary: { getPath: async (options) => { Object.defineProperty(globalThis, "fakeBinaryRequest", { value: options, configurable: true }); return ${resolver}; } } };
244
+ `,
245
+ });
246
+ }
247
+
248
+ /**
249
+ * @param {import("node:test").TestContext} t
250
+ */
251
+ function withoutConfiguredMongod(t) {
252
+ const saved = { ...process.env };
253
+ t.after(() => {
254
+ process.env = saved;
255
+ });
256
+ delete process.env.DMS_CONTROL_MONGOD;
257
+ process.env.PATH = "";
258
+ }
259
+
260
+ await test("the binary comes from the memory server package when none is configured", async (t) => {
261
+ withoutConfiguredMongod(t);
262
+ const folder = memoryServer(t, '"/cache/mongod"');
263
+ assert.equal(await mongodBinary([folder]), "/cache/mongod");
264
+ assert.deepEqual(
265
+ Object.getOwnPropertyDescriptor(globalThis, "fakeBinaryRequest")?.value,
266
+ { version: "8.0.8" },
267
+ );
268
+ });
269
+
270
+ await test("a binary path that is no string is refused", async (t) => {
271
+ withoutConfiguredMongod(t);
272
+ const folder = memoryServer(t, "42");
273
+ await assert.rejects(mongodBinary([folder]), {
274
+ message:
275
+ "MongoBinary.getPath is malformed: expected the path of a mongod binary",
276
+ });
277
+ });
278
+
279
+ await test("a MongoBinary whose getPath is no function is refused", async (t) => {
280
+ withoutConfiguredMongod(t);
281
+ const folder = project(t, {
282
+ "mongodb-memory-server-core":
283
+ "module.exports = { MongoBinary: { getPath: 42 } };",
284
+ });
285
+ await assert.rejects(mongodBinary([folder]), {
286
+ message: "mongodb-memory-server-core exports no MongoBinary",
287
+ });
288
+ });
289
+
290
+ await test("a memory server package without MongoBinary is refused", async (t) => {
291
+ withoutConfiguredMongod(t);
292
+ const folder = project(t, {
293
+ "mongodb-memory-server-core": "module.exports = {};",
294
+ });
295
+ await assert.rejects(mongodBinary([folder]), {
296
+ message: "mongodb-memory-server-core exports no MongoBinary",
297
+ });
298
+ });
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The command line of dms-control: every option any command reads.
3
+ */
4
+ export const OPTIONS = /** @type {const} */ ({
5
+ prepare: { type: "boolean" },
6
+ playground: { type: "string" },
7
+ renderer: { type: "string" },
8
+ runs: { type: "string" },
9
+ "mongo-url": { type: "string" },
10
+ env: { type: "string", multiple: true },
11
+ base: { type: "string", default: "origin/main" },
12
+ skill: { type: "string" },
13
+ verbose: { type: "boolean" },
14
+ "platform-name": { type: "string" },
15
+ as: { type: "string" },
16
+ tab: { type: "string" },
17
+ role: { type: "string" },
18
+ name: { type: "string" },
19
+ exact: { type: "boolean" },
20
+ nth: { type: "string" },
21
+ "within-role": { type: "string" },
22
+ "within-name": { type: "string" },
23
+ value: { type: "string" },
24
+ key: { type: "string" },
25
+ path: { type: "string" },
26
+ body: { type: "string" },
27
+ pr: { type: "string" },
28
+ all: { type: "boolean" },
29
+ });
30
+
31
+ /**
32
+ * @typedef {ReturnType<typeof import("node:util").parseArgs<{ options: typeof OPTIONS, allowPositionals: true }>>["values"]} Values The options of one command line
33
+ */
@@ -17,6 +17,14 @@ const PROBE_TIMEOUT_MS = 3_000;
17
17
  const LISTEN_STATE = "0A";
18
18
  const HEX = 16;
19
19
 
20
+ /**
21
+ * @param {string | import("node:net").AddressInfo | null} address What a listening server reports
22
+ * @returns {address is import("node:net").AddressInfo} True for the address of a TCP listener
23
+ */
24
+ function isTcpAddress(address) {
25
+ return typeof address === "object" && address !== null;
26
+ }
27
+
20
28
  /**
21
29
  * Asks the kernel for a free TCP port on the loopback interface.
22
30
  *
@@ -27,8 +35,14 @@ export function freePort() {
27
35
  const server = createServer();
28
36
  server.once("error", reject);
29
37
  server.listen(0, "127.0.0.1", () => {
30
- const { port } = server.address();
31
- server.close(() => resolve(port));
38
+ const address = server.address();
39
+ if (!isTcpAddress(address)) {
40
+ server.close(() =>
41
+ reject(new Error("the probe server has no TCP port")),
42
+ );
43
+ return;
44
+ }
45
+ server.close(() => resolve(address.port));
32
46
  });
33
47
  });
34
48
  }
@@ -150,6 +164,7 @@ export async function waitForOk(url, record, timeoutMs) {
150
164
  );
151
165
  }
152
166
 
167
+ /** @param {number} port */
153
168
  function accepts(port) {
154
169
  return new Promise((resolve) => {
155
170
  const socket = createConnection({ host: "127.0.0.1", port });
@@ -213,6 +228,7 @@ function procEntries() {
213
228
  }
214
229
  }
215
230
 
231
+ /** @param {string} pid */
216
232
  function parentOf(pid) {
217
233
  try {
218
234
  const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
@@ -247,6 +263,7 @@ export function descendantsOf(pids) {
247
263
  return [...found].filter((pid) => !pids.includes(pid));
248
264
  }
249
265
 
266
+ /** @param {number} port */
250
267
  function listeningInodes(port) {
251
268
  const inodes = new Set();
252
269
  for (const table of ["/proc/net/tcp", "/proc/net/tcp6"]) {
@@ -266,6 +283,7 @@ function listeningInodes(port) {
266
283
  return inodes;
267
284
  }
268
285
 
286
+ /** @param {number} pid */
269
287
  function socketInodes(pid) {
270
288
  try {
271
289
  return readdirSync(`/proc/${pid}/fd`).flatMap((fd) => {
@@ -293,6 +311,10 @@ export function listenersOf(port) {
293
311
  .filter((pid) => socketInodes(pid).some((inode) => inodes.has(inode)));
294
312
  }
295
313
 
314
+ /**
315
+ * @param {number} pid
316
+ * @param {NodeJS.Signals} signal
317
+ */
296
318
  function signalGroup(pid, signal) {
297
319
  try {
298
320
  process.kill(-pid, signal);
@@ -0,0 +1,39 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { stringField } from "./json.mjs";
4
+
5
+ /** The package a renderer checkout must be. */
6
+ export const RENDERER_PACKAGE = "@antelopejs/dms-frontend";
7
+
8
+ /** The built CLI of a renderer checkout, relative to it. */
9
+ export const RENDERER_CLI = join("dist", "index.js");
10
+
11
+ /**
12
+ * The renderer checkout a run serves: an absolute folder holding the
13
+ * `@antelopejs/dms-frontend` package with its build.
14
+ *
15
+ * @param {string} folder The `--renderer` folder
16
+ * @returns {string} Its absolute path
17
+ * @throws When the folder is not that package, or is not built
18
+ */
19
+ export function resolveRenderer(folder) {
20
+ const root = resolve(folder);
21
+ const manifest = join(root, "package.json");
22
+ if (!existsSync(manifest)) {
23
+ throw new Error(`--renderer ${root} has no package.json`);
24
+ }
25
+ /** @type {unknown} */
26
+ const parsed = JSON.parse(readFileSync(manifest, "utf8"));
27
+ const name = stringField(parsed, "name");
28
+ if (name !== RENDERER_PACKAGE) {
29
+ throw new Error(
30
+ `--renderer ${root} is ${name ?? "unnamed"}, not ${RENDERER_PACKAGE}`,
31
+ );
32
+ }
33
+ if (!existsSync(join(root, RENDERER_CLI))) {
34
+ throw new Error(
35
+ `--renderer ${root} has no ${RENDERER_CLI}: build it first (pnpm install, pnpm build)`,
36
+ );
37
+ }
38
+ return root;
39
+ }