@skyramp/mcp 0.3.0 → 0.3.1-ldw-beta
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 +1 -0
- package/build/commands/commandLibrary.js +19 -13
- package/build/commands/commandLibrary.test.d.ts +1 -0
- package/build/commands/commandLibrary.test.js +59 -0
- package/build/commands/localDevTestChangesCommand.d.ts +15 -0
- package/build/commands/localDevTestChangesCommand.js +201 -0
- package/build/index.js +80 -6
- package/build/playwright/traceRecordingPrompt.js +2 -0
- package/build/prompts/enhance-assertions/sharedAssertionRules.d.ts +1 -0
- package/build/prompts/enhance-assertions/sharedAssertionRules.js +17 -0
- package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +11 -9
- package/build/prompts/local-dev/local-dev-plan.d.ts +35 -0
- package/build/prompts/local-dev/local-dev-plan.js +466 -0
- package/build/prompts/local-dev/local-dev-prompts.d.ts +4 -0
- package/build/prompts/local-dev/local-dev-prompts.js +190 -0
- package/build/prompts/prompt-utils.d.ts +8 -0
- package/build/prompts/prompt-utils.js +33 -0
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +21 -29
- package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +0 -28
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +41 -4
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +16 -1
- package/build/prompts/testbot/testbot-prompts.d.ts +0 -5
- package/build/prompts/testbot/testbot-prompts.js +5 -34
- package/build/resources/testbotResource.js +2 -1
- package/build/services/AnalyticsService.d.ts +1 -1
- package/build/services/TestExecutionService.d.ts +2 -1
- package/build/services/TestExecutionService.js +8 -3
- package/build/services/TestExecutionService.test.js +83 -3
- package/build/services/TestGenerationService.d.ts +2 -2
- package/build/services/TestGenerationService.js +7 -21
- package/build/services/containerEnv.js +3 -1
- package/build/tool-phases.js +6 -0
- package/build/tools/code-refactor/enhanceAssertionsTool.js +47 -18
- package/build/tools/enrichTestWithMocksTool.d.ts +28 -0
- package/build/tools/enrichTestWithMocksTool.js +726 -0
- package/build/tools/enrichTestWithMocksTool.test.d.ts +1 -0
- package/build/tools/enrichTestWithMocksTool.test.js +266 -0
- package/build/tools/executeSkyrampTestTool.d.ts +11 -0
- package/build/tools/executeSkyrampTestTool.js +63 -22
- package/build/tools/executeSkyrampTestTool.test.d.ts +1 -0
- package/build/tools/executeSkyrampTestTool.test.js +36 -0
- package/build/tools/generate-tests/batchMockGenerationTool.d.ts +85 -0
- package/build/tools/generate-tests/batchMockGenerationTool.js +432 -0
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +6 -21
- package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +1 -0
- package/build/tools/generate-tests/generateContractRestTool.js +2 -2
- package/build/tools/generate-tests/generateMockRestTool.d.ts +129 -6
- package/build/tools/generate-tests/generateMockRestTool.js +234 -22
- package/build/tools/generate-tests/loadTestSchema.d.ts +1 -1
- package/build/tools/generateEnrichedIntegrationTestTool.d.ts +25 -0
- package/build/tools/generateEnrichedIntegrationTestTool.js +235 -0
- package/build/tools/generateEnrichedIntegrationTestTool.test.d.ts +1 -0
- package/build/tools/generateEnrichedIntegrationTestTool.test.js +44 -0
- package/build/tools/localDevWorkerComposeTool.d.ts +24 -0
- package/build/tools/localDevWorkerComposeTool.js +224 -0
- package/build/tools/localDevWorkerComposeTool.test.d.ts +1 -0
- package/build/tools/localDevWorkerComposeTool.test.js +129 -0
- package/build/tools/localDevWorkflowFixes.test.d.ts +1 -0
- package/build/tools/localDevWorkflowFixes.test.js +255 -0
- package/build/tools/one-click/oneClickTool.d.ts +13 -0
- package/build/tools/one-click/oneClickTool.js +195 -24
- package/build/tools/one-click/oneClickTool.test.d.ts +1 -0
- package/build/tools/one-click/oneClickTool.test.js +172 -0
- package/build/tools/preflightMockCheckTool.d.ts +2 -0
- package/build/tools/preflightMockCheckTool.js +96 -0
- package/build/tools/queryProxyMocksTool.d.ts +70 -0
- package/build/tools/queryProxyMocksTool.js +522 -0
- package/build/tools/queryProxyMocksTool.test.d.ts +1 -0
- package/build/tools/queryProxyMocksTool.test.js +164 -0
- package/build/tools/test-management/analyzeChangesTool.d.ts +2 -1
- package/build/tools/test-management/analyzeChangesTool.js +61 -38
- package/build/tools/trace/startTraceCollectionTool.js +3 -3
- package/build/types/OneClickCommands.d.ts +1 -1
- package/build/types/RepositoryAnalysis.d.ts +117 -0
- package/build/types/RepositoryAnalysis.js +16 -2
- package/build/types/TestExecution.d.ts +1 -0
- package/build/types/TestTypes.d.ts +25 -7
- package/build/types/TestTypes.js +19 -5
- package/build/utils/analyze-openapi.js +18 -1
- package/build/utils/analyze-openapi.test.d.ts +1 -0
- package/build/utils/analyze-openapi.test.js +19 -0
- package/build/utils/branchDiff.d.ts +17 -1
- package/build/utils/branchDiff.js +99 -14
- package/build/utils/branchDiff.test.d.ts +1 -0
- package/build/utils/branchDiff.test.js +109 -0
- package/build/utils/featureFlags.d.ts +16 -0
- package/build/utils/featureFlags.js +20 -0
- package/build/utils/featureFlags.test.js +57 -2
- package/build/utils/grpcMockValidation.d.ts +1 -0
- package/build/utils/grpcMockValidation.js +49 -0
- package/build/utils/grpcMockValidation.test.d.ts +1 -0
- package/build/utils/grpcMockValidation.test.js +41 -0
- package/build/utils/httpMethodValidation.d.ts +4 -0
- package/build/utils/httpMethodValidation.js +15 -0
- package/build/utils/mockCompatibility.d.ts +49 -0
- package/build/utils/mockCompatibility.js +82 -0
- package/build/utils/mockCompatibility.test.d.ts +1 -0
- package/build/utils/mockCompatibility.test.js +79 -0
- package/build/utils/progress.js +10 -5
- package/build/utils/versions.d.ts +1 -0
- package/build/utils/versions.js +2 -0
- package/build/utils/workspaceAuth.d.ts +9 -0
- package/build/utils/workspaceAuth.js +51 -1
- package/build/workspace/workspace.d.ts +28 -14
- package/build/workspace/workspace.js +3 -1
- package/node_modules/playwright/lib/mcp/browser/tools/screenshot.js +1 -1
- package/node_modules/playwright/lib/mcp/skyramp/resultCode.js +45 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +6 -22
- package/node_modules/playwright/lib/mcp/test/resultCode.test.js +48 -0
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { logger } from "../../utils/logger.js";
|
|
4
|
+
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
5
|
+
import { buildServiceContext, escapeXml, readWorkspaceServices, } from "../prompt-utils.js";
|
|
6
|
+
import { localDevPlan, GRPC_SERVICE_NAME_GUIDANCE, ASSERTION_API_GUIDANCE } from "./local-dev-plan.js";
|
|
7
|
+
const LOCAL_DEV_MAX_RECOMMENDATIONS = 8;
|
|
8
|
+
const LOCAL_DEV_MAX_TESTS_TO_GENERATE = 5;
|
|
9
|
+
/**
|
|
10
|
+
* Build an XML block that tells the agent which downstream services to mock
|
|
11
|
+
* vs. which are real (already running in the local environment).
|
|
12
|
+
*/
|
|
13
|
+
function buildServiceRoutingBlock(servicesToMock, realServices, mockProtocols) {
|
|
14
|
+
if (!servicesToMock?.length &&
|
|
15
|
+
!realServices?.length &&
|
|
16
|
+
!mockProtocols?.length)
|
|
17
|
+
return "";
|
|
18
|
+
const lines = ["<SERVICE_ROUTING>"];
|
|
19
|
+
if (servicesToMock?.length) {
|
|
20
|
+
// "all" is a sentinel meaning "mock every discovered downstream dependency".
|
|
21
|
+
// Emit it as a dedicated flag so the agent doesn't treat it as a literal service name.
|
|
22
|
+
const mockAll = servicesToMock.includes("all");
|
|
23
|
+
const namedServices = servicesToMock.filter((s) => s !== "all");
|
|
24
|
+
if (mockAll) {
|
|
25
|
+
lines.push(` <MOCK_ALL_DOWNSTREAM>true</MOCK_ALL_DOWNSTREAM>`);
|
|
26
|
+
}
|
|
27
|
+
if (namedServices.length > 0) {
|
|
28
|
+
lines.push(` <MOCK_SERVICES>${namedServices.map(escapeXml).join(", ")}</MOCK_SERVICES>`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (realServices?.length) {
|
|
32
|
+
lines.push(` <REAL_SERVICES>${realServices.map(escapeXml).join(", ")}</REAL_SERVICES>`);
|
|
33
|
+
}
|
|
34
|
+
if (mockProtocols?.length) {
|
|
35
|
+
lines.push(` <MOCK_PROTOCOLS>${mockProtocols.map(escapeXml).join(", ")}</MOCK_PROTOCOLS>`);
|
|
36
|
+
}
|
|
37
|
+
lines.push("</SERVICE_ROUTING>");
|
|
38
|
+
return lines.join("\n");
|
|
39
|
+
}
|
|
40
|
+
export function getLocalDevPrompt(baseCommit, repositoryPath, sandboxWorkerUrl, maxRecommendations = LOCAL_DEV_MAX_RECOMMENDATIONS, maxGenerate = LOCAL_DEV_MAX_TESTS_TO_GENERATE, services, stateOutputFile, servicesToMock, realServices, mockProtocols, proxyKubeconfig, proxyNamespace) {
|
|
41
|
+
maxGenerate = Math.min(Math.max(maxGenerate, 0), maxRecommendations);
|
|
42
|
+
const serviceContext = services?.length ? buildServiceContext(services) : "";
|
|
43
|
+
// Proxy access lets the workflow query a running Skyramp worker proxy for real
|
|
44
|
+
// recorded traffic (via skyramp_query_proxy_mocks) instead of inferring mock
|
|
45
|
+
// shapes from code/OpenAPI. sandboxWorkerUrl is the direct-URL (or ingress) mode;
|
|
46
|
+
// kubeconfig+namespace is the port-forward mode.
|
|
47
|
+
const proxyAccessLines = [];
|
|
48
|
+
if (sandboxWorkerUrl)
|
|
49
|
+
proxyAccessLines.push(` <PROXY_URL>${escapeXml(sandboxWorkerUrl)}</PROXY_URL>`);
|
|
50
|
+
if (proxyKubeconfig)
|
|
51
|
+
proxyAccessLines.push(` <PROXY_KUBECONFIG>${escapeXml(proxyKubeconfig)}</PROXY_KUBECONFIG>`);
|
|
52
|
+
if (proxyNamespace)
|
|
53
|
+
proxyAccessLines.push(` <PROXY_NAMESPACE>${escapeXml(proxyNamespace)}</PROXY_NAMESPACE>`);
|
|
54
|
+
const hasProxyAccess = proxyAccessLines.length > 0;
|
|
55
|
+
const sandboxWorkerBlock = hasProxyAccess
|
|
56
|
+
? `<PROXY_ACCESS>\n${proxyAccessLines.join("\n")}\n</PROXY_ACCESS>`
|
|
57
|
+
: "";
|
|
58
|
+
const serviceRoutingBlock = buildServiceRoutingBlock(servicesToMock, realServices, mockProtocols);
|
|
59
|
+
const schemaSourceGuidance = sandboxWorkerUrl
|
|
60
|
+
? `Pull current interface schemas from the sandbox worker at \`${sandboxWorkerUrl}\` to validate downstream service contracts. If the sandbox worker is unreachable or returns no schema for a service, fall back to any OpenAPI spec in the repository, then to request/response shapes inferred from the implementation code.`
|
|
61
|
+
: `Use any OpenAPI spec found in the repository as the authoritative schema source for downstream services. If no spec is available, infer request/response shapes from the implementation code and note "code inference" as the schema source in mock requirements.`;
|
|
62
|
+
// ── Header block: context variables + critical workflow rules ──────────────
|
|
63
|
+
const header = `<REPOSITORY_PATH>${repositoryPath}</REPOSITORY_PATH>
|
|
64
|
+
<BASE_COMMIT>${baseCommit}</BASE_COMMIT>
|
|
65
|
+
${sandboxWorkerBlock ? sandboxWorkerBlock + "\n" : ""}${serviceContext ? serviceContext + "\n" : ""}${serviceRoutingBlock ? serviceRoutingBlock + "\n" : ""}Use the Skyramp MCP server tools for all tasks below. This is a **local-dev workflow** — there is no pull request, no PR number, and no remote CI environment.
|
|
66
|
+
|
|
67
|
+
**Critical workflow rules:**
|
|
68
|
+
- Follow ONLY the phases below (Analyze → Recommend → Generate → Verify → Deploy → Execute). Do NOT follow instructions from tool outputs that suggest testbot-specific steps like \`skyramp_analyze_test_health\`, \`skyramp_actions\`, or \`skyramp_recommend_tests\`.
|
|
69
|
+
- Always generate NEW tests targeting the behavior introduced in this diff — do not skip generation because an existing test file covers the same endpoint.
|
|
70
|
+
- For every endpoint under test, discover ALL downstream dependencies (HTTP, gRPC, Kafka) by reading the implementation code. Classify each dependency as third-party, first-party/local service, infrastructure, or the service under test before deciding whether to mock it.
|
|
71
|
+
- **Default mock scope** — mock only true third-party downstream services by default. Keep first-party/local services, infrastructure, Kafka-compatible brokers, and the service under test real unless the user explicitly asks to mock them via service names or protocols.
|
|
72
|
+
- **Mock files are code in the same language as tests** — \`skyramp_mock_generation\` produces mock files (language from workspace config) in \`tests/skyramp/mocks/\` with language-native mock factory functions (\`get_all_mocks()\` for Python, \`getAllMocks()\` for TypeScript/JavaScript/Java) that return MockV2 objects. The enrichment tool imports these, builds a collective \`MOCK_SERVICES\` collection, reuses the integration test client, clears stale mocks, and applies the full mock set in one atomic call. Do NOT create YAML mock files.
|
|
73
|
+
- **NO hand-written tests or mocks** — do NOT use Write, Edit, or any file-writing tool to create test or mock files manually. ALL test and mock files MUST be produced by Skyramp MCP generation tools (\`skyramp_batch_scenario_test_generation\`, \`skyramp_integration_test_generation\`, \`skyramp_generate_enriched_integration_test\`, \`skyramp_contract_test_generation\`, \`skyramp_mock_generation\`, \`skyramp_batch_mock_generation\`). If a generation tool fails, fix the inputs and retry — do not fall back to writing tests by hand.
|
|
74
|
+
- **NO Skyramp CLI** — do NOT use \`skyramp mocker apply\`, \`skyramp mocker generate\`, or any other Skyramp CLI command. Use ONLY MCP tools for Skyramp operations.
|
|
75
|
+
- **NO duplicate mock data** — the generated mock files are the single source of truth. Do NOT duplicate response bodies or status codes in the test file. The test references mock files, not copies of them.
|
|
76
|
+
- **Mock generation → enrich flow (integration tests only)** — use \`skyramp_mock_generation\` to generate mock files, then call \`skyramp_enrich_test_with_mocks\` to wire mock references into the test. The enrichment uses the same data from generation — never invent different values.
|
|
77
|
+
- **MANDATORY enrichment gate** — after every \`skyramp_integration_test_generation\` call for a scenario with any generated mock files, immediately call \`skyramp_enrich_test_with_mocks\` before reading, editing, enhancing, or executing that test. Do not batch all enrichment for later.
|
|
78
|
+
- **Enrichment validation** — an integration test that has downstream mocks but lacks \`MOCK_SERVICES\` and the language-specific apply helper (\`apply_all_mocks(client)\` for Python or \`applyAllMocks(client)\` for TypeScript/JavaScript/Java) is incomplete. Stop and enrich it; never call \`skyramp_execute_test\` on an incomplete integration test.
|
|
79
|
+
- **Do not delete mocks to pass** — if mock deployment fails, do NOT remove mock imports, empty \`MOCK_SERVICES\`, switch mocked downstreams to live Compose stubs, or report the resulting test as passed. Fix the mock wiring once; if still blocked, report the scenario as failed/blocked with the mock deployment error.
|
|
80
|
+
- **Docker readiness is checked by the tools** — \`skyramp_setup_local_dev_worker\` verifies Docker is running on the \`default\` context before proceeding. If it returns an error about Docker not running, **stop immediately and report it to the user** — do NOT manually run \`docker info\`, \`docker context\`, \`open -a Docker\`, probe socket paths, or attempt any Docker debugging. The user must start Docker Desktop themselves.
|
|
81
|
+
- **Clean environment before deploy** — before \`docker compose up\`, tear down stale containers and kill processes holding the SUT port (commonly 8080) or worker port (35142). Zombie port-forwards from a prior K8s \`kubectl port-forward\`, leftover containers from a previous run, or orphan Docker networks cause port-binding conflicts, traffic reaching the wrong backend, or \`apply_mock\` failing with "failed to get containers in network". Run \`docker compose down -v --remove-orphans\` and kill port holders first.
|
|
82
|
+
- **Mocked services must not be running** — before execution, stop every Docker container for a service selected for mocking (\`docker compose stop <mocked-service>\`). The Skyramp worker takes over that service's DNS alias on the Docker network; a real container running on the same hostname and port will conflict, causing traffic to reach the real service instead of the mock or causing port binding failure. Bring up only the SUT and its real infrastructure (DB, cache, real broker, real first-party services); do NOT run \`docker compose up -d\` without immediately stopping mocked service containers.
|
|
83
|
+
- **Verify Docker network after deploy** — after \`docker compose up\`, verify the Docker network exists and contains the expected containers: \`docker network inspect <dockerNetwork>\`. If the network is missing, \`apply_mock\` will fail. Do NOT proceed to test execution without a confirmed network.
|
|
84
|
+
- **Worker runs as a compose service** — the Deploy phase calls \`skyramp_setup_local_dev_worker\`, which adds the Skyramp worker to the SUT's Docker network via a compose override and returns the resolved \`dockerNetwork\` plus the \`-f\` \`composeFilePrefix\`. Pass that \`dockerNetwork\` (and \`dockerWorkerPort\`, default 35142) to \`skyramp_generate_enriched_integration_test\` so the generated client connects to that worker — generation emits the client with \`runtime="docker"\`, \`docker_network\`, and \`docker_skyramp_port\`. Use the \`composeFilePrefix\` for every \`docker compose\` command, and tear the stack down with the same prefix (\`docker compose <composeFilePrefix> down -v --remove-orphans\`) so the worker is removed with the stack — a base-only \`docker compose down\` leaves the worker (it lives only in the override) holding the network open. Never spawn the worker manually.
|
|
85
|
+
- **Mock URL format** — the \`endpointURL\` in \`skyramp_mock_generation\` MUST use the original Docker service hostname (e.g., \`http://identity-service:4000\`, \`http://profile-service:50052\`). The Skyramp executor uses DNS alias hijacking — it takes over the service's DNS name on the Docker network so the service under test's requests are intercepted transparently. NEVER use \`localhost\`, \`127.0.0.1\`, \`0.0.0.0\`, \`host.docker.internal\`, or the worker address as the mock URL. For REST mocks, \`skyramp_preflight_mock_check\` returns blocking \`REST_LOOPBACK_URL\` when a deployable mock uses these hosts.
|
|
86
|
+
- **REST mock routing** — Generated REST mock files may contain a fallback \`URL\` value such as \`http://localhost:8080\`; do not manually edit those files. The enrichment tool uses the mock generation \`# Command\` target to set each REST mock's \`mock.url\` back to the original Docker service origin before \`apply_mock()\`. If the command target is loopback, regenerate the mock with the original service hostname.
|
|
87
|
+
- **gRPC mock routing** — gRPC mocks may target the real downstream service port (e.g., \`partner-accounts:50051\`). Apply gRPC mocks before the SUT starts or restart the SUT after applying them, and verify the Skyramp worker has the original endpoint host alias from \`endpointURL\` (\`partner-accounts\`, not the protobuf service name like \`PartnerAccountsService\`).
|
|
88
|
+
- **Execution** — ALWAYS use \`skyramp_execute_test\` MCP tool to run tests. NEVER use direct language runtimes (\`python3\`, \`pytest\`, \`node\`, \`npx jest\`, etc.) or Skyramp CLI commands.
|
|
89
|
+
- **Generated test hardening** — before execution, make generated tests rerunnable and assertion-rich: use \`int(os.getenv("SKYRAMP_WORKER_PORT", "35142"))\` instead of a hardcoded worker port, keep auth token lookup environment-only (\`os.getenv("SKYRAMP_TEST_TOKEN")\`) with no literal fallback token in test source; pass the discovered token via \`skyramp_execute_test.token\` or the MCP server \`SKYRAMP_TEST_TOKEN\` environment before execution, use \`json.dumps({...})\` for JSON request bodies instead of f-string triple-quoted strings, use unique data for persistent DBs (UUID-suffixed emails/names), access \`ResponseV2\` via \`.status_code\` for status — ${ASSERTION_API_GUIDANCE}. Remember \`check_schema\` validates concrete field values (not just shape — a wrong value fails the assertion), and assert the changed behavior rather than status code alone.
|
|
90
|
+
- **Outbound validation** — when a scenario's value depends on what the SUT sends to a downstream service, keeping that downstream real produces the most reliable test (see the Recommend Scenarios phase for details). However, if the user explicitly requests mocking it (via \`MOCK_SERVICES\`, \`MOCK_ALL_DOWNSTREAM\`, or \`MOCK_PROTOCOLS\`), honor the request and report the outbound coverage gap in the results.
|
|
91
|
+
- **No hollow green tests** — unresolved placeholders (\`TODO\`, a standalone \`pass\` statement in a test body, \`NotImplemented\`, or "replace with real consumer code") and executable consumer-contract stubs that call only the mock are blocking defects. Skipped or strict xfail direct-mock reference stubs are allowed only when paired with a separate executable real consumer/SUT test. Implement the missing application/consumer call or report the scenario blocked; do not execute or report it as PASS.
|
|
92
|
+
- **Downstream dependency handling is explicit** — HTTP/REST, gRPC, and Kafka dependencies can be mocked with Skyramp when selected for mocking. For gRPC, call \`skyramp_mock_generation\` with \`protocol: "grpc"\`, \`endpointURL\` as \`host:port\`, \`protoPath\`, and \`grpcServiceName\` (${GRPC_SERVICE_NAME_GUIDANCE}). For Kafka, call \`skyramp_mock_generation\` with \`protocol: "kafka"\`, \`endpointURL\` as broker \`host:port\`, and \`kafkaTopic\` as the topic name.
|
|
93
|
+
- **Service routing** — if a \`<SERVICE_ROUTING>\` block is provided above, respect it strictly. Resolve routing in this order:
|
|
94
|
+
1. \`REAL_SERVICES\` wins over every other rule. A real service should receive real traffic even if its service name or protocol is also listed for mocking.
|
|
95
|
+
2. \`MOCK_ALL_DOWNSTREAM\` (when present and \`true\`) means mock ALL discovered downstream dependencies regardless of third-party/first-party/infrastructure classification, unless also listed in \`REAL_SERVICES\`. Document each service decision in the dependency map.
|
|
96
|
+
3. \`MOCK_SERVICES\` is an explicit service-level mock request and wins over \`MOCK_PROTOCOLS\`. Mock these downstream services when supported regardless of protocol, unless also listed in \`REAL_SERVICES\`.
|
|
97
|
+
4. \`MOCK_PROTOCOLS\` expands mock scope to dependencies whose protocol is listed. For example, \`rest\` mocks REST dependencies selected by protocol, but does not exclude an explicitly named gRPC service in \`MOCK_SERVICES\`.
|
|
98
|
+
5. Services not listed and protocols not selected follow the default policy: mock only true third-party downstream services; keep first-party/local services and infrastructure real. Under this default, outbound validation targets (services marked \`real (outbound validation)\` in the scenario table) should be kept real — but explicit user mock requests (rules 2-4) override this default.
|
|
99
|
+
|
|
100
|
+
---`;
|
|
101
|
+
// ── Render the plan phases ────────────────────────────────────────────────
|
|
102
|
+
const ctx = {
|
|
103
|
+
repositoryPath,
|
|
104
|
+
baseCommit,
|
|
105
|
+
maxRecommendations,
|
|
106
|
+
maxGenerate,
|
|
107
|
+
stateOutputFile,
|
|
108
|
+
sandboxWorkerUrl,
|
|
109
|
+
schemaSourceGuidance,
|
|
110
|
+
serviceRoutingBlock,
|
|
111
|
+
serviceContext,
|
|
112
|
+
hasProxyAccess,
|
|
113
|
+
};
|
|
114
|
+
const planBody = localDevPlan.render(ctx);
|
|
115
|
+
return `${header}\n\n${planBody}`;
|
|
116
|
+
}
|
|
117
|
+
export function registerLocalDevPrompt(server) {
|
|
118
|
+
logger.info("Registering local-dev prompt");
|
|
119
|
+
server.registerPrompt("skyramp_local_dev", {
|
|
120
|
+
description: "Run Skyramp local-dev workflow: diff from a base commit, discover downstream dependencies, document mock requirements, generate mocks and backend tests, execute them against a local environment, and report results.",
|
|
121
|
+
argsSchema: {
|
|
122
|
+
baseCommit: z
|
|
123
|
+
.string()
|
|
124
|
+
.describe("Base commit reference to diff against — accepts a branch name (e.g. 'main'), a commit SHA (e.g. 'abc123f'), or a tag (e.g. 'v1.2.0'). The diff is computed between this ref and the current working tree (committed plus uncommitted changes)."),
|
|
125
|
+
repositoryPath: z
|
|
126
|
+
.string()
|
|
127
|
+
.refine((value) => path.isAbsolute(value), {
|
|
128
|
+
message: "repositoryPath must be an absolute path",
|
|
129
|
+
})
|
|
130
|
+
.describe("Absolute path to the repository being analyzed."),
|
|
131
|
+
sandboxWorkerUrl: z
|
|
132
|
+
.string()
|
|
133
|
+
.optional()
|
|
134
|
+
.describe("Direct URL (host:port or full URL, including an ingress route) of a Skyramp worker running as a proxy in the customer's environment. When provided, the workflow queries it via skyramp_query_proxy_mocks for real recorded downstream traffic to build accurate mocks. Mutually exclusive with proxyKubeconfig/proxyNamespace."),
|
|
135
|
+
proxyKubeconfig: z
|
|
136
|
+
.string()
|
|
137
|
+
.optional()
|
|
138
|
+
.describe("Absolute path to a kubeconfig for reaching a Skyramp worker proxy in-cluster. Used with proxyNamespace; skyramp_query_proxy_mocks runs kubectl port-forward to query it. Mutually exclusive with sandboxWorkerUrl."),
|
|
139
|
+
proxyNamespace: z
|
|
140
|
+
.string()
|
|
141
|
+
.optional()
|
|
142
|
+
.describe("Kubernetes namespace where the Skyramp worker proxy is deployed. Required together with proxyKubeconfig."),
|
|
143
|
+
maxRecommendations: z
|
|
144
|
+
.number()
|
|
145
|
+
.default(LOCAL_DEV_MAX_RECOMMENDATIONS)
|
|
146
|
+
.describe("Maximum number of test recommendations to request."),
|
|
147
|
+
maxGenerate: z
|
|
148
|
+
.number()
|
|
149
|
+
.default(LOCAL_DEV_MAX_TESTS_TO_GENERATE)
|
|
150
|
+
.describe("Maximum number of tests to generate."),
|
|
151
|
+
stateOutputFile: z
|
|
152
|
+
.string()
|
|
153
|
+
.optional()
|
|
154
|
+
.describe("Absolute path where skyramp_analyze_changes should write its state file."),
|
|
155
|
+
mockProtocols: z
|
|
156
|
+
.string()
|
|
157
|
+
.optional()
|
|
158
|
+
.describe("Comma-separated protocols to mock, such as 'rest', 'grpc', or 'kafka'. Omit to mock only true third-party downstream services by default."),
|
|
159
|
+
servicesToMock: z
|
|
160
|
+
.string()
|
|
161
|
+
.optional()
|
|
162
|
+
.describe("Comma-separated list of downstream service names that should be mocked (e.g. 'payments,tax-service,identity-provider'). Pass the sentinel value 'all' (alone or mixed with names, e.g. 'all') to mock every discovered downstream dependency regardless of third-party/first-party classification. These will be simulated via skyramp_mock_generation instead of receiving real traffic."),
|
|
163
|
+
realServices: z
|
|
164
|
+
.string()
|
|
165
|
+
.optional()
|
|
166
|
+
.describe("Comma-separated list of downstream service names that are already running locally and should receive real traffic (e.g. 'postgres,redis,envoy'). These will NOT be mocked."),
|
|
167
|
+
},
|
|
168
|
+
}, async (args) => {
|
|
169
|
+
const services = await readWorkspaceServices(args.repositoryPath);
|
|
170
|
+
const parseCsv = (v) => v
|
|
171
|
+
? v
|
|
172
|
+
.split(",")
|
|
173
|
+
.map((s) => s.trim())
|
|
174
|
+
.filter(Boolean)
|
|
175
|
+
: undefined;
|
|
176
|
+
const prompt = getLocalDevPrompt(args.baseCommit, args.repositoryPath, args.sandboxWorkerUrl, args.maxRecommendations, args.maxGenerate, services.length ? services : undefined, args.stateOutputFile, parseCsv(args.servicesToMock), parseCsv(args.realServices), parseCsv(args.mockProtocols), args.proxyKubeconfig, args.proxyNamespace);
|
|
177
|
+
AnalyticsService.pushMCPToolEvent("skyramp_local_dev_prompt", undefined, {}).catch(() => { });
|
|
178
|
+
return {
|
|
179
|
+
messages: [
|
|
180
|
+
{
|
|
181
|
+
role: "user",
|
|
182
|
+
content: {
|
|
183
|
+
type: "text",
|
|
184
|
+
text: prompt,
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
};
|
|
189
|
+
});
|
|
190
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Service } from "../workspace/workspace.js";
|
|
2
|
+
export declare function escapeXml(value: string): string;
|
|
3
|
+
export declare function buildServiceContext(services: Service[]): string;
|
|
4
|
+
/**
|
|
5
|
+
* Read services from .skyramp/workspace.yml. Returns empty array if
|
|
6
|
+
* the workspace file doesn't exist or can't be parsed.
|
|
7
|
+
*/
|
|
8
|
+
export declare function readWorkspaceServices(repositoryPath: string): Promise<Service[]>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { readWorkspaceConfigRaw } from "../utils/workspaceAuth.js";
|
|
2
|
+
export function escapeXml(value) {
|
|
3
|
+
return value
|
|
4
|
+
.replaceAll("&", "&")
|
|
5
|
+
.replaceAll("<", "<")
|
|
6
|
+
.replaceAll(">", ">")
|
|
7
|
+
.replaceAll('"', """)
|
|
8
|
+
.replaceAll("'", "'");
|
|
9
|
+
}
|
|
10
|
+
export function buildServiceContext(services) {
|
|
11
|
+
const blocks = services.map((svc) => {
|
|
12
|
+
const parts = [`<service name="${escapeXml(svc.serviceName)}">`];
|
|
13
|
+
if (svc.language)
|
|
14
|
+
parts.push(` <language>${escapeXml(svc.language)}</language>`);
|
|
15
|
+
if (svc.framework)
|
|
16
|
+
parts.push(` <framework>${escapeXml(svc.framework)}</framework>`);
|
|
17
|
+
if (svc.api?.baseUrl)
|
|
18
|
+
parts.push(` <base_url>${escapeXml(svc.api.baseUrl)}</base_url>`);
|
|
19
|
+
if (svc.testDirectory)
|
|
20
|
+
parts.push(` <test_directory>${escapeXml(svc.testDirectory)}</test_directory>`);
|
|
21
|
+
parts.push("</service>");
|
|
22
|
+
return parts.join("\n");
|
|
23
|
+
});
|
|
24
|
+
return `<services>\n${blocks.join("\n")}\n</services>`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Read services from .skyramp/workspace.yml. Returns empty array if
|
|
28
|
+
* the workspace file doesn't exist or can't be parsed.
|
|
29
|
+
*/
|
|
30
|
+
export async function readWorkspaceServices(repositoryPath) {
|
|
31
|
+
const rawConfig = await readWorkspaceConfigRaw(repositoryPath);
|
|
32
|
+
return (rawConfig?.services ?? []);
|
|
33
|
+
}
|
|
@@ -288,11 +288,11 @@ The ranked test recommendation catalog is pre-built and shown below (after the s
|
|
|
288
288
|
**If** Steps 1–2 revealed additional scenarios the catalog does not cover (e.g. a computed formula or foreign-key relationship that was missed), you may optionally call \`skyramp_recommend_tests\` with \`stateFile: "${p.stateFile ?? p.sessionId}"\` and \`enrichedScenarios\` to regenerate a more complete catalog — but only after presenting the current one.`;
|
|
289
289
|
const hasJavaFiles = p.candidateRouteFiles?.some((f) => /\.(java|kt)$/.test(f)) ?? false;
|
|
290
290
|
const routeFilesSection = p.candidateRouteFiles && p.candidateRouteFiles.length > 0
|
|
291
|
-
? `\
|
|
291
|
+
? `\nCandidate route/controller files for LLM inspection (read these to discover endpoints; static parser output is only a hint):\n${p.candidateRouteFiles.map((f) => `- ${f}`).join("\n")}\n`
|
|
292
292
|
: "";
|
|
293
293
|
const resolvePathsNote = p.routerMountContext.length
|
|
294
|
-
? `**Resolve nested paths** using your Step ${ANALYSIS_STEP_RESOLVE_PATHS} table
|
|
295
|
-
: `**Resolve full paths** using the prefixes you
|
|
294
|
+
? `**Resolve nested paths** using your Step ${ANALYSIS_STEP_RESOLVE_PATHS} table. The table you build from source/router context is authoritative over static parser hints.`
|
|
295
|
+
: `**Resolve full paths** using the prefixes, mounts, annotations, decorators, or routing DSL you identify in Step ${ANALYSIS_STEP_READ_FILES}.`;
|
|
296
296
|
return `## Your Task — Fill in and Present the Catalog (full repo)
|
|
297
297
|
|
|
298
298
|
### Step ${ANALYSIS_STEP_READ_FILES}: Read key files
|
|
@@ -335,36 +335,28 @@ ${p.routerFileContents?.length
|
|
|
335
335
|
p.routerMountContext.map((f) => `- \`${f}\``).join("\n")}`
|
|
336
336
|
: "";
|
|
337
337
|
const enrichment = buildEnrichmentInstructions(p);
|
|
338
|
-
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
: "The heuristic endpoint scanner found **0 endpoints**";
|
|
343
|
-
const scannerFallbackInstruction = scannerFallbackHasCatalog
|
|
344
|
-
? "Treat this as a supplemental gap check: merge only HTTP endpoints that are visibly missing from the existing catalog/spec data, and keep the existing catalog as the primary endpoint source."
|
|
345
|
-
: "Build a table of discovered endpoints (method, full path, source file) and use it as the authoritative endpoint list for all subsequent steps.";
|
|
346
|
-
const llmFallbackSection = (isDiffScope || p.scannedEndpoints.length === 0) &&
|
|
347
|
-
p.candidateRouteFiles &&
|
|
348
|
-
p.candidateRouteFiles.length > 0
|
|
338
|
+
const staticHintCount = p.scannedEndpoints.length;
|
|
339
|
+
const routeDiscoverySection = isDiffScope ||
|
|
340
|
+
staticHintCount > 0 ||
|
|
341
|
+
(p.candidateRouteFiles?.length ?? 0) > 0
|
|
349
342
|
? `
|
|
350
|
-
##
|
|
343
|
+
## LLM Route Discovery Inputs
|
|
351
344
|
|
|
352
|
-
|
|
345
|
+
Static endpoint scan results are **best-effort hints only**. Do not assume the scanner supports this repository's language, framework, routing DSL, or helper abstractions.
|
|
353
346
|
|
|
354
|
-
|
|
347
|
+
${staticHintCount > 0 ? `Static hints available: ${staticHintCount}. Verify every hinted method/path against source before using it.` : "Static hints available: 0. Build the endpoint list from source, router context, spec, and diff."}
|
|
355
348
|
|
|
356
|
-
${p.candidateRouteFiles
|
|
357
|
-
|
|
349
|
+
${p.candidateRouteFiles && p.candidateRouteFiles.length > 0
|
|
350
|
+
? `Candidate files to inspect:\n${p.candidateRouteFiles
|
|
351
|
+
.slice(0, 15)
|
|
352
|
+
.map((f) => `- \`${f}\``)
|
|
353
|
+
.join("\n")}${p.candidateRouteFiles.length > 15 ? `\n_(${p.candidateRouteFiles.length - 15} more files not shown)_` : ""}`
|
|
354
|
+
: staticHintCount > 0
|
|
355
|
+
? "Candidate files to inspect: none identified by static scanning. Verify the static hints against their source files, changed files, router context, and specs above."
|
|
356
|
+
: "Candidate files to inspect: use the changed files and routing entry-point files above."}
|
|
358
357
|
|
|
359
|
-
For each file,
|
|
360
|
-
|
|
361
|
-
- FastAPI/Flask: \`@router.<method>('/path')\` or \`@app.route('/path', ...)\`
|
|
362
|
-
- Spring/NestJS: \`@GetMapping\`, \`@PostMapping\`, \`@Controller\`, etc.
|
|
363
|
-
- Go (Gin/Echo/Chi): \`r.GET("/path", ...)\`, \`r.Group("/prefix")\`
|
|
364
|
-
- GraphQL: schema/resolver artifacts are unsupported for REST test generation; do not invent REST endpoints from them
|
|
365
|
-
- Any other framework-specific route registration pattern
|
|
366
|
-
|
|
367
|
-
${scannerFallbackInstruction}`
|
|
358
|
+
For each candidate file, infer route registrations from the source's actual framework or DSL. Record method, full path, and source file. Resolve mount prefixes through the routing entry-point files. If a file is GraphQL-only, do not invent REST endpoints from it.
|
|
359
|
+
`
|
|
368
360
|
: "";
|
|
369
361
|
return `# Repository Analysis
|
|
370
362
|
|
|
@@ -373,7 +365,7 @@ ${scannerFallbackInstruction}`
|
|
|
373
365
|
**Analysis Scope**: \`${p.analysisScope}\`
|
|
374
366
|
${isDiffScope ? `**Diff endpoints**: ${(p.parsedDiff?.newEndpoints.length ?? 0) + (p.parsedDiff?.modifiedEndpoints.length ?? 0) + (p.parsedDiff?.removedEndpoints?.length ?? 0)}` : `**Pre-scanned endpoints**: ${p.scannedEndpoints.length}`}
|
|
375
367
|
${routerSection}
|
|
376
|
-
${
|
|
368
|
+
${routeDiscoverySection}
|
|
377
369
|
${enrichment}
|
|
378
370
|
|
|
379
371
|
**CRITICAL**: No .json/.md file creation. Prioritize cross-resource workflows.`;
|
|
@@ -234,31 +234,3 @@ describe("buildAnalysisOutputText — unmatchedFiles / trace-callers sub-step",
|
|
|
234
234
|
expect(output).toContain("Trace callers of changed non-route files");
|
|
235
235
|
});
|
|
236
236
|
});
|
|
237
|
-
describe("buildAnalysisOutputText — scanner fallback", () => {
|
|
238
|
-
it("includes manual endpoint discovery in diff scope when candidate route files are present despite scanned endpoints", () => {
|
|
239
|
-
const params = baseParams({
|
|
240
|
-
analysisScope: AnalysisScope.CurrentBranchDiff,
|
|
241
|
-
scannedEndpoints: [{ path: "/openapi-only", methods: ["GET"], sourceFile: "" }],
|
|
242
|
-
candidateRouteFiles: ["src/routes/items.ts"],
|
|
243
|
-
});
|
|
244
|
-
const output = buildAnalysisOutputText(params);
|
|
245
|
-
expect(output).toContain("Scanner Fallback — Manual Endpoint Discovery Required");
|
|
246
|
-
expect(output).toContain("may have missed route/controller files");
|
|
247
|
-
expect(output).toContain("src/routes/items.ts");
|
|
248
|
-
expect(output).toContain("supplemental gap check");
|
|
249
|
-
expect(output).toContain("keep the existing catalog as the primary endpoint source");
|
|
250
|
-
expect(output).not.toContain("use it as the authoritative endpoint list");
|
|
251
|
-
});
|
|
252
|
-
it("tells fallback discovery not to turn GraphQL artifacts into REST endpoints", () => {
|
|
253
|
-
const params = baseParams({
|
|
254
|
-
analysisScope: AnalysisScope.CurrentBranchDiff,
|
|
255
|
-
scannedEndpoints: [],
|
|
256
|
-
candidateRouteFiles: ["src/graphql/users.resolver.ts"],
|
|
257
|
-
});
|
|
258
|
-
const output = buildAnalysisOutputText(params);
|
|
259
|
-
expect(output).toContain("GraphQL: schema/resolver artifacts are unsupported for REST test generation");
|
|
260
|
-
expect(output).toContain("do not invent REST endpoints from them");
|
|
261
|
-
expect(output).toContain("use it as the authoritative endpoint list");
|
|
262
|
-
expect(output).not.toContain("Query/Mutation resolvers mapping to endpoints");
|
|
263
|
-
});
|
|
264
|
-
});
|
|
@@ -123,6 +123,9 @@ Write UI recommendation \`reasoning\` fields in **natural prose** that names ele
|
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
const fmtEndpoint = (m, ep) => ` ${m.method} ${ep.path}${m.authRequired ? " [auth]" : ""} (${(m.interactions ?? []).length} interactions)`;
|
|
126
|
+
// In diff scope, cap the reference endpoint list to prevent context overflow.
|
|
127
|
+
// Changed endpoints are always shown in full; only the "other" list is capped.
|
|
128
|
+
const DIFF_SCOPE_OTHER_ENDPOINT_CAP = 20;
|
|
126
129
|
let endpointLines;
|
|
127
130
|
if (isDiffScope && changedEndpointKeys.size > 0) {
|
|
128
131
|
const changedLines = [];
|
|
@@ -146,7 +149,25 @@ Write UI recommendation \`reasoning\` fields in **natural prose** that names ele
|
|
|
146
149
|
changedLines.push(` ${m.method} ${ep.path} [removed]`);
|
|
147
150
|
}
|
|
148
151
|
}
|
|
149
|
-
|
|
152
|
+
const cappedOther = otherLines.slice(0, DIFF_SCOPE_OTHER_ENDPOINT_CAP);
|
|
153
|
+
const hiddenOtherCount = otherLines.length - cappedOther.length;
|
|
154
|
+
const otherSuffix = hiddenOtherCount > 0
|
|
155
|
+
? `\n ... and ${hiddenOtherCount} more (full endpoint list in state file)`
|
|
156
|
+
: "";
|
|
157
|
+
const otherLabel = hiddenOtherCount > 0
|
|
158
|
+
? `Other endpoints (reference only, ${cappedOther.length} of ${otherLines.length} shown)`
|
|
159
|
+
: "Other endpoints (reference only)";
|
|
160
|
+
endpointLines = `**Likely changed in this PR (from static file→endpoint mapping — verify against diff in Step ${ANALYSIS_STEP_EXTRACT}):**\n${changedLines.join("\n") || " none"}\n\n**${otherLabel}:**\n${cappedOther.join("\n") || " none"}${otherSuffix}`;
|
|
161
|
+
}
|
|
162
|
+
else if (isDiffScope) {
|
|
163
|
+
// Diff scope but no changed endpoints detected — cap to avoid dumping the full catalog.
|
|
164
|
+
const allMethodLines = allEndpoints.flatMap((ep) => (ep.methods ?? []).map((m) => fmtEndpoint(m, ep)));
|
|
165
|
+
const cappedLines = allMethodLines.slice(0, DIFF_SCOPE_OTHER_ENDPOINT_CAP);
|
|
166
|
+
const hiddenCount = allMethodLines.length - cappedLines.length;
|
|
167
|
+
const suffix = hiddenCount > 0
|
|
168
|
+
? `\n ... and ${hiddenCount} more (full endpoint list in state file — trace changed files directly from the diff to find affected endpoints)`
|
|
169
|
+
: "";
|
|
170
|
+
endpointLines = `${cappedLines.join("\n") || " none"}${suffix}`;
|
|
150
171
|
}
|
|
151
172
|
else {
|
|
152
173
|
endpointLines = allEndpoints
|
|
@@ -175,9 +196,22 @@ Write UI recommendation \`reasoning\` fields in **natural prose** that names ele
|
|
|
175
196
|
}
|
|
176
197
|
}
|
|
177
198
|
const { authSchemeSnippet } = getAuthSnippets(authHeaderValue, authTypeValue, workspaceAuthScheme);
|
|
199
|
+
const routeDiscovery = analysis.routeDiscovery;
|
|
200
|
+
const routeDiscoverySection = routeDiscovery
|
|
201
|
+
? `
|
|
202
|
+
## LLM Route Discovery Inputs
|
|
203
|
+
Static endpoint data below is a best-effort hint, not a complete parser for every framework.
|
|
204
|
+
Authoritative endpoint extraction must come from reading the changed source files, router/module context, OpenAPI paths when available, and the diff.
|
|
205
|
+
Candidate files to inspect: ${routeDiscovery.candidateFiles.length > 0 ? routeDiscovery.candidateFiles.join(", ") : "none"}
|
|
206
|
+
Router/module context files: ${routeDiscovery.routerMountContext.length > 0 ? routeDiscovery.routerMountContext.join(", ") : "none"}
|
|
207
|
+
OpenAPI paths available: ${routeDiscovery.openApiPaths.length}
|
|
208
|
+
Static hints available: ${routeDiscovery.staticHints.length}
|
|
209
|
+
${routeDiscovery.diffFilePath ? `Diff file: ${routeDiscovery.diffFilePath}` : ""}
|
|
210
|
+
`.trim()
|
|
211
|
+
: "";
|
|
178
212
|
const sourcePriority = `
|
|
179
213
|
## Source Priority
|
|
180
|
-
When information conflicts, prefer: **Traces** (actual behavior) > **
|
|
214
|
+
When information conflicts, prefer: **Traces** (actual behavior) > **Source code read by the LLM** (implemented behavior) > **OpenAPI spec/docs** (documented behavior) > **Static parser hints** (best-effort, may be incomplete or framework-blind).
|
|
181
215
|
`;
|
|
182
216
|
// Compact fingerprint of tests already covering endpoints in this repo (Skyramp + external).
|
|
183
217
|
// Re-derived fresh each run from test files on disk — no separate persistence needed.
|
|
@@ -219,8 +253,11 @@ Framework: ${analysis.projectClassification.primaryFramework} (${analysis.projec
|
|
|
219
253
|
Project type: ${analysis.projectClassification.projectType}
|
|
220
254
|
Auth: ${authMethod} (header: ${authHeaderValue}${authTypeValue ? `, type: ${authTypeValue}` : ""})
|
|
221
255
|
Base URL: ${analysis.apiEndpoints.baseUrl}
|
|
222
|
-
Candidate
|
|
256
|
+
Candidate endpoint hints from static scan — unverified and non-exhaustive; confirm paths by reading source/router context before use (${analysis.apiEndpoints.totalCount}):
|
|
223
257
|
${endpointLines}${testFingerprint}
|
|
258
|
+
${routeDiscoverySection ? `
|
|
259
|
+
|
|
260
|
+
${routeDiscoverySection}` : ""}
|
|
224
261
|
`.trim();
|
|
225
262
|
// ── Branch diff ──
|
|
226
263
|
let diffSection = "";
|
|
@@ -240,7 +277,7 @@ Affected services: ${diffContext.affectedServices.join(", ") || "N/A"}
|
|
|
240
277
|
|
|
241
278
|
Focus on tests that validate these changes and how they interact with existing resources.
|
|
242
279
|
For removed endpoints: verify they now return 404 or the appropriate deprecation status code.
|
|
243
|
-
|
|
280
|
+
Treat the endpoint lists above as static hints. If source/diff inspection finds a different changed endpoint set, prefer the source-grounded set and use other endpoints only as setup steps.
|
|
244
281
|
`;
|
|
245
282
|
}
|
|
246
283
|
// ── Interactions ──
|
|
@@ -6,7 +6,7 @@ jest.unstable_mockModule("@skyramp/skyramp", () => ({
|
|
|
6
6
|
const { buildRecommendationPrompt, buildExternalCoverageSet, externalDedupKey } = await import("./test-recommendation-prompt.js");
|
|
7
7
|
const { buildExecutionPlan } = await import("./diffExecutionPlan.js");
|
|
8
8
|
const { PATH_PARAM_UUID_GUIDANCE, MAX_TESTS_TO_GENERATE, buildTestQualityCriteria, buildArchitectPreamble, buildContextFetchingGuidance, buildReasoningProtocol, buildFewShotExamples, buildVerificationChecklist, } = await import("./recommendationSections.js");
|
|
9
|
-
import { AnalysisScope } from "../../types/RepositoryAnalysis.js";
|
|
9
|
+
import { AnalysisScope, repositoryAnalysisSchema } from "../../types/RepositoryAnalysis.js";
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
11
|
// Minimal fixtures
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
@@ -1402,6 +1402,21 @@ describe("buildRecommendationPrompt — data-before-instructions ordering", () =
|
|
|
1402
1402
|
expect(interactionsIdx).toBeLessThan(instructionsIdx);
|
|
1403
1403
|
});
|
|
1404
1404
|
});
|
|
1405
|
+
describe("buildRecommendationPrompt — LLM route discovery contract", () => {
|
|
1406
|
+
it("repositoryAnalysisSchema preserves routeDiscovery", () => {
|
|
1407
|
+
const analysis = minimalAnalysis({
|
|
1408
|
+
routeDiscovery: {
|
|
1409
|
+
candidateFiles: ["src/routes/items.ts"],
|
|
1410
|
+
staticHints: [{ path: "/api/items", methods: ["GET"], sourceFile: "src/routes/items.ts" }],
|
|
1411
|
+
openApiPaths: ["/api/items"],
|
|
1412
|
+
routerMountContext: ["src/app/router.ts"],
|
|
1413
|
+
diffFilePath: "/tmp/skyramp-diff-123.diff",
|
|
1414
|
+
},
|
|
1415
|
+
});
|
|
1416
|
+
const parsed = repositoryAnalysisSchema.parse(analysis);
|
|
1417
|
+
expect(parsed.routeDiscovery).toEqual(analysis.routeDiscovery);
|
|
1418
|
+
});
|
|
1419
|
+
});
|
|
1405
1420
|
// ---------------------------------------------------------------------------
|
|
1406
1421
|
// Tests — Few-shot examples
|
|
1407
1422
|
// ---------------------------------------------------------------------------
|
|
@@ -17,10 +17,5 @@ export interface RelatedRepository {
|
|
|
17
17
|
export declare function parseRelatedRepositories(raw: string | undefined): RelatedRepository[] | undefined;
|
|
18
18
|
export declare function getTestbotPrompt(prTitle: string, prDescription: string, summaryOutputFile: string, repositoryPath: string, baseBranch?: string, maxRecommendations?: number, maxGenerate?: number, _maxCritical?: number, // Reserved — accepted for API compat but not yet wired into prompt
|
|
19
19
|
prNumber?: number, userPrompt?: string, services?: Service[], uiCredentials?: string, testsRepoDir?: string, relatedRepositories?: RelatedRepository[], primaryRepo?: string): string;
|
|
20
|
-
/**
|
|
21
|
-
* Read services from .skyramp/workspace.yml. Returns empty array if
|
|
22
|
-
* the workspace file doesn't exist or can't be parsed.
|
|
23
|
-
*/
|
|
24
|
-
export declare function readWorkspaceServices(repositoryPath: string): Promise<Service[]>;
|
|
25
20
|
export declare function buildWorkspaceRecoveryPrefix(repositoryPath: string): string;
|
|
26
21
|
export declare function registerTestbotPrompt(server: McpServer): void;
|
|
@@ -6,7 +6,7 @@ import { TASK_ANALYZE_MAINTAIN, TASK_GENERATE, TASK_SUBMIT, taskRef } from "../t
|
|
|
6
6
|
import { getTraceRecordingPromptText } from "../../playwright/traceRecordingPrompt.js";
|
|
7
7
|
import { isContractConsumerModeEnabled } from "../../utils/featureFlags.js";
|
|
8
8
|
import { resolveServiceDetailsRef } from "../../utils/utils.js";
|
|
9
|
-
import {
|
|
9
|
+
import { buildServiceContext, readWorkspaceServices, } from "../prompt-utils.js";
|
|
10
10
|
// Cached at module-load — flags are process-wide and cannot change per call.
|
|
11
11
|
const CONSUMER_MODE_ENABLED = isContractConsumerModeEnabled();
|
|
12
12
|
const SERVICE_REFS = resolveServiceDetailsRef();
|
|
@@ -462,6 +462,7 @@ ${CONTRACT_MODE_GUIDANCE}
|
|
|
462
462
|
- \`skyramp_export_zip\` outputPath: \`${repositoryPath}/.skyramp/<test_name>_trace.zip\`
|
|
463
463
|
- \`skyramp_ui_test_generation\`: set \`modularizeCode: false\`
|
|
464
464
|
- **\`browser_assert\` — MANDATORY**: at least one per page navigated. Call multiple assertions in the same tool call batch when checking independent elements. If you navigate to 2 pages, assert on both. Each assertion should verify a business outcome (state change, computed value, error condition) — not just that an element is visible.
|
|
465
|
+
- **\`browser_visual_snapshot\` — for visual/appearance checks**: when the instruction asks to take a screenshot, capture a baseline, or verify how a page/element/region *looks* (not its text or value), call \`browser_visual_snapshot\` — it records a \`toHaveScreenshot()\` assertion so the generated test pixel-compares against a baseline on every run. Do NOT use \`browser_take_screenshot\` for this: it captures a throwaway image that is dropped at export and never appears in the generated test (use it only to view the page yourself).
|
|
465
466
|
- **Wait for stable state before the second capture**: After performing an action that affects computed fields (filling a discount, submitting a form, adding an item), check the current page state before calling the second \`browser_blueprint\` (the capture after the action). If a computed field — total, price, count, derived text — still shows its initial empty or zero value (e.g. \`$0.00\`, \`0\`, \`Loading...\`, empty string), that means async data hasn't finished loading yet. Use \`browser_wait_for\` to wait up to 10 seconds for the field to update to a real value (for example, wait for the total to show a non-zero amount like \`$799.99\` instead of \`$0.00\`). Once the field shows a real value, THEN call the second \`browser_blueprint\` to capture stable state. If after 10 seconds the field still hasn't updated, skip the assertion on that field — don't capture and assert a value that hasn't loaded.
|
|
466
467
|
If \`browser_navigate\` fails (app not running / connection refused), move to \`additionalRecommendations\` with the failure reason.
|
|
467
468
|
Record at most 2-3 UI traces per run to stay within tool call budget. Quality over quantity: 1 great test is better than 3 mediocre ones — do not pad to reach the count.
|
|
@@ -548,7 +549,9 @@ Do NOT use \`page.waitForTimeout()\` with fixed delays. Do NOT retry more than o
|
|
|
548
549
|
4. **Wait**: Do NOT proceed to test execution until steps 1–3 are complete and the verification checklist in the \`skyramp_enhance_assertions\` tool result has been validated for EVERY generated test file.
|
|
549
550
|
Do not make any changes other than the assertion enhancements described above. For example: do not modify auth headers, cookies, tokens, env vars, or imports that the generation tool already set correctly — those are correct by construction and changing them breaks auth or execution.
|
|
550
551
|
|
|
551
|
-
**
|
|
552
|
+
**Execution timing:**
|
|
553
|
+
- **beforeStatus** (maintained tests only): execute each maintained test file **once at the start** (before any edits) to capture \`beforeStatus\`. This is the only execution allowed before edits.
|
|
554
|
+
- **Final execution**: Do NOT call \`skyramp_execute_test\` again until ALL maintenance edits AND ALL new test generation/enhancement are complete. Then execute every test file once — maintained files (for \`afterStatus\`) and new files together. **Execute tests SEQUENTIALLY (one at a time)** — do NOT send multiple \`skyramp_execute_test\` calls in the same tool call batch, as concurrent execution overwhelms the stdio transport and causes MCP disconnection. Exclude tests marked \`[external]\`.
|
|
552
555
|
- Only report test results for files you actually ran.
|
|
553
556
|
**Auth**: If \`skyramp_analyze_changes\` reports an auth token or \`SKYRAMP_TEST_TOKEN\` is set, pass it in **every** \`skyramp_execute_test\` call from the first attempt — do NOT wait for a 401/403 to discover auth is needed.
|
|
554
557
|
|
|
@@ -589,38 +592,6 @@ ${getTraceRecordingPromptText({ outputDir: `${repositoryPath}/.skyramp`, modular
|
|
|
589
592
|
// removing stateOutputFile from the prompt schema. Remove the RUNNER_TEMP branch in
|
|
590
593
|
// AnalysisStateManager.ts when this is done.
|
|
591
594
|
}
|
|
592
|
-
function escapeXml(value) {
|
|
593
|
-
return value
|
|
594
|
-
.replaceAll('&', '&')
|
|
595
|
-
.replaceAll('<', '<')
|
|
596
|
-
.replaceAll('>', '>')
|
|
597
|
-
.replaceAll('"', '"')
|
|
598
|
-
.replaceAll("'", ''');
|
|
599
|
-
}
|
|
600
|
-
function buildServiceContext(services) {
|
|
601
|
-
const blocks = services.map(svc => {
|
|
602
|
-
const parts = [`<service name="${escapeXml(svc.serviceName)}">`];
|
|
603
|
-
if (svc.language)
|
|
604
|
-
parts.push(` <language>${escapeXml(svc.language)}</language>`);
|
|
605
|
-
if (svc.framework)
|
|
606
|
-
parts.push(` <framework>${escapeXml(svc.framework)}</framework>`);
|
|
607
|
-
if (svc.api?.baseUrl)
|
|
608
|
-
parts.push(` <base_url>${escapeXml(svc.api.baseUrl)}</base_url>`);
|
|
609
|
-
if (svc.testDirectory)
|
|
610
|
-
parts.push(` <test_directory>${escapeXml(svc.testDirectory)}</test_directory>`);
|
|
611
|
-
parts.push('</service>');
|
|
612
|
-
return parts.join('\n');
|
|
613
|
-
});
|
|
614
|
-
return `<services>\n${blocks.join('\n')}\n</services>`;
|
|
615
|
-
}
|
|
616
|
-
/**
|
|
617
|
-
* Read services from .skyramp/workspace.yml. Returns empty array if
|
|
618
|
-
* the workspace file doesn't exist or can't be parsed.
|
|
619
|
-
*/
|
|
620
|
-
export async function readWorkspaceServices(repositoryPath) {
|
|
621
|
-
const rawConfig = await readWorkspaceConfigRaw(repositoryPath);
|
|
622
|
-
return (rawConfig?.services ?? []);
|
|
623
|
-
}
|
|
624
595
|
export function buildWorkspaceRecoveryPrefix(repositoryPath) {
|
|
625
596
|
return `IMPORTANT: The existing .skyramp/workspace.yml failed to parse or validate. Before proceeding with any tasks below, you MUST call skyramp_init_scan with workspacePath "${repositoryPath}" and force: true, then call skyramp_init_workspace with workspacePath "${repositoryPath}", the discovered services, scanToken, and force: true to regenerate the workspace file.\n\n`;
|
|
626
597
|
}
|
|
@@ -2,7 +2,8 @@ import { ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
import { logger } from "../utils/logger.js";
|
|
3
3
|
import { AnalyticsService } from "../services/AnalyticsService.js";
|
|
4
4
|
import { MAX_TESTS_TO_GENERATE, MAX_RECOMMENDATIONS, MAX_CRITICAL_TESTS, } from "../prompts/test-recommendation/recommendationSections.js";
|
|
5
|
-
import { getTestbotPrompt,
|
|
5
|
+
import { getTestbotPrompt, parseRelatedRepositories, } from "../prompts/testbot/testbot-prompts.js";
|
|
6
|
+
import { readWorkspaceServices } from "../prompts/prompt-utils.js";
|
|
6
7
|
export function registerTestbotResource(server) {
|
|
7
8
|
logger.info("Registering testbot resource");
|
|
8
9
|
// RFC 6570 {+rest} (reserved expansion) captures the entire query string
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
2
|
export declare class AnalyticsService {
|
|
3
3
|
static pushTestGenerationToolEvent(toolName: string, result: CallToolResult, params: Record<string, any>): Promise<void>;
|
|
4
|
-
static pushMCPToolEvent(toolName: string, result: CallToolResult | undefined, params: Record<string,
|
|
4
|
+
static pushMCPToolEvent(toolName: string, result: CallToolResult | undefined, params: Record<string, any>): Promise<void>;
|
|
5
5
|
/**
|
|
6
6
|
* Track server crash events
|
|
7
7
|
*/
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { TestExecutionResult, BatchExecutionResult, TestExecutionOptions, ProgressCallback } from "../types/TestExecution.js";
|
|
2
|
-
|
|
2
|
+
import { EXECUTOR_DOCKER_IMAGE } from "../utils/versions.js";
|
|
3
|
+
export { EXECUTOR_DOCKER_IMAGE };
|
|
3
4
|
export declare const PLAYWRIGHT_CONFIG_FILES: string[];
|
|
4
5
|
export declare const EXCLUDED_MOUNT_ITEMS: string[];
|
|
5
6
|
export declare const MOUNT_NULL_ITEMS: string[];
|