@elench/testkit 0.1.97 → 0.1.98

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 (74) hide show
  1. package/lib/app/browser-bridge.mjs +1 -1
  2. package/lib/cli/assistant/app.mjs +25 -1
  3. package/lib/cli/assistant/composer.mjs +1 -1
  4. package/lib/cli/assistant/context-pack.mjs +4 -4
  5. package/lib/cli/assistant/interactive.mjs +1 -1
  6. package/lib/cli/assistant/prompt-builder.mjs +2 -2
  7. package/lib/cli/{agents → assistant/providers}/index.mjs +3 -3
  8. package/lib/cli/assistant/session.mjs +5 -5
  9. package/lib/cli/assistant/slash-commands.mjs +22 -1
  10. package/lib/cli/assistant/state.mjs +148 -75
  11. package/lib/cli/assistant/tool-registry.mjs +305 -39
  12. package/lib/cli/assistant/view-model.mjs +1 -1
  13. package/lib/cli/commands/assistant.mjs +4 -3
  14. package/lib/cli/commands/browser/serve.mjs +5 -23
  15. package/lib/cli/commands/cleanup.mjs +8 -2
  16. package/lib/cli/commands/db/snapshot/capture.mjs +8 -4
  17. package/lib/cli/commands/destroy.mjs +8 -2
  18. package/lib/cli/commands/discover.mjs +5 -27
  19. package/lib/cli/commands/doctor.mjs +5 -5
  20. package/lib/cli/commands/flags.mjs +61 -0
  21. package/lib/cli/commands/run.mjs +10 -2
  22. package/lib/cli/commands/status.mjs +10 -2
  23. package/lib/cli/commands/typecheck.mjs +5 -5
  24. package/lib/cli/{tui/inspect-app.mjs → components/blocks/run-tree.mjs} +29 -54
  25. package/lib/cli/{tui → components/primitives}/filter-bar.mjs +1 -1
  26. package/lib/cli/{presentation → components/primitives}/summary-box.mjs +1 -1
  27. package/lib/cli/config.mjs +63 -0
  28. package/lib/cli/operations/browser/serve/operation.mjs +23 -0
  29. package/lib/cli/operations/cleanup/operation.mjs +8 -0
  30. package/lib/cli/{db.mjs → operations/db/snapshot/capture/operation.mjs} +15 -9
  31. package/lib/cli/operations/destroy/operation.mjs +12 -0
  32. package/lib/cli/operations/discover/operation.mjs +32 -0
  33. package/lib/cli/operations/doctor/operation.mjs +5 -0
  34. package/lib/cli/operations/run/operation.mjs +129 -0
  35. package/lib/cli/operations/status/operation.mjs +7 -0
  36. package/lib/cli/operations/typecheck/operation.mjs +5 -0
  37. package/lib/cli/renderers/browser-serve/text.mjs +6 -0
  38. package/lib/cli/renderers/cleanup/text.mjs +3 -0
  39. package/lib/cli/renderers/db-snapshot-capture/text.mjs +3 -0
  40. package/lib/cli/renderers/destroy/text.mjs +3 -0
  41. package/lib/cli/{presentation/discovery-reporter.mjs → renderers/discover/report.mjs} +3 -3
  42. package/lib/cli/renderers/discover/text.mjs +7 -0
  43. package/lib/cli/renderers/doctor/text.mjs +7 -0
  44. package/lib/cli/{presentation/failure-presentation.mjs → renderers/run/failure.mjs} +6 -6
  45. package/lib/cli/renderers/run/interactive.mjs +119 -0
  46. package/lib/cli/{presentation/run-reporter.mjs → renderers/run/text-reporter.mjs} +5 -5
  47. package/lib/cli/renderers/status/text.mjs +7 -0
  48. package/lib/cli/renderers/typecheck/text.mjs +7 -0
  49. package/lib/cli/{tui/inspect-model.mjs → state/run/model.mjs} +11 -26
  50. package/lib/cli/{tui/inspect-state.mjs → state/run/state.mjs} +11 -18
  51. package/lib/cli/{tui → state/tree}/fuzzy-match.mjs +1 -1
  52. package/lib/cli/terminal/capabilities.mjs +33 -0
  53. package/lib/database/index.mjs +9 -21
  54. package/lib/{cli/viewer.mjs → results/artifacts.mjs} +1 -1
  55. package/lib/{cli/context-resources.mjs → results/context.mjs} +1 -1
  56. package/lib/runner/maintenance.mjs +25 -14
  57. package/lib/runner/readiness.mjs +5 -4
  58. package/lib/runner/state-io.mjs +10 -4
  59. package/node_modules/@elench/next-analysis/package.json +1 -1
  60. package/node_modules/@elench/testkit-bridge/package.json +2 -2
  61. package/node_modules/@elench/testkit-protocol/package.json +1 -1
  62. package/node_modules/@elench/ts-analysis/package.json +1 -1
  63. package/package.json +6 -7
  64. package/lib/cli/command-helpers.mjs +0 -191
  65. package/lib/cli/presentation/tree-reporter.mjs +0 -96
  66. package/lib/cli/tui/inspect-artifact-adapter.mjs +0 -3
  67. package/lib/cli/tui/inspect-live-adapter.mjs +0 -15
  68. /package/lib/cli/{agents → assistant}/providers/claude.mjs +0 -0
  69. /package/lib/cli/{agents → assistant}/providers/codex.mjs +0 -0
  70. /package/lib/cli/{agents → assistant}/providers/shared.mjs +0 -0
  71. /package/lib/cli/{presentation/events-reporter.mjs → renderers/run/events.mjs} +0 -0
  72. /package/lib/cli/{presentation → terminal}/colors.mjs +0 -0
  73. /package/lib/cli/{presentation/terminal-layout.mjs → terminal/layout.mjs} +0 -0
  74. /package/lib/{cli/presentation → results}/code-frames.mjs +0 -0
