@elench/testkit 0.1.55 → 0.1.57

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,182 @@
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+ import { discoverTests } from "./index.mjs";
6
+ import { buildHistoryTestId, saveHistory } from "../history/index.mjs";
7
+
8
+ const cleanups = [];
9
+
10
+ afterEach(() => {
11
+ while (cleanups.length > 0) {
12
+ cleanups.pop()();
13
+ }
14
+ });
15
+
16
+ describe("public discovery", () => {
17
+ it("returns resolved suites/files with human labels and persisted history", async () => {
18
+ const productDir = createProduct();
19
+ writeFile(
20
+ productDir,
21
+ "testkit.setup.ts",
22
+ `
23
+ import { defineTestkitSetup } from "@elench/testkit/setup";
24
+
25
+ export default defineTestkitSetup({
26
+ services: {
27
+ api: {
28
+ local: {
29
+ cwd: ".",
30
+ start: "node server.js",
31
+ baseUrl: "http://127.0.0.1:3000",
32
+ readyUrl: "http://127.0.0.1:3000"
33
+ },
34
+ requirements: {
35
+ files: [
36
+ {
37
+ path: "src/api/routes/__testkit__/agent-configs-auth-gate.int.testkit.ts",
38
+ locks: ["route-lock"]
39
+ }
40
+ ]
41
+ }
42
+ },
43
+ frontend: {
44
+ local: {
45
+ cwd: "frontend",
46
+ start: "node server.js",
47
+ baseUrl: "http://127.0.0.1:3001",
48
+ readyUrl: "http://127.0.0.1:3001"
49
+ },
50
+ dependsOn: ["api"],
51
+ skip: {
52
+ files: [
53
+ {
54
+ path: "frontend/src/app/login/__testkit__/auth.pw.testkit.ts",
55
+ reason: "Auth is stubbed locally"
56
+ }
57
+ ]
58
+ }
59
+ }
60
+ }
61
+ });
62
+ `
63
+ );
64
+ writeFile(productDir, "src/api/routes/__testkit__/agent-configs-auth-gate.int.testkit.ts");
65
+ writeFile(productDir, "frontend/src/app/login/__testkit__/auth.pw.testkit.ts");
66
+
67
+ saveHistory(productDir, {
68
+ version: 1,
69
+ tests: {
70
+ [buildHistoryTestId("api", "int", "src/api/routes/__testkit__/agent-configs-auth-gate.int.testkit.ts")]: {
71
+ id: buildHistoryTestId("api", "int", "src/api/routes/__testkit__/agent-configs-auth-gate.int.testkit.ts"),
72
+ path: "src/api/routes/__testkit__/agent-configs-auth-gate.int.testkit.ts",
73
+ service: "api",
74
+ suiteName: "routes",
75
+ selectionType: "int",
76
+ framework: "k6",
77
+ firstSeenAt: "2026-04-01T00:00:00.000Z",
78
+ lastSeenAt: "2026-04-02T00:00:00.000Z",
79
+ lastRunAt: "2026-04-02T00:00:00.000Z",
80
+ runCount: 4,
81
+ passCount: 3,
82
+ failCount: 1,
83
+ skipCount: 0,
84
+ notRunCount: 0,
85
+ avgDurationMs: 2500,
86
+ durationCount: 4,
87
+ lastStatus: "passed",
88
+ },
89
+ },
90
+ });
91
+
92
+ const result = await discoverTests({ dir: productDir });
93
+ expect(result.summary).toMatchObject({
94
+ services: 2,
95
+ suites: 2,
96
+ files: 2,
97
+ activeFiles: 1,
98
+ skippedFiles: 1,
99
+ byType: {
100
+ int: 1,
101
+ pw: 1,
102
+ },
103
+ });
104
+ expect(result.history.available).toBe(true);
105
+ expect(result.services.map((entry) => entry.name)).toEqual(["api", "frontend"]);
106
+
107
+ const apiFile = result.files.find((entry) => entry.service === "api");
108
+ expect(apiFile).toMatchObject({
109
+ displayName: "Agent Configs Auth Gate",
110
+ suiteName: "routes",
111
+ groupLabel: "Routes",
112
+ selectionType: "int",
113
+ skipped: false,
114
+ locks: ["route-lock"],
115
+ });
116
+ expect(apiFile.history).toMatchObject({
117
+ firstSeenAt: "2026-04-01T00:00:00.000Z",
118
+ runCount: 4,
119
+ passCount: 3,
120
+ failCount: 1,
121
+ avgDurationMs: 2500,
122
+ });
123
+
124
+ const frontendFile = result.files.find((entry) => entry.service === "frontend");
125
+ expect(frontendFile).toMatchObject({
126
+ displayName: "Auth",
127
+ selectionType: "pw",
128
+ skipped: true,
129
+ skipReason: "Auth is stubbed locally",
130
+ dependsOn: ["api"],
131
+ });
132
+ });
133
+
134
+ it("reports legacy discovery diagnostics without failing in report mode", async () => {
135
+ const productDir = createProduct();
136
+ writeFile(
137
+ productDir,
138
+ "testkit.setup.ts",
139
+ `
140
+ import { defineTestkitSetup } from "@elench/testkit/setup";
141
+
142
+ export default defineTestkitSetup({
143
+ services: {
144
+ api: {
145
+ local: {
146
+ cwd: ".",
147
+ start: "node server.js",
148
+ baseUrl: "http://127.0.0.1:3000",
149
+ readyUrl: "http://127.0.0.1:3000"
150
+ }
151
+ }
152
+ }
153
+ });
154
+ `
155
+ );
156
+ writeFile(productDir, "src/api/routes/__testkit__/health/health.int.testkit.ts");
157
+ writeFile(productDir, "tests/api/integration/legacy.int.testkit.ts");
158
+
159
+ const result = await discoverTests({ dir: productDir, diagnostics: "report" });
160
+ expect(result.files).toHaveLength(1);
161
+ expect(result.diagnostics).toEqual(
162
+ expect.arrayContaining([
163
+ expect.objectContaining({
164
+ code: "legacy_path",
165
+ path: "tests/api/integration/legacy.int.testkit.ts",
166
+ }),
167
+ ])
168
+ );
169
+ });
170
+ });
171
+
172
+ function createProduct() {
173
+ const productDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-discovery-public-"));
174
+ cleanups.push(() => fs.rmSync(productDir, { recursive: true, force: true }));
175
+ return productDir;
176
+ }
177
+
178
+ function writeFile(productDir, relativePath, contents = "export {};\n") {
179
+ const absolutePath = path.join(productDir, relativePath);
180
+ fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
181
+ fs.writeFileSync(absolutePath, contents);
182
+ }
@@ -0,0 +1,46 @@
1
+ export interface TestHistorySummary {
2
+ firstSeenAt: string | null;
3
+ lastSeenAt: string | null;
4
+ lastRunAt: string | null;
5
+ runCount: number;
6
+ passCount: number;
7
+ failCount: number;
8
+ skipCount: number;
9
+ avgDurationMs: number;
10
+ lastStatus: "passed" | "failed" | "skipped" | "not_run" | null;
11
+ }
12
+
13
+ export interface TestHistoryEntry extends TestHistorySummary {
14
+ id: string;
15
+ path: string;
16
+ service: string;
17
+ suiteName: string;
18
+ selectionType: string;
19
+ framework: string;
20
+ notRunCount: number;
21
+ durationCount: number;
22
+ }
23
+
24
+ export interface TestHistoryDocument {
25
+ version: number;
26
+ tests: Record<string, TestHistoryEntry>;
27
+ }
28
+
29
+ export declare function historyFilePath(productDir: string): string;
30
+ export declare function createEmptyHistory(): TestHistoryDocument;
31
+ export declare function loadHistory(productDir: string): TestHistoryDocument;
32
+ export declare function saveHistory(productDir: string, history: TestHistoryDocument): void;
33
+ export declare function updateHistoryFromRunArtifact(
34
+ history: TestHistoryDocument,
35
+ runArtifact: unknown,
36
+ recordedAt?: string | null
37
+ ): TestHistoryDocument;
38
+ export declare function summarizeHistoryForFiles(
39
+ history: TestHistoryDocument,
40
+ files?: Array<{ id: string; service: string; selectionType: string; path: string }>
41
+ ): Map<string, TestHistorySummary>;
42
+ export declare function buildHistoryTestId(
43
+ serviceName: string,
44
+ selectionType: string,
45
+ filePath: string
46
+ ): string;
@@ -0,0 +1,166 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ const HISTORY_SCHEMA_VERSION = 1;
5
+ const HISTORY_DIRNAME = "history";
6
+ const TEST_HISTORY_FILENAME = "tests.json";
7
+
8
+ export function historyFilePath(productDir) {
9
+ return path.join(productDir, ".testkit", HISTORY_DIRNAME, TEST_HISTORY_FILENAME);
10
+ }
11
+
12
+ export function createEmptyHistory() {
13
+ return {
14
+ version: HISTORY_SCHEMA_VERSION,
15
+ tests: {},
16
+ };
17
+ }
18
+
19
+ export function loadHistory(productDir) {
20
+ const filePath = historyFilePath(productDir);
21
+ if (!fs.existsSync(filePath)) {
22
+ return createEmptyHistory();
23
+ }
24
+
25
+ try {
26
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
27
+ return normalizeHistory(parsed);
28
+ } catch {
29
+ return createEmptyHistory();
30
+ }
31
+ }
32
+
33
+ export function saveHistory(productDir, history) {
34
+ const filePath = historyFilePath(productDir);
35
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
36
+ fs.writeFileSync(filePath, `${JSON.stringify(normalizeHistory(history), null, 2)}\n`);
37
+ }
38
+
39
+ export function updateHistoryFromRunArtifact(history, runArtifact, recordedAt = null) {
40
+ const normalized = normalizeHistory(history);
41
+ const seenAt = recordedAt || runArtifact?.generatedAt || new Date().toISOString();
42
+
43
+ for (const service of runArtifact?.services || []) {
44
+ for (const suite of service.suites || []) {
45
+ for (const file of suite.files || []) {
46
+ const id = buildHistoryTestId(service.name, suite.type, file.path);
47
+ const existing = normalized.tests[id];
48
+ const next = existing
49
+ ? { ...existing }
50
+ : {
51
+ id,
52
+ path: file.path,
53
+ service: service.name,
54
+ suiteName: suite.name,
55
+ selectionType: suite.type,
56
+ framework: normalizeArtifactFramework(suite.framework),
57
+ firstSeenAt: seenAt,
58
+ lastSeenAt: seenAt,
59
+ lastRunAt: seenAt,
60
+ runCount: 0,
61
+ passCount: 0,
62
+ failCount: 0,
63
+ skipCount: 0,
64
+ notRunCount: 0,
65
+ avgDurationMs: 0,
66
+ durationCount: 0,
67
+ lastStatus: null,
68
+ };
69
+
70
+ next.path = file.path;
71
+ next.service = service.name;
72
+ next.suiteName = suite.name;
73
+ next.selectionType = suite.type;
74
+ next.framework = normalizeArtifactFramework(suite.framework);
75
+ next.lastSeenAt = seenAt;
76
+ next.lastRunAt = seenAt;
77
+ next.runCount += 1;
78
+ next.lastStatus = file.status;
79
+
80
+ if (file.status === "passed") next.passCount += 1;
81
+ else if (file.status === "failed") next.failCount += 1;
82
+ else if (file.status === "skipped") next.skipCount += 1;
83
+ else next.notRunCount += 1;
84
+
85
+ if (Number(file.durationMs || 0) > 0 && file.status !== "skipped" && file.status !== "not_run") {
86
+ const durationCount = Number(next.durationCount || 0) + 1;
87
+ next.avgDurationMs = Math.max(
88
+ 1,
89
+ Math.round(
90
+ ((Number(next.avgDurationMs || 0) * Number(next.durationCount || 0)) + Number(file.durationMs || 0)) /
91
+ durationCount
92
+ )
93
+ );
94
+ next.durationCount = durationCount;
95
+ }
96
+
97
+ normalized.tests[id] = next;
98
+ }
99
+ }
100
+ }
101
+
102
+ return normalized;
103
+ }
104
+
105
+ export function summarizeHistoryForFiles(history, files = []) {
106
+ const normalized = normalizeHistory(history);
107
+ const byId = new Map();
108
+ for (const file of files) {
109
+ const entry = normalized.tests[file.id] || normalized.tests[buildHistoryTestId(file.service, file.selectionType, file.path)];
110
+ if (!entry) continue;
111
+ byId.set(file.id, {
112
+ firstSeenAt: entry.firstSeenAt || null,
113
+ lastSeenAt: entry.lastSeenAt || null,
114
+ lastRunAt: entry.lastRunAt || null,
115
+ runCount: Number(entry.runCount || 0),
116
+ passCount: Number(entry.passCount || 0),
117
+ failCount: Number(entry.failCount || 0),
118
+ skipCount: Number(entry.skipCount || 0),
119
+ avgDurationMs: Number(entry.avgDurationMs || 0),
120
+ lastStatus: entry.lastStatus || null,
121
+ });
122
+ }
123
+ return byId;
124
+ }
125
+
126
+ export function buildHistoryTestId(serviceName, selectionType, filePath) {
127
+ return [serviceName, selectionType, normalizePath(filePath)].join("|");
128
+ }
129
+
130
+ function normalizeHistory(parsed) {
131
+ const tests = {};
132
+ for (const [id, entry] of Object.entries(parsed?.tests || {})) {
133
+ tests[id] = {
134
+ id,
135
+ path: normalizePath(entry.path || ""),
136
+ service: String(entry.service || ""),
137
+ suiteName: String(entry.suiteName || ""),
138
+ selectionType: String(entry.selectionType || ""),
139
+ framework: normalizeArtifactFramework(entry.framework || "k6"),
140
+ firstSeenAt: entry.firstSeenAt || null,
141
+ lastSeenAt: entry.lastSeenAt || null,
142
+ lastRunAt: entry.lastRunAt || null,
143
+ runCount: Number(entry.runCount || 0),
144
+ passCount: Number(entry.passCount || 0),
145
+ failCount: Number(entry.failCount || 0),
146
+ skipCount: Number(entry.skipCount || 0),
147
+ notRunCount: Number(entry.notRunCount || 0),
148
+ avgDurationMs: Number(entry.avgDurationMs || 0),
149
+ durationCount: Number(entry.durationCount || 0),
150
+ lastStatus: entry.lastStatus || null,
151
+ };
152
+ }
153
+ return {
154
+ version: HISTORY_SCHEMA_VERSION,
155
+ tests,
156
+ };
157
+ }
158
+
159
+ function normalizeArtifactFramework(value) {
160
+ if (value === "default") return "k6";
161
+ return value || "k6";
162
+ }
163
+
164
+ function normalizePath(filePath) {
165
+ return String(filePath).split(path.sep).join("/").replace(/^\.\/+/, "");
166
+ }
@@ -0,0 +1,115 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildHistoryTestId, createEmptyHistory, summarizeHistoryForFiles, updateHistoryFromRunArtifact } from "./index.mjs";
3
+
4
+ describe("test history", () => {
5
+ it("tracks first seen and pass/fail counters across runs", () => {
6
+ const history = createEmptyHistory();
7
+ const first = updateHistoryFromRunArtifact(
8
+ history,
9
+ {
10
+ generatedAt: "2026-04-30T10:00:00.000Z",
11
+ services: [
12
+ {
13
+ name: "api",
14
+ suites: [
15
+ {
16
+ name: "routes",
17
+ type: "int",
18
+ framework: "default",
19
+ files: [
20
+ {
21
+ path: "src/api/routes/__testkit__/health.int.testkit.ts",
22
+ status: "passed",
23
+ durationMs: 2000,
24
+ },
25
+ ],
26
+ },
27
+ ],
28
+ },
29
+ ],
30
+ }
31
+ );
32
+ const second = updateHistoryFromRunArtifact(
33
+ first,
34
+ {
35
+ generatedAt: "2026-05-01T10:00:00.000Z",
36
+ services: [
37
+ {
38
+ name: "api",
39
+ suites: [
40
+ {
41
+ name: "routes",
42
+ type: "int",
43
+ framework: "default",
44
+ files: [
45
+ {
46
+ path: "src/api/routes/__testkit__/health.int.testkit.ts",
47
+ status: "failed",
48
+ durationMs: 4000,
49
+ },
50
+ ],
51
+ },
52
+ ],
53
+ },
54
+ ],
55
+ }
56
+ );
57
+
58
+ const id = buildHistoryTestId("api", "int", "src/api/routes/__testkit__/health.int.testkit.ts");
59
+ expect(second.tests[id]).toMatchObject({
60
+ firstSeenAt: "2026-04-30T10:00:00.000Z",
61
+ lastSeenAt: "2026-05-01T10:00:00.000Z",
62
+ lastRunAt: "2026-05-01T10:00:00.000Z",
63
+ runCount: 2,
64
+ passCount: 1,
65
+ failCount: 1,
66
+ skipCount: 0,
67
+ avgDurationMs: 3000,
68
+ lastStatus: "failed",
69
+ });
70
+ });
71
+
72
+ it("summarizes history for discovery file entries", () => {
73
+ const history = {
74
+ version: 1,
75
+ tests: {
76
+ [buildHistoryTestId("api", "int", "src/api/routes/__testkit__/health.int.testkit.ts")]: {
77
+ id: buildHistoryTestId("api", "int", "src/api/routes/__testkit__/health.int.testkit.ts"),
78
+ path: "src/api/routes/__testkit__/health.int.testkit.ts",
79
+ service: "api",
80
+ suiteName: "routes",
81
+ selectionType: "int",
82
+ framework: "k6",
83
+ firstSeenAt: "2026-04-30T10:00:00.000Z",
84
+ lastSeenAt: "2026-05-01T10:00:00.000Z",
85
+ lastRunAt: "2026-05-01T10:00:00.000Z",
86
+ runCount: 2,
87
+ passCount: 1,
88
+ failCount: 1,
89
+ skipCount: 0,
90
+ notRunCount: 0,
91
+ avgDurationMs: 3000,
92
+ durationCount: 2,
93
+ lastStatus: "failed",
94
+ },
95
+ },
96
+ };
97
+
98
+ const summaries = summarizeHistoryForFiles(history, [
99
+ {
100
+ id: buildHistoryTestId("api", "int", "src/api/routes/__testkit__/health.int.testkit.ts"),
101
+ service: "api",
102
+ selectionType: "int",
103
+ path: "src/api/routes/__testkit__/health.int.testkit.ts",
104
+ },
105
+ ]);
106
+
107
+ expect(summaries.get(buildHistoryTestId("api", "int", "src/api/routes/__testkit__/health.int.testkit.ts"))).toMatchObject({
108
+ runCount: 2,
109
+ passCount: 1,
110
+ failCount: 1,
111
+ avgDurationMs: 3000,
112
+ lastStatus: "failed",
113
+ });
114
+ });
115
+ });
package/lib/index.d.ts CHANGED
@@ -38,6 +38,55 @@ export interface HttpSuiteContext<TSetup = unknown> {
38
38
  session: TSetup | null;
39
39
  }
