@elench/testkit 0.1.111 → 0.1.113

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.
Files changed (43) hide show
  1. package/README.md +1 -1
  2. package/lib/bundler/index.mjs +95 -1
  3. package/lib/cli/args.mjs +1 -1
  4. package/lib/cli/assistant/app.mjs +70 -20
  5. package/lib/cli/assistant/command-normalize.mjs +22 -0
  6. package/lib/cli/assistant/command-observer.mjs +49 -4
  7. package/lib/cli/assistant/command-results.mjs +10 -1
  8. package/lib/cli/assistant/context-pack.mjs +45 -15
  9. package/lib/cli/assistant/domain.d.mts +59 -0
  10. package/lib/cli/assistant/domain.d.mts.map +1 -0
  11. package/lib/cli/assistant/domain.mjs +2 -0
  12. package/lib/cli/assistant/domain.mjs.map +1 -0
  13. package/lib/cli/assistant/session.mjs +3 -1
  14. package/lib/cli/assistant/state.mjs +109 -2
  15. package/lib/cli/assistant/view-model.mjs +69 -9
  16. package/lib/cli/commands/run.mjs +1 -1
  17. package/lib/cli/components/blocks/run-tree.mjs +30 -64
  18. package/lib/cli/entrypoint.mjs +1 -1
  19. package/lib/cli/renderers/run/inline-detail.mjs +64 -0
  20. package/lib/cli/state/run/model.mjs +24 -95
  21. package/lib/cli/state/run/state.mjs +0 -22
  22. package/lib/config/discovery.mjs +0 -10
  23. package/lib/discovery/index.mjs +1 -1
  24. package/lib/domain/test-types.mjs +5 -14
  25. package/lib/runner/default-runtime-runner.mjs +3 -1
  26. package/lib/runner/failure-details.mjs +22 -0
  27. package/lib/runner/maintenance.mjs +1 -1
  28. package/lib/runner/provenance.mjs +4 -1
  29. package/lib/runner/results.mjs +31 -0
  30. package/lib/runner/status-model.mjs +15 -7
  31. package/lib/runner/suite-selection.mjs +2 -3
  32. package/node_modules/@elench/next-analysis/package.json +1 -1
  33. package/node_modules/@elench/testkit-bridge/package.json +2 -2
  34. package/node_modules/@elench/testkit-protocol/package.json +1 -1
  35. package/node_modules/@elench/ts-analysis/package.json +1 -1
  36. package/package.json +5 -5
  37. package/lib/cli/components/primitives/filter-bar.mjs +0 -12
  38. package/lib/cli/state/tree/fuzzy-match.mjs +0 -106
  39. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.d.ts +0 -188
  40. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.d.ts.map +0 -1
  41. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.js +0 -293
  42. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.js.map +0 -1
  43. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/package.json +0 -25
@@ -1,7 +1,6 @@
1
1
  import { fileDisplayName } from "../../../discovery/index.mjs";
2
2
  import { suiteSelectionType } from "../../../runner/suite-selection.mjs";
3
3
  import { formatDuration } from "../../../runner/formatting.mjs";
4
- import { matchRunTreeEntry } from "../tree/fuzzy-match.mjs";
5
4
 
6
5
  export function buildSummaryRows({
7
6
  result,
@@ -51,9 +50,6 @@ export function createEmptyRunModel(dataSource = "live", options = {}) {
51
50
  totalCount: 0,
52
51
  completedCount: 0,
53
52
  phase: null,
54
- filterActive: false,
55
- filterQuery: "",
56
- filterMatches: new Map(),
57
53
  collapsedOverrides: new Map(),
58
54
  selectedEntryId: null,
59
55
  };
@@ -69,9 +65,6 @@ export function resetRunModel(model, dataSource = model.dataSource) {
69
65
  model.totalCount = 0;
70
66
  model.completedCount = 0;
71
67
  model.phase = null;
72
- model.filterActive = false;
73
- model.filterQuery = "";
74
- model.filterMatches = new Map();
75
68
  model.collapsedOverrides = new Map();
76
69
  model.selectedEntryId = null;
77
70
  }
@@ -111,6 +104,7 @@ export function initModelFromPlans(model, servicePlans) {
111
104
  diagnosis: null,
112
105
  skipReason: null,
113
106
  artifacts: [],
107
+ checkDetails: [],
114
108
  });
115
109
  }
