@reventlessdev/reventless-seed 1.0.0-alpha.2 → 1.0.0-alpha.20
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 +137 -0
- package/lib/bs/.compiler.log +2 -2
- package/lib/bs/.sourcedirs.json +1 -0
- package/lib/bs/ReventlessSeed.cmi +0 -0
- package/lib/bs/ReventlessSeed.cmt +0 -0
- package/lib/bs/ReventlessSeed.mlmap +5 -0
- package/lib/bs/compiler-info.json +2 -2
- 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 +6 -3
- package/lib/bs/src/Seed.res.mjs +3 -0
- 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 +228 -15
- package/lib/bs/src/Seed_Client.res.mjs +196 -23
- 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 +27 -18
- package/lib/bs/src/Seed_Connect.res.mjs +10 -6
- 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 +110 -35
- package/lib/bs/src/Seed_Prompt.res.mjs +73 -8
- package/lib/bs/src/Seed_Random-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Runner-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Runner-ReventlessSeed.cmj +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 +38 -7
- package/lib/bs/src/Seed_Runner.res.mjs +19 -1
- package/lib/bs/src/Seed_Types-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Upload-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Upload-ReventlessSeed.cmj +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 +40 -42
- package/lib/bs/src/Seed_Upload.res.mjs +65 -69
- 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 +80 -0
- package/lib/bs/src/Seed_Users.res.mjs +90 -0
- 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 +157 -0
- package/lib/bs/tests/Seed_ClientTest.res.mjs +167 -0
- package/lib/bs/tests/Seed_PromptTest-ReventlessSeed.cmi +0 -0
- package/lib/bs/tests/Seed_PromptTest-ReventlessSeed.cmj +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 +59 -0
- package/lib/bs/tests/Seed_PromptTest.res.mjs +63 -0
- package/lib/bs/tests/Seed_RandomTest-ReventlessSeed.cmi +0 -0
- package/lib/bs/tests/Seed_RandomTest-ReventlessSeed.cmj +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 +160 -0
- package/lib/bs/tests/Seed_RandomTest.res.mjs +208 -0
- package/lib/bs/tests/Seed_UsersTest-ReventlessSeed.cmi +0 -0
- package/lib/bs/tests/Seed_UsersTest-ReventlessSeed.cmj +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 +92 -0
- package/lib/bs/tests/Seed_UsersTest.res.mjs +95 -0
- 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 +5 -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 +6 -3
- 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 +228 -15
- 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 +157 -0
- 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 +27 -18
- 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 +110 -35
- package/lib/ocaml/Seed_PromptTest-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_PromptTest-ReventlessSeed.cmj +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 +59 -0
- package/lib/ocaml/Seed_Random-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_RandomTest-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_RandomTest-ReventlessSeed.cmj +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 +160 -0
- package/lib/ocaml/Seed_Runner-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Runner-ReventlessSeed.cmj +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 +38 -7
- package/lib/ocaml/Seed_Types-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Upload-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Upload-ReventlessSeed.cmj +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 +40 -42
- 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 +80 -0
- package/lib/ocaml/Seed_UsersTest-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_UsersTest-ReventlessSeed.cmj +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 +92 -0
- package/package.json +7 -2
- package/rescript.json +9 -1
- package/src/Seed.res +6 -3
- package/src/Seed.res.mjs +3 -0
- package/src/Seed_Client.res +228 -15
- package/src/Seed_Client.res.mjs +196 -23
- package/src/Seed_Connect.res +27 -18
- package/src/Seed_Connect.res.mjs +10 -6
- package/src/Seed_Prompt.res +110 -35
- package/src/Seed_Prompt.res.mjs +73 -8
- package/src/Seed_Runner.res +38 -7
- package/src/Seed_Runner.res.mjs +19 -1
- package/src/Seed_Upload.res +40 -42
- package/src/Seed_Upload.res.mjs +65 -69
- package/src/Seed_Users.res +80 -0
- package/src/Seed_Users.res.mjs +90 -0
- package/tests/Seed_ClientTest.res +157 -0
- package/tests/Seed_ClientTest.res.mjs +167 -0
- package/tests/Seed_PromptTest.res +59 -0
- package/tests/Seed_PromptTest.res.mjs +63 -0
- package/tests/Seed_RandomTest.res +160 -0
- package/tests/Seed_RandomTest.res.mjs +208 -0
- package/tests/Seed_UsersTest.res +92 -0
- package/tests/Seed_UsersTest.res.mjs +95 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Seed_Client$ReventlessSeed from "../src/Seed_Client.res.mjs";
|
|
4
|
+
|
|
5
|
+
function errors(entries) {
|
|
6
|
+
return entries.map(prim => prim);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function errorOfType(errorType) {
|
|
10
|
+
return Object.fromEntries([[
|
|
11
|
+
"errorType",
|
|
12
|
+
errorType
|
|
13
|
+
]]);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
globalThis.describe("Seed_Client.isTransient:", () => {
|
|
17
|
+
globalThis.test("resends a fault on the endpoint's own side", () => {
|
|
18
|
+
[
|
|
19
|
+
"InternalFailure",
|
|
20
|
+
"ServiceUnavailable",
|
|
21
|
+
"Throttling",
|
|
22
|
+
"TooManyRequestsException"
|
|
23
|
+
].forEach(t => {
|
|
24
|
+
let entries = [errorOfType(t)];
|
|
25
|
+
globalThis.expect(Seed_Client$ReventlessSeed.isTransient(entries.map(prim => prim))).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
globalThis.test("resends the shape AppSync actually returned", () => {
|
|
29
|
+
let payload = JSON.parse(`[{"errorType":"InternalFailure","message":"An internal failure occurred."}]`);
|
|
30
|
+
globalThis.expect(Seed_Client$ReventlessSeed.isTransient(payload)).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
globalThis.test("fails fast on an error that is an answer", () => {
|
|
33
|
+
[
|
|
34
|
+
"ValidationException",
|
|
35
|
+
"Unauthorized",
|
|
36
|
+
"FieldUndefined"
|
|
37
|
+
].forEach(t => {
|
|
38
|
+
let entries = [errorOfType(t)];
|
|
39
|
+
globalThis.expect(Seed_Client$ReventlessSeed.isTransient(entries.map(prim => prim))).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
globalThis.test("refuses to resend a batch that mixes a real error in", () => {
|
|
43
|
+
let entries = [
|
|
44
|
+
errorOfType("InternalFailure"),
|
|
45
|
+
errorOfType("ValidationException")
|
|
46
|
+
];
|
|
47
|
+
globalThis.expect(Seed_Client$ReventlessSeed.isTransient(entries.map(prim => prim))).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
globalThis.test("treats an empty or untyped error list as final", () => {
|
|
50
|
+
globalThis.expect(Seed_Client$ReventlessSeed.isTransient([].map(prim => prim))).toBe(false);
|
|
51
|
+
let entries = [Object.fromEntries([[
|
|
52
|
+
"message",
|
|
53
|
+
"boom"
|
|
54
|
+
]])];
|
|
55
|
+
globalThis.expect(Seed_Client$ReventlessSeed.isTransient(entries.map(prim => prim))).toBe(false);
|
|
56
|
+
globalThis.expect(Seed_Client$ReventlessSeed.isTransient(null)).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
function b64url(payload) {
|
|
61
|
+
return Buffer.from(payload, "utf8").toString("base64url");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function jwt(claims) {
|
|
65
|
+
let payload = JSON.stringify(Object.fromEntries(claims));
|
|
66
|
+
return `header.` + Buffer.from(payload, "utf8").toString("base64url") + `.signature`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function localToken(claims) {
|
|
70
|
+
let payload = JSON.stringify(Object.fromEntries(claims));
|
|
71
|
+
return Buffer.from(payload, "utf8").toString("base64url") + `.signature`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function strings(values) {
|
|
75
|
+
return values.map(prim => prim);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function clientWith(token) {
|
|
79
|
+
let c = Seed_Client$ReventlessSeed.make({
|
|
80
|
+
endpoint: "http://example.invalid/graphql"
|
|
81
|
+
});
|
|
82
|
+
Seed_Client$ReventlessSeed.useToken(c, token);
|
|
83
|
+
return c;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
globalThis.describe("Seed_Client.effectiveGroups:", () => {
|
|
87
|
+
globalThis.test("reads the payload wherever the provider put it", () => {
|
|
88
|
+
globalThis.expect(Seed_Client$ReventlessSeed.effectiveGroups(clientWith(jwt([[
|
|
89
|
+
"cognito:groups",
|
|
90
|
+
[
|
|
91
|
+
"Admin",
|
|
92
|
+
"Shopper"
|
|
93
|
+
].map(prim => prim)
|
|
94
|
+
]])))).toEqual([
|
|
95
|
+
"Admin",
|
|
96
|
+
"Shopper"
|
|
97
|
+
]);
|
|
98
|
+
globalThis.expect(Seed_Client$ReventlessSeed.effectiveGroups(clientWith(localToken([[
|
|
99
|
+
"groups",
|
|
100
|
+
["Merchandiser"].map(prim => prim)
|
|
101
|
+
]])))).toEqual(["Merchandiser"]);
|
|
102
|
+
});
|
|
103
|
+
globalThis.test("says nothing about a token it cannot read", () => {
|
|
104
|
+
globalThis.expect(Seed_Client$ReventlessSeed.effectiveGroups(clientWith("not-a-token"))).toEqual(undefined);
|
|
105
|
+
globalThis.expect(Seed_Client$ReventlessSeed.effectiveGroups(Seed_Client$ReventlessSeed.make({
|
|
106
|
+
endpoint: "http://example.invalid/graphql"
|
|
107
|
+
}))).toEqual(undefined);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
globalThis.describe("Seed_Client.identitySummary:", () => {
|
|
112
|
+
globalThis.test("names the membership a narrowed token gave up", () => {
|
|
113
|
+
globalThis.expect(Seed_Client$ReventlessSeed.identitySummary(clientWith(jwt([
|
|
114
|
+
[
|
|
115
|
+
"cognito:groups",
|
|
116
|
+
["Shopper"].map(prim => prim)
|
|
117
|
+
],
|
|
118
|
+
[
|
|
119
|
+
"availableRoles",
|
|
120
|
+
"Admin,Shopper"
|
|
121
|
+
]
|
|
122
|
+
])))).toEqual("Shopper — narrowed to this role from Admin, Shopper");
|
|
123
|
+
});
|
|
124
|
+
globalThis.test("stays quiet about an unnarrowed token", () => {
|
|
125
|
+
globalThis.expect(Seed_Client$ReventlessSeed.identitySummary(clientWith(jwt([[
|
|
126
|
+
"cognito:groups",
|
|
127
|
+
[
|
|
128
|
+
"Admin",
|
|
129
|
+
"Shopper"
|
|
130
|
+
].map(prim => prim)
|
|
131
|
+
]])))).toEqual("Admin, Shopper");
|
|
132
|
+
});
|
|
133
|
+
globalThis.test("says so when the token carries no groups at all", () => {
|
|
134
|
+
globalThis.expect(Seed_Client$ReventlessSeed.identitySummary(clientWith(jwt([[
|
|
135
|
+
"cognito:groups",
|
|
136
|
+
[].map(prim => prim)
|
|
137
|
+
]])))).toEqual("no groups");
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
globalThis.describe("Seed_Client.isDenied:", () => {
|
|
142
|
+
globalThis.test("recognises a refusal in either platform's vocabulary", () => {
|
|
143
|
+
let entries = [errorOfType("Unauthorized")];
|
|
144
|
+
globalThis.expect(Seed_Client$ReventlessSeed.isDenied(entries.map(prim => prim))).toBe(true);
|
|
145
|
+
let entries$1 = [Object.fromEntries([[
|
|
146
|
+
"message",
|
|
147
|
+
"Not Authorized to access X"
|
|
148
|
+
]])];
|
|
149
|
+
globalThis.expect(Seed_Client$ReventlessSeed.isDenied(entries$1.map(prim => prim))).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
globalThis.test("leaves every other failure alone", () => {
|
|
152
|
+
let entries = [errorOfType("ValidationException")];
|
|
153
|
+
globalThis.expect(Seed_Client$ReventlessSeed.isDenied(entries.map(prim => prim))).toBe(false);
|
|
154
|
+
globalThis.expect(Seed_Client$ReventlessSeed.isDenied([].map(prim => prim))).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
export {
|
|
159
|
+
errors,
|
|
160
|
+
errorOfType,
|
|
161
|
+
b64url,
|
|
162
|
+
jwt,
|
|
163
|
+
localToken,
|
|
164
|
+
strings,
|
|
165
|
+
clientWith,
|
|
166
|
+
}
|
|
167
|
+
/* Not a pure module */
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// `isAffirmative` is the one vocabulary for "yes", shared by a typed `[y/N]`
|
|
4
|
+
// reply and by the env var that stands in for it on a run with no terminal.
|
|
5
|
+
//
|
|
6
|
+
// It exists because those two drifted: the prompt took `y`, the env var took
|
|
7
|
+
// only the literals `1` and `yes`, and every other value — `y` included, and an
|
|
8
|
+
// explicit `no` with it — fell through to the interactive branch, which throws
|
|
9
|
+
// when there is no TTY. So the variable that exists to make the tool
|
|
10
|
+
// non-interactive had two spellings that worked and a crash for the rest.
|
|
11
|
+
//
|
|
12
|
+
// The suite is written around the shape of that bug rather than around the
|
|
13
|
+
// function: what must confirm, what must decline, and — the case that actually
|
|
14
|
+
// broke CI — that declining is a normal answer and not a fall-through.
|
|
15
|
+
|
|
16
|
+
describe("Seed_Prompt.isAffirmative:", () => {
|
|
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
|
+
)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
// A prompt that prints `[y/N]` invites a capital as readily as a lowercase
|
|
24
|
+
// one, and an env var set by hand is written however the hand felt.
|
|
25
|
+
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
|
+
)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
testSync("ignores surrounding space", () => {
|
|
32
|
+
[" y", "y ", " yes "]->Array.forEach(v =>
|
|
33
|
+
expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(true)
|
|
34
|
+
)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// The half that matters most: every one of these is an ANSWER, and the caller
|
|
38
|
+
// reads it as "no". None may be mistaken for "nothing was said", which is what
|
|
39
|
+
// sent the old code to a prompt it could not open.
|
|
40
|
+
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)
|
|
43
|
+
)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
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)
|
|
49
|
+
)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// `true` is deliberately absent from the vocabulary above. Named here so that
|
|
53
|
+
// adding it is a decision someone takes against a failing test rather than a
|
|
54
|
+
// kindness someone slips in: the prompt this stands in for asks `[y/N]`, and a
|
|
55
|
+
// reply nobody would type at that prompt should not work around it either.
|
|
56
|
+
testSync("does not quietly grow a fourth spelling", () => {
|
|
57
|
+
expect(Seed_Prompt.isAffirmative("true"))->Expect.toBe(false)
|
|
58
|
+
})
|
|
59
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Seed_Prompt$ReventlessSeed from "../src/Seed_Prompt.res.mjs";
|
|
4
|
+
|
|
5
|
+
globalThis.describe("Seed_Prompt.isAffirmative:", () => {
|
|
6
|
+
globalThis.test("takes the three spellings of yes", () => {
|
|
7
|
+
[
|
|
8
|
+
"y",
|
|
9
|
+
"yes",
|
|
10
|
+
"1"
|
|
11
|
+
].forEach(v => {
|
|
12
|
+
globalThis.expect(Seed_Prompt$ReventlessSeed.isAffirmative(v)).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
globalThis.test("ignores case, so Y and YES answer as y does", () => {
|
|
16
|
+
[
|
|
17
|
+
"Y",
|
|
18
|
+
"Yes",
|
|
19
|
+
"YES"
|
|
20
|
+
].forEach(v => {
|
|
21
|
+
globalThis.expect(Seed_Prompt$ReventlessSeed.isAffirmative(v)).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
globalThis.test("ignores surrounding space", () => {
|
|
25
|
+
[
|
|
26
|
+
" y",
|
|
27
|
+
"y ",
|
|
28
|
+
" yes "
|
|
29
|
+
].forEach(v => {
|
|
30
|
+
globalThis.expect(Seed_Prompt$ReventlessSeed.isAffirmative(v)).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
globalThis.test("declines every spelling of no, capital N included", () => {
|
|
34
|
+
[
|
|
35
|
+
"n",
|
|
36
|
+
"N",
|
|
37
|
+
"no",
|
|
38
|
+
"No",
|
|
39
|
+
"NO"
|
|
40
|
+
].forEach(v => {
|
|
41
|
+
globalThis.expect(Seed_Prompt$ReventlessSeed.isAffirmative(v)).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
globalThis.test("declines anything it does not recognise", () => {
|
|
45
|
+
[
|
|
46
|
+
"",
|
|
47
|
+
" ",
|
|
48
|
+
"0",
|
|
49
|
+
"true",
|
|
50
|
+
"yep",
|
|
51
|
+
"y.",
|
|
52
|
+
"yesplease",
|
|
53
|
+
"affirmative"
|
|
54
|
+
].forEach(v => {
|
|
55
|
+
globalThis.expect(Seed_Prompt$ReventlessSeed.isAffirmative(v)).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
globalThis.test("does not quietly grow a fourth spelling", () => {
|
|
59
|
+
globalThis.expect(Seed_Prompt$ReventlessSeed.isAffirmative("true")).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
/* Not a pure module */
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// `Seed_Random` exists so that two seed runs against a fresh store produce the
|
|
4
|
+
// same data — that is what makes a seeded store usable as a screenshot baseline
|
|
5
|
+
// or a comparison target. Its correctness is therefore not "looks random" but
|
|
6
|
+
// "repeats exactly, and stays inside the bounds it advertises".
|
|
7
|
+
//
|
|
8
|
+
// The suite is written around that: every case below fails for a generator that
|
|
9
|
+
// is subtly non-reproducible or that walks out of its range, and none of them
|
|
10
|
+
// would notice a change of algorithm that kept both properties.
|
|
11
|
+
|
|
12
|
+
describe("Seed_Random:", () => {
|
|
13
|
+
let take = (~seed, ~n) => {
|
|
14
|
+
let r = Seed_Random.make(~seed)
|
|
15
|
+
Array.make(~length=n, 0.0)->Array.map(_ => Seed_Random.float(r))
|
|
16
|
+
}
|
|
17
|
+
|
|
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))
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
// Without this, a generator that returned a constant would satisfy the
|
|
24
|
+
// 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)
|
|
27
|
+
)
|
|
28
|
+
|
|
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
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe("float stays a unit interval:", () => {
|
|
38
|
+
// mulberry32's last step is a signed 32-bit value folded back to unsigned.
|
|
39
|
+
// Get that fold wrong and roughly half the draws come out negative, which
|
|
40
|
+
// 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)
|
|
43
|
+
)
|
|
44
|
+
|
|
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
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
describe("int is inclusive on both ends, as documented:", () => {
|
|
52
|
+
let draws = (~min, ~max, ~n) => {
|
|
53
|
+
let r = Seed_Random.make(~seed=5)
|
|
54
|
+
Array.make(~length=n, 0)->Array.map(_ => Seed_Random.int(r, ~min, ~max))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
testSync("never escapes the range", () =>
|
|
58
|
+
expect(draws(~min=3, ~max=7, ~n=500)->Array.every(i => i >= 3 && i <= 7))->toBe(true)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
// The documented contract is "inclusive on both ends"; an off-by-one in the
|
|
62
|
+
// `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
|
+
})
|
|
67
|
+
|
|
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)
|
|
70
|
+
)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe("pick:", () => {
|
|
74
|
+
testSync("returns None for an empty array", () =>
|
|
75
|
+
expect(Seed_Random.make(~seed=1)->Seed_Random.pick([]))->toEqual(None)
|
|
76
|
+
)
|
|
77
|
+
|
|
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
|
+
})
|
|
86
|
+
|
|
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
|
+
)
|
|
91
|
+
)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
describe("sampleWeighted is without replacement:", () => {
|
|
95
|
+
let equal = xs => xs->Array.map(x => (x, 1.0))
|
|
96
|
+
|
|
97
|
+
// The documented shuffle use: equal weights, count = length. If sampling
|
|
98
|
+
// 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
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
describe("zipfWeights skews toward the head:", () => {
|
|
141
|
+
let weights = Seed_Random.zipfWeights(["a", "b", "c", "d", "e"], ~exponent=1.1)
|
|
142
|
+
|
|
143
|
+
testSync("keeps every item, in order", () =>
|
|
144
|
+
expect(weights->Array.map(((item, _)) => item))->toEqual(["a", "b", "c", "d", "e"])
|
|
145
|
+
)
|
|
146
|
+
|
|
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
|
+
})
|
|
153
|
+
|
|
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
|
+
})
|
|
159
|
+
})
|
|
160
|
+
})
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
6
|
+
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
7
|
+
import * as Seed_Random$ReventlessSeed from "../src/Seed_Random.res.mjs";
|
|
8
|
+
|
|
9
|
+
globalThis.describe("Seed_Random:", () => {
|
|
10
|
+
let take = (seed, n) => {
|
|
11
|
+
let r = Seed_Random$ReventlessSeed.make(seed);
|
|
12
|
+
return Stdlib_Array.make(n, 0.0).map(param => Seed_Random$ReventlessSeed.float(r));
|
|
13
|
+
};
|
|
14
|
+
globalThis.describe("reproducibility — the reason this module exists:", () => {
|
|
15
|
+
globalThis.test("the same seed replays the same sequence", () => {
|
|
16
|
+
globalThis.expect(take(42, 25)).toEqual(take(42, 25));
|
|
17
|
+
});
|
|
18
|
+
globalThis.test("different seeds diverge", () => {
|
|
19
|
+
globalThis.expect(Primitive_object.equal(take(42, 25), take(43, 25))).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
globalThis.test("a fresh generator does not continue the previous one", () => {
|
|
22
|
+
let r = Seed_Random$ReventlessSeed.make(7);
|
|
23
|
+
let first = Seed_Random$ReventlessSeed.float(r);
|
|
24
|
+
Seed_Random$ReventlessSeed.float(r);
|
|
25
|
+
globalThis.expect(Seed_Random$ReventlessSeed.float(Seed_Random$ReventlessSeed.make(7))).toEqual(first);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
globalThis.describe("float stays a unit interval:", () => {
|
|
29
|
+
globalThis.test("every draw is in [0, 1)", () => {
|
|
30
|
+
globalThis.expect(take(99, 500).every(f => {
|
|
31
|
+
if (f >= 0.0) {
|
|
32
|
+
return f < 1.0;
|
|
33
|
+
} else {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
})).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
globalThis.test("and the draws are not all the same value", () => {
|
|
39
|
+
let draws = take(99, 100);
|
|
40
|
+
globalThis.expect(draws.some(f => f !== draws[0])).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
globalThis.describe("int is inclusive on both ends, as documented:", () => {
|
|
44
|
+
let draws = (min, max, n) => {
|
|
45
|
+
let r = Seed_Random$ReventlessSeed.make(5);
|
|
46
|
+
return Stdlib_Array.make(n, 0).map(param => Seed_Random$ReventlessSeed.int(r, min, max));
|
|
47
|
+
};
|
|
48
|
+
globalThis.test("never escapes the range", () => {
|
|
49
|
+
globalThis.expect(draws(3, 7, 500).every(i => {
|
|
50
|
+
if (i >= 3) {
|
|
51
|
+
return i <= 7;
|
|
52
|
+
} else {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
})).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
globalThis.test("reaches both bounds", () => {
|
|
58
|
+
let d = draws(3, 7, 500);
|
|
59
|
+
globalThis.expect([
|
|
60
|
+
d.includes(3),
|
|
61
|
+
d.includes(7)
|
|
62
|
+
]).toEqual([
|
|
63
|
+
true,
|
|
64
|
+
true
|
|
65
|
+
]);
|
|
66
|
+
});
|
|
67
|
+
globalThis.test("a single-value range is that value", () => {
|
|
68
|
+
globalThis.expect(draws(4, 4, 10).every(i => i === 4)).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
globalThis.describe("pick:", () => {
|
|
72
|
+
globalThis.test("returns None for an empty array", () => {
|
|
73
|
+
globalThis.expect(Seed_Random$ReventlessSeed.pick(Seed_Random$ReventlessSeed.make(1), [])).toEqual(undefined);
|
|
74
|
+
});
|
|
75
|
+
globalThis.test("always lands on a member", () => {
|
|
76
|
+
let r = Seed_Random$ReventlessSeed.make(2);
|
|
77
|
+
let xs = [
|
|
78
|
+
"a",
|
|
79
|
+
"b",
|
|
80
|
+
"c",
|
|
81
|
+
"d"
|
|
82
|
+
];
|
|
83
|
+
let hits = Stdlib_Array.make(200, "").map(param => Seed_Random$ReventlessSeed.pickOr(r, "MISS", xs));
|
|
84
|
+
globalThis.expect(hits.every(h => xs.includes(h))).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
globalThis.test("pickOr falls back only when there is nothing to pick", () => {
|
|
87
|
+
globalThis.expect(Seed_Random$ReventlessSeed.pickOr(Seed_Random$ReventlessSeed.make(3), "fallback", [])).toBe("fallback");
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
globalThis.describe("sampleWeighted is without replacement:", () => {
|
|
91
|
+
let equal = xs => xs.map(x => [
|
|
92
|
+
x,
|
|
93
|
+
1.0
|
|
94
|
+
]);
|
|
95
|
+
globalThis.test("count = length yields a permutation, not a multiset", () => {
|
|
96
|
+
let xs = [
|
|
97
|
+
"a",
|
|
98
|
+
"b",
|
|
99
|
+
"c",
|
|
100
|
+
"d",
|
|
101
|
+
"e"
|
|
102
|
+
];
|
|
103
|
+
let out = Seed_Random$ReventlessSeed.sampleWeighted(Seed_Random$ReventlessSeed.make(11), equal(xs), 5);
|
|
104
|
+
let sorted = out.toSorted(Primitive_string.compare);
|
|
105
|
+
globalThis.expect([
|
|
106
|
+
out.length,
|
|
107
|
+
sorted
|
|
108
|
+
]).toEqual([
|
|
109
|
+
5,
|
|
110
|
+
[
|
|
111
|
+
"a",
|
|
112
|
+
"b",
|
|
113
|
+
"c",
|
|
114
|
+
"d",
|
|
115
|
+
"e"
|
|
116
|
+
]
|
|
117
|
+
]);
|
|
118
|
+
});
|
|
119
|
+
globalThis.test("count < length returns exactly count distinct items", () => {
|
|
120
|
+
let xs = [
|
|
121
|
+
"a",
|
|
122
|
+
"b",
|
|
123
|
+
"c",
|
|
124
|
+
"d",
|
|
125
|
+
"e"
|
|
126
|
+
];
|
|
127
|
+
let out = Seed_Random$ReventlessSeed.sampleWeighted(Seed_Random$ReventlessSeed.make(12), equal(xs), 3);
|
|
128
|
+
let distinct = new Set(out).size;
|
|
129
|
+
globalThis.expect([
|
|
130
|
+
out.length,
|
|
131
|
+
distinct
|
|
132
|
+
]).toEqual([
|
|
133
|
+
3,
|
|
134
|
+
3
|
|
135
|
+
]);
|
|
136
|
+
});
|
|
137
|
+
globalThis.test("count > length clamps to the pool", () => {
|
|
138
|
+
globalThis.expect(Seed_Random$ReventlessSeed.sampleWeighted(Seed_Random$ReventlessSeed.make(13), equal([
|
|
139
|
+
"a",
|
|
140
|
+
"b"
|
|
141
|
+
]), 99).length).toBe(2);
|
|
142
|
+
});
|
|
143
|
+
globalThis.test("a zero-weight entry is never chosen while others remain", () => {
|
|
144
|
+
let out = Seed_Random$ReventlessSeed.sampleWeighted(Seed_Random$ReventlessSeed.make(14), [
|
|
145
|
+
[
|
|
146
|
+
"never",
|
|
147
|
+
0.0
|
|
148
|
+
],
|
|
149
|
+
[
|
|
150
|
+
"a",
|
|
151
|
+
1.0
|
|
152
|
+
],
|
|
153
|
+
[
|
|
154
|
+
"b",
|
|
155
|
+
1.0
|
|
156
|
+
]
|
|
157
|
+
], 2);
|
|
158
|
+
globalThis.expect(out.includes("never")).toBe(false);
|
|
159
|
+
});
|
|
160
|
+
globalThis.test("is reproducible for a given seed", () => {
|
|
161
|
+
let run = () => Seed_Random$ReventlessSeed.sampleWeighted(Seed_Random$ReventlessSeed.make(15), equal([
|
|
162
|
+
"a",
|
|
163
|
+
"b",
|
|
164
|
+
"c",
|
|
165
|
+
"d",
|
|
166
|
+
"e",
|
|
167
|
+
"f"
|
|
168
|
+
]), 4);
|
|
169
|
+
globalThis.expect(run()).toEqual(run());
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
globalThis.describe("zipfWeights skews toward the head:", () => {
|
|
173
|
+
let weights = Seed_Random$ReventlessSeed.zipfWeights([
|
|
174
|
+
"a",
|
|
175
|
+
"b",
|
|
176
|
+
"c",
|
|
177
|
+
"d",
|
|
178
|
+
"e"
|
|
179
|
+
], 1.1);
|
|
180
|
+
globalThis.test("keeps every item, in order", () => {
|
|
181
|
+
globalThis.expect(weights.map(param => param[0])).toEqual([
|
|
182
|
+
"a",
|
|
183
|
+
"b",
|
|
184
|
+
"c",
|
|
185
|
+
"d",
|
|
186
|
+
"e"
|
|
187
|
+
]);
|
|
188
|
+
});
|
|
189
|
+
globalThis.test("weight decreases monotonically with rank", () => {
|
|
190
|
+
let ws = weights.map(param => param[1]);
|
|
191
|
+
let decreasing = ws.every((w, i) => {
|
|
192
|
+
if (i === 0) {
|
|
193
|
+
return true;
|
|
194
|
+
} else {
|
|
195
|
+
return w < ws[i - 1 | 0];
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
globalThis.expect(decreasing).toBe(true);
|
|
199
|
+
});
|
|
200
|
+
globalThis.test("the leader is clear — first outweighs last several times over", () => {
|
|
201
|
+
let first = Stdlib_Option.mapOr(weights[0], 0.0, param => param[1]);
|
|
202
|
+
let last = Stdlib_Option.mapOr(weights[4], 0.0, param => param[1]);
|
|
203
|
+
globalThis.expect(first > last * 3.0).toBe(true);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
/* Not a pure module */
|