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

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 (152) hide show
  1. package/CHANGELOG.md +12 -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 +47 -23
  28. package/lib/bs/src/Seed_Prompt.res.mjs +32 -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.cmt +0 -0
  43. package/lib/bs/src/Seed_Users.ast +0 -0
  44. package/lib/bs/src/Seed_Users.res +15 -3
  45. package/lib/bs/src/Seed_Users.res.mjs +2 -1
  46. package/lib/bs/tests/Seed_ClientTest-ReventlessSeed.cmi +0 -0
  47. package/lib/bs/tests/Seed_ClientTest-ReventlessSeed.cmj +0 -0
  48. package/lib/bs/tests/Seed_ClientTest-ReventlessSeed.cmt +0 -0
  49. package/lib/bs/tests/Seed_ClientTest.ast +0 -0
  50. package/lib/bs/tests/Seed_ClientTest.res +146 -16
  51. package/lib/bs/tests/Seed_ClientTest.res.mjs +102 -1
  52. package/lib/bs/tests/Seed_ConnectTest-ReventlessSeed.cmi +0 -0
  53. package/lib/bs/tests/Seed_ConnectTest-ReventlessSeed.cmj +0 -0
  54. package/lib/bs/tests/Seed_ConnectTest-ReventlessSeed.cmt +0 -0
  55. package/lib/bs/tests/Seed_ConnectTest.ast +0 -0
  56. package/lib/bs/tests/Seed_ConnectTest.res +64 -0
  57. package/lib/bs/tests/Seed_ConnectTest.res.mjs +56 -0
  58. package/lib/bs/tests/Seed_PromptTest-ReventlessSeed.cmi +0 -0
  59. package/lib/bs/tests/Seed_PromptTest-ReventlessSeed.cmt +0 -0
  60. package/lib/bs/tests/Seed_PromptTest.ast +0 -0
  61. package/lib/bs/tests/Seed_PromptTest.res +8 -12
  62. package/lib/bs/tests/Seed_RandomTest-ReventlessSeed.cmi +0 -0
  63. package/lib/bs/tests/Seed_RandomTest-ReventlessSeed.cmt +0 -0
  64. package/lib/bs/tests/Seed_RandomTest.ast +0 -0
  65. package/lib/bs/tests/Seed_RandomTest.res +130 -90
  66. package/lib/bs/tests/Seed_UsersTest-ReventlessSeed.cmi +0 -0
  67. package/lib/bs/tests/Seed_UsersTest-ReventlessSeed.cmt +0 -0
  68. package/lib/bs/tests/Seed_UsersTest.ast +0 -0
  69. package/lib/bs/tests/Seed_UsersTest.res +39 -2
  70. package/lib/bs/tests/Seed_UsersTest.res.mjs +20 -2
  71. package/lib/ocaml/.compiler.log +2 -2
  72. package/lib/ocaml/ReventlessSeed.cmi +0 -0
  73. package/lib/ocaml/ReventlessSeed.cmt +0 -0
  74. package/lib/ocaml/ReventlessSeed.mlmap +1 -0
  75. package/lib/ocaml/Seed-ReventlessSeed.cmi +0 -0
  76. package/lib/ocaml/Seed-ReventlessSeed.cmt +0 -0
  77. package/lib/ocaml/Seed.ast +0 -0
  78. package/lib/ocaml/Seed.res +8 -2
  79. package/lib/ocaml/Seed_Client-ReventlessSeed.cmi +0 -0
  80. package/lib/ocaml/Seed_Client-ReventlessSeed.cmj +0 -0
  81. package/lib/ocaml/Seed_Client-ReventlessSeed.cmt +0 -0
  82. package/lib/ocaml/Seed_Client.ast +0 -0
  83. package/lib/ocaml/Seed_Client.res +169 -21
  84. package/lib/ocaml/Seed_ClientTest-ReventlessSeed.cmi +0 -0
  85. package/lib/ocaml/Seed_ClientTest-ReventlessSeed.cmj +0 -0
  86. package/lib/ocaml/Seed_ClientTest-ReventlessSeed.cmt +0 -0
  87. package/lib/ocaml/Seed_ClientTest.ast +0 -0
  88. package/lib/ocaml/Seed_ClientTest.res +146 -16
  89. package/lib/ocaml/Seed_Connect-ReventlessSeed.cmi +0 -0
  90. package/lib/ocaml/Seed_Connect-ReventlessSeed.cmj +0 -0
  91. package/lib/ocaml/Seed_Connect-ReventlessSeed.cmt +0 -0
  92. package/lib/ocaml/Seed_Connect.ast +0 -0
  93. package/lib/ocaml/Seed_Connect.res +258 -19
  94. package/lib/ocaml/Seed_ConnectTest-ReventlessSeed.cmi +0 -0
  95. package/lib/ocaml/Seed_ConnectTest-ReventlessSeed.cmj +0 -0
  96. package/lib/ocaml/Seed_ConnectTest-ReventlessSeed.cmt +0 -0
  97. package/lib/ocaml/Seed_ConnectTest.ast +0 -0
  98. package/lib/ocaml/Seed_ConnectTest.res +64 -0
  99. package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmi +0 -0
  100. package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmj +0 -0
  101. package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmt +0 -0
  102. package/lib/ocaml/Seed_Prompt.ast +0 -0
  103. package/lib/ocaml/Seed_Prompt.res +47 -23
  104. package/lib/ocaml/Seed_PromptTest-ReventlessSeed.cmi +0 -0
  105. package/lib/ocaml/Seed_PromptTest-ReventlessSeed.cmt +0 -0
  106. package/lib/ocaml/Seed_PromptTest.ast +0 -0
  107. package/lib/ocaml/Seed_PromptTest.res +8 -12
  108. package/lib/ocaml/Seed_Random-ReventlessSeed.cmi +0 -0
  109. package/lib/ocaml/Seed_Random-ReventlessSeed.cmt +0 -0
  110. package/lib/ocaml/Seed_Random.ast +0 -0
  111. package/lib/ocaml/Seed_Random.res +4 -2
  112. package/lib/ocaml/Seed_RandomTest-ReventlessSeed.cmi +0 -0
  113. package/lib/ocaml/Seed_RandomTest-ReventlessSeed.cmt +0 -0
  114. package/lib/ocaml/Seed_RandomTest.ast +0 -0
  115. package/lib/ocaml/Seed_RandomTest.res +130 -90
  116. package/lib/ocaml/Seed_Runner-ReventlessSeed.cmi +0 -0
  117. package/lib/ocaml/Seed_Runner-ReventlessSeed.cmt +0 -0
  118. package/lib/ocaml/Seed_Runner.ast +0 -0
  119. package/lib/ocaml/Seed_Runner.res +7 -12
  120. package/lib/ocaml/Seed_Upload-ReventlessSeed.cmi +0 -0
  121. package/lib/ocaml/Seed_Upload-ReventlessSeed.cmt +0 -0
  122. package/lib/ocaml/Seed_Upload.ast +0 -0
  123. package/lib/ocaml/Seed_Upload.res +4 -2
  124. package/lib/ocaml/Seed_Users-ReventlessSeed.cmi +0 -0
  125. package/lib/ocaml/Seed_Users-ReventlessSeed.cmt +0 -0
  126. package/lib/ocaml/Seed_Users.ast +0 -0
  127. package/lib/ocaml/Seed_Users.res +15 -3
  128. package/lib/ocaml/Seed_UsersTest-ReventlessSeed.cmi +0 -0
  129. package/lib/ocaml/Seed_UsersTest-ReventlessSeed.cmt +0 -0
  130. package/lib/ocaml/Seed_UsersTest.ast +0 -0
  131. package/lib/ocaml/Seed_UsersTest.res +39 -2
  132. package/package.json +2 -2
  133. package/src/Seed.res +8 -2
  134. package/src/Seed_Client.res +169 -21
  135. package/src/Seed_Client.res.mjs +142 -7
  136. package/src/Seed_Connect.res +258 -19
  137. package/src/Seed_Connect.res.mjs +158 -8
  138. package/src/Seed_Prompt.res +47 -23
  139. package/src/Seed_Prompt.res.mjs +32 -22
  140. package/src/Seed_Random.res +4 -2
  141. package/src/Seed_Runner.res +7 -12
  142. package/src/Seed_Upload.res +4 -2
  143. package/src/Seed_Users.res +15 -3
  144. package/src/Seed_Users.res.mjs +2 -1
  145. package/tests/Seed_ClientTest.res +146 -16
  146. package/tests/Seed_ClientTest.res.mjs +102 -1
  147. package/tests/Seed_ConnectTest.res +64 -0
  148. package/tests/Seed_ConnectTest.res.mjs +56 -0
  149. package/tests/Seed_PromptTest.res +8 -12
  150. package/tests/Seed_RandomTest.res +130 -90
  151. package/tests/Seed_UsersTest.res +39 -2
  152. package/tests/Seed_UsersTest.res.mjs +20 -2
