@gigzen/populace 0.1.0 → 1.1.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 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
- ![Populace report for Buzz Buzz, 9 August 2026 — no failures across 400 API calls](docs/buzzbuzz-run-2026-08-09.svg)
30
+ ![Populace report for Buzz, 21 August 2026 — no failures across 932,455 API calls from 200 simulated drivers across 20 cities in 11 countries](docs/buzz-run-2026-08-21.svg)
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 Buzz** — a gig-worker platform
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 run repeated: the report at the top of this page.
101
- **No failures across 400 API calls.** Six accounts created, six accounts deleted,
102
- nothing left behind.
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
- **What this does not claim.** Six users for three minutes is a **correctness run,
105
- not a load test**, and it ran against that project while it was still empty. The
106
- latencies above are what six concurrent users saw and nothing more. Populace has
107
- been pointed at exactly one real backend so far, and that backend was ours — the
108
- next one should be someone else's.
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,217 @@ 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. What is sent, exactly: the method name, the error
272
+ message truncated to 500 characters, how many times it happened, and two
273
+ latency numbers. No URL, no credential, no persona, nothing about your
274
+ configuration. Note that the error message is your application's own text, so
275
+ it can carry a table or column name — read one before you enable this if that
276
+ matters to you. Explanations that came from the model are labelled as such.
277
+
278
+ **What is sent, exactly:** the method name, the error text, how many times it
279
+ happened, and that method's p50/p95. Nothing else — no target URL, no keys, no
280
+ tokens, no request bodies, no simulated users' content. Run with `--verbose` to
281
+ print it before it leaves.
282
+
283
+ ```
284
+ Sending to the model — this is everything, nothing else leaves this machine:
285
+ [ { "method": "post", "error": "quorum lost on shard 3 …",
286
+ "occurrences": 9, "p50ms": 40, "p95ms": 120 } ]
287
+ ```
288
+
289
+ The model layer can never break a run. No key, a rejected key, a rate limit, a
290
+ refusal, a malformed reply — each prints one line and falls back to the rule
291
+ output. `populace explain` is a separate command for the same reason: a run
292
+ should not wait on somebody else's network, and a report is complete without it.
293
+
294
+ ## Start from your API description
295
+
296
+ Writing the adapter is the slow part: thirteen methods against your endpoints.
297
+ If your API has an OpenAPI description, Populace can fill most of it in:
298
+
299
+ ```bash
300
+ populace init --from-openapi openapi.json
301
+ ```
302
+
303
+ It reads every operation, matches them against the thirteen contract methods by
304
+ path, verb and description, and writes the adapter with each confidence marked:
305
+
306
+ ```
307
+ Read 18 operations from openapi.json and matched 13 of 13.
308
+
309
+ ✔ createUser POST /auth/signup
310
+ ✔ like POST /posts/{postId}/likes
311
+ ? joinGroup POST /groups/{id}/members
312
+ low confidence — check this one
313
+ ✖ reportLocation left as the template default
314
+ ```
315
+
316
+ Path parameters become real template literals — `` `/posts/${postId}/likes` ``,
317
+ not the literal string — so the adapter runs rather than requesting a URL with
318
+ braces in it.
319
+
320
+ **This is a guess and the generated file says so.** Request bodies, field names
321
+ and response shapes are still the template's defaults. It removes the half hour
322
+ of looking endpoints up; it does not finish the job. Run `populace smoke` next —
323
+ it calls every method once and names the first one that is wrong.
324
+
325
+ A verb match alone is never enough to claim a method: a spec containing only
326
+ `/health` matches nothing, rather than guessing that a lone `GET` is your feed.
327
+
328
+ JSON only. A YAML parser would be Populace's first runtime dependency, and
329
+ `npx js-yaml openapi.yaml > openapi.json` covers it.
330
+
331
+ ## Run it in CI
332
+
333
+ Populace is a GitHub Action, so a population can run against your staging
334
+ environment on every pull request:
335
+
336
+ ```yaml
337
+ - uses: Shakhtar-Sankur/populace@v1
338
+ with:
339
+ agents: 30
340
+ minutes: 10
341
+ env:
342
+ MY_API_URL: ${{ secrets.STAGING_URL }}
343
+ MY_API_KEY: ${{ secrets.STAGING_KEY }}
344
+ ```
345
+
346
+ Your credentials stay in your repository's secrets and are read by your own
347
+ adapter. **Populace never sees them and nothing is sent anywhere** — the run
348
+ happens on your runner, against your server.
349
+
350
+ The job fails when the run is not clean, and the per-method table is written to
351
+ the job summary, so the result is visible in the pull request without
352
+ downloading an artifact.
353
+
354
+ | input | default | |
355
+ |---|---|---|
356
+ | `config` | `populace.config.mjs` | path to your config |
357
+ | `agents` `minutes` `tick` `engagement` `cities` | from the config | overrides |
358
+ | `report` | `populace-report.json` | the HTML lands beside it |
359
+ | `working-directory` | `.` | |
360
+ | `fail-on-problems` | `true` | set `false` to record without breaking the build |
361
+
362
+ Outputs `verdict`, `calls`, `api-failures`, `transport-failures`,
363
+ `report-json` and `report-html` for later steps:
364
+
365
+ ```yaml
366
+ - uses: actions/upload-artifact@v4
367
+ if: always()
368
+ with:
369
+ name: populace-report
370
+ path: ${{ steps.populace.outputs.report-html }}
371
+ ```
372
+
373
+ **`api-failures` is the number about your app.** `transport-failures` counts
374
+ calls that never reached your API at all — a flaky runner, not your code — and
375
+ Populace reports a run with any of those as *inconclusive* rather than clean,
376
+ because it cannot vouch for a call it could not make.
377
+
378
+ ## Staying current
379
+
380
+ ```bash
381
+ populace update
382
+ ```
383
+
384
+ Checks the npm registry and tells you if there is a newer release. A run mentions
385
+ it at most once a day, **after** the report rather than before it, and says
386
+ nothing at all if the registry cannot be reached — a version check is never worth
387
+ an error message at the end of a good run.
388
+
389
+ It is a plain anonymous GET to the public registry: no identifiers, no telemetry,
390
+ nothing about your app or your runs. Off entirely with `POPULACE_NO_UPDATE_CHECK=1`,
391
+ and off by default under `CI`, because a build server should not make an outbound
392
+ call nobody asked for.
393
+
173
394
  ## The safety guard
