@elench/testkit 0.1.100 → 0.1.101
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 +6 -3
- package/lib/cli/args.mjs +0 -19
- package/lib/cli/assistant/command-observer.mjs +75 -44
- package/lib/cli/assistant/command-results.mjs +29 -2
- package/lib/cli/assistant/context-pack.mjs +21 -1
- package/lib/cli/assistant/settings.mjs +27 -5
- package/lib/cli/assistant/state.mjs +7 -1
- package/lib/cli/command-flags.mjs +0 -3
- package/lib/cli/entrypoint.mjs +0 -2
- package/lib/cli/operations/run/operation.mjs +0 -3
- package/lib/runner/live-run.mjs +5 -1
- package/lib/runner/orchestrator.mjs +26 -26
- package/lib/runner/planning.mjs +0 -75
- package/lib/runner/provenance.mjs +20 -0
- package/lib/runner/reporting.mjs +14 -9
- package/lib/runner/run-finalization.mjs +5 -2
- package/lib/runner/run-guards.mjs +0 -1
- package/lib/runner/scheduler/estimates.mjs +61 -0
- package/lib/runner/scheduler/identity.mjs +31 -0
- package/lib/runner/scheduler/index.mjs +126 -0
- package/lib/runner/scheduler/observations.mjs +27 -0
- package/lib/runner/selection.mjs +1 -2
- package/lib/runner/worker-loop.mjs +3 -4
- package/lib/timing/index.mjs +33 -33
- package/node_modules/@elench/next-analysis/package.json +1 -1
- package/node_modules/@elench/testkit-bridge/package.json +2 -2
- package/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/node_modules/@elench/ts-analysis/package.json +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -43,9 +43,6 @@ npx @elench/testkit --workers 8
|
|
|
43
43
|
# One file-level wall clock budget for every suite file
|
|
44
44
|
npx @elench/testkit --file-timeout-seconds 60
|
|
45
45
|
|
|
46
|
-
# Run a deterministic shard
|
|
47
|
-
npx @elench/testkit --shard 1/3
|
|
48
|
-
|
|
49
46
|
# Specific service / suite
|
|
50
47
|
npx @elench/testkit --service frontend --type pw -s navigation
|
|
51
48
|
npx @elench/testkit --service api --type int -s health
|
|
@@ -126,6 +123,12 @@ output, emitted artifacts, and assistant-visible run state are persisted under
|
|
|
126
123
|
run counts, pass/fail/skip counts, average duration, and last observed status,
|
|
127
124
|
and those summaries are exposed in compact, verbose, and JSON discovery output.
|
|
128
125
|
|
|
126
|
+
Test execution also maintains a scheduler cache at `.testkit/timings.json`.
|
|
127
|
+
Completed file-level task durations are used to rank future runs with a
|
|
128
|
+
longest-estimated-duration-first policy, so slow files start earlier when
|
|
129
|
+
workers are available. Run artifacts include compact scheduler metadata under
|
|
130
|
+
`planning` so ordering decisions are inspectable.
|
|
131
|
+
|
|
129
132
|
## Automatic Regression Diagnosis
|
|
130
133
|
|
|
131
134
|
If `regressions.file` is configured, every run automatically classifies observed
|
package/lib/cli/args.mjs
CHANGED
|
@@ -58,25 +58,6 @@ export {
|
|
|
58
58
|
parseWorkersOption,
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
-
export function parseShardOption(value) {
|
|
62
|
-
if (!value) return null;
|
|
63
|
-
|
|
64
|
-
const match = String(value).match(/^(\d+)\/(\d+)$/);
|
|
65
|
-
if (!match) {
|
|
66
|
-
throw new Error(
|
|
67
|
-
`Invalid --shard value "${value}". Expected the form "i/n", e.g. 1/3.`
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const index = Number.parseInt(match[1], 10);
|
|
72
|
-
const total = Number.parseInt(match[2], 10);
|
|
73
|
-
if (index <= 0 || total <= 0 || index > total) {
|
|
74
|
-
throw new Error(`Invalid --shard value "${value}". Expected 1 <= i <= n.`);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return { index, total };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
61
|
export function resolveRequestedFiles(fileNames, productDir, invocationCwd = process.cwd()) {
|
|
81
62
|
const resolved = [];
|
|
82
63
|
const seen = new Set();
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import { loadCurrentRunArtifact, loadLatestRunArtifact } from "../../results/artifacts.mjs";
|
|
4
3
|
|
|
5
4
|
const POLL_INTERVAL_MS = 150;
|
|
6
5
|
const OBSERVED_KINDS = new Set(["run", "discover", "status", "doctor", "typecheck"]);
|
|
6
|
+
const RUN_KINDS = new Set(["run", "int", "e2e", "scenario", "dal", "load", "pw", "all"]);
|
|
7
7
|
|
|
8
8
|
export function createAssistantCommandObserver({
|
|
9
9
|
productDir,
|
|
@@ -13,15 +13,16 @@ export function createAssistantCommandObserver({
|
|
|
13
13
|
intervalMs = POLL_INTERVAL_MS,
|
|
14
14
|
} = {}) {
|
|
15
15
|
const seenResultFiles = new Set();
|
|
16
|
+
const seenCommandLogEvents = new Set();
|
|
17
|
+
const observedRunCommandIds = new Set();
|
|
16
18
|
let timer = null;
|
|
17
19
|
let running = false;
|
|
18
|
-
let
|
|
19
|
-
let preferLatestArtifact = false;
|
|
20
|
+
let lastArtifactSignatures = new Map();
|
|
20
21
|
|
|
21
22
|
function start() {
|
|
22
23
|
if (running) return;
|
|
23
24
|
running = true;
|
|
24
|
-
|
|
25
|
+
lastArtifactSignatures = readArtifactSignatures();
|
|
25
26
|
scan();
|
|
26
27
|
timer = setInterval(scan, intervalMs);
|
|
27
28
|
}
|
|
@@ -34,10 +35,32 @@ export function createAssistantCommandObserver({
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
function scan() {
|
|
38
|
+
observeCommandLog();
|
|
37
39
|
observeCommandResults();
|
|
38
40
|
observeRunArtifact();
|
|
39
41
|
}
|
|
40
42
|
|
|
43
|
+
function observeCommandLog() {
|
|
44
|
+
const commandLogPath = commandLog?.commandLogPath;
|
|
45
|
+
if (!commandLogPath || !fs.existsSync(commandLogPath)) return;
|
|
46
|
+
const lines = fs.readFileSync(commandLogPath, "utf8").split(/\r?\n/).filter(Boolean);
|
|
47
|
+
for (const [index, line] of lines.entries()) {
|
|
48
|
+
const eventKey = `${index}:${line}`;
|
|
49
|
+
if (seenCommandLogEvents.has(eventKey)) continue;
|
|
50
|
+
seenCommandLogEvents.add(eventKey);
|
|
51
|
+
let event = null;
|
|
52
|
+
try {
|
|
53
|
+
event = JSON.parse(line);
|
|
54
|
+
} catch {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (event.sessionId && commandLog.sessionId && event.sessionId !== commandLog.sessionId) continue;
|
|
58
|
+
if (event.type === "command_start" && isRunCommand(event)) {
|
|
59
|
+
observedRunCommandIds.add(event.commandId);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
41
64
|
function observeCommandResults() {
|
|
42
65
|
const resultDir = commandLog?.resultDir;
|
|
43
66
|
if (!resultDir || !fs.existsSync(resultDir)) return;
|
|
@@ -54,10 +77,11 @@ export function createAssistantCommandObserver({
|
|
|
54
77
|
} catch {
|
|
55
78
|
continue;
|
|
56
79
|
}
|
|
80
|
+
if (document.sessionId && commandLog.sessionId && document.sessionId !== commandLog.sessionId) continue;
|
|
57
81
|
if (!OBSERVED_KINDS.has(document.kind)) continue;
|
|
58
82
|
seenResultFiles.add(filePath);
|
|
59
83
|
if (document.kind === "run") {
|
|
60
|
-
|
|
84
|
+
observedRunCommandIds.add(document.commandId);
|
|
61
85
|
hydrateRunArtifact("command-result", document);
|
|
62
86
|
}
|
|
63
87
|
onEvent?.({
|
|
@@ -68,29 +92,34 @@ export function createAssistantCommandObserver({
|
|
|
68
92
|
}
|
|
69
93
|
|
|
70
94
|
function observeRunArtifact() {
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
95
|
+
const signatures = readArtifactSignatures();
|
|
96
|
+
const changed = [...signatures.entries()]
|
|
97
|
+
.filter(([artifactPath, signature]) => lastArtifactSignatures.get(artifactPath) !== signature)
|
|
98
|
+
.map(([artifactPath, signature]) => ({
|
|
99
|
+
artifactPath,
|
|
100
|
+
signature,
|
|
101
|
+
mtimeMs: Number(signature.split(":").at(-2) || 0),
|
|
102
|
+
}))
|
|
103
|
+
.sort((left, right) => left.mtimeMs - right.mtimeMs);
|
|
104
|
+
lastArtifactSignatures = signatures;
|
|
77
105
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
106
|
+
for (const entry of changed) {
|
|
107
|
+
const artifact = readJsonFile(entry.artifactPath);
|
|
108
|
+
if (!artifact || !shouldHydrateObservedArtifact(artifact)) continue;
|
|
109
|
+
hydrateRunArtifact("artifact", { artifactPath: entry.artifactPath, artifact });
|
|
110
|
+
}
|
|
83
111
|
}
|
|
84
112
|
|
|
85
|
-
function
|
|
113
|
+
function readArtifactSignatures() {
|
|
86
114
|
const livePath = path.join(productDir, ".testkit", "results", "live.json");
|
|
87
115
|
const latestPath = path.join(productDir, ".testkit", "results", "latest.json");
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
116
|
+
const signatures = new Map();
|
|
117
|
+
for (const artifactPath of [livePath, latestPath]) {
|
|
118
|
+
const stat = safeStat(artifactPath);
|
|
119
|
+
if (!stat) continue;
|
|
120
|
+
signatures.set(artifactPath, `${artifactPath}:${stat.mtimeMs}:${stat.size}`);
|
|
121
|
+
}
|
|
122
|
+
return signatures;
|
|
94
123
|
}
|
|
95
124
|
|
|
96
125
|
function hydrateRunArtifact(source, command = null) {
|
|
@@ -107,29 +136,31 @@ export function createAssistantCommandObserver({
|
|
|
107
136
|
|
|
108
137
|
function loadObservedRunArtifact(command = null) {
|
|
109
138
|
if (command?.result?.runArtifact) return command.result.runArtifact;
|
|
110
|
-
if (command?.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
139
|
+
if (command?.artifact) return command.artifact;
|
|
140
|
+
if (command?.artifactPath && fs.existsSync(command.artifactPath)) return readJsonFile(command.artifactPath);
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function shouldHydrateObservedArtifact(artifact) {
|
|
145
|
+
const assistant = artifact?.provenance?.assistant || {};
|
|
146
|
+
if (!assistant.sessionId || !assistant.commandId) return false;
|
|
147
|
+
if (commandLog?.sessionId && assistant.sessionId !== commandLog.sessionId) return false;
|
|
148
|
+
return observedRunCommandIds.has(assistant.commandId);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function isRunCommand(event) {
|
|
152
|
+
if (!event?.commandId) return false;
|
|
153
|
+
if (event.kind === "run") return true;
|
|
154
|
+
if (RUN_KINDS.has(event.kind)) return true;
|
|
155
|
+
const argv = Array.isArray(event.argv) ? event.argv : [];
|
|
156
|
+
return argv[0] === "run" || RUN_KINDS.has(argv[0]);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function readJsonFile(filePath) {
|
|
125
160
|
try {
|
|
126
|
-
return
|
|
161
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
127
162
|
} catch {
|
|
128
|
-
|
|
129
|
-
return loadLatestRunArtifact(productDir);
|
|
130
|
-
} catch {
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
163
|
+
return null;
|
|
133
164
|
}
|
|
134
165
|
}
|
|
135
166
|
|
|
@@ -7,6 +7,19 @@ export const ASSISTANT_COMMAND_LOG_ENV = "TESTKIT_ASSISTANT_COMMAND_LOG";
|
|
|
7
7
|
export const ASSISTANT_COMMAND_ID_ENV = "TESTKIT_ASSISTANT_COMMAND_ID";
|
|
8
8
|
export const ASSISTANT_WRAPPER_LOGGED_ENV = "TESTKIT_ASSISTANT_WRAPPER_LOGGED";
|
|
9
9
|
|
|
10
|
+
const RUN_SHORTCUTS = new Set(["int", "e2e", "scenario", "dal", "load", "pw", "all"]);
|
|
11
|
+
const FLAGS_WITH_VALUES = new Set([
|
|
12
|
+
"--dir",
|
|
13
|
+
"--service",
|
|
14
|
+
"--type",
|
|
15
|
+
"--suite",
|
|
16
|
+
"--file",
|
|
17
|
+
"--workers",
|
|
18
|
+
"--file-timeout-seconds",
|
|
19
|
+
"--seed",
|
|
20
|
+
"--output-mode",
|
|
21
|
+
]);
|
|
22
|
+
|
|
10
23
|
export function createAssistantCommandContext({
|
|
11
24
|
kind,
|
|
12
25
|
argv = process.argv.slice(2),
|
|
@@ -136,8 +149,22 @@ export function appendAssistantCommandLog(context, event) {
|
|
|
136
149
|
}
|
|
137
150
|
|
|
138
151
|
function inferCommandKind(argv) {
|
|
139
|
-
const
|
|
140
|
-
|
|
152
|
+
const first = findFirstPositional(argv);
|
|
153
|
+
if (!first || RUN_SHORTCUTS.has(first)) return "run";
|
|
154
|
+
return first;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function findFirstPositional(argv) {
|
|
158
|
+
const args = Array.isArray(argv) ? argv : [];
|
|
159
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
160
|
+
const arg = String(args[index]);
|
|
161
|
+
if (FLAGS_WITH_VALUES.has(arg)) {
|
|
162
|
+
index += 1;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (!arg.startsWith("-")) return arg;
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
141
168
|
}
|
|
142
169
|
|
|
143
170
|
function inferExitCode(result) {
|
|
@@ -178,7 +178,27 @@ function appendCommandLog(event) {
|
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
function inferKind(args) {
|
|
181
|
-
|
|
181
|
+
const runShortcuts = new Set(["int", "e2e", "scenario", "dal", "load", "pw", "all"]);
|
|
182
|
+
const flagsWithValues = new Set([
|
|
183
|
+
"--dir",
|
|
184
|
+
"--service",
|
|
185
|
+
"--type",
|
|
186
|
+
"--suite",
|
|
187
|
+
"--file",
|
|
188
|
+
"--workers",
|
|
189
|
+
"--file-timeout-seconds",
|
|
190
|
+
"--seed",
|
|
191
|
+
"--output-mode",
|
|
192
|
+
]);
|
|
193
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
194
|
+
const arg = String(args[index]);
|
|
195
|
+
if (flagsWithValues.has(arg)) {
|
|
196
|
+
index += 1;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (!arg.startsWith("-")) return runShortcuts.has(arg) ? "run" : arg;
|
|
200
|
+
}
|
|
201
|
+
return "run";
|
|
182
202
|
}
|
|
183
203
|
`;
|
|
184
204
|
}
|
|
@@ -19,7 +19,11 @@ export function loadAssistantSettings(productDir) {
|
|
|
19
19
|
const filePath = assistantSettingsPath(productDir);
|
|
20
20
|
try {
|
|
21
21
|
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
22
|
-
|
|
22
|
+
const normalized = normalizeAssistantSettings(parsed);
|
|
23
|
+
if (JSON.stringify(parsed) !== JSON.stringify(normalized)) {
|
|
24
|
+
writeAssistantSettingsFile(filePath, normalized);
|
|
25
|
+
}
|
|
26
|
+
return normalized;
|
|
23
27
|
} catch {
|
|
24
28
|
return { ...DEFAULT_ASSISTANT_SETTINGS };
|
|
25
29
|
}
|
|
@@ -27,8 +31,7 @@ export function loadAssistantSettings(productDir) {
|
|
|
27
31
|
|
|
28
32
|
export function saveAssistantSettings(productDir, settings) {
|
|
29
33
|
const filePath = assistantSettingsPath(productDir);
|
|
30
|
-
|
|
31
|
-
fs.writeFileSync(filePath, `${JSON.stringify(normalizeAssistantSettings(settings), null, 2)}\n`);
|
|
34
|
+
writeAssistantSettingsFile(filePath, normalizeAssistantSettings(settings));
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
export function resetAssistantSettings(productDir) {
|
|
@@ -52,8 +55,9 @@ export function mergeAssistantSettings(...settingsObjects) {
|
|
|
52
55
|
|
|
53
56
|
export function normalizeAssistantSettings(value = {}) {
|
|
54
57
|
const provider = normalizeProvider(value.provider);
|
|
55
|
-
const
|
|
56
|
-
const
|
|
58
|
+
const extracted = extractEffortFromModel(value.model);
|
|
59
|
+
const effort = normalizeEffort(value.effort || extracted.effort);
|
|
60
|
+
const model = extracted.model;
|
|
57
61
|
const providerArgs = Array.isArray(value.providerArgs)
|
|
58
62
|
? value.providerArgs.map((entry) => normalizeOptionalString(entry)).filter(Boolean)
|
|
59
63
|
: [];
|
|
@@ -89,6 +93,19 @@ export function normalizeOptionalString(value) {
|
|
|
89
93
|
return stringValue || null;
|
|
90
94
|
}
|
|
91
95
|
|
|
96
|
+
export function extractEffortFromModel(value) {
|
|
97
|
+
const model = normalizeOptionalString(value);
|
|
98
|
+
if (!model) return { model: null, effort: null };
|
|
99
|
+
const tokens = model.split(/\s+/);
|
|
100
|
+
if (tokens.length < 2) return { model, effort: null };
|
|
101
|
+
const trailingEffort = tokens.at(-1);
|
|
102
|
+
if (!ASSISTANT_EFFORTS.includes(trailingEffort)) return { model, effort: null };
|
|
103
|
+
return {
|
|
104
|
+
model: tokens.slice(0, -1).join(" ").trim() || null,
|
|
105
|
+
effort: trailingEffort,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
92
109
|
function dropNullishSettings(settings) {
|
|
93
110
|
const result = {};
|
|
94
111
|
for (const [key, value] of Object.entries(settings)) {
|
|
@@ -96,3 +113,8 @@ function dropNullishSettings(settings) {
|
|
|
96
113
|
}
|
|
97
114
|
return result;
|
|
98
115
|
}
|
|
116
|
+
|
|
117
|
+
function writeAssistantSettingsFile(filePath, settings) {
|
|
118
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
119
|
+
fs.writeFileSync(filePath, `${JSON.stringify(settings, null, 2)}\n`);
|
|
120
|
+
}
|
|
@@ -470,7 +470,13 @@ async function executeSlashCommand({
|
|
|
470
470
|
}
|
|
471
471
|
if (slash.type === "model") {
|
|
472
472
|
state.setModel(slash.model, { custom: slash.custom });
|
|
473
|
-
|
|
473
|
+
const snapshot = state.getSnapshot();
|
|
474
|
+
appendMessage({
|
|
475
|
+
role: "assistant",
|
|
476
|
+
text: slash.model
|
|
477
|
+
? `Model set to ${snapshot.model}${snapshot.effort ? ` with ${snapshot.effort} effort` : ""}.`
|
|
478
|
+
: "Model reset to provider default.",
|
|
479
|
+
});
|
|
474
480
|
return;
|
|
475
481
|
}
|
|
476
482
|
if (slash.type === "model-list") {
|
|
@@ -32,9 +32,6 @@ export const runFlags = {
|
|
|
32
32
|
"file-timeout-seconds": Flags.string({
|
|
33
33
|
description: "Per-file wall-clock timeout in seconds",
|
|
34
34
|
}),
|
|
35
|
-
shard: Flags.string({
|
|
36
|
-
description: "Run only shard i of n at suite granularity",
|
|
37
|
-
}),
|
|
38
35
|
seed: Flags.string({
|
|
39
36
|
description: "Deterministic seed for scenario suites",
|
|
40
37
|
}),
|
package/lib/cli/entrypoint.mjs
CHANGED
|
@@ -23,7 +23,6 @@ export function normalizeCliArgs(argv) {
|
|
|
23
23
|
"--file",
|
|
24
24
|
"--workers",
|
|
25
25
|
"--file-timeout-seconds",
|
|
26
|
-
"--shard",
|
|
27
26
|
"--seed",
|
|
28
27
|
"--input",
|
|
29
28
|
"--output",
|
|
@@ -54,7 +53,6 @@ export function normalizeCliArgs(argv) {
|
|
|
54
53
|
"-f",
|
|
55
54
|
"--workers",
|
|
56
55
|
"--file-timeout-seconds",
|
|
57
|
-
"--shard",
|
|
58
56
|
"--seed",
|
|
59
57
|
"--write-status",
|
|
60
58
|
"--allow-partial-status",
|
|
@@ -2,7 +2,6 @@ import * as runner from "../../../runner/index.mjs";
|
|
|
2
2
|
import { loadManagedConfigs } from "../../../app/configs.mjs";
|
|
3
3
|
import {
|
|
4
4
|
parseFileTimeoutOption,
|
|
5
|
-
parseShardOption,
|
|
6
5
|
parseSuiteOption,
|
|
7
6
|
parseTypeOption,
|
|
8
7
|
parseWorkersOption,
|
|
@@ -25,7 +24,6 @@ export async function buildRunRequest(flags, positionalType = null, cwd = proces
|
|
|
25
24
|
flags["file-timeout-seconds"] == null
|
|
26
25
|
? null
|
|
27
26
|
: parseFileTimeoutOption(flags["file-timeout-seconds"]);
|
|
28
|
-
const shard = parseShardOption(flags.shard);
|
|
29
27
|
const typeValues = parseTypeOption(flags.type, positionalType);
|
|
30
28
|
const suiteSelectors = parseSuiteOption(flags.suite);
|
|
31
29
|
const rawFileNames = Array.isArray(flags.file) ? flags.file : [flags.file].filter(Boolean);
|
|
@@ -44,7 +42,6 @@ export async function buildRunRequest(flags, positionalType = null, cwd = proces
|
|
|
44
42
|
fileNames,
|
|
45
43
|
workers,
|
|
46
44
|
fileTimeoutSeconds,
|
|
47
|
-
shard,
|
|
48
45
|
scenarioSeed: flags.seed || null,
|
|
49
46
|
serviceFilter: flags.service || null,
|
|
50
47
|
writeStatus: flags["write-status"],
|
package/lib/runner/live-run.mjs
CHANGED
|
@@ -4,12 +4,14 @@ import { writeLiveRunArtifact } from "./artifacts.mjs";
|
|
|
4
4
|
|
|
5
5
|
export function createLiveSnapshotWriter({
|
|
6
6
|
productDir,
|
|
7
|
+
runId,
|
|
7
8
|
configs,
|
|
8
9
|
trackers,
|
|
9
10
|
startedAt,
|
|
10
11
|
execution,
|
|
11
12
|
workerState,
|
|
12
13
|
selection,
|
|
14
|
+
provenance = null,
|
|
13
15
|
metadata,
|
|
14
16
|
logRegistry,
|
|
15
17
|
setupRegistry,
|
|
@@ -21,6 +23,7 @@ export function createLiveSnapshotWriter({
|
|
|
21
23
|
productDir,
|
|
22
24
|
buildLiveRunArtifact({
|
|
23
25
|
productDir,
|
|
26
|
+
runId,
|
|
24
27
|
results: partialResults,
|
|
25
28
|
startedAt,
|
|
26
29
|
updatedAt: now,
|
|
@@ -31,9 +34,10 @@ export function createLiveSnapshotWriter({
|
|
|
31
34
|
typeValues: selection.typeValues,
|
|
32
35
|
suiteSelectors: selection.suiteSelectors,
|
|
33
36
|
fileNames: selection.fileNames,
|
|
34
|
-
shard: selection.shard,
|
|
35
37
|
serviceFilter: selection.serviceFilter,
|
|
36
38
|
scenarioSeed: selection.scenarioSeed,
|
|
39
|
+
planning: selection.planning || null,
|
|
40
|
+
provenance,
|
|
37
41
|
metadata,
|
|
38
42
|
summarizeDbBackend,
|
|
39
43
|
serviceLogs: logRegistry.listServiceLogs(),
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
-
applyShard,
|
|
3
2
|
buildRuntimeGraphs,
|
|
4
|
-
buildTaskQueue,
|
|
5
3
|
claimNextTask,
|
|
6
4
|
collectSuites,
|
|
7
5
|
resolveRuntimeConfigs,
|
|
8
6
|
} from "./planning.mjs";
|
|
7
|
+
import { buildRunPlanningMetadata, buildScheduledQueue } from "./scheduler/index.mjs";
|
|
9
8
|
import {
|
|
10
9
|
addTrackerError,
|
|
11
10
|
buildServiceTrackers,
|
|
@@ -20,6 +19,7 @@ import {
|
|
|
20
19
|
resetResultArtifacts,
|
|
21
20
|
saveTimings,
|
|
22
21
|
} from "./artifacts.mjs";
|
|
22
|
+
import { loadHistory } from "../history/index.mjs";
|
|
23
23
|
import { createRunLogRegistry } from "./logs.mjs";
|
|
24
24
|
import { createSetupOperationRegistry } from "./setup-operations.mjs";
|
|
25
25
|
import {
|
|
@@ -40,12 +40,14 @@ import { createWorker, runWorker } from "./worker-loop.mjs";
|
|
|
40
40
|
import { ensureRequestedFilesMatch, ensureStatusWriteAllowed } from "./run-guards.mjs";
|
|
41
41
|
import { createLiveSnapshotWriter } from "./live-run.mjs";
|
|
42
42
|
import { finalizeRunArtifacts } from "./run-finalization.mjs";
|
|
43
|
+
import { buildRunProvenance } from "./provenance.mjs";
|
|
43
44
|
|
|
44
45
|
export async function runAll(configs, typeValues, suiteSelectors, opts, allConfigs = configs) {
|
|
45
46
|
const configMap = new Map(allConfigs.map((config) => [config.name, config]));
|
|
46
47
|
const startedAt = Date.now();
|
|
47
48
|
const telemetry = configs[0]?.telemetry || null;
|
|
48
49
|
const productDir = configs[0]?.productDir || process.cwd();
|
|
50
|
+
const provenance = buildRunProvenance(opts.env || process.env);
|
|
49
51
|
await cleanupStaleRuns(productDir);
|
|
50
52
|
resetResultArtifacts(productDir);
|
|
51
53
|
const metadata = {
|
|
@@ -87,30 +89,33 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
87
89
|
);
|
|
88
90
|
reporter?.setServicePlans?.(servicePlans);
|
|
89
91
|
const trackers = buildServiceTrackers(servicePlans, startedAt);
|
|
92
|
+
const runSelection = {
|
|
93
|
+
typeValues,
|
|
94
|
+
suiteSelectors,
|
|
95
|
+
fileNames: requestedFiles,
|
|
96
|
+
serviceFilter: opts.serviceFilter || null,
|
|
97
|
+
scenarioSeed: opts.scenarioSeed || null,
|
|
98
|
+
planning: null,
|
|
99
|
+
};
|
|
90
100
|
let writeLiveSnapshot = () => {};
|
|
91
101
|
const setupRegistry = createSetupOperationRegistry({ logRegistry, onChange: () => writeLiveSnapshot() });
|
|
102
|
+
const executedPlans = servicePlans.filter((plan) => !plan.skipped);
|
|
103
|
+
let exitCode = 0;
|
|
104
|
+
const lifecycle = createRunLifecycle(productDir);
|
|
92
105
|
writeLiveSnapshot = createLiveSnapshotWriter({
|
|
93
106
|
productDir,
|
|
107
|
+
runId: lifecycle.runId,
|
|
94
108
|
configs,
|
|
95
109
|
trackers,
|
|
96
110
|
startedAt,
|
|
97
111
|
execution,
|
|
98
112
|
workerState,
|
|
99
|
-
selection:
|
|
100
|
-
|
|
101
|
-
suiteSelectors,
|
|
102
|
-
fileNames: requestedFiles,
|
|
103
|
-
shard: opts.shard || null,
|
|
104
|
-
serviceFilter: opts.serviceFilter || null,
|
|
105
|
-
scenarioSeed: opts.scenarioSeed || null,
|
|
106
|
-
},
|
|
113
|
+
selection: runSelection,
|
|
114
|
+
provenance,
|
|
107
115
|
metadata,
|
|
108
116
|
logRegistry,
|
|
109
117
|
setupRegistry,
|
|
110
118
|
});
|
|
111
|
-
const executedPlans = servicePlans.filter((plan) => !plan.skipped);
|
|
112
|
-
let exitCode = 0;
|
|
113
|
-
const lifecycle = createRunLifecycle(productDir);
|
|
114
119
|
lifecycle.markRunning();
|
|
115
120
|
lifecycle.installSignalHandlers();
|
|
116
121
|
let results = [];
|
|
@@ -120,8 +125,11 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
120
125
|
try {
|
|
121
126
|
if (executedPlans.length > 0) {
|
|
122
127
|
const timings = loadTimings(productDir);
|
|
128
|
+
const history = loadHistory(productDir);
|
|
123
129
|
const graphs = buildRuntimeGraphs(executedPlans);
|
|
124
|
-
const queue =
|
|
130
|
+
const queue = buildScheduledQueue(executedPlans, graphs, { timings, history });
|
|
131
|
+
runSelection.planning = buildRunPlanningMetadata(queue);
|
|
132
|
+
writeLiveSnapshot();
|
|
125
133
|
reporter?.setTotalFileCount?.(queue.length);
|
|
126
134
|
for (const task of queue) {
|
|
127
135
|
task.scenarioSeed = opts.scenarioSeed || null;
|
|
@@ -191,6 +199,7 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
191
199
|
);
|
|
192
200
|
const finalized = await finalizeRunArtifacts({
|
|
193
201
|
productDir,
|
|
202
|
+
runId: lifecycle.runId,
|
|
194
203
|
results,
|
|
195
204
|
startedAt,
|
|
196
205
|
finishedAt,
|
|
@@ -198,14 +207,8 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
198
207
|
workerCount: workerState.workerCount,
|
|
199
208
|
runtimeInstanceCount: workerState.runtimeInstanceCount,
|
|
200
209
|
runtimeStats: workerState.runtimeStats,
|
|
201
|
-
selection:
|
|
202
|
-
|
|
203
|
-
suiteSelectors,
|
|
204
|
-
fileNames: requestedFiles,
|
|
205
|
-
shard: opts.shard || null,
|
|
206
|
-
serviceFilter: opts.serviceFilter || null,
|
|
207
|
-
scenarioSeed: opts.scenarioSeed || null,
|
|
208
|
-
},
|
|
210
|
+
selection: runSelection,
|
|
211
|
+
provenance,
|
|
209
212
|
metadata,
|
|
210
213
|
logRegistry,
|
|
211
214
|
setupRegistry,
|
|
@@ -245,10 +248,7 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
245
248
|
|
|
246
249
|
function collectServicePlans(configs, configMap, typeValues, suiteSelectors, opts, execution, reporter) {
|
|
247
250
|
return configs.map((config) => {
|
|
248
|
-
const suites =
|
|
249
|
-
collectSuites(config, typeValues, suiteSelectors, opts.fileNames || [], opts),
|
|
250
|
-
opts.shard
|
|
251
|
-
);
|
|
251
|
+
const suites = collectSuites(config, typeValues, suiteSelectors, opts.fileNames || [], opts);
|
|
252
252
|
|
|
253
253
|
if (suites.length === 0) {
|
|
254
254
|
reporter?.serviceSkipped?.(
|