@skyramp/mcp 0.2.1-rc.1 → 0.2.150-rc.sut

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.
Files changed (24) hide show
  1. package/build/index.js +7 -2
  2. package/build/playwright/registerPlaywrightTools.js +1 -0
  3. package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +82 -0
  4. package/build/prompts/sut-setup/registerSetupSutPrompt.js +49 -0
  5. package/build/prompts/sut-setup/shared.js +80 -0
  6. package/build/prompts/test-maintenance/drift-analysis-prompt.js +98 -87
  7. package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +92 -60
  8. package/build/prompts/test-maintenance/driftAnalysisSections.js +139 -197
  9. package/build/prompts/testbot/testbot-prompts.js +4 -7
  10. package/build/prompts/testbot/testbot-prompts.test.js +17 -22
  11. package/build/resources/sutSetupResource.js +47 -0
  12. package/build/services/TestDiscoveryService.js +39 -9
  13. package/build/tools/test-management/actionsTool.js +166 -148
  14. package/build/tools/test-management/analyzeChangesTool.js +2 -10
  15. package/build/tools/test-management/analyzeTestHealthTool.js +10 -22
  16. package/node_modules/playwright/lib/mcp/skyramp/assertApiRequestTool.js +46 -0
  17. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +298 -51
  18. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +5 -0
  19. package/package.json +1 -1
  20. package/node_modules/playwright/lib/mcp/browser/tools/domAnalyzer.js +0 -261
  21. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.3.tgz +0 -0
  22. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.4.tgz +0 -0
  23. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.5.tgz +0 -0
  24. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.6.tgz +0 -0
package/build/index.js CHANGED
@@ -26,6 +26,8 @@ import { registerBatchScenarioTestTool } from "./tools/generate-tests/generateBa
26
26
  import { registerMockTool } from "./tools/generate-tests/generateMockRestTool.js";
27
27
  import { registerAnalyzeChangesTool, registerUiAnalyzeChangesTool, registerAnalyzeTestHealthTool, registerActionsTool, } from "./tools/test-management/index.js";
28
28
  import { registerTestbotPrompt } from "./prompts/testbot/testbot-prompts.js";
29
+ import { registerSetupSutPrompt } from "./prompts/sut-setup/registerSetupSutPrompt.js";
30
+ import { registerSutSetupResource } from "./resources/sutSetupResource.js";
29
31
  import { registerTestbotResource } from "./resources/testbotResource.js";
30
32
  import { registerSubmitReportTool } from "./tools/submitReportTool.js";
31
33
  import { registerInitializeWorkspaceTool } from "./tools/workspace/initializeWorkspaceTool.js";