174
395
 
175
396
  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
@@ -1,4 +1,4 @@
1
- // Adapter: Buzz Buzz — a gig-driver tracking app on Supabase.
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
  /**
@@ -1,4 +1,4 @@
1
- // Populace's first customer: Buzz Buzz, a gig-driver tracking app.
1
+ // Populace's first customer: Buzz, a gig-driver tracking app.
2
2
  //
3
3
  // Run it — PowerShell (Windows):
4
4
  // $env:BUZZBUZZ_TEST_URL="https://<your-TEST-project>.supabase.co"
@@ -17,7 +17,7 @@
17
17
  // otherwise nobody can sign in after signing up.
18
18
 
19
19
  export default {
20
- app: "Buzz Buzz",
20
+ app: "Buzz",
21
21
  adapter: "../../adapters/buzzbuzz.mjs",
22
22
  environment: "test",
23
23
 
@@ -26,7 +26,7 @@ export default {
26
26
  key: process.env.BUZZBUZZ_TEST_KEY,
27
27
  },
28
28
 
29
- // Buzz Buzz's LIVE project. If BUZZBUZZ_TEST_URL is ever set to this by
29
+ // Buzz's LIVE project. If BUZZBUZZ_TEST_URL is ever set to this by
30
30
  // accident, Populace refuses to start. Real drivers must never see invented
31
31
  // people on their map.
32
32
  // Both of these are real. rqzuuvlougzhynckvqzd is the original project, which
@@ -0,0 +1,63 @@
1
+ // Buzz against the local Supabase stack, at a scale the hosted project refuses.
2
+ //
3
+ // Why this file exists rather than reusing examples/buzzbuzz:
4
+ //
5
+ // The hosted test project applies Supabase's default auth rate limit of 30 sign
6
+ // ups per five minutes per IP address. Measured on 2026-08-24: a 250-driver run
7
+ // signs in 35 people and every remaining identity fails with "Request rate limit
8
+ // reached". That limit is correct for real users, who each arrive from their own
9
+ // address. A load test is the one case where every request shares a single IP,
10
+ // so it is the one case the limit cannot accommodate.
11
+ //
12
+ // The local stack raises it in supabase/config.toml, which a hosted project
13
+ // cannot do from a file. So large runs belong here.
14
+ //
15
+ // Everything is self-contained: no environment variables. Studio passes a config
16
+ // path and nothing else, so a config that reads process.env works from a shell
17
+ // and silently targets `undefined` when launched from Explorer.
18
+ //
19
+ // Start the backend first, from i-want-to-make-one-app:
20
+ // npx supabase start
21
+ //
22
+ // Then either pick this file in Populace Studio, or:
23
+ // node src/cli.mjs run --config examples/buzzbuzz-local/populace.config.mjs
24
+
25
+ export default {
26
+ app: "Buzz",
27
+ adapter: "../../adapters/buzzbuzz.mjs",
28
+ environment: "test",
29
+
30
+ // The local stack's fixed development address and publishable key. These are
31
+ // the same on every machine that runs `supabase start` — they are not secrets
32
+ // and nothing outside this computer can reach them.
33
+ target: {
34
+ url: "http://127.0.0.1:54321",
35
+ key: "sb_publishable_ACJWlzQHlZjBrEguHvfOxg_3BJgxAaH",
36
+ },
37
+
38
+ // Kept identical to examples/buzzbuzz. A local run is no reason to relax the
39
+ // guard: if one of these ever appears in `target.url` above, refuse to start.
40
+ neverRunAgainst: [
41
+ "https://rqzuuvlougzhynckvqzd.supabase.co",
42
+ "https://ypdaetbeexyepswyhbui.supabase.co",
43
+ ],
44
+
45
+ population: {
46
+ agents: 250,
47
+ // The same twenty-five cities as the 200-driver run of 23 August, so head
48
+ // count is the only variable that changed between the two.
49
+ cities: [
50
+ "manila", "mumbai", "delhi", "jakarta", "saopaulo",
51
+ "mexicocity", "bogota", "lima", "lagos", "nairobi",
52
+ "accra", "cairo", "johannesburg", "istanbul", "dubai",
53
+ "riyadh", "moscow", "kyiv", "warsaw", "madrid",
54
+ "almaty", "tashkent", "baku", "casablanca", "amman",
55
+ ],
56
+ minutes: 20,
57
+ tickSeconds: 2,
58
+ engagement: 4,
59
+ },
60
+
61
+ identity: { phonePrefix: "0900" },
62
+ report: { path: "populace-report.json" },
63
+ };
@@ -29,9 +29,9 @@ test · 6 people · 124.7s
29
29
 
30
30
  ## Why this proves something
31
31
 
32
- The server disagrees with Buzz Buzz on every axis that could hide an assumption:
32
+ The server disagrees with Buzz on every axis that could hide an assumption:
33
33
 
34
- | | Buzz Buzz | this server |
34
+ | | Buzz | this server |
35
35
  |---|---|---|
36
36
  | transport | `supabase-js` client | plain `fetch` |
37
37
  | ids | UUID strings | **integers** |
@@ -11,11 +11,14 @@
11
11
  */
