@skyramp/mcp 0.2.1-rc.1 → 0.2.1

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.
@@ -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',
@@ -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.