@skyramp/mcp 0.3.8 → 0.4.0-rc.1
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/commands/commandLibrary.d.ts +1 -1
- package/build/commands/commandLibrary.js +3 -3
- package/build/commands/recommendTestsAndExecuteCommand.d.ts +1 -1
- package/build/commands/recommendTestsAndExecuteCommand.js +35 -20
- package/build/commands/testThisEndpointCommand.js +35 -19
- package/build/index.js +9 -3
- package/build/playwright/blueprintDigest.d.ts +15 -0
- package/build/playwright/blueprintDigest.js +152 -0
- package/build/playwright/blueprintDigestStore.d.ts +31 -0
- package/build/playwright/blueprintDigestStore.js +117 -0
- package/build/playwright/registerPlaywrightTools.js +60 -12
- package/build/playwright/traceRecordingPrompt.js +8 -7
- package/build/prompts/enhance-assertions/sharedAssertionRules.js +9 -8
- package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +24 -2
- package/build/prompts/promptAssets.d.ts +20 -0
- package/build/prompts/promptAssets.js +55 -0
- package/build/prompts/sut-setup/modes/dockerComposePrompt.js +19 -5
- package/build/prompts/test-maintenance/actionsInstructions.d.ts +4 -0
- package/build/prompts/test-maintenance/actionsInstructions.js +14 -2
- package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +0 -10
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +2 -11
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +8 -4
- package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +5 -22
- package/build/prompts/test-recommendation/diffExecutionPlan.js +37 -465
- package/build/prompts/test-recommendation/recommendationSections.d.ts +7 -17
- package/build/prompts/test-recommendation/recommendationSections.js +67 -309
- package/build/prompts/test-recommendation/recommendationShared.d.ts +19 -47
- package/build/prompts/test-recommendation/recommendationShared.js +49 -155
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.d.ts +0 -5
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +10 -153
- package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +2 -29
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +32 -457
- package/build/prompts/testbot/planDeclarations.d.ts +6 -0
- package/build/prompts/testbot/planDeclarations.js +9 -0
- package/build/prompts/testbot/testbot-prompts.d.ts +8 -0
- package/build/prompts/testbot/testbot-prompts.js +256 -381
- package/build/recommendation/answers.d.ts +35 -0
- package/build/recommendation/answers.js +96 -0
- package/build/recommendation/registerPlan.d.ts +49 -0
- package/build/recommendation/registerPlan.js +117 -0
- package/build/recommendation/runVerifiers.d.ts +10 -0
- package/build/recommendation/runVerifiers.js +49 -0
- package/build/recommendation/subjectStep.d.ts +42 -0
- package/build/recommendation/subjectStep.js +86 -0
- package/build/recommendation/types.d.ts +163 -0
- package/build/recommendation/types.js +20 -0
- package/build/recommendation/verifierContracts.d.ts +382 -0
- package/build/recommendation/verifierContracts.js +263 -0
- package/build/recommendation/verifiers/changedFile.d.ts +2 -0
- package/build/recommendation/verifiers/changedFile.js +82 -0
- package/build/recommendation/verifiers/citedPath.d.ts +12 -0
- package/build/recommendation/verifiers/citedPath.js +35 -0
- package/build/recommendation/verifiers/coverage.d.ts +7 -0
- package/build/recommendation/verifiers/coverage.js +617 -0
- package/build/recommendation/verifiers/deliveredMatchesPlan.d.ts +11 -0
- package/build/recommendation/verifiers/deliveredMatchesPlan.js +33 -0
- package/build/recommendation/verifiers/endpointGrounded.d.ts +17 -0
- package/build/recommendation/verifiers/endpointGrounded.js +128 -0
- package/build/recommendation/verifiers/existingCoverage.d.ts +6 -0
- package/build/recommendation/verifiers/existingCoverage.js +51 -0
- package/build/recommendation/verifiers/expectedOutcome.d.ts +31 -0
- package/build/recommendation/verifiers/expectedOutcome.js +105 -0
- package/build/recommendation/verifiers/removedElementGuarded.d.ts +2 -0
- package/build/recommendation/verifiers/removedElementGuarded.js +57 -0
- package/build/recommendation/verifiers/reportedCategory.d.ts +26 -0
- package/build/recommendation/verifiers/reportedCategory.js +84 -0
- package/build/recommendation/verifiers/screenRoute.d.ts +10 -0
- package/build/recommendation/verifiers/screenRoute.js +118 -0
- package/build/recommendation/verifiers/statedDifference.d.ts +6 -0
- package/build/recommendation/verifiers/statedDifference.js +140 -0
- package/build/recommendation/verifiers/uiElementGrounded.d.ts +7 -0
- package/build/recommendation/verifiers/uiElementGrounded.js +318 -0
- package/build/resources/analysisResources.js +1 -114
- package/build/resources/testbotResource.js +23 -13
- package/build/services/ModularizationService.js +2 -1
- package/build/services/TestDiscoveryService.d.ts +3 -72
- package/build/services/TestDiscoveryService.js +10 -303
- package/build/services/containerEnv.d.ts +1 -1
- package/build/services/containerEnv.js +12 -0
- package/build/skills/fixTestImportErrorsSkill.d.ts +13 -0
- package/build/skills/fixTestImportErrorsSkill.js +20 -0
- package/build/toolNames.d.ts +1 -0
- package/build/toolNames.js +1 -0
- package/build/tools/code-refactor/enhanceAssertionsTool.js +3 -3
- package/build/tools/code-refactor/modularizationTool.js +2 -1
- package/build/tools/executeSkyrampTestTool.d.ts +80 -0
- package/build/tools/executeSkyrampTestTool.js +246 -19
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +6 -0
- package/build/tools/generate-tests/generateContractRestTool.js +3 -3
- package/build/tools/generate-tests/planGuard.d.ts +2 -2
- package/build/tools/generate-tests/planGuard.js +78 -18
- package/build/tools/one-click/oneClickTool.d.ts +0 -1
- package/build/tools/one-click/oneClickTool.js +0 -5
- package/build/tools/submitReportTool.d.ts +48 -42
- package/build/tools/submitReportTool.js +576 -193
- package/build/tools/test-management/actionsTool.js +72 -4
- package/build/tools/test-management/analyzeChangesTool.d.ts +144 -48
- package/build/tools/test-management/analyzeChangesTool.js +212 -1219
- package/build/tools/test-management/analyzeTestHealthTool.js +13 -24
- package/build/tools/test-management/index.d.ts +1 -0
- package/build/tools/test-management/index.js +1 -0
- package/build/tools/test-management/registerTestPlanTool.d.ts +795 -172
- package/build/tools/test-management/registerTestPlanTool.js +609 -542
- package/build/tools/test-management/resolveScreenTool.d.ts +75 -0
- package/build/tools/test-management/resolveScreenTool.js +289 -0
- package/build/types/BlueprintDigest.d.ts +34 -0
- package/build/types/BlueprintDigest.js +1 -0
- package/build/types/RepositoryAnalysis.d.ts +20 -1559
- package/build/types/RepositoryAnalysis.js +2 -58
- package/build/types/StepMethod.d.ts +40 -0
- package/build/types/StepMethod.js +77 -0
- package/build/types/TestAnalysis.d.ts +12 -0
- package/build/types/TestExecution.d.ts +4 -0
- package/build/types/TestRecommendation.d.ts +24 -24
- package/build/types/TestRecommendation.js +91 -89
- package/build/types/TestbotPromptOptions.d.ts +0 -4
- package/build/types/TestbotReport.d.ts +64 -2
- package/build/utils/AnalysisStateManager.d.ts +79 -113
- package/build/utils/AnalysisStateManager.js +147 -57
- package/build/utils/assertion-verify/api-shared-lints.js +1 -1
- package/build/utils/assertion-verify/metrics.js +85 -36
- package/build/utils/assertion-verify/ui-lints.d.ts +0 -5
- package/build/utils/assertion-verify/ui-lints.js +32 -0
- package/build/utils/branchDiff.d.ts +63 -31
- package/build/utils/branchDiff.js +242 -94
- package/build/utils/containedPath.d.ts +18 -0
- package/build/utils/containedPath.js +73 -0
- package/build/utils/dartRouteExtractor.d.ts +18 -34
- package/build/utils/dartRouteExtractor.js +101 -173
- package/build/utils/featureFlags.d.ts +12 -0
- package/build/utils/featureFlags.js +14 -0
- package/build/utils/frontendSelectors.d.ts +48 -27
- package/build/utils/frontendSelectors.js +241 -80
- package/build/utils/pathMatching.d.ts +2 -4
- package/build/utils/pathMatching.js +2 -4
- package/build/utils/planMatchKeys.d.ts +38 -47
- package/build/utils/planMatchKeys.js +143 -81
- package/build/utils/rebaselineSnapshots.d.ts +24 -0
- package/build/utils/rebaselineSnapshots.js +65 -0
- package/build/utils/removedUiElements.d.ts +22 -0
- package/build/utils/removedUiElements.js +106 -0
- package/build/utils/reportVerification.d.ts +2 -6
- package/build/utils/reportVerification.js +61 -2
- package/build/utils/screenRoutes.d.ts +66 -0
- package/build/utils/screenRoutes.js +727 -0
- package/build/utils/sourceRouteExtractor.js +320 -112
- package/build/utils/testFileClassification.d.ts +11 -2
- package/build/utils/testFileClassification.js +44 -2
- package/build/utils/testFixtures.d.ts +5 -0
- package/build/utils/testFixtures.js +13 -0
- package/build/utils/utils.d.ts +0 -1
- package/build/utils/utils.js +0 -11
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/workspace/workspace.d.ts +12 -12
- package/node_modules/playwright/lib/mcp/skyramp/assertHiddenTool.js +56 -0
- package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +2 -1
- package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +10 -0
- package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +4 -1
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +160 -1
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +4 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +1 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderSignalProcessor.js +2 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder.js +5 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.-Id052Lr.js → index.B7KbSQcC.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +1 -1
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderSignalProcessor.ts +7 -0
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder.ts +6 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +4 -3
- package/plugin/.claude-plugin/plugin.json +8 -0
- package/plugin/plugin.json +6 -0
- package/plugin/prompts/declaring-a-plan.md +20 -0
- package/plugin/prompts/generate-tests/context-fetching.md +4 -0
- package/plugin/prompts/generate-tests/execution-plan.md +63 -0
- package/plugin/prompts/generate-tests/generation.md +108 -0
- package/plugin/prompts/generate-tests/path-parameters.md +1 -0
- package/plugin/prompts/generate-tests/reasoning-protocol.md +17 -0
- package/plugin/prompts/generate-tests/tool-workflow-variants.md +61 -0
- package/plugin/prompts/generate-tests/tool-workflows.md +65 -0
- package/plugin/prompts/plan-tests.md +42 -0
- package/plugin/prompts/testbot-task1.md +82 -0
- package/plugin/skills/fix-test-import-errors/SKILL.md +98 -0
- package/build/prompts/test-recommendation/analysisOutputPrompt.d.ts +0 -84
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +0 -369
- package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +0 -7
- package/build/prompts/test-recommendation/fullRepoCatalog.js +0 -283
- package/build/prompts/test-recommendation/scopeAssessment.d.ts +0 -81
- package/build/prompts/test-recommendation/scopeAssessment.js +0 -359
- package/build/recommendation/budgeters/diversityBalancedBudgeter.d.ts +0 -7
- package/build/recommendation/budgeters/diversityBalancedBudgeter.js +0 -105
- package/build/recommendation/budgeters/fixedNBudgeter.d.ts +0 -7
- package/build/recommendation/budgeters/fixedNBudgeter.js +0 -11
- package/build/recommendation/budgeters/shared.d.ts +0 -32
- package/build/recommendation/budgeters/shared.js +0 -246
- package/build/recommendation/discriminators.d.ts +0 -37
- package/build/recommendation/discriminators.js +0 -379
- package/build/recommendation/diversity.d.ts +0 -47
- package/build/recommendation/diversity.js +0 -101
- package/build/recommendation/planRanker.d.ts +0 -65
- package/build/recommendation/planRanker.js +0 -83
- package/build/recommendation/testFixtures.d.ts +0 -25
- package/build/recommendation/testFixtures.js +0 -45
- package/build/types/FrontendIntegration.d.ts +0 -28
- package/build/types/FrontendIntegration.js +0 -22
- package/build/types/Recommendation.d.ts +0 -146
- package/build/types/Recommendation.js +0 -74
- package/build/utils/changedRoutes.d.ts +0 -29
- package/build/utils/changedRoutes.js +0 -87
- package/build/utils/frontendIntegration.d.ts +0 -9
- package/build/utils/frontendIntegration.js +0 -243
- package/build/utils/importerHop.d.ts +0 -135
- package/build/utils/importerHop.js +0 -489
- package/build/utils/pathAffinityClassification.d.ts +0 -49
- package/build/utils/pathAffinityClassification.js +0 -180
- package/build/utils/pythonMountPrefixes.d.ts +0 -25
- package/build/utils/pythonMountPrefixes.js +0 -347
- package/build/utils/repoScanner.d.ts +0 -34
- package/build/utils/repoScanner.js +0 -300
- package/build/utils/routeParsers.d.ts +0 -95
- package/build/utils/routeParsers.js +0 -951
- package/build/utils/scenarioDrafting.d.ts +0 -92
- package/build/utils/scenarioDrafting.js +0 -951
- package/build/utils/subjectEndpoints.d.ts +0 -19
- package/build/utils/subjectEndpoints.js +0 -98
- package/build/utils/uiPageEnumerator.d.ts +0 -172
- package/build/utils/uiPageEnumerator.js +0 -474
|
@@ -1,16 +1,12 @@
|
|
|
1
|
-
import * as crypto from "crypto";
|
|
2
1
|
import { AnalysisScope, isDiff, } from "../../types/RepositoryAnalysis.js";
|
|
3
2
|
import { WorkspaceAuthType, getDefaultAuthHeader } from "../../utils/workspaceAuth.js";
|
|
4
3
|
import { logger } from "../../utils/logger.js";
|
|
5
|
-
import { buildArchitectPreamble, buildContextFetchingGuidance, buildReasoningProtocol, buildToolWorkflows,
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
import { TASK_GENERATE, buildExternalCoverageSet, externalDedupKeys, isAttackSurfaceSecurityBoundary, taskRef, } from "./recommendationShared.js";
|
|
12
|
-
// Re-export for backward compatibility (tests and external callers import these from this module)
|
|
13
|
-
export { buildExternalCoverageSet, externalDedupKeys };
|
|
4
|
+
import { buildArchitectPreamble, buildContextFetchingGuidance, buildReasoningProtocol, buildToolWorkflows, buildVerificationChecklist, } from "./recommendationSections.js";
|
|
5
|
+
import { buildExecutionPlan, EXEC_STEP_ENRICH } from "./diffExecutionPlan.js";
|
|
6
|
+
import { readPromptAsset } from "../promptAssets.js";
|
|
7
|
+
import { renderPlanDeclarationGuidance } from "../testbot/planDeclarations.js";
|
|
8
|
+
import { TASK_GENERATE, taskRef, } from "./recommendationShared.js";
|
|
9
|
+
// Re-export for backward compatibility (tests and external callers import this from this module)
|
|
14
10
|
function formatTestLocations(locs) {
|
|
15
11
|
const entries = Object.entries(locs || {});
|
|
16
12
|
if (entries.length === 0)
|
|
@@ -19,218 +15,28 @@ function formatTestLocations(locs) {
|
|
|
19
15
|
return ("\n**Existing test coverage (Skyramp + external):**\n" +
|
|
20
16
|
"| Test type | File (covers: endpoints) |\n" +
|
|
21
17
|
"|--------------|---------------------------------------------------------|\n" +
|
|
22
|
-
rows + "\n
|
|
23
|
-
"**Deduplication rule (apply this table before generating anything):**\n" +
|
|
24
|
-
"- `[external]` tests: if a resource is covered by an `[external]` test, do NOT create a new parallel test for the same HTTP method + resource + test type. These tests still break when the API changes — Task 1 maintenance applies to them the same as Skyramp tests (in-place UPDATE only; do not regenerate or delete).\n" +
|
|
25
|
-
"- `[skyramp]` contract test: if the HTTP method + path already appears in a `[skyramp]` `covers:` entry of type `contract` → UPDATE that file, do NOT create a new one.\n" +
|
|
26
|
-
"- `[skyramp]` integration test: use the endpoints this PR changed. A setup step or a cleanup step is not an endpoint under test. A scenario can test more than one changed endpoint. UPDATE an existing `[skyramp]` `covers:` entry of type `integration` only when it already covers EVERY changed endpoint the scenario tests. If it covers some but not all of them, the scenario is not a duplicate — create it.\n" +
|
|
27
|
-
"- UI/E2E test: always create a new file — traces are distinct recordings.\n" +
|
|
28
|
-
"For `[skyramp]` contract and integration tests: if in doubt, prefer UPDATE over creating a duplicate.");
|
|
29
|
-
}
|
|
30
|
-
// ── Priority-tier ordering (replaces numeric CATEGORY_WEIGHTS) ──
|
|
31
|
-
// Categories map to HIGH / MEDIUM / LOW tiers.
|
|
32
|
-
// Within a tier, novelty (new > modified > existing) breaks ties,
|
|
33
|
-
// then cross-resource, step count, and finally the deterministic SHA-256 seed.
|
|
34
|
-
// CATEGORY_PRIORITY and PriorityTier imported from ../../types/TestRecommendation.js
|
|
35
|
-
const PRIORITY_ORDER = { CRITICAL: 4, HIGH: 3, MEDIUM: 2, LOW: 1 };
|
|
36
|
-
const NOVELTY_ORDER = { new: 3, modified: 2, existing: 1 };
|
|
37
|
-
function classifyNovelty(scenario, diffContext) {
|
|
38
|
-
if (!diffContext)
|
|
39
|
-
return Novelty.EXISTING;
|
|
40
|
-
const paths = scenario.steps.map(s => s.path);
|
|
41
|
-
const newPaths = new Set((diffContext.newEndpoints || []).map(ep => ep.path));
|
|
42
|
-
const modPaths = new Set((diffContext.modifiedEndpoints || []).map(ep => ep.path));
|
|
43
|
-
const removedPaths = new Set((diffContext.removedEndpoints || []).map(ep => ep.path));
|
|
44
|
-
if (paths.some(p => newPaths.has(p)))
|
|
45
|
-
return Novelty.NEW;
|
|
46
|
-
if (paths.some(p => modPaths.has(p) || removedPaths.has(p)))
|
|
47
|
-
return Novelty.MODIFIED;
|
|
48
|
-
return Novelty.EXISTING;
|
|
49
|
-
}
|
|
50
|
-
function prioritiseCandidate(scenario, diffContext) {
|
|
51
|
-
const priority = isAttackSurfaceSecurityBoundary(scenario)
|
|
52
|
-
? PriorityTier.CRITICAL
|
|
53
|
-
: CATEGORY_PRIORITY[scenario.category] ?? PriorityTier.LOW;
|
|
54
|
-
const novelty = classifyNovelty(scenario, diffContext);
|
|
55
|
-
return { priority, novelty };
|
|
56
|
-
}
|
|
57
|
-
function computeTiebreakerSeed(endpoints, diffFiles) {
|
|
58
|
-
const canonical = [...endpoints].sort().join("|") + "::" + [...diffFiles].sort().join("|");
|
|
59
|
-
return crypto.createHash("sha256").update(canonical).digest("hex").slice(0, 8);
|
|
18
|
+
rows + "\n");
|
|
60
19
|
}
|
|
20
|
+
// ── Execution Plan (replaces pre-ranked + scenarios + heuristic sections) ──
|
|
61
21
|
// Prevents bot-committed test files from being treated as application changes
|
|
62
22
|
// on subsequent testbot runs on the same PR.
|
|
63
23
|
const SKYRAMP_TEST_FILE_PATTERN = /(?:_test|_smoke|_contract|_fuzz|_integration|_load|_e2e|_ui)\.[^/]+$|scenario_[^/]+\.json$/;
|
|
64
|
-
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
export function
|
|
24
|
+
/** How this prompt tells the agent to plan: the whole procedure, every time.
|
|
25
|
+
* Rendered from `plugin/prompts/plan-tests.md` and `declaring-a-plan.md` rather
|
|
26
|
+
* than retyped, so there is one text and nothing to drift from. `isDiffScope` is
|
|
27
|
+
* unused here — the procedure reads the same for a diff and for a repository. */
|
|
28
|
+
function planningInstruction(_isDiffScope) {
|
|
29
|
+
return `${readPromptAsset("plan-tests.md").trim()}\n\n${renderPlanDeclarationGuidance()}`;
|
|
30
|
+
}
|
|
31
|
+
export function buildRecommendationPrompt(analysis, analysisScope = AnalysisScope.FullRepo, prContext, workspaceAuthHeader, workspaceAuthType, workspaceAuthScheme, sessionId) {
|
|
72
32
|
const isDiffScope = isDiff(analysisScope);
|
|
73
33
|
const diffContext = analysis.branchDiffContext;
|
|
74
|
-
|
|
34
|
+
const openApiSpec = analysis.artifacts?.openApiSpecs?.[0];
|
|
75
35
|
const filteredChangedFiles = diffContext
|
|
76
36
|
? diffContext.changedFiles.filter(f => !SKYRAMP_TEST_FILE_PATTERN.test(f))
|
|
77
37
|
: [];
|
|
78
|
-
// ── Frontend / UI change detection ──
|
|
79
|
-
// Used for isUIOnlyPR detection and mode preamble text.
|
|
80
|
-
// The actual UI vs backend test split is determined by LLM judgment via the
|
|
81
|
-
// scope assessment embedded in buildExecutionPlan (see scopeAssessment.ts).
|
|
82
|
-
const hasFrontendChanges = isDiffScope && diffContext
|
|
83
|
-
? filteredChangedFiles.some(f => isFrontendFile(f))
|
|
84
|
-
: false;
|
|
85
|
-
const hasApiChanges = isDiffScope && diffContext
|
|
86
|
-
? (diffContext.newEndpoints.length > 0 || diffContext.modifiedEndpoints.length > 0 || (diffContext.removedEndpoints?.length ?? 0) > 0)
|
|
87
|
-
: false;
|
|
88
|
-
const isUIOnlyPR = hasFrontendChanges && !hasApiChanges;
|
|
89
|
-
// ── Scoring ──
|
|
90
|
-
const baseMaxGen = Math.min(Math.max(maxGenerateOverride ?? (isDiffScope ? MAX_TESTS_TO_GENERATE : topN), 0), topN);
|
|
91
|
-
const maxGen = isUIOnlyPR ? Math.max(baseMaxGen, 1) : baseMaxGen;
|
|
92
|
-
const scenarios = analysis.businessContext.draftedScenarios;
|
|
93
|
-
let scored = [];
|
|
94
|
-
let seed = "";
|
|
95
|
-
if (!isUIOnlyPR && scenarios.length > 0) {
|
|
96
|
-
const diffFiles = filteredChangedFiles; // use filtered list so bot-committed test files don't shift the seed
|
|
97
|
-
const endpointPaths = analysis.apiEndpoints.endpoints.map(ep => ep.path);
|
|
98
|
-
seed = computeTiebreakerSeed(endpointPaths, diffFiles);
|
|
99
|
-
scored = scenarios.map(s => {
|
|
100
|
-
const result = prioritiseCandidate(s, diffContext ?? undefined);
|
|
101
|
-
return { scenario: s, ...result };
|
|
102
|
-
});
|
|
103
|
-
scored.sort((a, b) => {
|
|
104
|
-
const pa = PRIORITY_ORDER[a.priority], pb = PRIORITY_ORDER[b.priority];
|
|
105
|
-
if (pb !== pa)
|
|
106
|
-
return pb - pa;
|
|
107
|
-
const na = NOVELTY_ORDER[a.novelty], nb = NOVELTY_ORDER[b.novelty];
|
|
108
|
-
if (nb !== na)
|
|
109
|
-
return nb - na;
|
|
110
|
-
const crossA = a.scenario.steps.length > 2 ? 1 : 0;
|
|
111
|
-
const crossB = b.scenario.steps.length > 2 ? 1 : 0;
|
|
112
|
-
if (crossB !== crossA)
|
|
113
|
-
return crossB - crossA;
|
|
114
|
-
if (b.scenario.steps.length !== a.scenario.steps.length)
|
|
115
|
-
return b.scenario.steps.length - a.scenario.steps.length;
|
|
116
|
-
const errorA = a.scenario.steps.some(s => s.interactionType === "error" || s.interactionType === "edge-case") ? 1 : 0;
|
|
117
|
-
const errorB = b.scenario.steps.some(s => s.interactionType === "error" || s.interactionType === "edge-case") ? 1 : 0;
|
|
118
|
-
if (errorB !== errorA)
|
|
119
|
-
return errorB - errorA;
|
|
120
|
-
// Use locale-independent comparison to avoid runtime-locale non-determinism
|
|
121
|
-
const nameA = a.scenario.scenarioName;
|
|
122
|
-
const nameB = b.scenario.scenarioName;
|
|
123
|
-
if (nameA < nameB)
|
|
124
|
-
return -1;
|
|
125
|
-
if (nameA > nameB)
|
|
126
|
-
return 1;
|
|
127
|
-
const hashA = parseInt(crypto.createHash("sha256").update(seed + a.scenario.scenarioName).digest("hex").slice(0, 8), 16);
|
|
128
|
-
const hashB = parseInt(crypto.createHash("sha256").update(seed + b.scenario.scenarioName).digest("hex").slice(0, 8), 16);
|
|
129
|
-
return hashA - hashB;
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
return { scored, filteredChangedFiles, isUIOnlyPR, hasFrontendChanges, hasApiChanges, maxGen, seed };
|
|
133
|
-
}
|
|
134
|
-
export function buildRecommendationPrompt(analysis, analysisScope = AnalysisScope.FullRepo, topN = MAX_RECOMMENDATIONS, prContext, workspaceAuthHeader, workspaceAuthType, workspaceAuthScheme, maxGenerateOverride, sessionId) {
|
|
135
|
-
const isDiffScope = isDiff(analysisScope);
|
|
136
|
-
const diffContext = analysis.branchDiffContext;
|
|
137
|
-
const openApiSpec = analysis.artifacts?.openApiSpecs?.[0];
|
|
138
|
-
const { scored, filteredChangedFiles, isUIOnlyPR, hasFrontendChanges, hasApiChanges, maxGen, seed, } = computeScoredCandidates(analysis, analysisScope, topN, maxGenerateOverride);
|
|
139
38
|
const hasTraces = (analysis.artifacts?.traceFiles?.length ?? 0) > 0 ||
|
|
140
39
|
(analysis.artifacts?.playwrightRecordings?.length ?? 0) > 0;
|
|
141
|
-
// ── Mode preamble ──
|
|
142
|
-
const modePreamble = isDiffScope
|
|
143
|
-
? `You are in **PR mode**. Maximize test coverage for the branch changes.
|
|
144
|
-
Focus on tests that validate the changed fields, endpoints, and their interactions.
|
|
145
|
-
${isUIOnlyPR ? `\n**UI-only PR** — no backend changes. UI and E2E tests are most relevant.`
|
|
146
|
-
: hasFrontendChanges ? `\n**Mixed PR** — both frontend and backend changes detected. Backend and E2E/UI tests are both required.`
|
|
147
|
-
: ``}
|
|
148
|
-
Output should be concise and immediately actionable.`
|
|
149
|
-
: `You are in **Repo mode**. Comprehensive test strategy across all endpoints.`;
|
|
150
|
-
// ── UI rec authoring rules ──
|
|
151
|
-
// Anchors every UI recommendation regardless of whether the agent captured
|
|
152
|
-
// browser_blueprints (from the UI Blueprint Capture section this tool returns).
|
|
153
|
-
// When the agent has prior captures in its own tool-result history, those serve
|
|
154
|
-
// as grounding; when it doesn't, recommendations fall back to source-grounded
|
|
155
|
-
// prose. Either way, inventing elements / leaking jargon / duplicating UI
|
|
156
|
-
// tests are wrong, so the rules fire unconditionally.
|
|
157
|
-
const uiRecRulesSection = `
|
|
158
|
-
|
|
159
|
-
<ui_recommendation_authoring_rules>
|
|
160
|
-
**Blueprints inform *how* you describe UI tests, not *which* tests to recommend.** The recommendation catalog derives from the same change-signals listed elsewhere in this prompt (new endpoints, schema/field changes, security boundaries, business-logic modifications, frontend route or component additions, layout additions, etc.) — refer to those signals for the source-of-truth list, not this section.
|
|
161
|
-
|
|
162
|
-
**Do not generate near-duplicate UI tests of the same surface;** one well-targeted UI test per surface is enough.
|
|
163
|
-
|
|
164
|
-
For UI recommendations you *do* emit, ground the \`reasoning\` field in elements you have actually observed via \`browser_blueprint\` calls earlier in this session. If a recommendation's target element is not in any blueprint you have observed, either rephrase the recommendation around an element that IS observed, or describe the test target in higher-level terms. Do not invent element names from the PR description, source diff, or component name. If you have not captured any blueprints yet (e.g. backend-only PR, or pre-flight skipped), UI recommendations fall back to source-grounded prose drawn from the diff alone — that is a legitimate outcome, not a reason to invent.
|
|
165
|
-
|
|
166
|
-
**That rule governs element *identification*, not what you may assert about an identified element.** It does NOT forbid a description-derived **negative** assertion. When the PR description says an element should be gone and a captured page still renders it, the name is blueprint-grounded — you read it off the live page, not off the description — so a failing "is not present" assertion on that page is the correct output, and the requirement conflict is what it documents. Ground \`targetElements\` against the captured page where the element DOES render (the same grounding the negative-test guidance elsewhere in this prompt requires), and cite the test as a \`requirement_conflict\`. What stays forbidden is naming an element **no** blueprint contains because the description mentioned it.
|
|
167
|
-
|
|
168
|
-
Write UI recommendation \`reasoning\` fields in **natural prose** that names elements as a human would describe them (e.g. "the Notifications heading", "the disabled Mark all as read button"). **Do NOT mention "blueprint", "captured blueprint", "DOM analyzer", or any other internal MCP terminology in the reasoning text.** The reader of the report is a developer reviewing test recommendations on a PR; they don't know what a blueprint is and shouldn't have to. Phrases like "the captured blueprint shows X" or "visible from the captured blueprint" leak builder internals — instead just describe what the test verifies in plain product terms ("the disabled mark-all-read button in the empty state"). Likewise do NOT use internal-identifier syntax like \`role=button, accessibleName='X', logicalName=...\`.
|
|
169
|
-
</ui_recommendation_authoring_rules>
|
|
170
|
-
`;
|
|
171
|
-
// ── Endpoint listing ──
|
|
172
|
-
const allEndpoints = analysis.apiEndpoints.endpoints;
|
|
173
|
-
// In PR mode, identify which endpoints were changed so we can partition the listing.
|
|
174
|
-
const changedEndpointKeys = new Set();
|
|
175
|
-
if (isDiffScope && diffContext) {
|
|
176
|
-
for (const ep of [...(diffContext.newEndpoints || []), ...(diffContext.modifiedEndpoints || []), ...(diffContext.removedEndpoints || [])]) {
|
|
177
|
-
for (const m of (ep.methods ?? [])) {
|
|
178
|
-
changedEndpointKeys.add(`${m.method} ${ep.path}`);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
const fmtEndpoint = (m, ep) => ` ${m.method} ${ep.path}${m.authRequired ? " [auth]" : ""} (${(m.interactions ?? []).length} interactions)`;
|
|
183
|
-
// In diff scope, cap the reference endpoint list to prevent context overflow.
|
|
184
|
-
// Changed endpoints are always shown in full; only the "other" list is capped.
|
|
185
|
-
const DIFF_SCOPE_OTHER_ENDPOINT_CAP = 20;
|
|
186
|
-
let endpointLines;
|
|
187
|
-
if (isDiffScope && changedEndpointKeys.size > 0) {
|
|
188
|
-
const changedLines = [];
|
|
189
|
-
const otherLines = [];
|
|
190
|
-
for (const ep of allEndpoints) {
|
|
191
|
-
for (const m of (ep.methods ?? [])) {
|
|
192
|
-
const line = fmtEndpoint(m, ep);
|
|
193
|
-
if (changedEndpointKeys.has(`${m.method} ${ep.path}`)) {
|
|
194
|
-
changedLines.push(line);
|
|
195
|
-
}
|
|
196
|
-
else {
|
|
197
|
-
otherLines.push(line);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
// Removed endpoints no longer exist in allEndpoints (current catalog), so they
|
|
202
|
-
// would be silently absent from changedLines. Append them explicitly with a
|
|
203
|
-
// [removed] marker so the LLM knows to generate verify-404/deprecation tests.
|
|
204
|
-
for (const ep of (diffContext?.removedEndpoints || [])) {
|
|
205
|
-
for (const m of (ep.methods ?? [])) {
|
|
206
|
-
changedLines.push(` ${m.method} ${ep.path} [removed]`);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
const cappedOther = otherLines.slice(0, DIFF_SCOPE_OTHER_ENDPOINT_CAP);
|
|
210
|
-
const hiddenOtherCount = otherLines.length - cappedOther.length;
|
|
211
|
-
const otherSuffix = hiddenOtherCount > 0
|
|
212
|
-
? `\n ... and ${hiddenOtherCount} more (full endpoint list in state file)`
|
|
213
|
-
: "";
|
|
214
|
-
const otherLabel = hiddenOtherCount > 0
|
|
215
|
-
? `Other endpoints (reference only, ${cappedOther.length} of ${otherLines.length} shown)`
|
|
216
|
-
: "Other endpoints (reference only)";
|
|
217
|
-
endpointLines = `**Likely changed in this PR (from static file→endpoint mapping — verify against diff in Step ${ANALYSIS_STEP_EXTRACT}):**\n${changedLines.join("\n") || " none"}\n\n**${otherLabel}:**\n${cappedOther.join("\n") || " none"}${otherSuffix}`;
|
|
218
|
-
}
|
|
219
|
-
else if (isDiffScope) {
|
|
220
|
-
// Diff scope but no changed endpoints detected — cap to avoid dumping the full catalog.
|
|
221
|
-
const allMethodLines = allEndpoints.flatMap((ep) => (ep.methods ?? []).map((m) => fmtEndpoint(m, ep)));
|
|
222
|
-
const cappedLines = allMethodLines.slice(0, DIFF_SCOPE_OTHER_ENDPOINT_CAP);
|
|
223
|
-
const hiddenCount = allMethodLines.length - cappedLines.length;
|
|
224
|
-
const suffix = hiddenCount > 0
|
|
225
|
-
? `\n ... and ${hiddenCount} more (full endpoint list in state file — trace changed files directly from the diff to find affected endpoints)`
|
|
226
|
-
: "";
|
|
227
|
-
endpointLines = `${cappedLines.join("\n") || " none"}${suffix}`;
|
|
228
|
-
}
|
|
229
|
-
else {
|
|
230
|
-
endpointLines = allEndpoints
|
|
231
|
-
.flatMap((ep) => (ep.methods ?? []).map((m) => fmtEndpoint(m, ep)))
|
|
232
|
-
.join("\n");
|
|
233
|
-
}
|
|
234
40
|
const authMethod = analysis.authentication.method || "unknown";
|
|
235
41
|
const authTypeValue = workspaceAuthType ?? "";
|
|
236
42
|
let authHeaderValue;
|
|
@@ -252,192 +58,24 @@ Write UI recommendation \`reasoning\` fields in **natural prose** that names ele
|
|
|
252
58
|
"Add authHeader: <X-Your-Key-Header> to .skyramp/workspace.yml.");
|
|
253
59
|
}
|
|
254
60
|
}
|
|
255
|
-
const { authSchemeSnippet } = getAuthSnippets(authHeaderValue, authTypeValue, workspaceAuthScheme);
|
|
256
|
-
const routeDiscovery = analysis.routeDiscovery;
|
|
257
|
-
const routeDiscoverySection = routeDiscovery
|
|
258
|
-
? `
|
|
259
|
-
## LLM Route Discovery Inputs
|
|
260
|
-
Static endpoint data below is a best-effort hint, not a complete parser for every framework.
|
|
261
|
-
Authoritative endpoint extraction must come from reading the changed source files, router/module context, OpenAPI paths when available, and the diff.
|
|
262
|
-
Candidate files to inspect: ${routeDiscovery.candidateFiles.length > 0 ? routeDiscovery.candidateFiles.join(", ") : "none"}
|
|
263
|
-
Router/module context files: ${routeDiscovery.routerMountContext.length > 0 ? routeDiscovery.routerMountContext.join(", ") : "none"}
|
|
264
|
-
OpenAPI paths available: ${routeDiscovery.openApiPaths.length}
|
|
265
|
-
Static hints available: ${routeDiscovery.staticHints.length}
|
|
266
|
-
${routeDiscovery.diffFilePath ? `Diff file: ${routeDiscovery.diffFilePath}` : ""}
|
|
267
|
-
`.trim()
|
|
268
|
-
: "";
|
|
269
|
-
// ── Source Priority ──
|
|
270
|
-
// The ladder ranks sources that describe what the system DOES. The PR
|
|
271
|
-
// title/description states what the change is SUPPOSED to do, so it is not a
|
|
272
|
-
// rung — resolving a description-vs-code disagreement by rung produced tests
|
|
273
|
-
// that asserted the code and buried the contradiction in prose (SKYR-4291).
|
|
274
|
-
// Only the ladder-placement rule lives here; the Execution Plan's Code Review
|
|
275
|
-
// step is the canonical home for how a conflict is handled, so this document
|
|
276
|
-
// states it once. Diff-scoped: repo mode has no PR to compare against.
|
|
277
|
-
const requirementConflictRules = isDiffScope ? `
|
|
278
|
-
That ladder ranks sources that describe what the system **does**. The **PR title and description** — plus any requirements file they reference — state what this change is **supposed to do**. They are the *intent* side; the ladder is the *implementation* side. Never settle a disagreement between the two by picking the higher rung: a requirement the implemented behavior contradicts is a \`requirement_conflict\`, NOT a case of "the description loses". The Code Review step of the Execution Plan below specifies how to detect, check, and handle one — follow it there.
|
|
279
|
-
` : "";
|
|
280
|
-
const sourcePriority = `
|
|
281
|
-
## Source Priority
|
|
282
|
-
When information conflicts, prefer: **Traces** (actual behavior) > **Source code read by the LLM** (implemented behavior) > **OpenAPI spec/docs** (documented behavior) > **Static parser hints** (best-effort, may be incomplete or framework-blind).
|
|
283
|
-
${requirementConflictRules}`;
|
|
284
|
-
// Compact fingerprint of tests already covering endpoints in this repo (Skyramp + external).
|
|
285
|
-
// Re-derived fresh each run from test files on disk — no separate persistence needed.
|
|
286
|
-
const testLocations = analysis.existingTests?.testLocations ?? {};
|
|
287
|
-
const testFingerprint = (() => {
|
|
288
|
-
const entries = Object.entries(testLocations);
|
|
289
|
-
if (entries.length === 0)
|
|
290
|
-
return "";
|
|
291
|
-
// Each value is a comma-joined list of "file (covers: ep1, ep2)" entries — one per file.
|
|
292
|
-
// Count files by splitting on "), " boundaries (each entry ends with ")").
|
|
293
|
-
let totalFiles = 0;
|
|
294
|
-
let externalCount = 0;
|
|
295
|
-
const byType = new Map();
|
|
296
|
-
for (const [type, fileList] of entries) {
|
|
297
|
-
// Use matchAll to extract covers from ALL files of this type, not just the first.
|
|
298
|
-
const allEndpoints = [...fileList.matchAll(/covers:\s*([^)]+)/g)].map(m => m[1].trim());
|
|
299
|
-
// Count files: strip "[external]", "[skyramp]", and "(covers: ...)" clauses then split on ", " to count entries,
|
|
300
|
-
// correctly handling both files-with-covers and files-without-covers (e.g. UI tests).
|
|
301
|
-
const strippedList = fileList.replace(/\s*\[(?:external|skyramp)\]/g, "").replace(/\s*\(covers:[^)]*\)/g, "");
|
|
302
|
-
totalFiles += strippedList.split(", ").filter(s => s.trim().length > 0).length;
|
|
303
|
-
// Count external files from [external] annotations
|
|
304
|
-
externalCount += (fileList.match(/\[external\]/g) || []).length;
|
|
305
|
-
if (!byType.has(type))
|
|
306
|
-
byType.set(type, []);
|
|
307
|
-
byType.get(type).push(...allEndpoints);
|
|
308
|
-
}
|
|
309
|
-
const lines = [...byType.entries()]
|
|
310
|
-
.filter(([, eps]) => eps.length > 0)
|
|
311
|
-
.map(([type, eps]) => ` ${type}: ${[...new Set(eps)].join(", ")}`);
|
|
312
|
-
const skyrampCount = totalFiles - externalCount;
|
|
313
|
-
const breakdown = externalCount > 0
|
|
314
|
-
? `${skyrampCount} Skyramp + ${externalCount} external`
|
|
315
|
-
: `${totalFiles} files`;
|
|
316
|
-
return `\nTests already covering endpoints in this repo (${breakdown}):\n${lines.join("\n")}\n(Use this to focus on coverage gaps. External tests block new recommendations but cannot be updated.)`;
|
|
317
|
-
})();
|
|
318
61
|
const repoContext = `
|
|
319
62
|
Repository: ${analysis.metadata.repositoryName}
|
|
320
63
|
Framework: ${analysis.projectClassification.primaryFramework} (${analysis.projectClassification.primaryLanguage})
|
|
321
64
|
Project type: ${analysis.projectClassification.projectType}
|
|
322
65
|
Auth: ${authMethod} (header: ${authHeaderValue}${authTypeValue ? `, type: ${authTypeValue}` : ""})
|
|
323
|
-
Base URL: ${analysis.
|
|
324
|
-
Candidate endpoint hints from static scan — unverified and non-exhaustive; confirm paths by reading source/router context before use (${analysis.apiEndpoints.totalCount}):
|
|
325
|
-
${endpointLines}${testFingerprint}
|
|
326
|
-
${routeDiscoverySection ? `
|
|
327
|
-
|
|
328
|
-
${routeDiscoverySection}` : ""}
|
|
66
|
+
Base URL: ${analysis.workspace.baseUrl}
|
|
329
67
|
`.trim();
|
|
330
68
|
// ── Branch diff ──
|
|
331
69
|
let diffSection = "";
|
|
332
70
|
if (isDiffScope && diffContext) {
|
|
333
|
-
const fmtEps = (eps, detail) => eps.flatMap((ep) => (ep.methods ?? []).map((m) => ` ${m.method} ${ep.path} (${detail(m)})`)).join("\n") || " none";
|
|
334
71
|
diffSection = `
|
|
335
72
|
## Branch Diff Context
|
|
336
73
|
Branch: \`${diffContext.currentBranch}\` → base: \`${diffContext.baseBranch}\`
|
|
337
74
|
Changed files: ${filteredChangedFiles.join(", ")}
|
|
338
|
-
New endpoints:
|
|
339
|
-
${fmtEps(diffContext.newEndpoints, (m) => `${m.sourceFile}, ${m.interactionCount} interactions`)}
|
|
340
|
-
Modified endpoints:
|
|
341
|
-
${fmtEps(diffContext.modifiedEndpoints, (m) => `${m.sourceFile}, ${m.changeType}`)}
|
|
342
|
-
Removed endpoints:
|
|
343
|
-
${fmtEps(diffContext.removedEndpoints ?? [], (m) => `${m.sourceFile}, removed`)}
|
|
344
|
-
Affected services: ${diffContext.affectedServices.join(", ") || "N/A"}
|
|
345
|
-
|
|
346
|
-
Focus on tests that validate these changes and how they interact with existing resources.
|
|
347
|
-
For removed endpoints: verify they now return 404 or the appropriate deprecation status code.
|
|
348
|
-
Treat the endpoint lists above as static hints. If source/diff inspection finds a different changed endpoint set, prefer the source-grounded set and use other endpoints only as setup steps.
|
|
349
|
-
`;
|
|
350
|
-
}
|
|
351
|
-
// ── Interactions ──
|
|
352
|
-
let interactionSection = "";
|
|
353
|
-
{
|
|
354
|
-
const diffEndpointKeys = new Set();
|
|
355
|
-
if (isDiffScope && diffContext) {
|
|
356
|
-
for (const ep of [...(diffContext.newEndpoints || []), ...(diffContext.modifiedEndpoints || [])]) {
|
|
357
|
-
for (const m of (ep.methods ?? []))
|
|
358
|
-
diffEndpointKeys.add(`${m.method} ${ep.path}`);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
// SKYR-4188: `description` held exactly "<METHOD> <path>" in 11,273 of 11,273
|
|
362
|
-
// interaction lines on eval run 32227152533 — a copy of the start of its own
|
|
363
|
-
// line. Emit it only when it says something the line does not already say.
|
|
364
|
-
const interactionLabel = (i, method, path) => i.description && i.description !== `${method} ${path}` ? `: ${i.description}` : "";
|
|
365
|
-
// detailBlocks repeats every summary line and adds the bodies. The two cover
|
|
366
|
-
// the same endpoints unless the scope is a diff, so outside a diff the summary
|
|
367
|
-
// is pure repetition.
|
|
368
|
-
const summaryLines = !isDiffScope ? "" : allEndpoints
|
|
369
|
-
.flatMap((ep) => (ep.methods ?? []).flatMap((m) => (m.interactions ?? []).map((i) => ` ${m.method} ${ep.path} → ${i.response.statusCode} (${i.type})${interactionLabel(i, m.method, ep.path)}`)))
|
|
370
|
-
.join("\n");
|
|
371
|
-
const detailEndpoints = isDiffScope
|
|
372
|
-
? allEndpoints.filter((ep) => (ep.methods ?? []).some((m) => diffEndpointKeys.has(`${m.method} ${ep.path}`)))
|
|
373
|
-
: allEndpoints;
|
|
374
|
-
const detailBlocks = detailEndpoints
|
|
375
|
-
.flatMap((ep) => (ep.methods ?? []).flatMap((m) => (m.interactions ?? []).map((i) => {
|
|
376
|
-
const reqBody = i.request.body ? `\n requestBody: ${JSON.stringify(i.request.body)} (pass as JSON string when calling tools)` : "";
|
|
377
|
-
const qParams = i.request.queryParams ? `\n queryParams: ${JSON.stringify(i.request.queryParams)}` : "";
|
|
378
|
-
const resBody = i.response.body ? `\n responseBody: ${JSON.stringify(i.response.body)}` : "";
|
|
379
|
-
const headers = i.request.headers ? `\n headers: ${JSON.stringify(i.request.headers)}` : "";
|
|
380
|
-
return ` ${m.method} ${ep.path} → ${i.response.statusCode} (${i.type})${interactionLabel(i, m.method, ep.path)}${reqBody}${qParams}${resBody}${headers}`;
|
|
381
|
-
})))
|
|
382
|
-
.join("\n");
|
|
383
|
-
interactionSection = `
|
|
384
|
-
## Endpoint Interactions
|
|
385
|
-
${summaryLines ? `${summaryLines}\n` : ""}
|
|
386
|
-
### Detailed (request/response bodies)
|
|
387
|
-
${isDiffScope ? "Changed endpoints only. " : ""}Use source code schemas (Zod/Pydantic/DTOs) for actual request bodies and query parameters.
|
|
388
|
-
${detailBlocks}
|
|
389
|
-
`;
|
|
390
|
-
}
|
|
391
|
-
const endpointCount = allEndpoints.reduce((acc, ep) => acc + (ep.methods ?? []).length, 0);
|
|
392
|
-
// ── Main section: execution plan, UI-only guidance, or draft-your-own ──
|
|
393
|
-
let mainSection;
|
|
394
|
-
if (!isDiffScope && scored.length > 0) {
|
|
395
|
-
const projectType = analysis.projectClassification.projectType;
|
|
396
|
-
const isFrontendProject = projectType === "full-stack" || projectType === "frontend";
|
|
397
|
-
const isFrontendOnlyProject = projectType === "frontend";
|
|
398
|
-
const externalCoverageFullRepo = buildExternalCoverageSet(testLocations);
|
|
399
|
-
if (externalCoverageFullRepo.size > 0) {
|
|
400
|
-
logger.info(`External test coverage keys (full-repo): ${[...externalCoverageFullRepo].join(", ")}`);
|
|
401
|
-
}
|
|
402
|
-
mainSection = buildFullRepoRecommendations(scored, topN, analysis.apiEndpoints.baseUrl, authHeaderValue, authSchemeSnippet, authTypeValue, isFrontendProject, isFrontendOnlyProject, externalCoverageFullRepo);
|
|
403
|
-
}
|
|
404
|
-
else if (isDiffScope) {
|
|
405
|
-
// Always use the full execution plan in diff scope — even when scored.length === 0
|
|
406
|
-
// (no pre-ranked scenarios). The execution plan includes the dynamic Code Review step,
|
|
407
|
-
// bug-catching insertion, and the dynamic bug-coverage gate
|
|
408
|
-
// that are critical for catching seeded bugs in new endpoints.
|
|
409
|
-
const externalCoverage = buildExternalCoverageSet(testLocations);
|
|
410
|
-
if (externalCoverage.size > 0) {
|
|
411
|
-
logger.info(`External test coverage keys: ${[...externalCoverage].join(", ")}`);
|
|
412
|
-
}
|
|
413
|
-
mainSection = buildExecutionPlan(scored, maxGen, topN, analysis.apiEndpoints.baseUrl, authHeaderValue, authSchemeSnippet, authTypeValue, seed, endpointCount, isUIOnlyPR, hasFrontendChanges, hasTraces, externalCoverage, analysis.existingTests.relevantExternalTestPaths ?? [],
|
|
414
|
-
// Zero-classified gating (SKYR-3820) requires an actual diff that classified
|
|
415
|
-
// nothing — a missing diffContext is absence of evidence, not an empty
|
|
416
|
-
// change surface, so it keeps the legacy fixed-budget rendering.
|
|
417
|
-
diffContext ? hasApiChanges : true);
|
|
418
|
-
}
|
|
419
|
-
else {
|
|
420
|
-
// Full-repo scope with no scored items — rare fallback
|
|
421
|
-
mainSection = `
|
|
422
|
-
## Draft Your Execution Plan
|
|
423
|
-
|
|
424
|
-
No pre-drafted scenarios available.
|
|
425
|
-
|
|
426
|
-
${buildScopeAssessmentSection(topN, maxGen)}
|
|
427
|
-
|
|
428
|
-
Draft tests from the endpoint interactions and source code above, following the same tool pipeline described in Tool Workflows below. Prioritize critical categories: security_boundary > data_integrity > business_rule > workflow > crud.
|
|
429
|
-
|
|
430
|
-
For each test: pick the highest-impact endpoint(s), draft a realistic scenario with actual request/response shapes from source code, and execute the generation tool pipeline.
|
|
431
|
-
|
|
432
|
-
**Honor your Budget Plan: produce exactly the total you committed to (GENERATE + ADDITIONAL). No fewer, no padding with low-value tests.**
|
|
433
|
-
|
|
434
|
-
## Recommendation Stability
|
|
435
|
-
- **Carry forward** previous additionalRecommendations that still apply — match by scenarioName (multi-step) or endpoint (single-endpoint). Re-derive category and priority from test content.
|
|
436
|
-
- **Only drop** a previous recommendation if its target endpoint was removed, its business logic changed, or it is now covered by a generated test.
|
|
437
|
-
- **Only add** new recommendations for code paths introduced since the last run.
|
|
438
|
-
- Do not churn recommendations without cause.
|
|
439
75
|
`;
|
|
440
76
|
}
|
|
77
|
+
// ── Main section: how to fill a generation tool call, in both scopes ──
|
|
78
|
+
const mainSection = buildExecutionPlan(hasTraces, isDiffScope);
|
|
441
79
|
// ── OpenAPI spec note ──
|
|
442
80
|
const specNote = openApiSpec
|
|
443
81
|
? `\n**OpenAPI Spec available**: \`${openApiSpec.path}\`
|
|
@@ -448,7 +86,6 @@ Use it for contract tests (\`apiSchema: "${openApiSpec.path}"\`) and to extract
|
|
|
448
86
|
let prHistorySection = "";
|
|
449
87
|
if (prContext && prContext.previousRecommendations.length > 0) {
|
|
450
88
|
const implemented = prContext.previousRecommendations.filter(r => r.status === "implemented");
|
|
451
|
-
const recommended = prContext.previousRecommendations.filter(r => r.status === "recommended");
|
|
452
89
|
let historyBody = "";
|
|
453
90
|
if (implemented.length > 0) {
|
|
454
91
|
const implLines = implemented
|
|
@@ -466,73 +103,31 @@ ${implLines}${fileLines}`;
|
|
|
466
103
|
.join("\n");
|
|
467
104
|
historyBody += `### Execution Results from Prior Run
|
|
468
105
|
${resultLines}
|
|
469
|
-
If a test failed previously, check whether the failure was environmental or a real bug,
|
|
470
|
-
and adjust the test approach if needed.
|
|
471
|
-
`;
|
|
472
|
-
}
|
|
473
|
-
if (recommended.length > 0) {
|
|
474
|
-
// De-duplicate: the parser creates one entry per step-endpoint for
|
|
475
|
-
// multi-step scenarios. Collapse them so the LLM sees one line per
|
|
476
|
-
// unique (testType, scenarioName) — or (testType, endpoint) when no
|
|
477
|
-
// scenarioName exists.
|
|
478
|
-
const seen = new Set();
|
|
479
|
-
const dedupedRecs = recommended.filter((r) => {
|
|
480
|
-
const key = r.scenarioName
|
|
481
|
-
? `${r.testType}::${r.scenarioName}`
|
|
482
|
-
: `${r.testType}::${r.endpoint}`;
|
|
483
|
-
if (seen.has(key))
|
|
484
|
-
return false;
|
|
485
|
-
seen.add(key);
|
|
486
|
-
return true;
|
|
487
|
-
});
|
|
488
|
-
const recLines = dedupedRecs
|
|
489
|
-
.map((r) => ` - ${r.testType} — ${r.endpoint}${r.scenarioName ? ` (scenario: ${r.scenarioName})` : ""}`)
|
|
490
|
-
.join("\n");
|
|
491
|
-
historyBody += `
|
|
492
|
-
### Previously Recommended (not generated)
|
|
493
|
-
${recLines}
|
|
494
|
-
**Stability rule**: If a previously recommended test still applies to the current code
|
|
495
|
-
(the endpoint exists, the business logic hasn't changed), carry it forward in your
|
|
496
|
-
additionalRecommendations — match by scenarioName (for multi-step scenarios) or by
|
|
497
|
-
endpoint (for single-endpoint tests). Re-derive category and priority from the test
|
|
498
|
-
content. Do NOT drop a previous recommendation unless the underlying code was removed
|
|
499
|
-
or the test is now covered by a generated test.
|
|
500
|
-
Only add NEW recommendations for code paths introduced in the latest commit.
|
|
501
106
|
`;
|
|
502
107
|
}
|
|
503
108
|
prHistorySection = `
|
|
504
109
|
## PR History (PR #${prContext.prNumber})
|
|
505
|
-
Tests from prior bot runs are still in the working tree
|
|
506
|
-
|
|
507
|
-
coverage and to fill gaps:
|
|
508
|
-
- **Do NOT re-recommend** tests listed under "Previously Generated Tests" — they already
|
|
509
|
-
exist and are maintained automatically.
|
|
510
|
-
- **Carry forward** previously recommended-but-not-generated tests unchanged in
|
|
511
|
-
additionalRecommendations if they still apply. Promote the highest-priority ones
|
|
512
|
-
into generation slots if capacity allows.
|
|
513
|
-
- **Add new** recommendations only for endpoints or code paths introduced in the latest
|
|
514
|
-
commit that aren't covered by existing tests or previous recommendations.
|
|
515
|
-
- **Stability**: When the code diff between commits is small, the recommendation set
|
|
516
|
-
should be mostly stable. Do not churn recommendations without cause.
|
|
517
|
-
- If prior execution results show failures, note the issue but do not re-recommend
|
|
518
|
-
the test — ${taskRef(TASK_GENERATE)} handles fixes for existing tests.
|
|
110
|
+
Tests from prior bot runs are still in the working tree, and ${taskRef(TASK_GENERATE)} keeps them
|
|
111
|
+
up to date.
|
|
519
112
|
${historyBody}`;
|
|
520
113
|
}
|
|
521
114
|
// ── Compose all sections ──
|
|
522
115
|
// Long-context best practice: all data at top, all instructions + query at bottom.
|
|
523
116
|
// See: https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices#long-context-prompting
|
|
117
|
+
//
|
|
118
|
+
// What to test, and how to declare and submit it, is written ONCE — in
|
|
119
|
+
// `plugin/prompts/plan-tests.md`. This prompt carries the run's data and the
|
|
120
|
+
// generation mechanics, and renders that file for the rest.
|
|
524
121
|
const scopeNote = isDiffScope
|
|
525
122
|
? "Scoped to current branch changes."
|
|
526
123
|
: "Covers the full repository.";
|
|
527
124
|
return `
|
|
528
125
|
${buildArchitectPreamble(isDiffScope)}
|
|
529
126
|
|
|
530
|
-
${modePreamble}
|
|
531
|
-
|
|
532
127
|
Scope: ${scopeNote}
|
|
533
128
|
|
|
534
|
-
${
|
|
535
|
-
|
|
129
|
+
${planningInstruction(isDiffScope)}
|
|
130
|
+
|
|
536
131
|
<repository_context>
|
|
537
132
|
## Repository Context
|
|
538
133
|
|
|
@@ -540,16 +135,11 @@ ${repoContext}
|
|
|
540
135
|
${specNote}
|
|
541
136
|
</repository_context>
|
|
542
137
|
${diffSection ? `<branch_diff>\n${diffSection}\n</branch_diff>` : ""}
|
|
543
|
-
<endpoint_interactions>
|
|
544
|
-
${interactionSection}
|
|
545
|
-
</endpoint_interactions>
|
|
546
|
-
|
|
547
138
|
<existing_tests>
|
|
548
139
|
## Existing Tests
|
|
549
140
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
- \`[external]\` — user-written tests (pytest, jest, junit, etc.). Do not generate a new parallel test file for an endpoint already covered by an external test. These tests still break when the API changes — Task 1 maintenance applies to them the same as to Skyramp tests (in-place UPDATE only; do not regenerate or delete).
|
|
141
|
+
Two categories of test file, by tag: \`[skyramp]\` was generated by Skyramp tools, and
|
|
142
|
+
\`[external]\` was written by a user. Both break when the API changes.
|
|
553
143
|
|
|
554
144
|
- Frameworks: ${analysis.existingTests.frameworks.join(", ") || "none"}
|
|
555
145
|
${formatTestLocations(analysis.existingTests.testLocations)}
|
|
@@ -562,26 +152,11 @@ ${buildContextFetchingGuidance(sessionId)}
|
|
|
562
152
|
|
|
563
153
|
${buildReasoningProtocol()}
|
|
564
154
|
|
|
565
|
-
${
|
|
566
|
-
|
|
567
|
-
${isDiffScope
|
|
568
|
-
? (isUIOnlyPR
|
|
569
|
-
? `## How to Generate Tests — Tool Workflows
|
|
570
|
-
|
|
571
|
-
**For UI tests (AI-driven recording):**
|
|
572
|
-
1. \`browser_navigate({ url: "<frontend_url>" })\`
|
|
573
|
-
2. Interact with changed components (\`browser_click\`, \`browser_type\`, \`browser_fill_form\`, etc.)
|
|
574
|
-
3. \`browser_snapshot()\` after each interaction
|
|
575
|
-
4. \`skyramp_export_zip({ outputPath: "<repositoryPath>/.skyramp/<component>_trace.zip" })\` — absolute path
|
|
576
|
-
5. \`skyramp_ui_test_generation({ playwrightInput: "<absolute_path_to_zip>" })\`
|
|
577
|
-
|
|
578
|
-
**For E2E tests:** Same browser recording flow, then call \`skyramp_e2e_test_generation\` with the zip.`
|
|
579
|
-
: buildToolWorkflows(authHeaderValue, authTypeValue, workspaceAuthScheme, EXEC_STEP_ENRICH))
|
|
580
|
-
: ""}
|
|
155
|
+
${buildToolWorkflows(authHeaderValue, authTypeValue, workspaceAuthScheme, EXEC_STEP_ENRICH)}
|
|
581
156
|
|
|
582
157
|
${mainSection}
|
|
583
158
|
|
|
584
|
-
${isDiffScope ? buildVerificationChecklist(
|
|
159
|
+
${isDiffScope ? buildVerificationChecklist() : ""}
|
|
585
160
|
</instructions>
|
|
586
161
|
`;
|
|
587
162
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Plan-time guidance for the v2 recommendation path. The text lives in
|
|
2
|
+
* `plugin/prompts/declaring-a-plan.md`. It carries no
|
|
3
|
+
* `{placeholder}`: what each field is for and what its check reads is stated in
|
|
4
|
+
* the plan tool's schema, and the judgment a check cannot make is stated in that
|
|
5
|
+
* check's own objection. */
|
|
6
|
+
export declare function renderPlanDeclarationGuidance(): string;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { readPromptAsset } from "../promptAssets.js";
|
|
2
|
+
/** Plan-time guidance for the v2 recommendation path. The text lives in
|
|
3
|
+
* `plugin/prompts/declaring-a-plan.md`. It carries no
|
|
4
|
+
* `{placeholder}`: what each field is for and what its check reads is stated in
|
|
5
|
+
* the plan tool's schema, and the judgment a check cannot make is stated in that
|
|
6
|
+
* check's own objection. */
|
|
7
|
+
export function renderPlanDeclarationGuidance() {
|
|
8
|
+
return readPromptAsset("declaring-a-plan.md").trim();
|
|
9
|
+
}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import type { RelatedRepository, TestbotPromptOptions } from "../../types/TestbotPromptOptions.js";
|
|
3
|
+
/** "After generation, complete these steps" list at the end of Task 2. Exported
|
|
4
|
+
* for `referenceIntegrity.test.ts`, which compares it against the ordinals the
|
|
5
|
+
* prompt renders. */
|
|
6
|
+
export declare const POSTGEN: Record<"INTEGRATION_ENHANCE" | "CONTRACT_ENHANCE" | "UI_CODE_REUSE" | "UI_ENHANCE" | "WAIT", string>;
|
|
7
|
+
/** The capture-act-capture pattern list inside the UI recording section. */
|
|
8
|
+
export declare const CAPTURE: Record<"BEFORE" | "CUSTOM_WIDGET" | "ACT" | "AFTER" | "POSSIBLE_ASSERTIONS", string>;
|
|
9
|
+
/** The "test execution fails" recovery list under Failure Recovery. */
|
|
10
|
+
export declare const EXEC_FAIL: Record<"DIAGNOSE" | "EXPECTED_FAILURE" | "INFRA_RETRY" | "REPORT_FAIL", string>;
|
|
3
11
|
/**
|
|
4
12
|
* Parse the JSON-encoded `relatedRepositories` argument passed via the testbot
|
|
5
13
|
* prompt/resource. Returns undefined for missing/blank input or malformed JSON so the
|