@elench/testkit 0.1.111 → 0.1.112

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 (34) hide show
  1. package/README.md +1 -1
  2. package/lib/cli/args.mjs +1 -1
  3. package/lib/cli/assistant/app.mjs +70 -20
  4. package/lib/cli/assistant/command-normalize.mjs +22 -0
  5. package/lib/cli/assistant/command-observer.mjs +49 -4
  6. package/lib/cli/assistant/command-results.mjs +10 -1
  7. package/lib/cli/assistant/context-pack.mjs +45 -15
  8. package/lib/cli/assistant/domain.d.mts +59 -0
  9. package/lib/cli/assistant/domain.d.mts.map +1 -0
  10. package/lib/cli/assistant/domain.mjs +2 -0
  11. package/lib/cli/assistant/domain.mjs.map +1 -0
  12. package/lib/cli/assistant/session.mjs +3 -1
  13. package/lib/cli/assistant/state.mjs +109 -2
  14. package/lib/cli/assistant/view-model.mjs +69 -9
  15. package/lib/cli/commands/run.mjs +1 -1
  16. package/lib/cli/components/blocks/run-tree.mjs +2 -1
  17. package/lib/cli/entrypoint.mjs +1 -1
  18. package/lib/config/discovery.mjs +0 -10
  19. package/lib/discovery/index.mjs +1 -1
  20. package/lib/domain/test-types.mjs +5 -14
  21. package/lib/runner/maintenance.mjs +1 -1
  22. package/lib/runner/provenance.mjs +4 -1
  23. package/lib/runner/status-model.mjs +15 -7
  24. package/lib/runner/suite-selection.mjs +2 -3
  25. package/node_modules/@elench/next-analysis/package.json +1 -1
  26. package/node_modules/@elench/testkit-bridge/package.json +2 -2
  27. package/node_modules/@elench/testkit-protocol/package.json +1 -1
  28. package/node_modules/@elench/ts-analysis/package.json +1 -1
  29. package/package.json +5 -5
  30. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.d.ts +0 -188
  31. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.d.ts.map +0 -1
  32. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.js +0 -293
  33. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.js.map +0 -1
  34. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/package.json +0 -25
@@ -17,6 +17,7 @@ import { parseSlashCommand, formatSlashHelpLines } from "./slash-commands.mjs";
17
17
  import { executeAssistantAction } from "./actions.mjs";
18
18
  import { runAssistantConversationTurn } from "./session.mjs";
19
19
  import { prepareAssistantContextPack } from "./context-pack.mjs";
