@elench/testkit 0.1.110 → 0.1.112
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/lib/cli/args.mjs +1 -1
- package/lib/cli/assistant/actions.mjs +10 -7
- package/lib/cli/assistant/app.mjs +70 -20
- package/lib/cli/assistant/command-classifier.d.mts +6 -0
- package/lib/cli/assistant/command-classifier.d.mts.map +1 -0
- package/lib/cli/assistant/command-classifier.mjs +48 -0
- package/lib/cli/assistant/command-classifier.mjs.map +1 -0
- package/lib/cli/assistant/command-normalize.mjs +22 -0
- package/lib/cli/assistant/command-observer.mjs +69 -15
- package/lib/cli/assistant/command-results.mjs +12 -35
- package/lib/cli/assistant/context-pack.mjs +95 -57
- package/lib/cli/assistant/domain.d.mts +59 -0
- package/lib/cli/assistant/domain.d.mts.map +1 -0
- package/lib/cli/assistant/domain.mjs +2 -0
- package/lib/cli/assistant/domain.mjs.map +1 -0
- package/lib/cli/assistant/prompt-builder.mjs +21 -13
- package/lib/cli/assistant/providers/claude.mjs +77 -19
- package/lib/cli/assistant/providers/codex.mjs +8 -12
- package/lib/cli/assistant/providers/index.mjs +3 -2
- package/lib/cli/assistant/providers/shared.mjs +22 -3
- package/lib/cli/assistant/session-paths.d.mts +23 -0
- package/lib/cli/assistant/session-paths.d.mts.map +1 -0
- package/lib/cli/assistant/session-paths.mjs +31 -0
- package/lib/cli/assistant/session-paths.mjs.map +1 -0
- package/lib/cli/assistant/session.mjs +13 -3
- package/lib/cli/assistant/state.mjs +159 -3
- package/lib/cli/assistant/view-model.mjs +69 -9
- package/lib/cli/commands/assistant.mjs +3 -0
- package/lib/cli/commands/run.mjs +1 -1
- package/lib/cli/components/blocks/run-tree.mjs +2 -1
- package/lib/cli/entrypoint.mjs +1 -1
- package/lib/config/discovery.mjs +0 -10
- package/lib/discovery/index.mjs +1 -1
- package/lib/domain/test-types.mjs +5 -14
- package/lib/runner/maintenance.mjs +2 -2
- package/lib/runner/provenance.mjs +4 -1
- package/lib/runner/status-model.mjs +26 -9
- package/lib/runner/suite-selection.mjs +2 -3
- package/node_modules/@elench/next-analysis/package.json +1 -1
- package/node_modules/@elench/testkit-bridge/package.json +2 -2
- package/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/node_modules/@elench/ts-analysis/package.json +1 -1
- package/package.json +10 -9
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.d.ts +0 -188
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.d.ts.map +0 -1
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.js +0 -293
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.js.map +0 -1
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/package.json +0 -25
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`@elench/testkit` discovers `*.testkit.ts` files, infers suite ownership from the
|
|
4
4
|
filesystem, starts local services, provisions Docker-managed local Postgres
|
|
5
|
-
databases, and runs
|
|
5
|
+
databases, and runs test suites.
|
|
6
6
|
|
|
7
7
|
The package is now driven by `testkit.config.ts`, not `testkit.config.json`.
|
|
8
8
|
|
package/lib/cli/args.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
parseWorkersOption,
|
|
7
7
|
} from "../runner/execution-config.mjs";
|
|
8
8
|
|
|
9
|
-
export const POSITIONAL_TYPES = new Set(publicTestTypeList({ includeAll: true
|
|
9
|
+
export const POSITIONAL_TYPES = new Set(publicTestTypeList({ includeAll: true }));
|
|
10
10
|
export const LIFECYCLE = new Set(["status", "destroy", "cleanup"]);
|
|
11
11
|
export const RESERVED = new Set([...POSITIONAL_TYPES, ...LIFECYCLE]);
|
|
12
12
|
|
|
@@ -60,13 +60,16 @@ function readContextAction(args, context) {
|
|
|
60
60
|
function readFileAction(args, context) {
|
|
61
61
|
const file = String(args.path || args.file || "").trim();
|
|
62
62
|
if (!file) throw new Error("read_file requires a path");
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
throw new Error("read_file only supports paths inside the current repository");
|
|
66
|
-
}
|
|
63
|
+
const root = fs.realpathSync(path.resolve(context.productDir));
|
|
64
|
+
const resolved = path.resolve(root, file);
|
|
67
65
|
if (!fs.existsSync(resolved)) {
|
|
68
66
|
throw new Error(`File not found: ${file}`);
|
|
69
67
|
}
|
|
68
|
+
const realResolved = fs.realpathSync(resolved);
|
|
69
|
+
const relativeResolved = path.relative(root, realResolved);
|
|
70
|
+
if (relativeResolved.startsWith("..") || path.isAbsolute(relativeResolved)) {
|
|
71
|
+
throw new Error("read_file only supports paths inside the current repository");
|
|
72
|
+
}
|
|
70
73
|
const startLine = Math.max(1, Number(args.startLine || args.start || 1) || 1);
|
|
71
74
|
const requestedEnd = Number(args.endLine || args.end || startLine + FILE_LINE_LIMIT - 1) || startLine + FILE_LINE_LIMIT - 1;
|
|
72
75
|
const endLine = Math.max(startLine, Math.min(requestedEnd, startLine + FILE_LINE_LIMIT - 1));
|
|
@@ -75,14 +78,14 @@ function readFileAction(args, context) {
|
|
|
75
78
|
for (let lineNumber = startLine; lineNumber <= Math.min(endLine, lines.length); lineNumber += 1) {
|
|
76
79
|
selected.push(`${lineNumber}: ${lines[lineNumber - 1]}`);
|
|
77
80
|
}
|
|
78
|
-
const title = `File ${
|
|
81
|
+
const title = `File ${relativeResolved || path.basename(realResolved)}`;
|
|
79
82
|
return {
|
|
80
83
|
ok: true,
|
|
81
84
|
title,
|
|
82
85
|
text: selected.join("\n"),
|
|
83
86
|
data: {
|
|
84
|
-
path:
|
|
85
|
-
relativePath:
|
|
87
|
+
path: realResolved,
|
|
88
|
+
relativePath: relativeResolved,
|
|
86
89
|
startLine,
|
|
87
90
|
endLine,
|
|
88
91
|
lines: selected,
|
|
@@ -2,15 +2,16 @@ import React, { createElement, useEffect, useMemo, useRef, useState } from "reac
|
|
|
2
2
|
import { Box, Text, useApp, useBoxMetrics, useCursor, useInput, useStdout } from "ink";
|
|
3
3
|
import { bold, cyan, dim, green, red, yellow } from "../terminal/colors.mjs";
|
|
4
4
|
import { RunTreeView } from "../components/blocks/run-tree.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { colorCodeLine } from "./code-block.mjs";
|
|
6
6
|
import { getComposerDisplayModel } from "./composer.mjs";
|
|
7
7
|
import { MarkdownBlock } from "./markdown-block.mjs";
|
|
8
8
|
import { QualitySignalStrip } from "./quality-signal-strip.mjs";
|
|
9
9
|
import { buildAssistantViewModel } from "./view-model.mjs";
|
|
10
|
-
import { truncateText
|
|
10
|
+
import { truncateText } from "../terminal/layout.mjs";
|
|
11
11
|
|
|
12
12
|
const FALLBACK_COMMAND_BLOCK_WIDTH = 100;
|
|
13
13
|
const COMMAND_BLOCK_CHROME_WIDTH = 4;
|
|
14
|
+
const COMMAND_BLOCK_BODY_ROWS = 8;
|
|
14
15
|
|
|
15
16
|
export function AssistantApp({
|
|
16
17
|
assistantState,
|
|
@@ -58,6 +59,10 @@ export function AssistantApp({
|
|
|
58
59
|
const runSession = assistantState.getLiveRunSession?.() || assistantState.getLastRunSession?.() || null;
|
|
59
60
|
const serializedRunSession = snapshot.liveRunSession || snapshot.lastRunSession || null;
|
|
60
61
|
const runSessionProductDir = serializedRunSession?.productDir || runSession?.productDir || snapshot.productDir;
|
|
62
|
+
const visibleBlocks = useMemo(
|
|
63
|
+
() => runSession ? view.blocks.filter((block) => !isRunCommandBlock(block)) : view.blocks,
|
|
64
|
+
[runSession, view.blocks]
|
|
65
|
+
);
|
|
61
66
|
|
|
62
67
|
return createElement(
|
|
63
68
|
Box,
|
|
@@ -70,9 +75,9 @@ export function AssistantApp({
|
|
|
70
75
|
})
|
|
71
76
|
: null,
|
|
72
77
|
createElement(HeaderChrome, { view }),
|
|
73
|
-
|
|
78
|
+
visibleBlocks.length === 0
|
|
74
79
|
? createElement(WelcomePanel, { view })
|
|
75
|
-
: createElement(Transcript, { view }),
|
|
80
|
+
: createElement(Transcript, { view: { ...view, blocks: visibleBlocks } }),
|
|
76
81
|
runSession
|
|
77
82
|
? createElement(
|
|
78
83
|
Box,
|
|
@@ -105,6 +110,12 @@ export function AssistantApp({
|
|
|
105
110
|
);
|
|
106
111
|
}
|
|
107
112
|
|
|
113
|
+
function isRunCommandBlock(block) {
|
|
114
|
+
if (!block || block.kind !== "testkit-run") return false;
|
|
115
|
+
const command = String(block.command || "").trim();
|
|
116
|
+
return /^testkit\s+(run\s+)?(ui|e2e|scenario|int|dal|load|all|run)\b/.test(command);
|
|
117
|
+
}
|
|
118
|
+
|
|
108
119
|
function HeaderChrome({ view }) {
|
|
109
120
|
const provider = view.welcome.rows.find(([label]) => label === "Provider")?.[1] || "";
|
|
110
121
|
return createElement(
|
|
@@ -257,12 +268,16 @@ function renderCommandBlock(block, view = {}) {
|
|
|
257
268
|
: block.outputPreview?.omittedLineCount || block.omittedOutputLineCount || 0;
|
|
258
269
|
const blockWidth = Math.max(1, Number(view.terminalWidth) || FALLBACK_COMMAND_BLOCK_WIDTH);
|
|
259
270
|
const contentWidth = Math.max(1, blockWidth - COMMAND_BLOCK_CHROME_WIDTH);
|
|
260
|
-
const
|
|
271
|
+
const commandLine = command ? truncateText(`${dim("$")} ${command}`, contentWidth) : null;
|
|
261
272
|
const statusLine = status ? truncateText(status, contentWidth) : null;
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
273
|
+
const bodyRows = buildCommandBodyRows({
|
|
274
|
+
commandLine,
|
|
275
|
+
statusLine,
|
|
276
|
+
codeBlock,
|
|
277
|
+
outputLines,
|
|
278
|
+
omitted,
|
|
279
|
+
contentWidth,
|
|
280
|
+
});
|
|
266
281
|
|
|
267
282
|
return [
|
|
268
283
|
createElement(
|
|
@@ -277,21 +292,56 @@ function renderCommandBlock(block, view = {}) {
|
|
|
277
292
|
width: blockWidth,
|
|
278
293
|
},
|
|
279
294
|
createElement(Text, { key: "title" }, `${marker} ${title}`),
|
|
280
|
-
...
|
|
281
|
-
statusLine ? createElement(Text, { key: "status" }, colorCommandStatus(block, statusLine)) : null,
|
|
282
|
-
codeBlock ? createElement(Text, { key: "code-gap" }, "") : null,
|
|
283
|
-
...(codeBlock ? CodeBlock({ lines: codeBlock.lines, language: codeBlock.language, width: contentWidth }) : []),
|
|
284
|
-
...previewLines.map((line, index) => (
|
|
285
|
-
createElement(Text, { key: `output-${index}` }, dim(line))
|
|
286
|
-
)),
|
|
287
|
-
omittedLine ? createElement(Text, { key: "omitted" }, dim(omittedLine)) : null,
|
|
288
|
-
block.text && !command && outputLines.length === 0
|
|
289
|
-
? createElement(Text, { key: "text" }, colorBlockText(block, truncateText(block.text, contentWidth)))
|
|
290
|
-
: null
|
|
295
|
+
...bodyRows.map((row, index) => renderCommandBodyRow(block, row, index)),
|
|
291
296
|
),
|
|
292
297
|
];
|
|
293
298
|
}
|
|
294
299
|
|
|
300
|
+
function buildCommandBodyRows({ commandLine, statusLine, codeBlock, outputLines = [], omitted = 0, contentWidth }) {
|
|
301
|
+
const rows = [];
|
|
302
|
+
if (commandLine) rows.push({ tone: "command", text: commandLine });
|
|
303
|
+
if (statusLine) rows.push({ tone: "status", text: statusLine });
|
|
304
|
+
|
|
305
|
+
if (codeBlock) {
|
|
306
|
+
const available = Math.max(0, COMMAND_BLOCK_BODY_ROWS - rows.length - (omitted > 0 ? 1 : 0));
|
|
307
|
+
for (const line of (codeBlock.lines || []).slice(0, available)) {
|
|
308
|
+
rows.push({ tone: "code", language: codeBlock.language, text: truncateText(line, contentWidth) });
|
|
309
|
+
}
|
|
310
|
+
const omittedCount = Math.max(omitted, (codeBlock.lines || []).length - available);
|
|
311
|
+
if (omittedCount > 0 && rows.length < COMMAND_BLOCK_BODY_ROWS) {
|
|
312
|
+
rows.push({ tone: "muted", text: truncateText(`… ${omittedCount} more line${omittedCount === 1 ? "" : "s"} omitted`, contentWidth) });
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
const available = Math.max(0, COMMAND_BLOCK_BODY_ROWS - rows.length - (omitted > 0 ? 1 : 0));
|
|
316
|
+
for (const line of outputLines.slice(0, available)) {
|
|
317
|
+
rows.push({ tone: "muted", text: truncateText(line, contentWidth) });
|
|
318
|
+
}
|
|
319
|
+
const omittedCount = Math.max(omitted, outputLines.length - available);
|
|
320
|
+
if (omittedCount > 0 && rows.length < COMMAND_BLOCK_BODY_ROWS) {
|
|
321
|
+
rows.push({ tone: "muted", text: truncateText(`… ${omittedCount} more line${omittedCount === 1 ? "" : "s"} omitted`, contentWidth) });
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
while (rows.length < COMMAND_BLOCK_BODY_ROWS) rows.push({ tone: "blank", text: "" });
|
|
326
|
+
return rows.slice(0, COMMAND_BLOCK_BODY_ROWS);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function renderCommandBodyRow(block, row, index) {
|
|
330
|
+
if (row.tone === "status") {
|
|
331
|
+
return createElement(Text, { key: `body-${index}` }, colorCommandStatus(block, row.text));
|
|
332
|
+
}
|
|
333
|
+
if (row.tone === "code") {
|
|
334
|
+
return createElement(Text, { key: `body-${index}` }, colorCodeLine(row.text, row.language));
|
|
335
|
+
}
|
|
336
|
+
if (row.tone === "muted") {
|
|
337
|
+
return createElement(Text, { key: `body-${index}` }, dim(row.text));
|
|
338
|
+
}
|
|
339
|
+
if (row.tone === "blank") {
|
|
340
|
+
return createElement(Text, { key: `body-${index}` }, "");
|
|
341
|
+
}
|
|
342
|
+
return createElement(Text, { key: `body-${index}` }, row.text);
|
|
343
|
+
}
|
|
344
|
+
|
|
295
345
|
function formatCommandLine(block) {
|
|
296
346
|
if (!block.command) return null;
|
|
297
347
|
if (typeof block.command === "string") return block.command;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const ASSISTANT_RUN_SHORTCUTS: readonly string[];
|
|
2
|
+
export declare const ASSISTANT_COMMAND_VALUE_FLAGS: readonly string[];
|
|
3
|
+
export type AssistantObservedCommandKind = "run" | "discover" | "status" | "doctor" | "typecheck" | string;
|
|
4
|
+
export declare function classifyAssistantCommandKind(argv?: readonly string[]): AssistantObservedCommandKind;
|
|
5
|
+
export declare function isAssistantRunCommand(kind: string | null | undefined, argv?: readonly string[]): boolean;
|
|
6
|
+
//# sourceMappingURL=command-classifier.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command-classifier.d.mts","sourceRoot":"","sources":["../../../src/cli/assistant/command-classifier.mts"],"names":[],"mappings":"AAAA,eAAO,MAAM,uBAAuB,mBAQlC,CAAC;AAEH,eAAO,MAAM,6BAA6B,mBAUxC,CAAC;AAEH,MAAM,MAAM,4BAA4B,GACpC,KAAK,GACL,UAAU,GACV,QAAQ,GACR,QAAQ,GACR,WAAW,GACX,MAAM,CAAC;AAEX,wBAAgB,4BAA4B,CAAC,IAAI,GAAE,SAAS,MAAM,EAAO,GAAG,4BAA4B,CAIvG;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,IAAI,GAAE,SAAS,MAAM,EAAO,GAAG,OAAO,CAK5G"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export const ASSISTANT_RUN_SHORTCUTS = Object.freeze([
|
|
2
|
+
"ui",
|
|
3
|
+
"e2e",
|
|
4
|
+
"scenario",
|
|
5
|
+
"int",
|
|
6
|
+
"dal",
|
|
7
|
+
"load",
|
|
8
|
+
"all",
|
|
9
|
+
]);
|
|
10
|
+
export const ASSISTANT_COMMAND_VALUE_FLAGS = Object.freeze([
|
|
11
|
+
"--dir",
|
|
12
|
+
"--service",
|
|
13
|
+
"--type",
|
|
14
|
+
"--suite",
|
|
15
|
+
"--file",
|
|
16
|
+
"--workers",
|
|
17
|
+
"--file-timeout-seconds",
|
|
18
|
+
"--seed",
|
|
19
|
+
"--output-mode",
|
|
20
|
+
]);
|
|
21
|
+
export function classifyAssistantCommandKind(argv = []) {
|
|
22
|
+
const first = findFirstPositional(argv, ASSISTANT_COMMAND_VALUE_FLAGS);
|
|
23
|
+
if (!first || ASSISTANT_RUN_SHORTCUTS.includes(first))
|
|
24
|
+
return "run";
|
|
25
|
+
return first;
|
|
26
|
+
}
|
|
27
|
+
export function isAssistantRunCommand(kind, argv = []) {
|
|
28
|
+
if (kind === "run")
|
|
29
|
+
return true;
|
|
30
|
+
if (kind && ASSISTANT_RUN_SHORTCUTS.includes(kind))
|
|
31
|
+
return true;
|
|
32
|
+
const first = argv[0] || null;
|
|
33
|
+
return first === "run" || Boolean(first && ASSISTANT_RUN_SHORTCUTS.includes(first));
|
|
34
|
+
}
|
|
35
|
+
function findFirstPositional(args, flagsWithValues) {
|
|
36
|
+
const valueFlags = new Set(flagsWithValues);
|
|
37
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
38
|
+
const value = String(args[index] || "");
|
|
39
|
+
if (valueFlags.has(value)) {
|
|
40
|
+
index += 1;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (!value.startsWith("-"))
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=command-classifier.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command-classifier.mjs","sourceRoot":"","sources":["../../../src/cli/assistant/command-classifier.mts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC,MAAM,CAAC;IACnD,IAAI;IACJ,KAAK;IACL,UAAU;IACV,KAAK;IACL,KAAK;IACL,MAAM;IACN,KAAK;CACN,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,6BAA6B,GAAG,MAAM,CAAC,MAAM,CAAC;IACzD,OAAO;IACP,WAAW;IACX,QAAQ;IACR,SAAS;IACT,QAAQ;IACR,WAAW;IACX,wBAAwB;IACxB,QAAQ;IACR,eAAe;CAChB,CAAC,CAAC;AAUH,MAAM,UAAU,4BAA4B,CAAC,OAA0B,EAAE;IACvE,MAAM,KAAK,GAAG,mBAAmB,CAAC,IAAI,EAAE,6BAA6B,CAAC,CAAC;IACvE,IAAI,CAAC,KAAK,IAAI,uBAAuB,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACpE,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,IAA+B,EAAE,OAA0B,EAAE;IACjG,IAAI,IAAI,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IAChC,IAAI,IAAI,IAAI,uBAAuB,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAChE,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;IAC9B,OAAO,KAAK,KAAK,KAAK,IAAI,OAAO,CAAC,KAAK,IAAI,uBAAuB,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;AACtF,CAAC;AAED,SAAS,mBAAmB,CAAC,IAAuB,EAAE,eAAkC;IACtF,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,eAAe,CAAC,CAAC;IAC5C,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QACpD,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;QACxC,IAAI,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YAC1B,KAAK,IAAI,CAAC,CAAC;YACX,SAAS;QACX,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;IAC3C,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function normalizeCommandLine(value) {
|
|
2
|
+
const text = unwrapShellCommand(value)
|
|
3
|
+
.replace(/\s+/g, " ")
|
|
4
|
+
.trim();
|
|
5
|
+
if (!text) return null;
|
|
6
|
+
return canonicalizeTestkitInvocation(text);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function unwrapShellCommand(value) {
|
|
10
|
+
const text = String(value || "").trim();
|
|
11
|
+
const match = text.match(/(?:^|\s)(?:[^\s'"]*\/)?(?:bash|sh|zsh)\s+-lc\s+(['"])([\s\S]*)\1\s*$/);
|
|
12
|
+
if (!match) return text;
|
|
13
|
+
return match[2].replace(/\\(["'\\$`])/g, "$1").trim();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function canonicalizeTestkitInvocation(command) {
|
|
17
|
+
return command
|
|
18
|
+
.replace(/^npx\s+(?:--yes\s+|-y\s+)?testkit\b/, "testkit")
|
|
19
|
+
.replace(/^node(?:\s+--[^\s]+)*\s+(?:(?:\.?\.?\/)?[^\s]+\/)?(?:bin\/)?testkit(?:\.(?:mjs|js))?\b/, "testkit")
|
|
20
|
+
.replace(/^(?:(?:\.?\.?\/)?[^\s]+\/)?(?:bin\/)?testkit(?:\.(?:mjs|js))?\b/, "testkit")
|
|
21
|
+
.trim();
|
|
22
|
+
}
|
|
@@ -1,30 +1,32 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import {
|
|
3
|
+
import { isAssistantRunCommand } from "./command-classifier.mjs";
|
|
4
4
|
|
|
5
5
|
const POLL_INTERVAL_MS = 150;
|
|
6
6
|
const OBSERVED_KINDS = new Set(["run", "discover", "status", "doctor", "typecheck"]);
|
|
7
|
-
const RUN_KINDS = new Set(["run", ...publicTestTypeList({ includeAll: true, includeLegacy: true })]);
|
|
8
7
|
|
|
9
8
|
export function createAssistantCommandObserver({
|
|
10
9
|
productDir,
|
|
11
10
|
commandLog,
|
|
11
|
+
turnId = null,
|
|
12
12
|
runState,
|
|
13
13
|
onEvent,
|
|
14
14
|
intervalMs = POLL_INTERVAL_MS,
|
|
15
15
|
} = {}) {
|
|
16
16
|
const seenResultFiles = new Set();
|
|
17
|
-
const seenCommandLogEvents = new Set();
|
|
18
17
|
const observedRunCommandIds = new Set();
|
|
18
|
+
const hydratedArtifactKeys = new Set();
|
|
19
19
|
let timer = null;
|
|
20
20
|
let running = false;
|
|
21
21
|
let lastArtifactSignatures = new Map();
|
|
22
|
+
let commandLogOffset = 0;
|
|
22
23
|
|
|
23
24
|
function start() {
|
|
24
25
|
if (running) return;
|
|
25
26
|
running = true;
|
|
27
|
+
commandLogOffset = currentCommandLogSize();
|
|
28
|
+
markExistingResultFilesSeen();
|
|
26
29
|
lastArtifactSignatures = readArtifactSignatures();
|
|
27
|
-
scan();
|
|
28
30
|
timer = setInterval(scan, intervalMs);
|
|
29
31
|
}
|
|
30
32
|
|
|
@@ -44,18 +46,30 @@ export function createAssistantCommandObserver({
|
|
|
44
46
|
function observeCommandLog() {
|
|
45
47
|
const commandLogPath = commandLog?.commandLogPath;
|
|
46
48
|
if (!commandLogPath || !fs.existsSync(commandLogPath)) return;
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
const stat = safeStat(commandLogPath);
|
|
50
|
+
if (!stat) return;
|
|
51
|
+
if (stat.size < commandLogOffset) commandLogOffset = 0;
|
|
52
|
+
if (stat.size === commandLogOffset) return;
|
|
53
|
+
const file = fs.openSync(commandLogPath, "r");
|
|
54
|
+
let chunk = "";
|
|
55
|
+
try {
|
|
56
|
+
const length = stat.size - commandLogOffset;
|
|
57
|
+
const buffer = Buffer.alloc(length);
|
|
58
|
+
fs.readSync(file, buffer, 0, length, commandLogOffset);
|
|
59
|
+
commandLogOffset = stat.size;
|
|
60
|
+
chunk = buffer.toString("utf8");
|
|
61
|
+
} finally {
|
|
62
|
+
fs.closeSync(file);
|
|
63
|
+
}
|
|
64
|
+
const lines = chunk.split(/\r?\n/).filter(Boolean);
|
|
65
|
+
for (const line of lines) {
|
|
52
66
|
let event = null;
|
|
53
67
|
try {
|
|
54
68
|
event = JSON.parse(line);
|
|
55
69
|
} catch {
|
|
56
70
|
continue;
|
|
57
71
|
}
|
|
58
|
-
if (event
|
|
72
|
+
if (!isCurrentObservation(event)) continue;
|
|
59
73
|
if (event.type === "command_start" && isRunCommand(event)) {
|
|
60
74
|
observedRunCommandIds.add(event.commandId);
|
|
61
75
|
}
|
|
@@ -78,9 +92,12 @@ export function createAssistantCommandObserver({
|
|
|
78
92
|
} catch {
|
|
79
93
|
continue;
|
|
80
94
|
}
|
|
81
|
-
if (document
|
|
82
|
-
|
|
95
|
+
if (!isCurrentObservation(document)) {
|
|
96
|
+
seenResultFiles.add(filePath);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
83
99
|
seenResultFiles.add(filePath);
|
|
100
|
+
if (!OBSERVED_KINDS.has(document.kind)) continue;
|
|
84
101
|
if (document.kind === "run") {
|
|
85
102
|
observedRunCommandIds.add(document.commandId);
|
|
86
103
|
hydrateRunArtifact("command-result", document);
|
|
@@ -126,6 +143,9 @@ export function createAssistantCommandObserver({
|
|
|
126
143
|
function hydrateRunArtifact(source, command = null) {
|
|
127
144
|
const artifact = loadObservedRunArtifact(command);
|
|
128
145
|
if (!artifact) return;
|
|
146
|
+
const key = artifactKey(source, command, artifact);
|
|
147
|
+
if (hydratedArtifactKeys.has(key)) return;
|
|
148
|
+
hydratedArtifactKeys.add(key);
|
|
129
149
|
runState?.hydrateFromArtifact?.(artifact);
|
|
130
150
|
onEvent?.({
|
|
131
151
|
type: "observed-run-artifact",
|
|
@@ -146,15 +166,14 @@ export function createAssistantCommandObserver({
|
|
|
146
166
|
const assistant = artifact?.provenance?.assistant || {};
|
|
147
167
|
if (!assistant.sessionId || !assistant.commandId) return false;
|
|
148
168
|
if (commandLog?.sessionId && assistant.sessionId !== commandLog.sessionId) return false;
|
|
169
|
+
if (turnId && assistant.turnId !== turnId) return false;
|
|
149
170
|
return observedRunCommandIds.has(assistant.commandId);
|
|
150
171
|
}
|
|
151
172
|
|
|
152
173
|
function isRunCommand(event) {
|
|
153
174
|
if (!event?.commandId) return false;
|
|
154
|
-
if (event.kind === "run") return true;
|
|
155
|
-
if (RUN_KINDS.has(event.kind)) return true;
|
|
156
175
|
const argv = Array.isArray(event.argv) ? event.argv : [];
|
|
157
|
-
return
|
|
176
|
+
return isAssistantRunCommand(event.kind, argv);
|
|
158
177
|
}
|
|
159
178
|
|
|
160
179
|
function readJsonFile(filePath) {
|
|
@@ -165,6 +184,29 @@ export function createAssistantCommandObserver({
|
|
|
165
184
|
}
|
|
166
185
|
}
|
|
167
186
|
|
|
187
|
+
function isCurrentObservation(entry) {
|
|
188
|
+
if (entry?.sessionId && commandLog?.sessionId && entry.sessionId !== commandLog.sessionId) return false;
|
|
189
|
+
if (turnId && entry?.turnId !== turnId) return false;
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function markExistingResultFilesSeen() {
|
|
194
|
+
const resultDir = commandLog?.resultDir;
|
|
195
|
+
if (!resultDir || !fs.existsSync(resultDir)) return;
|
|
196
|
+
for (const entry of fs.readdirSync(resultDir, { withFileTypes: true })) {
|
|
197
|
+
if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
198
|
+
seenResultFiles.add(path.join(resultDir, entry.name));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function currentCommandLogSize() {
|
|
204
|
+
const commandLogPath = commandLog?.commandLogPath;
|
|
205
|
+
if (!commandLogPath) return 0;
|
|
206
|
+
const stat = safeStat(commandLogPath);
|
|
207
|
+
return stat?.size || 0;
|
|
208
|
+
}
|
|
209
|
+
|
|
168
210
|
function safeStat(filePath) {
|
|
169
211
|
try {
|
|
170
212
|
return fs.statSync(filePath);
|
|
@@ -173,6 +215,18 @@ export function createAssistantCommandObserver({
|
|
|
173
215
|
}
|
|
174
216
|
}
|
|
175
217
|
|
|
218
|
+
function artifactKey(source, command, artifact) {
|
|
219
|
+
const assistant = artifact?.provenance?.assistant || {};
|
|
220
|
+
const runId = artifact?.run?.id || command?.result?.runArtifact?.run?.id || command?.artifact?.run?.id || "unknown-run";
|
|
221
|
+
return [
|
|
222
|
+
source,
|
|
223
|
+
assistant.sessionId || commandLog?.sessionId || "",
|
|
224
|
+
assistant.turnId || turnId || "",
|
|
225
|
+
assistant.commandId || command?.commandId || "",
|
|
226
|
+
runId,
|
|
227
|
+
].join("|");
|
|
228
|
+
}
|
|
229
|
+
|
|
176
230
|
return {
|
|
177
231
|
start,
|
|
178
232
|
stop,
|
|
@@ -1,26 +1,14 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import {
|
|
3
|
+
import { classifyAssistantCommandKind } from "./command-classifier.mjs";
|
|
4
4
|
|
|
5
5
|
export const ASSISTANT_SESSION_ENV = "TESTKIT_ASSISTANT_SESSION_ID";
|
|
6
6
|
export const ASSISTANT_RESULT_DIR_ENV = "TESTKIT_ASSISTANT_RESULT_DIR";
|
|
7
7
|
export const ASSISTANT_COMMAND_LOG_ENV = "TESTKIT_ASSISTANT_COMMAND_LOG";
|
|
8
8
|
export const ASSISTANT_COMMAND_ID_ENV = "TESTKIT_ASSISTANT_COMMAND_ID";
|
|
9
|
+
export const ASSISTANT_TURN_ENV = "TESTKIT_ASSISTANT_TURN_ID";
|
|
9
10
|
export const ASSISTANT_WRAPPER_LOGGED_ENV = "TESTKIT_ASSISTANT_WRAPPER_LOGGED";
|
|
10
11
|
|
|
11
|
-
const RUN_SHORTCUTS = new Set(publicTestTypeList({ includeAll: true, includeLegacy: true }));
|
|
12
|
-
const FLAGS_WITH_VALUES = new Set([
|
|
13
|
-
"--dir",
|
|
14
|
-
"--service",
|
|
15
|
-
"--type",
|
|
16
|
-
"--suite",
|
|
17
|
-
"--file",
|
|
18
|
-
"--workers",
|
|
19
|
-
"--file-timeout-seconds",
|
|
20
|
-
"--seed",
|
|
21
|
-
"--output-mode",
|
|
22
|
-
]);
|
|
23
|
-
|
|
24
12
|
export function createAssistantCommandContext({
|
|
25
13
|
kind,
|
|
26
14
|
argv = process.argv.slice(2),
|
|
@@ -28,6 +16,7 @@ export function createAssistantCommandContext({
|
|
|
28
16
|
env = process.env,
|
|
29
17
|
} = {}) {
|
|
30
18
|
const sessionId = env[ASSISTANT_SESSION_ENV] || null;
|
|
19
|
+
const turnId = env[ASSISTANT_TURN_ENV] || null;
|
|
31
20
|
const resultDir = env[ASSISTANT_RESULT_DIR_ENV] || null;
|
|
32
21
|
const commandLogPath = env[ASSISTANT_COMMAND_LOG_ENV] || null;
|
|
33
22
|
if (!sessionId && !resultDir && !commandLogPath) return null;
|
|
@@ -36,10 +25,11 @@ export function createAssistantCommandContext({
|
|
|
36
25
|
const startedAt = new Date().toISOString();
|
|
37
26
|
return {
|
|
38
27
|
sessionId,
|
|
28
|
+
turnId,
|
|
39
29
|
resultDir,
|
|
40
30
|
commandLogPath,
|
|
41
31
|
commandId,
|
|
42
|
-
kind: kind ||
|
|
32
|
+
kind: kind || classifyAssistantCommandKind(argv),
|
|
43
33
|
argv: Array.isArray(argv) ? argv.map(String) : [],
|
|
44
34
|
cwd,
|
|
45
35
|
startedAt,
|
|
@@ -116,6 +106,7 @@ export function writeAssistantCommandResult(context, payload = {}) {
|
|
|
116
106
|
schemaVersion: 1,
|
|
117
107
|
source: "testkit-command-result",
|
|
118
108
|
sessionId: context.sessionId,
|
|
109
|
+
turnId: context.turnId,
|
|
119
110
|
commandId: context.commandId,
|
|
120
111
|
kind: context.kind,
|
|
121
112
|
argv: context.argv,
|
|
@@ -141,7 +132,12 @@ export function appendAssistantCommandLog(context, event) {
|
|
|
141
132
|
fs.mkdirSync(path.dirname(commandLogPath), { recursive: true });
|
|
142
133
|
fs.appendFileSync(
|
|
143
134
|
commandLogPath,
|
|
144
|
-
`${JSON.stringify({
|
|
135
|
+
`${JSON.stringify({
|
|
136
|
+
timestamp: new Date().toISOString(),
|
|
137
|
+
sessionId: context?.sessionId || null,
|
|
138
|
+
turnId: context?.turnId || null,
|
|
139
|
+
...event,
|
|
140
|
+
})}\n`,
|
|
145
141
|
"utf8"
|
|
146
142
|
);
|
|
147
143
|
} catch {
|
|
@@ -149,25 +145,6 @@ export function appendAssistantCommandLog(context, event) {
|
|
|
149
145
|
}
|
|
150
146
|
}
|
|
151
147
|
|
|
152
|
-
function inferCommandKind(argv) {
|
|
153
|
-
const first = findFirstPositional(argv);
|
|
154
|
-
if (!first || RUN_SHORTCUTS.has(first)) return "run";
|
|
155
|
-
return first;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function findFirstPositional(argv) {
|
|
159
|
-
const args = Array.isArray(argv) ? argv : [];
|
|
160
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
161
|
-
const arg = String(args[index]);
|
|
162
|
-
if (FLAGS_WITH_VALUES.has(arg)) {
|
|
163
|
-
index += 1;
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
if (!arg.startsWith("-")) return arg;
|
|
167
|
-
}
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
148
|
function inferExitCode(result) {
|
|
172
149
|
if (Number.isInteger(result?.exitCode)) return result.exitCode;
|
|
173
150
|
if (result?.ok === false) return 1;
|