116
110
  }
@@ -175,6 +169,7 @@ export function applyArtifactToModel(model, artifact) {
175
169
  diagnosis: fileResult.diagnosis || null,
176
170
  skipReason: fileResult.reason || null,
177
171
  artifacts: Array.isArray(fileResult.artifacts) ? fileResult.artifacts : [],
172
+ checkDetails: Array.isArray(fileResult.checkDetails) ? fileResult.checkDetails : [],
178
173
  });
179
174
  }
180
175
  }
@@ -210,7 +205,10 @@ export function markFileFinished(model, task, outcome) {
210
205
  file.status = "passed";
211
206
  file.error = null;
212
207
  file.failureDetails = [];
208
+ file.artifacts = Array.isArray(outcome.artifacts) ? outcome.artifacts : file.artifacts;
209
+ file.diagnosis = outcome.diagnosis || null;
213
210
  }
211
+ file.checkDetails = Array.isArray(outcome.checkDetails) ? outcome.checkDetails : file.checkDetails;
214
212
  file.durationMs = outcome.durationMs || null;
215
213
  model.completedCount += 1;
216
214
  if (file.status === "failed" && (!selectedEntry || selectedEntry.kind !== "file" || selectedEntry.status !== "failed")) {
@@ -277,23 +275,9 @@ export function finishModel(model, results, durationMs, regressionReport) {
277
275
  model.selectedEntryId = findFirstFailureEntryId(model) || model.selectedEntryId || findFirstNavigableEntryId(model);
278
276
  }
279
277
 
280
- export function updateFilter(model, query) {
281
- model.filterActive = true;
282
- model.filterQuery = String(query || "");
283
- model.filterMatches = new Map();
284
- const normalizedQuery = model.filterQuery.trim();
285
- if (!normalizedQuery) return;
286
- for (const entry of collectAllEntries(model)) {
287
- const match = matchRunTreeEntry(normalizedQuery, entry);
288
- if (match.matched) {
289
- model.filterMatches.set(entry.id, match);
290
- }
291
- }
292
- }
293
-
294
278
  export function toggleCollapsed(model, entryId) {
295
279
  const entry = getEntryById(model, entryId);
296
- if (!entry || (entry.kind !== "type" && entry.kind !== "suite")) return;
280
+ if (!entry || (entry.kind !== "type" && entry.kind !== "suite" && entry.kind !== "file")) return;
297
281
  const current = isCollapsed(model, entry);
298
282
  model.collapsedOverrides.set(entryId, !current);
299
283
  }
@@ -317,9 +301,6 @@ export function buildSnapshot(model) {
317
301
  || null;
318
302
  const selectedEntryId = selectedEntry?.id || null;
319
303
  const selectedFailure = toSelectedFailure(selectedEntry);
320
- const filterResults = [...model.filterMatches.entries()]
321
- .map(([id, match]) => ({ id, ...match }))
322
- .sort((left, right) => right.score - left.score || left.id.localeCompare(right.id));
323
304
 
324
305
  return {
325
306
  dataSource: model.dataSource,
@@ -336,12 +317,6 @@ export function buildSnapshot(model) {
336
317
  finished: model.finished,
337
318
  summaryData: model.summaryData,
338
319
  regressionCatalog: model.regressionCatalog,
339
- filter: {
340
- active: model.filterActive,
341
- query: model.filterQuery,
342
- results: filterResults,
343
- count: filterResults.length,
344
- },
345
320
  runArtifact: model.runArtifact,
346
321
  };
347
322
  }
@@ -445,22 +420,18 @@ function buildNestedServices(model) {
445
420
 
446
421
  function buildVisibleEntries(model, services) {
447
422
  const entries = [];
448
- const filterActive = model.filterActive && model.filterQuery.trim().length > 0;
449
- const includedIds = filterActive ? buildFilteredIdSet(model, services) : null;
450
423
 
451
424
  for (const service of services) {
452
- pushVisibleEntry(entries, model, service, 0, includedIds);
425
+ pushVisibleEntry(entries, model, service, 0);
453
426
  if (service.skipped) continue;
454
427
  for (const typeNode of service.types) {
455
- if (!pushVisibleEntry(entries, model, typeNode, 1, includedIds)) continue;
456
- const typeExpanded = filterActive || !typeNode.collapsed;
457
- if (!typeExpanded) continue;
428
+ if (!pushVisibleEntry(entries, model, typeNode, 1)) continue;
429
+ if (typeNode.collapsed) continue;
458
430
  for (const suite of typeNode.suites) {
459
- if (!pushVisibleEntry(entries, model, suite, 2, includedIds)) continue;
460
- const suiteExpanded = filterActive || !suite.collapsed;
461
- if (!suiteExpanded) continue;
431
+ if (!pushVisibleEntry(entries, model, suite, 2)) continue;
432
+ if (suite.collapsed) continue;
462
433
  for (const file of suite.files) {
463
- pushVisibleEntry(entries, model, file, 3, includedIds);
434
+ pushVisibleEntry(entries, model, file, 3);
464
435
  }
465
436
  }
466
437
  }
@@ -469,62 +440,12 @@ function buildVisibleEntries(model, services) {
469
440
  return entries;
470
441
  }
471
442
 
472
- function pushVisibleEntry(entries, model, entry, depth, includedIds) {
473
- if (includedIds && !includedIds.has(entry.id)) return false;
474
- const base = toPublicEntry(entry, depth);
475
- const match = model.filterMatches.get(entry.id) || null;
476
- entries.push({
477
- ...base,
478
- match,
479
- });
443
+ function pushVisibleEntry(entries, model, entry, depth) {
444
+ entries.push(toPublicEntry(entry, depth, model));
480
445
  return true;
481
446
  }
482
447
 
483
- function buildFilteredIdSet(model, services) {
484
- const included = new Set();
485
- const childrenById = new Map();
486
- const parentsById = new Map();
487
-
488
- for (const service of services) {
489
- childrenById.set(service.id, service.types.map((entry) => entry.id));
490
- for (const typeNode of service.types) {
491
- parentsById.set(typeNode.id, service.id);
492
- childrenById.set(typeNode.id, typeNode.suites.map((entry) => entry.id));
493
- for (const suite of typeNode.suites) {
494
- parentsById.set(suite.id, typeNode.id);
495
- childrenById.set(suite.id, suite.files.map((entry) => entry.id));
496
- for (const file of suite.files) {
497
- parentsById.set(file.id, suite.id);
498
- childrenById.set(file.id, []);
499
- }
500
- }
501
- }
502
- }
503
-
504
- for (const entryId of model.filterMatches.keys()) {
505
- included.add(entryId);
506
- let current = parentsById.get(entryId) || null;
507
- while (current) {
508
- included.add(current);
509
- current = parentsById.get(current) || null;
510
- }
511
- const entry = getEntryById(model, entryId);
512
- if (entry && entry.kind !== "file") {
513
- includeDescendants(entryId, childrenById, included);
514
- }
515
- }
516
-
517
- return included;
518
- }
519
-
520
- function includeDescendants(entryId, childrenById, included) {
521
- for (const childId of childrenById.get(entryId) || []) {
522
- included.add(childId);
523
- includeDescendants(childId, childrenById, included);
524
- }
525
- }
526
-
527
- function toPublicEntry(entry, depth) {
448
+ function toPublicEntry(entry, depth, model = null) {
528
449
  if (entry.kind === "service") {
529
450
  return {
530
451
  id: entry.id,
@@ -567,6 +488,7 @@ function toPublicEntry(entry, depth) {
567
488
  framework: entry.framework,
568
489
  };
569
490
  }
491
+ const autoCollapsed = defaultAutoCollapsed(entry);
570
492
  return {
571
493
  id: entry.id,
572
494
  kind: "file",
@@ -586,6 +508,8 @@ function toPublicEntry(entry, depth) {
586
508
  diagnosis: entry.diagnosis,
587
509
  skipReason: entry.skipReason,
588
510
  artifacts: entry.artifacts,
511
+ checkDetails: entry.checkDetails,
512
+ collapsed: model ? isCollapsed(model, { id: entry.id, autoCollapsed }) : autoCollapsed,
589
513
  };
590
514
  }
591
515
 
@@ -625,7 +549,12 @@ function isCollapsed(model, entry) {
625
549
  if (model.collapsedOverrides.has(entry.id)) {
626
550
  return Boolean(model.collapsedOverrides.get(entry.id));
627
551
  }
628
- return Boolean(entry.autoCollapsed);
552
+ return Boolean(entry.autoCollapsed ?? defaultAutoCollapsed(entry));
553
+ }
554
+
555
+ function defaultAutoCollapsed(entry) {
556
+ if (entry?.kind !== "file") return false;
557
+ return entry.status === "passed" || entry.status === "skipped" || entry.status === "pending";
629
558
  }
630
559
 
631
560
  function summarizeFiles(files) {
@@ -17,7 +17,6 @@ import {
17
17
  setRegressionCatalog,
18
18
  setTotalFileCount,
19
19
  toggleCollapsed,
20
- updateFilter,
21
20
  } from "./model.mjs";
22
21
 
23
22
  export function createRunState({ dataSource = "live", autoCollapsePassedTreeBranches = true } = {}) {
@@ -143,27 +142,6 @@ export function createRunState({ dataSource = "live", autoCollapsePassedTreeBran
143
142
  notify();
144
143
  },
145
144
 
146
- activateFilter() {
147
- model.filterActive = true;
148
- notify();
149
- },
150
-
151
- updateFilterQuery(query) {
152
- updateFilter(model, query);
153
- const snapshot = buildSnapshot(model);
154
- if (snapshot.filter.results.length > 0) {
155
- model.selectedEntryId = snapshot.filter.results[0].id;
156
- }
157
- notify();
158
- },
159
-
160
- deactivateFilter() {
161
- model.filterActive = false;
162
- model.filterQuery = "";
163
- model.filterMatches = new Map();
164
- notify();
165
- },
166
-
167
145
  revealFile(serviceName, filePath) {
168
146
  const entryId = findEntryIdForFile(model, serviceName, filePath);
169
147
  if (!entryId) return false;
@@ -16,7 +16,6 @@ const DISCOVERY_RULES = [
16
16
  { suffix: ".dal.testkit.ts", type: "dal", framework: "k6" },
17
17
  { suffix: ".load.testkit.ts", type: "load", framework: "k6" },
18
18
  { suffix: ".ui.testkit.ts", type: "ui", framework: "playwright" },
19
- { suffix: ".pw.testkit.ts", type: "ui", framework: "playwright", legacySuffix: true },
20
19
  ];
21
20
 
22
21
  export function discoverProject(productDir, explicitServices = {}, options = {}) {
@@ -31,15 +30,6 @@ export function discoverProject(productDir, explicitServices = {}, options = {})
31
30
  for (const filePath of suiteFiles) {
32
31
  const rule = inferRule(filePath);
33
32
  if (!rule) continue;
34
- if (rule.legacySuffix) {
35
- diagnostics.push({
36
- code: "legacy_ui_suffix",
37
- severity: "warning",
38
- message: `Legacy UI test suffix ".pw.testkit.ts" is deprecated. Rename to ".ui.testkit.ts": ${filePath}`,
39
- path: filePath,
40
- });
41
- }
42
-
43
33
  const owners = inferOwners(filePath, explicitServices, repoDiscovery);
44
34
  if (owners === null) continue;
45
35
  if (owners.length === 0) {
@@ -489,7 +489,7 @@ function normalizePath(filePath) {
489
489
  export function fileDisplayName(filePath) {
490
490
  const base = path.posix
491
491
  .basename(filePath)
492
- .replace(/(\.int|\.e2e|\.scenario|\.dal|\.load|\.ui|\.pw)\.testkit\.ts$/, "");
492
+ .replace(/(\.int|\.e2e|\.scenario|\.dal|\.load|\.ui)\.testkit\.ts$/, "");
493
493
  return formatDisplayName(base);
494
494
  }
495
495
 
@@ -2,9 +2,6 @@ export const TEST_TYPE_ORDER = ["ui", "e2e", "scenario", "int", "dal", "load"];
2
2
  export const TEST_TYPES = new Set(TEST_TYPE_ORDER);
3
3
  export const RUN_TYPE_ORDER = [...TEST_TYPE_ORDER, "all"];
4
4
  export const RUN_TYPES = new Set(RUN_TYPE_ORDER);
5
- export const LEGACY_TEST_TYPE_ALIASES = new Map([
6
- ["pw", "ui"],
7
- ]);
8
5
 
9
6
  const TEST_TYPE_LABELS = {
10
7
  ui: "UI",
@@ -15,28 +12,22 @@ const TEST_TYPE_LABELS = {
15
12
  load: "Load",
16
13
  };
17
14
 
18
- export function normalizePublicTestType(value) {
19
- const normalized = String(value || "").trim();
20
- return LEGACY_TEST_TYPE_ALIASES.get(normalized) || normalized;
21
- }
22
-
23
15
  export function isPublicTestType(value) {
24
- return TEST_TYPES.has(normalizePublicTestType(value));
16
+ return TEST_TYPES.has(String(value || "").trim());
25
17
  }
26
18
 
27
19
  export function isRunType(value) {
28
- const normalized = normalizePublicTestType(value);
20
+ const normalized = String(value || "").trim();
29
21
  return normalized === "all" || TEST_TYPES.has(normalized);
30
22
  }
31
23
 
32
24
  export function formatPublicTestType(value) {
33
- const normalized = normalizePublicTestType(value);
25
+ const normalized = String(value || "").trim();
34
26
  return TEST_TYPE_LABELS[normalized] || String(value || "");
35
27
  }
36
28
 
37
- export function publicTestTypeList({ includeAll = false, includeLegacy = false } = {}) {
38
- const values = includeAll ? RUN_TYPE_ORDER : TEST_TYPE_ORDER;
39
- return includeLegacy ? [...values, "pw"] : [...values];
29
+ export function publicTestTypeList({ includeAll = false } = {}) {
30
+ return includeAll ? [...RUN_TYPE_ORDER] : [...TEST_TYPE_ORDER];
40
31
  }
41
32
 
42
33
  export function publicTestTypeListText(options = {}) {
@@ -12,7 +12,7 @@ import {
12
12
  determineDefaultRuntimeFailure,
13
13
  extractDefaultRuntimeFatalDetail,
14
14
  } from "./default-runtime-errors.mjs";
15
- import { collectFailureDetailsFromRuntimeArtifacts } from "./failure-details.mjs";
15
+ import { collectFailureDetailsFromRuntimeArtifacts, collectCheckDetailsFromRuntimeArtifacts } from "./failure-details.mjs";
16
16
  import { RUNTIME_ARTIFACT_MARKER } from "../runtime-src/k6/artifacts.js";
17
17
  import { readDatabaseUrl } from "./state-io.mjs";
18
18
  import { buildTaskExecutionEnv } from "./template.mjs";
@@ -147,6 +147,7 @@ export async function runDefaultRuntimeTask(
147
147
  : null,
148
148
  ]);
149
149
  const failureDetails = collectFailureDetailsFromRuntimeArtifacts(rawRuntimeArtifacts);
150
+ const checkDetails = collectCheckDetailsFromRuntimeArtifacts(rawRuntimeArtifacts);
150
151
  const fatalRuntimeDetail = extractDefaultRuntimeFatalDetail(result.stderr || "", getFirstLine);
151
152
  if (fatalRuntimeDetail && !failureDetails.some((detail) => detail?.kind === "runtime-exception")) {
152
153
  failureDetails.unshift(fatalRuntimeDetail);
@@ -165,6 +166,7 @@ export async function runDefaultRuntimeTask(
165
166
  finishedAt,
166
167
  artifacts: [...runtimeArtifacts, ...outputArtifacts],
167
168
  failureDetails,
169
+ checkDetails,
168
170
  };
169
171
  }
170
172
 
@@ -90,6 +90,28 @@ export function collectFailureDetailsFromRuntimeArtifacts(artifacts) {
90
90
  return mergeFailureDetails(details);
91
91
  }
92
92
 
93
+ export function collectCheckDetailsFromRuntimeArtifacts(artifacts) {
94
+ const checks = [];
95
+
96
+ for (const artifact of Array.isArray(artifacts) ? artifacts : []) {
97
+ if (artifact?.kind !== "testkit.checks") continue;
98
+ for (const check of Array.isArray(artifact?.data?.checks) ? artifact.data.checks : []) {
99
+ const name = normalizeNonEmptyString(check?.name) || "unnamed";
100
+ const passes = Math.max(0, Number(check?.passes) || 0);
101
+ const fails = Math.max(0, Number(check?.fails) || 0);
102
+ checks.push({
103
+ name,
104
+ path: normalizeStringArray(check?.path),
105
+ passes,
106
+ fails,
107
+ passed: passes > 0 && fails === 0,
108
+ });
109
+ }
110
+ }
111
+
112
+ return checks;
113
+ }
114
+
93
115
  function normalizeStringArray(value) {
94
116
  if (!Array.isArray(value)) return [];
95
117
  return value
@@ -90,7 +90,7 @@ export async function cleanup(productDir, options = {}) {
90
90
  appendBoundedFileCleanupLines(lines, {
91
91
  productDir,
92
92
  targets: targets.assistant,
93
- label: "assistant command result",
93
+ label: "assistant session",
94
94
  dryRun,
95
95
  });
96
96
 
@@ -1,13 +1,16 @@
1
1
  const ASSISTANT_SESSION_ENV = "TESTKIT_ASSISTANT_SESSION_ID";
2
+ const ASSISTANT_TURN_ENV = "TESTKIT_ASSISTANT_TURN_ID";
2
3
  const ASSISTANT_COMMAND_ID_ENV = "TESTKIT_ASSISTANT_COMMAND_ID";
3
4
 
4
5
  export function buildRunProvenance(env = process.env) {
5
6
  const sessionId = normalizeOptionalString(env?.[ASSISTANT_SESSION_ENV]);
7
+ const turnId = normalizeOptionalString(env?.[ASSISTANT_TURN_ENV]);
6
8
  const commandId = normalizeOptionalString(env?.[ASSISTANT_COMMAND_ID_ENV]);
7
- if (!sessionId && !commandId) return null;
9
+ if (!sessionId && !turnId && !commandId) return null;
8
10
  return {
9
11
  assistant: {
10
12
  sessionId,
13
+ turnId,
11
14
  commandId,
12
15
  },
13
16
  };
@@ -48,6 +48,7 @@ export function buildServiceTrackers(servicePlans, startedAt) {
48
48
  status: "not_run",
49
49
  artifacts: [],
50
50
  failureDetails: [],
51
+ checkDetails: [],
51
52
  },
52
53
  ];
53
54
  }),
@@ -62,6 +63,7 @@ export function buildServiceTrackers(servicePlans, startedAt) {
62
63
  status: "skipped",
63
64
  artifacts: [],
64
65
  failureDetails: [],
66
+ checkDetails: [],
65
67
  },
66
68
  ]),
67
69
  ]),
@@ -125,6 +127,7 @@ export function recordTaskOutcome(trackers, task, outcome, finishedAt = Date.now
125
127
  existingFileResult.status = status;
126
128
  existingFileResult.artifacts = Array.isArray(outcome.artifacts) ? outcome.artifacts : [];
127
129
  existingFileResult.failureDetails = mergeFailureDetails(outcome.failureDetails);
130
+ existingFileResult.checkDetails = normalizeCheckDetails(outcome.checkDetails);
128
131
  } else {
129
132
  suite.fileResultsByPath.set(normalizedPath, {
130
133
  path: normalizedPath,
@@ -135,6 +138,7 @@ export function recordTaskOutcome(trackers, task, outcome, finishedAt = Date.now
135
138
  status,
136
139
  artifacts: Array.isArray(outcome.artifacts) ? outcome.artifacts : [],
137
140
  failureDetails: mergeFailureDetails(outcome.failureDetails),
141
+ checkDetails: normalizeCheckDetails(outcome.checkDetails),
138
142
  });
139
143
  }
140
144
  if (status === "failed" && !suite.failedFileSet.has(task.file)) {
@@ -258,6 +262,9 @@ function finalizeSuite(suite) {
258
262
  ...(Array.isArray(file.artifacts) && file.artifacts.length > 0
259
263
  ? { artifacts: file.artifacts }
260
264
  : {}),
265
+ ...(Array.isArray(file.checkDetails) && file.checkDetails.length > 0
266
+ ? { checkDetails: file.checkDetails }
267
+ : {}),
261
268
  }));
262
269
 
263
270
  return {
@@ -289,3 +296,27 @@ function normalizeOutcomeStatus(outcome) {
289
296
  if (outcome?.status === "skipped") return "skipped";
290
297
  return outcome?.failed ? "failed" : "passed";
291
298
  }
299
+
300
+ function normalizeCheckDetails(value) {
301
+ if (!Array.isArray(value)) return [];
302
+ return value.map((check) => {
303
+ const passes = normalizeCount(check?.passes);
304
+ const fails = normalizeCount(check?.fails);
305
+ return {
306
+ name: typeof check?.name === "string" && check.name.trim().length > 0 ? check.name.trim() : "unnamed",
307
+ path: Array.isArray(check?.path)
308
+ ? check.path
309
+ .map((entry) => String(entry || "").trim())
310
+ .filter(Boolean)
311
+ : [],
312
+ passes,
313
+ fails,
314
+ passed: passes > 0 && fails === 0,
315
+ };
316
+ });
317
+ }
318
+
319
+ function normalizeCount(value) {
320
+ const count = Number(value);
321
+ return Number.isFinite(count) && count > 0 ? count : 0;
322
+ }
@@ -242,13 +242,21 @@ export function collectBundleCleanupTargets(productDir, { allConfigs = [], servi
242
242
  export function collectAssistantCleanupTargets(productDir) {
243
243
  const now = Date.now();
244
244
  const dir = path.join(productDir, ".testkit", "assistant", "sessions");
245
- return listFiles(dir)
246
- .filter((file) => file.size >= ASSISTANT_LARGE_RESULT_BYTES || now - file.mtimeMs >= ASSISTANT_RESULT_TTL_MS)
247
- .map((file) => ({
248
- path: file.path,
249
- reason: file.size >= ASSISTANT_LARGE_RESULT_BYTES ? "large" : "expired",
250
- sizeBytes: file.size,
251
- }));
245
+ return listDirectories(dir)
246
+ .map((sessionDir) => {
247
+ const files = listFiles(sessionDir);
248
+ const sizeBytes = files.reduce((sum, file) => sum + file.size, 0);
249
+ const newestMtimeMs = files.reduce((latest, file) => Math.max(latest, file.mtimeMs), 0);
250
+ const expired = newestMtimeMs > 0 && now - newestMtimeMs >= ASSISTANT_RESULT_TTL_MS;
251
+ const large = sizeBytes >= ASSISTANT_LARGE_RESULT_BYTES;
252
+ if (!expired && !large) return null;
253
+ return {
254
+ path: sessionDir,
255
+ reason: large ? "large" : "expired",
256
+ sizeBytes,
257
+ };
258
+ })
259
+ .filter(Boolean);
252
260
  }
253
261
 
254
262
  function listDirectories(dir) {
@@ -3,7 +3,6 @@ import {
3
3
  RUN_TYPES,
4
4
  TEST_TYPE_ORDER,
5
5
  TEST_TYPES,
6
- normalizePublicTestType,
7
6
  publicTestTypeListText,
8
7
  } from "../domain/test-types.mjs";
9
8
 
@@ -14,7 +13,7 @@ export function normalizeTypeValues(values = []) {
14
13
  for (const part of String(rawValue).split(",")) {
15
14
  const value = part.trim();
16
15
  if (!value) continue;
17
- const normalized = normalizePublicTestType(value);
16
+ const normalized = value;
18
17
  if (!RUN_TYPES.has(normalized)) {
19
18
  throw new Error(
20
19
  `Unknown type "${value}". Expected one of: ${publicTestTypeListText({ includeAll: true })}.`
@@ -57,7 +56,7 @@ export function parseSuiteSelectors(values = []) {
57
56
 
58
57
  const type = typeMatch[1];
59
58
  const name = typeMatch[2].trim();
60
- const normalizedType = normalizePublicTestType(type);
59
+ const normalizedType = type.trim();
61
60
  if (!TEST_TYPES.has(normalizedType)) {
62
61
  throw new Error(
63
62
  `Unknown suite selector type "${type}". Expected one of: ${publicTestTypeListText()}.`
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/next-analysis",
3
- "version": "0.1.111",
3
+ "version": "0.1.113",
4
4
  "description": "SWC-backed Next.js source analysis primitives for Erench tools",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit-bridge",
3
- "version": "0.1.111",
3
+ "version": "0.1.113",
4
4
  "description": "Browser bridge helpers for testkit",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -22,7 +22,7 @@
22
22
  "typecheck": "tsc -p tsconfig.json --noEmit"
23
23
  },
24
24
  "dependencies": {
25
- "@elench/testkit-protocol": "0.1.111"
25
+ "@elench/testkit-protocol": "0.1.113"
26
26
  },
27
27
  "private": false
28
28
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit-protocol",
3
- "version": "0.1.111",
3
+ "version": "0.1.113",
4
4
  "description": "Shared browser protocol for testkit bridge and extension consumers",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/ts-analysis",
3
- "version": "0.1.111",
3
+ "version": "0.1.113",
4
4
  "description": "TypeScript compiler-backed source analysis primitives for Erench tools",
5
5
  "type": "module",
6
6
  "exports": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit",
3
- "version": "0.1.111",
3
+ "version": "0.1.113",
4
4
  "description": "Assistant-first CLI for running, inspecting, and debugging local testkit suites",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -90,10 +90,10 @@
90
90
  },
91
91
  "dependencies": {
92
92
  "@babel/code-frame": "^7.29.0",
93
- "@elench/next-analysis": "0.1.111",
94
- "@elench/testkit-bridge": "0.1.111",
95
- "@elench/testkit-protocol": "0.1.111",
96
- "@elench/ts-analysis": "0.1.111",
93
+ "@elench/next-analysis": "0.1.113",
94
+ "@elench/testkit-bridge": "0.1.113",
95
+ "@elench/testkit-protocol": "0.1.113",
96
+ "@elench/ts-analysis": "0.1.113",
97
97
  "@oclif/core": "^4.10.6",
98
98
  "@playwright/test": "^1.52.0",
99
99
  "esbuild": "^0.25.11",
@@ -1,12 +0,0 @@
1
- import React, { createElement } from "react";
2
- import { Text } from "ink";
3
- import { bold, dim } from "../../terminal/colors.mjs";
4
-
5
- export function FilterBar({ filter } = {}) {
6
- if (!filter?.active) return null;
7
- return createElement(
8
- Text,
9
- null,
10
- `${bold("/")}${filter.query}${dim(` ${filter.count} ${filter.count === 1 ? "match" : "matches"}`)}`
11
- );
12
- }