superghost 0.1.1 → 0.3.0
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/package.json +7 -2
- package/src/agent/agent-runner.ts +23 -10
- package/src/agent/mcp-manager.ts +7 -14
- package/src/agent/model-factory.ts +1 -1
- package/src/agent/types.ts +1 -18
- package/src/cache/cache-manager.ts +52 -5
- package/src/cache/step-recorder.ts +1 -1
- package/src/cache/step-replayer.ts +11 -6
- package/src/cache/types.ts +1 -1
- package/src/cli.ts +282 -103
- package/src/config/loader.ts +6 -14
- package/src/config/types.ts +3 -2
- package/src/infra/preflight.ts +13 -0
- package/src/infra/process-manager.ts +6 -2
- package/src/infra/signals.ts +1 -1
- package/src/output/banner.ts +66 -0
- package/src/output/json-formatter.ts +150 -0
- package/src/output/reporter.ts +49 -20
- package/src/output/tool-name-map.ts +62 -0
- package/src/output/types.ts +27 -1
- package/src/runner/test-executor.ts +36 -33
- package/src/runner/test-runner.ts +7 -15
- package/src/runner/types.ts +1 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { type RunResult } from "../runner/types.ts";
|
|
2
|
+
|
|
3
|
+
/** Metadata about the test run environment and configuration */
|
|
4
|
+
export interface JsonOutputMetadata {
|
|
5
|
+
model: string;
|
|
6
|
+
provider: string;
|
|
7
|
+
configFile: string;
|
|
8
|
+
baseUrl: string | undefined;
|
|
9
|
+
timestamp: string;
|
|
10
|
+
filter?: {
|
|
11
|
+
pattern: string;
|
|
12
|
+
matched: number;
|
|
13
|
+
total: number;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Top-level JSON output structure for all output modes */
|
|
18
|
+
export interface JsonOutput {
|
|
19
|
+
version: string;
|
|
20
|
+
success: boolean;
|
|
21
|
+
exitCode: number;
|
|
22
|
+
dryRun?: boolean;
|
|
23
|
+
error?: string;
|
|
24
|
+
metadata: JsonOutputMetadata;
|
|
25
|
+
summary: {
|
|
26
|
+
passed: number;
|
|
27
|
+
failed: number;
|
|
28
|
+
cached: number;
|
|
29
|
+
skipped: number;
|
|
30
|
+
total?: number;
|
|
31
|
+
totalDurationMs?: number;
|
|
32
|
+
};
|
|
33
|
+
tests: Array<{
|
|
34
|
+
testName: string;
|
|
35
|
+
testCase: string;
|
|
36
|
+
status?: string;
|
|
37
|
+
source: string;
|
|
38
|
+
durationMs?: number;
|
|
39
|
+
selfHealed?: boolean;
|
|
40
|
+
error?: string;
|
|
41
|
+
}>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Format a completed run result as JSON.
|
|
46
|
+
* Only includes selfHealed when true, only includes error when present.
|
|
47
|
+
*/
|
|
48
|
+
export function formatJsonOutput(
|
|
49
|
+
runResult: RunResult,
|
|
50
|
+
metadata: JsonOutputMetadata,
|
|
51
|
+
version: string,
|
|
52
|
+
exitCode: number,
|
|
53
|
+
): string {
|
|
54
|
+
const output: JsonOutput = {
|
|
55
|
+
version,
|
|
56
|
+
success: exitCode === 0,
|
|
57
|
+
exitCode,
|
|
58
|
+
metadata,
|
|
59
|
+
summary: {
|
|
60
|
+
passed: runResult.passed,
|
|
61
|
+
failed: runResult.failed,
|
|
62
|
+
cached: runResult.cached,
|
|
63
|
+
skipped: runResult.skipped,
|
|
64
|
+
totalDurationMs: runResult.totalDurationMs,
|
|
65
|
+
},
|
|
66
|
+
tests: runResult.results.map((r) => {
|
|
67
|
+
const entry: Record<string, unknown> = {
|
|
68
|
+
testName: r.testName,
|
|
69
|
+
testCase: r.testCase,
|
|
70
|
+
status: r.status,
|
|
71
|
+
source: r.source,
|
|
72
|
+
durationMs: r.durationMs,
|
|
73
|
+
};
|
|
74
|
+
if (r.selfHealed === true) {
|
|
75
|
+
entry.selfHealed = true;
|
|
76
|
+
}
|
|
77
|
+
if (r.error !== undefined) {
|
|
78
|
+
entry.error = r.error;
|
|
79
|
+
}
|
|
80
|
+
return entry as JsonOutput["tests"][number];
|
|
81
|
+
}),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return JSON.stringify(output, null, 2);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Format a dry-run test listing as JSON.
|
|
89
|
+
* Produces dryRun: true, exitCode: 0, success: true.
|
|
90
|
+
*/
|
|
91
|
+
export function formatJsonDryRun(
|
|
92
|
+
tests: Array<{ name: string; case: string; source: "cache" | "ai" }>,
|
|
93
|
+
metadata: JsonOutputMetadata,
|
|
94
|
+
version: string,
|
|
95
|
+
): string {
|
|
96
|
+
const cachedCount = tests.filter((t) => t.source === "cache").length;
|
|
97
|
+
|
|
98
|
+
const output: JsonOutput = {
|
|
99
|
+
version,
|
|
100
|
+
success: true,
|
|
101
|
+
exitCode: 0,
|
|
102
|
+
dryRun: true,
|
|
103
|
+
metadata,
|
|
104
|
+
summary: {
|
|
105
|
+
passed: 0,
|
|
106
|
+
failed: 0,
|
|
107
|
+
cached: cachedCount,
|
|
108
|
+
skipped: 0,
|
|
109
|
+
total: tests.length,
|
|
110
|
+
},
|
|
111
|
+
tests: tests.map((t) => ({
|
|
112
|
+
testName: t.name,
|
|
113
|
+
testCase: t.case,
|
|
114
|
+
source: t.source,
|
|
115
|
+
})),
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
return JSON.stringify(output, null, 2);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Format an error condition as JSON.
|
|
123
|
+
* Produces success: false, exitCode: 2, with the error message.
|
|
124
|
+
*/
|
|
125
|
+
export function formatJsonError(errorMessage: string, version: string, metadata: Partial<JsonOutputMetadata>): string {
|
|
126
|
+
const fullMetadata: JsonOutputMetadata = {
|
|
127
|
+
model: metadata.model ?? "",
|
|
128
|
+
provider: metadata.provider ?? "",
|
|
129
|
+
configFile: metadata.configFile ?? "",
|
|
130
|
+
baseUrl: metadata.baseUrl,
|
|
131
|
+
timestamp: metadata.timestamp ?? new Date().toISOString(),
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const output: JsonOutput = {
|
|
135
|
+
version,
|
|
136
|
+
success: false,
|
|
137
|
+
exitCode: 2,
|
|
138
|
+
error: errorMessage,
|
|
139
|
+
metadata: fullMetadata,
|
|
140
|
+
summary: {
|
|
141
|
+
passed: 0,
|
|
142
|
+
failed: 0,
|
|
143
|
+
cached: 0,
|
|
144
|
+
skipped: 0,
|
|
145
|
+
},
|
|
146
|
+
tests: [],
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
return JSON.stringify(output, null, 2);
|
|
150
|
+
}
|
package/src/output/reporter.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import pc from "picocolors";
|
|
2
1
|
import { createSpinner } from "nanospinner";
|
|
3
|
-
import
|
|
4
|
-
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
|
|
4
|
+
import { type RunResult, type TestResult } from "../runner/types.ts";
|
|
5
|
+
import { type Reporter, type StepInfo } from "./types.ts";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Format milliseconds as a human-readable duration string.
|
|
@@ -14,16 +15,29 @@ export function formatDuration(ms: number): string {
|
|
|
14
15
|
return `${(ms / 1000).toFixed(1)}s`;
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
/** Write a line of text to stderr */
|
|
19
|
+
export function writeStderr(text: string): void {
|
|
20
|
+
Bun.write(Bun.stderr, `${text}\n`);
|
|
21
|
+
}
|
|
22
|
+
|
|
17
23
|
/**
|
|
18
24
|
* Console reporter with colored output, spinners, and box summary.
|
|
25
|
+
* All output routes to stderr so stdout is reserved for structured output.
|
|
19
26
|
* Colors auto-disable when stdout is not a TTY (via picocolors).
|
|
20
27
|
* Spinner animation auto-disables in non-TTY (via nanospinner).
|
|
21
28
|
*/
|
|
22
29
|
export class ConsoleReporter implements Reporter {
|
|
23
30
|
private spinner: ReturnType<typeof createSpinner> | null = null;
|
|
31
|
+
private readonly verbose: boolean;
|
|
32
|
+
private currentTestName: string | null = null;
|
|
33
|
+
|
|
34
|
+
constructor(verbose = false) {
|
|
35
|
+
this.verbose = verbose;
|
|
36
|
+
}
|
|
24
37
|
|
|
25
38
|
/** Creates a spinner with the test name and starts it */
|
|
26
39
|
onTestStart(testName: string): void {
|
|
40
|
+
this.currentTestName = testName;
|
|
27
41
|
this.spinner = createSpinner(testName).start();
|
|
28
42
|
}
|
|
29
43
|
|
|
@@ -38,35 +52,50 @@ export class ConsoleReporter implements Reporter {
|
|
|
38
52
|
this.spinner?.error({ text: `${testName} ${duration}` });
|
|
39
53
|
}
|
|
40
54
|
if (selfHealed) {
|
|
41
|
-
|
|
55
|
+
writeStderr(pc.dim(" Cache was stale \u2014 re-executed and updated"));
|
|
42
56
|
}
|
|
43
57
|
this.spinner = null;
|
|
58
|
+
this.currentTestName = null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Handles per-step progress during AI execution */
|
|
62
|
+
onStepProgress(step: StepInfo): void {
|
|
63
|
+
if (this.verbose) {
|
|
64
|
+
writeStderr(pc.dim(` Step ${step.stepNumber}: ${step.description.full}`));
|
|
65
|
+
} else if (this.spinner) {
|
|
66
|
+
let spinnerText = `${this.currentTestName} \u2014 ${step.description.full}`;
|
|
67
|
+
if (spinnerText.length > 60) {
|
|
68
|
+
spinnerText = `${spinnerText.slice(0, 57)}...`;
|
|
69
|
+
}
|
|
70
|
+
this.spinner.update(spinnerText);
|
|
71
|
+
}
|
|
44
72
|
}
|
|
45
73
|
|
|
46
74
|
/** Prints bordered box summary and lists failed tests with error messages */
|
|
47
75
|
onRunComplete(data: RunResult): void {
|
|
48
76
|
const bar = "\u2501".repeat(40);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
77
|
+
writeStderr("");
|
|
78
|
+
writeStderr(` ${bar}`);
|
|
79
|
+
writeStderr(" SuperGhost Results");
|
|
80
|
+
writeStderr(` ${bar}`);
|
|
81
|
+
writeStderr(` Total: ${data.results.length}`);
|
|
82
|
+
writeStderr(` Passed: ${pc.green(String(data.passed))}`);
|
|
83
|
+
writeStderr(` Failed: ${data.failed > 0 ? pc.red(String(data.failed)) : String(data.failed)}`);
|
|
84
|
+
if (data.skipped > 0) {
|
|
85
|
+
writeStderr(` Skipped: ${data.skipped}`);
|
|
86
|
+
}
|
|
87
|
+
writeStderr(` Cached: ${data.cached}`);
|
|
88
|
+
writeStderr(` Time: ${pc.dim(formatDuration(data.totalDurationMs))}`);
|
|
89
|
+
writeStderr(` ${bar}`);
|
|
61
90
|
|
|
62
91
|
if (data.failed > 0) {
|
|
63
|
-
|
|
64
|
-
|
|
92
|
+
writeStderr("");
|
|
93
|
+
writeStderr(pc.red(" Failed tests:"));
|
|
65
94
|
for (const result of data.results) {
|
|
66
95
|
if (result.status === "failed") {
|
|
67
|
-
|
|
96
|
+
writeStderr(` ${pc.red("-")} ${result.testName}`);
|
|
68
97
|
if (result.error) {
|
|
69
|
-
|
|
98
|
+
writeStderr(` ${pc.dim(result.error)}`);
|
|
70
99
|
}
|
|
71
100
|
}
|
|
72
101
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type StepDescription } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
/** Maps raw MCP tool names to human-readable action names */
|
|
4
|
+
const PREFIX_MAP: Record<string, string> = {
|
|
5
|
+
browser_navigate: "Navigate",
|
|
6
|
+
browser_click: "Click",
|
|
7
|
+
browser_type: "Type",
|
|
8
|
+
browser_screenshot: "Screenshot",
|
|
9
|
+
browser_wait_for_text: "Wait for text",
|
|
10
|
+
browser_hover: "Hover",
|
|
11
|
+
browser_select_option: "Select",
|
|
12
|
+
browser_go_back: "Go back",
|
|
13
|
+
browser_go_forward: "Go forward",
|
|
14
|
+
browser_press_key: "Press key",
|
|
15
|
+
browser_drag: "Drag",
|
|
16
|
+
browser_resize: "Resize",
|
|
17
|
+
browser_handle_dialog: "Handle dialog",
|
|
18
|
+
browser_file_upload: "Upload file",
|
|
19
|
+
browser_pdf_save: "Save PDF",
|
|
20
|
+
browser_close: "Close",
|
|
21
|
+
browser_console_messages: "Console messages",
|
|
22
|
+
browser_install: "Install browser",
|
|
23
|
+
browser_tab_list: "List tabs",
|
|
24
|
+
browser_tab_new: "New tab",
|
|
25
|
+
browser_tab_select: "Select tab",
|
|
26
|
+
browser_tab_close: "Close tab",
|
|
27
|
+
browser_network_requests: "Network requests",
|
|
28
|
+
browser_snapshot: "Snapshot",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Maps tool names to the input field used as the key argument */
|
|
32
|
+
const KEY_ARG_MAP: Record<string, string> = {
|
|
33
|
+
browser_navigate: "url",
|
|
34
|
+
browser_click: "element",
|
|
35
|
+
browser_type: "element",
|
|
36
|
+
browser_hover: "element",
|
|
37
|
+
browser_select_option: "element",
|
|
38
|
+
browser_press_key: "key",
|
|
39
|
+
browser_wait_for_text: "text",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Convert a raw tool call into a human-readable description.
|
|
44
|
+
*
|
|
45
|
+
* Known tools (browser_navigate, browser_click, etc.) map to friendly names.
|
|
46
|
+
* Unknown tools fall back to: strip underscores, capitalize first letter.
|
|
47
|
+
* Key arguments are extracted based on tool type (e.g., "url" for navigate).
|
|
48
|
+
*/
|
|
49
|
+
export function describeToolCall(toolName: string, input: Record<string, unknown>): StepDescription {
|
|
50
|
+
// Look up human name, or derive from raw name as fallback
|
|
51
|
+
const action = PREFIX_MAP[toolName] ?? toolName.replace(/_/g, " ").replace(/^\w/, (c) => c.toUpperCase());
|
|
52
|
+
|
|
53
|
+
// Look up which input field is the key argument for this tool
|
|
54
|
+
const keyArgField = KEY_ARG_MAP[toolName];
|
|
55
|
+
const rawKeyArg = keyArgField ? input[keyArgField] : undefined;
|
|
56
|
+
const keyArg =
|
|
57
|
+
rawKeyArg !== undefined && rawKeyArg !== null && String(rawKeyArg) !== "" ? String(rawKeyArg) : undefined;
|
|
58
|
+
|
|
59
|
+
const full = keyArg ? `${action} \u2192 ${keyArg}` : action;
|
|
60
|
+
|
|
61
|
+
return { action, keyArg, full };
|
|
62
|
+
}
|
package/src/output/types.ts
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type RunResult, type TestResult } from "../runner/types.ts";
|
|
2
|
+
|
|
3
|
+
/** Describes a tool call in human-readable form */
|
|
4
|
+
export interface StepDescription {
|
|
5
|
+
/** Human-readable action name, e.g. "Navigate", "Click" */
|
|
6
|
+
action: string;
|
|
7
|
+
/** Key argument value, e.g. "/login", "button.submit" */
|
|
8
|
+
keyArg?: string;
|
|
9
|
+
/** Full description string, e.g. "Navigate \u2192 /login" */
|
|
10
|
+
full: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Information about a single step (tool call) during AI execution */
|
|
14
|
+
export interface StepInfo {
|
|
15
|
+
/** 1-based step counter for the current test */
|
|
16
|
+
stepNumber: number;
|
|
17
|
+
/** Raw tool name, e.g. "browser_navigate" */
|
|
18
|
+
toolName: string;
|
|
19
|
+
/** Tool call input arguments */
|
|
20
|
+
input: Record<string, unknown>;
|
|
21
|
+
/** Human-readable description of the tool call */
|
|
22
|
+
description: StepDescription;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Callback invoked for each tool call during AI execution */
|
|
26
|
+
export type OnStepProgress = (step: StepInfo) => void;
|
|
2
27
|
|
|
3
28
|
/** Interface for output reporting */
|
|
4
29
|
export interface Reporter {
|
|
5
30
|
onTestStart(testName: string): void;
|
|
6
31
|
onTestComplete(result: TestResult): void;
|
|
7
32
|
onRunComplete(data: RunResult): void;
|
|
33
|
+
onStepProgress?(step: StepInfo): void;
|
|
8
34
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type
|
|
3
|
-
import type
|
|
4
|
-
import type
|
|
5
|
-
import type
|
|
1
|
+
import { type AgentExecutionResult } from "../agent/types.ts";
|
|
2
|
+
import { type CacheManager } from "../cache/cache-manager.ts";
|
|
3
|
+
import { type StepReplayer } from "../cache/step-replayer.ts";
|
|
4
|
+
import { type Config } from "../config/types.ts";
|
|
5
|
+
import { type OnStepProgress } from "../output/types.ts";
|
|
6
|
+
import { type TestResult } from "./types.ts";
|
|
6
7
|
|
|
7
8
|
/** Function signature for executing a test via the AI agent */
|
|
8
9
|
type ExecuteAgentFn = (config: {
|
|
@@ -13,6 +14,7 @@ type ExecuteAgentFn = (config: {
|
|
|
13
14
|
recursionLimit: number;
|
|
14
15
|
globalContext?: string;
|
|
15
16
|
testContext?: string;
|
|
17
|
+
onStepProgress?: OnStepProgress;
|
|
16
18
|
}) => Promise<AgentExecutionResult>;
|
|
17
19
|
|
|
18
20
|
/**
|
|
@@ -27,11 +29,12 @@ export class TestExecutor {
|
|
|
27
29
|
private readonly executeAgentFn: ExecuteAgentFn;
|
|
28
30
|
private readonly model: any;
|
|
29
31
|
private readonly tools: Record<string, any>;
|
|
30
|
-
private readonly config: Pick<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
> & { context?: string };
|
|
32
|
+
private readonly config: Pick<Config, "maxAttempts" | "recursionLimit" | "model" | "modelProvider"> & {
|
|
33
|
+
context?: string;
|
|
34
|
+
};
|
|
34
35
|
private readonly globalContext?: string;
|
|
36
|
+
private readonly noCache: boolean;
|
|
37
|
+
private readonly onStepProgress?: OnStepProgress;
|
|
35
38
|
|
|
36
39
|
constructor(opts: {
|
|
37
40
|
cacheManager: CacheManager;
|
|
@@ -39,11 +42,10 @@ export class TestExecutor {
|
|
|
39
42
|
executeAgentFn: ExecuteAgentFn;
|
|
40
43
|
model?: any;
|
|
41
44
|
tools?: Record<string, any>;
|
|
42
|
-
config: Pick<
|
|
43
|
-
Config,
|
|
44
|
-
"maxAttempts" | "recursionLimit" | "model" | "modelProvider"
|
|
45
|
-
> & { context?: string };
|
|
45
|
+
config: Pick<Config, "maxAttempts" | "recursionLimit" | "model" | "modelProvider"> & { context?: string };
|
|
46
46
|
globalContext?: string;
|
|
47
|
+
noCache?: boolean;
|
|
48
|
+
onStepProgress?: OnStepProgress;
|
|
47
49
|
}) {
|
|
48
50
|
this.cacheManager = opts.cacheManager;
|
|
49
51
|
this.replayer = opts.replayer;
|
|
@@ -52,34 +54,34 @@ export class TestExecutor {
|
|
|
52
54
|
this.tools = opts.tools ?? {};
|
|
53
55
|
this.config = opts.config;
|
|
54
56
|
this.globalContext = opts.globalContext;
|
|
57
|
+
this.noCache = opts.noCache ?? false;
|
|
58
|
+
this.onStepProgress = opts.onStepProgress;
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
/** Execute a single test case with cache-first strategy */
|
|
58
|
-
async execute(
|
|
59
|
-
testCase: string,
|
|
60
|
-
baseUrl: string,
|
|
61
|
-
testContext?: string,
|
|
62
|
-
): Promise<TestResult> {
|
|
62
|
+
async execute(testCase: string, baseUrl: string, testContext?: string): Promise<TestResult> {
|
|
63
63
|
const start = Date.now();
|
|
64
64
|
|
|
65
|
-
// Phase 1: Try cache replay
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
65
|
+
// Phase 1: Try cache replay (unless noCache)
|
|
66
|
+
if (!this.noCache) {
|
|
67
|
+
const cached = await this.cacheManager.load(testCase, baseUrl);
|
|
68
|
+
if (cached) {
|
|
69
|
+
const replay = await this.replayer.replay(cached.steps, this.onStepProgress);
|
|
70
|
+
if (replay.success) {
|
|
71
|
+
return {
|
|
72
|
+
testName: testCase,
|
|
73
|
+
testCase,
|
|
74
|
+
status: "passed",
|
|
75
|
+
source: "cache",
|
|
76
|
+
durationMs: Date.now() - start,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
// Cache stale — fall through to AI with self-heal flag
|
|
80
|
+
return this.executeWithAgent(testCase, baseUrl, start, true, testContext);
|
|
77
81
|
}
|
|
78
|
-
// Cache stale — fall through to AI with self-heal flag
|
|
79
|
-
return this.executeWithAgent(testCase, baseUrl, start, true, testContext);
|
|
80
82
|
}
|
|
81
83
|
|
|
82
|
-
// Phase 2: No cache — go directly to AI
|
|
84
|
+
// Phase 2: No cache or noCache — go directly to AI
|
|
83
85
|
return this.executeWithAgent(testCase, baseUrl, start, false, testContext);
|
|
84
86
|
}
|
|
85
87
|
|
|
@@ -102,6 +104,7 @@ export class TestExecutor {
|
|
|
102
104
|
recursionLimit: this.config.recursionLimit,
|
|
103
105
|
globalContext: this.globalContext,
|
|
104
106
|
testContext,
|
|
107
|
+
onStepProgress: this.onStepProgress,
|
|
105
108
|
});
|
|
106
109
|
|
|
107
110
|
if (result.passed) {
|
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type
|
|
3
|
-
import type
|
|
1
|
+
import { type Config } from "../config/types.ts";
|
|
2
|
+
import { type Reporter } from "../output/types.ts";
|
|
3
|
+
import { type RunResult, type TestResult } from "./types.ts";
|
|
4
4
|
|
|
5
5
|
/** Function signature for executing a single test case */
|
|
6
|
-
export type ExecuteFn = (
|
|
7
|
-
testCase: string,
|
|
8
|
-
baseUrl: string,
|
|
9
|
-
testContext?: string,
|
|
10
|
-
) => Promise<TestResult>;
|
|
6
|
+
export type ExecuteFn = (testCase: string, baseUrl: string, testContext?: string) => Promise<TestResult>;
|
|
11
7
|
|
|
12
8
|
/**
|
|
13
9
|
* Orchestrates sequential execution of all test cases.
|
|
@@ -50,17 +46,13 @@ export class TestRunner {
|
|
|
50
46
|
}
|
|
51
47
|
|
|
52
48
|
/** Aggregate individual test results into a run summary */
|
|
53
|
-
function aggregateResults(
|
|
54
|
-
results: TestResult[],
|
|
55
|
-
totalDurationMs: number,
|
|
56
|
-
): RunResult {
|
|
49
|
+
function aggregateResults(results: TestResult[], totalDurationMs: number): RunResult {
|
|
57
50
|
return {
|
|
58
51
|
results,
|
|
59
52
|
totalDurationMs,
|
|
60
53
|
passed: results.filter((r) => r.status === "passed").length,
|
|
61
54
|
failed: results.filter((r) => r.status === "failed").length,
|
|
62
|
-
cached: results.filter(
|
|
63
|
-
|
|
64
|
-
).length,
|
|
55
|
+
cached: results.filter((r) => r.source === "cache" && r.status === "passed").length,
|
|
56
|
+
skipped: 0,
|
|
65
57
|
};
|
|
66
58
|
}
|