@@ -0,0 +1,64 @@
1
+ open JestGlobals
2
+
3
+ // `SEED_ROLE` is how a run says what to act as when nobody can answer a prompt —
4
+ // a CI run wedged by a stored narrowing, or a deliberate restricted run.
5
+ //
6
+ // It carries two different intentions in one variable, which is why the parsing
7
+ // is worth pinning: naming a role narrows to it, and the "give me everything"
8
+ // answer has several spellings because an operator setting it by hand will write
9
+ // whichever one comes to mind. Getting that wrong is not a typo — `SEED_ROLE=full`
10
+ // read as a role named "full" would ask the platform to act as a group nobody
11
+ // holds, and be refused.
12
+
13
+ let withEnv = (value: option<string>, f: unit => unit) => {
14
+ let key = "SEED_ROLE"
15
+ let previous = NodeProcess.env->Dict.get(key)
16
+ switch value {
17
+ | Some(v) => NodeProcess.env->Dict.set(key, v)
18
+ | None => NodeProcess.env->Dict.delete(key)
19
+ }
20
+ f()
21
+ switch previous {
22
+ | Some(v) => NodeProcess.env->Dict.set(key, v)
23
+ | None => NodeProcess.env->Dict.delete(key)
24
+ }
25
+ }
26
+
27
+ describe("Seed_Connect.roleFromEnv:", () => {
28
+ testSync("is absent when nothing asked for a role", () =>
29
+ withEnv(None, () => expect(Seed_Connect.roleFromEnv())->Expect.toEqual(None))
30
+ )
31
+
32
+ testSync("reads a named role as a narrowing to it", () =>
33
+ withEnv(
34
+ Some("Shopper"),
35
+ () =>
36
+ expect(Seed_Connect.roleFromEnv())->Expect.toEqual(Some(Seed_Connect.Narrowed("Shopper"))),
37
+ )
38
+ )
39
+
40
+ // Every spelling of "everything" an operator might reach for. They all mean
41
+ // clear the stored choice, which is what seeding wants.
42
+ testSync("takes each spelling of full membership as a clear", () =>
43
+ ["full", "all", "none", "clear", "FULL", "All"]->Array.forEach(
44
+ v =>
45
+ withEnv(
46
+ Some(v),
47
+ () => expect(Seed_Connect.roleFromEnv())->Expect.toEqual(Some(Seed_Connect.Full)),
48
+ ),
49
+ )
50
+ )
51
+
52
+ // Role names are case-sensitive to the platform (`mayActAs` compares exactly),
53
+ // so a named role is passed through untouched — only the clear vocabulary is
54
+ // case-folded.
55
+ testSync("passes a role name through with its case intact", () =>
56
+ withEnv(
57
+ Some("Merchandiser"),
58
+ () =>
59
+ expect(Seed_Connect.roleFromEnv())->Expect.toEqual(
60
+ Some(Seed_Connect.Narrowed("Merchandiser")),
61
+ ),
62
+ )
63
+ )
64
+ })
@@ -0,0 +1,56 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
4
+ import * as Seed_Connect$ReventlessSeed from "../src/Seed_Connect.res.mjs";
5
+
6
+ function withEnv(value, f) {
7
+ let key = "SEED_ROLE";
8
+ let previous = process.env[key];
9
+ if (value !== undefined) {
10
+ process.env[key] = value;
11
+ } else {
12
+ Stdlib_Dict.$$delete(process.env, key);
13
+ }
14
+ f();
15
+ if (previous !== undefined) {
16
+ process.env[key] = previous;
17
+ return;
18
+ } else {
19
+ return Stdlib_Dict.$$delete(process.env, key);
20
+ }
21
+ }
22
+
23
+ globalThis.describe("Seed_Connect.roleFromEnv:", () => {
24
+ globalThis.test("is absent when nothing asked for a role", () => withEnv(undefined, () => {
25
+ globalThis.expect(Seed_Connect$ReventlessSeed.roleFromEnv()).toEqual(undefined);
26
+ }));
27
+ globalThis.test("reads a named role as a narrowing to it", () => withEnv("Shopper", () => {
28
+ globalThis.expect(Seed_Connect$ReventlessSeed.roleFromEnv()).toEqual({
29
+ TAG: "Narrowed",
30
+ _0: "Shopper"
31
+ });
32
+ }));
33
+ globalThis.test("takes each spelling of full membership as a clear", () => {
34
+ [
35
+ "full",
36
+ "all",
37
+ "none",
38
+ "clear",
39
+ "FULL",
40
+ "All"
41
+ ].forEach(v => withEnv(v, () => {
42
+ globalThis.expect(Seed_Connect$ReventlessSeed.roleFromEnv()).toEqual("Full");
43
+ }));
44
+ });
45
+ globalThis.test("passes a role name through with its case intact", () => withEnv("Merchandiser", () => {
46
+ globalThis.expect(Seed_Connect$ReventlessSeed.roleFromEnv()).toEqual({
47
+ TAG: "Narrowed",
48
+ _0: "Merchandiser"
49
+ });
50
+ }));
51
+ });
52
+
53
+ export {
54
+ withEnv,
55
+ }
56
+ /* Not a pure module */
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
@@ -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
@@ -50,6 +50,22 @@ describe("Seed_Users.parseString:", () => {
50
50
  ])
