@skyramp/mcp 0.3.0-rc.1 → 0.3.0
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/prompts/test-maintenance/actionsInstructions.js +4 -2
- package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +9 -0
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +8 -7
- package/build/prompts/test-maintenance/driftAnalysisSections.js +4 -5
- package/build/prompts/test-maintenance/driftAnalysisShared.d.ts +11 -0
- package/build/prompts/test-maintenance/driftAnalysisShared.js +22 -6
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +23 -31
- package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +7 -1
- package/build/prompts/test-recommendation/diffExecutionPlan.js +20 -6
- package/build/prompts/test-recommendation/scopeAssessment.d.ts +10 -1
- package/build/prompts/test-recommendation/scopeAssessment.js +27 -2
- package/build/prompts/test-recommendation/scopeAssessment.test.js +35 -0
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +5 -1
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +61 -0
- package/build/prompts/testbot/testbot-prompts.js +7 -5
- package/build/prompts/testbot/testbot-prompts.test.js +1 -1
- package/build/services/AnalyticsService.js +37 -2
- package/build/services/TestDiscoveryService.d.ts +38 -4
- package/build/services/TestDiscoveryService.js +187 -16
- package/build/services/TestDiscoveryService.test.js +232 -10
- package/build/services/TestExecutionService.d.ts +12 -1
- package/build/services/TestExecutionService.js +20 -41
- package/build/services/TestGenerationService.d.ts +8 -0
- package/build/services/TestGenerationService.js +62 -17
- package/build/services/TestGenerationService.test.js +67 -1
- package/build/tools/auth/loginTool.js +1 -1
- package/build/tools/auth/logoutTool.js +1 -1
- package/build/tools/code-refactor/codeReuseTool.js +1 -1
- package/build/tools/code-refactor/modularizationTool.js +1 -1
- package/build/tools/executeSkyrampTestTool.js +30 -1
- package/build/tools/fixErrorTool.js +1 -1
- package/build/tools/one-click/oneClickTool.js +1 -1
- package/build/tools/test-management/actionsTool.js +76 -41
- package/build/tools/test-management/actionsTool.test.js +104 -38
- package/build/tools/test-management/analyzeChangesTool.js +102 -4
- package/build/tools/test-management/analyzeChangesTool.test.js +3 -1
- package/build/tools/test-management/analyzeTestHealthTool.js +9 -1
- package/build/tools/test-management/analyzeTestHealthTool.test.js +1 -1
- package/build/tools/trace/startTraceCollectionTool.js +1 -1
- package/build/tools/trace/stopTraceCollectionTool.js +1 -1
- package/build/tools/workspace/initScanWorkspaceTool.js +1 -1
- package/build/tools/workspace/initializeWorkspaceTool.js +1 -1
- package/build/tools/workspace/initializeWorkspaceTool.test.js +1 -1
- package/build/types/RepositoryAnalysis.d.ts +6 -6
- package/build/types/TestAnalysis.d.ts +13 -0
- package/build/utils/dartRouteExtractor.js +8 -30
- package/build/utils/docker.test.js +1 -1
- package/build/utils/fileWalk.d.ts +71 -0
- package/build/utils/fileWalk.js +79 -0
- package/build/utils/fileWalk.test.d.ts +1 -0
- package/build/utils/fileWalk.test.js +252 -0
- package/build/utils/frontendIntegration.js +20 -36
- package/build/utils/frontendSelectors.d.ts +34 -0
- package/build/utils/frontendSelectors.js +124 -0
- package/build/utils/frontendSelectors.test.d.ts +1 -0
- package/build/utils/frontendSelectors.test.js +118 -0
- package/build/utils/importerHop.d.ts +18 -0
- package/build/utils/importerHop.js +48 -3
- package/build/utils/importerHop.test.js +71 -1
- package/build/utils/pythonMountPrefixes.d.ts +25 -0
- package/build/utils/pythonMountPrefixes.js +347 -0
- package/build/utils/pythonMountPrefixes.test.d.ts +1 -0
- package/build/utils/pythonMountPrefixes.test.js +113 -0
- package/build/utils/repoScanner.js +30 -23
- package/build/utils/repoScanner.test.js +98 -0
- package/build/utils/routeParsers.d.ts +7 -1
- package/build/utils/routeParsers.js +32 -18
- package/build/utils/sourceRouteExtractor.js +17 -40
- package/build/utils/trace-parser.js +7 -19
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/utils/walkerCharacterization.test.d.ts +1 -0
- package/build/utils/walkerCharacterization.test.js +233 -0
- package/node_modules/playwright/lib/common/config.js +1 -1
- package/node_modules/playwright/lib/common/configLoader.js +1 -1
- package/node_modules/playwright/lib/common/fixtures.js +1 -1
- package/node_modules/playwright/lib/common/testType.js +1 -1
- package/node_modules/playwright/lib/index.js +2 -2
- package/node_modules/playwright/lib/isomorphic/testTree.js +1 -1
- package/node_modules/playwright/lib/matchers/expect.js +1 -1
- package/node_modules/playwright/lib/mcp/browser/tab.js +1 -1
- package/node_modules/playwright/lib/mcp/browser/tools/navigate.js +1 -1
- package/node_modules/playwright/lib/mcp/browser/tools/tracing.js +1 -1
- package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +7 -0
- package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +2 -1
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +3 -1
- package/node_modules/playwright/lib/reporters/base.js +2 -2
- package/node_modules/playwright/lib/reporters/list.js +1 -1
- package/node_modules/playwright/lib/reporters/teleEmitter.js +1 -1
- package/node_modules/playwright/lib/worker/testInfo.js +2 -2
- package/node_modules/playwright/node_modules/playwright-core/ThirdPartyNotices.txt +23 -126
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +2 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-Cqp3cwEJ.js → codeMirrorModule-aszq5EdG.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-CYf9adZh.js → defaultSettingsView-BxS7Jm4s.js} +94 -94
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.d4gkVSou.js → index.D4JTTy4R.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.u4_8VnCV.js → uiMode.DaRMQKOI.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +3 -3
- package/node_modules/playwright/._ThirdPartyNotices.txt +0 -0
- package/node_modules/playwright/._cli.js +0 -0
- package/node_modules/playwright/._index.d.ts +0 -0
- package/node_modules/playwright/._index.js +0 -0
- package/node_modules/playwright/._index.mjs +0 -0
- package/node_modules/playwright/._jsx-runtime.js +0 -0
- package/node_modules/playwright/._jsx-runtime.mjs +0 -0
- package/node_modules/playwright/._lib +0 -0
- package/node_modules/playwright/._node_modules +0 -0
- package/node_modules/playwright/._package.json +0 -0
- package/node_modules/playwright/._test.d.ts +0 -0
- package/node_modules/playwright/._test.js +0 -0
- package/node_modules/playwright/._test.mjs +0 -0
- package/node_modules/playwright/._types +0 -0
- package/node_modules/playwright/._vitest.config.ts +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._.DS_Store +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._.npmignore +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._README.md +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._ThirdPartyNotices.txt +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._bin +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._browsers.json +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._bundles +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._cli.js +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._index.d.ts +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._index.js +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._index.mjs +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._lib +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._package.json +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._src +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._types +0 -0
|
@@ -23,7 +23,8 @@ import { classifyUnmatchedFilesByPathAffinity } from "../../utils/pathAffinityCl
|
|
|
23
23
|
import { scanAllRepoEndpoints, scanRelatedEndpoints, grepRouterMountingContext, findCandidateRouteFiles, } from "../../utils/repoScanner.js";
|
|
24
24
|
import { detectProjectMetadata } from "../../utils/projectMetadata.js";
|
|
25
25
|
import { draftScenariosFromEndpoints, draftResponseShapeScenarios, capScenarios } from "../../utils/scenarioDrafting.js";
|
|
26
|
-
import { mapUnmatchedFilesToImporterEndpoints, buildHopResponseShapeInputs } from "../../utils/importerHop.js";
|
|
26
|
+
import { mapUnmatchedFilesToImporterEndpoints, buildHopResponseShapeInputs, deriveResourceTokensForFile, deriveResourceToken, extractChangedSymbols, sliceDiffByFile } from "../../utils/importerHop.js";
|
|
27
|
+
import { extractChangedSelectors, MAX_CHANGED_SELECTORS } from "../../utils/frontendSelectors.js";
|
|
27
28
|
import { buildAnalysisOutputText } from "../../prompts/test-recommendation/analysisOutputPrompt.js";
|
|
28
29
|
import { parseTraceFile, discoverTraceFiles, discoverPlaywrightZips, } from "../../utils/trace-parser.js";
|
|
29
30
|
import { TestSource } from "../../types/TestAnalysis.js";
|
|
@@ -613,19 +614,114 @@ export function registerAnalyzeChangesTool(server) {
|
|
|
613
614
|
});
|
|
614
615
|
// PR mode → [] excludes external tests from pre-loading (agent discovers via grep).
|
|
615
616
|
// No diff context → undefined triggers full-repo mode in TestDiscoveryService.
|
|
616
|
-
|
|
617
|
+
// SKYR-3924: a diff that changed NO endpoints but did change schema/model/DTO files
|
|
618
|
+
// yields no endpoint signal, so external tests would be skipped entirely and the
|
|
619
|
+
// breaking existing tests never enter the maintenance scope. Derive resource tokens
|
|
620
|
+
// from the changed files via the shared importer-hop derivation and use them, so
|
|
621
|
+
// discoverTests partitions and PRE-LOADS the relevant existing tests.
|
|
622
|
+
// Per-file diff slices — lazily computed once and shared by the changedSymbols
|
|
623
|
+
// (SKYR-3924) and changedSelectors (UI promotion pass 3) derivations below, so a diff
|
|
624
|
+
// that triggers both isn't sliced twice.
|
|
625
|
+
let _diffByFile = null;
|
|
626
|
+
const getDiffByFile = () => (_diffByFile ??= diffData?.diffContent ? sliceDiffByFile(diffData.diffContent) : new Map());
|
|
627
|
+
let changedResources = classifiedEndpoints ? [] : undefined;
|
|
628
|
+
let changedSymbols;
|
|
629
|
+
if (classifiedEndpoints &&
|
|
630
|
+
classifiedEndpoints.changedEndpoints.length === 0 &&
|
|
631
|
+
classifiedEndpoints.newEndpoints.length === 0 &&
|
|
632
|
+
classifiedEndpoints.removedEndpoints.length === 0 &&
|
|
633
|
+
diffData?.diffContent) {
|
|
634
|
+
// Only changed application-source files carry a resource signal — skip test
|
|
635
|
+
// files, configs, workflows, and docs (they'd contribute filename noise like
|
|
636
|
+
// "workspace" or "skyramp-testbot").
|
|
637
|
+
const SOURCE_EXT = /\.(ts|tsx|js|jsx|py|java|kt|go|rb|php|rs|cs|ex|exs)$/;
|
|
638
|
+
const files = (classifiedEndpoints.changedFiles ?? diffData.changedFiles ?? [])
|
|
639
|
+
.filter((f) => SOURCE_EXT.test(f) && !isTestFile(f));
|
|
640
|
+
// A schema/DTO/model file is named for its module, not the REST resource — its
|
|
641
|
+
// class names are the real signal (DeploymentCreate → "deployment", not the
|
|
642
|
+
// "actions" filename). For those files prefer the symbol-derived tokens and drop
|
|
643
|
+
// the generic filename token; otherwise use all tokens.
|
|
644
|
+
const SCHEMA_FILE = /\/(schemas?|models?|dtos?|entities|serializers?|types)\/|\.(dto|schema|model|entity|serializer)\.[^/]+$/i;
|
|
645
|
+
// Per-file diff sections (with @@ headers) so each file's tokens derive
|
|
646
|
+
// only from its own hunk — passing the full multi-file diff would mix
|
|
647
|
+
// symbols across files. Falls back to the full diff if a path doesn't
|
|
648
|
+
// match (no worse than the prior behavior). SKYR-3924.
|
|
649
|
+
const diffByFile = getDiffByFile();
|
|
650
|
+
const derived = new Set();
|
|
651
|
+
const symbols = new Set();
|
|
652
|
+
for (const f of files) {
|
|
653
|
+
const fileDiff = diffByFile.get(f) ?? diffData.diffContent;
|
|
654
|
+
const all = deriveResourceTokensForFile(f, fileDiff);
|
|
655
|
+
const filenameToken = deriveResourceToken(f);
|
|
656
|
+
const symbolTokens = all.filter((t) => t !== filenameToken);
|
|
657
|
+
// deriveResourceTokensForFile already drops generic tokens (via importerHop's
|
|
658
|
+
// GENERIC_RESOURCE_TOKENS / GENERIC_SYMBOL_TOKENS), so no extra filter is needed here.
|
|
659
|
+
const chosen = SCHEMA_FILE.test(f) && symbolTokens.length > 0 ? symbolTokens : all;
|
|
660
|
+
for (const t of chosen)
|
|
661
|
+
derived.add(t);
|
|
662
|
+
// Raw changed symbol names, derived from THIS file's diff slice only (not the full
|
|
663
|
+
// multi-file diff) so an unrelated changed file — a test or doc — can't inject its
|
|
664
|
+
// symbols. Uses the per-file slice from diffByFile.get(f).
|
|
665
|
+
for (const s of extractChangedSymbols(diffByFile.get(f) ?? ""))
|
|
666
|
+
symbols.add(s);
|
|
667
|
+
}
|
|
668
|
+
if (derived.size > 0) {
|
|
669
|
+
changedResources = [...derived];
|
|
670
|
+
// Raw changed symbol names (e.g. "DeploymentCreate") for a content grep against
|
|
671
|
+
// test bodies — catches tests that reference the changed type by name but whose
|
|
672
|
+
// path/URL doesn't token-match the resource (e.g. a schema unit test).
|
|
673
|
+
changedSymbols = [...symbols];
|
|
674
|
+
logger.info("Schema-only diff: derived changedResources from changed files (SKYR-3924)", {
|
|
675
|
+
changedResources,
|
|
676
|
+
changedSymbols,
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
}
|
|
617
680
|
// Compute changed frontend files once — reused for both discoverTests (component
|
|
618
681
|
// test promotion) and uiContext below (page enumeration). Uses classifiedEndpoints
|
|
619
682
|
// changedFiles so it reflects the same diff scope as the rest of the analysis.
|
|
620
683
|
const flutterSdkDep = hasFlutterSdkDep(params.repositoryPath);
|
|
621
684
|
const changedFrontendFiles = (classifiedEndpoints?.changedFiles ?? diffData?.changedFiles ?? [])
|
|
622
685
|
.filter(f => isFrontendFile(f, { hasFlutterSdkDep: flutterSdkDep }) && !isTestFile(f));
|
|
686
|
+
// Selector literals (data-testid values, CSS class tokens) added/removed in the
|
|
687
|
+
// frontend diff — drives discovery pass 3, which promotes page objects/specs that
|
|
688
|
+
// couple to a changed component by selector rather than by filename or import
|
|
689
|
+
// (see TestDiscoveryService Step 2). Derived from each changed frontend file's own
|
|
690
|
+
// diff slice so an unrelated file can't inject selectors.
|
|
691
|
+
let changedSelectors;
|
|
692
|
+
if (changedFrontendFiles.length > 0 && diffData?.diffContent) {
|
|
693
|
+
const diffByFile = getDiffByFile();
|
|
694
|
+
// Total cap across all changed files (each file is separately capped inside
|
|
695
|
+
// extractChangedSelectors). Bounds pass 3's O(externalFiles × selectors) scan so a
|
|
696
|
+
// large frontend PR can't produce a pathologically long selector set.
|
|
697
|
+
const MAX_TOTAL_SELECTORS = 200;
|
|
698
|
+
const selectors = new Set();
|
|
699
|
+
outer: for (const f of changedFrontendFiles) {
|
|
700
|
+
const fileSelectors = extractChangedSelectors(diffByFile.get(f) ?? "");
|
|
701
|
+
if (fileSelectors.length >= MAX_CHANGED_SELECTORS) {
|
|
702
|
+
logger.warning(`changedSelectors: file '${f}' hit the per-file cap (${MAX_CHANGED_SELECTORS}); its selector extraction may be truncated`);
|
|
703
|
+
}
|
|
704
|
+
for (const s of fileSelectors) {
|
|
705
|
+
selectors.add(s);
|
|
706
|
+
if (selectors.size >= MAX_TOTAL_SELECTORS) {
|
|
707
|
+
logger.warning(`changedSelectors: hit the total cap (${MAX_TOTAL_SELECTORS}) across ${changedFrontendFiles.length} changed frontend file(s); remaining selectors ignored for UI promotion pass 3`);
|
|
708
|
+
break outer;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
if (selectors.size > 0) {
|
|
713
|
+
changedSelectors = [...selectors];
|
|
714
|
+
logger.info("Derived changedSelectors from frontend diff (UI promotion pass 3)", {
|
|
715
|
+
count: changedSelectors.length,
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
}
|
|
623
719
|
let existingTests = [];
|
|
624
720
|
let discoveredRelevantExternalPaths = [];
|
|
625
721
|
try {
|
|
626
722
|
const testDiscoveryService = new TestDiscoveryService();
|
|
627
723
|
setTestsRepoDir(params.testsRepoDir);
|
|
628
|
-
const discoveryResult = await testDiscoveryService.discoverTests(testDir ?? params.repositoryPath, { changedResources,
|
|
724
|
+
const discoveryResult = await testDiscoveryService.discoverTests(testDir ?? params.repositoryPath, { changedResources, changedSymbols, preciseResources: (changedSymbols?.length ?? 0) > 0, changedFrontendFiles, changedSelectors });
|
|
629
725
|
existingTests = discoveryResult.tests.map((test) => ({
|
|
630
726
|
testFile: test.testFile,
|
|
631
727
|
testType: test.testType,
|
|
@@ -1374,6 +1470,8 @@ export function registerAnalyzeChangesTool(server) {
|
|
|
1374
1470
|
primaryFramework: projectMeta.primaryFramework,
|
|
1375
1471
|
existingTestCount: existingTests.length,
|
|
1376
1472
|
newEndpointCount: classifiedEndpoints?.newEndpoints.length ?? 0,
|
|
1473
|
+
modifiedEndpointCount: classifiedEndpoints?.changedEndpoints.length ?? 0,
|
|
1474
|
+
removedEndpointCount: classifiedEndpoints?.removedEndpoints.length ?? 0,
|
|
1377
1475
|
endpointCount: skeletonEndpoints.reduce((acc, ep) => acc + ep.methods.length, 0),
|
|
1378
1476
|
},
|
|
1379
1477
|
// Surface uiContext inline so the testbot prompt can iterate
|
|
@@ -1454,7 +1552,7 @@ export function registerAnalyzeChangesTool(server) {
|
|
|
1454
1552
|
finally {
|
|
1455
1553
|
AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, {
|
|
1456
1554
|
repositoryPath: params.repositoryPath,
|
|
1457
|
-
});
|
|
1555
|
+
}).catch((err) => { logger.warning("Analytics event failed", { error: String(err) }); });
|
|
1458
1556
|
}
|
|
1459
1557
|
});
|
|
1460
1558
|
}
|
|
@@ -5,7 +5,7 @@ import { jest } from "@jest/globals";
|
|
|
5
5
|
// graph touches must use unstable_mockModule before the dynamic import below.
|
|
6
6
|
jest.unstable_mockModule("simple-git", () => ({ simpleGit: jest.fn() }));
|
|
7
7
|
jest.unstable_mockModule("../../services/AnalyticsService.js", () => ({
|
|
8
|
-
AnalyticsService: { pushMCPToolEvent: jest.fn() },
|
|
8
|
+
AnalyticsService: { pushMCPToolEvent: jest.fn().mockResolvedValue(undefined) },
|
|
9
9
|
}));
|
|
10
10
|
jest.unstable_mockModule("../../prompts/test-recommendation/test-recommendation-prompt.js", () => ({
|
|
11
11
|
buildRecommendationPrompt: jest.fn(),
|
|
@@ -49,6 +49,8 @@ jest.unstable_mockModule("../../utils/importerHop.js", () => ({
|
|
|
49
49
|
deriveResourceTokensForFile: jest.fn(() => []),
|
|
50
50
|
endpointResourceTokens: jest.fn(() => new Set()),
|
|
51
51
|
sliceAddedLinesByFile: jest.fn(() => new Map()),
|
|
52
|
+
sliceDiffByFile: jest.fn(() => new Map()),
|
|
53
|
+
extractChangedSymbols: jest.fn(() => []),
|
|
52
54
|
}));
|
|
53
55
|
jest.unstable_mockModule("../../utils/repoScanner.js", () => ({
|
|
54
56
|
scanAllRepoEndpoints: jest.fn(),
|
|
@@ -95,10 +95,18 @@ export function registerAnalyzeTestHealthTool(server) {
|
|
|
95
95
|
let uiDriftParams;
|
|
96
96
|
if (hasUiFrontend) {
|
|
97
97
|
logger.info(`Including UI drift analysis for ${uiTests.length} UI test(s), ${apiTests.length} API test(s)`);
|
|
98
|
+
// Shared components = changed frontend files imported by 2+ production files
|
|
99
|
+
// (server-computed importer graph, SKYR-3855). Surfacing the concrete importer
|
|
100
|
+
// list makes the "shared component → cannot IGNORE unread" drift rule enforceable
|
|
101
|
+
// against real data instead of the agent's qualitative guess.
|
|
102
|
+
const sharedComponents = (uiContext.frontendFileIntegration ?? [])
|
|
103
|
+
.filter((f) => f.importers.length >= 2)
|
|
104
|
+
.map((f) => ({ file: f.file, importers: f.importers }));
|
|
98
105
|
uiDriftParams = {
|
|
99
106
|
tests: uiTests.map(t => ({ testFile: t.testFile, source: t.source })),
|
|
100
107
|
changedFrontendFiles: uiContext.changedFrontendFiles,
|
|
101
108
|
blueprintCaptured: args.blueprintCaptured,
|
|
109
|
+
sharedComponents,
|
|
102
110
|
};
|
|
103
111
|
}
|
|
104
112
|
// Collect all checked-out repo paths so the grep instruction covers
|
|
@@ -124,7 +132,7 @@ export function registerAnalyzeTestHealthTool(server) {
|
|
|
124
132
|
return errorResult;
|
|
125
133
|
}
|
|
126
134
|
finally {
|
|
127
|
-
AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, {});
|
|
135
|
+
AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, {}).catch((err) => { logger.warning("Analytics event failed", { error: String(err) }); });
|
|
128
136
|
}
|
|
129
137
|
});
|
|
130
138
|
}
|
|
@@ -15,7 +15,7 @@ jest.unstable_mockModule("../../utils/logger.js", () => ({
|
|
|
15
15
|
},
|
|
16
16
|
}));
|
|
17
17
|
jest.unstable_mockModule("../../services/AnalyticsService.js", () => ({
|
|
18
|
-
AnalyticsService: { pushMCPToolEvent: jest.fn() },
|
|
18
|
+
AnalyticsService: { pushMCPToolEvent: jest.fn().mockResolvedValue(undefined) },
|
|
19
19
|
}));
|
|
20
20
|
const mockBuildDriftAnalysisPrompt = jest.fn().mockImplementation((_stateFile, apiTests, ui) => {
|
|
21
21
|
const parts = [];
|
|
@@ -251,7 +251,7 @@ For detailed documentation visit: https://www.skyramp.dev/docs/load-test/advance
|
|
|
251
251
|
prompt: params.prompt,
|
|
252
252
|
playwright: params.playwright.toString(),
|
|
253
253
|
};
|
|
254
|
-
AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, recordParams);
|
|
254
|
+
AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, recordParams).catch((err) => { logger.warning("Analytics event failed", { error: String(err) }); });
|
|
255
255
|
}
|
|
256
256
|
});
|
|
257
257
|
}
|
|
@@ -140,7 +140,7 @@ For detailed documentation visit: https://www.skyramp.dev/docs/load-test/advance
|
|
|
140
140
|
prompt: params.prompt,
|
|
141
141
|
playwright: params.playwrightEnabled.toString(),
|
|
142
142
|
};
|
|
143
|
-
AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, recordParams);
|
|
143
|
+
AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, recordParams).catch((err) => { logger.warning("Analytics event failed", { error: String(err) }); });
|
|
144
144
|
}
|
|
145
145
|
});
|
|
146
146
|
}
|
|
@@ -70,7 +70,7 @@ export function registerInitScanWorkspaceTool(server) {
|
|
|
70
70
|
return errorResult;
|
|
71
71
|
}
|
|
72
72
|
finally {
|
|
73
|
-
AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, recordParams);
|
|
73
|
+
AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, recordParams).catch((err) => { logger.warning("Analytics event failed", { error: String(err) }); });
|
|
74
74
|
}
|
|
75
75
|
});
|
|
76
76
|
}
|
|
@@ -201,7 +201,7 @@ export function registerInitializeWorkspaceTool(server) {
|
|
|
201
201
|
return errorResult;
|
|
202
202
|
}
|
|
203
203
|
finally {
|
|
204
|
-
AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, recordParams);
|
|
204
|
+
AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, recordParams).catch((err) => { logger.warning("Analytics event failed", { error: String(err) }); });
|
|
205
205
|
}
|
|
206
206
|
});
|
|
207
207
|
}
|
|
@@ -18,7 +18,7 @@ jest.unstable_mockModule("../../utils/logger.js", () => ({
|
|
|
18
18
|
logger: { info: jest.fn(), error: jest.fn() },
|
|
19
19
|
}));
|
|
20
20
|
jest.unstable_mockModule("../../services/AnalyticsService.js", () => ({
|
|
21
|
-
AnalyticsService: { pushMCPToolEvent: jest.fn() },
|
|
21
|
+
AnalyticsService: { pushMCPToolEvent: jest.fn().mockResolvedValue(undefined) },
|
|
22
22
|
}));
|
|
23
23
|
jest.unstable_mockModule("./initScanWorkspaceTool.js", () => ({
|
|
24
24
|
validateAndConsumeScanToken: jest.fn(),
|
|
@@ -1431,12 +1431,12 @@ export declare const analysisMetadataSchema: z.ZodObject<{
|
|
|
1431
1431
|
}, "strip", z.ZodTypeAny, {
|
|
1432
1432
|
repositoryName: string;
|
|
1433
1433
|
analysisDate: string;
|
|
1434
|
-
scanDepth: "
|
|
1434
|
+
scanDepth: "full" | "quick";
|
|
1435
1435
|
analysisScope?: AnalysisScope | undefined;
|
|
1436
1436
|
}, {
|
|
1437
1437
|
repositoryName: string;
|
|
1438
1438
|
analysisDate: string;
|
|
1439
|
-
scanDepth: "
|
|
1439
|
+
scanDepth: "full" | "quick";
|
|
1440
1440
|
analysisScope?: AnalysisScope | undefined;
|
|
1441
1441
|
}>;
|
|
1442
1442
|
export declare const projectClassificationSchema: z.ZodObject<{
|
|
@@ -1464,12 +1464,12 @@ export declare const repositoryAnalysisSchema: z.ZodObject<{
|
|
|
1464
1464
|
}, "strip", z.ZodTypeAny, {
|
|
1465
1465
|
repositoryName: string;
|
|
1466
1466
|
analysisDate: string;
|
|
1467
|
-
scanDepth: "
|
|
1467
|
+
scanDepth: "full" | "quick";
|
|
1468
1468
|
analysisScope?: AnalysisScope | undefined;
|
|
1469
1469
|
}, {
|
|
1470
1470
|
repositoryName: string;
|
|
1471
1471
|
analysisDate: string;
|
|
1472
|
-
scanDepth: "
|
|
1472
|
+
scanDepth: "full" | "quick";
|
|
1473
1473
|
analysisScope?: AnalysisScope | undefined;
|
|
1474
1474
|
}>;
|
|
1475
1475
|
projectClassification: z.ZodObject<{
|
|
@@ -2601,7 +2601,7 @@ export declare const repositoryAnalysisSchema: z.ZodObject<{
|
|
|
2601
2601
|
metadata: {
|
|
2602
2602
|
repositoryName: string;
|
|
2603
2603
|
analysisDate: string;
|
|
2604
|
-
scanDepth: "
|
|
2604
|
+
scanDepth: "full" | "quick";
|
|
2605
2605
|
analysisScope?: AnalysisScope | undefined;
|
|
2606
2606
|
};
|
|
2607
2607
|
projectClassification: {
|
|
@@ -2803,7 +2803,7 @@ export declare const repositoryAnalysisSchema: z.ZodObject<{
|
|
|
2803
2803
|
metadata: {
|
|
2804
2804
|
repositoryName: string;
|
|
2805
2805
|
analysisDate: string;
|
|
2806
|
-
scanDepth: "
|
|
2806
|
+
scanDepth: "full" | "quick";
|
|
2807
2807
|
analysisScope?: AnalysisScope | undefined;
|
|
2808
2808
|
};
|
|
2809
2809
|
projectClassification: {
|
|
@@ -48,8 +48,21 @@ export interface DriftRecommendation extends MaintenanceActionCore {
|
|
|
48
48
|
newPath: string;
|
|
49
49
|
method: string;
|
|
50
50
|
}>;
|
|
51
|
+
/** Line-anchored edit sites the assessment located while reading a large UPDATE file —
|
|
52
|
+
* surfaced in update_context as a checklist so the apply step edits each site directly
|
|
53
|
+
* instead of re-scanning the file. */
|
|
54
|
+
editSites?: Array<{
|
|
55
|
+
line: number;
|
|
56
|
+
anchor: string;
|
|
57
|
+
change?: string;
|
|
58
|
+
}>;
|
|
51
59
|
/** Computed during instruction building — suggested renamed file path if applicable. */
|
|
52
60
|
_suggestedNewFile?: string;
|
|
61
|
+
/** REGENERATE/DELETE on an external (user-authored) test is report-only: the verdict is
|
|
62
|
+
* recorded and rendered in the report so the developer sees the recommendation, but the
|
|
63
|
+
* file is never touched (the apply loops skip reportOnly entries). Lets the report keep
|
|
64
|
+
* the real DELETE/REGENERATE action instead of flattening it to VERIFY. */
|
|
65
|
+
reportOnly?: boolean;
|
|
53
66
|
}
|
|
54
67
|
/**
|
|
55
68
|
* Complete test analysis result that combines discovery, drift, execution, and health data
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
*/
|
|
32
32
|
import * as fs from "fs";
|
|
33
33
|
import * as path from "path";
|
|
34
|
+
import { collectFiles } from "./fileWalk.js";
|
|
34
35
|
const MAX_CANDIDATE_FILES = 200;
|
|
35
36
|
const MAX_WALK_DEPTH = 10;
|
|
36
37
|
const SKIP_DIRS = new Set([
|
|
@@ -283,37 +284,14 @@ function collectImports(source, filePath, repoPath) {
|
|
|
283
284
|
function snakeToCamel(s) {
|
|
284
285
|
return s
|
|
285
286
|
.split("_")
|
|
286
|
-
.map((p
|
|
287
|
+
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
|
|
287
288
|
.join("");
|
|
288
289
|
}
|
|
289
290
|
function findDartFiles(repoPath) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
let entries;
|
|
297
|
-
try {
|
|
298
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
299
|
-
}
|
|
300
|
-
catch {
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
for (const entry of entries) {
|
|
304
|
-
if (matches.length >= MAX_CANDIDATE_FILES)
|
|
305
|
-
return;
|
|
306
|
-
const full = path.join(dir, entry.name);
|
|
307
|
-
if (entry.isDirectory()) {
|
|
308
|
-
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith("."))
|
|
309
|
-
continue;
|
|
310
|
-
walk(full, depth + 1);
|
|
311
|
-
}
|
|
312
|
-
else if (entry.isFile() && entry.name.endsWith(".dart")) {
|
|
313
|
-
matches.push(full);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
walk(repoPath, 0);
|
|
318
|
-
return matches;
|
|
291
|
+
return collectFiles(repoPath, {
|
|
292
|
+
match: (entry) => entry.name.endsWith(".dart"),
|
|
293
|
+
limit: MAX_CANDIDATE_FILES,
|
|
294
|
+
maxDepth: MAX_WALK_DEPTH,
|
|
295
|
+
shouldSkipDir: (entry) => SKIP_DIRS.has(entry.name) || entry.name.startsWith("."),
|
|
296
|
+
});
|
|
319
297
|
}
|
|
@@ -55,7 +55,7 @@ describe("dockerImageExistsLocally", () => {
|
|
|
55
55
|
});
|
|
56
56
|
});
|
|
57
57
|
describe("pullDockerImage", () => {
|
|
58
|
-
const IMAGE = "skyramp/executor:v1.3.
|
|
58
|
+
const IMAGE = "skyramp/executor:v1.3.34";
|
|
59
59
|
beforeEach(() => jest.clearAllMocks());
|
|
60
60
|
describe("on amd64 host", () => {
|
|
61
61
|
const originalArch = process.arch;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
export interface WalkDirOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Return `true` to skip descending into a directory entirely. Receives the
|
|
5
|
+
* directory's `Dirent` and its absolute path. Omit to descend into every
|
|
6
|
+
* directory.
|
|
7
|
+
*/
|
|
8
|
+
shouldSkipDir?: (entry: fs.Dirent, fullPath: string) => boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Maximum directory depth to descend, with the walk root at depth 0. A file
|
|
11
|
+
* is visited only if its parent directory's depth is `<= maxDepth`. Omit for
|
|
12
|
+
* unlimited depth. (`maxDepth: 0` visits only files directly under the root.)
|
|
13
|
+
*/
|
|
14
|
+
maxDepth?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Core depth-first directory traversal shared by every repo walker.
|
|
18
|
+
*
|
|
19
|
+
* Yields `[entry, fullPath]` for each regular file encountered (directories are
|
|
20
|
+
* never yielded). Callers early-exit simply by `break`ing out of the `for…of` —
|
|
21
|
+
* abandoning the generator stops the walk immediately, before any further
|
|
22
|
+
* `readdirSync`.
|
|
23
|
+
*
|
|
24
|
+
* Best-effort: an unreadable directory is skipped rather than throwing, matching
|
|
25
|
+
* the scanners' "one bad directory shouldn't abort discovery" contract.
|
|
26
|
+
*/
|
|
27
|
+
export declare function walkDir(rootDir: string, opts?: WalkDirOptions): Generator<[fs.Dirent, string]>;
|
|
28
|
+
export interface CollectFilesOptions {
|
|
29
|
+
/** Return `true` for files to include. Receives the file's `Dirent` and its absolute path. */
|
|
30
|
+
match: (entry: fs.Dirent, fullPath: string) => boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Stop once this many files have been collected — a backstop against
|
|
33
|
+
* pathological repos. Omit for no cap. When the cap is hit exactly `limit`
|
|
34
|
+
* files are returned, so callers can detect truncation via
|
|
35
|
+
* `result.length >= limit`.
|
|
36
|
+
*/
|
|
37
|
+
limit?: number;
|
|
38
|
+
/** Skip descending into a directory (see {@link WalkDirOptions.shouldSkipDir}). */
|
|
39
|
+
shouldSkipDir?: (entry: fs.Dirent, fullPath: string) => boolean;
|
|
40
|
+
/** Maximum descent depth, root = 0 (see {@link WalkDirOptions.maxDepth}). */
|
|
41
|
+
maxDepth?: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Collect the absolute paths of files matching `match`, up to `limit`.
|
|
45
|
+
*
|
|
46
|
+
* The cap-aware layer over {@link walkDir}: it owns the "accumulate matches and
|
|
47
|
+
* abort at the count cap" idiom once, so the repo's file collectors don't each
|
|
48
|
+
* re-derive it. Best-effort, matching `walkDir`.
|
|
49
|
+
*/
|
|
50
|
+
export declare function collectFiles(rootDir: string, opts: CollectFilesOptions): string[];
|
|
51
|
+
export interface WalkOptions {
|
|
52
|
+
/** Only files whose name ends with one of these extensions are returned (e.g. [".py"], [".ts", ".tsx"]). */
|
|
53
|
+
extensions: string[];
|
|
54
|
+
/** Directory names to skip entirely (matched on basename, not full path). */
|
|
55
|
+
skipDirs: Set<string>;
|
|
56
|
+
/**
|
|
57
|
+
* Stop once this many files have been collected — a backstop against
|
|
58
|
+
* pathological repos. Omit for no cap. When the cap is hit the walk returns
|
|
59
|
+
* exactly `limit` files, so callers can detect truncation via
|
|
60
|
+
* `result.length >= limit`.
|
|
61
|
+
*/
|
|
62
|
+
limit?: number;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Recursively collect source files under `rootDir`, returning absolute paths.
|
|
66
|
+
*
|
|
67
|
+
* Shared walker used by the repo scanners (endpoint catalog, Python mount-prefix
|
|
68
|
+
* resolver) so they don't each carry a near-identical `readdirSync` recursion.
|
|
69
|
+
* Thin wrapper over {@link collectFiles} that matches by extension suffix.
|
|
70
|
+
*/
|
|
71
|
+
export declare function walkSourceFiles(rootDir: string, opts: WalkOptions): string[];
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
/**
|
|
4
|
+
* Core depth-first directory traversal shared by every repo walker.
|
|
5
|
+
*
|
|
6
|
+
* Yields `[entry, fullPath]` for each regular file encountered (directories are
|
|
7
|
+
* never yielded). Callers early-exit simply by `break`ing out of the `for…of` —
|
|
8
|
+
* abandoning the generator stops the walk immediately, before any further
|
|
9
|
+
* `readdirSync`.
|
|
10
|
+
*
|
|
11
|
+
* Best-effort: an unreadable directory is skipped rather than throwing, matching
|
|
12
|
+
* the scanners' "one bad directory shouldn't abort discovery" contract.
|
|
13
|
+
*/
|
|
14
|
+
export function* walkDir(rootDir, opts = {}) {
|
|
15
|
+
const { shouldSkipDir, maxDepth } = opts;
|
|
16
|
+
function* walk(dir, depth) {
|
|
17
|
+
let entries;
|
|
18
|
+
try {
|
|
19
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return; // best-effort: skip unreadable directory, keep going
|
|
23
|
+
}
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
const full = path.join(dir, entry.name);
|
|
26
|
+
if (entry.isDirectory()) {
|
|
27
|
+
// Cheap integer bound before the (possibly non-trivial) skip callback.
|
|
28
|
+
if (maxDepth !== undefined && depth + 1 > maxDepth)
|
|
29
|
+
continue;
|
|
30
|
+
if (shouldSkipDir?.(entry, full))
|
|
31
|
+
continue;
|
|
32
|
+
yield* walk(full, depth + 1);
|
|
33
|
+
}
|
|
34
|
+
else if (entry.isFile()) {
|
|
35
|
+
yield [entry, full];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
yield* walk(rootDir, 0);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Collect the absolute paths of files matching `match`, up to `limit`.
|
|
43
|
+
*
|
|
44
|
+
* The cap-aware layer over {@link walkDir}: it owns the "accumulate matches and
|
|
45
|
+
* abort at the count cap" idiom once, so the repo's file collectors don't each
|
|
46
|
+
* re-derive it. Best-effort, matching `walkDir`.
|
|
47
|
+
*/
|
|
48
|
+
export function collectFiles(rootDir, opts) {
|
|
49
|
+
const { match, limit, shouldSkipDir, maxDepth } = opts;
|
|
50
|
+
if (limit !== undefined && limit <= 0)
|
|
51
|
+
return [];
|
|
52
|
+
const results = [];
|
|
53
|
+
for (const [entry, full] of walkDir(rootDir, { shouldSkipDir, maxDepth })) {
|
|
54
|
+
if (match(entry, full)) {
|
|
55
|
+
results.push(full);
|
|
56
|
+
// Stop the instant the cap is reached — breaking abandons the generator
|
|
57
|
+
// before it descends into (and readdirs) any further directories, so we
|
|
58
|
+
// return exactly `limit` files.
|
|
59
|
+
if (limit !== undefined && results.length >= limit)
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return results;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Recursively collect source files under `rootDir`, returning absolute paths.
|
|
67
|
+
*
|
|
68
|
+
* Shared walker used by the repo scanners (endpoint catalog, Python mount-prefix
|
|
69
|
+
* resolver) so they don't each carry a near-identical `readdirSync` recursion.
|
|
70
|
+
* Thin wrapper over {@link collectFiles} that matches by extension suffix.
|
|
71
|
+
*/
|
|
72
|
+
export function walkSourceFiles(rootDir, opts) {
|
|
73
|
+
const { extensions, skipDirs, limit } = opts;
|
|
74
|
+
return collectFiles(rootDir, {
|
|
75
|
+
match: (entry) => extensions.some((ext) => entry.name.endsWith(ext)),
|
|
76
|
+
limit,
|
|
77
|
+
shouldSkipDir: (entry) => skipDirs.has(entry.name),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|