velocious 1.0.666 → 1.0.668

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 (174) hide show
  1. package/README.md +40 -11
  2. package/build/background-jobs/adapter-client.js +22 -0
  3. package/build/background-jobs/adapter.js +15 -0
  4. package/build/background-jobs/client.js +147 -1
  5. package/build/background-jobs/job-semantics.js +30 -0
  6. package/build/background-jobs/job.js +26 -1
  7. package/build/background-jobs/local-adapter.js +36 -8
  8. package/build/background-jobs/local-store.js +410 -39
  9. package/build/background-jobs/main.js +69 -2
  10. package/build/background-jobs/platform-job.js +26 -1
  11. package/build/background-jobs/runtime.js +58 -0
  12. package/build/background-jobs/store.js +374 -6
  13. package/build/background-jobs/types.js +27 -2
  14. package/build/background-jobs/web/controller.js +1 -0
  15. package/build/configuration.js +30 -6
  16. package/build/database/drivers/mssql/column.js +2 -2
  17. package/build/database/drivers/mssql/table.js +19 -5
  18. package/build/database/pool/async-tracked-multi-connection.js +2 -1
  19. package/build/database/query/alter-table-base.js +8 -1
  20. package/build/database/record/attachments/store.js +2 -2
  21. package/build/environment-handlers/node/cli/commands/test/timing-manifest/merge.js +2 -48
  22. package/build/environment-handlers/node/cli/commands/test.js +17 -69
  23. package/build/frontend-model-controller.js +22 -2
  24. package/build/frontend-models/websocket-channel.js +18 -0
  25. package/build/http-server/client/websocket-session.js +41 -4
  26. package/build/http-server/websocket-channel.js +14 -0
  27. package/build/http-server/websocket-event-log-store.js +85 -12
  28. package/build/http-server/websocket-events-host.js +17 -7
  29. package/build/src/background-jobs/adapter-client.d.ts +17 -0
  30. package/build/src/background-jobs/adapter-client.d.ts.map +1 -1
  31. package/build/src/background-jobs/adapter-client.js +19 -1
  32. package/build/src/background-jobs/adapter.d.ts +15 -0
  33. package/build/src/background-jobs/adapter.d.ts.map +1 -1
  34. package/build/src/background-jobs/adapter.js +14 -1
  35. package/build/src/background-jobs/client.d.ts +17 -0
  36. package/build/src/background-jobs/client.d.ts.map +1 -1
  37. package/build/src/background-jobs/client.js +129 -1
  38. package/build/src/background-jobs/job-semantics.d.ts +18 -0
  39. package/build/src/background-jobs/job-semantics.d.ts.map +1 -1
  40. package/build/src/background-jobs/job-semantics.js +28 -1
  41. package/build/src/background-jobs/job.d.ts +15 -0
  42. package/build/src/background-jobs/job.d.ts.map +1 -1
  43. package/build/src/background-jobs/job.js +21 -2
  44. package/build/src/background-jobs/local-adapter.d.ts +23 -8
  45. package/build/src/background-jobs/local-adapter.d.ts.map +1 -1
  46. package/build/src/background-jobs/local-adapter.js +35 -9
  47. package/build/src/background-jobs/local-store.d.ts +120 -5
  48. package/build/src/background-jobs/local-store.d.ts.map +1 -1
  49. package/build/src/background-jobs/local-store.js +366 -40
  50. package/build/src/background-jobs/main.d.ts +24 -2
  51. package/build/src/background-jobs/main.d.ts.map +1 -1
  52. package/build/src/background-jobs/main.js +68 -3
  53. package/build/src/background-jobs/platform-job.d.ts +15 -0
  54. package/build/src/background-jobs/platform-job.d.ts.map +1 -1
  55. package/build/src/background-jobs/platform-job.js +19 -2
  56. package/build/src/background-jobs/runtime.d.ts +39 -0
  57. package/build/src/background-jobs/runtime.d.ts.map +1 -1
  58. package/build/src/background-jobs/runtime.js +49 -1
  59. package/build/src/background-jobs/store.d.ts +100 -1
  60. package/build/src/background-jobs/store.d.ts.map +1 -1
  61. package/build/src/background-jobs/store.js +334 -8
  62. package/build/src/background-jobs/types.d.ts +97 -4
  63. package/build/src/background-jobs/types.d.ts.map +1 -1
  64. package/build/src/background-jobs/types.js +28 -3
  65. package/build/src/background-jobs/web/controller.d.ts.map +1 -1
  66. package/build/src/background-jobs/web/controller.js +2 -1
  67. package/build/src/configuration.d.ts +18 -1
  68. package/build/src/configuration.d.ts.map +1 -1
  69. package/build/src/configuration.js +28 -6
  70. package/build/src/database/drivers/mssql/column.d.ts.map +1 -1
  71. package/build/src/database/drivers/mssql/column.js +2 -2
  72. package/build/src/database/drivers/mssql/table.d.ts.map +1 -1
  73. package/build/src/database/drivers/mssql/table.js +19 -5
  74. package/build/src/database/pool/async-tracked-multi-connection.d.ts.map +1 -1
  75. package/build/src/database/pool/async-tracked-multi-connection.js +3 -2
  76. package/build/src/database/query/alter-table-base.d.ts.map +1 -1
  77. package/build/src/database/query/alter-table-base.js +9 -2
  78. package/build/src/database/record/attachments/store.js +3 -3
  79. package/build/src/environment-handlers/node/cli/commands/test/timing-manifest/merge.d.ts.map +1 -1
  80. package/build/src/environment-handlers/node/cli/commands/test/timing-manifest/merge.js +3 -47
  81. package/build/src/environment-handlers/node/cli/commands/test.d.ts +2 -28
  82. package/build/src/environment-handlers/node/cli/commands/test.d.ts.map +1 -1
  83. package/build/src/environment-handlers/node/cli/commands/test.js +10 -61
  84. package/build/src/frontend-model-controller.d.ts.map +1 -1
  85. package/build/src/frontend-model-controller.js +18 -3
  86. package/build/src/frontend-models/websocket-channel.d.ts +9 -0
  87. package/build/src/frontend-models/websocket-channel.d.ts.map +1 -1
  88. package/build/src/frontend-models/websocket-channel.js +16 -1
  89. package/build/src/http-server/client/websocket-session.d.ts +25 -2
  90. package/build/src/http-server/client/websocket-session.d.ts.map +1 -1
  91. package/build/src/http-server/client/websocket-session.js +40 -5
  92. package/build/src/http-server/websocket-channel.d.ts +11 -0
  93. package/build/src/http-server/websocket-channel.d.ts.map +1 -1
  94. package/build/src/http-server/websocket-channel.js +14 -1
  95. package/build/src/http-server/websocket-event-log-store.d.ts +60 -41
  96. package/build/src/http-server/websocket-event-log-store.d.ts.map +1 -1
  97. package/build/src/http-server/websocket-event-log-store.js +79 -12
  98. package/build/src/http-server/websocket-events-host.d.ts +10 -3
  99. package/build/src/http-server/websocket-events-host.d.ts.map +1 -1
  100. package/build/src/http-server/websocket-events-host.js +17 -8
  101. package/build/src/testing/test-files-finder.d.ts +17 -117
  102. package/build/src/testing/test-files-finder.d.ts.map +1 -1
  103. package/build/src/testing/test-files-finder.js +29 -294
  104. package/build/src/testing/test-filter-parser.d.ts +32 -22
  105. package/build/src/testing/test-filter-parser.d.ts.map +1 -1
  106. package/build/src/testing/test-filter-parser.js +25 -217
  107. package/build/src/testing/test-profile-activity.d.ts +1 -6
  108. package/build/src/testing/test-profile-activity.d.ts.map +1 -1
  109. package/build/src/testing/test-profile-activity.js +2 -12
  110. package/build/src/testing/test-profile-output.d.ts +1 -40
  111. package/build/src/testing/test-profile-output.d.ts.map +1 -1
  112. package/build/src/testing/test-profile-output.js +2 -217
  113. package/build/src/testing/test-profiler.d.ts +15 -659
  114. package/build/src/testing/test-profiler.d.ts.map +1 -1
  115. package/build/src/testing/test-profiler.js +35 -833
  116. package/build/src/testing/test-runner.d.ts +10 -1
  117. package/build/src/testing/test-runner.d.ts.map +1 -1
  118. package/build/src/testing/test-runner.js +26 -4
  119. package/build/src/testing/test-suite-splitter.d.ts +1 -128
  120. package/build/src/testing/test-suite-splitter.d.ts.map +1 -1
  121. package/build/src/testing/test-suite-splitter.js +2 -259
  122. package/build/src/testing/timing-manifest.d.ts +5 -93
  123. package/build/src/testing/timing-manifest.d.ts.map +1 -1
  124. package/build/src/testing/timing-manifest.js +4 -283
  125. package/build/src/testing/velocious-attempt-executor.d.ts.map +1 -1
  126. package/build/src/testing/velocious-attempt-executor.js +8 -2
  127. package/build/testing/test-files-finder.js +32 -343
  128. package/build/testing/test-filter-parser.js +29 -248
  129. package/build/testing/test-profile-activity.js +1 -12
  130. package/build/testing/test-profile-output.js +8 -253
  131. package/build/testing/test-profiler.js +38 -895
  132. package/build/testing/test-runner.js +33 -9
  133. package/build/testing/test-suite-splitter.js +1 -301
  134. package/build/testing/timing-manifest.js +10 -344
  135. package/build/testing/velocious-attempt-executor.js +7 -1
  136. package/package.json +3 -3
  137. package/scripts/test-browser.js +30 -6
  138. package/scripts/verify-docker-dev-environment.js +13 -1
  139. package/src/background-jobs/adapter-client.js +22 -0
  140. package/src/background-jobs/adapter.js +15 -0
  141. package/src/background-jobs/client.js +147 -1
  142. package/src/background-jobs/job-semantics.js +30 -0
  143. package/src/background-jobs/job.js +26 -1
  144. package/src/background-jobs/local-adapter.js +36 -8
  145. package/src/background-jobs/local-store.js +410 -39
  146. package/src/background-jobs/main.js +69 -2
  147. package/src/background-jobs/platform-job.js +26 -1
  148. package/src/background-jobs/runtime.js +58 -0
  149. package/src/background-jobs/store.js +374 -6
  150. package/src/background-jobs/types.js +27 -2
  151. package/src/background-jobs/web/controller.js +1 -0
  152. package/src/configuration.js +30 -6
  153. package/src/database/drivers/mssql/column.js +2 -2
  154. package/src/database/drivers/mssql/table.js +19 -5
  155. package/src/database/pool/async-tracked-multi-connection.js +2 -1
  156. package/src/database/query/alter-table-base.js +8 -1
  157. package/src/database/record/attachments/store.js +2 -2
  158. package/src/environment-handlers/node/cli/commands/test/timing-manifest/merge.js +2 -48
  159. package/src/environment-handlers/node/cli/commands/test.js +17 -69
  160. package/src/frontend-model-controller.js +22 -2
  161. package/src/frontend-models/websocket-channel.js +18 -0
  162. package/src/http-server/client/websocket-session.js +41 -4
  163. package/src/http-server/websocket-channel.js +14 -0
  164. package/src/http-server/websocket-event-log-store.js +85 -12
  165. package/src/http-server/websocket-events-host.js +17 -7
  166. package/src/testing/test-files-finder.js +32 -343
  167. package/src/testing/test-filter-parser.js +29 -248
  168. package/src/testing/test-profile-activity.js +1 -12
  169. package/src/testing/test-profile-output.js +8 -253
  170. package/src/testing/test-profiler.js +38 -895
  171. package/src/testing/test-runner.js +33 -9
  172. package/src/testing/test-suite-splitter.js +1 -301
  173. package/src/testing/timing-manifest.js +10 -344
  174. package/src/testing/velocious-attempt-executor.js +7 -1
