@silasfmartins/testhub 1.0.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/.github/copilot-instructions.md +520 -0
- package/biome.json +37 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +169 -0
- package/dist/scripts/consumer-postinstall.d.ts +15 -0
- package/dist/scripts/consumer-postinstall.js +785 -0
- package/dist/scripts/generate-docs.d.ts +16 -0
- package/dist/scripts/generate-docs.js +1363 -0
- package/dist/scripts/generate-index.d.ts +2 -0
- package/dist/scripts/generate-index.js +314 -0
- package/dist/scripts/init-api.d.ts +2 -0
- package/dist/scripts/init-api.js +525 -0
- package/dist/scripts/init-banco.d.ts +2 -0
- package/dist/scripts/init-banco.js +347 -0
- package/dist/scripts/init-frontend.d.ts +2 -0
- package/dist/scripts/init-frontend.js +627 -0
- package/dist/scripts/init-mobile.d.ts +2 -0
- package/dist/scripts/init-mobile.js +481 -0
- package/dist/scripts/init-scenarios.d.ts +2 -0
- package/dist/scripts/init-scenarios.js +846 -0
- package/dist/scripts/init-ssh.d.ts +2 -0
- package/dist/scripts/init-ssh.js +639 -0
- package/dist/scripts/package-versions.d.ts +57 -0
- package/dist/scripts/package-versions.js +768 -0
- package/dist/scripts/postinstall.d.ts +1 -0
- package/dist/scripts/postinstall.js +527 -0
- package/dist/scripts/robust-build.d.ts +7 -0
- package/dist/scripts/robust-build.js +88 -0
- package/dist/scripts/setup-local-packages.d.ts +31 -0
- package/dist/scripts/setup-local-packages.js +237 -0
- package/dist/scripts/smart-override.d.ts +2 -0
- package/dist/scripts/smart-override.js +1360 -0
- package/dist/scripts/sync-configs.d.ts +27 -0
- package/dist/scripts/sync-configs.js +248 -0
- package/dist/scripts/test-biome-parse.d.ts +5 -0
- package/dist/scripts/test-biome-parse.js +84 -0
- package/dist/scripts/ultracite-setup.d.ts +4 -0
- package/dist/scripts/ultracite-setup.js +310 -0
- package/dist/scripts/update-all-init-scripts.d.ts +2 -0
- package/dist/scripts/update-all-init-scripts.js +52 -0
- package/dist/scripts/update-biome-schema.d.ts +15 -0
- package/dist/scripts/update-biome-schema.js +124 -0
- package/dist/src/AutoCoreFacade.d.ts +145 -0
- package/dist/src/AutoCoreFacade.js +217 -0
- package/dist/src/api/ApiActions.d.ts +297 -0
- package/dist/src/api/ApiActions.js +1905 -0
- package/dist/src/api/Certificate.d.ts +60 -0
- package/dist/src/api/Certificate.js +79 -0
- package/dist/src/api/JsonResponse.d.ts +116 -0
- package/dist/src/api/JsonResponse.js +206 -0
- package/dist/src/appium/DeviceFarmViewer.d.ts +79 -0
- package/dist/src/appium/DeviceFarmViewer.js +1083 -0
- package/dist/src/appium/MobileActions.d.ts +347 -0
- package/dist/src/appium/MobileActions.js +1632 -0
- package/dist/src/appium/MobileConnection.d.ts +160 -0
- package/dist/src/appium/MobileConnection.js +772 -0
- package/dist/src/config/envLoader.d.ts +123 -0
- package/dist/src/config/envLoader.js +361 -0
- package/dist/src/config/jest-safe-setup.d.ts +19 -0
- package/dist/src/config/jest-safe-setup.js +369 -0
- package/dist/src/config/timeouts.d.ts +32 -0
- package/dist/src/config/timeouts.js +38 -0
- package/dist/src/desktop/DesktopActions.d.ts +46 -0
- package/dist/src/desktop/DesktopActions.js +398 -0
- package/dist/src/desktop/DesktopConnection.d.ts +32 -0
- package/dist/src/desktop/DesktopConnection.js +84 -0
- package/dist/src/domain/entities/TestExecution.d.ts +117 -0
- package/dist/src/domain/entities/TestExecution.js +150 -0
- package/dist/src/domain/entities/TestReport.d.ts +114 -0
- package/dist/src/domain/entities/TestReport.js +179 -0
- package/dist/src/domain/repositories/ITestRepository.d.ts +196 -0
- package/dist/src/domain/repositories/ITestRepository.js +14 -0
- package/dist/src/domain/schemas/ValidationSchemas.d.ts +159 -0
- package/dist/src/domain/schemas/ValidationSchemas.js +181 -0
- package/dist/src/functions/errors/BaseError.d.ts +78 -0
- package/dist/src/functions/errors/BaseError.js +245 -0
- package/dist/src/functions/errors/ConfigurationError.d.ts +16 -0
- package/dist/src/functions/errors/ConfigurationError.js +48 -0
- package/dist/src/functions/errors/ErrorCatalog.d.ts +148 -0
- package/dist/src/functions/errors/ErrorCatalog.js +157 -0
- package/dist/src/functions/errors/GlobalErrorHandler.d.ts +101 -0
- package/dist/src/functions/errors/GlobalErrorHandler.js +281 -0
- package/dist/src/functions/errors/IntegrationError.d.ts +17 -0
- package/dist/src/functions/errors/IntegrationError.js +51 -0
- package/dist/src/functions/errors/SecurityError.d.ts +14 -0
- package/dist/src/functions/errors/SecurityError.js +42 -0
- package/dist/src/functions/errors/SystemError.d.ts +12 -0
- package/dist/src/functions/errors/SystemError.js +36 -0
- package/dist/src/functions/errors/ValidationError.d.ts +14 -0
- package/dist/src/functions/errors/ValidationError.js +61 -0
- package/dist/src/functions/errors/index.d.ts +12 -0
- package/dist/src/functions/errors/index.js +13 -0
- package/dist/src/global-setup.d.ts +1 -0
- package/dist/src/global-setup.js +1037 -0
- package/dist/src/helpers/BancoActions.d.ts +188 -0
- package/dist/src/helpers/BancoActions.js +581 -0
- package/dist/src/helpers/EnviromentHelper.d.ts +17 -0
- package/dist/src/helpers/EnviromentHelper.js +66 -0
- package/dist/src/helpers/ParallelExecutionHelper.d.ts +183 -0
- package/dist/src/helpers/ParallelExecutionHelper.js +375 -0
- package/dist/src/helpers/SyncSignal.d.ts +15 -0
- package/dist/src/helpers/SyncSignal.js +44 -0
- package/dist/src/hubdocs/CategoryDetector.d.ts +83 -0
- package/dist/src/hubdocs/CategoryDetector.js +401 -0
- package/dist/src/hubdocs/DirectStatementInterceptor.d.ts +54 -0
- package/dist/src/hubdocs/DirectStatementInterceptor.js +243 -0
- package/dist/src/hubdocs/ExecutionTracker.d.ts +107 -0
- package/dist/src/hubdocs/ExecutionTracker.js +702 -0
- package/dist/src/hubdocs/HubDocs.d.ts +395 -0
- package/dist/src/hubdocs/HubDocs.js +3586 -0
- package/dist/src/hubdocs/StatementMethodFilter.d.ts +71 -0
- package/dist/src/hubdocs/StatementMethodFilter.js +618 -0
- package/dist/src/hubdocs/StatementTracker.d.ts +417 -0
- package/dist/src/hubdocs/StatementTracker.js +2419 -0
- package/dist/src/hubdocs/SwaggerGenerator.d.ts +59 -0
- package/dist/src/hubdocs/SwaggerGenerator.js +405 -0
- package/dist/src/hubdocs/index.d.ts +9 -0
- package/dist/src/hubdocs/index.js +9 -0
- package/dist/src/hubdocs/types.d.ts +114 -0
- package/dist/src/hubdocs/types.js +5 -0
- package/dist/src/infrastructure/DependencyContainer.d.ts +142 -0
- package/dist/src/infrastructure/DependencyContainer.js +250 -0
- package/dist/src/infrastructure/adapters/AppiumAdapter.d.ts +168 -0
- package/dist/src/infrastructure/adapters/AppiumAdapter.js +468 -0
- package/dist/src/infrastructure/adapters/OracleAdapter.d.ts +150 -0
- package/dist/src/infrastructure/adapters/OracleAdapter.js +388 -0
- package/dist/src/infrastructure/adapters/PlaywrightAdapter.d.ts +192 -0
- package/dist/src/infrastructure/adapters/PlaywrightAdapter.js +382 -0
- package/dist/src/infrastructure/adapters/SSHAdapter.d.ts +141 -0
- package/dist/src/infrastructure/adapters/SSHAdapter.js +428 -0
- package/dist/src/interfaces.d.ts +501 -0
- package/dist/src/interfaces.js +25 -0
- package/dist/src/internal/fakes/__fake-actions__.d.ts +17 -0
- package/dist/src/internal/fakes/__fake-actions__.js +21 -0
- package/dist/src/internal/fakes/__forbidden__.d.ts +10 -0
- package/dist/src/internal/fakes/__forbidden__.js +18 -0
- package/dist/src/internal/fakes/__honeypot__.d.ts +15 -0
- package/dist/src/internal/fakes/__honeypot__.js +24 -0
- package/dist/src/octane/OctaneReporter.d.ts +13 -0
- package/dist/src/octane/OctaneReporter.js +61 -0
- package/dist/src/playwright/CryptoActions.d.ts +20 -0
- package/dist/src/playwright/CryptoActions.js +75 -0
- package/dist/src/playwright/EnhancedWebActions.d.ts +7 -0
- package/dist/src/playwright/EnhancedWebActions.js +65 -0
- package/dist/src/playwright/WebActions.d.ts +1599 -0
- package/dist/src/playwright/WebActions.js +11788 -0
- package/dist/src/playwright/actions/ActionTimeline.d.ts +36 -0
- package/dist/src/playwright/actions/ActionTimeline.js +101 -0
- package/dist/src/playwright/actions/RecoveryQueue.d.ts +82 -0
- package/dist/src/playwright/actions/RecoveryQueue.js +130 -0
- package/dist/src/playwright/actions/SelectorCache.d.ts +53 -0
- package/dist/src/playwright/actions/SelectorCache.js +96 -0
- package/dist/src/playwright/actions/index.d.ts +13 -0
- package/dist/src/playwright/actions/index.js +14 -0
- package/dist/src/playwright/actions/types.d.ts +147 -0
- package/dist/src/playwright/actions/types.js +5 -0
- package/dist/src/playwright/fixtures.d.ts +112 -0
- package/dist/src/playwright/fixtures.js +718 -0
- package/dist/src/playwright/network-logs-reporter.d.ts +7 -0
- package/dist/src/playwright/network-logs-reporter.js +66 -0
- package/dist/src/playwright/registerRecoveryWrappers.d.ts +1 -0
- package/dist/src/playwright/registerRecoveryWrappers.js +54 -0
- package/dist/src/security/BuildSecurity.d.ts +12 -0
- package/dist/src/security/BuildSecurity.js +138 -0
- package/dist/src/security/EulaProtection.d.ts +70 -0
- package/dist/src/security/EulaProtection.js +155 -0
- package/dist/src/security/HoneypotManager.d.ts +46 -0
- package/dist/src/security/HoneypotManager.js +234 -0
- package/dist/src/security/KeysManager.d.ts +36 -0
- package/dist/src/security/KeysManager.js +158 -0
- package/dist/src/security/ProofOfWorkIntegration.d.ts +64 -0
- package/dist/src/security/ProofOfWorkIntegration.js +206 -0
- package/dist/src/security/SecurityValidation.d.ts +21 -0
- package/dist/src/security/SecurityValidation.js +163 -0
- package/dist/src/security/SourceMapProtection.d.ts +55 -0
- package/dist/src/security/SourceMapProtection.js +220 -0
- package/dist/src/security/protector.d.ts +1 -0
- package/dist/src/security/protector.js +97 -0
- package/dist/src/ssh/SSHActions.d.ts +262 -0
- package/dist/src/ssh/SSHActions.js +790 -0
- package/dist/src/ssh/SSHClient.d.ts +99 -0
- package/dist/src/ssh/SSHClient.js +409 -0
- package/dist/src/statements/BaseStatement.d.ts +38 -0
- package/dist/src/statements/BaseStatement.js +78 -0
- package/dist/src/testContext/AuthStateManager.d.ts +93 -0
- package/dist/src/testContext/AuthStateManager.js +256 -0
- package/dist/src/testContext/CoverageManager.d.ts +198 -0
- package/dist/src/testContext/CoverageManager.js +917 -0
- package/dist/src/testContext/TestAnnotations.d.ts +476 -0
- package/dist/src/testContext/TestAnnotations.js +2647 -0
- package/dist/src/testContext/TestContext.d.ts +138 -0
- package/dist/src/testContext/TestContext.js +369 -0
- package/dist/src/testContext/UnifiedHtmlGenerator.d.ts +7 -0
- package/dist/src/testContext/UnifiedHtmlGenerator.js +264 -0
- package/dist/src/testContext/UnifiedReportManager.d.ts +211 -0
- package/dist/src/testContext/UnifiedReportManager.js +1206 -0
- package/dist/src/testhub/DynamicConfigManager.d.ts +121 -0
- package/dist/src/testhub/DynamicConfigManager.js +320 -0
- package/dist/src/testhub/SystemsManager.d.ts +119 -0
- package/dist/src/testhub/SystemsManager.js +365 -0
- package/dist/src/testhub/TestHubClient.d.ts +335 -0
- package/dist/src/testhub/TestHubClient.js +1215 -0
- package/dist/src/testhub/TestHubReporter.d.ts +62 -0
- package/dist/src/testhub/TestHubReporter.js +576 -0
- package/dist/src/testhub/TestHubVars.d.ts +116 -0
- package/dist/src/testhub/TestHubVars.js +273 -0
- package/dist/src/utils/ActionInterceptor.d.ts +59 -0
- package/dist/src/utils/ActionInterceptor.js +741 -0
- package/dist/src/utils/ArtifactsCompressor.d.ts +43 -0
- package/dist/src/utils/ArtifactsCompressor.js +181 -0
- package/dist/src/utils/AutoLogsFinal.d.ts +47 -0
- package/dist/src/utils/AutoLogsFinal.js +148 -0
- package/dist/src/utils/CodeGenSession.d.ts +114 -0
- package/dist/src/utils/CodeGenSession.js +264 -0
- package/dist/src/utils/ConfigLogger.d.ts +133 -0
- package/dist/src/utils/ConfigLogger.js +611 -0
- package/dist/src/utils/CustomReporter.d.ts +22 -0
- package/dist/src/utils/CustomReporter.js +352 -0
- package/dist/src/utils/DataStore.d.ts +171 -0
- package/dist/src/utils/DataStore.js +484 -0
- package/dist/src/utils/DatabaseInterceptor.d.ts +19 -0
- package/dist/src/utils/DatabaseInterceptor.js +295 -0
- package/dist/src/utils/DateHelper.d.ts +16 -0
- package/dist/src/utils/DateHelper.js +120 -0
- package/dist/src/utils/DateValidator.d.ts +4 -0
- package/dist/src/utils/DateValidator.js +51 -0
- package/dist/src/utils/DocumentGenerator.d.ts +35 -0
- package/dist/src/utils/DocumentGenerator.js +129 -0
- package/dist/src/utils/EvidenceCapture.d.ts +90 -0
- package/dist/src/utils/EvidenceCapture.js +600 -0
- package/dist/src/utils/EvidenceReportGenerator.d.ts +70 -0
- package/dist/src/utils/EvidenceReportGenerator.js +799 -0
- package/dist/src/utils/FrameManagementUtil.d.ts +42 -0
- package/dist/src/utils/FrameManagementUtil.js +75 -0
- package/dist/src/utils/GlobalStatementsInterceptor.d.ts +1 -0
- package/dist/src/utils/GlobalStatementsInterceptor.js +1 -0
- package/dist/src/utils/HTMLTemplate.d.ts +1 -0
- package/dist/src/utils/HTMLTemplate.js +1034 -0
- package/dist/src/utils/InterceptacaoMagica.d.ts +23 -0
- package/dist/src/utils/InterceptacaoMagica.js +365 -0
- package/dist/src/utils/LogSanitizer.d.ts +35 -0
- package/dist/src/utils/LogSanitizer.js +110 -0
- package/dist/src/utils/Logger.d.ts +65 -0
- package/dist/src/utils/Logger.js +284 -0
- package/dist/src/utils/McpLocalClient.d.ts +141 -0
- package/dist/src/utils/McpLocalClient.js +871 -0
- package/dist/src/utils/PDFEvidenceGenerator.d.ts +20 -0
- package/dist/src/utils/PDFEvidenceGenerator.js +156 -0
- package/dist/src/utils/SpecFileAnalyzer.d.ts +35 -0
- package/dist/src/utils/SpecFileAnalyzer.js +209 -0
- package/dist/src/utils/StatementInterceptor.d.ts +18 -0
- package/dist/src/utils/StatementInterceptor.js +87 -0
- package/dist/src/utils/StatementLogger.d.ts +33 -0
- package/dist/src/utils/StatementLogger.js +113 -0
- package/dist/src/utils/StatementsInterceptor.d.ts +1 -0
- package/dist/src/utils/StatementsInterceptor.js +1 -0
- package/dist/src/utils/TeamsFlushHook.d.ts +17 -0
- package/dist/src/utils/TeamsFlushHook.js +168 -0
- package/dist/src/utils/TerminalLogCapture.d.ts +158 -0
- package/dist/src/utils/TerminalLogCapture.js +531 -0
- package/dist/src/utils/TestMethodLogger.d.ts +70 -0
- package/dist/src/utils/TestMethodLogger.js +95 -0
- package/dist/src/utils/UnifiedTeardown.d.ts +4 -0
- package/dist/src/utils/UnifiedTeardown.js +400 -0
- package/dist/src/utils/XPathCatalog.d.ts +152 -0
- package/dist/src/utils/XPathCatalog.js +350 -0
- package/dist/src/utils/generators.d.ts +90 -0
- package/dist/src/utils/generators.js +167 -0
- package/dist/src/utils/testRecovery/ResilientPlaywright.d.ts +152 -0
- package/dist/src/utils/testRecovery/ResilientPlaywright.js +715 -0
- package/dist/src/utils/testRecovery/TestRecoveryClient.d.ts +801 -0
- package/dist/src/utils/testRecovery/TestRecoveryClient.js +1415 -0
- package/dist/src/utils/testRecovery/autoFixCode.d.ts +65 -0
- package/dist/src/utils/testRecovery/autoFixCode.js +32 -0
- package/dist/vitest.config.d.ts +2 -0
- package/dist/vitest.config.js +59 -0
- package/dist/wdio.conf.d.ts +1 -0
- package/dist/wdio.conf.js +420 -0
- package/package.json +137 -0
- package/protect-loader.mjs +643 -0
- package/scripts/consumer-postinstall.ts +975 -0
- package/scripts/generate-index.ts +343 -0
- package/scripts/init-api.ts +613 -0
- package/scripts/init-banco.ts +437 -0
- package/scripts/init-frontend.ts +727 -0
- package/scripts/init-mobile.ts +558 -0
- package/scripts/init-scenarios.ts +925 -0
- package/scripts/init-ssh.ts +734 -0
- package/scripts/package-versions.ts +978 -0
- package/scripts/postinstall.ts +605 -0
- package/scripts/smart-override.ts +1675 -0
- package/scripts/sync-configs.ts +302 -0
- package/scripts/ultracite-setup.ts +370 -0
- package/src/types/globals.d.ts +48 -0
- package/tsconfig.json +29 -0
- package/types/autocore-sync-signal.d.ts +10 -0
|
@@ -0,0 +1,871 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Local JSON-RPC Client (v2.7.44-r3 — items 22, 26, 27, 28)
|
|
3
|
+
*
|
|
4
|
+
* Permite ao framework chamar tools no MCP local (sidecar/background)
|
|
5
|
+
* via JSON-RPC sobre STDIO ou HTTP, sem depender de GitHub Copilot Chat.
|
|
6
|
+
*
|
|
7
|
+
* Transporte (ordem de prioridade — item 26.2):
|
|
8
|
+
* 1. STDIO direto (`autocore-hub-mcp`) com Content-Length framing (MCP oficial) — caminho principal
|
|
9
|
+
* 2. HTTP local (`HUB_MCP_LOCAL_URL`) — bridge HTTP explícita
|
|
10
|
+
* 3. Backend MCP remoto — fallback/control-plane (backend decide reroute local ou fallback)
|
|
11
|
+
*
|
|
12
|
+
* Uso principal: `fix_xpath_in_attributes` para auto-fix de XPaths
|
|
13
|
+
* nos arquivos Attributes* do projeto local após recovery bem-sucedido.
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from "fs";
|
|
16
|
+
import * as path from "path";
|
|
17
|
+
import { Logger } from "./Logger.js";
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Dedupe tracker (evitar edições repetidas na mesma execução)
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
const _appliedFixes = new Set();
|
|
22
|
+
function dedupeKey(oldXPath, newXPath, methodName) {
|
|
23
|
+
return `${oldXPath}||${newXPath}||${methodName || ""}`;
|
|
24
|
+
}
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Configuração (lazy reads — pega da env do Windows a cada chamada)
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
function getMcpLocalUrl() {
|
|
29
|
+
return (process.env.HUB_MCP_LOCAL_URL || "").trim();
|
|
30
|
+
}
|
|
31
|
+
function getMcpBackendUrl() {
|
|
32
|
+
return (process.env.AUTOCORE_HUB_URL ||
|
|
33
|
+
process.env.AUTOCORE_HUB_IP ||
|
|
34
|
+
"http://brtlvlty0559pl:3333");
|
|
35
|
+
}
|
|
36
|
+
const MCP_LOCAL_TIMEOUT = 15_000;
|
|
37
|
+
const MCP_LOCAL_MAX_RETRIES = 2;
|
|
38
|
+
/** Lê user_hash das variáveis de ambiente do Windows (lazy, a cada chamada) */
|
|
39
|
+
function getMcpUserHash() {
|
|
40
|
+
return process.env.HUB_MCP_USER_HASH || process.env.MCP_USER_HASH || "";
|
|
41
|
+
}
|
|
42
|
+
function makeId() {
|
|
43
|
+
return `autofix-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
44
|
+
}
|
|
45
|
+
function enrichArgsWithUserHash(args) {
|
|
46
|
+
const userHash = getMcpUserHash();
|
|
47
|
+
if (!userHash)
|
|
48
|
+
return { ...args };
|
|
49
|
+
return {
|
|
50
|
+
...args,
|
|
51
|
+
user_hash: args.user_hash || userHash,
|
|
52
|
+
userHash: args.userHash || userHash,
|
|
53
|
+
mcpUserHash: args.mcpUserHash || userHash,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// Cache de tools/list por canal para evitar health-check repetido na mesma execução
|
|
57
|
+
const _toolsListCache = new Map();
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// 1) STDIO transport com Content-Length framing (MCP oficial — item 27.1/28.2)
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
let _stdioProcess = null;
|
|
62
|
+
const _stdioPendingRequests = new Map();
|
|
63
|
+
let _stdioAvailable = null; // null = not checked yet
|
|
64
|
+
function splitCommandLine(value) {
|
|
65
|
+
const normalized = value.trim();
|
|
66
|
+
if (!normalized)
|
|
67
|
+
return [];
|
|
68
|
+
return (normalized
|
|
69
|
+
.match(/(?:[^\s"]+|"[^"]*")+/g)
|
|
70
|
+
?.map((part) => part.replace(/^"|"$/g, "")) || []);
|
|
71
|
+
}
|
|
72
|
+
function getMcpStdioCandidates() {
|
|
73
|
+
const candidates = [];
|
|
74
|
+
const custom = String(process.env.HUB_MCP_STDIO_BIN || "").trim();
|
|
75
|
+
if (custom) {
|
|
76
|
+
const parts = splitCommandLine(custom);
|
|
77
|
+
if (parts.length > 0) {
|
|
78
|
+
const [command, ...rest] = parts;
|
|
79
|
+
const args = rest.includes("--stdio") ? rest : [...rest, "--stdio"];
|
|
80
|
+
candidates.push({
|
|
81
|
+
command,
|
|
82
|
+
args,
|
|
83
|
+
label: `custom:${custom}`,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
candidates.push({
|
|
88
|
+
command: "autocore-hub-mcp",
|
|
89
|
+
args: ["--stdio"],
|
|
90
|
+
label: "global:autocore-hub-mcp",
|
|
91
|
+
});
|
|
92
|
+
candidates.push({
|
|
93
|
+
command: "npx",
|
|
94
|
+
args: ["-y", "@silasfmartins/testhub-orchestrator-mcp", "--stdio"],
|
|
95
|
+
label: "npx:@silasfmartins/testhub-orchestrator-mcp",
|
|
96
|
+
});
|
|
97
|
+
// remover duplicados mantendo ordem
|
|
98
|
+
const dedupe = new Set();
|
|
99
|
+
return candidates.filter((candidate) => {
|
|
100
|
+
const key = `${candidate.command} ${candidate.args.join(" ")}`;
|
|
101
|
+
if (dedupe.has(key))
|
|
102
|
+
return false;
|
|
103
|
+
dedupe.add(key);
|
|
104
|
+
return true;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function findHeaderEnd(buf) {
|
|
108
|
+
// Procura \r\n\r\n
|
|
109
|
+
for (let i = 0; i < buf.length - 3; i++) {
|
|
110
|
+
if (buf[i] === 0x0d &&
|
|
111
|
+
buf[i + 1] === 0x0a &&
|
|
112
|
+
buf[i + 2] === 0x0d &&
|
|
113
|
+
buf[i + 3] === 0x0a) {
|
|
114
|
+
return i;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return -1;
|
|
118
|
+
}
|
|
119
|
+
function attachStdioHandlers(processRef, label) {
|
|
120
|
+
processRef.on("error", (err) => {
|
|
121
|
+
Logger.warning(`[McpLocalClient] Falha no processo STDIO (${label}): ${err instanceof Error ? err.message : String(err)}`);
|
|
122
|
+
_stdioAvailable = false;
|
|
123
|
+
_stdioProcess = null;
|
|
124
|
+
});
|
|
125
|
+
processRef.on("exit", () => {
|
|
126
|
+
_stdioProcess = null;
|
|
127
|
+
for (const [id, pending] of _stdioPendingRequests) {
|
|
128
|
+
clearTimeout(pending.timer);
|
|
129
|
+
pending.resolve({
|
|
130
|
+
jsonrpc: "2.0",
|
|
131
|
+
id,
|
|
132
|
+
error: { code: -1, message: "STDIO process exited" },
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
_stdioPendingRequests.clear();
|
|
136
|
+
});
|
|
137
|
+
// Parser Content-Length (MCP/LSP framing — item 27.1/28.2)
|
|
138
|
+
// Formato: "Content-Length: <n>\r\n\r\n<json-body>"
|
|
139
|
+
// Também aceita newline-delimited como fallback para compatibilidade.
|
|
140
|
+
let rawBuffer = Buffer.alloc(0);
|
|
141
|
+
processRef.stdout?.on("data", (chunk) => {
|
|
142
|
+
rawBuffer = Buffer.concat([rawBuffer, chunk]);
|
|
143
|
+
parseStdioBuffer();
|
|
144
|
+
});
|
|
145
|
+
function parseStdioBuffer() {
|
|
146
|
+
// eslint-disable-next-line no-constant-condition
|
|
147
|
+
while (true) {
|
|
148
|
+
// Tenta Content-Length framing primeiro
|
|
149
|
+
const headerEnd = findHeaderEnd(rawBuffer);
|
|
150
|
+
if (headerEnd !== -1) {
|
|
151
|
+
const headerStr = rawBuffer.subarray(0, headerEnd).toString("utf-8");
|
|
152
|
+
const clMatch = headerStr.match(/Content-Length:\s*(\d+)/i);
|
|
153
|
+
if (clMatch) {
|
|
154
|
+
const bodyLen = parseInt(clMatch[1], 10);
|
|
155
|
+
const bodyStart = headerEnd + 4; // após \r\n\r\n
|
|
156
|
+
if (rawBuffer.length >= bodyStart + bodyLen) {
|
|
157
|
+
const bodyStr = rawBuffer
|
|
158
|
+
.subarray(bodyStart, bodyStart + bodyLen)
|
|
159
|
+
.toString("utf-8");
|
|
160
|
+
rawBuffer = rawBuffer.subarray(bodyStart + bodyLen);
|
|
161
|
+
dispatchStdioMessage(bodyStr);
|
|
162
|
+
continue; // pode haver mais mensagens no buffer
|
|
163
|
+
}
|
|
164
|
+
break; // aguardar mais dados
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Fallback: newline-delimited (compatibilidade)
|
|
168
|
+
const nlIdx = rawBuffer.indexOf(0x0a); // \n
|
|
169
|
+
if (nlIdx === -1)
|
|
170
|
+
break;
|
|
171
|
+
const line = rawBuffer.subarray(0, nlIdx).toString("utf-8").trim();
|
|
172
|
+
rawBuffer = rawBuffer.subarray(nlIdx + 1);
|
|
173
|
+
if (line)
|
|
174
|
+
dispatchStdioMessage(line);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function dispatchStdioMessage(text) {
|
|
178
|
+
try {
|
|
179
|
+
const msg = JSON.parse(text);
|
|
180
|
+
if (msg.id && _stdioPendingRequests.has(String(msg.id))) {
|
|
181
|
+
const pending = _stdioPendingRequests.get(String(msg.id));
|
|
182
|
+
clearTimeout(pending.timer);
|
|
183
|
+
_stdioPendingRequests.delete(String(msg.id));
|
|
184
|
+
pending.resolve(msg);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
/* ignore non-JSON */
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async function spawnCandidateWithProbe(candidate) {
|
|
193
|
+
const { spawn } = await import("child_process");
|
|
194
|
+
// No Windows, executáveis globais do npm são .cmd e precisam de shell: true
|
|
195
|
+
const isWindows = process.platform === "win32";
|
|
196
|
+
const processRef = spawn(candidate.command, candidate.args, {
|
|
197
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
198
|
+
env: { ...process.env },
|
|
199
|
+
windowsHide: true,
|
|
200
|
+
shell: isWindows,
|
|
201
|
+
});
|
|
202
|
+
return await new Promise((resolve) => {
|
|
203
|
+
let settled = false;
|
|
204
|
+
const finish = (result) => {
|
|
205
|
+
if (settled)
|
|
206
|
+
return;
|
|
207
|
+
settled = true;
|
|
208
|
+
processRef.off("error", onError);
|
|
209
|
+
processRef.off("exit", onExit);
|
|
210
|
+
clearTimeout(timer);
|
|
211
|
+
resolve(result);
|
|
212
|
+
};
|
|
213
|
+
const onError = () => {
|
|
214
|
+
try {
|
|
215
|
+
processRef.kill();
|
|
216
|
+
}
|
|
217
|
+
catch { }
|
|
218
|
+
finish(null);
|
|
219
|
+
};
|
|
220
|
+
const onExit = (code) => {
|
|
221
|
+
if (code === null)
|
|
222
|
+
return;
|
|
223
|
+
finish(null);
|
|
224
|
+
};
|
|
225
|
+
const timer = setTimeout(() => finish(processRef), 450);
|
|
226
|
+
processRef.once("error", onError);
|
|
227
|
+
processRef.once("exit", onExit);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Spawna o processo MCP local via STDIO se ainda não ativo.
|
|
232
|
+
* Protocolo: JSON-RPC sobre Content-Length framing (padrão MCP/LSP).
|
|
233
|
+
*/
|
|
234
|
+
async function ensureStdioProcess() {
|
|
235
|
+
if (_stdioAvailable === false)
|
|
236
|
+
return false;
|
|
237
|
+
if (_stdioProcess && !_stdioProcess.killed)
|
|
238
|
+
return true;
|
|
239
|
+
try {
|
|
240
|
+
const candidates = getMcpStdioCandidates();
|
|
241
|
+
for (const candidate of candidates) {
|
|
242
|
+
const processRef = await spawnCandidateWithProbe(candidate);
|
|
243
|
+
if (!processRef) {
|
|
244
|
+
Logger.info(`[McpLocalClient] MCP local indisponível via ${candidate.label}, tentando próximo...`);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
_stdioProcess = processRef;
|
|
248
|
+
attachStdioHandlers(processRef, candidate.label);
|
|
249
|
+
_stdioAvailable = true;
|
|
250
|
+
Logger.info(`[McpLocalClient] MCP local iniciado via ${candidate.label}`);
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
_stdioAvailable = false;
|
|
254
|
+
_stdioProcess = null;
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
_stdioAvailable = false;
|
|
259
|
+
_stdioProcess = null;
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Envia JSON-RPC via STDIO com Content-Length framing e aguarda resposta
|
|
265
|
+
*/
|
|
266
|
+
async function callMcpStdio(method, params) {
|
|
267
|
+
if (!_stdioProcess?.stdin?.writable) {
|
|
268
|
+
throw new Error("STDIO process not available");
|
|
269
|
+
}
|
|
270
|
+
const id = makeId();
|
|
271
|
+
const request = { jsonrpc: "2.0", id, method, params };
|
|
272
|
+
const body = JSON.stringify(request);
|
|
273
|
+
return new Promise((resolve) => {
|
|
274
|
+
const timer = setTimeout(() => {
|
|
275
|
+
_stdioPendingRequests.delete(id);
|
|
276
|
+
resolve({
|
|
277
|
+
jsonrpc: "2.0",
|
|
278
|
+
id,
|
|
279
|
+
error: { code: -1, message: "STDIO timeout" },
|
|
280
|
+
});
|
|
281
|
+
}, MCP_LOCAL_TIMEOUT);
|
|
282
|
+
_stdioPendingRequests.set(id, { resolve, timer });
|
|
283
|
+
// Content-Length framing (MCP/LSP padrão)
|
|
284
|
+
const frame = `Content-Length: ${Buffer.byteLength(body, "utf-8")}\r\n\r\n${body}`;
|
|
285
|
+
_stdioProcess.stdin.write(frame, "utf-8");
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
// 2) HTTP transport (local ou backend remoto)
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
async function callMcpHttp(toolName, args, baseUrl) {
|
|
292
|
+
const reqBody = {
|
|
293
|
+
jsonrpc: "2.0",
|
|
294
|
+
id: makeId(),
|
|
295
|
+
method: "tools/call",
|
|
296
|
+
params: { name: toolName, arguments: args },
|
|
297
|
+
};
|
|
298
|
+
const controller = new AbortController();
|
|
299
|
+
const timeoutId = setTimeout(() => controller.abort(), MCP_LOCAL_TIMEOUT);
|
|
300
|
+
try {
|
|
301
|
+
const userHash = getMcpUserHash();
|
|
302
|
+
let url = `${baseUrl.replace(/\/$/, "")}/mcp`;
|
|
303
|
+
if (userHash) {
|
|
304
|
+
url += `?user_hash=${encodeURIComponent(userHash)}`;
|
|
305
|
+
}
|
|
306
|
+
const headers = {
|
|
307
|
+
"Content-Type": "application/json",
|
|
308
|
+
};
|
|
309
|
+
if (userHash) {
|
|
310
|
+
headers["x-user-hash"] = userHash;
|
|
311
|
+
}
|
|
312
|
+
const res = await fetch(url, {
|
|
313
|
+
method: "POST",
|
|
314
|
+
headers,
|
|
315
|
+
body: JSON.stringify(reqBody),
|
|
316
|
+
signal: controller.signal,
|
|
317
|
+
});
|
|
318
|
+
if (!res.ok) {
|
|
319
|
+
const text = await res.text().catch(() => "");
|
|
320
|
+
return {
|
|
321
|
+
jsonrpc: "2.0",
|
|
322
|
+
id: reqBody.id,
|
|
323
|
+
error: { code: res.status, message: `HTTP ${res.status}: ${text}` },
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
return (await res.json());
|
|
327
|
+
}
|
|
328
|
+
finally {
|
|
329
|
+
clearTimeout(timeoutId);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
// 3) Health-check: tools/list (item 26.3)
|
|
334
|
+
// ---------------------------------------------------------------------------
|
|
335
|
+
/**
|
|
336
|
+
* Verifica se uma tool existe no canal informado via tools/list.
|
|
337
|
+
* Resultado é cacheado por canal para a execução inteira.
|
|
338
|
+
*/
|
|
339
|
+
async function hasToolOnChannel(channel, toolName) {
|
|
340
|
+
if (_toolsListCache.has(channel)) {
|
|
341
|
+
const cached = _toolsListCache.get(channel);
|
|
342
|
+
return cached ? cached.has(toolName) : false;
|
|
343
|
+
}
|
|
344
|
+
try {
|
|
345
|
+
let response;
|
|
346
|
+
if (channel === "stdio") {
|
|
347
|
+
const ok = await ensureStdioProcess();
|
|
348
|
+
if (!ok) {
|
|
349
|
+
_toolsListCache.set(channel, null);
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
response = await callMcpStdio("tools/list", {});
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
const baseUrl = channel === "http-local" ? getMcpLocalUrl() : getMcpBackendUrl();
|
|
356
|
+
if (!baseUrl) {
|
|
357
|
+
_toolsListCache.set(channel, null);
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
const userHash = getMcpUserHash();
|
|
361
|
+
// HTTP requer user_hash (item 28.1)
|
|
362
|
+
if (!userHash) {
|
|
363
|
+
_toolsListCache.set(channel, null);
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
const reqBody = {
|
|
367
|
+
jsonrpc: "2.0",
|
|
368
|
+
id: makeId(),
|
|
369
|
+
method: "tools/list",
|
|
370
|
+
params: {},
|
|
371
|
+
};
|
|
372
|
+
const controller = new AbortController();
|
|
373
|
+
const timeoutId = setTimeout(() => controller.abort(), 8_000);
|
|
374
|
+
try {
|
|
375
|
+
let url = `${baseUrl.replace(/\/$/, "")}/mcp`;
|
|
376
|
+
if (userHash)
|
|
377
|
+
url += `?user_hash=${encodeURIComponent(userHash)}`;
|
|
378
|
+
const headers = {
|
|
379
|
+
"Content-Type": "application/json",
|
|
380
|
+
};
|
|
381
|
+
if (userHash)
|
|
382
|
+
headers["x-user-hash"] = userHash;
|
|
383
|
+
const res = await fetch(url, {
|
|
384
|
+
method: "POST",
|
|
385
|
+
headers,
|
|
386
|
+
body: JSON.stringify(reqBody),
|
|
387
|
+
signal: controller.signal,
|
|
388
|
+
});
|
|
389
|
+
if (!res.ok) {
|
|
390
|
+
_toolsListCache.set(channel, null);
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
response = (await res.json());
|
|
394
|
+
}
|
|
395
|
+
finally {
|
|
396
|
+
clearTimeout(timeoutId);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (response.error) {
|
|
400
|
+
_toolsListCache.set(channel, null);
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
// Extrair nomes das tools
|
|
404
|
+
const tools = response.result;
|
|
405
|
+
const toolSet = new Set();
|
|
406
|
+
if (tools?.tools && Array.isArray(tools.tools)) {
|
|
407
|
+
for (const t of tools.tools) {
|
|
408
|
+
if (t.name)
|
|
409
|
+
toolSet.add(t.name);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
_toolsListCache.set(channel, toolSet);
|
|
413
|
+
return toolSet.has(toolName);
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
_toolsListCache.set(channel, null);
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// ---------------------------------------------------------------------------
|
|
421
|
+
// 4) Chamada com fallback por canal (items 26.2, 28.5)
|
|
422
|
+
// ---------------------------------------------------------------------------
|
|
423
|
+
/**
|
|
424
|
+
* Tenta chamar tool no MCP seguindo ordem de prioridade:
|
|
425
|
+
* 1. STDIO direto (não requer user_hash — item 28.1)
|
|
426
|
+
* 2. HTTP local (HUB_MCP_LOCAL_URL) — requer user_hash
|
|
427
|
+
* 3. Backend MCP remoto — quando habilitado em options.allowBackend
|
|
428
|
+
*
|
|
429
|
+
* Faz health-check (tools/list) antes de chamar, para evitar -32601.
|
|
430
|
+
*/
|
|
431
|
+
async function callToolWithFallback(toolName, args, options = {}) {
|
|
432
|
+
const channels = options.preferBackend && options.allowBackend
|
|
433
|
+
? ["http-backend", "stdio", "http-local"]
|
|
434
|
+
: ["stdio", "http-local"];
|
|
435
|
+
if (options.allowBackend && !channels.includes("http-backend")) {
|
|
436
|
+
channels.push("http-backend");
|
|
437
|
+
}
|
|
438
|
+
for (const channel of channels) {
|
|
439
|
+
const available = await hasToolOnChannel(channel, toolName);
|
|
440
|
+
if (!available) {
|
|
441
|
+
Logger.info(`[McpLocalClient] Tool '${toolName}' não disponível via ${channel}, tentando próximo...`);
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
try {
|
|
445
|
+
let response;
|
|
446
|
+
if (channel === "stdio") {
|
|
447
|
+
response = await callMcpStdio("tools/call", {
|
|
448
|
+
name: toolName,
|
|
449
|
+
arguments: args,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
const baseUrl = channel === "http-local" ? getMcpLocalUrl() : getMcpBackendUrl();
|
|
454
|
+
if (!baseUrl) {
|
|
455
|
+
Logger.info(`[McpLocalClient] URL não configurada para canal ${channel}, pulando...`);
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
response = await callMcpHttp(toolName, args, baseUrl);
|
|
459
|
+
}
|
|
460
|
+
if (response.error?.code === -32601) {
|
|
461
|
+
Logger.warning(`[McpLocalClient] Tool '${toolName}' retornou -32601 via ${channel}.`);
|
|
462
|
+
_toolsListCache.delete(channel);
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
return { response, channel };
|
|
466
|
+
}
|
|
467
|
+
catch (err) {
|
|
468
|
+
Logger.warning(`[McpLocalClient] Erro no canal ${channel}: ${err instanceof Error ? err.message : String(err)}`);
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return {
|
|
473
|
+
response: {
|
|
474
|
+
jsonrpc: "2.0",
|
|
475
|
+
id: makeId(),
|
|
476
|
+
error: {
|
|
477
|
+
code: -1,
|
|
478
|
+
message: "Nenhum canal MCP disponível para a tool solicitada.",
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
channel: "http-backend",
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
// ---------------------------------------------------------------------------
|
|
485
|
+
// 5) Parse robusto de resposta (item 27.2/28.3)
|
|
486
|
+
// ---------------------------------------------------------------------------
|
|
487
|
+
/**
|
|
488
|
+
* Extrai FixXPathInAttributesResult da resposta do MCP.
|
|
489
|
+
*
|
|
490
|
+
* Tenta na ordem:
|
|
491
|
+
* 1. response.result como objeto estruturado (top-level)
|
|
492
|
+
* 2. response.result.content[].text com JSON serializado
|
|
493
|
+
* 3. null se nenhum formato reconhecido
|
|
494
|
+
*/
|
|
495
|
+
function parseFixResult(raw) {
|
|
496
|
+
if (!raw || typeof raw !== "object")
|
|
497
|
+
return null;
|
|
498
|
+
const obj = raw;
|
|
499
|
+
// 0) Alguns retornos vêm encapsulados em payload
|
|
500
|
+
if (obj.payload && typeof obj.payload === "object") {
|
|
501
|
+
const payloadResult = parseFixResult(obj.payload);
|
|
502
|
+
if (payloadResult)
|
|
503
|
+
return payloadResult;
|
|
504
|
+
}
|
|
505
|
+
// 1) Top-level estruturado: { success, filePath, ... }
|
|
506
|
+
if ("success" in obj || "fileUpdated" in obj || "nextAction" in obj) {
|
|
507
|
+
return {
|
|
508
|
+
success: obj.success === true ||
|
|
509
|
+
obj.fileUpdated === true ||
|
|
510
|
+
obj.nextAction === "retry",
|
|
511
|
+
filePath: obj.filePath || undefined,
|
|
512
|
+
className: obj.className || undefined,
|
|
513
|
+
methodName: obj.methodName || undefined,
|
|
514
|
+
fileUpdated: obj.fileUpdated === true,
|
|
515
|
+
executedOn: obj.executedOn || undefined,
|
|
516
|
+
localMcpExecuted: obj.localMcpExecuted === true,
|
|
517
|
+
localMcpRequestId: obj.localMcpRequestId || undefined,
|
|
518
|
+
nextAction: obj.nextAction || undefined,
|
|
519
|
+
error: obj.error || undefined,
|
|
520
|
+
proof: obj.proof && typeof obj.proof === "object"
|
|
521
|
+
? obj.proof
|
|
522
|
+
: undefined,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
// 2) content[].text com JSON serializado
|
|
526
|
+
const content = obj.content;
|
|
527
|
+
if (Array.isArray(content)) {
|
|
528
|
+
for (const item of content) {
|
|
529
|
+
if (item && typeof item === "object" && typeof item.text === "string") {
|
|
530
|
+
try {
|
|
531
|
+
const parsed = JSON.parse(item.text);
|
|
532
|
+
if (parsed && typeof parsed === "object") {
|
|
533
|
+
return parseFixResult(parsed); // recurse
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
catch {
|
|
537
|
+
/* não é JSON válido, ignorar */
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Determina se o resultado é válido o suficiente para contabilizar no dedupe (item 28.4).
|
|
546
|
+
* Só deduplica quando houve processamento real:
|
|
547
|
+
* - success=true, ou
|
|
548
|
+
* - nextAction=manual_review com payload válido
|
|
549
|
+
*/
|
|
550
|
+
function isValidForDedupe(result) {
|
|
551
|
+
if (!result)
|
|
552
|
+
return false;
|
|
553
|
+
if (result.success)
|
|
554
|
+
return true;
|
|
555
|
+
if (result.nextAction === "manual_review" &&
|
|
556
|
+
(result.filePath || result.className))
|
|
557
|
+
return true;
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
function verifyFixOnDisk(params) {
|
|
561
|
+
const normalize = (value) => value
|
|
562
|
+
.replace(/\r\n/g, "\n")
|
|
563
|
+
.replace(/["']/g, "'")
|
|
564
|
+
.replace(/\s+/g, " ")
|
|
565
|
+
.trim();
|
|
566
|
+
const oldNormalized = normalize(params.oldXPath);
|
|
567
|
+
const newNormalized = normalize(params.newXPath);
|
|
568
|
+
if (!newNormalized)
|
|
569
|
+
return false;
|
|
570
|
+
try {
|
|
571
|
+
if (params.filePath && fs.existsSync(params.filePath)) {
|
|
572
|
+
const content = fs.readFileSync(params.filePath, "utf-8");
|
|
573
|
+
const normalizedContent = normalize(content);
|
|
574
|
+
return (normalizedContent.includes(newNormalized) &&
|
|
575
|
+
(!oldNormalized || !normalizedContent.includes(oldNormalized)));
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
// segue para fallback
|
|
580
|
+
}
|
|
581
|
+
// fallback: verificar arquivos Attributes dentro do projectRoot
|
|
582
|
+
try {
|
|
583
|
+
const queue = [params.projectRoot];
|
|
584
|
+
let scanned = 0;
|
|
585
|
+
const maxScanned = 400;
|
|
586
|
+
while (queue.length > 0 && scanned < maxScanned) {
|
|
587
|
+
const current = queue.shift();
|
|
588
|
+
let entries = [];
|
|
589
|
+
try {
|
|
590
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
591
|
+
}
|
|
592
|
+
catch {
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
for (const entry of entries) {
|
|
596
|
+
if (scanned >= maxScanned)
|
|
597
|
+
break;
|
|
598
|
+
const fullPath = path.join(current, entry.name);
|
|
599
|
+
if (entry.isDirectory()) {
|
|
600
|
+
if (entry.name === "node_modules" ||
|
|
601
|
+
entry.name === ".git" ||
|
|
602
|
+
entry.name === "dist") {
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
queue.push(fullPath);
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
scanned += 1;
|
|
609
|
+
const lower = entry.name.toLowerCase();
|
|
610
|
+
if (!lower.endsWith(".ts"))
|
|
611
|
+
continue;
|
|
612
|
+
if (!lower.includes("attributes"))
|
|
613
|
+
continue;
|
|
614
|
+
let content = "";
|
|
615
|
+
try {
|
|
616
|
+
content = fs.readFileSync(fullPath, "utf-8");
|
|
617
|
+
}
|
|
618
|
+
catch {
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
const normalizedContent = normalize(content);
|
|
622
|
+
if (normalizedContent.includes(newNormalized) &&
|
|
623
|
+
(!oldNormalized || !normalizedContent.includes(oldNormalized))) {
|
|
624
|
+
return true;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
catch {
|
|
630
|
+
// ignore
|
|
631
|
+
}
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
// ---------------------------------------------------------------------------
|
|
635
|
+
// Public API
|
|
636
|
+
// ---------------------------------------------------------------------------
|
|
637
|
+
/**
|
|
638
|
+
* Chama `fix_xpath_in_attributes` no MCP local para corrigir XPath em Attributes*.
|
|
639
|
+
*
|
|
640
|
+
* Transporte (item 26.2/28.5):
|
|
641
|
+
* 1. STDIO direto (Content-Length framing) — não requer user_hash (item 28.1)
|
|
642
|
+
* 2. HTTP local (HUB_MCP_LOCAL_URL) — requer user_hash
|
|
643
|
+
* 3. Backend MCP remoto (control-plane para reroute local por user_hash)
|
|
644
|
+
*
|
|
645
|
+
* - Faz health-check (tools/list) antes de chamar (item 26.3)
|
|
646
|
+
* - Parse robusto: top-level → content[].text (item 27.2/28.3)
|
|
647
|
+
* - Dedupe somente após resultado válido (item 28.4)
|
|
648
|
+
* - Retry curto (2 tentativas) para erros transitórios
|
|
649
|
+
* - Não bloqueia o teste em caso de falha
|
|
650
|
+
* - Pula em execução Azure (sem filesystem local)
|
|
651
|
+
*
|
|
652
|
+
* @returns Resultado da correção ou null se ignorado/falhou
|
|
653
|
+
*/
|
|
654
|
+
export async function fixXPathViaLocalMcp(params) {
|
|
655
|
+
// Não executar em Azure/CI
|
|
656
|
+
if (process.env.TF_BUILD === "True" ||
|
|
657
|
+
process.env.AGENT_NAME ||
|
|
658
|
+
process.env.CI === "true") {
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
// (item 28.1) user_hash agora é exigido apenas para HTTP, não para STDIO.
|
|
662
|
+
// A validação ocorre dentro de hasToolOnChannel/callMcpHttp para canais HTTP.
|
|
663
|
+
// Dedupe
|
|
664
|
+
const key = dedupeKey(params.oldXPath, params.newXPath, params.methodName);
|
|
665
|
+
if (_appliedFixes.has(key)) {
|
|
666
|
+
Logger.info(`[McpLocalClient] Auto-fix já aplicado nesta execução, ignorando: ${params.methodName || params.oldXPath}`);
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
// Enviar projectRoot exatamente como recebido (Windows/UNC)
|
|
670
|
+
let projectRoot = params.projectRoot || process.env.AUTOCORE_PROJECT_ROOT || "";
|
|
671
|
+
if (!projectRoot) {
|
|
672
|
+
try {
|
|
673
|
+
projectRoot = process.cwd();
|
|
674
|
+
}
|
|
675
|
+
catch { }
|
|
676
|
+
}
|
|
677
|
+
if (!projectRoot) {
|
|
678
|
+
Logger.warning("[McpLocalClient] projectRoot não disponível, auto-fix de arquivo ignorado.");
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
// Não normalizar nem converter barras; aceitar UNC/Windows
|
|
682
|
+
const args = {
|
|
683
|
+
projectRoot,
|
|
684
|
+
oldXPath: params.oldXPath,
|
|
685
|
+
newXPath: params.newXPath,
|
|
686
|
+
confidence: params.confidence ?? 0.8,
|
|
687
|
+
executionSource: params.executionSource || "Local",
|
|
688
|
+
};
|
|
689
|
+
if (params.attributesFile)
|
|
690
|
+
args.attributesFile = params.attributesFile;
|
|
691
|
+
if (params.className)
|
|
692
|
+
args.className = params.className;
|
|
693
|
+
if (params.methodName)
|
|
694
|
+
args.methodName = params.methodName;
|
|
695
|
+
for (let attempt = 1; attempt <= MCP_LOCAL_MAX_RETRIES; attempt++) {
|
|
696
|
+
try {
|
|
697
|
+
const { response, channel } = await callToolWithFallback("fix_xpath_in_attributes", args, {
|
|
698
|
+
allowBackend: true,
|
|
699
|
+
});
|
|
700
|
+
// (item 27.3) Log do canal utilizado
|
|
701
|
+
Logger.info(`[McpLocalClient] Canal utilizado: ${channel}`);
|
|
702
|
+
if (response.error) {
|
|
703
|
+
if (response.error.code === -32601 || response.error.code === -1) {
|
|
704
|
+
Logger.warning(`[McpLocalClient] fix_xpath_in_attributes indisponível em todos os canais.`);
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
throw new Error(response.error.message);
|
|
708
|
+
}
|
|
709
|
+
// (item 27.2/28.3) Parse robusto da resposta
|
|
710
|
+
const result = parseFixResult(response.result);
|
|
711
|
+
if (!result) {
|
|
712
|
+
Logger.warning("[McpLocalClient] Auto-fix sem payload estruturado de retorno.");
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
const diskConfirmed = verifyFixOnDisk({
|
|
716
|
+
filePath: result.filePath,
|
|
717
|
+
projectRoot,
|
|
718
|
+
oldXPath: params.oldXPath,
|
|
719
|
+
newXPath: params.newXPath,
|
|
720
|
+
});
|
|
721
|
+
const effectiveSuccess = result.success === true &&
|
|
722
|
+
(result.fileUpdated === true || diskConfirmed);
|
|
723
|
+
const normalizedResult = {
|
|
724
|
+
...result,
|
|
725
|
+
success: effectiveSuccess,
|
|
726
|
+
fileUpdated: result.fileUpdated === true || diskConfirmed,
|
|
727
|
+
};
|
|
728
|
+
if (isValidForDedupe(normalizedResult)) {
|
|
729
|
+
_appliedFixes.add(key);
|
|
730
|
+
}
|
|
731
|
+
if (effectiveSuccess) {
|
|
732
|
+
Logger.success(`[McpLocalClient] ✅ XPath corrigido via MCP (${channel}): ${normalizedResult.filePath || "arquivo não informado"} (${normalizedResult.className || "?"}.${normalizedResult.methodName || "?"})`);
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
735
|
+
Logger.warning(`[McpLocalClient] Auto-fix não confirmou escrita local (${channel}). Motivo: ${normalizedResult.error || "resultado incompleto"}`);
|
|
736
|
+
}
|
|
737
|
+
if (channel === "http-backend" &&
|
|
738
|
+
normalizedResult.executedOn === "backend-fallback") {
|
|
739
|
+
Logger.warning("[McpLocalClient] ⚠️ Auto-fix executado no backend-fallback; validação local recomendada.");
|
|
740
|
+
}
|
|
741
|
+
return normalizedResult;
|
|
742
|
+
}
|
|
743
|
+
catch (err) {
|
|
744
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
745
|
+
if (attempt < MCP_LOCAL_MAX_RETRIES) {
|
|
746
|
+
Logger.warning(`[McpLocalClient] Tentativa ${attempt}/${MCP_LOCAL_MAX_RETRIES} falhou: ${msg}. Retentando...`);
|
|
747
|
+
await new Promise((r) => setTimeout(r, 500 * attempt));
|
|
748
|
+
}
|
|
749
|
+
else {
|
|
750
|
+
Logger.warning(`[McpLocalClient] fix_xpath_in_attributes falhou após ${MCP_LOCAL_MAX_RETRIES} tentativas: ${msg}`);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
// ---------------------------------------------------------------------------
|
|
757
|
+
// CodeGen local-first reroute (item 27.4)
|
|
758
|
+
// ---------------------------------------------------------------------------
|
|
759
|
+
/**
|
|
760
|
+
* Chama uma tool MCP (genérica) priorizando MCP local.
|
|
761
|
+
* Se o backend responder com `requiresLocalMcp=true`, reencaminha automaticamente
|
|
762
|
+
* para o STDIO local com o mesmo payload.
|
|
763
|
+
*
|
|
764
|
+
* Usado para `codegen`/ações Playwright que precisam rodar na máquina do usuário.
|
|
765
|
+
*/
|
|
766
|
+
export async function callMcpToolLocalFirst(toolName, args) {
|
|
767
|
+
// Não executar em Azure/CI
|
|
768
|
+
if (process.env.TF_BUILD === "True" ||
|
|
769
|
+
process.env.AGENT_NAME ||
|
|
770
|
+
process.env.CI === "true") {
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
try {
|
|
774
|
+
const requestArgs = enrichArgsWithUserHash(args);
|
|
775
|
+
const { response, channel } = await callToolWithFallback(toolName, requestArgs, {
|
|
776
|
+
allowBackend: true,
|
|
777
|
+
});
|
|
778
|
+
if (response.error) {
|
|
779
|
+
// Verificar se backend indicou que precisa de MCP local
|
|
780
|
+
const errMsg = response.error.message || "";
|
|
781
|
+
const errData = response.error.data;
|
|
782
|
+
if (errData?.requiresLocalMcp === true ||
|
|
783
|
+
errMsg.includes("requiresLocalMcp")) {
|
|
784
|
+
Logger.info(`[McpLocalClient] Backend requer MCP local para '${toolName}', reencaminhando via STDIO...`);
|
|
785
|
+
const ok = await ensureStdioProcess();
|
|
786
|
+
if (ok) {
|
|
787
|
+
const localResponse = await callMcpStdio("tools/call", {
|
|
788
|
+
name: toolName,
|
|
789
|
+
arguments: requestArgs,
|
|
790
|
+
});
|
|
791
|
+
if (!localResponse.error) {
|
|
792
|
+
return { result: localResponse.result, channel: "stdio" };
|
|
793
|
+
}
|
|
794
|
+
Logger.warning(`[McpLocalClient] Reroute STDIO falhou: ${localResponse.error.message}`);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
Logger.warning(`[McpLocalClient] ${toolName} falhou: ${response.error.message}`);
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
return { result: response.result, channel };
|
|
801
|
+
}
|
|
802
|
+
catch (err) {
|
|
803
|
+
Logger.warning(`[McpLocalClient] Erro em ${toolName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
804
|
+
return null;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
export async function callMcpToolBackendFirst(toolName, args) {
|
|
808
|
+
if (process.env.TF_BUILD === "True" ||
|
|
809
|
+
process.env.AGENT_NAME ||
|
|
810
|
+
process.env.CI === "true") {
|
|
811
|
+
return null;
|
|
812
|
+
}
|
|
813
|
+
try {
|
|
814
|
+
const requestArgs = enrichArgsWithUserHash(args);
|
|
815
|
+
const { response, channel } = await callToolWithFallback(toolName, requestArgs, {
|
|
816
|
+
allowBackend: true,
|
|
817
|
+
preferBackend: true,
|
|
818
|
+
});
|
|
819
|
+
if (response.error) {
|
|
820
|
+
Logger.warning(`[McpLocalClient] ${toolName} falhou em modo backend-first: ${response.error.message}`);
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
return { result: response.result, channel };
|
|
824
|
+
}
|
|
825
|
+
catch (err) {
|
|
826
|
+
Logger.warning(`[McpLocalClient] Erro em ${toolName} (backend-first): ${err instanceof Error ? err.message : String(err)}`);
|
|
827
|
+
return null;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
export async function getPortalSavedTests(params = {}) {
|
|
831
|
+
const response = await callMcpToolBackendFirst("get_portal_saved_tests", params);
|
|
832
|
+
if (!response?.result || typeof response.result !== "object")
|
|
833
|
+
return null;
|
|
834
|
+
const raw = response.result;
|
|
835
|
+
const portalTests = Array.isArray(raw.portalTests)
|
|
836
|
+
? raw.portalTests
|
|
837
|
+
: [];
|
|
838
|
+
return {
|
|
839
|
+
total: typeof raw.total === "number" ? raw.total : portalTests.length,
|
|
840
|
+
includeCode: raw.includeCode === true,
|
|
841
|
+
retentionDays: typeof raw.retentionDays === "number" ? raw.retentionDays : undefined,
|
|
842
|
+
message: typeof raw.message === "string" ? raw.message : undefined,
|
|
843
|
+
portalTests,
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
export async function runLocalNpmScriptViaMcp(params) {
|
|
847
|
+
const response = await callMcpToolLocalFirst("run_local_npm_script", params);
|
|
848
|
+
if (!response?.result || typeof response.result !== "object")
|
|
849
|
+
return null;
|
|
850
|
+
const raw = response.result;
|
|
851
|
+
const payload = raw.payload && typeof raw.payload === "object"
|
|
852
|
+
? raw.payload
|
|
853
|
+
: raw;
|
|
854
|
+
return {
|
|
855
|
+
...payload,
|
|
856
|
+
success: payload.success === true || payload.exitCode === 0,
|
|
857
|
+
message: typeof payload.message === "string" ? payload.message : undefined,
|
|
858
|
+
command: typeof payload.command === "string" ? payload.command : undefined,
|
|
859
|
+
exitCode: typeof payload.exitCode === "number" ? payload.exitCode : undefined,
|
|
860
|
+
stdout: typeof payload.stdout === "string" ? payload.stdout : undefined,
|
|
861
|
+
stderr: typeof payload.stderr === "string" ? payload.stderr : undefined,
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
/** Limpa tracker de dedupe (chamar no início de cada execução, se desejado) */
|
|
865
|
+
export function clearFixDedupeTracker() {
|
|
866
|
+
_appliedFixes.clear();
|
|
867
|
+
}
|
|
868
|
+
/** Limpa cache de health-check de tools (útil para testes) */
|
|
869
|
+
export function clearToolsListCache() {
|
|
870
|
+
_toolsListCache.clear();
|
|
871
|
+
}
|