@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
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ npx @elench/testkit --type int
|
|
|
35
35
|
npx @elench/testkit --type dal
|
|
36
36
|
npx @elench/testkit --type e2e
|
|
37
37
|
npx @elench/testkit --type int,e2e,dal
|
|
38
|
-
npx @elench/testkit --type
|
|
38
|
+
npx @elench/testkit --type ui
|
|
39
39
|
|
|
40
40
|
# Parallel file execution
|
|
41
41
|
npx @elench/testkit --workers 8
|
|
@@ -44,7 +44,7 @@ npx @elench/testkit --workers 8
|
|
|
44
44
|
npx @elench/testkit --file-timeout-seconds 60
|
|
45
45
|
|
|
46
46
|
# Specific service / suite
|
|
47
|
-
npx @elench/testkit --service frontend --type
|
|
47
|
+
npx @elench/testkit --service frontend --type ui -s navigation
|
|
48
48
|
npx @elench/testkit --service api --type int -s health
|
|
49
49
|
npx @elench/testkit --type int,e2e,dal -s dal:queries
|
|
50
50
|
|
|
@@ -183,9 +183,8 @@ export default defineConfig({
|
|
|
183
183
|
```
|
|
184
184
|
|
|
185
185
|
```ts
|
|
186
|
-
//
|
|
187
|
-
import { defineConfig } from "@elench/testkit/
|
|
188
|
-
import { devices } from "@playwright/test";
|
|
186
|
+
// ui.config.ts
|
|
187
|
+
import { defineConfig, devices } from "@elench/testkit/ui";
|
|
189
188
|
|
|
190
189
|
export default defineConfig({
|
|
191
190
|
testDir: "./__testkit__",
|
|
@@ -589,7 +588,7 @@ Example layouts:
|
|
|
589
588
|
|
|
590
589
|
- `src/api/routes/__testkit__/auth/me.int.testkit.ts`
|
|
591
590
|
- `src/db/__testkit__/sessions/count-type.dal.testkit.ts`
|
|
592
|
-
- `frontend/__testkit__/navigation/navigation.
|
|
591
|
+
- `frontend/__testkit__/navigation/navigation.ui.testkit.ts`
|
|
593
592
|
- `src/internal/handler/__testkit__/repos/crud.int.testkit.ts`
|
|
594
593
|
|
|
595
594
|
`testkit` uses these suffixes automatically:
|
|
@@ -599,7 +598,9 @@ Example layouts:
|
|
|
599
598
|
- `*.scenario.testkit.ts`
|
|
600
599
|
- `*.dal.testkit.ts`
|
|
601
600
|
- `*.load.testkit.ts`
|
|
602
|
-
- `*.
|
|
601
|
+
- `*.ui.testkit.ts`
|
|
602
|
+
|
|
603
|
+
See [docs/test-types.md](docs/test-types.md) for the canonical type model.
|
|
603
604
|
|
|
604
605
|
Ownership is inferred from:
|
|
605
606
|
|
|
@@ -645,9 +646,8 @@ identifier plus canonical metadata such as:
|
|
|
645
646
|
- `path`
|
|
646
647
|
- `service`
|
|
647
648
|
- `suiteName`
|
|
648
|
-
- `
|
|
649
|
+
- `type`
|
|
649
650
|
- `internalType`
|
|
650
|
-
- `framework`
|
|
651
651
|
- `skipped`
|
|
652
652
|
- `skipReason`
|
|
653
653
|
- `locks`
|
package/lib/app/doctor.mjs
CHANGED
|
@@ -31,12 +31,12 @@ export async function runDoctor(options = {}) {
|
|
|
31
31
|
|
|
32
32
|
const playwrightViolations = findPlaywrightRuntimeImportViolations(productDir);
|
|
33
33
|
checks.push({
|
|
34
|
-
code: "
|
|
34
|
+
code: "ui-runtime-imports",
|
|
35
35
|
level: playwrightViolations.length === 0 ? "pass" : "fail",
|
|
36
36
|
message:
|
|
37
37
|
playwrightViolations.length === 0
|
|
38
|
-
? "No runtime @playwright/test imports found in testkit
|
|
39
|
-
: `Found ${playwrightViolations.length}
|
|
38
|
+
? "No runtime @playwright/test imports found in testkit UI suites"
|
|
39
|
+
: `Found ${playwrightViolations.length} UI runtime import violation(s); import from @elench/testkit/ui instead`,
|
|
40
40
|
details: playwrightViolations,
|
|
41
41
|
});
|
|
42
42
|
|
|
@@ -51,7 +51,7 @@ export async function runDoctor(options = {}) {
|
|
|
51
51
|
details: configImportViolations,
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
-
const hasBrowserOrNextWork = discovery.files.some((entry) => entry.
|
|
54
|
+
const hasBrowserOrNextWork = discovery.files.some((entry) => entry.type === "ui");
|
|
55
55
|
if (hasBrowserOrNextWork) {
|
|
56
56
|
const nodeCount = discovery.coverageGraph?.nodes?.length || 0;
|
|
57
57
|
checks.push({
|
|
@@ -159,7 +159,7 @@ function collectFiles(rootDir, out = []) {
|
|
|
159
159
|
collectFiles(absolutePath, out);
|
|
160
160
|
continue;
|
|
161
161
|
}
|
|
162
|
-
if (entry.isFile() && entry.name.endsWith(".
|
|
162
|
+
if (entry.isFile() && (entry.name.endsWith(".ui.testkit.ts") || entry.name.endsWith(".ui.testkit.ts"))) {
|
|
163
163
|
out.push(absolutePath);
|
|
164
164
|
}
|
|
165
165
|
}
|
package/lib/app/typecheck.mjs
CHANGED
|
@@ -83,7 +83,7 @@ function writeRootTypecheckConfig({ productDir, configFile, outputDir }) {
|
|
|
83
83
|
"**/*.load.testkit.ts",
|
|
84
84
|
"**/__testkit__/helpers/**/*.ts",
|
|
85
85
|
],
|
|
86
|
-
exclude: ["node_modules", "dist", ".testkit", "**/*.
|
|
86
|
+
exclude: ["node_modules", "dist", ".testkit", "**/*.ui.testkit.ts", "**/*.ui.testkit.ts"],
|
|
87
87
|
};
|
|
88
88
|
fs.writeFileSync(tsconfigPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
89
89
|
return tsconfigPath;
|
|
@@ -104,14 +104,15 @@ function writeNextServiceTypecheckConfig({ productDir, cwd, outputDir, serviceNa
|
|
|
104
104
|
}),
|
|
105
105
|
include: [
|
|
106
106
|
serviceExists(serviceDir, "next-env.d.ts") ? relativeJsonPath(tsconfigPath, path.join(serviceDir, "next-env.d.ts")) : undefined,
|
|
107
|
-
relativeGlob(tsconfigPath, serviceDir, "src/**/*.
|
|
107
|
+
relativeGlob(tsconfigPath, serviceDir, "src/**/*.ui.testkit.ts"),
|
|
108
|
+
relativeGlob(tsconfigPath, serviceDir, "src/**/*.ui.testkit.ts"),
|
|
108
109
|
relativeGlob(tsconfigPath, serviceDir, "tests/playwright-fixtures/**/*.ts"),
|
|
109
110
|
relativeGlob(tsconfigPath, serviceDir, "tests/playwright-fixtures/**/*.tsx"),
|
|
110
111
|
relativeGlob(tsconfigPath, serviceDir, "tests/playwright-fixtures/**/*.mts"),
|
|
111
112
|
relativeGlob(tsconfigPath, serviceDir, ".next/types/**/*.ts"),
|
|
112
113
|
relativeGlob(tsconfigPath, serviceDir, ".next/dev/types/**/*.ts"),
|
|
113
|
-
relativeGlob(tsconfigPath,
|
|
114
|
-
relativeGlob(tsconfigPath,
|
|
114
|
+
relativeGlob(tsconfigPath, productDir, ".next-testkit/**/dist/types/**/*.ts"),
|
|
115
|
+
relativeGlob(tsconfigPath, productDir, ".next-testkit/**/dist/dev/types/**/*.ts"),
|
|
115
116
|
].filter(Boolean),
|
|
116
117
|
exclude: ["node_modules", ".next/cache", ".next/dev"],
|
|
117
118
|
};
|
|
@@ -195,7 +196,7 @@ function packageTypePaths(tsconfigPath) {
|
|
|
195
196
|
"@elench/testkit/config": [relativeJsonPath(tsconfigPath, path.join(PACKAGE_ROOT, "lib", "config-api", "index.d.ts"))],
|
|
196
197
|
"@elench/testkit/drizzle": [relativeJsonPath(tsconfigPath, path.join(PACKAGE_ROOT, "lib", "drizzle", "index.d.ts"))],
|
|
197
198
|
"@elench/testkit/env": [relativeJsonPath(tsconfigPath, path.join(PACKAGE_ROOT, "lib", "env", "index.d.ts"))],
|
|
198
|
-
"@elench/testkit/
|
|
199
|
+
"@elench/testkit/ui": [relativeJsonPath(tsconfigPath, path.join(PACKAGE_ROOT, "lib", "ui", "index.d.ts"))],
|
|
199
200
|
"@elench/testkit/runtime": [relativeJsonPath(tsconfigPath, path.join(PACKAGE_ROOT, "lib", "runtime", "index.d.ts"))],
|
|
200
201
|
"@elench/testkit/vitest": [relativeJsonPath(tsconfigPath, path.join(PACKAGE_ROOT, "lib", "vitest", "index.d.ts"))],
|
|
201
202
|
};
|
package/lib/bundler/index.mjs
CHANGED
|
@@ -10,6 +10,7 @@ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)),
|
|
|
10
10
|
const ROOT_ENTRY = path.join(PACKAGE_ROOT, "lib", "index.mjs");
|
|
11
11
|
const CONFIG_ENTRY = path.join(PACKAGE_ROOT, "lib", "config-api", "index.mjs");
|
|
12
12
|
const RUNTIME_ENTRY = path.join(PACKAGE_ROOT, "lib", "runtime", "index.mjs");
|
|
13
|
+
const MANIFEST_FILE = "manifest.json";
|
|
13
14
|
const bundleCache = new Map();
|
|
14
15
|
|
|
15
16
|
export async function bundleK6File({
|
|
@@ -22,9 +23,18 @@ export async function bundleK6File({
|
|
|
22
23
|
fs.mkdirSync(bundleDir, { recursive: true });
|
|
23
24
|
|
|
24
25
|
const configFile = findConfigFile(productDir);
|
|
25
|
-
const
|
|
26
|
+
const metadata = await buildCacheMetadata(absoluteSource, configFile);
|
|
27
|
+
const { cacheKey } = metadata;
|
|
26
28
|
const cached = bundleCache.get(cacheKey);
|
|
27
29
|
if (cached && fs.existsSync(cached)) {
|
|
30
|
+
updateBundleManifest(bundleDir, {
|
|
31
|
+
...metadata,
|
|
32
|
+
serviceName: serviceName || "shared",
|
|
33
|
+
sourceFile: absoluteSource,
|
|
34
|
+
outputFile: cached,
|
|
35
|
+
entryFile: inferEntryFile(cached),
|
|
36
|
+
hasSourcemap: fileHasInlineSourcemap(cached),
|
|
37
|
+
});
|
|
28
38
|
return cached;
|
|
29
39
|
}
|
|
30
40
|
|
|
@@ -36,6 +46,20 @@ export async function bundleK6File({
|
|
|
36
46
|
bundleDir,
|
|
37
47
|
`${path.basename(sourceFile, path.extname(sourceFile))}-${cacheKey.slice(0, 12)}.entry.mjs`
|
|
38
48
|
);
|
|
49
|
+
|
|
50
|
+
if (fs.existsSync(outputFile) && fs.existsSync(entryFile)) {
|
|
51
|
+
bundleCache.set(cacheKey, outputFile);
|
|
52
|
+
updateBundleManifest(bundleDir, {
|
|
53
|
+
...metadata,
|
|
54
|
+
serviceName: serviceName || "shared",
|
|
55
|
+
sourceFile: absoluteSource,
|
|
56
|
+
outputFile,
|
|
57
|
+
entryFile,
|
|
58
|
+
hasSourcemap: fileHasInlineSourcemap(outputFile),
|
|
59
|
+
});
|
|
60
|
+
return outputFile;
|
|
61
|
+
}
|
|
62
|
+
|
|
39
63
|
fs.writeFileSync(entryFile, buildBundleEntryModule({
|
|
40
64
|
sourceFile: absoluteSource,
|
|
41
65
|
configFile,
|
|
@@ -49,7 +73,7 @@ export async function bundleK6File({
|
|
|
49
73
|
legalComments: "none",
|
|
50
74
|
outfile: outputFile,
|
|
51
75
|
platform: "neutral",
|
|
52
|
-
sourcemap: "inline",
|
|
76
|
+
sourcemap: bundleSourcemapEnabled() ? "inline" : false,
|
|
53
77
|
target: "es2020",
|
|
54
78
|
plugins: [testkitPackageAliasPlugin()],
|
|
55
79
|
external: [
|
|
@@ -59,26 +83,44 @@ export async function bundleK6File({
|
|
|
59
83
|
});
|
|
60
84
|
|
|
61
85
|
bundleCache.set(cacheKey, outputFile);
|
|
86
|
+
updateBundleManifest(bundleDir, {
|
|
87
|
+
...metadata,
|
|
88
|
+
serviceName: serviceName || "shared",
|
|
89
|
+
sourceFile: absoluteSource,
|
|
90
|
+
outputFile,
|
|
91
|
+
entryFile,
|
|
92
|
+
hasSourcemap: bundleSourcemapEnabled(),
|
|
93
|
+
});
|
|
62
94
|
return outputFile;
|
|
63
95
|
}
|
|
64
96
|
|
|
65
|
-
async function
|
|
97
|
+
async function buildCacheMetadata(sourceFile, configFile = null) {
|
|
66
98
|
const source = await fs.promises.readFile(sourceFile, "utf8");
|
|
67
|
-
const
|
|
99
|
+
const packageJsonText = await fs.promises.readFile(path.join(PACKAGE_ROOT, "package.json"), "utf8");
|
|
100
|
+
const packageJson = JSON.parse(packageJsonText);
|
|
101
|
+
const sourceHash = crypto.createHash("sha256").update(source).digest("hex");
|
|
68
102
|
const hash = crypto
|
|
69
103
|
.createHash("sha256")
|
|
70
104
|
.update(sourceFile)
|
|
71
105
|
.update("\0")
|
|
72
106
|
.update(source)
|
|
73
107
|
.update("\0")
|
|
74
|
-
.update(
|
|
108
|
+
.update(packageJsonText);
|
|
109
|
+
let configHash = null;
|
|
75
110
|
|
|
76
111
|
if (configFile && fs.existsSync(configFile)) {
|
|
112
|
+
const configText = await fs.promises.readFile(configFile, "utf8");
|
|
113
|
+
configHash = crypto.createHash("sha256").update(configFile).update("\0").update(configText).digest("hex");
|
|
77
114
|
hash.update("\0").update(configFile).update("\0");
|
|
78
|
-
hash.update(
|
|
115
|
+
hash.update(configText);
|
|
79
116
|
}
|
|
80
117
|
|
|
81
|
-
return
|
|
118
|
+
return {
|
|
119
|
+
cacheKey: hash.digest("hex"),
|
|
120
|
+
sourceHash,
|
|
121
|
+
configHash,
|
|
122
|
+
testkitVersion: packageJson.version || null,
|
|
123
|
+
};
|
|
82
124
|
}
|
|
83
125
|
|
|
84
126
|
function testkitPackageAliasPlugin() {
|
|
@@ -153,3 +195,88 @@ function normalizeTestkitSuite(module) {
|
|
|
153
195
|
}
|
|
154
196
|
`;
|
|
155
197
|
}
|
|
198
|
+
|
|
199
|
+
function updateBundleManifest(bundleDir, entry) {
|
|
200
|
+
const manifestPath = path.join(bundleDir, MANIFEST_FILE);
|
|
201
|
+
const manifest = readBundleManifest(manifestPath);
|
|
202
|
+
const now = new Date().toISOString();
|
|
203
|
+
const existingIndex = manifest.entries.findIndex((candidate) => candidate.cacheKey === entry.cacheKey);
|
|
204
|
+
const existing = existingIndex >= 0 ? manifest.entries[existingIndex] : null;
|
|
205
|
+
const document = {
|
|
206
|
+
sourceFile: entry.sourceFile,
|
|
207
|
+
cacheKey: entry.cacheKey,
|
|
208
|
+
outputFile: entry.outputFile,
|
|
209
|
+
entryFile: entry.entryFile,
|
|
210
|
+
createdAt: existing?.createdAt || now,
|
|
211
|
+
lastUsedAt: now,
|
|
212
|
+
sizeBytes: safeSize(entry.outputFile) + safeSize(entry.entryFile),
|
|
213
|
+
testkitVersion: entry.testkitVersion,
|
|
214
|
+
configHash: entry.configHash,
|
|
215
|
+
sourceHash: entry.sourceHash,
|
|
216
|
+
hasSourcemap: Boolean(entry.hasSourcemap),
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
if (existingIndex >= 0) {
|
|
220
|
+
manifest.entries[existingIndex] = document;
|
|
221
|
+
} else {
|
|
222
|
+
manifest.entries.push(document);
|
|
223
|
+
}
|
|
224
|
+
manifest.entries.sort((left, right) => String(left.sourceFile).localeCompare(String(right.sourceFile)) || String(left.cacheKey).localeCompare(String(right.cacheKey)));
|
|
225
|
+
writeBundleManifest(manifestPath, manifest);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function readBundleManifest(manifestPath) {
|
|
229
|
+
if (!fs.existsSync(manifestPath)) {
|
|
230
|
+
return {
|
|
231
|
+
schemaVersion: 1,
|
|
232
|
+
source: "testkit-bundle-cache",
|
|
233
|
+
entries: [],
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
const parsed = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
238
|
+
return {
|
|
239
|
+
schemaVersion: 1,
|
|
240
|
+
source: "testkit-bundle-cache",
|
|
241
|
+
...parsed,
|
|
242
|
+
entries: Array.isArray(parsed.entries) ? parsed.entries : [],
|
|
243
|
+
};
|
|
244
|
+
} catch {
|
|
245
|
+
return {
|
|
246
|
+
schemaVersion: 1,
|
|
247
|
+
source: "testkit-bundle-cache",
|
|
248
|
+
entries: [],
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function writeBundleManifest(manifestPath, manifest) {
|
|
254
|
+
const tempPath = `${manifestPath}.${process.pid}.${Date.now()}.tmp`;
|
|
255
|
+
fs.writeFileSync(tempPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
256
|
+
fs.renameSync(tempPath, manifestPath);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function bundleSourcemapEnabled() {
|
|
260
|
+
return process.env.TESTKIT_BUNDLE_SOURCEMAP === "1";
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function inferEntryFile(outputFile) {
|
|
264
|
+
return outputFile.replace(/\.js$/, ".entry.mjs");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function safeSize(filePath) {
|
|
268
|
+
try {
|
|
269
|
+
return fs.statSync(filePath).size;
|
|
270
|
+
} catch {
|
|
271
|
+
return 0;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function fileHasInlineSourcemap(filePath) {
|
|
276
|
+
try {
|
|
277
|
+
const text = fs.readFileSync(filePath, "utf8");
|
|
278
|
+
return /sourceMappingURL=data:application\/json/.test(text);
|
|
279
|
+
} catch {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
}
|
package/lib/cli/args.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import { normalizeTypeValues, parseSuiteSelectors } from "../runner/suite-selection.mjs";
|
|
3
|
+
import { publicTestTypeList, publicTestTypeListText } from "../domain/test-types.mjs";
|
|
3
4
|
import {
|
|
4
5
|
parseFileTimeoutOption,
|
|
5
6
|
parseWorkersOption,
|
|
6
7
|
} from "../runner/execution-config.mjs";
|
|
7
8
|
|
|
8
|
-
export const POSITIONAL_TYPES = new Set(
|
|
9
|
+
export const POSITIONAL_TYPES = new Set(publicTestTypeList({ includeAll: true, includeLegacy: true }));
|
|
9
10
|
export const LIFECYCLE = new Set(["status", "destroy", "cleanup"]);
|
|
10
11
|
export const RESERVED = new Set([...POSITIONAL_TYPES, ...LIFECYCLE]);
|
|
11
12
|
|
|
@@ -33,7 +34,7 @@ export function resolveCliSelection({ first, second, third }) {
|
|
|
33
34
|
} else if (first) {
|
|
34
35
|
throw new Error(
|
|
35
36
|
`Unknown argument "${first}". Expected a lifecycle command (status, destroy, cleanup) ` +
|
|
36
|
-
`or suite type (
|
|
37
|
+
`or suite type (${publicTestTypeListText({ includeAll: true })}).`
|
|
37
38
|
);
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -5,6 +5,7 @@ import { RunTreeView } from "../components/blocks/run-tree.mjs";
|
|
|
5
5
|
import { CodeBlock } from "./code-block.mjs";
|
|
6
6
|
import { getComposerDisplayModel } from "./composer.mjs";
|
|
7
7
|
import { MarkdownBlock } from "./markdown-block.mjs";
|
|
8
|
+
import { QualitySignalStrip } from "./quality-signal-strip.mjs";
|
|
8
9
|
import { buildAssistantViewModel } from "./view-model.mjs";
|
|
9
10
|
import { truncateText, wrapText } from "../terminal/layout.mjs";
|
|
10
11
|
|
|
@@ -68,6 +69,7 @@ export function AssistantApp({
|
|
|
68
69
|
onRequestClose,
|
|
69
70
|
})
|
|
70
71
|
: null,
|
|
72
|
+
createElement(HeaderChrome, { view }),
|
|
71
73
|
view.blocks.length === 0
|
|
72
74
|
? createElement(WelcomePanel, { view })
|
|
73
75
|
: createElement(Transcript, { view }),
|
|
@@ -103,6 +105,21 @@ export function AssistantApp({
|
|
|
103
105
|
);
|
|
104
106
|
}
|
|
105
107
|
|
|
108
|
+
function HeaderChrome({ view }) {
|
|
109
|
+
const provider = view.welcome.rows.find(([label]) => label === "Provider")?.[1] || "";
|
|
110
|
+
return createElement(
|
|
111
|
+
Box,
|
|
112
|
+
{ flexDirection: "column" },
|
|
113
|
+
createElement(Text, null, bold(view.title)),
|
|
114
|
+
provider ? createElement(Text, null, dim(provider)) : null,
|
|
115
|
+
createElement(QualitySignalStrip, {
|
|
116
|
+
signal: view.qualitySignal,
|
|
117
|
+
width: view.terminalWidth,
|
|
118
|
+
}),
|
|
119
|
+
createElement(Text, null, "")
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
106
123
|
function AssistantInputHandler({ assistantState, snapshot, onRequestClose }) {
|
|
107
124
|
const { exit } = useApp();
|
|
108
125
|
|
|
@@ -154,6 +171,7 @@ function AssistantInputHandler({ assistantState, snapshot, onRequestClose }) {
|
|
|
154
171
|
}
|
|
155
172
|
|
|
156
173
|
function WelcomePanel({ view }) {
|
|
174
|
+
const rows = view.welcome.rows.filter(([label]) => label !== "Provider");
|
|
157
175
|
return createElement(
|
|
158
176
|
Box,
|
|
159
177
|
{
|
|
@@ -162,10 +180,9 @@ function WelcomePanel({ view }) {
|
|
|
162
180
|
paddingLeft: 1,
|
|
163
181
|
paddingRight: 1,
|
|
164
182
|
},
|
|
165
|
-
createElement(Text, null, bold(view.title)),
|
|
166
183
|
createElement(Text, null, dim(view.welcome.subtitle)),
|
|
167
184
|
createElement(Text, null, ""),
|
|
168
|
-
...
|
|
185
|
+
...rows.map(([label, value]) => (
|
|
169
186
|
createElement(Text, { key: label }, `${padLabel(label)} ${colorWelcomeValue(label, value)}`)
|
|
170
187
|
)),
|
|
171
188
|
createElement(Text, null, ""),
|
|
@@ -180,9 +197,6 @@ function Transcript({ view }) {
|
|
|
180
197
|
return createElement(
|
|
181
198
|
Box,
|
|
182
199
|
{ flexDirection: "column" },
|
|
183
|
-
createElement(Text, null, bold(view.title)),
|
|
184
|
-
createElement(Text, null, dim(view.welcome.rows.find(([label]) => label === "Provider")?.[1] || "")),
|
|
185
|
-
createElement(Text, null, ""),
|
|
186
200
|
view.notice ? createElement(Text, null, yellow(view.notice)) : null,
|
|
187
201
|
...view.blocks.flatMap((block) => renderBlock(block, view))
|
|
188
202
|
);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import { publicTestTypeList } from "../../domain/test-types.mjs";
|
|
3
4
|
|
|
4
5
|
const POLL_INTERVAL_MS = 150;
|
|
5
6
|
const OBSERVED_KINDS = new Set(["run", "discover", "status", "doctor", "typecheck"]);
|
|
6
|
-
const RUN_KINDS = new Set(["run",
|
|
7
|
+
const RUN_KINDS = new Set(["run", ...publicTestTypeList({ includeAll: true, includeLegacy: true })]);
|
|
7
8
|
|
|
8
9
|
export function createAssistantCommandObserver({
|
|
9
10
|
productDir,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import { publicTestTypeList } from "../../domain/test-types.mjs";
|
|
3
4
|
|
|
4
5
|
export const ASSISTANT_SESSION_ENV = "TESTKIT_ASSISTANT_SESSION_ID";
|
|
5
6
|
export const ASSISTANT_RESULT_DIR_ENV = "TESTKIT_ASSISTANT_RESULT_DIR";
|
|
@@ -7,7 +8,7 @@ export const ASSISTANT_COMMAND_LOG_ENV = "TESTKIT_ASSISTANT_COMMAND_LOG";
|
|
|
7
8
|
export const ASSISTANT_COMMAND_ID_ENV = "TESTKIT_ASSISTANT_COMMAND_ID";
|
|
8
9
|
export const ASSISTANT_WRAPPER_LOGGED_ENV = "TESTKIT_ASSISTANT_WRAPPER_LOGGED";
|
|
9
10
|
|
|
10
|
-
const RUN_SHORTCUTS = new Set(
|
|
11
|
+
const RUN_SHORTCUTS = new Set(publicTestTypeList({ includeAll: true, includeLegacy: true }));
|
|
11
12
|
const FLAGS_WITH_VALUES = new Set([
|
|
12
13
|
"--dir",
|
|
13
14
|
"--service",
|
|
@@ -178,7 +178,7 @@ function appendCommandLog(event) {
|
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
function inferKind(args) {
|
|
181
|
-
const runShortcuts = new Set(["
|
|
181
|
+
const runShortcuts = new Set(["ui", "e2e", "scenario", "int", "dal", "load", "all"]);
|
|
182
182
|
const flagsWithValues = new Set([
|
|
183
183
|
"--dir",
|
|
184
184
|
"--service",
|
|
@@ -248,7 +248,7 @@ function buildContextMarkdown(productDir, snapshot, paths) {
|
|
|
248
248
|
"## Guidance",
|
|
249
249
|
"- Work normally in the repository and run real commands. Testkit observes recognized Testkit commands and renders them in the assistant UI.",
|
|
250
250
|
"- Prefer `testkit ...` commands when using Testkit directly; `npx testkit ...` and project scripts are also valid.",
|
|
251
|
-
"- `testkit run
|
|
251
|
+
"- `testkit run ui` selects UI suites. `testkit run e2e` selects e2e suites.",
|
|
252
252
|
"- Do not reinterpret CLI syntax after an execution failure unless `testkit run --help` confirms a syntax problem.",
|
|
253
253
|
"- Use the command log and focused context files before rereading artifacts manually.",
|
|
254
254
|
"- Prefer repo-local commands over guessing project-specific wrappers.",
|
|
@@ -15,8 +15,8 @@ export function buildAssistantPrompt({
|
|
|
15
15
|
"You are Testkit Assistant.",
|
|
16
16
|
"You are running as a coding agent inside the user's repository.",
|
|
17
17
|
"Work normally: inspect files, edit files, run real shell commands, and iterate until the user's request is handled.",
|
|
18
|
-
"When using Testkit, run real commands such as `testkit discover`, `testkit run
|
|
19
|
-
"`testkit run
|
|
18
|
+
"When using Testkit, run real commands such as `testkit discover`, `testkit run ui`, `testkit run e2e`, `npx testkit run int`, or the repository's package scripts.",
|
|
19
|
+
"`testkit run ui` selects UI suites. `testkit run e2e` selects e2e suites. Choose commands from the user's request and the command reference, not from Testkit prompt routing.",
|
|
20
20
|
"Testkit observes recognized Testkit commands and renders rich assistant UI from the real command output, sidecars, and artifacts.",
|
|
21
21
|
"Do not respond with a JSON tool envelope. Give the user a normal final answer when you are done.",
|
|
22
22
|
"",
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import React, { createElement } from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { cyan, dim, green, red, yellow } from "../terminal/colors.mjs";
|
|
4
|
+
import { measureWidth, truncateText } from "../terminal/layout.mjs";
|
|
5
|
+
|
|
6
|
+
const LABEL_ITEM_SEPARATOR = " ";
|
|
7
|
+
const ITEM_SEPARATOR = " · ";
|
|
8
|
+
const MIN_TRUNCATED_ITEM_WIDTH = 4;
|
|
9
|
+
|
|
10
|
+
export function fitQualitySignal(signal, { width = 100 } = {}) {
|
|
11
|
+
const availableWidth = Math.max(0, Number(width) || 0);
|
|
12
|
+
const label = normalizeText(signal?.label) || "Quality signal";
|
|
13
|
+
const items = normalizeItems(signal?.items);
|
|
14
|
+
if (availableWidth <= 0) return { label: "", items: [] };
|
|
15
|
+
if (items.length === 0) {
|
|
16
|
+
return {
|
|
17
|
+
label: truncateText(label, availableWidth),
|
|
18
|
+
items: [],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
for (let count = items.length; count >= 1; count -= 1) {
|
|
23
|
+
const candidate = {
|
|
24
|
+
label,
|
|
25
|
+
items: items.slice(0, count),
|
|
26
|
+
};
|
|
27
|
+
if (measureWidth(formatQualitySignalText(candidate)) <= availableWidth) return candidate;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const firstItemWidth = availableWidth - measureWidth(label) - measureWidth(LABEL_ITEM_SEPARATOR);
|
|
31
|
+
if (firstItemWidth >= MIN_TRUNCATED_ITEM_WIDTH) {
|
|
32
|
+
return {
|
|
33
|
+
label,
|
|
34
|
+
items: [
|
|
35
|
+
{
|
|
36
|
+
...items[0],
|
|
37
|
+
text: truncateText(items[0].text, firstItemWidth),
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
label: truncateText(label, availableWidth),
|
|
45
|
+
items: [],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function formatQualitySignalText(signal) {
|
|
50
|
+
const label = normalizeText(signal?.label) || "Quality signal";
|
|
51
|
+
const items = normalizeItems(signal?.items);
|
|
52
|
+
if (items.length === 0) return label;
|
|
53
|
+
return `${label}${LABEL_ITEM_SEPARATOR}${items.map((item) => item.text).join(ITEM_SEPARATOR)}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function QualitySignalStrip({ signal, width = 100 } = {}) {
|
|
57
|
+
const fitted = fitQualitySignal(signal, { width });
|
|
58
|
+
if (!fitted.label) return null;
|
|
59
|
+
return createElement(
|
|
60
|
+
Box,
|
|
61
|
+
{
|
|
62
|
+
height: 1,
|
|
63
|
+
flexDirection: "row",
|
|
64
|
+
},
|
|
65
|
+
createElement(
|
|
66
|
+
Text,
|
|
67
|
+
null,
|
|
68
|
+
dim(fitted.label),
|
|
69
|
+
fitted.items.length > 0 ? LABEL_ITEM_SEPARATOR : "",
|
|
70
|
+
...fitted.items.flatMap((item, index) => [
|
|
71
|
+
index > 0 ? dim(ITEM_SEPARATOR) : "",
|
|
72
|
+
colorQualitySignalItem(item),
|
|
73
|
+
])
|
|
74
|
+
)
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeItems(items) {
|
|
79
|
+
return (Array.isArray(items) ? items : [])
|
|
80
|
+
.map((item) => ({
|
|
81
|
+
id: normalizeText(item?.id),
|
|
82
|
+
text: normalizeText(item?.text),
|
|
83
|
+
tone: normalizeTone(item?.tone),
|
|
84
|
+
}))
|
|
85
|
+
.filter((item) => item.text);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizeText(value) {
|
|
89
|
+
return String(value ?? "").replace(/\s+/g, " ").trim();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function normalizeTone(value) {
|
|
93
|
+
if (value === "good" || value === "warning" || value === "danger" || value === "progress") return value;
|
|
94
|
+
return "neutral";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function colorQualitySignalItem(item) {
|
|
98
|
+
if (item.tone === "good") return green(item.text);
|
|
99
|
+
if (item.tone === "warning") return yellow(item.text);
|
|
100
|
+
if (item.tone === "danger") return red(item.text);
|
|
101
|
+
if (item.tone === "progress") return cyan(item.text);
|
|
102
|
+
return item.text;
|
|
103
|
+
}
|
|
@@ -3,10 +3,11 @@ import { truncateText, wrapText } from "../terminal/layout.mjs";
|
|
|
3
3
|
import { buildAssistantViewModel } from "./view-model.mjs";
|
|
4
4
|
import { renderCodeBlockText } from "./code-block.mjs";
|
|
5
5
|
import { renderMarkdownToAnsi } from "./markdown-block.mjs";
|
|
6
|
+
import { formatQualitySignalText, fitQualitySignal } from "./quality-signal-strip.mjs";
|
|
6
7
|
|
|
7
8
|
export function renderAssistantSnapshotText(snapshot, { cwd = process.cwd(), ansi = false, width = 100 } = {}) {
|
|
8
9
|
const view = buildAssistantViewModel(snapshot || {}, { cwd, terminalWidth: width });
|
|
9
|
-
const lines = [view.title, view.welcome.rows.find(([label]) => label === "Provider")?.[1] || ""]
|
|
10
|
+
const lines = [view.title, view.welcome.rows.find(([label]) => label === "Provider")?.[1] || "", formatQualitySignalText(fitQualitySignal(view.qualitySignal, { width }))]
|
|
10
11
|
.filter(Boolean);
|
|
11
12
|
for (const block of view.blocks || []) {
|
|
12
13
|
lines.push("");
|