@gfdelarue/testkit 0.6.0 → 0.6.1

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.
Files changed (156) hide show
  1. package/README.md +455 -56
  2. package/bin/testkit-windows-owner-arm64.exe +0 -0
  3. package/bin/testkit-windows-owner-x64.exe +0 -0
  4. package/dist/app/bugs/batch.js +1 -0
  5. package/dist/app/bugs/ci-report.js +2 -2
  6. package/dist/app/bugs/ci-summary.js +3 -3
  7. package/dist/app/bugs/draft.js +2 -2
  8. package/dist/app/bugs/file.js +1 -1
  9. package/dist/app/bugs/guide.js +1 -1
  10. package/dist/app/bugs/record.js +1 -1
  11. package/dist/app/bugs/run-reading.js +1 -1
  12. package/dist/app/bugs/store.js +1 -1
  13. package/dist/app/campaigns/summary.js +1 -1
  14. package/dist/app/cleanup/index.js +1 -1
  15. package/dist/app/db-schema/prepare.js +1 -1
  16. package/dist/app/db-schema/refresh.js +1 -1
  17. package/dist/app/db-schema/verify.js +1 -1
  18. package/dist/app/destroy/index.js +1 -1
  19. package/dist/app/environments/config-driven.js +1 -1
  20. package/dist/app/environments/file-setup.js +1 -1
  21. package/dist/app/environments/routing.js +1 -1
  22. package/dist/app/environments/static-values.js +1 -1
  23. package/dist/app/environments/supplied.js +1 -1
  24. package/dist/app/lint/shared.js +1 -1
  25. package/dist/app/run/live-run.js +1 -1
  26. package/dist/app/run/orchestrator.js +1 -1
  27. package/dist/app/run/planning.js +1 -1
  28. package/dist/app/run/results.js +1 -1
  29. package/dist/app/run/run-finalization.js +1 -1
  30. package/dist/app/run/run-finisher.js +1 -0
  31. package/dist/app/run/setup/runtime-node-setup-protocol.js +1 -1
  32. package/dist/app/run/setup/runtime-node-setup-worker.js +1 -1
  33. package/dist/app/run/setup/runtime-node-setup.js +2 -2
  34. package/dist/app/status/runs.js +1 -1
  35. package/dist/app/stop/index.js +1 -1
  36. package/dist/app/tracker/index.js +1 -1
  37. package/dist/app/tracker/token.js +2 -2
  38. package/dist/app/typecheck/index.js +4 -4
  39. package/dist/cli/command-catalog.js +1 -1
  40. package/dist/cli/command-flags.js +1 -1
  41. package/dist/cli/commands/bug/file.js +3 -2
  42. package/dist/cli/framework/help.js +5 -5
  43. package/dist/cli/framework/parser.js +1 -1
  44. package/dist/cli/json-errors.js +1 -1
  45. package/dist/cli/renderers/bug/text.js +1 -1
  46. package/dist/cli/renderers/run/formatting.js +1 -1
  47. package/dist/config/loader/config-loader.js +1 -1
  48. package/dist/config/loader/discover-tests.js +1 -1
  49. package/dist/config/loader/discovery-result.js +1 -0
  50. package/dist/config/loader/index.js +1 -1
  51. package/dist/config/loader/runtime-isolation.js +1 -1
  52. package/dist/config/loader/skip-config.js +1 -1
  53. package/dist/config/loader/test-selection.js +1 -1
  54. package/dist/database/admin.js +5 -2
  55. package/dist/database/call-caps.js +1 -0
  56. package/dist/database/cleanup.js +1 -1
  57. package/dist/database/configured-steps.js +1 -1
  58. package/dist/database/constants.js +1 -1
  59. package/dist/database/docker-endpoint.js +1 -0
  60. package/dist/database/docker-metadata.js +2 -2
  61. package/dist/database/docker.js +5 -5
  62. package/dist/database/index.js +2 -1
  63. package/dist/database/lifecycle-cleanup.js +1 -1
  64. package/dist/database/local-postgres.js +4 -1
  65. package/dist/database/naming.js +1 -1
  66. package/dist/database/operation-deadline.js +1 -0
  67. package/dist/database/postgres/connection-check.js +1 -1
  68. package/dist/database/postgres/index.js +2 -2
  69. package/dist/database/postgres/owned-exec.js +27 -0
  70. package/dist/database/postgres/protocol.js +2 -2
  71. package/dist/database/provider.js +1 -0
  72. package/dist/database/roles.js +2 -2
  73. package/dist/database/schema-operations.js +5 -1
  74. package/dist/database/schema-source.js +3 -3
  75. package/dist/database/steps.js +1 -1
  76. package/dist/database/template-steps.js +4 -4
  77. package/dist/domain/suite-selection.js +1 -1
  78. package/dist/engines/adapters/evidence-files.js +1 -1
  79. package/dist/engines/adapters/execution.js +1 -1
  80. package/dist/engines/node-vitest/adapter/adapter.js +1 -1
  81. package/dist/engines/node-vitest/adapter/driver.js +2 -2
  82. package/dist/engines/node-vitest/runtime/runtime-input.js +1 -1
  83. package/dist/engines/playwright/adapter/adapter.js +1 -1
  84. package/dist/engines/playwright/adapter/browser-network-evidence.js +4 -0
  85. package/dist/engines/playwright/adapter/config.js +16 -6
  86. package/dist/engines/playwright/adapter/evidence.js +2 -1
  87. package/dist/engines/playwright/adapter/failure-details.js +2 -2
  88. package/dist/engines/playwright/adapter/reporter.js +1 -1
  89. package/dist/engines/playwright/runtime/browser-network.js +1 -0
  90. package/dist/engines/playwright/runtime/browser-traffic.js +1 -0
  91. package/dist/engines/playwright/runtime/chromium-environment.js +1 -0
  92. package/dist/engines/playwright/runtime/emulation.js +1 -0
  93. package/dist/engines/playwright/runtime/index.js +1 -1
  94. package/dist/engines/playwright/runtime/managed-test.js +1 -1
  95. package/dist/engines/playwright/runtime/runtime-input.js +2 -2
  96. package/dist/environment/lifecycle-controller.js +1 -1
  97. package/dist/environment/lifecycle.js +1 -1
  98. package/dist/environment/maintenance.js +1 -1
  99. package/dist/environment/run-environment.js +1 -1
  100. package/dist/environment/run-stopping.js +1 -1
  101. package/dist/environment/runtime-manager.js +1 -1
  102. package/dist/evidence/artifacts.js +5 -2
  103. package/dist/evidence/bundles.js +2 -1
  104. package/dist/evidence/compressed-artifacts.js +1 -0
  105. package/dist/evidence/environment-record.js +1 -1
  106. package/dist/evidence/index.js +1 -1
  107. package/dist/evidence/live-run-summary.js +1 -0
  108. package/dist/evidence/local-paths.js +1 -0
  109. package/dist/evidence/maintenance.js +1 -1
  110. package/dist/evidence/operation-timings.js +1 -1
  111. package/dist/evidence/redaction.js +4 -4
  112. package/dist/evidence/run-evidence.js +1 -0
  113. package/dist/evidence/setup-progress.js +1 -1
  114. package/dist/evidence/storage-inventory.js +1 -1
  115. package/dist/evidence/storage.js +2 -2
  116. package/dist/kernel/redaction-targets.js +1 -1
  117. package/dist/kernel/redaction.js +7 -4
  118. package/dist/process/engine-environment-keys.js +1 -0
  119. package/dist/process/environment-overlay.js +1 -1
  120. package/dist/process/environment.js +1 -1
  121. package/dist/process/execa-bundle.json +3 -3
  122. package/dist/process/files/evidence-writes.js +1 -0
  123. package/dist/process/files/product-dir.js +1 -1
  124. package/dist/process/host-process.js +1 -1
  125. package/dist/process/index.js +1 -1
  126. package/dist/process/lifecycle-fence.js +1 -1
  127. package/dist/process/lock-progress.js +2 -0
  128. package/dist/process/locks.js +4 -4
  129. package/dist/process/process-identity.js +1 -1
  130. package/dist/process/process.js +35 -35
  131. package/dist/process/recovery.js +1 -1
  132. package/dist/process/runs/manifest.js +1 -1
  133. package/dist/process/runs/ownership.js +1 -1
  134. package/dist/process/runs/stop.js +1 -1
  135. package/dist/process/shutdown.js +1 -1
  136. package/dist/process/windows-native.js +1 -1
  137. package/dist/repo/history.js +7 -4
  138. package/dist/repo/managed-git.js +1 -1
  139. package/dist/repo/state.js +4 -4
  140. package/dist/steps/environment.js +1 -1
  141. package/dist/toolchains/node-provisioning.js +2 -2
  142. package/dist/tracker/gitea.js +1 -1
  143. package/dist/tracker/github.js +1 -1
  144. package/dist/tracker/http.js +1 -1
  145. package/dist/tracker/ownership.js +1 -0
  146. package/dist/tracker/redaction.js +1 -1
  147. package/dist/tracker/responses.js +1 -1
  148. package/dist/types/playwright.d.ts +21 -0
  149. package/dist/types/workspace.d.ts +4 -4
  150. package/docs/bug-records.md +16 -10
  151. package/docs/engines.md +52 -4
  152. package/docs/release-notes.md +730 -0
  153. package/package.json +14 -2
  154. package/testkit-config-schema.md +36 -42
  155. package/dist/config/loader/task-policy.js +0 -1
  156. package/dist/environment/lifecycle-artifacts.js +0 -1
