@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
|
@@ -13,6 +13,7 @@ export function createHostedSessionRunner({
|
|
|
13
13
|
child,
|
|
14
14
|
onEvent,
|
|
15
15
|
onRawLine,
|
|
16
|
+
timeoutMs = null,
|
|
16
17
|
parsePayload,
|
|
17
18
|
readFinalText,
|
|
18
19
|
shouldIgnoreStatus,
|
|
@@ -22,6 +23,7 @@ export function createHostedSessionRunner({
|
|
|
22
23
|
let assistantText = "";
|
|
23
24
|
let finalText = null;
|
|
24
25
|
let lastErrorMessage = null;
|
|
26
|
+
let timedOut = false;
|
|
25
27
|
|
|
26
28
|
const emit = (event) => {
|
|
27
29
|
if (!event) return;
|
|
@@ -38,6 +40,18 @@ export function createHostedSessionRunner({
|
|
|
38
40
|
};
|
|
39
41
|
|
|
40
42
|
emit(providerEvent("session-start"));
|
|
43
|
+
const timeout = Number.isFinite(Number(timeoutMs)) && Number(timeoutMs) > 0
|
|
44
|
+
? setTimeout(() => {
|
|
45
|
+
timedOut = true;
|
|
46
|
+
lastErrorMessage = `${provider} timed out after ${Math.floor(Number(timeoutMs))}ms`;
|
|
47
|
+
emit(providerError(lastErrorMessage));
|
|
48
|
+
try {
|
|
49
|
+
child.kill("SIGTERM");
|
|
50
|
+
} catch {
|
|
51
|
+
// Ignore timeout cancellation races.
|
|
52
|
+
}
|
|
53
|
+
}, Math.floor(Number(timeoutMs)))
|
|
54
|
+
: null;
|
|
41
55
|
|
|
42
56
|
const stdoutReader = readline.createInterface({ input: child.stdout });
|
|
43
57
|
const stdoutClosed = waitForReaderClose(stdoutReader);
|
|
@@ -62,11 +76,16 @@ export function createHostedSessionRunner({
|
|
|
62
76
|
});
|
|
63
77
|
|
|
64
78
|
const completion = (async () => {
|
|
65
|
-
|
|
66
|
-
|
|
79
|
+
let result;
|
|
80
|
+
try {
|
|
81
|
+
result = await child;
|
|
82
|
+
await Promise.all([stdoutClosed, stderrClosed]);
|
|
83
|
+
} finally {
|
|
84
|
+
if (timeout) clearTimeout(timeout);
|
|
85
|
+
}
|
|
67
86
|
const fileFinalText = readFinalText ? readFinalText(result) : null;
|
|
68
87
|
const resolvedFinalText = fileFinalText || finalText || assistantText.trim() || null;
|
|
69
|
-
if ((result.exitCode ?? 0) !== 0) {
|
|
88
|
+
if (timedOut || (result.exitCode ?? 0) !== 0) {
|
|
70
89
|
const message = lastErrorMessage || result.stderr || `${provider} exited with code ${result.exitCode ?? 1}`;
|
|
71
90
|
emit(providerError(message));
|
|
72
91
|
throw new Error(message);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface AssistantSessionPaths {
|
|
2
|
+
assistantRoot: string;
|
|
3
|
+
sessionsDir: string;
|
|
4
|
+
contextDir: string;
|
|
5
|
+
contextPath: string;
|
|
6
|
+
currentPath: string;
|
|
7
|
+
summaryPath: string;
|
|
8
|
+
selectionPath: string;
|
|
9
|
+
commandsPath: string;
|
|
10
|
+
commandLogPath: string;
|
|
11
|
+
resultDir: string;
|
|
12
|
+
focusedDetailPath: string;
|
|
13
|
+
focusedLogsPath: string;
|
|
14
|
+
focusedArtifactsPath: string;
|
|
15
|
+
focusedSetupPath: string;
|
|
16
|
+
binDir: string;
|
|
17
|
+
wrapperPath: string;
|
|
18
|
+
providerEventsPath: string;
|
|
19
|
+
providerRawPath: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function createAssistantSessionId(now?: number, random?: () => number): string;
|
|
22
|
+
export declare function assistantSessionPaths(productDir: string, sessionId: string): AssistantSessionPaths;
|
|
23
|
+
//# sourceMappingURL=session-paths.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-paths.d.mts","sourceRoot":"","sources":["../../../src/cli/assistant/session-paths.mts"],"names":[],"mappings":"AAEA,MAAM,WAAW,qBAAqB;IACpC,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,wBAAgB,wBAAwB,CAAC,GAAG,GAAE,MAAmB,EAAE,MAAM,GAAE,MAAM,MAAoB,GAAG,MAAM,CAE7G;AAED,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,qBAAqB,CAyBlG"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
export function createAssistantSessionId(now = Date.now(), random = Math.random) {
|
|
3
|
+
return `session-${now}-${random().toString(36).slice(2, 10)}`;
|
|
4
|
+
}
|
|
5
|
+
export function assistantSessionPaths(productDir, sessionId) {
|
|
6
|
+
const assistantRoot = path.join(productDir, ".testkit", "assistant");
|
|
7
|
+
const sessionsDir = path.join(assistantRoot, "sessions");
|
|
8
|
+
const contextDir = path.join(sessionsDir, sessionId);
|
|
9
|
+
const binDir = path.join(contextDir, "bin");
|
|
10
|
+
return {
|
|
11
|
+
assistantRoot,
|
|
12
|
+
sessionsDir,
|
|
13
|
+
contextDir,
|
|
14
|
+
contextPath: path.join(contextDir, "context.md"),
|
|
15
|
+
currentPath: path.join(assistantRoot, "current.json"),
|
|
16
|
+
summaryPath: path.join(contextDir, "latest-run-summary.json"),
|
|
17
|
+
selectionPath: path.join(contextDir, "current-selection.json"),
|
|
18
|
+
commandsPath: path.join(contextDir, "commands.md"),
|
|
19
|
+
commandLogPath: path.join(contextDir, "commands.jsonl"),
|
|
20
|
+
resultDir: path.join(contextDir, "command-results"),
|
|
21
|
+
focusedDetailPath: path.join(contextDir, "focused-detail.txt"),
|
|
22
|
+
focusedLogsPath: path.join(contextDir, "focused-logs.txt"),
|
|
23
|
+
focusedArtifactsPath: path.join(contextDir, "focused-artifacts.txt"),
|
|
24
|
+
focusedSetupPath: path.join(contextDir, "focused-setup.txt"),
|
|
25
|
+
binDir,
|
|
26
|
+
wrapperPath: path.join(binDir, "testkit"),
|
|
27
|
+
providerEventsPath: path.join(contextDir, "provider-events.jsonl"),
|
|
28
|
+
providerRawPath: path.join(contextDir, "provider-raw.jsonl"),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=session-paths.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-paths.mjs","sourceRoot":"","sources":["../../../src/cli/assistant/session-paths.mts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAuB7B,MAAM,UAAU,wBAAwB,CAAC,MAAc,IAAI,CAAC,GAAG,EAAE,EAAE,SAAuB,IAAI,CAAC,MAAM;IACnG,OAAO,WAAW,GAAG,IAAI,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;AAChE,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,UAAkB,EAAE,SAAiB;IACzE,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,UAAU,EAAE,WAAW,CAAC,CAAC;IACrE,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,UAAU,CAAC,CAAC;IACzD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;IACrD,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;IAC5C,OAAO;QACL,aAAa;QACb,WAAW;QACX,UAAU;QACV,WAAW,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,YAAY,CAAC;QAChD,WAAW,EAAE,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,cAAc,CAAC;QACrD,WAAW,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,yBAAyB,CAAC;QAC7D,aAAa,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,wBAAwB,CAAC;QAC9D,YAAY,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,aAAa,CAAC;QAClD,cAAc,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,gBAAgB,CAAC;QACvD,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,iBAAiB,CAAC;QACnD,iBAAiB,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,oBAAoB,CAAC;QAC9D,eAAe,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,kBAAkB,CAAC;QAC1D,oBAAoB,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,uBAAuB,CAAC;QACpE,gBAAgB,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,mBAAmB,CAAC;QAC5D,MAAM;QACN,WAAW,EAAE,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,SAAS,CAAC;QACzC,kBAAkB,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,uBAAuB,CAAC;QAClE,eAAe,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,oBAAoB,CAAC;KAC7D,CAAC;AACJ,CAAC"}
|
|
@@ -11,6 +11,7 @@ export async function runAssistantConversationTurn({
|
|
|
11
11
|
userMessage,
|
|
12
12
|
provider = "auto",
|
|
13
13
|
settings = null,
|
|
14
|
+
turnId = null,
|
|
14
15
|
env = process.env,
|
|
15
16
|
configs,
|
|
16
17
|
commandLog,
|
|
@@ -24,6 +25,7 @@ export async function runAssistantConversationTurn({
|
|
|
24
25
|
productDir,
|
|
25
26
|
runState,
|
|
26
27
|
commandLog,
|
|
28
|
+
turnId,
|
|
27
29
|
onEvent: onToolEvent,
|
|
28
30
|
});
|
|
29
31
|
|
|
@@ -38,12 +40,13 @@ export async function runAssistantConversationTurn({
|
|
|
38
40
|
|
|
39
41
|
const runtimeSettings = settings || { provider };
|
|
40
42
|
const resolvedProvider = resolvePreferredProvider(runtimeSettings.provider || provider, env);
|
|
41
|
-
const providerEnv = commandLog?.providerEnv?.(env) || env;
|
|
43
|
+
const providerEnv = commandLog?.providerEnv?.(env, { turnId }) || env;
|
|
44
|
+
const timeoutMs = resolveProviderTimeoutMs(providerEnv);
|
|
42
45
|
const tracePath = shouldTraceProviderEvents(env, providerEnv)
|
|
43
|
-
? path.join(commandLog?.contextDir || path.join(productDir, ".testkit", "assistant"), "provider-events.jsonl")
|
|
46
|
+
? commandLog?.providerEventsPath || path.join(commandLog?.contextDir || path.join(productDir, ".testkit", "assistant"), "provider-events.jsonl")
|
|
44
47
|
: null;
|
|
45
48
|
const rawTracePath = shouldTraceProviderEvents(env, providerEnv)
|
|
46
|
-
? path.join(commandLog?.contextDir || path.join(productDir, ".testkit", "assistant"), "provider-raw.jsonl")
|
|
49
|
+
? commandLog?.providerRawPath || path.join(commandLog?.contextDir || path.join(productDir, ".testkit", "assistant"), "provider-raw.jsonl")
|
|
47
50
|
: null;
|
|
48
51
|
onResolvedProvider?.(resolvedProvider);
|
|
49
52
|
onPrompt?.({
|
|
@@ -67,6 +70,7 @@ export async function runAssistantConversationTurn({
|
|
|
67
70
|
model: runtimeSettings.model || null,
|
|
68
71
|
effort: runtimeSettings.effort || null,
|
|
69
72
|
providerArgs: runtimeSettings.providerArgs || [],
|
|
73
|
+
timeoutMs,
|
|
70
74
|
cwd: productDir,
|
|
71
75
|
prompt,
|
|
72
76
|
purpose: "assistant",
|
|
@@ -91,6 +95,12 @@ export async function runAssistantConversationTurn({
|
|
|
91
95
|
}
|
|
92
96
|
}
|
|
93
97
|
|
|
98
|
+
function resolveProviderTimeoutMs(env) {
|
|
99
|
+
const explicit = Number(env?.TESTKIT_ASSISTANT_PROVIDER_TIMEOUT_MS);
|
|
100
|
+
if (Number.isFinite(explicit) && explicit > 0) return Math.floor(explicit);
|
|
101
|
+
return 600_000;
|
|
102
|
+
}
|
|
103
|
+
|
|
94
104
|
function formatProviderEvent(event) {
|
|
95
105
|
if (event.type === "tool-start") {
|
|
96
106
|
return `${event.provider}: ${event.name}${event.detail ? ` (${event.detail})` : ""}`;
|
|
@@ -17,6 +17,7 @@ import { parseSlashCommand, formatSlashHelpLines } from "./slash-commands.mjs";
|
|
|
17
17
|
import { executeAssistantAction } from "./actions.mjs";
|
|
18
18
|
import { runAssistantConversationTurn } from "./session.mjs";
|
|
19
19
|
import { prepareAssistantContextPack } from "./context-pack.mjs";
|
|
20
|
+
import { normalizeCommandLine, unwrapShellCommand } from "./command-normalize.mjs";
|
|
20
21
|
import {
|
|
21
22
|
discoverAssistantModels,
|
|
22
23
|
formatModelChoices,
|
|
@@ -41,6 +42,8 @@ import {
|
|
|
41
42
|
} from "./composer.mjs";
|
|
42
43
|
import { buildContextUsage } from "./context-window.mjs";
|
|
43
44
|
|
|
45
|
+
const SNAPSHOT_MESSAGE_TEXT_LIMIT = 20_000;
|
|
46
|
+
|
|
44
47
|
export function createAssistantState({
|
|
45
48
|
productDir,
|
|
46
49
|
provider,
|
|
@@ -62,6 +65,7 @@ export function createAssistantState({
|
|
|
62
65
|
|
|
63
66
|
const listeners = new Set();
|
|
64
67
|
const messages = [];
|
|
68
|
+
const diagnostics = [];
|
|
65
69
|
let composerState = createComposerState();
|
|
66
70
|
let notice = null;
|
|
67
71
|
let busy = false;
|
|
@@ -84,6 +88,8 @@ export function createAssistantState({
|
|
|
84
88
|
if (sanitizedStartup.notice) notice = sanitizedStartup.notice;
|
|
85
89
|
let activeStatus = null;
|
|
86
90
|
let startupNoticeEmitted = false;
|
|
91
|
+
let lastTurnError = null;
|
|
92
|
+
let activeTurn = idleTurn();
|
|
87
93
|
let contextUsage = buildContextUsage({
|
|
88
94
|
provider: resolvedProviderName || settings.provider,
|
|
89
95
|
model: settings.model,
|
|
@@ -105,6 +111,7 @@ export function createAssistantState({
|
|
|
105
111
|
function appendMessage(message) {
|
|
106
112
|
const entry = {
|
|
107
113
|
id: `msg-${messages.length + 1}`,
|
|
114
|
+
...(activeTurn?.id && !message.turnId ? { turnId: activeTurn.id } : {}),
|
|
108
115
|
...message,
|
|
109
116
|
};
|
|
110
117
|
messages.push(entry);
|
|
@@ -130,6 +137,14 @@ export function createAssistantState({
|
|
|
130
137
|
notify();
|
|
131
138
|
}
|
|
132
139
|
|
|
140
|
+
function appendDiagnostic(diagnostic) {
|
|
141
|
+
diagnostics.push({
|
|
142
|
+
timestamp: new Date().toISOString(),
|
|
143
|
+
...diagnostic,
|
|
144
|
+
});
|
|
145
|
+
notify();
|
|
146
|
+
}
|
|
147
|
+
|
|
133
148
|
function refreshContextPack() {
|
|
134
149
|
commandLog.refresh();
|
|
135
150
|
}
|
|
@@ -170,6 +185,7 @@ export function createAssistantState({
|
|
|
170
185
|
commandLog,
|
|
171
186
|
attachRunSession,
|
|
172
187
|
completeRunSession,
|
|
188
|
+
updateMessage,
|
|
173
189
|
|
|
174
190
|
async loadLatestArtifact() {
|
|
175
191
|
try {
|
|
@@ -334,11 +350,26 @@ export function createAssistantState({
|
|
|
334
350
|
async submitInput(input) {
|
|
335
351
|
const trimmed = String(input || "").trim();
|
|
336
352
|
if (!trimmed) return;
|
|
353
|
+
if (busy) {
|
|
354
|
+
const message = "Assistant is already handling a turn.";
|
|
355
|
+
lastTurnError = {
|
|
356
|
+
kind: "concurrency",
|
|
357
|
+
message,
|
|
358
|
+
};
|
|
359
|
+
appendDiagnostic({
|
|
360
|
+
level: "error",
|
|
361
|
+
code: "assistant_turn_already_active",
|
|
362
|
+
message,
|
|
363
|
+
});
|
|
364
|
+
throw new Error(message);
|
|
365
|
+
}
|
|
366
|
+
lastTurnError = null;
|
|
337
367
|
if (notice && !startupNoticeEmitted) {
|
|
338
368
|
startupNoticeEmitted = true;
|
|
339
369
|
appendMessage({ role: "system", text: notice });
|
|
340
370
|
}
|
|
341
|
-
|
|
371
|
+
const turnId = createAssistantTurnId();
|
|
372
|
+
appendMessage({ role: "user", text: trimmed, turnId });
|
|
342
373
|
|
|
343
374
|
const slash = parseSlashCommandSafe(trimmed);
|
|
344
375
|
if (slash?.type === "__error__") {
|
|
@@ -347,6 +378,8 @@ export function createAssistantState({
|
|
|
347
378
|
}
|
|
348
379
|
if (slash) {
|
|
349
380
|
try {
|
|
381
|
+
activeTurn = { id: turnId, state: "slash_running", input: trimmed, startedAt: new Date().toISOString() };
|
|
382
|
+
commandLog.setActiveTurnId?.(turnId);
|
|
350
383
|
setBusy(true, `Running ${slash.type}...`);
|
|
351
384
|
await executeSlashCommand({
|
|
352
385
|
slash,
|
|
@@ -363,6 +396,14 @@ export function createAssistantState({
|
|
|
363
396
|
text: error instanceof Error ? error.message : String(error),
|
|
364
397
|
});
|
|
365
398
|
} finally {
|
|
399
|
+
commandLog.setActiveTurnId?.(null);
|
|
400
|
+
activeTurn = {
|
|
401
|
+
id: turnId,
|
|
402
|
+
state: "idle",
|
|
403
|
+
input: trimmed,
|
|
404
|
+
startedAt: activeTurn?.startedAt || new Date().toISOString(),
|
|
405
|
+
finishedAt: new Date().toISOString(),
|
|
406
|
+
};
|
|
366
407
|
setBusy(false, null);
|
|
367
408
|
}
|
|
368
409
|
refreshContextPack();
|
|
@@ -371,6 +412,8 @@ export function createAssistantState({
|
|
|
371
412
|
}
|
|
372
413
|
|
|
373
414
|
try {
|
|
415
|
+
activeTurn = { id: turnId, state: "provider_running", input: trimmed, startedAt: new Date().toISOString() };
|
|
416
|
+
commandLog.setActiveTurnId?.(turnId);
|
|
374
417
|
setBusy(true, `Thinking with ${settings.provider === "auto" ? "provider" : settings.provider}...`);
|
|
375
418
|
const providerTurn = createProviderTurnState();
|
|
376
419
|
await runAssistantConversationTurn({
|
|
@@ -379,6 +422,7 @@ export function createAssistantState({
|
|
|
379
422
|
transcript: buildConversationTranscript(messages),
|
|
380
423
|
userMessage: trimmed,
|
|
381
424
|
settings,
|
|
425
|
+
turnId,
|
|
382
426
|
env,
|
|
383
427
|
configs,
|
|
384
428
|
commandLog,
|
|
@@ -413,12 +457,34 @@ export function createAssistantState({
|
|
|
413
457
|
},
|
|
414
458
|
});
|
|
415
459
|
} catch (error) {
|
|
460
|
+
lastTurnError = {
|
|
461
|
+
kind: "provider",
|
|
462
|
+
message: error instanceof Error ? error.message : String(error),
|
|
463
|
+
};
|
|
416
464
|
appendMessage({
|
|
417
465
|
role: "system",
|
|
418
|
-
text:
|
|
466
|
+
text: lastTurnError.message,
|
|
419
467
|
});
|
|
468
|
+
activeTurn = {
|
|
469
|
+
id: turnId,
|
|
470
|
+
state: "failed",
|
|
471
|
+
input: trimmed,
|
|
472
|
+
startedAt: activeTurn?.startedAt || new Date().toISOString(),
|
|
473
|
+
failedAt: new Date().toISOString(),
|
|
474
|
+
error: lastTurnError,
|
|
475
|
+
};
|
|
420
476
|
} finally {
|
|
477
|
+
commandLog.setActiveTurnId?.(null);
|
|
421
478
|
refreshContextPack();
|
|
479
|
+
if (activeTurn?.id === turnId && activeTurn.state !== "failed") {
|
|
480
|
+
activeTurn = {
|
|
481
|
+
id: turnId,
|
|
482
|
+
state: "idle",
|
|
483
|
+
input: trimmed,
|
|
484
|
+
startedAt: activeTurn?.startedAt || new Date().toISOString(),
|
|
485
|
+
finishedAt: new Date().toISOString(),
|
|
486
|
+
};
|
|
487
|
+
}
|
|
422
488
|
setBusy(false, null);
|
|
423
489
|
}
|
|
424
490
|
},
|
|
@@ -429,11 +495,13 @@ export function createAssistantState({
|
|
|
429
495
|
},
|
|
430
496
|
|
|
431
497
|
getSnapshot() {
|
|
498
|
+
const snapshotMessages = messages.map(serializeMessageForSnapshot);
|
|
432
499
|
return {
|
|
433
500
|
context: buildContextSelection(runState.getSnapshot()),
|
|
434
501
|
run: runState.getSnapshot(),
|
|
435
502
|
productDir,
|
|
436
|
-
messages:
|
|
503
|
+
messages: snapshotMessages,
|
|
504
|
+
activities: buildSnapshotActivities(snapshotMessages),
|
|
437
505
|
composer: composerState.text,
|
|
438
506
|
composerCursor: composerState.cursor,
|
|
439
507
|
notice,
|
|
@@ -445,15 +513,24 @@ export function createAssistantState({
|
|
|
445
513
|
providerArgs: [...settings.providerArgs],
|
|
446
514
|
cliConfig,
|
|
447
515
|
activeStatus,
|
|
516
|
+
turn: activeTurn,
|
|
517
|
+
lastTurnError,
|
|
518
|
+
diagnostics: [...diagnostics],
|
|
448
519
|
contextUsage,
|
|
449
520
|
liveRunSession: serializeRunSession(liveRunSession),
|
|
450
521
|
lastRunSession: serializeRunSession(lastRunSession),
|
|
451
522
|
contextPaths: {
|
|
523
|
+
sessionId: commandLog.sessionId,
|
|
524
|
+
currentPath: commandLog.currentPath,
|
|
525
|
+
contextDir: commandLog.contextDir,
|
|
452
526
|
contextPath: commandLog.contextPath,
|
|
453
527
|
summaryPath: commandLog.summaryPath,
|
|
454
528
|
selectionPath: commandLog.selectionPath,
|
|
455
529
|
commandsPath: commandLog.commandsPath,
|
|
456
530
|
commandLogPath: commandLog.commandLogPath,
|
|
531
|
+
resultDir: commandLog.resultDir,
|
|
532
|
+
providerEventsPath: commandLog.providerEventsPath,
|
|
533
|
+
providerRawPath: commandLog.providerRawPath,
|
|
457
534
|
},
|
|
458
535
|
};
|
|
459
536
|
},
|
|
@@ -470,6 +547,17 @@ function resolveInitialProvider(provider, env) {
|
|
|
470
547
|
return null;
|
|
471
548
|
}
|
|
472
549
|
|
|
550
|
+
function idleTurn() {
|
|
551
|
+
return {
|
|
552
|
+
id: null,
|
|
553
|
+
state: "idle",
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function createAssistantTurnId(now = Date.now(), random = Math.random) {
|
|
558
|
+
return `turn-${now}-${random().toString(36).slice(2, 10)}`;
|
|
559
|
+
}
|
|
560
|
+
|
|
473
561
|
async function executeSlashCommand({
|
|
474
562
|
slash,
|
|
475
563
|
state,
|
|
@@ -626,6 +714,7 @@ function handleAssistantToolEvent(state, event, appendMessage) {
|
|
|
626
714
|
return;
|
|
627
715
|
}
|
|
628
716
|
if (event.type === "observed-testkit-command") {
|
|
717
|
+
suppressMatchingProviderCommand(state, event.command);
|
|
629
718
|
appendMessage({
|
|
630
719
|
role: "tool",
|
|
631
720
|
toolName: event.command?.kind || "testkit",
|
|
@@ -641,6 +730,28 @@ function handleAssistantToolEvent(state, event, appendMessage) {
|
|
|
641
730
|
}
|
|
642
731
|
}
|
|
643
732
|
|
|
733
|
+
function suppressMatchingProviderCommand(state, command) {
|
|
734
|
+
const observed = normalizeCommandLine(formatObservedCommandLine(command));
|
|
735
|
+
if (!observed) return;
|
|
736
|
+
const snapshot = state.getSnapshot();
|
|
737
|
+
for (const message of snapshot.messages || []) {
|
|
738
|
+
if (message.role !== "provider-tool") continue;
|
|
739
|
+
const providerCommand = normalizeCommandLine(providerToolCommandLine(message.data));
|
|
740
|
+
if (!providerCommand || providerCommand !== observed) continue;
|
|
741
|
+
state.updateMessage?.(message.id, (current) => ({
|
|
742
|
+
data: {
|
|
743
|
+
...(current.data || {}),
|
|
744
|
+
supersededByTestkitCommand: command?.commandId || true,
|
|
745
|
+
},
|
|
746
|
+
}));
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function providerToolCommandLine(event) {
|
|
751
|
+
if (!event) return null;
|
|
752
|
+
return event.input || event.data?.command || event.data?.input || null;
|
|
753
|
+
}
|
|
754
|
+
|
|
644
755
|
function createProviderTurnState() {
|
|
645
756
|
return {
|
|
646
757
|
assistantMessageId: null,
|
|
@@ -891,6 +1002,51 @@ function serializeRunSession(session) {
|
|
|
891
1002
|
};
|
|
892
1003
|
}
|
|
893
1004
|
|
|
1005
|
+
function serializeMessageForSnapshot(message) {
|
|
1006
|
+
const text = String(message?.text || "");
|
|
1007
|
+
if (text.length <= SNAPSHOT_MESSAGE_TEXT_LIMIT) return { ...message };
|
|
1008
|
+
return {
|
|
1009
|
+
...message,
|
|
1010
|
+
text: `${text.slice(0, SNAPSHOT_MESSAGE_TEXT_LIMIT)}\n... ${text.length - SNAPSHOT_MESSAGE_TEXT_LIMIT} characters omitted from snapshot`,
|
|
1011
|
+
fullTextOmitted: true,
|
|
1012
|
+
fullTextLength: text.length,
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function buildSnapshotActivities(messages) {
|
|
1017
|
+
return (messages || []).map((message) => {
|
|
1018
|
+
const base = {
|
|
1019
|
+
id: message.id,
|
|
1020
|
+
turnId: message.turnId || null,
|
|
1021
|
+
title: message.title || null,
|
|
1022
|
+
text: message.text || "",
|
|
1023
|
+
status: message.status || null,
|
|
1024
|
+
data: message.data || null,
|
|
1025
|
+
};
|
|
1026
|
+
if (message.role === "user") return { ...base, kind: "user_message" };
|
|
1027
|
+
if (message.role === "assistant") return { ...base, kind: "assistant_message" };
|
|
1028
|
+
if (message.role === "provider-tool") {
|
|
1029
|
+
return {
|
|
1030
|
+
...base,
|
|
1031
|
+
kind: "provider_command",
|
|
1032
|
+
command: providerToolCommandLine(message.data),
|
|
1033
|
+
supersededBy: message.data?.supersededByTestkitCommand || null,
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
if (message.role === "provider-activity") return { ...base, kind: "provider_status" };
|
|
1037
|
+
if (message.role === "tool" && message.data?.testkitRelated) {
|
|
1038
|
+
const kind = message.data?.kind === "run" ? "testkit_run" : "testkit_command";
|
|
1039
|
+
return {
|
|
1040
|
+
...base,
|
|
1041
|
+
kind,
|
|
1042
|
+
command: message.data?.command || null,
|
|
1043
|
+
commandId: message.data?.commandId || null,
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
return { ...base, kind: "system_message" };
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
|
|
894
1050
|
function buildConversationTranscript(messages) {
|
|
895
1051
|
return (messages || [])
|
|
896
1052
|
.filter((entry) => !["provider-activity", "provider-tool", "provider-error"].includes(entry.role))
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import { formatContextRemaining } from "./context-window.mjs";
|
|
3
|
+
import { normalizeCommandLine, unwrapShellCommand } from "./command-normalize.mjs";
|
|
3
4
|
|
|
4
5
|
const PROVIDER_COMMAND_OUTPUT_PREVIEW_LINES = 12;
|
|
5
6
|
const PROVIDER_FILE_READ_OUTPUT_PREVIEW_LINES = 8;
|
|
@@ -13,7 +14,7 @@ export function buildAssistantViewModel(snapshot, { cwd = process.cwd(), termina
|
|
|
13
14
|
title: `testkit · ${repoName}`,
|
|
14
15
|
welcome: buildWelcomeModel(snapshot, { cwd, providerLabel }),
|
|
15
16
|
qualitySignal: buildQualitySignal(snapshot),
|
|
16
|
-
blocks: buildTranscriptBlocks(snapshot.messages || []),
|
|
17
|
+
blocks: snapshot.activities ? buildActivityBlocks(snapshot.activities) : buildTranscriptBlocks(snapshot.messages || []),
|
|
17
18
|
composer: {
|
|
18
19
|
text: snapshot.composer || "",
|
|
19
20
|
cursor: snapshot.composerCursor ?? 0,
|
|
@@ -134,7 +135,8 @@ export function buildWelcomeModel(snapshot, { cwd = process.cwd(), providerLabel
|
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
export function buildTranscriptBlocks(messages) {
|
|
137
|
-
|
|
138
|
+
const visibleMessages = filterSupersededProviderCommands(messages || []);
|
|
139
|
+
return visibleMessages.map((message) => {
|
|
138
140
|
const role = message.role || "system";
|
|
139
141
|
if (role === "tool") {
|
|
140
142
|
const outputPreview = summarizeOutput(message.text || "", TESTKIT_COMMAND_OUTPUT_PREVIEW_LINES);
|
|
@@ -204,6 +206,71 @@ export function buildTranscriptBlocks(messages) {
|
|
|
204
206
|
});
|
|
205
207
|
}
|
|
206
208
|
|
|
209
|
+
export function buildActivityBlocks(activities) {
|
|
210
|
+
const messages = (activities || [])
|
|
211
|
+
.filter((activity) => !activity.supersededBy)
|
|
212
|
+
.map((activity) => {
|
|
213
|
+
if (activity.kind === "user_message") return { id: activity.id, role: "user", text: activity.text || "" };
|
|
214
|
+
if (activity.kind === "assistant_message") return { id: activity.id, role: "assistant", text: activity.text || "" };
|
|
215
|
+
if (activity.kind === "provider_command") {
|
|
216
|
+
return {
|
|
217
|
+
id: activity.id,
|
|
218
|
+
role: "provider-tool",
|
|
219
|
+
title: activity.title || "provider command",
|
|
220
|
+
text: activity.text || "",
|
|
221
|
+
status: activity.status || null,
|
|
222
|
+
data: {
|
|
223
|
+
...(activity.data || {}),
|
|
224
|
+
input: activity.command || activity.data?.input || null,
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
if (activity.kind === "provider_status") {
|
|
229
|
+
return {
|
|
230
|
+
id: activity.id,
|
|
231
|
+
role: "provider-activity",
|
|
232
|
+
title: activity.title || null,
|
|
233
|
+
text: activity.text || "",
|
|
234
|
+
status: activity.status || null,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
if (activity.kind === "testkit_command" || activity.kind === "testkit_run") {
|
|
238
|
+
return {
|
|
239
|
+
id: activity.id,
|
|
240
|
+
role: "tool",
|
|
241
|
+
title: activity.title || (activity.kind === "testkit_run" ? "testkit run" : "testkit command"),
|
|
242
|
+
text: activity.text || "",
|
|
243
|
+
status: activity.status || null,
|
|
244
|
+
data: {
|
|
245
|
+
...(activity.data || {}),
|
|
246
|
+
testkitRelated: true,
|
|
247
|
+
kind: activity.kind === "testkit_run" ? "run" : activity.data?.kind,
|
|
248
|
+
command: activity.command || activity.data?.command || null,
|
|
249
|
+
commandId: activity.commandId || activity.data?.commandId || null,
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
return { id: activity.id, role: "system", title: activity.title || null, text: activity.text || "" };
|
|
254
|
+
});
|
|
255
|
+
return buildTranscriptBlocks(messages);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function filterSupersededProviderCommands(messages) {
|
|
259
|
+
const observedCommands = new Set(
|
|
260
|
+
(messages || [])
|
|
261
|
+
.filter((message) => message.role === "tool" && message.data?.testkitRelated)
|
|
262
|
+
.map((message) => normalizeCommandLine(message.data?.command))
|
|
263
|
+
.filter(Boolean)
|
|
264
|
+
);
|
|
265
|
+
return (messages || []).filter((message) => {
|
|
266
|
+
if (message.role !== "provider-tool") return true;
|
|
267
|
+
if (message.data?.supersededByTestkitCommand) return false;
|
|
268
|
+
const providerCommand = normalizeCommandLine(message.data?.input || message.data?.data?.command || message.data?.data?.input);
|
|
269
|
+
if (!providerCommand) return true;
|
|
270
|
+
return !observedCommands.has(providerCommand);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
207
274
|
export function buildStatusLine(snapshot, { cwd = process.cwd(), providerLabel = null } = {}) {
|
|
208
275
|
const context = formatContextRemaining(snapshot.contextUsage);
|
|
209
276
|
const provider = providerLabel || buildProviderLabel(snapshot);
|
|
@@ -339,13 +406,6 @@ function isDiffProducingShellCommand(command) {
|
|
|
339
406
|
return /^((git\s+diff|git\s+show|diff|apply_patch)\b|.*\bapply_patch\b)/.test(normalized);
|
|
340
407
|
}
|
|
341
408
|
|
|
342
|
-
function unwrapShellCommand(command) {
|
|
343
|
-
const text = String(command || "").trim();
|
|
344
|
-
const match = text.match(/(?:^|\s)(?:[^\s'"]*\/)?(?:bash|sh|zsh)\s+-lc\s+(['"])([\s\S]*)\1\s*$/);
|
|
345
|
-
if (!match) return text;
|
|
346
|
-
return match[2].replace(/\\(["'\\$`])/g, "$1").trim();
|
|
347
|
-
}
|
|
348
|
-
|
|
349
409
|
function stringifyMaybe(value) {
|
|
350
410
|
if (value == null) return "";
|
|
351
411
|
if (typeof value === "string") return value;
|
package/lib/cli/commands/run.mjs
CHANGED
|
@@ -14,7 +14,7 @@ export default class RunCommand extends Command {
|
|
|
14
14
|
type: Args.string({
|
|
15
15
|
description: `Optional suite type shortcut: ${publicTestTypeListText({ includeAll: true })}`,
|
|
16
16
|
required: false,
|
|
17
|
-
options: publicTestTypeList({ includeAll: true
|
|
17
|
+
options: publicTestTypeList({ includeAll: true }),
|
|
18
18
|
}),
|
|
19
19
|
};
|
|
20
20
|
|
|
@@ -25,8 +25,9 @@ export function RunTreeView({
|
|
|
25
25
|
interactive = true,
|
|
26
26
|
} = {}) {
|
|
27
27
|
const { exit } = useApp();
|
|
28
|
+
const controlledSnapshot = Boolean(snapshotOverride);
|
|
28
29
|
const [snapshot, setSnapshot] = useState(() => snapshotOverride || runState.getSnapshot());
|
|
29
|
-
const { frame } = useAnimation({ interval: 80, isActive: !snapshot.finished });
|
|
30
|
+
const { frame } = useAnimation({ interval: 80, isActive: !controlledSnapshot && !snapshot.finished });
|
|
30
31
|
const spinnerFrame = SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
|
|
31
32
|
|
|
32
33
|
useEffect(() => {
|
package/lib/cli/entrypoint.mjs
CHANGED
|
@@ -16,7 +16,7 @@ export function normalizeCliArgs(argv) {
|
|
|
16
16
|
"browser",
|
|
17
17
|
"db",
|
|
18
18
|
]);
|
|
19
|
-
const runTypeShortcuts = new Set(publicTestTypeList({ includeAll: true
|
|
19
|
+
const runTypeShortcuts = new Set(publicTestTypeList({ includeAll: true }));
|
|
20
20
|
const valueFlags = new Set([
|
|
21
21
|
"--dir",
|
|
22
22
|
"--service",
|
package/lib/config/discovery.mjs
CHANGED
|
@@ -16,7 +16,6 @@ const DISCOVERY_RULES = [
|
|
|
16
16
|
{ suffix: ".dal.testkit.ts", type: "dal", framework: "k6" },
|
|
17
17
|
{ suffix: ".load.testkit.ts", type: "load", framework: "k6" },
|
|
18
18
|
{ suffix: ".ui.testkit.ts", type: "ui", framework: "playwright" },
|
|
19
|
-
{ suffix: ".pw.testkit.ts", type: "ui", framework: "playwright", legacySuffix: true },
|
|
20
19
|
];
|
|
21
20
|
|
|
22
21
|
export function discoverProject(productDir, explicitServices = {}, options = {}) {
|
|
@@ -31,15 +30,6 @@ export function discoverProject(productDir, explicitServices = {}, options = {})
|
|
|
31
30
|
for (const filePath of suiteFiles) {
|
|
32
31
|
const rule = inferRule(filePath);
|
|
33
32
|
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
|
-
}
|
|
42
|
-
|
|
43
33
|
const owners = inferOwners(filePath, explicitServices, repoDiscovery);
|
|
44
34
|
if (owners === null) continue;
|
|
45
35
|
if (owners.length === 0) {
|
package/lib/discovery/index.mjs
CHANGED
|
@@ -489,7 +489,7 @@ function normalizePath(filePath) {
|
|
|
489
489
|
export function fileDisplayName(filePath) {
|
|
490
490
|
const base = path.posix
|
|
491
491
|
.basename(filePath)
|
|
492
|
-
.replace(/(\.int|\.e2e|\.scenario|\.dal|\.load|\.ui
|
|
492
|
+
.replace(/(\.int|\.e2e|\.scenario|\.dal|\.load|\.ui)\.testkit\.ts$/, "");
|
|
493
493
|
return formatDisplayName(base);
|
|
494
494
|
}
|
|
495
495
|
|