@skyramp/mcp 0.2.6 → 0.2.150-rc.mntnc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/commands/commandLibrary.d.ts +12 -0
- package/build/commands/recommendTestsAndExecuteCommand.d.ts +2 -0
- package/build/commands/testThisEndpointCommand.d.ts +2 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +6 -4
- package/build/playwright/PlaywrightTraceService.d.ts +32 -0
- package/build/playwright/index.d.ts +3 -0
- package/build/playwright/registerPlaywrightTools.d.ts +8 -0
- package/build/playwright/registerPlaywrightTools.js +12 -11
- package/build/playwright/traceRecordingPrompt.d.ts +13 -0
- package/build/playwright/traceRecordingPrompt.js +3 -11
- package/build/prompts/code-reuse.d.ts +1 -0
- package/build/prompts/enhance-assertions/contractProviderAssertionsPrompt.d.ts +2 -0
- package/build/prompts/enhance-assertions/integrationAssertionsPrompt.d.ts +2 -0
- package/build/prompts/enhance-assertions/sharedAssertionRules.d.ts +15 -0
- package/build/prompts/enhance-assertions/uiAssertionsPrompt.d.ts +2 -0
- package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +24 -5
- package/build/prompts/fix-error-prompt.d.ts +1 -0
- package/build/prompts/initialize-workspace/initializeWorkspacePrompt.d.ts +1 -0
- package/build/prompts/modularization/integration-test-modularization.d.ts +1 -0
- package/build/prompts/modularization/ui-test-modularization.d.ts +1 -0
- package/build/prompts/personas.d.ts +21 -0
- package/build/prompts/pom-aware-code-reuse.d.ts +1 -0
- package/build/prompts/startTraceCollectionPrompts.d.ts +2 -0
- package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.d.ts +2 -0
- package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +114 -0
- package/build/prompts/sut-setup/modes/dockerComposePrompt.d.ts +2 -0
- package/build/prompts/sut-setup/modes/dockerComposePrompt.js +292 -0
- package/build/prompts/sut-setup/shared.d.ts +39 -0
- package/build/prompts/sut-setup/shared.js +132 -0
- package/build/prompts/test-maintenance/actionsInstructions.d.ts +35 -0
- package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +4 -0
- package/build/prompts/test-maintenance/driftAnalysisSections.d.ts +13 -0
- package/build/prompts/test-maintenance/driftAnalysisSections.js +9 -3
- package/build/prompts/test-recommendation/analysisOutputPrompt.d.ts +84 -0
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +1 -1
- package/build/prompts/test-recommendation/analysisOutputPrompt.test.d.ts +1 -0
- package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +1 -0
- package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +17 -0
- package/build/prompts/test-recommendation/diffExecutionPlan.js +7 -16
- package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +7 -0
- package/build/prompts/test-recommendation/mergeEnrichedScenarios.test.d.ts +1 -0
- package/build/prompts/test-recommendation/mergeEnrichedScenarios.test.js +1 -0
- package/build/prompts/test-recommendation/promptPlan.d.ts +203 -0
- package/build/prompts/test-recommendation/promptPlan.test.d.ts +1 -0
- package/build/prompts/test-recommendation/recommendationSections.d.ts +35 -0
- package/build/prompts/test-recommendation/recommendationShared.d.ts +31 -0
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.d.ts +7 -0
- package/build/prompts/test-recommendation/scopeAssessment.d.ts +55 -0
- package/build/prompts/test-recommendation/scopeAssessment.js +2 -2
- package/build/prompts/test-recommendation/scopeAssessment.test.d.ts +1 -0
- package/build/prompts/test-recommendation/scopeAssessment.test.js +1 -0
- package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +6 -0
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +4 -4
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.d.ts +1 -0
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +6 -99
- package/build/prompts/testbot/testbot-prompts.d.ts +11 -0
- package/build/prompts/testbot/testbot-prompts.js +213 -42
- package/build/prompts/testbot/testbot-prompts.test.d.ts +1 -0
- package/build/prompts/testbot/testbot-prompts.test.js +15 -117
- package/build/resources/analysisResources.d.ts +17 -0
- package/build/resources/progressResource.d.ts +2 -0
- package/build/resources/sutSetupResource.d.ts +2 -0
- package/build/resources/sutSetupResource.js +45 -0
- package/build/resources/testbotResource.d.ts +2 -0
- package/build/resources/testbotResource.js +1 -1
- package/build/services/AnalyticsService.d.ts +13 -0
- package/build/services/AnalyticsService.test.d.ts +1 -0
- package/build/services/AnalyticsService.test.js +86 -0
- package/build/services/ModularizationService.d.ts +11 -0
- package/build/services/ScenarioGenerationService.d.ts +37 -0
- package/build/services/ScenarioGenerationService.integration.test.d.ts +1 -0
- package/build/services/ScenarioGenerationService.integration.test.js +4 -0
- package/build/services/ScenarioGenerationService.test.d.ts +1 -0
- package/build/services/TestDiscoveryService.d.ts +128 -0
- package/build/services/TestDiscoveryService.js +22 -12
- package/build/services/TestDiscoveryService.test.d.ts +1 -0
- package/build/services/TestDiscoveryService.test.js +4 -3
- package/build/services/TestExecutionService.d.ts +55 -0
- package/build/services/TestExecutionService.js +158 -26
- package/build/services/TestExecutionService.test.d.ts +1 -0
- package/build/services/TestExecutionService.test.js +339 -25
- package/build/services/TestGenerationService.d.ts +56 -0
- package/build/services/TestGenerationService.test.d.ts +1 -0
- package/build/services/TestGenerationService.test.js +10 -8
- package/build/services/containerEnv.d.ts +14 -0
- package/build/tool-phase-coverage.test.d.ts +1 -0
- package/build/tool-phase-coverage.test.js +2 -0
- package/build/tool-phases.d.ts +38 -0
- package/build/tools/auth/loginTool.d.ts +2 -0
- package/build/tools/auth/logoutTool.d.ts +2 -0
- package/build/tools/code-refactor/codeReuseTool.d.ts +2 -0
- package/build/tools/code-refactor/enhanceAssertionsTool.d.ts +2 -0
- package/build/tools/code-refactor/modularizationTool.d.ts +2 -0
- package/build/tools/executeSkyrampTestTool.d.ts +2 -0
- package/build/tools/executeSkyrampTestTool.js +17 -4
- package/build/tools/fixErrorTool.d.ts +2 -0
- package/build/tools/generate-tests/generateBatchScenarioRestTool.d.ts +45 -0
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +1 -1
- package/build/tools/generate-tests/generateBatchScenarioRestTool.test.d.ts +1 -0
- package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +11 -9
- package/build/tools/generate-tests/generateContractRestTool.d.ts +31 -0
- package/build/tools/generate-tests/generateE2ERestTool.d.ts +13 -0
- package/build/tools/generate-tests/generateFuzzRestTool.d.ts +10 -0
- package/build/tools/generate-tests/generateIntegrationRestTool.d.ts +16 -0
- package/build/tools/generate-tests/generateLoadRestTool.d.ts +18 -0
- package/build/tools/generate-tests/generateLoadRestTool.test.d.ts +1 -0
- package/build/tools/generate-tests/generateMockRestTool.d.ts +64 -0
- package/build/tools/generate-tests/generateSmokeRestTool.d.ts +8 -0
- package/build/tools/generate-tests/generateUIRestTool.d.ts +14 -0
- package/build/tools/generate-tests/generateUIRestTool.js +1 -1
- package/build/tools/generate-tests/loadTestSchema.d.ts +39 -0
- package/build/tools/one-click/oneClickTool.d.ts +11 -0
- package/build/tools/submitReportTool.d.ts +384 -0
- package/build/tools/submitReportTool.js +225 -37
- package/build/tools/submitReportTool.test.d.ts +1 -0
- package/build/tools/submitReportTool.test.js +479 -55
- package/build/tools/test-management/actionsTool.d.ts +10 -0
- package/build/tools/test-management/actionsTool.js +10 -0
- package/build/tools/test-management/analyzeChangesTool.d.ts +26 -0
- package/build/tools/test-management/analyzeChangesTool.js +128 -48
- package/build/tools/test-management/analyzeChangesTool.test.d.ts +1 -0
- package/build/tools/test-management/analyzeChangesTool.test.js +80 -73
- package/build/tools/test-management/analyzeTestHealthTool.d.ts +2 -0
- package/build/tools/test-management/analyzeTestHealthTool.js +6 -3
- package/build/tools/test-management/index.d.ts +3 -0
- package/build/tools/test-management/index.js +0 -1
- package/build/tools/trace/resolveSaveStoragePath.d.ts +7 -0
- package/build/tools/trace/resolveSaveStoragePath.test.d.ts +1 -0
- package/build/tools/trace/resolveSessionPaths.d.ts +40 -0
- package/build/tools/trace/resolveSessionPaths.test.d.ts +1 -0
- package/build/tools/trace/sessionState.d.ts +2 -0
- package/build/tools/trace/sessionState.test.d.ts +1 -0
- package/build/tools/trace/startTraceCollectionTool.d.ts +2 -0
- package/build/tools/trace/stopTraceCollectionTool.d.ts +2 -0
- package/build/tools/workspace/initScanWorkspaceTool.d.ts +4 -0
- package/build/tools/workspace/initializeWorkspaceTool.d.ts +2 -0
- package/build/tools/workspace/initializeWorkspaceTool.test.d.ts +1 -0
- package/build/tools/workspace/initializeWorkspaceTool.test.js +134 -0
- package/build/types/OneClickCommands.d.ts +68 -0
- package/build/types/RepositoryAnalysis.d.ts +3004 -0
- package/build/types/TestAnalysis.d.ts +97 -0
- package/build/types/TestExecution.d.ts +63 -0
- package/build/types/TestExecution.js +11 -1
- package/build/types/TestRecommendation.d.ts +12 -0
- package/build/types/TestTypes.d.ts +234 -0
- package/build/types/TestbotReport.d.ts +68 -0
- package/build/types/TestbotReport.js +1 -0
- package/build/types/index.d.ts +4 -0
- package/build/types/index.js +3 -0
- package/build/utils/AnalysisStateManager.d.ts +182 -0
- package/build/utils/AnalysisStateManager.js +10 -1
- package/build/utils/AnalysisStateManager.test.d.ts +1 -0
- package/build/utils/analyze-openapi.d.ts +1 -0
- package/build/utils/branchDiff.d.ts +18 -0
- package/build/utils/dartRouteExtractor.d.ts +45 -0
- package/build/utils/dartRouteExtractor.test.d.ts +1 -0
- package/build/utils/docker.d.ts +19 -0
- package/build/utils/docker.test.d.ts +1 -0
- package/build/utils/docker.test.js +10 -9
- package/build/utils/featureFlags.d.ts +37 -0
- package/build/utils/featureFlags.test.d.ts +1 -0
- package/build/utils/gitStaging.d.ts +24 -0
- package/build/utils/gitStaging.test.d.ts +1 -0
- package/build/utils/gitStaging.test.js +13 -7
- package/build/utils/httpDefaults.d.ts +10 -0
- package/build/utils/httpDefaults.test.d.ts +1 -0
- package/build/utils/initAgent.d.ts +36 -0
- package/build/utils/language-helper.d.ts +6 -0
- package/build/utils/logger.d.ts +11 -0
- package/build/utils/normalizeParams.d.ts +14 -0
- package/build/utils/normalizeSkyrampImports.d.ts +13 -0
- package/build/utils/pr-comment-parser.d.ts +31 -0
- package/build/utils/pr-comment-parser.test.d.ts +1 -0
- package/build/utils/pr-comment-parser.test.js +6 -5
- package/build/utils/projectMetadata.d.ts +15 -0
- package/build/utils/projectMetadata.test.d.ts +1 -0
- package/build/utils/proxy-terminal.d.ts +2 -0
- package/build/utils/repoScanner.d.ts +27 -0
- package/build/utils/repoScanner.test.d.ts +1 -0
- package/build/utils/routeParsers.d.ts +53 -0
- package/build/utils/routeParsers.js +31 -38
- package/build/utils/routeParsers.test.d.ts +1 -0
- package/build/utils/scenarioDrafting.d.ts +71 -0
- package/build/utils/scenarioDrafting.js +23 -1
- package/build/utils/scenarioDrafting.test.d.ts +1 -0
- package/build/utils/skyrampMdContent.d.ts +5 -0
- package/build/utils/sourceRouteExtractor.d.ts +48 -0
- package/build/utils/sourceRouteExtractor.test.d.ts +1 -0
- package/build/utils/telemetry.d.ts +32 -0
- package/build/utils/telemetry.test.d.ts +1 -0
- package/build/utils/trace-parser.d.ts +32 -0
- package/build/utils/trace-parser.test.d.ts +6 -0
- package/build/utils/uiPageEnumerator.d.ts +146 -0
- package/build/utils/uiPageEnumerator.js +58 -14
- package/build/utils/uiPageEnumerator.test.d.ts +1 -0
- package/build/utils/uiPageEnumerator.test.js +223 -43
- package/build/utils/utils.d.ts +45 -0
- package/build/utils/utils.js +10 -0
- package/build/utils/utils.test.d.ts +1 -0
- package/build/utils/versions.d.ts +3 -0
- package/build/utils/versions.js +1 -1
- package/build/utils/workspaceAuth.d.ts +127 -0
- package/build/utils/workspaceAuth.test.d.ts +1 -0
- package/build/utils/workspaceAuth.test.js +25 -25
- package/build/workspace/index.d.ts +1 -0
- package/build/workspace/workspace.d.ts +390 -0
- package/build/workspace/workspace.test.d.ts +1 -0
- package/node_modules/playwright/LICENSE +202 -0
- package/node_modules/playwright/NOTICE +5 -0
- package/node_modules/playwright/README.md +168 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +13 -9
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +4 -3
- package/node_modules/playwright/node_modules/playwright-core/LICENSE +202 -0
- package/node_modules/playwright/node_modules/playwright-core/NOTICE +5 -0
- package/node_modules/playwright/node_modules/playwright-core/ThirdPartyNotices.txt +23 -126
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-B0JOjboO.js → codeMirrorModule-B5kqh2EV.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-1anWeyDf.js → defaultSettingsView-CZ9npQ3N.js} +79 -79
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.Cc5029a3.js → index.BCnxj-_b.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.Wo5yvvVh.js → uiMode.1Ym0Ivn8.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/package.json +1 -1
- package/package.json +16 -4
- package/build/tools/test-management/uiAnalyzeChangesTool.js +0 -155
- package/build/tools/test-management/uiAnalyzeChangesTool.test.js +0 -147
- package/node_modules/playwright/node_modules/playwright-core/.DS_Store +0 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/.bin/node-which +0 -52
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/is-docker +0 -5
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/mime +0 -46
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/yaml +0 -11
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.CP3Y8Zsb.js +0 -2
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
/**
|
|
3
|
+
* Compute a suggested new filename when an endpoint is renamed.
|
|
4
|
+
*/
|
|
5
|
+
export declare function computeRenamedTestFile(testFile: string, renames: Array<{
|
|
6
|
+
oldPath: string;
|
|
7
|
+
newPath: string;
|
|
8
|
+
method: string;
|
|
9
|
+
}>): string | null;
|
|
10
|
+
export declare function registerActionsTool(server: McpServer): void;
|
|
@@ -6,6 +6,7 @@ import * as fs from "fs";
|
|
|
6
6
|
import * as path from "path";
|
|
7
7
|
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
8
8
|
import { toolError } from "../../utils/utils.js";
|
|
9
|
+
import { stageGeneratedPaths } from "../../utils/gitStaging.js";
|
|
9
10
|
import { buildRenameStrategy, buildFileRenameStrategy, buildUpdateStrategy, buildRegenerateStrategy, buildUpdateFileInstruction, buildRegenerateFileInstruction, } from "../../prompts/test-maintenance/actionsInstructions.js";
|
|
10
11
|
/**
|
|
11
12
|
* Compute a suggested new filename when an endpoint is renamed.
|
|
@@ -365,6 +366,15 @@ export function registerActionsTool(server) {
|
|
|
365
366
|
});
|
|
366
367
|
llmInstructionsObj.regenerate_strategy = buildRegenerateStrategy();
|
|
367
368
|
}
|
|
369
|
+
// Stage UPDATE and REGENERATE target paths so testbot's re-stage step
|
|
370
|
+
// commits the agent's edits even when files are outside testDirectory.
|
|
371
|
+
const stagedPaths = [
|
|
372
|
+
...updateRecommendations.map((r) => r.testFile),
|
|
373
|
+
...regenerateRecommendations.map((r) => r.testFile),
|
|
374
|
+
];
|
|
375
|
+
for (const p of stagedPaths) {
|
|
376
|
+
await stageGeneratedPaths(p).catch((e) => logger.warning(`Failed to stage maintained file ${p}: ${e.message}`));
|
|
377
|
+
}
|
|
368
378
|
const llmInstructions = JSON.stringify(llmInstructionsObj, null, 2);
|
|
369
379
|
const contentBlocks = [
|
|
370
380
|
{
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { CandidateUiPage } from "../../utils/uiPageEnumerator.js";
|
|
4
|
+
import { TraceFile } from "../../types/RepositoryAnalysis.js";
|
|
5
|
+
import { ScannedEndpoint } from "../../utils/repoScanner.js";
|
|
6
|
+
import { TraceParseResult } from "../../utils/trace-parser.js";
|
|
7
|
+
/** Exported for testing: maps a parsed trace result to a TraceFile. */
|
|
8
|
+
export declare function buildTraceFileEntry(tracePath: string, result: TraceParseResult): TraceFile;
|
|
9
|
+
export declare function isSecurityRelevantDiff(diffContent: string): boolean;
|
|
10
|
+
export declare function filterEndpointsBySpec(scannedEndpoints: ScannedEndpoint[], specPaths: Set<string>, specPathItems: Record<string, any>, diffChangedPaths: Set<string>): ScannedEndpoint[];
|
|
11
|
+
export declare function isGraphQLFile(filePath: string, repositoryPath: string): Promise<boolean>;
|
|
12
|
+
export declare function filterUnsupportedGraphQLEndpoints(endpoints: ScannedEndpoint[], repositoryPath: string): Promise<ScannedEndpoint[]>;
|
|
13
|
+
export declare const analyzeChangesInputSchema: {
|
|
14
|
+
repositoryPath: z.ZodString;
|
|
15
|
+
scope: z.ZodOptional<z.ZodDefault<z.ZodEnum<["full_repo", "branch_diff"]>>>;
|
|
16
|
+
baseBranch: z.ZodOptional<z.ZodString>;
|
|
17
|
+
testDirectory: z.ZodOptional<z.ZodString>;
|
|
18
|
+
topN: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
|
|
19
|
+
maxGenerate: z.ZodOptional<z.ZodNumber>;
|
|
20
|
+
prNumber: z.ZodOptional<z.ZodNumber>;
|
|
21
|
+
testsRepoDir: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
22
|
+
};
|
|
23
|
+
export declare const NO_UI_INSTRUCTIONS = "No UI changes detected \u2014 no blueprint capture needed.";
|
|
24
|
+
export declare const NO_RESOLVABLE_URLS_INSTRUCTIONS = "Frontend changes detected but no candidate URLs could be resolved (workspace baseUrl missing or no router files matched). UI recommendations will be source-grounded only.";
|
|
25
|
+
export declare function buildCaptureInstructions(pages: CandidateUiPage[]): string;
|
|
26
|
+
export declare function registerAnalyzeChangesTool(server: McpServer): void;
|
|
@@ -24,6 +24,32 @@ import { buildAnalysisOutputText } from "../../prompts/test-recommendation/analy
|
|
|
24
24
|
import { parseTraceFile, discoverTraceFiles, discoverPlaywrightZips, } from "../../utils/trace-parser.js";
|
|
25
25
|
import { TestSource } from "../../types/TestAnalysis.js";
|
|
26
26
|
import { parsePRComments } from "../../utils/pr-comment-parser.js";
|
|
27
|
+
// Generic module names that appear as imports in virtually every router file.
|
|
28
|
+
// Matching against these would extract ALL prefix= tokens from mount files,
|
|
29
|
+
// flooding changedResources with unrelated resources.
|
|
30
|
+
const GENERIC_MODULE_NAMES = new Set([
|
|
31
|
+
"models", "utils", "helpers", "schemas", "types", "constants",
|
|
32
|
+
"config", "settings", "exceptions", "errors", "base", "common",
|
|
33
|
+
"app", "main", "index", "server", "init", "deps", "dependencies",
|
|
34
|
+
]);
|
|
35
|
+
function resolveMountPrefixTokens(mountFileContents, changedFiles) {
|
|
36
|
+
const tokens = changedFiles.flatMap((file) => {
|
|
37
|
+
const mod = path.basename(file).replace(/\.[^.]+$/, "").toLowerCase();
|
|
38
|
+
if (GENERIC_MODULE_NAMES.has(mod))
|
|
39
|
+
return [];
|
|
40
|
+
const modRe = new RegExp(`\\b${mod.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
|
|
41
|
+
return mountFileContents.flatMap((content) => {
|
|
42
|
+
if (!modRe.test(content.toLowerCase()))
|
|
43
|
+
return [];
|
|
44
|
+
const matches = [...content.matchAll(/prefix\s*=\s*["'](\/?[^"']+)/g)];
|
|
45
|
+
return matches.flatMap((m) => {
|
|
46
|
+
const token = extractResourceFromPath(m[1]);
|
|
47
|
+
return token !== "unknown" ? [token] : [];
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
return [...new Set(tokens)];
|
|
52
|
+
}
|
|
27
53
|
/** Exported for testing: maps a parsed trace result to a TraceFile. */
|
|
28
54
|
export function buildTraceFileEntry(tracePath, result) {
|
|
29
55
|
return {
|
|
@@ -339,17 +365,40 @@ export const analyzeChangesInputSchema = {
|
|
|
339
365
|
.number()
|
|
340
366
|
.optional()
|
|
341
367
|
.describe("GitHub PR number. When provided, fetches previous TestBot comments on this PR and skips re-recommending tests already suggested in earlier commits — reduces duplicate recommendations across multiple pushes to the same PR."),
|
|
342
|
-
stateOutputFile: z
|
|
343
|
-
.string()
|
|
344
|
-
.refine((v) => path.isAbsolute(v), { message: "stateOutputFile must be an absolute path" })
|
|
345
|
-
.optional()
|
|
346
|
-
.describe("Absolute path where the state file should be written. When provided, overrides the default auto-generated temp path so the caller can locate it without log parsing."),
|
|
347
368
|
testsRepoDir: z
|
|
348
369
|
.string()
|
|
349
370
|
.refine((v) => path.isAbsolute(v), { message: "testsRepoDir must be an absolute path" })
|
|
350
371
|
.optional()
|
|
351
372
|
.describe("Absolute path to a separate test repository clone. When set, existing test discovery scans this directory instead of repositoryPath. Used in cross-repo test delivery mode where tests live in a separate repo."),
|
|
352
373
|
};
|
|
374
|
+
// ── UI blueprint-capture instructions ──
|
|
375
|
+
// Moved here from the former skyramp_ui_analyze_changes pre-flight tool. These
|
|
376
|
+
// strings + buildCaptureInstructions are appended to the tool output so the
|
|
377
|
+
// agent captures browser_blueprints on the candidate UI pages after this call.
|
|
378
|
+
// The captures stay in its tool-result history and serve as element vocabulary
|
|
379
|
+
// for UI recommendation reasoning (enforced at submit time by the Blueprint
|
|
380
|
+
// Citation Invariant in the testbot prompt).
|
|
381
|
+
export const NO_UI_INSTRUCTIONS = `No UI changes detected — no blueprint capture needed.`;
|
|
382
|
+
export const NO_RESOLVABLE_URLS_INSTRUCTIONS = `Frontend changes detected but no candidate URLs could be resolved (workspace baseUrl missing or no router files matched). UI recommendations will be source-grounded only.`;
|
|
383
|
+
export function buildCaptureInstructions(pages) {
|
|
384
|
+
const pagesYaml = pages
|
|
385
|
+
.map((p, i) => ` ${i + 1}. ${p.url} (sourcedFrom: ${p.sourcedFrom.join(", ") || "(none)"}, strategy: ${p.strategy})`)
|
|
386
|
+
.join("\n");
|
|
387
|
+
return `Frontend changes detected. **Before writing any UI recommendation \`reasoning\`, capture blueprints on the candidate UI pages below.** Those captures stay in your tool-result history and serve as element vocabulary — the recommendation catalog further down gives you the authoring rules; you bring the observed elements.
|
|
388
|
+
|
|
389
|
+
**Candidate URLs:**
|
|
390
|
+
${pagesYaml}
|
|
391
|
+
|
|
392
|
+
**For each candidate URL:**
|
|
393
|
+
- \`browser_navigate\` to the URL
|
|
394
|
+
- \`browser_blueprint\` to capture the page
|
|
395
|
+
|
|
396
|
+
You don't need to thread the blueprints back into a tool call — they're in your context once captured.
|
|
397
|
+
|
|
398
|
+
If a candidate URL 404s or redirects unexpectedly, navigate from the workspace baseUrl and explore (admin apps mount routes under base prefixes the source extraction can't see). If the page rendered but lacks the changed feature (gated UI: modal, dropdown, accordion), do NOT iterate further during this step — UI recs will fall back to source-grounded prose for those, and the agent's later trace recording (Task 2) will navigate into the gate via capture-act-capture.
|
|
399
|
+
|
|
400
|
+
If \`browser_blueprint\` fails on every candidate URL (app unreachable, all 404s), proceed and log an \`issuesFound\` info entry. Recommendations will be source-grounded; non-UI work is unaffected.`;
|
|
401
|
+
}
|
|
353
402
|
export function registerAnalyzeChangesTool(server) {
|
|
354
403
|
server.registerTool(TOOL_NAME, {
|
|
355
404
|
annotations: {
|
|
@@ -540,6 +589,30 @@ export function registerAnalyzeChangesTool(server) {
|
|
|
540
589
|
// testsRepoDir is a cross-repo path override — honour it when set.
|
|
541
590
|
// Otherwise always scan the full repo root.
|
|
542
591
|
const testDir = params.testsRepoDir ?? undefined;
|
|
592
|
+
const routerMountContext = grepRouterMountingContext(params.repositoryPath);
|
|
593
|
+
// Read contents once — used for token extraction and LLM prompt injection.
|
|
594
|
+
const ROUTER_INLINE_LIMIT = 4096;
|
|
595
|
+
const routerMountContextContents = routerMountContext.map((f) => {
|
|
596
|
+
try {
|
|
597
|
+
const abs = path.isAbsolute(f) ? f : path.join(params.repositoryPath, f);
|
|
598
|
+
if (fs.statSync(abs).size > ROUTER_INLINE_LIMIT * 10)
|
|
599
|
+
return ""; // skip very large files
|
|
600
|
+
return fs.readFileSync(abs, "utf-8");
|
|
601
|
+
}
|
|
602
|
+
catch {
|
|
603
|
+
return "";
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
// Size-capped subset for LLM prompt injection.
|
|
607
|
+
const ROUTER_INLINE_MAX_FILES = 3;
|
|
608
|
+
const routerFileContents = routerMountContext
|
|
609
|
+
.slice(0, ROUTER_INLINE_MAX_FILES)
|
|
610
|
+
.flatMap((f, i) => {
|
|
611
|
+
const content = routerMountContextContents[i] ?? "";
|
|
612
|
+
if (!content || content.length > ROUTER_INLINE_LIMIT)
|
|
613
|
+
return [];
|
|
614
|
+
return [{ file: f, content: content.trimEnd() }];
|
|
615
|
+
});
|
|
543
616
|
// Compute changedResources from classified endpoints for test discovery filtering.
|
|
544
617
|
// undefined → full-repo mode (no diff context)
|
|
545
618
|
// [] → PR mode, no endpoints found → skip external tests
|
|
@@ -568,14 +641,22 @@ export function registerAnalyzeChangesTool(server) {
|
|
|
568
641
|
}
|
|
569
642
|
else {
|
|
570
643
|
// All endpoints resolved to "unknown" (e.g. NestJS relative paths) —
|
|
571
|
-
// extract resource names from
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
644
|
+
// first try to extract resource names from router mount prefixes.
|
|
645
|
+
const changedSourceFiles = classifiedEndpoints.changedFiles ?? [];
|
|
646
|
+
const mountedResources = resolveMountPrefixTokens(routerMountContextContents, changedSourceFiles);
|
|
647
|
+
if (mountedResources.length > 0) {
|
|
648
|
+
changedResources = mountedResources;
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
// Fall back to extracting resource names from changed file paths.
|
|
652
|
+
const candidateFiles = classifiedEndpoints.unmatchedFiles.length > 0
|
|
653
|
+
? classifiedEndpoints.unmatchedFiles
|
|
654
|
+
: classifiedEndpoints.changedFiles ?? [];
|
|
655
|
+
const fromFiles = candidateFiles
|
|
656
|
+
.map((f) => extractResourceFromPath(f.replace(/\.[^./]+$/, "").replace(FRAMEWORK_SUFFIX_RE, "")))
|
|
657
|
+
.filter((r, i, arr) => r !== "unknown" && arr.indexOf(r) === i);
|
|
658
|
+
changedResources = fromFiles.length > 0 ? fromFiles : ["unknown"];
|
|
659
|
+
}
|
|
579
660
|
}
|
|
580
661
|
}
|
|
581
662
|
else if (classifiedEndpoints.unmatchedFiles.length > 0) {
|
|
@@ -594,7 +675,15 @@ export function registerAnalyzeChangesTool(server) {
|
|
|
594
675
|
changedResources = fromFiles.length > 0 ? fromFiles : ["unknown"];
|
|
595
676
|
}
|
|
596
677
|
else {
|
|
597
|
-
|
|
678
|
+
// No endpoints and no unmatched files — the diff touches only non-route
|
|
679
|
+
// files (e.g. schema, model, DTO). Extract resource tokens from the
|
|
680
|
+
// changed file paths so tests that import the changed symbol are still
|
|
681
|
+
// surfaced for drift assessment. Fall back to [] only if extraction
|
|
682
|
+
// yields nothing (e.g. a file with a non-resource name like utils.py).
|
|
683
|
+
const fromChangedFiles = (classifiedEndpoints.changedFiles ?? [])
|
|
684
|
+
.map((f) => extractResourceFromPath(f.replace(/\.[^./]+$/, "").replace(FRAMEWORK_SUFFIX_RE, "")))
|
|
685
|
+
.filter((r, i, arr) => r !== "unknown" && arr.indexOf(r) === i);
|
|
686
|
+
changedResources = fromChangedFiles.length > 0 ? fromChangedFiles : [];
|
|
598
687
|
}
|
|
599
688
|
}
|
|
600
689
|
let existingTests = [];
|
|
@@ -875,10 +964,15 @@ export function registerAnalyzeChangesTool(server) {
|
|
|
875
964
|
sourceFile: m.sourceFile ?? "",
|
|
876
965
|
})))
|
|
877
966
|
: [];
|
|
967
|
+
const removedEndpointsForDrafting = classifiedEndpoints?.removedEndpoints.flatMap((ep) => ep.methods.map((m) => ({
|
|
968
|
+
method: m,
|
|
969
|
+
path: ep.path,
|
|
970
|
+
sourceFile: ep.sourceFile,
|
|
971
|
+
}))) ?? [];
|
|
878
972
|
const codeInferredScenarios = draftScenariosFromEndpoints(skeletonEndpoints, scenarioDraftSeed, wsAuthType, {
|
|
879
973
|
changedEndpoints: changedEndpointsForSecurityExpansion,
|
|
880
974
|
securityRelevantDiff,
|
|
881
|
-
});
|
|
975
|
+
}, removedEndpointsForDrafting);
|
|
882
976
|
let allDraftedScenarios = codeInferredScenarios;
|
|
883
977
|
if (traceResult && traceResult.userFlows.length > 0) {
|
|
884
978
|
const traceScenarios = traceResult.userFlows
|
|
@@ -1081,7 +1175,6 @@ export function registerAnalyzeChangesTool(server) {
|
|
|
1081
1175
|
// can be persisted to the state file for downstream tools (health, drift).
|
|
1082
1176
|
// Without them, analyzeTestHealth would work only off the static catalog
|
|
1083
1177
|
// which has wrong paths for nested resources and unsupported frameworks.
|
|
1084
|
-
const routerMountContext = grepRouterMountingContext(params.repositoryPath);
|
|
1085
1178
|
const routeLikeUnmatchedFiles = [];
|
|
1086
1179
|
for (const file of classifiedEndpoints?.unmatchedFiles ?? []) {
|
|
1087
1180
|
const routeLike = SOURCE_EXTS.test(file) &&
|
|
@@ -1110,25 +1203,6 @@ export function registerAnalyzeChangesTool(server) {
|
|
|
1110
1203
|
diffFilePath = path.join(os.tmpdir(), `skyramp-diff-${sessionId}.diff`);
|
|
1111
1204
|
await fs.promises.writeFile(diffFilePath, diffData.diffContent, { encoding: "utf-8", mode: 0o600 });
|
|
1112
1205
|
}
|
|
1113
|
-
// Read router mount files server-side (size-capped) so the LLM has them
|
|
1114
|
-
// inline and doesn't need an extra read step when no spec is available.
|
|
1115
|
-
const ROUTER_INLINE_LIMIT = 4096; // bytes — skip files larger than ~4 KB
|
|
1116
|
-
const ROUTER_INLINE_MAX_FILES = 3;
|
|
1117
|
-
const routerFileContents = routerMountContext
|
|
1118
|
-
.slice(0, ROUTER_INLINE_MAX_FILES)
|
|
1119
|
-
.flatMap((f) => {
|
|
1120
|
-
try {
|
|
1121
|
-
const absPath = path.isAbsolute(f) ? f : path.join(params.repositoryPath, f);
|
|
1122
|
-
const stat = fs.statSync(absPath);
|
|
1123
|
-
if (stat.size > ROUTER_INLINE_LIMIT)
|
|
1124
|
-
return [];
|
|
1125
|
-
const content = fs.readFileSync(absPath, "utf-8").trimEnd();
|
|
1126
|
-
return [{ file: f, content }];
|
|
1127
|
-
}
|
|
1128
|
-
catch {
|
|
1129
|
-
return [];
|
|
1130
|
-
}
|
|
1131
|
-
});
|
|
1132
1206
|
// Compute UI context from the diff's changed files using the shared
|
|
1133
1207
|
// `isFrontendFile` classifier. Persisting this in the stateFile lets
|
|
1134
1208
|
// skyramp_analyze_test_health and the recommendation prompt consume the
|
|
@@ -1136,9 +1210,9 @@ export function registerAnalyzeChangesTool(server) {
|
|
|
1136
1210
|
//
|
|
1137
1211
|
// candidateUiPages is enumerated programmatically via the strategy
|
|
1138
1212
|
// ladder in uiPageEnumerator (framework route grep, source-grounded
|
|
1139
|
-
// routes, root fallback). The
|
|
1140
|
-
//
|
|
1141
|
-
//
|
|
1213
|
+
// routes, root fallback). The agent uses these to capture
|
|
1214
|
+
// browser_blueprints — see uiInstructions below, which this tool returns
|
|
1215
|
+
// so the agent captures element vocabulary for UI rec reasoning.
|
|
1142
1216
|
const uiContext = await (async () => {
|
|
1143
1217
|
const changedFiles = classifiedEndpoints?.changedFiles ?? [];
|
|
1144
1218
|
if (changedFiles.length === 0)
|
|
@@ -1149,9 +1223,8 @@ export function registerAnalyzeChangesTool(server) {
|
|
|
1149
1223
|
// enumeration in.
|
|
1150
1224
|
// Compute hasFlutterSdkDep once and pass it through so .dart files
|
|
1151
1225
|
// in a Flutter project are recognised as frontend. See Confluence
|
|
1152
|
-
// "Flutter support in Testbot" — this is the
|
|
1153
|
-
//
|
|
1154
|
-
// uiAnalyzeChangesTool). Both must agree to avoid silent divergence.
|
|
1226
|
+
// "Flutter support in Testbot" — this is the budget-driving call site
|
|
1227
|
+
// that must thread the flag so a Flutter PR enters the UI pipeline.
|
|
1155
1228
|
const flutterSdk = hasFlutterSdkDep(params.repositoryPath);
|
|
1156
1229
|
const frontendFiles = changedFiles.filter((f) => isFrontendFile(f, { hasFlutterSdkDep: flutterSdk }) && !isTestFile(f));
|
|
1157
1230
|
if (frontendFiles.length === 0)
|
|
@@ -1162,6 +1235,16 @@ export function registerAnalyzeChangesTool(server) {
|
|
|
1162
1235
|
candidateUiPages,
|
|
1163
1236
|
};
|
|
1164
1237
|
})();
|
|
1238
|
+
// Derive the UI blueprint-capture instructions the agent acts on after
|
|
1239
|
+
// this call. Mirrors the branching of the former skyramp_ui_analyze_changes
|
|
1240
|
+
// pre-flight tool, now folded in here so the agent makes one analysis call
|
|
1241
|
+
// instead of two. The captures stay in the agent's tool-result history and
|
|
1242
|
+
// serve as element vocabulary when it writes UI recommendation reasoning.
|
|
1243
|
+
const uiInstructions = !uiContext || uiContext.changedFrontendFiles.length === 0
|
|
1244
|
+
? NO_UI_INSTRUCTIONS
|
|
1245
|
+
: uiContext.candidateUiPages.length === 0
|
|
1246
|
+
? NO_RESOLVABLE_URLS_INSTRUCTIONS
|
|
1247
|
+
: buildCaptureInstructions(uiContext.candidateUiPages);
|
|
1165
1248
|
const unifiedState = {
|
|
1166
1249
|
existingTests,
|
|
1167
1250
|
newEndpoints: newEndpointsForDrafting,
|
|
@@ -1215,17 +1298,14 @@ export function registerAnalyzeChangesTool(server) {
|
|
|
1215
1298
|
relevantExternalTestPaths,
|
|
1216
1299
|
},
|
|
1217
1300
|
};
|
|
1218
|
-
// Clean up old state files (>24 hours) before creating new one
|
|
1219
|
-
const stateDir = params.stateOutputFile
|
|
1220
|
-
? path.dirname(path.resolve(params.stateOutputFile))
|
|
1221
|
-
: undefined;
|
|
1301
|
+
// Clean up old state files (>24 hours) before creating new one.
|
|
1222
1302
|
try {
|
|
1223
|
-
await StateManager.cleanupOldFiles(24
|
|
1303
|
+
await StateManager.cleanupOldFiles(24);
|
|
1224
1304
|
}
|
|
1225
1305
|
catch (error) {
|
|
1226
1306
|
logger.warning(`Failed to cleanup old state files: ${error.message}`);
|
|
1227
1307
|
}
|
|
1228
|
-
const stateManager = new StateManager("analysis", sessionId
|
|
1308
|
+
const stateManager = new StateManager("analysis", sessionId);
|
|
1229
1309
|
await stateManager.writeData(unifiedState, {
|
|
1230
1310
|
repositoryPath: params.repositoryPath,
|
|
1231
1311
|
step: "analyze_changes",
|
|
@@ -1348,7 +1428,7 @@ export function registerAnalyzeChangesTool(server) {
|
|
|
1348
1428
|
content: [
|
|
1349
1429
|
{
|
|
1350
1430
|
type: "text",
|
|
1351
|
-
text: `\`\`\`json\n${structuredSummary}\n\`\`\`\n\n${outputText}\n\n---\n\n## Pre-built Test Catalog — Fill in placeholders from source code, then display verbatim\n⚠️ Do NOT reformat, rename sections, or generate a new catalog. Replace \`<…from source>\` values, then show this output exactly as-is, grouped by test type.\n\n${recommendationPrompt}`,
|
|
1431
|
+
text: `\`\`\`json\n${structuredSummary}\n\`\`\`\n\n## UI Blueprint Capture — do this BEFORE writing UI recommendation reasoning\n${uiInstructions}\n\n${outputText}\n\n---\n\n## Pre-built Test Catalog — Fill in placeholders from source code, then display verbatim\n⚠️ Do NOT reformat, rename sections, or generate a new catalog. Replace \`<…from source>\` values, then show this output exactly as-is, grouped by test type.\n\n${recommendationPrompt}`,
|
|
1352
1432
|
},
|
|
1353
1433
|
],
|
|
1354
1434
|
isError: false,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,70 +1,91 @@
|
|
|
1
|
-
//
|
|
2
|
-
jest
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
// @ts-nocheck - Jest ESM type inference issues
|
|
2
|
+
import { jest } from "@jest/globals";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
// Mock all heavy dependencies so the module can be loaded in isolation.
|
|
5
|
+
// Under native ESM, jest.mock is not hoisted — every mock the subject's import
|
|
6
|
+
// graph touches must use unstable_mockModule before the dynamic import below.
|
|
7
|
+
jest.unstable_mockModule("simple-git", () => ({ simpleGit: jest.fn() }));
|
|
8
|
+
jest.unstable_mockModule("../../services/AnalyticsService.js", () => ({
|
|
5
9
|
AnalyticsService: { pushMCPToolEvent: jest.fn() },
|
|
6
10
|
}));
|
|
7
|
-
jest.
|
|
11
|
+
jest.unstable_mockModule("../../prompts/test-recommendation/test-recommendation-prompt.js", () => ({
|
|
8
12
|
buildRecommendationPrompt: jest.fn(),
|
|
9
13
|
}));
|
|
10
|
-
jest.
|
|
14
|
+
jest.unstable_mockModule("../../prompts/test-recommendation/recommendationSections.js", () => ({
|
|
11
15
|
MAX_RECOMMENDATIONS: 10,
|
|
12
16
|
MAX_TESTS_TO_GENERATE: 3,
|
|
13
17
|
}));
|
|
14
|
-
jest.
|
|
18
|
+
jest.unstable_mockModule("../../prompts/test-recommendation/analysisOutputPrompt.js", () => ({
|
|
15
19
|
buildAnalysisOutputText: jest.fn(),
|
|
16
20
|
}));
|
|
17
|
-
jest.
|
|
21
|
+
jest.unstable_mockModule("../../services/TestDiscoveryService.js", () => ({
|
|
18
22
|
TestDiscoveryService: jest.fn(),
|
|
19
23
|
}));
|
|
20
|
-
jest.
|
|
24
|
+
jest.unstable_mockModule("../../utils/branchDiff.js", () => ({
|
|
21
25
|
computeBranchDiff: jest.fn(),
|
|
22
26
|
}));
|
|
23
|
-
jest.
|
|
27
|
+
jest.unstable_mockModule("../../utils/routeParsers.js", () => ({
|
|
24
28
|
classifyEndpointsByChangedFiles: jest.fn(),
|
|
25
29
|
extractResourceFromPath: jest.fn(),
|
|
26
30
|
parseFileEndpoints: jest.fn(),
|
|
27
31
|
}));
|
|
28
|
-
jest.
|
|
32
|
+
jest.unstable_mockModule("../../utils/repoScanner.js", () => ({
|
|
29
33
|
scanAllRepoEndpoints: jest.fn(),
|
|
30
34
|
scanRelatedEndpoints: jest.fn(),
|
|
31
35
|
grepRouterMountingContext: jest.fn(),
|
|
32
36
|
findCandidateRouteFiles: jest.fn(),
|
|
33
37
|
}));
|
|
34
|
-
jest.
|
|
38
|
+
jest.unstable_mockModule("../../utils/projectMetadata.js", () => ({
|
|
35
39
|
detectProjectMetadata: jest.fn(),
|
|
36
40
|
}));
|
|
37
|
-
jest.
|
|
41
|
+
jest.unstable_mockModule("../../utils/scenarioDrafting.js", () => ({
|
|
38
42
|
draftScenariosFromEndpoints: jest.fn(),
|
|
39
43
|
}));
|
|
40
|
-
jest.
|
|
44
|
+
jest.unstable_mockModule("../../utils/trace-parser.js", () => ({
|
|
41
45
|
parseTraceFile: jest.fn(),
|
|
42
46
|
discoverTraceFiles: jest.fn(),
|
|
43
47
|
discoverPlaywrightZips: jest.fn(),
|
|
44
48
|
}));
|
|
45
|
-
jest.
|
|
49
|
+
jest.unstable_mockModule("../../utils/pr-comment-parser.js", () => ({
|
|
46
50
|
parsePRComments: jest.fn(),
|
|
47
51
|
}));
|
|
48
|
-
jest.
|
|
52
|
+
jest.unstable_mockModule("../../utils/AnalysisStateManager.js", () => ({
|
|
49
53
|
StateManager: jest.fn().mockImplementation(() => ({
|
|
50
54
|
writeData: jest.fn(),
|
|
51
55
|
})),
|
|
52
56
|
registerSession: jest.fn(),
|
|
53
57
|
storeSessionData: jest.fn(),
|
|
58
|
+
setTestsRepoDir: jest.fn(),
|
|
54
59
|
}));
|
|
55
|
-
|
|
60
|
+
// Keep every real workspaceAuth export the subject's import graph needs
|
|
61
|
+
// (readWorkspaceConfigRaw, getDefaultAuthHeader, WorkspaceAuthType, …) and
|
|
62
|
+
// override only parseWorkspaceAuthType. requireActual can't load ESM
|
|
63
|
+
// synchronously, so import the real module first (mock is not hoisted).
|
|
64
|
+
const realWorkspaceAuth = await import("../../utils/workspaceAuth.js");
|
|
65
|
+
jest.unstable_mockModule("../../utils/workspaceAuth.js", () => ({
|
|
66
|
+
...realWorkspaceAuth,
|
|
56
67
|
parseWorkspaceAuthType: jest.fn(),
|
|
57
68
|
}));
|
|
58
|
-
jest.
|
|
69
|
+
jest.unstable_mockModule("../../utils/logger.js", () => ({
|
|
59
70
|
logger: { info: jest.fn(), debug: jest.fn(), error: jest.fn(), warn: jest.fn(), warning: jest.fn() },
|
|
60
71
|
}));
|
|
61
|
-
jest.
|
|
72
|
+
jest.unstable_mockModule("@modelcontextprotocol/sdk/server/mcp.js", () => ({
|
|
62
73
|
McpServer: jest.fn(),
|
|
63
74
|
}));
|
|
64
|
-
jest.
|
|
65
|
-
jest.
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
jest.unstable_mockModule("@modelcontextprotocol/sdk/types.js", () => ({}));
|
|
76
|
+
jest.unstable_mockModule("@modelcontextprotocol/sdk/shared/protocol.js", () => ({}));
|
|
77
|
+
// All subject + mocked-module imports must be dynamic and live AFTER the
|
|
78
|
+
// unstable_mockModule calls above — static imports hoist above the mocks under
|
|
79
|
+
// native ESM, loading the real (heavy) modules unmocked.
|
|
80
|
+
const { analyzeChangesInputSchema, isSecurityRelevantDiff, filterEndpointsBySpec, filterUnsupportedGraphQLEndpoints, isGraphQLFile, registerAnalyzeChangesTool, buildTraceFileEntry, buildCaptureInstructions, NO_UI_INSTRUCTIONS, NO_RESOLVABLE_URLS_INSTRUCTIONS, } = await import("./analyzeChangesTool.js");
|
|
81
|
+
const { computeBranchDiff } = await import("../../utils/branchDiff.js");
|
|
82
|
+
const { classifyEndpointsByChangedFiles } = await import("../../utils/routeParsers.js");
|
|
83
|
+
const { scanRelatedEndpoints, scanAllRepoEndpoints } = await import("../../utils/repoScanner.js");
|
|
84
|
+
const { StateManager } = await import("../../utils/AnalysisStateManager.js");
|
|
85
|
+
const os = await import("os");
|
|
86
|
+
const path = await import("path");
|
|
87
|
+
const fsSync = await import("fs");
|
|
88
|
+
const fsModule = fsSync;
|
|
68
89
|
const schema = z.object(analyzeChangesInputSchema);
|
|
69
90
|
describe("isSecurityRelevantDiff", () => {
|
|
70
91
|
it("matches auth-specific middleware and admin-key signals", () => {
|
|
@@ -78,68 +99,35 @@ describe("isSecurityRelevantDiff", () => {
|
|
|
78
99
|
expect(isSecurityRelevantDiff("move compression middleware to server setup")).toBe(false);
|
|
79
100
|
});
|
|
80
101
|
});
|
|
81
|
-
describe("analyzeChangesInputSchema — stateOutputFile validation", () => {
|
|
82
|
-
it("accepts a valid absolute path", () => {
|
|
83
|
-
const result = schema.safeParse({
|
|
84
|
-
repositoryPath: "/repo",
|
|
85
|
-
stateOutputFile: "/tmp/analyze-changes-state.json",
|
|
86
|
-
});
|
|
87
|
-
expect(result.success).toBe(true);
|
|
88
|
-
});
|
|
89
|
-
it("rejects a relative path for stateOutputFile", () => {
|
|
90
|
-
// stateOutputFile must be absolute so the caller can guarantee the file location.
|
|
91
|
-
// Relative paths are silently ambiguous and should be rejected.
|
|
92
|
-
const result = schema.safeParse({
|
|
93
|
-
repositoryPath: "/repo",
|
|
94
|
-
stateOutputFile: "relative/path/state.json",
|
|
95
|
-
});
|
|
96
|
-
expect(result.success).toBe(false);
|
|
97
|
-
});
|
|
98
|
-
it("accepts absence of stateOutputFile (optional field)", () => {
|
|
99
|
-
const result = schema.safeParse({ repositoryPath: "/repo" });
|
|
100
|
-
expect(result.success).toBe(true);
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
102
|
describe("automatic old files cleanup", () => {
|
|
104
|
-
let StateManager;
|
|
105
103
|
let cleanupOldFilesSpy;
|
|
106
104
|
beforeEach(() => {
|
|
107
105
|
jest.clearAllMocks();
|
|
108
|
-
StateManager = require("../../utils/AnalysisStateManager.js").StateManager;
|
|
109
106
|
cleanupOldFilesSpy = jest.fn().mockResolvedValue(0);
|
|
110
107
|
StateManager.cleanupOldFiles = cleanupOldFilesSpy;
|
|
111
108
|
});
|
|
112
|
-
it("calls cleanupOldFiles with default temp dir
|
|
113
|
-
// This test verifies the cleanup call is made without stateDir when using default location
|
|
109
|
+
it("calls cleanupOldFiles with default temp dir", async () => {
|
|
114
110
|
expect(StateManager.cleanupOldFiles).toBeDefined();
|
|
115
111
|
await StateManager.cleanupOldFiles(24, undefined);
|
|
116
112
|
expect(cleanupOldFilesSpy).toHaveBeenCalledWith(24, undefined);
|
|
117
113
|
});
|
|
118
|
-
it("calls cleanupOldFiles with custom dir
|
|
119
|
-
// This test verifies the cleanup call is made with the directory of stateOutputFile
|
|
114
|
+
it("calls cleanupOldFiles with custom dir", async () => {
|
|
120
115
|
const customPath = "/custom/dir";
|
|
121
116
|
await StateManager.cleanupOldFiles(24, customPath);
|
|
122
117
|
expect(cleanupOldFilesSpy).toHaveBeenCalledWith(24, customPath);
|
|
123
118
|
});
|
|
124
|
-
it("calls cleanupOldFiles with empty stateTypes
|
|
125
|
-
// analyzeTestHealthTool passes [] so state files (skyramp-analysis-*, skyramp-recommendation-*)
|
|
126
|
-
// are never deleted — the caller still needs args.stateFile for skyramp_actions.
|
|
119
|
+
it("calls cleanupOldFiles with empty stateTypes", async () => {
|
|
127
120
|
await StateManager.cleanupOldFiles(24, undefined, []);
|
|
128
121
|
expect(cleanupOldFilesSpy).toHaveBeenCalledWith(24, undefined, []);
|
|
129
122
|
});
|
|
130
123
|
it("continues execution if cleanup fails", async () => {
|
|
131
|
-
// Cleanup failures should not crash the analyze flow
|
|
132
124
|
cleanupOldFilesSpy.mockRejectedValue(new Error("permission denied"));
|
|
133
|
-
await expect(StateManager.cleanupOldFiles(24).catch(() =>
|
|
134
|
-
// In the real code, this is caught and logged as a warning
|
|
135
|
-
return Promise.resolve();
|
|
136
|
-
})).resolves.toBeUndefined();
|
|
125
|
+
await expect(StateManager.cleanupOldFiles(24).catch(() => Promise.resolve())).resolves.toBeUndefined();
|
|
137
126
|
});
|
|
138
127
|
});
|
|
139
128
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
140
129
|
// filterEndpointsBySpec — spec filtering + merge logic (Step 4c / 4c-merge)
|
|
141
130
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
142
|
-
import { filterEndpointsBySpec } from "./analyzeChangesTool.js";
|
|
143
131
|
describe("filterEndpointsBySpec", () => {
|
|
144
132
|
const makeEp = (path, methods = ["GET"]) => ({ path, methods, sourceFile: "" });
|
|
145
133
|
it("removes scanned endpoints that are not in the spec and not diff-changed", () => {
|
|
@@ -224,10 +212,6 @@ describe("filterEndpointsBySpec", () => {
|
|
|
224
212
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
225
213
|
// isGraphQLFile — unit tests
|
|
226
214
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
227
|
-
import { filterUnsupportedGraphQLEndpoints, isGraphQLFile } from "./analyzeChangesTool.js";
|
|
228
|
-
import * as os from "os";
|
|
229
|
-
import * as path from "path";
|
|
230
|
-
import * as fsSync from "fs";
|
|
231
215
|
describe("isGraphQLFile", () => {
|
|
232
216
|
const tmpDir = os.tmpdir();
|
|
233
217
|
it("returns true for .graphql extension without reading content", async () => {
|
|
@@ -303,14 +287,6 @@ describe("filterUnsupportedGraphQLEndpoints", () => {
|
|
|
303
287
|
readSpy.mockRestore();
|
|
304
288
|
});
|
|
305
289
|
});
|
|
306
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
307
|
-
// analyzeChangesTool handler — GraphQL-only early return (handler-level)
|
|
308
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
309
|
-
import { registerAnalyzeChangesTool, buildTraceFileEntry } from "./analyzeChangesTool.js";
|
|
310
|
-
import { computeBranchDiff } from "../../utils/branchDiff.js";
|
|
311
|
-
import { classifyEndpointsByChangedFiles } from "../../utils/routeParsers.js";
|
|
312
|
-
import { scanRelatedEndpoints } from "../../utils/repoScanner.js";
|
|
313
|
-
import * as fsModule from "fs";
|
|
314
290
|
/** Register the tool against a minimal mock server and return the captured handler.
|
|
315
291
|
* The handler takes (params, extra) — extra is pre-filled with a no-op sendNotification. */
|
|
316
292
|
function captureAnalyzeHandler() {
|
|
@@ -399,7 +375,6 @@ describe("analyzeChangesTool handler — GraphQL-only early return", () => {
|
|
|
399
375
|
readSpy.mockRestore();
|
|
400
376
|
});
|
|
401
377
|
it("does NOT early-return when scope is full_repo (only fires for PR diffs)", async () => {
|
|
402
|
-
const { scanAllRepoEndpoints } = require("../../utils/repoScanner.js");
|
|
403
378
|
scanAllRepoEndpoints.mockReturnValue([]);
|
|
404
379
|
const handler = captureAnalyzeHandler();
|
|
405
380
|
const result = await handler({ ...baseParams, scope: "full_repo" });
|
|
@@ -437,3 +412,35 @@ describe("buildTraceFileEntry", () => {
|
|
|
437
412
|
expect(entry.userFlows).toEqual([]);
|
|
438
413
|
});
|
|
439
414
|
});
|
|
415
|
+
describe("UI blueprint-capture instructions (merged from ui_analyze_changes)", () => {
|
|
416
|
+
it("NO_UI_INSTRUCTIONS signals no capture is needed and references no follow-up tool", () => {
|
|
417
|
+
expect(NO_UI_INSTRUCTIONS).toMatch(/no blueprint capture needed/i);
|
|
418
|
+
// The former pre-flight tool told the agent to "proceed to
|
|
419
|
+
// skyramp_analyze_changes" — that's redundant now we're inside it.
|
|
420
|
+
expect(NO_UI_INSTRUCTIONS).not.toMatch(/skyramp_analyze_changes/);
|
|
421
|
+
});
|
|
422
|
+
it("NO_RESOLVABLE_URLS_INSTRUCTIONS falls back to source-grounded", () => {
|
|
423
|
+
expect(NO_RESOLVABLE_URLS_INSTRUCTIONS).toMatch(/source-grounded/i);
|
|
424
|
+
});
|
|
425
|
+
it("buildCaptureInstructions numbers each candidate URL with its provenance", () => {
|
|
426
|
+
const pages = [
|
|
427
|
+
{ url: "http://localhost:3000/notifications", sourcedFrom: ["app/routes.tsx"], strategy: "framework-route-grep" },
|
|
428
|
+
{ url: "http://localhost:3000/settings", sourcedFrom: [], strategy: "root-fallback" },
|
|
429
|
+
];
|
|
430
|
+
const text = buildCaptureInstructions(pages);
|
|
431
|
+
expect(text).toContain("1. http://localhost:3000/notifications");
|
|
432
|
+
expect(text).toContain("2. http://localhost:3000/settings");
|
|
433
|
+
expect(text).toContain("sourcedFrom: app/routes.tsx");
|
|
434
|
+
expect(text).toContain("sourcedFrom: (none)");
|
|
435
|
+
expect(text).toContain("browser_navigate");
|
|
436
|
+
expect(text).toContain("browser_blueprint");
|
|
437
|
+
});
|
|
438
|
+
it("buildCaptureInstructions does NOT emit a login step (uiCredentials dropped)", () => {
|
|
439
|
+
const pages = [
|
|
440
|
+
{ url: "http://localhost:3000/x", sourcedFrom: [], strategy: "root-fallback" },
|
|
441
|
+
];
|
|
442
|
+
const text = buildCaptureInstructions(pages);
|
|
443
|
+
expect(text).not.toMatch(/log in first/i);
|
|
444
|
+
expect(text).not.toMatch(/ui-credentials/i);
|
|
445
|
+
});
|
|
446
|
+
});
|
|
@@ -15,11 +15,11 @@ export function registerAnalyzeTestHealthTool(server) {
|
|
|
15
15
|
idempotentHint: true,
|
|
16
16
|
openWorldHint: false,
|
|
17
17
|
},
|
|
18
|
-
description: `Generate drift assessment instructions for existing tests — second step of the unified Test Health Analysis Flow
|
|
18
|
+
description: `Generate drift assessment instructions for existing tests — second step of the unified Test Health Analysis Flow.`,
|
|
19
19
|
inputSchema: {
|
|
20
20
|
stateFile: z
|
|
21
21
|
.string()
|
|
22
|
-
.describe("Path to state file
|
|
22
|
+
.describe("Path to the analyze-changes-state.json file returned by skyramp_analyze_changes (the stateFile field in its output). Do NOT pass summaryOutputFile or testbot-result.txt here."),
|
|
23
23
|
},
|
|
24
24
|
outputSchema: {
|
|
25
25
|
prompt: z.string().describe("LLM drift analysis prompt and assessment instructions."),
|
|
@@ -37,7 +37,10 @@ export function registerAnalyzeTestHealthTool(server) {
|
|
|
37
37
|
return toolError(`State file is empty or invalid: ${args.stateFile}. Call skyramp_analyze_changes first to generate a valid state file.`);
|
|
38
38
|
}
|
|
39
39
|
if (!repositoryPath || typeof repositoryPath !== "string") {
|
|
40
|
-
return toolError(`repositoryPath not found in state file metadata. The
|
|
40
|
+
return toolError(`repositoryPath not found in state file metadata. The stateFile parameter must be the analyze-changes-state.json path returned by skyramp_analyze_changes — not summaryOutputFile or testbot-result.txt. Re-run skyramp_analyze_changes to regenerate it.`);
|
|
41
|
+
}
|
|
42
|
+
if (!Array.isArray(stateData.existingTests)) {
|
|
43
|
+
return toolError(`stateFile does not contain test discovery data (existingTests missing). The stateFile parameter must be the analyze-changes-state.json path returned by skyramp_analyze_changes — not summaryOutputFile or testbot-result.txt.`);
|
|
41
44
|
}
|
|
42
45
|
// Skyramp tests: full drift analysis + all actions permitted.
|
|
43
46
|
// Relevant external tests (user-written, relevant to this PR's endpoints): drift analysis
|