@elench/testkit 0.1.97 → 0.1.99

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 (80) hide show
  1. package/README.md +8 -8
  2. package/lib/app/browser-bridge.mjs +1 -1
  3. package/lib/cli/assistant/actions.mjs +333 -0
  4. package/lib/cli/assistant/app.mjs +25 -1
  5. package/lib/cli/assistant/command-observer.mjs +110 -0
  6. package/lib/cli/assistant/command-results.mjs +167 -0
  7. package/lib/cli/assistant/composer.mjs +1 -1
  8. package/lib/cli/assistant/context-pack.mjs +73 -6
  9. package/lib/cli/assistant/interactive.mjs +1 -1
  10. package/lib/cli/assistant/prompt-builder.mjs +15 -8
  11. package/lib/cli/{agents → assistant}/providers/claude.mjs +2 -3
  12. package/lib/cli/{agents → assistant}/providers/codex.mjs +2 -6
  13. package/lib/cli/{agents → assistant/providers}/index.mjs +5 -5
  14. package/lib/cli/assistant/session.mjs +36 -94
  15. package/lib/cli/assistant/slash-commands.mjs +22 -1
  16. package/lib/cli/assistant/state.mjs +187 -100
  17. package/lib/cli/assistant/view-model.mjs +1 -1
  18. package/lib/cli/command-flags.mjs +61 -0
  19. package/lib/cli/commands/assistant.mjs +4 -3
  20. package/lib/cli/commands/browser/serve.mjs +5 -23
  21. package/lib/cli/commands/cleanup.mjs +8 -2
  22. package/lib/cli/commands/db/snapshot/capture.mjs +8 -4
  23. package/lib/cli/commands/destroy.mjs +8 -2
  24. package/lib/cli/commands/discover.mjs +13 -32
  25. package/lib/cli/commands/doctor.mjs +17 -14
  26. package/lib/cli/commands/run.mjs +14 -3
  27. package/lib/cli/commands/status.mjs +14 -3
  28. package/lib/cli/commands/typecheck.mjs +12 -9
  29. package/lib/cli/{tui/inspect-app.mjs → components/blocks/run-tree.mjs} +29 -54
  30. package/lib/cli/{tui → components/primitives}/filter-bar.mjs +1 -1
  31. package/lib/cli/{presentation → components/primitives}/summary-box.mjs +1 -1
  32. package/lib/cli/config.mjs +63 -0
  33. package/lib/cli/entrypoint.mjs +14 -5
  34. package/lib/cli/operations/browser/serve/operation.mjs +23 -0
  35. package/lib/cli/operations/cleanup/operation.mjs +8 -0
  36. package/lib/cli/{db.mjs → operations/db/snapshot/capture/operation.mjs} +15 -9
  37. package/lib/cli/operations/destroy/operation.mjs +12 -0
  38. package/lib/cli/operations/discover/operation.mjs +32 -0
  39. package/lib/cli/operations/doctor/operation.mjs +5 -0
  40. package/lib/cli/operations/run/operation.mjs +129 -0
  41. package/lib/cli/operations/status/operation.mjs +7 -0
  42. package/lib/cli/operations/typecheck/operation.mjs +5 -0
  43. package/lib/cli/renderers/browser-serve/text.mjs +6 -0
  44. package/lib/cli/renderers/cleanup/text.mjs +3 -0
  45. package/lib/cli/renderers/db-snapshot-capture/text.mjs +3 -0
  46. package/lib/cli/renderers/destroy/text.mjs +3 -0
  47. package/lib/cli/{presentation/discovery-reporter.mjs → renderers/discover/report.mjs} +3 -3
  48. package/lib/cli/renderers/discover/text.mjs +7 -0
  49. package/lib/cli/renderers/doctor/text.mjs +7 -0
  50. package/lib/cli/{presentation/failure-presentation.mjs → renderers/run/failure.mjs} +6 -6
  51. package/lib/cli/renderers/run/interactive.mjs +119 -0
  52. package/lib/cli/{presentation/run-reporter.mjs → renderers/run/text-reporter.mjs} +5 -5
  53. package/lib/cli/renderers/status/text.mjs +7 -0
  54. package/lib/cli/renderers/typecheck/text.mjs +7 -0
  55. package/lib/cli/{tui/inspect-model.mjs → state/run/model.mjs} +11 -26
  56. package/lib/cli/{tui/inspect-state.mjs → state/run/state.mjs} +11 -18
  57. package/lib/cli/{tui → state/tree}/fuzzy-match.mjs +1 -1
  58. package/lib/cli/terminal/capabilities.mjs +33 -0
  59. package/lib/database/index.mjs +9 -21
  60. package/lib/{cli/viewer.mjs → results/artifacts.mjs} +1 -1
  61. package/lib/{cli/context-resources.mjs → results/context.mjs} +1 -1
  62. package/lib/runner/maintenance.mjs +25 -14
  63. package/lib/runner/readiness.mjs +5 -4
  64. package/lib/runner/state-io.mjs +10 -4
  65. package/node_modules/@elench/next-analysis/package.json +1 -1
  66. package/node_modules/@elench/testkit-bridge/package.json +2 -2
  67. package/node_modules/@elench/testkit-protocol/package.json +1 -1
  68. package/node_modules/@elench/ts-analysis/package.json +1 -1
  69. package/package.json +7 -8
  70. package/lib/cli/assistant/protocol.mjs +0 -67
  71. package/lib/cli/assistant/tool-registry.mjs +0 -318
  72. package/lib/cli/command-helpers.mjs +0 -191
  73. package/lib/cli/presentation/tree-reporter.mjs +0 -96
  74. package/lib/cli/tui/inspect-artifact-adapter.mjs +0 -3
  75. package/lib/cli/tui/inspect-live-adapter.mjs +0 -15
  76. /package/lib/cli/{agents → assistant}/providers/shared.mjs +0 -0
  77. /package/lib/cli/{presentation/events-reporter.mjs → renderers/run/events.mjs} +0 -0
  78. /package/lib/cli/{presentation → terminal}/colors.mjs +0 -0
  79. /package/lib/cli/{presentation/terminal-layout.mjs → terminal/layout.mjs} +0 -0
  80. /package/lib/{cli/presentation → results}/code-frames.mjs +0 -0
