@reventlessdev/reventless-seed 1.0.0-alpha.20 → 1.0.0-alpha.22

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 (154) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/lib/bs/.compiler.log +2 -2
  3. package/lib/bs/ReventlessSeed.cmi +0 -0
  4. package/lib/bs/ReventlessSeed.cmt +0 -0
  5. package/lib/bs/ReventlessSeed.mlmap +1 -0
  6. package/lib/bs/compiler-info.json +1 -1
  7. package/lib/bs/src/Seed-ReventlessSeed.cmi +0 -0
  8. package/lib/bs/src/Seed-ReventlessSeed.cmt +0 -0
  9. package/lib/bs/src/Seed.ast +0 -0
  10. package/lib/bs/src/Seed.res +8 -2
  11. package/lib/bs/src/Seed_Client-ReventlessSeed.cmi +0 -0
  12. package/lib/bs/src/Seed_Client-ReventlessSeed.cmj +0 -0
  13. package/lib/bs/src/Seed_Client-ReventlessSeed.cmt +0 -0
  14. package/lib/bs/src/Seed_Client.ast +0 -0
  15. package/lib/bs/src/Seed_Client.res +169 -21
  16. package/lib/bs/src/Seed_Client.res.mjs +142 -7
  17. package/lib/bs/src/Seed_Connect-ReventlessSeed.cmi +0 -0
  18. package/lib/bs/src/Seed_Connect-ReventlessSeed.cmj +0 -0
  19. package/lib/bs/src/Seed_Connect-ReventlessSeed.cmt +0 -0
  20. package/lib/bs/src/Seed_Connect.ast +0 -0
  21. package/lib/bs/src/Seed_Connect.res +258 -19
  22. package/lib/bs/src/Seed_Connect.res.mjs +158 -8
  23. package/lib/bs/src/Seed_Prompt-ReventlessSeed.cmi +0 -0
  24. package/lib/bs/src/Seed_Prompt-ReventlessSeed.cmj +0 -0
  25. package/lib/bs/src/Seed_Prompt-ReventlessSeed.cmt +0 -0
  26. package/lib/bs/src/Seed_Prompt.ast +0 -0
  27. package/lib/bs/src/Seed_Prompt.res +48 -23
  28. package/lib/bs/src/Seed_Prompt.res.mjs +33 -22
  29. package/lib/bs/src/Seed_Random-ReventlessSeed.cmi +0 -0
  30. package/lib/bs/src/Seed_Random-ReventlessSeed.cmt +0 -0
  31. package/lib/bs/src/Seed_Random.ast +0 -0
  32. package/lib/bs/src/Seed_Random.res +4 -2
  33. package/lib/bs/src/Seed_Runner-ReventlessSeed.cmi +0 -0
  34. package/lib/bs/src/Seed_Runner-ReventlessSeed.cmt +0 -0
  35. package/lib/bs/src/Seed_Runner.ast +0 -0
  36. package/lib/bs/src/Seed_Runner.res +7 -12
  37. package/lib/bs/src/Seed_Upload-ReventlessSeed.cmi +0 -0
  38. package/lib/bs/src/Seed_Upload-ReventlessSeed.cmt +0 -0
  39. package/lib/bs/src/Seed_Upload.ast +0 -0
  40. package/lib/bs/src/Seed_Upload.res +4 -2
  41. package/lib/bs/src/Seed_Users-ReventlessSeed.cmi +0 -0
  42. package/lib/bs/src/Seed_Users-ReventlessSeed.cmj +0 -0
  43. package/lib/bs/src/Seed_Users-ReventlessSeed.cmt +0 -0
  44. package/lib/bs/src/Seed_Users.ast +0 -0
  45. package/lib/bs/src/Seed_Users.res +30 -3
  46. package/lib/bs/src/Seed_Users.res.mjs +14 -1
  47. package/lib/bs/tests/Seed_ClientTest-ReventlessSeed.cmi +0 -0
  48. package/lib/bs/tests/Seed_ClientTest-ReventlessSeed.cmj +0 -0
  49. package/lib/bs/tests/Seed_ClientTest-ReventlessSeed.cmt +0 -0
  50. package/lib/bs/tests/Seed_ClientTest.ast +0 -0
  51. package/lib/bs/tests/Seed_ClientTest.res +146 -16
  52. package/lib/bs/tests/Seed_ClientTest.res.mjs +102 -1
  53. package/lib/bs/tests/Seed_ConnectTest-ReventlessSeed.cmi +0 -0
  54. package/lib/bs/tests/Seed_ConnectTest-ReventlessSeed.cmj +0 -0
  55. package/lib/bs/tests/Seed_ConnectTest-ReventlessSeed.cmt +0 -0
  56. package/lib/bs/tests/Seed_ConnectTest.ast +0 -0
  57. package/lib/bs/tests/Seed_ConnectTest.res +64 -0
  58. package/lib/bs/tests/Seed_ConnectTest.res.mjs +56 -0
  59. package/lib/bs/tests/Seed_PromptTest-ReventlessSeed.cmi +0 -0
  60. package/lib/bs/tests/Seed_PromptTest-ReventlessSeed.cmt +0 -0
  61. package/lib/bs/tests/Seed_PromptTest.ast +0 -0
  62. package/lib/bs/tests/Seed_PromptTest.res +8 -12
  63. package/lib/bs/tests/Seed_RandomTest-ReventlessSeed.cmi +0 -0
  64. package/lib/bs/tests/Seed_RandomTest-ReventlessSeed.cmt +0 -0
  65. package/lib/bs/tests/Seed_RandomTest.ast +0 -0
  66. package/lib/bs/tests/Seed_RandomTest.res +130 -90
  67. package/lib/bs/tests/Seed_UsersTest-ReventlessSeed.cmi +0 -0
  68. package/lib/bs/tests/Seed_UsersTest-ReventlessSeed.cmt +0 -0
  69. package/lib/bs/tests/Seed_UsersTest.ast +0 -0
  70. package/lib/bs/tests/Seed_UsersTest.res +77 -2
  71. package/lib/bs/tests/Seed_UsersTest.res.mjs +45 -2
  72. package/lib/ocaml/.compiler.log +2 -2
  73. package/lib/ocaml/ReventlessSeed.cmi +0 -0
  74. package/lib/ocaml/ReventlessSeed.cmt +0 -0
  75. package/lib/ocaml/ReventlessSeed.mlmap +1 -0
  76. package/lib/ocaml/Seed-ReventlessSeed.cmi +0 -0
  77. package/lib/ocaml/Seed-ReventlessSeed.cmt +0 -0
  78. package/lib/ocaml/Seed.ast +0 -0
  79. package/lib/ocaml/Seed.res +8 -2
  80. package/lib/ocaml/Seed_Client-ReventlessSeed.cmi +0 -0
  81. package/lib/ocaml/Seed_Client-ReventlessSeed.cmj +0 -0
  82. package/lib/ocaml/Seed_Client-ReventlessSeed.cmt +0 -0
  83. package/lib/ocaml/Seed_Client.ast +0 -0
  84. package/lib/ocaml/Seed_Client.res +169 -21
  85. package/lib/ocaml/Seed_ClientTest-ReventlessSeed.cmi +0 -0
  86. package/lib/ocaml/Seed_ClientTest-ReventlessSeed.cmj +0 -0
  87. package/lib/ocaml/Seed_ClientTest-ReventlessSeed.cmt +0 -0
  88. package/lib/ocaml/Seed_ClientTest.ast +0 -0
  89. package/lib/ocaml/Seed_ClientTest.res +146 -16
  90. package/lib/ocaml/Seed_Connect-ReventlessSeed.cmi +0 -0
  91. package/lib/ocaml/Seed_Connect-ReventlessSeed.cmj +0 -0
  92. package/lib/ocaml/Seed_Connect-ReventlessSeed.cmt +0 -0
  93. package/lib/ocaml/Seed_Connect.ast +0 -0
  94. package/lib/ocaml/Seed_Connect.res +258 -19
  95. package/lib/ocaml/Seed_ConnectTest-ReventlessSeed.cmi +0 -0
  96. package/lib/ocaml/Seed_ConnectTest-ReventlessSeed.cmj +0 -0
  97. package/lib/ocaml/Seed_ConnectTest-ReventlessSeed.cmt +0 -0
  98. package/lib/ocaml/Seed_ConnectTest.ast +0 -0
  99. package/lib/ocaml/Seed_ConnectTest.res +64 -0
  100. package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmi +0 -0
  101. package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmj +0 -0
  102. package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmt +0 -0
  103. package/lib/ocaml/Seed_Prompt.ast +0 -0
  104. package/lib/ocaml/Seed_Prompt.res +48 -23
  105. package/lib/ocaml/Seed_PromptTest-ReventlessSeed.cmi +0 -0
  106. package/lib/ocaml/Seed_PromptTest-ReventlessSeed.cmt +0 -0
  107. package/lib/ocaml/Seed_PromptTest.ast +0 -0
  108. package/lib/ocaml/Seed_PromptTest.res +8 -12
  109. package/lib/ocaml/Seed_Random-ReventlessSeed.cmi +0 -0
  110. package/lib/ocaml/Seed_Random-ReventlessSeed.cmt +0 -0
  111. package/lib/ocaml/Seed_Random.ast +0 -0
  112. package/lib/ocaml/Seed_Random.res +4 -2
  113. package/lib/ocaml/Seed_RandomTest-ReventlessSeed.cmi +0 -0
  114. package/lib/ocaml/Seed_RandomTest-ReventlessSeed.cmt +0 -0
  115. package/lib/ocaml/Seed_RandomTest.ast +0 -0
  116. package/lib/ocaml/Seed_RandomTest.res +130 -90
  117. package/lib/ocaml/Seed_Runner-ReventlessSeed.cmi +0 -0
  118. package/lib/ocaml/Seed_Runner-ReventlessSeed.cmt +0 -0
  119. package/lib/ocaml/Seed_Runner.ast +0 -0
  120. package/lib/ocaml/Seed_Runner.res +7 -12
  121. package/lib/ocaml/Seed_Upload-ReventlessSeed.cmi +0 -0
  122. package/lib/ocaml/Seed_Upload-ReventlessSeed.cmt +0 -0
  123. package/lib/ocaml/Seed_Upload.ast +0 -0
  124. package/lib/ocaml/Seed_Upload.res +4 -2
  125. package/lib/ocaml/Seed_Users-ReventlessSeed.cmi +0 -0
  126. package/lib/ocaml/Seed_Users-ReventlessSeed.cmj +0 -0
  127. package/lib/ocaml/Seed_Users-ReventlessSeed.cmt +0 -0
  128. package/lib/ocaml/Seed_Users.ast +0 -0
  129. package/lib/ocaml/Seed_Users.res +30 -3
  130. package/lib/ocaml/Seed_UsersTest-ReventlessSeed.cmi +0 -0
  131. package/lib/ocaml/Seed_UsersTest-ReventlessSeed.cmt +0 -0
  132. package/lib/ocaml/Seed_UsersTest.ast +0 -0
  133. package/lib/ocaml/Seed_UsersTest.res +77 -2
  134. package/package.json +2 -2
  135. package/src/Seed.res +8 -2
  136. package/src/Seed_Client.res +169 -21
  137. package/src/Seed_Client.res.mjs +142 -7
  138. package/src/Seed_Connect.res +258 -19
  139. package/src/Seed_Connect.res.mjs +158 -8
  140. package/src/Seed_Prompt.res +48 -23
  141. package/src/Seed_Prompt.res.mjs +33 -22
  142. package/src/Seed_Random.res +4 -2
  143. package/src/Seed_Runner.res +7 -12
  144. package/src/Seed_Upload.res +4 -2
  145. package/src/Seed_Users.res +30 -3
  146. package/src/Seed_Users.res.mjs +14 -1
  147. package/tests/Seed_ClientTest.res +146 -16
  148. package/tests/Seed_ClientTest.res.mjs +102 -1
  149. package/tests/Seed_ConnectTest.res +64 -0
  150. package/tests/Seed_ConnectTest.res.mjs +56 -0
  151. package/tests/Seed_PromptTest.res +8 -12
  152. package/tests/Seed_RandomTest.res +130 -90
  153. package/tests/Seed_UsersTest.res +77 -2
  154. package/tests/Seed_UsersTest.res.mjs +45 -2
