@skyramp/mcp 0.2.7 → 0.2.8
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/build/index.js +5 -2
- package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +24 -5
- package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.d.ts +2 -0
- package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +114 -0
- package/build/prompts/sut-setup/modes/dockerComposePrompt.d.ts +2 -0
- package/build/prompts/sut-setup/modes/dockerComposePrompt.js +292 -0
- package/build/prompts/sut-setup/shared.d.ts +39 -0
- package/build/prompts/sut-setup/shared.js +132 -0
- package/build/prompts/test-maintenance/driftAnalysisSections.js +9 -3
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +1 -1
- package/build/prompts/test-recommendation/diffExecutionPlan.js +76 -2
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +91 -0
- package/build/prompts/testbot/testbot-prompts.d.ts +16 -1
- package/build/prompts/testbot/testbot-prompts.js +113 -22
- package/build/prompts/testbot/testbot-prompts.test.js +95 -45
- package/build/resources/sutSetupResource.d.ts +2 -0
- package/build/resources/sutSetupResource.js +45 -0
- package/build/resources/testbotResource.js +2 -2
- package/build/services/TestDiscoveryService.d.ts +2 -2
- package/build/services/TestDiscoveryService.js +22 -12
- package/build/services/TestExecutionService.d.ts +10 -1
- package/build/services/TestExecutionService.js +158 -26
- package/build/services/TestExecutionService.test.js +306 -0
- package/build/tools/executeSkyrampTestTool.js +26 -7
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +1 -1
- package/build/tools/submitReportTool.d.ts +10 -0
- package/build/tools/submitReportTool.js +106 -39
- package/build/tools/submitReportTool.test.js +296 -50
- package/build/tools/test-management/actionsTool.js +21 -7
- package/build/tools/test-management/analyzeChangesTool.d.ts +1 -1
- package/build/tools/test-management/analyzeChangesTool.js +106 -41
- package/build/tools/test-management/analyzeChangesTool.test.js +21 -41
- package/build/tools/test-management/analyzeTestHealthTool.js +16 -9
- package/build/tools/workspace/initializeWorkspaceTool.js +39 -22
- package/build/tools/workspace/serviceUpsert.d.ts +12 -0
- package/build/tools/workspace/serviceUpsert.js +23 -0
- package/build/tools/workspace/serviceUpsert.test.d.ts +1 -0
- package/build/tools/workspace/serviceUpsert.test.js +50 -0
- package/build/types/TestAnalysis.d.ts +2 -1
- package/build/types/TestExecution.d.ts +12 -2
- package/build/types/TestExecution.js +11 -1
- package/build/types/TestbotReport.d.ts +68 -0
- package/build/types/TestbotReport.js +1 -0
- package/build/types/index.d.ts +4 -0
- package/build/types/index.js +3 -0
- package/build/utils/AnalysisStateManager.d.ts +61 -1
- package/build/utils/AnalysisStateManager.js +144 -3
- package/build/utils/AnalysisStateManager.test.js +98 -0
- package/build/utils/docker.test.js +1 -1
- package/build/utils/routeParsers.d.ts +0 -10
- package/build/utils/routeParsers.js +31 -38
- package/build/utils/scenarioDrafting.d.ts +1 -1
- package/build/utils/scenarioDrafting.js +23 -1
- package/build/utils/utils.d.ts +7 -1
- package/build/utils/utils.js +10 -0
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/workspace/workspace.d.ts +10 -10
- package/build/workspace/workspace.js +7 -7
- package/build/workspace/workspace.test.js +6 -6
- package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +31 -13
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +88 -15
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +4 -3
- package/node_modules/playwright/node_modules/playwright-core/ThirdPartyNotices.txt +23 -126
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderApp.js +79 -19
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/skyramp/aliasRemapper.js +84 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/skyramp/replayEngine.js +15 -4
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/{codeMirrorModule-CPZKmjFR.js → codeMirrorModule-D0BjbCb7.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-lvTRGFx-.js +193 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/index.html +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-B0JOjboO.js → codeMirrorModule-Cqp3cwEJ.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-1anWeyDf.js → defaultSettingsView-CYf9adZh.js} +79 -79
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.Cc5029a3.js → index.B_7ywgmr.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.Wo5yvvVh.js → uiMode.u4_8VnCV.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderApp.ts +129 -43
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/skyramp/aliasRemapper.ts +119 -0
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/skyramp/replayEngine.ts +33 -10
- package/node_modules/playwright/package.json +1 -1
- package/package.json +10 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/.bin/node-which +0 -52
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/is-docker +0 -5
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/mime +0 -46
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/yaml +0 -11
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-CsgPR2wy.js +0 -193
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.CP3Y8Zsb.js +0 -2
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { stripVTControlCharacters } from "util";
|
|
3
3
|
import { TestExecutionService } from "../services/TestExecutionService.js";
|
|
4
4
|
import { AnalyticsService } from "../services/AnalyticsService.js";
|
|
5
|
+
import { TestExecutionStatus } from "../types/TestExecution.js";
|
|
5
6
|
import { getWorkspaceBaseUrl } from "../utils/workspaceAuth.js";
|
|
6
7
|
import { ProgrammingLanguage, TestType } from "../types/TestTypes.js";
|
|
7
8
|
import { StateManager } from "../utils/AnalysisStateManager.js";
|
|
@@ -66,7 +67,15 @@ For detailed documentation visit: https://www.skyramp.dev/docs/quickstart`,
|
|
|
66
67
|
stateFile: z
|
|
67
68
|
.string()
|
|
68
69
|
.optional()
|
|
69
|
-
.describe("
|
|
70
|
+
.describe("Path to state file from skyramp_analyze_changes. Always pass when available — results are written back so skyramp_submit_report can override before/afterStatus with ground-truth pass/fail."),
|
|
71
|
+
phase: z
|
|
72
|
+
.enum(["before", "after"])
|
|
73
|
+
.optional()
|
|
74
|
+
.describe("Execution phase for maintained tests: 'before' captures pre-edit baseline; 'after' (default) records post-edit result."),
|
|
75
|
+
repository: z
|
|
76
|
+
.string()
|
|
77
|
+
.optional()
|
|
78
|
+
.describe("The owner/repo whose analysis section to write execution results into (e.g. 'letsramp/api-insight'). In a multi-repo run, ALWAYS set it — the primary's owner/repo for a primary-repo test, or a related repo's owner/repo for that repo's section of the run-scoped stateFile. Omit only for a single-repo run."),
|
|
70
79
|
},
|
|
71
80
|
_meta: {
|
|
72
81
|
keywords: ["run test", "execute test"],
|
|
@@ -139,16 +148,23 @@ For detailed documentation visit: https://www.skyramp.dev/docs/quickstart`,
|
|
|
139
148
|
playwrightSaveStoragePath: params.playwrightSaveStoragePath,
|
|
140
149
|
useHostNetwork,
|
|
141
150
|
}, onExecutionProgress);
|
|
142
|
-
// Update stateFile with execution results if provided
|
|
151
|
+
// Update stateFile with execution results if provided. Multi-repo: write
|
|
152
|
+
// into the section for `repository` (primary when omitted/matching the
|
|
153
|
+
// root) of the run-scoped file.
|
|
143
154
|
if (params.stateFile) {
|
|
144
155
|
try {
|
|
145
156
|
const stateManager = StateManager.fromStatePath(params.stateFile);
|
|
146
|
-
const stateData = await stateManager.
|
|
157
|
+
const stateData = await stateManager.readRepoData(params.repository);
|
|
147
158
|
if (stateData && stateData.existingTests) {
|
|
148
159
|
const testIndex = stateData.existingTests.findIndex((t) => t.testFile === params.testFile);
|
|
149
160
|
if (testIndex >= 0) {
|
|
150
|
-
|
|
151
|
-
|
|
161
|
+
if (params.phase === "before") {
|
|
162
|
+
stateData.existingTests[testIndex].executionBefore = result;
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
stateData.existingTests[testIndex].executionAfter = result;
|
|
166
|
+
}
|
|
167
|
+
await stateManager.writeRepoData(stateData, { repo: params.repository });
|
|
152
168
|
logger.info(`Updated stateFile with execution results for ${params.testFile}`);
|
|
153
169
|
}
|
|
154
170
|
else {
|
|
@@ -163,7 +179,7 @@ For detailed documentation visit: https://www.skyramp.dev/docs/quickstart`,
|
|
|
163
179
|
}
|
|
164
180
|
// Progress is already reported by TestExecutionService
|
|
165
181
|
// Only report final status if not already at 100%
|
|
166
|
-
if (
|
|
182
|
+
if (result.status !== TestExecutionStatus.Pass) {
|
|
167
183
|
errorResult = {
|
|
168
184
|
content: [
|
|
169
185
|
{
|
|
@@ -177,11 +193,14 @@ For detailed documentation visit: https://www.skyramp.dev/docs/quickstart`,
|
|
|
177
193
|
return errorResult;
|
|
178
194
|
}
|
|
179
195
|
// Success - progress already reported by TestExecutionService
|
|
196
|
+
const videoInfo = result.videoPath
|
|
197
|
+
? `\n\nVideo recording: ${result.videoPath}`
|
|
198
|
+
: "";
|
|
180
199
|
return {
|
|
181
200
|
content: [
|
|
182
201
|
{
|
|
183
202
|
type: "text",
|
|
184
|
-
text: `Test execution result: ${stripVTControlCharacters(result.output || "")}`,
|
|
203
|
+
text: `Test execution result: ${stripVTControlCharacters(result.output || "")}${videoInfo}`,
|
|
185
204
|
},
|
|
186
205
|
],
|
|
187
206
|
};
|
|
@@ -67,7 +67,7 @@ CRITICAL: For search/filter/list endpoints (e.g., GET /products/search?q=bear&li
|
|
|
67
67
|
responseBody: z
|
|
68
68
|
.string()
|
|
69
69
|
.optional()
|
|
70
|
-
.describe("JSON string of the expected response body"),
|
|
70
|
+
.describe("JSON string of the expected response body. For steps that produce an id used by later steps, include that id field with a realistic UUID generated fresh for this scenario — the backend chains by value-matching any subsequent step path or requestBody that contains the same UUID."),
|
|
71
71
|
statusCode: z
|
|
72
72
|
.number()
|
|
73
73
|
.optional()
|
|
@@ -47,6 +47,7 @@ export declare const newTestSchema: z.ZodEffects<z.ZodObject<{
|
|
|
47
47
|
traceFile: z.ZodOptional<z.ZodString>;
|
|
48
48
|
frontendTrace: z.ZodOptional<z.ZodString>;
|
|
49
49
|
reasoning: z.ZodString;
|
|
50
|
+
repository: z.ZodOptional<z.ZodString>;
|
|
50
51
|
targetElements: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
|
|
51
52
|
role: z.ZodString;
|
|
52
53
|
accessibleName: z.ZodString;
|
|
@@ -90,6 +91,7 @@ export declare const newTestSchema: z.ZodEffects<z.ZodObject<{
|
|
|
90
91
|
testId: string;
|
|
91
92
|
reasoning: string;
|
|
92
93
|
description?: string | undefined;
|
|
94
|
+
repository?: string | undefined;
|
|
93
95
|
traceFile?: string | undefined;
|
|
94
96
|
scenarioFile?: string | undefined;
|
|
95
97
|
frontendTrace?: string | undefined;
|
|
@@ -113,6 +115,7 @@ export declare const newTestSchema: z.ZodEffects<z.ZodObject<{
|
|
|
113
115
|
testId: string;
|
|
114
116
|
reasoning: string;
|
|
115
117
|
description?: string | undefined;
|
|
118
|
+
repository?: string | undefined;
|
|
116
119
|
category?: unknown;
|
|
117
120
|
traceFile?: string | undefined;
|
|
118
121
|
scenarioFile?: string | undefined;
|
|
@@ -138,6 +141,7 @@ export declare const newTestSchema: z.ZodEffects<z.ZodObject<{
|
|
|
138
141
|
testId: string;
|
|
139
142
|
reasoning: string;
|
|
140
143
|
description?: string | undefined;
|
|
144
|
+
repository?: string | undefined;
|
|
141
145
|
traceFile?: string | undefined;
|
|
142
146
|
scenarioFile?: string | undefined;
|
|
143
147
|
frontendTrace?: string | undefined;
|
|
@@ -161,6 +165,7 @@ export declare const newTestSchema: z.ZodEffects<z.ZodObject<{
|
|
|
161
165
|
testId: string;
|
|
162
166
|
reasoning: string;
|
|
163
167
|
description?: string | undefined;
|
|
168
|
+
repository?: string | undefined;
|
|
164
169
|
category?: unknown;
|
|
165
170
|
traceFile?: string | undefined;
|
|
166
171
|
scenarioFile?: string | undefined;
|
|
@@ -213,6 +218,7 @@ export declare const additionalRecommendationSchema: z.ZodEffects<z.ZodObject<{
|
|
|
213
218
|
backendTrace: z.ZodOptional<z.ZodString>;
|
|
214
219
|
frontendTrace: z.ZodOptional<z.ZodString>;
|
|
215
220
|
reasoning: z.ZodString;
|
|
221
|
+
repository: z.ZodOptional<z.ZodString>;
|
|
216
222
|
targetElements: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
|
|
217
223
|
role: z.ZodString;
|
|
218
224
|
accessibleName: z.ZodString;
|
|
@@ -263,6 +269,7 @@ export declare const additionalRecommendationSchema: z.ZodEffects<z.ZodObject<{
|
|
|
263
269
|
}[];
|
|
264
270
|
testId: string;
|
|
265
271
|
reasoning: string;
|
|
272
|
+
repository?: string | undefined;
|
|
266
273
|
scenarioName?: string | undefined;
|
|
267
274
|
frontendTrace?: string | undefined;
|
|
268
275
|
targetElements?: {
|
|
@@ -295,6 +302,7 @@ export declare const additionalRecommendationSchema: z.ZodEffects<z.ZodObject<{
|
|
|
295
302
|
testId: string;
|
|
296
303
|
reasoning: string;
|
|
297
304
|
priority?: unknown;
|
|
305
|
+
repository?: string | undefined;
|
|
298
306
|
scenarioName?: string | undefined;
|
|
299
307
|
category?: unknown;
|
|
300
308
|
frontendTrace?: string | undefined;
|
|
@@ -329,6 +337,7 @@ export declare const additionalRecommendationSchema: z.ZodEffects<z.ZodObject<{
|
|
|
329
337
|
}[];
|
|
330
338
|
testId: string;
|
|
331
339
|
reasoning: string;
|
|
340
|
+
repository?: string | undefined;
|
|
332
341
|
scenarioName?: string | undefined;
|
|
333
342
|
frontendTrace?: string | undefined;
|
|
334
343
|
targetElements?: {
|
|
@@ -361,6 +370,7 @@ export declare const additionalRecommendationSchema: z.ZodEffects<z.ZodObject<{
|
|
|
361
370
|
testId: string;
|
|
362
371
|
reasoning: string;
|
|
363
372
|
priority?: unknown;
|
|
373
|
+
repository?: string | undefined;
|
|
364
374
|
scenarioName?: string | undefined;
|
|
365
375
|
category?: unknown;
|
|
366
376
|
frontendTrace?: string | undefined;
|
|
@@ -6,6 +6,9 @@ import { AnalyticsService } from "../services/AnalyticsService.js";
|
|
|
6
6
|
import { TEST_CATEGORIES, externalCategory } from "../types/TestRecommendation.js";
|
|
7
7
|
import { TestType, HttpMethod } from "../types/TestTypes.js";
|
|
8
8
|
import { DriftAction } from "../types/TestAnalysis.js";
|
|
9
|
+
import { TestExecutionStatus } from "../types/TestExecution.js";
|
|
10
|
+
import { StateManager } from "../utils/AnalysisStateManager.js";
|
|
11
|
+
import { toolError } from "../utils/utils.js";
|
|
9
12
|
// Drift actions that actually modify a test file. VERIFY and IGNORE are
|
|
10
13
|
// no-ops (the test was assessed but left unchanged), so they must not count
|
|
11
14
|
// toward "tests maintained" telemetry.
|
|
@@ -16,11 +19,29 @@ const MAINTENANCE_CHANGE_ACTIONS = new Set([
|
|
|
16
19
|
]);
|
|
17
20
|
const TOOL_NAME = "skyramp_submit_report";
|
|
18
21
|
const DEFAULT_COMMIT_MESSAGE = "Added recommendations by Skyramp Testbot.";
|
|
22
|
+
// Per-repo attribution. In a multi-repo run, every report item carries the
|
|
23
|
+
// owner/repo it pertains to (the primary's owner/repo for primary findings, or a
|
|
24
|
+
// related repo's owner/repo for findings from that repo's diff). Whitespace-only
|
|
25
|
+
// values are normalized to undefined so a blank string is never treated as a
|
|
26
|
+
// distinct repo. Single-repo runs may omit it.
|
|
27
|
+
const repositoryField = z
|
|
28
|
+
.string()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe("The 'owner/repo' this item pertains to (e.g. 'letsramp/api-insight'). In a multi-repo run, ALWAYS set it — the primary's owner/repo for primary-repo findings, or a related repo's owner/repo for findings derived from that repository's diff. May be omitted only in single-repo runs.");
|
|
31
|
+
/** Normalize a `repository` attribution: trim, and map a blank/whitespace-only
|
|
32
|
+
* value to undefined so it is never serialized as a distinct empty repo
|
|
33
|
+
* (downstream consumers treat absence as "the primary repo"). */
|
|
34
|
+
function normalizeRepository(item) {
|
|
35
|
+
const trimmed = item.repository?.trim();
|
|
36
|
+
return trimmed ? { ...item, repository: trimmed } : { ...item, repository: undefined };
|
|
37
|
+
}
|
|
19
38
|
const testResultSchema = z.object({
|
|
20
39
|
testType: z.nativeEnum(TestType).describe("Type of test. Do not include priority or other metadata in this field."),
|
|
21
40
|
endpoint: z.string().describe("HTTP verb and path, e.g. 'GET /api/v1/products'"),
|
|
22
41
|
status: z.enum(["Pass", "Fail", "Skipped"]).describe("Test execution result"),
|
|
23
42
|
details: z.string().describe("One sentence — no embedded newlines, no markdown. e.g. '10.8s, products_contract_test.py' or 'failed: <one-line error summary>, products_contract_test.py'"),
|
|
43
|
+
videoPath: z.string().optional().describe("Path to video recording (UI/E2E tests only)"),
|
|
44
|
+
repository: repositoryField,
|
|
24
45
|
});
|
|
25
46
|
// Structured grounding for UI recommendations. Lifted verbatim from a
|
|
26
47
|
// `browser_blueprint` element captured during this run. Source of truth for
|
|
@@ -61,6 +82,7 @@ export const newTestSchema = z.object({
|
|
|
61
82
|
traceFile: z.string().optional().describe("Path to the backend trace file if used or created"),
|
|
62
83
|
frontendTrace: z.string().optional().describe("Path to the Playwright/UI trace file if used or created"),
|
|
63
84
|
reasoning: z.string().describe("Why this test was created: what production risk it mitigates, what code pattern it targets, or what coverage gap it fills"),
|
|
85
|
+
repository: repositoryField,
|
|
64
86
|
targetElements: z.array(targetElementSchema).min(1).nullable().optional().describe("UI tests only: structured grounding for one or more elements the test targets. Most tests target a single element (array length 1); render-state and multi-step UI tests target several (array length 2+). Each entry must be lifted verbatim from a captured blueprint element. Set to null when blueprint capture failed (also requires '[no-blueprint-data]' marker in BOTH description and reasoning). See Blueprint Citation Invariant in testbot prompt."),
|
|
65
87
|
pageContext: pageContextSchema.optional().describe("UI tests only: page metadata for the test. Lifted from the BlueprintCapture used during grounding."),
|
|
66
88
|
}).superRefine((rec, ctx) => {
|
|
@@ -135,6 +157,7 @@ const descriptionSchema = z.object({
|
|
|
135
157
|
.describe("Issue severity. critical = feature broken/unusable (e.g. page doesn't load, data corruption). " +
|
|
136
158
|
"high = incorrect behavior (e.g. wrong calculation, stale data returned). " +
|
|
137
159
|
"medium = minor functional gap. low = cosmetic or informational."),
|
|
160
|
+
repository: repositoryField,
|
|
138
161
|
});
|
|
139
162
|
const scenarioStepSchema = z.object({
|
|
140
163
|
method: z.nativeEnum(HttpMethod).optional().describe("HTTP method. Required for API steps, omit for UI/E2E actions."),
|
|
@@ -158,6 +181,7 @@ export const additionalRecommendationSchema = z.object({
|
|
|
158
181
|
backendTrace: z.string().optional().describe("Path to backend trace file if available, e.g. 'tests/skyramp-traces.json'. Used by integration and E2E tests."),
|
|
159
182
|
frontendTrace: z.string().optional().describe("Path to Playwright/UI trace file if available, e.g. 'tests/skyramp-playwright.zip'. UI tests need this; E2E tests need both frontend and backend traces."),
|
|
160
183
|
reasoning: z.string().describe("Why this test is recommended: the specific production risk, business rule, or security boundary it would validate"),
|
|
184
|
+
repository: repositoryField,
|
|
161
185
|
targetElements: z.array(targetElementSchema).min(1).nullable().optional().describe("UI tests only: structured grounding for one or more elements the test targets. Most tests target a single element (array length 1); render-state and multi-step UI tests target several (array length 2+). Each entry must be lifted verbatim from a captured blueprint element. Set to null when blueprint capture failed (also requires '[no-blueprint-data]' marker in BOTH description and reasoning). See Blueprint Citation Invariant in testbot prompt."),
|
|
162
186
|
pageContext: pageContextSchema.optional().describe("UI tests only: page metadata for the test. Lifted from the BlueprintCapture used during grounding."),
|
|
163
187
|
}).superRefine((rec, ctx) => {
|
|
@@ -226,13 +250,25 @@ const testMaintenanceSchema = z.object({
|
|
|
226
250
|
fileName: z.string().describe("Test file that was maintained, e.g. 'products_smoke_test.py'"),
|
|
227
251
|
action: z.nativeEnum(DriftAction).describe("The drift action assigned to this test during maintenance triage."),
|
|
228
252
|
description: z.string().describe("What was changed and why"),
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
253
|
+
beforeDetails: z.string().describe("One line only — no embedded newlines, no raw HTTP headers or JSON blobs. " +
|
|
254
|
+
"For passing runs: count and timing, e.g. '4 passed in 15.09s'. " +
|
|
255
|
+
"For failing runs: failure name and one-line root cause, e.g. " +
|
|
256
|
+
"'FAILED test_foo — assert 403 got 200, auth middleware not enforced'. " +
|
|
257
|
+
"Empty string for VERIFY/IGNORE entries where no before-execution was run."),
|
|
258
|
+
afterDetails: z.string().describe("One line only — no embedded newlines, no raw HTTP headers or JSON blobs. " +
|
|
259
|
+
"For passing runs: count and timing, e.g. '5 passed in 10.96s'. " +
|
|
260
|
+
"For failing runs: failure name and one-line root cause, e.g. " +
|
|
261
|
+
"'FAILED test_foo — check_schema fails, order_id=1 has discount from prior PATCH test'. " +
|
|
262
|
+
"Empty string for VERIFY/IGNORE/DELETE entries where no after-execution was run."),
|
|
263
|
+
// Server-populated from stateFile execution records — never supplied by the LLM.
|
|
264
|
+
beforeStatus: z.nativeEnum(TestExecutionStatus),
|
|
265
|
+
afterStatus: z.nativeEnum(TestExecutionStatus),
|
|
266
|
+
});
|
|
267
|
+
// LLM input contract: status fields are server-populated from stateFile.
|
|
268
|
+
const testMaintenanceLLMSchema = testMaintenanceSchema.omit({
|
|
269
|
+
beforeStatus: true,
|
|
270
|
+
afterStatus: true,
|
|
271
|
+
});
|
|
236
272
|
/**
|
|
237
273
|
* Derive per-run analytics counts from a submitted report. These power the
|
|
238
274
|
* alpha-launch dashboards (tests generated/maintained, suite growth, bugs vs
|
|
@@ -256,7 +292,7 @@ function computeReportMetrics(params) {
|
|
|
256
292
|
const recommendations = params.additionalRecommendations ?? [];
|
|
257
293
|
const countBy = (items, pred) => items.filter(pred).length;
|
|
258
294
|
const changedMaintenance = (params.testMaintenance ?? []).filter(isMaintenanceChange);
|
|
259
|
-
const maintenanceRecovered = countBy(changedMaintenance, (m) => m.beforeStatus
|
|
295
|
+
const maintenanceRecovered = countBy(changedMaintenance, (m) => (m.beforeStatus === TestExecutionStatus.Fail || m.beforeStatus === TestExecutionStatus.Error) && m.afterStatus === TestExecutionStatus.Pass);
|
|
260
296
|
return {
|
|
261
297
|
testsGenerated: String(params.newTestsCreated.length),
|
|
262
298
|
testsMaintained: String(changedMaintenance.length),
|
|
@@ -272,6 +308,23 @@ function computeReportMetrics(params) {
|
|
|
272
308
|
issuesLow: String(countBy(params.issuesFound, (i) => i.severity === "low")),
|
|
273
309
|
};
|
|
274
310
|
}
|
|
311
|
+
function deduplicateById(items) {
|
|
312
|
+
const seen = new Set();
|
|
313
|
+
const result = [];
|
|
314
|
+
for (const item of items) {
|
|
315
|
+
let id = item.testId;
|
|
316
|
+
if (seen.has(id)) {
|
|
317
|
+
let suffix = 2;
|
|
318
|
+
while (seen.has(`${id}-${suffix}`))
|
|
319
|
+
suffix++;
|
|
320
|
+
id = `${id}-${suffix}`;
|
|
321
|
+
item.testId = id;
|
|
322
|
+
}
|
|
323
|
+
seen.add(id);
|
|
324
|
+
result.push(item);
|
|
325
|
+
}
|
|
326
|
+
return result;
|
|
327
|
+
}
|
|
275
328
|
export function registerSubmitReportTool(server) {
|
|
276
329
|
server.registerTool(TOOL_NAME, {
|
|
277
330
|
description: "Submit the final testbot report. Call this tool once after completing all test analysis, generation, and execution. " +
|
|
@@ -292,9 +345,9 @@ export function registerSubmitReportTool(server) {
|
|
|
292
345
|
.default([])
|
|
293
346
|
.describe("Recommended tests that were not generated (lower priority). Only include recommendations that add distinct coverage beyond generated tests — do not pad with variants testing the same endpoint and flow."),
|
|
294
347
|
testMaintenance: z
|
|
295
|
-
.array(
|
|
348
|
+
.array(testMaintenanceLLMSchema)
|
|
296
349
|
.optional()
|
|
297
|
-
.describe("One entry per test assessed in the drift analysis step. Omit this field when no existing tests were found."),
|
|
350
|
+
.describe("One entry per test assessed in the drift analysis step, including IGNORE and VERIFY entries — these provide triage transparency even when no edits were needed. Omit this field entirely when no existing tests were found."),
|
|
298
351
|
testResults: z
|
|
299
352
|
.array(testResultSchema)
|
|
300
353
|
.describe("List of ALL test execution results. One entry per test executed."),
|
|
@@ -317,6 +370,12 @@ export function registerSubmitReportTool(server) {
|
|
|
317
370
|
.describe("Succinct commit message (if possible, under 72 chars) summarizing what Testbot did, " +
|
|
318
371
|
"e.g. 'add contract tests for /products endpoint' or 'update smoke tests for order API changes'. " +
|
|
319
372
|
"Used as both the git commit subject and the side PR title — the consumer applies truncation as needed."),
|
|
373
|
+
stateFile: z
|
|
374
|
+
.string()
|
|
375
|
+
.describe("Path to the state file returned by skyramp_analyze_changes. Required — " +
|
|
376
|
+
"contains execution results for maintenance scoring (existingTests), " +
|
|
377
|
+
"discovered endpoints for coverage validation (newEndpoints), and " +
|
|
378
|
+
"UI page context for frontend test grounding (uiContext)."),
|
|
320
379
|
},
|
|
321
380
|
_meta: {
|
|
322
381
|
keywords: ["report", "summary", "testbot", "submit"],
|
|
@@ -324,40 +383,51 @@ export function registerSubmitReportTool(server) {
|
|
|
324
383
|
}, async (params) => {
|
|
325
384
|
const startTime = Date.now();
|
|
326
385
|
let errorResult;
|
|
327
|
-
const deduplicateById = (items) => {
|
|
328
|
-
const seen = new Set();
|
|
329
|
-
const result = [];
|
|
330
|
-
for (const item of items) {
|
|
331
|
-
let id = item.testId;
|
|
332
|
-
if (seen.has(id)) {
|
|
333
|
-
let suffix = 2;
|
|
334
|
-
while (seen.has(`${id}-${suffix}`))
|
|
335
|
-
suffix++;
|
|
336
|
-
id = `${id}-${suffix}`;
|
|
337
|
-
item.testId = id;
|
|
338
|
-
}
|
|
339
|
-
seen.add(id);
|
|
340
|
-
result.push(item);
|
|
341
|
-
}
|
|
342
|
-
return result;
|
|
343
|
-
};
|
|
344
386
|
const dedupedNewTests = deduplicateById([...params.newTestsCreated]);
|
|
345
387
|
const dedupedRecommendations = deduplicateById([...(params.additionalRecommendations ?? [])]);
|
|
388
|
+
let testMaintenance;
|
|
389
|
+
if (params.testMaintenance) {
|
|
390
|
+
try {
|
|
391
|
+
const stateData = await StateManager.fromStatePath(params.stateFile).readData();
|
|
392
|
+
if (!stateData) {
|
|
393
|
+
errorResult = toolError(`stateFile not found or empty: ${params.stateFile}`);
|
|
394
|
+
return errorResult;
|
|
395
|
+
}
|
|
396
|
+
// Status fields are stateFile-authoritative. Details are LLM-supplied when present
|
|
397
|
+
// (single-line, with root-cause context) and fall back to server-side extraction.
|
|
398
|
+
testMaintenance = params.testMaintenance.map((m) => {
|
|
399
|
+
const recorded = stateData.existingTests?.find((t) => t.testFile.endsWith(m.fileName) || path.basename(t.testFile) === path.basename(m.fileName));
|
|
400
|
+
const defaultBeforeStatus = MAINTENANCE_CHANGE_ACTIONS.has(m.action) ? TestExecutionStatus.Unknown : TestExecutionStatus.Skipped;
|
|
401
|
+
const defaultAfterStatus = m.action === DriftAction.Delete ? TestExecutionStatus.Skipped
|
|
402
|
+
: MAINTENANCE_CHANGE_ACTIONS.has(m.action) ? TestExecutionStatus.Unknown : TestExecutionStatus.Skipped;
|
|
403
|
+
const beforeStatus = recorded?.executionBefore?.status ?? defaultBeforeStatus;
|
|
404
|
+
const afterStatus = recorded?.executionAfter?.status ?? defaultAfterStatus;
|
|
405
|
+
logger.info(`${m.fileName}: before=${beforeStatus} after=${afterStatus}`);
|
|
406
|
+
return { ...m, beforeStatus, afterStatus };
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
catch (err) {
|
|
410
|
+
errorResult = toolError(`Failed to load stateFile for maintenance enrichment: ${err.message}`);
|
|
411
|
+
return errorResult;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
346
414
|
// Strip generation-artifact fields from newTestsCreated before writing.
|
|
347
415
|
// scenarioFile, traceFile, frontendTrace are internal paths used during
|
|
348
416
|
// generation — downstream scoring scripts don't expect them and fail if
|
|
349
417
|
// they encounter these string fields while traversing the object.
|
|
350
|
-
|
|
351
|
-
const
|
|
418
|
+
// Also normalize each item's `repository` (blank → undefined).
|
|
419
|
+
const sanitizedNewTests = dedupedNewTests.map(({ scenarioFile: _sf, traceFile: _tf, frontendTrace: _ft, ...rest }) => normalizeRepository(rest));
|
|
420
|
+
const report = {
|
|
352
421
|
businessCaseAnalysis: params.businessCaseAnalysis,
|
|
353
422
|
newTestsCreated: sanitizedNewTests,
|
|
354
|
-
additionalRecommendations: dedupedRecommendations,
|
|
355
|
-
testMaintenance
|
|
356
|
-
testResults: params.testResults,
|
|
357
|
-
issuesFound: params.issuesFound,
|
|
423
|
+
additionalRecommendations: dedupedRecommendations.map(normalizeRepository),
|
|
424
|
+
testMaintenance,
|
|
425
|
+
testResults: params.testResults.map(normalizeRepository),
|
|
426
|
+
issuesFound: params.issuesFound.map(normalizeRepository),
|
|
358
427
|
nextSteps: params.nextSteps ?? [],
|
|
359
428
|
commitMessage: (params.commitMessage ?? "").replace(/[\r\n]+/g, " ").trim() || DEFAULT_COMMIT_MESSAGE,
|
|
360
|
-
}
|
|
429
|
+
};
|
|
430
|
+
const reportJson = JSON.stringify(report, null, 2);
|
|
361
431
|
logger.info("Submitting testbot report", {
|
|
362
432
|
outputFile: params.summaryOutputFile,
|
|
363
433
|
payloadBytes: reportJson.length,
|
|
@@ -384,10 +454,7 @@ export function registerSubmitReportTool(server) {
|
|
|
384
454
|
const elapsed = Date.now() - startTime;
|
|
385
455
|
const errorMessage = `Failed to write report: ${error.message}`;
|
|
386
456
|
logger.error(errorMessage, { error, elapsedMs: elapsed });
|
|
387
|
-
errorResult =
|
|
388
|
-
content: [{ type: "text", text: errorMessage }],
|
|
389
|
-
isError: true,
|
|
390
|
-
};
|
|
457
|
+
errorResult = toolError(errorMessage);
|
|
391
458
|
return errorResult;
|
|
392
459
|
}
|
|
393
460
|
finally {
|
|
@@ -395,7 +462,7 @@ export function registerSubmitReportTool(server) {
|
|
|
395
462
|
summary_output_file: params.summaryOutputFile,
|
|
396
463
|
testResultCount: String(params.testResults.length),
|
|
397
464
|
payloadBytes: String(reportJson.length),
|
|
398
|
-
...computeReportMetrics(params),
|
|
465
|
+
...computeReportMetrics({ ...params, testMaintenance }),
|
|
399
466
|
}).catch(() => { });
|
|
400
467
|
}
|
|
401
468
|
});
|