20
+ import { normalizeCommandLine, unwrapShellCommand } from "./command-normalize.mjs";
20
21
  import {
21
22
  discoverAssistantModels,
22
23
  formatModelChoices,
@@ -88,6 +89,7 @@ export function createAssistantState({
88
89
  let activeStatus = null;
89
90
  let startupNoticeEmitted = false;
90
91
  let lastTurnError = null;
92
+ let activeTurn = idleTurn();
91
93
  let contextUsage = buildContextUsage({
92
94
  provider: resolvedProviderName || settings.provider,
93
95
  model: settings.model,
@@ -109,6 +111,7 @@ export function createAssistantState({
109
111
  function appendMessage(message) {
110
112
  const entry = {
111
113
  id: `msg-${messages.length + 1}`,
114
+ ...(activeTurn?.id && !message.turnId ? { turnId: activeTurn.id } : {}),
112
115
  ...message,
113
116
  };
114
117
  messages.push(entry);
@@ -182,6 +185,7 @@ export function createAssistantState({
182
185
  commandLog,
183
186
  attachRunSession,
184
187
  completeRunSession,
188
+ updateMessage,
185
189
 
186
190
  async loadLatestArtifact() {
187
191
  try {
@@ -364,7 +368,8 @@ export function createAssistantState({
364
368
  startupNoticeEmitted = true;
365
369
  appendMessage({ role: "system", text: notice });
366
370
  }
367
- appendMessage({ role: "user", text: trimmed });
371
+ const turnId = createAssistantTurnId();
372
+ appendMessage({ role: "user", text: trimmed, turnId });
368
373
 
369
374
  const slash = parseSlashCommandSafe(trimmed);
370
375
  if (slash?.type === "__error__") {
@@ -373,6 +378,8 @@ export function createAssistantState({
373
378
  }
374
379
  if (slash) {
375
380
  try {
381
+ activeTurn = { id: turnId, state: "slash_running", input: trimmed, startedAt: new Date().toISOString() };
382
+ commandLog.setActiveTurnId?.(turnId);
376
383
  setBusy(true, `Running ${slash.type}...`);
377
384
  await executeSlashCommand({
378
385
  slash,
@@ -389,6 +396,14 @@ export function createAssistantState({
389
396
  text: error instanceof Error ? error.message : String(error),
390
397
  });
391
398
  } finally {
399
+ commandLog.setActiveTurnId?.(null);
400
+ activeTurn = {
401
+ id: turnId,
402
+ state: "idle",
403
+ input: trimmed,
404
+ startedAt: activeTurn?.startedAt || new Date().toISOString(),
405
+ finishedAt: new Date().toISOString(),
406
+ };
392
407
  setBusy(false, null);
393
408
  }
394
409
  refreshContextPack();
@@ -397,6 +412,8 @@ export function createAssistantState({
397
412
  }
398
413
 
399
414
  try {
415
+ activeTurn = { id: turnId, state: "provider_running", input: trimmed, startedAt: new Date().toISOString() };
416
+ commandLog.setActiveTurnId?.(turnId);
400
417
  setBusy(true, `Thinking with ${settings.provider === "auto" ? "provider" : settings.provider}...`);
401
418
  const providerTurn = createProviderTurnState();
402
419
  await runAssistantConversationTurn({
@@ -405,6 +422,7 @@ export function createAssistantState({
405
422
  transcript: buildConversationTranscript(messages),
406
423
  userMessage: trimmed,
407
424
  settings,
425
+ turnId,
408
426
  env,
409
427
  configs,
410
428
  commandLog,
@@ -447,8 +465,26 @@ export function createAssistantState({
447
465
  role: "system",
448
466
  text: lastTurnError.message,
449
467
  });
468
+ activeTurn = {
469
+ id: turnId,
470
+ state: "failed",
471
+ input: trimmed,
472
+ startedAt: activeTurn?.startedAt || new Date().toISOString(),
473
+ failedAt: new Date().toISOString(),
474
+ error: lastTurnError,
475
+ };
450
476
  } finally {
477
+ commandLog.setActiveTurnId?.(null);
451
478
  refreshContextPack();
479
+ if (activeTurn?.id === turnId && activeTurn.state !== "failed") {
480
+ activeTurn = {
481
+ id: turnId,
482
+ state: "idle",
483
+ input: trimmed,
484
+ startedAt: activeTurn?.startedAt || new Date().toISOString(),
485
+ finishedAt: new Date().toISOString(),
486
+ };
487
+ }
452
488
  setBusy(false, null);
453
489
  }
454
490
  },
@@ -459,11 +495,13 @@ export function createAssistantState({
459
495
  },
460
496
 
461
497
  getSnapshot() {
498
+ const snapshotMessages = messages.map(serializeMessageForSnapshot);
462
499
  return {
463
500
  context: buildContextSelection(runState.getSnapshot()),
464
501
  run: runState.getSnapshot(),
465
502
  productDir,
466
- messages: messages.map(serializeMessageForSnapshot),
503
+ messages: snapshotMessages,
504
+ activities: buildSnapshotActivities(snapshotMessages),
467
505
  composer: composerState.text,
468
506
  composerCursor: composerState.cursor,
469
507
  notice,
@@ -475,6 +513,7 @@ export function createAssistantState({
475
513
  providerArgs: [...settings.providerArgs],
476
514
  cliConfig,
477
515
  activeStatus,
516
+ turn: activeTurn,
478
517
  lastTurnError,
479
518
  diagnostics: [...diagnostics],
480
519
  contextUsage,
@@ -508,6 +547,17 @@ function resolveInitialProvider(provider, env) {
508
547
  return null;
509
548
  }
510
549
 
550
+ function idleTurn() {
551
+ return {
552
+ id: null,
553
+ state: "idle",
554
+ };
555
+ }
556
+
557
+ function createAssistantTurnId(now = Date.now(), random = Math.random) {
558
+ return `turn-${now}-${random().toString(36).slice(2, 10)}`;
559
+ }
560
+
511
561
  async function executeSlashCommand({
512
562
  slash,
513
563
  state,
@@ -664,6 +714,7 @@ function handleAssistantToolEvent(state, event, appendMessage) {
664
714
  return;
665
715
  }
666
716
  if (event.type === "observed-testkit-command") {
717
+ suppressMatchingProviderCommand(state, event.command);
667
718
  appendMessage({
668
719
  role: "tool",
669
720
  toolName: event.command?.kind || "testkit",
@@ -679,6 +730,28 @@ function handleAssistantToolEvent(state, event, appendMessage) {
679
730
  }
680
731
  }
681
732
 
733
+ function suppressMatchingProviderCommand(state, command) {
734
+ const observed = normalizeCommandLine(formatObservedCommandLine(command));
735
+ if (!observed) return;
736
+ const snapshot = state.getSnapshot();
737
+ for (const message of snapshot.messages || []) {
738
+ if (message.role !== "provider-tool") continue;
739
+ const providerCommand = normalizeCommandLine(providerToolCommandLine(message.data));
740
+ if (!providerCommand || providerCommand !== observed) continue;
741
+ state.updateMessage?.(message.id, (current) => ({
742
+ data: {
743
+ ...(current.data || {}),
744
+ supersededByTestkitCommand: command?.commandId || true,
745
+ },
746
+ }));
747
+ }
748
+ }
749
+
750
+ function providerToolCommandLine(event) {
751
+ if (!event) return null;
752
+ return event.input || event.data?.command || event.data?.input || null;
753
+ }
754
+
682
755
  function createProviderTurnState() {
683
756
  return {
684
757
  assistantMessageId: null,
@@ -940,6 +1013,40 @@ function serializeMessageForSnapshot(message) {
940
1013
  };
941
1014
  }
942
1015
 
1016
+ function buildSnapshotActivities(messages) {
1017
+ return (messages || []).map((message) => {
1018
+ const base = {
1019
+ id: message.id,
1020
+ turnId: message.turnId || null,
1021
+ title: message.title || null,
1022
+ text: message.text || "",
1023
+ status: message.status || null,
1024
+ data: message.data || null,
1025
+ };
1026
+ if (message.role === "user") return { ...base, kind: "user_message" };
1027
+ if (message.role === "assistant") return { ...base, kind: "assistant_message" };
1028
+ if (message.role === "provider-tool") {
1029
+ return {
1030
+ ...base,
1031
+ kind: "provider_command",
1032
+ command: providerToolCommandLine(message.data),
1033
+ supersededBy: message.data?.supersededByTestkitCommand || null,
1034
+ };
1035
+ }
1036
+ if (message.role === "provider-activity") return { ...base, kind: "provider_status" };
1037
+ if (message.role === "tool" && message.data?.testkitRelated) {
1038
+ const kind = message.data?.kind === "run" ? "testkit_run" : "testkit_command";
1039
+ return {
1040
+ ...base,
1041
+ kind,
1042
+ command: message.data?.command || null,
1043
+ commandId: message.data?.commandId || null,
1044
+ };
1045
+ }
1046
+ return { ...base, kind: "system_message" };
1047
+ });
1048
+ }
1049
+
943
1050
  function buildConversationTranscript(messages) {
944
1051
  return (messages || [])
945
1052
  .filter((entry) => !["provider-activity", "provider-tool", "provider-error"].includes(entry.role))
@@ -1,5 +1,6 @@
1
1
  import path from "path";
2
2
  import { formatContextRemaining } from "./context-window.mjs";
3
+ import { normalizeCommandLine, unwrapShellCommand } from "./command-normalize.mjs";
3
4
 
4
5
  const PROVIDER_COMMAND_OUTPUT_PREVIEW_LINES = 12;
5
6
  const PROVIDER_FILE_READ_OUTPUT_PREVIEW_LINES = 8;
@@ -13,7 +14,7 @@ export function buildAssistantViewModel(snapshot, { cwd = process.cwd(), termina
13
14
  title: `testkit · ${repoName}`,
14
15
  welcome: buildWelcomeModel(snapshot, { cwd, providerLabel }),
15
16
  qualitySignal: buildQualitySignal(snapshot),
16
- blocks: buildTranscriptBlocks(snapshot.messages || []),
17
+ blocks: snapshot.activities ? buildActivityBlocks(snapshot.activities) : buildTranscriptBlocks(snapshot.messages || []),
17
18
  composer: {
18
19
  text: snapshot.composer || "",
19
20
  cursor: snapshot.composerCursor ?? 0,
@@ -134,7 +135,8 @@ export function buildWelcomeModel(snapshot, { cwd = process.cwd(), providerLabel
134
135
  }
135
136
 
136
137
  export function buildTranscriptBlocks(messages) {
137
- return (messages || []).map((message) => {
138
+ const visibleMessages = filterSupersededProviderCommands(messages || []);
139
+ return visibleMessages.map((message) => {
138
140
  const role = message.role || "system";
139
141
  if (role === "tool") {
140
142
  const outputPreview = summarizeOutput(message.text || "", TESTKIT_COMMAND_OUTPUT_PREVIEW_LINES);
@@ -204,6 +206,71 @@ export function buildTranscriptBlocks(messages) {
204
206
  });
205
207
  }
206
208
 
209
+ export function buildActivityBlocks(activities) {
210
+ const messages = (activities || [])
211
+ .filter((activity) => !activity.supersededBy)
212
+ .map((activity) => {
213
+ if (activity.kind === "user_message") return { id: activity.id, role: "user", text: activity.text || "" };
214
+ if (activity.kind === "assistant_message") return { id: activity.id, role: "assistant", text: activity.text || "" };
215
+ if (activity.kind === "provider_command") {
216
+ return {
217
+ id: activity.id,
218
+ role: "provider-tool",
219
+ title: activity.title || "provider command",
220
+ text: activity.text || "",
221
+ status: activity.status || null,
222
+ data: {
223
+ ...(activity.data || {}),
224
+ input: activity.command || activity.data?.input || null,
225
+ },
226
+ };
227
+ }
228
+ if (activity.kind === "provider_status") {
229
+ return {
230
+ id: activity.id,
231
+ role: "provider-activity",
232
+ title: activity.title || null,
233
+ text: activity.text || "",
234
+ status: activity.status || null,
235
+ };
236
+ }
237
+ if (activity.kind === "testkit_command" || activity.kind === "testkit_run") {
238
+ return {
239
+ id: activity.id,
240
+ role: "tool",
241
+ title: activity.title || (activity.kind === "testkit_run" ? "testkit run" : "testkit command"),
242
+ text: activity.text || "",
243
+ status: activity.status || null,
244
+ data: {
245
+ ...(activity.data || {}),
246
+ testkitRelated: true,
247
+ kind: activity.kind === "testkit_run" ? "run" : activity.data?.kind,
248
+ command: activity.command || activity.data?.command || null,
249
+ commandId: activity.commandId || activity.data?.commandId || null,
250
+ },
251
+ };
252
+ }
253
+ return { id: activity.id, role: "system", title: activity.title || null, text: activity.text || "" };
254
+ });
255
+ return buildTranscriptBlocks(messages);
256
+ }
257
+
258
+ function filterSupersededProviderCommands(messages) {
259
+ const observedCommands = new Set(
260
+ (messages || [])
261
+ .filter((message) => message.role === "tool" && message.data?.testkitRelated)
262
+ .map((message) => normalizeCommandLine(message.data?.command))
263
+ .filter(Boolean)
264
+ );
265
+ return (messages || []).filter((message) => {
266
+ if (message.role !== "provider-tool") return true;
267
+ if (message.data?.supersededByTestkitCommand) return false;
268
+ const providerCommand = normalizeCommandLine(message.data?.input || message.data?.data?.command || message.data?.data?.input);
269
+ if (!providerCommand) return true;
270
+ return !observedCommands.has(providerCommand);
271
+ });
272
+ }
273
+
207
274
  export function buildStatusLine(snapshot, { cwd = process.cwd(), providerLabel = null } = {}) {
208
275
  const context = formatContextRemaining(snapshot.contextUsage);
209
276
  const provider = providerLabel || buildProviderLabel(snapshot);
@@ -339,13 +406,6 @@ function isDiffProducingShellCommand(command) {
339
406
  return /^((git\s+diff|git\s+show|diff|apply_patch)\b|.*\bapply_patch\b)/.test(normalized);
340
407
  }
341
408
 
342
- function unwrapShellCommand(command) {
343
- const text = String(command || "").trim();
344
- const match = text.match(/(?:^|\s)(?:[^\s'"]*\/)?(?:bash|sh|zsh)\s+-lc\s+(['"])([\s\S]*)\1\s*$/);
345
- if (!match) return text;
346
- return match[2].replace(/\\(["'\\$`])/g, "$1").trim();
347
- }
348
-
349
409
  function stringifyMaybe(value) {
350
410
  if (value == null) return "";
351
411
  if (typeof value === "string") return value;
@@ -14,7 +14,7 @@ export default class RunCommand extends Command {
14
14
  type: Args.string({
15
15
  description: `Optional suite type shortcut: ${publicTestTypeListText({ includeAll: true })}`,
16
16
  required: false,
17
- options: publicTestTypeList({ includeAll: true, includeLegacy: true }),
17
+ options: publicTestTypeList({ includeAll: true }),
18
18
  }),
19
19
  };
20
20
 
@@ -25,8 +25,9 @@ export function RunTreeView({
25
25
  interactive = true,
26
26
  } = {}) {
27
27
  const { exit } = useApp();
28
+ const controlledSnapshot = Boolean(snapshotOverride);
28
29
  const [snapshot, setSnapshot] = useState(() => snapshotOverride || runState.getSnapshot());
29
- const { frame } = useAnimation({ interval: 80, isActive: !snapshot.finished });
30
+ const { frame } = useAnimation({ interval: 80, isActive: !controlledSnapshot && !snapshot.finished });
30
31
  const spinnerFrame = SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
31
32
 
32
33
  useEffect(() => {
@@ -16,7 +16,7 @@ export function normalizeCliArgs(argv) {
16
16
  "browser",
17
17
  "db",
18
18
  ]);
19
- const runTypeShortcuts = new Set(publicTestTypeList({ includeAll: true, includeLegacy: true }));
19
+ const runTypeShortcuts = new Set(publicTestTypeList({ includeAll: true }));
20
20
  const valueFlags = new Set([
21
21
  "--dir",
22
22
  "--service",
@@ -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 = {}) {
@@ -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
  };
@@ -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.112",
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.112",
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.112"
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.112",
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.112",
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.112",
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.112",
94
+ "@elench/testkit-bridge": "0.1.112",
95
+ "@elench/testkit-protocol": "0.1.112",
96
+ "@elench/ts-analysis": "0.1.112",
97
97
  "@oclif/core": "^4.10.6",
98
98
  "@playwright/test": "^1.52.0",
99
99
  "esbuild": "^0.25.11",