@gigzen/populace 0.1.0 → 1.0.0
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 +227 -10
- package/action.yml +145 -0
- package/adapters/buzzbuzz.mjs +1 -1
- package/adapters/template-rest.mjs +16 -0
- package/examples/buzzbuzz/populace-report.html +69 -60
- package/examples/buzzbuzz/populace-report.json +97 -79
- package/examples/buzzbuzz/populace.config.mjs +3 -3
- package/examples/buzzbuzz/run-test.ps1 +1 -1
- package/examples/rest-api/README.md +2 -2
- package/examples/rest-api/adapter.mjs +5 -2
- package/examples/rest-api/populace-report.html +245 -0
- package/examples/rest-api/populace-report.json +280 -0
- package/examples/rest-api/server.mjs +4 -4
- package/package.json +3 -2
- package/src/ai.mjs +158 -0
- package/src/cli.mjs +139 -2
- package/src/engine/personas.mjs +654 -11
- package/src/engine/world.mjs +3 -3
- package/src/explain.mjs +247 -0
- package/src/github-summary.mjs +152 -0
- package/src/openapi.mjs +322 -0
- package/src/report.mjs +37 -0
- package/src/selftest.mjs +251 -0
- package/src/update.mjs +141 -0
package/README.md
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
**A simulated population that uses your app through its real API — so you can
|
|
4
4
|
test what needs more than one person.**
|
|
5
5
|
|
|
6
|
+
```bash
|
|
7
|
+
npm install -g @gigzen/populace
|
|
8
|
+
populace demo
|
|
9
|
+
```
|
|
10
|
+
|
|
6
11
|
**Site:** https://shakhtar-sankur.github.io/populace/ ·
|
|
7
12
|
**Test report:** [the full engineering record](https://shakhtar-sankur.github.io/gigzen/test-report.html)
|
|
8
13
|
· A [Gigzen](https://shakhtar-sankur.github.io/gigzen/) product
|
|
@@ -22,13 +27,13 @@ applying**. Then it hands you a report on what broke.
|
|
|
22
27
|
|
|
23
28
|
## It has done this to a real, finished app
|
|
24
29
|
|
|
25
|
-

|
|
26
31
|
|
|
27
32
|
That is the *second* run. The first one is the interesting one.
|
|
28
33
|
|
|
29
34
|
### What happened, in plain English
|
|
30
35
|
|
|
31
|
-
On **9 August 2026** we pointed Populace at **Buzz
|
|
36
|
+
On **9 August 2026** we pointed Populace at **Buzz** — a gig-worker platform
|
|
32
37
|
on Android with a live Postgres backend, 17 tables and 48 row-level-security
|
|
33
38
|
policies. It was finished. It was signed. It had been through a full manual test
|
|
34
39
|
of every screen by the person who wrote it, and it had passed.
|
|
@@ -97,15 +102,20 @@ within seconds to six users signing up at once.
|
|
|
97
102
|
|
|
98
103
|
### After the fixes
|
|
99
104
|
|
|
100
|
-
All five were fixed and the
|
|
101
|
-
|
|
102
|
-
|
|
105
|
+
All five were fixed, and the runs since have grown by three orders of magnitude.
|
|
106
|
+
The report at the top of this page is the largest that was **clean end to end**:
|
|
107
|
+
**no failures across 932,455 API calls**, 200 concurrent drivers over 20 cities in
|
|
108
|
+
11 countries, 200 accounts created and 200 removed.
|
|
103
109
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
110
|
+
A later run put **300 drivers through 1,401,435 calls with zero API failures** — but
|
|
111
|
+
one call never reached the server, a socket exhausted on the test machine, so
|
|
112
|
+
Populace marked it *inconclusive* rather than clean. That distinction is the point:
|
|
113
|
+
a verdict that is never withheld is worth nothing when it is given.
|
|
114
|
+
|
|
115
|
+
**What this does not claim.** These runs are against a **local backend over
|
|
116
|
+
loopback**, so no network is in the latencies — the same calls cost about 175 ms
|
|
117
|
+
against a hosted project. Three hundred drivers is where throughput stops scaling,
|
|
118
|
+
not where the app breaks; that is still unfound.
|
|
109
119
|
|
|
110
120
|
---
|
|
111
121
|
|
|
@@ -170,6 +180,213 @@ your API for what were really expired tokens — and the run cannot even delete
|
|
|
170
180
|
its own accounts, stranding simulated users in your environment. See it happen:
|
|
171
181
|
`node examples/token-expiry/expiry-demo.mjs`
|
|
172
182
|
|
|
183
|
+
|
|
184
|
+
### And to an app we did not write
|
|
185
|
+
|
|
186
|
+
Pointed at a local [Gitea](https://gitea.io) instance — a git forge, not a social
|
|
187
|
+
app, and nobody here has touched its source:
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
POPULACE REPORT — Gitea
|
|
191
|
+
✔ No failures across 1106 API calls.
|
|
192
|
+
|
|
193
|
+
recentPostsByOthers 382 0 364ms 879ms
|
|
194
|
+
like 382 0 186ms 520ms
|
|
195
|
+
post 173 0 575ms 1.2s
|
|
196
|
+
comment 139 0 405ms 771ms
|
|
197
|
+
createUser 10 0 116ms 183ms
|
|
198
|
+
setProfile 10 0 59ms 77ms
|
|
199
|
+
deleteUser 10 0 336ms 410ms
|
|
200
|
+
|
|
201
|
+
NOT TESTED — adapter implements 8/13
|
|
202
|
+
✔ Cleanup complete — 10 accounts removed.
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Gitea has no location tracking, no direct messages and no group joins, so five
|
|
206
|
+
methods have no equivalent. They are **absent from the adapter rather than
|
|
207
|
+
stubbed**, and the report says which and what each would have covered — because
|
|
208
|
+
a stub returning a fake success turns "we did not test this" into "this works".
|
|
209
|
+
Posting is opening an issue; the feed is listing other people's; liking is a
|
|
210
|
+
reaction.
|
|
211
|
+
|
|
212
|
+
Ten accounts created through Gitea's own signup, driven under Gitea's own
|
|
213
|
+
permissions with per-user tokens, and ten removed — checked inside Gitea
|
|
214
|
+
afterwards, not taken from this tool's own report.
|
|
215
|
+
|
|
216
|
+
**It found five defects on the way in, all of them in Populace.** Gitea's OpenAPI
|
|
217
|
+
description has 482 operations against the 18 in the fixture the adapter
|
|
218
|
+
generator was built on, and at that scale it mismatched four methods whose
|
|
219
|
+
correct endpoint was right there in the spec: it summed path-word hits instead of
|
|
220
|
+
taking the best, penalised path depth too weakly, matched `"registration"` inside
|
|
221
|
+
a CI-runner token endpoint, and — the good one — matched `"dm"` inside
|
|
222
|
+
`"admin"`. All fixed; the confidence markers are what stopped the wrong guesses
|
|
223
|
+
from looking finished.
|
|
224
|
+
|
|
225
|
+
## It tells you what to do about it
|
|
226
|
+
|
|
227
|
+
A report that says `duplicate key value violates unique constraint` has told you
|
|
228
|
+
what happened and nothing about what to do. Every run now ends with the part
|
|
229
|
+
that matters:
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
WHAT TO DO
|
|
233
|
+
12 failures in the application.
|
|
234
|
+
|
|
235
|
+
[YOUR APP] like × 12
|
|
236
|
+
The database refused the write on permissions, not on data.
|
|
237
|
+
A row-level-security policy rejected this call for the signed-in role.
|
|
238
|
+
The request was well formed; the policy did not allow it. This is
|
|
239
|
+
invisible to whoever wrote the policy, because their own account
|
|
240
|
+
usually satisfies it.
|
|
241
|
+
Fix:
|
|
242
|
+
Check the policy for this table against the role the app
|
|
243
|
+
authenticates as, and confirm there is a policy for this specific
|
|
244
|
+
command — a table with only a SELECT policy refuses every INSERT.
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**The first judgement is the one that matters: was this your application at
|
|
248
|
+
all?** A socket that never opened and a 500 look equally red in a terminal and
|
|
249
|
+
mean completely different things. Every failure is attributed to your app, the
|
|
250
|
+
platform, or the test client, and the summary line says so before any detail:
|
|
251
|
+
|
|
252
|
+
```
|
|
253
|
+
No application failures — 21 never reached the server.
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
A failure Populace cannot recognise is reported as unrecognised. It is never
|
|
257
|
+
counted as "not your app", because that would turn *we could not tell* into an
|
|
258
|
+
all-clear.
|
|
259
|
+
|
|
260
|
+
### When no rule fits
|
|
261
|
+
|
|
262
|
+
Rules cover the shapes a simulated population usually provokes. For anything
|
|
263
|
+
else, `populace explain` can ask a model:
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
267
|
+
populace explain
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Rules run first and the model only ever sees what they could not name — so most
|
|
271
|
+
runs never call it at all, and the ones that do send a handful of short strings.
|
|
272
|
+
Explanations that came from the model are labelled as such.
|
|
273
|
+
|
|
274
|
+
**What is sent, exactly:** the method name, the error text, how many times it
|
|
275
|
+
happened, and that method's p50/p95. Nothing else — no target URL, no keys, no
|
|
276
|
+
tokens, no request bodies, no simulated users' content. Run with `--verbose` to
|
|
277
|
+
print it before it leaves.
|
|
278
|
+
|
|
279
|
+
```
|
|
280
|
+
Sending to the model — this is everything, nothing else leaves this machine:
|
|
281
|
+
[ { "method": "post", "error": "quorum lost on shard 3 …",
|
|
282
|
+
"occurrences": 9, "p50ms": 40, "p95ms": 120 } ]
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
The model layer can never break a run. No key, a rejected key, a rate limit, a
|
|
286
|
+
refusal, a malformed reply — each prints one line and falls back to the rule
|
|
287
|
+
output. `populace explain` is a separate command for the same reason: a run
|
|
288
|
+
should not wait on somebody else's network, and a report is complete without it.
|
|
289
|
+
|
|
290
|
+
## Start from your API description
|
|
291
|
+
|
|
292
|
+
Writing the adapter is the slow part: thirteen methods against your endpoints.
|
|
293
|
+
If your API has an OpenAPI description, Populace can fill most of it in:
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
populace init --from-openapi openapi.json
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
It reads every operation, matches them against the thirteen contract methods by
|
|
300
|
+
path, verb and description, and writes the adapter with each confidence marked:
|
|
301
|
+
|
|
302
|
+
```
|
|
303
|
+
Read 18 operations from openapi.json and matched 13 of 13.
|
|
304
|
+
|
|
305
|
+
✔ createUser POST /auth/signup
|
|
306
|
+
✔ like POST /posts/{postId}/likes
|
|
307
|
+
? joinGroup POST /groups/{id}/members
|
|
308
|
+
low confidence — check this one
|
|
309
|
+
✖ reportLocation left as the template default
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Path parameters become real template literals — `` `/posts/${postId}/likes` ``,
|
|
313
|
+
not the literal string — so the adapter runs rather than requesting a URL with
|
|
314
|
+
braces in it.
|
|
315
|
+
|
|
316
|
+
**This is a guess and the generated file says so.** Request bodies, field names
|
|
317
|
+
and response shapes are still the template's defaults. It removes the half hour
|
|
318
|
+
of looking endpoints up; it does not finish the job. Run `populace smoke` next —
|
|
319
|
+
it calls every method once and names the first one that is wrong.
|
|
320
|
+
|
|
321
|
+
A verb match alone is never enough to claim a method: a spec containing only
|
|
322
|
+
`/health` matches nothing, rather than guessing that a lone `GET` is your feed.
|
|
323
|
+
|
|
324
|
+
JSON only. A YAML parser would be Populace's first runtime dependency, and
|
|
325
|
+
`npx js-yaml openapi.yaml > openapi.json` covers it.
|
|
326
|
+
|
|
327
|
+
## Run it in CI
|
|
328
|
+
|
|
329
|
+
Populace is a GitHub Action, so a population can run against your staging
|
|
330
|
+
environment on every pull request:
|
|
331
|
+
|
|
332
|
+
```yaml
|
|
333
|
+
- uses: Shakhtar-Sankur/populace@v1
|
|
334
|
+
with:
|
|
335
|
+
agents: 30
|
|
336
|
+
minutes: 10
|
|
337
|
+
env:
|
|
338
|
+
MY_API_URL: ${{ secrets.STAGING_URL }}
|
|
339
|
+
MY_API_KEY: ${{ secrets.STAGING_KEY }}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Your credentials stay in your repository's secrets and are read by your own
|
|
343
|
+
adapter. **Populace never sees them and nothing is sent anywhere** — the run
|
|
344
|
+
happens on your runner, against your server.
|
|
345
|
+
|
|
346
|
+
The job fails when the run is not clean, and the per-method table is written to
|
|
347
|
+
the job summary, so the result is visible in the pull request without
|
|
348
|
+
downloading an artifact.
|
|
349
|
+
|
|
350
|
+
| input | default | |
|
|
351
|
+
|---|---|---|
|
|
352
|
+
| `config` | `populace.config.mjs` | path to your config |
|
|
353
|
+
| `agents` `minutes` `tick` `engagement` `cities` | from the config | overrides |
|
|
354
|
+
| `report` | `populace-report.json` | the HTML lands beside it |
|
|
355
|
+
| `working-directory` | `.` | |
|
|
356
|
+
| `fail-on-problems` | `true` | set `false` to record without breaking the build |
|
|
357
|
+
|
|
358
|
+
Outputs `verdict`, `calls`, `api-failures`, `transport-failures`,
|
|
359
|
+
`report-json` and `report-html` for later steps:
|
|
360
|
+
|
|
361
|
+
```yaml
|
|
362
|
+
- uses: actions/upload-artifact@v4
|
|
363
|
+
if: always()
|
|
364
|
+
with:
|
|
365
|
+
name: populace-report
|
|
366
|
+
path: ${{ steps.populace.outputs.report-html }}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
**`api-failures` is the number about your app.** `transport-failures` counts
|
|
370
|
+
calls that never reached your API at all — a flaky runner, not your code — and
|
|
371
|
+
Populace reports a run with any of those as *inconclusive* rather than clean,
|
|
372
|
+
because it cannot vouch for a call it could not make.
|
|
373
|
+
|
|
374
|
+
## Staying current
|
|
375
|
+
|
|
376
|
+
```bash
|
|
377
|
+
populace update
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Checks the npm registry and tells you if there is a newer release. A run mentions
|
|
381
|
+
it at most once a day, **after** the report rather than before it, and says
|
|
382
|
+
nothing at all if the registry cannot be reached — a version check is never worth
|
|
383
|
+
an error message at the end of a good run.
|
|
384
|
+
|
|
385
|
+
It is a plain anonymous GET to the public registry: no identifiers, no telemetry,
|
|
386
|
+
nothing about your app or your runs. Off entirely with `POPULACE_NO_UPDATE_CHECK=1`,
|
|
387
|
+
and off by default under `CI`, because a build server should not make an outbound
|
|
388
|
+
call nobody asked for.
|
|
389
|
+
|
|
173
390
|
## The safety guard
|
|
174
391
|
|
|
175
392
|
Populace creates real accounts and writes real rows. Pointed at production it
|
package/action.yml
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
name: Populace
|
|
2
|
+
description: Bring a population of simulated users to life against your app's own API, and report what broke.
|
|
3
|
+
author: Gigzen
|
|
4
|
+
|
|
5
|
+
branding:
|
|
6
|
+
icon: users
|
|
7
|
+
color: green
|
|
8
|
+
|
|
9
|
+
# Why a composite action rather than a Docker or JavaScript one.
|
|
10
|
+
#
|
|
11
|
+
# Docker would add a build and a pull to every job, for a tool whose entire
|
|
12
|
+
# claim is that it has nothing to install. A JavaScript action would need
|
|
13
|
+
# `@actions/core` bundled in, which is a runtime dependency in a package that
|
|
14
|
+
# advertises zero. Composite keeps the promise: this runs the same
|
|
15
|
+
# `node src/cli.mjs` a person runs locally, and nothing else.
|
|
16
|
+
|
|
17
|
+
inputs:
|
|
18
|
+
config:
|
|
19
|
+
description: Path to populace.config.mjs, relative to working-directory.
|
|
20
|
+
required: false
|
|
21
|
+
default: populace.config.mjs
|
|
22
|
+
agents:
|
|
23
|
+
description: How many simulated users. Overrides the config.
|
|
24
|
+
required: false
|
|
25
|
+
minutes:
|
|
26
|
+
description: How long the population runs for. Overrides the config.
|
|
27
|
+
required: false
|
|
28
|
+
tick:
|
|
29
|
+
description: Simulated seconds per step. Lower means each person decides more often.
|
|
30
|
+
required: false
|
|
31
|
+
engagement:
|
|
32
|
+
description: How busy people are. 1 is normal, 5 is relentless.
|
|
33
|
+
required: false
|
|
34
|
+
cities:
|
|
35
|
+
description: Comma-separated city keys. Overrides the config.
|
|
36
|
+
required: false
|
|
37
|
+
report:
|
|
38
|
+
description: Where to write the JSON report. The HTML lands beside it.
|
|
39
|
+
required: false
|
|
40
|
+
default: populace-report.json
|
|
41
|
+
working-directory:
|
|
42
|
+
description: Directory to run in. Defaults to the repository root.
|
|
43
|
+
required: false
|
|
44
|
+
default: "."
|
|
45
|
+
fail-on-problems:
|
|
46
|
+
description: >-
|
|
47
|
+
Fail the job when the run is not clean. Set to false to record the result
|
|
48
|
+
without breaking the build - useful while you are still wiring an adapter up.
|
|
49
|
+
required: false
|
|
50
|
+
default: "true"
|
|
51
|
+
summary:
|
|
52
|
+
description: Write the per-method table to the job summary page.
|
|
53
|
+
required: false
|
|
54
|
+
default: "true"
|
|
55
|
+
|
|
56
|
+
outputs:
|
|
57
|
+
verdict:
|
|
58
|
+
description: clean, problems-found, or inconclusive.
|
|
59
|
+
value: ${{ steps.report.outputs.verdict }}
|
|
60
|
+
calls:
|
|
61
|
+
description: Total API calls made.
|
|
62
|
+
value: ${{ steps.report.outputs.calls }}
|
|
63
|
+
api-failures:
|
|
64
|
+
description: Calls your API answered with an error. This is the number about your app.
|
|
65
|
+
value: ${{ steps.report.outputs.api-failures }}
|
|
66
|
+
transport-failures:
|
|
67
|
+
description: Calls that never reached your API. These say nothing about your app.
|
|
68
|
+
value: ${{ steps.report.outputs.transport-failures }}
|
|
69
|
+
report-json:
|
|
70
|
+
description: Path to the JSON report.
|
|
71
|
+
value: ${{ steps.report.outputs.report-json }}
|
|
72
|
+
report-html:
|
|
73
|
+
description: Path to the self-contained HTML report.
|
|
74
|
+
value: ${{ steps.report.outputs.report-html }}
|
|
75
|
+
|
|
76
|
+
runs:
|
|
77
|
+
using: composite
|
|
78
|
+
steps:
|
|
79
|
+
- name: Run the population
|
|
80
|
+
id: run
|
|
81
|
+
shell: bash
|
|
82
|
+
working-directory: ${{ inputs.working-directory }}
|
|
83
|
+
env:
|
|
84
|
+
POPULACE_CONFIG: ${{ inputs.config }}
|
|
85
|
+
POPULACE_AGENTS: ${{ inputs.agents }}
|
|
86
|
+
POPULACE_MINUTES: ${{ inputs.minutes }}
|
|
87
|
+
POPULACE_TICK: ${{ inputs.tick }}
|
|
88
|
+
POPULACE_ENGAGEMENT: ${{ inputs.engagement }}
|
|
89
|
+
POPULACE_CITIES: ${{ inputs.cities }}
|
|
90
|
+
POPULACE_REPORT: ${{ inputs.report }}
|
|
91
|
+
run: |
|
|
92
|
+
# Inputs arrive through the environment rather than being interpolated
|
|
93
|
+
# into this script. A city list or a path is attacker-controllable in a
|
|
94
|
+
# fork PR, and ${{ }} pasted straight into bash is a shell injection.
|
|
95
|
+
args=(run --config "$POPULACE_CONFIG" --report "$POPULACE_REPORT")
|
|
96
|
+
[ -n "$POPULACE_AGENTS" ] && args+=(--agents "$POPULACE_AGENTS")
|
|
97
|
+
[ -n "$POPULACE_MINUTES" ] && args+=(--minutes "$POPULACE_MINUTES")
|
|
98
|
+
[ -n "$POPULACE_TICK" ] && args+=(--tick "$POPULACE_TICK")
|
|
99
|
+
[ -n "$POPULACE_ENGAGEMENT" ] && args+=(--engagement "$POPULACE_ENGAGEMENT")
|
|
100
|
+
[ -n "$POPULACE_CITIES" ] && args+=(--cities "$POPULACE_CITIES")
|
|
101
|
+
|
|
102
|
+
set +e
|
|
103
|
+
node "$GITHUB_ACTION_PATH/src/cli.mjs" "${args[@]}"
|
|
104
|
+
code=$?
|
|
105
|
+
set -e
|
|
106
|
+
echo "exit-code=$code" >> "$GITHUB_OUTPUT"
|
|
107
|
+
echo "Populace exited $code"
|
|
108
|
+
|
|
109
|
+
- name: Read the report
|
|
110
|
+
id: report
|
|
111
|
+
if: always()
|
|
112
|
+
shell: bash
|
|
113
|
+
working-directory: ${{ inputs.working-directory }}
|
|
114
|
+
env:
|
|
115
|
+
POPULACE_REPORT: ${{ inputs.report }}
|
|
116
|
+
POPULACE_SUMMARY: ${{ inputs.summary }}
|
|
117
|
+
run: node "$GITHUB_ACTION_PATH/src/github-summary.mjs"
|
|
118
|
+
|
|
119
|
+
- name: Decide the job
|
|
120
|
+
if: always()
|
|
121
|
+
shell: bash
|
|
122
|
+
env:
|
|
123
|
+
EXIT_CODE: ${{ steps.run.outputs.exit-code }}
|
|
124
|
+
VERDICT: ${{ steps.report.outputs.verdict }}
|
|
125
|
+
API_FAILURES: ${{ steps.report.outputs.api-failures }}
|
|
126
|
+
FAIL_ON_PROBLEMS: ${{ inputs.fail-on-problems }}
|
|
127
|
+
run: |
|
|
128
|
+
if [ "$EXIT_CODE" = "0" ]; then
|
|
129
|
+
echo "Clean run."
|
|
130
|
+
exit 0
|
|
131
|
+
fi
|
|
132
|
+
if [ "$FAIL_ON_PROBLEMS" != "true" ]; then
|
|
133
|
+
echo "::warning::Populace reported '$VERDICT' ($API_FAILURES API failures), but fail-on-problems is false."
|
|
134
|
+
exit 0
|
|
135
|
+
fi
|
|
136
|
+
# An inconclusive run is called out separately: it means Populace could
|
|
137
|
+
# not make every call, which is usually the runner's network rather than
|
|
138
|
+
# the app under test. Saying so here saves someone hunting a bug that is
|
|
139
|
+
# not in their code.
|
|
140
|
+
if [ "$VERDICT" = "inconclusive" ]; then
|
|
141
|
+
echo "::error::Populace could not complete the run: $API_FAILURES API failures, but some calls never reached your API. This is usually the network between the runner and your server, not your app. See the job summary."
|
|
142
|
+
else
|
|
143
|
+
echo "::error::Populace found problems: $API_FAILURES API failures. See the job summary."
|
|
144
|
+
fi
|
|
145
|
+
exit 1
|
package/adapters/buzzbuzz.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Adapter: Buzz
|
|
1
|
+
// Adapter: Buzz — a gig-driver tracking app on Supabase.
|
|
2
2
|
//
|
|
3
3
|
// Populace's first customer, and the reference implementation. Read this
|
|
4
4
|
// alongside contract.md to see what a complete adapter looks like: it is ~140
|
|
@@ -19,6 +19,22 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
export function createAdapter(target, config) {
|
|
22
|
+
/**
|
|
23
|
+
* Fail with a sentence, not a TypeError.
|
|
24
|
+
*
|
|
25
|
+
* A fresh scaffold has `url: process.env.MY_APP_TEST_URL`, which is
|
|
26
|
+
* undefined until you set it — so the very first `populace doctor` a new
|
|
27
|
+
* user runs went straight into `target.url.replace(...)` and printed
|
|
28
|
+
* "Cannot read properties of undefined (reading 'replace')". Technically a
|
|
29
|
+
* correct refusal, and a terrible first five minutes.
|
|
30
|
+
*/
|
|
31
|
+
if (!target?.url) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
"No target URL. Set `target.url` in populace.config.mjs, or export the " +
|
|
34
|
+
"environment variable it reads. It must point at your TEST environment.",
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
22
38
|
const base = target.url.replace(/\/$/, "");
|
|
23
39
|
|
|
24
40
|
/**
|