@@ -0,0 +1,129 @@
1
+ import * as runner from "../../../runner/index.mjs";
2
+ import { loadManagedConfigs } from "../../../app/configs.mjs";
3
+ import {
4
+ parseFileTimeoutOption,
5
+ parseShardOption,
6
+ parseSuiteOption,
7
+ parseTypeOption,
8
+ parseWorkersOption,
9
+ resolveRequestedFiles,
10
+ } from "../../args.mjs";
11
+ import { createRunReporter } from "../../renderers/run/text-reporter.mjs";
12
+ import { createRunEventsReporter } from "../../renderers/run/events.mjs";
13
+ import { createRunSession, renderRunSessionView } from "../../renderers/run/interactive.mjs";
14
+ import { loadCliConfig } from "../../config.mjs";
15
+ import {
16
+ resolveRunOutputMode,
17
+ resolveTerminalCapabilities,
18
+ selectRunRenderer,
19
+ } from "../../terminal/capabilities.mjs";
20
+
21
+ export async function buildRunRequest(flags, positionalType = null, cwd = process.cwd(), invocationCwd = process.cwd()) {
22
+ const { allConfigs, configs } = await loadManagedConfigs({ dir: flags.dir, service: flags.service });
23
+ const workers = flags.workers == null ? null : parseWorkersOption(flags.workers);
24
+ const fileTimeoutSeconds =
25
+ flags["file-timeout-seconds"] == null
26
+ ? null
27
+ : parseFileTimeoutOption(flags["file-timeout-seconds"]);
28
+ const shard = parseShardOption(flags.shard);
29
+ const typeValues = parseTypeOption(flags.type, positionalType);
30
+ const suiteSelectors = parseSuiteOption(flags.suite);
31
+ const rawFileNames = Array.isArray(flags.file) ? flags.file : [flags.file].filter(Boolean);
32
+ const productDir = allConfigs[0]?.productDir || cwd;
33
+ const fileNames = resolveRequestedFiles(rawFileNames, productDir, invocationCwd);
34
+
35
+ return {
36
+ allConfigs,
37
+ configs,
38
+ productDir,
39
+ typeValues,
40
+ suiteSelectors,
41
+ runOptions: {
42
+ ...flags,
43
+ typeValues,
44
+ fileNames,
45
+ workers,
46
+ fileTimeoutSeconds,
47
+ shard,
48
+ scenarioSeed: flags.seed || null,
49
+ serviceFilter: flags.service || null,
50
+ writeStatus: flags["write-status"],
51
+ allowPartialStatus: flags["allow-partial-status"],
52
+ ignoreSkipRules: flags["ignore-skip-rules"],
53
+ },
54
+ };
55
+ }
56
+
57
+ export async function executeRunRequest(
58
+ request,
59
+ {
60
+ outputMode: requestedMode = "compact",
61
+ json = false,
62
+ debug = false,
63
+ terminal = {},
64
+ attachedRunSession = null,
65
+ } = {}
66
+ ) {
67
+ const capabilities = resolveTerminalCapabilities(terminal);
68
+ const outputMode = resolveRunOutputMode({
69
+ requestedMode,
70
+ json,
71
+ debug,
72
+ });
73
+ const renderer = selectRunRenderer(outputMode, capabilities);
74
+ const config = loadCliConfig(request.productDir);
75
+ let reporter;
76
+ let mountedView = null;
77
+ let runSession = attachedRunSession;
78
+
79
+ if (attachedRunSession) {
80
+ runSession = attachedRunSession;
81
+ reporter = runSession.reporter;
82
+ } else if (renderer === "tree") {
83
+ runSession = attachedRunSession || createRunSession({
84
+ productDir: request.productDir,
85
+ stderr: capabilities.stderr,
86
+ config,
87
+ });
88
+ reporter = runSession.reporter;
89
+ mountedView = renderRunSessionView(runSession, {
90
+ stdout: capabilities.stdout,
91
+ stderr: capabilities.stderr,
92
+ interactive: true,
93
+ });
94
+ } else if (renderer === "events") {
95
+ reporter = createRunEventsReporter({
96
+ stdout: capabilities.stdout,
97
+ stderr: capabilities.stderr,
98
+ });
99
+ } else {
100
+ reporter = createRunReporter({
101
+ outputMode,
102
+ stdout: capabilities.stdout,
103
+ stderr: capabilities.stderr,
104
+ });
105
+ }
106
+
107
+ try {
108
+ const result = await runner.runAll(
109
+ request.configs,
110
+ request.typeValues,
111
+ request.suiteSelectors,
112
+ {
113
+ reporter,
114
+ ...request.runOptions,
115
+ },
116
+ request.allConfigs
117
+ );
118
+ await mountedView?.finalize;
119
+ return {
120
+ outputMode,
121
+ runSession,
122
+ ...result,
123
+ };
124
+ } catch (error) {
125
+ mountedView?.close?.();
126
+ await mountedView?.finalize?.catch?.(() => {});
127
+ throw error;
128
+ }
129
+ }
@@ -0,0 +1,7 @@
1
+ import * as runner from "../../../runner/index.mjs";
2
+ import { loadManagedConfigs } from "../../../app/configs.mjs";
3
+
4
+ export async function executeStatusOperation(flags = {}) {
5
+ const { configs } = await loadManagedConfigs({ dir: flags.dir, service: flags.service });
6
+ return configs.map((config) => runner.showStatus(config));
7
+ }
@@ -0,0 +1,5 @@
1
+ import { runTestkitTypecheck } from "../../../app/typecheck.mjs";
2
+
3
+ export async function executeTypecheckOperation(flags = {}) {
4
+ return runTestkitTypecheck({ dir: flags.dir });
5
+ }
@@ -0,0 +1,6 @@
1
+ export function renderBrowserServeResult(result) {
2
+ return [
3
+ `testkit browser bridge serving ${result.productDir}`,
4
+ `Listening on ${result.url}`,
5
+ ];
6
+ }
@@ -0,0 +1,3 @@
1
+ export function renderCleanupResult(result) {
2
+ return Array.isArray(result?.lines) ? result.lines : [];
3
+ }
@@ -0,0 +1,3 @@
1
+ export function renderDatabaseSnapshotCaptureResult(result) {
2
+ return [`Wrote ${result.outputLabel}`];
3
+ }
@@ -0,0 +1,3 @@
1
+ export function renderDestroyResult(results) {
2
+ return (results || []).map((result) => `Destroyed ${result.name}`);
3
+ }
@@ -1,5 +1,5 @@
1
- import { formatDuration } from "../../runner/formatting.mjs";
2
- import { formatSelectionTypeLabel } from "../../discovery/index.mjs";
1
+ import { formatDuration } from "../../../runner/formatting.mjs";
2
+ import { formatSelectionTypeLabel } from "../../../discovery/index.mjs";
3
3
  import {
4
4
  bold,
5
5
  colorDiagnosticSeverity,
@@ -8,7 +8,7 @@ import {
8
8
  colorTypeBadge,
9
9
  muted,
10
10
  statusLabel,
11
- } from "./colors.mjs";
11
+ } from "../../terminal/colors.mjs";
12
12
 
