@elench/testkit 0.1.108 → 0.1.110
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.
- package/README.md +9 -9
- package/lib/app/doctor.mjs +5 -5
- package/lib/app/typecheck.mjs +6 -5
- package/lib/bundler/index.mjs +134 -7
- package/lib/cli/args.mjs +3 -2
- package/lib/cli/assistant/app.mjs +19 -5
- package/lib/cli/assistant/command-observer.mjs +2 -1
- package/lib/cli/assistant/command-results.mjs +2 -1
- package/lib/cli/assistant/context-pack.mjs +2 -2
- package/lib/cli/assistant/prompt-builder.mjs +2 -2
- package/lib/cli/assistant/quality-signal-strip.mjs +103 -0
- package/lib/cli/assistant/transcript-text.mjs +2 -1
- package/lib/cli/assistant/view-model.mjs +79 -0
- package/lib/cli/command-flags.mjs +2 -1
- package/lib/cli/commands/cleanup.mjs +13 -2
- package/lib/cli/commands/discover.mjs +2 -1
- package/lib/cli/commands/run.mjs +3 -2
- package/lib/cli/entrypoint.mjs +3 -1
- package/lib/cli/operations/cleanup/operation.mjs +6 -1
- package/lib/cli/operations/status/operation.mjs +2 -2
- package/lib/cli/renderers/discover/report.mjs +6 -8
- package/lib/cli/renderers/run/failure.mjs +1 -1
- package/lib/cli/renderers/run/text-reporter.mjs +1 -1
- package/lib/cli/renderers/status/text.mjs +101 -1
- package/lib/config/discovery.mjs +10 -1
- package/lib/config-api/index.mjs +2 -2
- package/lib/config-api/next-runtime-tsconfig.mjs +2 -1
- package/lib/coverage/graph-builder.mjs +2 -4
- package/lib/coverage/routing.mjs +1 -1
- package/lib/coverage/shared.mjs +1 -2
- package/lib/discovery/index.d.ts +5 -8
- package/lib/discovery/index.mjs +15 -24
- package/lib/domain/test-types.mjs +44 -0
- package/lib/history/index.d.ts +3 -4
- package/lib/history/index.mjs +6 -14
- package/lib/runner/formatting.mjs +2 -3
- package/lib/runner/maintenance.mjs +136 -35
- package/lib/runner/planning.mjs +1 -1
- package/lib/runner/results.mjs +0 -6
- package/lib/runner/status-model.mjs +520 -0
- package/lib/runner/suite-selection.mjs +20 -11
- package/lib/runner/template-steps.mjs +2 -2
- package/lib/runner/template.mjs +4 -0
- package/lib/ui/index.d.ts +1 -0
- package/lib/ui/index.mjs +1 -0
- package/lib/vitest/index.mjs +2 -1
- package/node_modules/@elench/next-analysis/package.json +1 -1
- package/node_modules/@elench/testkit-bridge/dist/index.js +9 -11
- package/node_modules/@elench/testkit-bridge/dist/index.js.map +1 -1
- package/node_modules/@elench/testkit-bridge/package.json +2 -2
- package/node_modules/@elench/testkit-protocol/dist/index.d.ts +1 -3
- package/node_modules/@elench/testkit-protocol/dist/index.d.ts.map +1 -1
- package/node_modules/@elench/testkit-protocol/dist/index.js +3 -6
- package/node_modules/@elench/testkit-protocol/dist/index.js.map +1 -1
- package/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/node_modules/@elench/ts-analysis/dist/requests.js +1 -1
- package/node_modules/@elench/ts-analysis/package.json +1 -1
- package/package.json +9 -9
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.d.ts +188 -0
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.d.ts.map +1 -0
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.js +293 -0
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.js.map +1 -0
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/package.json +25 -0
- package/node_modules/es-toolkit/CHANGELOG.md +0 -801
- package/node_modules/es-toolkit/src/compat/_internal/Equals.d.ts +0 -1
- package/node_modules/es-toolkit/src/compat/_internal/IsWritable.d.ts +0 -3
- package/node_modules/es-toolkit/src/compat/_internal/MutableList.d.ts +0 -4
- package/node_modules/es-toolkit/src/compat/_internal/RejectReadonly.d.ts +0 -4
- package/node_modules/esprima/ChangeLog +0 -235
|
@@ -12,6 +12,7 @@ export function buildAssistantViewModel(snapshot, { cwd = process.cwd(), termina
|
|
|
12
12
|
return {
|
|
13
13
|
title: `testkit · ${repoName}`,
|
|
14
14
|
welcome: buildWelcomeModel(snapshot, { cwd, providerLabel }),
|
|
15
|
+
qualitySignal: buildQualitySignal(snapshot),
|
|
15
16
|
blocks: buildTranscriptBlocks(snapshot.messages || []),
|
|
16
17
|
composer: {
|
|
17
18
|
text: snapshot.composer || "",
|
|
@@ -25,6 +26,84 @@ export function buildAssistantViewModel(snapshot, { cwd = process.cwd(), termina
|
|
|
25
26
|
};
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
export function buildQualitySignal(snapshot = {}) {
|
|
30
|
+
const summaryRows = snapshot?.run?.summaryData?.rows || snapshot?.summaryData?.rows || [];
|
|
31
|
+
const rowValue = (label) => summaryRows.find(([key]) => key === label)?.[1] || null;
|
|
32
|
+
const latestResult = rowValue("Result");
|
|
33
|
+
const summaryFiles = rowValue("Files");
|
|
34
|
+
const plannedFiles = snapshot?.run?.totalCount ? String(snapshot.run.totalCount) : null;
|
|
35
|
+
const files = summaryFiles || plannedFiles;
|
|
36
|
+
const failed = rowValue("Failed");
|
|
37
|
+
const duration = rowValue("Duration");
|
|
38
|
+
const newRegressions = rowValue("New regressions");
|
|
39
|
+
const fixedKnown = rowValue("Fixed known");
|
|
40
|
+
const contextSelection = snapshot?.context?.selection || {};
|
|
41
|
+
const items = [];
|
|
42
|
+
|
|
43
|
+
if (files && files !== "0") {
|
|
44
|
+
items.push({
|
|
45
|
+
id: summaryFiles ? "tested-files" : "planned-files",
|
|
46
|
+
text: `${files} ${summaryFiles ? "tested" : "planned"}`,
|
|
47
|
+
tone: "neutral",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (newRegressions && newRegressions !== "0") {
|
|
52
|
+
items.push({
|
|
53
|
+
id: "new-regressions",
|
|
54
|
+
text: `${newRegressions} new regression${newRegressions === "1" ? "" : "s"}`,
|
|
55
|
+
tone: "danger",
|
|
56
|
+
});
|
|
57
|
+
} else if (fixedKnown && fixedKnown !== "0") {
|
|
58
|
+
items.push({
|
|
59
|
+
id: "fixed-known",
|
|
60
|
+
text: `${fixedKnown} fixed known`,
|
|
61
|
+
tone: "good",
|
|
62
|
+
});
|
|
63
|
+
} else if (latestResult === "PASSED") {
|
|
64
|
+
items.push({
|
|
65
|
+
id: "no-regressions",
|
|
66
|
+
text: "no regressions",
|
|
67
|
+
tone: "good",
|
|
68
|
+
});
|
|
69
|
+
} else if (latestResult === "FAILED" && failed && failed !== "0") {
|
|
70
|
+
items.push({
|
|
71
|
+
id: "failed-files",
|
|
72
|
+
text: `${failed} failed`,
|
|
73
|
+
tone: "danger",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (latestResult) {
|
|
78
|
+
const status = latestResult === "PASSED" ? "passed" : latestResult === "FAILED" ? "failed" : latestResult.toLowerCase();
|
|
79
|
+
items.push({
|
|
80
|
+
id: "latest-status",
|
|
81
|
+
text: duration ? `${status} in ${duration}` : status,
|
|
82
|
+
tone: latestResult === "PASSED" ? "good" : latestResult === "FAILED" ? "danger" : "neutral",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (contextSelection.filePath) {
|
|
87
|
+
items.push({
|
|
88
|
+
id: "focus",
|
|
89
|
+
text: `focus ${path.basename(contextSelection.filePath)}`,
|
|
90
|
+
tone: "progress",
|
|
91
|
+
});
|
|
92
|
+
} else if (!latestResult) {
|
|
93
|
+
items.push({
|
|
94
|
+
id: "ready",
|
|
95
|
+
text: "ready to run",
|
|
96
|
+
tone: "progress",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
label: "Quality signal",
|
|
102
|
+
tone: latestResult === "FAILED" ? "danger" : latestResult === "PASSED" ? "good" : "neutral",
|
|
103
|
+
items,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
28
107
|
export function buildWelcomeModel(snapshot, { cwd = process.cwd(), providerLabel = null } = {}) {
|
|
29
108
|
const summaryRows = snapshot?.run?.summaryData?.rows || snapshot?.summaryData?.rows || [];
|
|
30
109
|
const rowValue = (label) => summaryRows.find(([key]) => key === label)?.[1] || null;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Flags } from "@oclif/core";
|
|
2
|
+
import { publicTestTypeListText } from "../domain/test-types.mjs";
|
|
2
3
|
|
|
3
4
|
export const sharedFlags = {
|
|
4
5
|
dir: Flags.string({
|
|
@@ -14,7 +15,7 @@ export const runFlags = {
|
|
|
14
15
|
type: Flags.string({
|
|
15
16
|
char: "t",
|
|
16
17
|
multiple: true,
|
|
17
|
-
description:
|
|
18
|
+
description: `Run specific suite type(s): ${publicTestTypeListText({ includeAll: true })}`,
|
|
18
19
|
}),
|
|
19
20
|
suite: Flags.string({
|
|
20
21
|
char: "s",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Command } from "@oclif/core";
|
|
1
|
+
import { Command, Flags } from "@oclif/core";
|
|
2
2
|
import { sharedFlags } from "../command-flags.mjs";
|
|
3
3
|
import { executeCleanupOperation } from "../operations/cleanup/operation.mjs";
|
|
4
4
|
import { renderCleanupResult } from "../renderers/cleanup/text.mjs";
|
|
@@ -8,7 +8,18 @@ export default class CleanupCommand extends Command {
|
|
|
8
8
|
|
|
9
9
|
static enableJsonFlag = true;
|
|
10
10
|
|
|
11
|
-
static flags =
|
|
11
|
+
static flags = {
|
|
12
|
+
...sharedFlags,
|
|
13
|
+
"dry-run": Flags.boolean({
|
|
14
|
+
description: "Show cleanup actions without deleting state",
|
|
15
|
+
default: false,
|
|
16
|
+
}),
|
|
17
|
+
cache: Flags.string({
|
|
18
|
+
description: "Clean cache state: runtime, bundles, assistant, or all",
|
|
19
|
+
multiple: true,
|
|
20
|
+
options: ["runtime", "bundles", "assistant", "all"],
|
|
21
|
+
}),
|
|
22
|
+
};
|
|
12
23
|
|
|
13
24
|
async run() {
|
|
14
25
|
const { flags } = await this.parse(CleanupCommand);
|
|
@@ -3,6 +3,7 @@ import { executeDiscoverOperation } from "../operations/discover/operation.mjs";
|
|
|
3
3
|
import { renderDiscoverResult } from "../renderers/discover/text.mjs";
|
|
4
4
|
import { sharedFlags } from "../command-flags.mjs";
|
|
5
5
|
import { withAssistantCommandResult } from "../assistant/command-results.mjs";
|
|
6
|
+
import { publicTestTypeListText } from "../../domain/test-types.mjs";
|
|
6
7
|
|
|
7
8
|
export default class DiscoverCommand extends Command {
|
|
8
9
|
static summary = "Discover managed tests and report their metadata";
|
|
@@ -14,7 +15,7 @@ export default class DiscoverCommand extends Command {
|
|
|
14
15
|
type: Flags.string({
|
|
15
16
|
char: "t",
|
|
16
17
|
multiple: true,
|
|
17
|
-
description:
|
|
18
|
+
description: `Filter by suite type(s): ${publicTestTypeListText({ includeAll: true })}`,
|
|
18
19
|
}),
|
|
19
20
|
suite: Flags.string({
|
|
20
21
|
char: "s",
|
package/lib/cli/commands/run.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { runFlags } from "../command-flags.mjs";
|
|
|
3
3
|
import { buildRunRequest, executeRunRequest } from "../operations/run/operation.mjs";
|
|
4
4
|
import { resolveTerminalCapabilities } from "../terminal/capabilities.mjs";
|
|
5
5
|
import { withAssistantCommandResult } from "../assistant/command-results.mjs";
|
|
6
|
+
import { publicTestTypeList, publicTestTypeListText } from "../../domain/test-types.mjs";
|
|
6
7
|
|
|
7
8
|
export default class RunCommand extends Command {
|
|
8
9
|
static summary = "Run test suites";
|
|
@@ -11,9 +12,9 @@ export default class RunCommand extends Command {
|
|
|
11
12
|
|
|
12
13
|
static args = {
|
|
13
14
|
type: Args.string({
|
|
14
|
-
description:
|
|
15
|
+
description: `Optional suite type shortcut: ${publicTestTypeListText({ includeAll: true })}`,
|
|
15
16
|
required: false,
|
|
16
|
-
options:
|
|
17
|
+
options: publicTestTypeList({ includeAll: true, includeLegacy: true }),
|
|
17
18
|
}),
|
|
18
19
|
};
|
|
19
20
|
|
package/lib/cli/entrypoint.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { publicTestTypeList } from "../domain/test-types.mjs";
|
|
2
|
+
|
|
1
3
|
export function normalizeCliArgs(argv) {
|
|
2
4
|
if (argv[0] === "help") return normalizeHelpInvocation(argv);
|
|
3
5
|
if (isOclifBuiltinInvocation(argv)) return argv;
|
|
@@ -14,7 +16,7 @@ export function normalizeCliArgs(argv) {
|
|
|
14
16
|
"browser",
|
|
15
17
|
"db",
|
|
16
18
|
]);
|
|
17
|
-
const runTypeShortcuts = new Set(
|
|
19
|
+
const runTypeShortcuts = new Set(publicTestTypeList({ includeAll: true, includeLegacy: true }));
|
|
18
20
|
const valueFlags = new Set([
|
|
19
21
|
"--dir",
|
|
20
22
|
"--service",
|
|
@@ -4,5 +4,10 @@ import { loadManagedConfigs } from "../../../app/configs.mjs";
|
|
|
4
4
|
export async function executeCleanupOperation(flags = {}) {
|
|
5
5
|
const { allConfigs } = await loadManagedConfigs({ dir: flags.dir, service: flags.service });
|
|
6
6
|
const productDir = allConfigs[0]?.productDir || process.cwd();
|
|
7
|
-
return runner.cleanup(productDir
|
|
7
|
+
return runner.cleanup(productDir, {
|
|
8
|
+
allConfigs,
|
|
9
|
+
serviceName: flags.service || null,
|
|
10
|
+
dryRun: flags["dry-run"],
|
|
11
|
+
cache: flags.cache || [],
|
|
12
|
+
});
|
|
8
13
|
}
|
|
@@ -2,6 +2,6 @@ import * as runner from "../../../runner/index.mjs";
|
|
|
2
2
|
import { loadManagedConfigs } from "../../../app/configs.mjs";
|
|
3
3
|
|
|
4
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));
|
|
5
|
+
const { allConfigs, configs } = await loadManagedConfigs({ dir: flags.dir, service: flags.service });
|
|
6
|
+
return configs.map((config) => runner.showStatus(config, { allConfigs }));
|
|
7
7
|
}
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
statusLabel,
|
|
11
11
|
} from "../../terminal/colors.mjs";
|
|
12
12
|
|
|
13
|
-
const TYPE_ORDER = ["
|
|
13
|
+
const TYPE_ORDER = ["ui", "e2e", "scenario", "int", "dal", "load"];
|
|
14
14
|
|
|
15
15
|
const TREE_BRANCH = "\u251C\u2500\u2500 ";
|
|
16
16
|
const TREE_LAST = "\u2514\u2500\u2500 ";
|
|
@@ -98,13 +98,11 @@ function buildVerboseLines(result) {
|
|
|
98
98
|
|
|
99
99
|
const suites = result.suites.filter((suite) => suite.service === service.name).sort(compareSuites);
|
|
100
100
|
for (const suite of suites) {
|
|
101
|
-
lines.push(
|
|
102
|
-
` suite ${suite.selectionType}:${suite.name} ${muted(`[${suite.framework}]`)} ${muted(`${suite.fileCount} files`)}`
|
|
103
|
-
);
|
|
101
|
+
lines.push(` suite ${suite.type}:${suite.name} ${muted(`${suite.fileCount} files`)}`);
|
|
104
102
|
if (suite.locks.length > 0) {
|
|
105
103
|
lines.push(` locks ${suite.locks.join(", ")}`);
|
|
106
104
|
}
|
|
107
|
-
for (const file of result.files.filter((entry) => entry.service === service.name && entry.suiteName === suite.name && entry.
|
|
105
|
+
for (const file of result.files.filter((entry) => entry.service === service.name && entry.suiteName === suite.name && entry.type === suite.type)) {
|
|
108
106
|
lines.push(` ${file.displayName}`);
|
|
109
107
|
lines.push(` path ${file.path}`);
|
|
110
108
|
lines.push(` id ${file.id}`);
|
|
@@ -142,9 +140,9 @@ function groupSuitesByServiceAndType(suites) {
|
|
|
142
140
|
byType = new Map();
|
|
143
141
|
grouped.set(suite.service, byType);
|
|
144
142
|
}
|
|
145
|
-
const list = byType.get(suite.
|
|
143
|
+
const list = byType.get(suite.type) || [];
|
|
146
144
|
list.push(suite);
|
|
147
|
-
byType.set(suite.
|
|
145
|
+
byType.set(suite.type, list.sort(compareSuites));
|
|
148
146
|
}
|
|
149
147
|
return grouped;
|
|
150
148
|
}
|
|
@@ -152,7 +150,7 @@ function groupSuitesByServiceAndType(suites) {
|
|
|
152
150
|
function groupFilesBySuite(files) {
|
|
153
151
|
const bySuite = new Map();
|
|
154
152
|
for (const file of files) {
|
|
155
|
-
const suiteId = [file.service, file.
|
|
153
|
+
const suiteId = [file.service, file.type, file.suiteName].join("|");
|
|
156
154
|
const list = bySuite.get(suiteId) || [];
|
|
157
155
|
list.push(file);
|
|
158
156
|
bySuite.set(
|
|
@@ -25,7 +25,7 @@ export function renderFailureBlock(task, outcome, { width, regressionCatalog } =
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
function normalizeRegressionType(task) {
|
|
28
|
-
if (task.framework === "playwright") return "
|
|
28
|
+
if (task.framework === "playwright" || task.type === "ui") return "ui";
|
|
29
29
|
if (task.type === "integration") return "int";
|
|
30
30
|
return task.type;
|
|
31
31
|
}
|
|
@@ -157,7 +157,7 @@ export function createRunReporter({ outputMode = "compact", stdout = process.std
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
function displayTaskType(task) {
|
|
160
|
-
if (task.framework === "playwright") return "
|
|
160
|
+
if (task.framework === "playwright" || task.type === "ui") return "ui";
|
|
161
161
|
if (task.type === "integration") return "int";
|
|
162
162
|
return task.type;
|
|
163
163
|
}
|
|
@@ -1,7 +1,107 @@
|
|
|
1
1
|
export function renderStatusResult(result) {
|
|
2
|
-
return normalizeLines(result
|
|
2
|
+
if (Array.isArray(result?.lines)) return normalizeLines(result.lines);
|
|
3
|
+
if (!result) return [];
|
|
4
|
+
|
|
5
|
+
const lines = [
|
|
6
|
+
`Testkit Status: ${result.name || result.product?.selectedService || "service"}`,
|
|
7
|
+
"",
|
|
8
|
+
"Product",
|
|
9
|
+
` dir: ${result.product?.dir || "unknown"}`,
|
|
10
|
+
` config: ${result.product?.configFile || "none"}`,
|
|
11
|
+
` services: ${formatList(result.product?.services)}`,
|
|
12
|
+
` dependencies: ${formatList(result.product?.dependencies)}`,
|
|
13
|
+
"",
|
|
14
|
+
"Runs",
|
|
15
|
+
` active: ${result.runs?.active || 0}`,
|
|
16
|
+
` stale: ${result.runs?.stale || 0}`,
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
for (const run of (result.runs?.runs || []).slice(0, 5)) {
|
|
20
|
+
const ports = run.ports?.length ? ` ports=${run.ports.join(",")}` : "";
|
|
21
|
+
lines.push(` ${run.runId}: ${run.status} pid=${run.pid}${ports}`);
|
|
22
|
+
}
|
|
23
|
+
if ((result.runs?.runs || []).length > 5) {
|
|
24
|
+
lines.push(` ... ${(result.runs.runs.length - 5)} more run${result.runs.runs.length - 5 === 1 ? "" : "s"}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
lines.push("", "Runtime Graphs");
|
|
28
|
+
if ((result.runtimeGraphs || []).length === 0) {
|
|
29
|
+
lines.push(" none");
|
|
30
|
+
} else {
|
|
31
|
+
for (const graph of result.runtimeGraphs || []) {
|
|
32
|
+
const marker = graph.orphan ? " orphan" : "";
|
|
33
|
+
lines.push(` ${graph.name}${marker}`);
|
|
34
|
+
lines.push(` desired instances: ${graph.desired?.instanceCount ?? "none"}`);
|
|
35
|
+
lines.push(` runtime dirs: ${graph.runtimeDirCount || 0}`);
|
|
36
|
+
lines.push(` current: ${formatRuntimeRange(graph.currentRuntimeDirs?.map((runtime) => runtime.id))}`);
|
|
37
|
+
lines.push(` stale: ${formatRuntimeRange(graph.staleRuntimeDirs?.map((runtime) => runtime.id))}`);
|
|
38
|
+
lines.push(` runtime services: ${formatList(graph.desired?.runtimeServices || graph.actual?.runtimeServices)}`);
|
|
39
|
+
lines.push(` target services: ${formatList(graph.desired?.targetServices || graph.actual?.targetServices)}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
lines.push(
|
|
44
|
+
"",
|
|
45
|
+
"Caches",
|
|
46
|
+
" bundles",
|
|
47
|
+
` size: ${formatBytes(result.caches?.bundles?.sizeBytes || 0)}`,
|
|
48
|
+
` files: ${result.caches?.bundles?.fileCount || 0}`,
|
|
49
|
+
` source files: ${result.caches?.bundles?.sourceFileCount || 0}`,
|
|
50
|
+
` duplicated sources: ${result.caches?.bundles?.duplicatedSourceCount || 0}`,
|
|
51
|
+
` inline sourcemaps: ${formatSourcemaps(result.caches?.bundles)}`,
|
|
52
|
+
` manifest entries: ${result.caches?.bundles?.manifest?.entryCount || 0}`,
|
|
53
|
+
` unmanaged files: ${result.caches?.bundles?.unmanagedFileCount || 0}`,
|
|
54
|
+
" assistant command results",
|
|
55
|
+
` size: ${formatBytes(result.caches?.assistant?.sizeBytes || 0)}`,
|
|
56
|
+
` files: ${result.caches?.assistant?.fileCount || 0}`,
|
|
57
|
+
` large files: ${result.caches?.assistant?.largeFileCount || 0}`
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
if (result.warnings?.length) {
|
|
61
|
+
lines.push("", "Warnings");
|
|
62
|
+
for (const warning of result.warnings) lines.push(` - ${warning}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!result.hasState) {
|
|
66
|
+
lines.push("", "No state — run tests first.");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return lines;
|
|
3
70
|
}
|
|
4
71
|
|
|
5
72
|
function normalizeLines(lines) {
|
|
6
73
|
return Array.isArray(lines) ? lines : [];
|
|
7
74
|
}
|
|
75
|
+
|
|
76
|
+
function formatList(values) {
|
|
77
|
+
const list = Array.isArray(values) ? values.filter(Boolean) : [];
|
|
78
|
+
return list.length > 0 ? list.join(", ") : "none";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function formatRuntimeRange(ids) {
|
|
82
|
+
const values = Array.isArray(ids) ? ids : [];
|
|
83
|
+
if (values.length === 0) return "none";
|
|
84
|
+
if (values.length === 1) return values[0];
|
|
85
|
+
return `${values[0]}..${values.at(-1)}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function formatBytes(value) {
|
|
89
|
+
const bytes = Number(value || 0);
|
|
90
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
91
|
+
const units = ["KB", "MB", "GB", "TB"];
|
|
92
|
+
let amount = bytes / 1024;
|
|
93
|
+
let unitIndex = 0;
|
|
94
|
+
while (amount >= 1024 && unitIndex < units.length - 1) {
|
|
95
|
+
amount /= 1024;
|
|
96
|
+
unitIndex += 1;
|
|
97
|
+
}
|
|
98
|
+
return `${amount >= 10 ? amount.toFixed(0) : amount.toFixed(1)}${units[unitIndex]}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function formatSourcemaps(bundles = {}) {
|
|
102
|
+
if (bundles.sourcemapFileCount == null) return bundles.sourcemapStatus || "unknown";
|
|
103
|
+
if (bundles.sourcemapStatus && bundles.sourcemapStatus !== "present" && bundles.sourcemapStatus !== "none") {
|
|
104
|
+
return `${bundles.sourcemapFileCount} (${bundles.sourcemapStatus})`;
|
|
105
|
+
}
|
|
106
|
+
return String(bundles.sourcemapFileCount || 0);
|
|
107
|
+
}
|
package/lib/config/discovery.mjs
CHANGED
|
@@ -15,7 +15,8 @@ const DISCOVERY_RULES = [
|
|
|
15
15
|
{ suffix: ".scenario.testkit.ts", type: "scenario", framework: "k6" },
|
|
16
16
|
{ suffix: ".dal.testkit.ts", type: "dal", framework: "k6" },
|
|
17
17
|
{ suffix: ".load.testkit.ts", type: "load", framework: "k6" },
|
|
18
|
-
{ suffix: ".
|
|
18
|
+
{ suffix: ".ui.testkit.ts", type: "ui", framework: "playwright" },
|
|
19
|
+
{ suffix: ".pw.testkit.ts", type: "ui", framework: "playwright", legacySuffix: true },
|
|
19
20
|
];
|
|
20
21
|
|
|
21
22
|
export function discoverProject(productDir, explicitServices = {}, options = {}) {
|
|
@@ -30,6 +31,14 @@ export function discoverProject(productDir, explicitServices = {}, options = {})
|
|
|
30
31
|
for (const filePath of suiteFiles) {
|
|
31
32
|
const rule = inferRule(filePath);
|
|
32
33
|
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
|
+
}
|
|
33
42
|
|
|
34
43
|
const owners = inferOwners(filePath, explicitServices, repoDiscovery);
|
|
35
44
|
if (owners === null) continue;
|
package/lib/config-api/index.mjs
CHANGED
|
@@ -225,9 +225,9 @@ function nextApp(options = {}) {
|
|
|
225
225
|
readyTimeoutMs,
|
|
226
226
|
env: mode === "start"
|
|
227
227
|
? {
|
|
228
|
-
NEXT_DIST_DIR: normalizedEnv.NEXT_DIST_DIR || "
|
|
228
|
+
NEXT_DIST_DIR: normalizedEnv.NEXT_DIST_DIR || "{productDir}/.next-testkit/{runtimeId}/dist",
|
|
229
229
|
NEXT_TSCONFIG_PATH:
|
|
230
|
-
normalizedEnv.NEXT_TSCONFIG_PATH || "
|
|
230
|
+
normalizedEnv.NEXT_TSCONFIG_PATH || "{productDir}/.next-testkit/{runtimeId}/tsconfig.json",
|
|
231
231
|
...normalizedEnv,
|
|
232
232
|
}
|
|
233
233
|
: normalizedEnv,
|
|
@@ -3,12 +3,13 @@ import path from "path";
|
|
|
3
3
|
|
|
4
4
|
export async function writeNextRuntimeTsconfig(context = {}) {
|
|
5
5
|
const serviceDir = context.cwd || context.productDir;
|
|
6
|
+
const productDir = context.productDir || serviceDir;
|
|
6
7
|
const runtimeId = context.runtimeId;
|
|
7
8
|
if (!serviceDir || !runtimeId) {
|
|
8
9
|
throw new Error("writeNextRuntimeTsconfig requires cwd and runtimeId");
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
const runtimeRoot = path.join(
|
|
12
|
+
const runtimeRoot = path.join(productDir, ".next-testkit", String(runtimeId));
|
|
12
13
|
const outputPath = path.join(runtimeRoot, "tsconfig.json");
|
|
13
14
|
const outputDir = path.dirname(outputPath);
|
|
14
15
|
const relative = (target) => path.relative(outputDir, target).replaceAll(path.sep, "/");
|
|
@@ -70,8 +70,7 @@ export function buildCoverageGraph({ productDir, repoDiscovery = {}, services =
|
|
|
70
70
|
confidence: "high",
|
|
71
71
|
service: entry.serviceName,
|
|
72
72
|
suiteName: entry.suiteName,
|
|
73
|
-
|
|
74
|
-
framework: entry.framework,
|
|
73
|
+
type: toSelectionType(entry.type, entry.framework),
|
|
75
74
|
testFilePath: entry.filePath,
|
|
76
75
|
coveredNodeIds,
|
|
77
76
|
details: buildEvidenceDetails(coveredNodeIds, graph, entry, context),
|
|
@@ -173,8 +172,7 @@ function createFallbackEvidence(entry, testNodeId) {
|
|
|
173
172
|
confidence: "medium",
|
|
174
173
|
service: entry.serviceName,
|
|
175
174
|
suiteName: entry.suiteName,
|
|
176
|
-
|
|
177
|
-
framework: entry.framework,
|
|
175
|
+
type: toSelectionType(entry.type, entry.framework),
|
|
178
176
|
testFilePath: entry.filePath,
|
|
179
177
|
coveredNodeIds: [testNodeId],
|
|
180
178
|
};
|
package/lib/coverage/routing.mjs
CHANGED
|
@@ -53,7 +53,7 @@ export function apiRouteLookupKey(method, route) {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
export function toSelectionType(type, framework) {
|
|
56
|
-
if (framework === "playwright") return "
|
|
56
|
+
if (framework === "playwright" || type === "ui") return "ui";
|
|
57
57
|
if (type === "integration") return "int";
|
|
58
58
|
return type;
|
|
59
59
|
}
|
package/lib/coverage/shared.mjs
CHANGED
|
@@ -67,7 +67,6 @@ export function createTestFileNode(graph, entry) {
|
|
|
67
67
|
filePath: entry.filePath,
|
|
68
68
|
metadata: {
|
|
69
69
|
suiteName: entry.suiteName,
|
|
70
|
-
framework: entry.framework,
|
|
71
70
|
type: toSelectionType(entry.type, entry.framework),
|
|
72
71
|
},
|
|
73
72
|
});
|
|
@@ -80,7 +79,7 @@ export function apiRouteLookupKey(method, route) {
|
|
|
80
79
|
}
|
|
81
80
|
|
|
82
81
|
export function toSelectionType(type, framework) {
|
|
83
|
-
if (framework === "playwright") return "
|
|
82
|
+
if (framework === "playwright" || type === "ui") return "ui";
|
|
84
83
|
if (type === "integration") return "int";
|
|
85
84
|
return type;
|
|
86
85
|
}
|
package/lib/discovery/index.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { CoverageGraph } from "@elench/testkit-protocol";
|
|
2
2
|
|
|
3
|
-
export type
|
|
4
|
-
export type DiscoveryInternalType = "integration" | "e2e" | "scenario" | "dal" | "load";
|
|
5
|
-
export type DiscoveryFramework = "k6" | "playwright";
|
|
3
|
+
export type DiscoveryTestType = "ui" | "e2e" | "scenario" | "int" | "dal" | "load";
|
|
4
|
+
export type DiscoveryInternalType = "ui" | "integration" | "e2e" | "scenario" | "dal" | "load";
|
|
6
5
|
|
|
7
6
|
export interface DiscoveryDiagnostic {
|
|
8
7
|
code: string;
|
|
@@ -28,9 +27,8 @@ export interface DiscoverySuite {
|
|
|
28
27
|
service: string;
|
|
29
28
|
name: string;
|
|
30
29
|
displayName: string;
|
|
31
|
-
|
|
30
|
+
type: DiscoveryTestType;
|
|
32
31
|
internalType: DiscoveryInternalType;
|
|
33
|
-
framework: DiscoveryFramework;
|
|
34
32
|
groupLabel: string;
|
|
35
33
|
fileCount: number;
|
|
36
34
|
activeFileCount: number;
|
|
@@ -59,9 +57,8 @@ export interface DiscoveryFile {
|
|
|
59
57
|
service: string;
|
|
60
58
|
suiteName: string;
|
|
61
59
|
groupLabel: string;
|
|
62
|
-
|
|
60
|
+
type: DiscoveryTestType;
|
|
63
61
|
internalType: DiscoveryInternalType;
|
|
64
|
-
framework: DiscoveryFramework;
|
|
65
62
|
skipped: boolean;
|
|
66
63
|
skipReason: string | null;
|
|
67
64
|
locks: string[];
|
|
@@ -79,7 +76,7 @@ export interface DiscoveryResult {
|
|
|
79
76
|
configFile: string | null;
|
|
80
77
|
filters: {
|
|
81
78
|
service: string | null;
|
|
82
|
-
types:
|
|
79
|
+
types: DiscoveryTestType[] | ["all"];
|
|
83
80
|
suiteSelectors: string[];
|
|
84
81
|
fileNames: string[];
|
|
85
82
|
runnableOnly: boolean;
|
package/lib/discovery/index.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { loadConfigContext, resolveProductDir } from "../config/index.mjs";
|
|
|
3
3
|
import { discoverProject } from "../config/discovery.mjs";
|
|
4
4
|
import { loadTestkitConfig } from "../config/config-loader.mjs";
|
|
5
5
|
import { buildCoverageGraph } from "../coverage/index.mjs";
|
|
6
|
+
import { formatPublicTestType } from "../domain/test-types.mjs";
|
|
6
7
|
import { historyFilePath, loadHistory, summarizeHistoryForFiles } from "../history/index.mjs";
|
|
7
8
|
import {
|
|
8
9
|
matchesSelectedTypes,
|
|
@@ -124,12 +125,7 @@ export async function discoverTests(options = {}) {
|
|
|
124
125
|
|
|
125
126
|
export function formatSelectionTypeLabel(type) {
|
|
126
127
|
if (type === "int") return "Integration";
|
|
127
|
-
|
|
128
|
-
if (type === "scenario") return "Scenario";
|
|
129
|
-
if (type === "dal") return "DAL";
|
|
130
|
-
if (type === "load") return "Load";
|
|
131
|
-
if (type === "pw") return "Playwright";
|
|
132
|
-
return type;
|
|
128
|
+
return formatPublicTestType(type);
|
|
133
129
|
}
|
|
134
130
|
|
|
135
131
|
export function formatDisplayName(value) {
|
|
@@ -226,9 +222,8 @@ function buildResolvedSuiteEntries(config, suite, internalType, filters) {
|
|
|
226
222
|
service: config.name,
|
|
227
223
|
suiteName: suite.name,
|
|
228
224
|
groupLabel: formatDisplayName(suite.name),
|
|
229
|
-
selectionType,
|
|
225
|
+
type: selectionType,
|
|
230
226
|
internalType,
|
|
231
|
-
framework,
|
|
232
227
|
skipped,
|
|
233
228
|
skipReason,
|
|
234
229
|
locks: [...new Set([...suiteLocks, ...fileLocks])].sort(),
|
|
@@ -239,13 +234,12 @@ function buildResolvedSuiteEntries(config, suite, internalType, filters) {
|
|
|
239
234
|
if (visibleFiles.length === 0) return null;
|
|
240
235
|
const skippedFileCount = visibleFiles.filter((entry) => entry.skipped).length;
|
|
241
236
|
const suiteEntry = {
|
|
242
|
-
id: buildSuiteId(config.name, selectionType, suite.name
|
|
237
|
+
id: buildSuiteId(config.name, selectionType, suite.name),
|
|
243
238
|
service: config.name,
|
|
244
239
|
name: suite.name,
|
|
245
240
|
displayName: formatDisplayName(suite.name),
|
|
246
|
-
selectionType,
|
|
241
|
+
type: selectionType,
|
|
247
242
|
internalType,
|
|
248
|
-
framework,
|
|
249
243
|
groupLabel: formatDisplayName(suite.name),
|
|
250
244
|
fileCount: visibleFiles.length,
|
|
251
245
|
activeFileCount: visibleFiles.length - skippedFileCount,
|
|
@@ -308,9 +302,8 @@ function buildRawDiscovery({ rawDiscovery, explicitServices, filters }) {
|
|
|
308
302
|
service: entry.serviceName,
|
|
309
303
|
suiteName: entry.suiteName,
|
|
310
304
|
groupLabel: formatDisplayName(entry.suiteName),
|
|
311
|
-
selectionType,
|
|
305
|
+
type: selectionType,
|
|
312
306
|
internalType: entry.type,
|
|
313
|
-
framework: entry.framework,
|
|
314
307
|
skipped: false,
|
|
315
308
|
skipReason: null,
|
|
316
309
|
locks: [],
|
|
@@ -318,7 +311,7 @@ function buildRawDiscovery({ rawDiscovery, explicitServices, filters }) {
|
|
|
318
311
|
};
|
|
319
312
|
files.push(fileEntry);
|
|
320
313
|
|
|
321
|
-
const suiteId = buildSuiteId(entry.serviceName, selectionType, entry.suiteName
|
|
314
|
+
const suiteId = buildSuiteId(entry.serviceName, selectionType, entry.suiteName);
|
|
322
315
|
const existingSuite = suitesById.get(suiteId);
|
|
323
316
|
if (existingSuite) {
|
|
324
317
|
existingSuite.filePaths.push(entry.filePath);
|
|
@@ -330,9 +323,8 @@ function buildRawDiscovery({ rawDiscovery, explicitServices, filters }) {
|
|
|
330
323
|
service: entry.serviceName,
|
|
331
324
|
name: entry.suiteName,
|
|
332
325
|
displayName: formatDisplayName(entry.suiteName),
|
|
333
|
-
selectionType,
|
|
326
|
+
type: selectionType,
|
|
334
327
|
internalType: entry.type,
|
|
335
|
-
framework: entry.framework,
|
|
336
328
|
groupLabel: formatDisplayName(entry.suiteName),
|
|
337
329
|
fileCount: 1,
|
|
338
330
|
activeFileCount: 1,
|
|
@@ -412,7 +404,7 @@ function buildSummary(services, suites, files, diagnostics) {
|
|
|
412
404
|
|
|
413
405
|
for (const file of files) {
|
|
414
406
|
byService[file.service] = (byService[file.service] || 0) + 1;
|
|
415
|
-
byType[file.
|
|
407
|
+
byType[file.type] = (byType[file.type] || 0) + 1;
|
|
416
408
|
}
|
|
417
409
|
|
|
418
410
|
return {
|
|
@@ -497,12 +489,12 @@ function normalizePath(filePath) {
|
|
|
497
489
|
export function fileDisplayName(filePath) {
|
|
498
490
|
const base = path.posix
|
|
499
491
|
.basename(filePath)
|
|
500
|
-
.replace(/(\.int|\.e2e|\.scenario|\.dal|\.load|\.pw)\.testkit\.ts$/, "");
|
|
492
|
+
.replace(/(\.int|\.e2e|\.scenario|\.dal|\.load|\.ui|\.pw)\.testkit\.ts$/, "");
|
|
501
493
|
return formatDisplayName(base);
|
|
502
494
|
}
|
|
503
495
|
|
|
504
|
-
function buildSuiteId(serviceName, selectionType, suiteName
|
|
505
|
-
return [serviceName, selectionType,
|
|
496
|
+
function buildSuiteId(serviceName, selectionType, suiteName) {
|
|
497
|
+
return [serviceName, selectionType, suiteName].join("|");
|
|
506
498
|
}
|
|
507
499
|
|
|
508
500
|
function buildFileId(serviceName, selectionType, filePath) {
|
|
@@ -512,16 +504,15 @@ function buildFileId(serviceName, selectionType, filePath) {
|
|
|
512
504
|
function compareSuiteEntries(left, right) {
|
|
513
505
|
return (
|
|
514
506
|
left.service.localeCompare(right.service) ||
|
|
515
|
-
left.
|
|
516
|
-
left.name.localeCompare(right.name)
|
|
517
|
-
left.framework.localeCompare(right.framework)
|
|
507
|
+
left.type.localeCompare(right.type) ||
|
|
508
|
+
left.name.localeCompare(right.name)
|
|
518
509
|
);
|
|
519
510
|
}
|
|
520
511
|
|
|
521
512
|
function compareFileEntries(left, right) {
|
|
522
513
|
return (
|
|
523
514
|
left.service.localeCompare(right.service) ||
|
|
524
|
-
left.
|
|
515
|
+
left.type.localeCompare(right.type) ||
|
|
525
516
|
left.suiteName.localeCompare(right.suiteName) ||
|
|
526
517
|
left.path.localeCompare(right.path)
|
|
527
518
|
);
|