@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.
- package/CHANGELOG.md +19 -0
- package/lib/bs/.compiler.log +2 -2
- package/lib/bs/ReventlessSeed.cmi +0 -0
- package/lib/bs/ReventlessSeed.cmt +0 -0
- package/lib/bs/ReventlessSeed.mlmap +1 -0
- package/lib/bs/compiler-info.json +1 -1
- package/lib/bs/src/Seed-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed.ast +0 -0
- package/lib/bs/src/Seed.res +8 -2
- package/lib/bs/src/Seed_Client-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Client-ReventlessSeed.cmj +0 -0
- package/lib/bs/src/Seed_Client-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Client.ast +0 -0
- package/lib/bs/src/Seed_Client.res +169 -21
- package/lib/bs/src/Seed_Client.res.mjs +142 -7
- package/lib/bs/src/Seed_Connect-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Connect-ReventlessSeed.cmj +0 -0
- package/lib/bs/src/Seed_Connect-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Connect.ast +0 -0
- package/lib/bs/src/Seed_Connect.res +258 -19
- package/lib/bs/src/Seed_Connect.res.mjs +158 -8
- package/lib/bs/src/Seed_Prompt-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Prompt-ReventlessSeed.cmj +0 -0
- package/lib/bs/src/Seed_Prompt-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Prompt.ast +0 -0
- package/lib/bs/src/Seed_Prompt.res +48 -23
- package/lib/bs/src/Seed_Prompt.res.mjs +33 -22
- package/lib/bs/src/Seed_Random-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Random-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Random.ast +0 -0
- package/lib/bs/src/Seed_Random.res +4 -2
- package/lib/bs/src/Seed_Runner-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Runner-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Runner.ast +0 -0
- package/lib/bs/src/Seed_Runner.res +7 -12
- package/lib/bs/src/Seed_Upload-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Upload-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Upload.ast +0 -0
- package/lib/bs/src/Seed_Upload.res +4 -2
- package/lib/bs/src/Seed_Users-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Users-ReventlessSeed.cmj +0 -0
- package/lib/bs/src/Seed_Users-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Users.ast +0 -0
- package/lib/bs/src/Seed_Users.res +30 -3
- package/lib/bs/src/Seed_Users.res.mjs +14 -1
- package/lib/bs/tests/Seed_ClientTest-ReventlessSeed.cmi +0 -0
- package/lib/bs/tests/Seed_ClientTest-ReventlessSeed.cmj +0 -0
- package/lib/bs/tests/Seed_ClientTest-ReventlessSeed.cmt +0 -0
- package/lib/bs/tests/Seed_ClientTest.ast +0 -0
- package/lib/bs/tests/Seed_ClientTest.res +146 -16
- package/lib/bs/tests/Seed_ClientTest.res.mjs +102 -1
- package/lib/bs/tests/Seed_ConnectTest-ReventlessSeed.cmi +0 -0
- package/lib/bs/tests/Seed_ConnectTest-ReventlessSeed.cmj +0 -0
- package/lib/bs/tests/Seed_ConnectTest-ReventlessSeed.cmt +0 -0
- package/lib/bs/tests/Seed_ConnectTest.ast +0 -0
- package/lib/bs/tests/Seed_ConnectTest.res +64 -0
- package/lib/bs/tests/Seed_ConnectTest.res.mjs +56 -0
- package/lib/bs/tests/Seed_PromptTest-ReventlessSeed.cmi +0 -0
- package/lib/bs/tests/Seed_PromptTest-ReventlessSeed.cmt +0 -0
- package/lib/bs/tests/Seed_PromptTest.ast +0 -0
- package/lib/bs/tests/Seed_PromptTest.res +8 -12
- package/lib/bs/tests/Seed_RandomTest-ReventlessSeed.cmi +0 -0
- package/lib/bs/tests/Seed_RandomTest-ReventlessSeed.cmt +0 -0
- package/lib/bs/tests/Seed_RandomTest.ast +0 -0
- package/lib/bs/tests/Seed_RandomTest.res +130 -90
- package/lib/bs/tests/Seed_UsersTest-ReventlessSeed.cmi +0 -0
- package/lib/bs/tests/Seed_UsersTest-ReventlessSeed.cmt +0 -0
- package/lib/bs/tests/Seed_UsersTest.ast +0 -0
- package/lib/bs/tests/Seed_UsersTest.res +77 -2
- package/lib/bs/tests/Seed_UsersTest.res.mjs +45 -2
- package/lib/ocaml/.compiler.log +2 -2
- package/lib/ocaml/ReventlessSeed.cmi +0 -0
- package/lib/ocaml/ReventlessSeed.cmt +0 -0
- package/lib/ocaml/ReventlessSeed.mlmap +1 -0
- package/lib/ocaml/Seed-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed.ast +0 -0
- package/lib/ocaml/Seed.res +8 -2
- package/lib/ocaml/Seed_Client-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Client-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_Client-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Client.ast +0 -0
- package/lib/ocaml/Seed_Client.res +169 -21
- package/lib/ocaml/Seed_ClientTest-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_ClientTest-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_ClientTest-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_ClientTest.ast +0 -0
- package/lib/ocaml/Seed_ClientTest.res +146 -16
- package/lib/ocaml/Seed_Connect-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Connect-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_Connect-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Connect.ast +0 -0
- package/lib/ocaml/Seed_Connect.res +258 -19
- package/lib/ocaml/Seed_ConnectTest-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_ConnectTest-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_ConnectTest-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_ConnectTest.ast +0 -0
- package/lib/ocaml/Seed_ConnectTest.res +64 -0
- package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Prompt.ast +0 -0
- package/lib/ocaml/Seed_Prompt.res +48 -23
- package/lib/ocaml/Seed_PromptTest-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_PromptTest-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_PromptTest.ast +0 -0
- package/lib/ocaml/Seed_PromptTest.res +8 -12
- package/lib/ocaml/Seed_Random-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Random-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Random.ast +0 -0
- package/lib/ocaml/Seed_Random.res +4 -2
- package/lib/ocaml/Seed_RandomTest-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_RandomTest-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_RandomTest.ast +0 -0
- package/lib/ocaml/Seed_RandomTest.res +130 -90
- package/lib/ocaml/Seed_Runner-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Runner-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Runner.ast +0 -0
- package/lib/ocaml/Seed_Runner.res +7 -12
- package/lib/ocaml/Seed_Upload-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Upload-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Upload.ast +0 -0
- package/lib/ocaml/Seed_Upload.res +4 -2
- package/lib/ocaml/Seed_Users-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Users-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_Users-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Users.ast +0 -0
- package/lib/ocaml/Seed_Users.res +30 -3
- package/lib/ocaml/Seed_UsersTest-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_UsersTest-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_UsersTest.ast +0 -0
- package/lib/ocaml/Seed_UsersTest.res +77 -2
- package/package.json +2 -2
- package/src/Seed.res +8 -2
- package/src/Seed_Client.res +169 -21
- package/src/Seed_Client.res.mjs +142 -7
- package/src/Seed_Connect.res +258 -19
- package/src/Seed_Connect.res.mjs +158 -8
- package/src/Seed_Prompt.res +48 -23
- package/src/Seed_Prompt.res.mjs +33 -22
- package/src/Seed_Random.res +4 -2
- package/src/Seed_Runner.res +7 -12
- package/src/Seed_Upload.res +4 -2
- package/src/Seed_Users.res +30 -3
- package/src/Seed_Users.res.mjs +14 -1
- package/tests/Seed_ClientTest.res +146 -16
- package/tests/Seed_ClientTest.res.mjs +102 -1
- package/tests/Seed_ConnectTest.res +64 -0
- package/tests/Seed_ConnectTest.res.mjs +56 -0
- package/tests/Seed_PromptTest.res +8 -12
- package/tests/Seed_RandomTest.res +130 -90
- package/tests/Seed_UsersTest.res +77 -2
- package/tests/Seed_UsersTest.res.mjs +45 -2
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
3
5
|
import * as Seed_Client$ReventlessSeed from "../src/Seed_Client.res.mjs";
|
|
4
6
|
|
|
5
7
|
function errors(entries) {
|
|
@@ -61,9 +63,11 @@ function b64url(payload) {
|
|
|
61
63
|
return Buffer.from(payload, "utf8").toString("base64url");
|
|
62
64
|
}
|
|
63
65
|
|
|
66
|
+
let joseHeader = Buffer.from(`{"kid":"abc123","alg":"RS256"}`, "utf8").toString("base64url");
|
|
67
|
+
|
|
64
68
|
function jwt(claims) {
|
|
65
69
|
let payload = JSON.stringify(Object.fromEntries(claims));
|
|
66
|
-
return
|
|
70
|
+
return joseHeader + `.` + Buffer.from(payload, "utf8").toString("base64url") + `.signature`;
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
function localToken(claims) {
|
|
@@ -83,6 +87,21 @@ function clientWith(token) {
|
|
|
83
87
|
return c;
|
|
84
88
|
}
|
|
85
89
|
|
|
90
|
+
globalThis.describe("Seed_Client.claims:", () => {
|
|
91
|
+
globalThis.test("skips the JOSE header and reads the payload behind it", () => {
|
|
92
|
+
globalThis.expect(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Seed_Client$ReventlessSeed.claims(clientWith(jwt([[
|
|
93
|
+
"sub",
|
|
94
|
+
"real-sub"
|
|
95
|
+
]]))), c => c["sub"]), Stdlib_JSON.Decode.string)).toEqual("real-sub");
|
|
96
|
+
});
|
|
97
|
+
globalThis.test("takes the first segment when there is no header", () => {
|
|
98
|
+
globalThis.expect(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Seed_Client$ReventlessSeed.claims(clientWith(localToken([[
|
|
99
|
+
"userId",
|
|
100
|
+
"local-shopper"
|
|
101
|
+
]]))), c => c["userId"]), Stdlib_JSON.Decode.string)).toEqual("local-shopper");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
86
105
|
globalThis.describe("Seed_Client.effectiveGroups:", () => {
|
|
87
106
|
globalThis.test("reads the payload wherever the provider put it", () => {
|
|
88
107
|
globalThis.expect(Seed_Client$ReventlessSeed.effectiveGroups(clientWith(jwt([[
|
|
@@ -108,6 +127,50 @@ globalThis.describe("Seed_Client.effectiveGroups:", () => {
|
|
|
108
127
|
});
|
|
109
128
|
});
|
|
110
129
|
|
|
130
|
+
globalThis.describe("Seed_Client.callerId:", () => {
|
|
131
|
+
globalThis.test("reads the id under whichever name the provider gave it", () => {
|
|
132
|
+
globalThis.expect(Seed_Client$ReventlessSeed.callerId(clientWith(jwt([[
|
|
133
|
+
"sub",
|
|
134
|
+
"4275e4a4-00c1-70e1"
|
|
135
|
+
]])))).toEqual("4275e4a4-00c1-70e1");
|
|
136
|
+
globalThis.expect(Seed_Client$ReventlessSeed.callerId(clientWith(localToken([[
|
|
137
|
+
"userId",
|
|
138
|
+
"local-shopper"
|
|
139
|
+
]])))).toEqual("local-shopper");
|
|
140
|
+
});
|
|
141
|
+
globalThis.test("prefers sub when a token carries both names", () => {
|
|
142
|
+
globalThis.expect(Seed_Client$ReventlessSeed.callerId(clientWith(jwt([
|
|
143
|
+
[
|
|
144
|
+
"sub",
|
|
145
|
+
"f215e4b4-30f1-700c"
|
|
146
|
+
],
|
|
147
|
+
[
|
|
148
|
+
"userId",
|
|
149
|
+
"local-shopper"
|
|
150
|
+
]
|
|
151
|
+
])))).toEqual("f215e4b4-30f1-700c");
|
|
152
|
+
});
|
|
153
|
+
globalThis.test("says nothing about a token it cannot read, or one naming no id", () => {
|
|
154
|
+
globalThis.expect(Seed_Client$ReventlessSeed.callerId(clientWith("not-a-token"))).toEqual(undefined);
|
|
155
|
+
globalThis.expect(Seed_Client$ReventlessSeed.callerId(clientWith(jwt([[
|
|
156
|
+
"cognito:groups",
|
|
157
|
+
["Admin"].map(prim => prim)
|
|
158
|
+
]])))).toEqual(undefined);
|
|
159
|
+
globalThis.expect(Seed_Client$ReventlessSeed.callerId(Seed_Client$ReventlessSeed.make({
|
|
160
|
+
endpoint: "http://example.invalid/graphql"
|
|
161
|
+
}))).toEqual(undefined);
|
|
162
|
+
});
|
|
163
|
+
globalThis.test("refuses a claim that is not a single string", () => {
|
|
164
|
+
globalThis.expect(Seed_Client$ReventlessSeed.callerId(clientWith(jwt([[
|
|
165
|
+
"sub",
|
|
166
|
+
[
|
|
167
|
+
"a",
|
|
168
|
+
"b"
|
|
169
|
+
].map(prim => prim)
|
|
170
|
+
]])))).toEqual(undefined);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
111
174
|
globalThis.describe("Seed_Client.identitySummary:", () => {
|
|
112
175
|
globalThis.test("names the membership a narrowed token gave up", () => {
|
|
113
176
|
globalThis.expect(Seed_Client$ReventlessSeed.identitySummary(clientWith(jwt([
|
|
@@ -138,6 +201,43 @@ globalThis.describe("Seed_Client.identitySummary:", () => {
|
|
|
138
201
|
});
|
|
139
202
|
});
|
|
140
203
|
|
|
204
|
+
globalThis.describe("Seed_Client.narrowedFrom:", () => {
|
|
205
|
+
globalThis.test("names the membership a narrowed token was reduced from", () => {
|
|
206
|
+
globalThis.expect(Seed_Client$ReventlessSeed.narrowedFrom(clientWith(jwt([
|
|
207
|
+
[
|
|
208
|
+
"cognito:groups",
|
|
209
|
+
["Shopper"].map(prim => prim)
|
|
210
|
+
],
|
|
211
|
+
[
|
|
212
|
+
"availableRoles",
|
|
213
|
+
"Admin,Shopper"
|
|
214
|
+
]
|
|
215
|
+
])))).toEqual([
|
|
216
|
+
"Admin",
|
|
217
|
+
"Shopper"
|
|
218
|
+
]);
|
|
219
|
+
});
|
|
220
|
+
globalThis.test("says nothing about an unnarrowed token", () => {
|
|
221
|
+
globalThis.expect(Seed_Client$ReventlessSeed.narrowedFrom(clientWith(jwt([[
|
|
222
|
+
"cognito:groups",
|
|
223
|
+
["Shopper"].map(prim => prim)
|
|
224
|
+
]])))).toEqual(undefined);
|
|
225
|
+
globalThis.expect(Seed_Client$ReventlessSeed.narrowedFrom(clientWith(jwt([[
|
|
226
|
+
"cognito:groups",
|
|
227
|
+
[
|
|
228
|
+
"Admin",
|
|
229
|
+
"Shopper"
|
|
230
|
+
].map(prim => prim)
|
|
231
|
+
]])))).toEqual(undefined);
|
|
232
|
+
});
|
|
233
|
+
globalThis.test("treats an empty availableRoles as not narrowed", () => {
|
|
234
|
+
globalThis.expect(Seed_Client$ReventlessSeed.narrowedFrom(clientWith(jwt([[
|
|
235
|
+
"availableRoles",
|
|
236
|
+
""
|
|
237
|
+
]])))).toEqual(undefined);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
141
241
|
globalThis.describe("Seed_Client.isDenied:", () => {
|
|
142
242
|
globalThis.test("recognises a refusal in either platform's vocabulary", () => {
|
|
143
243
|
let entries = [errorOfType("Unauthorized")];
|
|
@@ -159,6 +259,7 @@ export {
|
|
|
159
259
|
errors,
|
|
160
260
|
errorOfType,
|
|
161
261
|
b64url,
|
|
262
|
+
joseHeader,
|
|
162
263
|
jwt,
|
|
163
264
|
localToken,
|
|
164
265
|
strings,
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
|
Binary file
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
Binary file
|
|
Binary file
|
|
@@ -16,36 +16,45 @@ describe("Seed_Random:", () => {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
describe("reproducibility — the reason this module exists:", () => {
|
|
19
|
-
testSync(
|
|
20
|
-
|
|
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(
|
|
26
|
-
|
|
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(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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(
|
|
42
|
-
|
|
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(
|
|
46
|
-
|
|
47
|
-
|
|
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(
|
|
58
|
-
|
|
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(
|
|
64
|
-
|
|
65
|
-
|
|
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(
|
|
69
|
-
|
|
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(
|
|
75
|
-
|
|
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(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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(
|
|
88
|
-
|
|
89
|
-
|
|
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(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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(
|
|
144
|
-
|
|
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(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
ws->Array.
|
|
151
|
-
|
|
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(
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
Binary file
|
|
Binary file
|