@wix/pathgrade 0.37.0 → 1.0.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 (116) hide show
  1. package/README.md +116 -32
  2. package/dist/adapter-kit/index.d.ts +17 -0
  3. package/dist/adapter-kit/index.js +14 -0
  4. package/dist/adapters/jest/index.d.ts +3 -0
  5. package/dist/adapters/jest/index.js +3 -0
  6. package/dist/adapters/jest/invocation-adapter.d.ts +15 -0
  7. package/dist/adapters/jest/invocation-adapter.js +115 -0
  8. package/dist/adapters/jest/lifecycle.d.ts +21 -0
  9. package/dist/adapters/jest/lifecycle.js +62 -0
  10. package/dist/adapters/jest/metadata.d.ts +6 -0
  11. package/dist/adapters/jest/metadata.js +35 -0
  12. package/dist/adapters/jest/reporter.cjs +10 -0
  13. package/dist/adapters/jest/reporter.d.ts +5 -0
  14. package/dist/adapters/jest/reporter.js +67 -0
  15. package/dist/adapters/jest/results.d.ts +31 -0
  16. package/dist/adapters/jest/results.js +144 -0
  17. package/dist/adapters/jest/runner-adapter.d.ts +9 -0
  18. package/dist/adapters/jest/runner-adapter.js +49 -0
  19. package/dist/adapters/jest/setup.d.ts +1 -0
  20. package/dist/adapters/jest/setup.js +10 -0
  21. package/dist/adapters/node-test/index.d.ts +8 -0
  22. package/dist/adapters/node-test/index.js +106 -0
  23. package/dist/adapters/node-test/invocation-adapter.d.ts +5 -0
  24. package/dist/adapters/node-test/invocation-adapter.js +47 -0
  25. package/dist/adapters/node-test/runner-adapter.d.ts +2 -0
  26. package/dist/adapters/node-test/runner-adapter.js +116 -0
  27. package/dist/adapters/vitest/index.d.ts +9 -0
  28. package/dist/adapters/vitest/index.js +82 -0
  29. package/dist/adapters/vitest/lifecycle.d.ts +24 -0
  30. package/dist/adapters/vitest/lifecycle.js +41 -0
  31. package/dist/adapters/vitest/reporter.d.ts +12 -0
  32. package/dist/adapters/vitest/reporter.js +75 -0
  33. package/dist/adapters/vitest/setup.d.ts +1 -0
  34. package/dist/adapters/vitest/setup.js +3 -0
  35. package/dist/agents/codex-app-server/agent.js +5 -1
  36. package/dist/agents/codex-app-server/protocol/index.js +3 -3
  37. package/dist/agents/codex-app-server/turn-completion.d.ts +1 -0
  38. package/dist/agents/codex-app-server/turn-completion.js +9 -0
  39. package/dist/commands/affected.js +22 -15
  40. package/dist/commands/run-args.d.ts +4 -3
  41. package/dist/commands/run-args.js +12 -6
  42. package/dist/commands/run-changed.d.ts +4 -4
  43. package/dist/commands/run-changed.js +45 -58
  44. package/dist/config/pathgrade.d.ts +48 -0
  45. package/dist/config/pathgrade.js +204 -0
  46. package/dist/pathgrade.d.ts +2 -2
  47. package/dist/pathgrade.js +35 -34
  48. package/dist/plugin/index.d.ts +2 -9
  49. package/dist/plugin/index.js +1 -81
  50. package/dist/plugin/lifecycle.d.ts +1 -45
  51. package/dist/plugin/lifecycle.js +1 -146
  52. package/dist/plugin/reporter.d.ts +1 -37
  53. package/dist/plugin/reporter.js +1 -280
  54. package/dist/plugin/setup.d.ts +1 -1
  55. package/dist/plugin/setup.js +1 -3
  56. package/dist/providers/copy-filter.d.ts +5 -0
  57. package/dist/providers/copy-filter.js +9 -0
  58. package/dist/providers/sandbox.js +2 -2
  59. package/dist/providers/workspace.d.ts +6 -0
  60. package/dist/providers/workspace.js +34 -14
  61. package/dist/reporters/diagnostics.d.ts +2 -40
  62. package/dist/reporters/diagnostics.js +1 -80
  63. package/dist/reporters/report-summary.d.ts +6 -0
  64. package/dist/reporters/report-summary.js +29 -0
  65. package/dist/reporting/artifacts.d.ts +2 -0
  66. package/dist/reporting/artifacts.js +20 -0
  67. package/dist/reporting/core.d.ts +2 -0
  68. package/dist/reporting/core.js +173 -0
  69. package/dist/reporting/types.d.ts +57 -0
  70. package/dist/reporting/types.js +1 -0
  71. package/dist/reporting/vitest-edge.d.ts +1 -0
  72. package/dist/reporting/vitest-edge.js +1 -0
  73. package/dist/runners/adapter-loader.d.ts +17 -0
  74. package/dist/runners/adapter-loader.js +67 -0
  75. package/dist/runners/adapter.d.ts +65 -0
  76. package/dist/runners/adapter.js +1 -0
  77. package/dist/runners/invocation.d.ts +10 -0
  78. package/dist/runners/invocation.js +1 -0
  79. package/dist/runners/lifecycle-hooks.d.ts +2 -0
  80. package/dist/runners/lifecycle-hooks.js +18 -0
  81. package/dist/runners/model-builders.d.ts +4 -0
  82. package/dist/runners/model-builders.js +67 -0
  83. package/dist/runners/model-validation.d.ts +12 -0
  84. package/dist/runners/model-validation.js +213 -0
  85. package/dist/runners/model.d.ts +117 -0
  86. package/dist/runners/model.js +1 -0
  87. package/dist/runners/orchestrator.d.ts +30 -0
  88. package/dist/runners/orchestrator.js +74 -0
  89. package/dist/runners/report-projection.d.ts +6 -0
  90. package/dist/runners/report-projection.js +62 -0
  91. package/dist/runners/selection.d.ts +6 -0
  92. package/dist/runners/selection.js +10 -0
  93. package/dist/runners/vitest-adapter.d.ts +8 -0
  94. package/dist/runners/vitest-adapter.js +108 -0
  95. package/dist/runners/vitest-invocation.d.ts +10 -0
  96. package/dist/runners/vitest-invocation.js +44 -0
  97. package/dist/runners/vitest-lifecycle.d.ts +43 -0
  98. package/dist/runners/vitest-lifecycle.js +150 -0
  99. package/dist/sdk/agent.js +15 -16
  100. package/dist/sdk/case-context.d.ts +20 -0
  101. package/dist/sdk/case-context.js +31 -0
  102. package/dist/sdk/diagnostics.d.ts +40 -0
  103. package/dist/sdk/diagnostics.js +80 -0
  104. package/dist/sdk/eval-runtime.d.ts +4 -0
  105. package/dist/sdk/eval-runtime.js +11 -1
  106. package/dist/sdk/evaluate.js +2 -1
  107. package/dist/sdk/index.d.ts +2 -0
  108. package/dist/sdk/index.js +1 -0
  109. package/dist/sdk/lifecycle.d.ts +40 -0
  110. package/dist/sdk/lifecycle.js +170 -0
  111. package/dist/sdk/result-capture.d.ts +18 -0
  112. package/dist/sdk/result-capture.js +49 -0
  113. package/dist/sdk/types.d.ts +1 -1
  114. package/dist/types.d.ts +2 -2
  115. package/dist/utils/llm-providers/anthropic.js +9 -2
  116. package/package.json +56 -4
