@gate-forge/pack-playwright 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/README.md +31 -0
  2. package/dist/constants.d.ts +13 -140
  3. package/dist/constants.d.ts.map +1 -1
  4. package/dist/constants.js +13 -148
  5. package/dist/constants.js.map +1 -1
  6. package/dist/cypress/plugin.d.ts +53 -0
  7. package/dist/cypress/plugin.d.ts.map +1 -0
  8. package/dist/cypress/plugin.js +369 -0
  9. package/dist/cypress/plugin.js.map +1 -0
  10. package/dist/cypress/spec-scan.d.ts +39 -0
  11. package/dist/cypress/spec-scan.d.ts.map +1 -0
  12. package/dist/cypress/spec-scan.js +301 -0
  13. package/dist/cypress/spec-scan.js.map +1 -0
  14. package/dist/diagnosis.d.ts +39 -0
  15. package/dist/diagnosis.d.ts.map +1 -0
  16. package/dist/diagnosis.js +63 -0
  17. package/dist/diagnosis.js.map +1 -0
  18. package/dist/discovery/adapter-projection.d.ts +13 -0
  19. package/dist/discovery/adapter-projection.d.ts.map +1 -0
  20. package/dist/discovery/adapter-projection.js +84 -0
  21. package/dist/discovery/adapter-projection.js.map +1 -0
  22. package/dist/discovery/config-locations.d.ts +5 -0
  23. package/dist/discovery/config-locations.d.ts.map +1 -0
  24. package/dist/discovery/config-locations.js +19 -0
  25. package/dist/discovery/config-locations.js.map +1 -0
  26. package/dist/discovery/cypress-runner-adapter.d.ts +112 -0
  27. package/dist/discovery/cypress-runner-adapter.d.ts.map +1 -0
  28. package/dist/discovery/cypress-runner-adapter.js +501 -0
  29. package/dist/discovery/cypress-runner-adapter.js.map +1 -0
  30. package/dist/discovery/discover.d.ts +53 -1
  31. package/dist/discovery/discover.d.ts.map +1 -1
  32. package/dist/discovery/discover.js +100 -3
  33. package/dist/discovery/discover.js.map +1 -1
  34. package/dist/discovery/git-ignore.d.ts +13 -0
  35. package/dist/discovery/git-ignore.d.ts.map +1 -0
  36. package/dist/discovery/git-ignore.js +62 -0
  37. package/dist/discovery/git-ignore.js.map +1 -0
  38. package/dist/discovery/index.d.ts +9 -0
  39. package/dist/discovery/index.d.ts.map +1 -1
  40. package/dist/discovery/index.js +9 -0
  41. package/dist/discovery/index.js.map +1 -1
  42. package/dist/discovery/playwright-runner-adapter.d.ts +108 -0
  43. package/dist/discovery/playwright-runner-adapter.d.ts.map +1 -0
  44. package/dist/discovery/playwright-runner-adapter.js +185 -0
  45. package/dist/discovery/playwright-runner-adapter.js.map +1 -0
  46. package/dist/discovery/prepare-barrier.d.ts +522 -0
  47. package/dist/discovery/prepare-barrier.d.ts.map +1 -0
  48. package/dist/discovery/prepare-barrier.js +739 -0
  49. package/dist/discovery/prepare-barrier.js.map +1 -0
  50. package/dist/discovery/pytest-adapter.d.ts.map +1 -1
  51. package/dist/discovery/pytest-adapter.js +6 -2
  52. package/dist/discovery/pytest-adapter.js.map +1 -1
  53. package/dist/discovery/pytest-runner-adapter.d.ts +124 -0
  54. package/dist/discovery/pytest-runner-adapter.d.ts.map +1 -0
  55. package/dist/discovery/pytest-runner-adapter.js +454 -0
  56. package/dist/discovery/pytest-runner-adapter.js.map +1 -0
  57. package/dist/discovery/reconcile.d.ts +80 -11
  58. package/dist/discovery/reconcile.d.ts.map +1 -1
  59. package/dist/discovery/reconcile.js +395 -78
  60. package/dist/discovery/reconcile.js.map +1 -1
  61. package/dist/discovery/runner-env.d.ts +88 -2
  62. package/dist/discovery/runner-env.d.ts.map +1 -1
  63. package/dist/discovery/runner-env.js +142 -4
  64. package/dist/discovery/runner-env.js.map +1 -1
  65. package/dist/discovery/static-discovery.d.ts +38 -3
  66. package/dist/discovery/static-discovery.d.ts.map +1 -1
  67. package/dist/discovery/static-discovery.js +454 -15
  68. package/dist/discovery/static-discovery.js.map +1 -1
  69. package/dist/discovery/supervised-run.d.ts +199 -10
  70. package/dist/discovery/supervised-run.d.ts.map +1 -1
  71. package/dist/discovery/supervised-run.js +350 -39
  72. package/dist/discovery/supervised-run.js.map +1 -1
  73. package/dist/discovery/trusted-config.d.ts +180 -6
  74. package/dist/discovery/trusted-config.d.ts.map +1 -1
  75. package/dist/discovery/trusted-config.js +279 -10
  76. package/dist/discovery/trusted-config.js.map +1 -1
  77. package/dist/discovery/vitest-runner-adapter.d.ts +172 -0
  78. package/dist/discovery/vitest-runner-adapter.d.ts.map +1 -0
  79. package/dist/discovery/vitest-runner-adapter.js +607 -0
  80. package/dist/discovery/vitest-runner-adapter.js.map +1 -0
  81. package/dist/fixture/consumer-runner.d.ts +4 -0
  82. package/dist/fixture/consumer-runner.d.ts.map +1 -0
  83. package/dist/fixture/consumer-runner.js +38 -0
  84. package/dist/fixture/consumer-runner.js.map +1 -0
  85. package/dist/fixture/evidence.d.ts +27 -0
  86. package/dist/fixture/evidence.d.ts.map +1 -1
  87. package/dist/fixture/evidence.js +12 -0
  88. package/dist/fixture/evidence.js.map +1 -1
  89. package/dist/fixture/fixture.d.ts +2 -1
  90. package/dist/fixture/fixture.d.ts.map +1 -1
  91. package/dist/fixture/fixture.js +1 -17
  92. package/dist/fixture/fixture.js.map +1 -1
  93. package/dist/fixture/witness-client.d.ts +6 -174
  94. package/dist/fixture/witness-client.d.ts.map +1 -1
  95. package/dist/fixture/witness-client.js +6 -267
  96. package/dist/fixture/witness-client.js.map +1 -1
  97. package/dist/index.d.ts +9 -6
  98. package/dist/index.d.ts.map +1 -1
  99. package/dist/index.js +5 -4
  100. package/dist/index.js.map +1 -1
  101. package/dist/json.d.ts +6 -12
  102. package/dist/json.d.ts.map +1 -1
  103. package/dist/json.js +6 -17
  104. package/dist/json.js.map +1 -1
  105. package/dist/reporter/project-graph-reporter.d.ts +92 -0
  106. package/dist/reporter/project-graph-reporter.d.ts.map +1 -0
  107. package/dist/reporter/project-graph-reporter.js +92 -0
  108. package/dist/reporter/project-graph-reporter.js.map +1 -0
  109. package/dist/reporter/reporter.cjs.map +1 -1
  110. package/dist/reporter/reporter.d.ts +93 -4
  111. package/dist/reporter/reporter.d.ts.map +1 -1
  112. package/dist/reporter/reporter.js +174 -18
  113. package/dist/reporter/reporter.js.map +1 -1
  114. package/dist/runner-claims.d.ts +13 -0
  115. package/dist/runner-claims.d.ts.map +1 -0
  116. package/dist/runner-claims.js +46 -0
  117. package/dist/runner-claims.js.map +1 -0
  118. package/dist/runner-resolution.d.ts +3 -0
  119. package/dist/runner-resolution.d.ts.map +1 -0
  120. package/dist/runner-resolution.js +14 -0
  121. package/dist/runner-resolution.js.map +1 -0
  122. package/dist/supervisor/client.d.ts +23 -1
  123. package/dist/supervisor/client.d.ts.map +1 -1
  124. package/dist/supervisor/client.js +49 -0
  125. package/dist/supervisor/client.js.map +1 -1
  126. package/dist/supervisor/drain.d.ts +65 -0
  127. package/dist/supervisor/drain.d.ts.map +1 -1
  128. package/dist/supervisor/drain.js +353 -9
  129. package/dist/supervisor/drain.js.map +1 -1
  130. package/dist/supervisor/index.d.ts +7 -3
  131. package/dist/supervisor/index.d.ts.map +1 -1
  132. package/dist/supervisor/index.js +7 -3
  133. package/dist/supervisor/index.js.map +1 -1
  134. package/dist/supervisor/spool.d.ts +65 -2
  135. package/dist/supervisor/spool.d.ts.map +1 -1
  136. package/dist/supervisor/spool.js +47 -0
  137. package/dist/supervisor/spool.js.map +1 -1
  138. package/dist/surface.d.ts +6 -207
  139. package/dist/surface.d.ts.map +1 -1
  140. package/dist/surface.js +6 -264
  141. package/dist/surface.js.map +1 -1
  142. package/dist/vitest/index.d.ts +23 -0
  143. package/dist/vitest/index.d.ts.map +1 -0
  144. package/dist/vitest/index.js +356 -0
  145. package/dist/vitest/index.js.map +1 -0
  146. package/dist/vitest/reporter.d.ts +83 -0
  147. package/dist/vitest/reporter.d.ts.map +1 -0
  148. package/dist/vitest/reporter.js +211 -0
  149. package/dist/vitest/reporter.js.map +1 -0
  150. package/dist/vitest/worker-slot.d.ts +36 -0
  151. package/dist/vitest/worker-slot.d.ts.map +1 -0
  152. package/dist/vitest/worker-slot.js +51 -0
  153. package/dist/vitest/worker-slot.js.map +1 -0
  154. package/dist/witness/adapter-registry.d.ts +6 -50
  155. package/dist/witness/adapter-registry.d.ts.map +1 -1
  156. package/dist/witness/adapter-registry.js +6 -257
  157. package/dist/witness/adapter-registry.js.map +1 -1
  158. package/dist/witness/behavior-request.d.ts +6 -76
  159. package/dist/witness/behavior-request.d.ts.map +1 -1
  160. package/dist/witness/behavior-request.js +6 -273
  161. package/dist/witness/behavior-request.js.map +1 -1
  162. package/dist/witness/behavior.d.ts +6 -48
  163. package/dist/witness/behavior.d.ts.map +1 -1
  164. package/dist/witness/behavior.js +6 -114
  165. package/dist/witness/behavior.js.map +1 -1
  166. package/dist/witness/bin.d.ts +6 -17
  167. package/dist/witness/bin.d.ts.map +1 -1
  168. package/dist/witness/bin.js +6 -168
  169. package/dist/witness/bin.js.map +1 -1
  170. package/dist/witness/browser.d.ts +14 -206
  171. package/dist/witness/browser.d.ts.map +1 -1
  172. package/dist/witness/browser.js +15 -654
  173. package/dist/witness/browser.js.map +1 -1
  174. package/dist/witness/classifications.d.ts +6 -38
  175. package/dist/witness/classifications.d.ts.map +1 -1
  176. package/dist/witness/classifications.js +6 -83
  177. package/dist/witness/classifications.js.map +1 -1
  178. package/dist/witness/env-attestation.d.ts +6 -94
  179. package/dist/witness/env-attestation.d.ts.map +1 -1
  180. package/dist/witness/env-attestation.js +6 -198
  181. package/dist/witness/env-attestation.js.map +1 -1
  182. package/dist/witness/fixture-provider.d.ts +6 -82
  183. package/dist/witness/fixture-provider.d.ts.map +1 -1
  184. package/dist/witness/fixture-provider.js +6 -105
  185. package/dist/witness/fixture-provider.js.map +1 -1
  186. package/dist/witness/loopback-pins.d.ts +6 -75
  187. package/dist/witness/loopback-pins.d.ts.map +1 -1
  188. package/dist/witness/loopback-pins.js +6 -240
  189. package/dist/witness/loopback-pins.js.map +1 -1
  190. package/dist/witness/server.d.ts +8 -26
  191. package/dist/witness/server.d.ts.map +1 -1
  192. package/dist/witness/server.js +24 -3761
  193. package/dist/witness/server.js.map +1 -1
  194. package/dist/witness/types.d.ts +6 -882
  195. package/dist/witness/types.d.ts.map +1 -1
  196. package/dist/witness/types.js +9 -1
  197. package/dist/witness/types.js.map +1 -1
  198. package/examples/overlay-proof.spec.js +75 -0
  199. package/package.json +8 -2
  200. package/python/gateforge_pytest_plugin.py +462 -0
