@skyramp/mcp 0.3.1 → 0.3.2-rc.pom-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/index.js +2 -1
- package/build/prompts/code-reuse.d.ts +7 -1
- package/build/prompts/code-reuse.js +8 -5
- package/build/prompts/code-reuse.test.d.ts +1 -0
- package/build/prompts/code-reuse.test.js +62 -0
- package/build/prompts/pom-aware-code-reuse.d.ts +6 -1
- package/build/prompts/pom-aware-code-reuse.js +98 -52
- package/build/prompts/pom-aware-code-reuse.test.d.ts +1 -0
- package/build/prompts/pom-aware-code-reuse.test.js +11 -0
- package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +4 -2
- package/build/prompts/test-recommendation/diffExecutionPlan.js +11 -65
- package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +2 -2
- package/build/prompts/test-recommendation/recommendationSections.js +5 -2
- package/build/prompts/test-recommendation/scopeAssessment.js +1 -1
- package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +26 -1
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +68 -56
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +48 -11
- package/build/prompts/testbot/testbot-prompts.d.ts +1 -1
- package/build/prompts/testbot/testbot-prompts.js +67 -21
- package/build/prompts/testbot/testbot-prompts.test.js +44 -0
- package/build/recommendation/budgeters/diversityBalancedBudgeter.d.ts +7 -0
- package/build/recommendation/budgeters/diversityBalancedBudgeter.js +71 -0
- package/build/recommendation/budgeters/diversityBalancedBudgeter.test.d.ts +1 -0
- package/build/recommendation/budgeters/diversityBalancedBudgeter.test.js +75 -0
- package/build/recommendation/budgeters/fixedNBudgeter.d.ts +7 -0
- package/build/recommendation/budgeters/fixedNBudgeter.js +11 -0
- package/build/recommendation/budgeters/fixedNBudgeter.test.d.ts +1 -0
- package/build/recommendation/budgeters/fixedNBudgeter.test.js +66 -0
- package/build/recommendation/budgeters/shared.d.ts +19 -0
- package/build/recommendation/budgeters/shared.js +66 -0
- package/build/recommendation/discriminators.d.ts +31 -0
- package/build/recommendation/discriminators.js +355 -0
- package/build/recommendation/discriminators.test.d.ts +1 -0
- package/build/recommendation/discriminators.test.js +324 -0
- package/build/recommendation/diversity.d.ts +47 -0
- package/build/recommendation/diversity.js +101 -0
- package/build/recommendation/diversity.test.d.ts +1 -0
- package/build/recommendation/diversity.test.js +77 -0
- package/build/recommendation/planRanker.d.ts +50 -0
- package/build/recommendation/planRanker.js +67 -0
- package/build/recommendation/planRanker.test.d.ts +1 -0
- package/build/recommendation/planRanker.test.js +110 -0
- package/build/recommendation/testFixtures.d.ts +25 -0
- package/build/recommendation/testFixtures.js +45 -0
- package/build/resources/testbotResource.js +4 -1
- package/build/services/ScenarioGenerationService.d.ts +5 -0
- package/build/services/ScenarioGenerationService.js +16 -1
- package/build/services/ScenarioGenerationService.test.js +44 -0
- package/build/services/TestExecutionService.d.ts +15 -1
- package/build/services/TestExecutionService.js +210 -55
- package/build/services/TestExecutionService.test.js +397 -0
- package/build/services/TestGenerationService.js +19 -1
- package/build/services/TestGenerationService.test.js +58 -0
- package/build/tool-phases.js +1 -0
- package/build/toolNames.d.ts +19 -0
- package/build/toolNames.js +19 -0
- package/build/tools/code-refactor/codeReuseTool.d.ts +3 -0
- package/build/tools/code-refactor/codeReuseTool.js +76 -4
- package/build/tools/code-refactor/codeReuseTool.test.d.ts +1 -0
- package/build/tools/code-refactor/codeReuseTool.test.js +51 -0
- package/build/tools/executeSkyrampTestTool.js +8 -2
- package/build/tools/generate-tests/generateBatchScenarioRestTool.d.ts +6 -1
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +110 -17
- package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +147 -0
- package/build/tools/generate-tests/generateContractRestTool.js +11 -1
- package/build/tools/generate-tests/generateIntegrationRestTool.js +24 -1
- package/build/tools/generate-tests/generateIntegrationRestTool.test.d.ts +1 -0
- package/build/tools/generate-tests/generateIntegrationRestTool.test.js +159 -0
- package/build/tools/generate-tests/planGuard.d.ts +13 -0
- package/build/tools/generate-tests/planGuard.js +78 -0
- package/build/tools/generate-tests/planGuard.test.d.ts +1 -0
- package/build/tools/generate-tests/planGuard.test.js +185 -0
- package/build/tools/generate-tests/scenarioFileIdentity.d.ts +10 -0
- package/build/tools/generate-tests/scenarioFileIdentity.js +46 -0
- package/build/tools/generate-tests/scenarioLint.d.ts +30 -0
- package/build/tools/generate-tests/scenarioLint.js +150 -0
- package/build/tools/generate-tests/scenarioLint.test.d.ts +1 -0
- package/build/tools/generate-tests/scenarioLint.test.js +100 -0
- package/build/tools/submitReportTool.js +78 -0
- package/build/tools/submitReportTool.test.js +255 -0
- package/build/tools/test-management/analyzeChangesTool.js +55 -2
- package/build/tools/test-management/analyzeChangesTool.test.js +12 -0
- 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 +2 -0
- package/build/tools/test-management/registerTestPlanTool.js +329 -0
- package/build/tools/test-management/registerTestPlanTool.test.d.ts +1 -0
- package/build/tools/test-management/registerTestPlanTool.test.js +296 -0
- package/build/types/Recommendation.d.ts +97 -0
- package/build/types/Recommendation.js +48 -0
- package/build/types/RepositoryAnalysis.d.ts +14 -14
- package/build/types/TestExecution.d.ts +2 -0
- package/build/types/TestRecommendation.d.ts +12 -1
- package/build/types/TestRecommendation.js +26 -11
- package/build/types/TestTypes.js +1 -1
- package/build/utils/AnalysisStateManager.d.ts +47 -0
- package/build/utils/docker.test.js +1 -1
- package/build/utils/planMatchKeys.d.ts +61 -0
- package/build/utils/planMatchKeys.js +125 -0
- package/build/utils/pom-scope/import-expansion.d.ts +5 -0
- package/build/utils/pom-scope/import-expansion.js +32 -0
- package/build/utils/pom-scope/index.d.ts +39 -0
- package/build/utils/pom-scope/index.js +120 -0
- package/build/utils/pom-scope/index.test.d.ts +1 -0
- package/build/utils/pom-scope/index.test.js +239 -0
- package/build/utils/pom-scope/pom-files.d.ts +3 -0
- package/build/utils/pom-scope/pom-files.js +48 -0
- package/build/utils/pom-scope/pom-files.test.d.ts +1 -0
- package/build/utils/pom-scope/pom-files.test.js +29 -0
- package/build/utils/pom-scope/scoring.d.ts +20 -0
- package/build/utils/pom-scope/scoring.js +45 -0
- package/build/utils/pom-scope/scoring.test.d.ts +1 -0
- package/build/utils/pom-scope/scoring.test.js +39 -0
- package/build/utils/pom-scope/selector-extractor.d.ts +7 -0
- package/build/utils/pom-scope/selector-extractor.js +57 -0
- package/build/utils/pom-scope/selector-extractor.test.d.ts +1 -0
- package/build/utils/pom-scope/selector-extractor.test.js +67 -0
- package/build/utils/pom-verify/__fixtures__/af-style/asset-list.page.d.ts +5 -0
- package/build/utils/pom-verify/__fixtures__/af-style/asset-list.page.js +5 -0
- package/build/utils/pom-verify/__fixtures__/af-style/pageobjects/asset-list-page.d.ts +5 -0
- package/build/utils/pom-verify/__fixtures__/af-style/pageobjects/asset-list-page.js +9 -0
- package/build/utils/pom-verify/__fixtures__/af-style/report.iframe.page.d.ts +4 -0
- package/build/utils/pom-verify/__fixtures__/af-style/report.iframe.page.js +4 -0
- package/build/utils/pom-verify/__fixtures__/af-style/workflow-footer.page.d.ts +4 -0
- package/build/utils/pom-verify/__fixtures__/af-style/workflow-footer.page.js +4 -0
- package/build/utils/pom-verify/bindings.d.ts +19 -0
- package/build/utils/pom-verify/bindings.js +161 -0
- package/build/utils/pom-verify/bindings.test.d.ts +1 -0
- package/build/utils/pom-verify/bindings.test.js +164 -0
- package/build/utils/pom-verify/calls.d.ts +16 -0
- package/build/utils/pom-verify/calls.js +42 -0
- package/build/utils/pom-verify/calls.test.d.ts +1 -0
- package/build/utils/pom-verify/calls.test.js +61 -0
- package/build/utils/pom-verify/index.d.ts +4 -0
- package/build/utils/pom-verify/index.js +4 -0
- package/build/utils/pom-verify/resolve.d.ts +7 -0
- package/build/utils/pom-verify/resolve.js +27 -0
- package/build/utils/pom-verify/resolve.test.d.ts +1 -0
- package/build/utils/pom-verify/resolve.test.js +68 -0
- package/build/utils/pom-verify/strip.d.ts +9 -0
- package/build/utils/pom-verify/strip.js +89 -0
- package/build/utils/pom-verify/verify.d.ts +14 -0
- package/build/utils/pom-verify/verify.js +158 -0
- package/build/utils/pom-verify/verify.test.d.ts +1 -0
- package/build/utils/pom-verify/verify.test.js +325 -0
- package/build/utils/reportVerification.d.ts +61 -0
- package/build/utils/reportVerification.js +104 -0
- package/build/utils/reportVerification.test.d.ts +1 -0
- package/build/utils/reportVerification.test.js +185 -0
- package/build/utils/scenarioDrafting.js +5 -5
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/utils/workspaceAuth.d.ts +9 -1
- package/build/utils/workspaceAuth.js +25 -5
- package/build/utils/workspaceAuth.test.js +48 -0
- package/build/workspace/workspace.d.ts +20 -0
- package/build/workspace/workspace.js +4 -0
- package/build/workspace/workspace.test.js +10 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +77 -8
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/utils/isomorphic/volatileDate.js +101 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/utils.js +2 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-aszq5EdG.js → codeMirrorModule-Bzd72-bG.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-BxS7Jm4s.js → defaultSettingsView-DzxTioTK.js} +101 -101
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.D4JTTy4R.js → index.BGc30U3S.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.DaRMQKOI.js → uiMode.IaDrb29A.js} +1 -1
- 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/injectedScriptSource.ts +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/utils/isomorphic/volatileDate.ts +131 -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 +3 -3
- package/node_modules/playwright/node_modules/playwright-core/.DS_Store +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DraftedScenario } from "../../types/RepositoryAnalysis.js";
|
|
2
|
-
import { PriorityTier } from "../../types/TestRecommendation.js";
|
|
2
|
+
import { Novelty, PriorityTier } from "../../types/TestRecommendation.js";
|
|
3
3
|
export declare function buildFullRepoRecommendations(scored: Array<{
|
|
4
4
|
scenario: DraftedScenario;
|
|
5
5
|
priority: PriorityTier;
|
|
6
|
-
novelty:
|
|
6
|
+
novelty: Novelty;
|
|
7
7
|
}>, topN: number, baseUrl: string, authHeaderValue: string, authSchemeSnippet: string, authTypeValue: string, isFrontendProject?: boolean, isFrontendOnlyProject?: boolean, externalCoverage?: Set<string>): string;
|
|
@@ -51,7 +51,7 @@ Before each GENERATE tool call, confirm WHERE each key value comes from:
|
|
|
51
51
|
- **endpointURL** → workspace \`baseUrl\` + endpoint path (both required — never path alone)
|
|
52
52
|
- **authHeader / authScheme** → workspace config or OpenAPI \`securitySchemes\`
|
|
53
53
|
- **Foreign-key path params** → chained from a prior step's response — never invented or hardcoded. Common field names: \`id\`, \`uuid\`, \`_id\`, \`*_id\`; use whatever identifier field the server returns for this resource. The chaining source can be a response body (POST or GET), a response header (e.g. \`Location\`), or a cookie.
|
|
54
|
-
- **Names / string values** → realistic
|
|
54
|
+
- **Names / string values** → realistic. Do NOT hardcode a timestamp/uuid suffix. Instead, for create fields that carry a UNIQUE constraint (e.g. \`name\`, \`slug\`, \`email\` — confirm from the source schema: \`unique=True\`, SQL \`UNIQUE\`, DTO), list their field paths in the step's \`uniqueFields\` (gjson notation) — the generator injects a run-unique value so re-runs don't 409.
|
|
55
55
|
|
|
56
56
|
## Ranking Rule
|
|
57
57
|
For each GENERATE item, include one sentence in your output (before the tool calls) stating the specific bug or failure it targets — derived from \`bugCatchingTarget\` or your source-code reading. Example: "Targets: order total miscalculation — total_amount = sum(item.price × item.quantity) should recompute when items array changes."
|
|
@@ -332,7 +332,10 @@ ${authGuidance}
|
|
|
332
332
|
collection array (e.g. \`"items": [{"product_id": <chained from prior POST>, "quantity": 2}]\`).
|
|
333
333
|
Never send a PATCH that only modifies metadata (discount, status) without also including the
|
|
334
334
|
items/products collection — such a test will not catch collection-level or total-recalculation bugs.
|
|
335
|
-
|
|
335
|
+
For UNIQUE-constrained create fields, set the step's \`uniqueFields\` (gjson paths) rather than
|
|
336
|
+
hardcoding timestamp suffixes — the generator makes them run-unique so re-runs don't 409.
|
|
337
|
+
For every resource-creating step (POST/PUT), also add a matching DELETE step for the created
|
|
338
|
+
resource (path ID chained from the create response) so the scenario cleans up after itself.
|
|
336
339
|
For GET/PUT/DELETE with path IDs, use a placeholder — chaining resolves the real ID.
|
|
337
340
|
2. Produces a \`scenario_<name>.json\` in the same \`outputDir\` as the test files (not \`.skyramp/\`).
|
|
338
341
|
3. Call \`skyramp_integration_test_generation\` with \`scenarioFile\`: ${integrationAuthNote}
|
|
@@ -200,7 +200,7 @@ Budget Plan: 0 total — no new, modified, or removed endpoints were classified
|
|
|
200
200
|
|
|
201
201
|
With a 0-total Budget Plan: generate zero tests, recommend zero tests, and follow the zero-test report path. Do NOT draft baseline or generic tests for unchanged endpoints to fill a budget — an empty diff surface is a valid, expected outcome.
|
|
202
202
|
|
|
203
|
-
**Exception — claim the ceiling only with evidence:** if your code review of the changed files shows an observable API behavior change the classifier missed (e.g. a DTO/serializer/service change that alters a response shape,
|
|
203
|
+
**Exception — claim the ceiling only with evidence:** if your code review of the changed files shows an observable API behavior change the classifier missed (e.g. a DTO/serializer/service change that alters a response shape, a deployment/config change that newly exposes or removes endpoints, or a schema-defined API contract change — a CRD type/kubebuilder validation marker, GraphQL schema, or gRPC proto edit that adds, removes, or re-validates what the server accepts or returns), raise your Budget Plan to cover exactly those affected endpoints, up to ${maxTotal} total (${effectiveGenerate} generate + ${additional} additional), 0% UI/E2E. Note: repositories whose entire API surface is schema-defined (e.g. a Kubernetes operator serving CRDs through the kube-apiserver) ALWAYS classify zero endpoints — for these, a schema change in the diff IS the endpoint change; evaluate this exception against the schema files instead of concluding there is nothing to test. Every test must name the changed file that justifies it. State your raised plan now in the canonical format — \`Budget Plan: <total> total (<generate> generate + <additional> additional), 0% UI/E2E\` — and use those exact numbers throughout the rest of the prompt; the raised generate count is your committed generate count.`;
|
|
204
204
|
}
|
|
205
205
|
// Unambiguous backend-only or UI-only: emit a single Budget Plan line — no LLM counting needed.
|
|
206
206
|
if (precomputedUIPct !== undefined) {
|
|
@@ -1,6 +1,31 @@
|
|
|
1
|
-
import { RepositoryAnalysis, AnalysisScope } from "../../types/RepositoryAnalysis.js";
|
|
1
|
+
import { RepositoryAnalysis, AnalysisScope, DraftedScenario } from "../../types/RepositoryAnalysis.js";
|
|
2
2
|
import { WorkspaceAuthType } from "../../utils/workspaceAuth.js";
|
|
3
3
|
import { PRTestContext } from "../../utils/pr-comment-parser.js";
|
|
4
|
+
import { Novelty, PriorityTier } from "../../types/TestRecommendation.js";
|
|
4
5
|
import { buildExternalCoverageSet, externalDedupKey } from "./recommendationShared.js";
|
|
5
6
|
export { buildExternalCoverageSet, externalDedupKey };
|
|
7
|
+
/** Result of {@link computeScoredCandidates} — the scoring/classification
|
|
8
|
+
* inputs shared between prompt rendering and the SKYR-3879 register-plan
|
|
9
|
+
* pre-seed (analyzeChangesTool.ts), so both derive the exact same numbers. */
|
|
10
|
+
export interface ScoredCandidatesResult {
|
|
11
|
+
scored: Array<{
|
|
12
|
+
scenario: DraftedScenario;
|
|
13
|
+
priority: PriorityTier;
|
|
14
|
+
novelty: Novelty;
|
|
15
|
+
}>;
|
|
16
|
+
filteredChangedFiles: string[];
|
|
17
|
+
isUIOnlyPR: boolean;
|
|
18
|
+
hasFrontendChanges: boolean;
|
|
19
|
+
hasApiChanges: boolean;
|
|
20
|
+
maxGen: number;
|
|
21
|
+
seed: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Compute the pre-ranked scenario scoring, the UI/API classification flags,
|
|
25
|
+
* and the effective GENERATE cap — extracted from buildRecommendationPrompt
|
|
26
|
+
* so skyramp_analyze_changes (SKYR-3879 Path B pre-seed) can derive the exact
|
|
27
|
+
* same candidate set the Execution Plan prompt is built from, without
|
|
28
|
+
* duplicating (and risking drift from) this scoring logic.
|
|
29
|
+
*/
|
|
30
|
+
export declare function computeScoredCandidates(analysis: RepositoryAnalysis, analysisScope?: AnalysisScope, topN?: number, maxGenerateOverride?: number): ScoredCandidatesResult;
|
|
6
31
|
export declare function buildRecommendationPrompt(analysis: RepositoryAnalysis, analysisScope?: AnalysisScope, topN?: number, prContext?: PRTestContext, workspaceAuthHeader?: string, workspaceAuthType?: WorkspaceAuthType, workspaceAuthScheme?: string, maxGenerateOverride?: number, sessionId?: string): string;
|
|
@@ -3,7 +3,7 @@ import { AnalysisScope, isDiff, } from "../../types/RepositoryAnalysis.js";
|
|
|
3
3
|
import { WorkspaceAuthType, getDefaultAuthHeader } from "../../utils/workspaceAuth.js";
|
|
4
4
|
import { logger } from "../../utils/logger.js";
|
|
5
5
|
import { buildArchitectPreamble, buildContextFetchingGuidance, buildReasoningProtocol, buildToolWorkflows, buildFewShotExamples, buildVerificationChecklist, getAuthSnippets, MAX_TESTS_TO_GENERATE, MAX_RECOMMENDATIONS, } from "./recommendationSections.js";
|
|
6
|
-
import { CATEGORY_PRIORITY } from "../../types/TestRecommendation.js";
|
|
6
|
+
import { CATEGORY_PRIORITY, Novelty, PriorityTier } from "../../types/TestRecommendation.js";
|
|
7
7
|
import { buildScopeAssessmentSection, isFrontendFile } from "./scopeAssessment.js";
|
|
8
8
|
import { buildExecutionPlan, EXEC_STEP_CODE_REVIEW } from "./diffExecutionPlan.js";
|
|
9
9
|
import { buildFullRepoRecommendations } from "./fullRepoCatalog.js";
|
|
@@ -36,21 +36,21 @@ const PRIORITY_ORDER = { CRITICAL: 4, HIGH: 3, MEDIUM: 2, LOW: 1 };
|
|
|
36
36
|
const NOVELTY_ORDER = { new: 3, modified: 2, existing: 1 };
|
|
37
37
|
function classifyNovelty(scenario, diffContext) {
|
|
38
38
|
if (!diffContext)
|
|
39
|
-
return
|
|
39
|
+
return Novelty.EXISTING;
|
|
40
40
|
const paths = scenario.steps.map(s => s.path);
|
|
41
41
|
const newPaths = new Set((diffContext.newEndpoints || []).map(ep => ep.path));
|
|
42
42
|
const modPaths = new Set((diffContext.modifiedEndpoints || []).map(ep => ep.path));
|
|
43
43
|
const removedPaths = new Set((diffContext.removedEndpoints || []).map(ep => ep.path));
|
|
44
44
|
if (paths.some(p => newPaths.has(p)))
|
|
45
|
-
return
|
|
45
|
+
return Novelty.NEW;
|
|
46
46
|
if (paths.some(p => modPaths.has(p) || removedPaths.has(p)))
|
|
47
|
-
return
|
|
48
|
-
return
|
|
47
|
+
return Novelty.MODIFIED;
|
|
48
|
+
return Novelty.EXISTING;
|
|
49
49
|
}
|
|
50
50
|
function prioritiseCandidate(scenario, diffContext) {
|
|
51
51
|
const priority = isAttackSurfaceSecurityBoundary(scenario)
|
|
52
|
-
?
|
|
53
|
-
: CATEGORY_PRIORITY[scenario.category] ??
|
|
52
|
+
? PriorityTier.CRITICAL
|
|
53
|
+
: CATEGORY_PRIORITY[scenario.category] ?? PriorityTier.LOW;
|
|
54
54
|
const novelty = classifyNovelty(scenario, diffContext);
|
|
55
55
|
return { priority, novelty };
|
|
56
56
|
}
|
|
@@ -58,15 +58,20 @@ function computeTiebreakerSeed(endpoints, diffFiles) {
|
|
|
58
58
|
const canonical = [...endpoints].sort().join("|") + "::" + [...diffFiles].sort().join("|");
|
|
59
59
|
return crypto.createHash("sha256").update(canonical).digest("hex").slice(0, 8);
|
|
60
60
|
}
|
|
61
|
-
//
|
|
62
|
-
|
|
61
|
+
// Prevents bot-committed test files from being treated as application changes
|
|
62
|
+
// on subsequent testbot runs on the same PR.
|
|
63
|
+
const SKYRAMP_TEST_FILE_PATTERN = /(?:_test|_smoke|_contract|_fuzz|_integration|_load|_e2e|_ui)\.[^/]+$|scenario_[^/]+\.json$/;
|
|
64
|
+
/**
|
|
65
|
+
* Compute the pre-ranked scenario scoring, the UI/API classification flags,
|
|
66
|
+
* and the effective GENERATE cap — extracted from buildRecommendationPrompt
|
|
67
|
+
* so skyramp_analyze_changes (SKYR-3879 Path B pre-seed) can derive the exact
|
|
68
|
+
* same candidate set the Execution Plan prompt is built from, without
|
|
69
|
+
* duplicating (and risking drift from) this scoring logic.
|
|
70
|
+
*/
|
|
71
|
+
export function computeScoredCandidates(analysis, analysisScope = AnalysisScope.FullRepo, topN = MAX_RECOMMENDATIONS, maxGenerateOverride) {
|
|
63
72
|
const isDiffScope = isDiff(analysisScope);
|
|
64
73
|
const diffContext = analysis.branchDiffContext;
|
|
65
|
-
const openApiSpec = analysis.artifacts?.openApiSpecs?.[0];
|
|
66
74
|
// ── Filter out bot-generated test files from changedFiles ──
|
|
67
|
-
// Prevents bot-committed test files from being treated as application changes
|
|
68
|
-
// on subsequent testbot runs on the same PR.
|
|
69
|
-
const SKYRAMP_TEST_FILE_PATTERN = /(?:_test|_smoke|_contract|_fuzz|_integration|_load|_e2e|_ui)\.[^/]+$|scenario_[^/]+\.json$/;
|
|
70
75
|
const filteredChangedFiles = diffContext
|
|
71
76
|
? diffContext.changedFiles.filter(f => !SKYRAMP_TEST_FILE_PATTERN.test(f))
|
|
72
77
|
: [];
|
|
@@ -81,6 +86,56 @@ export function buildRecommendationPrompt(analysis, analysisScope = AnalysisScop
|
|
|
81
86
|
? (diffContext.newEndpoints.length > 0 || diffContext.modifiedEndpoints.length > 0 || (diffContext.removedEndpoints?.length ?? 0) > 0)
|
|
82
87
|
: false;
|
|
83
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);
|
|
84
139
|
const hasTraces = (analysis.artifacts?.traceFiles?.length ?? 0) > 0 ||
|
|
85
140
|
(analysis.artifacts?.playwrightRecordings?.length ?? 0) > 0;
|
|
86
141
|
// ── Mode preamble ──
|
|
@@ -277,50 +332,7 @@ ${isDiffScope ? "Changed endpoints only. " : ""}Use source code schemas (Zod/Pyd
|
|
|
277
332
|
${detailBlocks}
|
|
278
333
|
`;
|
|
279
334
|
}
|
|
280
|
-
// ── Scoring ──
|
|
281
335
|
const endpointCount = allEndpoints.reduce((acc, ep) => acc + (ep.methods ?? []).length, 0);
|
|
282
|
-
const baseMaxGen = Math.min(Math.max(maxGenerateOverride ?? (isDiffScope ? MAX_TESTS_TO_GENERATE : topN), 0), topN);
|
|
283
|
-
const maxGen = isUIOnlyPR ? Math.max(baseMaxGen, 1) : baseMaxGen;
|
|
284
|
-
const scenarios = analysis.businessContext.draftedScenarios;
|
|
285
|
-
let scored = [];
|
|
286
|
-
let seed = "";
|
|
287
|
-
if (!isUIOnlyPR && scenarios.length > 0) {
|
|
288
|
-
const diffFiles = filteredChangedFiles; // use filtered list so bot-committed test files don't shift the seed
|
|
289
|
-
const endpointPaths = allEndpoints.map(ep => ep.path);
|
|
290
|
-
seed = computeTiebreakerSeed(endpointPaths, diffFiles);
|
|
291
|
-
scored = scenarios.map(s => {
|
|
292
|
-
const result = prioritiseCandidate(s, diffContext ?? undefined);
|
|
293
|
-
return { scenario: s, ...result };
|
|
294
|
-
});
|
|
295
|
-
scored.sort((a, b) => {
|
|
296
|
-
const pa = PRIORITY_ORDER[a.priority], pb = PRIORITY_ORDER[b.priority];
|
|
297
|
-
if (pb !== pa)
|
|
298
|
-
return pb - pa;
|
|
299
|
-
const na = NOVELTY_ORDER[a.novelty], nb = NOVELTY_ORDER[b.novelty];
|
|
300
|
-
if (nb !== na)
|
|
301
|
-
return nb - na;
|
|
302
|
-
const crossA = a.scenario.steps.length > 2 ? 1 : 0;
|
|
303
|
-
const crossB = b.scenario.steps.length > 2 ? 1 : 0;
|
|
304
|
-
if (crossB !== crossA)
|
|
305
|
-
return crossB - crossA;
|
|
306
|
-
if (b.scenario.steps.length !== a.scenario.steps.length)
|
|
307
|
-
return b.scenario.steps.length - a.scenario.steps.length;
|
|
308
|
-
const errorA = a.scenario.steps.some(s => s.interactionType === "error" || s.interactionType === "edge-case") ? 1 : 0;
|
|
309
|
-
const errorB = b.scenario.steps.some(s => s.interactionType === "error" || s.interactionType === "edge-case") ? 1 : 0;
|
|
310
|
-
if (errorB !== errorA)
|
|
311
|
-
return errorB - errorA;
|
|
312
|
-
// Use locale-independent comparison to avoid runtime-locale non-determinism
|
|
313
|
-
const nameA = a.scenario.scenarioName;
|
|
314
|
-
const nameB = b.scenario.scenarioName;
|
|
315
|
-
if (nameA < nameB)
|
|
316
|
-
return -1;
|
|
317
|
-
if (nameA > nameB)
|
|
318
|
-
return 1;
|
|
319
|
-
const hashA = parseInt(crypto.createHash("sha256").update(seed + a.scenario.scenarioName).digest("hex").slice(0, 8), 16);
|
|
320
|
-
const hashB = parseInt(crypto.createHash("sha256").update(seed + b.scenario.scenarioName).digest("hex").slice(0, 8), 16);
|
|
321
|
-
return hashA - hashB;
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
336
|
// ── Main section: execution plan, UI-only guidance, or draft-your-own ──
|
|
325
337
|
let mainSection;
|
|
326
338
|
if (!isDiffScope && scored.length > 0) {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { jest } from "@jest/globals";
|
|
2
|
+
import { Novelty, PriorityTier } from "../../types/TestRecommendation.js";
|
|
2
3
|
import { TestType } from "../../types/TestTypes.js";
|
|
3
4
|
jest.unstable_mockModule("@skyramp/skyramp", () => ({
|
|
4
5
|
WorkspaceConfigManager: { create: jest.fn() },
|
|
5
6
|
}));
|
|
6
7
|
const { buildRecommendationPrompt, buildExternalCoverageSet, externalDedupKey } = await import("./test-recommendation-prompt.js");
|
|
7
|
-
const { buildExecutionPlan } = await import("./diffExecutionPlan.js");
|
|
8
|
+
const { buildExecutionPlan, EXEC_STEP_REGISTER } = await import("./diffExecutionPlan.js");
|
|
8
9
|
const { PATH_PARAM_UUID_GUIDANCE, MAX_TESTS_TO_GENERATE, buildTestQualityCriteria, buildArchitectPreamble, buildContextFetchingGuidance, buildReasoningProtocol, buildFewShotExamples, buildVerificationChecklist, } = await import("./recommendationSections.js");
|
|
9
10
|
import { AnalysisScope } from "../../types/RepositoryAnalysis.js";
|
|
10
11
|
// ---------------------------------------------------------------------------
|
|
@@ -746,6 +747,42 @@ describe("buildRecommendationPrompt — zero-classified diff (SKYR-3820)", () =>
|
|
|
746
747
|
});
|
|
747
748
|
});
|
|
748
749
|
// ---------------------------------------------------------------------------
|
|
750
|
+
// Tests — REGISTER step (SKYR-3879 Path B checkpoint)
|
|
751
|
+
// ---------------------------------------------------------------------------
|
|
752
|
+
describe("buildRecommendationPrompt — REGISTER step (SKYR-3879 Path B)", () => {
|
|
753
|
+
it("diff-scope execution plan includes the REGISTER step and its skyramp_register_test_plan instruction", () => {
|
|
754
|
+
const prompt = buildRecommendationPrompt(minimalAnalysis(), AnalysisScope.CurrentBranchDiff, 5);
|
|
755
|
+
expect(prompt).toContain(`Step ${EXEC_STEP_REGISTER} — Register your test plan`);
|
|
756
|
+
expect(prompt).toContain("skyramp_register_test_plan");
|
|
757
|
+
});
|
|
758
|
+
it("diff-scope execution plan's GENERATE header references the REGISTER step", () => {
|
|
759
|
+
const prompt = buildRecommendationPrompt(minimalAnalysis(), AnalysisScope.CurrentBranchDiff, 5);
|
|
760
|
+
expect(prompt).toContain(`registering via Step ${EXEC_STEP_REGISTER}`);
|
|
761
|
+
});
|
|
762
|
+
it("full-repo mode (no execution plan) never mentions skyramp_register_test_plan", () => {
|
|
763
|
+
const scenariosForFullRepo = [
|
|
764
|
+
minimalScenario({
|
|
765
|
+
scenarioName: "scenario-0",
|
|
766
|
+
category: "crud",
|
|
767
|
+
priority: "low",
|
|
768
|
+
testType: TestType.CONTRACT,
|
|
769
|
+
steps: [{ order: 1, method: "GET", path: "/api/items", description: "Get items", interactionType: "success", expectedStatusCode: 200 }],
|
|
770
|
+
}),
|
|
771
|
+
];
|
|
772
|
+
const analysis = minimalAnalysis({
|
|
773
|
+
businessContext: { mainPurpose: "Test API", userFlows: [], dataFlows: [], integrationPatterns: [], draftedScenarios: scenariosForFullRepo },
|
|
774
|
+
});
|
|
775
|
+
const prompt = buildRecommendationPrompt(analysis, AnalysisScope.FullRepo, 6);
|
|
776
|
+
expect(prompt).not.toContain("skyramp_register_test_plan");
|
|
777
|
+
});
|
|
778
|
+
it("diff-scope 'draft your own' fallback (no pre-drafted scenarios) still includes the REGISTER step — the execution plan is always used in diff scope", () => {
|
|
779
|
+
const prompt = buildRecommendationPrompt(minimalAnalysis(), AnalysisScope.CurrentBranchDiff, 5);
|
|
780
|
+
// minimalAnalysis() has no draftedScenarios and no branchDiffContext, so scored=[]
|
|
781
|
+
// but buildExecutionPlan still renders (see buildRecommendationPrompt's isDiffScope branch).
|
|
782
|
+
expect(prompt).toContain("skyramp_register_test_plan");
|
|
783
|
+
});
|
|
784
|
+
});
|
|
785
|
+
// ---------------------------------------------------------------------------
|
|
749
786
|
// Tests — GENERATE slot allocation (UI vs backend slots)
|
|
750
787
|
// ---------------------------------------------------------------------------
|
|
751
788
|
describe("buildRecommendationPrompt — GENERATE slot allocation", () => {
|
|
@@ -920,13 +957,13 @@ describe("buildRecommendationPrompt — GENERATE slot allocation", () => {
|
|
|
920
957
|
});
|
|
921
958
|
const prompt = buildExecutionPlan([attackSurface, bugCaught, ordinary].map((scenario) => ({
|
|
922
959
|
scenario,
|
|
923
|
-
priority: scenario.category === "crud" ?
|
|
924
|
-
novelty:
|
|
960
|
+
priority: scenario.category === "crud" ? PriorityTier.LOW : PriorityTier.CRITICAL,
|
|
961
|
+
novelty: Novelty.MODIFIED,
|
|
925
962
|
})), 3, 3, "http://localhost:3000", "Authorization", ", authScheme: \"Bearer\"", "", "seed", 3, false, false, false, new Set(["POST::flows::contract", "POST::orders::contract", "POST::customers::contract"]));
|
|
926
963
|
expect(prompt).toContain("POST /api/flows/bulk_delete → 401");
|
|
927
964
|
expect(prompt).toContain("POST /api/orders → 201");
|
|
928
965
|
expect(prompt).not.toContain("POST /api/customers → 201");
|
|
929
|
-
expect(prompt).toContain("
|
|
966
|
+
expect(prompt).toContain("`bug_caught` and attack-surface `security_boundary` items get the strictest reading");
|
|
930
967
|
});
|
|
931
968
|
it("keeps symmetric attack-surface siblings together before ordinary auth boundaries", () => {
|
|
932
969
|
const directWidgets = minimalScenario({
|
|
@@ -959,8 +996,8 @@ describe("buildRecommendationPrompt — GENERATE slot allocation", () => {
|
|
|
959
996
|
});
|
|
960
997
|
const prompt = buildExecutionPlan([directWidgets, directGadgets, widgetsBulk, gadgetsBulk].map((scenario) => ({
|
|
961
998
|
scenario,
|
|
962
|
-
priority:
|
|
963
|
-
novelty:
|
|
999
|
+
priority: PriorityTier.CRITICAL,
|
|
1000
|
+
novelty: Novelty.MODIFIED,
|
|
964
1001
|
})), 3, 4, "http://localhost:3000", "Authorization", ", authScheme: \"Bearer\"", "", "seed", 4, false);
|
|
965
1002
|
const generatedBlock = prompt.slice(0, prompt.indexOf("#4 [ADDITIONAL]"));
|
|
966
1003
|
const additionalBlock = prompt.slice(prompt.indexOf("#4 [ADDITIONAL]"));
|
|
@@ -975,7 +1012,7 @@ describe("buildExecutionPlan — even-with-spillover test-type distribution", ()
|
|
|
975
1012
|
// Backend-only PR (hasFrontendChanges=false) so every GENERATE slot is a backend
|
|
976
1013
|
// scenario drawn from `scored` — this is where contract-vs-integration distribution
|
|
977
1014
|
// applies. baseUrl/auth args mirror the existing buildExecutionPlan tests above.
|
|
978
|
-
const contract = (name, path, priority =
|
|
1015
|
+
const contract = (name, path, priority = PriorityTier.HIGH) => ({
|
|
979
1016
|
scenario: minimalScenario({
|
|
980
1017
|
scenarioName: name,
|
|
981
1018
|
category: "crud",
|
|
@@ -983,9 +1020,9 @@ describe("buildExecutionPlan — even-with-spillover test-type distribution", ()
|
|
|
983
1020
|
steps: [{ order: 1, method: "POST", path, description: `POST ${path}`, interactionType: "success", expectedStatusCode: 201 }],
|
|
984
1021
|
}),
|
|
985
1022
|
priority: priority,
|
|
986
|
-
novelty:
|
|
1023
|
+
novelty: Novelty.NEW,
|
|
987
1024
|
});
|
|
988
|
-
const integration = (name, p1, p2, priority =
|
|
1025
|
+
const integration = (name, p1, p2, priority = PriorityTier.HIGH) => ({
|
|
989
1026
|
scenario: minimalScenario({
|
|
990
1027
|
scenarioName: name,
|
|
991
1028
|
category: "data_integrity",
|
|
@@ -996,7 +1033,7 @@ describe("buildExecutionPlan — even-with-spillover test-type distribution", ()
|
|
|
996
1033
|
],
|
|
997
1034
|
}),
|
|
998
1035
|
priority: priority,
|
|
999
|
-
novelty:
|
|
1036
|
+
novelty: Novelty.NEW,
|
|
1000
1037
|
});
|
|
1001
1038
|
const plan = (items, maxGen, topN) => buildExecutionPlan(items, maxGen, topN, "http://localhost:3000", "Authorization", ", authScheme: \"Bearer\"", "", "seed", items.length, false);
|
|
1002
1039
|
it("distributes GENERATE slots across contract AND integration when budget < candidates", () => {
|
|
@@ -1025,7 +1062,7 @@ describe("buildExecutionPlan — even-with-spillover test-type distribution", ()
|
|
|
1025
1062
|
// A CRITICAL integration sits after several HIGH contracts in rank order.
|
|
1026
1063
|
const items = [
|
|
1027
1064
|
contract("c1", "/api/a"), contract("c2", "/api/b"), contract("c3", "/api/c"),
|
|
1028
|
-
integration("crit", "/api/orders", "/api/orders/{id}",
|
|
1065
|
+
integration("crit", "/api/orders", "/api/orders/{id}", PriorityTier.CRITICAL),
|
|
1029
1066
|
];
|
|
1030
1067
|
const prompt = plan(items, 3, 6);
|
|
1031
1068
|
const generated = prompt.slice(0, prompt.indexOf("[ADDITIONAL]") >= 0 ? prompt.indexOf("[ADDITIONAL]") : prompt.length);
|
|
@@ -16,7 +16,7 @@ export interface RelatedRepository {
|
|
|
16
16
|
*/
|
|
17
17
|
export declare function parseRelatedRepositories(raw: string | undefined): RelatedRepository[] | undefined;
|
|
18
18
|
export declare function getTestbotPrompt(prTitle: string, prDescription: string, summaryOutputFile: string, repositoryPath: string, baseBranch?: string, maxRecommendations?: number, maxGenerate?: number, _maxCritical?: number, // Reserved — accepted for API compat but not yet wired into prompt
|
|
19
|
-
prNumber?: number, userPrompt?: string, services?: Service[], uiCredentials?: string, testsRepoDir?: string, relatedRepositories?: RelatedRepository[], primaryRepo?: string): string;
|
|
19
|
+
prNumber?: number, userPrompt?: string, services?: Service[], uiCredentials?: string, testsRepoDir?: string, relatedRepositories?: RelatedRepository[], primaryRepo?: string, planOnly?: boolean): string;
|
|
20
20
|
/**
|
|
21
21
|
* Read services from .skyramp/workspace.yml. Returns empty array if
|
|
22
22
|
* the workspace file doesn't exist or can't be parsed.
|
|
@@ -47,7 +47,7 @@ export function parseRelatedRepositories(raw) {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
export function getTestbotPrompt(prTitle, prDescription, summaryOutputFile, repositoryPath, baseBranch, maxRecommendations = MAX_RECOMMENDATIONS, maxGenerate = MAX_TESTS_TO_GENERATE, _maxCritical = MAX_CRITICAL_TESTS, // Reserved — accepted for API compat but not yet wired into prompt
|
|
50
|
-
prNumber, userPrompt, services, uiCredentials, testsRepoDir, relatedRepositories, primaryRepo) {
|
|
50
|
+
prNumber, userPrompt, services, uiCredentials, testsRepoDir, relatedRepositories, primaryRepo, planOnly = false) {
|
|
51
51
|
maxGenerate = Math.min(Math.max(maxGenerate, 0), maxRecommendations);
|
|
52
52
|
// TODO(SKYR-3636 follow-up): migrate Task 1 + Task 2 step bodies to PromptPlan
|
|
53
53
|
// (src/prompts/test-recommendation/promptPlan.ts) so step numbers don't have
|
|
@@ -65,6 +65,17 @@ prNumber, userPrompt, services, uiCredentials, testsRepoDir, relatedRepositories
|
|
|
65
65
|
// testDirectory, relative to the delivery root (the configured test repo if set,
|
|
66
66
|
// otherwise the primary repo).
|
|
67
67
|
const hasRelatedRepos = !!relatedRepositories?.length;
|
|
68
|
+
// Task 1 maintenance step 2c branches in plan-only eval runs (SKYR-3879
|
|
69
|
+
// plan-only lane): the SUT is not running, so the pre-edit baseline
|
|
70
|
+
// execution is replaced by static-analysis-only verdicts. Defined before
|
|
71
|
+
// task1Section, which interpolates it.
|
|
72
|
+
let maintenanceBeforeExecStep;
|
|
73
|
+
if (planOnly) {
|
|
74
|
+
maintenanceBeforeExecStep = ` c. Plan-only run: skip the pre-edit baseline execution — the application is not running. Record every maintenance verdict from static drift analysis alone; execution statuses simply remain unrecorded.`;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
maintenanceBeforeExecStep = ` c. Call \`skyramp_execute_test\` with \`phase: "before"\` and \`stateFile\` for every UPDATE/REGENERATE/DELETE test. Exclude tests marked \`[external]\`. Run them sequentially, not in parallel. This captures the pre-edit baseline — do not skip even if you expect the test to fail.`;
|
|
78
|
+
}
|
|
68
79
|
// For follow-up requests: emit the @skyramp-testbot header + guardrails + retrieve-recommendations step.
|
|
69
80
|
// For first-run prompts: emit the full Task 1 analysis + maintenance section.
|
|
70
81
|
const task1Section = userPrompt
|
|
@@ -131,7 +142,7 @@ ${hasRelatedRepos ? `
|
|
|
131
142
|
|
|
132
143
|
b. Write \`updateInstructions\` for each UPDATE or REGENERATE test before calling \`skyramp_actions\` — articulating the change first prevents file content from overriding diff-based reasoning.
|
|
133
144
|
|
|
134
|
-
|
|
145
|
+
${maintenanceBeforeExecStep}
|
|
135
146
|
|
|
136
147
|
d. Call \`skyramp_actions\` with \`stateFile\` (from \`skyramp_analyze_changes\` output) and apply the edits it returns.
|
|
137
148
|
|
|
@@ -343,19 +354,37 @@ ${hasRelatedRepos ? `
|
|
|
343
354
|
const testDirInstruction = testsRepoDir
|
|
344
355
|
? `the \`<output_dir>\` from the \`<services>\` block, rooted under the test repository at \`${testsRepoDir}\` (i.e. \`${testsRepoDir}/<output_dir>\`). Write ALL test output files to paths under \`${testsRepoDir}\`, not under \`${repositoryPath}\`. Do NOT write any test files to the app repository.`
|
|
345
356
|
: `${SERVICE_REFS.testDirRef}. Do NOT create a new \`tests/\` directory at the repo root — use that path. If no \`testDirectory\` is configured, default to the language-conventional location (e.g. \`src/test/java/...\` for Java, \`tests/\` for Python).`;
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
357
|
+
// Task 3's non-zero-budget count rule branches in plan-only eval runs: the
|
|
358
|
+
// report DECLARES the final GENERATE selection instead of recording
|
|
359
|
+
// generated files. The zero-surface abstention rules above it are shared.
|
|
360
|
+
let task3CountRule;
|
|
361
|
+
if (planOnly) {
|
|
362
|
+
task3CountRule = `Otherwise (your Budget Plan is non-zero): this is a plan-only run — \`newTestsCreated\` DECLARES your final GENERATE selection instead of recording generated files: exactly one entry per item of your final GENERATE list (the list returned by \`skyramp_register_test_plan\` when that tool was available — it may contain fewer items than your committed budget — otherwise your Budget Plan selection, at most ${maxGenerate}), with \`fileName\` set to the file name you would have used. \`testResults\` must be \`[]\` — nothing was executed. The declaration itself is the deliverable; do not generate or backfill.`;
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
task3CountRule = `Otherwise (your Budget Plan is non-zero): in \`newTestsCreated\`, you must have exactly as many budget-counting new tests as your committed Budget Plan's generate count (at most ${maxGenerate}). Only new files (ADD) created for the planned GENERATE items count toward this target — GENERATE items converted to UPDATE do not. You may also include at most one additional discovered-scenario file in \`newTestsCreated\` (the bug-catching test generated after all planned items); that extra test does **not** count against the budget. If you have fewer budget-counting new tests than your generate count, backfill from the remaining ADDITIONAL candidates before proceeding. Only proceed with fewer if all candidates failed after retry AND the fallback single-contract test also failed.`;
|
|
366
|
+
}
|
|
367
|
+
// Task 2 branches wholesale in plan-only eval runs (SKYR-3879 plan-only
|
|
368
|
+
// lane): the standard task mandates generation and execution, which a
|
|
369
|
+
// plan-only run must not do — branching the section (rather than appending
|
|
370
|
+
// an override) means the agent never sees conflicting instructions.
|
|
371
|
+
let task2Section;
|
|
372
|
+
if (planOnly) {
|
|
373
|
+
task2Section = `## Task 2: Commit the Test Plan (plan-only run)
|
|
374
|
+
|
|
375
|
+
This is a plan-only evaluation run: the application under test is NOT running, and this run evaluates test SELECTION only. Nothing is generated or executed in this task.
|
|
376
|
+
|
|
377
|
+
- Draft your complete candidate list exactly as the Execution Plan directs — every API test (contract / integration / batch-scenario) you would generate OR recommend for this PR, grounded in the analysis output and the diff. Favor tests that would FAIL if the changed logic were buggy, not just tests that exercise the new surface.
|
|
378
|
+
- If a tool named \`skyramp_register_test_plan\` is available, call it with the full candidate union — include a \`discriminator\` claim \`{kind, changedCodeAnchor}\` for every candidate that probes changed logic — and treat its returned GENERATE list as your final selection. If that tool is not available, commit to your Budget Plan's GENERATE selection (at most ${maxGenerate}).
|
|
379
|
+
- Skip UI and E2E candidates entirely — with no running app there are no blueprints to ground them, and this lane evaluates API test selection only.
|
|
380
|
+
- Take no other actions in this task: no test generation tools, no browser traces or blueprint captures, no test files written, no test executions. Proceed directly to ${taskRef(TASK_SUBMIT)}.`;
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
task2Section = `## Task 2: Generate New Tests
|
|
355
384
|
|
|
356
385
|
${userPrompt ? "Generate only the tests that the user requested from the Additional Recommendations. The rules below still apply." : "Drift-based maintenance (Task 1) is complete. This step only processes the GENERATE list. Exception: if a GENERATE item targets a resource with an existing `[skyramp]` contract test, UPDATE that test file (see covered-resource handling below) — a new test case added to an existing file counts toward the budget and is reported in `newTestsCreated`."}
|
|
357
386
|
|
|
358
|
-
- **MANDATORY — use the pre-ranked GENERATE list
|
|
387
|
+
- **MANDATORY — use the plan returned by \`skyramp_register_test_plan\` as-is**: Before generating anything, call \`skyramp_register_test_plan\` (\`stateFile\` required) with your complete candidate list — every test you would generate OR recommend, including the Execution Plan's own pre-ranked GENERATE/ADDITIONAL items and any candidate you drafted yourself, with a \`discriminator\` claim \`{kind, changedCodeAnchor}\` for candidates probing changed logic. Its returned GENERATE list — not the Execution Plan's raw pre-ranked GENERATE section — governs ADD actions from this point on. You MUST generate exactly those scenarios in the exact order listed, keeping each item's \`scenarioName\` exactly as registered — the generation tools match on it and reject renamed or substituted scenarios. If parameter grounding uncovers a distinct bug-catching scenario not already registered, generate it after all planned GENERATE items are complete and report it in \`newTestsCreated\` — this is an additional test driven by source-code analysis and does not count against the GENERATE budget.${hasRelatedRepos ? `\n - **Multi-repo exception:** this run has related repositories, so the per-repo GENERATE lists are NOT final — they are candidates re-selected by the cross-repo round-robin described in Task 1's "Cross-repo test generation". Register the pooled, type-distributed selection instead of any single repo's GENERATE list. (In single-repo runs, register the GENERATE list exactly as-is.)` : ""}
|
|
359
388
|
- **Do not fabricate tests outside the GENERATE list.** New test files cover NEW observable surface only — a new endpoint, or a newly-integrated component/route not already covered by an existing test. Changes that only modify, delete, or add fields to an EXISTING covered endpoint or component are maintenance: handle them in ${taskRef(TASK_ANALYZE_MAINTAIN)} by UPDATE/DELETE of the existing test, never by creating a new spec. If the GENERATE list is empty (deletion-only, cosmetic, or modification-of-existing PRs with no new surface), create zero new tests and proceed to ${taskRef(TASK_SUBMIT)} — do not invent a new spec to have something to report.
|
|
360
389
|
- Scenario JSON files are always new files — always generate them for new methods. Every generated scenario JSON must have a corresponding new integration test generated from it via \`skyramp_integration_test_generation\`.
|
|
361
390
|
- Covered-resource handling (aligns with Execution Plan Step 0): When a GENERATE item targets a resource that already has an existing test file covering the same endpoint:
|
|
@@ -416,7 +445,7 @@ ${userPrompt ? "Generate only the tests that the user requested from the Additio
|
|
|
416
445
|
${CONTRACT_MODE_GUIDANCE}
|
|
417
446
|
- ${PATH_PARAM_UUID_GUIDANCE}
|
|
418
447
|
- **UI**: First check for existing Playwright trace \`.zip\` files in the repo (Testbot scans recursively up to 5 directory levels — the per-service output directories, \`frontend/\`, \`public/\`, \`.skyramp/\`, or any subdirectory).
|
|
419
|
-
If a relevant trace exists (covers the UI changes in this PR), use it directly with \`skyramp_ui_test_generation\` and \`
|
|
448
|
+
If a relevant trace exists (covers the UI changes in this PR), use it directly with \`skyramp_ui_test_generation\`, \`modularizeCode: false\`, and \`codeReuse: true\` (when generating a TypeScript/JavaScript Playwright test — the default; leave \`codeReuse\` unset for other languages).
|
|
420
449
|
If NO relevant trace exists, **you MUST write out your full trace plan as text BEFORE calling \`browser_navigate\`**. Do not touch the browser until the plan is written.
|
|
421
450
|
|
|
422
451
|
**Browser authentication (check BEFORE navigating)**: If \`<ui-credentials>\` appears in your context above, the app requires login. Parse the credentials — one per line, two supported formats:
|
|
@@ -460,7 +489,7 @@ ${CONTRACT_MODE_GUIDANCE}
|
|
|
460
489
|
Follow the **UI Recording Workflow** section at the end of this prompt. Additional CI constraints:
|
|
461
490
|
- Navigate **directly** to the deepest relevant URL (e.g. \`/orders/1/edit\` instead of \`/\` then \`/orders\` then \`/orders/1\`) — minimize multi-hop navigation so the trace stays focused on the scenario under test.
|
|
462
491
|
- \`skyramp_export_zip\` outputPath: \`${repositoryPath}/.skyramp/<test_name>_trace.zip\`
|
|
463
|
-
- \`skyramp_ui_test_generation\`: set \`modularizeCode: false\`
|
|
492
|
+
- \`skyramp_ui_test_generation\`: set \`modularizeCode: false\` and \`codeReuse: true\` (TypeScript/JavaScript Playwright only — the default; leave \`codeReuse\` unset for other languages)
|
|
464
493
|
- **\`browser_assert\` — MANDATORY**: at least one per page navigated. Call multiple assertions in the same tool call batch when checking independent elements. If you navigate to 2 pages, assert on both. Each assertion should verify a business outcome (state change, computed value, error condition) — not just that an element is visible.
|
|
465
494
|
- **\`browser_visual_snapshot\` — for visual/appearance checks**: when the instruction asks to take a screenshot, capture a baseline, or verify how a page/element/region *looks* (not its text or value), call \`browser_visual_snapshot\` — it records a \`toHaveScreenshot()\` assertion so the generated test pixel-compares against a baseline on every run. Do NOT use \`browser_take_screenshot\` for this: it captures a throwaway image that is dropped at export and never appears in the generated test (use it only to view the page yourself).
|
|
466
495
|
- **Wait for stable state before the second capture**: After performing an action that affects computed fields (filling a discount, submitting a form, adding an item), check the current page state before calling the second \`browser_blueprint\` (the capture after the action). If a computed field — total, price, count, derived text — still shows its initial empty or zero value (e.g. \`$0.00\`, \`0\`, \`Loading...\`, empty string), that means async data hasn't finished loading yet. Use \`browser_wait_for\` to wait up to 10 seconds for the field to update to a real value (for example, wait for the total to show a non-zero amount like \`$799.99\` instead of \`$0.00\`). Once the field shows a real value, THEN call the second \`browser_blueprint\` to capture stable state. If after 10 seconds the field still hasn't updated, skip the assertion on that field — don't capture and assert a value that hasn't loaded.
|
|
@@ -512,7 +541,7 @@ ${CONTRACT_MODE_GUIDANCE}
|
|
|
512
541
|
|
|
513
542
|
**The Blueprint Citation Invariant applies during recording too.** Every assertion you emit cites element names — those names must come from blueprint captures, not invention. For N user-intent-level actions, the reference target is N+1 \`browser_blueprint\` calls (the first returns full, the rest return deltas). Traces that follow the pattern produce assertions grounded in observable state changes; traces that skip captures fall back to author-inferred assertions and risk citing names that don't exist in the rendered DOM.
|
|
514
543
|
|
|
515
|
-
The rest of the UI workflow stays the same: trace plan, browser auth, navigation, export (\`skyramp_export_zip\`), generation (\`skyramp_ui_test_generation\`), \`skyramp_enhance_assertions\` post-
|
|
544
|
+
The rest of the UI workflow stays the same: trace plan, browser auth, navigation, export (\`skyramp_export_zip\`), generation (\`skyramp_ui_test_generation\`), then \`skyramp_reuse_code\` (when \`codeReuse: true\`) and \`skyramp_enhance_assertions\` post-calls. Capture-act-capture adds blueprint captures alongside the existing steps; it doesn't replace anything.
|
|
516
545
|
- **E2E**: Only if BOTH a backend trace \`.json\` AND a Playwright \`.zip\` already exist in the repo. Without both, move to \`additionalRecommendations\`.
|
|
517
546
|
- Skip smoke tests entirely.
|
|
518
547
|
|
|
@@ -545,13 +574,24 @@ Do NOT use \`page.waitForTimeout()\` with fixed delays. Do NOT retry more than o
|
|
|
545
574
|
**After generation, you MUST do exactly these steps — nothing more, nothing less:**
|
|
546
575
|
1. **[MANDATORY] After \`skyramp_integration_test_generation\`**: Call \`skyramp_enhance_assertions\` with \`testFile\` set to the absolute path of the generated integration test file, \`testType: "integration"\`, and \`enhanceType: "generation"\`. Apply every instruction returned to that file.
|
|
547
576
|
2. **[MANDATORY] After \`skyramp_contract_test_generation\` with \`providerMode\`**: Call \`skyramp_enhance_assertions\` with \`testFile\` set to the absolute path of the generated provider contract test file, \`testType: "contract"\`, and \`enhanceType: "generation"\`. Apply every instruction returned to that file.
|
|
548
|
-
3. **[MANDATORY] After \`skyramp_ui_test_generation
|
|
549
|
-
4. **
|
|
550
|
-
|
|
577
|
+
3. **[MANDATORY] After \`skyramp_ui_test_generation\` with \`codeReuse: true\`**: the generation result directs you to call \`skyramp_reuse_code\` — do this BEFORE \`skyramp_enhance_assertions\`. Two outcomes: (1) a response starting "No reusable POM layer detected" — this is a normal outcome, continue immediately (do NOT retry), and note "no POM layer — reuse skipped" in that test's \`testResults\` entry \`details\`; (2) a refactoring workflow — follow it to completion INCLUDING its verification loop (\`skyramp_reuse_code\` with \`verify: true\`), finish only when it reports PASSED, and include \`verification: passed\` in that test's \`testResults\` entry \`details\` in your final report. If a reused test later fails execution and the failure points at a substituted POM call, restore the saved \`<testFile>.raw.bak\` over the test file and re-run — do NOT hand-edit the customer's POM methods (this re-run counts toward the 2-attempt execution cap — prefer this restore over the generic timeout fix-up when the failing locator came from a POM substitution).
|
|
578
|
+
4. **[MANDATORY] After \`skyramp_ui_test_generation\`**: Call \`skyramp_enhance_assertions\` with \`testFile\` set to the absolute path of the generated UI test file, \`testType: "ui"\`, and \`enhanceType: "generation"\`. Apply every instruction returned to that file. The HIGH-tier \`possibleAssertions\` from your second \`browser_blueprint\` captures (after each action) during trace recording are in your context — when the enhance instructions ask you to add assertions for state-changing actions, use those grounded candidates first (they contain exact computed values from the DOM delta, e.g. \`toHaveText('Total: $899.98')\`). Only fall back to deriving values from the test file or source code when no HIGH-tier candidate covers the action.
|
|
579
|
+
5. **Wait**: Do NOT proceed to test execution until steps 1–4 are complete and the verification checklist in the \`skyramp_enhance_assertions\` tool result has been validated for EVERY generated test file.
|
|
580
|
+
Do not make any changes other than the code-reuse refactoring (step 3) and the assertion enhancements described above. For example: do not modify auth headers, cookies, tokens, env vars, or imports that the generation tool already set correctly — those are correct by construction and changing them breaks auth or execution.
|
|
551
581
|
|
|
552
582
|
**Final execution (mandatory):** Do NOT call \`skyramp_execute_test\` until ALL maintenance edits AND ALL new test generation/enhancement are complete. Run these calls sequentially, not in parallel. Exclude tests marked \`[external]\`.
|
|
553
583
|
- Only report test results for files you actually ran.
|
|
554
|
-
**Auth**: If \`skyramp_analyze_changes\` reports an auth token or \`SKYRAMP_TEST_TOKEN\` is set, pass it in **every** \`skyramp_execute_test\` call from the first attempt — do NOT wait for a 401/403 to discover auth is needed
|
|
584
|
+
**Auth**: If \`skyramp_analyze_changes\` reports an auth token or \`SKYRAMP_TEST_TOKEN\` is set, pass it in **every** \`skyramp_execute_test\` call from the first attempt — do NOT wait for a 401/403 to discover auth is needed.`;
|
|
585
|
+
}
|
|
586
|
+
const primaryRepoBlock = primaryRepo ? `<REPOSITORY>${primaryRepo}</REPOSITORY>\n` : '';
|
|
587
|
+
return `<TITLE>${prTitle}</TITLE>
|
|
588
|
+
<DESCRIPTION>${prDescription}</DESCRIPTION>
|
|
589
|
+
${primaryRepoBlock}<REPOSITORY PATH>${repositoryPath}</REPOSITORY PATH>
|
|
590
|
+
${relatedReposBlock}${testsRepoDirBlock}${serviceContext ? serviceContext + '\n' : ''}${uiCredentialsBlock ? uiCredentialsBlock + '\n' : ''}Use the Skyramp MCP server tools for all tasks below.
|
|
591
|
+
|
|
592
|
+
${task1Section}
|
|
593
|
+
|
|
594
|
+
${task2Section}
|
|
555
595
|
|
|
556
596
|
## Task 3: Submit Report
|
|
557
597
|
|
|
@@ -571,7 +611,7 @@ In these cases:
|
|
|
571
611
|
- \`businessCaseAnalysis\` must be a one-sentence summary of what the PR actually does (do NOT leave it blank)
|
|
572
612
|
- \`additionalRecommendations\` must be \`[]\` — do NOT recommend tests for a no-surface PR
|
|
573
613
|
|
|
574
|
-
|
|
614
|
+
${task3CountRule}
|
|
575
615
|
|
|
576
616
|
Call \`skyramp_submit_report\` with \`summaryOutputFile\`: "${summaryOutputFile}" and \`stateFile\` (from \`skyramp_analyze_changes\` output) — the stateFile is required for execution outcome tracking. Field names, types, and formats are defined in the tool's parameter schema — follow them exactly.
|
|
577
617
|
|
|
@@ -683,10 +723,16 @@ export function registerTestbotPrompt(server) {
|
|
|
683
723
|
.string()
|
|
684
724
|
.optional()
|
|
685
725
|
.describe("The primary repository's owner/repo slug (where the testbot workflow runs). Used verbatim as the `repository` attribution for the primary repo's analysis, tests, and report items in multi-repo runs."),
|
|
726
|
+
planOnly: z
|
|
727
|
+
// Prompt/resource URL args arrive as strings — accept "true"/"1"
|
|
728
|
+
// alongside a real boolean; anything else (incl. undefined) is false.
|
|
729
|
+
.preprocess((v) => v === true || v === "true" || v === "1", z.boolean())
|
|
730
|
+
.optional()
|
|
731
|
+
.describe("Plan-only eval mode (eval harness only, SKYR-3879): run the full recommendation phase — analysis, maintenance verdicts, candidate drafting, plan registration — but generate and execute nothing; the report declares the final plan. Used by the eval pipeline to A/B selection changes without a running SUT."),
|
|
686
732
|
},
|
|
687
733
|
}, async (args) => {
|
|
688
734
|
const services = await readWorkspaceServices(args.repositoryPath);
|
|
689
|
-
let prompt = getTestbotPrompt(args.prTitle, args.prDescription, args.summaryOutputFile, args.repositoryPath, args.baseBranch, args.maxRecommendations, args.maxGenerate, args.maxCritical, args.prNumber, args.userPrompt, services.length ? services : undefined, args.uiCredentials, args.testsRepoDir, parseRelatedRepositories(args.relatedRepositories), args.primaryRepo);
|
|
735
|
+
let prompt = getTestbotPrompt(args.prTitle, args.prDescription, args.summaryOutputFile, args.repositoryPath, args.baseBranch, args.maxRecommendations, args.maxGenerate, args.maxCritical, args.prNumber, args.userPrompt, services.length ? services : undefined, args.uiCredentials, args.testsRepoDir, parseRelatedRepositories(args.relatedRepositories), args.primaryRepo, args.planOnly);
|
|
690
736
|
if (args.workspaceValidationFailed) {
|
|
691
737
|
prompt = buildWorkspaceRecoveryPrefix(args.repositoryPath) + prompt;
|
|
692
738
|
}
|