51
51
  })
52
52
 
53
+ // The half that was dropped, and the reason owner-scoped demo rows were keyed
54
+ // to ids nobody on a Cognito deployment holds: the file records the id the
55
+ // platform stamps, and reading it only for a password threw that away.
56
+ //
57
+ // Absent stays absent rather than defaulting to the username — a local file
58
+ // may omit it and let the auth adapter default it, and an AWS file records a
59
+ // sub no default could invent. Guessing here would look like an answer.
60
+ testSync("keeps the userId an entry declares, and reads a missing one as absent", () => {
61
+ let users = Seed_Users.parseString(document)
62
+ expect(users->Array.map(u => u.Seed_Users.userId))->Expect.toEqual([
63
+ Some("local-admin"),
64
+ None,
65
+ None,
66
+ ])
67
+ })
68
+
53
69
  // An entry half-written by hand is the likely flaw in this file. Dropping it
54
70
  // keeps the other accounts selectable; failing the parse would not.
55
71
  testSync("skips an entry that names no password", () => {
@@ -82,11 +98,32 @@ describe("Seed_Users.load:", () => {
82
98
  describe("Seed_Users.label:", () => {
83
99
  testSync("names the groups an account carries", () => {
84
100
  expect(
85
- Seed_Users.label({username: "admin", password: "x", groups: ["Admin", "Shopper"]}),
101
+ Seed_Users.label({
102
+ username: "admin",
103
+ password: "x",
104
+ groups: ["Admin", "Shopper"],
105
+ userId: None,
106
+ }),
86
107
  )->Expect.toBe("admin [Admin, Shopper]")
87
108
  })
88
109
 
89
110
  testSync("is the bare username when the file records no groups", () => {
90
- expect(Seed_Users.label({username: "admin", password: "x", groups: []}))->Expect.toBe("admin")
111
+ expect(
112
+ Seed_Users.label({username: "admin", password: "x", groups: [], userId: None}),
113
+ )->Expect.toBe("admin")
114
+ })
115
+
116
+ // The menu is unchanged by the id: an operator picks an account by name and
117
+ // membership, and a Cognito sub in the list would be noise at the one moment
118
+ // they are choosing a login.
119
+ testSync("says nothing about the userId", () => {
120
+ expect(
121
+ Seed_Users.label({
122
+ username: "admin",
123
+ password: "x",
124
+ groups: ["Admin"],
125
+ userId: Some("4275e4a4-00c1-70e1-1cb5-363b06b992b5"),
126
+ }),
127
+ )->Expect.toBe("admin [Admin]")
91
128
  })
92
129
  })
@@ -46,6 +46,14 @@ globalThis.describe("Seed_Users.parseString:", () => {
46
46
  []
47
47
  ]);
48
48
  });
49
+ globalThis.test("keeps the userId an entry declares, and reads a missing one as absent", () => {
50
+ let users = Seed_Users$ReventlessSeed.parseString(document);
51
+ globalThis.expect(users.map(u => u.userId)).toEqual([
52
+ "local-admin",
53
+ undefined,
54
+ undefined
55
+ ]);
56
+ });
49
57
  globalThis.test("skips an entry that names no password", () => {
50
58
  let users = Seed_Users$ReventlessSeed.parseString(`
51
59
  - username: admin
@@ -77,16 +85,26 @@ globalThis.describe("Seed_Users.label:", () => {
77
85
  groups: [
78
86
  "Admin",
79
87
  "Shopper"
80
- ]
88
+ ],
89
+ userId: undefined
81
90
  })).toBe("admin [Admin, Shopper]");
82
91
  });
83
92
  globalThis.test("is the bare username when the file records no groups", () => {
84
93
  globalThis.expect(Seed_Users$ReventlessSeed.label({
85
94
  username: "admin",
86
95
  password: "x",
87
- groups: []
96
+ groups: [],
97
+ userId: undefined
88
98
  })).toBe("admin");
89
99
  });
100
+ globalThis.test("says nothing about the userId", () => {
101
+ globalThis.expect(Seed_Users$ReventlessSeed.label({
102
+ username: "admin",
103
+ password: "x",
104
+ groups: ["Admin"],
105
+ userId: "4275e4a4-00c1-70e1-1cb5-363b06b992b5"
106
+ })).toBe("admin [Admin]");
107
+ });
90
108
  });
91
109
 
92
110
  export {
@@ -1,2 +1,2 @@
1
- #Start(1788869322834)
2
- #Done(1788869322875)
1
+ #Start(1788992686773)
2
+ #Done(1788992686834)
Binary file
Binary file
@@ -3,6 +3,7 @@ Seed
3
3
  Seed_Client
4
4
  Seed_ClientTest
5
5
  Seed_Connect
6
+ Seed_ConnectTest
6
7
  Seed_Prompt
7
8
  Seed_PromptTest
8
9
  Seed_Random
Binary file
Binary file
Binary file
@@ -16,12 +16,18 @@ module Connect = Seed_Connect
16
16
  exception Failed = Seed_Types.Failed
17
17
 
18
18
  // A live target a data set seeds against: an authenticated client, whether uploads
19
- // are skipped this run, and a label. Uploads mint through the domain API's
20
- // `Upload_Presign` mutation on the client (route B), so no upload endpoint is carried.
19
+ // are skipped this run, a label, and who the run is acting as. Uploads mint through
20
+ // the domain API's `Upload_Presign` mutation on the client (route B), so no upload
21
+ // endpoint is carried.
21
22
  type connection = Seed_Connect.connection = {
22
23
  client: Seed_Client.t,
23
24
  uploadsSkipped: bool,
24
25
  label: string,
26
+ accounts: array<Seed_Users.user>,
27
+ caller: Seed_Users.user,
28
+ callerId: option<string>,
29
+ login: (~username: string, ~password: string) => promise<string>,
30
+ roleSwitch: option<Seed_Connect.roleSwitch>,
25
31
  }
26
32
 
27
33
  // A named, seedable data set. `seed` owns everything domain-specific.
Binary file