40
40
 
41
+ export interface ScenarioStepResult {
42
+ name: string;
43
+ status: "passed" | "failed";
44
+ startedAt?: string;
45
+ finishedAt?: string;
46
+ durationMs?: number;
47
+ failureCount?: number;
48
+ error?: string;
49
+ }
50
+
51
+ export interface ScenarioResource<TValue = unknown> {
52
+ get(): TValue;
53
+ }
54
+
55
+ export interface ScenarioRuntime {
56
+ readonly seed: string;
57
+ readonly scenarioName: string | null;
58
+ choose<TChoice extends unknown[]>(
59
+ name: string,
60
+ choices: TChoice
61
+ ): TChoice[number];
62
+ choose<TShape extends Record<string, unknown>>(
63
+ name: string,
64
+ shape: TShape
65
+ ): TShape;
66
+ maybe(name: string, probability?: number): boolean;
67
+ note<TValue = unknown>(name: string, value: TValue): TValue;
68
+ pick<TChoice extends unknown[]>(name: string, choices: TChoice): TChoice[number];
69
+ resource<TValue = unknown>(
70
+ name: string,
71
+ factory: () => TValue,
72
+ options?: { scope?: "file" | "scenario" | "step" }
73
+ ): ScenarioResource<TValue>;
74
+ step<TValue = unknown>(name: string, fn: () => TValue): TValue;
75
+ snapshot(): {
76
+ schemaVersion: number;
77
+ seed: string;
78
+ scenarioName: string | null;
79
+ choices: Record<string, unknown>;
80
+ notes: Record<string, unknown>;
81
+ resources: Array<{ name: string; scope: "file" | "scenario" | "step" }>;
82
+ steps: ScenarioStepResult[];
83
+ };
84
+ }
85
+
86
+ export interface ScenarioSuiteContext<TSetup = unknown> extends HttpSuiteContext<TSetup> {
87
+ scenario: ScenarioRuntime;
88
+ }
89
+
41
90
  export interface HttpSuiteConfig<TSetup = unknown> {
42
91
  auth?: AuthAdapter<TSetup> | null;
43
92
  env?: RuntimeEnv;
@@ -68,6 +117,15 @@ export declare function defineHttpSuite<TSetup = unknown>(
68
117
  run: (context: HttpSuiteContext<TSetup>) => unknown
69
118
  ): TestkitSuite<TSetup>;
70
119
 
120
+ export declare function defineScenarioSuite<TSetup = unknown>(
121
+ run: (context: ScenarioSuiteContext<TSetup>) => unknown
122
+ ): TestkitSuite<TSetup>;
123
+
124
+ export declare function defineScenarioSuite<TSetup = unknown>(
125
+ config: HttpSuiteConfig<TSetup>,
126
+ run: (context: ScenarioSuiteContext<TSetup>) => unknown
127
+ ): TestkitSuite<TSetup>;
128
+
71
129
  export declare function defineDalSuite<TSetup = unknown>(
72
130
  run: (context: DalSuiteContext<TSetup>) => unknown
73
131
  ): TestkitSuite<TSetup>;
package/lib/index.mjs CHANGED
@@ -4,6 +4,9 @@ export {
4
4
  export {
5
5
  defineHttpSuite,
6
6
  } from "./runtime-src/k6/suite.js";
7
+ export {
8
+ defineScenarioSuite,
9
+ } from "./runtime-src/k6/scenario-suite.js";
7
10
 
8
11
  export function createAuthAdapter({ setup, headers } = {}) {
9
12
  return {
@@ -22,6 +22,10 @@ describe("package metadata", () => {
22
22
  types: "./lib/runtime/index.d.ts",
23
23
  default: "./lib/runtime/index.mjs",
24
24
  });
25
+ expect(packageJson.exports["./discovery"]).toEqual({
26
+ types: "./lib/discovery/index.d.ts",
27
+ default: "./lib/discovery/index.mjs",
28
+ });
25
29
  expect(packageJson.exports["./known-failures"]).toEqual({
26
30
  types: "./lib/known-failures/index.d.ts",
27
31
  default: "./lib/known-failures/index.mjs",
@@ -29,6 +33,7 @@ describe("package metadata", () => {
29
33
  expect(fs.existsSync(path.join(rootDir, "lib", "index.d.ts"))).toBe(true);
30
34
  expect(fs.existsSync(path.join(rootDir, "lib", "setup", "index.d.ts"))).toBe(true);
31
35
  expect(fs.existsSync(path.join(rootDir, "lib", "runtime", "index.d.ts"))).toBe(true);
36
+ expect(fs.existsSync(path.join(rootDir, "lib", "discovery", "index.d.ts"))).toBe(true);
32
37
  expect(fs.existsSync(path.join(rootDir, "lib", "known-failures", "index.d.ts"))).toBe(true);
33
38
  });
34
39
  });
@@ -95,7 +95,10 @@ export async function runDefaultRuntimeTask(
95
95
  env: buildTaskExecutionEnv(
96
96
  targetConfig,
97
97
  lease,
98
- buildFileTimeoutEnv(fileTimeoutSeconds, startedAt),
98
+ {
99
+ ...buildFileTimeoutEnv(fileTimeoutSeconds, startedAt),
100
+ ...(task.scenarioSeed ? { TESTKIT_SCENARIO_SEED: task.scenarioSeed } : {}),
101
+ },
99
102
  process.env
100
103
  ),
101
104
  reject: false,
@@ -52,6 +52,7 @@ import { createRuntimeManager } from "./runtime-manager.mjs";
52
52
  import { createWorker, runWorker } from "./worker-loop.mjs";
53
53
  import { findUnmatchedRequestedFiles, isFullRunSelection } from "./selection.mjs";
54
54
  import { uploadTelemetryArtifact } from "../telemetry/index.mjs";
55
+ import { loadHistory, saveHistory, updateHistoryFromRunArtifact } from "../history/index.mjs";
55
56
 
56
57
  export async function runAll(configs, typeValues, suiteSelectors, opts, allConfigs = configs) {
57
58
  const configMap = new Map(allConfigs.map((config) => [config.name, config]));
@@ -146,6 +147,7 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
146
147
  fileNames: requestedFiles,
147
148
  shard: opts.shard || null,
148
149
  serviceFilter: opts.serviceFilter || null,
150
+ scenarioSeed: opts.scenarioSeed || null,
149
151
  metadata,
150
152
  summarizeDbBackend,
151
153
  serviceLogs: logRegistry.listServiceLogs(),
@@ -173,6 +175,9 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
173
175
  const timings = loadTimings(productDir);
174
176
  const graphs = buildRuntimeGraphs(executedPlans);
175
177
  const queue = buildTaskQueue(executedPlans, graphs, timings);
178
+ for (const task of queue) {
179
+ task.scenarioSeed = opts.scenarioSeed || null;
180
+ }
176
181
  workerCount = Math.max(1, Math.min(execution.workers, queue.length));
177
182
  runtimeInstanceCount = graphs.reduce((sum, graph) => sum + graph.instanceCount, 0);
178
183
  const workers = Array.from({ length: workerCount }, (_unused, index) =>
@@ -247,10 +252,11 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
247
252
  runtimeStats,
248
253
  typeValues,
249
254
  suiteSelectors,
250
- fileNames: requestedFiles,
251
- shard: opts.shard || null,
252
- serviceFilter: opts.serviceFilter || null,
253
- metadata,
255
+ fileNames: requestedFiles,
256
+ shard: opts.shard || null,
257
+ serviceFilter: opts.serviceFilter || null,
258
+ scenarioSeed: opts.scenarioSeed || null,
259
+ metadata,
254
260
  summarizeDbBackend,
255
261
  serviceLogs: logRegistry.listServiceLogs(),
256
262
  setupLogs: logRegistry.listSetupLogs(),
@@ -262,10 +268,11 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
262
268
  results,
263
269
  typeValues,
264
270
  suiteSelectors,
265
- fileNames: requestedFiles,
266
- shard: opts.shard || null,
267
- serviceFilter: opts.serviceFilter || null,
268
- metadata,
271
+ fileNames: requestedFiles,
272
+ shard: opts.shard || null,
273
+ serviceFilter: opts.serviceFilter || null,
274
+ scenarioSeed: opts.scenarioSeed || null,
275
+ metadata,
269
276
  })
270
277
  : null;
271
278
  const enrichedArtifacts = applyKnownFailuresToArtifacts(
@@ -296,6 +303,12 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
296
303
  if (opts.writeStatus) {
297
304
  writeStatusArtifact(productDir, enrichedArtifacts.statusArtifact);
298
305
  }
306
+ const nextHistory = updateHistoryFromRunArtifact(
307
+ loadHistory(productDir),
308
+ enrichedArtifacts.runArtifact,
309
+ enrichedArtifacts.runArtifact.generatedAt
310
+ );
311
+ saveHistory(productDir, nextHistory);
299
312
 
300
313
  reporter?.runSummary?.(results, finishedAt - startedAt, knownFailureIssueValidation);
301
314
  await reportTelemetry(telemetry, enrichedArtifacts.runArtifact, reporter);