@@ -123,7 +125,10 @@ const prompts = [
123
125
  if (isTestbotEnabled()) {
124
126
  prompts.push(registerTestbotPrompt);
125
127
  registerTestbotResource(server);
126
- logger.info("TestBot prompt enabled via SKYRAMP_FEATURE_TESTBOT");
128
+ logger.info("Testbot prompt enabled via SKYRAMP_FEATURE_TESTBOT");
129
+ prompts.push(registerSetupSutPrompt);
130
+ registerSutSetupResource(server);
131
+ logger.info("Testbot SUT setup prompt and resource enabled via SKYRAMP_FEATURE_TESTBOT");
127
132
  }
128
133
  prompts.forEach((registerPrompt) => registerPrompt(server));
129
134
  logger.info("All prompts registered successfully");
@@ -174,7 +179,7 @@ const infrastructureTools = [
174
179
  ];
175
180
  if (isTestbotEnabled()) {
176
181
  infrastructureTools.push(registerSubmitReportTool);
177
- logger.info("TestBot tools enabled via SKYRAMP_FEATURE_TESTBOT");
182
+ logger.info("Testbot tools enabled via SKYRAMP_FEATURE_TESTBOT");
178
183
  }
179
184
  infrastructureTools.forEach((registerTool) => registerTool(server));
180
185
  // Register Playwright browser tools (trace recording via browser automation)
@@ -45,6 +45,7 @@ export async function registerPlaywrightTools(server, options) {
45
45
  'browser_wait_for',
46
46
  'browser_take_screenshot',
47
47
  'browser_assert',
48
+ 'browser_assert_api_request',
48
49
  'skyramp_export_zip',
49
50
  // DOM Analyzer tools (Phase C)
50
51
  'browser_blueprint',
@@ -0,0 +1,82 @@
1
+ import { TESTBOT_WORKFLOW_PATH, buildContextBlock, buildCommonSutErrorsSection, buildLocalValidationSection, buildTestbotLifecycleInputsSection, getPersonaPrefix, } from "../shared.js";
2
+ export function getAdaptWorkflowPrompt(args) {
3
+ const sourceLine = args.sutSourceWorkflowFile
4
+ ? `The user pointed at \`${args.sutSourceWorkflowFile}\` as the source workflow whose setup/teardown steps should be copied into the Testbot workflow. Use it as your primary source of setup steps — but still read the rest of the repo (other workflows, docker-compose, Kubernetes, Helm) to understand the full SUT shape.`
5
+ : `No specific source workflow was provided. Read ALL available GitHub workflows in this repository to learn how services are built, started, and authenticated, and pick the source workflow whose setup steps best match a SUT for testing (typically an e2e/integration workflow, not lint/release/security workflows).`;
6
+ const scanReadStep = args.sutSourceWorkflowFile
7
+ ? `Read the given source GitHub workflow file \`${args.sutSourceWorkflowFile}\` (the user's hint about which steps to lift), then ALSO read the rest of \`.github/workflows/\` and all infrastructure files (docker-compose, Kubernetes manifests, Helm charts) — the full SUT shape often lives across multiple files, so do NOT tunnel into the source workflow alone. Workflows show *how* the SUT is started; infrastructure files show *what* the SUT is. When a workflow step references an infra file (e.g., \`docker compose -f infra/compose.yml up\`, \`helm upgrade <chart>\`, \`kubectl apply -f k8s/\`), open that file too — services, ports, env vars, and dependencies live there.`
8
+ : `Read all GitHub workflow files in \`.github/workflows/\` AND all infrastructure files (docker-compose, Kubernetes manifests, Helm charts). Workflows show *how* the SUT is started; infrastructure files show *what* the SUT is. When a workflow step references an infra file (e.g., \`docker compose -f infra/compose.yml up\`, \`helm upgrade <chart>\`, \`kubectl apply -f k8s/\`), open that file too — services, ports, env vars, and dependencies live there. Pick the source workflow (the one whose SETUP/TEST/TEARDOWN steps will be lifted) once you have the full picture.`;
9
+ const thinkingSourceClause = args.sutSourceWorkflowFile
10
+ ? `confirms you are using \`${args.sutSourceWorkflowFile}\` as the source of setup steps (or explains why a different workflow is a better fit)`
11
+ : `names the workflow you selected as the source of setup steps and explains why`;
12
+ return `${getPersonaPrefix()}
13
+ ### Goal
14
+ 1. Generate working System Under Test (SUT) files that enable the Skyramp Testbot workflow (\`${TESTBOT_WORKFLOW_PATH}\`) to test any PR-specific code in this repository end-to-end for the services supported (REST or gRPC service APIs, queues, frontend UIs).
15
+ 2. Source workflow: ${sourceLine}
16
+ 3. Required output:
17
+ a. Output a <thinking> block that: (i) ${thinkingSourceClause}, (ii) summarizes what you learned about the SUT — services, secrets, auth, runner size — pulling from ALL workflows and infrastructure files, and (iii) decides how the SUT will be brought up — prefer Testbot lifecycle commands alone (targetSetupCommand, targetReadyCheckCommand, targetTeardownCommand) when the setup can be expressed as a single shell command; add GHA pre-steps only for tooling that cannot run in a shell (Buildx, cache, registry login) while still using lifecycle commands for service start; escalate to full GHA steps replacing service startup only when a single command cannot express it; use fully wrapped GHA steps for both setup and teardown only as a last resort. State clearly which approach was chosen and why.
18
+ b. Adapt the Testbot workflow file (\`${TESTBOT_WORKFLOW_PATH}\`) for bringing up the system under test.
19
+ ${buildContextBlock(args)}
20
+
21
+ ### Scan repo and understand the SUT
22
+ 1. ${scanReadStep}
23
+ 2. From the source workflow, classify each step by its *purpose* — not by its name or surface mechanism (\`env:\`, \`with:\`, and \`\${{ secrets.* }}\` can appear on any category and do not determine it):
24
+ a. SETUP — prepares the SUT (install, build, start services, migrate, seed, configure env or inject secrets for the SUT). Common attributes: \`env:\` blocks (e.g. \`DATABASE_URL\`, build flags), \`with:\` parameters (e.g. \`actions/cache\`, \`actions/setup-node\`), \`\${{ secrets.* }}\` references (e.g. registry login, deploy keys), dependency installs (e.g. \`npm ci\` before build).
25
+ b. TEST — runs the test suite (will be replaced by \`skyramp/testbot\`). Common attributes: \`env:\` blocks (e.g. \`API_BASE_URL\`, \`AUTH_TOKEN\`), \`with:\` parameters (e.g. \`actions/cache\`, custom test-runner actions), \`\${{ secrets.* }}\` references (e.g. test credentials, live-integration keys), dependency installs (e.g. \`npm ci\` right before \`npm test\` in the same step).
26
+ c. TEARDOWN — stops or cleans up.
27
+ d. UNRELATED — does none of those (lint, format, notifications, badges).
28
+
29
+ Output as:
30
+ \`\`\`
31
+ Step: "<step name>" → SETUP | TEST | TEARDOWN | UNRELATED
32
+ \`\`\`
33
+ 3. Summarize the SUT, pulling from all sources:
34
+ a. Setup commands needed to bring up all services (may come from multiple workflows / infra files).
35
+ b. Required secrets and env vars (with their \`\${{ secrets.* }}\` references and pass-through style — \`env:\` blocks vs. \`secrets: inherit\`).
36
+ c. Runner needed (\`runs-on\` value; large-ubuntu for heavy builds).
37
+ d. How an auth token is obtained (script, step output, or secret).
38
+
39
+ ### Decide SUT pattern
40
+ The SUT must always be built from the PR's source code — Testbot reuses this workflow for every future PR, so it must validate the code in the PR being tested, not a stale snapshot. Use build: blocks in compose files (or docker build against the PR checkout) so images come from PR source. Pull-only image references and pinned upstream tags are fine for sidecars (databases, queues, caches), but the application services under test must be built locally.
41
+ Evaluate the four patterns in strict priority order — always start with Pattern A and only escalate when it genuinely cannot express the setup. Full semantics for each lifecycle input are in the reference section below.
42
+ 1. Pattern A — Testbot lifecycle commands only (try this first, always)
43
+ Use when the entire setup — build, start, seed — can be expressed as one or two shell commands (e.g., docker compose up -d --build, make start, ./scripts/start.sh). Do NOT add any GHA steps. Set targetSetupCommand, targetReadyCheckCommand, targetReadyCheckTimeout, and targetTeardownCommand on the Testbot action; leave skipTargetSetup unset.
44
+ 2. Pattern B — Hybrid: GHA pre-steps for tool/env setup + Testbot lifecycle commands for service start
45
+ Use when the source workflow requires GHA-specific steps purely for environment or tooling (e.g., docker/setup-buildx-action, actions/cache, private registry login) but the actual service start can still be expressed as a single command. Copy only those tool/env pre-steps BEFORE the Testbot action. Set targetSetupCommand, targetReadyCheckCommand, targetReadyCheckTimeout, and targetTeardownCommand on the Testbot action; leave skipTargetSetup unset. Preserve every env:, with:, and \${{ secrets.* }} reference from the pre-steps exactly as written.
46
+ 3. Pattern C — GHA steps replace service startup + Testbot lifecycle validation
47
+ Use when the service startup itself requires multiple GHA steps that cannot be collapsed into a single command. Copy the source workflow's SETUP steps verbatim BEFORE the Testbot action. Set skipTargetSetup: 'true' and do not set targetSetupCommand. Set targetReadyCheckCommand and targetReadyCheckTimeout on the Testbot action. Add TEARDOWN steps AFTER the Testbot action with if: always(), or set targetTeardownCommand if a single command suffices — pick one, not both. Preserve every env:, with:, and \${{ secrets.* }} reference exactly as written.
48
+ 4. Pattern D — Fully wrapped by GHA steps
49
+ Use when teardown is also too complex for a single command. Copy the SETUP steps BEFORE and TEARDOWN steps AFTER the Testbot action (with if: always()). Set skipTargetSetup: 'true'. Do not set targetSetupCommand or targetTeardownCommand. Preserve every env:, with:, and \${{ secrets.* }} reference exactly as written.
50
+
51
+ ${buildTestbotLifecycleInputsSection()}
52
+
53
+ ### Adapt Testbot workflow
54
+ #### What to do
55
+ 1. Edit \`${TESTBOT_WORKFLOW_PATH}\` (already created by the Testbot installer):
56
+ a. Apply the chosen pattern:
57
+ - Pattern A: set lifecycle inputs on the Testbot action, no GHA steps.
58
+ - Pattern B: add GHA tool/env pre-steps BEFORE the Testbot action, set lifecycle inputs (\`targetSetupCommand\`, \`targetReadyCheckCommand\`, \`targetTeardownCommand\`), leave \`skipTargetSetup\` unset.
59
+ - Pattern C: add GHA SETUP steps BEFORE the Testbot action, set \`skipTargetSetup: 'true'\`, set \`targetReadyCheckCommand\`, add TEARDOWN steps AFTER or set \`targetTeardownCommand\` — do NOT set \`targetSetupCommand\`.
60
+ - Pattern D: add GHA SETUP steps BEFORE and TEARDOWN steps AFTER the Testbot action, set \`skipTargetSetup: 'true'\`, do NOT set \`targetSetupCommand\` or \`targetTeardownCommand\`.
61
+ b. Update \`runs-on\` to match the runner needed by the source workflow's setup (some repos need large-ubuntu).
62
+ c. Pass all required secrets through — reuse the same \`\${{ secrets.* }}\` references or \`secrets: inherit\`. For GHA steps (Patterns B, C, D), set secrets via \`env:\` on the individual steps that need them; for lifecycle inputs (Patterns A, B), set them on the Testbot action or at the job level.
63
+ d. The basic file already contains the \`skyramp/testbot\` action — REPLACE the original workflow's test-run step with it, do NOT add a duplicate test runner step.
64
+ 2. Configure auth — use the lifecycle input reference above for full semantics of \`authTokenCommand\` and \`uiCredentials\`:
65
+ a. If the source workflow exports or seeds an auth token, set \`authTokenCommand\` on the Testbot action (or create \`.skyramp/sut/get-auth-token.sh\`).
66
+ b. If any service requires browser login, set \`uiCredentials\` on the Testbot action.
67
+ c. Omit both when the SUT is unauthenticated.
68
+ 3. Handle source-workflow shape:
69
+ a. If the original workflow uses \`matrix\` builds, pick ONE configuration for the Testbot workflow (matrix runs would multiply Testbot invocations).
70
+ b. If the original workflow uses \`needs:\` (job dependencies), inline the dependent job's steps into the Testbot job — Testbot runs as a single job.
71
+
72
+ #### What not to do
73
+ 1. Do not modify any other GitHub workflow files referenced. The only github workflow file you may edit is \`${TESTBOT_WORKFLOW_PATH}\`; every other workflow under \`.github/workflows/\` (including the source workflow) is read-only reference material.
74
+ 2. Do not change the \`sutSetupMode\` input in \`${TESTBOT_WORKFLOW_PATH}\`. Testbot manages this value automatically — it must stay as provided so the next CI run knows to validate the adapted workflow rather than run tests. Changing it to \`none\` or any other value will break the bootstrap cycle.
75
+ 3. Do not point the SUT at a prebuilt upstream image (e.g. \`image: org/app:latest\` or a fixed commit SHA from \`main\`) for any application service under test. The image will lag the PR and Testbot will validate stale code.
76
+ 4. Do not point the SUT at a remote staging or production environment for application services under test. Staging code drifts from PR source and turns Testbot's results into noise. External infra is acceptable only for sidecar dependencies the PR does not change (e.g. a managed test database).
77
+
78
+ ### Verify
79
+ ${buildCommonSutErrorsSection()}
80
+ ${buildLocalValidationSection()}
81
+ `;
82
+ }
@@ -0,0 +1,49 @@
1
+ import { z } from "zod";
2
+ import { logger } from "../../utils/logger.js";
3
+ import { AnalyticsService } from "../../services/AnalyticsService.js";
4
+ import { getAdaptWorkflowPrompt } from "./modes/adaptWorkflowPrompt.js";
5
+ export var SutSetupMode;
6
+ (function (SutSetupMode) {
7
+ SutSetupMode["None"] = "none";
8
+ SutSetupMode["AdaptWorkflow"] = "adapt_workflow";
9
+ })(SutSetupMode || (SutSetupMode = {}));
10
+ export function registerSetupSutPrompt(server) {
11
+ logger.info("Registering SUT setup prompt");
12
+ server.registerPrompt("skyramp_testbot_sut", {
13
+ description: "Scan a repository and generate SUT (System Under Test) setup files for Skyramp Testbot. " +
14
+ "First milestone: adapts an existing CI/e2e workflow into the Testbot workflow.",
15
+ argsSchema: {
16
+ repositoryPath: z
17
+ .string()
18
+ .describe("Absolute path to the repository root"),
19
+ sutSetupMode: z
20
+ .nativeEnum(SutSetupMode)
21
+ .default(SutSetupMode.None)
22
+ .describe("SUT setup mode: none | adapt_workflow"),
23
+ sutSourceWorkflowFile: z
24
+ .string()
25
+ .default("")
26
+ .describe("Path to an existing workflow file to adapt. Example: .github/workflows/e2e-tests.yml"),
27
+ },
28
+ }, async (args) => {
29
+ const prompt = buildSutPrompt(args);
30
+ AnalyticsService.pushMCPToolEvent("skyramp_testbot_sut_prompt", undefined, { mode: args.sutSetupMode }).catch(() => { });
31
+ return {
32
+ messages: [
33
+ {
34
+ role: "user",
35
+ content: { type: "text", text: prompt },
36
+ },
37
+ ],
38
+ };
39
+ });
40
+ }
41
+ function buildSutPrompt(args) {
42
+ switch (args.sutSetupMode) {
43
+ case SutSetupMode.AdaptWorkflow:
44
+ return getAdaptWorkflowPrompt(args);
45
+ case SutSetupMode.None:
46
+ default:
47
+ return "sutSetupMode is 'none'. No SUT setup required.";
48
+ }
49
+ }
@@ -0,0 +1,80 @@
1
+ import { getPersonaPrefix } from "../personas.js";
2
+ /**
3
+ * Path of the Testbot workflow file relative to the repo root. Created by the
4
+ * Testbot installer and edited in-place by every SUT setup mode.
5
+ */
6
+ export const TESTBOT_WORKFLOW_PATH = ".github/workflows/skyramp-testbot.yml";
7
+ export function buildContextBlock(args) {
8
+ const lines = [];
9
+ lines.push(`<context>`);
10
+ lines.push(`Repository path: ${args.repositoryPath}`);
11
+ lines.push(`Setup mode: ${args.sutSetupMode}`);
12
+ if (args.sutSourceWorkflowFile)
13
+ lines.push(`Source workflow file: ${args.sutSourceWorkflowFile}`);
14
+ lines.push(`</context>`);
15
+ return lines.join("\n");
16
+ }
17
+ export function buildLocalValidationSection() {
18
+ return `Before reporting success, exercise the adapted workflow locally. Testbot's external fix loop only retries when SUT lifecycle commands are set on the Testbot action. When the SUT is brought up by surrounding GHA steps (skipTargetSetup: 'true'), the fix loop is skipped — so the local check below is the only safety net before the workflow is committed.
19
+ Run commands one at a time in your shell — individual execution pinpoints failures far faster than running the whole workflow at once. Any non-zero exit is a fix-needed signal: adjust the workflow, re-run the failing command, and do not proceed until it passes.
20
+ 1. Validate build scripts and helper programs first — run these before anything else so build and auth failures surface cheaply:
21
+ a. Every helper script the workflow calls (e.g., \`./scripts/*.sh\`, \`make <target>\`, \`./gradlew <task>\`) — run each and confirm exit 0.
22
+ b. \`.skyramp/sut/get-auth-token.sh\` if present — run it and confirm it prints a non-empty token to stdout.
23
+ c. Every Dockerfile referenced by build steps — run \`docker build\` against each.
24
+ d. Every docker-compose file the workflow references — run \`docker compose -f <path> config\` to validate the YAML, then \`docker compose -f <path> build\`.
25
+ 2. Validate the SUT lifecycle — choose the branch that matches the chosen pattern:
26
+ a. If lifecycle commands are set on the Testbot action (skipTargetSetup is unset):
27
+ i. Run \`targetSetupCommand\` — confirm exit 0.
28
+ ii. Poll \`targetReadyCheckCommand\` until it exits 0 within \`targetReadyCheckTimeout\` seconds.
29
+ iii. Run \`targetTeardownCommand\` — confirm cleanup and exit 0.
30
+ b. If GHA steps wrap the Testbot action (skipTargetSetup: 'true'):
31
+ i. Extract the shell body of each SETUP step and run them in order. If a step depends entirely on a \${{ secrets.* }} value your shell cannot resolve, note it in <thinking> and skip — do not fabricate a value.
32
+ ii. Poll \`targetReadyCheckCommand\` until it exits 0.
33
+ iii. Extract and run each TEARDOWN step's body in order so the next iteration starts clean.
34
+ Only proceed to the success report once setup → health check → teardown all pass.`;
35
+ }
36
+ /**
37
+ * Returns a reference block describing every Testbot action input that controls
38
+ * the SUT lifecycle and auth. Embed this wherever a prompt needs the LLM to
39
+ * understand what knobs are available before choosing how to wire up the SUT.
40
+ */
41
+ export function buildTestbotLifecycleInputsSection() {
42
+ return `#### Testbot action inputs — target lifecycle and auth
43
+ Target lifecycle inputs (control how Testbot starts, validates, and stops the SUT):
44
+ 1. \`targetSetupCommand\` — shell command that builds and starts all SUT services. Testbot runs this before tests.
45
+ a. Default when unset: \`docker compose up -d\` (starts existing images, does NOT build).
46
+ b. For PR-source builds use \`docker compose up -d --build\` or the equivalent for helm/script-based SUTs.
47
+ c. Override whenever the source workflow's start sequence differs from the default.
48
+ 2. \`targetReadyCheckCommand\` — polling command (e.g., \`curl -sf http://localhost:8080/health\`) that Testbot runs repeatedly after setup until it exits 0 (service is ready) or the timeout is reached.
49
+ a. Always set this — without it Testbot proceeds immediately and tests may hit a not-yet-ready service.
50
+ 3. \`targetReadyCheckTimeout\` — seconds to wait for \`targetReadyCheckCommand\` to succeed.
51
+ a. Default: \`30\`. Cold Docker builds routinely take 60–180 s; always set this to \`'120'\` or higher for compose-based SUTs.
52
+ 4. \`targetTeardownCommand\` — shell command that stops and cleans up the SUT after tests (e.g., \`docker compose down -v\`).
53
+ a. Always set this so each run starts from a clean state.
54
+ b. Omit only when teardown is handled by surrounding GHA steps with \`if: always()\`.
55
+ 5. \`skipTargetSetup\` — when \`'true'\`, Testbot skips running \`targetSetupCommand\` entirely.
56
+ a. Set this only when GHA steps surrounding the Testbot action already bring the SUT up.
57
+ b. Leave it unset (or \`'false'\`) when Testbot should run the full lifecycle via the inputs above.
58
+ Auth inputs (used by Testbot to authenticate against the running SUT during test recording and execution):
59
+ 6. \`authTokenCommand\` — shell command whose stdout is the auth credential for REST/gRPC API testing (e.g., a Bearer token, API key, or session cookie). Testbot captures the output and injects it as the \`Authorization\` header.
60
+ a. Set this when the SUT requires authentication for API calls.
61
+ b. If the source workflow exports a token via a step output or script, wire that same script here (e.g., \`bash .skyramp/sut/get-auth-token.sh\`).
62
+ c. If the workflow seeds a test user during setup, create \`.skyramp/sut/get-auth-token.sh\` that logs in with those credentials and prints the token.
63
+ d. Omit when the SUT APIs are unauthenticated.
64
+ 7. \`uiCredentials\` — \`username:password\` pair typed into the browser login form during UI test recording (format: \`myuser:mypassword\`).
65
+ a. Set for any frontend service that requires browser-based login.
66
+ b. Use \`\${{ secrets.SKYRAMP_UI_CREDENTIALS }}\` if the secret exists, otherwise use credentials seeded during SUT setup.`;
67
+ }
68
+ export function buildCommonSutErrorsSection() {
69
+ return `Before finishing, verify the adapted Testbot workflow setup against common errors seen during SUT setup:
70
+ 1. port_conflict — another container is already using the port. Pick a free host port or stop the conflicting container.
71
+ 2. image_not_found / build_failed — the referenced image does not exist for this SHA, or the build step has the wrong context/Dockerfile path. Build from PR source, do not rely on upstream commit-SHA tags.
72
+ 3. healthcheck_timeout — the service is slow to come up. Use \`targetReadyCheckTimeout >= 120\` and ensure the health endpoint actually exists.
73
+ 4. connection_refused — the service is not listening on the expected host/port (often binding to 127.0.0.1 inside the container; bind to 0.0.0.0).
74
+ 5. dependency_error — required service (DB, cache, queue) is missing from compose or not healthy before the app starts. Add \`depends_on\` with \`condition: service_healthy\`.
75
+ 6. permission_denied — script is not executable (\`chmod +x\`) or volume mount has wrong ownership.
76
+ 7. auth_endpoint_404 / auth_credentials_invalid — the auth script hits a wrong path, or the database has no seeded user. The auth script must create the user before login when starting from a fresh DB.
77
+ 8. missing env vars / secrets — required env vars are unset on the runner. Either default them in the script or pass them through workflow \`env:\`.
78
+ Do not add an in-prompt fix loop — Testbot's external validator will retry with concrete error context if any of these occur.`;
79
+ }
80
+ export { getPersonaPrefix };
@@ -1,91 +1,102 @@
1
- import { buildActionDecisionTree, buildCheckAdditiveFields, buildCheckEndpointExistence, buildCheckResponseShape, buildCheckAuthAndAuthorization, buildCheckBehavioralContract, buildCheckAssignAction, buildDriftOutputChecklist, buildUpdateExecutionRules, } from "./driftAnalysisSections.js";
1
+ import { buildActionDecisionMatrix, buildBreakingChangePatterns, buildTestAssessmentGuidelines, buildAddRecommendationGuidelines, buildDriftOutputChecklist, buildUpdateExecutionRules, } from "./driftAnalysisSections.js";
2
+ import { isTestbotEnabled } from "../../utils/featureFlags.js";
2
3
  import { readDiffFile } from "../../utils/utils.js";
3
- import { PromptPlan } from "../test-recommendation/promptPlan.js";
4
- // ── Private body helpers ──────────────────────────────────────────────────────
5
- // Each receives DriftAnalysisPromptParams and returns the step body string.
6
- // The "### Step N: Title" header is added by PromptPlan.render().
7
- function _assessBody(_p) {
8
- return buildActionDecisionTree();
9
- }
10
- function _checkAdditiveFieldsBody(_p) {
11
- return buildCheckAdditiveFields();
12
- }
13
- function _checkEndpointExistenceBody(_p) {
14
- return buildCheckEndpointExistence();
15
- }
16
- function _checkResponseShapeBody(_p) {
17
- return buildCheckResponseShape();
18
- }
19
- function _checkAuthAndAuthorizationBody(_p) {
20
- return buildCheckAuthAndAuthorization();
21
- }
22
- function _checkBehavioralContractBody(_p) {
23
- return buildCheckBehavioralContract();
24
- }
25
- function _checkAssignActionBody(_p) {
26
- return buildCheckAssignAction();
27
- }
28
- function _applyBody(_p) {
29
- return buildUpdateExecutionRules();
30
- }
31
- function _callToolBody(p) {
32
- return buildDriftOutputChecklist(p.existingTests.length, p.newEndpointCount ?? 0, p.stateFile);
33
- }
34
- // ── PromptPlan declaration ────────────────────────────────────────────────────
35
- // All steps are unconditional — both MCP and testbot callers render the same
36
- // five steps. The only per-caller variation is skipContextHeader (context
37
- // section prepended by buildDriftAnalysisPrompt, not inside the plan).
38
- const _plan = new PromptPlan()
39
- .addPhase("maintenance", "Test Maintenance Assessment", {
40
- headerLevel: "##",
41
- stepFormat: "hash",
42
- })
43
- .step("ASSESS", "Action Decision Tree — assess each existing test against the diff", _assessBody)
44
- .subStep("ENDPOINT_EXISTENCE", "Endpoint existence", _checkEndpointExistenceBody)
45
- .subStep("RESPONSE_SHAPE", "Request/response shape (breaking changes)", _checkResponseShapeBody)
46
- .subStep("ADDITIVE_FIELDS", "Additive response fields (coverage gaps)", _checkAdditiveFieldsBody)
47
- .subStep("AUTH_AUTHZ", "Auth and authorization changes", _checkAuthAndAuthorizationBody)
48
- .subStep("BEHAVIORAL_CONTRACT", "Behavioral and semantic contract changes", _checkBehavioralContractBody)
49
- .subStep("ASSIGN_ACTION", "Assign action", _checkAssignActionBody)
50
- .step("APPLY", "Apply update execution rules", _applyBody)
51
- .step("CALL_TOOL", "Submit recommendations", _callToolBody)
52
- .done();
53
- // ── Exported step label constants ─────────────────────────────────────────────
54
- // Static — safe to export at module load; renumber automatically on insertion.
55
- /** "1" — Assess each test against the diff */
56
- export const DRIFT_STEP_ASSESS = _plan.labels.ASSESS; // "1"
57
- /** "1.1" — Endpoint existence check */
58
- export const DRIFT_STEP_ENDPOINT_EXISTENCE = _plan.labels.ENDPOINT_EXISTENCE; // "1.1"
59
- /** "1.2" — Request/response shape check */
60
- export const DRIFT_STEP_RESPONSE_SHAPE = _plan.labels.RESPONSE_SHAPE; // "1.2"
61
- /** "1.3" — Additive response fields check */
62
- export const DRIFT_STEP_ADDITIVE_FIELDS = _plan.labels.ADDITIVE_FIELDS; // "1.3"
63
- /** "1.4" — Auth and authorization changes check */
64
- export const DRIFT_STEP_AUTH_AUTHZ = _plan.labels.AUTH_AUTHZ; // "1.4"
65
- /** "1.5" — Behavioral and semantic contract changes check */
66
- export const DRIFT_STEP_BEHAVIORAL_CONTRACT = _plan.labels.BEHAVIORAL_CONTRACT; // "1.5"
67
- /** "1.6" — Assign action */
68
- export const DRIFT_STEP_ASSIGN_ACTION = _plan.labels.ASSIGN_ACTION; // "1.6"
69
- /** "2" — Apply update execution rules */
70
- export const DRIFT_STEP_APPLY = _plan.labels.APPLY; // "2"
71
- /** "3" — Submit recommendations (calls skyramp_actions) */
72
- export const DRIFT_STEP_CALL_TOOL = _plan.labels.CALL_TOOL; // "3"
73
- // ── Public builder ────────────────────────────────────────────────────────────
74
4
  export function buildDriftAnalysisPrompt(params) {
75
- // Pre-compute newEndpointCount from rawDiff only when caller did not supply it.
76
- // Use strict undefined checkan explicit 0 means "no new endpoints" and must
77
- // not trigger a diff read.
78
- let newEndpointCount = params.newEndpointCount ?? 0;
79
- if (params.newEndpointCount === undefined) {
80
- const rawDiff = readDiffFile(params.diffFilePath);
81
- if (rawDiff) {
82
- const m = rawDiff.match(/\*\*New Endpoints\*\*\s+\((\d+)\)/);
83
- if (m)
84
- newEndpointCount = parseInt(m[1], 10);
85
- }
5
+ const { existingTests, scannedEndpoints, repositoryPath, stateFile, routerMountContext, candidateRouteFiles, diffFilePath } = params;
6
+ // Read raw diff onceused for both the inline summary block and the per-line file reference.
7
+ const rawDiff = readDiffFile(diffFilePath);
8
+ let newEndpointCount = 0;
9
+ let diffSection = "";
10
+ if (rawDiff) {
11
+ const lines = rawDiff.split("\n");
12
+ const newEndpointMatch = rawDiff.match(/\*\*New Endpoints\*\*\s+\((\d+)\)/);
13
+ if (newEndpointMatch)
14
+ newEndpointCount = parseInt(newEndpointMatch[1], 10);
15
+ diffSection = `## Branch Diff
16
+ \`\`\`
17
+ ${lines.slice(0, 200).join("\n")}
18
+ \`\`\`
19
+ `;
20
+ }
21
+ const testListSection = existingTests.length > 0
22
+ ? `## Existing Test Files (${existingTests.length})
23
+ ${existingTests.map((t) => `- ${t.testFile} (${t.testType})`).join("\n")}
24
+ `
25
+ : `## Existing Test Files
26
+ No existing Skyramp tests found in repository.
27
+ `;
28
+ const scannedSection = scannedEndpoints.length > 0
29
+ ? `## Scanned Endpoints (${scannedEndpoints.length})
30
+ Note: paths below come from static analysis and may be incomplete for nested resources or unsupported frameworks. Use the Routing entry-point files section below to verify and reconstruct full paths.
31
+ ${scannedEndpoints.map((ep) => {
32
+ let methods;
33
+ if (Array.isArray(ep.methods)) {
34
+ methods = ep.methods.map((m) => (typeof m === "string" ? m : m.method)).join("|");
35
+ }
36
+ else {
37
+ methods = ep.method;
38
+ }
39
+ return `- ${methods} ${ep.path}`;
40
+ }).join("\n")}
41
+ `
42
+ : "";
43
+ const mountSection = routerMountContext?.length
44
+ ? `## Routing entry-point files
45
+ Read these to trace the full router/module hierarchy when verifying endpoint paths:
46
+ ${routerMountContext.map(f => `- \`${f}\``).join("\n")}
47
+ `
48
+ : "";
49
+ const hasJavaFiles = candidateRouteFiles?.some(f => /\.(java|kt)$/.test(f)) ?? false;
50
+ const candidateFilesSection = candidateRouteFiles && candidateRouteFiles.length > 0
51
+ ? `## Route Files (read these to find endpoints from any framework)
52
+ ${candidateRouteFiles.map(f => `- ${f}`).join("\n")}
53
+ ${hasJavaFiles ? "Note — Java Spring: full URL = class-level `@RequestMapping` prefix + method-level path. If the prefix is a constant reference (e.g. `@RequestMapping(Url.PAGE_URL)`), find the constant — same file, inner class, or a separate `Url.java` — and resolve it (including `+` concatenation)." : ""}
54
+ `
55
+ : "";
56
+ const diffFileSection = diffFilePath
57
+ ? `## Raw Diff File
58
+ Read \`${diffFilePath}\` to get the full line-by-line diff. Use it to detect:
59
+ - Additive response fields: lines starting with \`+\` inside a view/serializer/controller (e.g. \`+ "newField":\`, \`+ newField =\`)
60
+ - Renamed routes: \`- @app.route("/old")\` / \`+ @app.route("/new")\` or similar framework patterns
61
+ - Status code changes: \`- return 200\` / \`+ return 201\`, \`- res.status(200)\` / \`+ res.status(204)\`
62
+ - Auth additions/removals: \`+ @require_auth\`, \`- @login_required\`, middleware changes
63
+ Read the file once and cache its contents — it is the primary source for per-line breaking-change detection. Use it as evidence for Checks A–D below.
64
+ `
65
+ : "";
66
+ // In inline mode (testbot), skip the context header — existing tests and diff
67
+ // are provided by skyramp_analyze_changes at runtime, not at prompt-build time.
68
+ const contextSection = isTestbotEnabled()
69
+ ? ""
70
+ : `# Test Health Analysis
71
+
72
+ **Repository**: \`${repositoryPath}\`
73
+ **Existing tests**: ${existingTests.length}
74
+ **New endpoints in diff**: ${newEndpointCount}
75
+
76
+ ${diffSection}
77
+ ${diffFileSection}
78
+ ${testListSection}
79
+ ${scannedSection}
80
+ ${mountSection}
81
+ ${candidateFilesSection}`;
82
+ if (isTestbotEnabled()) {
83
+ // Testbot inline mode: all maintenance logic lives here so the testbot
84
+ // prompt only orchestrates steps without duplicating rules.
85
+ // No persona statement here — the outer testbot prompt already establishes
86
+ // the agent's context; a nested identity statement causes role confusion.
87
+ return `<drift_analysis_rules>
88
+ ${buildActionDecisionMatrix()}
89
+ ${buildUpdateExecutionRules()}
90
+ ${buildDriftOutputChecklist(existingTests.length, newEndpointCount, isTestbotEnabled())}
91
+ </drift_analysis_rules>`;
86
92
  }
87
- const resolvedParams = { ...params, newEndpointCount };
88
- // Always emit the lean wrapped form — context is already in the conversation
89
- // from skyramp_analyze_changes, which always runs before this tool.
90
- return `<drift_analysis_rules>\n${_plan.render(resolvedParams)}\n</drift_analysis_rules>`;
93
+ return `You are acting as a Skyramp Integration Architect. Your responsibility is to assess each existing test against the branch diff and determine the correct maintenance action.
94
+
95
+ ${contextSection}
96
+ ${buildActionDecisionMatrix()}
97
+ ${buildBreakingChangePatterns()}
98
+ ${buildTestAssessmentGuidelines()}
99
+ ${buildUpdateExecutionRules()}
100
+ ${buildAddRecommendationGuidelines()}
101
+ ${buildDriftOutputChecklist(existingTests.length, newEndpointCount, isTestbotEnabled(), stateFile)}`;
91
102
  }
@@ -1,84 +1,116 @@
1
- import { buildDriftAnalysisPrompt, DRIFT_STEP_ASSESS, DRIFT_STEP_ENDPOINT_EXISTENCE, DRIFT_STEP_RESPONSE_SHAPE, DRIFT_STEP_ADDITIVE_FIELDS, DRIFT_STEP_AUTH_AUTHZ, DRIFT_STEP_BEHAVIORAL_CONTRACT, DRIFT_STEP_ASSIGN_ACTION, DRIFT_STEP_APPLY, DRIFT_STEP_CALL_TOOL, } from "./drift-analysis-prompt.js";
1
+ import { buildDriftAnalysisPrompt } from "./drift-analysis-prompt.js";
2
2
  import { buildDriftOutputChecklist } from "./driftAnalysisSections.js";
3
- const STATE_FILE = "/tmp/skyramp-analysis-abc123.json";
4
- // ── Step label constants ──────────────────────────────────────────────────────
5
- describe("DRIFT_STEP_* label constants", () => {
6
- it("main steps are sequentially numbered from 1", () => {
7
- expect(DRIFT_STEP_ASSESS).toBe("1");
8
- expect(DRIFT_STEP_APPLY).toBe("2");
9
- expect(DRIFT_STEP_CALL_TOOL).toBe("3");
10
- });
11
- it("sub-steps are numbered within their parent", () => {
12
- expect(DRIFT_STEP_ENDPOINT_EXISTENCE).toBe("1.1");
13
- expect(DRIFT_STEP_RESPONSE_SHAPE).toBe("1.2");
14
- expect(DRIFT_STEP_ADDITIVE_FIELDS).toBe("1.3");
15
- expect(DRIFT_STEP_AUTH_AUTHZ).toBe("1.4");
16
- expect(DRIFT_STEP_BEHAVIORAL_CONTRACT).toBe("1.5");
17
- expect(DRIFT_STEP_ASSIGN_ACTION).toBe("1.6");
18
- });
19
- });
20
- // ── buildDriftOutputChecklist ─────────────────────────────────────────────────
21
- describe("buildDriftOutputChecklist", () => {
22
- it("includes recommendations, updateInstructions, and skyramp_actions CTA", () => {
23
- const checklist = buildDriftOutputChecklist(3, 0, STATE_FILE);
3
+ describe("buildDriftOutputChecklist final-step recommendations guidance", () => {
4
+ const STATE_FILE = "/tmp/skyramp-analysis-abc123.json";
5
+ it("non-inline mode includes recommendations and updateInstructions in final step", () => {
6
+ const checklist = buildDriftOutputChecklist(3, 0, false, STATE_FILE);
7
+ // Must instruct the LLM to pass recommendations to skyramp_actions
24
8
  expect(checklist).toContain("recommendations");
9
+ // Must mention updateInstructions so the LLM knows to populate it
25
10
  expect(checklist).toContain("updateInstructions");
11
+ // Must reference the stateFile path
26
12
  expect(checklist).toContain(STATE_FILE);
13
+ // Must call skyramp_actions as the final action
27
14
  expect(checklist).toContain("skyramp_actions");
28
15
  });
29
- it("does not contain JSON shape — schema is authoritative", () => {
30
- const checklist = buildDriftOutputChecklist(3, 0, STATE_FILE);
16
+ it("non-inline mode does not contain JSON shape — schema is authoritative", () => {
17
+ const checklist = buildDriftOutputChecklist(3, 0, false, STATE_FILE);
18
+ // The JSON shape was moved to inputSchema — prompt must not duplicate it
31
19
  expect(checklist).not.toContain('"testFile":');
32
20
  expect(checklist).not.toContain('"action":');
33
21
  });
34
- it("CTA appears exactly once", () => {
35
- const checklist = buildDriftOutputChecklist(3, 0, STATE_FILE);
36
- const ctaCount = (checklist.match(/call `skyramp_actions`/g) || []).length;
37
- expect(ctaCount).toBe(1);
22
+ it("inline mode does not reference skyramp_actions or stateFile", () => {
23
+ const checklist = buildDriftOutputChecklist(3, 0, true, STATE_FILE);
24
+ // Inline mode applies changes directly — no skyramp_actions call
25
+ expect(checklist).not.toContain("skyramp_actions");
26
+ expect(checklist).not.toContain(STATE_FILE);
27
+ });
28
+ it("full prompt (non-inline) includes recommendations guidance", () => {
29
+ const prompt = buildDriftAnalysisPrompt({
30
+ existingTests: [],
31
+ scannedEndpoints: [],
32
+ repositoryPath: "/repo",
33
+ stateFile: STATE_FILE,
34
+ });
35
+ expect(prompt).toContain("recommendations");
36
+ expect(prompt).toContain("updateInstructions");
38
37
  });
39
38
  });
40
- // ── buildDriftAnalysisPrompt ──────────────────────────────────────────────────
41
- describe("buildDriftAnalysisPrompt", () => {
42
- function prompt() {
39
+ describe("buildDriftAnalysisPrompt - inline mode", () => {
40
+ beforeEach(() => { process.env.SKYRAMP_FEATURE_TESTBOT = "1"; });
41
+ afterEach(() => { delete process.env.SKYRAMP_FEATURE_TESTBOT; });
42
+ function inlinePrompt() {
43
43
  return buildDriftAnalysisPrompt({
44
44
  existingTests: [],
45
45
  scannedEndpoints: [],
46
46
  repositoryPath: "/repo",
47
- stateFile: STATE_FILE,
47
+ // stateFile omitted → inline mode
48
48
  });
49
49
  }
50
- it("wraps output in drift_analysis_rules XML tags", () => {
51
- expect(prompt()).toContain("<drift_analysis_rules>");
52
- expect(prompt()).toContain("</drift_analysis_rules>");
50
+ it("wraps inline rules in drift_analysis_rules XML tags", () => {
51
+ const prompt = inlinePrompt();
52
+ expect(prompt).toContain("<drift_analysis_rules>");
53
+ expect(prompt).toContain("</drift_analysis_rules>");
53
54
  });
54
- it("does not contain the persona statement or context header", () => {
55
- expect(prompt()).not.toContain("You are acting as a Skyramp Integration Architect");
56
- expect(prompt()).not.toContain("# Test Health Analysis");
55
+ it("does not contain the persona statement", () => {
56
+ const prompt = inlinePrompt();
57
+ expect(prompt).not.toContain("You are acting as a Skyramp Integration Architect");
57
58
  });
58
- it("includes recommendations guidance and updateInstructions", () => {
59
- expect(prompt()).toContain("recommendations");
60
- expect(prompt()).toContain("updateInstructions");
59
+ it("does not contain the standalone Test Health Analysis header", () => {
60
+ const prompt = inlinePrompt();
61
+ expect(prompt).not.toContain("# Test Health Analysis");
61
62
  });
62
- it("includes all PromptPlan steps", () => {
63
- const p = prompt();
64
- expect(p).toContain(`### Step ${DRIFT_STEP_ASSESS}:`);
65
- expect(p).toContain(`### Step ${DRIFT_STEP_ENDPOINT_EXISTENCE}:`);
66
- expect(p).toContain(`### Step ${DRIFT_STEP_RESPONSE_SHAPE}:`);
67
- expect(p).toContain(`### Step ${DRIFT_STEP_ADDITIVE_FIELDS}:`);
68
- expect(p).toContain(`### Step ${DRIFT_STEP_AUTH_AUTHZ}:`);
69
- expect(p).toContain(`### Step ${DRIFT_STEP_BEHAVIORAL_CONTRACT}:`);
70
- expect(p).toContain(`### Step ${DRIFT_STEP_ASSIGN_ACTION}:`);
71
- expect(p).toContain(`### Step ${DRIFT_STEP_APPLY}:`);
72
- expect(p).toContain(`### Step ${DRIFT_STEP_CALL_TOOL}:`);
63
+ it("does not contain the skyramp_actions CTA (that belongs to standalone mode)", () => {
64
+ const prompt = inlinePrompt();
65
+ // Inline mode final step directs applying changes directly, not calling skyramp_actions
66
+ expect(prompt).not.toContain("call `skyramp_actions`");
73
67
  });
74
- it("skyramp_actions CTA appears exactly once", () => {
75
- const ctaCount = (prompt().match(/call `skyramp_actions`/g) || []).length;
68
+ });
69
+ describe("buildDriftAnalysisPrompt - scanned endpoints rendering", () => {
70
+ // Reproduces the [object Object] bug: skeletonEndpoints from analyzeChangesTool
71
+ // stores methods as objects { method: string, ... }, not plain strings.
72
+ const skeletonMethodObjects = [
73
+ {
74
+ path: "/api/v1/",
75
+ methods: [{ method: "GET", description: "", queryParams: [], authRequired: true, sourceFile: "main.py", interactions: [] }],
76
+ resourceGroup: "v1",
77
+ pathParams: [],
78
+ },
79
+ {
80
+ path: "/api/v1/orders",
81
+ methods: [
82
+ { method: "GET", description: "", queryParams: [], authRequired: true, sourceFile: "orders.py", interactions: [] },
83
+ { method: "POST", description: "", queryParams: [], authRequired: true, sourceFile: "orders.py", interactions: [] },
84
+ ],
85
+ resourceGroup: "orders",
86
+ pathParams: [],
87
+ },
88
+ ];
89
+ it("renders HTTP methods as strings, not [object Object]", () => {
90
+ const prompt = buildDriftAnalysisPrompt({
91
+ existingTests: [],
92
+ scannedEndpoints: skeletonMethodObjects,
93
+ repositoryPath: "/repo",
94
+ stateFile: "/tmp/state.json",
95
+ });
96
+ expect(prompt).not.toContain("[object Object]");
97
+ expect(prompt).toContain("GET /api/v1/");
98
+ expect(prompt).toContain("GET|POST /api/v1/orders");
99
+ // CTA should appear exactly once (not duplicated)
100
+ const ctaCount = (prompt.match(/call `skyramp_actions`/g) || []).length;
76
101
  expect(ctaCount).toBe(1);
77
102
  });
103
+ it("also works with plain string methods (ScannedEndpoint format)", () => {
104
+ const stringMethods = [
105
+ { path: "/api/v1/products", methods: ["GET", "POST"], sourceFile: "products.py" },
106
+ ];
107
+ const prompt = buildDriftAnalysisPrompt({
108
+ existingTests: [],
109
+ scannedEndpoints: stringMethods,
110
+ repositoryPath: "/repo",
111
+ stateFile: "/tmp/state.json",
112
+ });
113
+ expect(prompt).not.toContain("[object Object]");
114
+ expect(prompt).toContain("GET|POST /api/v1/products");
115
+ });
78
116
  });
79
- // ── Scanned endpoints no longer in prompt output ─────────────────────────────
80
- // The context header (repo, diff, test list, scanned endpoints) was removed —
81
- // skyramp_analyze_changes already delivers that context to the conversation.
82
- // The scanned endpoints rendering tests were removed along with the header.
83
- // The [object Object] bug that was guarded against is no longer reachable via
84
- // this prompt path.