@@ -0,0 +1,61 @@
1
+ import { Flags } from "@oclif/core";
2
+
3
+ export const sharedFlags = {
4
+ dir: Flags.string({
5
+ description: "Explicit product directory",
6
+ }),
7
+ service: Flags.string({
8
+ description: "Limit the operation or assistant context to one service",
9
+ }),
10
+ };
11
+
12
+ export const runFlags = {
13
+ ...sharedFlags,
14
+ type: Flags.string({
15
+ char: "t",
16
+ multiple: true,
17
+ description: "Run specific suite type(s): int, e2e, scenario, dal, load, pw, all",
18
+ }),
19
+ suite: Flags.string({
20
+ char: "s",
21
+ multiple: true,
22
+ description: "Run specific suite(s)",
23
+ }),
24
+ file: Flags.string({
25
+ char: "f",
26
+ multiple: true,
27
+ description: "Run specific file(s)",
28
+ }),
29
+ workers: Flags.string({
30
+ description: "Number of test executors for the whole run",
31
+ }),
32
+ "file-timeout-seconds": Flags.string({
33
+ description: "Per-file wall-clock timeout in seconds",
34
+ }),
35
+ shard: Flags.string({
36
+ description: "Run only shard i of n at suite granularity",
37
+ }),
38
+ seed: Flags.string({
39
+ description: "Deterministic seed for scenario suites",
40
+ }),
41
+ "write-status": Flags.boolean({
42
+ description: "Write a deterministic testkit.status.json snapshot",
43
+ default: false,
44
+ }),
45
+ "allow-partial-status": Flags.boolean({
46
+ description: "Allow --write-status for filtered runs",
47
+ default: false,
48
+ }),
49
+ "ignore-skip-rules": Flags.boolean({
50
+ description: "Run files even if testkit.config.ts marks them skipped",
51
+ default: false,
52
+ }),
53
+ "output-mode": Flags.string({
54
+ description: "Reporter mode",
55
+ options: ["compact", "debug", "events"],
56
+ }),
57
+ debug: Flags.boolean({
58
+ description: "Alias for --output-mode debug",
59
+ default: false,
60
+ }),
61
+ };
@@ -1,5 +1,7 @@
1
1
  import { Args, Command } from "@oclif/core";
2
- import { executeRunCommand, runFlags } from "../command-helpers.mjs";
2
+ import { runFlags } from "./flags.mjs";
3
+ import { buildRunRequest, executeRunRequest } from "../operations/run/operation.mjs";
4
+ import { resolveTerminalCapabilities } from "../terminal/capabilities.mjs";
3
5
 