13
13
  const TYPE_ORDER = ["int", "e2e", "scenario", "dal", "load", "pw"];
14
14
 
@@ -0,0 +1,7 @@
1
+ import { buildDiscoveryReportLines } from "./report.mjs";
2
+
3
+ export function renderDiscoverResult(result, options = {}) {
4
+ const lines = buildDiscoveryReportLines(result, { outputMode: options.outputMode || "compact" });
5
+ if (result.outputLabel) lines.push(`Wrote ${result.outputLabel}`);
6
+ return lines;
7
+ }
@@ -0,0 +1,7 @@
1
+ export function renderDoctorResult(result) {
2
+ const lines = [`testkit doctor ${result.ok ? "passed" : "failed"} for ${result.productDir}`];
3
+ for (const check of result.checks || []) {
4
+ lines.push(`${check.level.toUpperCase()} ${check.code} ${check.message}`);
5
+ }
6
+ return lines;
7
+ }
@@ -1,8 +1,8 @@
1
- import { buildFailurePresentation } from "../../runner/formatting.mjs";
2
- import { renderIndentedBlock } from "./terminal-layout.mjs";
1
+ import { buildFailurePresentation } from "../../../runner/formatting.mjs";
2
+ import { renderIndentedBlock } from "../../terminal/layout.mjs";
3
3
 