12
12
 
13
13
  export function createAdapter(target) {
14
+ if (!target?.url) {
15
+ throw new Error("No target URL. Set `target.url` in populace.config.mjs.");
16
+ }
14
17
  const base = target.url.replace(/\/$/, "");
15
18
 
16
19
  /**
17
20
  * The contract hands an adapter a name, a phone and a persona — never a
18
- * password. Choosing one is the adapter's job, exactly as the Buzz Buzz
21
+ * password. Choosing one is the adapter's job, exactly as the Buzz
19
22
  * reference adapter does, because only you know what your API will accept.
20
23
  *
21
24
  * A constant is right here: identities are deterministic so that a re-run
@@ -74,7 +77,7 @@ export function createAdapter(target) {
74
77
  body: { phone, password: PASSWORD, name },
75
78
  });
76
79
  // The handle is whatever later calls need. Here that is an integer id and
77
- // a token; for Buzz Buzz it was a UUID and a client object. The engine
80
+ // a token; for Buzz it was a UUID and a client object. The engine
78
81
  // only ever reads `.id`.
79
82
  return { id: out.id, token: out.token };
80
83
  },
@@ -6,9 +6,9 @@
6
6
  * real backend, and that backend was ours — a testing tool that has only tested
7
7
  * its author's own app has not been shown to be portable.
8
8
  *
9
- * So every architectural choice here disagrees with Buzz Buzz on purpose:
9
+ * So every architectural choice here disagrees with Buzz on purpose:
10
10
  *
11
- * Buzz Buzz / Supabase this server
11
+ * Buzz / Supabase this server
12
12
  * ──────────────────── ───────────────────────────────
13
13
  * supabase-js client plain fetch over HTTP
14
14
  * UUID string ids INTEGER ids
@@ -18,7 +18,7 @@
18
18
  * RLS refuses in the database the handler refuses in application code
19
19
  *
20
20
  * The bare id arrays matter most: that shape is what caught the engine reading
21
- * `target.id` off a string. If this file agreed with Buzz Buzz it would prove
21
+ * `target.id` off a string. If this file agreed with Buzz it would prove
22
22
  * nothing.
23
23
  *
24
24
  * Zero dependencies, in-memory, and it binds to 127.0.0.1 only — it is a test
@@ -196,7 +196,7 @@ const routes = {
196
196
  const thread = db.threads.find((t) => t.id === Number(body.threadId));
197
197
  if (!thread) return [404, { error: "no such thread" }];
198
198
  // Refused in application code rather than by a database policy — a
199
- // different enforcement point from Buzz Buzz, deliberately.
199
+ // different enforcement point from Buzz, deliberately.
200
200
  if (thread.a !== auth.user.id && thread.b !== auth.user.id) {
201
201
  return [403, { error: "not a participant in this thread" }];
202
202
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gigzen/populace",
3
- "version": "0.1.0",
3
+ "version": "1.1.0",
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
5
  "type": "module",
6
6
  "bin": {
@@ -15,9 +15,13 @@
15
15
  "src",
16
16
  "adapters",
17
17
  "examples",
18
+ "!**/populace-report.json",
19
+ "!**/populace-report.html",
20
+ "!examples/buzzbuzz/run-test.ps1",
18
21
  "populace.config.example.mjs",
19
22
  "README.md",
20
- "LICENSE"
23
+ "LICENSE",
24
+ "action.yml"
21
25
  ],
22
26
  "scripts": {
23
27
  "demo": "node src/cli.mjs demo",