@skyramp/mcp 0.4.4-rc.1 → 0.4.4
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 +239 -210
- package/build/playwright/PlaywrightTraceService.d.ts +1 -1
- package/build/playwright/blueprintDigestStore.d.ts +1 -1
- package/build/playwright/registerPlaywrightTools.d.ts +1 -1
- package/build/playwright/traceRecordingPrompt.d.ts +1 -1
- package/build/playwright/traceRecordingPrompt.js +2 -2
- package/build/prompts/local-dev/local-dev-prompts.d.ts +1 -1
- package/build/prompts/local-dev/local-dev-prompts.js +2 -2
- package/build/prompts/startTraceCollectionPrompts.d.ts +1 -1
- package/build/prompts/startTraceCollectionPrompts.js +2 -3
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.d.ts +1 -1
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +2 -2
- package/build/prompts/testbot/testbot-prompts.d.ts +1 -1
- package/build/prompts/testbot/testbot-prompts.js +4 -7
- package/build/recommendation/answers.d.ts +17 -52
- package/build/recommendation/answers.js +29 -99
- package/build/recommendation/pullRequestText.d.ts +0 -2
- package/build/recommendation/pullRequestText.js +0 -16
- package/build/recommendation/registerPlan.d.ts +4 -7
- package/build/recommendation/registerPlan.js +11 -37
- package/build/recommendation/types.d.ts +4 -21
- package/build/recommendation/verifierContracts.d.ts +4 -4
- package/build/recommendation/verifierContracts.js +4 -4
- package/build/recommendation/verifiers/citedPath.d.ts +2 -2
- package/build/recommendation/verifiers/citedPath.js +2 -2
- package/build/recommendation/verifiers/issueTraceability.d.ts +5 -1
- package/build/recommendation/verifiers/issueTraceability.js +35 -4
- package/build/resources/analysisResources.d.ts +1 -1
- package/build/resources/analysisResources.js +1 -1
- package/build/resources/progressResource.d.ts +1 -1
- package/build/resources/sutSetupResource.d.ts +1 -1
- package/build/resources/sutSetupResource.js +1 -1
- package/build/resources/testbotResource.d.ts +1 -1
- package/build/resources/testbotResource.js +1 -1
- package/build/services/AnalyticsService.d.ts +1 -1
- package/build/services/FleetExecutionService.d.ts +130 -0
- package/build/services/FleetExecutionService.js +391 -0
- package/build/services/ModularizationService.d.ts +1 -1
- package/build/services/TestExecutionService.d.ts +7 -0
- package/build/services/TestExecutionService.js +21 -0
- package/build/services/TestGenerationService.d.ts +1 -1
- package/build/tool-phases.js +2 -0
- package/build/tools/auth/loginTool.d.ts +1 -1
- package/build/tools/auth/loginTool.js +2 -2
- package/build/tools/auth/logoutTool.d.ts +1 -1
- package/build/tools/auth/logoutTool.js +2 -2
- package/build/tools/code-refactor/alignAssertionsTool.d.ts +1 -1
- package/build/tools/code-refactor/alignAssertionsTool.js +1 -1
- package/build/tools/code-refactor/assertionSkillTools.d.ts +1 -1
- package/build/tools/code-refactor/assertionSkillTools.js +2 -2
- package/build/tools/code-refactor/codeReuseTool.d.ts +1 -1
- package/build/tools/code-refactor/codeReuseTool.js +2 -2
- package/build/tools/code-refactor/enhanceAssertionsTool.d.ts +1 -1
- package/build/tools/code-refactor/enhanceAssertionsTool.js +1 -1
- package/build/tools/code-refactor/modularizationTool.d.ts +1 -1
- package/build/tools/code-refactor/modularizationTool.js +2 -2
- package/build/tools/enrichTestWithMocksTool.d.ts +1 -1
- package/build/tools/enrichTestWithMocksTool.js +2 -2
- package/build/tools/executeFleetTool.d.ts +28 -0
- package/build/tools/executeFleetTool.js +107 -0
- package/build/tools/executeSkyrampTestTool.d.ts +8 -5
- package/build/tools/executeSkyrampTestTool.js +5 -5
- package/build/tools/fixErrorTool.d.ts +1 -1
- package/build/tools/fixErrorTool.js +1 -1
- package/build/tools/generate-tests/batchMockGenerationTool.d.ts +6 -57
- package/build/tools/generate-tests/batchMockGenerationTool.js +3 -3
- package/build/tools/generate-tests/generateBatchScenarioRestTool.d.ts +9 -45
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +29 -11
- package/build/tools/generate-tests/generateContractRestTool.d.ts +1 -2
- package/build/tools/generate-tests/generateContractRestTool.js +1 -1
- package/build/tools/generate-tests/generateE2ERestTool.d.ts +1 -2
- package/build/tools/generate-tests/generateE2ERestTool.js +1 -1
- package/build/tools/generate-tests/generateFuzzRestTool.d.ts +1 -1
- package/build/tools/generate-tests/generateFuzzRestTool.js +1 -1
- package/build/tools/generate-tests/generateIntegrationRestTool.d.ts +1 -2
- package/build/tools/generate-tests/generateIntegrationRestTool.js +1 -1
- package/build/tools/generate-tests/generateLoadRestTool.d.ts +1 -2
- package/build/tools/generate-tests/generateLoadRestTool.js +2 -1
- package/build/tools/generate-tests/generateMockRestTool.d.ts +1 -2
- package/build/tools/generate-tests/generateMockRestTool.js +3 -3
- package/build/tools/generate-tests/generateSmokeRestTool.d.ts +1 -1
- package/build/tools/generate-tests/generateSmokeRestTool.js +1 -1
- package/build/tools/generate-tests/generateUILoadRestTool.d.ts +64 -0
- package/build/tools/generate-tests/generateUILoadRestTool.js +512 -0
- package/build/tools/generate-tests/generateUIRestTool.d.ts +6 -7
- package/build/tools/generate-tests/generateUIRestTool.js +1 -1
- package/build/tools/generate-tests/loadTestSchema.d.ts +15 -15
- package/build/tools/generate-tests/trace-reuse-guard.d.ts +1 -1
- package/build/tools/generateEnrichedIntegrationTestTool.d.ts +1 -2
- package/build/tools/generateEnrichedIntegrationTestTool.js +1 -1
- package/build/tools/localDevWorkerComposeTool.d.ts +1 -1
- package/build/tools/localDevWorkerComposeTool.js +2 -2
- package/build/tools/one-click/oneClickTool.d.ts +1 -1
- package/build/tools/one-click/oneClickTool.js +1 -1
- package/build/tools/preflightMockCheckTool.d.ts +1 -1
- package/build/tools/preflightMockCheckTool.js +3 -3
- package/build/tools/queryProxyMocksTool.d.ts +1 -1
- package/build/tools/queryProxyMocksTool.js +3 -3
- package/build/tools/submitReportTool.d.ts +111 -414
- package/build/tools/submitReportTool.js +154 -138
- package/build/tools/test-management/actionsTool.d.ts +1 -1
- package/build/tools/test-management/actionsTool.js +7 -7
- package/build/tools/test-management/analyzeChangesTool.d.ts +22 -118
- package/build/tools/test-management/analyzeChangesTool.js +4 -4
- package/build/tools/test-management/analyzeTestHealthTool.d.ts +1 -1
- package/build/tools/test-management/analyzeTestHealthTool.js +4 -4
- package/build/tools/test-management/registerTestPlanTool.d.ts +111 -743
- package/build/tools/test-management/registerTestPlanTool.js +20 -36
- package/build/tools/test-management/resolveScreenTool.d.ts +1 -1
- package/build/tools/test-management/resolveScreenTool.js +6 -2
- package/build/tools/trace/startTraceCollectionTool.d.ts +1 -1
- package/build/tools/trace/startTraceCollectionTool.js +4 -4
- package/build/tools/trace/stopTraceCollectionTool.d.ts +1 -1
- package/build/tools/trace/stopTraceCollectionTool.js +2 -2
- package/build/tools/verifyTestDependenciesTool.d.ts +1 -1
- package/build/tools/verifyTestDependenciesTool.js +1 -1
- package/build/tools/workspace/initScanWorkspaceTool.d.ts +1 -1
- package/build/tools/workspace/initScanWorkspaceTool.js +1 -1
- package/build/tools/workspace/initializeWorkspaceTool.d.ts +1 -1
- package/build/tools/workspace/initializeWorkspaceTool.js +1 -1
- package/build/types/RepositoryAnalysis.d.ts +164 -1170
- package/build/types/RepositoryAnalysis.js +12 -10
- package/build/types/StepMethod.d.ts +24 -1
- package/build/types/StepMethod.js +1 -1
- package/build/types/TestTypes.d.ts +34 -133
- package/build/types/TestTypes.js +3 -3
- package/build/types/TestbotReport.d.ts +37 -0
- package/build/types/TestbotReport.js +22 -0
- package/build/types/index.d.ts +1 -1
- package/build/types/index.js +1 -1
- package/build/utils/docker.d.ts +29 -7
- package/build/utils/docker.js +83 -27
- package/build/utils/initAgent.d.ts +3 -3
- package/build/utils/initAgent.js +58 -26
- package/build/utils/progress.d.ts +3 -4
- package/build/utils/progress.js +4 -4
- package/build/utils/utils.d.ts +1 -1
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/workspace/workspace.d.ts +117 -538
- package/build/workspace/workspace.js +2 -2
- package/package.json +6 -5
- package/plugin/prompts/testbot-task1.md +1 -1
- package/plugin/skills/repair-staged-files/SKILL.md +17 -0
- package/build/recommendation/blockerProbes.d.ts +0 -13
- package/build/recommendation/blockerProbes.js +0 -91
- package/build/recommendation/blockers.d.ts +0 -65
- package/build/recommendation/blockers.js +0 -135
|
@@ -9,13 +9,11 @@ import { ProgrammingLanguage, TESTBOT_SUPPORTED_TEST_TYPES, TestType, } from "..
|
|
|
9
9
|
import { STEP_METHOD_DESCRIPTION, stepMethodSchema, } from "../types/StepMethod.js";
|
|
10
10
|
import { DriftAction } from "../types/TestAnalysis.js";
|
|
11
11
|
import { isResult, TestExecutionStatus } from "../types/TestExecution.js";
|
|
12
|
-
import { IssueFoundCategory } from "../types/TestbotReport.js";
|
|
12
|
+
import { EnvironmentFamily, FailureCauseKind, IssueFoundCategory, } from "../types/TestbotReport.js";
|
|
13
13
|
import { StateManager, runArtifactDir, getTestsRepoDir, resolveOwnRunStatePath, } from "../utils/AnalysisStateManager.js";
|
|
14
14
|
import { toolError, testFileMatches } from "../utils/utils.js";
|
|
15
15
|
import { isTestbotEnabled, isUtilsReuseEnabled, } from "../utils/featureFlags.js";
|
|
16
|
-
import {
|
|
17
|
-
import { blockersIn, probeBlockers } from "../recommendation/blockerProbes.js";
|
|
18
|
-
import { blockerSchema, blockerText, hasBlocker, refutedEvidence, verifyBlocker, } from "../recommendation/blockers.js";
|
|
16
|
+
import { answerFor, unknownAnswerObjections, } from "../recommendation/answers.js";
|
|
19
17
|
import { checkDeliveredMatchesPlan, checkMaintenanceDelivered, } from "../recommendation/verifiers/deliveredMatchesPlan.js";
|
|
20
18
|
import { checkReportedCategoryMatchesPlan, checkRequirementConflictReported, } from "../recommendation/verifiers/reportedCategory.js";
|
|
21
19
|
import { checkDefectsReported, checkIssueTraceability, } from "../recommendation/verifiers/issueTraceability.js";
|
|
@@ -117,17 +115,46 @@ const repositoryField = z
|
|
|
117
115
|
.trim()
|
|
118
116
|
.min(1)
|
|
119
117
|
.describe("The 'owner/repo' this item pertains to (e.g. 'letsramp/api-insight'). Set it on every item — the primary's owner/repo for primary-repo objections, or a related repo's owner/repo for objections derived from that repository's diff.");
|
|
118
|
+
// One home for the family meanings: a `.describe()` on the base enum is replaced
|
|
119
|
+
// by the `.optional().describe()` at each use site, so the meanings go there.
|
|
120
|
+
const ENVIRONMENT_FAMILY_MEANINGS = "auth_token = an API token is missing or expired. " +
|
|
121
|
+
"secret = another secret or credential is missing. " +
|
|
122
|
+
"data_connection = the database or data store is not reachable. " +
|
|
123
|
+
"app_not_started = the app under test did not start or is not reachable. " +
|
|
124
|
+
"login = the sign-in step failed. " +
|
|
125
|
+
"external_dependency = an external service is not configured. " +
|
|
126
|
+
"recording_replay = recording or replay of the trace failed.";
|
|
127
|
+
const environmentFamilyField = z.enum(EnvironmentFamily);
|
|
128
|
+
// SKYR-4555. Why a Fail or Skipped row did not pass; the row-level refinement on
|
|
129
|
+
// testResultSchema decides when it is required and which shapes are consistent.
|
|
130
|
+
const failureCauseSchema = z
|
|
131
|
+
.object({
|
|
132
|
+
kind: z
|
|
133
|
+
.enum(FailureCauseKind)
|
|
134
|
+
.describe("application = the code under test misbehaved, or a test written to prove a defect failed as intended. " +
|
|
135
|
+
"environment = the test could not exercise the behaviour because of the test environment: a missing token or secret, no data connection, the app did not start, sign-in failed, an external service is not configured, or recording or replay failed. Name the gap in `family`. " +
|
|
136
|
+
"test_defect = the generated test itself is wrong, not the app: an ambiguous locator, a strict-mode clash, a bad assertion setup."),
|
|
137
|
+
family: environmentFamilyField
|
|
138
|
+
.optional()
|
|
139
|
+
.describe("The test-environment gap. Required when kind is 'environment'. Not allowed for the other kinds. " +
|
|
140
|
+
ENVIRONMENT_FAMILY_MEANINGS),
|
|
141
|
+
})
|
|
142
|
+
.describe("Why this test did not pass. REQUIRED when status is 'Fail' or 'Skipped'. Not allowed when status is 'Pass'. " +
|
|
143
|
+
"It must agree with reachedBehavior: 'application' needs reachedBehavior true or omitted; 'environment' needs reachedBehavior false or omitted.");
|
|
120
144
|
// videoPath is deliberately absent from this input contract: it is attached server-side
|
|
121
145
|
// from the run's execution records (see attachVideoPath), and zod strips any the model
|
|
122
146
|
// supplies anyway. SKYR-4156 is what happens when the agent owns that field instead.
|
|
123
|
-
const testResultSchema = z
|
|
147
|
+
const testResultSchema = z
|
|
148
|
+
.object({
|
|
124
149
|
testType: z
|
|
125
|
-
.
|
|
150
|
+
.enum(TestType)
|
|
126
151
|
.describe("Type of test. Do not include priority or other metadata in this field."),
|
|
127
152
|
endpoint: z
|
|
128
153
|
.string()
|
|
129
154
|
.describe("For contract and integration tests, the HTTP verb and path, e.g. 'GET /api/v1/products'. For ui and e2e tests, the page path the test opens, e.g. '/notifications'."),
|
|
130
|
-
status: z
|
|
155
|
+
status: z
|
|
156
|
+
.enum(["Pass", "Fail", "Skipped"])
|
|
157
|
+
.describe("Test execution result"),
|
|
131
158
|
details: z
|
|
132
159
|
.string()
|
|
133
160
|
.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'"),
|
|
@@ -147,7 +174,76 @@ const testResultSchema = z.object({
|
|
|
147
174
|
.describe("For a failing test only. `true` when the request that carries the behaviour under test was sent and answered by the code under test, and the assertion that failed is about that behaviour. " +
|
|
148
175
|
"`false` when the failure came earlier: a 404, 401, 403 or 5xx on a route the change adds; an expired token or a sign-in page; a control or setup step that failed; a generator or SDK error. " +
|
|
149
176
|
"If your own `details` text says the request was not sent or an earlier step tripped, this is `false`. Omit only if you cannot tell."),
|
|
177
|
+
failureCause: failureCauseSchema.optional(),
|
|
150
178
|
repository: repositoryField,
|
|
179
|
+
})
|
|
180
|
+
.superRefine((row, ctx) => {
|
|
181
|
+
// SKYR-4555: the business-user report sorts failures by cause, so every row
|
|
182
|
+
// that did not pass must name one and a passing row must not. The kind must
|
|
183
|
+
// agree with reachedBehavior where the agent set it: the two answer the same
|
|
184
|
+
// question — did the test reach the behaviour under test — from two sides.
|
|
185
|
+
const cause = row.failureCause;
|
|
186
|
+
if (row.status === "Pass") {
|
|
187
|
+
if (cause === undefined)
|
|
188
|
+
return;
|
|
189
|
+
ctx.addIssue({
|
|
190
|
+
code: "custom",
|
|
191
|
+
path: ["failureCause"],
|
|
192
|
+
message: `failureCause is not allowed when status is 'Pass'. A passing test has no failure cause. ` +
|
|
193
|
+
`Remove failureCause from this row.`,
|
|
194
|
+
});
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (cause === undefined) {
|
|
198
|
+
ctx.addIssue({
|
|
199
|
+
code: "custom",
|
|
200
|
+
path: ["failureCause"],
|
|
201
|
+
message: `failureCause is required when status is '${row.status}'. Set failureCause.kind to one of: ` +
|
|
202
|
+
`'application' (the code under test misbehaved, or a test written to prove a defect failed as intended); ` +
|
|
203
|
+
`'environment' (the test environment stopped the test before it reached the behaviour — also set failureCause.family); ` +
|
|
204
|
+
`'test_defect' (the generated test itself is wrong, not the app).`,
|
|
205
|
+
});
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (cause.kind === FailureCauseKind.Environment) {
|
|
209
|
+
if (cause.family === undefined) {
|
|
210
|
+
ctx.addIssue({
|
|
211
|
+
code: "custom",
|
|
212
|
+
path: ["failureCause", "family"],
|
|
213
|
+
message: `failureCause.family is required when failureCause.kind is 'environment'. ` +
|
|
214
|
+
`Set it to the gap that stopped the test: ${Object.values(EnvironmentFamily).join(", ")}.`,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else if (cause.family !== undefined) {
|
|
219
|
+
ctx.addIssue({
|
|
220
|
+
code: "custom",
|
|
221
|
+
path: ["failureCause", "family"],
|
|
222
|
+
message: `failureCause.family is allowed only when failureCause.kind is 'environment'. ` +
|
|
223
|
+
`Remove failureCause.family, or set kind to 'environment' if the test environment stopped the test.`,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
if (cause.kind === FailureCauseKind.Application) {
|
|
227
|
+
if (row.reachedBehavior === false) {
|
|
228
|
+
ctx.addIssue({
|
|
229
|
+
code: "custom",
|
|
230
|
+
path: ["failureCause", "kind"],
|
|
231
|
+
message: `failureCause.kind 'application' contradicts reachedBehavior false. 'application' means the test reached the behaviour under test and the code misbehaved. ` +
|
|
232
|
+
`If the failure came earlier, set kind to 'environment' or 'test_defect'. If the test did reach the behaviour, set reachedBehavior to true.`,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
else if (cause.kind === FailureCauseKind.Environment &&
|
|
237
|
+
row.reachedBehavior === true) {
|
|
238
|
+
// test_defect is not checked: a wrong assertion can fail after the test
|
|
239
|
+
// reached the behaviour, or a bad setup step can fail before it.
|
|
240
|
+
ctx.addIssue({
|
|
241
|
+
code: "custom",
|
|
242
|
+
path: ["failureCause", "kind"],
|
|
243
|
+
message: `failureCause.kind 'environment' contradicts reachedBehavior true. reachedBehavior true means the test reached the behaviour under test, so the environment did not stop it. ` +
|
|
244
|
+
`Set kind to 'application' or 'test_defect', or set reachedBehavior to false if the failure came earlier.`,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
151
247
|
});
|
|
152
248
|
// Structured grounding for UI recommendations. Lifted verbatim from a
|
|
153
249
|
// `browser_blueprint` element captured during this run. Source of truth for
|
|
@@ -237,7 +333,7 @@ export const newTestSchema = z.preprocess(stripNullGroundingFields, z
|
|
|
237
333
|
"Required on EVERY entry, UI included. That is deliberately wider than the approved-plan generation gate, which stays contract and integration only: " +
|
|
238
334
|
"this id is the exact key the report is joined back to the plan on, and an entry without one falls back to guessing from the test name and endpoint."),
|
|
239
335
|
testType: z
|
|
240
|
-
.
|
|
336
|
+
.enum(TestType)
|
|
241
337
|
.describe("Type of test created. Do not include priority or other metadata in this field."),
|
|
242
338
|
category: z
|
|
243
339
|
.enum(SCENARIO_CATEGORIES)
|
|
@@ -284,7 +380,7 @@ export const newTestSchema = z.preprocess(stripNullGroundingFields, z
|
|
|
284
380
|
// optional above and required here so the message says what to do.
|
|
285
381
|
if ((rec.plannedTestId ?? "").trim().length === 0) {
|
|
286
382
|
ctx.addIssue({
|
|
287
|
-
code:
|
|
383
|
+
code: "custom",
|
|
288
384
|
path: ["plannedTestId"],
|
|
289
385
|
message: "plannedTestId is required on every newTestsCreated entry. Use the plannedTestId of the plan entry this test was approved under, from skyramp_register_test_plan.",
|
|
290
386
|
});
|
|
@@ -294,7 +390,7 @@ export const newTestSchema = z.preprocess(stripNullGroundingFields, z
|
|
|
294
390
|
for (const field of ["targetElements", "pageContext"]) {
|
|
295
391
|
if (rec[field] !== undefined) {
|
|
296
392
|
ctx.addIssue({
|
|
297
|
-
code:
|
|
393
|
+
code: "custom",
|
|
298
394
|
path: [field],
|
|
299
395
|
message: `${field} is only valid for testType: 'ui'`,
|
|
300
396
|
});
|
|
@@ -305,7 +401,7 @@ export const newTestSchema = z.preprocess(stripNullGroundingFields, z
|
|
|
305
401
|
// description is required for UI tests (walkthrough of steps/assertions).
|
|
306
402
|
if (!rec.description) {
|
|
307
403
|
ctx.addIssue({
|
|
308
|
-
code:
|
|
404
|
+
code: "custom",
|
|
309
405
|
path: ["description"],
|
|
310
406
|
message: "description is required for testType: 'ui'.",
|
|
311
407
|
});
|
|
@@ -313,7 +409,7 @@ export const newTestSchema = z.preprocess(stripNullGroundingFields, z
|
|
|
313
409
|
// targetElements must be present (array or null — not omitted entirely).
|
|
314
410
|
if (rec.targetElements === undefined) {
|
|
315
411
|
ctx.addIssue({
|
|
316
|
-
code:
|
|
412
|
+
code: "custom",
|
|
317
413
|
path: ["targetElements"],
|
|
318
414
|
message: "targetElements is required for testType: 'ui'. Use null when blueprint capture failed; the null is the signal and needs no marker in description or reasoning.",
|
|
319
415
|
});
|
|
@@ -324,7 +420,7 @@ export const newTestSchema = z.preprocess(stripNullGroundingFields, z
|
|
|
324
420
|
if (Array.isArray(rec.targetElements) &&
|
|
325
421
|
rec.pageContext === undefined) {
|
|
326
422
|
ctx.addIssue({
|
|
327
|
-
code:
|
|
423
|
+
code: "custom",
|
|
328
424
|
path: ["pageContext"],
|
|
329
425
|
message: "pageContext is required for testType: 'ui' when targetElements is an array.",
|
|
330
426
|
});
|
|
@@ -355,7 +451,7 @@ const issueFoundSchema = z
|
|
|
355
451
|
"report an issue here at 'critical' or 'high'. A stated requirement the code does not meet is not a " +
|
|
356
452
|
"medium. The check raises an objection, never a rejection."),
|
|
357
453
|
category: z
|
|
358
|
-
.
|
|
454
|
+
.enum(IssueFoundCategory)
|
|
359
455
|
.describe("Issue classification. bug = a product/code defect, e.g. found by a test or in the diff. " +
|
|
360
456
|
"lint = a linter or formatter objection (eslint, flake8, prettier). " +
|
|
361
457
|
"type = a type-check failure (tsc, mypy). " +
|
|
@@ -374,7 +470,7 @@ const issueFoundSchema = z
|
|
|
374
470
|
.optional()
|
|
375
471
|
.describe("The `id` of the plan defect this issue reports, from the `defects` list you registered with skyramp_register_test_plan. Spell it as that list spells it: an id the plan does not declare traces nothing. " +
|
|
376
472
|
"Set it on the `category: bug` entry that reports the defect. A `defectId` on a `lint`, `type` or `config` entry does not report the defect — the report renders those in its own Configuration Errors section. " +
|
|
377
|
-
"CHECKED: every plan defect has a `bug` issue that names it, and a `bug` entry with no `plannedTestId` traces through this defect when a delivered test that expects to fail cites it,
|
|
473
|
+
"CHECKED: every plan defect has a `bug` issue that names it, and a `bug` entry with no `plannedTestId` traces through this defect when a delivered test that expects to fail cites it, when a `maintains` entry you registered cites it, or when you answered `defects:untested:<id>` at plan time. Leave it out for an issue the plan did not declare."),
|
|
378
474
|
sourceFile: citationString.describe("Path of the application file whose code is missing or wrong, relative to the repository root (e.g. 'src/crud/products.py'). " +
|
|
379
475
|
"REQUIRED when category is 'bug'. " +
|
|
380
476
|
"For code that is MISSING — an unmounted router, an unregistered route, an import never added — name the file where the line should be, not the file that defines what is unmounted. " +
|
|
@@ -390,8 +486,25 @@ const issueFoundSchema = z
|
|
|
390
486
|
.describe("1-based line number in sourceFile. Optional and advisory. " +
|
|
391
487
|
"Give the line of the offending statement itself, not the line of the enclosing function's signature — review views place the objection on exactly this line. " +
|
|
392
488
|
"The tool does not check that the line still holds the cited code, because line numbers drift."),
|
|
489
|
+
environmentFamily: environmentFamilyField
|
|
490
|
+
.optional()
|
|
491
|
+
.describe("Only for category 'config'. Not allowed for the other categories. " +
|
|
492
|
+
"When this config issue is a test-environment gap, name the gap. Omit it for a config issue that is not a test-environment gap. " +
|
|
493
|
+
ENVIRONMENT_FAMILY_MEANINGS),
|
|
393
494
|
})
|
|
394
495
|
.superRefine((issue, ctx) => {
|
|
496
|
+
// SKYR-4555: the family names a test-environment gap, which only a config
|
|
497
|
+
// entry reports. On a bug, lint or type entry it would put the issue in two
|
|
498
|
+
// places at once.
|
|
499
|
+
if (issue.environmentFamily !== undefined &&
|
|
500
|
+
issue.category !== IssueFoundCategory.Config) {
|
|
501
|
+
ctx.addIssue({
|
|
502
|
+
code: "custom",
|
|
503
|
+
path: ["environmentFamily"],
|
|
504
|
+
message: `environmentFamily is allowed only when category is 'config'. It names the test-environment gap behind a config issue. ` +
|
|
505
|
+
`Remove environmentFamily from this '${issue.category}' entry, or set category to 'config' if the issue is a test-environment gap.`,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
395
508
|
// A `bug` entry asserts that product code is wrong, so it has to name the code:
|
|
396
509
|
// without a file the entry is prose a reviewer cannot act on, and nothing
|
|
397
510
|
// downstream can place it in the diff. Only the file is required — a config file
|
|
@@ -408,7 +521,7 @@ const issueFoundSchema = z
|
|
|
408
521
|
if (issue.sourceFile !== undefined)
|
|
409
522
|
return;
|
|
410
523
|
ctx.addIssue({
|
|
411
|
-
code:
|
|
524
|
+
code: "custom",
|
|
412
525
|
path: ["sourceFile"],
|
|
413
526
|
message: `sourceFile is required when category is 'bug' — a bug entry must name the code it is about. ` +
|
|
414
527
|
`Open the file, then set sourceFile (path relative to the repository root). Also set sourceSymbol (the function or identifier inside it) when the file has one, and sourceLine (the line of the offending statement) when you have it — both are optional. ` +
|
|
@@ -431,11 +544,11 @@ const scenarioStepSchema = z.object({
|
|
|
431
544
|
.optional()
|
|
432
545
|
.describe("Expected HTTP status code, e.g. 200, 201, 404"),
|
|
433
546
|
requestBody: z
|
|
434
|
-
.record(z.any())
|
|
547
|
+
.record(z.string(), z.any())
|
|
435
548
|
.optional()
|
|
436
549
|
.describe("Example request body with realistic field values"),
|
|
437
550
|
responseBody: z
|
|
438
|
-
.union([z.record(z.any()), z.array(z.any())])
|
|
551
|
+
.union([z.record(z.string(), z.any()), z.array(z.any())])
|
|
439
552
|
.optional()
|
|
440
553
|
.describe("Key response fields to verify, e.g. { id: 'number', name: 'string', in_stock: 'boolean?' }. An array for a collection/list endpoint that returns a JSON array."),
|
|
441
554
|
});
|
|
@@ -445,7 +558,7 @@ export const additionalRecommendationSchema = z.preprocess(stripNullGroundingFie
|
|
|
445
558
|
.string()
|
|
446
559
|
.describe("Human-readable kebab-case identifier, e.g. 'integration-products-orders-workflow' or 'e2e-checkout-flow'. Format: '<testType>-<scenario-slug>'. Must be unique within the report."),
|
|
447
560
|
testType: z
|
|
448
|
-
.
|
|
561
|
+
.enum(TestType)
|
|
449
562
|
.describe("Type of test. Do not include priority or other metadata in this field."),
|
|
450
563
|
category: z
|
|
451
564
|
.enum(SCENARIO_CATEGORIES)
|
|
@@ -499,7 +612,7 @@ export const additionalRecommendationSchema = z.preprocess(stripNullGroundingFie
|
|
|
499
612
|
.superRefine((rec, ctx) => {
|
|
500
613
|
if (rec.testType === TestType.CONTRACT && !rec.primaryEndpoint) {
|
|
501
614
|
ctx.addIssue({
|
|
502
|
-
code:
|
|
615
|
+
code: "custom",
|
|
503
616
|
path: ["primaryEndpoint"],
|
|
504
617
|
message: "primaryEndpoint is required for contract tests — provide the HTTP verb and path, e.g. 'PATCH /api/v1/orders/{order_id}'.",
|
|
505
618
|
});
|
|
@@ -509,7 +622,7 @@ export const additionalRecommendationSchema = z.preprocess(stripNullGroundingFie
|
|
|
509
622
|
for (const field of ["targetElements", "pageContext"]) {
|
|
510
623
|
if (rec[field] !== undefined) {
|
|
511
624
|
ctx.addIssue({
|
|
512
|
-
code:
|
|
625
|
+
code: "custom",
|
|
513
626
|
path: [field],
|
|
514
627
|
message: `${field} is only valid for testType: 'ui'`,
|
|
515
628
|
});
|
|
@@ -519,7 +632,7 @@ export const additionalRecommendationSchema = z.preprocess(stripNullGroundingFie
|
|
|
519
632
|
else {
|
|
520
633
|
if (rec.targetElements === undefined) {
|
|
521
634
|
ctx.addIssue({
|
|
522
|
-
code:
|
|
635
|
+
code: "custom",
|
|
523
636
|
path: ["targetElements"],
|
|
524
637
|
message: "targetElements is required for testType: 'ui'. Use null when no capture covers the screen.",
|
|
525
638
|
});
|
|
@@ -527,7 +640,7 @@ export const additionalRecommendationSchema = z.preprocess(stripNullGroundingFie
|
|
|
527
640
|
if (Array.isArray(rec.targetElements) &&
|
|
528
641
|
rec.pageContext === undefined) {
|
|
529
642
|
ctx.addIssue({
|
|
530
|
-
code:
|
|
643
|
+
code: "custom",
|
|
531
644
|
path: ["pageContext"],
|
|
532
645
|
message: "pageContext is required for testType: 'ui' when targetElements is an array.",
|
|
533
646
|
});
|
|
@@ -537,7 +650,7 @@ export const additionalRecommendationSchema = z.preprocess(stripNullGroundingFie
|
|
|
537
650
|
}
|
|
538
651
|
}));
|
|
539
652
|
const testMaintenanceSchema = z.object({
|
|
540
|
-
testType: z.
|
|
653
|
+
testType: z.enum(TestType).describe("Type of test."),
|
|
541
654
|
endpoint: z
|
|
542
655
|
.string()
|
|
543
656
|
.describe("For contract and integration tests, the HTTP verb and path, e.g. 'GET /api/v1/products'. For ui and e2e tests, the page path the test opens, e.g. '/notifications'."),
|
|
@@ -548,7 +661,7 @@ const testMaintenanceSchema = z.object({
|
|
|
548
661
|
})
|
|
549
662
|
.describe("Absolute path of the test file that was maintained, e.g. '/repo/tests/products_smoke_test.py' — the same path you passed to skyramp_execute_test's testFile param. Consumers should basename this for display."),
|
|
550
663
|
action: z
|
|
551
|
-
.
|
|
664
|
+
.enum(DriftAction)
|
|
552
665
|
.describe("The drift action assigned to this test during maintenance triage."),
|
|
553
666
|
description: z.string().describe("What was changed and why"),
|
|
554
667
|
beforeDetails: z
|
|
@@ -576,8 +689,8 @@ const testMaintenanceSchema = z.object({
|
|
|
576
689
|
// "probably the primary" cannot tell a single-repo row from a mis-stamped one.
|
|
577
690
|
repository: z.string(),
|
|
578
691
|
// Server-populated from stateFile execution records — never supplied by the LLM.
|
|
579
|
-
beforeStatus: z.
|
|
580
|
-
afterStatus: z.
|
|
692
|
+
beforeStatus: z.enum(TestExecutionStatus),
|
|
693
|
+
afterStatus: z.enum(TestExecutionStatus),
|
|
581
694
|
});
|
|
582
695
|
// LLM input contract: status fields are server-populated from stateFile, and
|
|
583
696
|
// `defects` is attached from the per-file detail the agent drafts, not from the
|
|
@@ -1361,7 +1474,7 @@ export function registerSubmitReportTool(server) {
|
|
|
1361
1474
|
"This is the ONLY way to submit the report — do NOT write the report to a file manually. " +
|
|
1362
1475
|
"If a check objects to what you reported, the objection names an id: fix what you can, and answer the rest through `answers` on a second call. " +
|
|
1363
1476
|
"On an ACCEPTED call this tool also edits the files this run changed: a comment addressed to a Skyramp gate — `reuse-verify: allow …` or `// kept inline: …` — is removed from each of them, because it speaks to a gate and not to the customer who owns the file. Nothing else in those files is touched. A REFUSED call leaves every source file exactly as it found it; it still records its own run state, such as the refusal count and the objections it raised.",
|
|
1364
|
-
inputSchema: {
|
|
1477
|
+
inputSchema: z.object({
|
|
1365
1478
|
businessCaseAnalysis: z
|
|
1366
1479
|
.string()
|
|
1367
1480
|
.min(1, "businessCaseAnalysis is required — describe what the PR does, even for no-test runs (config-only, non-application, Testbot onboarding). It is never optional and must not be empty.")
|
|
@@ -1412,12 +1525,11 @@ export function registerSubmitReportTool(server) {
|
|
|
1412
1525
|
answer: z
|
|
1413
1526
|
.string()
|
|
1414
1527
|
.describe("Why the objection does not apply, or what you decided instead. A blank or whitespace-only answer closes nothing and the objection stays open."),
|
|
1415
|
-
blocker: blockerSchema.optional().describe(BLOCKER_DESCRIPTION),
|
|
1416
1528
|
})
|
|
1417
1529
|
.strict())
|
|
1418
1530
|
.optional()
|
|
1419
1531
|
.default([])
|
|
1420
|
-
.describe("One answer per check that objected to your previous call and that you do not intend to fix. An answered objection is published with your answer beside it and no longer counted open; an unanswered one is published on its own.
|
|
1532
|
+
.describe("One answer per check that objected to your previous call and that you do not intend to fix. An answered objection is published with your answer beside it and no longer counted open; an unanswered one is published on its own. Answers persist across calls, so an id answered once stays answered. There is no call limit."),
|
|
1421
1533
|
stateFile: z
|
|
1422
1534
|
.string()
|
|
1423
1535
|
.optional()
|
|
@@ -1426,7 +1538,7 @@ export function registerSubmitReportTool(server) {
|
|
|
1426
1538
|
"another run's execution results, and the call is refused. " +
|
|
1427
1539
|
"The report is written beside it, and it holds the execution results, the " +
|
|
1428
1540
|
"page captures and the videos this report needs."),
|
|
1429
|
-
},
|
|
1541
|
+
}),
|
|
1430
1542
|
_meta: {
|
|
1431
1543
|
keywords: ["report", "summary", "testbot", "submit"],
|
|
1432
1544
|
},
|
|
@@ -2513,14 +2625,6 @@ export function registerSubmitReportTool(server) {
|
|
|
2513
2625
|
typeof entry?.answer === "string" &&
|
|
2514
2626
|
entry.answer.trim().length > 0);
|
|
2515
2627
|
const carriedByKey = new Map(carried.map((entry) => [answerKey(entry.objection), entry]));
|
|
2516
|
-
// Probed here for the same reason the plan tool probes before building its
|
|
2517
|
-
// context: the checks below are synchronous. Both this call's blockers and
|
|
2518
|
-
// the carried ones, so a carried close is re-judged rather than trusted.
|
|
2519
|
-
const blockerChecks = await probeBlockers([
|
|
2520
|
-
...blockersIn(acknowledged),
|
|
2521
|
-
...blockersIn(carried),
|
|
2522
|
-
]);
|
|
2523
|
-
const judge = (blocker) => verifyBlocker(blocker, blockerChecks);
|
|
2524
2628
|
// What the LAST call raised. Without it there is nothing to notice a
|
|
2525
2629
|
// fixed objection's absence against.
|
|
2526
2630
|
const previouslyRaised = (Array.isArray(stateData.reportObjections?.raisedObjections)
|
|
@@ -2547,35 +2651,12 @@ export function registerSubmitReportTool(server) {
|
|
|
2547
2651
|
...raised,
|
|
2548
2652
|
...previouslyRaised.map((entry) => entry.objection),
|
|
2549
2653
|
];
|
|
2550
|
-
const raisedKeys = new Set(raised.map(answerKey));
|
|
2551
2654
|
const reportStageObjections = [
|
|
2552
2655
|
...raised,
|
|
2553
2656
|
...unknownAnswerObjections(answerable, acknowledged, {
|
|
2554
2657
|
scope: "this run raised",
|
|
2555
2658
|
suggestion: "Answer only the ids this tool has listed on this run. A plan-time objection is answered through skyramp_register_test_plan's own answers, not here.",
|
|
2556
2659
|
}),
|
|
2557
|
-
// The same refusal plan time raises: a sentence sent for an objection that
|
|
2558
|
-
// takes a blocker leaves that objection open, and this says so rather than
|
|
2559
|
-
// letting the answer pass in silence. Against this call's answers only,
|
|
2560
|
-
// like `registerPlan` — a carried one that closed nothing simply stays
|
|
2561
|
-
// carried, and its objection stays published open.
|
|
2562
|
-
...refusedAnswerObjections(raised, acknowledged, judge, (id) =>
|
|
2563
|
-
// The same rule the loop below applies to a carried answer. Without
|
|
2564
|
-
// it a second, weaker answer for an objection an earlier call already
|
|
2565
|
-
// closed raised `answers:refused:` beside the close this call still
|
|
2566
|
-
// publishes as answered. Matched by id AND message, as `carriedByKey`
|
|
2567
|
-
// is: one id can name a different bug on a later call, and a close
|
|
2568
|
-
// carried for the first must not silence a refusal for the second.
|
|
2569
|
-
carried.some((entry) => {
|
|
2570
|
-
if (entry.objection?.objectionId !== id)
|
|
2571
|
-
return false;
|
|
2572
|
-
if (!raisedKeys.has(answerKey(entry.objection)))
|
|
2573
|
-
return false;
|
|
2574
|
-
const blocker = hasBlocker(entry.blocker)
|
|
2575
|
-
? entry.blocker
|
|
2576
|
-
: undefined;
|
|
2577
|
-
return (!needsBlocker(id) || (!!blocker && judge(blocker) !== "refuted"));
|
|
2578
|
-
})),
|
|
2579
2660
|
];
|
|
2580
2661
|
const raisedNow = new Set(reportStageObjections.map(answerKey));
|
|
2581
2662
|
// A call with no execution result gives these checks nothing to judge, so
|
|
@@ -2605,57 +2686,12 @@ export function registerSubmitReportTool(server) {
|
|
|
2605
2686
|
const answeredReportObjections = [];
|
|
2606
2687
|
const openReportObjections = [];
|
|
2607
2688
|
for (const objection of reportStageObjections) {
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
// blocker sent, refuted or belonging to no answer at all.
|
|
2614
|
-
const wantsBlocker = needsBlocker(objection.objectionId);
|
|
2615
|
-
const closingNow = closingAnswerFor(objection, acknowledged, judge);
|
|
2616
|
-
const blockerNow = hasBlocker(closingNow?.blocker)
|
|
2617
|
-
? closingNow.blocker
|
|
2618
|
-
: undefined;
|
|
2619
|
-
const carriedEntry = carriedByKey.get(answerKey(objection));
|
|
2620
|
-
const carriedBlocker = hasBlocker(carriedEntry?.blocker)
|
|
2621
|
-
? carriedEntry?.blocker
|
|
2622
|
-
: undefined;
|
|
2623
|
-
const answerNow = closingNow?.answer;
|
|
2624
|
-
// A carried answer meets the same rule as the call that sent it: a
|
|
2625
|
-
// blocker, and one this call's probe does not refute.
|
|
2626
|
-
const carriedCloses = carriedEntry &&
|
|
2627
|
-
(!wantsBlocker ||
|
|
2628
|
-
(!!carriedBlocker && judge(carriedBlocker) !== "refuted"));
|
|
2629
|
-
const answer = answerNow ?? (carriedCloses ? carriedEntry?.answer : undefined);
|
|
2630
|
-
if (!answer) {
|
|
2689
|
+
const answer = answerFor(objection, acknowledged) ??
|
|
2690
|
+
carriedByKey.get(answerKey(objection))?.answer;
|
|
2691
|
+
if (answer)
|
|
2692
|
+
answeredReportObjections.push({ objection, answer });
|
|
2693
|
+
else
|
|
2631
2694
|
openReportObjections.push(objection);
|
|
2632
|
-
// A carried answer that closed this objection on an earlier call and
|
|
2633
|
-
// does not now is the probe changing its mind — the service came back
|
|
2634
|
-
// up. Without this the objection simply reappears with no reason.
|
|
2635
|
-
if (carriedEntry &&
|
|
2636
|
-
carriedBlocker &&
|
|
2637
|
-
judge(carriedBlocker) === "refuted") {
|
|
2638
|
-
openReportObjections.push({
|
|
2639
|
-
objectionId: `${REFUSED_ANSWER_PREFIX}${objection.objectionId}`,
|
|
2640
|
-
verifier: "answers",
|
|
2641
|
-
message: "The server checked the blocker this answer names and found the opposite, so the objection stays open.",
|
|
2642
|
-
evidence: `answered "${objection.objectionId}" on an earlier call with \`${carriedBlocker.kind}\`; ${refutedEvidence(carriedBlocker)}`,
|
|
2643
|
-
suggestion: "Write the test: what you named as the blocker is not in the way. If something else is, send the blocker that names it — the server checks each kind against the run it can see.",
|
|
2644
|
-
});
|
|
2645
|
-
}
|
|
2646
|
-
continue;
|
|
2647
|
-
}
|
|
2648
|
-
// A refuted blocker is not published, as at plan time: on an objection any
|
|
2649
|
-
// answer closes, the sentence closes it and the untrue claim is dropped.
|
|
2650
|
-
const sent = answerNow ? blockerNow : carriedBlocker;
|
|
2651
|
-
const blocker = sent && judge(sent) !== "refuted" ? sent : undefined;
|
|
2652
|
-
answeredReportObjections.push({
|
|
2653
|
-
objection,
|
|
2654
|
-
answer,
|
|
2655
|
-
...(blocker
|
|
2656
|
-
? { blocker, blockerVerified: judge(blocker) === "verified" }
|
|
2657
|
-
: {}),
|
|
2658
|
-
});
|
|
2659
2695
|
}
|
|
2660
2696
|
objections = [
|
|
2661
2697
|
...stillOpen.map((objection) => ({
|
|
@@ -2668,16 +2704,6 @@ export function registerSubmitReportTool(server) {
|
|
|
2668
2704
|
...entry.objection,
|
|
2669
2705
|
stage: "plan",
|
|
2670
2706
|
answer: typeof entry.answer === "string" ? entry.answer : "",
|
|
2671
|
-
// A plan-time close that a blocker carried says so here, exactly as
|
|
2672
|
-
// the post-execution closes below do. Without this a probed close
|
|
2673
|
-
// was published as an ordinary sentence and the report lost the one
|
|
2674
|
-
// fact that distinguishes them.
|
|
2675
|
-
...(hasBlocker(entry.blocker)
|
|
2676
|
-
? {
|
|
2677
|
-
unverifiedBlocker: blockerText(entry.blocker),
|
|
2678
|
-
blockerVerified: !!entry.blockerVerified,
|
|
2679
|
-
}
|
|
2680
|
-
: {}),
|
|
2681
2707
|
})),
|
|
2682
2708
|
...unverified
|
|
2683
2709
|
.filter((entry) => entry?.objection)
|
|
@@ -2685,29 +2711,16 @@ export function registerSubmitReportTool(server) {
|
|
|
2685
2711
|
...entry.objection,
|
|
2686
2712
|
stage: "plan",
|
|
2687
2713
|
answer: typeof entry.answer === "string" ? entry.answer : "",
|
|
2688
|
-
|
|
2689
|
-
// string published an empty one on every plan-time close.
|
|
2690
|
-
...(hasBlocker(entry.blocker)
|
|
2691
|
-
? { unverifiedBlocker: blockerText(entry.blocker) }
|
|
2692
|
-
: {}),
|
|
2714
|
+
unverifiedBlocker: typeof entry.blocker === "string" ? entry.blocker : "",
|
|
2693
2715
|
})),
|
|
2694
2716
|
...openReportObjections.map((objection) => ({
|
|
2695
2717
|
...objection,
|
|
2696
2718
|
stage: "post-execution",
|
|
2697
2719
|
})),
|
|
2698
|
-
|
|
2699
|
-
// closes above are, and `blockerVerified` says whether a probe backed it
|
|
2700
|
-
// or the server could only take it as given.
|
|
2701
|
-
...answeredReportObjections.map(({ objection, answer, blocker, blockerVerified }) => ({
|
|
2720
|
+
...answeredReportObjections.map(({ objection, answer }) => ({
|
|
2702
2721
|
...objection,
|
|
2703
2722
|
stage: "post-execution",
|
|
2704
2723
|
answer,
|
|
2705
|
-
...(blocker
|
|
2706
|
-
? {
|
|
2707
|
-
unverifiedBlocker: blockerText(blocker),
|
|
2708
|
-
blockerVerified: !!blockerVerified,
|
|
2709
|
-
}
|
|
2710
|
-
: {}),
|
|
2711
2724
|
})),
|
|
2712
2725
|
...resolvedObjections.map(({ objection, resolvedBy }) => ({
|
|
2713
2726
|
...objection,
|
|
@@ -2852,7 +2865,10 @@ export function registerSubmitReportTool(server) {
|
|
|
2852
2865
|
// these objects and don't expect it).
|
|
2853
2866
|
testResults: params.testResults.map((row) => {
|
|
2854
2867
|
const { testFilePath: _tfp, ...wire } = attachVideoPath(row, stateData.executionVideos);
|
|
2855
|
-
|
|
2868
|
+
// SKYR-4555: the basename is the join key to newTestsCreated and
|
|
2869
|
+
// testMaintenance (consumers compare it with the basename of their
|
|
2870
|
+
// fileName, which may be a relative path); the path stays internal.
|
|
2871
|
+
return { ...wire, fileName: path.basename(row.testFilePath) };
|
|
2856
2872
|
}),
|
|
2857
2873
|
issuesFound,
|
|
2858
2874
|
executionCoverage,
|
|
@@ -69,19 +69,19 @@ const recommendationSchema = z.object({
|
|
|
69
69
|
})
|
|
70
70
|
.describe("Absolute path to the page object (POM) file to edit, when the spec delegates selectors to a page object. When set, skyramp_actions edits this file instead of testFilePath. testFilePath (the spec) is still used for catalog validation and the maintenance report."),
|
|
71
71
|
action: z
|
|
72
|
-
.
|
|
72
|
+
.enum(DriftAction)
|
|
73
73
|
.refine((a) => a !== DriftAction.Ignore, {
|
|
74
74
|
message: "Omit IGNORE entries entirely — a discovered test you don't recommend is automatically treated as needing no action.",
|
|
75
75
|
})
|
|
76
76
|
.describe("The maintenance action for this test (UPDATE, REGENERATE, DELETE, or VERIFY). Do not send IGNORE — omit no-action tests from `recommendations` entirely; anything you leave out is recorded as needing no action."),
|
|
77
77
|
testType: z
|
|
78
|
-
.
|
|
78
|
+
.enum(TestType)
|
|
79
79
|
.describe("Type of this test — carried through to the maintenance report."),
|
|
80
80
|
endpoint: z
|
|
81
81
|
.string()
|
|
82
82
|
.describe("HTTP verb and path this test covers, e.g. 'GET /api/v1/products'."),
|
|
83
83
|
priority: z
|
|
84
|
-
.
|
|
84
|
+
.enum(RecommendationPriority)
|
|
85
85
|
.optional()
|
|
86
86
|
.describe("Update priority"),
|
|
87
87
|
rationale: z
|
|
@@ -90,7 +90,7 @@ const recommendationSchema = z.object({
|
|
|
90
90
|
.min(1)
|
|
91
91
|
.describe("1-2 sentence explanation of why this action is needed — becomes the maintenance report's description, so it must not be blank."),
|
|
92
92
|
estimatedWork: z
|
|
93
|
-
.
|
|
93
|
+
.enum(EstimatedWork)
|
|
94
94
|
.optional()
|
|
95
95
|
.describe("Estimated effort to apply the update"),
|
|
96
96
|
updateInstructions: z
|
|
@@ -169,15 +169,15 @@ export function registerActionsTool(server) {
|
|
|
169
169
|
description: `Execute test maintenance actions — final step of the unified Test Health Analysis Flow. Call this once per run even when no tests need action (pass \`recommendations: []\`) — it is where the maintenance verdicts are persisted before the run moves on. Supply only the tests that need an action; any discovered test you omit from \`recommendations\` is treated as needing no action and recorded automatically, so you never enumerate the no-action tests yourself.`,
|
|
170
170
|
// SKYR-4194: the payload is returned via structuredContent, which requires a
|
|
171
171
|
// declared outputSchema — see dualChannelResult.
|
|
172
|
-
outputSchema: {
|
|
172
|
+
outputSchema: z.object({
|
|
173
173
|
summary: z
|
|
174
174
|
.string()
|
|
175
175
|
.describe("Human-readable report of the maintenance actions to take."),
|
|
176
176
|
instructions: z
|
|
177
177
|
.string()
|
|
178
178
|
.describe("JSON instructions for carrying out those actions — file lists, per-file edit context, and the update/regenerate/delete strategies. Read and follow this field."),
|
|
179
|
-
},
|
|
180
|
-
inputSchema: actionsSchema,
|
|
179
|
+
}),
|
|
180
|
+
inputSchema: z.object(actionsSchema),
|
|
181
181
|
}, async (args) => {
|
|
182
182
|
let errorResult;
|
|
183
183
|
try {
|