@skyramp/mcp 0.3.2 → 0.3.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 +47 -2
- package/build/playwright/PlaywrightTraceService.d.ts +8 -0
- package/build/playwright/PlaywrightTraceService.js +1 -0
- package/build/playwright/registerPlaywrightTools.js +42 -1
- package/build/prompts/enhance-assertions/sharedAssertionRules.js +19 -0
- package/build/prompts/pom-aware-code-reuse.js +17 -8
- package/build/prompts/test-maintenance/actionsInstructions.js +2 -2
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +1 -4
- package/build/prompts/test-recommendation/recommendationSections.d.ts +1 -1
- package/build/prompts/test-recommendation/recommendationSections.js +5 -5
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +13 -7
- package/build/prompts/testbot/testbot-prompts.d.ts +2 -12
- package/build/prompts/testbot/testbot-prompts.js +28 -16
- package/build/recommendation/discriminators.d.ts +7 -1
- package/build/recommendation/discriminators.js +16 -3
- package/build/resources/testbotResource.js +20 -4
- package/build/services/ScenarioGenerationService.js +5 -2
- package/build/services/TestExecutionService.d.ts +13 -8
- package/build/services/TestExecutionService.js +73 -26
- package/build/services/TestGenerationService.js +24 -9
- package/build/services/containerEnv.d.ts +12 -1
- package/build/services/containerEnv.js +118 -1
- package/build/tools/executeSkyrampTestTool.d.ts +9 -0
- package/build/tools/executeSkyrampTestTool.js +20 -6
- package/build/tools/execution-video-state.d.ts +21 -0
- package/build/tools/execution-video-state.js +51 -0
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +31 -11
- package/build/tools/generate-tests/planGuard.d.ts +5 -5
- package/build/tools/generate-tests/planGuard.js +5 -17
- package/build/tools/queryProxyMocksTool.js +0 -1
- package/build/tools/submitReportTool.d.ts +83 -10
- package/build/tools/submitReportTool.js +179 -29
- package/build/tools/test-management/actionsTool.js +52 -41
- package/build/tools/test-management/analyzeChangesTool.d.ts +11 -0
- package/build/tools/test-management/analyzeChangesTool.js +37 -33
- package/build/tools/test-management/analyzeTestHealthTool.js +3 -3
- package/build/tools/test-management/registerTestPlanTool.js +113 -31
- package/build/types/TestAnalysis.d.ts +7 -3
- package/build/types/TestExecution.d.ts +14 -0
- package/build/types/TestTypes.js +3 -2
- package/build/types/TestbotPromptOptions.d.ts +34 -0
- package/build/types/TestbotPromptOptions.js +1 -0
- package/build/types/TestbotReport.d.ts +10 -0
- package/build/types/TestbotReport.js +10 -1
- package/build/types/index.d.ts +2 -0
- package/build/types/index.js +1 -0
- package/build/utils/AnalysisStateManager.d.ts +36 -2
- package/build/utils/AnalysisStateManager.js +34 -13
- package/build/utils/frontendSelectors.js +0 -1
- package/build/utils/gitStaging.d.ts +5 -0
- package/build/utils/gitStaging.js +1 -1
- package/build/utils/pom-catalog.d.ts +23 -0
- package/build/utils/pom-catalog.js +30 -0
- package/build/utils/pom-scope/pom-files.d.ts +14 -0
- package/build/utils/pom-scope/pom-files.js +32 -6
- package/build/utils/pom-scope/testIdDiscovery.d.ts +40 -0
- package/build/utils/pom-scope/testIdDiscovery.js +104 -0
- package/build/utils/reportVerification.d.ts +7 -2
- package/build/utils/reportVerification.js +9 -3
- package/build/utils/scenarioDrafting.js +7 -1
- package/build/utils/skyrampMdContent.d.ts +1 -1
- package/build/utils/skyrampMdContent.js +1 -1
- package/build/utils/urlPath.d.ts +37 -0
- package/build/utils/urlPath.js +55 -0
- package/build/utils/utils.d.ts +45 -0
- package/build/utils/utils.js +50 -0
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/utils/workspaceAuth.d.ts +15 -15
- package/build/utils/workspaceAuth.js +32 -17
- package/build/workspace/queryParamResolution.d.ts +93 -0
- package/build/workspace/queryParamResolution.js +201 -0
- package/build/workspace/workspace.d.ts +104 -0
- package/build/workspace/workspace.js +24 -0
- package/node_modules/playwright/ThirdPartyNotices.txt +319 -266
- package/node_modules/playwright/lib/dom-analyzer/blueprint.js +154 -27
- package/node_modules/playwright/lib/dom-analyzer/crawler.js +2 -2
- package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.js +1 -1
- package/node_modules/playwright/lib/mcp/browser/tools/sitemap.js +6 -2
- package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +3 -2
- package/node_modules/playwright/lib/mcp/skyramp/resultCode.js +4 -3
- package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +15 -3
- package/node_modules/playwright/lib/mcp/skyramp/specImport.js +781 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +14 -3
- package/node_modules/playwright/lib/mcp/test/resultCode.test.js +2 -1
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +1 -1
- package/node_modules/playwright/lib/mcp/test/skyRampExport.test.js +30 -0
- package/node_modules/playwright/lib/transform/babelBundleImpl.js +200 -199
- package/node_modules/playwright/node_modules/playwright-core/ThirdPartyNotices.txt +3 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/.package-lock.json +3 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/adapter/aws-lambda/handler.js +16 -25
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/adapter/bun/websocket.js +3 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/adapter/lambda-edge/handler.js +20 -4
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/adapter/aws-lambda/handler.js +16 -25
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/adapter/bun/websocket.js +3 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/adapter/lambda-edge/handler.js +20 -4
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/client/client.js +10 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/client/utils.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/helper/css/common.js +3 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/helper/css/index.js +9 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/jsx/base.js +8 -14
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/jsx/components.js +41 -21
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/jsx/context.js +131 -5
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/jsx/streaming.js +9 -7
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/bearer-auth/index.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/cache/index.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/compress/index.js +2 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/cors/index.js +2 -5
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/etag/index.js +2 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/language/language.js +10 -32
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/method-override/index.js +5 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/serve-static/index.js +2 -2
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/timing/timing.js +3 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/router/trie-router/node.js +9 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/utils/body.js +12 -4
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/utils/buffer.js +2 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/utils/ipaddr.js +6 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/validator/validator.js +3 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/client/client.js +10 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/client/utils.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/helper/css/common.js +3 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/helper/css/index.js +9 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/jsx/base.js +15 -15
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/jsx/components.js +42 -22
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/jsx/context.js +129 -5
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/jsx/streaming.js +10 -8
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/bearer-auth/index.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/cache/index.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/compress/index.js +2 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/cors/index.js +2 -5
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/etag/index.js +2 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/language/language.js +10 -32
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/method-override/index.js +5 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/serve-static/index.js +2 -2
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/timing/timing.js +3 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/router/trie-router/node.js +9 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/adapter/aws-lambda/handler.d.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/adapter/lambda-edge/handler.d.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/helper/websocket/index.d.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/jsx/base.d.ts +1 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/jsx/context.d.ts +39 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/middleware/context-storage/index.d.ts +2 -2
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/middleware/language/language.d.ts +18 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/utils/body.d.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/utils/types.d.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/utils/body.js +12 -4
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/utils/buffer.js +2 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/utils/ipaddr.js +6 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/validator/validator.js +3 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/package.json +29 -22
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/package-lock.json +3 -3
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +5 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/specReader.js +781 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderApp.js +25 -6
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/skyramp/replayEngine.js +3 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/utils.js +2 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/htmlReport/index.html +253 -27
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/{codeMirrorModule-D0BjbCb7.js → codeMirrorModule-DtudTj_v.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-BpDwp16L.js +422 -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-Bzd72-bG.js → codeMirrorModule-FNMuBzX1.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-Co9upU5h.js +1035 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.DXNIQ_dx.js +2 -0
- 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.CIKB3XSv.js +5 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +4 -0
- package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/specReader.ts +1028 -0
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderApp.ts +31 -8
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/skyramp/replayEngine.ts +1 -0
- package/node_modules/playwright/node_modules/playwright-core/src/utils.ts +1 -0
- package/node_modules/playwright/package.json +1 -1
- package/package.json +10 -6
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/tsconfig.build.tsbuildinfo +0 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-lvTRGFx-.js +0 -193
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-DzxTioTK.js +0 -809
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.BGc30U3S.js +0 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.IaDrb29A.js +0 -5
|
@@ -7,7 +7,8 @@ import { TEST_CATEGORIES, externalCategory } from "../types/TestRecommendation.j
|
|
|
7
7
|
import { TestType, HttpMethod } from "../types/TestTypes.js";
|
|
8
8
|
import { DriftAction } from "../types/TestAnalysis.js";
|
|
9
9
|
import { TestExecutionStatus } from "../types/TestExecution.js";
|
|
10
|
-
import {
|
|
10
|
+
import { IssueFoundCategory } from "../types/TestbotReport.js";
|
|
11
|
+
import { StateManager, runArtifactDir } from "../utils/AnalysisStateManager.js";
|
|
11
12
|
import { toolError, testFileMatches } from "../utils/utils.js";
|
|
12
13
|
import { matchesApprovedPlan } from "../utils/planMatchKeys.js";
|
|
13
14
|
import { isTestbotEnabled } from "../utils/featureFlags.js";
|
|
@@ -19,13 +20,49 @@ import { rederiveReuseOutcome } from "./code-refactor/reuse-state.js";
|
|
|
19
20
|
// NOT gated at generation time (see planGuard.ts wiring), so they are excluded
|
|
20
21
|
// here too rather than surprising the agent with a report-time-only gate.
|
|
21
22
|
const PLAN_GATED_TEST_TYPES = new Set([TestType.CONTRACT, TestType.INTEGRATION]);
|
|
22
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* Filename of the report, written beside the state file. SKYR-4147: the report path is
|
|
25
|
+
* derived here rather than accepted as a parameter. The caller builds the state file and
|
|
26
|
+
* the report path from a single temp dir (testbot's `skyrampTempDir()`), so the state
|
|
27
|
+
* file's directory IS the directory the caller reads the report back from. Taking the
|
|
28
|
+
* path from the agent instead meant the agent had to retype it out of a 600-character
|
|
29
|
+
* encoded prompt URI; one wrong character wrote the report somewhere nobody reads, and
|
|
30
|
+
* the tool still reported success, throwing away the whole run without a word.
|
|
31
|
+
*
|
|
32
|
+
* Must stay in sync with testbot, which hardcodes the same string in main.ts, post.ts and
|
|
33
|
+
* eval-entry.ts — the same duplication across the two repos that already exists for
|
|
34
|
+
* `RUN_STATE_FILE_NAME` / testbot's `STATE_FILE_NAME`.
|
|
35
|
+
*/
|
|
36
|
+
export const REPORT_FILE_NAME = "testbot-result.txt";
|
|
37
|
+
/**
|
|
38
|
+
* The names to match a report entry against the approved plan's `name:` keys.
|
|
39
|
+
* `testId` is documented as `<testType>-<scenario-slug>`, so the plan's bare
|
|
40
|
+
* scenarioName only matches once that prefix is dropped — but agents also submit
|
|
41
|
+
* the bare name (both forms appear in SKYR-4123's runs), so try each.
|
|
42
|
+
*/
|
|
43
|
+
function planNameCandidates(testId, testType) {
|
|
44
|
+
const id = (testId ?? "").trim();
|
|
45
|
+
if (!id)
|
|
46
|
+
return [];
|
|
47
|
+
const prefix = `${testType}-`;
|
|
48
|
+
return id.toLowerCase().startsWith(prefix) ? [id, id.slice(prefix.length)] : [id];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Split an `endpoint` field into one {method, path} per endpoint it names.
|
|
52
|
+
* A multi-step test writes them comma-separated ("POST /orders, DELETE
|
|
53
|
+
* /orders/{id}"); read as a single endpoint the whole string becomes one
|
|
54
|
+
* unmatchable path, so such an entry could match nothing (SKYR-4123).
|
|
55
|
+
*/
|
|
23
56
|
function parseEndpointField(endpoint) {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
57
|
+
const parts = (endpoint ?? "").split(",").map((p) => p.trim()).filter(Boolean);
|
|
58
|
+
if (parts.length === 0)
|
|
59
|
+
return [{}];
|
|
60
|
+
return parts.map((part) => {
|
|
61
|
+
const spaceIdx = part.indexOf(" ");
|
|
62
|
+
if (spaceIdx <= 0)
|
|
63
|
+
return { path: part };
|
|
64
|
+
return { method: part.slice(0, spaceIdx), path: part.slice(spaceIdx + 1).trim() };
|
|
65
|
+
});
|
|
29
66
|
}
|
|
30
67
|
// Drift actions that actually modify a test file. VERIFY and IGNORE are
|
|
31
68
|
// no-ops (the test was assessed but left unchanged), so they must not count
|
|
@@ -53,12 +90,22 @@ function normalizeRepository(item) {
|
|
|
53
90
|
const trimmed = item.repository?.trim();
|
|
54
91
|
return trimmed ? { ...item, repository: trimmed } : { ...item, repository: undefined };
|
|
55
92
|
}
|
|
93
|
+
// videoPath is deliberately absent from this input contract: it is attached server-side
|
|
94
|
+
// from the run's execution records (see attachVideoPath), and zod strips any the model
|
|
95
|
+
// supplies anyway. SKYR-4156 is what happens when the agent owns that field instead.
|
|
56
96
|
const testResultSchema = z.object({
|
|
57
97
|
testType: z.nativeEnum(TestType).describe("Type of test. Do not include priority or other metadata in this field."),
|
|
58
98
|
endpoint: z.string().describe("HTTP verb and path, e.g. 'GET /api/v1/products'"),
|
|
59
99
|
status: z.enum(["Pass", "Fail", "Skipped"]).describe("Test execution result"),
|
|
60
100
|
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'"),
|
|
61
|
-
|
|
101
|
+
// Required for every row: each one reports a specific test file the agent ran, so it
|
|
102
|
+
// can always name it. It is what identifies the row server-side — `endpoint` cannot,
|
|
103
|
+
// since several tests routinely exercise one endpoint — and for ui/e2e it is what
|
|
104
|
+
// attaches the recorded video (SKYR-4156). Not included in the report itself.
|
|
105
|
+
testFilePath: z
|
|
106
|
+
.string()
|
|
107
|
+
.refine((p) => path.isAbsolute(p), { message: "testFilePath must be an absolute path" })
|
|
108
|
+
.describe("Absolute path of the test file this result is for — the same path you passed to skyramp_execute_test's testFile param. Consumers basename it for display."),
|
|
62
109
|
repository: repositoryField,
|
|
63
110
|
});
|
|
64
111
|
// Structured grounding for UI recommendations. Lifted verbatim from a
|
|
@@ -85,11 +132,39 @@ export const pageContextSchema = z.object({
|
|
|
85
132
|
url: z.string().describe("URL of the page where the test runs. Lifted from BlueprintCapture.url."),
|
|
86
133
|
pageHash: z.string().optional().describe("Opaque hash of the captured page state (BlueprintCapture.pageHash). Lets the verifier confirm the recommendation was grounded in a still-current capture."),
|
|
87
134
|
});
|
|
135
|
+
/**
|
|
136
|
+
* SKYR-4193: LLMs habitually emit every key a schema declares, using a
|
|
137
|
+
* literal null for "not applicable". A null is stripped (normalized to
|
|
138
|
+
* omitted) before validation wherever null carries no meaning, instead of
|
|
139
|
+
* costing the agent a rejection/rewrite round-trip (run 32182635458 lost
|
|
140
|
+
* ~2m40s to exactly this):
|
|
141
|
+
* - `pageContext: null` is always stripped — the field is optional but never
|
|
142
|
+
* nullable, so null is never a valid value, UI or not.
|
|
143
|
+
* - `targetElements: null` is stripped only on non-UI entries — on UI tests
|
|
144
|
+
* null is load-bearing ("blueprint capture failed", requires the
|
|
145
|
+
* '[no-blueprint-data]' markers) and must reach the refinements intact.
|
|
146
|
+
* Non-null values on non-UI entries still reach the refinements and are
|
|
147
|
+
* still rejected.
|
|
148
|
+
*/
|
|
149
|
+
function stripNullGroundingFields(val) {
|
|
150
|
+
if (!val || typeof val !== "object" || Array.isArray(val))
|
|
151
|
+
return val;
|
|
152
|
+
const entry = val;
|
|
153
|
+
const stripTargetElements = entry.targetElements === null && entry.testType !== TestType.UI;
|
|
154
|
+
if (!stripTargetElements && entry.pageContext !== null)
|
|
155
|
+
return val;
|
|
156
|
+
const copy = { ...entry };
|
|
157
|
+
if (stripTargetElements)
|
|
158
|
+
delete copy.targetElements;
|
|
159
|
+
if (copy.pageContext === null)
|
|
160
|
+
delete copy.pageContext;
|
|
161
|
+
return copy;
|
|
162
|
+
}
|
|
88
163
|
// TODO: Unify newTestSchema and additionalRecommendationSchema into a single
|
|
89
164
|
// interface that adds an `implemented: boolean` field. Both describe the same
|
|
90
165
|
// concept (a test recommendation) — the only difference is whether it was
|
|
91
166
|
// generated in this run or left for later. Tracked per Archit's review comment.
|
|
92
|
-
export const newTestSchema = z.object({
|
|
167
|
+
export const newTestSchema = z.preprocess(stripNullGroundingFields, z.object({
|
|
93
168
|
testId: z.string().describe("Human-readable kebab-case identifier, e.g. 'contract-get-products' or 'integration-users-orders-workflow'. Format: '<testType>-<method>-<resource>' for single-endpoint tests or '<testType>-<scenario-slug>' for multi-step tests. Must be unique within the report."),
|
|
94
169
|
testType: z.nativeEnum(TestType).describe("Type of test created. Do not include priority or other metadata in this field."),
|
|
95
170
|
category: z.preprocess((val) => externalCategory(val), z.enum(TEST_CATEGORIES)).describe("Test category — critical categories (security_boundary, business_rule, data_integrity, breaking_change) get generation priority over workflow"),
|
|
@@ -166,7 +241,7 @@ export const newTestSchema = z.object({
|
|
|
166
241
|
}
|
|
167
242
|
}
|
|
168
243
|
}
|
|
169
|
-
});
|
|
244
|
+
}));
|
|
170
245
|
const issueFoundSchema = z.object({
|
|
171
246
|
description: z.string().describe("One-line description. Do NOT prefix with the severity level — severity is a separate field. Include code logic bugs from the diff, test generation/execution failures, and environment misconfiguration."),
|
|
172
247
|
severity: z
|
|
@@ -175,6 +250,13 @@ const issueFoundSchema = z.object({
|
|
|
175
250
|
.describe("Issue severity. critical = feature broken/unusable (e.g. page doesn't load, data corruption). " +
|
|
176
251
|
"high = incorrect behavior (e.g. wrong calculation, stale data returned). " +
|
|
177
252
|
"medium = minor functional gap. low = cosmetic or informational."),
|
|
253
|
+
category: z
|
|
254
|
+
.nativeEnum(IssueFoundCategory)
|
|
255
|
+
.describe("Issue classification. bug = a product/code defect, e.g. found by a test or in the diff. " +
|
|
256
|
+
"lint = a linter or formatter finding (eslint, flake8, prettier). " +
|
|
257
|
+
"type = a type-check failure (tsc, mypy). " +
|
|
258
|
+
"config = environment or tooling misconfiguration (wrong workspace auth type, missing env var, setup command failure). " +
|
|
259
|
+
"The report renders lint/type/config entries in a separate 'Configuration Errors' section so product bugs stay prominent under 'Issues Found'."),
|
|
178
260
|
repository: repositoryField,
|
|
179
261
|
});
|
|
180
262
|
const scenarioStepSchema = z.object({
|
|
@@ -185,7 +267,7 @@ const scenarioStepSchema = z.object({
|
|
|
185
267
|
requestBody: z.record(z.any()).optional().describe("Example request body with realistic field values"),
|
|
186
268
|
responseBody: z.record(z.any()).optional().describe("Key response fields to verify, e.g. { id: 'number', name: 'string', in_stock: 'boolean?' }"),
|
|
187
269
|
});
|
|
188
|
-
export const additionalRecommendationSchema = z.object({
|
|
270
|
+
export const additionalRecommendationSchema = z.preprocess(stripNullGroundingFields, z.object({
|
|
189
271
|
testId: z.string().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."),
|
|
190
272
|
testType: z.nativeEnum(TestType).describe("Type of test. Do not include priority or other metadata in this field."),
|
|
191
273
|
category: z.preprocess((val) => externalCategory(val), z.enum(TEST_CATEGORIES)).describe("Test category — critical categories get generation priority over workflow"),
|
|
@@ -261,7 +343,7 @@ export const additionalRecommendationSchema = z.object({
|
|
|
261
343
|
}
|
|
262
344
|
}
|
|
263
345
|
}
|
|
264
|
-
});
|
|
346
|
+
}));
|
|
265
347
|
// TODO(multi-repo maintenance): no `repository` field yet — see readData() TODO below.
|
|
266
348
|
const testMaintenanceSchema = z.object({
|
|
267
349
|
testType: z.nativeEnum(TestType).describe("Type of test."),
|
|
@@ -366,6 +448,20 @@ async function attachReuseOutcome(test, outcomes) {
|
|
|
366
448
|
const reuse = await rederiveReuseOutcome(found);
|
|
367
449
|
return reuse ? { ...test, reuse } : test;
|
|
368
450
|
}
|
|
451
|
+
/**
|
|
452
|
+
* Attach the video recorded for this execution, matched by the row's testFilePath
|
|
453
|
+
* basename — the key skyramp_execute_test records under.
|
|
454
|
+
*
|
|
455
|
+
* Server-derived, never taken from the LLM, and the recorded value always wins: any
|
|
456
|
+
* videoPath the model supplied is discarded. That is the whole point of SKYR-4156 —
|
|
457
|
+
* the field used to be the agent's job, so it decayed to nothing on every row and
|
|
458
|
+
* testbot (which uploads only the video directories the report references) shipped
|
|
459
|
+
* no recordings at all. A row with no recorded execution gets no video, which
|
|
460
|
+
* consumers already treat as "no recording for this test".
|
|
461
|
+
*/
|
|
462
|
+
function attachVideoPath(row, videos) {
|
|
463
|
+
return { ...row, videoPath: videos?.[path.basename(row.testFilePath)]?.videoPath };
|
|
464
|
+
}
|
|
369
465
|
function deduplicateById(items) {
|
|
370
466
|
const seen = new Set();
|
|
371
467
|
const result = [];
|
|
@@ -387,16 +483,13 @@ export function registerSubmitReportTool(server) {
|
|
|
387
483
|
server.registerTool(TOOL_NAME, {
|
|
388
484
|
annotations: {
|
|
389
485
|
readOnlyHint: false,
|
|
390
|
-
destructiveHint: true, // overwrites
|
|
486
|
+
destructiveHint: true, // overwrites the report file on disk
|
|
391
487
|
idempotentHint: false,
|
|
392
488
|
openWorldHint: false,
|
|
393
489
|
},
|
|
394
490
|
description: "Submit the final testbot report. Call this tool once after completing all test analysis, generation, and execution. " +
|
|
395
491
|
"This is the ONLY way to submit the report — do NOT write the report to a file manually.",
|
|
396
492
|
inputSchema: {
|
|
397
|
-
summaryOutputFile: z
|
|
398
|
-
.string()
|
|
399
|
-
.describe("The file path where the report should be written (provided in the task instructions)"),
|
|
400
493
|
businessCaseAnalysis: z
|
|
401
494
|
.string()
|
|
402
495
|
.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.")
|
|
@@ -440,8 +533,9 @@ export function registerSubmitReportTool(server) {
|
|
|
440
533
|
.string()
|
|
441
534
|
.describe("Path to the state file returned by skyramp_analyze_changes. Required — " +
|
|
442
535
|
"contains execution results for maintenance scoring (existingTests), " +
|
|
443
|
-
"discovered endpoints for coverage validation (newEndpoints),
|
|
444
|
-
"UI page context for frontend test grounding (uiContext)
|
|
536
|
+
"discovered endpoints for coverage validation (newEndpoints), " +
|
|
537
|
+
"UI page context for frontend test grounding (uiContext), and the " +
|
|
538
|
+
"videos recorded for each executed browser test (executionVideos)."),
|
|
445
539
|
},
|
|
446
540
|
_meta: {
|
|
447
541
|
keywords: ["report", "summary", "testbot", "submit"],
|
|
@@ -449,6 +543,30 @@ export function registerSubmitReportTool(server) {
|
|
|
449
543
|
}, async (params) => {
|
|
450
544
|
const startTime = Date.now();
|
|
451
545
|
let errorResult;
|
|
546
|
+
// The report goes next to the state file, so in a Testbot run the state file has to
|
|
547
|
+
// be in the run directory — that is where the action reads the report back from.
|
|
548
|
+
// Two ways that breaks, both ending in a report nobody reads while this tool says
|
|
549
|
+
// it succeeded, which is the failure SKYR-4147 fixed: no run directory at all (the
|
|
550
|
+
// state file then falls back to a temp path of its own), or a readable but stale
|
|
551
|
+
// state file from somewhere else. Reject both rather than write. Checked only in a
|
|
552
|
+
// Testbot run; local and IDE use have no run directory and no consumer waiting.
|
|
553
|
+
if (isTestbotEnabled()) {
|
|
554
|
+
const runDir = runArtifactDir();
|
|
555
|
+
if (!runDir) {
|
|
556
|
+
return toolError("Cannot write the report: this is a Testbot run but RUNNER_TEMP is not set, so " +
|
|
557
|
+
"there is no run directory to write it to. The Testbot action reads the report " +
|
|
558
|
+
"from $RUNNER_TEMP/skyramp. Set RUNNER_TEMP for the run. Retrying or changing " +
|
|
559
|
+
"the report will not help — this is an environment problem.");
|
|
560
|
+
}
|
|
561
|
+
const stateDir = path.dirname(params.stateFile);
|
|
562
|
+
if (path.resolve(stateDir) !== path.resolve(runDir)) {
|
|
563
|
+
return toolError(`Cannot write the report: stateFile is in ${stateDir}, but this run's directory ` +
|
|
564
|
+
`is ${runDir}. The report is written next to the state file and the Testbot ` +
|
|
565
|
+
`action reads it from the run directory, so writing it would lose it. Pass the ` +
|
|
566
|
+
`stateFile from this run's skyramp_analyze_changes output — not a path from an ` +
|
|
567
|
+
`earlier run or one typed by hand.`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
452
570
|
const dedupedNewTests = deduplicateById([...params.newTestsCreated]);
|
|
453
571
|
const dedupedRecommendations = deduplicateById([...(params.additionalRecommendations ?? [])]);
|
|
454
572
|
const stateManager = StateManager.fromStatePath(params.stateFile);
|
|
@@ -483,17 +601,31 @@ export function registerSubmitReportTool(server) {
|
|
|
483
601
|
// is non-empty), so a generation the gate permitted is never rejected at
|
|
484
602
|
// report time. Absent approvedPlan (register tool never ran / never
|
|
485
603
|
// pre-seeded anything) → no check, back-compat.
|
|
604
|
+
//
|
|
605
|
+
// SKYR-4123: the name must be part of the query for that parity to hold.
|
|
606
|
+
// Every generation tool passes scenarioName; this side did not, so the plan
|
|
607
|
+
// item's `name:` key was dead here and a test the gate had already let
|
|
608
|
+
// through was rejected while being listed as approved in the same message.
|
|
486
609
|
if (stateData.approvedPlan) {
|
|
487
610
|
const approvedPlan = stateData.approvedPlan;
|
|
488
611
|
const unapproved = dedupedNewTests.filter((t) => {
|
|
489
612
|
if (!PLAN_GATED_TEST_TYPES.has(t.testType))
|
|
490
613
|
return false;
|
|
491
|
-
const
|
|
492
|
-
|
|
614
|
+
const endpoints = parseEndpointField(t.endpoint);
|
|
615
|
+
const names = planNameCandidates(t.testId, t.testType);
|
|
616
|
+
return !(names.length > 0 ? names : [undefined]).some((scenarioName) => endpoints.some(({ method, path }) => matchesApprovedPlan(approvedPlan, { scenarioName, testType: t.testType, method, path })));
|
|
493
617
|
});
|
|
494
618
|
if (unapproved.length > 0) {
|
|
495
619
|
const approvedList = approvedPlan.generate.length > 0
|
|
496
|
-
|
|
620
|
+
// Show each item's endpoint keys: without them a rejection that turns on
|
|
621
|
+
// the endpoint reads as self-contradicting, because the entry's own name
|
|
622
|
+
// is printed in this same list (SKYR-4123).
|
|
623
|
+
? approvedPlan.generate
|
|
624
|
+
.map((item) => {
|
|
625
|
+
const eps = item.matchKeys.filter((k) => k.startsWith("ep:"));
|
|
626
|
+
return `[${item.testType}] ${item.scenarioName}${eps.length > 0 ? ` covering ${eps.join(", ")}` : ""}`;
|
|
627
|
+
})
|
|
628
|
+
.join("; ")
|
|
497
629
|
: "(none)";
|
|
498
630
|
errorResult = toolError(`${unapproved.length} newTestsCreated entr${unapproved.length === 1 ? "y" : "ies"} not in the approved plan from ` +
|
|
499
631
|
`skyramp_register_test_plan (plan ${approvedPlan.planId}) — neither its GENERATE list nor its ADDITIONAL backfill pool: ` +
|
|
@@ -591,9 +723,18 @@ export function registerSubmitReportTool(server) {
|
|
|
591
723
|
// without it, the cheapest compliant move is a token no-op edit to the
|
|
592
724
|
// mis-claimed file, which pollutes the delivered branch and leaves the report
|
|
593
725
|
// crediting the wrong file (observed in the Ghost rc.pom-3 tester run, D-04).
|
|
726
|
+
// The two claim kinds have DIFFERENT remedies and must say so. newTestsCreated
|
|
727
|
+
// is a tool input, so it can be corrected here. A testMaintenance row is built
|
|
728
|
+
// from stateData.maintenanceVerdicts and cannot be — saying so stops the agent
|
|
729
|
+
// permuting testMaintenanceDetails, which is what it did for 15 rounds in
|
|
730
|
+
// SKYR-4129. Do NOT offer re-running skyramp_actions to restate the verdict:
|
|
731
|
+
// this branch only fires when the edit is genuinely absent, so rewriting the
|
|
732
|
+
// record to match that would be the tamper path, not the fix.
|
|
594
733
|
const changedList = changedFiles.slice(0, 20).map((f) => ` - ${f}`).join("\n");
|
|
595
734
|
errorResult = toolError(`${unbacked.length} report claim(s) are not backed by any change in the working tree — ` +
|
|
596
|
-
`Testbot will not report file work that hasn't actually been made.
|
|
735
|
+
`Testbot will not report file work that hasn't actually been made. Do NOT make a token edit to the claimed file just to satisfy this check.\n` +
|
|
736
|
+
`For a newTestsCreated claim: create the file, correct the claim's fileName to the file you actually created (see the changed files below), or remove the claim.\n` +
|
|
737
|
+
`For a testMaintenance claim: make the edit on the named file. That row is derived from the maintenance triage, not from this tool's input, so it cannot be corrected by changing what you pass here.\n` +
|
|
597
738
|
`Unbacked claims:\n${unbacked.map((u) => ` - ${u}`).join("\n")}\n` +
|
|
598
739
|
`Files with actual working-tree changes:\n${changedList || " (none)"}`);
|
|
599
740
|
return errorResult;
|
|
@@ -624,30 +765,39 @@ export function registerSubmitReportTool(server) {
|
|
|
624
765
|
...row,
|
|
625
766
|
fileName: path.basename(testFilePath),
|
|
626
767
|
})),
|
|
627
|
-
|
|
768
|
+
// videoPath is filled from the run's execution records; testFilePath is the
|
|
769
|
+
// match-only key and is stripped from the wire format, the same line drawn for
|
|
770
|
+
// testMaintenance's own testFilePath above (downstream scoring scripts traverse
|
|
771
|
+
// these objects and don't expect it).
|
|
772
|
+
testResults: params.testResults.map((row) => {
|
|
773
|
+
const { testFilePath: _tfp, ...wire } = attachVideoPath(normalizeRepository(row), stateData.executionVideos);
|
|
774
|
+
return wire;
|
|
775
|
+
}),
|
|
628
776
|
issuesFound: params.issuesFound.map(normalizeRepository),
|
|
629
777
|
nextSteps: params.nextSteps ?? [],
|
|
630
778
|
commitMessage: (params.commitMessage ?? "").replace(/[\r\n]+/g, " ").trim() || DEFAULT_COMMIT_MESSAGE,
|
|
631
779
|
};
|
|
632
780
|
const reportJson = JSON.stringify(report, null, 2);
|
|
781
|
+
// Beside the state file, which was read successfully above — so this directory is
|
|
782
|
+
// known to exist and no mkdir is needed. See REPORT_FILE_NAME.
|
|
783
|
+
const reportPath = path.join(path.dirname(params.stateFile), REPORT_FILE_NAME);
|
|
633
784
|
logger.info("Submitting testbot report", {
|
|
634
|
-
outputFile:
|
|
785
|
+
outputFile: reportPath,
|
|
635
786
|
payloadBytes: reportJson.length,
|
|
636
787
|
testResultCount: params.testResults.length,
|
|
637
788
|
});
|
|
638
789
|
try {
|
|
639
|
-
await fs.
|
|
640
|
-
await fs.writeFile(params.summaryOutputFile, reportJson, "utf-8");
|
|
790
|
+
await fs.writeFile(reportPath, reportJson, "utf-8");
|
|
641
791
|
const elapsed = Date.now() - startTime;
|
|
642
792
|
logger.info("Testbot report written successfully", {
|
|
643
|
-
outputFile:
|
|
793
|
+
outputFile: reportPath,
|
|
644
794
|
elapsedMs: elapsed,
|
|
645
795
|
});
|
|
646
796
|
return {
|
|
647
797
|
content: [
|
|
648
798
|
{
|
|
649
799
|
type: "text",
|
|
650
|
-
text: `Report submitted successfully to ${
|
|
800
|
+
text: `Report submitted successfully to ${reportPath}`,
|
|
651
801
|
},
|
|
652
802
|
],
|
|
653
803
|
};
|
|
@@ -661,7 +811,7 @@ export function registerSubmitReportTool(server) {
|
|
|
661
811
|
}
|
|
662
812
|
finally {
|
|
663
813
|
AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, {
|
|
664
|
-
summary_output_file:
|
|
814
|
+
summary_output_file: reportPath,
|
|
665
815
|
testResultCount: String(params.testResults.length),
|
|
666
816
|
payloadBytes: String(reportJson.length),
|
|
667
817
|
...computeReportMetrics({ ...params, testMaintenance }),
|
|
@@ -6,7 +6,7 @@ import { TestType } from "../../types/TestTypes.js";
|
|
|
6
6
|
import * as fs from "fs";
|
|
7
7
|
import * as path from "path";
|
|
8
8
|
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
9
|
-
import { toolError } from "../../utils/utils.js";
|
|
9
|
+
import { dualChannelResult, toolError } from "../../utils/utils.js";
|
|
10
10
|
import { isTestFile } from "../../prompts/test-recommendation/scopeAssessment.js";
|
|
11
11
|
import { buildRenameStrategy, buildFileRenameStrategy, buildUpdateStrategy, buildRegenerateStrategy, buildDeleteStrategy, buildUpdateFileInstruction, buildRegenerateFileInstruction, } from "../../prompts/test-maintenance/actionsInstructions.js";
|
|
12
12
|
/**
|
|
@@ -136,6 +136,16 @@ export function registerActionsTool(server) {
|
|
|
136
136
|
openWorldHint: true,
|
|
137
137
|
},
|
|
138
138
|
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.`,
|
|
139
|
+
// SKYR-4194: the payload is returned via structuredContent, which requires a
|
|
140
|
+
// declared outputSchema — see dualChannelResult.
|
|
141
|
+
outputSchema: {
|
|
142
|
+
summary: z
|
|
143
|
+
.string()
|
|
144
|
+
.describe("Human-readable report of the maintenance actions to take."),
|
|
145
|
+
instructions: z
|
|
146
|
+
.string()
|
|
147
|
+
.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."),
|
|
148
|
+
},
|
|
139
149
|
inputSchema: actionsSchema,
|
|
140
150
|
}, async (args) => {
|
|
141
151
|
let errorResult;
|
|
@@ -166,6 +176,7 @@ export function registerActionsTool(server) {
|
|
|
166
176
|
const externalTestFiles = new Set(testAnalysisResults
|
|
167
177
|
.filter((t) => t.source === TestSource.External)
|
|
168
178
|
.map((t) => t.testFile));
|
|
179
|
+
const catalogByFile = new Map(testAnalysisResults.map((t) => [t.testFile, t]));
|
|
169
180
|
// ── Build recommendations from LLM-supplied drift assessment ──
|
|
170
181
|
// The LLM performs the drift assessment in context after skyramp_analyze_test_health
|
|
171
182
|
// and passes results here directly — analyzeTestHealthTool never writes assessment
|
|
@@ -265,14 +276,20 @@ export function registerActionsTool(server) {
|
|
|
265
276
|
});
|
|
266
277
|
return;
|
|
267
278
|
}
|
|
268
|
-
// Resolve and validate pomFile with the same path-traversal guard as testFilePath
|
|
279
|
+
// Resolve and validate pomFile with the same path-traversal guard as testFilePath,
|
|
280
|
+
// minus that guard's `=== repoRoot` allowance: the repo root is a directory, never
|
|
281
|
+
// a page object. Admitting it would make it the edit target, and every consumer
|
|
282
|
+
// that resolves it relative to repoRoot gets the empty string — which the report's
|
|
283
|
+
// working-tree check reads as "outside this checkout, exempt", waving through an
|
|
284
|
+
// UPDATE that changed nothing. Dropping it here means pomFile falls back to the
|
|
285
|
+
// spec (`pomFile ?? testFilePath`) for both the edit and the check.
|
|
269
286
|
let resolvedPomFile;
|
|
270
287
|
if (rec.pomFile) {
|
|
271
288
|
const rawPom = path.isAbsolute(rec.pomFile)
|
|
272
289
|
? rec.pomFile
|
|
273
290
|
: path.resolve(repoRoot ?? "", rec.pomFile);
|
|
274
291
|
resolvedPomFile = path.resolve(rawPom);
|
|
275
|
-
if (repoRoot && !resolvedPomFile.startsWith(repoRoot + path.sep)
|
|
292
|
+
if (repoRoot && !resolvedPomFile.startsWith(repoRoot + path.sep)) {
|
|
276
293
|
logger.warning(`Skipping pomFile outside repo root: ${rec.pomFile}`);
|
|
277
294
|
resolvedPomFile = undefined;
|
|
278
295
|
}
|
|
@@ -315,6 +332,10 @@ export function registerActionsTool(server) {
|
|
|
315
332
|
ignoredTestFiles,
|
|
316
333
|
maintenanceVerdicts: recommendations.map((r) => ({
|
|
317
334
|
testFilePath: r.testFilePath,
|
|
335
|
+
// Persisted because the edit lands here, not in testFilePath. Dropping it
|
|
336
|
+
// left skyramp_submit_report unable to tell a POM-backed UPDATE from a
|
|
337
|
+
// fabricated one, so it rejected real maintenance work (SKYR-4129).
|
|
338
|
+
pomFile: r.pomFile,
|
|
318
339
|
action: r.action,
|
|
319
340
|
rationale: r.rationale,
|
|
320
341
|
testType: r.testType,
|
|
@@ -393,8 +414,7 @@ export function registerActionsTool(server) {
|
|
|
393
414
|
const testFilesToDelete = deleteRecommendations.map((rec) => rec.testFilePath);
|
|
394
415
|
const fileInstructions = [];
|
|
395
416
|
const testFilesToUpdate = [];
|
|
396
|
-
|
|
397
|
-
// ── UPDATE: read file, emit targeted edit instructions ──
|
|
417
|
+
// ── UPDATE: emit targeted edit instructions ──
|
|
398
418
|
for (const rec of updateRecommendations) {
|
|
399
419
|
if (!rec.testFilePath) {
|
|
400
420
|
logger.warning("Recommendation missing testFilePath", rec);
|
|
@@ -404,11 +424,12 @@ export function registerActionsTool(server) {
|
|
|
404
424
|
// rather than the spec. testFilePath (the spec) remains in the report.
|
|
405
425
|
const editTarget = rec.pomFile ?? rec.testFilePath;
|
|
406
426
|
testFilesToUpdate.push(editTarget);
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
427
|
+
// Existence check only — the agent reads the file itself before editing.
|
|
428
|
+
// Skips this file's instruction block exactly as the readFileSync failure did
|
|
429
|
+
// before. The path still reaches files_to_update and update_context; that was
|
|
430
|
+
// true before this change too.
|
|
431
|
+
if (!fs.existsSync(editTarget)) {
|
|
432
|
+
logger.error(`Missing ${rec.pomFile ? "page object" : "test"} file: ${editTarget}`);
|
|
412
433
|
continue;
|
|
413
434
|
}
|
|
414
435
|
const renames = rec.renamedEndpoints || [];
|
|
@@ -430,22 +451,15 @@ export function registerActionsTool(server) {
|
|
|
430
451
|
rationale: rec.rationale,
|
|
431
452
|
}));
|
|
432
453
|
}
|
|
433
|
-
// ── REGENERATE:
|
|
454
|
+
// ── REGENERATE: emit overwrite instructions ──
|
|
434
455
|
const regenerateInstructions = [];
|
|
435
456
|
const testFilesToRegenerate = [];
|
|
436
|
-
const regenerateContentMap = new Map();
|
|
437
457
|
for (const rec of regenerateRecommendations) {
|
|
438
458
|
if (!rec.testFilePath) {
|
|
439
459
|
logger.warning("Recommendation missing testFilePath", rec);
|
|
440
460
|
continue;
|
|
441
461
|
}
|
|
442
462
|
testFilesToRegenerate.push(rec.testFilePath);
|
|
443
|
-
try {
|
|
444
|
-
regenerateContentMap.set(rec.testFilePath, fs.readFileSync(rec.testFilePath, "utf-8"));
|
|
445
|
-
}
|
|
446
|
-
catch (error) {
|
|
447
|
-
logger.warning(`Could not read file for REGENERATE context ${rec.testFilePath}: ${error.message}`);
|
|
448
|
-
}
|
|
449
463
|
regenerateInstructions.push(buildRegenerateFileInstruction({
|
|
450
464
|
testFile: rec.testFilePath,
|
|
451
465
|
updateInstructions: rec.updateInstructions,
|
|
@@ -523,12 +537,10 @@ export function registerActionsTool(server) {
|
|
|
523
537
|
.filter((rec) => !!rec.updateInstructions)
|
|
524
538
|
.map((rec) => {
|
|
525
539
|
const editTarget = rec.pomFile ?? rec.testFilePath;
|
|
526
|
-
const current_content = testFileContentMap.get(editTarget);
|
|
527
540
|
return {
|
|
528
541
|
file: editTarget,
|
|
529
542
|
context: rec.updateInstructions,
|
|
530
543
|
...(rec.editSites?.length ? { edit_sites: rec.editSites } : {}),
|
|
531
|
-
...(current_content !== undefined && { current_content }),
|
|
532
544
|
};
|
|
533
545
|
});
|
|
534
546
|
if (updateContext.length > 0) {
|
|
@@ -539,8 +551,22 @@ export function registerActionsTool(server) {
|
|
|
539
551
|
// endpoint URL, auth pattern, test type, and language to replicate.
|
|
540
552
|
if (regenerateRecommendations.length > 0) {
|
|
541
553
|
llmInstructionsObj.regenerate_context = regenerateRecommendations.map((rec) => {
|
|
542
|
-
|
|
543
|
-
|
|
554
|
+
// The generation tool needs test type, language, framework and endpoint to
|
|
555
|
+
// replicate the file. Those come from the discovery catalog, so the file
|
|
556
|
+
// text is not needed to carry them.
|
|
557
|
+
const cataloged = catalogByFile.get(rec.testFilePath);
|
|
558
|
+
return {
|
|
559
|
+
file: rec.testFilePath,
|
|
560
|
+
rationale: rec.updateInstructions || rec.rationale,
|
|
561
|
+
// test_type and endpoint are always emitted — the strategy tells the agent to
|
|
562
|
+
// use them instead of reading the file, so an absent one leaves the generation
|
|
563
|
+
// call without a target. The catalog's apiEndpoint is optional and can be an
|
|
564
|
+
// empty string; both fields are required on the recommendation itself.
|
|
565
|
+
test_type: cataloged?.testType ?? rec.testType,
|
|
566
|
+
endpoint: cataloged?.apiEndpoint || rec.endpoint,
|
|
567
|
+
...(cataloged?.language && { language: cataloged.language }),
|
|
568
|
+
...(cataloged?.framework && { framework: cataloged.framework }),
|
|
569
|
+
};
|
|
544
570
|
});
|
|
545
571
|
llmInstructionsObj.regenerate_strategy = buildRegenerateStrategy();
|
|
546
572
|
}
|
|
@@ -548,25 +574,10 @@ export function registerActionsTool(server) {
|
|
|
548
574
|
llmInstructionsObj.delete_strategy = buildDeleteStrategy();
|
|
549
575
|
}
|
|
550
576
|
const llmInstructions = JSON.stringify(llmInstructionsObj, null, 2);
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
annotations: {
|
|
556
|
-
audience: ["user"],
|
|
557
|
-
},
|
|
558
|
-
},
|
|
559
|
-
{
|
|
560
|
-
type: "text",
|
|
561
|
-
text: llmInstructions,
|
|
562
|
-
annotations: {
|
|
563
|
-
audience: ["assistant"],
|
|
564
|
-
},
|
|
565
|
-
},
|
|
566
|
-
];
|
|
567
|
-
return {
|
|
568
|
-
content: contentBlocks,
|
|
569
|
-
};
|
|
577
|
+
// The two payloads were previously separate content[] blocks annotated
|
|
578
|
+
// audience: ["user"] and ["assistant"]. Nothing in this repo read those
|
|
579
|
+
// annotations, and the field names carry the same distinction.
|
|
580
|
+
return dualChannelResult({ summary: responseText, instructions: llmInstructions });
|
|
570
581
|
}
|
|
571
582
|
catch (error) {
|
|
572
583
|
logger.error(`Actions tool failed: ${error.message}`, error);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
4
|
import { CandidateUiPage } from "../../utils/uiPageEnumerator.js";
|
|
4
5
|
import type { FrontendFileIntegration } from "../../types/FrontendIntegration.js";
|
|
5
6
|
import { TraceFile } from "../../types/RepositoryAnalysis.js";
|
|
@@ -34,4 +35,14 @@ export declare function buildCaptureInstructions(pages: CandidateUiPage[]): stri
|
|
|
34
35
|
* (`buildCaptureInstructions` still runs for the rest in that case).
|
|
35
36
|
*/
|
|
36
37
|
export declare function buildUnintegratedInstructions(results: FrontendFileIntegration[]): string;
|
|
38
|
+
/**
|
|
39
|
+
* Assemble the tool result. See `dualChannelResult` for why the payload is
|
|
40
|
+
* carried on both result channels.
|
|
41
|
+
*/
|
|
42
|
+
export declare function buildAnalyzeChangesResult(parts: {
|
|
43
|
+
structuredSummary: string;
|
|
44
|
+
uiInstructions: string;
|
|
45
|
+
outputText: string;
|
|
46
|
+
recommendationPrompt: string;
|
|
47
|
+
}): CallToolResult;
|
|
37
48
|
export declare function registerAnalyzeChangesTool(server: McpServer): void;
|