@wix/pathgrade 0.37.0 → 1.0.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.
- package/README.md +116 -32
- package/dist/adapter-kit/index.d.ts +17 -0
- package/dist/adapter-kit/index.js +14 -0
- package/dist/adapters/jest/index.d.ts +3 -0
- package/dist/adapters/jest/index.js +3 -0
- package/dist/adapters/jest/invocation-adapter.d.ts +15 -0
- package/dist/adapters/jest/invocation-adapter.js +115 -0
- package/dist/adapters/jest/lifecycle.d.ts +21 -0
- package/dist/adapters/jest/lifecycle.js +62 -0
- package/dist/adapters/jest/metadata.d.ts +6 -0
- package/dist/adapters/jest/metadata.js +35 -0
- package/dist/adapters/jest/reporter.cjs +10 -0
- package/dist/adapters/jest/reporter.d.ts +5 -0
- package/dist/adapters/jest/reporter.js +67 -0
- package/dist/adapters/jest/results.d.ts +31 -0
- package/dist/adapters/jest/results.js +144 -0
- package/dist/adapters/jest/runner-adapter.d.ts +9 -0
- package/dist/adapters/jest/runner-adapter.js +49 -0
- package/dist/adapters/jest/setup.d.ts +1 -0
- package/dist/adapters/jest/setup.js +10 -0
- package/dist/adapters/node-test/index.d.ts +8 -0
- package/dist/adapters/node-test/index.js +106 -0
- package/dist/adapters/node-test/invocation-adapter.d.ts +5 -0
- package/dist/adapters/node-test/invocation-adapter.js +47 -0
- package/dist/adapters/node-test/runner-adapter.d.ts +2 -0
- package/dist/adapters/node-test/runner-adapter.js +116 -0
- package/dist/adapters/vitest/index.d.ts +9 -0
- package/dist/adapters/vitest/index.js +82 -0
- package/dist/adapters/vitest/lifecycle.d.ts +24 -0
- package/dist/adapters/vitest/lifecycle.js +41 -0
- package/dist/adapters/vitest/reporter.d.ts +12 -0
- package/dist/adapters/vitest/reporter.js +75 -0
- package/dist/adapters/vitest/setup.d.ts +1 -0
- package/dist/adapters/vitest/setup.js +3 -0
- package/dist/agents/claude/sdk-options.js +6 -0
- package/dist/agents/codex-app-server/agent.js +5 -1
- package/dist/agents/codex-app-server/protocol/index.js +3 -3
- package/dist/agents/codex-app-server/turn-completion.d.ts +1 -0
- package/dist/agents/codex-app-server/turn-completion.js +9 -0
- package/dist/commands/affected.js +22 -15
- package/dist/commands/run-args.d.ts +4 -3
- package/dist/commands/run-args.js +12 -6
- package/dist/commands/run-changed.d.ts +4 -4
- package/dist/commands/run-changed.js +45 -58
- package/dist/config/pathgrade.d.ts +48 -0
- package/dist/config/pathgrade.js +204 -0
- package/dist/pathgrade.d.ts +2 -2
- package/dist/pathgrade.js +35 -34
- package/dist/plugin/index.d.ts +2 -9
- package/dist/plugin/index.js +1 -81
- package/dist/plugin/lifecycle.d.ts +1 -45
- package/dist/plugin/lifecycle.js +1 -146
- package/dist/plugin/reporter.d.ts +1 -37
- package/dist/plugin/reporter.js +1 -280
- package/dist/plugin/setup.d.ts +1 -1
- package/dist/plugin/setup.js +1 -3
- package/dist/providers/copy-filter.d.ts +5 -0
- package/dist/providers/copy-filter.js +9 -0
- package/dist/providers/credentials.js +1 -1
- package/dist/providers/sandbox-lifecycle.d.ts +22 -0
- package/dist/providers/sandbox-lifecycle.js +133 -0
- package/dist/providers/sandbox.js +4 -3
- package/dist/providers/workspace.js +4 -14
- package/dist/reporters/diagnostics.d.ts +2 -40
- package/dist/reporters/diagnostics.js +1 -80
- package/dist/reporters/report-summary.d.ts +6 -0
- package/dist/reporters/report-summary.js +29 -0
- package/dist/reporting/artifacts.d.ts +2 -0
- package/dist/reporting/artifacts.js +20 -0
- package/dist/reporting/core.d.ts +2 -0
- package/dist/reporting/core.js +173 -0
- package/dist/reporting/types.d.ts +57 -0
- package/dist/reporting/types.js +1 -0
- package/dist/reporting/vitest-edge.d.ts +1 -0
- package/dist/reporting/vitest-edge.js +1 -0
- package/dist/runners/adapter-loader.d.ts +17 -0
- package/dist/runners/adapter-loader.js +67 -0
- package/dist/runners/adapter.d.ts +65 -0
- package/dist/runners/adapter.js +1 -0
- package/dist/runners/invocation.d.ts +10 -0
- package/dist/runners/invocation.js +1 -0
- package/dist/runners/lifecycle-hooks.d.ts +2 -0
- package/dist/runners/lifecycle-hooks.js +18 -0
- package/dist/runners/model-builders.d.ts +4 -0
- package/dist/runners/model-builders.js +67 -0
- package/dist/runners/model-validation.d.ts +12 -0
- package/dist/runners/model-validation.js +213 -0
- package/dist/runners/model.d.ts +117 -0
- package/dist/runners/model.js +1 -0
- package/dist/runners/orchestrator.d.ts +30 -0
- package/dist/runners/orchestrator.js +74 -0
- package/dist/runners/report-projection.d.ts +6 -0
- package/dist/runners/report-projection.js +62 -0
- package/dist/runners/selection.d.ts +6 -0
- package/dist/runners/selection.js +10 -0
- package/dist/runners/vitest-adapter.d.ts +8 -0
- package/dist/runners/vitest-adapter.js +108 -0
- package/dist/runners/vitest-invocation.d.ts +10 -0
- package/dist/runners/vitest-invocation.js +44 -0
- package/dist/runners/vitest-lifecycle.d.ts +43 -0
- package/dist/runners/vitest-lifecycle.js +150 -0
- package/dist/sdk/agent.js +15 -16
- package/dist/sdk/case-context.d.ts +20 -0
- package/dist/sdk/case-context.js +31 -0
- package/dist/sdk/diagnostics.d.ts +40 -0
- package/dist/sdk/diagnostics.js +80 -0
- package/dist/sdk/eval-runtime.d.ts +4 -0
- package/dist/sdk/eval-runtime.js +11 -1
- package/dist/sdk/evaluate.js +2 -1
- package/dist/sdk/index.d.ts +2 -0
- package/dist/sdk/index.js +1 -0
- package/dist/sdk/lifecycle.d.ts +40 -0
- package/dist/sdk/lifecycle.js +170 -0
- package/dist/sdk/result-capture.d.ts +18 -0
- package/dist/sdk/result-capture.js +49 -0
- package/dist/sdk/types.d.ts +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/utils/llm-providers/anthropic.js +9 -2
- package/package.json +56 -4
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { discoverPathgradeEvalFiles } from '../evals/discovery.js';
|
|
2
|
+
import { buildNormalizedRunSnapshotFromReportGroups } from './model-builders.js';
|
|
3
|
+
import { buildDiagnosticsReport } from '../sdk/diagnostics.js';
|
|
4
|
+
export function createVitestAdapter(options = {}) {
|
|
5
|
+
return {
|
|
6
|
+
name: 'vitest',
|
|
7
|
+
async discover(input) {
|
|
8
|
+
const include = input.include ?? ['**/*.eval.ts'];
|
|
9
|
+
const exclude = input.exclude ?? [];
|
|
10
|
+
const files = discoverPathgradeEvalFiles({
|
|
11
|
+
cwd: input.cwd,
|
|
12
|
+
include,
|
|
13
|
+
exclude,
|
|
14
|
+
});
|
|
15
|
+
return {
|
|
16
|
+
units: files.map(file => ({
|
|
17
|
+
id: file,
|
|
18
|
+
displayName: file,
|
|
19
|
+
sourceRef: file,
|
|
20
|
+
native: { file },
|
|
21
|
+
})),
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
async invoke(input) {
|
|
25
|
+
return {
|
|
26
|
+
adapterName: this.name,
|
|
27
|
+
status: input.signal?.aborted ? 'cancelled' : 'completed',
|
|
28
|
+
exitCode: input.signal?.aborted ? 1 : 0,
|
|
29
|
+
native: {
|
|
30
|
+
testModules: options.testModules ?? [],
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
async collectNormalizedRunSnapshot(run) {
|
|
35
|
+
return buildNormalizedRunSnapshotFromReportGroups(run, collectVitestReportGroups(readVitestRunNative(run).testModules));
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function collectVitestReportGroups(testModules) {
|
|
40
|
+
const groupMap = new Map();
|
|
41
|
+
for (const mod of testModules) {
|
|
42
|
+
for (const testCase of mod.children.allTests()) {
|
|
43
|
+
const groupName = getGroupName(testCase);
|
|
44
|
+
const entry = toReportCaseInput(testCase, groupName);
|
|
45
|
+
if (!groupMap.has(groupName)) {
|
|
46
|
+
groupMap.set(groupName, []);
|
|
47
|
+
}
|
|
48
|
+
groupMap.get(groupName).push(entry);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return Array.from(groupMap.entries()).map(([groupName, cases]) => ({
|
|
52
|
+
groupName,
|
|
53
|
+
cases,
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
function readVitestRunNative(run) {
|
|
57
|
+
if (isVitestRunNative(run.native))
|
|
58
|
+
return run.native;
|
|
59
|
+
return { testModules: [] };
|
|
60
|
+
}
|
|
61
|
+
function isVitestRunNative(native) {
|
|
62
|
+
return typeof native === 'object'
|
|
63
|
+
&& native !== null
|
|
64
|
+
&& Array.isArray(native.testModules);
|
|
65
|
+
}
|
|
66
|
+
function getGroupName(testCase) {
|
|
67
|
+
const modulePath = testCase.module.relativeModuleId;
|
|
68
|
+
if (testCase.parent.type === 'suite') {
|
|
69
|
+
return `${modulePath} > ${testCase.parent.fullName}`;
|
|
70
|
+
}
|
|
71
|
+
return modulePath;
|
|
72
|
+
}
|
|
73
|
+
function toReportCaseInput(testCase, groupName) {
|
|
74
|
+
const pathgradeMeta = testCase.meta().pathgrade;
|
|
75
|
+
const result = testCase.result();
|
|
76
|
+
const diagnostics = testCase.diagnostic();
|
|
77
|
+
const normalized = normalizeState(result.state);
|
|
78
|
+
const filePath = testCase.module.relativeModuleId;
|
|
79
|
+
const runnerCaseId = typeof testCase.id === 'string' ? testCase.id : undefined;
|
|
80
|
+
return {
|
|
81
|
+
...(runnerCaseId ? { caseId: runnerCaseId, runnerCaseId } : {}),
|
|
82
|
+
name: testCase.name,
|
|
83
|
+
state: normalized.state,
|
|
84
|
+
runnerDurationMs: diagnostics?.duration ?? 0,
|
|
85
|
+
sourceRef: filePath,
|
|
86
|
+
filePath,
|
|
87
|
+
groupName,
|
|
88
|
+
evaluations: pathgradeMeta?.map(entry => ({
|
|
89
|
+
score: entry.score,
|
|
90
|
+
trial: entry.trial,
|
|
91
|
+
diagnostics: entry.diagnostics,
|
|
92
|
+
})),
|
|
93
|
+
diagnostics: normalized.diagnostics,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function normalizeState(state) {
|
|
97
|
+
if (state === 'passed' || state === 'failed' || state === 'skipped' || state === 'pending') {
|
|
98
|
+
return { state };
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
state: 'failed',
|
|
102
|
+
diagnostics: buildDiagnosticsReport({
|
|
103
|
+
completionReason: state,
|
|
104
|
+
score: 0,
|
|
105
|
+
log: [],
|
|
106
|
+
}),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { RunnerInvocationAdapter } from './invocation.js';
|
|
2
|
+
export interface SpawnVitestRequest {
|
|
3
|
+
argv: string[];
|
|
4
|
+
env: NodeJS.ProcessEnv;
|
|
5
|
+
cwd: string;
|
|
6
|
+
}
|
|
7
|
+
export type SpawnVitest = (req: SpawnVitestRequest) => Promise<number> | number;
|
|
8
|
+
export declare function createVitestInvocationAdapter(input?: {
|
|
9
|
+
spawnVitest?: SpawnVitest;
|
|
10
|
+
}): RunnerInvocationAdapter;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export function createVitestInvocationAdapter(input = {}) {
|
|
2
|
+
const spawnVitest = input.spawnVitest ?? defaultSpawnVitest;
|
|
3
|
+
return {
|
|
4
|
+
name: 'vitest',
|
|
5
|
+
async run(runInput) {
|
|
6
|
+
if (runInput.selectedFiles && hasPassWithNoTests(runInput.runnerArgs)) {
|
|
7
|
+
process.stderr.write('pathgrade run: --passWithNoTests cannot be used with pathgrade run --changed. ' +
|
|
8
|
+
'The command already exits 0 when no evals are selected; if selected evals resolve to no Vitest files, CI must fail.\n');
|
|
9
|
+
return 1;
|
|
10
|
+
}
|
|
11
|
+
const argv = [
|
|
12
|
+
'run',
|
|
13
|
+
...(runInput.selectedFiles ?? []),
|
|
14
|
+
...runInput.runnerArgs,
|
|
15
|
+
];
|
|
16
|
+
return await spawnVitest({
|
|
17
|
+
argv,
|
|
18
|
+
env: runInput.env,
|
|
19
|
+
cwd: runInput.cwd,
|
|
20
|
+
});
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function hasPassWithNoTests(args) {
|
|
25
|
+
return args.some(arg => {
|
|
26
|
+
if (arg === '--passWithNoTests')
|
|
27
|
+
return true;
|
|
28
|
+
if (!arg.startsWith('--passWithNoTests='))
|
|
29
|
+
return false;
|
|
30
|
+
return arg.slice('--passWithNoTests='.length).toLowerCase() !== 'false';
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
async function defaultSpawnVitest(req) {
|
|
34
|
+
const { spawn } = await import('child_process');
|
|
35
|
+
return await new Promise(resolve => {
|
|
36
|
+
const child = spawn('npx', ['vitest', ...req.argv], {
|
|
37
|
+
stdio: 'inherit',
|
|
38
|
+
env: req.env,
|
|
39
|
+
cwd: req.cwd,
|
|
40
|
+
shell: true,
|
|
41
|
+
});
|
|
42
|
+
child.on('close', code => resolve(code ?? 0));
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Agent, PathgradeTestMeta } from '../sdk/types.js';
|
|
2
|
+
import { type ResultObserverHandle } from '../sdk/result-capture.js';
|
|
3
|
+
import { type CaseContextProviderHandle } from '../sdk/case-context.js';
|
|
4
|
+
import { type LifecycleAgentOwner } from '../sdk/lifecycle.js';
|
|
5
|
+
import type { AdapterLifecycleHooks, EvalResultEvent } from './adapter.js';
|
|
6
|
+
export type VitestAfterEachFn = (fn: (ctx: {
|
|
7
|
+
task: {
|
|
8
|
+
id: string;
|
|
9
|
+
meta: Record<string, unknown>;
|
|
10
|
+
};
|
|
11
|
+
}) => Promise<void>) => void;
|
|
12
|
+
export type VitestAfterAllFn = (fn: () => Promise<void>) => void;
|
|
13
|
+
export type VitestAroundEachFn = (fn: (runTest: () => Promise<void>, ctx: {
|
|
14
|
+
task: {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
meta: Record<string, unknown>;
|
|
18
|
+
suite?: unknown;
|
|
19
|
+
};
|
|
20
|
+
}) => Promise<void>) => void;
|
|
21
|
+
export interface VitestLifecycleInstallInput {
|
|
22
|
+
afterEach: VitestAfterEachFn;
|
|
23
|
+
afterAll?: VitestAfterAllFn;
|
|
24
|
+
aroundEach?: VitestAroundEachFn;
|
|
25
|
+
lifecycle?: AdapterLifecycleHooks;
|
|
26
|
+
subscribeToResults?: (callback: (event: EvalResultEvent) => void) => ResultObserverHandle;
|
|
27
|
+
installFileContextProvider?: () => CaseContextProviderHandle;
|
|
28
|
+
}
|
|
29
|
+
export interface VitestLifecycleInstallHandle {
|
|
30
|
+
restore(): void;
|
|
31
|
+
}
|
|
32
|
+
export declare function createVitestLifecycleHooks(): AdapterLifecycleHooks;
|
|
33
|
+
export declare function installVitestLifecycle(input: VitestLifecycleInstallInput): VitestLifecycleInstallHandle;
|
|
34
|
+
export declare function currentVitestAgentOwner(resultCaptureActive: boolean): LifecycleAgentOwner | null;
|
|
35
|
+
export declare function trackVitestAgent(agent: Agent, resultCaptureActive: boolean): void;
|
|
36
|
+
export declare function untrackVitestAgent(agent: Agent): void;
|
|
37
|
+
export declare function releaseVitestAgent(agent: Agent): void;
|
|
38
|
+
export declare function resetVitestLifecycle(handle: VitestLifecycleInstallHandle | null): void;
|
|
39
|
+
declare module 'vitest' {
|
|
40
|
+
interface TaskMeta {
|
|
41
|
+
pathgrade?: PathgradeTestMeta[];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { subscribeToEvalResults } from '../sdk/result-capture.js';
|
|
3
|
+
import { getCurrentCaseContext, installCaseContextProvider, runWithCaseContext, } from '../sdk/case-context.js';
|
|
4
|
+
import { lifecycleCore } from '../sdk/lifecycle.js';
|
|
5
|
+
export function createVitestLifecycleHooks() {
|
|
6
|
+
return {
|
|
7
|
+
onResult: event => recordVitestResult(event.result, event.agent),
|
|
8
|
+
withCaseContext: (context, run) => runWithCaseContext(toCaseContext(context), run),
|
|
9
|
+
flushCase: caseId => lifecycleCore.flushCase({ caseId }),
|
|
10
|
+
cleanupRun: () => lifecycleCore.cleanupAll(),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function installVitestLifecycle(input) {
|
|
14
|
+
const lifecycle = input.lifecycle ?? createVitestLifecycleHooks();
|
|
15
|
+
const resultCaptureHandle = (input.subscribeToResults ?? defaultSubscribeToResults)(event => lifecycle.onResult(event));
|
|
16
|
+
const fileContextHandle = (input.installFileContextProvider ?? defaultInstallFileContextProvider)();
|
|
17
|
+
let fileContextRestored = false;
|
|
18
|
+
let resultsUnsubscribed = false;
|
|
19
|
+
input.aroundEach?.(async (runTest, { task }) => lifecycle.withCaseContext(caseContextForTask(task), runTest));
|
|
20
|
+
input.afterEach(async ({ task }) => {
|
|
21
|
+
const results = await lifecycle.flushCase(task.id);
|
|
22
|
+
if (results.length > 0) {
|
|
23
|
+
task.meta.pathgrade = results;
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
input.afterAll?.(async () => {
|
|
27
|
+
await lifecycle.cleanupRun();
|
|
28
|
+
restoreFileContext();
|
|
29
|
+
});
|
|
30
|
+
return {
|
|
31
|
+
restore() {
|
|
32
|
+
restoreFileContext();
|
|
33
|
+
if (!resultsUnsubscribed) {
|
|
34
|
+
resultsUnsubscribed = true;
|
|
35
|
+
resultCaptureHandle.unsubscribe();
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
function restoreFileContext() {
|
|
40
|
+
if (fileContextRestored)
|
|
41
|
+
return;
|
|
42
|
+
fileContextRestored = true;
|
|
43
|
+
fileContextHandle.restore();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function currentVitestAgentOwner(resultCaptureActive) {
|
|
47
|
+
const current = getCurrentCaseContext();
|
|
48
|
+
if (current.status === 'active') {
|
|
49
|
+
return current.context.scope === 'runner-case'
|
|
50
|
+
? { type: 'runner-case', caseId: current.context.caseId }
|
|
51
|
+
: { type: 'runner-suite-shared', caseId: current.context.caseId };
|
|
52
|
+
}
|
|
53
|
+
const taskId = currentTaskId();
|
|
54
|
+
if (resultCaptureActive)
|
|
55
|
+
return null;
|
|
56
|
+
return taskId ? { type: 'runner-case', caseId: taskId } : { type: 'manual' };
|
|
57
|
+
}
|
|
58
|
+
export function trackVitestAgent(agent, resultCaptureActive) {
|
|
59
|
+
lifecycleCore.registerAgent(agent, currentVitestAgentOwner(resultCaptureActive));
|
|
60
|
+
}
|
|
61
|
+
export function untrackVitestAgent(agent) {
|
|
62
|
+
lifecycleCore.untrackAgent(agent);
|
|
63
|
+
}
|
|
64
|
+
export function releaseVitestAgent(agent) {
|
|
65
|
+
lifecycleCore.releaseAgent(agent);
|
|
66
|
+
}
|
|
67
|
+
export function resetVitestLifecycle(handle) {
|
|
68
|
+
lifecycleCore.reset();
|
|
69
|
+
handle?.restore();
|
|
70
|
+
}
|
|
71
|
+
function defaultSubscribeToResults(callback) {
|
|
72
|
+
return subscribeToEvalResults(({ result, agent }) => callback({ result, agent }), { owner: 'adapter', key: 'vitest-lifecycle' });
|
|
73
|
+
}
|
|
74
|
+
function defaultInstallFileContextProvider() {
|
|
75
|
+
return installCaseContextProvider(currentFileContext);
|
|
76
|
+
}
|
|
77
|
+
function toCaseContext(context) {
|
|
78
|
+
return {
|
|
79
|
+
caseId: context.caseId,
|
|
80
|
+
caseName: context.caseName,
|
|
81
|
+
filePath: context.filePath ?? context.sourceRef ?? '',
|
|
82
|
+
scope: context.scope,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function currentTaskId() {
|
|
86
|
+
try {
|
|
87
|
+
return globalThis.__vitest_worker__?.current?.id ?? '';
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return '';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function filePathForTask(task) {
|
|
94
|
+
let current = task.suite;
|
|
95
|
+
while (current) {
|
|
96
|
+
if (typeof current.filepath === 'string')
|
|
97
|
+
return current.filepath;
|
|
98
|
+
current = current.suite;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
return globalThis.__vitest_worker__?.filepath ?? '';
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return '';
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function caseContextForTask(task) {
|
|
108
|
+
const filePath = filePathForTask(task);
|
|
109
|
+
return {
|
|
110
|
+
caseId: task.id,
|
|
111
|
+
caseName: task.name,
|
|
112
|
+
filePath,
|
|
113
|
+
sourceRef: filePath,
|
|
114
|
+
runnerNativeId: task.id,
|
|
115
|
+
scope: 'runner-case',
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function currentFilePath() {
|
|
119
|
+
try {
|
|
120
|
+
return globalThis.__vitest_worker__?.filepath ?? '';
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return '';
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function currentFileContext() {
|
|
127
|
+
const filePath = currentFilePath();
|
|
128
|
+
if (!filePath)
|
|
129
|
+
return null;
|
|
130
|
+
return {
|
|
131
|
+
caseId: `file:${filePath}`,
|
|
132
|
+
caseName: path.basename(filePath),
|
|
133
|
+
filePath,
|
|
134
|
+
scope: 'runner-suite-shared',
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function recordVitestResult(result, agent) {
|
|
138
|
+
const current = getCurrentCaseContext();
|
|
139
|
+
const taskId = currentTaskId();
|
|
140
|
+
const owner = lifecycleCore.getAgentOwner(agent);
|
|
141
|
+
if (owner?.type !== 'runner-case' && (current.status !== 'active' || current.context.scope !== 'runner-case')) {
|
|
142
|
+
if (!taskId) {
|
|
143
|
+
lifecycleCore.recordResult(result, agent);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
lifecycleCore.recordResult(result, agent, { type: 'runner-case', caseId: taskId });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
lifecycleCore.recordResult(result, agent);
|
|
150
|
+
}
|
package/dist/sdk/agent.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { prepareWorkspace } from '../providers/workspace.js';
|
|
2
2
|
import { resolveAgentName, resolveCodexTransport } from './agent-resolution.js';
|
|
3
|
-
import {
|
|
3
|
+
import { lifecycleCore } from './lifecycle.js';
|
|
4
4
|
import { ChatSessionImpl } from './chat.js';
|
|
5
5
|
import { runConversation } from './converse.js';
|
|
6
6
|
import { createPersona } from './persona.js';
|
|
@@ -10,6 +10,7 @@ import { createAgentLLM } from '../utils/llm.js';
|
|
|
10
10
|
import { buildRunSnapshot } from './snapshots.js';
|
|
11
11
|
import { buildModelAgentResultLogEntry } from './agent-result-log.js';
|
|
12
12
|
import { getVisibleAssistantMessage } from './visible-turn.js';
|
|
13
|
+
import { getCurrentCaseContext } from './case-context.js';
|
|
13
14
|
import { createVerboseEmitter } from '../reporters/verbose-emitter.js';
|
|
14
15
|
import fs from 'fs-extra';
|
|
15
16
|
import * as path from 'path';
|
|
@@ -293,9 +294,9 @@ class AgentImpl {
|
|
|
293
294
|
if (this.disposed)
|
|
294
295
|
return;
|
|
295
296
|
this.disposed = true;
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
|
|
297
|
+
// Runner-owned agents stay tracked until flush consumes metadata.
|
|
298
|
+
// Manual agents have no runner flush, so dispose releases them.
|
|
299
|
+
lifecycleCore.releaseAgent(this);
|
|
299
300
|
if (this.debugOpt) {
|
|
300
301
|
const dest = typeof this.debugOpt === 'string'
|
|
301
302
|
? this.debugOpt
|
|
@@ -329,16 +330,14 @@ function slugify(s) {
|
|
|
329
330
|
.replace(/^-|-$/g, '')
|
|
330
331
|
.toLowerCase();
|
|
331
332
|
}
|
|
332
|
-
function
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const name = worker?.current?.name ? slugify(worker.current.name) : '';
|
|
336
|
-
const dir = worker?.filepath ? path.dirname(worker.filepath) : '';
|
|
337
|
-
return { name, dir };
|
|
338
|
-
}
|
|
339
|
-
catch {
|
|
333
|
+
function resolveCaseDebugContext() {
|
|
334
|
+
const current = getCurrentCaseContext();
|
|
335
|
+
if (current.status !== 'active')
|
|
340
336
|
return { name: '', dir: '' };
|
|
341
|
-
|
|
337
|
+
return {
|
|
338
|
+
name: current.context.caseName ? slugify(current.context.caseName) : '',
|
|
339
|
+
dir: current.context.filePath ? path.dirname(current.context.filePath) : '',
|
|
340
|
+
};
|
|
342
341
|
}
|
|
343
342
|
export async function createAgent(opts) {
|
|
344
343
|
const agentName = resolveAgentName(opts, process.env);
|
|
@@ -346,8 +345,8 @@ export async function createAgent(opts) {
|
|
|
346
345
|
? resolveCodexTransport(opts, process.env)
|
|
347
346
|
: undefined;
|
|
348
347
|
const timeoutSetting = opts.timeout ?? 300;
|
|
349
|
-
// Capture
|
|
350
|
-
const testCtx = opts.debug ?
|
|
348
|
+
// Capture runner context now; adapters own installation and restoration.
|
|
349
|
+
const testCtx = opts.debug ? resolveCaseDebugContext() : { name: '', dir: '' };
|
|
351
350
|
const { timeout: _, mcpMock, mcpConfigFile, agent: __, debug: ___, model: ____, transport: _____, mcpSafety: ______, ...rest } = opts;
|
|
352
351
|
const workspace = await prepareWorkspace({
|
|
353
352
|
...rest,
|
|
@@ -367,6 +366,6 @@ export async function createAgent(opts) {
|
|
|
367
366
|
testName: testCtx.name || undefined,
|
|
368
367
|
});
|
|
369
368
|
const agent = new AgentImpl(workspace, agentName, llm, timeoutSetting, opts.conversationWindow, opts.model, opts.debug, debugName, debugBaseDir, verbose, transport, opts.mcpSafety);
|
|
370
|
-
|
|
369
|
+
lifecycleCore.registerAgent(agent);
|
|
371
370
|
return agent;
|
|
372
371
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type CaseContextScope = 'runner-case' | 'runner-suite-shared';
|
|
2
|
+
export interface CaseContext {
|
|
3
|
+
caseId: string;
|
|
4
|
+
caseName: string;
|
|
5
|
+
filePath: string;
|
|
6
|
+
scope: CaseContextScope;
|
|
7
|
+
}
|
|
8
|
+
export type CurrentCaseContext = {
|
|
9
|
+
status: 'empty';
|
|
10
|
+
} | {
|
|
11
|
+
status: 'active';
|
|
12
|
+
context: CaseContext;
|
|
13
|
+
};
|
|
14
|
+
export interface CaseContextProviderHandle {
|
|
15
|
+
restore(): void;
|
|
16
|
+
}
|
|
17
|
+
export type CaseContextProvider = () => CaseContext | null | undefined;
|
|
18
|
+
export declare function getCurrentCaseContext(): CurrentCaseContext;
|
|
19
|
+
export declare function runWithCaseContext<T>(context: CaseContext, run: () => T): T;
|
|
20
|
+
export declare function installCaseContextProvider(provider: CaseContextProvider): CaseContextProviderHandle;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
const caseContextStorage = new AsyncLocalStorage();
|
|
3
|
+
const providers = [];
|
|
4
|
+
export function getCurrentCaseContext() {
|
|
5
|
+
const context = caseContextStorage.getStore();
|
|
6
|
+
if (context)
|
|
7
|
+
return { status: 'active', context };
|
|
8
|
+
for (const provider of providers.toReversed()) {
|
|
9
|
+
const provided = provider();
|
|
10
|
+
if (provided)
|
|
11
|
+
return { status: 'active', context: provided };
|
|
12
|
+
}
|
|
13
|
+
return { status: 'empty' };
|
|
14
|
+
}
|
|
15
|
+
export function runWithCaseContext(context, run) {
|
|
16
|
+
return caseContextStorage.run(context, run);
|
|
17
|
+
}
|
|
18
|
+
export function installCaseContextProvider(provider) {
|
|
19
|
+
providers.push(provider);
|
|
20
|
+
let restored = false;
|
|
21
|
+
return {
|
|
22
|
+
restore() {
|
|
23
|
+
if (restored)
|
|
24
|
+
return;
|
|
25
|
+
restored = true;
|
|
26
|
+
const index = providers.lastIndexOf(provider);
|
|
27
|
+
if (index >= 0)
|
|
28
|
+
providers.splice(index, 1);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { LogEntry } from '../types.js';
|
|
2
|
+
import type { ReactionFiredEntry, TurnDetail } from './types.js';
|
|
3
|
+
import { type RuntimePolicyAuditEntry } from './runtime-policy.js';
|
|
4
|
+
export interface DiagnosticsScorer {
|
|
5
|
+
name: string;
|
|
6
|
+
type: string;
|
|
7
|
+
score: number;
|
|
8
|
+
weight: number;
|
|
9
|
+
details?: string;
|
|
10
|
+
status?: 'ok' | 'error' | 'skipped';
|
|
11
|
+
}
|
|
12
|
+
export interface DiagnosticsTurnDetail extends TurnDetail {
|
|
13
|
+
apiRetries: number;
|
|
14
|
+
}
|
|
15
|
+
export type DiagnosticsRuntimePolicyEntry = RuntimePolicyAuditEntry;
|
|
16
|
+
export interface DiagnosticsReport {
|
|
17
|
+
turns: number;
|
|
18
|
+
totalDurationMs: number;
|
|
19
|
+
completionReason?: string;
|
|
20
|
+
completionDetail?: string;
|
|
21
|
+
score?: number;
|
|
22
|
+
turnDetails: DiagnosticsTurnDetail[];
|
|
23
|
+
reactionsFired: ReactionFiredEntry[];
|
|
24
|
+
runtimePoliciesApplied: DiagnosticsRuntimePolicyEntry[];
|
|
25
|
+
recommendedTimeoutMs: number;
|
|
26
|
+
warnings: string[];
|
|
27
|
+
scorers: Array<DiagnosticsScorer & {
|
|
28
|
+
status: 'ok' | 'error' | 'skipped';
|
|
29
|
+
}>;
|
|
30
|
+
}
|
|
31
|
+
export interface BuildDiagnosticsReportInput {
|
|
32
|
+
completionReason?: string;
|
|
33
|
+
completionDetail?: string;
|
|
34
|
+
score?: number;
|
|
35
|
+
turnDetails?: TurnDetail[];
|
|
36
|
+
reactionsFired?: ReactionFiredEntry[];
|
|
37
|
+
scorers?: DiagnosticsScorer[];
|
|
38
|
+
log: LogEntry[];
|
|
39
|
+
}
|
|
40
|
+
export declare function buildDiagnosticsReport(input: BuildDiagnosticsReportInput): DiagnosticsReport;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { extractRuntimePolicyAuditEntries } from './runtime-policy.js';
|
|
2
|
+
export function buildDiagnosticsReport(input) {
|
|
3
|
+
const baseTurnDetails = (input.turnDetails && input.turnDetails.length > 0)
|
|
4
|
+
? [...input.turnDetails]
|
|
5
|
+
: deriveTurnDetailsFromLog(input.log);
|
|
6
|
+
const retriesByTurn = countRetriesByTurn(input.log);
|
|
7
|
+
const turnDetails = baseTurnDetails
|
|
8
|
+
.sort((a, b) => a.turn - b.turn)
|
|
9
|
+
.map((detail) => ({
|
|
10
|
+
...detail,
|
|
11
|
+
apiRetries: retriesByTurn.get(detail.turn) ?? 0,
|
|
12
|
+
}));
|
|
13
|
+
const warnings = turnDetails
|
|
14
|
+
.filter((detail) => detail.outputLines > 500)
|
|
15
|
+
.map((detail) => `Turn ${detail.turn}: output exceeded 500 lines (${detail.outputLines} lines)`);
|
|
16
|
+
return {
|
|
17
|
+
turns: turnDetails.length,
|
|
18
|
+
totalDurationMs: turnDetails.reduce((sum, detail) => sum + detail.durationMs, 0),
|
|
19
|
+
completionReason: input.completionReason,
|
|
20
|
+
completionDetail: input.completionDetail,
|
|
21
|
+
score: input.score,
|
|
22
|
+
turnDetails,
|
|
23
|
+
reactionsFired: input.reactionsFired ?? [],
|
|
24
|
+
runtimePoliciesApplied: extractRuntimePolicyAuditEntries(input.log),
|
|
25
|
+
recommendedTimeoutMs: recommendTimeoutMs(turnDetails),
|
|
26
|
+
warnings,
|
|
27
|
+
scorers: (input.scorers ?? []).map((scorer) => ({
|
|
28
|
+
...scorer,
|
|
29
|
+
status: scorer.status ?? 'ok',
|
|
30
|
+
})),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function deriveTurnDetailsFromLog(log) {
|
|
34
|
+
return log
|
|
35
|
+
.filter((entry) => entry.type === 'agent_result'
|
|
36
|
+
&& entry.synthetic_blocked_prompt !== true
|
|
37
|
+
&& typeof entry.turn_number === 'number'
|
|
38
|
+
&& typeof entry.duration_ms === 'number'
|
|
39
|
+
&& typeof entry.output_lines === 'number'
|
|
40
|
+
&& typeof entry.output_chars === 'number')
|
|
41
|
+
.map((entry) => ({
|
|
42
|
+
turn: entry.turn_number,
|
|
43
|
+
durationMs: entry.duration_ms,
|
|
44
|
+
outputLines: entry.output_lines,
|
|
45
|
+
outputChars: entry.output_chars,
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
function countRetriesByTurn(log) {
|
|
49
|
+
const retriesByTurn = new Map();
|
|
50
|
+
let pendingRetries = 0;
|
|
51
|
+
for (const entry of log) {
|
|
52
|
+
if (isRetryEntry(entry)) {
|
|
53
|
+
pendingRetries++;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (entry.type === 'agent_result' && typeof entry.turn_number === 'number') {
|
|
57
|
+
if (pendingRetries > 0) {
|
|
58
|
+
retriesByTurn.set(entry.turn_number, (retriesByTurn.get(entry.turn_number) ?? 0) + pendingRetries);
|
|
59
|
+
pendingRetries = 0;
|
|
60
|
+
}
|
|
61
|
+
if (!retriesByTurn.has(entry.turn_number)) {
|
|
62
|
+
retriesByTurn.set(entry.turn_number, 0);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return retriesByTurn;
|
|
67
|
+
}
|
|
68
|
+
function isRetryEntry(entry) {
|
|
69
|
+
if (entry.type !== 'agent_result')
|
|
70
|
+
return false;
|
|
71
|
+
const text = `${entry.assistant_message ?? ''} ${entry.output ?? ''}`.toLowerCase();
|
|
72
|
+
return text.includes('retry') || text.includes('api_retry');
|
|
73
|
+
}
|
|
74
|
+
function recommendTimeoutMs(turnDetails) {
|
|
75
|
+
if (turnDetails.length <= 1) {
|
|
76
|
+
return (turnDetails[0]?.durationMs ?? 0) + 30_000;
|
|
77
|
+
}
|
|
78
|
+
const totalDurationMs = turnDetails.reduce((sum, detail) => sum + detail.durationMs, 0);
|
|
79
|
+
return Math.max(totalDurationMs + 200_000, 30_000);
|
|
80
|
+
}
|
|
@@ -3,6 +3,10 @@ import type { Agent, RecordedEvalResult } from './types.js';
|
|
|
3
3
|
export type { LLMPort } from '../utils/llm-types.js';
|
|
4
4
|
export interface EvalRuntime {
|
|
5
5
|
llm: LLMPort;
|
|
6
|
+
/**
|
|
7
|
+
* Legacy compatibility seam. `setRuntime({ onResult })` is bridged into
|
|
8
|
+
* user-owned result observers; live capture is owned by result-capture.
|
|
9
|
+
*/
|
|
6
10
|
onResult: (result: RecordedEvalResult, agent: Agent) => void;
|
|
7
11
|
}
|
|
8
12
|
export declare function getRuntime(): EvalRuntime;
|
package/dist/sdk/eval-runtime.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { callLLM } from '../utils/llm.js';
|
|
2
|
+
import { resetUserResultObservers, subscribeToEvalResults, } from './result-capture.js';
|
|
2
3
|
function makeDefaultRuntime() {
|
|
3
4
|
return {
|
|
4
5
|
llm: { call: (prompt, opts) => callLLM(prompt, opts) },
|
|
@@ -6,12 +7,21 @@ function makeDefaultRuntime() {
|
|
|
6
7
|
};
|
|
7
8
|
}
|
|
8
9
|
let current = makeDefaultRuntime();
|
|
10
|
+
let legacyRuntimeResultHandle = null;
|
|
9
11
|
export function getRuntime() {
|
|
10
12
|
return current;
|
|
11
13
|
}
|
|
12
14
|
export function setRuntime(partial) {
|
|
13
|
-
|
|
15
|
+
const { onResult, ...runtimeConfig } = partial;
|
|
16
|
+
current = { ...current, ...runtimeConfig };
|
|
17
|
+
if (onResult) {
|
|
18
|
+
legacyRuntimeResultHandle?.unsubscribe();
|
|
19
|
+
legacyRuntimeResultHandle = subscribeToEvalResults(({ result, agent }) => onResult(result, agent), { owner: 'user', key: 'legacy-runtime-onResult' });
|
|
20
|
+
}
|
|
14
21
|
}
|
|
15
22
|
export function resetRuntime() {
|
|
16
23
|
current = makeDefaultRuntime();
|
|
24
|
+
legacyRuntimeResultHandle?.unsubscribe();
|
|
25
|
+
legacyRuntimeResultHandle = null;
|
|
26
|
+
resetUserResultObservers();
|
|
17
27
|
}
|
package/dist/sdk/evaluate.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { extractSkillsFromLog } from '../tool-events.js';
|
|
2
2
|
import { getRuntime } from './eval-runtime.js';
|
|
3
|
+
import { emitEvalResult } from './result-capture.js';
|
|
3
4
|
import { runJudgePipeline } from './judge-pipeline.js';
|
|
4
5
|
import { runScorer } from './run-scorer.js';
|
|
5
6
|
import { createLLMClient } from '../utils/llm.js';
|
|
@@ -76,7 +77,7 @@ function makeEvaluateAgent() {
|
|
|
76
77
|
tokenUsage: deltaTokenUsage,
|
|
77
78
|
trial: buildTrialResult(agent.log, { ...evalResult, tokenUsage: deltaTokenUsage }, conversationTokens, conversationCost),
|
|
78
79
|
};
|
|
79
|
-
|
|
80
|
+
emitEvalResult({ result: recordedResult, agent });
|
|
80
81
|
maybeThrowOnScorerErrors(recordedResult, opts?.onScorerError ?? 'skip');
|
|
81
82
|
return recordedResult;
|
|
82
83
|
};
|