@@ -1,3 +1,733 @@
1
+ # 0.6.1
2
+
3
+ ## Each `ui` test's browser traffic is in the run evidence
4
+
5
+ Testkit now records the requests every `ui` test's pages send
6
+ ([gfdelarue/testkit#496](https://github.com/gfdelarue/testkit/issues/496)).
7
+ For each `ui` file whose tests sent requests, the run evidence has
8
+ `browser-network.json` and `browser-network-report.md`, from each test's final
9
+ attempt. Rows group page loads and API calls by service, method, and path, with
10
+ IDs replaced by `:id`. Other requests count toward totals by type, and other
11
+ origins appear by host only. The record holds no query strings, headers, or
12
+ bodies.
13
+
14
+ The report lists leads: a write sent more than once with the same body, and a
15
+ page load or API call that failed in a test that passed. Leads never fail a
16
+ test. Recording is always on. On a page that sent 400 requests it added no time
17
+ we could measure.
18
+
19
+ `browserRequests()` from `@gfdelarue/testkit/playwright` returns the test's
20
+ requests so a test can assert on them:
21
+
22
+ ```ts
23
+ const payments = (await browserRequests()).filter(
24
+ (request) => request.service === "api" && request.method === "POST" && request.path === "/payments",
25
+ );
26
+ expect(payments).toHaveLength(1);
27
+ ```
28
+
29
+ [Check a ui test's browser traffic](../README.md#check-a-ui-tests-browser-traffic)
30
+ describes the record, its limits, and the entry fields.
31
+
32
+ ## A `ui` test can slow its browser's network and CPU
33
+
34
+ `@gfdelarue/testkit/playwright` exports `emulateNetwork(page, conditions)` and
35
+ `emulateCpu(page, slowdown)`
36
+ ([gfdelarue/testkit#495](https://github.com/gfdelarue/testkit/issues/495)).
37
+ They set Chromium's own throttling through the DevTools protocol, so a test can
38
+ fail when a change makes a page unusable on a slow connection or a slow device:
39
+
40
+ ```ts
41
+ test("the dashboard is usable on a slow connection", async ({ page }) => {
42
+ await emulateNetwork(page, "slow-4g");
43
+ await page.goto("/dashboard");
44
+ await expect(page.getByRole("heading", { name: "Orders" })).toBeVisible({ timeout: 10_000 });
45
+ });
46
+ ```
47
+
48
+ `emulateNetwork` takes `"fast-4g"`, `"slow-4g"`, or `"3g"`, with the values of
49
+ DevTools' presets of the same name, or custom
50
+ `{ latencyMs, downloadKbps, uploadKbps }`. It converts kilobits per second to
51
+ the protocol's bytes per second. `emulateCpu` takes a fixed multiplier from 1
52
+ to 100, which slows the page relative to the machine that runs it. The network
53
+ conditions apply to the page you pass in, and the CPU slowdown to the browser
54
+ process that runs it.
55
+ [Slow the browser's network and CPU](../README.md#slow-the-browsers-network-and-cpu)
56
+ lists the profile values, what each setting reaches, and how to set time limits
57
+ that don't fail on a slow CI runner.
58
+
59
+ Testkit's own Postgres client calls ran with the 30-minute timeout meant for
60
+ long user SQL, and the source schema's replay into a template with the
61
+ 60-minute step timeout. A schema-only `pg_dump` that couldn't finish used up
62
+ all of `db schema refresh`'s or `verify`'s time limit, and the error said only
63
+ that a Postgres client was cancelled
64
+ ([gfdelarue/testkit#468](https://github.com/gfdelarue/testkit/issues/468)).
65
+
66
+ Each call now has a cap:
67
+
68
+ - the server version probe before a source schema dump: 60 seconds;
69
+ - a schema-only `pg_dump`: 5 minutes;
70
+ - the source schema's replay into a template: 10 minutes.
71
+
72
+ When the probe or a dump reaches its cap, the error names the call and its
73
+ cap, such as `Dockerized Postgres client for the schema-only pg_dump (300 s
74
+ cap) exceeded its managed deadline`.
75
+
76
+ In `db schema verify` and `refresh`, a `--timeout-seconds` above 600 lengthens
77
+ these caps in proportion, along with the admin statements' `lock_timeout` and
78
+ `statement_timeout` and the role setup's 60-second timeout: a limit of 1200
79
+ seconds doubles them. Elsewhere, including `testkit run`, the caps keep their
80
+ defaults. Configured template steps keep their 60-minute timeout.
81
+
82
+ ## A schema operation that stops waiting for a lock says who holds it
83
+
84
+ When `db schema verify` or `refresh` ran out of its time limit while another
85
+ operation on the same product held a lock it needed, the error said only which
86
+ service it was preparing
87
+ ([gfdelarue/testkit#468](https://github.com/gfdelarue/testkit/issues/468)). It
88
+ now names the lock, how long it waited, the holder's process and host, and the
89
+ step the holder is on and for how long. A holder from an older Testkit, or one
90
+ that recorded no step, is reported with its progress unknown.
91
+
92
+ A lock whose release failed in Testkit's own process used to block that
93
+ process's later waits until it exited. Testkit now reports it at once.
94
+
95
+ A configured template step's toolchain installation now counts against the
96
+ operation's limit. Only the step's own run doesn't.
97
+
98
+ ## `db schema verify` and `refresh` have a time limit
99
+
100
+ `db schema verify` and `db schema refresh` had no overall bound. A Testkit
101
+ statement blocked on another session's lock waited until its own
102
+ `lock_timeout`, and any wait without a limit of its own could hang the
103
+ command. Their cleanup ran with no bound and dropped its errors
104
+ ([gfdelarue/testkit#468](https://github.com/gfdelarue/testkit/issues/468)).
105
+
106
+ Each operation now has a time limit, 600 seconds unless `--timeout-seconds`
107
+ sets it. Your configured template steps don't spend it: they keep their own
108
+ step timeout. When the limit runs out, the error lists the setup steps with
109
+ their times, the service being prepared and the wait it stopped in.
110
+
111
+ Cleanup gets its own 3-minute allowance. If it can't finish, the error says
112
+ what it left, and the command fails even when the operation's work
113
+ succeeded. `testkit destroy` removes what's left.
114
+
115
+ A process Testkit stops is awaited for at most 15 seconds after its
116
+ termination begins, or 15 seconds after a local service's shutdown grace.
117
+ After that, Testkit reports it as unsettled instead of waiting.
118
+
119
+ ## A retained local database that won't start is rebuilt once
120
+
121
+ Testkit keeps each product's local Postgres cluster in a Docker volume between
122
+ runs. If that cluster was damaged, for example by a host crash while its files
123
+ were half written, its server exited while starting, and every later run failed
124
+ the same way until `testkit destroy`.
125
+
126
+ Testkit now notices when the server exits while it waits for the database to
127
+ become ready. If the cluster was on a volume an earlier run left, and the volume
128
+ carries this product's Testkit labels, Testkit removes the container and the
129
+ volume, says so, and provisions again on a fresh volume:
130
+
131
+ ```text
132
+ [testkit] Testkit is replacing database volume <volume>, because Local database container <container> exited while starting with exit code 1: <the server's last FATAL line, redacted>. The new volume starts with an empty cluster, so its template is rebuilt.
133
+ ```
134
+
135
+ It does this at most once per provisioning. A server that exits on a volume
136
+ created in the same provisioning fails it, with its exit code and its last FATAL
137
+ or PANIC line. A server that is slow to start, or that refuses connections while
138
+ it starts, is left alone, and readiness still times out after 60 seconds.
139
+
140
+ Testkit also creates new clusters with `initdb --no-sync`, through
141
+ `POSTGRES_INITDB_ARGS`. On Docker Desktop that saves about a second of a cold
142
+ start. A cluster that a host crash catches right after `initdb` is now one of
143
+ the clusters the rebuild replaces.
144
+
145
+ ## Admin statements on local Postgres time out
146
+
147
+ A statement Testkit runs on its local Postgres container, such as
148
+ `CREATE DATABASE` or `DROP DATABASE`, waited indefinitely when another session
149
+ held a lock it needed. A `psql` client that didn't return after its statement
150
+ kept the command waiting too. Neither printed anything
151
+ ([gfdelarue/testkit#468](https://github.com/gfdelarue/testkit/issues/468)).
152
+
153
+ Each admin statement now runs with a 60-second `lock_timeout` and a 10-minute
154
+ `statement_timeout`. Testkit stops its `docker exec` 30 seconds after the
155
+ statement timeout, then ends the client and its session inside the container,
156
+ as it already did after a cancellation. The error names the statement and its
157
+ limits. When the statement ran long enough to be checked, the error also says
158
+ what it was last seen doing, such as the lock it was waiting on and the session
159
+ that held it.
160
+
161
+ ## The local database keeps its data in Testkit's volume, whatever the image's tag
162
+
163
+ Testkit chose where to mount the local PostgreSQL container's named volume from
164
+ a major version it read out of the image's tag
165
+ ([gfdelarue/testkit#481](https://github.com/gfdelarue/testkit/issues/481)). A tag
166
+ that only looked like a version, such as `team-db:33ab` on a PostgreSQL 16
167
+ image, got PostgreSQL 18's mount, `/var/lib/postgresql`. The cluster then lived
168
+ in an anonymous volume that Docker mounted at the image's
169
+ `/var/lib/postgresql/data`, so a recreated container started empty, and
170
+ removing the container deleted the data. A PostgreSQL 18 image whose tag read
171
+ below 18 got the older mount, and its container never became ready.
172
+
173
+ Testkit now reads the image's own configuration with `docker image inspect`:
174
+ its `PGDATA` and the volumes it declares. It pulls the image first when it
175
+ isn't local. The named volume takes the place of the deepest declared volume
176
+ that holds `PGDATA`, or mounts at `PGDATA` when no declared volume holds it.
177
+ Testkit stops with an error rather than guess when the image sets no `PGDATA`,
178
+ sets a relative one, or declares a volume inside it:
179
+
180
+ ```text
181
+ Testkit cannot tell where image team-db:33ab keeps its PostgreSQL data: its configuration sets no PGDATA. Use an image built from the official postgres image, or set PGDATA in it.
182
+ ```
183
+
184
+ A container that an earlier version mounted in the wrong place is recreated on
185
+ a fresh named volume, so its database starts empty once.
186
+
187
+ ## A crashed run no longer blocks the next one after Windows reuses its PID
188
+
189
+ On Windows, once a crashed run's processes had ended and Windows had given one
190
+ of their process IDs to another process, the next run, `testkit cleanup` and
191
+ `testkit run stop` failed:
192
+
193
+ ```text
194
+ Application service process creation identity no longer matches pid 6944; refusing unsafe cleanup
195
+ ```
196
+
197
+ The crashed run's record stayed until the unrelated process exited
198
+ ([gfdelarue/testkit#485](https://github.com/gfdelarue/testkit/issues/485)).
199
+ Linux and macOS could hit the same error when the new process led its own
200
+ process group.
201
+
202
+ A recorded process whose PID now belongs to a different process has ended.
203
+ Testkit leaves the new process alone and clears the crashed run's record. It
204
+ still refuses to signal a live process whose creation identity it can't read.
205
+
206
+ ## The local database is ready when it accepts a connection from the host
207
+
208
+ Testkit waited for its local Postgres container by running `pg_isready` inside
209
+ it through `docker exec`, up to about once a second. On Docker Desktop each
210
+ exec costs about 0.17 s of the WSL VM's CPU, and a cold start ran seven or
211
+ eight of them.
212
+
213
+ Testkit now waits by connecting to the container's published port on
214
+ `127.0.0.1`, the address the services use, with the startup, authentication
215
+ and `SELECT 1` of its database checks. It tries every 10 to 100 ms, gives each
216
+ attempt 5 seconds, and still gives up after 60 seconds. A port that accepts the
217
+ connection but doesn't answer as PostgreSQL, as Docker Desktop's port proxy
218
+ does before Postgres listens, and a server that rejects the startup, such as
219
+ one still starting, don't count as ready. When readiness times out, the error
220
+ gives the last check's reason. A server that refuses the configured user or
221
+ password, or has no `postgres` database, fails at once with that reason instead
222
+ of waiting out the 60 seconds.
223
+
224
+ Testkit waits on the published port only when it reaches Docker through a
225
+ local socket or named pipe, whether `DOCKER_HOST`, `DOCKER_CONTEXT` or the
226
+ configuration's current context names it. A daemon behind a `tcp://` or `ssh://`
227
+ endpoint, at any address, can publish ports in another network namespace, as
228
+ Docker-in-Docker does, so readiness there still runs `pg_isready` in the
229
+ container. Through a local socket, Testkit runs that probe as well, at most once
230
+ a second, when nothing answers on the port: the connection is refused, an
231
+ attempt gets no reply, or what answers isn't PostgreSQL. After 10 seconds it
232
+ probes on any failed attempt. When the probe finds the server ready, readiness
233
+ succeeds. A socket that forwards to a daemon in another namespace works this
234
+ way. As before, the database URL Testkit gives tests names `127.0.0.1` and the
235
+ published port, which such a daemon doesn't serve on this host.
236
+
237
+ ## Managed Chromium runs with its sandbox on
238
+
239
+ Playwright starts Chromium with `--no-sandbox` unless `chromiumSandbox` is
240
+ `true`, and Testkit didn't set it. Every UI file ran without Chromium's
241
+ sandbox, so a compromised renderer ran with the user's access to the checkout,
242
+ SSH keys and env files
243
+ ([gfdelarue/testkit#190](https://github.com/gfdelarue/testkit/issues/190)).
244
+
245
+ Testkit now sets `chromiumSandbox: true` in the Playwright config it generates
246
+ and in the managed test's launch options. It holds whether a file imports
247
+ `test` from `@playwright/test`, `playwright/test`, or
248
+ `@gfdelarue/testkit/playwright`, when a project or a file sets other
249
+ `launchOptions`, and for browsers that `globalSetup` and `globalTeardown`
250
+ launch. An explicit `chromiumSandbox: false` in `playwright.config.ts` or in a
251
+ file's `test.use` still turns it off.
252
+
253
+ On a host that can't start the sandbox, such as root in a container, Chromium
254
+ now fails to launch. Testkit reports that as an infrastructure failure and puts
255
+ the opt-out first:
256
+
257
+ ```text
258
+ Testkit starts managed Chromium with its sandbox on, and this host can't start it. To run without the sandbox here, set use.launchOptions.chromiumSandbox to false in playwright.config.ts, or in one file with test.use.
259
+ ```
260
+
261
+ [Browser launch options](engines.md#browser-launch-options) shows how to turn
262
+ it off for one host.
263
+
264
+ ## Managed Chromium no longer crashes at launch under older glibc
265
+
266
+ Chromium sometimes crashed with `SIGSEGV` in `libdbus` during
267
+ `browserType.launch` on Ubuntu 24.04, and the UI file failed
268
+ ([gfdelarue/testkit#453](https://github.com/gfdelarue/testkit/issues/453)).
269
+ At startup, Chromium sets `FC_FONTATIONS` on a worker thread while its D-Bus
270
+ thread reads the environment. glibc before 2.41 can move the environment and
271
+ free the old copy under that reader, unless the distribution backported the
272
+ fix. Ubuntu 24.04's glibc 2.39 hasn't.
273
+
274
+ Playwright files now run with `DBUS_SESSION_BUS_ADDRESS=disabled:` and
275
+ `FC_FONTATIONS=1`, the values Chromium sets itself. Chromium finds both already
276
+ set and doesn't grow the environment at startup.
277
+
278
+ - Tests, and any process or browser they start, see both variables.
279
+ - A `launchOptions.env` in `playwright.config.ts` or in a file's `test.use`
280
+ gets both added, under its own values. A browser that a test or global setup
281
+ launches directly with its own `env` gets only what that sets.
282
+ - The Playwright engine reserves both keys, so a group's environment can't set
283
+ them.
284
+
285
+ ## A capture records only requests sent to its url
286
+
287
+ An HTTP capture recorded a request to any path outside `/__testkit__/` from any
288
+ caller that found its port, whatever its `Host` header
289
+ ([gfdelarue/testkit#193](https://github.com/gfdelarue/testkit/issues/193)).
290
+ Another local user, or a web page through DNS rebinding, could add captures and
291
+ push real ones out of the list.
292
+
293
+ A capture's `url` now holds an unguessable segment, as its `requestsUrl`
294
+ already did, such as `http://127.0.0.1:41000/__testkit__/<segment>/webhook`.
295
+ The capture records a request only when its path starts with that segment, and
296
+ answers any other path with 404. A captured request's `path` leaves the segment
297
+ out, so a test that checks `path` still sees `/webhook`. The capture answers
298
+ 403 to a request whose `Host` header isn't `127.0.0.1:<port>`. Testkit keeps
299
+ the new segment out of the run's records. A capture's `path` may no longer
300
+ contain `.` or `..` segments, because a client would resolve them against the
301
+ `url` and drop the segment.
302
+
303
+ If your product builds a URL from the capture's origin, give it the whole `url`
304
+ and append to that instead. [Handles](../README.md#handles) describes the
305
+ capture.
306
+
307
+ ## Tokens, temporary files, results and Git calls are harder to reach
308
+
309
+ These changes keep another bug, or another user on a shared host, from reaching
310
+ what Testkit handles
311
+ ([gfdelarue/testkit#196](https://github.com/gfdelarue/testkit/issues/196)).
312
+
313
+ - On Linux and macOS, `.testkit` is now private to its owner. Loading the
314
+ config and starting a run create it with mode `0700`, and any command that
315
+ resolves the product removes group and other access from an existing one the
316
+ user owns. Sealed objects are `0400` instead of `0444`. Another user, such as
317
+ a host user reading results that a container wrote as root, now needs the
318
+ owner to share them. On Windows, `.testkit` keeps the access its parent
319
+ folder grants.
320
+ - Services and setup steps no longer inherit `GITHUB_TOKEN`, `GH_TOKEN` or
321
+ `GITEA_TOKEN` from Testkit's environment. Testkit reads them itself for
322
+ known-failure checks and bug filing. A service that needs one declares it in
323
+ its `env`.
324
+ - Lint's `git ls-files` and bug filing's `git diff` no longer run the command
325
+ that the repository's `core.fsmonitor` names. Run fingerprinting already
326
+ turned it off.
327
+ - The `testkit-engine-*` directories that Testkit creates in the system's
328
+ temporary directory, which hold each browser profile, now record their
329
+ owner. The first engine invocation of a Testkit process removes those whose
330
+ owner has exited, such as after a crash. On Linux it only removes those from
331
+ its own PID namespace, so a container that shares the temporary directory
332
+ with its host leaves the host's alone, and the reverse. Directories from
333
+ earlier versions have no record and stay.
334
+ - On Linux, a recorded process identity now includes the boot ID, so after a
335
+ reboot a run or lock record no longer matches an unrelated process that got
336
+ the same PID and start time. A record from an earlier Testkit still matches on
337
+ start time alone, as before.
338
+
339
+ ## A Playwright companion decides a Next app's support files
340
+
341
+ In a Next application, `testkit typecheck` added `next-env.d.ts`,
342
+ `tests/playwright-fixtures`, and the generated types under `.next` and
343
+ `.next-testkit` to the Playwright program even when a `tsconfig.playwright.json`
344
+ left them out or excluded them
345
+ ([gfdelarue/testkit#108](https://github.com/gfdelarue/testkit/issues/108)). A
346
+ fixture the companion excluded, or another service's stale build output, could
347
+ then fail the check. With a companion, the program now holds the discovered
348
+ suites, the Playwright config, the files the companion selects, and what they
349
+ import. List the Next files the suites need in the companion. Without a
350
+ companion, the Next defaults stay.
351
+
352
+ ## `typecheck` checks test files outside the tsconfig's `rootDir`
353
+
354
+ The generated programs took `rootDir` and `outDir` from the tsconfig they
355
+ extend. A test file outside that `rootDir`, such as `tests/sum.test.ts` beside a
356
+ tsconfig whose `rootDir` is `src`, failed `typecheck` with `TS6059` before any of
357
+ its types were checked. TypeScript 6 also defaults `rootDir` to the directory of
358
+ the tsconfig it compiles, which is Testkit's generated one, so under TypeScript 6
359
+ every test file failed when the tsconfig set `outDir` and no `rootDir`
360
+ ([gfdelarue/testkit#407](https://github.com/gfdelarue/testkit/issues/407)).
361
+
362
+ The generated configs now clear the inherited output paths, `outDir`,
363
+ `outFile`, `declarationDir`, `sourceRoot`, and `mapRoot`, and the test programs'
364
+ configs clear `rootDir` too. TypeScript holds every file to an explicit
365
+ `rootDir`, and when an output path is set without one, TypeScript 6 uses the
366
+ tsconfig's own directory. With both cleared, it holds test files to neither. When the tsconfig sets `incremental` or `composite`,
367
+ Testkit now writes its build info beside the generated config in
368
+ `.testkit/_typecheck`, instead of under the repository's `outDir` or into the
369
+ repository's own build info file.
370
+
371
+ ## A run stops its retained database when it releases it
372
+
373
+ A run that provisioned the local Postgres container now stops it as part of releasing the run's resources, after it drops the run's task databases. It stops the container by the name in `.testkit/_infra/local-postgres` and lists no Docker resources to do it. A container that is already stopped counts as released.
374
+
375
+ If that container no longer exists, the release fails, and so does the run, with `Could not stop the retained database container <name>: it no longer exists`. Before, a successful run stopped the container later, during finish-time maintenance.
376
+
377
+ ## A run leaves Docker resources to `testkit cleanup`
378
+
379
+ A run's start and its post-run maintenance no longer list Docker resources, remove unreferenced containers and volumes, or stop the retained database. Provisioning and release still create, replace, and remove what the run needs, and release stops the retained database by name. `testkit cleanup --resources` removes the product's unreferenced containers and volumes, and stops an idle retained database, as before.
380
+
381
+ A run's start still removes stale manifests of runs whose processes have ended, and post-run maintenance still removes other runs' stale manifests.
382
+
383
+ `.testkit/results/maintenance.json` no longer has the `removed`, `skipped` and `stopped` lists, since maintenance no longer removes or stops Docker resources.
384
+
385
+ ## `run.json`'s storage inventory covers only `.testkit/results`
386
+
387
+ `run.json`'s `storage.testkit` inventory now covers only `.testkit/results`, and its `root` is `.testkit/results`. A run no longer walks the database and graph caches to build it. When Testkit can't read part of `.testkit/results`, `run.json` records `"storage": { "testkit": { "status": "failed", "error": "…" } }` and the run keeps its result.
388
+
389
+ ## Testkit's own clients of the owned database run inside its container
390
+
391
+ Testkit used to start a client container, from `postgres:16` or
392
+ `TESTKIT_POSTGRES_CLIENT_IMAGE`, to declare roles and take schema snapshots in
393
+ the database it owns. Before a snapshot, it also asked the server for its
394
+ version to choose a `pg_dump` image. Both now run inside the database container
395
+ with `docker exec`, using the database image's own `psql` and `pg_dump`, as
396
+ Testkit's admin queries already did. They no longer start a container
397
+ ([gfdelarue/testkit#139](https://github.com/gfdelarue/testkit/issues/139)).
398
+
399
+ This changes two things:
400
+
401
+ - `TESTKIT_POSTGRES_CLIENT_IMAGE` no longer applies to those operations. Your
402
+ SQL, in `sql-file` steps and `database-steps` helpers, still runs in a client
403
+ container from that image, and so do clients of external databases.
404
+ - The `image` that `database.postgres()` uses must include `psql` and
405
+ `pg_dump`, and its `pg_dump` must be at least as new as its server.
406
+ Otherwise the operation fails with a message that names the image:
407
+
408
+ ```text
409
+ pg_dump in the database image <image> is older than its server: pg_dump: error: aborting because of server version mismatch
410
+ ```
411
+
412
+ ## Redaction recognises more secret names, API key headers, and bearer tokens
413
+
414
+ Sealing, filed bugs, and error messages kept a secret under a key such as
415
+ `STRIPE_KEY`, `GITHUB_PAT`, `SMTP_PASS`, or `SENTRY_DSN`, and the value of an
416
+ `X-Api-Key` header or an `apiKey` member. They kept a bearer token outside an
417
+ `Authorization` header, and a header that a trace stores as a
418
+ `{"name": …, "value": …}` pair
419
+ ([gfdelarue/testkit#188](https://github.com/gfdelarue/testkit/issues/188)).
420
+
421
+ - A key whose last word is `PASS`, `PASSWD`, `PAT`, `DSN`, `CREDENTIAL`, or
422
+ `CREDENTIALS` now names a secret, as does a key whose last two words are
423
+ `API` and `KEY`. An upper-case key whose last word is `KEY` names a secret
424
+ unless the word before it is `PUBLIC` or `PUBLISHABLE`. A run's environment
425
+ values under these keys are redacted as other secrets are, so a value under
426
+ a key such as `CACHE_KEY` is now redacted too. `PATH`, `BYPASS`, and
427
+ `cacheKey` still name no secret.
428
+ - `Api-Key` and `X-Api-Key` header values are redacted like `Authorization`.
429
+ - The token after `Bearer` is redacted wherever it appears when it is at least
430
+ eight characters long and holds a digit, so prose such as
431
+ `Bearer authentication` stays.
432
+ - In a `name` and `value` pair, the value is redacted when the name names a
433
+ secret, such as `Authorization` or `Cookie`. When `testkit run` stores a
434
+ result, every value in a `cookies` array is redacted too.
435
+
436
+ `docs/bug-records.md` lists the rules.
437
+
438
+ ## Redaction recognises more secret forms and fewer ordinary words
439
+
440
+ Sealing, filed bugs, and error messages kept these secrets
441
+ ([gfdelarue/testkit#215](https://github.com/gfdelarue/testkit/issues/215)):
442
+
443
+ - A `privateKey`, `signingKey`, or `encryptionKey` value.
444
+ - A single-quoted member under any secret name other than `password`, as in
445
+ `{'token': '…'}`.
446
+ - The strings in a JSON array or object under a secret name, as in
447
+ `{"tokens":["…"]}`. `session` and `auth` settings stay, and an object with a
448
+ `name` and a `value` member loses only its `value` and members under a secret
449
+ name.
450
+ - A value after `password:`. A quoted value is redacted anywhere, and an
451
+ unquoted one when it is the one word of a line that it starts. A single word
452
+ of letters stays, so `password: 'Required'` and `password: required!` stay.
453
+ - A URL password that holds a `/`, a URL password in a directory name, such as
454
+ `postgres:/app:…@db`, and a token of at least 20 characters with a digit that
455
+ stands alone as the user of an `http` or `https` URL, as in
456
+ `https://ghp_…@github.com`. A resolved URL's password or token is redacted
457
+ wherever it appears, too.
458
+ - A secret right after another one with no space between them, as in
459
+ `PASSWORD=…"Cookie: sid=…`.
460
+ - A URL password that output cut short. The first line of a task's output, as
461
+ an artifact summary or a failure message, is redacted before Testkit cuts it.
462
+
463
+ They redacted these values that are not secrets:
464
+
465
+ - A value under `author`, `authority`, or `GIT_AUTHOR_NAME`, and under
466
+ `sessionCount`, `SESSION_TIMEOUT`, and other keys where `SESSION` comes before
467
+ a count or a time. A run's environment no longer redacts every occurrence of
468
+ `GIT_AUTHOR_NAME`'s value either.
469
+ - A path assigned to a key that ends in `_URL`, such as
470
+ `AUTH_URL=/oauth/authorize`. A `token=` parameter in the path is still
471
+ redacted. A JSON member such as `{"AUTH_URL":"/oauth/authorize"}` is still
472
+ redacted whole.
473
+ - The text after a quoted header value that ends in an escaped backslash.
474
+
475
+ `testkit bug record` now rejects a record whose reproduce command holds text
476
+ that Testkit redacts, as `testkit bug file` already did. Before, a Vitest
477
+ project name such as `TOKEN=…` passed `bug record` and stopped `bug file`.
478
+
479
+ ## Sealing redacts inside traces and other compressed files
480
+
481
+ Sealing didn't look inside `.zip` and `.gz` files. A Playwright `trace.zip`
482
+ recorded with `use.trace` kept form values, `Authorization` and `Cookie`
483
+ headers, and session cookies in the sealed run, compressed, while the run's
484
+ own copies of the same output were redacted
485
+ ([gfdelarue/testkit#187](https://github.com/gfdelarue/testkit/issues/187)).
486
+
487
+ Sealing now reads `.zip` and `.gz` files, and files that start like one. It
488
+ redacts each text entry with the same rules as a text file and writes the
489
+ archive again, so the trace is kept and opens in the trace viewer with
490
+ `<redacted>` in place of the secrets. A text entry is valid UTF-8 without binary
491
+ control characters, or UTF-16 with a byte order mark. Entries it doesn't change
492
+ keep their bytes. A ZIP with nothing to redact and no comment or extra fields
493
+ stays byte for byte, and a gzip file is always written again without the name
494
+ and comment in its header.
495
+
496
+ - The bundle's redaction counts include the redacted archives: each counts as
497
+ a redacted file, its text entries count as scanned files, and its
498
+ replacements count too.
499
+ - Sealing refuses an archive it can't redact: one it can't read, such as an
500
+ encrypted, ZIP64, damaged, or larger than 512 MiB archive, one whose binary
501
+ entry holds a value the run resolved, in UTF-8 or UTF-16, or one that holds
502
+ another archive.
503
+ - Sealing removes each file it refuses, an archive or a binary file that holds
504
+ a resolved value, from the run's results. It fails the run with the reasons,
505
+ then seals the rest and records them. `testkit stop`, `testkit cleanup` and
506
+ the next run do the same when they seal a run that a killed process left.
507
+ Before, a refused run left its results in place, and those commands sealed
508
+ them without the values the run had resolved, the refused file included.
509
+ - Screenshots and video, in a trace or on their own, aren't redacted.
510
+
511
+ ## Sealed bundles leave out the product and home directories
512
+
513
+ Sealing removed the product directory and host from `run.json`, but the rest of
514
+ the bundle kept them
515
+ ([gfdelarue/testkit#189](https://github.com/gfdelarue/testkit/issues/189)). A
516
+ failing Playwright file's stack in `run.json`, the native report's `sourceFile`
517
+ and attachment paths, and a `trace.zip` held the absolute product path. A trace
518
+ also held the home directory, in the path of the Chromium that Testkit manages.
519
+ On a developer machine those paths name the user.
520
+
521
+ Sealing now rewrites local paths in the run record, in every text file of the
522
+ bundle, and in every text entry of a compressed file such as `trace.zip`. A
523
+ path under the product directory becomes relative to it, and a path under the
524
+ home directory starts with `~`. When sealing changes a file, by this rewrite or
525
+ by redaction, the file's size and SHA-256 in `run.json` now describe the sealed
526
+ file.
527
+
528
+ ## An exit code set after a plain file's tests fails the run
529
+
530
+ When a reporter or global teardown set Vitest's exit code to 1 after a file's
531
+ tests failed, Testkit read the run as those failed tests alone
532
+ ([gfdelarue/testkit#215](https://github.com/gfdelarue/testkit/issues/215)). A
533
+ run whose failed tests all had issues passed the known-failure gate under
534
+ `"allow"`, though something else had also failed it.
535
+
536
+ Testkit now notes Vitest's own verdict when the tests end, and fails the run
537
+ with a run error when a reporter or global teardown sets the exit code
538
+ afterwards:
539
+
540
+ ```text
541
+ FAIL run error
542
+ Vitest failed the run after its tests finished: exit code 1 while Vitest closed
543
+ ```
544
+
545
+ When the tests passed, the run error says "after its tests passed", as it does
546
+ for a coverage threshold, and each file keeps its own outcome. Before, such an
547
+ exit failed every file as an unexplained infrastructure failure. A coverage
548
+ threshold that falls short beside a failed test still counts as part of that
549
+ failure, because a failed test leaves code unrun.
550
+
551
+ ## The known-failure gate stops missing three failures
552
+
553
+ Three failures could pass the known-failure gate under `"allow"`, or never be
554
+ known at all
555
+ ([gfdelarue/testkit#215](https://github.com/gfdelarue/testkit/issues/215)):
556
+
557
+ - A plain Vitest file whose data-flow evidence was malformed lost that error
558
+ when one of its tests also failed. The file now fails with the evidence
559
+ error beside its failed tests, which an issue for the tests doesn't cover.
560
+ - A Playwright spec that failed in another file, such as a dependency or
561
+ teardown project, now always counts as a failure that names no test, even
562
+ when the run reports nothing else about it.
563
+ - On macOS and Linux, a test file whose name holds a backslash could not be
564
+ filed, and its failures were always new. A bug record now keeps the
565
+ backslash, and treats it as a separator only on Windows.
566
+
567
+ Testkit reads the file a Playwright spec was loaded from out of its file
568
+ suite's title, resolved against `rootDir`. It now checks, as the run begins,
569
+ that each title names a file. If a Playwright release changes the title so that
570
+ it names no file, the file fails with an error that says so, instead of
571
+ reporting the spec under a path where no file exists.
572
+
573
+ ## Four Playwright failures stop passing as known
574
+
575
+ Four Playwright failures could pass the known-failure gate under `"allow"`, or
576
+ never be known at all
577
+ ([gfdelarue/testkit#215](https://github.com/gfdelarue/testkit/issues/215)):
578
+
579
+ - A reporter in the Playwright config that ended the process with exit code 1
580
+ read as Playwright's own exit for failed specs, so an issue for those specs
581
+ covered the run. Testkit's reporter runs last and marks the run once every
582
+ reporter's hooks have returned. Without that mark, exit code 1 is a failure
583
+ that names no test. A `PLAYWRIGHT_SPECS_FAILED` failure carries
584
+ `reportersFinished` in its details. A run stored by an earlier release has no
585
+ such mark, so `testkit ci report` reads its exit code 1 the same way. A
586
+ Playwright config that sets `PW_TEST_REPORTER` now fails, because Playwright
587
+ runs that reporter after Testkit's.
588
+ - A failed spec in a serial group, or a failed `beforeAll` hook, could stop
589
+ later specs of the selected file. Playwright reports those specs as skipped,
590
+ so an issue for the failed spec let the run pass. Each such spec is now a
591
+ detail of kind `playwright-not-run`, which names no test. A spec that the
592
+ file skips with `test.skip` or `test.fixme` is not one. A spec that a run
593
+ stops at `maxFailures` gets the same detail, though that run already failed
594
+ the gate on the error Playwright reports when it stops.
595
+ - Under `failOnFlakyTests`, a spec that passed only on a retry failed the run
596
+ as a failure that names no test, so no issue could cover it. It is now a
597
+ `playwright-spec` detail with its title, and an issue for that title covers
598
+ it.
599
+ - A file whose every spec came from one helper with another extension, such
600
+ as a `.ts` file and a `.mjs` helper, ran no specs and failed with
601
+ `PLAYWRIGHT_NO_TESTS`. Playwright moves the file suite's location to that
602
+ helper, and Testkit selected the file by location. Testkit now selects it
603
+ with `--grep` on its file suite's title, the file's path from `rootDir`, so
604
+ its specs run.
605
+
606
+ ## Tracker tokens go only where they belong
607
+
608
+ Five gaps in how Testkit talks to GitHub and Gitea are closed
609
+ ([gfdelarue/testkit#215](https://github.com/gfdelarue/testkit/issues/215)):
610
+
611
+ - `GITEA_TOKEN` names no host, so Testkit sent it to whichever Gitea host the
612
+ checkout's remote named. Outside a Gitea Actions job, Testkit now sends it
613
+ only to the host that `GITEA_SERVER_URL` names. Set
614
+ `GITEA_SERVER_URL=https://gitea.example.com` beside `GITEA_TOKEN`, or keep the
615
+ token in a Git credential helper, which is bound to its host already.
616
+ - Testkit sends a token only over HTTPS. A plain `http://` remote or repository
617
+ URL now fails with a message that says so, unless it names a loopback host.
618
+ - `testkit ci report` updated the first pull request comment that started with
619
+ its marker, whoever wrote it. It now updates only a comment written by its
620
+ token's user. It takes that user from the commit status it has just set, or
621
+ asks the tracker when the status names no one. When neither names the user,
622
+ it posts a new comment each time rather than edit one.
623
+ - When the tracker created an issue without its labels, `testkit bug file`
624
+ failed and left the issue open. It now closes that issue first, and says
625
+ whether it could.
626
+ - `testkit ci report` now leaves out of the summary and the commit status any CI
627
+ run or issue link that holds credentials or a secret.
628
+
629
+ ## `bug file` asks the remote for the commit, and closes a duplicate it filed
630
+
631
+ Two gaps in `testkit bug file` are closed, along with a Gitea problem that the
632
+ second turned up ([gfdelarue/testkit#215](https://github.com/gfdelarue/testkit/issues/215)):
633
+
634
+ - It took the commit as pushed when a local remote-tracking branch held it,
635
+ even after the remote rewound or deleted that branch. It now lists the
636
+ remote's branches with `git ls-remote` and checks their tips, fetching a tip
637
+ the checkout lacks. A renamed branch still counts. When a remote doesn't
638
+ answer, or a shallow clone's history stops before Git can tell, Testkit
639
+ stops and says why, unless another remote has the commit.
640
+ - Two checkouts that filed the same test at once could each create an issue,
641
+ because neither saw the other's in the open issues it read first. After it
642
+ creates the issue, Testkit now records it in the draft and reads the open
643
+ issues again. GitHub can list a new issue late, so it waits up to 7.5 seconds
644
+ for its own to appear, and on GitHub it then reads again two seconds later.
645
+ It says so when its issue never appears. When an older open issue covers the
646
+ same test, Testkit closes its own, comments with the older one's number, and
647
+ stops as it would have if it had seen that issue first.
648
+ - On Gitea, two checkouts creating the same label at once made two labels of
649
+ that name, and Gitea's label filter then returned no issue. Testkit now
650
+ deletes a label it created when an older one has the same name, and lists
651
+ the open bugs without the filter when the repository already has two labels
652
+ of one name.
653
+
654
+ ## A failed database drop no longer hides the error before it
655
+
656
+ When a template step failed, Testkit dropped the half-built template database
657
+ before it reported the step's error. If the drop failed too, the run reported
658
+ only the drop's error. A run interrupted during a template `sql-file` step,
659
+ after its Docker connection had closed, reported only `Cannot connect to the
660
+ Docker daemon`, and nothing about the step or the Postgres client container it
661
+ left running
662
+ ([gfdelarue/testkit#400](https://github.com/gfdelarue/testkit/issues/400)). A
663
+ `resetBefore` file whose database clone failed had the same problem.
664
+
665
+ The run now reports both errors, and names the database Testkit couldn't drop:
666
+
667
+ ```text
668
+ Dockerized Postgres client was cancelled
669
+ Postgres client container cleanup failed: Cannot connect to the Docker daemon at tcp://127.0.0.1:36893. Is the docker daemon running?
670
+ Dropping the template database tk_tpl_api_f14201a8f06684a4 also failed: Cannot connect to the Docker daemon at tcp://127.0.0.1:36893. Is the docker daemon running?
671
+ ```
672
+
673
+ ## A failure block prints each message once, without color codes
674
+
675
+ A failed Playwright assertion printed its message twice in the run's failure
676
+ block, once for the test and once for Playwright's overall failure. Both copies
677
+ kept Playwright's ANSI color codes, even with `NO_COLOR` set, and both were cut
678
+ off mid-word
679
+ ([gfdelarue/testkit#220](https://github.com/gfdelarue/testkit/issues/220)).
680
+
681
+ The block now strips the engine's color codes, prints each distinct message
682
+ once, and says how much of a long message it left out. It cuts at a space when
683
+ one falls within 40 characters of the limit, and otherwise at the limit, without
684
+ splitting a character:
685
+
686
+ ```text
687
+ [1/1] FAIL app ui __testkit__/failure.ui.testkit.ts 3s
688
+ Error: expect(locator).toHaveText(expected) failed Locator: locator('#status') Expected:
689
+ "expected marker" Received: "actual" Timeout: 1000ms Call log: - Expect "toHaveText" with
690
+ timeout 1000ms - waiting for locator('#status') 5 × locator … (69 more characters)
691
+ ```
692
+
693
+ The run's failure details keep the full message. The block's second and later
694
+ messages are now redacted like the first.
695
+
696
+ ## Long command output no longer warns about listeners
697
+
698
+ Commands write their text output one `log` call at a time, usually a line each,
699
+ and each call added an `error` listener to stdout until its write finished. More
700
+ than ten calls in a row, as in a longer `discover`, `status`, `artifacts`, or
701
+ `workspace` listing, printed `MaxListenersExceededWarning` on stderr
702
+ ([gfdelarue/testkit#221](https://github.com/gfdelarue/testkit/issues/221)).
703
+ Testkit now adds one listener.
704
+
705
+ ## Testkit's runtime keys take the `@gfdelarue/testkit` prefix
706
+
707
+ Three keys that Testkit's modules use to share state inside an engine process
708
+ had kept the old `elench.testkit.*` names
709
+ ([gfdelarue/testkit#99](https://github.com/gfdelarue/testkit/issues/99)). They
710
+ are now `@gfdelarue/testkit/node-vitest-runtime-input/v1`,
711
+ `@gfdelarue/testkit/node-vitest-runtime-task/v1` and
712
+ `@gfdelarue/testkit/engine-process-environment-overlay/v1`.
713
+
714
+ There is nothing to do when you upgrade. Testkit writes the keys afresh for
715
+ every invocation, stored runs don't hold them, and the CLI refuses to run when
716
+ the product's installed Testkit is a different version, so both sides always
717
+ come from one release. Code that read these undocumented globals directly needs
718
+ the new names. The `com.elench.testkit.*` Docker labels are unchanged.
719
+
720
+ ## `bug file --batch` files several bugs in one call
721
+
722
+ `testkit bug file --batch` files the drafts it is given, or every unfiled draft
723
+ when it is given none
724
+ ([gfdelarue/testkit#254](https://github.com/gfdelarue/testkit/issues/254)). It
725
+ checks the checkout once, runs the drafts' tests together, and reports filed,
726
+ already filed, rejected, or not filed for each draft. It exits non-zero when
727
+ any draft is rejected or not filed, and a rerun files only what is left. A
728
+ campaign's pull request summary now suggests it for the bugs that are recorded
729
+ and not filed.
730
+
1
731
  # 0.6.0
2
732
 
3
733
  0.6.0 removes the `contract` and `data` test types and legacy service