@elench/testkit 0.1.85 → 0.1.87
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 +43 -10
- package/lib/cli/commands/inspect.mjs +124 -0
- package/lib/cli/entrypoint.mjs +2 -4
- package/lib/cli/presentation/tree-reporter.mjs +34 -28
- package/lib/cli/tui/detail-pane.mjs +161 -0
- package/lib/cli/tui/filter-bar.mjs +12 -0
- package/lib/cli/tui/fuzzy-match.mjs +106 -0
- package/lib/cli/tui/inspect-app.mjs +306 -0
- package/lib/cli/tui/inspect-artifact-adapter.mjs +3 -0
- package/lib/cli/tui/inspect-live-adapter.mjs +15 -0
- package/lib/cli/tui/inspect-model.mjs +817 -0
- package/lib/cli/tui/inspect-state.mjs +321 -0
- package/lib/index.d.ts +30 -8
- package/lib/index.mjs +3 -0
- package/lib/runtime/index.d.ts +77 -0
- package/lib/runtime-src/k6/dal-fixtures.js +66 -0
- package/lib/runtime-src/k6/dal-suite.js +21 -1
- package/lib/runtime-src/shared/fixture-engine.mjs +320 -0
- 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/lib/cli/commands/artifacts.mjs +0 -45
- package/lib/cli/commands/logs.mjs +0 -47
- package/lib/cli/commands/show.mjs +0 -47
- package/lib/cli/commands/watch.mjs +0 -23
- package/lib/cli/tui/run-app.mjs +0 -1
- package/lib/cli/tui/run-session-app.mjs +0 -432
- package/lib/cli/tui/run-session-state.mjs +0 -505
- package/lib/cli/tui/run-tree-state.mjs +0 -1
- package/lib/cli/tui/watch-app.mjs +0 -220
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyArtifactToModel,
|
|
3
|
+
buildSnapshot,
|
|
4
|
+
createEmptyInspectModel,
|
|
5
|
+
cyclePane,
|
|
6
|
+
findEntryIdForFile,
|
|
7
|
+
findEntryIdForService,
|
|
8
|
+
finishModel,
|
|
9
|
+
initModelFromPlans,
|
|
10
|
+
markFileFinished,
|
|
11
|
+
markFileRunning,
|
|
12
|
+
markPlannedSkip,
|
|
13
|
+
markRuntimeError,
|
|
14
|
+
markServiceSkipped,
|
|
15
|
+
resetInspectModel,
|
|
16
|
+
revealEntry,
|
|
17
|
+
setPane,
|
|
18
|
+
setPhase,
|
|
19
|
+
setRegressionCatalog,
|
|
20
|
+
setTotalFileCount,
|
|
21
|
+
toggleCollapsed,
|
|
22
|
+
updateFilter,
|
|
23
|
+
} from "./inspect-model.mjs";
|
|
24
|
+
|
|
25
|
+
export function createInspectState({ dataSource = "live" } = {}) {
|
|
26
|
+
const model = createEmptyInspectModel(dataSource);
|
|
27
|
+
let mode = dataSource === "artifact" ? "complete" : "running";
|
|
28
|
+
let notice = null;
|
|
29
|
+
let agentSession = null;
|
|
30
|
+
const listeners = new Set();
|
|
31
|
+
|
|
32
|
+
function notify() {
|
|
33
|
+
for (const callback of listeners) callback();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function appendTranscriptEntry(kind, text) {
|
|
37
|
+
if (!agentSession || !text) return;
|
|
38
|
+
const normalizedText = String(text);
|
|
39
|
+
const entries = agentSession.transcriptEntries || [];
|
|
40
|
+
const lastEntry = entries.at(-1);
|
|
41
|
+
if (kind === "assistant" && lastEntry?.kind === "assistant") {
|
|
42
|
+
lastEntry.text += normalizedText;
|
|
43
|
+
agentSession.updatedAt = Date.now();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
entries.push({ kind, text: normalizedText });
|
|
47
|
+
agentSession.transcriptEntries = entries;
|
|
48
|
+
agentSession.updatedAt = Date.now();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function finishAgentSession(status, extra = {}) {
|
|
52
|
+
if (!agentSession) return;
|
|
53
|
+
agentSession = {
|
|
54
|
+
...agentSession,
|
|
55
|
+
...extra,
|
|
56
|
+
status,
|
|
57
|
+
endedAt: Date.now(),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function moveCursor(delta) {
|
|
62
|
+
const snapshot = getSnapshot();
|
|
63
|
+
if (snapshot.visibleEntries.length === 0) return;
|
|
64
|
+
const currentIndex = snapshot.visibleEntries.findIndex((entry) => entry.id === snapshot.selectedEntryId);
|
|
65
|
+
const nextIndex = clampIndex((currentIndex < 0 ? 0 : currentIndex) + delta, snapshot.visibleEntries.length);
|
|
66
|
+
model.selectedEntryId = snapshot.visibleEntries[nextIndex].id;
|
|
67
|
+
notify();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getSnapshot() {
|
|
71
|
+
return {
|
|
72
|
+
...buildSnapshot(model),
|
|
73
|
+
mode,
|
|
74
|
+
notice,
|
|
75
|
+
agentSession,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
initFromPlans(servicePlans) {
|
|
81
|
+
initModelFromPlans(model, servicePlans);
|
|
82
|
+
notify();
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
hydrateFromArtifact(artifact) {
|
|
86
|
+
applyArtifactToModel(model, artifact);
|
|
87
|
+
mode = model.finished ? "complete" : "running";
|
|
88
|
+
notify();
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
resetForLive() {
|
|
92
|
+
resetInspectModel(model, "live");
|
|
93
|
+
mode = "running";
|
|
94
|
+
notice = null;
|
|
95
|
+
agentSession = null;
|
|
96
|
+
notify();
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
setRegressionCatalog(document) {
|
|
100
|
+
setRegressionCatalog(model, document);
|
|
101
|
+
notify();
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
markFileRunning(serviceName, suiteKey, filePath) {
|
|
105
|
+
markFileRunning(model, serviceName, suiteKey, filePath);
|
|
106
|
+
notify();
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
markFileFinished(task, outcome) {
|
|
110
|
+
markFileFinished(model, task, outcome);
|
|
111
|
+
notify();
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
markServiceSkipped(serviceName, reason) {
|
|
115
|
+
markServiceSkipped(model, serviceName, reason);
|
|
116
|
+
notify();
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
markPlannedSkip(entry) {
|
|
120
|
+
markPlannedSkip(model, entry);
|
|
121
|
+
notify();
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
markRuntimeError(task, message) {
|
|
125
|
+
markRuntimeError(model, task, message);
|
|
126
|
+
notify();
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
setTotalFileCount(count) {
|
|
130
|
+
setTotalFileCount(model, count);
|
|
131
|
+
notify();
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
setPhase(label) {
|
|
135
|
+
setPhase(model, label);
|
|
136
|
+
notify();
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
finish(results, durationMs, regressionReport) {
|
|
140
|
+
finishModel(model, results, durationMs, regressionReport);
|
|
141
|
+
mode = "complete";
|
|
142
|
+
notify();
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
setNotice(message) {
|
|
146
|
+
notice = message ? String(message) : null;
|
|
147
|
+
notify();
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
clearNotice() {
|
|
151
|
+
if (!notice) return;
|
|
152
|
+
notice = null;
|
|
153
|
+
notify();
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
moveCursorUp() {
|
|
157
|
+
moveCursor(-1);
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
moveCursorDown() {
|
|
161
|
+
moveCursor(1);
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
moveCursorToEntry(entryId) {
|
|
165
|
+
model.selectedEntryId = entryId || null;
|
|
166
|
+
notify();
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
toggleExpand() {
|
|
170
|
+
const snapshot = getSnapshot();
|
|
171
|
+
if (!snapshot.selectedEntry) return;
|
|
172
|
+
toggleCollapsed(model, snapshot.selectedEntry.id);
|
|
173
|
+
notify();
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
activateFilter() {
|
|
177
|
+
model.filterActive = true;
|
|
178
|
+
notify();
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
updateFilterQuery(query) {
|
|
182
|
+
updateFilter(model, query);
|
|
183
|
+
const snapshot = buildSnapshot(model);
|
|
184
|
+
if (snapshot.filter.results.length > 0) {
|
|
185
|
+
model.selectedEntryId = snapshot.filter.results[0].id;
|
|
186
|
+
}
|
|
187
|
+
notify();
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
deactivateFilter() {
|
|
191
|
+
model.filterActive = false;
|
|
192
|
+
model.filterQuery = "";
|
|
193
|
+
model.filterMatches = new Map();
|
|
194
|
+
notify();
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
revealFile(serviceName, filePath) {
|
|
198
|
+
const entryId = findEntryIdForFile(model, serviceName, filePath);
|
|
199
|
+
if (!entryId) return false;
|
|
200
|
+
revealEntry(model, entryId);
|
|
201
|
+
notify();
|
|
202
|
+
return true;
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
revealService(serviceName) {
|
|
206
|
+
const entryId = findEntryIdForService(model, serviceName);
|
|
207
|
+
if (!entryId) return false;
|
|
208
|
+
revealEntry(model, entryId);
|
|
209
|
+
notify();
|
|
210
|
+
return true;
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
cyclePaneMode() {
|
|
214
|
+
cyclePane(model);
|
|
215
|
+
notify();
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
setPaneMode(paneMode) {
|
|
219
|
+
setPane(model, paneMode);
|
|
220
|
+
notify();
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
beginInvestigation({ provider, userMessage } = {}) {
|
|
224
|
+
mode = "investigating";
|
|
225
|
+
notice = null;
|
|
226
|
+
agentSession = {
|
|
227
|
+
provider: provider || "auto",
|
|
228
|
+
userMessage: userMessage || "",
|
|
229
|
+
status: "starting",
|
|
230
|
+
startedAt: Date.now(),
|
|
231
|
+
updatedAt: Date.now(),
|
|
232
|
+
rawEvents: [],
|
|
233
|
+
transcriptEntries: [],
|
|
234
|
+
timeline: [],
|
|
235
|
+
summary: null,
|
|
236
|
+
activePhase: "planning",
|
|
237
|
+
activeStep: null,
|
|
238
|
+
viewMode: "summary",
|
|
239
|
+
};
|
|
240
|
+
notify();
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
recordInvestigationProgress(event, presentation = null) {
|
|
244
|
+
if (!agentSession || !event) return;
|
|
245
|
+
agentSession.rawEvents.push({ ...event });
|
|
246
|
+
if (event.type === "start") {
|
|
247
|
+
agentSession.status = "running";
|
|
248
|
+
} else if (event.type === "delta") {
|
|
249
|
+
appendTranscriptEntry("assistant", event.text || "");
|
|
250
|
+
} else if (event.type === "final") {
|
|
251
|
+
if (event.text && !(agentSession.transcriptEntries || []).some((entry) => entry.kind === "assistant")) {
|
|
252
|
+
appendTranscriptEntry("assistant", event.text);
|
|
253
|
+
}
|
|
254
|
+
agentSession.finalText = event.text || agentSession.finalText || "";
|
|
255
|
+
} else if (event.type === "tool") {
|
|
256
|
+
appendTranscriptEntry("tool", event.detail ? `${event.name}: ${event.detail}` : event.name);
|
|
257
|
+
} else if (event.type === "status") {
|
|
258
|
+
appendTranscriptEntry("status", event.message || "");
|
|
259
|
+
} else if (event.type === "error") {
|
|
260
|
+
appendTranscriptEntry("error", event.message || "Agent error");
|
|
261
|
+
} else if (event.type === "exit") {
|
|
262
|
+
agentSession.exitCode = event.code;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (presentation) {
|
|
266
|
+
agentSession.activePhase = presentation.phase || agentSession.activePhase || null;
|
|
267
|
+
agentSession.activeStep = presentation.activeStep || null;
|
|
268
|
+
agentSession.timeline = presentation.timeline || [];
|
|
269
|
+
agentSession.summary = presentation.summary || null;
|
|
270
|
+
}
|
|
271
|
+
notify();
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
toggleInvestigationViewMode() {
|
|
275
|
+
if (!agentSession) return;
|
|
276
|
+
agentSession.viewMode = agentSession.viewMode === "summary" ? "transcript" : "summary";
|
|
277
|
+
notify();
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
completeAgentSession(result = {}) {
|
|
281
|
+
finishAgentSession("complete", {
|
|
282
|
+
finalText: result.finalText || agentSession?.finalText || "",
|
|
283
|
+
exitCode: result.exitCode ?? agentSession?.exitCode ?? 0,
|
|
284
|
+
});
|
|
285
|
+
notify();
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
failAgentSession(error) {
|
|
289
|
+
finishAgentSession("error", {
|
|
290
|
+
error: error instanceof Error ? error.message : String(error || "Agent error"),
|
|
291
|
+
});
|
|
292
|
+
notify();
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
cancelAgentSession(message = "Cancelled investigation.") {
|
|
296
|
+
finishAgentSession("cancelled");
|
|
297
|
+
mode = "complete";
|
|
298
|
+
notice = message;
|
|
299
|
+
notify();
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
returnToSummary() {
|
|
303
|
+
mode = "complete";
|
|
304
|
+
notify();
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
subscribe(callback) {
|
|
308
|
+
listeners.add(callback);
|
|
309
|
+
return () => listeners.delete(callback);
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
getSnapshot,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function clampIndex(index, length) {
|
|
317
|
+
if (length <= 0) return 0;
|
|
318
|
+
if (index < 0) return 0;
|
|
319
|
+
if (index >= length) return length - 1;
|
|
320
|
+
return index;
|
|
321
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ActorRequestClient,
|
|
3
|
+
DalFixtureDefinition,
|
|
3
4
|
HttpClient,
|
|
4
5
|
HttpClientConfig,
|
|
5
6
|
RawRequestClient,
|
|
6
7
|
RuntimeDb,
|
|
7
8
|
RuntimeDalContext,
|
|
8
9
|
RuntimeEnv,
|
|
10
|
+
RuntimeFixtureScope,
|
|
9
11
|
RuntimeHeaders,
|
|
10
12
|
RuntimeOptions,
|
|
11
13
|
RuntimeResponse,
|
|
@@ -120,16 +122,24 @@ export interface HttpSuiteConfig<TSession = unknown> {
|
|
|
120
122
|
options?: RuntimeOptions;
|
|
121
123
|
}
|
|
122
124
|
|
|
123
|
-
export interface DalSuiteContext<TSetup = unknown
|
|
125
|
+
export interface DalSuiteContext<TSetup = unknown, TFixtures = Record<string, unknown>> {
|
|
124
126
|
db: RuntimeDb;
|
|
125
127
|
dal: RuntimeDalContext;
|
|
128
|
+
fixtureScope: RuntimeFixtureScope;
|
|
129
|
+
fixtures: TFixtures;
|
|
126
130
|
setupData: TSetup | null;
|
|
127
131
|
}
|
|
128
132
|
|
|
129
|
-
export interface DalSuiteConfig<TSetup = unknown
|
|
133
|
+
export interface DalSuiteConfig<TSetup = unknown, TFixtures = Record<string, unknown>> {
|
|
130
134
|
db?: RuntimeDb;
|
|
135
|
+
fixtures?: DalFixtureDefinition<TFixtures> | null;
|
|
131
136
|
options?: RuntimeOptions;
|
|
132
|
-
setup?: (context: {
|
|
137
|
+
setup?: (context: {
|
|
138
|
+
db: RuntimeDb;
|
|
139
|
+
dal: RuntimeDalContext;
|
|
140
|
+
fixtureScope: RuntimeFixtureScope;
|
|
141
|
+
fixtures: TFixtures;
|
|
142
|
+
}) => TSetup;
|
|
133
143
|
}
|
|
134
144
|
|
|
135
145
|
export declare function defineHttpSuite<TSetup = unknown>(
|
|
@@ -150,13 +160,23 @@ export declare function defineScenarioSuite<TSetup = unknown>(
|
|
|
150
160
|
run: (context: ScenarioSuiteContext<TSetup>) => unknown
|
|
151
161
|
): TestkitSuite<TSetup>;
|
|
152
162
|
|
|
153
|
-
export declare function
|
|
154
|
-
|
|
163
|
+
export declare function defineDalFixtures<TFixtures = Record<string, unknown>>(
|
|
164
|
+
factory: (context: {
|
|
165
|
+
db: RuntimeDb;
|
|
166
|
+
dal: RuntimeDalContext;
|
|
167
|
+
fixtureScope: RuntimeFixtureScope;
|
|
168
|
+
seed: RuntimeFixtureScope["seed"];
|
|
169
|
+
values: RuntimeFixtureScope["values"];
|
|
170
|
+
}) => TFixtures
|
|
171
|
+
): DalFixtureDefinition<TFixtures>;
|
|
172
|
+
|
|
173
|
+
export declare function defineDalSuite<TSetup = unknown, TFixtures = Record<string, unknown>>(
|
|
174
|
+
run: (context: DalSuiteContext<TSetup, TFixtures>) => unknown
|
|
155
175
|
): TestkitSuite<TSetup>;
|
|
156
176
|
|
|
157
|
-
export declare function defineDalSuite<TSetup = unknown
|
|
158
|
-
config: DalSuiteConfig<TSetup>,
|
|
159
|
-
run: (context: DalSuiteContext<TSetup>) => unknown
|
|
177
|
+
export declare function defineDalSuite<TSetup = unknown, TFixtures = Record<string, unknown>>(
|
|
178
|
+
config: DalSuiteConfig<TSetup, TFixtures>,
|
|
179
|
+
run: (context: DalSuiteContext<TSetup, TFixtures>) => unknown
|
|
160
180
|
): TestkitSuite<TSetup>;
|
|
161
181
|
|
|
162
182
|
export declare function createAuthAdapter<TSetup = unknown>(
|
|
@@ -164,11 +184,13 @@ export declare function createAuthAdapter<TSetup = unknown>(
|
|
|
164
184
|
): AuthAdapter<TSetup>;
|
|
165
185
|
|
|
166
186
|
export type {
|
|
187
|
+
DalFixtureDefinition,
|
|
167
188
|
HttpClient,
|
|
168
189
|
HttpClientConfig,
|
|
169
190
|
RuntimeDb,
|
|
170
191
|
RuntimeDalContext,
|
|
171
192
|
RuntimeEnv,
|
|
193
|
+
RuntimeFixtureScope,
|
|
172
194
|
RuntimeHeaders,
|
|
173
195
|
RuntimeOptions,
|
|
174
196
|
RuntimeResponse,
|
package/lib/index.mjs
CHANGED
package/lib/runtime/index.d.ts
CHANGED
|
@@ -82,6 +82,83 @@ export interface RuntimeDalContext {
|
|
|
82
82
|
truncate(...tables: string[]): void;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
export interface RuntimeFixtureLocation {
|
|
86
|
+
column: number;
|
|
87
|
+
line: number;
|
|
88
|
+
path: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface RuntimeFixtureReference {
|
|
92
|
+
key: string;
|
|
93
|
+
kind: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface RuntimeFixtureRecord {
|
|
97
|
+
dependencies: RuntimeFixtureReference[];
|
|
98
|
+
key: string;
|
|
99
|
+
kind: string;
|
|
100
|
+
location?: RuntimeFixtureLocation;
|
|
101
|
+
order: number;
|
|
102
|
+
reuseCount: number;
|
|
103
|
+
signature: unknown;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface RuntimeFixtureStringOptions {
|
|
107
|
+
fallback?: string;
|
|
108
|
+
maxLength?: number;
|
|
109
|
+
prefix?: string;
|
|
110
|
+
suffixLength?: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface RuntimeFixtureEmailOptions {
|
|
114
|
+
domain?: string;
|
|
115
|
+
fallback?: string;
|
|
116
|
+
suffixLength?: number;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface RuntimeFixtureSlugOptions {
|
|
120
|
+
fallback?: string;
|
|
121
|
+
maxLength?: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface RuntimeFixtureValueHelpers {
|
|
125
|
+
email(label: string, options?: RuntimeFixtureEmailOptions): string;
|
|
126
|
+
slug(label: string, options?: RuntimeFixtureSlugOptions): string;
|
|
127
|
+
string(label: string, options?: RuntimeFixtureStringOptions): string;
|
|
128
|
+
token(label: string, options?: RuntimeFixtureStringOptions): string;
|
|
129
|
+
uuid(label: string): string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface RuntimeFixtureScope {
|
|
133
|
+
email(label: string, options?: RuntimeFixtureEmailOptions): string;
|
|
134
|
+
id(label: string): string;
|
|
135
|
+
namespace: string;
|
|
136
|
+
records(): RuntimeFixtureRecord[];
|
|
137
|
+
seed<TResult>(
|
|
138
|
+
kind: string,
|
|
139
|
+
key: string,
|
|
140
|
+
signature: unknown,
|
|
141
|
+
create: () => TResult
|
|
142
|
+
): TResult;
|
|
143
|
+
slug(label: string, options?: RuntimeFixtureSlugOptions): string;
|
|
144
|
+
string(label: string, options?: RuntimeFixtureStringOptions): string;
|
|
145
|
+
token(label: string, options?: RuntimeFixtureStringOptions): string;
|
|
146
|
+
uuid(label: string): string;
|
|
147
|
+
values: RuntimeFixtureValueHelpers;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface DalFixtureDefinitionContext {
|
|
151
|
+
dal: RuntimeDalContext;
|
|
152
|
+
db: RuntimeDb;
|
|
153
|
+
fixtureScope: RuntimeFixtureScope;
|
|
154
|
+
seed: RuntimeFixtureScope["seed"];
|
|
155
|
+
values: RuntimeFixtureValueHelpers;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface DalFixtureDefinition<TFixtures = Record<string, unknown>> {
|
|
159
|
+
create(context: DalFixtureDefinitionContext): TFixtures;
|
|
160
|
+
}
|
|
161
|
+
|
|
85
162
|
export interface HttpRequestParams {
|
|
86
163
|
headers?: RuntimeHeaders;
|
|
87
164
|
redirects?: number;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { emitArtifact } from "./artifacts.js";
|
|
2
|
+
import { createFixtureScope } from "../shared/fixture-engine.mjs";
|
|
3
|
+
import { readTestkitContext } from "../../shared/test-context.mjs";
|
|
4
|
+
|
|
5
|
+
export function defineDalFixtures(factory) {
|
|
6
|
+
if (typeof factory !== "function") {
|
|
7
|
+
throw new Error("defineDalFixtures(factory) requires a fixture factory function.");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
create: factory,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createDalFixtureRuntime({ db, dal, definition = null, env = __ENV } = {}) {
|
|
16
|
+
const runtimeContext = readTestkitContext(env);
|
|
17
|
+
const fixtureScope = createFixtureScope({
|
|
18
|
+
namespace: `dal-${runtimeContext.namespace}`,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const fixtureDefinition = normalizeFixtureDefinition(definition);
|
|
22
|
+
const fixtures = fixtureDefinition
|
|
23
|
+
? fixtureDefinition.create({
|
|
24
|
+
db,
|
|
25
|
+
dal,
|
|
26
|
+
fixtureScope,
|
|
27
|
+
seed: fixtureScope.seed,
|
|
28
|
+
values: fixtureScope.values,
|
|
29
|
+
}) || {}
|
|
30
|
+
: {};
|
|
31
|
+
|
|
32
|
+
let artifactEmitted = false;
|
|
33
|
+
|
|
34
|
+
function emitArtifactOnce() {
|
|
35
|
+
if (artifactEmitted) return;
|
|
36
|
+
const records = fixtureScope.records();
|
|
37
|
+
if (records.length === 0) return;
|
|
38
|
+
|
|
39
|
+
emitArtifact(
|
|
40
|
+
"dal-fixtures",
|
|
41
|
+
{
|
|
42
|
+
namespace: fixtureScope.namespace,
|
|
43
|
+
fixtures: records,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
kind: "testkit.dal-fixtures",
|
|
47
|
+
summary: `${records.length} DAL fixture(s)`,
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
artifactEmitted = true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
fixtures,
|
|
55
|
+
fixtureScope,
|
|
56
|
+
emitArtifactOnce,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeFixtureDefinition(definition) {
|
|
61
|
+
if (!definition) return null;
|
|
62
|
+
if (typeof definition.create !== "function") {
|
|
63
|
+
throw new Error("DAL fixture definitions must be created with defineDalFixtures(...).");
|
|
64
|
+
}
|
|
65
|
+
return definition;
|
|
66
|
+
}
|
|
@@ -6,23 +6,40 @@ import {
|
|
|
6
6
|
startFailureCollection,
|
|
7
7
|
} from "./checks.js";
|
|
8
8
|
import { createDalContext, openDb } from "./dal.js";
|
|
9
|
+
import { createDalFixtureRuntime } from "./dal-fixtures.js";
|
|
9
10
|
|
|
10
11
|
export function defineDalSuite(configOrRun, maybeRun) {
|
|
11
12
|
const { config, run } = normalizeSuiteArgs(configOrRun, maybeRun);
|
|
12
13
|
const db = config.db || openDb();
|
|
13
14
|
const dal = createDalContext(db);
|
|
15
|
+
const fixtureRuntime = createDalFixtureRuntime({
|
|
16
|
+
db,
|
|
17
|
+
dal,
|
|
18
|
+
definition: config.fixtures || null,
|
|
19
|
+
});
|
|
14
20
|
|
|
15
21
|
return {
|
|
16
22
|
options: config.options || defaultOptions,
|
|
17
23
|
setup() {
|
|
18
24
|
if (typeof config.setup !== "function") return null;
|
|
19
25
|
startFailureCollection("setup");
|
|
26
|
+
let setupCompleted = false;
|
|
20
27
|
try {
|
|
21
|
-
|
|
28
|
+
const result = config.setup({
|
|
29
|
+
db,
|
|
30
|
+
dal,
|
|
31
|
+
fixtures: fixtureRuntime.fixtures,
|
|
32
|
+
fixtureScope: fixtureRuntime.fixtureScope,
|
|
33
|
+
});
|
|
34
|
+
setupCompleted = true;
|
|
35
|
+
return result;
|
|
22
36
|
} catch (error) {
|
|
23
37
|
recordRuntimeFailure();
|
|
24
38
|
fail(formatFatalSuiteError("setup", error));
|
|
25
39
|
} finally {
|
|
40
|
+
if (!setupCompleted) {
|
|
41
|
+
fixtureRuntime.emitArtifactOnce();
|
|
42
|
+
}
|
|
26
43
|
emitFailureCollectionArtifact();
|
|
27
44
|
}
|
|
28
45
|
},
|
|
@@ -32,12 +49,15 @@ export function defineDalSuite(configOrRun, maybeRun) {
|
|
|
32
49
|
return run({
|
|
33
50
|
db,
|
|
34
51
|
dal,
|
|
52
|
+
fixtures: fixtureRuntime.fixtures,
|
|
53
|
+
fixtureScope: fixtureRuntime.fixtureScope,
|
|
35
54
|
setupData,
|
|
36
55
|
});
|
|
37
56
|
} catch (error) {
|
|
38
57
|
recordRuntimeFailure();
|
|
39
58
|
fail(formatFatalSuiteError("exec", error));
|
|
40
59
|
} finally {
|
|
60
|
+
fixtureRuntime.emitArtifactOnce();
|
|
41
61
|
emitFailureCollectionArtifact();
|
|
42
62
|
}
|
|
43
63
|
},
|