@gfdelarue/testkit 0.6.0 → 0.6.2

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