@@ -1,918 +1,61 @@
1
1
  // @ts-check
2
2
 
3
- import { createHash } from "node:crypto"
4
- import path from "node:path"
5
- import { registerTestProfileContextReader } from "./test-profile-context.js"
6
- import { validateTestActivityName } from "./test-profile-activity.js"
7
- import { compareTimingManifestPaths } from "./timing-manifest.js"
8
-
9
- /** @typedef {"passed" | "failed" | "interrupted" | "timed-out"} TestProfileAttemptStatus */
10
- /** @typedef {{user: number, system: number}} ProcessCpuUsage */
11
- /** @typedef {{queryCount: number, failedQueryCount: number, totalMs: number, maxMs: number}} ProfileDatabaseAggregate */
12
- /** @typedef {{count: number, failedCount: number, totalMs: number, maxMs: number}} ProfileActionAggregate */
13
- /** @typedef {{identifier: string, connectionCreation: ProfileActionAggregate, checkoutWait: {count: number, totalMs: number, maxMs: number}, checkoutTimeoutCount: number, idleReap: ProfileActionAggregate & {disposalCount: number}, peakLiveConnections: number}} ProfilePoolAggregate */
14
-
15
- /**
16
- * @typedef {object} TestProfileAsyncContext
17
- * @property {TestProfiler} profiler - Owning profiler.
18
- * @property {TestProfileAttemptRecord | undefined} attempt - Active test attempt.
19
- * @property {boolean} active - Whether attribution is still open.
20
- * @property {Set<TestProfileAsyncContext> | undefined} [attemptContexts] - Contexts sharing one attempt lifetime.
21
- * @property {string | undefined} filePath - Project-relative owning test file.
22
- * @property {TestProfileSpan | undefined} [span] - Innermost active profile span.
23
- * @property {number} [spanStartedAt] - Span monotonic start time.
24
- * @property {ProcessCpuUsage} [spanCpuStartedAt] - Span CPU start time.
25
- */
26
-
27
- /**
28
- * @typedef {object} TestProfileSpan
29
- * @property {string} phase - Profile phase.
30
- * @property {number} executionOrder - Monotonic span start order.
31
- * @property {number} durationMs - Real duration.
32
- * @property {{user: number, system: number, total: number}} cpuMs - Process CPU duration.
33
- * @property {string} [activity] - Validated custom activity name.
34
- * @property {number} [declarationIndex] - Hook index within its declaration scope.
35
- * @property {string} [declarationScopeId] - Opaque declaration scope identifier.
36
- * @property {string} [file] - Project-relative source path.
37
- * @property {ProfileDatabaseAggregate} [database] - Span database aggregate.
38
- * @property {ProfilePoolAggregate[]} [pools] - Span pool aggregates.
39
- */
40
-
41
- /**
42
- * @typedef {object} TestProfileAttemptRecord
43
- * @property {number} number - One-indexed attempt number.
44
- * @property {TestProfileAttemptStatus | "running"} status - Attempt result.
45
- * @property {number} durationMs - Real duration.
46
- * @property {{user: number, system: number, total: number}} cpuMs - Process CPU duration.
47
- * @property {TestProfileSpan[]} spans - Attempt-owned spans.
48
- */
49
-
50
- /**
51
- * @typedef {object} TestProfileAttemptHandle
52
- * @property {TestProfileAsyncContext} context - Async attribution context.
53
- * @property {TestProfileAttemptRecord} attempt - Output attempt record.
54
- * @property {number} startedAt - Real start time.
55
- * @property {ProcessCpuUsage} cpuStartedAt - CPU start time.
56
- */
57
-
58
- /**
59
- * @typedef {object} InternalPhaseAggregate
60
- * @property {number} count - Invocation count.
61
- * @property {number} totalMs - Total real duration.
62
- * @property {number} maxMs - Maximum real duration.
63
- * @property {number} cpuUserMs - Total user CPU time.
64
- * @property {number} cpuSystemMs - Total system CPU time.
65
- */
66
-
67
- /**
68
- * @typedef {object} InternalFileAggregate
69
- * @property {string} path - Project-relative path.
70
- * @property {number} importMs - Import duration.
71
- * @property {number} hooksMs - Hook duration.
72
- * @property {number} testsMs - Test body duration.
73
- * @property {number} attemptsMs - Complete test-attempt duration.
74
- * @property {number} totalMs - Total file weight duration.
75
- */
76
-
77
- /**
78
- * @typedef {object} InternalTestRecord
79
- * @property {string} id - Opaque stable test identifier.
80
- * @property {string} file - Project-relative source path.
81
- * @property {number | undefined} line - Declaration line.
82
- * @property {TestProfileAttemptRecord[]} attempts - Attempt records.
83
- */
84
-
85
- const BUILT_IN_PHASES = [
86
- "discovery",
87
- "imports",
88
- "testing config/global setup",
89
- "beforeAll",
90
- "beforeEach",
91
- "test body",
92
- "afterEach",
93
- "afterAll"
94
- ]
95
- const MAX_CUSTOM_ACTIVITY_NAMES = 20
96
- const SAFE_SQL_OPERATIONS = new Set([
97
- "ALTER",
98
- "ANALYZE",
99
- "BEGIN",
100
- "CALL",
101
- "COMMIT",
102
- "COPY",
103
- "CREATE",
104
- "DELETE",
105
- "DESCRIBE",
106
- "DROP",
107
- "EXEC",
108
- "EXECUTE",
109
- "EXPLAIN",
110
- "GRANT",
111
- "INSERT",
112
- "MERGE",
113
- "PRAGMA",
114
- "RELEASE",
115
- "REPLACE",
116
- "REVOKE",
117
- "ROLLBACK",
118
- "SAVEPOINT",
119
- "SELECT",
120
- "SET",
121
- "SHOW",
122
- "TRUNCATE",
123
- "UPDATE",
124
- "VACUUM",
125
- "VALUES",
126
- "WITH"
127
- ])
128
-
129
- /**
130
- * Rounds and bounds duration values for public output.
131
- * @param {number} durationMs - Raw duration.
132
- * @returns {number} - Safe duration.
133
- */
134
- export function roundProfileDuration(durationMs) {
135
- if (!Number.isFinite(durationMs) || durationMs < 0) return 0
136
-
137
- return Math.round(durationMs * 1000) / 1000
138
- }
139
-
140
- /**
141
- * Collects opt-in test-run timing without retaining application payloads.
142
- */
143
- export default class TestProfiler {
144
- /**
145
- * Creates an opt-in test profile collector.
146
- * @param {object} args - Profiler options.
147
- * @param {import("../configuration.js").default} args.configuration - Test configuration.
148
- * @param {string} args.projectDirectory - Project root used for portable paths.
149
- * @param {Record<string, ReturnType<typeof JSON.parse>>} [args.selection] - Sanitized selection metadata.
150
- */
151
- constructor({configuration, projectDirectory, selection = {}}) {
152
- this._configuration = configuration
153
- this._projectDirectory = path.resolve(projectDirectory)
154
- this._selection = selection
155
- this._startedAt = this.now()
156
- this._cpuStartedAt = process.cpuUsage()
157
- this._executionOrder = 0
158
- this._unattributedLateEventCount = 0
159
- /** @type {Map<string, InternalPhaseAggregate>} */
160
- this._phases = new Map()
161
- /** @type {Map<string, InternalFileAggregate>} */
162
- this._files = new Map()
163
- /** @type {InternalTestRecord[]} */
164
- this._tests = []
165
- /** @type {Map<string, InternalTestRecord>} */
166
- this._testsById = new Map()
167
- /** @type {Set<TestProfileAttemptHandle>} */
168
- this._activeAttempts = new Set()
169
- /** @type {Set<TestProfileAsyncContext>} */
170
- this._activeSpanContexts = new Set()
171
- /** @type {TestProfileSpan[]} */
172
- this._spans = []
173
- /** @type {Array<{id: string, parentId: string | undefined, file: string, line: number | undefined}>} */
174
- this._scopes = []
175
- /** @type {WeakMap<import("./test-runner.js").TestsArgument | import("@velocious/testing/runner").SuiteDeclaration, string>} */
176
- this._scopeIds = new WeakMap()
177
- /** @type {Set<string>} */
178
- this._customActivityNames = new Set()
179
- this._database = this.emptyDatabaseAggregate()
180
- /** @type {Map<string, {hash: string, operation: string, count: number, failedCount: number, totalMs: number, maxMs: number}>} */
181
- this._queryFingerprints = new Map()
182
- this._transactions = {
183
- start: this.phaseAggregateShape(),
184
- commit: this.phaseAggregateShape(),
185
- rollback: this.phaseAggregateShape()
186
- }
187
- /** @type {Map<string, ProfilePoolAggregate>} */
188
- this._pools = new Map()
189
- this._finishedProfile = undefined
190
-
191
- registerTestProfileContextReader(configuration, () => {
192
- return configuration.getEnvironmentHandler().getCurrentTestProfileContext()
193
- })
194
-
195
- for (const phase of BUILT_IN_PHASES) this.phaseAggregate(phase)
196
- }
197
-
198
- /**
199
- * Returns the profiler's monotonic clock.
200
- * @returns {number} - Monotonic milliseconds.
201
- */
202
- now() {
203
- return globalThis.performance.now()
204
- }
205
-
206
- /**
207
- * Converts a source path to a project-relative path or an opaque hash.
208
- * @param {string | undefined} filePath - Source path.
209
- * @returns {string} - Safe source identifier.
210
- */
211
- safeSourcePath(filePath) {
212
- if (!filePath) return "sha256:unknown"
213
-
214
- const absolutePath = path.resolve(filePath)
215
- const relativePath = path.relative(this._projectDirectory, absolutePath)
216
-
217
- if (relativePath && !relativePath.startsWith("..") && !path.isAbsolute(relativePath)) {
218
- return relativePath.replaceAll(path.sep, "/")
219
- }
220
-
221
- if (!relativePath) return path.basename(absolutePath)
222
-
223
- return this.hash(`external-source:${absolutePath}`)
224
- }
225
-
226
- /**
227
- * Returns a bounded opaque SHA-256 identifier.
228
- * @param {string} value - Value to hash.
229
- * @returns {string} - Opaque hash.
230
- */
231
- hash(value) {
232
- return `sha256:${createHash("sha256").update(value).digest("hex").slice(0, 24)}`
233
- }
234
-
235
- /**
236
- * Adds aggregate-only selection metadata as discovery progresses.
237
- * @param {Record<string, ReturnType<typeof JSON.parse>>} selection - Safe selection metadata.
238
- * @returns {void}
239
- */
240
- setSelection(selection) {
241
- Object.assign(this._selection, selection)
242
- }
243
-
244
- /**
245
- * Registers and returns a deterministic scope identifier.
246
- * @param {import("./test-runner.js").TestsArgument | import("@velocious/testing/runner").SuiteDeclaration} scope - Scope object.
247
- * @param {object} args - Scope metadata.
248
- * @param {string[]} args.descriptions - Scope description path.
249
- * @param {string | undefined} args.filePath - Scope source path.
250
- * @param {number | undefined} [args.line] - Scope source line.
251
- * @param {string | undefined} [args.parentId] - Parent scope identifier.
252
- * @returns {string} - Opaque scope identifier.
253
- */
254
- scopeId(scope, {descriptions, filePath, line, parentId}) {
255
- const existing = this._scopeIds.get(scope)
256
- if (existing) return existing
257
-
258
- const safeFilePath = this.safeSourcePath(filePath)
259
- const id = this.hash(`scope:${safeFilePath}:${line ?? 0}:${descriptions.join("\u0000")}`)
260
-
261
- this._scopeIds.set(scope, id)
262
- this._scopes.push({id, parentId, file: safeFilePath, line})
263
- return id
264
- }
265
-
266
- /**
267
- * Starts an attempt and its async attribution context.
268
- * @param {object} args - Attempt metadata.
269
- * @param {string[]} args.descriptions - Parent descriptions.
270
- * @param {number} args.attemptNumber - Attempt number.
271
- * @param {import("./test-runner.js").TestData} args.testData - Test declaration.
272
- * @param {string} args.testDescription - Test description.
273
- * @returns {TestProfileAttemptHandle} - Active attempt handle.
274
- */
275
- startAttempt({descriptions, attemptNumber, testData, testDescription}) {
276
- const file = this.safeSourcePath(testData.ownerFilePath ?? testData.filePath)
277
- const id = this.hash(`test:${file}:${testData.line ?? 0}:${[...descriptions, testDescription].join("\u0000")}`)
278
- let testRecord = this._testsById.get(id)
279
-
280
- if (!testRecord) {
281
- testRecord = {id, file, line: testData.line, attempts: []}
282
- this._testsById.set(id, testRecord)
283
- this._tests.push(testRecord)
284
- }
285
-
286
- /** @type {TestProfileAttemptRecord} */
287
- const attempt = {
288
- number: attemptNumber,
289
- status: "running",
290
- durationMs: 0,
291
- cpuMs: {user: 0, system: 0, total: 0},
292
- spans: []
293
- }
294
- /** @type {Set<TestProfileAsyncContext>} */
295
- const attemptContexts = new Set()
296
- const context = {profiler: this, attempt, active: true, attemptContexts, filePath: file, span: undefined}
297
-
298
- attemptContexts.add(context)
299
-
300
- const handle = {context, attempt, startedAt: this.now(), cpuStartedAt: process.cpuUsage()}
301
-
302
- testRecord.attempts.push(attempt)
303
- this._activeAttempts.add(handle)
304
-
305
- return handle
306
- }
307
-
308
- /**
309
- * Runs work inside an attempt's async context.
310
- * @template T
311
- * @param {TestProfileAttemptHandle} handle - Attempt handle.
312
- * @param {() => Promise<T>} callback - Attempt lifecycle.
313
- * @returns {Promise<T>} - Callback result.
314
- */
315
- async runAttempt(handle, callback) {
316
- return await this._configuration
317
- .getEnvironmentHandler()
318
- .runWithTestProfileContext(handle.context, callback)
319
- }
320
-
321
- /**
322
- * Completes an attempt and prevents descendant late work from retaining attribution.
323
- * @param {TestProfileAttemptHandle} handle - Attempt handle.
324
- * @param {TestProfileAttemptStatus} status - Attempt result.
325
- * @returns {void}
326
- */
327
- finishAttempt(handle, status) {
328
- if (handle.attempt.status !== "running") return
329
-
330
- for (const context of handle.context.attemptContexts || []) {
331
- if (context.span) this.finishSpanContext(context)
332
- context.active = false
333
- }
334
-
335
- handle.attempt.status = status
336
- handle.attempt.durationMs = roundProfileDuration(this.now() - handle.startedAt)
337
- handle.attempt.cpuMs = this.cpuDuration(handle.cpuStartedAt)
338
- this.addFileAttemptDuration(handle.context.filePath, handle.attempt.durationMs)
339
- this._activeAttempts.delete(handle)
340
- }
341
-
342
- /**
343
- * Closes every active attempt and nested span at the current interruption boundary.
344
- * @returns {void}
345
- */
346
- interrupt() {
347
- for (const handle of [...this._activeAttempts]) {
348
- this.finishAttempt(handle, "interrupted")
349
- }
350
-
351
- for (const context of [...this._activeSpanContexts]) {
352
- this.finishSpanContext(context)
353
- }
354
- }
3
+ import {
4
+ TestProfiler as PackageTestProfiler,
5
+ roundProfileDuration
6
+ } from "@velocious/testing/node"
355
7
 
356
- /**
357
- * Runs a runner-owned phase span.
358
- * @template T
359
- * @param {object} args - Span metadata.
360
- * @param {string} args.phase - Phase name.
361
- * @param {string} [args.activity] - Custom activity label.
362
- * @param {number} [args.declarationIndex] - Hook declaration index.
363
- * @param {string} [args.declarationScopeId] - Hook declaration scope.
364
- * @param {string} [args.filePath] - Source or owning test file.
365
- * @param {() => (T | Promise<T>)} callback - Timed work.
366
- * @returns {Promise<T>} - Callback result.
367
- */
368
- async runSpan({phase, activity, declarationIndex, declarationScopeId, filePath}, callback) {
369
- const currentContext = this._configuration.getEnvironmentHandler().getCurrentTestProfileContext()
370
-
371
- if (currentContext && !this.contextIsActive(currentContext)) {
372
- this._unattributedLateEventCount++
373
- return await callback()
374
- }
375
-
376
- const safeFilePath = currentContext?.filePath ?? (filePath ? this.safeSourcePath(filePath) : undefined)
377
- const startedAt = this.now()
378
- const cpuStartedAt = process.cpuUsage()
379
- /** @type {TestProfileSpan} */
380
- const span = {
381
- phase,
382
- executionOrder: ++this._executionOrder,
383
- durationMs: 0,
384
- cpuMs: {user: 0, system: 0, total: 0}
385
- }
386
-
387
- if (activity) span.activity = activity
388
- if (declarationIndex !== undefined) span.declarationIndex = declarationIndex
389
- if (declarationScopeId) span.declarationScopeId = declarationScopeId
390
- if (safeFilePath) span.file = safeFilePath
391
-
392
- if (currentContext?.attempt) {
393
- currentContext.attempt.spans.push(span)
394
- } else {
395
- this._spans.push(span)
396
- }
397
-
398
- const spanContext = {
399
- profiler: this,
400
- attempt: currentContext?.attempt,
401
- active: true,
402
- attemptContexts: currentContext?.attemptContexts,
403
- filePath: safeFilePath,
404
- span,
405
- spanStartedAt: startedAt,
406
- spanCpuStartedAt: cpuStartedAt
407
- }
408
-
409
- spanContext.attemptContexts?.add(spanContext)
410
- this._activeSpanContexts.add(spanContext)
411
-
412
- try {
413
- return await this._configuration
414
- .getEnvironmentHandler()
415
- .runWithTestProfileContext(spanContext, callback)
416
- } finally {
417
- if (spanContext.active) {
418
- this.finishSpanContext(spanContext)
419
- } else if (spanContext.attempt) {
420
- this._unattributedLateEventCount++
421
- }
422
- }
423
- }
8
+ import restArgsError from "../utils/rest-args-error.js"
9
+ import { registerTestProfileContextReader } from "./test-profile-context.js"
424
10
 
425
- /**
426
- * Finalizes an open span, including partial work closed by a timeout.
427
- * @param {TestProfileAsyncContext} context - Span context.
428
- * @returns {void}
429
- */
430
- finishSpanContext(context) {
431
- if (!context.active || !context.span || context.spanStartedAt === undefined || !context.spanCpuStartedAt) return
11
+ /** @typedef {import("@velocious/testing/node").TestProfileAsyncContext} TestProfileAsyncContext */
12
+ /** @typedef {import("@velocious/testing/node").TestProfileAttemptHandle} TestProfileAttemptHandle */
13
+ /** @typedef {import("@velocious/testing/node").TestProfileAttemptStatus} TestProfileAttemptStatus */
432
14
 
433
- context.active = false
434
- context.span.durationMs = roundProfileDuration(this.now() - context.spanStartedAt)
435
- context.span.cpuMs = this.cpuDuration(context.spanCpuStartedAt)
436
- this.addPhaseDuration(context.span.phase, context.span.durationMs, context.span.cpuMs)
437
- this.addFileDuration(context.filePath, context.span.phase, context.span.durationMs, Boolean(context.attempt))
438
- this._activeSpanContexts.delete(context)
439
- }
15
+ export { roundProfileDuration }
440
16
 
17
+ /** Connects the package profiler to Velocious's environment-owned async context. */
18
+ class VelociousProfileContextAdapter {
441
19
  /**
442
- * Checks both a nested span boundary and its owning attempt boundary.
443
- * @param {TestProfileAsyncContext} context - Candidate profile context.
444
- * @returns {boolean} - Whether events may still be attributed.
20
+ * Creates the environment context adapter.
21
+ * @param {import("../environment-handlers/base.js").default} environmentHandler - Active environment handler.
445
22
  */
446
- contextIsActive(context) {
447
- return context.active && (!context.attempt || context.attempt.status === "running")
23
+ constructor(environmentHandler) {
24
+ this.environmentHandler = environmentHandler
448
25
  }
449
26
 
450
27
  /**
451
- * Runs a validated application-defined activity.
452
- * @template T
453
- * @param {TestProfileAsyncContext} context - Captured async context.
454
- * @param {string} name - Validated activity name.
455
- * @param {() => (T | Promise<T>)} callback - Activity callback.
456
- * @returns {Promise<T>} - Callback result.
28
+ * Gets the active framework profile context.
29
+ * @returns {TestProfileAsyncContext | undefined} - Current profile attribution.
457
30
  */
458
- async profileActivity(context, name, callback) {
459
- if (!this.contextIsActive(context)) {
460
- this._unattributedLateEventCount++
461
- return await callback()
462
- }
463
-
464
- const validatedName = validateTestActivityName(name)
465
- let outputName = validatedName
466
-
467
- if (!this._customActivityNames.has(validatedName)) {
468
- if (this._customActivityNames.size >= MAX_CUSTOM_ACTIVITY_NAMES) {
469
- outputName = "other"
470
- } else {
471
- this._customActivityNames.add(validatedName)
472
- }
473
- }
474
-
475
- return await this.runSpan({phase: "custom", activity: outputName}, callback)
476
- }
31
+ current() { return this.environmentHandler.getCurrentTestProfileContext() }
477
32
 
478
33
  /**
479
- * Measures a command-level phase outside an attempt.
34
+ * Runs work with framework profile attribution.
480
35
  * @template T
481
- * @param {string} phase - Phase name.
482
- * @param {() => (T | Promise<T>)} callback - Timed work.
483
- * @param {{filePath?: string}} [metadata] - Optional source ownership.
484
- * @returns {Promise<T>} - Callback result.
485
- */
486
- async measurePhase(phase, callback, metadata = {}) {
487
- return await this.runSpan({phase, filePath: metadata.filePath}, callback)
488
- }
489
-
490
- /**
491
- * Records one successful or failed physical database query attempt.
492
- * @param {TestProfileAsyncContext} context - Context captured when the query began.
493
- * @param {{durationMs: number, failed: boolean, sqlFingerprint: string, sqlOperation: string}} args - Query aggregate values.
494
- * @returns {void}
36
+ * @param {TestProfileAsyncContext} context - Profile attribution to install.
37
+ * @param {() => T} callback - Work to run.
38
+ * @returns {T} - Callback result.
495
39
  */
496
- recordDatabaseQuery(context, {durationMs, failed, sqlFingerprint, sqlOperation}) {
497
- if (!this.contextIsActive(context)) {
498
- this._unattributedLateEventCount++
499
- return
500
- }
501
-
502
- const safeDurationMs = roundProfileDuration(durationMs)
503
-
504
- this.addDatabaseQueryAggregate(this._database, {durationMs: safeDurationMs, failed})
505
- if (context.span) {
506
- context.span.database ||= this.emptyDatabaseAggregate()
507
- this.addDatabaseQueryAggregate(context.span.database, {durationMs: safeDurationMs, failed})
508
- }
509
-
510
- const safeOperation = SAFE_SQL_OPERATIONS.has(sqlOperation) ? sqlOperation : "UNKNOWN"
511
- const fingerprintKey = `${safeOperation}:${sqlFingerprint}`
512
- let fingerprint = this._queryFingerprints.get(fingerprintKey)
513
-
514
- if (!fingerprint && this._queryFingerprints.size < 50) {
515
- fingerprint = {
516
- hash: sqlFingerprint,
517
- operation: safeOperation,
518
- count: 0,
519
- failedCount: 0,
520
- totalMs: 0,
521
- maxMs: 0
522
- }
523
- this._queryFingerprints.set(fingerprintKey, fingerprint)
524
- }
525
-
526
- if (fingerprint) {
527
- fingerprint.count++
528
- if (failed) fingerprint.failedCount++
529
- fingerprint.totalMs += safeDurationMs
530
- fingerprint.maxMs = Math.max(fingerprint.maxMs, safeDurationMs)
531
- }
532
- }
533
-
534
- /**
535
- * Records a physical transaction action.
536
- * @param {TestProfileAsyncContext} context - Context captured when the action began.
537
- * @param {{action: "start" | "commit" | "rollback", durationMs: number, failed: boolean}} args - Transaction aggregate values.
538
- * @returns {void}
539
- */
540
- recordDatabaseTransaction(context, {action, durationMs, failed}) {
541
- if (!this.contextIsActive(context)) {
542
- this._unattributedLateEventCount++
543
- return
544
- }
545
-
546
- const aggregate = this._transactions[action]
547
- const safeDurationMs = roundProfileDuration(durationMs)
548
-
549
- aggregate.count++
550
- if (failed) aggregate.failedCount++
551
- aggregate.totalMs += safeDurationMs
552
- aggregate.maxMs = Math.max(aggregate.maxMs, safeDurationMs)
553
- }
554
-
555
- /**
556
- * Records one low-cardinality pool lifecycle delta.
557
- * @param {TestProfileAsyncContext} context - Context captured when the operation began.
558
- * @param {string} identifier - Logical pool identifier.
559
- * @param {"connectionCreation" | "checkoutWait" | "checkoutTimeout" | "idleReap" | "idleReapDisposal" | "peakLiveConnections"} metric - Metric name.
560
- * @param {{durationMs?: number, failed?: boolean, value?: number}} [values] - Aggregate values.
561
- * @returns {void}
562
- */
563
- recordPoolMetric(context, identifier, metric, values = {}) {
564
- if (!this.contextIsActive(context)) {
565
- this._unattributedLateEventCount++
566
- return
567
- }
568
-
569
- const aggregate = this.poolAggregate(this._pools, identifier)
570
-
571
- this.addPoolMetric(aggregate, metric, values)
572
-
573
- if (context.span) {
574
- context.span.pools ||= []
575
- const spanPools = new Map(context.span.pools.map((pool) => [pool.identifier, pool]))
576
- const spanAggregate = this.poolAggregate(spanPools, identifier)
577
-
578
- if (!context.span.pools.includes(spanAggregate)) context.span.pools.push(spanAggregate)
579
- this.addPoolMetric(spanAggregate, metric, values)
580
- }
581
- }
582
-
583
- /**
584
- * Builds the final privacy-safe profile document.
585
- * @param {object} args - Run result.
586
- * @param {{discovered: number, executed: number, failed: number, passed: number}} args.counts - Run counts.
587
- * @param {boolean} args.focused - Whether focused tests were selected.
588
- * @param {string} args.status - Run status.
589
- * @returns {ReturnType<typeof JSON.parse>} - Rich profile document.
590
- */
591
- finish({counts, focused, status}) {
592
- if (this._finishedProfile) return this._finishedProfile
593
-
594
- const totalDurationMs = roundProfileDuration(this.now() - this._startedAt)
595
- const totalCpuMs = this.cpuDuration(this._cpuStartedAt)
596
- const exclusivePhaseNames = BUILT_IN_PHASES
597
- const measuredDurationMs = exclusivePhaseNames.reduce((sum, phase) => sum + this.phaseAggregate(phase).totalMs, 0)
598
- const measuredCpuUserMs = exclusivePhaseNames.reduce((sum, phase) => sum + this.phaseAggregate(phase).cpuUserMs, 0)
599
- const measuredCpuSystemMs = exclusivePhaseNames.reduce((sum, phase) => sum + this.phaseAggregate(phase).cpuSystemMs, 0)
600
-
601
- this._phases.set("runner overhead", {
602
- count: 1,
603
- totalMs: Math.max(0, totalDurationMs - measuredDurationMs),
604
- maxMs: Math.max(0, totalDurationMs - measuredDurationMs),
605
- cpuUserMs: Math.max(0, totalCpuMs.user - measuredCpuUserMs),
606
- cpuSystemMs: Math.max(0, totalCpuMs.system - measuredCpuSystemMs)
607
- })
608
- this._phases.set("total", {
609
- count: 1,
610
- totalMs: totalDurationMs,
611
- maxMs: totalDurationMs,
612
- cpuUserMs: totalCpuMs.user,
613
- cpuSystemMs: totalCpuMs.system
614
- })
615
-
616
- const phases = Object.fromEntries([...this._phases.entries()].map(([phase, aggregate]) => [
617
- phase,
618
- this.publicAggregate(aggregate)
619
- ]))
620
- const files = [...this._files.values()]
621
- .map((file) => ({
622
- path: file.path,
623
- importMs: roundProfileDuration(file.importMs),
624
- hooksMs: roundProfileDuration(file.hooksMs),
625
- testsMs: roundProfileDuration(file.testsMs),
626
- attemptsMs: roundProfileDuration(file.attemptsMs),
627
- totalMs: roundProfileDuration(file.totalMs)
628
- }))
629
- .sort((fileA, fileB) => compareTimingManifestPaths(fileA.path, fileB.path))
630
- const timingManifest = Object.fromEntries(files.map((file) => [file.path, file.totalMs]))
631
- const attempts = this._tests.reduce((sum, test) => sum + test.attempts.length, 0)
632
-
633
- this._finishedProfile = {
634
- schema: "velocious.test-profile",
635
- schemaVersion: 1,
636
- status,
637
- selection: {...this._selection, focused},
638
- counts: {...counts, attempts},
639
- durationMs: totalDurationMs,
640
- cpuMs: totalCpuMs,
641
- phases,
642
- files,
643
- scopes: [...this._scopes].sort((scopeA, scopeB) => scopeA.id.localeCompare(scopeB.id)),
644
- tests: this._tests.map((test) => ({
645
- id: test.id,
646
- file: test.file,
647
- line: test.line,
648
- durationMs: roundProfileDuration(test.attempts.reduce((sum, attempt) => sum + attempt.durationMs, 0)),
649
- attempts: test.attempts
650
- })),
651
- spans: this._spans,
652
- database: {
653
- queryCount: this._database.queryCount,
654
- failedQueryCount: this._database.failedQueryCount,
655
- totalMs: roundProfileDuration(this._database.totalMs),
656
- maxMs: roundProfileDuration(this._database.maxMs),
657
- fingerprints: [...this._queryFingerprints.values()]
658
- .map((fingerprint) => ({
659
- ...fingerprint,
660
- totalMs: roundProfileDuration(fingerprint.totalMs),
661
- maxMs: roundProfileDuration(fingerprint.maxMs)
662
- }))
663
- .sort((fingerprintA, fingerprintB) => {
664
- return fingerprintA.operation.localeCompare(fingerprintB.operation) || fingerprintA.hash.localeCompare(fingerprintB.hash)
665
- }),
666
- transactions: {
667
- start: this.publicActionAggregate(this._transactions.start),
668
- commit: this.publicActionAggregate(this._transactions.commit),
669
- rollback: this.publicActionAggregate(this._transactions.rollback)
670
- }
671
- },
672
- pools: [...this._pools.values()]
673
- .map((pool) => this.publicPoolAggregate(pool))
674
- .sort((poolA, poolB) => poolA.identifier.localeCompare(poolB.identifier)),
675
- unattributedLateEventCount: this._unattributedLateEventCount,
676
- timingManifest
677
- }
678
-
679
- return this._finishedProfile
680
- }
681
-
682
- /**
683
- * Returns an internal phase aggregate.
684
- * @param {string} phase - Phase name.
685
- * @returns {InternalPhaseAggregate} - Aggregate.
686
- */
687
- phaseAggregate(phase) {
688
- let aggregate = this._phases.get(phase)
689
-
690
- if (!aggregate) {
691
- aggregate = {count: 0, totalMs: 0, maxMs: 0, cpuUserMs: 0, cpuSystemMs: 0}
692
- this._phases.set(phase, aggregate)
693
- }
694
-
695
- return aggregate
696
- }
697
-
698
- /**
699
- * Adds a completed span to its aggregate phase.
700
- * @param {string} phase - Phase name.
701
- * @param {number} durationMs - Real duration.
702
- * @param {{user: number, system: number, total: number}} cpuMs - CPU duration.
703
- * @returns {void}
704
- */
705
- addPhaseDuration(phase, durationMs, cpuMs) {
706
- const aggregate = this.phaseAggregate(phase)
707
-
708
- aggregate.count++
709
- aggregate.totalMs += durationMs
710
- aggregate.maxMs = Math.max(aggregate.maxMs, durationMs)
711
- aggregate.cpuUserMs += cpuMs.user
712
- aggregate.cpuSystemMs += cpuMs.system
713
- }
714
-
715
- /**
716
- * Adds an exclusive file-owned phase to timing-manifest weight.
717
- * @param {string | undefined} filePath - Safe project-relative path.
718
- * @param {string} phase - Phase name.
719
- * @param {number} durationMs - Real duration.
720
- * @param {boolean} [insideAttempt] - Whether the phase is already covered by complete attempt time.
721
- * @returns {void}
722
- */
723
- addFileDuration(filePath, phase, durationMs, insideAttempt = false) {
724
- if (!filePath) return
725
- if (!["imports", "beforeAll", "beforeEach", "test body", "afterEach", "afterAll"].includes(phase)) return
726
-
727
- let file = this._files.get(filePath)
728
-
729
- if (!file) {
730
- file = {path: filePath, importMs: 0, hooksMs: 0, testsMs: 0, attemptsMs: 0, totalMs: 0}
731
- this._files.set(filePath, file)
732
- }
733
-
734
- if (phase === "imports") file.importMs += durationMs
735
- if (phase === "test body") file.testsMs += durationMs
736
- if (["beforeAll", "beforeEach", "afterEach", "afterAll"].includes(phase)) file.hooksMs += durationMs
737
- if (!insideAttempt) file.totalMs += durationMs
738
- }
739
-
740
- /**
741
- * Adds the complete cost of an attempt to its owning file weight.
742
- * @param {string | undefined} filePath - Safe project-relative path.
743
- * @param {number} durationMs - Attempt duration.
744
- * @returns {void}
745
- */
746
- addFileAttemptDuration(filePath, durationMs) {
747
- if (!filePath) return
748
-
749
- let file = this._files.get(filePath)
750
-
751
- if (!file) {
752
- file = {path: filePath, importMs: 0, hooksMs: 0, testsMs: 0, attemptsMs: 0, totalMs: 0}
753
- this._files.set(filePath, file)
754
- }
755
-
756
- file.attemptsMs += durationMs
757
- file.totalMs += durationMs
758
- }
759
-
760
- /**
761
- * Calculates process CPU time since a start sample.
762
- * @param {ProcessCpuUsage} start - CPU start sample.
763
- * @returns {{user: number, system: number, total: number}} - Millisecond CPU duration.
764
- */
765
- cpuDuration(start) {
766
- const cpu = process.cpuUsage(start)
767
- const user = roundProfileDuration(cpu.user / 1000)
768
- const system = roundProfileDuration(cpu.system / 1000)
769
-
770
- return {user, system, total: roundProfileDuration(user + system)}
771
- }
772
-
773
- /**
774
- * Converts an internal aggregate to public rounded values.
775
- * @param {InternalPhaseAggregate} [aggregate] - Internal aggregate.
776
- * @returns {{count: number, totalMs: number, maxMs: number, cpuMs: {user: number, system: number, total: number}}} - Public aggregate.
777
- */
778
- publicAggregate(aggregate = {count: 0, totalMs: 0, maxMs: 0, cpuUserMs: 0, cpuSystemMs: 0}) {
779
- const user = roundProfileDuration(aggregate.cpuUserMs)
780
- const system = roundProfileDuration(aggregate.cpuSystemMs)
781
-
782
- return {
783
- count: aggregate.count,
784
- totalMs: roundProfileDuration(aggregate.totalMs),
785
- maxMs: roundProfileDuration(aggregate.maxMs),
786
- cpuMs: {user, system, total: roundProfileDuration(user + system)}
787
- }
788
- }
789
-
790
- /**
791
- * Returns an empty query aggregate.
792
- * @returns {ProfileDatabaseAggregate} - Empty aggregate.
793
- */
794
- emptyDatabaseAggregate() {
795
- return {queryCount: 0, failedQueryCount: 0, totalMs: 0, maxMs: 0}
796
- }
797
-
798
- /**
799
- * Adds a query attempt to a database aggregate.
800
- * @param {ProfileDatabaseAggregate} aggregate - Aggregate to update.
801
- * @param {{durationMs: number, failed: boolean}} args - Attempt values.
802
- * @returns {void}
803
- */
804
- addDatabaseQueryAggregate(aggregate, {durationMs, failed}) {
805
- aggregate.queryCount++
806
- if (failed) aggregate.failedQueryCount++
807
- aggregate.totalMs += durationMs
808
- aggregate.maxMs = Math.max(aggregate.maxMs, durationMs)
809
- }
810
-
811
- /**
812
- * Returns an empty action aggregate.
813
- * @returns {ProfileActionAggregate} - Empty aggregate.
814
- */
815
- phaseAggregateShape() {
816
- return {count: 0, failedCount: 0, totalMs: 0, maxMs: 0}
817
- }
818
-
819
- /**
820
- * Rounds a transaction or pool action aggregate.
821
- * @param {ProfileActionAggregate} aggregate - Internal aggregate.
822
- * @returns {ProfileActionAggregate} - Public aggregate.
823
- */
824
- publicActionAggregate(aggregate) {
825
- return {
826
- count: aggregate.count,
827
- failedCount: aggregate.failedCount,
828
- totalMs: roundProfileDuration(aggregate.totalMs),
829
- maxMs: roundProfileDuration(aggregate.maxMs)
830
- }
831
- }
832
-
833
- /**
834
- * Returns an empty pool aggregate.
835
- * @param {string} [identifier] - Logical pool identifier.
836
- * @returns {ProfilePoolAggregate} - Empty aggregate.
837
- */
838
- emptyPoolAggregate(identifier = "") {
839
- return {
840
- identifier,
841
- connectionCreation: this.phaseAggregateShape(),
842
- checkoutWait: {count: 0, totalMs: 0, maxMs: 0},
843
- checkoutTimeoutCount: 0,
844
- idleReap: {...this.phaseAggregateShape(), disposalCount: 0},
845
- peakLiveConnections: 0
846
- }
847
- }
848
-
849
- /**
850
- * Gets or creates a pool aggregate in a map.
851
- * @param {Map<string, ProfilePoolAggregate>} pools - Pool map.
852
- * @param {string} identifier - Logical pool identifier.
853
- * @returns {ProfilePoolAggregate} - Pool aggregate.
854
- */
855
- poolAggregate(pools, identifier) {
856
- let aggregate = pools.get(identifier)
857
-
858
- if (!aggregate) {
859
- aggregate = this.emptyPoolAggregate(identifier)
860
- pools.set(identifier, aggregate)
861
- }
862
-
863
- return aggregate
40
+ run(context, callback) {
41
+ return this.environmentHandler.runWithTestProfileContext(context, callback)
864
42
  }
43
+ }
865
44
 
45
+ /** Velocious compatibility facade around the package-owned profiler. */
46
+ export default class TestProfiler extends PackageTestProfiler {
866
47
  /**
867
- * Applies one pool metric delta.
868
- * @param {ProfilePoolAggregate} aggregate - Pool aggregate.
869
- * @param {"connectionCreation" | "checkoutWait" | "checkoutTimeout" | "idleReap" | "idleReapDisposal" | "peakLiveConnections"} metric - Metric name.
870
- * @param {{durationMs?: number, failed?: boolean, value?: number}} values - Metric values.
871
- * @returns {void}
48
+ * Creates the package profiler with Velocious environment context.
49
+ * @param {object} args - Profiler options.
50
+ * @param {import("../configuration.js").default} args.configuration - Test configuration.
51
+ * @param {string} args.projectDirectory - Project root used for portable paths.
52
+ * @param {Partial<import("@velocious/testing/node").TestProfileSelection>} [args.selection] - Selection metadata.
872
53
  */
873
- addPoolMetric(aggregate, metric, values) {
874
- const durationMs = roundProfileDuration(values.durationMs ?? 0)
875
-
876
- if (metric === "connectionCreation" || metric === "idleReap") {
877
- const actionAggregate = metric === "connectionCreation" ? aggregate.connectionCreation : aggregate.idleReap
878
-
879
- actionAggregate.count++
880
- if (values.failed) actionAggregate.failedCount++
881
- actionAggregate.totalMs += durationMs
882
- actionAggregate.maxMs = Math.max(actionAggregate.maxMs, durationMs)
883
- } else if (metric === "checkoutWait") {
884
- aggregate.checkoutWait.count++
885
- aggregate.checkoutWait.totalMs += durationMs
886
- aggregate.checkoutWait.maxMs = Math.max(aggregate.checkoutWait.maxMs, durationMs)
887
- } else if (metric === "checkoutTimeout") {
888
- aggregate.checkoutTimeoutCount++
889
- } else if (metric === "idleReapDisposal") {
890
- aggregate.idleReap.disposalCount++
891
- } else if (metric === "peakLiveConnections") {
892
- aggregate.peakLiveConnections = Math.max(aggregate.peakLiveConnections, values.value ?? 0)
893
- }
894
- }
54
+ constructor({configuration, projectDirectory, selection, ...restArgs}) {
55
+ restArgsError(restArgs)
56
+ const contextAdapter = new VelociousProfileContextAdapter(configuration.getEnvironmentHandler())
895
57
 
896
- /**
897
- * Rounds a pool aggregate for output.
898
- * @param {ProfilePoolAggregate} aggregate - Internal aggregate.
899
- * @returns {ProfilePoolAggregate} - Public aggregate.
900
- */
901
- publicPoolAggregate(aggregate) {
902
- return {
903
- identifier: aggregate.identifier,
904
- connectionCreation: this.publicActionAggregate(aggregate.connectionCreation),
905
- checkoutWait: {
906
- count: aggregate.checkoutWait.count,
907
- totalMs: roundProfileDuration(aggregate.checkoutWait.totalMs),
908
- maxMs: roundProfileDuration(aggregate.checkoutWait.maxMs)
909
- },
910
- checkoutTimeoutCount: aggregate.checkoutTimeoutCount,
911
- idleReap: {
912
- ...this.publicActionAggregate(aggregate.idleReap),
913
- disposalCount: aggregate.idleReap.disposalCount
914
- },
915
- peakLiveConnections: aggregate.peakLiveConnections
916
- }
58
+ super({contextAdapter, projectDirectory, selection})
59
+ registerTestProfileContextReader(configuration, () => contextAdapter.current())
917
60
  }
918
61
  }