@test-station/core 0.1.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.
@@ -0,0 +1,284 @@
1
+ import path from 'node:path';
2
+ import { loadConfig, applyConfigOverrides, resolveProjectContext } from './config.js';
3
+ import { preparePolicyContext, applyPolicyPipeline } from './policy.js';
4
+ import { resolveAdapterForSuite } from './adapters.js';
5
+ import { normalizeSuiteResult, buildReportFromSuiteResults, createSummary, normalizeStatus } from './report.js';
6
+ import { writeReportArtifacts } from './artifacts.js';
7
+
8
+ export async function runReport(options = {}) {
9
+ const loaded = await loadConfig(options.configPath || './test-station.config.mjs', {
10
+ cwd: options.cwd,
11
+ });
12
+ const effectiveConfig = applyConfigOverrides(loaded.config, loaded.configDir, {
13
+ cwd: options.cwd,
14
+ outputDir: options.outputDir,
15
+ workspaceFilters: options.workspaceFilters,
16
+ });
17
+ const effectiveLoaded = {
18
+ ...loaded,
19
+ config: effectiveConfig,
20
+ };
21
+ const context = {
22
+ config: effectiveConfig,
23
+ loadedConfig: effectiveLoaded,
24
+ project: resolveProjectContext(effectiveConfig, loaded.configDir),
25
+ execution: {
26
+ dryRun: options.dryRun ?? Boolean(effectiveConfig?.execution?.dryRun),
27
+ coverage: options.coverage ?? Boolean(effectiveConfig?.execution?.defaultCoverage),
28
+ },
29
+ };
30
+ context.policy = await preparePolicyContext(effectiveLoaded, context.project);
31
+
32
+ const suiteDefinitions = Array.isArray(effectiveConfig?.suites) ? effectiveConfig.suites : [];
33
+ const normalizedSuites = suiteDefinitions.map((suiteDefinition) => normalizeSuiteDefinition(suiteDefinition, context));
34
+ const packageCatalog = resolvePackageCatalog(effectiveConfig, normalizedSuites, context.project);
35
+ context.packageCatalog = packageCatalog;
36
+ const startedAt = Date.now();
37
+ const suiteResults = [];
38
+ emitEvent(options, {
39
+ type: 'run-start',
40
+ totalPackages: packageCatalog.length,
41
+ projectName: context.project.name,
42
+ });
43
+
44
+ for (const packageEntry of packageCatalog) {
45
+ const packageSuites = normalizedSuites.filter((suite) => suite.packageName === packageEntry.name);
46
+ emitEvent(options, {
47
+ type: 'package-start',
48
+ packageName: packageEntry.name,
49
+ packageLocation: packageEntry.location,
50
+ packageIndex: packageEntry.index + 1,
51
+ totalPackages: packageCatalog.length,
52
+ });
53
+
54
+ if (packageSuites.length === 0) {
55
+ emitEvent(options, {
56
+ type: 'package-complete',
57
+ packageName: packageEntry.name,
58
+ packageLocation: packageEntry.location,
59
+ packageIndex: packageEntry.index + 1,
60
+ totalPackages: packageCatalog.length,
61
+ status: 'skipped',
62
+ durationMs: 0,
63
+ summary: createSummary(),
64
+ });
65
+ continue;
66
+ }
67
+
68
+ const packageSuiteResults = [];
69
+
70
+ for (const suite of packageSuites) {
71
+ emitEvent(options, {
72
+ type: 'suite-start',
73
+ packageName: packageEntry.name,
74
+ packageLocation: packageEntry.location,
75
+ suiteId: suite.id,
76
+ suiteLabel: suite.label,
77
+ runtime: suite.adapter,
78
+ });
79
+
80
+ let normalized;
81
+ if (context.execution.dryRun) {
82
+ normalized = createDryRunSuiteResult(suite);
83
+ } else {
84
+ const adapter = await resolveAdapterForSuite(suite, effectiveLoaded);
85
+ const suiteStartedAt = Date.now();
86
+ try {
87
+ const result = await adapter.run({
88
+ config: effectiveConfig,
89
+ project: context.project,
90
+ suite,
91
+ execution: context.execution,
92
+ });
93
+ normalized = normalizeSuiteResult({
94
+ ...result,
95
+ durationMs: Number.isFinite(result?.durationMs) ? result.durationMs : (Date.now() - suiteStartedAt),
96
+ }, suite, suite.packageName);
97
+ } catch (error) {
98
+ normalized = normalizeSuiteResult({
99
+ status: 'failed',
100
+ durationMs: Date.now() - suiteStartedAt,
101
+ summary: createSummary({ total: 1, failed: 1 }),
102
+ tests: [
103
+ {
104
+ name: `${suite.label} failed`,
105
+ fullName: `${suite.label} failed`,
106
+ status: 'failed',
107
+ durationMs: Date.now() - suiteStartedAt,
108
+ failureMessages: [error instanceof Error ? error.message : String(error)],
109
+ assertions: [],
110
+ setup: [],
111
+ mocks: [],
112
+ rawDetails: {
113
+ stack: error instanceof Error ? error.stack || null : null,
114
+ },
115
+ module: 'uncategorized',
116
+ theme: 'uncategorized',
117
+ classificationSource: 'default',
118
+ },
119
+ ],
120
+ warnings: [],
121
+ output: {
122
+ stdout: '',
123
+ stderr: error instanceof Error ? error.stack || error.message : String(error),
124
+ },
125
+ }, suite, suite.packageName);
126
+ }
127
+ }
128
+
129
+ suiteResults.push(normalized);
130
+ packageSuiteResults.push(normalized);
131
+ emitEvent(options, {
132
+ type: 'suite-complete',
133
+ packageName: packageEntry.name,
134
+ packageLocation: packageEntry.location,
135
+ suiteId: suite.id,
136
+ suiteLabel: suite.label,
137
+ runtime: normalized.runtime,
138
+ result: normalized,
139
+ });
140
+ }
141
+
142
+ emitEvent(options, {
143
+ type: 'package-complete',
144
+ packageName: packageEntry.name,
145
+ packageLocation: packageEntry.location,
146
+ packageIndex: packageEntry.index + 1,
147
+ totalPackages: packageCatalog.length,
148
+ status: derivePackageStatus(packageSuiteResults),
149
+ durationMs: summarizeSuiteDurations(packageSuiteResults),
150
+ summary: summarizeSuiteCollection(packageSuiteResults),
151
+ });
152
+ }
153
+
154
+ const policyAdjustedSuites = await applyPolicyPipeline(context, suiteResults);
155
+ const report = buildReportFromSuiteResults(context, policyAdjustedSuites, Date.now() - startedAt);
156
+ const artifactPaths = options.writeArtifacts === false
157
+ ? { reportJsonPath: null, rawSuitePaths: [] }
158
+ : writeReportArtifacts(context, report, policyAdjustedSuites);
159
+
160
+ return {
161
+ context,
162
+ report,
163
+ suiteResults: policyAdjustedSuites,
164
+ artifactPaths,
165
+ };
166
+ }
167
+
168
+ function normalizeSuiteDefinition(suiteDefinition, context) {
169
+ const packageName = suiteDefinition.package || suiteDefinition.project || 'default';
170
+ return {
171
+ ...suiteDefinition,
172
+ id: suiteDefinition.id,
173
+ label: suiteDefinition.label || suiteDefinition.id,
174
+ adapter: suiteDefinition.adapter || 'custom',
175
+ packageName,
176
+ cwd: suiteDefinition.cwd || context.project.rootDir,
177
+ command: suiteDefinition.command || [],
178
+ };
179
+ }
180
+
181
+ function resolvePackageCatalog(config, suites, project) {
182
+ const configuredPackages = Array.isArray(config?.workspaceDiscovery?.packages)
183
+ ? config.workspaceDiscovery.packages
184
+ : [];
185
+ const catalog = [];
186
+ const seen = new Set();
187
+
188
+ for (const packageName of configuredPackages) {
189
+ const normalizedName = String(packageName || '').trim();
190
+ if (!normalizedName || seen.has(normalizedName)) {
191
+ continue;
192
+ }
193
+ seen.add(normalizedName);
194
+ catalog.push({
195
+ name: normalizedName,
196
+ location: `packages/${normalizedName}`,
197
+ index: catalog.length,
198
+ });
199
+ }
200
+
201
+ for (const suite of suites) {
202
+ if (seen.has(suite.packageName)) {
203
+ continue;
204
+ }
205
+ seen.add(suite.packageName);
206
+ catalog.push({
207
+ name: suite.packageName,
208
+ location: derivePackageLocation(suite, project),
209
+ index: catalog.length,
210
+ });
211
+ }
212
+
213
+ return catalog;
214
+ }
215
+
216
+ function createDryRunSuiteResult(suite) {
217
+ return {
218
+ id: suite.id,
219
+ label: suite.label,
220
+ runtime: suite.adapter,
221
+ command: Array.isArray(suite.command) ? suite.command.join(' ') : String(suite.command || ''),
222
+ cwd: suite.cwd,
223
+ status: normalizeStatus('skipped'),
224
+ durationMs: 0,
225
+ summary: createSummary(),
226
+ coverage: null,
227
+ tests: [],
228
+ warnings: ['Dry run: suite was not executed.'],
229
+ output: { stdout: '', stderr: '' },
230
+ rawArtifacts: [],
231
+ packageName: suite.packageName,
232
+ };
233
+ }
234
+
235
+ function emitEvent(options, event) {
236
+ if (typeof options.onEvent === 'function') {
237
+ options.onEvent(event);
238
+ }
239
+ }
240
+
241
+ function summarizeSuiteCollection(suites) {
242
+ return suites.reduce((summary, suite) => ({
243
+ total: summary.total + (suite.summary?.total || 0),
244
+ passed: summary.passed + (suite.summary?.passed || 0),
245
+ failed: summary.failed + (suite.summary?.failed || 0),
246
+ skipped: summary.skipped + (suite.summary?.skipped || 0),
247
+ }), createSummary());
248
+ }
249
+
250
+ function summarizeSuiteDurations(suites) {
251
+ return suites.reduce((total, suite) => total + (suite.durationMs || 0), 0);
252
+ }
253
+
254
+ function derivePackageStatus(suites) {
255
+ const summary = summarizeSuiteCollection(suites);
256
+ if (summary.failed > 0) {
257
+ return 'failed';
258
+ }
259
+ if (summary.passed > 0) {
260
+ return 'passed';
261
+ }
262
+ return 'skipped';
263
+ }
264
+
265
+ function derivePackageLocation(suite, project) {
266
+ if (!suite?.cwd) {
267
+ return suite?.packageName ? `packages/${suite.packageName}` : null;
268
+ }
269
+
270
+ const relativePath = project?.rootDir
271
+ ? relativePathSafe(project.rootDir, suite.cwd)
272
+ : null;
273
+
274
+ return relativePath || (suite?.packageName ? `packages/${suite.packageName}` : null);
275
+ }
276
+
277
+ function relativePathSafe(fromPath, toPath) {
278
+ try {
279
+ const relativePath = path.relative(fromPath, toPath);
280
+ return relativePath && !relativePath.startsWith('..') ? relativePath : null;
281
+ } catch {
282
+ return null;
283
+ }
284
+ }