@@ -0,0 +1,356 @@
1
+ /**
2
+ * The gateforge Vitest helper (plan 2026-09-25 phase 4): the in-test
3
+ * half of the Vitest+supertest witness channel.
4
+ *
5
+ * `gateforgeSupertest(request)` wraps the suite's own supertest import
6
+ * and returns a tagged request function:
7
+ * - `await gate(appBaseUrl)` resolves the running test's session and
8
+ * returns a supertest builder whose traffic is REWRITTEN from the app
9
+ * origin onto that test's session proxy origin (resolved from the
10
+ * supervisor through the exact `(workerIndex, testId)` identity the
11
+ * pack's Vitest reporter spools) — every exchange is attributed to
12
+ * THAT session, the same `session-proxy` channel the Playwright
13
+ * fixture uses. Chain `.get/.post/.send` on the awaited builder
14
+ * exactly as without gateforge.
15
+ * - `gate(app)` — the IN-PROCESS mode, where supertest calls the app
16
+ * function/object directly — is REFUSED with a clear message and the
17
+ * run fails. An in-process call never crosses the proxy, so it can
18
+ * never be witnessed evidence; silently passing it would be a
19
+ * counterfeit green.
20
+ *
21
+ * Without supervised wiring (`GATEFORGE_WITNESS_URL` /
22
+ * `GATEFORGE_RUN_TOKEN` / `GATEFORGE_STATE_DIR` / `GATEFORGE_RUN_ID`)
23
+ * a tagged call fails loudly — never silently unwitnessed.
24
+ */
25
+ import { isAbsolute, relative } from 'node:path';
26
+ import { getCurrentTest } from 'vitest/suite';
27
+ import { humanMessage } from '@gate-forge/core';
28
+ import { onTestFinished } from 'vitest';
29
+ import { appendSpoolEvent, spoolPathFor } from '../supervisor/spool.js';
30
+ import { claimInjectionsFor } from '../runner-claims.js';
31
+ import { vitestWorkerSlot } from './worker-slot.js';
32
+ /** Env the runner child receives from the adapter's allowlist. */
33
+ const ENV_WITNESS_URL = 'GATEFORGE_WITNESS_URL';
34
+ const ENV_RUN_TOKEN = 'GATEFORGE_RUN_TOKEN';
35
+ const ENV_APP_BASE_URL = 'GATEFORGE_APP_BASE_URL';
36
+ const ENV_STATE_DIR = 'GATEFORGE_STATE_DIR';
37
+ const ENV_RUN_ID = 'GATEFORGE_RUN_ID';
38
+ const RUN_HEADER = 'x-gateforge-run';
39
+ /**
40
+ * How long a tagged call waits for the drain to open the session. The
41
+ * wait starts AFTER the test spooled its own begin, so it bounds the
42
+ * supervisor's own work (one 50ms poll plus the witness round trip),
43
+ * never the runner's event delivery. An unanswered resolve still fails
44
+ * the test loudly — never a silent untagged pass.
45
+ */
46
+ const SESSION_RESOLVE_TIMEOUT_MS = 5_000;
47
+ /** Poll cadence for the session resolve (supervisor→witness latency). */
48
+ const SESSION_RESOLVE_POLL_MS = 50;
49
+ /** Tests whose worker-side end is already announced (one per test). */
50
+ const WORKER_END_ANNOUNCED = new Set();
51
+ /** Session credentials resolved per test id (one HTTP round-trip each). */
52
+ const RESOLVED_BY_TEST_ID = new Map();
53
+ /**
54
+ * The title path of the running test: the suite chain's names, then the
55
+ * leaf title (the same join the pack's Vitest reporter spools).
56
+ */
57
+ function titlePathOfCurrent(current) {
58
+ const segments = [];
59
+ let ancestor = current.suite ?? current.parent;
60
+ while (typeof ancestor === 'object' && ancestor !== null) {
61
+ const suite = ancestor;
62
+ if (suite.type !== 'suite')
63
+ break;
64
+ segments.unshift(suite.name ?? '');
65
+ ancestor = suite.suite ?? suite.parent;
66
+ }
67
+ segments.push(current.name);
68
+ return segments;
69
+ }
70
+ /**
71
+ * The reconciliation identity of the RUNNING test, exactly as the pack's
72
+ * Vitest reporter spools it: `<repo-relative posix file>#<titlePath>`.
73
+ *
74
+ * Returns:
75
+ * CurrentIdentity | null: the identity, or null outside a test.
76
+ */
77
+ function currentIdentity() {
78
+ const current = getCurrentTest();
79
+ if (current === undefined || current === null)
80
+ return null;
81
+ const file = current.file !== undefined && current.file !== null
82
+ ? fileRelativize(String(current.file.filepath))
83
+ : null;
84
+ if (file === null)
85
+ return null;
86
+ const titlePath = titlePathOfCurrent(current);
87
+ return { file, titlePath, testId: `${file}#${titlePath.join('>')}` };
88
+ }
89
+ /**
90
+ * Appends the running test's own `testBegin` to the run's lifecycle
91
+ * spool so the trusted drain can open its session without waiting for
92
+ * the runner's main-process reporter (see the call site). Best effort
93
+ * by contract: a failed append only means the reporter's own begin
94
+ * still has to arrive, never a crash and never a silent pass.
95
+ *
96
+ * Args:
97
+ * identity: the running test's reconciliation identity.
98
+ *
99
+ * Returns:
100
+ * void: nothing; the append is fire-and-forget.
101
+ */
102
+ function spoolTestBegin(identity) {
103
+ const stateDir = process.env[ENV_STATE_DIR];
104
+ const runId = process.env[ENV_RUN_ID];
105
+ if (stateDir === undefined || stateDir === '' || runId === undefined || runId === '')
106
+ return;
107
+ const claims = claimInjectionsFor(stateDir, identity.testId);
108
+ appendSpoolEvent(spoolPathFor(stateDir, runId), {
109
+ kind: 'testBegin',
110
+ testId: identity.testId,
111
+ // The SAME derived slot the pack's reporter writes (see
112
+ // ./worker-slot.ts): vitest runs files in parallel, so a flat slot
113
+ // would make two running tests fight over one session.
114
+ workerIndex: vitestWorkerSlot(identity.file),
115
+ file: identity.file,
116
+ titlePath: identity.titlePath,
117
+ project: null,
118
+ ...(claims.length > 0 ? { claims } : {}),
119
+ });
120
+ }
121
+ /**
122
+ * Spools the running test's own `testEnd` — the WORKER-side end, with
123
+ * NO outcome (only the runner's main process knows the result, and only
124
+ * its reporter may state it).
125
+ *
126
+ * Vitest runs the tests of one file one at a time in one worker, and
127
+ * the main process reports each finished test back over IPC: on a
128
+ * loaded machine that report can trail by seconds. The next test's
129
+ * begin therefore used to queue behind the PREVIOUS test's end, and its
130
+ * session resolve timed out. Announcing the end here, in the same
131
+ * process and the same order as the begin, removes that dependency: the
132
+ * drain releases the worker slot and opens the next session at once,
133
+ * while this test's outcome still arrives (and seals) from the
134
+ * reporter's own end.
135
+ *
136
+ * Registered through `onTestFinished`, which vitest runs after the test
137
+ * body AND after every `afterEach` hook and cleanup — the last worker
138
+ * callback before the test's result is final, so no consumer hook can
139
+ * lose its session to it. Best effort like the begin: a failed append
140
+ * only means the reporter's own end still governs the seal.
141
+ *
142
+ * Args:
143
+ * identity: the finished test's reconciliation identity.
144
+ *
145
+ * Returns:
146
+ * void: nothing; the append is fire-and-forget.
147
+ */
148
+ function spoolWorkerTestEnd(identity) {
149
+ const stateDir = process.env[ENV_STATE_DIR];
150
+ const runId = process.env[ENV_RUN_ID];
151
+ if (stateDir === undefined || stateDir === '' || runId === undefined || runId === '')
152
+ return;
153
+ appendSpoolEvent(spoolPathFor(stateDir, runId), {
154
+ kind: 'testEnd',
155
+ testId: identity.testId,
156
+ workerIndex: vitestWorkerSlot(identity.file),
157
+ file: identity.file,
158
+ titlePath: identity.titlePath,
159
+ project: null,
160
+ // No `outcome`: this event says the test is over, never how it went.
161
+ });
162
+ }
163
+ /**
164
+ * Registers the worker-side end for the RUNNING test (once per test:
165
+ * a repeated registration is a re-delivery the drain ignores).
166
+ *
167
+ * Args:
168
+ * identity: the running test's reconciliation identity.
169
+ *
170
+ * Returns:
171
+ * void: nothing; the registration is best effort.
172
+ */
173
+ function announceWorkerTestEnd(identity) {
174
+ if (WORKER_END_ANNOUNCED.has(identity.testId))
175
+ return;
176
+ WORKER_END_ANNOUNCED.add(identity.testId);
177
+ try {
178
+ onTestFinished(() => {
179
+ spoolWorkerTestEnd(identity);
180
+ });
181
+ }
182
+ catch {
183
+ // Outside a test (or in a vitest that does not expose the hook):
184
+ // the reporter's own end still seals the session, so this is a
185
+ // missing optimisation, never a wrong verdict.
186
+ }
187
+ }
188
+ /** Repo-relative posix form of an absolute module path (process.cwd()). */
189
+ function fileRelativize(path) {
190
+ const clean = path.replace(/^file:\/\//, '');
191
+ return (isAbsolute(clean) ? relative(process.cwd(), clean) : clean).split('\\').join('/');
192
+ }
193
+ /**
194
+ * Resolves the supervisor-issued session credential for the RUNNING
195
+ * test (polls briefly: the drain opens the session asynchronously after
196
+ * the reporter's testBegin event lands).
197
+ *
198
+ * Returns:
199
+ * Promise<SessionCredential>: the open session's credential.
200
+ *
201
+ * Throws:
202
+ * Error: when the wiring is absent, the test is not a gateforge-tagged
203
+ * vitest test, or no open session answers — fail closed.
204
+ */
205
+ async function resolveCurrentSession() {
206
+ const witnessUrl = process.env[ENV_WITNESS_URL];
207
+ const runToken = process.env[ENV_RUN_TOKEN];
208
+ if (witnessUrl === undefined ||
209
+ witnessUrl === '' ||
210
+ runToken === undefined ||
211
+ runToken === '' ||
212
+ process.env['GATEFORGE_STATE_DIR'] === undefined ||
213
+ process.env['GATEFORGE_RUN_ID'] === undefined) {
214
+ throw new Error(humanMessage({
215
+ type: 'witness-wiring-missing',
216
+ detail: 'this vitest test addresses the witness without the supervised wiring ' +
217
+ `(missing ${ENV_WITNESS_URL} / ${ENV_RUN_TOKEN} / GATEFORGE_STATE_DIR / GATEFORGE_RUN_ID)`,
218
+ nextAction: 'run under gateforge test-gates or the VitestRunnerAdapter',
219
+ }));
220
+ }
221
+ const identity = currentIdentity();
222
+ if (identity === null) {
223
+ throw new Error(humanMessage({
224
+ type: 'witness-session-unknown-test',
225
+ detail: 'gateforgeSupertest can only tag traffic inside a running vitest test',
226
+ nextAction: 'call it from the test body',
227
+ }));
228
+ }
229
+ const cached = RESOLVED_BY_TEST_ID.get(identity.testId);
230
+ if (cached !== undefined)
231
+ return cached;
232
+ // The lifecycle `testBegin` the drain opens this session from is
233
+ // normally spooled by the pack's reporter — which runs in vitest's
234
+ // MAIN process, whose event delivery trails the worker by as much as
235
+ // the whole test (its transform and reporter work is not the test's).
236
+ // The running test therefore announces its OWN begin before it waits:
237
+ // the spool is the untrusted runner's channel either way, the drain's
238
+ // open is idempotent for a repeated begin of the same test, and the
239
+ // witness still refuses any identity outside the registered expected
240
+ // set — so this races the reporter, it never widens what the suite can
241
+ // prove.
242
+ spoolTestBegin(identity);
243
+ // The same reasoning, mirrored for the END of this test: the reporter
244
+ // in the main process may be seconds behind, and the NEXT test of
245
+ // this file would otherwise queue its begin behind that report. The
246
+ // worker announces its own end (without an outcome — the reporter
247
+ // alone states the verdict), so the drain can free the slot now.
248
+ announceWorkerTestEnd(identity);
249
+ const deadline = Date.now() + SESSION_RESOLVE_TIMEOUT_MS;
250
+ let lastDetail = 'no answer';
251
+ while (Date.now() < deadline) {
252
+ const response = await fetch(`${witnessUrl.replace(/\/$/, '')}/sessions/resolve`, {
253
+ method: 'POST',
254
+ headers: { 'content-type': 'application/json', [RUN_HEADER]: runToken },
255
+ body: JSON.stringify({ testId: identity.testId, workerIndex: vitestWorkerSlot(identity.file) }),
256
+ });
257
+ if (response.ok) {
258
+ const body = (await response.json());
259
+ if (typeof body.sessionId === 'string' && body.sessionId !== '') {
260
+ const credential = {
261
+ sessionId: body.sessionId,
262
+ sessionToken: String(body.sessionToken ?? ''),
263
+ proxyUrl: typeof body.proxyUrl === 'string' && body.proxyUrl !== '' ? body.proxyUrl : null,
264
+ };
265
+ RESOLVED_BY_TEST_ID.set(identity.testId, credential);
266
+ return credential;
267
+ }
268
+ lastDetail = 'malformed resolve response';
269
+ }
270
+ else if (response.status !== 404) {
271
+ lastDetail = `HTTP ${String(response.status)} from /sessions/resolve`;
272
+ }
273
+ await new Promise((resolveSleep) => setTimeout(resolveSleep, SESSION_RESOLVE_POLL_MS));
274
+ }
275
+ throw new Error(humanMessage({
276
+ type: 'witness-session-unresolved',
277
+ detail: `no open witness session answered for test '${identity.testId}' within ${String(SESSION_RESOLVE_TIMEOUT_MS)}ms (${lastDetail})`,
278
+ nextAction: 'run under the supervised window so the drain opens a session per started test',
279
+ }));
280
+ }
281
+ /**
282
+ * Rewrites an app-origin URL onto the session proxy origin (same
283
+ * loopback discipline as the Playwright page routing): the path, query,
284
+ * and hash stay exactly as the suite addressed them.
285
+ *
286
+ * A base URL with no path of its own (`http://127.0.0.1:3000`) becomes
287
+ * the bare proxy ORIGIN, never `<origin>/`: supertest concatenates the
288
+ * per-request path onto the address it is handed, so a trailing slash
289
+ * here would address `//api/accounts`.
290
+ */
291
+ function rewriteThroughProxy(target, proxyUrl, appBaseUrl) {
292
+ if (appBaseUrl === '')
293
+ return target;
294
+ try {
295
+ const app = new URL(appBaseUrl);
296
+ const proxy = new URL(proxyUrl);
297
+ const parsed = new URL(target, appBaseUrl);
298
+ if (parsed.origin !== app.origin)
299
+ return target;
300
+ const bare = parsed.pathname === '/' && parsed.search === '' && parsed.hash === '';
301
+ return `${proxy.origin}${bare ? '' : parsed.pathname}${parsed.search}${parsed.hash}`;
302
+ }
303
+ catch {
304
+ return target;
305
+ }
306
+ }
307
+ /**
308
+ * Wraps the suite's supertest entry with the gateforge session channel.
309
+ *
310
+ * Args:
311
+ * request: the suite's own supertest import (the callable). Only the
312
+ * URL-string host mode is supported: in-process supertest calls the
313
+ * app directly, which bypasses the witness proxy entirely.
314
+ *
315
+ * Returns:
316
+ * (target: string) => Promise<SupertestTest>: the tagged request
317
+ * factory. Pass it the app base URL (or an absolute URL) and AWAIT
318
+ * it: the session must be resolved from the supervisor before any
319
+ * request is addressed, so the suite reads
320
+ * `await (await gate(app)).post('/api/accounts').send(...)`. A
321
+ * non-string target (the app itself) throws immediately with the
322
+ * refusal, before any request is sent.
323
+ */
324
+ export function gateforgeSupertest(request) {
325
+ if (typeof request !== 'function') {
326
+ throw new TypeError('gateforgeSupertest expects the supertest callable (the default export)');
327
+ }
328
+ return (target) => {
329
+ if (typeof target !== 'string' || target.trim() === '') {
330
+ // The in-process mode fails LOUDLY: supertest would call the app
331
+ // function/object directly, no packet ever crosses the session
332
+ // proxy, and the "pass" would be counterfeit.
333
+ throw new Error(humanMessage({
334
+ type: 'in-process-client-refused',
335
+ detail: 'supertest was handed the app itself (in-process mode), which bypasses the gateforge ' +
336
+ 'session proxy entirely — such a request can never be witnessed evidence, so it is ' +
337
+ 'refused instead of silently passing',
338
+ nextAction: 'pass the app base URL (await gate(baseUrl)), not the app instance',
339
+ }));
340
+ }
341
+ const appBaseUrl = process.env[ENV_APP_BASE_URL] ?? '';
342
+ return (async () => {
343
+ const credential = await resolveCurrentSession();
344
+ if (credential.proxyUrl === null) {
345
+ throw new Error(humanMessage({
346
+ type: 'witness-proxy-missing',
347
+ detail: 'the witness issued no per-session proxy for this run — the run must wire an ' +
348
+ 'observation proxy so supertest traffic can be attributed to the session',
349
+ nextAction: 'wire the witness with proxyTarget (test-gates does this for strict runs)',
350
+ }));
351
+ }
352
+ return request(rewriteThroughProxy(target, credential.proxyUrl, appBaseUrl));
353
+ })();
354
+ };
355
+ }
356
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/vitest/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * TEST SEAM (inert unless set): the absolute path of a BARRIER FILE
3
+ * this reporter waits for before it spools ANY `testEnd`.
4
+ *
5
+ * The lag between a worker finishing a test and this reporter seeing
6
+ * its result is exactly the race a serial project must survive, and it
7
+ * is load-dependent — a timing assertion can never pin it. The barrier
8
+ * makes that lag deterministic: whoever lifts the file holds the
9
+ * previous test's end for exactly as long as it wants, with no sleep,
10
+ * no CPU load and no wall-clock guess. Unset in every real run, so a
11
+ * supervised run is byte-for-byte the same run.
12
+ */
13
+ export declare const VITEST_END_BARRIER_ENV = "GATEFORGE_VITEST_END_BARRIER";
14
+ /** One observed Vitest test entity (structural subset of vitest's TestCase). */
15
+ interface VitestTestCase {
16
+ /** The runner-assigned test id (a string in vitest 3.x). */
17
+ readonly id: string;
18
+ /** The leaf test title. */
19
+ readonly name: string;
20
+ /** The suite chain (vitest 3.x entities carry `parent`). */
21
+ readonly parent?: {
22
+ readonly name: string;
23
+ readonly type: string;
24
+ readonly parent?: unknown;
25
+ } | null;
26
+ /** Legacy alias for the suite chain (tasks carry `suite`). */
27
+ readonly suite?: {
28
+ readonly name: string;
29
+ readonly type: string;
30
+ readonly suite?: unknown;
31
+ } | null;
32
+ /** The owning module. */
33
+ readonly module: {
34
+ readonly moduleId: string;
35
+ };
36
+ /** The observed result (state is `passed`/`failed`/`skipped` when done). */
37
+ readonly result?: () => {
38
+ readonly state?: string;
39
+ readonly errors?: readonly unknown[];
40
+ } | undefined;
41
+ /** The raw task (retryCount lives on `task.result.retryCount`). */
42
+ readonly task?: {
43
+ readonly result?: {
44
+ readonly retryCount?: number;
45
+ } | null;
46
+ };
47
+ }
48
+ /**
49
+ * The gateforge Vitest reporter. No options today; the constructor
50
+ * signature is Vitest's custom-reporter contract (options object).
51
+ */
52
+ export default class GateforgeVitestReporter {
53
+ private readonly stateDir;
54
+ private readonly spoolFile;
55
+ private readonly flagsFile;
56
+ private readonly endBarrier;
57
+ private retriesDetected;
58
+ constructor(_options?: object);
59
+ /** Spools one lifecycle event (never crashes the run on failure). */
60
+ private appendEvent;
61
+ /** testBegin: the trusted drain opens the session for this identity. */
62
+ onTestCaseReady(testCase: VitestTestCase): void;
63
+ /**
64
+ * testEnd: the observed outcome plus the attempt count (a
65
+ * runner-assisted retry shows up as retryCount > 0 and is recorded in
66
+ * the runner-flags document, which the adapter surfaces as
67
+ * `retriesDetected` — required retries are zero). When the test-seam
68
+ * end barrier is set, the event is held until the barrier file
69
+ * appears: the worker's lifecycle then races this reporter exactly as
70
+ * it does on a loaded machine, but deterministically.
71
+ */
72
+ onTestCaseResult(testCase: VitestTestCase): Promise<void>;
73
+ /** Seals the runner-flags document with the observed retry signal. */
74
+ onTestRunEnd(): void;
75
+ /**
76
+ * Mapped obligation ids for one reconciliation key (from the
77
+ * CLI-written claim-injections document; malformed documents
78
+ * contribute nothing).
79
+ */
80
+ private claimsFor;
81
+ }
82
+ export {};
83
+ //# sourceMappingURL=reporter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reporter.d.ts","sourceRoot":"","sources":["../../src/vitest/reporter.ts"],"names":[],"mappings":"AAqCA;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,sBAAsB,iCAAiC,CAAC;AA+BrE,gFAAgF;AAChF,UAAU,cAAc;IACtB,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,2BAA2B;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;IACrG,8DAA8D;IAC9D,QAAQ,CAAC,KAAK,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;IACnG,yBAAyB;IACzB,QAAQ,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C,4EAA4E;IAC5E,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM;QAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,OAAO,EAAE,CAAA;KAAE,GAAG,SAAS,CAAC;IACtG,mEAAmE;IACnE,QAAQ,CAAC,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE;YAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAA;KAAE,CAAC;CAC/E;AAmDD;;;GAGG;AACH,MAAM,CAAC,OAAO,OAAO,uBAAuB;IAC1C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgB;IACzC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgB;IAC1C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgB;IAC1C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAgB;IAC3C,OAAO,CAAC,eAAe,CAAS;gBAEpB,QAAQ,GAAE,MAAW;IAUjC,qEAAqE;IACrE,OAAO,CAAC,WAAW;IAUnB,wEAAwE;IACxE,eAAe,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI;IAkB/C;;;;;;;;OAQG;IACG,gBAAgB,CAAC,QAAQ,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IA0B/D,sEAAsE;IACtE,YAAY,IAAI,IAAI;IAcpB;;;;OAIG;IACH,OAAO,CAAC,SAAS;CAKlB"}
@@ -0,0 +1,211 @@
1
+ /**
2
+ * The gateforge Vitest reporter (plan 2026-09-25 phase 4): the
3
+ * runner-side half of the Vitest+supertest witness channel.
4
+ *
5
+ * While the supervised Vitest run executes, this reporter writes the
6
+ * same lifecycle spool events the Playwright reporter writes
7
+ * (testBegin / testEnd with the test identity, outcome, attempt) plus a
8
+ * runner-flags document recording runner-assisted retries — the
9
+ * TRUSTED CLI drain reads the spool and performs the witness's
10
+ * supervisor calls (session open/seal); this process holds no
11
+ * supervisor rights at all.
12
+ *
13
+ * Identity: a test's reconciliation key is
14
+ * `<repo-relative posix file>#<title path joined by '>'>` — the same
15
+ * key shape every runner adapter uses. The in-test helper
16
+ * (`@gate-forge/pack-playwright/vitest`) resolves its session by the
17
+ * EXACT identity this reporter writes, so the two must never drift.
18
+ *
19
+ * The reporter is INERT without supervised wiring: without the
20
+ * run-scoped `GATEFORGE_STATE_DIR`/`GATEFORGE_RUN_ID` variables every
21
+ * hook is a no-op, so a plain `vitest run` behaves exactly as without
22
+ * the reporter.
23
+ *
24
+ * Loaded by the VitestRunnerAdapter through `--reporter=<this module>`;
25
+ * Vitest instantiates custom reporters with `new`.
26
+ */
27
+ import { appendFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
28
+ import { dirname, join, relative } from 'node:path';
29
+ import { canonicalOf } from '../json.js';
30
+ import { failureDiagnosisOf } from '../diagnosis.js';
31
+ import { claimInjectionsFor } from '../runner-claims.js';
32
+ import { vitestWorkerSlot } from './worker-slot.js';
33
+ /** Env the runner child receives from the adapter's allowlist. */
34
+ const ENV_STATE_DIR = 'GATEFORGE_STATE_DIR';
35
+ const ENV_RUN_ID = 'GATEFORGE_RUN_ID';
36
+ /**
37
+ * TEST SEAM (inert unless set): the absolute path of a BARRIER FILE
38
+ * this reporter waits for before it spools ANY `testEnd`.
39
+ *
40
+ * The lag between a worker finishing a test and this reporter seeing
41
+ * its result is exactly the race a serial project must survive, and it
42
+ * is load-dependent — a timing assertion can never pin it. The barrier
43
+ * makes that lag deterministic: whoever lifts the file holds the
44
+ * previous test's end for exactly as long as it wants, with no sleep,
45
+ * no CPU load and no wall-clock guess. Unset in every real run, so a
46
+ * supervised run is byte-for-byte the same run.
47
+ */
48
+ export const VITEST_END_BARRIER_ENV = 'GATEFORGE_VITEST_END_BARRIER';
49
+ /** Bound on the barrier wait (a barrier nobody lifts must never hang the run). */
50
+ const END_BARRIER_TIMEOUT_MS = 60_000;
51
+ /** Poll cadence while the barrier is closed (the wait is on a file, never on a guess). */
52
+ const END_BARRIER_POLL_MS = 25;
53
+ /** The runner-flags document the adapter merges into its envelope. */
54
+ const RUNNER_FLAGS_FILE = 'runner-flags.json';
55
+ /**
56
+ * Waits until the end-barrier file exists (see {@link VITEST_END_BARRIER_ENV}).
57
+ *
58
+ * Returns at once when the seam is unset or the file is already there;
59
+ * a barrier nobody lifts times out with a warning instead of hanging
60
+ * the runner forever.
61
+ *
62
+ * Returns:
63
+ * Promise<void>: resolves once the barrier is open (or the bound elapsed).
64
+ */
65
+ async function awaitEndBarrier(barrier) {
66
+ if (barrier === null || barrier === '')
67
+ return;
68
+ const deadline = Date.now() + END_BARRIER_TIMEOUT_MS;
69
+ while (Date.now() < deadline) {
70
+ if (existsSync(barrier))
71
+ return;
72
+ await new Promise((resolveSleep) => setTimeout(resolveSleep, END_BARRIER_POLL_MS));
73
+ }
74
+ console.warn(`[gateforge] the vitest reporter end barrier '${barrier}' was never lifted; continuing`);
75
+ }
76
+ /**
77
+ * The title path of one test entity: the suite chain's names, then the
78
+ * leaf title (the same join `vitest list --json` prints with ' > ').
79
+ */
80
+ function titlePathOf(testCase) {
81
+ const segments = [];
82
+ let current = testCase.parent ?? testCase.suite;
83
+ while (typeof current === 'object' && current !== null) {
84
+ const suite = current;
85
+ if (suite.type !== 'suite')
86
+ break;
87
+ segments.unshift(suite.name);
88
+ current = suite.parent ?? suite.suite;
89
+ }
90
+ segments.push(testCase.name);
91
+ return segments;
92
+ }
93
+ /**
94
+ * The reconciliation identity of one test entity: repo-relative posix
95
+ * file plus the title path, keyed `<file>#<titlePath.join('>')>`.
96
+ */
97
+ function identityOf(testCase) {
98
+ const file = relative(process.cwd(), testCase.module.moduleId).split('\\').join('/');
99
+ const titlePath = titlePathOf(testCase);
100
+ return { file, titlePath, testId: `${file}#${titlePath.join('>')}` };
101
+ }
102
+ /**
103
+ * The gateforge Vitest reporter. No options today; the constructor
104
+ * signature is Vitest's custom-reporter contract (options object).
105
+ */
106
+ export default class GateforgeVitestReporter {
107
+ stateDir;
108
+ spoolFile;
109
+ flagsFile;
110
+ endBarrier;
111
+ retriesDetected = false;
112
+ constructor(_options = {}) {
113
+ const stateDir = process.env[ENV_STATE_DIR];
114
+ const runId = process.env[ENV_RUN_ID];
115
+ const wired = stateDir !== undefined && stateDir !== '' && runId !== undefined && runId !== '';
116
+ this.stateDir = wired ? stateDir : null;
117
+ this.spoolFile = wired ? join(stateDir, 'spool', runId, 'events.jsonl') : null;
118
+ this.flagsFile = wired ? join(stateDir, 'vitest', runId, RUNNER_FLAGS_FILE) : null;
119
+ this.endBarrier = process.env[VITEST_END_BARRIER_ENV] ?? null;
120
+ }
121
+ /** Spools one lifecycle event (never crashes the run on failure). */
122
+ appendEvent(event) {
123
+ if (this.spoolFile === null)
124
+ return;
125
+ try {
126
+ mkdirSync(dirname(this.spoolFile), { recursive: true });
127
+ appendFileSync(this.spoolFile, `${canonicalOf(event)}\n`, 'utf8');
128
+ }
129
+ catch (error) {
130
+ console.warn(`[gateforge] cannot append to the lifecycle spool: ${error.message}`);
131
+ }
132
+ }
133
+ /** testBegin: the trusted drain opens the session for this identity. */
134
+ onTestCaseReady(testCase) {
135
+ if (this.spoolFile === null)
136
+ return;
137
+ const identity = identityOf(testCase);
138
+ // The slot is derived from the file both processes agree on (see
139
+ // ./worker-slot.ts): vitest runs FILES in parallel, so one flat slot
140
+ // would collide two running tests and fail a genuinely parallel
141
+ // project closed.
142
+ this.appendEvent({
143
+ kind: 'testBegin',
144
+ testId: identity.testId,
145
+ workerIndex: vitestWorkerSlot(identity.file),
146
+ file: identity.file,
147
+ titlePath: identity.titlePath,
148
+ project: null,
149
+ ...this.claimsFor(identity),
150
+ });
151
+ }
152
+ /**
153
+ * testEnd: the observed outcome plus the attempt count (a
154
+ * runner-assisted retry shows up as retryCount > 0 and is recorded in
155
+ * the runner-flags document, which the adapter surfaces as
156
+ * `retriesDetected` — required retries are zero). When the test-seam
157
+ * end barrier is set, the event is held until the barrier file
158
+ * appears: the worker's lifecycle then races this reporter exactly as
159
+ * it does on a loaded machine, but deterministically.
160
+ */
161
+ async onTestCaseResult(testCase) {
162
+ if (this.spoolFile === null)
163
+ return;
164
+ await awaitEndBarrier(this.endBarrier);
165
+ const identity = identityOf(testCase);
166
+ const result = testCase.result?.();
167
+ const state = result?.state;
168
+ const outcome = state === 'passed' || state === 'failed' || state === 'skipped' ? state : 'failed';
169
+ const retryCount = testCase.task?.result?.retryCount ?? 0;
170
+ if (retryCount > 0)
171
+ this.retriesDetected = true;
172
+ this.appendEvent({
173
+ kind: 'testEnd',
174
+ testId: identity.testId,
175
+ workerIndex: vitestWorkerSlot(identity.file),
176
+ file: identity.file,
177
+ titlePath: identity.titlePath,
178
+ project: null,
179
+ outcome,
180
+ attempt: retryCount + 1,
181
+ // Additive diagnosis for a non-passing test: the CI progress
182
+ // stream and the failures artifact are the only readers, and the
183
+ // CLI screens the message before printing or writing it.
184
+ ...(outcome === 'passed' ? {} : (failureDiagnosisOf(result?.errors) ?? {})),
185
+ });
186
+ }
187
+ /** Seals the runner-flags document with the observed retry signal. */
188
+ onTestRunEnd() {
189
+ if (this.flagsFile === null)
190
+ return;
191
+ try {
192
+ mkdirSync(dirname(this.flagsFile), { recursive: true });
193
+ writeFileSync(this.flagsFile, `${canonicalOf({ schemaVersion: 1, retriesDetected: this.retriesDetected })}\n`, 'utf8');
194
+ }
195
+ catch (error) {
196
+ console.warn(`[gateforge] cannot write the runner flags: ${error.message}`);
197
+ }
198
+ }
199
+ /**
200
+ * Mapped obligation ids for one reconciliation key (from the
201
+ * CLI-written claim-injections document; malformed documents
202
+ * contribute nothing).
203
+ */
204
+ claimsFor(identity) {
205
+ if (this.stateDir === null)
206
+ return {};
207
+ const claims = claimInjectionsFor(this.stateDir, identity.testId);
208
+ return claims.length > 0 ? { claims } : {};
209
+ }
210
+ }
211
+ //# sourceMappingURL=reporter.js.map