@@ -0,0 +1,67 @@
1
+ import * as path from 'node:path';
2
+ import { execSync } from 'node:child_process';
3
+ import { fmt, getPathgradeDir, printReportSummary, readSidecar, resolvePathgradeConfig, runWithAdapter, } from '@wix/pathgrade/adapter-kit';
4
+ import { getJestLifecycleMetadata } from './lifecycle.js';
5
+ import { readJestMetadata, removeJestMetadata } from './metadata.js';
6
+ import { createJestAdapter } from './runner-adapter.js';
7
+ export default class PathgradeJestReporter {
8
+ async onRunComplete(_contexts, results) {
9
+ const cwd = process.cwd();
10
+ const config = await resolvePathgradeConfig({ cwd });
11
+ const mode = config.reporter ?? 'cli';
12
+ const outputDir = getPathgradeDir(cwd);
13
+ const metadataByCaseId = new Map([
14
+ ...readJestMetadata(),
15
+ ...getJestLifecycleMetadata(),
16
+ ]);
17
+ const exitCode = await runWithAdapter({
18
+ adapter: createJestAdapter({
19
+ results,
20
+ metadataByCaseId,
21
+ }),
22
+ options: {
23
+ cwd,
24
+ discovery: { cwd },
25
+ runnerArgs: [],
26
+ env: process.env,
27
+ artifactRoot: outputDir,
28
+ reporterMode: mode,
29
+ threshold: config.ci.threshold,
30
+ writeEmptyReport: false,
31
+ warn: warning => console.warn(warning),
32
+ log: () => console.log(`\n ${fmt.dim('Results written to')} ${outputDir}\n`),
33
+ loadSelection: async () => (await readSidecar(cwd, msg => {
34
+ console.warn(`[pathgrade] ${msg}`);
35
+ })) ?? undefined,
36
+ printSummary: summaries => {
37
+ printReportSummary(summaries, {
38
+ forceVerbose: config.diagnostics
39
+ || process.env.PATHGRADE_DIAGNOSTICS === '1',
40
+ });
41
+ },
42
+ openBrowser: () => this.openBrowserViewer(),
43
+ onThresholdFailure: ({ overallPassRate, threshold }) => {
44
+ process.exitCode = 1;
45
+ console.log(`\n ${fmt.fail('CI THRESHOLD FAILED')} avg score ${fmt.bold(overallPassRate.toFixed(3))} < threshold ${fmt.bold(String(threshold))}\n`);
46
+ },
47
+ },
48
+ });
49
+ if (exitCode !== 0 && (process.exitCode === undefined || process.exitCode === 0)) {
50
+ process.exitCode = exitCode;
51
+ }
52
+ removeJestMetadata();
53
+ }
54
+ openBrowserViewer() {
55
+ const viewerPath = path.resolve(import.meta.dirname, '..', '..', 'viewer.html');
56
+ try {
57
+ const openCmd = process.platform === 'darwin' ? 'open'
58
+ : process.platform === 'win32' ? 'start'
59
+ : 'xdg-open';
60
+ execSync(`${openCmd} "${viewerPath}"`, { stdio: 'ignore' });
61
+ console.log(`\n ${fmt.dim('Opened viewer in browser')}\n`);
62
+ }
63
+ catch {
64
+ console.log(`\n ${fmt.dim('Open manually:')} ${viewerPath}\n`);
65
+ }
66
+ }
67
+ }
@@ -0,0 +1,31 @@
1
+ import type { AdapterRunHandle, NormalizedRunSnapshot, PathgradeTestMeta } from '@wix/pathgrade/adapter-kit';
2
+ export interface JestAggregatedResult {
3
+ testResults?: JestFileResult[];
4
+ }
5
+ export interface JestFileResult {
6
+ testFilePath: string;
7
+ assertionResults?: JestAssertionResult[];
8
+ testResults?: JestAssertionResult[];
9
+ }
10
+ export interface JestAssertionResult {
11
+ ancestorTitles?: string[];
12
+ title: string;
13
+ fullName?: string;
14
+ status: string;
15
+ duration?: number | null;
16
+ failureMessages?: string[];
17
+ location?: {
18
+ line?: number | null;
19
+ column?: number | null;
20
+ } | null;
21
+ }
22
+ export declare function normalizeJestRunResults(input: {
23
+ run: AdapterRunHandle;
24
+ results: JestAggregatedResult;
25
+ metadataByCaseId?: Map<string, PathgradeTestMeta[]>;
26
+ }): NormalizedRunSnapshot;
27
+ export declare function jestCaseId(input: {
28
+ filePath: string;
29
+ fullName: string;
30
+ occurrenceCounts?: Map<string, number>;
31
+ }): string;
@@ -0,0 +1,144 @@
1
+ export function normalizeJestRunResults(input) {
2
+ const runId = `${input.run.adapterName}:run`;
3
+ const files = input.results.testResults ?? [];
4
+ const units = files.map((fileResult, index) => ({
5
+ id: unitId(index),
6
+ runId,
7
+ displayName: fileResult.testFilePath,
8
+ groupingHints: [{
9
+ kind: 'source',
10
+ key: fileResult.testFilePath,
11
+ label: fileResult.testFilePath,
12
+ order: index,
13
+ }],
14
+ nativeReferences: [{ kind: 'jest-file', id: fileResult.testFilePath }],
15
+ }));
16
+ const occurrenceCounts = new Map();
17
+ return {
18
+ version: 1,
19
+ completeness: 'final',
20
+ model: {
21
+ run: {
22
+ id: runId,
23
+ adapterName: input.run.adapterName,
24
+ status: input.run.status,
25
+ },
26
+ units,
27
+ cases: files.flatMap((fileResult, fileIndex) => assertionsForFile(fileResult).map(assertion => {
28
+ const caseId = jestCaseId({
29
+ filePath: fileResult.testFilePath,
30
+ fullName: assertion.fullName ?? fullCaseName(assertion),
31
+ occurrenceCounts,
32
+ });
33
+ return runCase({
34
+ runId,
35
+ unitId: unitId(fileIndex),
36
+ caseId,
37
+ assertion,
38
+ filePath: fileResult.testFilePath,
39
+ evaluations: input.metadataByCaseId?.get(caseId) ?? [],
40
+ });
41
+ })),
42
+ },
43
+ };
44
+ }
45
+ function assertionsForFile(fileResult) {
46
+ return fileResult.assertionResults ?? fileResult.testResults ?? [];
47
+ }
48
+ export function jestCaseId(input) {
49
+ const base = `jest:${input.filePath}:${slug(input.fullName)}`;
50
+ const current = input.occurrenceCounts?.get(base) ?? 0;
51
+ const occurrence = current + 1;
52
+ input.occurrenceCounts?.set(base, occurrence);
53
+ return `${base}:${occurrence}`;
54
+ }
55
+ function runCase(input) {
56
+ const state = normalizeState(input.assertion.status);
57
+ const attemptId = `${input.caseId}:attempt-1`;
58
+ const evaluations = input.evaluations.map((entry, index) => ({
59
+ id: `${attemptId}:evaluation-${index + 1}`,
60
+ attemptId,
61
+ score: entry.score,
62
+ ...(entry.trial ? { trial: entry.trial } : {}),
63
+ ...(entry.diagnostics ? { diagnostics: entry.diagnostics } : {}),
64
+ }));
65
+ const failureMessage = input.assertion.failureMessages?.find(message => message.trim().length > 0);
66
+ return {
67
+ id: input.caseId,
68
+ runId: input.runId,
69
+ unitId: input.unitId,
70
+ name: fullCaseName(input.assertion),
71
+ state,
72
+ scoringPolicy: scoringPolicyForCase(state, evaluations),
73
+ groupingHints: [{
74
+ kind: 'source',
75
+ key: input.filePath,
76
+ label: input.filePath,
77
+ order: 0,
78
+ }],
79
+ nativeReferences: [{
80
+ kind: 'jest-case',
81
+ id: input.caseId,
82
+ metadata: {
83
+ status: input.assertion.status,
84
+ ...(input.assertion.location?.line ? { line: input.assertion.location.line } : {}),
85
+ ...(input.assertion.location?.column ? { column: input.assertion.location.column } : {}),
86
+ },
87
+ }],
88
+ attempts: [{
89
+ id: attemptId,
90
+ caseId: input.caseId,
91
+ outcome: outcomeForCase(state, failureMessage),
92
+ durationMs: input.assertion.duration ?? 0,
93
+ ...(evaluations.length > 0 ? { evaluations } : {}),
94
+ ...(failureMessage ? {
95
+ assertions: [{
96
+ id: `${attemptId}:assertion-1`,
97
+ attemptId,
98
+ name: 'Jest assertion failure',
99
+ status: 'failed',
100
+ severity: 'error',
101
+ message: failureMessage,
102
+ }],
103
+ } : {}),
104
+ }],
105
+ };
106
+ }
107
+ function fullCaseName(assertion) {
108
+ const ancestors = assertion.ancestorTitles ?? [];
109
+ return [...ancestors, assertion.title].filter(Boolean).join(' > ');
110
+ }
111
+ function normalizeState(status) {
112
+ if (status === 'passed')
113
+ return 'passed';
114
+ if (status === 'skipped')
115
+ return 'skipped';
116
+ if (status === 'pending' || status === 'todo' || status === 'disabled')
117
+ return 'pending';
118
+ return 'failed';
119
+ }
120
+ function scoringPolicyForCase(state, evaluations) {
121
+ if (state === 'skipped' || state === 'pending')
122
+ return { kind: 'non-scoring', reason: state };
123
+ if (evaluations.length > 0)
124
+ return { kind: 'from-evaluations' };
125
+ return { kind: 'score', score: state === 'passed' ? 1 : 0 };
126
+ }
127
+ function outcomeForCase(state, failureMessage) {
128
+ if (state === 'passed')
129
+ return { kind: 'passed' };
130
+ if (state === 'failed')
131
+ return { kind: 'failed', ...(failureMessage ? { reason: failureMessage } : {}) };
132
+ return { kind: 'not-run', reason: state };
133
+ }
134
+ function unitId(index) {
135
+ return `jest:unit-${index + 1}`;
136
+ }
137
+ function slug(value) {
138
+ const clean = value
139
+ .trim()
140
+ .toLowerCase()
141
+ .replaceAll(/[^a-z0-9]+/g, '-')
142
+ .replaceAll(/^-|-$/g, '');
143
+ return clean || 'case';
144
+ }
@@ -0,0 +1,9 @@
1
+ import { type JestAggregatedResult } from './results.js';
2
+ import { type PathgradeTestMeta, RunnerAdapter } from '@wix/pathgrade/adapter-kit';
3
+ interface JestRunNative {
4
+ results?: JestAggregatedResult;
5
+ metadataByCaseId?: Map<string, PathgradeTestMeta[]>;
6
+ }
7
+ export declare function createJestAdapter(options?: JestRunNative): RunnerAdapter;
8
+ export declare function createPathgradeAdapter(): RunnerAdapter;
9
+ export {};
@@ -0,0 +1,49 @@
1
+ import { normalizeJestRunResults } from './results.js';
2
+ import { discoverPathgradeEvalFiles, } from '@wix/pathgrade/adapter-kit';
3
+ export function createJestAdapter(options = {}) {
4
+ return {
5
+ name: 'jest',
6
+ async discover(input) {
7
+ const include = input.include ?? ['**/*.eval.ts'];
8
+ const exclude = input.exclude ?? [];
9
+ const files = discoverPathgradeEvalFiles({
10
+ cwd: input.cwd,
11
+ include,
12
+ exclude,
13
+ });
14
+ return {
15
+ units: files.map(file => ({
16
+ id: file,
17
+ displayName: file,
18
+ sourceRef: file,
19
+ native: { file, cwd: input.cwd },
20
+ })),
21
+ };
22
+ },
23
+ async invoke(input) {
24
+ return {
25
+ adapterName: this.name,
26
+ status: input.signal?.aborted ? 'cancelled' : 'completed',
27
+ exitCode: input.signal?.aborted ? 1 : 0,
28
+ native: options,
29
+ };
30
+ },
31
+ async collectNormalizedRunSnapshot(run) {
32
+ const native = readJestRunNative(run);
33
+ return normalizeJestRunResults({
34
+ run,
35
+ results: native.results ?? { testResults: [] },
36
+ metadataByCaseId: native.metadataByCaseId,
37
+ });
38
+ },
39
+ };
40
+ }
41
+ export function createPathgradeAdapter() {
42
+ return createJestAdapter();
43
+ }
44
+ function readJestRunNative(run) {
45
+ if (typeof run.native === 'object' && run.native !== null) {
46
+ return run.native;
47
+ }
48
+ return {};
49
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import { installJestLifecycle } from './lifecycle.js';
2
+ const jestGlobals = globalThis;
3
+ if (jestGlobals.beforeEach && jestGlobals.afterEach && jestGlobals.expect?.getState) {
4
+ installJestLifecycle({
5
+ beforeEach: jestGlobals.beforeEach,
6
+ afterEach: jestGlobals.afterEach,
7
+ afterAll: jestGlobals.afterAll,
8
+ getState: () => jestGlobals.expect?.getState?.() ?? {},
9
+ });
10
+ }
@@ -0,0 +1,8 @@
1
+ import type { TestContext } from 'node:test';
2
+ type NodeTestFn = (t: TestContext) => unknown | Promise<unknown>;
3
+ type NodeTestOptions = Record<string, unknown>;
4
+ export declare function test(name: string, fn: NodeTestFn): void;
5
+ export declare function test(name: string, options: NodeTestOptions, fn: NodeTestFn): void;
6
+ export default test;
7
+ export { createNodeTestAdapter } from './runner-adapter.js';
8
+ export { createNodeTestInvocationAdapter } from './invocation-adapter.js';
@@ -0,0 +1,106 @@
1
+ import nodeTest from 'node:test';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { subscribeToEvalResults } from '../../sdk/result-capture.js';
6
+ import { createRunnerLifecycleHooks } from '../../runners/lifecycle-hooks.js';
7
+ const lifecycle = createRunnerLifecycleHooks();
8
+ let subscribed = false;
9
+ let caseCounter = 0;
10
+ export function test(name, optionsOrFn, maybeFn) {
11
+ installResultCapture();
12
+ const options = typeof optionsOrFn === 'function' ? undefined : optionsOrFn;
13
+ const fn = typeof optionsOrFn === 'function' ? optionsOrFn : maybeFn;
14
+ const sourceRef = callerSourceRef();
15
+ const caseId = `${sourceRef}:${name}:${++caseCounter}`;
16
+ nodeTest(name, options, async (ctx) => {
17
+ const startedAt = performance.now();
18
+ let state = 'passed';
19
+ let thrown;
20
+ try {
21
+ await lifecycle.withCaseContext({
22
+ caseId,
23
+ caseName: name,
24
+ filePath: sourceRef,
25
+ sourceRef,
26
+ runnerNativeId: caseId,
27
+ scope: 'runner-case',
28
+ }, async () => {
29
+ await fn?.(ctx);
30
+ });
31
+ }
32
+ catch (err) {
33
+ state = 'failed';
34
+ thrown = err;
35
+ }
36
+ finally {
37
+ const evaluations = await lifecycle.flushCase(caseId);
38
+ writeCase({
39
+ caseId,
40
+ name,
41
+ state,
42
+ runnerDurationMs: performance.now() - startedAt,
43
+ sourceRef,
44
+ filePath: sourceRef,
45
+ groupName: sourceRef,
46
+ runnerCaseId: caseId,
47
+ evaluations: toReportEvaluations(evaluations),
48
+ });
49
+ }
50
+ if (thrown)
51
+ throw thrown;
52
+ });
53
+ }
54
+ export default test;
55
+ export { createNodeTestAdapter } from './runner-adapter.js';
56
+ export { createNodeTestInvocationAdapter } from './invocation-adapter.js';
57
+ function installResultCapture() {
58
+ if (subscribed)
59
+ return;
60
+ subscribed = true;
61
+ subscribeToEvalResults(event => lifecycle.onResult(event), {
62
+ owner: 'adapter',
63
+ key: 'node-test-lifecycle',
64
+ });
65
+ }
66
+ function toReportEvaluations(evaluations) {
67
+ if (evaluations.length === 0)
68
+ return undefined;
69
+ return evaluations.map(entry => ({
70
+ score: entry.score,
71
+ trial: entry.trial,
72
+ diagnostics: entry.diagnostics,
73
+ }));
74
+ }
75
+ function writeCase(testCase) {
76
+ const resultsPath = process.env.PATHGRADE_NODE_TEST_RESULTS;
77
+ if (!resultsPath)
78
+ return;
79
+ fs.mkdirSync(path.dirname(resultsPath), { recursive: true });
80
+ const existing = fs.existsSync(resultsPath)
81
+ ? JSON.parse(fs.readFileSync(resultsPath, 'utf8'))
82
+ : [];
83
+ existing.push(testCase);
84
+ fs.writeFileSync(resultsPath, `${JSON.stringify(existing, null, 2)}\n`);
85
+ }
86
+ function callerSourceRef() {
87
+ const stack = new Error().stack ?? '';
88
+ const cwd = realpath(process.env.PATHGRADE_NODE_TEST_CWD ?? process.cwd());
89
+ for (const line of stack.split('\n')) {
90
+ const match = line.match(/\(?((?:file:\/\/)?[^() ]+\.eval\.[cm]?[jt]s)(?::\d+)?(?::\d+)?\)?/);
91
+ if (!match)
92
+ continue;
93
+ const raw = match[1];
94
+ const abs = realpath(raw.startsWith('file://') ? fileURLToPath(raw) : raw);
95
+ return path.relative(cwd, abs).replaceAll(path.sep, '/');
96
+ }
97
+ return 'node-test';
98
+ }
99
+ function realpath(value) {
100
+ try {
101
+ return fs.realpathSync(value);
102
+ }
103
+ catch {
104
+ return value;
105
+ }
106
+ }
@@ -0,0 +1,5 @@
1
+ import type { ResolvedPathgradeConfig } from '../../config/pathgrade.js';
2
+ import type { RunnerInvocationAdapter } from '../../runners/invocation.js';
3
+ export declare function createNodeTestInvocationAdapter(input: {
4
+ config: ResolvedPathgradeConfig;
5
+ }): RunnerInvocationAdapter;
@@ -0,0 +1,47 @@
1
+ import { readSidecar } from '../../affected/sidecar.js';
2
+ import { getPathgradeDir } from '../../reporters/results-path.js';
3
+ import { printReportSummary } from '../../reporters/report-summary.js';
4
+ import { runWithAdapter } from '../../runners/orchestrator.js';
5
+ import { createNodeTestAdapter } from './runner-adapter.js';
6
+ export function createNodeTestInvocationAdapter(input) {
7
+ return {
8
+ name: 'node-test',
9
+ async run(runInput) {
10
+ const selection = await readSidecar(runInput.cwd, msg => {
11
+ process.stderr.write(`[pathgrade] ${msg}\n`);
12
+ }) ?? undefined;
13
+ const include = runInput.selectedFiles && runInput.selectedFiles.length > 0
14
+ ? runInput.selectedFiles
15
+ : input.config.evals.include;
16
+ return await runWithAdapter({
17
+ adapter: createNodeTestAdapter(),
18
+ options: {
19
+ cwd: runInput.cwd,
20
+ discovery: {
21
+ cwd: runInput.cwd,
22
+ include,
23
+ exclude: runInput.selectedFiles ? [] : input.config.evals.exclude,
24
+ },
25
+ runnerArgs: runInput.runnerArgs,
26
+ env: runInput.env,
27
+ artifactRoot: getPathgradeDir(runInput.cwd),
28
+ reporterMode: input.config.reporter ?? 'cli',
29
+ threshold: input.config.ci.threshold,
30
+ selection,
31
+ printSummary: summaries => {
32
+ printReportSummary(summaries, {
33
+ forceVerbose: input.config.diagnostics
34
+ || runInput.env.PATHGRADE_DIAGNOSTICS === '1',
35
+ });
36
+ },
37
+ log: () => {
38
+ process.stdout.write(`\n Results written to ${getPathgradeDir(runInput.cwd)}\n\n`);
39
+ },
40
+ onThresholdFailure: ({ overallPassRate, threshold }) => {
41
+ process.stdout.write(`\n CI THRESHOLD FAILED avg score ${overallPassRate.toFixed(3)} < threshold ${threshold}\n\n`);
42
+ },
43
+ },
44
+ });
45
+ },
46
+ };
47
+ }
@@ -0,0 +1,2 @@
1
+ import type { RunnerAdapter } from '../../runners/adapter.js';
2
+ export declare function createNodeTestAdapter(): RunnerAdapter;
@@ -0,0 +1,116 @@
1
+ import * as path from 'node:path';
2
+ import * as os from 'node:os';
3
+ import { spawn } from 'node:child_process';
4
+ import fs from 'fs-extra';
5
+ import { discoverPathgradeEvalFiles } from '../../evals/discovery.js';
6
+ import { buildNormalizedRunSnapshotFromReportGroups } from '../../runners/model-builders.js';
7
+ export function createNodeTestAdapter() {
8
+ return {
9
+ name: 'node-test',
10
+ async discover(input) {
11
+ const include = input.include ?? ['**/*.eval.ts'];
12
+ const exclude = input.exclude ?? [];
13
+ const files = discoverPathgradeEvalFiles({
14
+ cwd: input.cwd,
15
+ include,
16
+ exclude,
17
+ });
18
+ return {
19
+ units: files.map(file => ({
20
+ id: file,
21
+ displayName: file,
22
+ sourceRef: file,
23
+ native: { file, cwd: input.cwd },
24
+ })),
25
+ };
26
+ },
27
+ async invoke(input) {
28
+ const resultsDir = await fs.mkdtemp(path.join(os.tmpdir(), 'pathgrade-node-test-'));
29
+ const resultsPath = path.join(resultsDir, 'cases.json');
30
+ const cwd = readDiscoveredCwd(input.discovered.units[0]?.native) ?? process.cwd();
31
+ const files = input.discovered.units
32
+ .map(unit => unit.sourceRef)
33
+ .filter((sourceRef) => typeof sourceRef === 'string');
34
+ if (files.length === 0) {
35
+ await fs.writeJson(resultsPath, []);
36
+ return {
37
+ adapterName: this.name,
38
+ status: 'completed',
39
+ exitCode: 0,
40
+ native: { resultsPath },
41
+ };
42
+ }
43
+ const exitCode = await spawnNodeTest({
44
+ testCwd: cwd,
45
+ files,
46
+ argv: input.argv,
47
+ env: {
48
+ ...input.env,
49
+ PATHGRADE_NODE_TEST_RESULTS: resultsPath,
50
+ PATHGRADE_NODE_TEST_CWD: cwd,
51
+ },
52
+ });
53
+ return {
54
+ adapterName: this.name,
55
+ status: input.signal?.aborted ? 'cancelled' : (exitCode === 0 ? 'completed' : 'failed'),
56
+ exitCode,
57
+ native: { resultsPath },
58
+ };
59
+ },
60
+ async collectNormalizedRunSnapshot(run) {
61
+ const native = readNodeTestRunNative(run);
62
+ const cases = await readCases(native.resultsPath);
63
+ const groupMap = new Map();
64
+ for (const testCase of cases) {
65
+ const groupName = testCase.groupName ?? testCase.filePath ?? testCase.sourceRef ?? 'node-test';
66
+ if (!groupMap.has(groupName))
67
+ groupMap.set(groupName, []);
68
+ groupMap.get(groupName).push(testCase);
69
+ }
70
+ return buildNormalizedRunSnapshotFromReportGroups(run, Array.from(groupMap.entries()).map(([groupName, groupedCases]) => ({
71
+ groupName,
72
+ cases: groupedCases,
73
+ })));
74
+ },
75
+ };
76
+ }
77
+ async function spawnNodeTest(input) {
78
+ const tsxLoader = import.meta.resolve('tsx');
79
+ const args = [
80
+ '--import',
81
+ tsxLoader,
82
+ '--test',
83
+ ...input.files,
84
+ ...input.argv,
85
+ ];
86
+ return await new Promise(resolve => {
87
+ const child = spawn(process.execPath, args, {
88
+ cwd: input.testCwd,
89
+ env: input.env,
90
+ stdio: 'inherit',
91
+ });
92
+ child.on('close', code => resolve(code ?? 0));
93
+ child.on('error', () => resolve(1));
94
+ });
95
+ }
96
+ function readNodeTestRunNative(run) {
97
+ if (typeof run.native === 'object'
98
+ && run.native !== null
99
+ && typeof run.native.resultsPath === 'string') {
100
+ return run.native;
101
+ }
102
+ return { resultsPath: '' };
103
+ }
104
+ async function readCases(resultsPath) {
105
+ if (!resultsPath || !(await fs.pathExists(resultsPath)))
106
+ return [];
107
+ return await fs.readJson(resultsPath);
108
+ }
109
+ function readDiscoveredCwd(native) {
110
+ if (typeof native === 'object'
111
+ && native !== null
112
+ && typeof native.cwd === 'string') {
113
+ return native.cwd;
114
+ }
115
+ return undefined;
116
+ }
@@ -0,0 +1,9 @@
1
+ import type { PathgradePluginOptions } from '../../sdk/types.js';
2
+ export type { PathgradePluginOptions };
3
+ /**
4
+ * Vitest plugin for pathgrade evaluations.
5
+ *
6
+ * Configures test file patterns, timeout, auto-dispose lifecycle hooks,
7
+ * and the pathgrade reporter for aggregate statistics.
8
+ */
9
+ export declare function pathgrade(opts?: PathgradePluginOptions): any;