@skyramp/mcp 0.1.8 → 0.2.0-rc.2
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 +4 -2
- package/build/playwright/registerPlaywrightTools.js +12 -0
- package/build/playwright/traceRecordingPrompt.js +15 -0
- package/build/prompts/code-reuse.js +106 -7
- package/build/prompts/pom-aware-code-reuse.js +106 -7
- package/build/prompts/startTraceCollectionPrompts.js +37 -15
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +26 -31
- package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +40 -1
- package/build/prompts/test-maintenance/driftAnalysisSections.js +90 -86
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +286 -163
- package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +154 -45
- package/build/prompts/test-recommendation/diffExecutionPlan.js +246 -117
- package/build/prompts/test-recommendation/promptPlan.js +290 -0
- package/build/prompts/test-recommendation/promptPlan.test.js +336 -0
- package/build/prompts/test-recommendation/recommendationSections.js +4 -3
- package/build/prompts/test-recommendation/recommendationShared.js +23 -1
- package/build/prompts/test-recommendation/scopeAssessment.js +65 -14
- package/build/prompts/test-recommendation/scopeAssessment.test.js +93 -2
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +36 -12
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +316 -1
- package/build/prompts/testbot/testbot-prompts.js +73 -13
- package/build/prompts/testbot/testbot-prompts.test.js +114 -1
- package/build/resources/testbotResource.js +1 -1
- package/build/services/ScenarioGenerationService.integration.test.js +158 -0
- package/build/services/ScenarioGenerationService.js +47 -4
- package/build/services/ScenarioGenerationService.test.js +158 -22
- package/build/services/TestExecutionService.js +73 -15
- package/build/services/TestExecutionService.test.js +105 -0
- package/build/services/TestGenerationService.js +11 -1
- package/build/tools/executeSkyrampTestTool.js +1 -10
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +16 -4
- package/build/tools/generate-tests/generateIntegrationRestTool.js +2 -0
- package/build/tools/generate-tests/generateUIRestTool.js +2 -0
- package/build/tools/test-management/actionsTool.js +152 -63
- package/build/tools/test-management/analyzeChangesTool.js +178 -64
- package/build/tools/test-management/analyzeChangesTool.test.js +103 -16
- package/build/tools/test-management/analyzeTestHealthTool.js +30 -81
- package/build/tools/test-management/index.js +1 -0
- package/build/tools/test-management/uiAnalyzeChangesTool.js +149 -0
- package/build/tools/test-management/uiAnalyzeChangesTool.test.js +100 -0
- package/build/tools/trace/resolveSaveStoragePath.js +16 -0
- package/build/tools/trace/resolveSaveStoragePath.test.js +17 -0
- package/build/tools/trace/resolveSessionPaths.js +39 -0
- package/build/tools/trace/resolveSessionPaths.test.js +103 -0
- package/build/tools/trace/sessionState.js +14 -0
- package/build/tools/trace/sessionState.test.js +17 -0
- package/build/tools/trace/startTraceCollectionTool.js +84 -14
- package/build/tools/trace/stopTraceCollectionTool.js +9 -2
- package/build/types/TestAnalysis.js +50 -0
- package/build/types/TestRecommendation.js +6 -58
- package/build/types/TestTypes.js +1 -1
- package/build/utils/AnalysisStateManager.js +22 -11
- package/build/utils/branchDiff.js +11 -2
- package/build/utils/docker.test.js +1 -1
- package/build/utils/gitStaging.js +52 -3
- package/build/utils/gitStaging.test.js +19 -1
- package/build/utils/repoScanner.js +18 -10
- package/build/utils/repoScanner.test.js +92 -0
- package/build/utils/routeParsers.js +180 -25
- package/build/utils/routeParsers.test.js +180 -1
- package/build/utils/scenarioDrafting.js +220 -17
- package/build/utils/scenarioDrafting.test.js +182 -9
- package/build/utils/sourceRouteExtractor.js +806 -0
- package/build/utils/sourceRouteExtractor.test.js +565 -0
- package/build/utils/uiPageEnumerator.js +319 -0
- package/build/utils/uiPageEnumerator.test.js +422 -0
- package/build/utils/utils.js +27 -0
- package/build/utils/versions.js +1 -1
- package/build/utils/workspaceAuth.js +33 -4
- package/node_modules/playwright/ThirdPartyNotices.txt +6 -6
- package/node_modules/playwright/lib/dom-analyzer/analyze.js +111 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprint.js +1210 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprint.test.js +396 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprintCache.js +57 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprintCache.test.js +57 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.js +254 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.test.js +304 -0
- package/node_modules/playwright/lib/dom-analyzer/crawler.js +384 -0
- package/node_modules/playwright/lib/dom-analyzer/curatedWidgets.js +73 -0
- package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +43 -0
- package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +85 -0
- package/node_modules/playwright/lib/dom-analyzer/fingerprint.js +90 -0
- package/node_modules/playwright/lib/dom-analyzer/fingerprint.test.js +231 -0
- package/node_modules/playwright/lib/dom-analyzer/fingerprintAblation.fixtures.js +145 -0
- package/node_modules/playwright/lib/dom-analyzer/fingerprintAblation.test.js +41 -0
- package/node_modules/playwright/lib/dom-analyzer/graph.js +36 -0
- package/node_modules/playwright/lib/dom-analyzer/liveFingerprints.js +43 -0
- package/node_modules/playwright/lib/dom-analyzer/logicalNameResolver.js +72 -0
- package/node_modules/playwright/lib/dom-analyzer/logicalNameResolver.test.js +182 -0
- package/node_modules/playwright/lib/dom-analyzer/possibleAssertions.js +150 -0
- package/node_modules/playwright/lib/dom-analyzer/possibleAssertions.test.js +470 -0
- package/node_modules/playwright/lib/dom-analyzer/sectionGrouper.js +169 -0
- package/node_modules/playwright/lib/dom-analyzer/sectionGrouper.test.js +269 -0
- package/node_modules/playwright/lib/dom-analyzer/serialization.js +75 -0
- package/node_modules/playwright/lib/dom-analyzer/slug.js +30 -0
- package/node_modules/playwright/lib/dom-analyzer/slug.test.js +84 -0
- package/node_modules/playwright/lib/dom-analyzer/widgetContract.js +127 -0
- package/node_modules/playwright/lib/dom-analyzer/widgetContract.test.js +212 -0
- package/node_modules/playwright/lib/mcp/browser/browserContextFactory.js +3 -1
- package/node_modules/playwright/lib/mcp/browser/config.js +1 -1
- package/node_modules/playwright/lib/mcp/browser/context.js +17 -1
- package/node_modules/playwright/lib/mcp/browser/tab.js +38 -0
- package/node_modules/playwright/lib/mcp/browser/tools/domAnalyzer.js +261 -0
- package/node_modules/playwright/lib/mcp/browser/tools/keyboard.js +3 -3
- package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.js +146 -0
- package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.test.js +140 -0
- package/node_modules/playwright/lib/mcp/browser/tools/sitemap.js +226 -0
- package/node_modules/playwright/lib/mcp/browser/tools/snapshot.js +2 -2
- package/node_modules/playwright/lib/mcp/browser/tools/widgetContract.js +168 -0
- package/node_modules/playwright/lib/mcp/browser/tools.js +6 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +52 -12
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +64 -13
- package/node_modules/playwright/package.json +1 -1
- package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.3.tgz +0 -0
- package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.4.tgz +0 -0
- package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.5.tgz +0 -0
- package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.6.tgz +0 -0
- package/package.json +3 -3
- package/build/services/TestHealthService.js +0 -694
- package/build/services/TestHealthService.test.js +0 -241
- package/build/types/TestDriftAnalysis.js +0 -1
- package/build/types/TestHealth.js +0 -4
|
@@ -3,6 +3,7 @@ jest.mock("@skyramp/skyramp", () => ({
|
|
|
3
3
|
}));
|
|
4
4
|
import { TestType } from "../../types/TestTypes.js";
|
|
5
5
|
import { buildRecommendationPrompt, buildExternalCoverageSet, externalDedupKey } from "./test-recommendation-prompt.js";
|
|
6
|
+
import { buildExecutionPlan } from "./diffExecutionPlan.js";
|
|
6
7
|
import { PATH_PARAM_UUID_GUIDANCE, MAX_TESTS_TO_GENERATE, buildTestQualityCriteria, buildArchitectPreamble, buildContextFetchingGuidance, buildReasoningProtocol, buildFewShotExamples, buildVerificationChecklist, } from "./recommendationSections.js";
|
|
7
8
|
import { AnalysisScope } from "../../types/RepositoryAnalysis.js";
|
|
8
9
|
// ---------------------------------------------------------------------------
|
|
@@ -757,6 +758,156 @@ describe("buildRecommendationPrompt — GENERATE slot allocation", () => {
|
|
|
757
758
|
expect(prompt).not.toContain("ui-test-for-changed-components");
|
|
758
759
|
expect(prompt).not.toContain("ui_mixed_pr_trace.zip");
|
|
759
760
|
});
|
|
761
|
+
it("promotes attack-surface security boundaries into generated slots", () => {
|
|
762
|
+
const attackSurface = minimalScenario({
|
|
763
|
+
scenarioName: "flows-bulk-delete-post-auth-boundary",
|
|
764
|
+
description: "Attack-surface auth boundary: /api/flows/bulk_delete is a destructive sibling of changed DELETE /api/flows/{id}; verify it rejects missing authentication",
|
|
765
|
+
category: "security_boundary",
|
|
766
|
+
priority: "high",
|
|
767
|
+
testType: TestType.CONTRACT,
|
|
768
|
+
steps: [{
|
|
769
|
+
order: 1,
|
|
770
|
+
method: "POST",
|
|
771
|
+
path: "/api/flows/bulk_delete",
|
|
772
|
+
description: "POST /api/flows/bulk_delete without auth",
|
|
773
|
+
interactionType: "error",
|
|
774
|
+
expectedStatusCode: 401,
|
|
775
|
+
}],
|
|
776
|
+
});
|
|
777
|
+
const directDelete = minimalScenario({
|
|
778
|
+
scenarioName: "flows-delete-auth-boundary",
|
|
779
|
+
description: "Auth boundary: DELETE /api/flows/{id} rejects missing authentication",
|
|
780
|
+
category: "security_boundary",
|
|
781
|
+
priority: "high",
|
|
782
|
+
testType: TestType.CONTRACT,
|
|
783
|
+
steps: [{
|
|
784
|
+
order: 1,
|
|
785
|
+
method: "DELETE",
|
|
786
|
+
path: "/api/flows/{id}",
|
|
787
|
+
description: "DELETE /api/flows/{id} without auth",
|
|
788
|
+
interactionType: "error",
|
|
789
|
+
expectedStatusCode: 401,
|
|
790
|
+
}],
|
|
791
|
+
});
|
|
792
|
+
const validation = minimalScenario({
|
|
793
|
+
scenarioName: "flows-delete-invalid-id",
|
|
794
|
+
description: "Validate malformed IDs",
|
|
795
|
+
category: "data_validation",
|
|
796
|
+
priority: "medium",
|
|
797
|
+
testType: TestType.CONTRACT,
|
|
798
|
+
steps: [{
|
|
799
|
+
order: 1,
|
|
800
|
+
method: "DELETE",
|
|
801
|
+
path: "/api/flows/not-a-uuid",
|
|
802
|
+
description: "DELETE /api/flows/not-a-uuid",
|
|
803
|
+
interactionType: "error",
|
|
804
|
+
expectedStatusCode: 422,
|
|
805
|
+
}],
|
|
806
|
+
});
|
|
807
|
+
const analysis = minimalAnalysis({
|
|
808
|
+
businessContext: {
|
|
809
|
+
mainPurpose: "Test",
|
|
810
|
+
userFlows: [],
|
|
811
|
+
dataFlows: [],
|
|
812
|
+
integrationPatterns: [],
|
|
813
|
+
draftedScenarios: [directDelete, validation, attackSurface],
|
|
814
|
+
},
|
|
815
|
+
branchDiffContext: {
|
|
816
|
+
baseBranch: "main",
|
|
817
|
+
currentBranch: "feature/admin-key",
|
|
818
|
+
changedFiles: ["src/prefect/server/api/flows.py"],
|
|
819
|
+
newEndpoints: [],
|
|
820
|
+
modifiedEndpoints: [{
|
|
821
|
+
path: "/api/flows/{id}",
|
|
822
|
+
methods: [{ method: "DELETE", sourceFile: "flows.py", changeType: "modified" }],
|
|
823
|
+
}],
|
|
824
|
+
affectedServices: [],
|
|
825
|
+
},
|
|
826
|
+
});
|
|
827
|
+
const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 5, undefined, undefined, undefined, undefined, 2);
|
|
828
|
+
const attackIdx = prompt.indexOf("POST /api/flows/bulk_delete → 401");
|
|
829
|
+
const directIdx = prompt.indexOf("DELETE /api/flows/{id} → 401");
|
|
830
|
+
expect(attackIdx).toBeGreaterThanOrEqual(0);
|
|
831
|
+
expect(directIdx).toBeGreaterThanOrEqual(0);
|
|
832
|
+
expect(attackIdx).toBeLessThan(directIdx);
|
|
833
|
+
expect(prompt).toContain("#1 — GENERATE** | contract | security_boundary");
|
|
834
|
+
expect(prompt).toContain("preserve attack-surface `security_boundary` items");
|
|
835
|
+
});
|
|
836
|
+
it("does not external-dedup protected bug-catching or attack-surface scenarios", () => {
|
|
837
|
+
const attackSurface = minimalScenario({
|
|
838
|
+
scenarioName: "flows-bulk-delete-post-auth-boundary",
|
|
839
|
+
description: "Verify destructive sibling rejects missing authentication",
|
|
840
|
+
category: "security_boundary",
|
|
841
|
+
isAttackSurfaceSecurityBoundary: true,
|
|
842
|
+
testType: TestType.CONTRACT,
|
|
843
|
+
steps: [{ order: 1, method: "POST", path: "/api/flows/bulk_delete", description: "POST /api/flows/bulk_delete without auth", interactionType: "error", expectedStatusCode: 401 }],
|
|
844
|
+
});
|
|
845
|
+
const bugCaught = minimalScenario({
|
|
846
|
+
scenarioName: "orders-discount-bug-caught",
|
|
847
|
+
description: "Bug-catching test: discount should subtract from total",
|
|
848
|
+
category: "bug_caught",
|
|
849
|
+
testType: TestType.CONTRACT,
|
|
850
|
+
steps: [{ order: 1, method: "POST", path: "/api/orders", description: "POST /api/orders exposes discount bug", interactionType: "success", expectedStatusCode: 201 }],
|
|
851
|
+
});
|
|
852
|
+
const ordinary = minimalScenario({
|
|
853
|
+
scenarioName: "customers-create-contract",
|
|
854
|
+
description: "Create customer contract",
|
|
855
|
+
category: "crud",
|
|
856
|
+
testType: TestType.CONTRACT,
|
|
857
|
+
steps: [{ order: 1, method: "POST", path: "/api/customers", description: "POST /api/customers", interactionType: "success", expectedStatusCode: 201 }],
|
|
858
|
+
});
|
|
859
|
+
const prompt = buildExecutionPlan([attackSurface, bugCaught, ordinary].map((scenario) => ({
|
|
860
|
+
scenario,
|
|
861
|
+
priority: scenario.category === "crud" ? "LOW" : "CRITICAL",
|
|
862
|
+
novelty: "modified",
|
|
863
|
+
})), 3, 3, "http://localhost:3000", "Authorization", ", authScheme: \"Bearer\"", "", "seed", 3, false, false, false, new Set(["POST::flows::contract", "POST::orders::contract", "POST::customers::contract"]));
|
|
864
|
+
expect(prompt).toContain("POST /api/flows/bulk_delete → 401");
|
|
865
|
+
expect(prompt).toContain("POST /api/orders → 201");
|
|
866
|
+
expect(prompt).not.toContain("POST /api/customers → 201");
|
|
867
|
+
expect(prompt).toContain("except for `bug_caught` and attack-surface `security_boundary` items");
|
|
868
|
+
});
|
|
869
|
+
it("keeps symmetric attack-surface siblings together before ordinary auth boundaries", () => {
|
|
870
|
+
const directWidgets = minimalScenario({
|
|
871
|
+
scenarioName: "widgets-delete-auth-boundary",
|
|
872
|
+
description: "Auth boundary: DELETE /api/widgets/{id} rejects missing authentication",
|
|
873
|
+
category: "security_boundary",
|
|
874
|
+
testType: TestType.CONTRACT,
|
|
875
|
+
steps: [{ order: 1, method: "DELETE", path: "/api/widgets/{id}", description: "DELETE /api/widgets/{id} without auth", interactionType: "error", expectedStatusCode: 401 }],
|
|
876
|
+
});
|
|
877
|
+
const directGadgets = minimalScenario({
|
|
878
|
+
scenarioName: "gadgets-delete-auth-boundary",
|
|
879
|
+
description: "Auth boundary: DELETE /api/gadgets/{id} rejects missing authentication",
|
|
880
|
+
category: "security_boundary",
|
|
881
|
+
testType: TestType.CONTRACT,
|
|
882
|
+
steps: [{ order: 1, method: "DELETE", path: "/api/gadgets/{id}", description: "DELETE /api/gadgets/{id} without auth", interactionType: "error", expectedStatusCode: 401 }],
|
|
883
|
+
});
|
|
884
|
+
const widgetsBulk = minimalScenario({
|
|
885
|
+
scenarioName: "widgets-bulk-delete-auth-boundary",
|
|
886
|
+
description: "Attack-surface auth boundary: /api/widgets/bulk_delete is a destructive sibling of changed DELETE /api/widgets/{id}; verify it rejects missing authentication",
|
|
887
|
+
category: "security_boundary",
|
|
888
|
+
testType: TestType.CONTRACT,
|
|
889
|
+
steps: [{ order: 1, method: "POST", path: "/api/widgets/bulk_delete", description: "POST /api/widgets/bulk_delete without auth", interactionType: "error", expectedStatusCode: 401 }],
|
|
890
|
+
});
|
|
891
|
+
const gadgetsBulk = minimalScenario({
|
|
892
|
+
scenarioName: "gadgets-bulk-delete-auth-boundary",
|
|
893
|
+
description: "Attack-surface auth boundary: /api/gadgets/bulk_delete is a destructive sibling of changed DELETE /api/gadgets/{id}; verify it rejects missing authentication",
|
|
894
|
+
category: "security_boundary",
|
|
895
|
+
testType: TestType.CONTRACT,
|
|
896
|
+
steps: [{ order: 1, method: "POST", path: "/api/gadgets/bulk_delete", description: "POST /api/gadgets/bulk_delete without auth", interactionType: "error", expectedStatusCode: 401 }],
|
|
897
|
+
});
|
|
898
|
+
const prompt = buildExecutionPlan([directWidgets, directGadgets, widgetsBulk, gadgetsBulk].map((scenario) => ({
|
|
899
|
+
scenario,
|
|
900
|
+
priority: "CRITICAL",
|
|
901
|
+
novelty: "modified",
|
|
902
|
+
})), 3, 4, "http://localhost:3000", "Authorization", ", authScheme: \"Bearer\"", "", "seed", 4, false);
|
|
903
|
+
const generatedBlock = prompt.slice(0, prompt.indexOf("#4 [ADDITIONAL]"));
|
|
904
|
+
const additionalBlock = prompt.slice(prompt.indexOf("#4 [ADDITIONAL]"));
|
|
905
|
+
expect(generatedBlock).toContain("POST /api/widgets/bulk_delete → 401");
|
|
906
|
+
expect(generatedBlock).toContain("POST /api/gadgets/bulk_delete → 401");
|
|
907
|
+
expect(additionalBlock).not.toContain("POST /api/widgets/bulk_delete → 401");
|
|
908
|
+
expect(additionalBlock).not.toContain("POST /api/gadgets/bulk_delete → 401");
|
|
909
|
+
expect(additionalBlock).toContain("DELETE /api/gadgets/{id} → 401");
|
|
910
|
+
});
|
|
760
911
|
});
|
|
761
912
|
// ---------------------------------------------------------------------------
|
|
762
913
|
// Tests — buildTestQualityCriteria contract-test guidance (regression guard)
|
|
@@ -1175,6 +1326,12 @@ describe("buildVerificationChecklist — self-check at end of prompt", () => {
|
|
|
1175
1326
|
const checklist = buildVerificationChecklist(10, 3);
|
|
1176
1327
|
expect(checklist).toContain("bugCatchingTarget");
|
|
1177
1328
|
});
|
|
1329
|
+
it("includes issue coverage check with promotion cap", () => {
|
|
1330
|
+
const checklist = buildVerificationChecklist(10, 3);
|
|
1331
|
+
expect(checklist).toContain("Issue coverage");
|
|
1332
|
+
expect(checklist).toContain("highest-severity flaw");
|
|
1333
|
+
expect(checklist).toContain("At most one promotion per run");
|
|
1334
|
+
});
|
|
1178
1335
|
it("includes distinct code path check", () => {
|
|
1179
1336
|
const checklist = buildVerificationChecklist(10, 3);
|
|
1180
1337
|
expect(checklist).toContain("distinct code path");
|
|
@@ -1243,7 +1400,7 @@ describe("buildRecommendationPrompt — reduced over-prompting", () => {
|
|
|
1243
1400
|
});
|
|
1244
1401
|
const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10);
|
|
1245
1402
|
expect(prompt).not.toContain("(MANDATORY before executing anything)");
|
|
1246
|
-
expect(prompt).toContain("(before
|
|
1403
|
+
expect(prompt).toContain("(before anything else)");
|
|
1247
1404
|
});
|
|
1248
1405
|
it("uses XML tags in context fetching guidance", () => {
|
|
1249
1406
|
const guidance = buildContextFetchingGuidance("session-1");
|
|
@@ -1512,3 +1669,161 @@ describe("externalDedupKey", () => {
|
|
|
1512
1669
|
expect(externalDedupKey(scenario)).toBe("POST::orders::contract");
|
|
1513
1670
|
});
|
|
1514
1671
|
});
|
|
1672
|
+
describe("UI grounding guidance (Phase C D-1.a)", () => {
|
|
1673
|
+
const editOrderScenario = minimalScenario({
|
|
1674
|
+
scenarioName: 'edit-order',
|
|
1675
|
+
description: 'edit order',
|
|
1676
|
+
category: 'new_endpoint',
|
|
1677
|
+
priority: 'medium',
|
|
1678
|
+
steps: [
|
|
1679
|
+
{ order: 1, method: 'PATCH', path: '/api/v1/orders/{id}', description: 'edit', interactionType: 'success', expectedStatusCode: 200 },
|
|
1680
|
+
],
|
|
1681
|
+
chainingKeys: [],
|
|
1682
|
+
});
|
|
1683
|
+
const uiGroundingBusinessContext = {
|
|
1684
|
+
mainPurpose: "Test API", userFlows: [], dataFlows: [], integrationPatterns: [],
|
|
1685
|
+
draftedScenarios: [editOrderScenario],
|
|
1686
|
+
};
|
|
1687
|
+
const uiOnlyAnalysis = () => minimalAnalysis({
|
|
1688
|
+
branchDiffContext: {
|
|
1689
|
+
currentBranch: "feature", baseBranch: "main",
|
|
1690
|
+
changedFiles: ['frontend/src/components/EditOrder.tsx'],
|
|
1691
|
+
newEndpoints: [],
|
|
1692
|
+
modifiedEndpoints: [],
|
|
1693
|
+
affectedServices: [],
|
|
1694
|
+
},
|
|
1695
|
+
businessContext: uiGroundingBusinessContext,
|
|
1696
|
+
});
|
|
1697
|
+
const mixedAnalysis = () => minimalAnalysis({
|
|
1698
|
+
branchDiffContext: {
|
|
1699
|
+
currentBranch: "feature", baseBranch: "main",
|
|
1700
|
+
changedFiles: ['frontend/src/components/EditOrder.tsx', 'backend/orders.py'],
|
|
1701
|
+
newEndpoints: [{
|
|
1702
|
+
path: '/api/v1/orders/{id}',
|
|
1703
|
+
methods: [{ method: 'PATCH', sourceFile: 'backend/orders.py', interactionCount: 1 }],
|
|
1704
|
+
}],
|
|
1705
|
+
modifiedEndpoints: [],
|
|
1706
|
+
affectedServices: [],
|
|
1707
|
+
},
|
|
1708
|
+
businessContext: uiGroundingBusinessContext,
|
|
1709
|
+
});
|
|
1710
|
+
const backendOnlyAnalysis = () => minimalAnalysis({
|
|
1711
|
+
branchDiffContext: {
|
|
1712
|
+
currentBranch: "feature", baseBranch: "main",
|
|
1713
|
+
changedFiles: ['backend/orders.py'],
|
|
1714
|
+
newEndpoints: [{
|
|
1715
|
+
path: '/api/v1/orders',
|
|
1716
|
+
methods: [{ method: 'POST', sourceFile: 'backend/orders.py', interactionCount: 1 }],
|
|
1717
|
+
}],
|
|
1718
|
+
modifiedEndpoints: [],
|
|
1719
|
+
affectedServices: [],
|
|
1720
|
+
},
|
|
1721
|
+
businessContext: uiGroundingBusinessContext,
|
|
1722
|
+
});
|
|
1723
|
+
it("emits UI grounding block on UI-only PRs", () => {
|
|
1724
|
+
const out = buildRecommendationPrompt(uiOnlyAnalysis(), AnalysisScope.CurrentBranchDiff, 10);
|
|
1725
|
+
expect(out).toContain("UI recommendation grounding");
|
|
1726
|
+
expect(out).toContain("role=<role>");
|
|
1727
|
+
expect(out).toContain("accessibleName=");
|
|
1728
|
+
});
|
|
1729
|
+
it("emits UI grounding block on mixed PRs", () => {
|
|
1730
|
+
const out = buildRecommendationPrompt(mixedAnalysis(), AnalysisScope.CurrentBranchDiff, 10);
|
|
1731
|
+
expect(out).toContain("UI recommendation grounding");
|
|
1732
|
+
expect(out).toContain("contextText=");
|
|
1733
|
+
});
|
|
1734
|
+
it("does not emit UI grounding block on backend-only PRs", () => {
|
|
1735
|
+
const out = buildRecommendationPrompt(backendOnlyAnalysis(), AnalysisScope.CurrentBranchDiff, 10);
|
|
1736
|
+
expect(out).not.toContain("UI recommendation grounding");
|
|
1737
|
+
});
|
|
1738
|
+
it("carves out non-UI entries from the grounding format", () => {
|
|
1739
|
+
const out = buildRecommendationPrompt(mixedAnalysis(), AnalysisScope.CurrentBranchDiff, 10);
|
|
1740
|
+
expect(out).toMatch(/Contract, integration, e2e.*existing conventions/);
|
|
1741
|
+
});
|
|
1742
|
+
it("includes contextText example for repeating elements", () => {
|
|
1743
|
+
const out = buildRecommendationPrompt(uiOnlyAnalysis(), AnalysisScope.CurrentBranchDiff, 10);
|
|
1744
|
+
expect(out).toContain('contextText=["customer@example.com"');
|
|
1745
|
+
});
|
|
1746
|
+
// Phase C D-1.a/7b: directive instruction strengthening + Validates coverage
|
|
1747
|
+
// + marked fallback. Asserts the post-7b language is present.
|
|
1748
|
+
it("uses MUST directive for tuple presence in reasoning field", () => {
|
|
1749
|
+
const out = buildRecommendationPrompt(uiOnlyAnalysis(), AnalysisScope.CurrentBranchDiff, 10);
|
|
1750
|
+
expect(out).toContain("MUST contain at least three of");
|
|
1751
|
+
expect(out).toContain("not valid output for UI test types");
|
|
1752
|
+
});
|
|
1753
|
+
it("instructs the agent to ground the Validates line for UI entries", () => {
|
|
1754
|
+
const out = buildRecommendationPrompt(uiOnlyAnalysis(), AnalysisScope.CurrentBranchDiff, 10);
|
|
1755
|
+
expect(out).toContain("Validates line — applies to");
|
|
1756
|
+
expect(out).toContain("observable behavior the test verifies");
|
|
1757
|
+
expect(out).toContain("structural facts");
|
|
1758
|
+
});
|
|
1759
|
+
it("requires a [no-blueprint-data] marker on source-grounded fallback entries", () => {
|
|
1760
|
+
const out = buildRecommendationPrompt(uiOnlyAnalysis(), AnalysisScope.CurrentBranchDiff, 10);
|
|
1761
|
+
expect(out).toContain("[no-blueprint-data]");
|
|
1762
|
+
expect(out).toContain("issuesFound");
|
|
1763
|
+
expect(out).toContain("Do NOT silently produce ungrounded reasoning");
|
|
1764
|
+
});
|
|
1765
|
+
});
|
|
1766
|
+
// ---------------------------------------------------------------------------
|
|
1767
|
+
// Tests — UI recommendation authoring rules
|
|
1768
|
+
// ---------------------------------------------------------------------------
|
|
1769
|
+
//
|
|
1770
|
+
// The recommendation prompt always emits a "UI Recommendation Authoring Rules"
|
|
1771
|
+
// section that anchors UI rec reasoning. Earlier iterations of this code
|
|
1772
|
+
// accepted a `capturedBlueprints` parameter and rendered the captures inline,
|
|
1773
|
+
// but that was redundant: the agent has the captures in its own tool-result
|
|
1774
|
+
// history. The prompt now ships the rules; the agent supplies the vocabulary.
|
|
1775
|
+
describe("buildRecommendationPrompt UI authoring rules", () => {
|
|
1776
|
+
function minimalDiffAnalysis() {
|
|
1777
|
+
return minimalAnalysis({
|
|
1778
|
+
branchDiffContext: {
|
|
1779
|
+
baseBranch: "main",
|
|
1780
|
+
currentBranch: "feature/test",
|
|
1781
|
+
changedFiles: ["src/components/OrderForm.tsx"],
|
|
1782
|
+
newEndpoints: [],
|
|
1783
|
+
modifiedEndpoints: [],
|
|
1784
|
+
affectedServices: [],
|
|
1785
|
+
},
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
it("emits UI authoring rules wrapped in an XML tag, unconditionally", () => {
|
|
1789
|
+
const analysis = minimalDiffAnalysis();
|
|
1790
|
+
const prompt = buildRecommendationPrompt(analysis);
|
|
1791
|
+
expect(prompt).toContain("<ui_recommendation_authoring_rules>");
|
|
1792
|
+
expect(prompt).toContain("</ui_recommendation_authoring_rules>");
|
|
1793
|
+
expect(prompt).toMatch(/do NOT mention "blueprint"/i);
|
|
1794
|
+
expect(prompt).toMatch(/do not invent element names/i);
|
|
1795
|
+
});
|
|
1796
|
+
it("does not render a 'Captured Blueprints' data section (param removed)", () => {
|
|
1797
|
+
// Regression on the previous design: capturedBlueprints used to be
|
|
1798
|
+
// threaded through the call and rendered as a "## Captured Blueprints"
|
|
1799
|
+
// data section. That path is gone — the agent's own browser_blueprint
|
|
1800
|
+
// tool-result history is the source of truth for element vocabulary.
|
|
1801
|
+
const analysis = minimalDiffAnalysis();
|
|
1802
|
+
const prompt = buildRecommendationPrompt(analysis);
|
|
1803
|
+
expect(prompt).not.toContain("## Captured Blueprints");
|
|
1804
|
+
});
|
|
1805
|
+
it("instructs the LLM to ground UI recommendations in elements observed via earlier browser_blueprint calls", () => {
|
|
1806
|
+
const analysis = minimalDiffAnalysis();
|
|
1807
|
+
const prompt = buildRecommendationPrompt(analysis);
|
|
1808
|
+
expect(prompt).toMatch(/ground the [`]?reasoning[`]?\s*field in elements you have actually observed/i);
|
|
1809
|
+
expect(prompt).toMatch(/inform.*how.*describe.*not.*which/i);
|
|
1810
|
+
});
|
|
1811
|
+
it("instructs the LLM not to leak internal MCP terminology into reasoning", () => {
|
|
1812
|
+
const analysis = minimalDiffAnalysis();
|
|
1813
|
+
const prompt = buildRecommendationPrompt(analysis);
|
|
1814
|
+
expect(prompt).toMatch(/do NOT mention "blueprint"/i);
|
|
1815
|
+
expect(prompt).toMatch(/leak builder internals/i);
|
|
1816
|
+
});
|
|
1817
|
+
it("does not abbreviate 'recommendation' to 'rec' in the rules section", () => {
|
|
1818
|
+
// Per PR review: shortform 'rec' invites the LLM to hallucinate the term.
|
|
1819
|
+
// Spell it out to keep the language consistent with the rest of the prompt.
|
|
1820
|
+
const analysis = minimalDiffAnalysis();
|
|
1821
|
+
const prompt = buildRecommendationPrompt(analysis);
|
|
1822
|
+
const rulesStart = prompt.indexOf("<ui_recommendation_authoring_rules>");
|
|
1823
|
+
const rulesEnd = prompt.indexOf("</ui_recommendation_authoring_rules>");
|
|
1824
|
+
const rulesBlock = prompt.slice(rulesStart, rulesEnd);
|
|
1825
|
+
// Allow "recs" as a substring within "recommendations" — match only the
|
|
1826
|
+
// standalone word.
|
|
1827
|
+
expect(rulesBlock).not.toMatch(/\b(rec|recs)\b/);
|
|
1828
|
+
});
|
|
1829
|
+
});
|
|
@@ -19,7 +19,7 @@ const CONTRACT_MODE_GUIDANCE = CONSUMER_MODE_ENABLED
|
|
|
19
19
|
Both modes (\`providerMode: true, consumerMode: true\`): For diff that contains BOTH provider signals (such as new/modified endpoint handlers, route changes this service owns) AND consumer signals (outbound HTTP client calls to another service, no new endpoint handlers).`
|
|
20
20
|
: ` Always add \`providerMode: true\` — the tool generates provider-side contract tests only.`;
|
|
21
21
|
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
|
|
22
|
-
prNumber, userPrompt, services, stateOutputFile, uiCredentials) {
|
|
22
|
+
prNumber, userPrompt, services, stateOutputFile, uiCredentials, testsRepoDir) {
|
|
23
23
|
maxGenerate = Math.min(Math.max(maxGenerate, 0), maxRecommendations);
|
|
24
24
|
// For follow-up requests: emit the @skyramp-testbot header + guardrails + retrieve-recommendations step.
|
|
25
25
|
// For first-run prompts: emit the full Task 1 analysis + maintenance section.
|
|
@@ -42,24 +42,35 @@ Verify the prompt inside <USER_PROMPT> is related to adding or removing tests fr
|
|
|
42
42
|
- If the prompt matches one or more tests in the Additional Recommendations → proceed to Task 1 (Skip Analysis).
|
|
43
43
|
|
|
44
44
|
### Task 1: Retrieve Previous Recommendations
|
|
45
|
-
Call \`skyramp_analyze_changes\` with \`repositoryPath\`: "${repositoryPath}", \`scope\`: "branch_diff"${baseBranch ? `, \`baseBranch\`: "${baseBranch}"` : ""}${prNumber ? `, \`prNumber\`: ${prNumber}` : ""}${stateOutputFile ? `, \`stateOutputFile\`: "${stateOutputFile}"` : ""}.
|
|
45
|
+
Call \`skyramp_analyze_changes\` with \`repositoryPath\`: "${repositoryPath}", \`scope\`: "branch_diff"${baseBranch ? `, \`baseBranch\`: "${baseBranch}"` : ""}${prNumber ? `, \`prNumber\`: ${prNumber}` : ""}${stateOutputFile ? `, \`stateOutputFile\`: "${stateOutputFile}"` : ""}${testsRepoDir ? `, \`testsRepoDir\`: "${testsRepoDir}"` : ""}.
|
|
46
46
|
This will fetch the previous TestBot report from the PR comments and return deduplicated recommendations.
|
|
47
47
|
Use those recommendations as your baseline. Only add or remove tests that the user requested AND that appear in the Additional Recommendations. Then proceed straight to Task 2: Generate New Tests.
|
|
48
48
|
`
|
|
49
49
|
: `
|
|
50
50
|
**Incremental mode:** Task 1 handles maintenance of existing tests. Task 2 handles new test generation from the GENERATE list. The two tasks are independent — maintenance completions never reduce the generate budget. Only generate tests for NEW endpoints not already covered by existing bot tests.
|
|
51
51
|
|
|
52
|
+
<!-- TODO(SKYR-3636 follow-up): migrate Task 1 + Task 2 step bodies to PromptPlan
|
|
53
|
+
(src/prompts/test-recommendation/promptPlan.ts) so step numbers don't have
|
|
54
|
+
to be hand-maintained when steps are added or reordered. -->
|
|
52
55
|
## Task 1: Analyze & Maintain
|
|
53
56
|
|
|
54
|
-
1. Call \`
|
|
57
|
+
1. **Pre-flight UI enumeration.** Call \`skyramp_ui_analyze_changes\` with \`repositoryPath\`: "${repositoryPath}"${uiCredentials ? `, \`uiCredentials\`: <use the value from <ui-credentials> in your context>` : ""}. The response returns \`uiContext\` (\`changedFrontendFiles\`, \`candidateUiPages\`) and capture instructions.
|
|
58
|
+
|
|
59
|
+
**If the response says "No UI changes detected"** → skip ahead to step 2.
|
|
60
|
+
|
|
61
|
+
**Otherwise:** for each candidate URL in the response${uiCredentials ? " (after logging in via the credentials provided)" : ""}, \`browser_navigate\` to the URL, then \`browser_blueprint\` to capture. The captures stay in your tool-result history — they're the element vocabulary you'll use when writing UI rec \`reasoning\` fields in step 2. You do NOT need to thread them back into a tool call.
|
|
62
|
+
|
|
63
|
+
If a candidate URL 404s or redirects, navigate from the workspace baseUrl and explore. If \`browser_blueprint\` fails on every candidate, proceed to step 2 and log an \`issuesFound\` info entry — UI recommendations will fall back to source-grounded prose.
|
|
64
|
+
|
|
65
|
+
2. Call \`skyramp_analyze_changes\` with \`repositoryPath\`: "${repositoryPath}", \`scope\`: "branch_diff", \`topN\`: ${maxRecommendations}, \`maxGenerate\`: ${maxGenerate}${baseBranch ? `, \`baseBranch\`: "${baseBranch}"` : ""}${prNumber ? `, \`prNumber\`: ${prNumber}` : ""}${stateOutputFile ? `, \`stateOutputFile\`: "${stateOutputFile}"` : ""}${testsRepoDir ? `, \`testsRepoDir\`: "${testsRepoDir}"` : ""} — discovers existing Skyramp tests, scans endpoints changed in the diff, loads workspace config, and returns ${maxRecommendations} ranked ADD recommendations${prNumber ? " (using PR comment history to avoid re-recommending already-generated tests)" : ""} along with the UI recommendation authoring rules. Use the blueprints already in your context (from step 1) to ground UI rec reasoning.
|
|
55
66
|
**If \`skyramp_analyze_changes\` returns an error:** retry once only if the error is transient (timeout, network blip, temporary unavailability) — do NOT retry for permanent errors (invalid repository path, missing required parameter, authentication failure). If it fails again, call \`skyramp_submit_report\` with a minimal valid payload: leave all test arrays empty and add the error to \`issuesFound\`. Refer to the \`skyramp_submit_report\` schema for required fields. Do NOT attempt Task 2 without a valid stateFile.
|
|
56
67
|
**If all changed files are non-application** (CI/CD, docs, lock files, config) → skip to Task 3 (Submit Report) with empty arrays and a single \`issuesFound\` entry explaining why (same format as the zero-test path below).
|
|
57
68
|
|
|
58
|
-
|
|
69
|
+
3. **Maintain existing tests** using the rules in \`<drift_analysis_rules>\` below. For each existing test reported by \`skyramp_analyze_changes\`, score it and choose the action exactly as directed by the Action Decision Matrix in \`<drift_analysis_rules>\`. Only read test files that require action per that matrix — do NOT read files that will be IGNORED. **Do NOT read source files (routers, models, CRUD, components) — all the information you need is in the \`skyramp_analyze_changes\` output and the diff.** When reading multiple test files, **read them all in a single parallel batch** — do NOT read them one at a time. Apply actions directly. Results go in \`testMaintenance\`.
|
|
59
70
|
|
|
60
71
|
${buildDriftAnalysisPrompt({ existingTests: [], scannedEndpoints: [], repositoryPath })}
|
|
61
72
|
|
|
62
|
-
|
|
73
|
+
4. **Code review:** From the \`skyramp_analyze_changes\` output and the existing test files you read for maintenance, note any logic bugs. Do NOT read additional source files just for code review — use what is already available from the analysis and test file reads. Common patterns to flag:
|
|
63
74
|
- Computed fields not recalculated after mutation (e.g. \`total_amount\` unchanged after items are added/removed)
|
|
64
75
|
- Incomplete CRUD: create without cleanup, update that adds new records without removing old ones
|
|
65
76
|
- Missing input validation on new endpoints
|
|
@@ -67,6 +78,8 @@ ${buildDriftAnalysisPrompt({ existingTests: [], scannedEndpoints: [], repository
|
|
|
67
78
|
- Incorrect arithmetic in business logic (discount calculations, price aggregation)
|
|
68
79
|
Log each finding in \`issuesFound\` with a \`severity\` (critical/high/medium/low). These bugs should inform your test design in Task 2.
|
|
69
80
|
|
|
81
|
+
5. **Apply the UI Recommendation Authoring Rules.** \`skyramp_analyze_changes\` returns an authoring-rules section that defines how UI recommendation \`reasoning\` fields should be written (natural prose, no internal-identifier syntax, ground in elements observed via earlier \`browser_blueprint\` calls, fall back to source-grounded prose when no captures are available). Apply those rules when authoring UI rec reasoning. Non-UI recommendations (contract / integration / e2e / batch-scenario) are unaffected by these rules and use their pre-existing formats — do not reformat them.
|
|
82
|
+
|
|
70
83
|
---`;
|
|
71
84
|
const serviceContext = services?.length ? buildServiceContext(services) : '';
|
|
72
85
|
// The <ui-credentials> tags are framing for the agent's prompt context —
|
|
@@ -80,10 +93,14 @@ ${buildDriftAnalysisPrompt({ existingTests: [], scannedEndpoints: [], repository
|
|
|
80
93
|
const uiCredentialsBlock = trimmedCredentials
|
|
81
94
|
? `<ui-credentials>\n${trimmedCredentials}\n</ui-credentials>`
|
|
82
95
|
: '';
|
|
96
|
+
const testsRepoDirBlock = testsRepoDir ? `<TESTS REPO DIR>${testsRepoDir}</TESTS REPO DIR>\n` : '';
|
|
97
|
+
const testDirInstruction = testsRepoDir
|
|
98
|
+
? `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.`
|
|
99
|
+
: `${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).`;
|
|
83
100
|
return `<TITLE>${prTitle}</TITLE>
|
|
84
101
|
<DESCRIPTION>${prDescription}</DESCRIPTION>
|
|
85
102
|
<REPOSITORY PATH>${repositoryPath}</REPOSITORY PATH>
|
|
86
|
-
${serviceContext ? serviceContext + '\n' : ''}${uiCredentialsBlock ? uiCredentialsBlock + '\n' : ''}Use the Skyramp MCP server tools for all tasks below.
|
|
103
|
+
${testsRepoDirBlock}${serviceContext ? serviceContext + '\n' : ''}${uiCredentialsBlock ? uiCredentialsBlock + '\n' : ''}Use the Skyramp MCP server tools for all tasks below.
|
|
87
104
|
|
|
88
105
|
${task1Section}
|
|
89
106
|
|
|
@@ -143,7 +160,7 @@ ${userPrompt ? "Generate only the tests that the user requested from the Additio
|
|
|
143
160
|
**How to generate each type (for ADD):**
|
|
144
161
|
- **Integration**: call \`skyramp_batch_scenario_test_generation\` with ALL steps in a single call (pass the \`steps\` array with method, path, requestBody, statusCode for each step). Then call \`skyramp_integration_test_generation\` with the returned scenario file.
|
|
145
162
|
**Use the pre-built scenario JSON from the Execution Plan** — pass the steps array directly. Do NOT read source code models to construct request bodies if the plan already provides them.
|
|
146
|
-
Scenario JSON and test files go in ${
|
|
163
|
+
Scenario JSON and test files go in ${testDirInstruction}
|
|
147
164
|
**Pipeline for speed**: Call ALL \`skyramp_batch_scenario_test_generation\` calls in one batch. When they return, call ALL \`skyramp_integration_test_generation\` calls in the next batch. Do NOT serialize per-scenario (batch→integration→batch→integration) — batch ALL scenarios first, then generate ALL integration tests.
|
|
148
165
|
- **Contract**: call \`skyramp_contract_test_generation\` with \`endpointURL\`, \`method\`, and \`requestData\` for POST/PUT/PATCH.
|
|
149
166
|
Pass \`apiSchema\` if an OpenAPI spec exists.
|
|
@@ -153,13 +170,20 @@ ${CONTRACT_MODE_GUIDANCE}
|
|
|
153
170
|
If a relevant trace exists (covers the UI changes in this PR), use it directly with \`skyramp_ui_test_generation\` and \`modularizeCode: false\`.
|
|
154
171
|
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.
|
|
155
172
|
|
|
156
|
-
**Browser authentication (check BEFORE navigating)**: If \`<ui-credentials>\` appears in your context above, the app requires login. Parse the credentials —
|
|
173
|
+
**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:
|
|
174
|
+
- New format: \`username=<value>;password=<value>\` or \`username=<value>;password=<value>;role=<value>\` — fields are \`;\`-delimited key=value pairs. The \`=\` and \`;\` characters are reserved delimiters and must not appear in the values themselves.
|
|
175
|
+
- Legacy format: \`username:password\` — the first \`:\` splits username from password.
|
|
176
|
+
|
|
177
|
+
**Credential selection**: Use the first credential by default. When the scenario requires a specific role, find the credential whose \`role\` field matches (e.g. \`role=admin\`). If no credential matches the required role, use the first credential and add a note to \`issuesFound\` that no matching role was found.
|
|
178
|
+
|
|
179
|
+
Type all values verbatim. Before navigating to ANY feature URL:
|
|
157
180
|
1. \`browser_navigate\` to the login URL (e.g. \`{baseUrl}/login\`, \`/user/login\`, \`/signin\` — infer from the app's base URL and framework)
|
|
158
181
|
2. \`browser_snapshot\` to find the username/email and password fields
|
|
159
182
|
3. \`browser_type\` the username into the email/username field
|
|
160
183
|
4. \`browser_type\` the password into the password field
|
|
161
|
-
5.
|
|
162
|
-
6.
|
|
184
|
+
5. If a role selector is present and a \`role\` was specified in the credential, select it before submitting
|
|
185
|
+
6. \`browser_click\` the submit button, then \`browser_wait_for\` redirect away from the login page
|
|
186
|
+
7. Now navigate directly to the feature URL and begin recording
|
|
163
187
|
The login steps ARE part of the trace — the generated test will authenticate automatically.
|
|
164
188
|
|
|
165
189
|
Use this exact format:
|
|
@@ -198,6 +222,38 @@ ${CONTRACT_MODE_GUIDANCE}
|
|
|
198
222
|
- **List integrity after form save**: assert the list item count is unchanged unless the action explicitly added or removed items — catches duplication bugs
|
|
199
223
|
- Do NOT assert page headings, static labels, boilerplate text, intermediate states, or values already guaranteed by the action
|
|
200
224
|
- Do NOT assert the same value with multiple selectors
|
|
225
|
+
|
|
226
|
+
**Capture-act-capture (applies only when recording a UI trace):**
|
|
227
|
+
|
|
228
|
+
**Skip this entire section if \`uiContext\` was absent or \`changedFrontendFiles\` was empty in step 1's response** (backend-only PR). The capture-act-capture pattern is for UI trace recording only — there's no UI trace to record on a backend-only PR. Continue to the non-UI test-type instructions below.
|
|
229
|
+
|
|
230
|
+
**Reminder — the UI test priority rule above still applies.** If the diff contains frontend/UI changes, you still MUST attempt to generate at least one UI test. Capture-act-capture is **how** you record that test, not **whether** you record one — do not substitute UI recommendations for actually recording a trace. UI rec reasoning was already grounded in the upstream blueprints from Task 1 step 1; Task 2's capture-act-capture is for the trace's own assertions, not for retroactively rewriting recommendation reasoning.
|
|
231
|
+
|
|
232
|
+
This pattern produces delta-derived assertions from blueprint diffs. Diff-derived assertions catch state changes more reliably than author-inference — the diff tells you what actually changed on the page so the assertion is grounded in observable state, not in guessing what "success" looks like.
|
|
233
|
+
|
|
234
|
+
Capture-act-capture applies **only** to a UI trace in progress (inside the \`browser_navigate\` + \`browser_*\` interaction block). It does **not** apply to contract, integration, e2e, or batch-scenario test generation — those run on their pre-existing patterns without any capture-act-capture involvement. On mixed PRs with backend + UI work, generate the non-UI tests normally; the only thing that changes is how the UI trace itself is recorded.
|
|
235
|
+
|
|
236
|
+
\`browser_snapshot\` remains the source of ephemeral refs that interaction tools require. \`browser_blueprint\` provides durable semantic identity. Use both when recording: blueprint to decide the target and what "done" looks like; snapshot to get the ref needed to dispatch the click or type.
|
|
237
|
+
|
|
238
|
+
An **action** for this pattern is one user-intent-level operation whose completion changes the app's observable state — a click, a form submit (button or Enter), a navigation, a complete text fill of one field, or a meaningful keyboard shortcut (\`Ctrl+V\` paste, \`Ctrl+A\` select-all followed by a mutation, \`Escape\` to dismiss a modal). **Not** intermediate input mechanics: the individual typed characters \`browser_type\` emits, \`Tab\` between fields, arrow-key highlight in a listbox, focus-only changes. The browser-authentication flow (login) is boilerplate — no capture-act-capture there; login stays on the existing \`browser_snapshot\` + \`browser_type\` + \`browser_click\` pattern.
|
|
239
|
+
|
|
240
|
+
The pattern for each action:
|
|
241
|
+
|
|
242
|
+
1. **Before** the action: \`browser_blueprint\`. Identify the semantic target by \`role\`, \`accessibleName\`, and \`stableId\`/\`testId\`.
|
|
243
|
+
|
|
244
|
+
2. If the target's \`widgetType\` is \`"custom"\` or \`"unknown"\`: \`browser_widget_contract_lookup\` with the element's \`fingerprint\` and \`ref\`. On \`"found"\`, execute the contract steps. On \`"needs_inference"\`, fall through to snapshot-driven trial clicks (\`browser_wait_for\` between retries). Inference-and-cache is out of scope for this slice, so don't attempt to synthesize and cache contracts.
|
|
245
|
+
|
|
246
|
+
3. Execute the action via \`browser_click\` / \`browser_type\` / \`browser_navigate\`. The \`ref\` comes from \`browser_snapshot\` as today.
|
|
247
|
+
|
|
248
|
+
4. **After** the action: \`browser_blueprint\` again. The response shape depends on whether the action navigated:
|
|
249
|
+
- **Same URL (modal/tab/in-place mutation):** \`{ isFullCapture: false, pageHash, previousPageHash, delta, possibleAssertions }\`. The \`delta\` field contains \`elementsAdded\`, \`elementsRemoved\`, \`textChanges\`, \`repeatingCountChanges\`. The \`possibleAssertions\` field is a mechanical translation of those entries into Playwright \`expect(...)\` candidates — see step 5. You do **not** need to call \`browser_blueprint_diff\` here — that tool is only for cross-URL comparisons. An empty delta (all arrays empty) is itself a meaningful signal: the action did not change observable DOM (e.g. a silent failure the test should catch).
|
|
250
|
+
- **Navigated to a new URL** (e.g. router transition, link click, programmatic \`browser_navigate\`): \`{ isFullCapture: true, pageHash, blueprint }\` — a fresh full capture of the new page. No \`possibleAssertions\` here (no delta to translate). If you need a structured cross-URL diff, call \`browser_blueprint_diff(beforeBlueprint, afterBlueprint)\` explicitly; otherwise search the new blueprint for the elements your assertion will target.
|
|
251
|
+
|
|
252
|
+
5. **The AFTER response includes a \`possibleAssertions[]\` array — these are mechanical translations of delta entries into Playwright \`expect(...)\` candidates, available if any of them happen to match the assertion you'd write anyway.** Each entry has \`{ code, rationale, tier }\` where \`code\` is ready-to-use, \`rationale\` explains the source delta entry, and \`tier\` is HIGH/MEDIUM/LOW. **Read them, but do not feel obligated to use them.** They are heavily biased toward visibility checks (\`toBeVisible\` / \`not.toBeVisible\`), which are often shallow assertions — a passing visibility check does not mean the feature works. The right assertion target depends on what the test is *for*: if you're testing a state-changing action (form submit, button click that mutates data), prefer assertions on the post-action state (computed values, count changes, server-derived fields). Use a \`possibleAssertions\` candidate when its \`code\` already expresses what you would have written; ignore the array entirely when none of the candidates match the test's actual purpose. Adding visibility assertions just because they're available reduces test value; one well-targeted assertion beats five visibility checks of incidental DOM elements (modal scaffolding, navigation chrome). The pre-existing rule still applies: **at least one \`browser_assert\` per page navigated, verifying a business outcome — not just that an element is visible.**
|
|
253
|
+
|
|
254
|
+
**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.
|
|
255
|
+
|
|
256
|
+
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-call. Capture-act-capture adds blueprint captures alongside the existing steps; it doesn't replace anything.
|
|
201
257
|
- **E2E**: Only if BOTH a backend trace \`.json\` AND a Playwright \`.zip\` already exist in the repo. Without both, move to \`additionalRecommendations\`.
|
|
202
258
|
- Skip smoke tests entirely.
|
|
203
259
|
|
|
@@ -267,7 +323,7 @@ Call \`skyramp_submit_report\` with \`summaryOutputFile\`: "${summaryOutputFile}
|
|
|
267
323
|
- **additionalRecommendations**: AT MOST ${maxRecommendations - maxGenerate} items.
|
|
268
324
|
- For \`testType: "contract"\` entries: **\`primaryEndpoint\` is required** (e.g. \`"GET /api/v1/users/{user_id}"\`). The tool will reject the submission without it — do not omit it or you will be forced to resubmit.
|
|
269
325
|
- For \`testType: "integration"\` or \`"e2e"\` entries: omit \`primaryEndpoint\` — use \`description\` to list the endpoints involved instead.
|
|
270
|
-
- **testMaintenance**:
|
|
326
|
+
- **testMaintenance**: Use \`[]\` **only** if no existing Skyramp tests were found in the repository. If existing tests were found (any score), include one entry per test. For UPDATE/REGENERATE/DELETE tests that were modified and executed, populate all fields from real before/after execution results. For IGNORE-scored tests (not modified or executed), derive \`beforeStatus\` from the \`skyramp_analyze_test_health\` health score (typically \`"Pass"\` if drift score is 0 and no health issues were flagged), set \`afterStatus\` to \`"Skipped"\`, and use \`afterDetails\` to explain why (e.g. "IGNORE: drift score 0 — endpoint not modified in this PR"). Do **not** add entries for tests that were not returned by the health analysis.
|
|
271
327
|
|
|
272
328
|
---
|
|
273
329
|
|
|
@@ -353,15 +409,19 @@ export function registerTestbotPrompt(server) {
|
|
|
353
409
|
uiCredentials: z
|
|
354
410
|
.string()
|
|
355
411
|
.optional()
|
|
356
|
-
.describe("Browser login credentials for UI test recording
|
|
412
|
+
.describe("Browser login credentials for UI test recording. One credential per line. Supported formats: 'username=<val>;password=<val>' or 'username=<val>;password=<val>;role=<val>' (role optional), or legacy 'username:password'. Note: = and ; are reserved delimiters in the new format and must not appear in values. Injected into the prompt as a <ui-credentials> block so the agent logs in before recording traces."),
|
|
357
413
|
workspaceValidationFailed: z
|
|
358
414
|
.boolean()
|
|
359
415
|
.default(false)
|
|
360
416
|
.describe("Set to true when the testbot detected that .skyramp/workspace.yml exists but failed schema validation. Instructs the agent to regenerate the workspace file before proceeding."),
|
|
417
|
+
testsRepoDir: z
|
|
418
|
+
.string()
|
|
419
|
+
.optional()
|
|
420
|
+
.describe("Absolute path to a cloned test repository. When set, the agent writes generated test files there instead of the app repository (cross-repo test delivery)."),
|
|
361
421
|
},
|
|
362
422
|
}, async (args) => {
|
|
363
423
|
const services = await readWorkspaceServices(args.repositoryPath);
|
|
364
|
-
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.stateOutputFile, args.uiCredentials);
|
|
424
|
+
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.stateOutputFile, args.uiCredentials, args.testsRepoDir);
|
|
365
425
|
if (args.workspaceValidationFailed) {
|
|
366
426
|
prompt = buildWorkspaceRecoveryPrefix(args.repositoryPath) + prompt;
|
|
367
427
|
}
|