@elench/testkit 0.1.111 → 0.1.113
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/bundler/index.mjs +95 -1
- package/lib/cli/args.mjs +1 -1
- package/lib/cli/assistant/app.mjs +70 -20
- package/lib/cli/assistant/command-normalize.mjs +22 -0
- package/lib/cli/assistant/command-observer.mjs +49 -4
- package/lib/cli/assistant/command-results.mjs +10 -1
- package/lib/cli/assistant/context-pack.mjs +45 -15
- 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/session.mjs +3 -1
- package/lib/cli/assistant/state.mjs +109 -2
- package/lib/cli/assistant/view-model.mjs +69 -9
- package/lib/cli/commands/run.mjs +1 -1
- package/lib/cli/components/blocks/run-tree.mjs +30 -64
- package/lib/cli/entrypoint.mjs +1 -1
- package/lib/cli/renderers/run/inline-detail.mjs +64 -0
- package/lib/cli/state/run/model.mjs +24 -95
- package/lib/cli/state/run/state.mjs +0 -22
- 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/default-runtime-runner.mjs +3 -1
- package/lib/runner/failure-details.mjs +22 -0
- package/lib/runner/maintenance.mjs +1 -1
- package/lib/runner/provenance.mjs +4 -1
- package/lib/runner/results.mjs +31 -0
- package/lib/runner/status-model.mjs +15 -7
- 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 +5 -5
- package/lib/cli/components/primitives/filter-bar.mjs +0 -12
- package/lib/cli/state/tree/fuzzy-match.mjs +0 -106
- 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
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export type AssistantTurnState = "idle" | "slash_running" | "provider_running" | "cancelling" | "failed";
|
|
2
|
+
export interface AssistantTurn {
|
|
3
|
+
id: string | null;
|
|
4
|
+
state: AssistantTurnState;
|
|
5
|
+
input?: string;
|
|
6
|
+
startedAt?: string;
|
|
7
|
+
finishedAt?: string;
|
|
8
|
+
failedAt?: string;
|
|
9
|
+
error?: AssistantDiagnostic;
|
|
10
|
+
}
|
|
11
|
+
export interface AssistantDiagnostic {
|
|
12
|
+
level?: "info" | "warning" | "error";
|
|
13
|
+
code?: string;
|
|
14
|
+
message: string;
|
|
15
|
+
timestamp?: string;
|
|
16
|
+
}
|
|
17
|
+
export type AssistantActivityKind = "user_message" | "assistant_message" | "system_message" | "provider_command" | "provider_status" | "testkit_command" | "testkit_run";
|
|
18
|
+
export interface AssistantActivity {
|
|
19
|
+
id: string;
|
|
20
|
+
kind: AssistantActivityKind;
|
|
21
|
+
turnId: string | null;
|
|
22
|
+
title?: string | null;
|
|
23
|
+
text?: string;
|
|
24
|
+
status?: "pending" | "running" | "done" | "error" | null;
|
|
25
|
+
command?: string | null;
|
|
26
|
+
commandId?: string | null;
|
|
27
|
+
supersededBy?: string | null;
|
|
28
|
+
data?: unknown;
|
|
29
|
+
}
|
|
30
|
+
export interface AssistantCommandIdentity {
|
|
31
|
+
sessionId: string | null;
|
|
32
|
+
turnId: string | null;
|
|
33
|
+
commandId: string;
|
|
34
|
+
}
|
|
35
|
+
export interface AssistantCommandObservation {
|
|
36
|
+
type: "command_start" | "command_exit" | "command_result" | "run_artifact";
|
|
37
|
+
identity: AssistantCommandIdentity;
|
|
38
|
+
kind?: string;
|
|
39
|
+
argv?: string[];
|
|
40
|
+
cwd?: string;
|
|
41
|
+
exitCode?: number | null;
|
|
42
|
+
signal?: string | null;
|
|
43
|
+
artifactRunId?: string | null;
|
|
44
|
+
}
|
|
45
|
+
export type AssistantProviderEventType = "session-start" | "status" | "assistant-delta" | "assistant-final" | "tool-start" | "tool-update" | "tool-end" | "error" | "session-end";
|
|
46
|
+
export interface AssistantProviderEvent {
|
|
47
|
+
type: AssistantProviderEventType;
|
|
48
|
+
provider?: "codex" | "claude" | string;
|
|
49
|
+
id?: string | null;
|
|
50
|
+
name?: string;
|
|
51
|
+
text?: string;
|
|
52
|
+
status?: string;
|
|
53
|
+
input?: unknown;
|
|
54
|
+
output?: unknown;
|
|
55
|
+
transient?: boolean;
|
|
56
|
+
display?: boolean;
|
|
57
|
+
data?: unknown;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=domain.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain.d.mts","sourceRoot":"","sources":["../../../src/cli/assistant/domain.mts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,eAAe,GAAG,kBAAkB,GAAG,YAAY,GAAG,QAAQ,CAAC;AAEzG,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,KAAK,EAAE,kBAAkB,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,mBAAmB,CAAC;CAC7B;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,qBAAqB,GAC7B,cAAc,GACd,mBAAmB,GACnB,gBAAgB,GAChB,kBAAkB,GAClB,iBAAiB,GACjB,iBAAiB,GACjB,aAAa,CAAC;AAElB,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,qBAAqB,CAAC;IAC5B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;IACzD,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,eAAe,GAAG,cAAc,GAAG,gBAAgB,GAAG,cAAc,CAAC;IAC3E,QAAQ,EAAE,wBAAwB,CAAC;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,MAAM,0BAA0B,GAClC,eAAe,GACf,QAAQ,GACR,iBAAiB,GACjB,iBAAiB,GACjB,YAAY,GACZ,aAAa,GACb,UAAU,GACV,OAAO,GACP,aAAa,CAAC;AAElB,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,0BAA0B,CAAC;IACjC,QAAQ,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC;IACvC,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain.mjs","sourceRoot":"","sources":["../../../src/cli/assistant/domain.mts"],"names":[],"mappings":""}
|
|
@@ -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,7 +40,7 @@ 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;
|
|
42
44
|
const timeoutMs = resolveProviderTimeoutMs(providerEnv);
|
|
43
45
|
const tracePath = shouldTraceProviderEvents(env, providerEnv)
|
|
44
46
|
? commandLog?.providerEventsPath || path.join(commandLog?.contextDir || path.join(productDir, ".testkit", "assistant"), "provider-events.jsonl")
|
|
@@ -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,
|
|
@@ -88,6 +89,7 @@ export function createAssistantState({
|
|
|
88
89
|
let activeStatus = null;
|
|
89
90
|
let startupNoticeEmitted = false;
|
|
90
91
|
let lastTurnError = null;
|
|
92
|
+
let activeTurn = idleTurn();
|
|
91
93
|
let contextUsage = buildContextUsage({
|
|
92
94
|
provider: resolvedProviderName || settings.provider,
|
|
93
95
|
model: settings.model,
|
|
@@ -109,6 +111,7 @@ export function createAssistantState({
|
|
|
109
111
|
function appendMessage(message) {
|
|
110
112
|
const entry = {
|
|
111
113
|
id: `msg-${messages.length + 1}`,
|
|
114
|
+
...(activeTurn?.id && !message.turnId ? { turnId: activeTurn.id } : {}),
|
|
112
115
|
...message,
|
|
113
116
|
};
|
|
114
117
|
messages.push(entry);
|
|
@@ -182,6 +185,7 @@ export function createAssistantState({
|
|
|
182
185
|
commandLog,
|
|
183
186
|
attachRunSession,
|
|
184
187
|
completeRunSession,
|
|
188
|
+
updateMessage,
|
|
185
189
|
|
|
186
190
|
async loadLatestArtifact() {
|
|
187
191
|
try {
|
|
@@ -364,7 +368,8 @@ export function createAssistantState({
|
|
|
364
368
|
startupNoticeEmitted = true;
|
|
365
369
|
appendMessage({ role: "system", text: notice });
|
|
366
370
|
}
|
|
367
|
-
|
|
371
|
+
const turnId = createAssistantTurnId();
|
|
372
|
+
appendMessage({ role: "user", text: trimmed, turnId });
|
|
368
373
|
|
|
369
374
|
const slash = parseSlashCommandSafe(trimmed);
|
|
370
375
|
if (slash?.type === "__error__") {
|
|
@@ -373,6 +378,8 @@ export function createAssistantState({
|
|
|
373
378
|
}
|
|
374
379
|
if (slash) {
|
|
375
380
|
try {
|
|
381
|
+
activeTurn = { id: turnId, state: "slash_running", input: trimmed, startedAt: new Date().toISOString() };
|
|
382
|
+
commandLog.setActiveTurnId?.(turnId);
|
|
376
383
|
setBusy(true, `Running ${slash.type}...`);
|
|
377
384
|
await executeSlashCommand({
|
|
378
385
|
slash,
|
|
@@ -389,6 +396,14 @@ export function createAssistantState({
|
|
|
389
396
|
text: error instanceof Error ? error.message : String(error),
|
|
390
397
|
});
|
|
391
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
|
+
};
|
|
392
407
|
setBusy(false, null);
|
|
393
408
|
}
|
|
394
409
|
refreshContextPack();
|
|
@@ -397,6 +412,8 @@ export function createAssistantState({
|
|
|
397
412
|
}
|
|
398
413
|
|
|
399
414
|
try {
|
|
415
|
+
activeTurn = { id: turnId, state: "provider_running", input: trimmed, startedAt: new Date().toISOString() };
|
|
416
|
+
commandLog.setActiveTurnId?.(turnId);
|
|
400
417
|
setBusy(true, `Thinking with ${settings.provider === "auto" ? "provider" : settings.provider}...`);
|
|
401
418
|
const providerTurn = createProviderTurnState();
|
|
402
419
|
await runAssistantConversationTurn({
|
|
@@ -405,6 +422,7 @@ export function createAssistantState({
|
|
|
405
422
|
transcript: buildConversationTranscript(messages),
|
|
406
423
|
userMessage: trimmed,
|
|
407
424
|
settings,
|
|
425
|
+
turnId,
|
|
408
426
|
env,
|
|
409
427
|
configs,
|
|
410
428
|
commandLog,
|
|
@@ -447,8 +465,26 @@ export function createAssistantState({
|
|
|
447
465
|
role: "system",
|
|
448
466
|
text: lastTurnError.message,
|
|
449
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
|
+
};
|
|
450
476
|
} finally {
|
|
477
|
+
commandLog.setActiveTurnId?.(null);
|
|
451
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
|
+
}
|
|
452
488
|
setBusy(false, null);
|
|
453
489
|
}
|
|
454
490
|
},
|
|
@@ -459,11 +495,13 @@ export function createAssistantState({
|
|
|
459
495
|
},
|
|
460
496
|
|
|
461
497
|
getSnapshot() {
|
|
498
|
+
const snapshotMessages = messages.map(serializeMessageForSnapshot);
|
|
462
499
|
return {
|
|
463
500
|
context: buildContextSelection(runState.getSnapshot()),
|
|
464
501
|
run: runState.getSnapshot(),
|
|
465
502
|
productDir,
|
|
466
|
-
messages:
|
|
503
|
+
messages: snapshotMessages,
|
|
504
|
+
activities: buildSnapshotActivities(snapshotMessages),
|
|
467
505
|
composer: composerState.text,
|
|
468
506
|
composerCursor: composerState.cursor,
|
|
469
507
|
notice,
|
|
@@ -475,6 +513,7 @@ export function createAssistantState({
|
|
|
475
513
|
providerArgs: [...settings.providerArgs],
|
|
476
514
|
cliConfig,
|
|
477
515
|
activeStatus,
|
|
516
|
+
turn: activeTurn,
|
|
478
517
|
lastTurnError,
|
|
479
518
|
diagnostics: [...diagnostics],
|
|
480
519
|
contextUsage,
|
|
@@ -508,6 +547,17 @@ function resolveInitialProvider(provider, env) {
|
|
|
508
547
|
return null;
|
|
509
548
|
}
|
|
510
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
|
+
|
|
511
561
|
async function executeSlashCommand({
|
|
512
562
|
slash,
|
|
513
563
|
state,
|
|
@@ -664,6 +714,7 @@ function handleAssistantToolEvent(state, event, appendMessage) {
|
|
|
664
714
|
return;
|
|
665
715
|
}
|
|
666
716
|
if (event.type === "observed-testkit-command") {
|
|
717
|
+
suppressMatchingProviderCommand(state, event.command);
|
|
667
718
|
appendMessage({
|
|
668
719
|
role: "tool",
|
|
669
720
|
toolName: event.command?.kind || "testkit",
|
|
@@ -679,6 +730,28 @@ function handleAssistantToolEvent(state, event, appendMessage) {
|
|
|
679
730
|
}
|
|
680
731
|
}
|
|
681
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
|
+
|
|
682
755
|
function createProviderTurnState() {
|
|
683
756
|
return {
|
|
684
757
|
assistantMessageId: null,
|
|
@@ -940,6 +1013,40 @@ function serializeMessageForSnapshot(message) {
|
|
|
940
1013
|
};
|
|
941
1014
|
}
|
|
942
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
|
+
|
|
943
1050
|
function buildConversationTranscript(messages) {
|
|
944
1051
|
return (messages || [])
|
|
945
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
|
|
|
@@ -12,8 +12,8 @@ import {
|
|
|
12
12
|
yellow,
|
|
13
13
|
} from "../../terminal/colors.mjs";
|
|
14
14
|
import { renderSummaryBox } from "../primitives/summary-box.mjs";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { getTerminalWidth } from "../../terminal/layout.mjs";
|
|
16
|
+
import { renderFailureDetail, renderPassedDetail } from "../../renderers/run/inline-detail.mjs";
|
|
17
17
|
|
|
18
18
|
const SPINNER_FRAMES = ["|", "/", "-", "\\"];
|
|
19
19
|
|
|
@@ -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(() => {
|
|
@@ -49,38 +50,10 @@ export function RunTreeView({
|
|
|
49
50
|
return;
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
if (snapshot.filter.active) {
|
|
53
|
-
if (key.escape) {
|
|
54
|
-
runState.deactivateFilter();
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
if (key.return) return;
|
|
58
|
-
if (key.downArrow || input === "j") {
|
|
59
|
-
runState.moveCursorDown();
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
if (key.upArrow || input === "k") {
|
|
63
|
-
runState.moveCursorUp();
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
if (key.backspace || key.delete) {
|
|
67
|
-
runState.updateFilterQuery(snapshot.filter.query.slice(0, -1));
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
if (isPrintableInput(input, key)) {
|
|
71
|
-
runState.updateFilterQuery(`${snapshot.filter.query}${input}`);
|
|
72
|
-
}
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
53
|
if (input === "q") {
|
|
77
54
|
(onRequestClose || exit)();
|
|
78
55
|
return;
|
|
79
56
|
}
|
|
80
|
-
if (input === "/") {
|
|
81
|
-
runState.activateFilter();
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
57
|
if (key.downArrow || input === "j") {
|
|
85
58
|
runState.moveCursorDown();
|
|
86
59
|
return;
|
|
@@ -96,6 +69,7 @@ export function RunTreeView({
|
|
|
96
69
|
}, { isActive: interactive });
|
|
97
70
|
|
|
98
71
|
const visibleTreeEntries = useMemo(() => snapshot.visibleEntries || [], [snapshot.visibleEntries]);
|
|
72
|
+
const terminalWidth = getTerminalWidth(stdout, 100);
|
|
99
73
|
const summaryLines = snapshot.finished && snapshot.summaryData
|
|
100
74
|
? renderSummaryBox(snapshot.summaryData.rows, { stdout })
|
|
101
75
|
: [];
|
|
@@ -108,10 +82,8 @@ export function RunTreeView({
|
|
|
108
82
|
createElement(
|
|
109
83
|
Box,
|
|
110
84
|
{ key: "main", marginTop: 1, flexDirection: "column" },
|
|
111
|
-
...visibleTreeEntries.
|
|
85
|
+
...visibleTreeEntries.flatMap(renderTreeLine.bind(null, snapshot, spinnerFrame, terminalWidth))
|
|
112
86
|
),
|
|
113
|
-
snapshot.filter.active ? createElement(Text, { key: "filter-gap" }, "") : null,
|
|
114
|
-
snapshot.filter.active ? createElement(FilterBar, { key: "filter-bar", filter: snapshot.filter }) : null,
|
|
115
87
|
summaryLines.length > 0 ? createElement(Text, { key: "summary-gap" }, "") : null,
|
|
116
88
|
...summaryLines.map((line, index) => createElement(Text, { key: `summary-${index}` }, line)),
|
|
117
89
|
createElement(Text, { key: "footer-gap" }, ""),
|
|
@@ -123,31 +95,36 @@ export function buildHeaderText(snapshot) {
|
|
|
123
95
|
const progressText = snapshot.totalCount > 0 ? `[${snapshot.completedCount}/${snapshot.totalCount}]` : "[0/0]";
|
|
124
96
|
const phaseText = snapshot.phase || (snapshot.finished ? "run complete" : "preparing");
|
|
125
97
|
const sourceText = snapshot.dataSource === "artifact" ? "artifact run" : snapshot.finished ? "live summary" : "live run";
|
|
126
|
-
|
|
127
|
-
return [progressText, phaseText, sourceText, filterText].filter(Boolean).join(" · ");
|
|
98
|
+
return [progressText, phaseText, sourceText].filter(Boolean).join(" · ");
|
|
128
99
|
}
|
|
129
100
|
|
|
130
101
|
export function buildFooterText(snapshot, { interactive = true } = {}) {
|
|
131
102
|
if (!snapshot.finished) return "Run in progress";
|
|
132
|
-
|
|
133
|
-
return "type to filter · ↑/↓ move · Esc clear filter · q quit";
|
|
134
|
-
}
|
|
135
|
-
return interactive ? "↑/↓ move · Enter collapse/expand · / filter · q quit" : "Run complete";
|
|
103
|
+
return interactive ? "↑/↓ move · Enter toggle detail · q quit" : "Run complete";
|
|
136
104
|
}
|
|
137
105
|
|
|
138
|
-
function renderTreeLine(snapshot, spinnerFrame, entry) {
|
|
106
|
+
function renderTreeLine(snapshot, spinnerFrame, terminalWidth, entry) {
|
|
139
107
|
const selected = entry.id === snapshot.selectedEntryId;
|
|
140
108
|
const pointer = selected ? `${bold(">")} ` : " ";
|
|
141
109
|
const indent = " ".repeat(entry.depth);
|
|
142
|
-
const
|
|
143
|
-
const match = entry.match;
|
|
144
|
-
const highlightedLabel = match?.field === "label"
|
|
145
|
-
? applyHighlight(rawLabel, match.positions, bold)
|
|
146
|
-
: rawLabel;
|
|
147
|
-
const renderedLabel = decorateEntryLabel(entry, highlightedLabel, match);
|
|
110
|
+
const renderedLabel = decorateEntryLabel(entry, entry.label);
|
|
148
111
|
const icon = entryIcon(entry, spinnerFrame);
|
|
149
112
|
const line = `${pointer}${indent}${icon ? `${icon} ` : ""}${renderedLabel}${entrySuffix(entry)}`;
|
|
150
|
-
|
|
113
|
+
|
|
114
|
+
const elements = [createElement(Text, { key: entry.id }, line)];
|
|
115
|
+
|
|
116
|
+
if (entry.kind === "file" && !entry.collapsed && snapshot.finished) {
|
|
117
|
+
const detailLines = entry.status === "failed"
|
|
118
|
+
? renderFailureDetail(entry, { width: terminalWidth, regressionCatalog: snapshot.regressionCatalog })
|
|
119
|
+
: entry.status === "passed"
|
|
120
|
+
? renderPassedDetail(entry, { width: terminalWidth })
|
|
121
|
+
: [];
|
|
122
|
+
for (let i = 0; i < detailLines.length; i++) {
|
|
123
|
+
elements.push(createElement(Text, { key: `${entry.id}-detail-${i}` }, detailLines[i]));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return elements;
|
|
151
128
|
}
|
|
152
129
|
|
|
153
130
|
function entryIcon(entry, spinnerFrame) {
|
|
@@ -161,16 +138,11 @@ function entryIcon(entry, spinnerFrame) {
|
|
|
161
138
|
return dim("·");
|
|
162
139
|
}
|
|
163
140
|
|
|
164
|
-
function decorateEntryLabel(entry, label
|
|
165
|
-
|
|
166
|
-
if (entry.kind === "
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (match?.field === "path" && entry.filePath) {
|
|
171
|
-
rendered += ` ${dim(`(${applyHighlight(entry.filePath, match.positions, bold)})`)}`;
|
|
172
|
-
}
|
|
173
|
-
return rendered;
|
|
141
|
+
function decorateEntryLabel(entry, label) {
|
|
142
|
+
if (entry.kind === "service") return colorService(label);
|
|
143
|
+
if (entry.kind === "type") return colorTypeBadge(label.toUpperCase());
|
|
144
|
+
if (entry.kind === "suite") return bold(label);
|
|
145
|
+
return label;
|
|
174
146
|
}
|
|
175
147
|
|
|
176
148
|
function entrySuffix(entry) {
|
|
@@ -186,9 +158,3 @@ function entrySuffix(entry) {
|
|
|
186
158
|
}
|
|
187
159
|
return "";
|
|
188
160
|
}
|
|
189
|
-
|
|
190
|
-
function isPrintableInput(input, key) {
|
|
191
|
-
if (!input) return false;
|
|
192
|
-
if (key.ctrl || key.meta || key.escape || key.return || key.tab) return false;
|
|
193
|
-
return input >= " ";
|
|
194
|
-
}
|
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",
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { buildFailurePresentation } from "../../../runner/formatting.mjs";
|
|
2
|
+
import { renderIndentedBlock } from "../../terminal/layout.mjs";
|
|
3
|
+
import { dim, green, red } from "../../terminal/colors.mjs";
|
|
4
|
+
import figures from "figures";
|
|
5
|
+
|
|
6
|
+
export function renderFailureDetail(entry, { width, regressionCatalog } = {}) {
|
|
7
|
+
const fileSummary = {
|
|
8
|
+
service: entry.serviceName,
|
|
9
|
+
type: normalizeType(entry),
|
|
10
|
+
path: entry.filePath,
|
|
11
|
+
error: entry.error || null,
|
|
12
|
+
failureDetails: Array.isArray(entry.failureDetails) ? entry.failureDetails : [],
|
|
13
|
+
suiteError: null,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const failureView = buildFailurePresentation(fileSummary, regressionCatalog);
|
|
17
|
+
const lines = [];
|
|
18
|
+
const indent = " ".repeat(2 + (entry.depth || 0) * 2 + 2);
|
|
19
|
+
|
|
20
|
+
if (failureView.primary) {
|
|
21
|
+
lines.push(...renderIndentedBlock(failureView.primary, { width, indent }));
|
|
22
|
+
}
|
|
23
|
+
for (const detail of failureView.details) {
|
|
24
|
+
lines.push(...renderIndentedBlock(detail, { width, indent }));
|
|
25
|
+
}
|
|
26
|
+
return lines;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function renderPassedDetail(entry, { width } = {}) {
|
|
30
|
+
const lines = [];
|
|
31
|
+
const indent = " ".repeat(2 + (entry.depth || 0) * 2 + 2);
|
|
32
|
+
|
|
33
|
+
const checks = Array.isArray(entry.checkDetails) ? entry.checkDetails : [];
|
|
34
|
+
if (checks.length > 0) {
|
|
35
|
+
const passed = checks.filter((c) => c.passed).length;
|
|
36
|
+
lines.push(...renderIndentedBlock(dim(`${passed}/${checks.length} checks passed`), { width, indent }));
|
|
37
|
+
const maxDisplay = 8;
|
|
38
|
+
const displayed = checks.slice(0, maxDisplay);
|
|
39
|
+
for (const check of displayed) {
|
|
40
|
+
const icon = check.passed ? green(figures.tick) : red(figures.cross);
|
|
41
|
+
lines.push(...renderIndentedBlock(`${icon} ${dim(check.name)}`, { width, indent: `${indent} ` }));
|
|
42
|
+
}
|
|
43
|
+
if (checks.length > maxDisplay) {
|
|
44
|
+
lines.push(...renderIndentedBlock(dim(`+${checks.length - maxDisplay} more`), { width, indent: `${indent} ` }));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const artifacts = Array.isArray(entry.artifacts) ? entry.artifacts : [];
|
|
49
|
+
for (const artifact of artifacts) {
|
|
50
|
+
if (artifact.kind === "testkit.checks") continue;
|
|
51
|
+
if (artifact.kind === "runtime.output") continue;
|
|
52
|
+
if (artifact.summary) {
|
|
53
|
+
lines.push(...renderIndentedBlock(dim(artifact.summary), { width, indent }));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return lines;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeType(entry) {
|
|
61
|
+
if (entry.framework === "playwright" || entry.type === "ui") return "ui";
|
|
62
|
+
if (entry.type === "integration") return "int";
|
|
63
|
+
return entry.type;
|
|
64
|
+
}
|