4
4
  export function renderFailureBlock(task, outcome, { width, regressionCatalog } = {}) {
5
- const presentation = buildFailurePresentation(
5
+ const failureView = buildFailurePresentation(
6
6
  {
7
7
  service: task.serviceName,
8
8
  type: normalizeRegressionType(task),
@@ -15,10 +15,10 @@ export function renderFailureBlock(task, outcome, { width, regressionCatalog } =
15
15
  );
16
16
 
17
17
  const lines = [];
18
- if (presentation.primary) {
19
- lines.push(...renderIndentedBlock(presentation.primary, { width, indent: " " }));
18
+ if (failureView.primary) {
19
+ lines.push(...renderIndentedBlock(failureView.primary, { width, indent: " " }));
20
20
  }
21
- for (const detail of presentation.details) {
21
+ for (const detail of failureView.details) {
22
22
  lines.push(...renderIndentedBlock(detail, { width, indent: " " }));
23
23
  }
24
24
  return lines;
@@ -0,0 +1,119 @@
1
+ import React, { createElement } from "react";
2
+ import { render } from "ink";
3
+ import { createRunState } from "../../state/run/state.mjs";
4
+ import { RunTreeView } from "../../components/blocks/run-tree.mjs";
5
+ import { suiteSelectionType } from "../../../runner/suite-selection.mjs";
6
+
7
+ export function createRunSession({
8
+ productDir,
9
+ dataSource = "live",
10
+ stderr = process.stderr,
11
+ onSnapshot,
12
+ config = {},
13
+ } = {}) {
14
+ const runState = createRunState({
15
+ dataSource,
16
+ autoCollapsePassedTreeBranches: config.autoCollapsePassedTreeBranches,
17
+ });
18
+ if (typeof onSnapshot === "function") {
19
+ runState.subscribe(() => {
20
+ onSnapshot(runState.getSnapshot());
21
+ });
22
+ }
23
+
24
+ const reporter = {
25
+ outputMode: "compact",
26
+
27
+ setServicePlans(plans) {
28
+ runState.initFromPlans(plans);
29
+ },
30
+
31
+ setTotalFileCount(count) {
32
+ runState.setTotalFileCount(count);
33
+ },
34
+
35
+ setRegressionCatalog(document) {
36
+ runState.setRegressionCatalog(document);
37
+ },
38
+
39
+ serviceSkipped(config, reason) {
40
+ runState.markServiceSkipped(config.name, reason);
41
+ },
42
+
43
+ plannedSkip(entry) {
44
+ runState.markPlannedSkip(entry);
45
+ },
46
+
47
+ taskStarted(task) {
48
+ const suiteKey = `${task.displayType || suiteSelectionType(task.type, task.framework)}:${task.suiteName}`;
49
+ runState.markFileRunning(task.serviceName, suiteKey, task.file);
50
+ },
51
+
52
+ taskFinished(task, outcome) {
53
+ runState.markFileFinished(task, outcome);
54
+ },
55
+
56
+ runtimeError(task, message) {
57
+ runState.markRuntimeError(task, message);
58
+ },
59
+
60
+ setupOperationFinished() {},
61
+
62
+ phaseStarted(label) {
63
+ runState.setPhase(label);
64
+ },
65
+
66
+ toolchainResolved() {},
67
+ localServiceStarting() {},
68
+ writeLine() {},
69
+ writeDebugLine() {},
70
+ telemetry() {},
71
+
72
+ runSummary(results, durationMs, regressionReport) {
73
+ runState.finish(results, durationMs, regressionReport);
74
+ },
75
+
76
+ error(message) {
77
+ stderr.write(`${message}\n`);
78
+ },
79
+ };
80
+
81
+ return {
82
+ productDir,
83
+ runState,
84
+ reporter,
85
+ getSnapshot() {
86
+ return runState.getSnapshot();
87
+ },
88
+ };
89
+ }
90
+
91
+ export function renderRunSessionView(
92
+ session,
93
+ {
94
+ stdout = process.stdout,
95
+ stderr = process.stderr,
96
+ interactive = true,
97
+ onRequestClose,
98
+ } = {}
99
+ ) {
100
+ const app = render(
101
+ createElement(RunTreeView, {
102
+ runState: session.runState,
103
+ stdout,
104
+ onRequestClose: close,
105
+ interactive,
106
+ }),
107
+ { stdout, stderr, exitOnCtrlC: false }
108
+ );
109
+
110
+ return {
111
+ finalize: app.waitUntilExit(),
112
+ close,
113
+ };
114
+
115
+ function close() {
116
+ app.unmount();
117
+ if (typeof onRequestClose === "function") onRequestClose();
118
+ }
119
+ }
@@ -3,11 +3,11 @@ import {
3
3
  buildRunSummaryData,
4
4
  buildDebugRunSummaryLines,
5
5
  formatDuration,
6
- } from "../../runner/formatting.mjs";
7
- import { boldRed, colorSectionLine, dim, statusLabel } from "./colors.mjs";
8
- import { renderFailureBlock } from "./failure-presentation.mjs";
9
- import { renderSummaryBox } from "./summary-box.mjs";
10
- import { getTerminalWidth } from "./terminal-layout.mjs";
6
+ } from "../../../runner/formatting.mjs";
7
+ import { boldRed, colorSectionLine, dim, statusLabel } from "../../terminal/colors.mjs";
8
+ import { renderFailureBlock } from "./failure.mjs";
9
+ import { renderSummaryBox } from "../../components/primitives/summary-box.mjs";
10
+ import { getTerminalWidth } from "../../terminal/layout.mjs";
11
11
 
12
12
  export function createRunReporter({ outputMode = "compact", stdout = process.stdout, stderr = process.stderr } = {}) {
13
13
  const mode = outputMode || "compact";
@@ -0,0 +1,7 @@
1
+ export function renderStatusResult(result) {
2
+ return normalizeLines(result?.lines);
3
+ }
4
+
5
+ function normalizeLines(lines) {
6
+ return Array.isArray(lines) ? lines : [];
7
+ }
@@ -0,0 +1,7 @@
1
+ export function renderTypecheckResult(result) {
2
+ const lines = [`Typechecked ${result.results.length} testkit program(s) in ${result.productDir}`];
3
+ for (const entry of result.results) {
4
+ lines.push(`PASS ${entry.label} ${entry.tsconfigPath}`);
5
+ }
6
+ return lines;
7
+ }
@@ -1,9 +1,7 @@
1
- import { fileDisplayName } from "../../discovery/index.mjs";
2
- import { suiteSelectionType } from "../../runner/suite-selection.mjs";
3
- import { formatDuration } from "../../runner/formatting.mjs";
4
- import { matchInspectEntry } from "./fuzzy-match.mjs";
5
-
6
- export const INSPECT_PANES = ["detail", "artifacts", "logs", "setup"];
1
+ import { fileDisplayName } from "../../../discovery/index.mjs";
2
+ import { suiteSelectionType } from "../../../runner/suite-selection.mjs";
3
+ import { formatDuration } from "../../../runner/formatting.mjs";
4
+ import { matchRunTreeEntry } from "../tree/fuzzy-match.mjs";
7
5
 
8
6
  export function buildSummaryRows({
9
7
  result,
@@ -41,9 +39,10 @@ export function buildSummaryRows({
41
39
  return rows;
42
40
  }
43
41
 
44
- export function createEmptyInspectModel(dataSource = "live") {
42
+ export function createEmptyRunModel(dataSource = "live", options = {}) {
45
43
  return {
46
44
  dataSource,
45
+ autoCollapsePassedTreeBranches: options.autoCollapsePassedTreeBranches !== false,
47
46
  services: new Map(),
48
47
  summaryData: null,
49
48
  regressionCatalog: null,
@@ -57,11 +56,10 @@ export function createEmptyInspectModel(dataSource = "live") {
57
56
  filterMatches: new Map(),
58
57
  collapsedOverrides: new Map(),
59
58
  selectedEntryId: null,
60
- paneMode: "detail",
61
59
  };
62
60
  }
63
61
 
64
- export function resetInspectModel(model, dataSource = model.dataSource) {
62
+ export function resetRunModel(model, dataSource = model.dataSource) {
65
63
  model.dataSource = dataSource;
66
64
  model.services = new Map();
67
65
  model.summaryData = null;
@@ -76,7 +74,6 @@ export function resetInspectModel(model, dataSource = model.dataSource) {
76
74
  model.filterMatches = new Map();
77
75
  model.collapsedOverrides = new Map();
78
76
  model.selectedEntryId = null;
79
- model.paneMode = "detail";
80
77
  }
81
78
 
82
79
  export function initModelFromPlans(model, servicePlans) {
@@ -124,7 +121,7 @@ export function initModelFromPlans(model, servicePlans) {
124
121
  }
125
122
 
126
123
  export function applyArtifactToModel(model, artifact) {
127
- resetInspectModel(model, artifact?.run?.status === "running" ? "live" : "artifact");
124
+ resetRunModel(model, artifact?.run?.status === "running" ? "live" : "artifact");
128
125
  model.runArtifact = artifact;
129
126
  model.finished = artifact?.run?.status !== "running";
130
127
  model.phase = model.finished ? "run complete" : "live artifact";
@@ -287,24 +284,13 @@ export function updateFilter(model, query) {
287
284
  const normalizedQuery = model.filterQuery.trim();
288
285
  if (!normalizedQuery) return;
289
286
  for (const entry of collectAllEntries(model)) {
290
- const match = matchInspectEntry(normalizedQuery, entry);
287
+ const match = matchRunTreeEntry(normalizedQuery, entry);
291
288
  if (match.matched) {
292
289
  model.filterMatches.set(entry.id, match);
293
290
  }
294
291
  }
295
292
  }
296
293
 
297
- export function cyclePane(model) {
298
- const index = INSPECT_PANES.indexOf(model.paneMode);
299
- model.paneMode = INSPECT_PANES[(index + 1) % INSPECT_PANES.length];
300
- }
301
-
302
- export function setPane(model, paneMode) {
303
- if (INSPECT_PANES.includes(paneMode)) {
304
- model.paneMode = paneMode;
305
- }
306
- }
307
-
308
294
  export function toggleCollapsed(model, entryId) {
309
295
  const entry = getEntryById(model, entryId);
310
296
  if (!entry || (entry.kind !== "type" && entry.kind !== "suite")) return;
@@ -350,7 +336,6 @@ export function buildSnapshot(model) {
350
336
  finished: model.finished,
351
337
  summaryData: model.summaryData,
352
338
  regressionCatalog: model.regressionCatalog,
353
- paneMode: model.paneMode,
354
339
  filter: {
355
340
  active: model.filterActive,
356
341
  query: model.filterQuery,
@@ -413,7 +398,7 @@ function buildNestedServices(model) {
413
398
  const suites = [...typeNode.suites.values()].map((suite) => {
414
399
  const files = [...suite.files.values()];
415
400
  const summary = summarizeFiles(files);
416
- const autoCollapsed = summary.total > 0 && (summary.passed === summary.total || summary.skipped === summary.total);
401
+ const autoCollapsed = model.autoCollapsePassedTreeBranches && summary.total > 0 && (summary.passed === summary.total || summary.skipped === summary.total);
417
402
  return {
418
403
  id: suite.id,
419
404
  kind: "suite",
@@ -430,7 +415,7 @@ function buildNestedServices(model) {
430
415
  };
431
416
  });
432
417
  const summary = summarizeNestedEntries(suites.map((suite) => suite.summary));
433
- const autoCollapsed = suites.length > 0 && suites.every((suite) => suite.collapsed);
418
+ const autoCollapsed = model.autoCollapsePassedTreeBranches && suites.length > 0 && suites.every((suite) => suite.collapsed);
434
419
  return {
435
420
  id: typeNode.id,
436
421
  kind: "type",
@@ -1,8 +1,7 @@
1
1
  import {
2
2
  applyArtifactToModel,
3
3
  buildSnapshot,
4
- createEmptyInspectModel,
5
- cyclePane,
4
+ createEmptyRunModel,
6
5
  findEntryIdForFile,
7
6
  findEntryIdForService,
8
7
  finishModel,
@@ -12,18 +11,17 @@ import {
12
11
  markPlannedSkip,
13
12
  markRuntimeError,
14
13
  markServiceSkipped,
15
- resetInspectModel,
14
+ resetRunModel,
16
15
  revealEntry,
17
- setPane,
18
16
  setPhase,
19
17
  setRegressionCatalog,
20
18
  setTotalFileCount,
21
19
  toggleCollapsed,
22
20
  updateFilter,
23
- } from "./inspect-model.mjs";
21
+ } from "./model.mjs";
24
22
 
25
- export function createInspectState({ dataSource = "live" } = {}) {
26
- const model = createEmptyInspectModel(dataSource);
23
+ export function createRunState({ dataSource = "live", autoCollapsePassedTreeBranches = true } = {}) {
24
+ const model = createEmptyRunModel(dataSource, { autoCollapsePassedTreeBranches });
27
25
  let notice = null;
28
26
  const listeners = new Set();
29
27
 
@@ -59,11 +57,16 @@ export function createInspectState({ dataSource = "live" } = {}) {
59
57
  },
60
58
 
61
59
  resetForLive() {
62
- resetInspectModel(model, "live");
60
+ resetRunModel(model, "live");
63
61
  notice = null;
64
62
  notify();
65
63
  },
66
64
 
65
+ setAutoCollapsePassedTreeBranches(enabled) {
66
+ model.autoCollapsePassedTreeBranches = Boolean(enabled);
67
+ notify();
68
+ },
69
+
67
70
  setRegressionCatalog(document) {
68
71
  setRegressionCatalog(model, document);
69
72
  notify();
@@ -177,16 +180,6 @@ export function createInspectState({ dataSource = "live" } = {}) {
177
180
  return true;
178
181
  },
179
182
 
180
- cyclePaneMode() {
181
- cyclePane(model);
182
- notify();
183
- },
184
-
185
- setPaneMode(paneMode) {
186
- setPane(model, paneMode);
187
- notify();
188
- },
189
-
190
183
  subscribe(callback) {
191
184
  listeners.add(callback);
192
185
  return () => listeners.delete(callback);
@@ -44,7 +44,7 @@ export function fuzzyMatch(query, candidate) {
44
44
  return { matched: false, score: Number.NEGATIVE_INFINITY, positions: [] };
45
45
  }
46
46
 
47
- export function matchInspectEntry(query, entry) {
47
+ export function matchRunTreeEntry(query, entry) {
48
48
  const fields = buildEntryMatchFields(entry);
49
49
  let best = null;
50
50
 
@@ -0,0 +1,33 @@
1
+ export function resolveTerminalCapabilities({
2
+ stdout = process.stdout,
3
+ stderr = process.stderr,
4
+ stdin = process.stdin,
5
+ env = process.env,
6
+ forceInteractive = false,
7
+ } = {}) {
8
+ const interactiveOutput = Boolean(stdout?.isTTY || forceInteractive);
9
+ const interactiveInput = Boolean(stdin?.isTTY);
10
+ return {
11
+ stdout,
12
+ stderr,
13
+ stdin,
14
+ env,
15
+ forceInteractive,
16
+ interactiveOutput,
17
+ interactiveInput,
18
+ colorEnabled: Boolean(interactiveOutput || env?.FORCE_COLOR),
19
+ width: stdout?.columns || process.stdout?.columns || 100,
20
+ };
21
+ }
22
+
23
+ export function resolveRunOutputMode({ requestedMode = "compact", json = false, debug = false } = {}) {
24
+ if (json) return "json";
25
+ if (debug) return "debug";
26
+ return requestedMode || "compact";
27
+ }
28
+
29
+ export function selectRunRenderer(outputMode, capabilities) {
30
+ if (outputMode === "events") return "events";
31
+ if (outputMode === "compact" && capabilities?.interactiveOutput) return "tree";
32
+ return "text";
33
+ }
@@ -26,6 +26,7 @@ import {
26
26
  visitDirs as visitDirsModel,
27
27
  } from "./state.mjs";
28
28
  import { captureTemplateSnapshot, runTemplateStage } from "./template-steps.mjs";
29
+ import { collectStateDirLines } from "../runner/state-io.mjs";
29
30
 
30
31
  const LOCAL_IMAGE = "pgvector/pgvector:pg16";
31
32
  const LOCAL_USER = "testkit";
@@ -154,22 +155,23 @@ export function isDatabaseStateDir(dir) {
154
155
  return fs.existsSync(path.join(dir, "database_backend"));
155
156
  }
156
157
 
157
- export function showServiceDatabaseStatus(productDir, serviceName) {
158
+ export function collectServiceDatabaseStatus(productDir, serviceName) {
158
159
  const cacheDir = getLocalServiceCacheDir(productDir, serviceName);
159
160
  const infraDir = getLocalInfraDir(productDir);
160
- if (!fs.existsSync(cacheDir) && !fs.existsSync(infraDir)) return false;
161
+ if (!fs.existsSync(cacheDir) && !fs.existsSync(infraDir)) return [];
161
162
 
163
+ const lines = [];
162
164
  if (fs.existsSync(cacheDir)) {
163
- console.log(" database-cache/");
164
- printStateDir(cacheDir, " ");
165
+ lines.push(" database-cache/");
166
+ lines.push(...collectStateDirLines(cacheDir, " "));
165
167
  }
166
168
 
167
169
  if (fs.existsSync(infraDir)) {
168
- console.log(" database-infra/");
169
- printStateDir(infraDir, " ");
170
+ lines.push(" database-infra/");
171
+ lines.push(...collectStateDirLines(infraDir, " "));
170
172
  }
171
173
 
172
- return true;
174
+ return lines;
173
175
  }
174
176
 
175
177
  async function prepareLocalDatabase(config, options = {}) {
@@ -664,20 +666,6 @@ function visitDirs(root, visitor) {
664
666
  return visitDirsModel(root, visitor);
665
667
  }
666
668
 
667
- function printStateDir(dir, indent) {
668
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
669
- const filePath = path.join(dir, entry.name);
670
- if (entry.isDirectory()) {
671
- console.log(`${indent}${entry.name}/`);
672
- printStateDir(filePath, `${indent} `);
673
- continue;
674
- }
675
- const value =
676
- entry.name === "password" ? "********" : fs.readFileSync(filePath, "utf8").trim();
677
- console.log(`${indent}${entry.name}: ${value}`);
678
- }
679
- }
680
-
681
669
  async function withLock(lockPath, fn) {
682
670
  const lockDir = `${lockPath}.dir`;
683
671
  const timeoutMs = 60_000;
@@ -6,7 +6,7 @@ import {
6
6
  findFailureLocation,
7
7
  formatLocation,
8
8
  renderCodeFrame,
9
- } from "./presentation/code-frames.mjs";
9
+ } from "./code-frames.mjs";
10
10
 
11
11
  export function loadLatestRunArtifact(productDir) {
12
12
  const artifactPath = path.join(productDir, ".testkit", "results", "latest.json");