@gigzen/populace 1.3.2 → 1.3.4
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/README.md +21 -6
- package/adapters/{buzzbuzz.mjs → waggle.mjs} +7 -3
- package/examples/rest-api/adapter.mjs +1 -1
- package/examples/{buzzbuzz → waggle}/populace.config.mjs +3 -3
- package/examples/{buzzbuzz-local → waggle-local}/populace.config.mjs +4 -4
- package/package.json +60 -60
- package/src/html-report.mjs +20 -2
- package/src/report.mjs +38 -6
- package/src/selftest.mjs +53 -0
package/README.md
CHANGED
|
@@ -25,18 +25,19 @@ applying**. Then it hands you a report on what broke.
|
|
|
25
25
|
|
|
26
26
|
---
|
|
27
27
|
|
|
28
|
-
## It has done this to a real
|
|
28
|
+
## It has done this to a real app
|
|
29
29
|
|
|
30
30
|

|
|
31
31
|
|
|
32
|
-
That is
|
|
32
|
+
That is a later run. The first one is the interesting one.
|
|
33
33
|
|
|
34
34
|
### What happened, in plain English
|
|
35
35
|
|
|
36
36
|
On **9 August 2026** we pointed Populace at **Buzz** — a gig-worker platform
|
|
37
37
|
on Android with a live Postgres backend, 17 tables and 48 row-level-security
|
|
38
|
-
policies. It
|
|
39
|
-
|
|
38
|
+
policies. It has since been renamed [**Waggle**](https://shakhtar-sankur.github.io/gigzen/waggle.html).
|
|
39
|
+
It had not launched yet, but it was signed, and it had been through a full manual
|
|
40
|
+
test of every screen by the person who wrote it, and it had passed.
|
|
40
41
|
|
|
41
42
|
We started six simulated drivers: three in **Manila**, three in **Mumbai**. Each
|
|
42
43
|
one signed up for a real account, set a profile, started driving a plausible
|
|
@@ -137,6 +138,20 @@ Two files land in whatever directory you ran it from: `populace-report.json` for
|
|
|
137
138
|
CI, and `populace-report.html`, which is one self-contained page you can email
|
|
138
139
|
to someone who was not watching your terminal.
|
|
139
140
|
|
|
141
|
+
### Or skip the terminal
|
|
142
|
+
|
|
143
|
+
[**Populace Studio**](https://github.com/Shakhtar-Sankur/populace/releases/latest) is
|
|
144
|
+
the same engine in a window, for Windows, with no Node or npm needed. Download the
|
|
145
|
+
installer or the portable build and check it against `SHA256SUMS.txt` on the release.
|
|
146
|
+
Neither is code-signed yet, so Windows SmartScreen will warn before the first launch.
|
|
147
|
+
|
|
148
|
+
Studio shows every simulated person on a world map as they move, and a box per contract
|
|
149
|
+
method with its live latency. A method your adapter implements but the run never called is
|
|
150
|
+
drawn dashed and named in the verdict, so it is not counted as covered. When something breaks,
|
|
151
|
+
Studio shows the failing method and the database's own error text while the run is still
|
|
152
|
+
going. It does not reimplement the engine: every run is the same command a terminal would
|
|
153
|
+
issue, and Studio prints that command.
|
|
154
|
+
|
|
140
155
|
---
|
|
141
156
|
|
|
142
157
|
## Point it at your own app
|
|
@@ -152,7 +167,7 @@ You write **one adapter** — thirteen small methods, each answering "how does
|
|
|
152
167
|
this happen in my app?". Two are required (`createUser`, `deleteUser`);
|
|
153
168
|
everything else is optional and anything you skip is reported as untested rather
|
|
154
169
|
than quietly passing. Full spec in [adapters/contract.md](adapters/contract.md),
|
|
155
|
-
and [adapters/
|
|
170
|
+
and [adapters/waggle.mjs](adapters/waggle.mjs) is a complete real-world
|
|
156
171
|
example in ~150 lines.
|
|
157
172
|
|
|
158
173
|
`populace doctor` on a fresh scaffold says `2/13` and refuses to run — a method
|
|
@@ -418,7 +433,7 @@ multi-user simulation is best at finding.
|
|
|
418
433
|
- **A populated app** instead of an empty one — the cold-start problem, solved for demos and for judging your own UX
|
|
419
434
|
- **Multi-user paths exercised** without recruiting humans: presence, receipts, realtime fan-out, membership counts
|
|
420
435
|
- **Permission rules tested by users who genuinely have different identities**
|
|
421
|
-
- **Latency per endpoint** (p50/p95/
|
|
436
|
+
- **Latency per endpoint** (p50/p95/max) under N concurrent users
|
|
422
437
|
- **Failures grouped by shape**, not exact text, so one bug is one line rather than fifty
|
|
423
438
|
- **Account deletion actually tested** — the path almost nobody exercises and the one regulators ask about
|
|
424
439
|
- **`populace-report.json`** for CI; the run exits non-zero when problems are found
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
// Adapter:
|
|
1
|
+
// Adapter: Waggle — a gig-driver tracking app on Supabase.
|
|
2
|
+
//
|
|
3
|
+
// (This file was adapters/buzzbuzz.mjs until the app was renamed Waggle. The
|
|
4
|
+
// table names and the phone-to-email scheme below are the app's own and did
|
|
5
|
+
// not change with the name.)
|
|
2
6
|
//
|
|
3
7
|
// Populace's first customer, and the reference implementation. Read this
|
|
4
8
|
// alongside contract.md to see what a complete adapter looks like: it is ~140
|
|
@@ -20,13 +24,13 @@ export function createAdapter(target) {
|
|
|
20
24
|
|
|
21
25
|
if (!url || !key) {
|
|
22
26
|
throw new Error(
|
|
23
|
-
"
|
|
27
|
+
"waggle adapter needs target.url and target.key.\n" +
|
|
24
28
|
" Set BUZZBUZZ_TEST_URL and BUZZBUZZ_TEST_KEY in your environment.",
|
|
25
29
|
);
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
return {
|
|
29
|
-
name: "
|
|
33
|
+
name: "waggle",
|
|
30
34
|
|
|
31
35
|
// supabase-js RETURNS network errors rather than throwing, so without this
|
|
32
36
|
// a wrong URL looks identical to "that account doesn't exist" — and a run
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* An adapter for a plain REST API — the second one Populace has ever had, and
|
|
3
3
|
* the reason the portability claim is more than a hope.
|
|
4
4
|
*
|
|
5
|
-
* Read this next to adapters/
|
|
5
|
+
* Read this next to adapters/waggle.mjs. They share no library, no id type,
|
|
6
6
|
* no error convention and no auth mechanism, and the engine cannot tell them
|
|
7
7
|
* apart. That is the whole argument.
|
|
8
8
|
*
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
// Run it — PowerShell (Windows):
|
|
4
4
|
// $env:BUZZBUZZ_TEST_URL="https://<your-TEST-project>.supabase.co"
|
|
5
5
|
// $env:BUZZBUZZ_TEST_KEY="<test project publishable key>"
|
|
6
|
-
// node src/cli.mjs run --config examples/
|
|
6
|
+
// node src/cli.mjs run --config examples/waggle/populace.config.mjs
|
|
7
7
|
//
|
|
8
8
|
// Run it — bash/zsh (macOS, Linux):
|
|
9
9
|
// BUZZBUZZ_TEST_URL=... BUZZBUZZ_TEST_KEY=... \
|
|
10
|
-
// node src/cli.mjs run --config examples/
|
|
10
|
+
// node src/cli.mjs run --config examples/waggle/populace.config.mjs
|
|
11
11
|
//
|
|
12
12
|
// The test project needs the same schema as production. Run these against it:
|
|
13
13
|
// schema.sql · social_features.sql · groups.sql · direct_messages.sql
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
export default {
|
|
20
20
|
app: "Buzz",
|
|
21
|
-
adapter: "../../adapters/
|
|
21
|
+
adapter: "../../adapters/waggle.mjs",
|
|
22
22
|
environment: "test",
|
|
23
23
|
|
|
24
24
|
target: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Buzz against the local Supabase stack, at a scale the hosted project refuses.
|
|
2
2
|
//
|
|
3
|
-
// Why this file exists rather than reusing examples/
|
|
3
|
+
// Why this file exists rather than reusing examples/waggle:
|
|
4
4
|
//
|
|
5
5
|
// The hosted test project applies Supabase's default auth rate limit of 30 sign
|
|
6
6
|
// ups per five minutes per IP address. Measured on 2026-08-24: a 250-driver run
|
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
// npx supabase start
|
|
21
21
|
//
|
|
22
22
|
// Then either pick this file in Populace Studio, or:
|
|
23
|
-
// node src/cli.mjs run --config examples/
|
|
23
|
+
// node src/cli.mjs run --config examples/waggle-local/populace.config.mjs
|
|
24
24
|
|
|
25
25
|
export default {
|
|
26
26
|
app: "Buzz",
|
|
27
|
-
adapter: "../../adapters/
|
|
27
|
+
adapter: "../../adapters/waggle.mjs",
|
|
28
28
|
environment: "test",
|
|
29
29
|
|
|
30
30
|
// The local stack's fixed development address and publishable key. These are
|
|
@@ -35,7 +35,7 @@ export default {
|
|
|
35
35
|
key: "sb_publishable_ACJWlzQHlZjBrEguHvfOxg_3BJgxAaH",
|
|
36
36
|
},
|
|
37
37
|
|
|
38
|
-
// Kept identical to examples/
|
|
38
|
+
// Kept identical to examples/waggle. A local run is no reason to relax the
|
|
39
39
|
// guard: if one of these ever appears in `target.url` above, refuse to start.
|
|
40
40
|
neverRunAgainst: [
|
|
41
41
|
"https://rqzuuvlougzhynckvqzd.supabase.co",
|
package/package.json
CHANGED
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@gigzen/populace",
|
|
3
|
-
"version": "1.3.
|
|
4
|
-
"description": "A simulated population that uses your app through its real API, so you can test what needs more than one person.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"populace": "./src/cli.mjs"
|
|
8
|
-
},
|
|
9
|
-
"main": "./src/index.mjs",
|
|
10
|
-
"exports": {
|
|
11
|
-
".": "./src/index.mjs",
|
|
12
|
-
"./engine": "./src/engine/index.mjs"
|
|
13
|
-
},
|
|
14
|
-
"files": [
|
|
15
|
-
"src",
|
|
16
|
-
"adapters",
|
|
17
|
-
"examples",
|
|
18
|
-
"!**/populace-report.json",
|
|
19
|
-
"!**/populace-report.html",
|
|
20
|
-
"!examples/
|
|
21
|
-
"populace.config.example.mjs",
|
|
22
|
-
"README.md",
|
|
23
|
-
"LICENSE",
|
|
24
|
-
"action.yml"
|
|
25
|
-
],
|
|
26
|
-
"scripts": {
|
|
27
|
-
"demo": "node src/cli.mjs demo",
|
|
28
|
-
"doctor": "node src/cli.mjs doctor",
|
|
29
|
-
"test": "node src/selftest.mjs",
|
|
30
|
-
"prepublishOnly": "node src/selftest.mjs"
|
|
31
|
-
},
|
|
32
|
-
"engines": {
|
|
33
|
-
"node": ">=18"
|
|
34
|
-
},
|
|
35
|
-
"keywords": [
|
|
36
|
-
"simulation",
|
|
37
|
-
"testing",
|
|
38
|
-
"load-testing",
|
|
39
|
-
"multi-user",
|
|
40
|
-
"synthetic-users",
|
|
41
|
-
"qa"
|
|
42
|
-
],
|
|
43
|
-
"license": "AGPL-3.0-only",
|
|
44
|
-
"dependencies": {},
|
|
45
|
-
"devDependencies": {
|
|
46
|
-
"@supabase/supabase-js": "^2.45.0"
|
|
47
|
-
},
|
|
48
|
-
"author": "Sankur Kundu <sankur.kundu.tw@gmail.com>",
|
|
49
|
-
"repository": {
|
|
50
|
-
"type": "git",
|
|
51
|
-
"url": "git+https://github.com/Shakhtar-Sankur/populace.git"
|
|
52
|
-
},
|
|
53
|
-
"homepage": "https://github.com/Shakhtar-Sankur/populace#readme",
|
|
54
|
-
"bugs": {
|
|
55
|
-
"url": "https://github.com/Shakhtar-Sankur/populace/issues"
|
|
56
|
-
},
|
|
57
|
-
"publishConfig": {
|
|
58
|
-
"access": "public"
|
|
59
|
-
}
|
|
60
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@gigzen/populace",
|
|
3
|
+
"version": "1.3.4",
|
|
4
|
+
"description": "A simulated population that uses your app through its real API, so you can test what needs more than one person.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"populace": "./src/cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"main": "./src/index.mjs",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/index.mjs",
|
|
12
|
+
"./engine": "./src/engine/index.mjs"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src",
|
|
16
|
+
"adapters",
|
|
17
|
+
"examples",
|
|
18
|
+
"!**/populace-report.json",
|
|
19
|
+
"!**/populace-report.html",
|
|
20
|
+
"!examples/waggle/run-test.ps1",
|
|
21
|
+
"populace.config.example.mjs",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE",
|
|
24
|
+
"action.yml"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"demo": "node src/cli.mjs demo",
|
|
28
|
+
"doctor": "node src/cli.mjs doctor",
|
|
29
|
+
"test": "node src/selftest.mjs",
|
|
30
|
+
"prepublishOnly": "node src/selftest.mjs"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"simulation",
|
|
37
|
+
"testing",
|
|
38
|
+
"load-testing",
|
|
39
|
+
"multi-user",
|
|
40
|
+
"synthetic-users",
|
|
41
|
+
"qa"
|
|
42
|
+
],
|
|
43
|
+
"license": "AGPL-3.0-only",
|
|
44
|
+
"dependencies": {},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@supabase/supabase-js": "^2.45.0"
|
|
47
|
+
},
|
|
48
|
+
"author": "Sankur Kundu <sankur.kundu.tw@gmail.com>",
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/Shakhtar-Sankur/populace.git"
|
|
52
|
+
},
|
|
53
|
+
"homepage": "https://github.com/Shakhtar-Sankur/populace#readme",
|
|
54
|
+
"bugs": {
|
|
55
|
+
"url": "https://github.com/Shakhtar-Sankur/populace/issues"
|
|
56
|
+
},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public"
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/html-report.mjs
CHANGED
|
@@ -50,6 +50,15 @@ export function renderHtmlReport(report) {
|
|
|
50
50
|
)
|
|
51
51
|
.join("");
|
|
52
52
|
|
|
53
|
+
// Implemented but never called. Kept apart from notTested because the fix is
|
|
54
|
+
// different: nothing to write, the run just has to reach it.
|
|
55
|
+
const notExercised = (r.coverage.notExercised || [])
|
|
56
|
+
.map(
|
|
57
|
+
(c) =>
|
|
58
|
+
`<li><span class="mono">${esc(c.method)}</span><span class="would">still untested: ${esc(c.wouldHaveTested)}</span></li>`,
|
|
59
|
+
)
|
|
60
|
+
.join("");
|
|
61
|
+
|
|
53
62
|
const problems = r.verdict.problems.map((p) => `<li>${esc(p)}</li>`).join("");
|
|
54
63
|
|
|
55
64
|
const engineBlock = engineBroke
|
|
@@ -170,7 +179,7 @@ ${engineBlock}
|
|
|
170
179
|
<div class="stat"><span class="v" style="color:${r.api.failures ? "var(--bad)" : "var(--ok)"}">${r.api.failures}</span><span class="l">failed</span></div>
|
|
171
180
|
<div class="stat"><span class="v">${pct(r.api.failureRate)}</span><span class="l">failure rate</span></div>
|
|
172
181
|
<div class="stat"><span class="v">${r.population.signedIn}</span><span class="l">concurrent users</span></div>
|
|
173
|
-
<div class="stat"><span class="v">${esc(r.coverage.label)}</span><span class="l">
|
|
182
|
+
<div class="stat"><span class="v">${esc(r.coverage.label)}</span><span class="l">methods exercised</span></div>
|
|
174
183
|
</div>
|
|
175
184
|
|
|
176
185
|
<section>
|
|
@@ -194,10 +203,19 @@ ${engineBlock}
|
|
|
194
203
|
</div>
|
|
195
204
|
</section>
|
|
196
205
|
|
|
206
|
+
${
|
|
207
|
+
notExercised
|
|
208
|
+
? `<section>
|
|
209
|
+
<h2>Not exercised — implemented, but this run never called it</h2>
|
|
210
|
+
<div class="panel"><ul class="cov">${notExercised}</ul></div>
|
|
211
|
+
</section>`
|
|
212
|
+
: ""
|
|
213
|
+
}
|
|
214
|
+
|
|
197
215
|
${
|
|
198
216
|
notTested
|
|
199
217
|
? `<section>
|
|
200
|
-
<h2>Not tested — adapter implements ${esc(r.coverage.label)}</h2>
|
|
218
|
+
<h2>Not tested — adapter implements ${esc(r.coverage.implementedLabel ?? r.coverage.label)}</h2>
|
|
201
219
|
<div class="panel"><ul class="cov">${notTested}</ul></div>
|
|
202
220
|
</section>`
|
|
203
221
|
: ""
|
package/src/report.mjs
CHANGED
|
@@ -66,15 +66,39 @@ export function buildReport({ config, adapter, world, metrics, teardown, started
|
|
|
66
66
|
groupJoins: totals.groups,
|
|
67
67
|
},
|
|
68
68
|
api,
|
|
69
|
-
coverage:
|
|
70
|
-
label: coverage.label,
|
|
71
|
-
implemented: coverage.implemented.map((c) => c.method),
|
|
72
|
-
notTested: coverage.missing.map((c) => ({ method: c.method, wouldHaveTested: c.exercises })),
|
|
73
|
-
},
|
|
69
|
+
coverage: coverageFor(coverage, api),
|
|
74
70
|
cleanup: teardown ?? { skipped: true, note: "Agents were left in place. Run `populace clean`." },
|
|
75
71
|
};
|
|
76
72
|
}
|
|
77
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Coverage counts what RAN, not what exists.
|
|
76
|
+
*
|
|
77
|
+
* It used to be the adapter's implemented count, so a three-minute run printed
|
|
78
|
+
* "Coverage was 13/13" having never once called refreshSession, which fires
|
|
79
|
+
* every thirty minutes. That is the same overclaim `isStub` exists to stop — a
|
|
80
|
+
* method credited as tested because it is there — just one step later.
|
|
81
|
+
*
|
|
82
|
+
* `label` is exercised/total. `implementedLabel` keeps what the adapter
|
|
83
|
+
* provides, because "not tested because you never wrote it" and "not tested
|
|
84
|
+
* because this run never reached it" have different fixes.
|
|
85
|
+
*/
|
|
86
|
+
function coverageFor(coverage, api) {
|
|
87
|
+
const total = coverage.implemented.length + coverage.missing.length;
|
|
88
|
+
const called = new Set((api.methods || []).filter((m) => m.calls > 0).map((m) => m.method));
|
|
89
|
+
const exercised = coverage.implemented.filter((c) => called.has(c.method));
|
|
90
|
+
return {
|
|
91
|
+
label: `${exercised.length}/${total}`,
|
|
92
|
+
implementedLabel: coverage.label,
|
|
93
|
+
implemented: coverage.implemented.map((c) => c.method),
|
|
94
|
+
exercised: exercised.map((c) => c.method),
|
|
95
|
+
notExercised: coverage.implemented
|
|
96
|
+
.filter((c) => !called.has(c.method))
|
|
97
|
+
.map((c) => ({ method: c.method, wouldHaveTested: c.exercises })),
|
|
98
|
+
notTested: coverage.missing.map((c) => ({ method: c.method, wouldHaveTested: c.exercises })),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
78
102
|
/**
|
|
79
103
|
* Deliberately conservative. Anything unproven is called unproven, never
|
|
80
104
|
* "passed" — including a run where nothing failed because nothing ran.
|
|
@@ -256,8 +280,16 @@ export function renderReport(report) {
|
|
|
256
280
|
}
|
|
257
281
|
}
|
|
258
282
|
|
|
283
|
+
if (report.coverage.notExercised?.length) {
|
|
284
|
+
L.push(` NOT EXERCISED — implemented, but this run never called it (coverage ${report.coverage.label})`);
|
|
285
|
+
for (const c of report.coverage.notExercised) {
|
|
286
|
+
L.push(` · ${c.method.padEnd(21)} still untested: ${c.wouldHaveTested}`);
|
|
287
|
+
}
|
|
288
|
+
L.push("");
|
|
289
|
+
}
|
|
290
|
+
|
|
259
291
|
if (report.coverage.notTested.length) {
|
|
260
|
-
L.push(` NOT TESTED — adapter implements ${report.coverage.label}`);
|
|
292
|
+
L.push(` NOT TESTED — adapter implements ${report.coverage.implementedLabel ?? report.coverage.label}`);
|
|
261
293
|
for (const c of report.coverage.notTested) {
|
|
262
294
|
L.push(` · ${c.method.padEnd(21)} would have tested ${c.wouldHaveTested}`);
|
|
263
295
|
}
|
package/src/selftest.mjs
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
} from "./instrument.mjs";
|
|
28
28
|
import { guardProduction } from "./config.mjs";
|
|
29
29
|
import { buildReport, renderReport } from "./report.mjs";
|
|
30
|
+
import { renderHtmlReport } from "./html-report.mjs";
|
|
30
31
|
import { canSignInOnly, CONTRACT_METHODS, coverageOf, isStub } from "./contract.mjs";
|
|
31
32
|
import { diagnose } from "./diagnose.mjs";
|
|
32
33
|
import { fill, match } from "./openapi.mjs";
|
|
@@ -348,6 +349,58 @@ check("skipped methods are listed with what they would have tested", () => {
|
|
|
348
349
|
assert.ok(skipped?.wouldHaveTested.length > 10, "a gap should say what it costs you");
|
|
349
350
|
});
|
|
350
351
|
|
|
352
|
+
// Coverage is what RAN. A hosted run on 12 September printed "Coverage was
|
|
353
|
+
// 13/13" having never called refreshSession, which fires every 30 minutes and
|
|
354
|
+
// the run lasted under five. Implemented is not exercised.
|
|
355
|
+
check("coverage counts methods that ran, not methods that exist", () => {
|
|
356
|
+
const ran = new Set(cleanReport.api.methods.filter((m) => m.calls > 0).map((m) => m.method));
|
|
357
|
+
const { coverage } = cleanReport;
|
|
358
|
+
for (const m of coverage.exercised) assert.ok(ran.has(m), `${m} counted as exercised with no calls`);
|
|
359
|
+
for (const c of coverage.notExercised) assert.ok(!ran.has(c.method), `${c.method} ran but is listed as not exercised`);
|
|
360
|
+
assert.equal(
|
|
361
|
+
coverage.exercised.length + coverage.notExercised.length,
|
|
362
|
+
coverage.implemented.length,
|
|
363
|
+
"every implemented method is either exercised or named as not",
|
|
364
|
+
);
|
|
365
|
+
assert.equal(coverage.label, `${coverage.exercised.length}/13`, "the headline label is the exercised count");
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// A first draft of the next check guarded itself with "if the adapter does not
|
|
369
|
+
// implement refreshSession, return" — and the in-memory adapter does not, so it
|
|
370
|
+
// passed without asserting anything. It now builds the exact case it is about:
|
|
371
|
+
// refreshSession implemented for real, and a run far too short to reach it.
|
|
372
|
+
const idleAdapter = {
|
|
373
|
+
...inMemoryAdapter(),
|
|
374
|
+
async refreshSession(user) {
|
|
375
|
+
await Promise.resolve();
|
|
376
|
+
return user;
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
const idleMetrics = createMetrics();
|
|
380
|
+
const idleWorld = new World({
|
|
381
|
+
adapter: instrument(idleAdapter, idleMetrics),
|
|
382
|
+
personas: buildPersonas(3, ["manila"]),
|
|
383
|
+
});
|
|
384
|
+
await idleWorld.populate({ staggerMs: 0 });
|
|
385
|
+
await idleWorld.run({ minutes: 1, tickSeconds: 5, realtime: false });
|
|
386
|
+
const idleTeardown = await idleWorld.teardown();
|
|
387
|
+
idleMetrics.endedAt = Date.now();
|
|
388
|
+
const idleReport = buildReport({
|
|
389
|
+
config, adapter: idleAdapter, world: idleWorld,
|
|
390
|
+
metrics: idleMetrics, teardown: idleTeardown, startedAt: Date.now() - 5000,
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
check("an implemented method that never ran is named, and not counted", () => {
|
|
394
|
+
const { coverage } = idleReport;
|
|
395
|
+
assert.ok(coverage.implemented.includes("refreshSession"), "setup: refreshSession must be implemented");
|
|
396
|
+
assert.ok(!coverage.exercised.includes("refreshSession"), "a one-minute run cannot reach a 30-minute refresh");
|
|
397
|
+
const idle = coverage.notExercised.find((c) => c.method === "refreshSession");
|
|
398
|
+
assert.ok(idle?.wouldHaveTested.length > 10, "a never-called method should say what stays untested");
|
|
399
|
+
assert.notEqual(coverage.label, coverage.implementedLabel, "exercised must not quietly equal implemented");
|
|
400
|
+
assert.ok(/NOT EXERCISED/.test(renderReport(idleReport)), "the terminal report must say so");
|
|
401
|
+
assert.ok(/Not exercised/.test(renderHtmlReport(idleReport)), "and so must the page people share");
|
|
402
|
+
});
|
|
403
|
+
|
|
351
404
|
// --- 4b. cleanup that does not write to the customer's database -----------
|
|
352
405
|
// clean reaches an account through createUser, which SIGNS UP when the identity
|
|
353
406
|
// is absent. On an already-clean environment that creates every simulated
|