@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.
@@ -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: { db: RuntimeDb; dal: RuntimeDalContext }) => TSetup;
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 defineDalSuite<TSetup = unknown>(
154
- run: (context: DalSuiteContext<TSetup>) => unknown
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
@@ -1,6 +1,9 @@
1
1
  export {
2
2
  defineDalSuite,
3
3
  } from "./runtime-src/k6/dal-suite.js";
4
+ export {
5
+ defineDalFixtures,
6
+ } from "./runtime-src/k6/dal-fixtures.js";
4
7
  export {
5
8
  defineHttpSuite,
6
9
  } from "./runtime-src/k6/suite.js";
@@ -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
- return config.setup({ db, dal });
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
  },