@arizeai/phoenix-client 6.10.1 → 6.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/README.md +62 -0
  2. package/dist/esm/__generated__/api/v1.d.ts +254 -2
  3. package/dist/esm/__generated__/api/v1.d.ts.map +1 -1
  4. package/dist/esm/jest/index.d.ts +5 -0
  5. package/dist/esm/jest/index.d.ts.map +1 -0
  6. package/dist/esm/jest/index.js +49 -0
  7. package/dist/esm/jest/index.js.map +1 -0
  8. package/dist/esm/jest/reporter.d.ts +13 -0
  9. package/dist/esm/jest/reporter.d.ts.map +1 -0
  10. package/dist/esm/jest/reporter.js +19 -0
  11. package/dist/esm/jest/reporter.js.map +1 -0
  12. package/dist/esm/prompts/sdks/toAI.d.ts +2 -2
  13. package/dist/esm/prompts/sdks/toAI.d.ts.map +1 -1
  14. package/dist/esm/prompts/sdks/toAI.js.map +1 -1
  15. package/dist/esm/prompts/sdks/toAnthropic.d.ts +2 -2
  16. package/dist/esm/prompts/sdks/toAnthropic.d.ts.map +1 -1
  17. package/dist/esm/prompts/sdks/toAnthropic.js.map +1 -1
  18. package/dist/esm/prompts/sdks/toOpenAI.d.ts +2 -2
  19. package/dist/esm/prompts/sdks/toOpenAI.d.ts.map +1 -1
  20. package/dist/esm/prompts/sdks/toOpenAI.js.map +1 -1
  21. package/dist/esm/prompts/sdks/toSDK.d.ts +8 -8
  22. package/dist/esm/prompts/sdks/toSDK.d.ts.map +1 -1
  23. package/dist/esm/prompts/sdks/toSDK.js.map +1 -1
  24. package/dist/esm/prompts/sdks/types.d.ts +2 -2
  25. package/dist/esm/prompts/sdks/types.d.ts.map +1 -1
  26. package/dist/esm/schemas/llm/anthropic/converters.d.ts +8 -8
  27. package/dist/esm/schemas/llm/anthropic/messagePartSchemas.d.ts +4 -4
  28. package/dist/esm/schemas/llm/anthropic/messageSchemas.d.ts +6 -6
  29. package/dist/esm/schemas/llm/constants.d.ts +3 -3
  30. package/dist/esm/schemas/llm/converters.d.ts +12 -12
  31. package/dist/esm/schemas/llm/openai/converters.d.ts +3 -3
  32. package/dist/esm/schemas/llm/schemas.d.ts +2 -2
  33. package/dist/esm/testing/acceptance.d.ts +20 -0
  34. package/dist/esm/testing/acceptance.d.ts.map +1 -0
  35. package/dist/esm/testing/acceptance.js +129 -0
  36. package/dist/esm/testing/acceptance.js.map +1 -0
  37. package/dist/esm/testing/define-api.d.ts +157 -0
  38. package/dist/esm/testing/define-api.d.ts.map +1 -0
  39. package/dist/esm/testing/define-api.js +78 -0
  40. package/dist/esm/testing/define-api.js.map +1 -0
  41. package/dist/esm/testing/helpers.d.ts +55 -0
  42. package/dist/esm/testing/helpers.d.ts.map +1 -0
  43. package/dist/esm/testing/helpers.js +179 -0
  44. package/dist/esm/testing/helpers.js.map +1 -0
  45. package/dist/esm/testing/phoenix-test-tracking.d.ts +68 -0
  46. package/dist/esm/testing/phoenix-test-tracking.d.ts.map +1 -0
  47. package/dist/esm/testing/phoenix-test-tracking.js +521 -0
  48. package/dist/esm/testing/phoenix-test-tracking.js.map +1 -0
  49. package/dist/esm/testing/report-artifacts.d.ts +45 -0
  50. package/dist/esm/testing/report-artifacts.d.ts.map +1 -0
  51. package/dist/esm/testing/report-artifacts.js +218 -0
  52. package/dist/esm/testing/report-artifacts.js.map +1 -0
  53. package/dist/esm/testing/report-run.d.ts +22 -0
  54. package/dist/esm/testing/report-run.d.ts.map +1 -0
  55. package/dist/esm/testing/report-run.js +41 -0
  56. package/dist/esm/testing/report-run.js.map +1 -0
  57. package/dist/esm/testing/reporter-format.d.ts +83 -0
  58. package/dist/esm/testing/reporter-format.d.ts.map +1 -0
  59. package/dist/esm/testing/reporter-format.js +852 -0
  60. package/dist/esm/testing/reporter-format.js.map +1 -0
  61. package/dist/esm/testing/runner.d.ts +31 -0
  62. package/dist/esm/testing/runner.d.ts.map +1 -0
  63. package/dist/esm/testing/runner.js +238 -0
  64. package/dist/esm/testing/runner.js.map +1 -0
  65. package/dist/esm/testing/state.d.ts +138 -0
  66. package/dist/esm/testing/state.d.ts.map +1 -0
  67. package/dist/esm/testing/state.js +31 -0
  68. package/dist/esm/testing/state.js.map +1 -0
  69. package/dist/esm/testing/types.d.ts +319 -0
  70. package/dist/esm/testing/types.d.ts.map +1 -0
  71. package/dist/esm/testing/types.js +9 -0
  72. package/dist/esm/testing/types.js.map +1 -0
  73. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  74. package/dist/esm/utils/channel.d.ts +7 -7
  75. package/dist/esm/utils/channel.d.ts.map +1 -1
  76. package/dist/esm/utils/channel.js +1 -1
  77. package/dist/esm/utils/channel.js.map +1 -1
  78. package/dist/esm/utils/formatPromptMessages.d.ts.map +1 -1
  79. package/dist/esm/utils/getPromptBySelector.d.ts.map +1 -1
  80. package/dist/esm/utils/promisifyResult.d.ts +1 -1
  81. package/dist/esm/utils/promisifyResult.d.ts.map +1 -1
  82. package/dist/esm/utils/promisifyResult.js.map +1 -1
  83. package/dist/esm/utils/schemaMatches.d.ts +5 -5
  84. package/dist/esm/utils/schemaMatches.d.ts.map +1 -1
  85. package/dist/esm/utils/schemaMatches.js.map +1 -1
  86. package/dist/esm/vitest/index.d.ts +5 -0
  87. package/dist/esm/vitest/index.d.ts.map +1 -0
  88. package/dist/esm/vitest/index.js +15 -0
  89. package/dist/esm/vitest/index.js.map +1 -0
  90. package/dist/esm/vitest/reporter.d.ts +19 -0
  91. package/dist/esm/vitest/reporter.d.ts.map +1 -0
  92. package/dist/esm/vitest/reporter.js +27 -0
  93. package/dist/esm/vitest/reporter.js.map +1 -0
  94. package/dist/src/__generated__/api/v1.d.ts +254 -2
  95. package/dist/src/__generated__/api/v1.d.ts.map +1 -1
  96. package/dist/src/jest/index.d.ts +5 -0
  97. package/dist/src/jest/index.d.ts.map +1 -0
  98. package/dist/src/jest/index.js +58 -0
  99. package/dist/src/jest/index.js.map +1 -0
  100. package/dist/src/jest/reporter.d.ts +13 -0
  101. package/dist/src/jest/reporter.d.ts.map +1 -0
  102. package/dist/src/jest/reporter.js +23 -0
  103. package/dist/src/jest/reporter.js.map +1 -0
  104. package/dist/src/prompts/sdks/toAI.d.ts +2 -2
  105. package/dist/src/prompts/sdks/toAI.d.ts.map +1 -1
  106. package/dist/src/prompts/sdks/toAI.js.map +1 -1
  107. package/dist/src/prompts/sdks/toAnthropic.d.ts +2 -2
  108. package/dist/src/prompts/sdks/toAnthropic.d.ts.map +1 -1
  109. package/dist/src/prompts/sdks/toAnthropic.js.map +1 -1
  110. package/dist/src/prompts/sdks/toOpenAI.d.ts +2 -2
  111. package/dist/src/prompts/sdks/toOpenAI.d.ts.map +1 -1
  112. package/dist/src/prompts/sdks/toOpenAI.js.map +1 -1
  113. package/dist/src/prompts/sdks/toSDK.d.ts +8 -8
  114. package/dist/src/prompts/sdks/toSDK.d.ts.map +1 -1
  115. package/dist/src/prompts/sdks/toSDK.js.map +1 -1
  116. package/dist/src/prompts/sdks/types.d.ts +2 -2
  117. package/dist/src/prompts/sdks/types.d.ts.map +1 -1
  118. package/dist/src/schemas/llm/anthropic/converters.d.ts +8 -8
  119. package/dist/src/schemas/llm/anthropic/messagePartSchemas.d.ts +4 -4
  120. package/dist/src/schemas/llm/anthropic/messageSchemas.d.ts +6 -6
  121. package/dist/src/schemas/llm/constants.d.ts +3 -3
  122. package/dist/src/schemas/llm/converters.d.ts +12 -12
  123. package/dist/src/schemas/llm/openai/converters.d.ts +3 -3
  124. package/dist/src/schemas/llm/schemas.d.ts +2 -2
  125. package/dist/src/testing/acceptance.d.ts +20 -0
  126. package/dist/src/testing/acceptance.d.ts.map +1 -0
  127. package/dist/src/testing/acceptance.js +114 -0
  128. package/dist/src/testing/acceptance.js.map +1 -0
  129. package/dist/src/testing/define-api.d.ts +157 -0
  130. package/dist/src/testing/define-api.d.ts.map +1 -0
  131. package/dist/src/testing/define-api.js +81 -0
  132. package/dist/src/testing/define-api.js.map +1 -0
  133. package/dist/src/testing/helpers.d.ts +55 -0
  134. package/dist/src/testing/helpers.d.ts.map +1 -0
  135. package/dist/src/testing/helpers.js +182 -0
  136. package/dist/src/testing/helpers.js.map +1 -0
  137. package/dist/src/testing/phoenix-test-tracking.d.ts +68 -0
  138. package/dist/src/testing/phoenix-test-tracking.d.ts.map +1 -0
  139. package/dist/src/testing/phoenix-test-tracking.js +530 -0
  140. package/dist/src/testing/phoenix-test-tracking.js.map +1 -0
  141. package/dist/src/testing/report-artifacts.d.ts +45 -0
  142. package/dist/src/testing/report-artifacts.d.ts.map +1 -0
  143. package/dist/src/testing/report-artifacts.js +225 -0
  144. package/dist/src/testing/report-artifacts.js.map +1 -0
  145. package/dist/src/testing/report-run.d.ts +22 -0
  146. package/dist/src/testing/report-run.d.ts.map +1 -0
  147. package/dist/src/testing/report-run.js +47 -0
  148. package/dist/src/testing/report-run.js.map +1 -0
  149. package/dist/src/testing/reporter-format.d.ts +83 -0
  150. package/dist/src/testing/reporter-format.d.ts.map +1 -0
  151. package/dist/src/testing/reporter-format.js +870 -0
  152. package/dist/src/testing/reporter-format.js.map +1 -0
  153. package/dist/src/testing/runner.d.ts +31 -0
  154. package/dist/src/testing/runner.d.ts.map +1 -0
  155. package/dist/src/testing/runner.js +258 -0
  156. package/dist/src/testing/runner.js.map +1 -0
  157. package/dist/src/testing/state.d.ts +138 -0
  158. package/dist/src/testing/state.d.ts.map +1 -0
  159. package/dist/src/testing/state.js +38 -0
  160. package/dist/src/testing/state.js.map +1 -0
  161. package/dist/src/testing/types.d.ts +319 -0
  162. package/dist/src/testing/types.d.ts.map +1 -0
  163. package/dist/src/testing/types.js +13 -0
  164. package/dist/src/testing/types.js.map +1 -0
  165. package/dist/src/utils/channel.d.ts +7 -7
  166. package/dist/src/utils/channel.d.ts.map +1 -1
  167. package/dist/src/utils/channel.js +1 -1
  168. package/dist/src/utils/channel.js.map +1 -1
  169. package/dist/src/utils/formatPromptMessages.d.ts.map +1 -1
  170. package/dist/src/utils/getPromptBySelector.d.ts.map +1 -1
  171. package/dist/src/utils/promisifyResult.d.ts +1 -1
  172. package/dist/src/utils/promisifyResult.d.ts.map +1 -1
  173. package/dist/src/utils/promisifyResult.js.map +1 -1
  174. package/dist/src/utils/schemaMatches.d.ts +5 -5
  175. package/dist/src/utils/schemaMatches.d.ts.map +1 -1
  176. package/dist/src/utils/schemaMatches.js.map +1 -1
  177. package/dist/src/vitest/index.d.ts +5 -0
  178. package/dist/src/vitest/index.d.ts.map +1 -0
  179. package/dist/src/vitest/index.js +23 -0
  180. package/dist/src/vitest/index.js.map +1 -0
  181. package/dist/src/vitest/reporter.d.ts +19 -0
  182. package/dist/src/vitest/reporter.d.ts.map +1 -0
  183. package/dist/src/vitest/reporter.js +34 -0
  184. package/dist/src/vitest/reporter.js.map +1 -0
  185. package/dist/tsconfig.tsbuildinfo +1 -1
  186. package/docs/ci-evals-annotations.mdx +190 -0
  187. package/docs/ci-evals-jest.mdx +78 -0
  188. package/docs/ci-evals-vitest.mdx +240 -0
  189. package/docs/ci-evals.mdx +263 -0
  190. package/docs/overview.mdx +9 -1
  191. package/package.json +49 -17
  192. package/src/__generated__/api/v1.ts +254 -2
  193. package/src/jest/index.ts +124 -0
  194. package/src/jest/reporter.ts +22 -0
  195. package/src/prompts/sdks/toAI.ts +4 -3
  196. package/src/prompts/sdks/toAnthropic.ts +4 -3
  197. package/src/prompts/sdks/toOpenAI.ts +4 -3
  198. package/src/prompts/sdks/toSDK.ts +16 -11
  199. package/src/prompts/sdks/types.ts +2 -2
  200. package/src/testing/acceptance.ts +190 -0
  201. package/src/testing/define-api.ts +279 -0
  202. package/src/testing/helpers.ts +251 -0
  203. package/src/testing/phoenix-test-tracking.ts +637 -0
  204. package/src/testing/report-artifacts.ts +272 -0
  205. package/src/testing/report-run.ts +44 -0
  206. package/src/testing/reporter-format.ts +1072 -0
  207. package/src/testing/runner.ts +350 -0
  208. package/src/testing/state.ts +165 -0
  209. package/src/testing/types.ts +366 -0
  210. package/src/utils/channel.ts +17 -15
  211. package/src/utils/promisifyResult.ts +6 -4
  212. package/src/utils/schemaMatches.ts +12 -10
  213. package/src/vitest/index.ts +57 -0
  214. package/src/vitest/reporter.ts +32 -0
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Cross-process bridge for the end-of-run Phoenix summary.
3
+ *
4
+ * Both Vitest and Jest run test files in separate worker processes, so a
5
+ * suite's results live in a different process than the reporter that prints
6
+ * the final summary. To bridge that gap, each worker writes a small JSON
7
+ * artifact describing its suite to a shared temp directory when the suite
8
+ * finishes (see {@link writeSuiteSummaryArtifact}). At the end of the run the
9
+ * reporter — back in the main process — reads every artifact written since the
10
+ * run began (see {@link readSuiteSummaryArtifacts}) and prints them together.
11
+ *
12
+ * Everything here is best-effort: a failure to write or read an artifact must
13
+ * never fail a user's test, so the file system calls swallow their errors.
14
+ */
15
+ import { createHash } from "node:crypto";
16
+ import {
17
+ existsSync,
18
+ mkdirSync,
19
+ readdirSync,
20
+ readFileSync,
21
+ renameSync,
22
+ rmSync,
23
+ statSync,
24
+ writeFileSync,
25
+ } from "node:fs";
26
+ import { tmpdir } from "node:os";
27
+ import { join } from "node:path";
28
+
29
+ import type { SuiteSummary } from "./reporter-format";
30
+ import type { SuiteState, TestResult } from "./state";
31
+
32
+ /**
33
+ * Env var that overrides where suite summary artifacts are written. When unset,
34
+ * a stable per-project subdirectory of the OS temp dir is used (see
35
+ * {@link getSuiteSummaryReportDir}).
36
+ */
37
+ export const PHOENIX_TEST_REPORT_DIR_ENV_VAR = "PHOENIX_TEST_REPORT_DIR";
38
+
39
+ const ARTIFACT_VERSION = 1;
40
+ const ARTIFACT_FILE_PREFIX = "suite-";
41
+ let artifactCounter = 0;
42
+
43
+ /** The on-disk shape of a single artifact file. */
44
+ interface SuiteSummaryArtifact {
45
+ version: typeof ARTIFACT_VERSION;
46
+ createdAtMs: number;
47
+ suite: SuiteSummary;
48
+ }
49
+
50
+ /**
51
+ * Resolve the directory where suite summary artifacts are written and read.
52
+ *
53
+ * Honors {@link PHOENIX_TEST_REPORT_DIR_ENV_VAR} when set. Otherwise it derives
54
+ * a directory under the OS temp dir, namespaced by a hash of the current
55
+ * working directory so that concurrent test runs in different projects don't
56
+ * read each other's artifacts.
57
+ */
58
+ export function getSuiteSummaryReportDir(): string {
59
+ const configuredReportDir = process.env[PHOENIX_TEST_REPORT_DIR_ENV_VAR];
60
+ if (configuredReportDir && configuredReportDir.trim().length > 0) {
61
+ return configuredReportDir;
62
+ }
63
+ const cwdHash = createHash("sha256")
64
+ .update(process.cwd())
65
+ .digest("hex")
66
+ .slice(0, 16);
67
+ return join(tmpdir(), "phoenix-client-test-reports", cwdHash);
68
+ }
69
+
70
+ /**
71
+ * Delete any artifacts left over from a previous run. The reporter calls this
72
+ * at the start of every run so summaries from an earlier watch-mode invocation
73
+ * don't bleed into the next one.
74
+ */
75
+ export function clearSuiteSummaryArtifacts(): void {
76
+ const reportDir = getSuiteSummaryReportDir();
77
+ mkdirSync(reportDir, { recursive: true });
78
+ for (const fileName of readdirSync(reportDir)) {
79
+ if (!isArtifactFileName(fileName)) {
80
+ continue;
81
+ }
82
+ try {
83
+ rmSync(join(reportDir, fileName), { force: true });
84
+ } catch {
85
+ continue;
86
+ }
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Write one suite's summary to the report directory. Called from each worker
92
+ * when a suite finishes.
93
+ *
94
+ * The file is written to a `.tmp` path first and then atomically renamed into
95
+ * place, so the reader never observes a half-written artifact. Failures are
96
+ * intentionally swallowed — reporting must not break the user's tests.
97
+ */
98
+ export function writeSuiteSummaryArtifact(suite: SuiteState): void {
99
+ try {
100
+ const reportDir = getSuiteSummaryReportDir();
101
+ mkdirSync(reportDir, { recursive: true });
102
+ const artifact: SuiteSummaryArtifact = {
103
+ version: ARTIFACT_VERSION,
104
+ createdAtMs: Date.now(),
105
+ suite: createSuiteSummary(suite),
106
+ };
107
+ const fileName = `${ARTIFACT_FILE_PREFIX}${process.pid}-${Date.now()}-${artifactCounter++}.json`;
108
+ const artifactPath = join(reportDir, fileName);
109
+ const temporaryPath = `${artifactPath}.tmp`;
110
+ writeFileSync(temporaryPath, JSON.stringify(artifact), "utf8");
111
+ renameSync(temporaryPath, artifactPath);
112
+ } catch {
113
+ // Reporting artifacts are best-effort and should not fail user tests.
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Read back all suite summaries the workers wrote during this run, sorted by
119
+ * suite name for stable output.
120
+ *
121
+ * @param sinceMs - When provided, only artifacts created at or after this
122
+ * timestamp are returned, so summaries from a previous run that share the
123
+ * report directory are ignored. A one-second slack is allowed to absorb
124
+ * small clock differences between worker processes.
125
+ */
126
+ export function readSuiteSummaryArtifacts({
127
+ sinceMs,
128
+ }: {
129
+ sinceMs?: number;
130
+ } = {}): SuiteSummary[] {
131
+ const reportDir = getSuiteSummaryReportDir();
132
+ if (!existsSync(reportDir)) {
133
+ return [];
134
+ }
135
+
136
+ const suites: SuiteSummary[] = [];
137
+ for (const fileName of readdirSync(reportDir)) {
138
+ if (
139
+ !fileName.startsWith(ARTIFACT_FILE_PREFIX) ||
140
+ !fileName.endsWith(".json")
141
+ ) {
142
+ continue;
143
+ }
144
+ const artifactPath = join(reportDir, fileName);
145
+ try {
146
+ const stats = statSync(artifactPath);
147
+ if (sinceMs !== undefined && stats.mtimeMs < sinceMs - 1000) {
148
+ continue;
149
+ }
150
+ const artifact = JSON.parse(readFileSync(artifactPath, "utf8"));
151
+ if (!isSuiteSummaryArtifact(artifact)) {
152
+ continue;
153
+ }
154
+ if (sinceMs !== undefined && artifact.createdAtMs < sinceMs - 1000) {
155
+ continue;
156
+ }
157
+ suites.push(artifact.suite);
158
+ } catch {
159
+ continue;
160
+ }
161
+ }
162
+
163
+ return suites.sort((leftSuite, rightSuite) =>
164
+ leftSuite.name.localeCompare(rightSuite.name)
165
+ );
166
+ }
167
+
168
+ /**
169
+ * Project the live in-memory {@link SuiteState} down to the serializable
170
+ * {@link SuiteSummary} that gets written to disk. Drops anything that can't or
171
+ * shouldn't cross the process boundary (clients, tracers, raw Error objects).
172
+ */
173
+ function createSuiteSummary(suite: SuiteState): SuiteSummary {
174
+ return {
175
+ name: suite.name,
176
+ trackingDisabled: suite.trackingDisabled,
177
+ trackingDisabledReason: suite.trackingDisabledReason,
178
+ setupError: suite.setupError
179
+ ? { message: suite.setupError.message }
180
+ : undefined,
181
+ uploadFailureCount: suite.uploadFailureCount,
182
+ results: suite.results.map(createTestResultSummary),
183
+ acceptanceResults: suite.acceptanceResults,
184
+ links: suite.links.map(({ label, url }) => ({ label, url })),
185
+ };
186
+ }
187
+
188
+ function createTestResultSummary(result: TestResult): TestResult {
189
+ return {
190
+ suiteName: result.suiteName,
191
+ testName: result.testName,
192
+ status: result.status,
193
+ output: toJsonSafeValue(result.output),
194
+ annotations: result.annotations.map(
195
+ ({
196
+ name,
197
+ score,
198
+ label,
199
+ explanation,
200
+ annotatorKind,
201
+ metadata,
202
+ traceId,
203
+ }) => ({
204
+ name,
205
+ score,
206
+ label,
207
+ explanation,
208
+ annotatorKind,
209
+ metadata,
210
+ traceId,
211
+ })
212
+ ),
213
+ error: result.error,
214
+ durationMs: result.durationMs,
215
+ repetitionNumber: result.repetitionNumber,
216
+ repetitions: result.repetitions,
217
+ dryRun: result.dryRun,
218
+ traceId: result.traceId,
219
+ runId: result.runId,
220
+ exampleId: result.exampleId,
221
+ };
222
+ }
223
+
224
+ /**
225
+ * Make a recorded test output safe to JSON-serialize: `bigint` values are
226
+ * stringified, and anything that still can't be serialized (cycles, etc.)
227
+ * falls back to its `String()` form rather than throwing.
228
+ */
229
+ function toJsonSafeValue(value: unknown): unknown {
230
+ if (value === undefined) {
231
+ return undefined;
232
+ }
233
+ try {
234
+ return JSON.parse(
235
+ JSON.stringify(value, (_key, nestedValue: unknown) =>
236
+ typeof nestedValue === "bigint" ? nestedValue.toString() : nestedValue
237
+ )
238
+ );
239
+ } catch {
240
+ return String(value);
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Validate a parsed artifact before trusting it. Guards against version skew
246
+ * and partially-written or unrelated JSON files in the report directory.
247
+ */
248
+ function isSuiteSummaryArtifact(value: unknown): value is SuiteSummaryArtifact {
249
+ if (!isRecord(value)) {
250
+ return false;
251
+ }
252
+ const suite = value.suite;
253
+ return (
254
+ value.version === ARTIFACT_VERSION &&
255
+ typeof value.createdAtMs === "number" &&
256
+ isRecord(suite) &&
257
+ typeof suite.name === "string" &&
258
+ Array.isArray(suite.results) &&
259
+ Array.isArray(suite.links)
260
+ );
261
+ }
262
+
263
+ function isArtifactFileName(fileName: string): boolean {
264
+ return (
265
+ fileName.startsWith(ARTIFACT_FILE_PREFIX) &&
266
+ (fileName.endsWith(".json") || fileName.endsWith(".json.tmp"))
267
+ );
268
+ }
269
+
270
+ function isRecord(value: unknown): value is Record<string, unknown> {
271
+ return typeof value === "object" && value !== null;
272
+ }
@@ -0,0 +1,44 @@
1
+ import {
2
+ clearSuiteSummaryArtifacts,
3
+ readSuiteSummaryArtifacts,
4
+ } from "./report-artifacts";
5
+ import { printSuiteSummaries } from "./reporter-format";
6
+ import { clearAllSuites, getAllSuites } from "./runner";
7
+
8
+ /**
9
+ * Shared "print the Phoenix summary once per run" policy for the jest and
10
+ * vitest reporters. The two frameworks differ only in which lifecycle methods
11
+ * they call; both delegate to {@link begin} on run start and {@link finish}
12
+ * on run end.
13
+ */
14
+ export class SuiteSummaryReportRun {
15
+ private hasPrinted = false;
16
+ private runStartedAtMs = 0;
17
+
18
+ /**
19
+ * Reset state at the start of a run so suites/artifacts from a previous
20
+ * watch-mode invocation don't leak forward.
21
+ */
22
+ begin(): void {
23
+ this.hasPrinted = false;
24
+ this.runStartedAtMs = Date.now();
25
+ clearAllSuites();
26
+ clearSuiteSummaryArtifacts();
27
+ }
28
+
29
+ /**
30
+ * Print the Phoenix summary once. Prefers the on-disk artifacts written by
31
+ * suite workers, falling back to the in-process registry. A no-op once the
32
+ * summary has already been printed for this run.
33
+ */
34
+ finish(): void {
35
+ if (this.hasPrinted) return;
36
+ const artifactSuites = readSuiteSummaryArtifacts({
37
+ sinceMs: this.runStartedAtMs,
38
+ });
39
+ const suites = artifactSuites.length > 0 ? artifactSuites : getAllSuites();
40
+ if (suites.length === 0) return;
41
+ this.hasPrinted = true;
42
+ printSuiteSummaries(suites);
43
+ }
44
+ }