@elench/testkit 0.1.98 → 0.1.99
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/lib/cli/assistant/actions.mjs +333 -0
- package/lib/cli/assistant/command-observer.mjs +110 -0
- package/lib/cli/assistant/command-results.mjs +167 -0
- package/lib/cli/assistant/context-pack.mjs +70 -3
- package/lib/cli/assistant/prompt-builder.mjs +14 -7
- package/lib/cli/assistant/providers/claude.mjs +2 -3
- package/lib/cli/assistant/providers/codex.mjs +2 -6
- package/lib/cli/assistant/providers/index.mjs +2 -2
- package/lib/cli/assistant/session.mjs +32 -90
- package/lib/cli/assistant/state.mjs +43 -29
- package/lib/cli/commands/assistant.mjs +1 -1
- package/lib/cli/commands/cleanup.mjs +1 -1
- package/lib/cli/commands/db/snapshot/capture.mjs +1 -1
- package/lib/cli/commands/destroy.mjs +1 -1
- package/lib/cli/commands/discover.mjs +11 -8
- package/lib/cli/commands/doctor.mjs +15 -12
- package/lib/cli/commands/run.mjs +11 -8
- package/lib/cli/commands/status.mjs +11 -8
- package/lib/cli/commands/typecheck.mjs +10 -7
- package/lib/cli/entrypoint.mjs +14 -5
- 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 +6 -6
- package/lib/cli/assistant/protocol.mjs +0 -67
- package/lib/cli/assistant/tool-registry.mjs +0 -584
- /package/lib/cli/{commands/flags.mjs → command-flags.mjs} +0 -0
|
@@ -2,6 +2,13 @@ import fs from "fs";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
4
|
import { readContextContent, buildContextSelection } from "../../results/context.mjs";
|
|
5
|
+
import {
|
|
6
|
+
ASSISTANT_COMMAND_ID_ENV,
|
|
7
|
+
ASSISTANT_COMMAND_LOG_ENV,
|
|
8
|
+
ASSISTANT_RESULT_DIR_ENV,
|
|
9
|
+
ASSISTANT_SESSION_ENV,
|
|
10
|
+
ASSISTANT_WRAPPER_LOGGED_ENV,
|
|
11
|
+
} from "./command-results.mjs";
|
|
5
12
|
|
|
6
13
|
export function prepareAssistantContextPack({
|
|
7
14
|
productDir,
|
|
@@ -9,7 +16,11 @@ export function prepareAssistantContextPack({
|
|
|
9
16
|
} = {}) {
|
|
10
17
|
const contextDir = path.join(productDir, ".testkit", "assistant");
|
|
11
18
|
const binDir = path.join(contextDir, "bin");
|
|
19
|
+
const resultDir = path.join(contextDir, "command-results");
|
|
20
|
+
const sessionId = `session-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
12
21
|
fs.mkdirSync(binDir, { recursive: true });
|
|
22
|
+
fs.rmSync(resultDir, { recursive: true, force: true });
|
|
23
|
+
fs.mkdirSync(resultDir, { recursive: true });
|
|
13
24
|
|
|
14
25
|
const commandLogPath = path.join(contextDir, "commands.jsonl");
|
|
15
26
|
const contextPath = path.join(contextDir, "context.md");
|
|
@@ -55,7 +66,7 @@ export function prepareAssistantContextPack({
|
|
|
55
66
|
}),
|
|
56
67
|
"utf8"
|
|
57
68
|
);
|
|
58
|
-
fs.writeFileSync(wrapperPath, buildWrapperScript({ cliPath: resolveCliPath() }), {
|
|
69
|
+
fs.writeFileSync(wrapperPath, buildWrapperScript({ cliPath: resolveCliPath(), sessionId, resultDir, commandLogPath }), {
|
|
59
70
|
encoding: "utf8",
|
|
60
71
|
mode: 0o755,
|
|
61
72
|
});
|
|
@@ -71,12 +82,23 @@ export function prepareAssistantContextPack({
|
|
|
71
82
|
selectionPath,
|
|
72
83
|
commandsPath,
|
|
73
84
|
commandLogPath,
|
|
85
|
+
resultDir,
|
|
86
|
+
sessionId,
|
|
74
87
|
focusedDetailPath,
|
|
75
88
|
focusedLogsPath,
|
|
76
89
|
focusedArtifactsPath,
|
|
77
90
|
focusedSetupPath,
|
|
78
91
|
binDir,
|
|
79
92
|
wrapperPath,
|
|
93
|
+
providerEnv(baseEnv = process.env) {
|
|
94
|
+
return {
|
|
95
|
+
...baseEnv,
|
|
96
|
+
PATH: [binDir, baseEnv?.PATH, process.env.PATH].filter(Boolean).join(path.delimiter),
|
|
97
|
+
[ASSISTANT_SESSION_ENV]: sessionId,
|
|
98
|
+
[ASSISTANT_RESULT_DIR_ENV]: resultDir,
|
|
99
|
+
[ASSISTANT_COMMAND_LOG_ENV]: commandLogPath,
|
|
100
|
+
};
|
|
101
|
+
},
|
|
80
102
|
refresh,
|
|
81
103
|
appendCommandLog(event) {
|
|
82
104
|
if (!event || typeof event !== "object") return;
|
|
@@ -97,15 +119,36 @@ function resolveCliPath() {
|
|
|
97
119
|
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "bin", "testkit.mjs");
|
|
98
120
|
}
|
|
99
121
|
|
|
100
|
-
function buildWrapperScript({ cliPath } = {}) {
|
|
122
|
+
function buildWrapperScript({ cliPath, sessionId, resultDir, commandLogPath } = {}) {
|
|
101
123
|
return `#!/usr/bin/env node
|
|
102
124
|
import { spawnSync } from "child_process";
|
|
125
|
+
import fs from "fs";
|
|
126
|
+
import path from "path";
|
|
127
|
+
|
|
128
|
+
const commandId = process.env.${ASSISTANT_COMMAND_ID_ENV} || \`cmd-\${Date.now()}-\${Math.random().toString(36).slice(2, 10)}\`;
|
|
129
|
+
const commandLogPath = process.env.${ASSISTANT_COMMAND_LOG_ENV} || ${JSON.stringify(commandLogPath)};
|
|
130
|
+
const sessionId = process.env.${ASSISTANT_SESSION_ENV} || ${JSON.stringify(sessionId)};
|
|
131
|
+
const argv = process.argv.slice(2);
|
|
132
|
+
|
|
133
|
+
appendCommandLog({
|
|
134
|
+
type: "command_start",
|
|
135
|
+
commandId,
|
|
136
|
+
command: "testkit",
|
|
137
|
+
kind: inferKind(argv),
|
|
138
|
+
argv,
|
|
139
|
+
cwd: process.cwd(),
|
|
140
|
+
});
|
|
103
141
|
|
|
104
142
|
const result = spawnSync(process.execPath, [${JSON.stringify(cliPath)}, ...process.argv.slice(2)], {
|
|
105
143
|
cwd: process.cwd(),
|
|
106
144
|
env: {
|
|
107
145
|
...process.env,
|
|
108
146
|
TESTKIT_NO_ASSISTANT_DEFAULT: "1",
|
|
147
|
+
${ASSISTANT_SESSION_ENV}: sessionId,
|
|
148
|
+
${ASSISTANT_RESULT_DIR_ENV}: process.env.${ASSISTANT_RESULT_DIR_ENV} || ${JSON.stringify(resultDir)},
|
|
149
|
+
${ASSISTANT_COMMAND_LOG_ENV}: commandLogPath,
|
|
150
|
+
${ASSISTANT_COMMAND_ID_ENV}: commandId,
|
|
151
|
+
${ASSISTANT_WRAPPER_LOGGED_ENV}: "1",
|
|
109
152
|
},
|
|
110
153
|
stdio: "inherit",
|
|
111
154
|
});
|
|
@@ -113,7 +156,30 @@ const result = spawnSync(process.execPath, [${JSON.stringify(cliPath)}, ...proce
|
|
|
113
156
|
if (result.error) {
|
|
114
157
|
throw result.error;
|
|
115
158
|
}
|
|
159
|
+
appendCommandLog({
|
|
160
|
+
type: "command_exit",
|
|
161
|
+
commandId,
|
|
162
|
+
command: "testkit",
|
|
163
|
+
kind: inferKind(argv),
|
|
164
|
+
argv,
|
|
165
|
+
cwd: process.cwd(),
|
|
166
|
+
code: result.status ?? 0,
|
|
167
|
+
signal: result.signal ?? null,
|
|
168
|
+
});
|
|
116
169
|
process.exit(result.status ?? 0);
|
|
170
|
+
|
|
171
|
+
function appendCommandLog(event) {
|
|
172
|
+
try {
|
|
173
|
+
fs.mkdirSync(path.dirname(commandLogPath), { recursive: true });
|
|
174
|
+
fs.appendFileSync(commandLogPath, \`\${JSON.stringify({ timestamp: new Date().toISOString(), sessionId, ...event })}\\n\`, "utf8");
|
|
175
|
+
} catch {
|
|
176
|
+
// Command observation must not affect command execution.
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function inferKind(args) {
|
|
181
|
+
return args.find((arg) => !String(arg).startsWith("-")) || "run";
|
|
182
|
+
}
|
|
117
183
|
`;
|
|
118
184
|
}
|
|
119
185
|
|
|
@@ -160,7 +226,8 @@ function buildContextMarkdown(productDir, snapshot, paths) {
|
|
|
160
226
|
lines.push(
|
|
161
227
|
"",
|
|
162
228
|
"## Guidance",
|
|
163
|
-
"-
|
|
229
|
+
"- Work normally in the repository and run real commands. Testkit observes recognized Testkit commands and renders them in the assistant UI.",
|
|
230
|
+
"- Prefer `testkit ...` commands when using Testkit directly; `npx testkit ...` and project scripts are also valid.",
|
|
164
231
|
"- Do not reinterpret CLI syntax after an execution failure unless `testkit run --help` confirms a syntax problem.",
|
|
165
232
|
"- Use the command log and focused context files before rereading artifacts manually.",
|
|
166
233
|
"- Prefer repo-local commands over guessing project-specific wrappers.",
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { readContextContent } from "../../results/context.mjs";
|
|
2
|
-
import { buildAssistantResponseContract } from "./protocol.mjs";
|
|
3
2
|
|
|
4
3
|
export function buildAssistantPrompt({
|
|
5
4
|
productDir,
|
|
6
5
|
snapshot,
|
|
7
6
|
transcript = [],
|
|
8
|
-
tools = [],
|
|
9
7
|
userMessage,
|
|
8
|
+
commandLog = null,
|
|
10
9
|
} = {}) {
|
|
11
10
|
const selectionSummary = buildSelectionSummary(snapshot);
|
|
12
11
|
const focusPreview = buildFocusPreview(productDir, snapshot);
|
|
@@ -14,11 +13,19 @@ export function buildAssistantPrompt({
|
|
|
14
13
|
|
|
15
14
|
return [
|
|
16
15
|
"You are Testkit Assistant.",
|
|
17
|
-
"You
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
|
|
16
|
+
"You are running as a coding agent inside the user's repository.",
|
|
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 int`, `npx testkit run int`, or the repository's package scripts.",
|
|
19
|
+
"Testkit observes recognized Testkit commands and renders rich assistant UI from the real command output, sidecars, and artifacts.",
|
|
20
|
+
"Do not respond with a JSON tool envelope. Give the user a normal final answer when you are done.",
|
|
21
|
+
"",
|
|
22
|
+
"Assistant context files:",
|
|
23
|
+
...(commandLog ? [
|
|
24
|
+
`- Context: ${commandLog.contextPath}`,
|
|
25
|
+
`- Command reference: ${commandLog.commandsPath}`,
|
|
26
|
+
`- Command log: ${commandLog.commandLogPath}`,
|
|
27
|
+
`- Current selection: ${commandLog.selectionPath}`,
|
|
28
|
+
] : ["- No assistant context pack is available."]),
|
|
22
29
|
"",
|
|
23
30
|
"Current run summary:",
|
|
24
31
|
...(summaryRows.length > 0 ? summaryRows.map(([label, value]) => `- ${label}: ${value}`) : ["- No run artifact is currently loaded."]),
|
|
@@ -16,6 +16,7 @@ export function startClaudeHostedSession({
|
|
|
16
16
|
model = null,
|
|
17
17
|
effort = null,
|
|
18
18
|
providerArgs = [],
|
|
19
|
+
env = process.env,
|
|
19
20
|
} = {}) {
|
|
20
21
|
const args = [
|
|
21
22
|
"-p",
|
|
@@ -25,9 +26,6 @@ export function startClaudeHostedSession({
|
|
|
25
26
|
"--include-partial-messages",
|
|
26
27
|
];
|
|
27
28
|
|
|
28
|
-
if (purpose === "assistant") {
|
|
29
|
-
args.push("--permission-mode", "plan");
|
|
30
|
-
}
|
|
31
29
|
if (model) {
|
|
32
30
|
args.push("--model", String(model));
|
|
33
31
|
}
|
|
@@ -44,6 +42,7 @@ export function startClaudeHostedSession({
|
|
|
44
42
|
stdout: "pipe",
|
|
45
43
|
stderr: "pipe",
|
|
46
44
|
reject: false,
|
|
45
|
+
env,
|
|
47
46
|
});
|
|
48
47
|
|
|
49
48
|
return createHostedSessionRunner({
|
|
@@ -19,6 +19,7 @@ export function startCodexHostedSession({
|
|
|
19
19
|
purpose = "assistant",
|
|
20
20
|
model = null,
|
|
21
21
|
providerArgs = [],
|
|
22
|
+
env = process.env,
|
|
22
23
|
} = {}) {
|
|
23
24
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-codex-"));
|
|
24
25
|
const outputFile = path.join(tempDir, "final-message.txt");
|
|
@@ -28,7 +29,6 @@ export function startCodexHostedSession({
|
|
|
28
29
|
model,
|
|
29
30
|
providerArgs,
|
|
30
31
|
prompt,
|
|
31
|
-
sandbox: process.env.TESTKIT_CODEX_SANDBOX,
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
const child = execa(command, args, {
|
|
@@ -37,6 +37,7 @@ export function startCodexHostedSession({
|
|
|
37
37
|
stdout: "pipe",
|
|
38
38
|
stderr: "pipe",
|
|
39
39
|
reject: false,
|
|
40
|
+
env,
|
|
40
41
|
});
|
|
41
42
|
|
|
42
43
|
const session = createHostedSessionRunner({
|
|
@@ -64,18 +65,13 @@ export function startCodexHostedSession({
|
|
|
64
65
|
|
|
65
66
|
export function buildCodexArgs({
|
|
66
67
|
outputFile,
|
|
67
|
-
purpose = "assistant",
|
|
68
68
|
model = null,
|
|
69
69
|
providerArgs = [],
|
|
70
70
|
prompt = "",
|
|
71
|
-
sandbox = null,
|
|
72
71
|
} = {}) {
|
|
73
72
|
const args = ["exec", "--json"];
|
|
74
73
|
if (outputFile) args.push("-o", outputFile);
|
|
75
74
|
|
|
76
|
-
if (purpose === "assistant") {
|
|
77
|
-
args.push("-s", String(sandbox || "workspace-write"));
|
|
78
|
-
}
|
|
79
75
|
if (model) {
|
|
80
76
|
args.push("--model", String(model));
|
|
81
77
|
}
|
|
@@ -69,7 +69,7 @@ export function startProviderSession({
|
|
|
69
69
|
const resolvedProvider = resolvePreferredProvider(provider, env);
|
|
70
70
|
const command = resolveProviderBinary(resolvedProvider, env);
|
|
71
71
|
if (resolvedProvider === "claude") {
|
|
72
|
-
return startClaudeHostedSession({ command, cwd, prompt, onEvent, purpose, model, effort, providerArgs });
|
|
72
|
+
return startClaudeHostedSession({ command, cwd, prompt, onEvent, purpose, model, effort, providerArgs, env });
|
|
73
73
|
}
|
|
74
|
-
return startCodexHostedSession({ command, cwd, prompt, onEvent, purpose, model, providerArgs });
|
|
74
|
+
return startCodexHostedSession({ command, cwd, prompt, onEvent, purpose, model, providerArgs, env });
|
|
75
75
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { startProviderSession, resolvePreferredProvider } from "./providers/index.mjs";
|
|
2
2
|
import { buildAssistantPrompt } from "./prompt-builder.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import { parseAssistantEnvelope } from "./protocol.mjs";
|
|
3
|
+
import { createAssistantCommandObserver } from "./command-observer.mjs";
|
|
5
4
|
|
|
6
5
|
export async function runAssistantConversationTurn({
|
|
7
6
|
productDir,
|
|
@@ -18,40 +17,36 @@ export async function runAssistantConversationTurn({
|
|
|
18
17
|
onResolvedProvider,
|
|
19
18
|
onPrompt,
|
|
20
19
|
} = {}) {
|
|
21
|
-
const
|
|
22
|
-
const toolContext = {
|
|
20
|
+
const observer = createAssistantCommandObserver({
|
|
23
21
|
productDir,
|
|
24
22
|
runState,
|
|
25
|
-
configs,
|
|
26
|
-
env,
|
|
27
23
|
commandLog,
|
|
28
24
|
onEvent: onToolEvent,
|
|
29
|
-
};
|
|
25
|
+
});
|
|
30
26
|
|
|
31
|
-
|
|
32
|
-
const
|
|
27
|
+
const snapshot = runState.getSnapshot();
|
|
28
|
+
const prompt = buildAssistantPrompt({
|
|
29
|
+
productDir,
|
|
30
|
+
snapshot,
|
|
31
|
+
transcript: [...(transcript || []), { role: "user", text: userMessage }],
|
|
32
|
+
userMessage,
|
|
33
|
+
commandLog,
|
|
34
|
+
});
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
36
|
+
const runtimeSettings = settings || { provider };
|
|
37
|
+
const resolvedProvider = resolvePreferredProvider(runtimeSettings.provider || provider, env);
|
|
38
|
+
const providerEnv = commandLog?.providerEnv?.(env) || env;
|
|
39
|
+
onResolvedProvider?.(resolvedProvider);
|
|
40
|
+
onPrompt?.({
|
|
41
|
+
prompt,
|
|
42
|
+
provider: resolvedProvider,
|
|
43
|
+
model: runtimeSettings.model || null,
|
|
44
|
+
effort: runtimeSettings.effort || null,
|
|
45
|
+
});
|
|
46
|
+
onStatus?.(`Thinking with ${resolvedProvider}...`);
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
onResolvedProvider?.(resolvedProvider);
|
|
47
|
-
onPrompt?.({
|
|
48
|
-
prompt,
|
|
49
|
-
provider: resolvedProvider,
|
|
50
|
-
model: runtimeSettings.model || null,
|
|
51
|
-
effort: runtimeSettings.effort || null,
|
|
52
|
-
});
|
|
53
|
-
onStatus?.(`Thinking with ${resolvedProvider}...`);
|
|
54
|
-
const events = [];
|
|
48
|
+
observer.start();
|
|
49
|
+
try {
|
|
55
50
|
const session = startProviderSession({
|
|
56
51
|
provider: runtimeSettings.provider || provider,
|
|
57
52
|
model: runtimeSettings.model || null,
|
|
@@ -60,74 +55,21 @@ export async function runAssistantConversationTurn({
|
|
|
60
55
|
cwd: productDir,
|
|
61
56
|
prompt,
|
|
62
57
|
purpose: "assistant",
|
|
63
|
-
env,
|
|
58
|
+
env: providerEnv,
|
|
64
59
|
onEvent(event) {
|
|
65
|
-
events.push(event);
|
|
66
60
|
if (event.type === "status" || event.type === "tool") onStatus?.(formatProviderEvent(event));
|
|
67
61
|
},
|
|
68
62
|
});
|
|
69
63
|
const result = await session.completion;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (envelope.commentary) {
|
|
74
|
-
emitted.push({ role: "assistant", text: envelope.commentary });
|
|
75
|
-
currentTranscript.push({ role: "assistant", text: envelope.commentary });
|
|
76
|
-
}
|
|
77
|
-
let toolResult;
|
|
78
|
-
try {
|
|
79
|
-
toolResult = await executeAssistantTool(envelope.tool, envelope.arguments, toolContext);
|
|
80
|
-
} catch (error) {
|
|
81
|
-
const toolText = formatToolError(envelope.tool, error);
|
|
82
|
-
emitted.push({
|
|
83
|
-
role: "tool",
|
|
84
|
-
text: toolText,
|
|
85
|
-
toolName: envelope.tool,
|
|
86
|
-
title: `${envelope.tool} error`,
|
|
87
|
-
data: { ok: false, error: toolText },
|
|
88
|
-
});
|
|
89
|
-
currentTranscript.push({
|
|
90
|
-
role: "tool",
|
|
91
|
-
text: `${envelope.tool}: ${toolText}`,
|
|
92
|
-
});
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
const toolText = toolResult.text || `${envelope.tool} completed`;
|
|
96
|
-
emitted.push({
|
|
97
|
-
role: "tool",
|
|
98
|
-
text: toolText,
|
|
99
|
-
toolName: envelope.tool,
|
|
100
|
-
title: toolResult.title || envelope.tool,
|
|
101
|
-
data: toolResult.data || null,
|
|
102
|
-
});
|
|
103
|
-
currentTranscript.push({
|
|
104
|
-
role: "tool",
|
|
105
|
-
text: `${envelope.tool}: ${toolText}`,
|
|
106
|
-
});
|
|
107
|
-
onToolEvent?.({ type: "tool-result", tool: envelope.tool, text: toolText });
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
emitted.push({
|
|
64
|
+
observer.scan();
|
|
65
|
+
commandLog?.refresh?.();
|
|
66
|
+
return [{
|
|
112
67
|
role: "assistant",
|
|
113
|
-
text:
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
emitted.push({
|
|
119
|
-
role: "assistant",
|
|
120
|
-
text: "I hit the assistant tool-call limit before reaching a final answer.",
|
|
121
|
-
});
|
|
122
|
-
return emitted;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function formatToolError(tool, error) {
|
|
126
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
127
|
-
if (tool === "shell_exec" && /command string/.test(message)) {
|
|
128
|
-
return "The assistant requested shell_exec without a command. Retry with arguments.command set to the exact shell command.";
|
|
68
|
+
text: result.finalText || "",
|
|
69
|
+
}];
|
|
70
|
+
} finally {
|
|
71
|
+
observer.stop();
|
|
129
72
|
}
|
|
130
|
-
return `Tool failed: ${message}`;
|
|
131
73
|
}
|
|
132
74
|
|
|
133
75
|
function formatProviderEvent(event) {
|
|
@@ -10,7 +10,7 @@ import { createRunState } from "../state/run/state.mjs";
|
|
|
10
10
|
import { buildContextSelection } from "../../results/context.mjs";
|
|
11
11
|
import { isProviderInstalled } from "./providers/index.mjs";
|
|
12
12
|
import { parseSlashCommand, formatSlashHelpLines } from "./slash-commands.mjs";
|
|
13
|
-
import {
|
|
13
|
+
import { executeAssistantAction } from "./actions.mjs";
|
|
14
14
|
import { runAssistantConversationTurn } from "./session.mjs";
|
|
15
15
|
import { prepareAssistantContextPack } from "./context-pack.mjs";
|
|
16
16
|
import {
|
|
@@ -598,6 +598,27 @@ function handleAssistantToolEvent(state, event, appendMessage) {
|
|
|
598
598
|
}
|
|
599
599
|
if (event.type === "run-session-end") {
|
|
600
600
|
state.completeRunSession(event.session);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
if (event.type === "observed-run-artifact") {
|
|
604
|
+
const observedSession = {
|
|
605
|
+
productDir: state.getSnapshot().productDir,
|
|
606
|
+
runState: state.runState,
|
|
607
|
+
getSnapshot() {
|
|
608
|
+
return state.runState.getSnapshot();
|
|
609
|
+
},
|
|
610
|
+
};
|
|
611
|
+
state.attachRunSession(observedSession, { active: false });
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
if (event.type === "observed-testkit-command") {
|
|
615
|
+
appendMessage({
|
|
616
|
+
role: "tool",
|
|
617
|
+
toolName: event.command?.kind || "testkit",
|
|
618
|
+
title: formatObservedCommandTitle(event.command),
|
|
619
|
+
text: formatObservedCommandText(event.command),
|
|
620
|
+
data: event.command || null,
|
|
621
|
+
});
|
|
601
622
|
}
|
|
602
623
|
}
|
|
603
624
|
|
|
@@ -613,34 +634,7 @@ function formatSettings(snapshot) {
|
|
|
613
634
|
}
|
|
614
635
|
|
|
615
636
|
async function executeSlashTool(slash, context) {
|
|
616
|
-
|
|
617
|
-
case "inspect":
|
|
618
|
-
return executeAssistantTool(
|
|
619
|
-
"read_context",
|
|
620
|
-
{ file: slash.file || null, mode: "detail" },
|
|
621
|
-
context
|
|
622
|
-
);
|
|
623
|
-
case "logs":
|
|
624
|
-
return executeAssistantTool("read_context", { service: slash.service || null, mode: "logs" }, context);
|
|
625
|
-
case "artifacts":
|
|
626
|
-
return executeAssistantTool("read_context", { file: slash.file || null, mode: "artifacts" }, context);
|
|
627
|
-
case "setup":
|
|
628
|
-
return executeAssistantTool("read_context", { service: slash.service || null, mode: "setup" }, context);
|
|
629
|
-
case "file":
|
|
630
|
-
return executeAssistantTool("read_file", { path: slash.file }, context);
|
|
631
|
-
case "service":
|
|
632
|
-
return executeAssistantTool("read_context", { service: slash.service, mode: "detail" }, context);
|
|
633
|
-
case "status":
|
|
634
|
-
return executeAssistantTool("show_status", {}, context);
|
|
635
|
-
case "discover":
|
|
636
|
-
return executeAssistantTool("discover_tests", {}, context);
|
|
637
|
-
case "doctor":
|
|
638
|
-
return executeAssistantTool("run_doctor", {}, context);
|
|
639
|
-
case "run":
|
|
640
|
-
return executeAssistantTool("run_tests", slash.options, context);
|
|
641
|
-
default:
|
|
642
|
-
throw new Error(`Unsupported slash command "${slash.type}"`);
|
|
643
|
-
}
|
|
637
|
+
return executeAssistantAction(slash.type, slash, context);
|
|
644
638
|
}
|
|
645
639
|
|
|
646
640
|
function parseSlashCommandSafe(input) {
|
|
@@ -692,3 +686,23 @@ function serializeRunSession(session) {
|
|
|
692
686
|
snapshot: session.getSnapshot(),
|
|
693
687
|
};
|
|
694
688
|
}
|
|
689
|
+
|
|
690
|
+
function formatObservedCommandTitle(command) {
|
|
691
|
+
const kind = command?.kind || "command";
|
|
692
|
+
return `testkit ${kind}`;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function formatObservedCommandText(command) {
|
|
696
|
+
if (!command) return "Observed Testkit command.";
|
|
697
|
+
const lines = [];
|
|
698
|
+
lines.push(`$ testkit ${command.argv?.join(" ") || command.kind || ""}`.trim());
|
|
699
|
+
if (Number.isInteger(command.exitCode)) lines.push(`exit code: ${command.exitCode}`);
|
|
700
|
+
const rows = command.result?.runSession?.summaryData?.rows || command.result?.summaryRows || command.result?.runArtifact?.summaryRows || null;
|
|
701
|
+
if (Array.isArray(rows) && rows.length > 0) {
|
|
702
|
+
for (const [label, value] of rows) lines.push(`${label}: ${value}`);
|
|
703
|
+
} else if (command.kind === "run" && command.result?.runArtifact?.summary?.files) {
|
|
704
|
+
const files = command.result.runArtifact.summary.files;
|
|
705
|
+
lines.push(`Files: ${files.total || 0} total, ${files.passed || 0} passed, ${files.failed || 0} failed, ${files.skipped || 0} skipped`);
|
|
706
|
+
}
|
|
707
|
+
return lines.join("\n");
|
|
708
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Command, Flags } from "@oclif/core";
|
|
2
2
|
import { loadManagedConfigs } from "../../app/configs.mjs";
|
|
3
3
|
import { loadLatestRunArtifact, resolveFileSubject } from "../../results/artifacts.mjs";
|
|
4
|
-
import { sharedFlags } from "
|
|
4
|
+
import { sharedFlags } from "../command-flags.mjs";
|
|
5
5
|
import { createAssistantState } from "../assistant/state.mjs";
|
|
6
6
|
import { runInteractiveAssistant } from "../assistant/interactive.mjs";
|
|
7
7
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from "@oclif/core";
|
|
2
|
-
import { sharedFlags } from "
|
|
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";
|
|
5
5
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command, Flags } from "@oclif/core";
|
|
2
|
-
import { sharedFlags } from "
|
|
2
|
+
import { sharedFlags } from "../../../command-flags.mjs";
|
|
3
3
|
import { executeDatabaseSnapshotCaptureOperation } from "../../../operations/db/snapshot/capture/operation.mjs";
|
|
4
4
|
import { renderDatabaseSnapshotCaptureResult } from "../../../renderers/db-snapshot-capture/text.mjs";
|
|
5
5
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from "@oclif/core";
|
|
2
|
-
import { sharedFlags } from "
|
|
2
|
+
import { sharedFlags } from "../command-flags.mjs";
|
|
3
3
|
import { executeDestroyOperation } from "../operations/destroy/operation.mjs";
|
|
4
4
|
import { renderDestroyResult } from "../renderers/destroy/text.mjs";
|
|
5
5
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Command, Flags } from "@oclif/core";
|
|
2
2
|
import { executeDiscoverOperation } from "../operations/discover/operation.mjs";
|
|
3
3
|
import { renderDiscoverResult } from "../renderers/discover/text.mjs";
|
|
4
|
-
import { sharedFlags } from "
|
|
4
|
+
import { sharedFlags } from "../command-flags.mjs";
|
|
5
|
+
import { withAssistantCommandResult } from "../assistant/command-results.mjs";
|
|
5
6
|
|
|
6
7
|
export default class DiscoverCommand extends Command {
|
|
7
8
|
static summary = "Discover managed tests and report their metadata";
|
|
@@ -44,15 +45,17 @@ export default class DiscoverCommand extends Command {
|
|
|
44
45
|
};
|
|
45
46
|
|
|
46
47
|
async run() {
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
return withAssistantCommandResult("discover", async () => {
|
|
49
|
+
const { flags } = await this.parse(DiscoverCommand);
|
|
50
|
+
const result = await executeDiscoverOperation(flags, process.cwd());
|
|
49
51
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
if (!this.jsonEnabled()) {
|
|
53
|
+
for (const line of renderDiscoverResult(result, { outputMode: flags["output-mode"] })) {
|
|
54
|
+
this.log(line);
|
|
55
|
+
}
|
|
53
56
|
}
|
|
54
|
-
}
|
|
55
57
|
|
|
56
|
-
|
|
58
|
+
return result;
|
|
59
|
+
});
|
|
57
60
|
}
|
|
58
61
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command, Flags } from "@oclif/core";
|
|
2
2
|
import { executeDoctorOperation } from "../operations/doctor/operation.mjs";
|
|
3
3
|
import { renderDoctorResult } from "../renderers/doctor/text.mjs";
|
|
4
|
+
import { withAssistantCommandResult } from "../assistant/command-results.mjs";
|
|
4
5
|
|
|
5
6
|
export default class DoctorCommand extends Command {
|
|
6
7
|
static summary = "Run built-in config, discovery, and hygiene checks";
|
|
@@ -19,21 +20,23 @@ export default class DoctorCommand extends Command {
|
|
|
19
20
|
};
|
|
20
21
|
|
|
21
22
|
async run() {
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
return withAssistantCommandResult("doctor", async () => {
|
|
24
|
+
const { flags } = await this.parse(DoctorCommand);
|
|
25
|
+
const result = await executeDoctorOperation(flags);
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
if (!this.jsonEnabled()) {
|
|
28
|
+
for (const line of renderDoctorResult(result)) {
|
|
29
|
+
this.log(line);
|
|
30
|
+
}
|
|
28
31
|
}
|
|
29
|
-
}
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
if (!result.ok) {
|
|
34
|
+
const error = new Error("testkit doctor failed");
|
|
35
|
+
error.result = result;
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
return result;
|
|
40
|
+
});
|
|
38
41
|
}
|
|
39
42
|
}
|
package/lib/cli/commands/run.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Args, Command } from "@oclif/core";
|
|
2
|
-
import { runFlags } from "
|
|
2
|
+
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
|
+
import { withAssistantCommandResult } from "../assistant/command-results.mjs";
|
|
5
6
|
|
|
6
7
|
export default class RunCommand extends Command {
|
|
7
8
|
static summary = "Run test suites";
|
|
@@ -19,13 +20,15 @@ export default class RunCommand extends Command {
|
|
|
19
20
|
static flags = runFlags;
|
|
20
21
|
|
|
21
22
|
async run() {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
return withAssistantCommandResult("run", async () => {
|
|
24
|
+
const { args, flags } = await this.parse(RunCommand);
|
|
25
|
+
const request = await buildRunRequest(flags, args.type || null, process.cwd(), process.cwd());
|
|
26
|
+
return executeRunRequest(request, {
|
|
27
|
+
outputMode: flags["output-mode"] || "compact",
|
|
28
|
+
json: this.jsonEnabled(),
|
|
29
|
+
debug: flags.debug,
|
|
30
|
+
terminal: resolveTerminalCapabilities(),
|
|
31
|
+
});
|
|
29
32
|
});
|
|
30
33
|
}
|
|
31
34
|
}
|