@@ -63,12 +63,16 @@ let envValue = (key: string): option<string> =>
63
63
  | _ => None
64
64
  }
65
65
 
66
+ /** Whether an interactive prompt is possible at all. Callers that can carry on
67
+ without an answer ask this first; a prompt nobody can answer is worse than a
68
+ default, because it turns a workable run into a hang or a throw. */
69
+ let hasTty = (): bool => NodeProcess.stdin->NodeProcess.isTTY->Option.getOr(false)
70
+
66
71
  let requireTty = (): unit =>
67
- if !(NodeProcess.stdin->NodeProcess.isTTY->Option.getOr(false)) {
72
+ if !hasTty() {
68
73
  throw(
69
74
  Failed(
70
- "no TTY for an interactive prompt — set the documented SEED_* / REVENTLESS_DEMO_* " ++
71
- "env vars to run non-interactively.",
75
+ "no TTY for an interactive prompt — set the documented SEED_* / REVENTLESS_DEMO_* " ++ "env vars to run non-interactively.",
72
76
  ),
73
77
  )
74
78
  }
@@ -125,12 +129,7 @@ let isAffirmative = (answer: string): bool =>
125
129
  * `defaultIndex` makes Enter a valid answer, selecting that 0-based option;
126
130
  * without one every reply must name a choice.
127
131
  */
128
- let select = async (
129
- ~title: string,
130
- ~options: array<(string, 'a)>,
131
- ~env=?,
132
- ~defaultIndex=?,
133
- ): 'a =>
132
+ let select = async (~title: string, ~options: array<(string, 'a)>, ~env=?, ~defaultIndex=?): 'a =>
134
133
  switch options {
135
134
  | [(_, only)] => only
136
135
  | _ =>
@@ -163,9 +162,7 @@ let select = async (
163
162
  | _ => ""
164
163
  }
165
164
  let rec pick = async (): 'a => {
166
- let answer = await ask(
167
- `\nSelect [1-${(options->Array.length)->Int.toString}]${hint}: `,
168
- )
165
+ let answer = await ask(`\nSelect [1-${options->Array.length->Int.toString}]${hint}: `)
169
166
  switch (answer, fallback) {
170
167
  | ("", Some((_, value))) => value
171
168
  | _ =>
@@ -183,11 +180,31 @@ let select = async (
183
180
  }
184
181
  }
185
182
 
183
+ /**
184
+ * The account a run authenticated as, alongside every account the platform's
185
+ * accounts file declares.
186
+ *
187
+ * `accounts` is `[]` whenever no file was read — the
188
+ * `REVENTLESS_DEMO_USER`/`REVENTLESS_DEMO_PASSWORD` path bypasses it entirely,
189
+ * and so does typing both halves — so a consumer must handle empty rather than
190
+ * assume the file was there. `caller` is always populated, but carries no
191
+ * `groups` and no `userId` on those paths: nothing read them off a file.
192
+ */
193
+ type resolved = {caller: Seed_Users.user, accounts: array<Seed_Users.user>}
194
+
195
+ let typedUser = (~username: string, ~password: string): Seed_Users.user => {
196
+ username,
197
+ password,
198
+ groups: [],
199
+ userId: None,
200
+ demoOwner: None,
201
+ }
202
+
186
203
  // Typing both halves is the fallback, not the first offer: it is what happens
187
204
  // when the platform keeps no accounts file. `envUser` is threaded in so a
188
205
  // `REVENTLESS_DEMO_USER` that named nobody in the file still skips the username
189
206
  // prompt rather than asking for a name that was already given.
190
- let askCredentials = async (~localDefaults: bool, ~envUser: option<string>): (string, string) => {
207
+ let askCredentials = async (~localDefaults: bool, ~envUser: option<string>): Seed_Users.user => {
191
208
  let username = switch envUser {
192
209
  | Some(u) => u
193
210
  | None =>
@@ -202,7 +219,7 @@ let askCredentials = async (~localDefaults: bool, ~envUser: option<string>): (st
202
219
  let entered = await askHidden(localDefaults ? "Password [admin]: " : "Password: ")
203
220
  entered == "" && localDefaults ? "admin" : entered
204
221
  }
205
- (username, password)
222
+ typedUser(~username, ~password)
206
223
  }
207
224
 
208
225
  // The accounts file the platform already keeps (`.reventless/users.yaml`, or
@@ -212,7 +229,12 @@ let askCredentials = async (~localDefaults: bool, ~envUser: option<string>): (st
212
229
  // A named `REVENTLESS_DEMO_USER` picks its entry directly; otherwise the file's
213
230
  // accounts are offered in the order it defines them, first as the default.
214
231
  // `None` means the file has nothing to offer and the caller should ask.
215
- let fromUsersFile = async (~envUser: option<string>): option<(string, string)> =>
232
+ //
233
+ // The whole list travels back with the chosen account, not just the credentials
234
+ // it supplied: the file is also the only place a demo owner's id can be looked
235
+ // up, and reading it once to pick a login and never again is what let a data set
236
+ // seed rows under ids nobody on the platform holds.
237
+ let fromUsersFile = async (~envUser: option<string>): option<resolved> =>
216
238
  switch Seed_Users.load(~path=?envValue("SEED_USERS_FILE")) {
217
239
  | None => None
218
240
  | Some((path, users)) =>
@@ -233,7 +255,7 @@ let fromUsersFile = async (~envUser: option<string>): option<(string, string)> =
233
255
  // operator checks when owner-scoped rows turn up under the wrong account.
234
256
  chosen->Option.map(u => {
235
257
  Console.log(`Logging in as ${u.username} (from ${path})`)
236
- (u.username, u.password)
258
+ {caller: u, accounts: users}
237
259
  })
238
260
  }
239
261
 
@@ -245,19 +267,22 @@ let fromUsersFile = async (~envUser: option<string>): option<(string, string)> =
245
267
  * `.reventless/users.yaml` supplies the accounts to choose from, and only a
246
268
  * platform without one falls back to typing both halves — where, with
247
269
  * `localDefaults`, empty input means `admin`/`admin`.
270
+ *
271
+ * Returns the resolved account rather than a pair, and the accounts file
272
+ * alongside it when there was one — see `resolved`.
248
273
  */
249
- let credentials = async (~localDefaults: bool=false): (string, string) => {
274
+ let credentials = async (~localDefaults: bool=false): resolved => {
250
275
  let envUser = envValue("REVENTLESS_DEMO_USER")
251
- let (username, password) = switch (envUser, envValue("REVENTLESS_DEMO_PASSWORD")) {
252
- | (Some(u), Some(p)) => (u, p)
276
+ let resolved = switch (envUser, envValue("REVENTLESS_DEMO_PASSWORD")) {
277
+ | (Some(username), Some(password)) => {caller: typedUser(~username, ~password), accounts: []}
253
278
  | _ =>
254
279
  switch await fromUsersFile(~envUser) {
255
- | Some(pair) => pair
256
- | None => await askCredentials(~localDefaults, ~envUser)
280
+ | Some(resolved) => resolved
281
+ | None => {caller: await askCredentials(~localDefaults, ~envUser), accounts: []}
257
282
  }
258
283
  }
259
- if username == "" || password == "" {
284
+ if resolved.caller.username == "" || resolved.caller.password == "" {
260
285
  throw(Failed("username and password are required."))
261
286
  }
262
- (username, password)
287
+ resolved
263
288
  }
Binary file
@@ -15,22 +15,18 @@ open JestGlobals
15
15
 
16
16
  describe("Seed_Prompt.isAffirmative:", () => {
17
17
  testSync("takes the three spellings of yes", () => {
18
- ["y", "yes", "1"]->Array.forEach(v =>
19
- expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(true)
20
- )
18
+ ["y", "yes", "1"]->Array.forEach(v => expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(true))
21
19
  })
22
20
 
23
21
  // A prompt that prints `[y/N]` invites a capital as readily as a lowercase
24
22
  // one, and an env var set by hand is written however the hand felt.
25
23
  testSync("ignores case, so Y and YES answer as y does", () => {
26
- ["Y", "Yes", "YES"]->Array.forEach(v =>
27
- expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(true)
28
- )
24
+ ["Y", "Yes", "YES"]->Array.forEach(v => expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(true))
29
25
  })
30
26
 
31
27
  testSync("ignores surrounding space", () => {
32
- [" y", "y ", " yes "]->Array.forEach(v =>
33
- expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(true)
28
+ [" y", "y ", " yes "]->Array.forEach(
29
+ v => expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(true),
34
30
  )
35
31
  })
36
32
 
@@ -38,14 +34,14 @@ describe("Seed_Prompt.isAffirmative:", () => {
38
34
  // reads it as "no". None may be mistaken for "nothing was said", which is what
39
35
  // sent the old code to a prompt it could not open.
40
36
  testSync("declines every spelling of no, capital N included", () => {
41
- ["n", "N", "no", "No", "NO"]->Array.forEach(v =>
42
- expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(false)
37
+ ["n", "N", "no", "No", "NO"]->Array.forEach(
38
+ v => expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(false),
43
39
  )
44
40
  })
45
41
 
46
42
  testSync("declines anything it does not recognise", () => {
47
- ["", " ", "0", "true", "yep", "y.", "yesplease", "affirmative"]->Array.forEach(v =>
48
- expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(false)
43
+ ["", " ", "0", "true", "yep", "y.", "yesplease", "affirmative"]->Array.forEach(
44
+ v => expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(false),
49
45
  )
50
46
  })
51
47
 
Binary file
@@ -10,13 +10,15 @@ type t = {mutable state: int}
10
10
 
11
11
  let make = (~seed: int): t => {state: seed}
12
12
 
13
-
14
13
  /** mulberry32 — small, fast, and stable across JS engines and Node versions. */
15
14
  let float = (r: t): float => {
16
15
  r.state = (r.state + 0x6d2b79f5)->Int.bitwiseOr(0)
17
16
  let a = r.state
18
17
  let b = imul(a->Int.bitwiseXor(a->Int.shiftRightUnsigned(15)), a->Int.bitwiseOr(1))
19
- let c = b->Int.bitwiseXor(b + imul(b->Int.bitwiseXor(b->Int.shiftRightUnsigned(7)), b->Int.bitwiseOr(61)))
18
+ let c =
19
+ b->Int.bitwiseXor(
20
+ b + imul(b->Int.bitwiseXor(b->Int.shiftRightUnsigned(7)), b->Int.bitwiseOr(61)),
21
+ )
20
22
  let bits = c->Int.bitwiseXor(c->Int.shiftRightUnsigned(14))
21
23
  // `bits` is a signed 32-bit result; fold the sign bit back in to get the
22
24
  // unsigned value a `>>> 0` would produce.
Binary file
@@ -16,36 +16,45 @@ describe("Seed_Random:", () => {
16
16
  }
17
17
 
18
18
  describe("reproducibility — the reason this module exists:", () => {
19
- testSync("the same seed replays the same sequence", () =>
20
- expect(take(~seed=42, ~n=25))->toEqual(take(~seed=42, ~n=25))
19
+ testSync(
20
+ "the same seed replays the same sequence",
21
+ () => expect(take(~seed=42, ~n=25))->toEqual(take(~seed=42, ~n=25)),
21
22
  )
22
23
 
23
24
  // Without this, a generator that returned a constant would satisfy the
24
25
  // case above and nothing else in the suite would catch it.
25
- testSync("different seeds diverge", () =>
26
- expect(take(~seed=42, ~n=25) == take(~seed=43, ~n=25))->toBe(false)
26
+ testSync(
27
+ "different seeds diverge",
28
+ () => expect(take(~seed=42, ~n=25) == take(~seed=43, ~n=25))->toBe(false),
27
29
  )
28
30
 
29
- testSync("a fresh generator does not continue the previous one", () => {
30
- let r = Seed_Random.make(~seed=7)
31
- let first = Seed_Random.float(r)
32
- let _ = Seed_Random.float(r)
33
- expect(Seed_Random.float(Seed_Random.make(~seed=7)))->toEqual(first)
34
- })
31
+ testSync(
32
+ "a fresh generator does not continue the previous one",
33
+ () => {
34
+ let r = Seed_Random.make(~seed=7)
35
+ let first = Seed_Random.float(r)
36
+ let _ = Seed_Random.float(r)
37
+ expect(Seed_Random.float(Seed_Random.make(~seed=7)))->toEqual(first)
38
+ },
39
+ )
35
40
  })
36
41
 
37
42
  describe("float stays a unit interval:", () => {
38
43
  // mulberry32's last step is a signed 32-bit value folded back to unsigned.
39
44
  // Get that fold wrong and roughly half the draws come out negative, which
40
45
  // every consumer below (int, pick, sampleWeighted) silently mis-scales.
41
- testSync("every draw is in [0, 1)", () =>
42
- expect(take(~seed=99, ~n=500)->Array.every(f => f >= 0.0 && f < 1.0))->toBe(true)
46
+ testSync(
47
+ "every draw is in [0, 1)",
48
+ () => expect(take(~seed=99, ~n=500)->Array.every(f => f >= 0.0 && f < 1.0))->toBe(true),
43
49
  )
44
50
 
45
- testSync("and the draws are not all the same value", () => {
46
- let draws = take(~seed=99, ~n=100)
47
- expect(draws->Array.some(f => f != draws->Array.getUnsafe(0)))->toBe(true)
48
- })
51
+ testSync(
52
+ "and the draws are not all the same value",
53
+ () => {
54
+ let draws = take(~seed=99, ~n=100)
55
+ expect(draws->Array.some(f => f != draws->Array.getUnsafe(0)))->toBe(true)
56
+ },
57
+ )
49
58
  })
50
59
 
51
60
  describe("int is inclusive on both ends, as documented:", () => {
@@ -54,40 +63,50 @@ describe("Seed_Random:", () => {
54
63
  Array.make(~length=n, 0)->Array.map(_ => Seed_Random.int(r, ~min, ~max))
55
64
  }
56
65
 
57
- testSync("never escapes the range", () =>
58
- expect(draws(~min=3, ~max=7, ~n=500)->Array.every(i => i >= 3 && i <= 7))->toBe(true)
66
+ testSync(
67
+ "never escapes the range",
68
+ () => expect(draws(~min=3, ~max=7, ~n=500)->Array.every(i => i >= 3 && i <= 7))->toBe(true),
59
69
  )
60
70
 
61
71
  // The documented contract is "inclusive on both ends"; an off-by-one in the
62
72
  // `max - min + 1` scaling shows up as a bound that is never drawn.
63
- testSync("reaches both bounds", () => {
64
- let d = draws(~min=3, ~max=7, ~n=500)
65
- expect((d->Array.includes(3), d->Array.includes(7)))->toEqual((true, true))
66
- })
73
+ testSync(
74
+ "reaches both bounds",
75
+ () => {
76
+ let d = draws(~min=3, ~max=7, ~n=500)
77
+ expect((d->Array.includes(3), d->Array.includes(7)))->toEqual((true, true))
78
+ },
79
+ )
67
80
 
68
- testSync("a single-value range is that value", () =>
69
- expect(draws(~min=4, ~max=4, ~n=10)->Array.every(i => i == 4))->toBe(true)
81
+ testSync(
82
+ "a single-value range is that value",
83
+ () => expect(draws(~min=4, ~max=4, ~n=10)->Array.every(i => i == 4))->toBe(true),
70
84
  )
71
85
  })
72
86
 
73
87
  describe("pick:", () => {
74
- testSync("returns None for an empty array", () =>
75
- expect(Seed_Random.make(~seed=1)->Seed_Random.pick([]))->toEqual(None)
88
+ testSync(
89
+ "returns None for an empty array",
90
+ () => expect(Seed_Random.make(~seed=1)->Seed_Random.pick([]))->toEqual(None),
76
91
  )
77
92
 
78
- testSync("always lands on a member", () => {
79
- let r = Seed_Random.make(~seed=2)
80
- let xs = ["a", "b", "c", "d"]
81
- let hits = Array.make(~length=200, "")->Array.map(_ =>
82
- r->Seed_Random.pickOr(~fallback="MISS", xs)
83
- )
84
- expect(hits->Array.every(h => xs->Array.includes(h)))->toBe(true)
85
- })
93
+ testSync(
94
+ "always lands on a member",
95
+ () => {
96
+ let r = Seed_Random.make(~seed=2)
97
+ let xs = ["a", "b", "c", "d"]
98
+ let hits =
99
+ Array.make(~length=200, "")->Array.map(_ => r->Seed_Random.pickOr(~fallback="MISS", xs))
100
+ expect(hits->Array.every(h => xs->Array.includes(h)))->toBe(true)
101
+ },
102
+ )
86
103
 
87
- testSync("pickOr falls back only when there is nothing to pick", () =>
88
- expect(Seed_Random.make(~seed=3)->Seed_Random.pickOr(~fallback="fallback", []))->toBe(
89
- "fallback",
90
- )
104
+ testSync(
105
+ "pickOr falls back only when there is nothing to pick",
106
+ () =>
107
+ expect(Seed_Random.make(~seed=3)->Seed_Random.pickOr(~fallback="fallback", []))->toBe(
108
+ "fallback",
109
+ ),
91
110
  )
92
111
  })
93
112
 
@@ -96,65 +115,86 @@ describe("Seed_Random:", () => {
96
115
 
97
116
  // The documented shuffle use: equal weights, count = length. If sampling
98
117
  // ever replaced instead of removing, duplicates would appear here.
99
- testSync("count = length yields a permutation, not a multiset", () => {
100
- let xs = ["a", "b", "c", "d", "e"]
101
- let out = Seed_Random.make(~seed=11)->Seed_Random.sampleWeighted(equal(xs), ~count=5)
102
- let sorted = out->Array.toSorted(String.compare)
103
- expect((out->Array.length, sorted))->toEqual((5, ["a", "b", "c", "d", "e"]))
104
- })
105
-
106
- testSync("count < length returns exactly count distinct items", () => {
107
- let xs = ["a", "b", "c", "d", "e"]
108
- let out = Seed_Random.make(~seed=12)->Seed_Random.sampleWeighted(equal(xs), ~count=3)
109
- let distinct = Set.fromArray(out)->Set.size
110
- expect((out->Array.length, distinct))->toEqual((3, 3))
111
- })
112
-
113
- testSync("count > length clamps to the pool", () =>
114
- expect(
115
- Seed_Random.make(~seed=13)
116
- ->Seed_Random.sampleWeighted(equal(["a", "b"]), ~count=99)
117
- ->Array.length,
118
- )->toBe(2)
119
- )
120
-
121
- testSync("a zero-weight entry is never chosen while others remain", () => {
122
- let out =
123
- Seed_Random.make(~seed=14)->Seed_Random.sampleWeighted(
124
- [("never", 0.0), ("a", 1.0), ("b", 1.0)],
125
- ~count=2,
126
- )
127
- expect(out->Array.includes("never"))->toBe(false)
128
- })
129
-
130
- testSync("is reproducible for a given seed", () => {
131
- let run = () =>
132
- Seed_Random.make(~seed=15)->Seed_Random.sampleWeighted(
133
- equal(["a", "b", "c", "d", "e", "f"]),
134
- ~count=4,
135
- )
136
- expect(run())->toEqual(run())
137
- })
118
+ testSync(
119
+ "count = length yields a permutation, not a multiset",
120
+ () => {
121
+ let xs = ["a", "b", "c", "d", "e"]
122
+ let out = Seed_Random.make(~seed=11)->Seed_Random.sampleWeighted(equal(xs), ~count=5)
123
+ let sorted = out->Array.toSorted(String.compare)
124
+ expect((out->Array.length, sorted))->toEqual((5, ["a", "b", "c", "d", "e"]))
125
+ },
126
+ )
127
+
128
+ testSync(
129
+ "count < length returns exactly count distinct items",
130
+ () => {
131
+ let xs = ["a", "b", "c", "d", "e"]
132
+ let out = Seed_Random.make(~seed=12)->Seed_Random.sampleWeighted(equal(xs), ~count=3)
133
+ let distinct = Set.fromArray(out)->Set.size
134
+ expect((out->Array.length, distinct))->toEqual((3, 3))
135
+ },
136
+ )
137
+
138
+ testSync(
139
+ "count > length clamps to the pool",
140
+ () =>
141
+ expect(
142
+ Seed_Random.make(~seed=13)
143
+ ->Seed_Random.sampleWeighted(equal(["a", "b"]), ~count=99)
144
+ ->Array.length,
145
+ )->toBe(2),
146
+ )
147
+
148
+ testSync(
149
+ "a zero-weight entry is never chosen while others remain",
150
+ () => {
151
+ let out =
152
+ Seed_Random.make(~seed=14)->Seed_Random.sampleWeighted(
153
+ [("never", 0.0), ("a", 1.0), ("b", 1.0)],
154
+ ~count=2,
155
+ )
156
+ expect(out->Array.includes("never"))->toBe(false)
157
+ },
158
+ )
159
+
160
+ testSync(
161
+ "is reproducible for a given seed",
162
+ () => {
163
+ let run = () =>
164
+ Seed_Random.make(~seed=15)->Seed_Random.sampleWeighted(
165
+ equal(["a", "b", "c", "d", "e", "f"]),
166
+ ~count=4,
167
+ )
168
+ expect(run())->toEqual(run())
169
+ },
170
+ )
138
171
  })
139
172
 
140
173
  describe("zipfWeights skews toward the head:", () => {
141
174
  let weights = Seed_Random.zipfWeights(["a", "b", "c", "d", "e"], ~exponent=1.1)
142
175
 
143
- testSync("keeps every item, in order", () =>
144
- expect(weights->Array.map(((item, _)) => item))->toEqual(["a", "b", "c", "d", "e"])
176
+ testSync(
177
+ "keeps every item, in order",
178
+ () => expect(weights->Array.map(((item, _)) => item))->toEqual(["a", "b", "c", "d", "e"]),
145
179
  )
146
180
 
147
- testSync("weight decreases monotonically with rank", () => {
148
- let ws = weights->Array.map(((_, w)) => w)
149
- let decreasing =
150
- ws->Array.everyWithIndex((w, i) => i == 0 || w < ws->Array.getUnsafe(i - 1))
151
- expect(decreasing)->toBe(true)
152
- })
181
+ testSync(
182
+ "weight decreases monotonically with rank",
183
+ () => {
184
+ let ws = weights->Array.map(((_, w)) => w)
185
+ let decreasing =
186
+ ws->Array.everyWithIndex((w, i) => i == 0 || w < ws->Array.getUnsafe(i - 1))
187
+ expect(decreasing)->toBe(true)
188
+ },
189
+ )
153
190
 
154
- testSync("the leader is clear — first outweighs last several times over", () => {
155
- let first = weights->Array.get(0)->Option.mapOr(0.0, ((_, w)) => w)
156
- let last = weights->Array.get(4)->Option.mapOr(0.0, ((_, w)) => w)
157
- expect(first > last *. 3.0)->toBe(true)
158
- })
191
+ testSync(
192
+ "the leader is clear — first outweighs last several times over",
193
+ () => {
194
+ let first = weights->Array.get(0)->Option.mapOr(0.0, ((_, w)) => w)
195
+ let last = weights->Array.get(4)->Option.mapOr(0.0, ((_, w)) => w)
196
+ expect(first > last *. 3.0)->toBe(true)
197
+ },
198
+ )
159
199
  })
160
200
  })
Binary file
@@ -2,7 +2,6 @@
2
2
 
3
3
  open Seed_Types
4
4
 
5
-
6
5
  let envOr = (key: string, fallback: string): string =>
7
6
  NodeProcess.env->Dict.get(key)->Option.getOr(fallback)
8
7
 
@@ -58,14 +57,12 @@ let verifyViews = async (client: Seed_Client.t, ~views: array<view>): dict<int>
58
57
  Console.log(` ${count->Int.toString->String.padStart(4, " ")} ${name}${note}`)
59
58
  })
60
59
 
61
- let empty =
62
- views
63
- ->Array.filterMap(v =>
64
- switch v {
65
- | Seeded(name) if counts->Dict.get(name)->Option.getOr(0) == 0 => Some(name)
66
- | _ => None
67
- }
68
- )
60
+ let empty = views->Array.filterMap(v =>
61
+ switch v {
62
+ | Seeded(name) if counts->Dict.get(name)->Option.getOr(0) == 0 => Some(name)
63
+ | _ => None
64
+ }
65
+ )
69
66
  if empty->Array.length > 0 {
70
67
  throw(Failed(`these views are still empty after seeding: ${empty->Array.join(", ")}`))
71
68
  }
@@ -138,9 +135,7 @@ let run = async (main: unit => promise<unit>): unit =>
138
135
  Console.error("")
139
136
  Console.error("Seeding aborted with an unexpected error:")
140
137
  Console.error("")
141
- Console.error(
142
- exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("unknown"),
143
- )
138
+ Console.error(exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("unknown"))
144
139
  NodeProcess.exit(1)
145
140
  }
146
141
 
Binary file
@@ -64,7 +64,9 @@ let uploadAsset = async (
64
64
  // GraphQL string literals: JSON-quote the argument values (JSON string escaping is a
65
65
  // superset-safe subset of GraphQL's for these ASCII-ish inputs).
66
66
  let q = s => JSON.stringify(JSON.Encode.string(s))
67
- let query = `mutation { r: Upload_Presign(store: ${q(store)}, fileName: ${q(fileName)}, contentType: ${q(contentType)}) { uploadUrl storageRef } }`
67
+ let query = `mutation { r: Upload_Presign(store: ${q(store)}, fileName: ${q(
68
+ fileName,
69
+ )}, contentType: ${q(contentType)}) { uploadUrl storageRef } }`
68
70
 
69
71
  let presign = try Ok(await Seed_Client.gql(client, ~query, ~label="Upload_Presign")) catch {
70
72
  | Seed_Types.Failed(m) => Error(m)
@@ -89,7 +91,7 @@ let uploadAsset = async (
89
91
  | Ok(putRes) if putRes->responseOk => Ok(storageRef)
90
92
  | Ok(putRes) =>
91
93
  let detail = await putRes->responseText
92
- Error(`upload PUT failed with HTTP ${(putRes->responseStatus)->Int.toString}: ${detail}`)
94
+ Error(`upload PUT failed with HTTP ${putRes->responseStatus->Int.toString}: ${detail}`)
93
95
  }
94
96
  | _ => Error("Upload_Presign response missing uploadUrl/storageRef")
95
97
  }
Binary file
@@ -11,8 +11,21 @@
11
11
 
12
12
  @module("yaml") external parseYaml: string => JSON.t = "parse"
13
13
 
14
- /** One account: what to log in as, and what to send with it. */
15
- type user = {username: string, password: string, groups: array<string>}
14
+ /**
15
+ * One account: what to log in as, what to send with it, and — when the file
16
+ * records one — the id the platform stamps on rows this account writes.
17
+ *
18
+ * `userId` is optional because absence is normal on both platforms: a local file
19
+ * may omit it and let the auth adapter default it to the username, and a
20
+ * hand-maintained AWS file records subs the pool minted, so it can lag.
21
+ */
22
+ type user = {
23
+ username: string,
24
+ password: string,
25
+ groups: array<string>,
26
+ userId: option<string>,
27
+ demoOwner: option<string>,
28
+ }
16
29
 
17
30
  let asString = (json: JSON.t): option<string> =>
18
31
  switch json {
@@ -32,7 +45,13 @@ let userOf = (json: JSON.t): option<user> =>
32
45
  | Some(JSON.Array(items)) => items->Array.filterMap(asString)
33
46
  | _ => []
34
47
  }
35
- Some({username, password, groups})
48
+ Some({
49
+ username,
50
+ password,
51
+ groups,
52
+ userId: obj->Dict.get("userId")->Option.flatMap(asString),
53
+ demoOwner: obj->Dict.get("demoOwner")->Option.flatMap(asString),
54
+ })
36
55
  | _ => None
37
56
  }
38
57
  | _ => None
@@ -75,6 +94,14 @@ let load = (~path: option<string>=?): option<(string, array<user>)> => {
75
94
  users->Array.length == 0 ? None : Some((file, users))
76
95
  }
77
96
 
97
+ /** The account that plays `name` in a demo: the one whose `demoOwner` says so,
98
+ else the one named `name` — which is how a file without the field reads. */
99
+ let playing = (users: array<user>, name: string): option<user> =>
100
+ switch users->Array.find(u => u.demoOwner == Some(name)) {
101
+ | Some(_) as declared => declared
102
+ | None => users->Array.find(u => u.username == name)
103
+ }
104
+
78
105
  /** Menu label: the username, plus whatever groups the file records for it. */
79
106
  let label = (u: user): string =>
80
107
  u.groups->Array.length == 0 ? u.username : `${u.username} [${u.groups->Array.join(", ")}]`