4
6
  export default class RunCommand extends Command {
5
7
  static summary = "Run test suites";
@@ -18,6 +20,12 @@ export default class RunCommand extends Command {
18
20
 
19
21
  async run() {
20
22
  const { args, flags } = await this.parse(RunCommand);
21
- return executeRunCommand(this, flags, args.type || null);
23
+ const request = await buildRunRequest(flags, args.type || null, process.cwd(), process.cwd());
24
+ return executeRunRequest(request, {
25
+ outputMode: flags["output-mode"] || "compact",
26
+ json: this.jsonEnabled(),
27
+ debug: flags.debug,
28
+ terminal: resolveTerminalCapabilities(),
29
+ });
22
30
  }
23
31
  }
@@ -1,5 +1,7 @@
1
1
  import { Command } from "@oclif/core";
2
- import { runStatusLike, sharedFlags } from "../command-helpers.mjs";
2
+ import { sharedFlags } from "./flags.mjs";
3
+ import { executeStatusOperation } from "../operations/status/operation.mjs";
4
+ import { renderStatusResult } from "../renderers/status/text.mjs";
3
5
 
4
6
  export default class StatusCommand extends Command {
5
7
  static summary = "Show local testkit state";
@@ -10,6 +12,12 @@ export default class StatusCommand extends Command {
10
12
 
11
13
  async run() {
12
14
  const { flags } = await this.parse(StatusCommand);
13
- return runStatusLike("status", flags);
15
+ const results = await executeStatusOperation(flags);
16
+ if (!this.jsonEnabled()) {
17
+ for (const result of results) {
18
+ for (const line of renderStatusResult(result)) this.log(line);
19
+ }
20
+ }
21
+ return { ok: true, results };
14
22
  }
15
23
  }
@@ -1,5 +1,6 @@
1
1
  import { Command, Flags } from "@oclif/core";
2
- import { runTestkitTypecheck } from "../../app/typecheck.mjs";
2
+ import { executeTypecheckOperation } from "../operations/typecheck/operation.mjs";
3
+ import { renderTypecheckResult } from "../renderers/typecheck/text.mjs";
3
4
 
4
5
  export default class TypecheckCommand extends Command {
5
6
  static summary = "Typecheck testkit config, helpers, and suites";
@@ -14,12 +15,11 @@ export default class TypecheckCommand extends Command {
14
15
 
15
16
  async run() {
16
17
  const { flags } = await this.parse(TypecheckCommand);
17
- const result = await runTestkitTypecheck({ dir: flags.dir });
18
+ const result = await executeTypecheckOperation(flags);
18
19
 
19
20
  if (!this.jsonEnabled()) {
20
- this.log(`Typechecked ${result.results.length} testkit program(s) in ${result.productDir}`);
21
- for (const entry of result.results) {
22
- this.log(`PASS ${entry.label} ${entry.tsconfigPath}`);
21
+ for (const line of renderTypecheckResult(result)) {
22
+ this.log(line);
23
23
  }
24
24
  }
25
25
 
@@ -1,7 +1,7 @@
1
1
  import React, { createElement, useEffect, useMemo, useState } from "react";
2
2
  import { Box, Text, useAnimation, useApp, useInput } from "ink";
3
3
  import figures from "figures";
4
- import { formatDuration } from "../../runner/formatting.mjs";
4
+ import { formatDuration } from "../../../runner/formatting.mjs";
5
5
  import {
6
6
  bold,
7
7
  colorService,
@@ -10,61 +10,60 @@ import {
10
10
  green,
11
11
  red,
12
12
  yellow,
13
- } from "../presentation/colors.mjs";
14
- import { renderSummaryBox } from "../presentation/summary-box.mjs";
15
- import { getTerminalWidth } from "../presentation/terminal-layout.mjs";
16
- import { readContextContent } from "../context-resources.mjs";
17
- import { applyHighlight } from "./fuzzy-match.mjs";
18
- import { FilterBar } from "./filter-bar.mjs";
13
+ } from "../../terminal/colors.mjs";
14
+ import { renderSummaryBox } from "../primitives/summary-box.mjs";
15
+ import { applyHighlight } from "../../state/tree/fuzzy-match.mjs";
16
+ import { FilterBar } from "../primitives/filter-bar.mjs";
19
17
 
20
18
  const SPINNER_FRAMES = ["|", "/", "-", "\\"];
21
19
 
22
- export function InspectApp({
23
- inspectState,
20
+ export function RunTreeView({
21
+ runState,
24
22
  stdout,
25
- productDir,
26
23
  onRequestClose,
24
+ interactive = true,
27
25
  } = {}) {
28
26
  const { exit } = useApp();
29
- const [snapshot, setSnapshot] = useState(() => inspectState.getSnapshot());
27
+ const [snapshot, setSnapshot] = useState(() => runState.getSnapshot());
30
28
  const { frame } = useAnimation({ interval: 80, isActive: !snapshot.finished });
31
29
  const spinnerFrame = SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
32
30
 
33
31
  useEffect(() => {
34
- const unsubscribe = inspectState.subscribe(() => {
35
- setSnapshot(inspectState.getSnapshot());
32
+ const unsubscribe = runState.subscribe(() => {
33
+ setSnapshot(runState.getSnapshot());
36
34
  });
37
35
  return unsubscribe;
38
- }, [inspectState]);
36
+ }, [runState]);
39
37
 
40
38
  useInput((input, key) => {
39
+ if (!interactive) return;
41
40
  if (!snapshot.finished) {
42
41
  if (input === "q") {
43
- inspectState.setNotice("Run is still in progress. Wait for completion before closing.");
42
+ runState.setNotice("Run is still in progress. Wait for completion before closing.");
44
43
  }
45
44
  return;
46
45
  }
47
46
 
48
47
  if (snapshot.filter.active) {
49
48
  if (key.escape) {
50
- inspectState.deactivateFilter();
49
+ runState.deactivateFilter();
51
50
  return;
52
51
  }
53
52
  if (key.return) return;
54
53
  if (key.downArrow || input === "j") {
55
- inspectState.moveCursorDown();
54
+ runState.moveCursorDown();
56
55
  return;
57
56
  }
58
57
  if (key.upArrow || input === "k") {
59
- inspectState.moveCursorUp();
58
+ runState.moveCursorUp();
60
59
  return;
61
60
  }
62
61
  if (key.backspace || key.delete) {
63
- inspectState.updateFilterQuery(snapshot.filter.query.slice(0, -1));
62
+ runState.updateFilterQuery(snapshot.filter.query.slice(0, -1));
64
63
  return;
65
64
  }
66
65
  if (isPrintableInput(input, key)) {
67
- inspectState.updateFilterQuery(`${snapshot.filter.query}${input}`);
66
+ runState.updateFilterQuery(`${snapshot.filter.query}${input}`);
68
67
  }
69
68
  return;
70
69
  }
@@ -74,43 +73,27 @@ export function InspectApp({
74
73
  return;
75
74
  }
76
75
  if (input === "/") {
77
- inspectState.activateFilter();
76
+ runState.activateFilter();
78
77
  return;
79
78
  }
80
79
  if (key.downArrow || input === "j") {
81
- inspectState.moveCursorDown();
80
+ runState.moveCursorDown();
82
81
  return;
83
82
  }
84
83
  if (key.upArrow || input === "k") {
85
- inspectState.moveCursorUp();
84
+ runState.moveCursorUp();
86
85
  return;
87
86
  }
88
87
  if (key.return) {
89
- inspectState.toggleExpand();
88
+ runState.toggleExpand();
90
89
  return;
91
90
  }
92
- if (key.tab) {
93
- inspectState.cyclePaneMode();
94
- return;
95
- }
96
- });
91
+ }, { isActive: interactive });
97
92
 
98
- const terminalWidth = getTerminalWidth(stdout, 100);
99
- const leftWidth = Math.max(42, Math.floor(terminalWidth * 0.52));
100
- const rightWidth = Math.max(28, terminalWidth - leftWidth - 1);
101
93
  const visibleTreeEntries = useMemo(
102
- () => buildTreeViewport(snapshot.visibleEntries, snapshot.selectedEntryId, 17),
94
+ () => buildTreeViewport(snapshot.visibleEntries, snapshot.selectedEntryId, 24),
103
95
  [snapshot.visibleEntries, snapshot.selectedEntryId]
104
96
  );
105
- const paneContent = useMemo(
106
- () =>
107
- readContextContent({
108
- productDir,
109
- snapshot,
110
- mode: snapshot.paneMode,
111
- }),
112
- [productDir, snapshot]
113
- );
114
97
  const summaryLines = snapshot.finished && snapshot.summaryData
115
98
  ? renderSummaryBox(snapshot.summaryData.rows, { stdout })
116
99
  : [];
@@ -122,15 +105,8 @@ export function InspectApp({
122
105
  snapshot.notice ? createElement(Text, { key: "notice" }, yellow(snapshot.notice)) : null,
123
106
  createElement(
124
107
  Box,
125
- { key: "main", marginTop: 1, flexDirection: "row" },
126
- createElement(Box, { width: leftWidth, flexDirection: "column", paddingRight: 1 }, ...visibleTreeEntries.map(renderTreeLine.bind(null, snapshot, spinnerFrame))),
127
- createElement(
128
- Box,
129
- { width: rightWidth, flexDirection: "column", paddingLeft: 1 },
130
- createElement(Text, { key: "pane-title" }, bold(paneContent.title)),
131
- createElement(Text, { key: "pane-gap" }, ""),
132
- ...paneContent.lines.slice(0, 34).map((line, index) => createElement(Text, { key: `pane-${index}` }, line))
133
- )
108
+ { key: "main", marginTop: 1, flexDirection: "column" },
109
+ ...visibleTreeEntries.map(renderTreeLine.bind(null, snapshot, spinnerFrame))
134
110
  ),
135
111
  snapshot.filter.active ? createElement(Text, { key: "filter-gap" }, "") : null,
136
112
  snapshot.filter.active ? createElement(FilterBar, { key: "filter-bar", filter: snapshot.filter }) : null,
@@ -144,7 +120,7 @@ export function InspectApp({
144
120
  export function buildHeaderText(snapshot) {
145
121
  const progressText = snapshot.totalCount > 0 ? `[${snapshot.completedCount}/${snapshot.totalCount}]` : "[0/0]";
146
122
  const phaseText = snapshot.phase || (snapshot.finished ? "run complete" : "preparing");
147
- const sourceText = snapshot.dataSource === "artifact" ? "artifact inspect" : snapshot.finished ? "live summary" : "live run";
123
+ const sourceText = snapshot.dataSource === "artifact" ? "artifact run" : snapshot.finished ? "live summary" : "live run";
148
124
  const filterText = snapshot.filter.active ? `filter ${snapshot.filter.count}` : null;
149
125
  return [progressText, phaseText, sourceText, filterText].filter(Boolean).join(" · ");
150
126
  }
@@ -154,8 +130,7 @@ export function buildFooterText(snapshot) {
154
130
  if (snapshot.filter.active) {
155
131
  return "type to filter · ↑/↓ move · Esc clear filter · q quit";
156
132
  }
157
- const inspectKeys = "↑/↓ move · Enter collapse/expand · Tab cycle pane · / filter";
158
- return `${inspectKeys} · q quit`;
133
+ return "↑/↓ move · Enter collapse/expand · / filter · q quit";
159
134
  }
160
135
 
161
136
  function buildTreeViewport(entries, selectedEntryId, radius) {
@@ -1,6 +1,6 @@
1
1
  import React, { createElement } from "react";
2
2
  import { Text } from "ink";
3
- import { bold, dim } from "../presentation/colors.mjs";
3
+ import { bold, dim } from "../../terminal/colors.mjs";
4
4
 
5
5
  export function FilterBar({ filter } = {}) {
6
6
  if (!filter?.active) return null;
@@ -1,5 +1,5 @@
1
1
  import figures from "figures";
2
- import { clamp, getTerminalWidth, measureWidth, padEndVisible, wrapText } from "./terminal-layout.mjs";
2
+ import { clamp, getTerminalWidth, measureWidth, padEndVisible, wrapText } from "../../terminal/layout.mjs";
3
3
 
4
4
  export function renderSummaryBox(
5
5
  rows,
@@ -0,0 +1,63 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ export const DEFAULT_CLI_CONFIG = Object.freeze({
5
+ autoCollapsePassedTreeBranches: true,
6
+ });
7
+
8
+ export function cliConfigPath(productDir) {
9
+ return path.join(productDir || process.cwd(), ".testkit", "config.json");
10
+ }
11
+
12
+ export function loadCliConfig(productDir) {
13
+ try {
14
+ return normalizeCliConfig(JSON.parse(fs.readFileSync(cliConfigPath(productDir), "utf8")));
15
+ } catch {
16
+ return { ...DEFAULT_CLI_CONFIG };
17
+ }
18
+ }
19
+
20
+ export function saveCliConfig(productDir, config) {
21
+ const filePath = cliConfigPath(productDir);
22
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
23
+ fs.writeFileSync(filePath, `${JSON.stringify(normalizeCliConfig(config), null, 2)}\n`, "utf8");
24
+ }
25
+
26
+ export function resetCliConfig(productDir) {
27
+ fs.rmSync(cliConfigPath(productDir), { force: true });
28
+ return { ...DEFAULT_CLI_CONFIG };
29
+ }
30
+
31
+ export function mergeCliConfig(...configs) {
32
+ let merged = { ...DEFAULT_CLI_CONFIG };
33
+ for (const config of configs) {
34
+ if (!config || typeof config !== "object") continue;
35
+ merged = normalizeCliConfig({ ...merged, ...dropUndefined(config) });
36
+ }
37
+ return merged;
38
+ }
39
+
40
+ export function normalizeCliConfig(config = {}) {
41
+ return {
42
+ autoCollapsePassedTreeBranches:
43
+ config.autoCollapsePassedTreeBranches == null
44
+ ? DEFAULT_CLI_CONFIG.autoCollapsePassedTreeBranches
45
+ : Boolean(config.autoCollapsePassedTreeBranches),
46
+ };
47
+ }
48
+
49
+ export function formatCliConfig(config) {
50
+ const normalized = normalizeCliConfig(config);
51
+ return [
52
+ `autoCollapsePassedTreeBranches: ${normalized.autoCollapsePassedTreeBranches}`,
53
+ `path: .testkit/config.json`,
54
+ ].join("\n");
55
+ }
56
+
57
+ function dropUndefined(value) {
58
+ const result = {};
59
+ for (const [key, entry] of Object.entries(value)) {
60
+ if (entry !== undefined) result[key] = entry;
61
+ }
62
+ return result;
63
+ }
@@ -0,0 +1,23 @@
1
+ import { startBrowserBridgeServer } from "@elench/testkit-bridge";
2
+ import { loadBrowserBridgeContext } from "../../../../app/browser-bridge.mjs";
3
+
4
+ export async function executeBrowserServeOperation(flags = {}) {
5
+ const { productDir, context } = await loadBrowserBridgeContext({ dir: flags.dir });
6
+
7
+ const adapter = {
8
+ loadProductContext: async () => context,
9
+ };
10
+
11
+ const serverRef = await startBrowserBridgeServer(adapter, {
12
+ host: flags.host,
13
+ port: flags.port,
14
+ });
15
+
16
+ return {
17
+ ok: true,
18
+ productDir,
19
+ host: serverRef.host,
20
+ port: serverRef.port,
21
+ url: serverRef.url,
22
+ };
23
+ }
@@ -0,0 +1,8 @@
1
+ import * as runner from "../../../runner/index.mjs";
2
+ import { loadManagedConfigs } from "../../../app/configs.mjs";
3
+
4
+ export async function executeCleanupOperation(flags = {}) {
5
+ const { allConfigs } = await loadManagedConfigs({ dir: flags.dir, service: flags.service });
6
+ const productDir = allConfigs[0]?.productDir || process.cwd();
7
+ return runner.cleanup(productDir);
8
+ }
@@ -1,15 +1,15 @@
1
1
  import fs from "fs";
2
2
  import os from "os";
3
3
  import path from "path";
4
- import { loadManagedConfigs, resolveTargetConfig, collectRequiredConfigs, topologicallySortConfigs } from "../app/configs.mjs";
5
- import { resolveProductDir } from "../config/index.mjs";
6
- import { captureDatabaseTemplateSnapshot, prepareDatabaseRuntime } from "../database/index.mjs";
7
- import { createRunReporter } from "./presentation/run-reporter.mjs";
8
- import { createRunLogRegistry } from "../runner/logs.mjs";
9
- import { createSetupOperationRegistry } from "../runner/setup-operations.mjs";
10
- import { resolveRuntimeInstanceConfigs } from "../runner/template.mjs";
4
+ import { loadManagedConfigs, resolveTargetConfig, collectRequiredConfigs, topologicallySortConfigs } from "../../../../../app/configs.mjs";
5
+ import { resolveProductDir } from "../../../../../config/index.mjs";
6
+ import { captureDatabaseTemplateSnapshot, prepareDatabaseRuntime } from "../../../../../database/index.mjs";
7
+ import { createRunReporter } from "../../../../renderers/run/text-reporter.mjs";
8
+ import { createRunLogRegistry } from "../../../../../runner/logs.mjs";
9
+ import { createSetupOperationRegistry } from "../../../../../runner/setup-operations.mjs";
10
+ import { resolveRuntimeInstanceConfigs } from "../../../../../runner/template.mjs";
11
11
 
12
- export async function runDatabaseSnapshotCaptureCommand(options = {}) {
12
+ export async function executeDatabaseSnapshotCaptureOperation(options = {}) {
13
13
  const productDir = resolveProductDir(process.cwd(), options.dir);
14
14
  const { configs } = await loadManagedConfigs({ dir: productDir });
15
15
  const target = resolveTargetConfig(configs, options.service);
@@ -51,7 +51,13 @@ export async function runDatabaseSnapshotCaptureCommand(options = {}) {
51
51
  logRegistry,
52
52
  setupRegistry,
53
53
  });
54
- console.log(`Wrote ${path.relative(productDir, absoluteOutputPath)}`);
54
+ return {
55
+ ok: true,
56
+ productDir,
57
+ service: target.name,
58
+ outputPath: absoluteOutputPath,
59
+ outputLabel: path.relative(productDir, absoluteOutputPath) || path.basename(absoluteOutputPath),
60
+ };
55
61
  } finally {
56
62
  logRegistry.closeAll();
57
63
  fs.rmSync(runtimeRoot, { recursive: true, force: true });
@@ -0,0 +1,12 @@
1
+ import * as runner from "../../../runner/index.mjs";
2
+ import { loadManagedConfigs } from "../../../app/configs.mjs";
3
+
4
+ export async function executeDestroyOperation(flags = {}) {
5
+ const { configs } = await loadManagedConfigs({ dir: flags.dir, service: flags.service });
6
+ const results = [];
7
+ for (const config of configs) {
8
+ await runner.destroy(config);
9
+ results.push({ name: config.name, destroyed: true });
10
+ }
11
+ return results;
12
+ }
@@ -0,0 +1,32 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { discoverTests } from "../../../discovery/index.mjs";
4
+ import { resolveProductDir } from "../../../config/index.mjs";
5
+ import { resolveRequestedFiles } from "../../args.mjs";
6
+
7
+ export async function executeDiscoverOperation(flags = {}, cwd = process.cwd()) {
8
+ const productDir = resolveProductDir(cwd, flags.dir);
9
+ const fileNames = resolveRequestedFiles(flags.file || [], productDir, cwd);
10
+ const result = await discoverTests({
11
+ dir: productDir,
12
+ service: flags.service || null,
13
+ type: flags.type || [],
14
+ suite: flags.suite || [],
15
+ file: fileNames,
16
+ runnableOnly: flags["runnable-only"],
17
+ diagnostics: flags.strict ? "error" : "report",
18
+ });
19
+
20
+ let outputLabel = null;
21
+ if (flags.output) {
22
+ const outputPath = path.resolve(productDir, flags.output);
23
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
24
+ fs.writeFileSync(outputPath, `${JSON.stringify(result, null, 2)}\n`);
25
+ outputLabel = path.relative(productDir, outputPath) || path.basename(outputPath);
26
+ }
27
+
28
+ return {
29
+ ...result,
30
+ outputLabel,
31
+ };
32
+ }
@@ -0,0 +1,5 @@
1
+ import { runDoctor } from "../../../app/doctor.mjs";
2
+
3
+ export async function executeDoctorOperation(flags = {}) {
4
+ return runDoctor({ dir: flags.dir, typecheck: flags.typecheck });
5
+ }
@@ -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
+ }