@skyramp/mcp 0.4.4-rc.2 → 0.4.4

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.
@@ -9,7 +9,7 @@ import { ProgrammingLanguage, TESTBOT_SUPPORTED_TEST_TYPES, TestType, } from "..
9
9
  import { STEP_METHOD_DESCRIPTION, stepMethodSchema, } from "../types/StepMethod.js";
10
10
  import { DriftAction } from "../types/TestAnalysis.js";
11
11
  import { isResult, TestExecutionStatus } from "../types/TestExecution.js";
12
- import { IssueFoundCategory } from "../types/TestbotReport.js";
12
+ import { EnvironmentFamily, FailureCauseKind, IssueFoundCategory, } from "../types/TestbotReport.js";
13
13
  import { StateManager, runArtifactDir, getTestsRepoDir, resolveOwnRunStatePath, } from "../utils/AnalysisStateManager.js";
14
14
  import { toolError, testFileMatches } from "../utils/utils.js";
15
15
  import { isTestbotEnabled, isUtilsReuseEnabled, } from "../utils/featureFlags.js";
@@ -115,17 +115,46 @@ const repositoryField = z
115
115
  .trim()
116
116
  .min(1)
117
117
  .describe("The 'owner/repo' this item pertains to (e.g. 'letsramp/api-insight'). Set it on every item — the primary's owner/repo for primary-repo objections, or a related repo's owner/repo for objections derived from that repository's diff.");
118
+ // One home for the family meanings: a `.describe()` on the base enum is replaced
119
+ // by the `.optional().describe()` at each use site, so the meanings go there.
120
+ const ENVIRONMENT_FAMILY_MEANINGS = "auth_token = an API token is missing or expired. " +
121
+ "secret = another secret or credential is missing. " +
122
+ "data_connection = the database or data store is not reachable. " +
123
+ "app_not_started = the app under test did not start or is not reachable. " +
124
+ "login = the sign-in step failed. " +
125
+ "external_dependency = an external service is not configured. " +
126
+ "recording_replay = recording or replay of the trace failed.";
127
+ const environmentFamilyField = z.enum(EnvironmentFamily);
128
+ // SKYR-4555. Why a Fail or Skipped row did not pass; the row-level refinement on
129
+ // testResultSchema decides when it is required and which shapes are consistent.
130
+ const failureCauseSchema = z
131
+ .object({
132
+ kind: z
133
+ .enum(FailureCauseKind)
134
+ .describe("application = the code under test misbehaved, or a test written to prove a defect failed as intended. " +
135
+ "environment = the test could not exercise the behaviour because of the test environment: a missing token or secret, no data connection, the app did not start, sign-in failed, an external service is not configured, or recording or replay failed. Name the gap in `family`. " +
136
+ "test_defect = the generated test itself is wrong, not the app: an ambiguous locator, a strict-mode clash, a bad assertion setup."),
137
+ family: environmentFamilyField
138
+ .optional()
139
+ .describe("The test-environment gap. Required when kind is 'environment'. Not allowed for the other kinds. " +
140
+ ENVIRONMENT_FAMILY_MEANINGS),
141
+ })
142
+ .describe("Why this test did not pass. REQUIRED when status is 'Fail' or 'Skipped'. Not allowed when status is 'Pass'. " +
143
+ "It must agree with reachedBehavior: 'application' needs reachedBehavior true or omitted; 'environment' needs reachedBehavior false or omitted.");
118
144
  // videoPath is deliberately absent from this input contract: it is attached server-side
119
145
  // from the run's execution records (see attachVideoPath), and zod strips any the model
120
146
  // supplies anyway. SKYR-4156 is what happens when the agent owns that field instead.
121
- const testResultSchema = z.object({
147
+ const testResultSchema = z
148
+ .object({
122
149
  testType: z
123
150
  .enum(TestType)
124
151
  .describe("Type of test. Do not include priority or other metadata in this field."),
125
152
  endpoint: z
126
153
  .string()
127
154
  .describe("For contract and integration tests, the HTTP verb and path, e.g. 'GET /api/v1/products'. For ui and e2e tests, the page path the test opens, e.g. '/notifications'."),
128
- status: z.enum(["Pass", "Fail", "Skipped"]).describe("Test execution result"),
155
+ status: z
156
+ .enum(["Pass", "Fail", "Skipped"])
157
+ .describe("Test execution result"),
129
158
  details: z
130
159
  .string()
131
160
  .describe("One sentence — no embedded newlines, no markdown. e.g. '10.8s, products_contract_test.py' or 'failed: <one-line error summary>, products_contract_test.py'"),
@@ -145,7 +174,76 @@ const testResultSchema = z.object({
145
174
  .describe("For a failing test only. `true` when the request that carries the behaviour under test was sent and answered by the code under test, and the assertion that failed is about that behaviour. " +
146
175
  "`false` when the failure came earlier: a 404, 401, 403 or 5xx on a route the change adds; an expired token or a sign-in page; a control or setup step that failed; a generator or SDK error. " +
147
176
  "If your own `details` text says the request was not sent or an earlier step tripped, this is `false`. Omit only if you cannot tell."),
177
+ failureCause: failureCauseSchema.optional(),
148
178
  repository: repositoryField,
179
+ })
180
+ .superRefine((row, ctx) => {
181
+ // SKYR-4555: the business-user report sorts failures by cause, so every row
182
+ // that did not pass must name one and a passing row must not. The kind must
183
+ // agree with reachedBehavior where the agent set it: the two answer the same
184
+ // question — did the test reach the behaviour under test — from two sides.
185
+ const cause = row.failureCause;
186
+ if (row.status === "Pass") {
187
+ if (cause === undefined)
188
+ return;
189
+ ctx.addIssue({
190
+ code: "custom",
191
+ path: ["failureCause"],
192
+ message: `failureCause is not allowed when status is 'Pass'. A passing test has no failure cause. ` +
193
+ `Remove failureCause from this row.`,
194
+ });
195
+ return;
196
+ }
197
+ if (cause === undefined) {
198
+ ctx.addIssue({
199
+ code: "custom",
200
+ path: ["failureCause"],
201
+ message: `failureCause is required when status is '${row.status}'. Set failureCause.kind to one of: ` +
202
+ `'application' (the code under test misbehaved, or a test written to prove a defect failed as intended); ` +
203
+ `'environment' (the test environment stopped the test before it reached the behaviour — also set failureCause.family); ` +
204
+ `'test_defect' (the generated test itself is wrong, not the app).`,
205
+ });
206
+ return;
207
+ }
208
+ if (cause.kind === FailureCauseKind.Environment) {
209
+ if (cause.family === undefined) {
210
+ ctx.addIssue({
211
+ code: "custom",
212
+ path: ["failureCause", "family"],
213
+ message: `failureCause.family is required when failureCause.kind is 'environment'. ` +
214
+ `Set it to the gap that stopped the test: ${Object.values(EnvironmentFamily).join(", ")}.`,
215
+ });
216
+ }
217
+ }
218
+ else if (cause.family !== undefined) {
219
+ ctx.addIssue({
220
+ code: "custom",
221
+ path: ["failureCause", "family"],
222
+ message: `failureCause.family is allowed only when failureCause.kind is 'environment'. ` +
223
+ `Remove failureCause.family, or set kind to 'environment' if the test environment stopped the test.`,
224
+ });
225
+ }
226
+ if (cause.kind === FailureCauseKind.Application) {
227
+ if (row.reachedBehavior === false) {
228
+ ctx.addIssue({
229
+ code: "custom",
230
+ path: ["failureCause", "kind"],
231
+ message: `failureCause.kind 'application' contradicts reachedBehavior false. 'application' means the test reached the behaviour under test and the code misbehaved. ` +
232
+ `If the failure came earlier, set kind to 'environment' or 'test_defect'. If the test did reach the behaviour, set reachedBehavior to true.`,
233
+ });
234
+ }
235
+ }
236
+ else if (cause.kind === FailureCauseKind.Environment &&
237
+ row.reachedBehavior === true) {
238
+ // test_defect is not checked: a wrong assertion can fail after the test
239
+ // reached the behaviour, or a bad setup step can fail before it.
240
+ ctx.addIssue({
241
+ code: "custom",
242
+ path: ["failureCause", "kind"],
243
+ message: `failureCause.kind 'environment' contradicts reachedBehavior true. reachedBehavior true means the test reached the behaviour under test, so the environment did not stop it. ` +
244
+ `Set kind to 'application' or 'test_defect', or set reachedBehavior to false if the failure came earlier.`,
245
+ });
246
+ }
149
247
  });
150
248
  // Structured grounding for UI recommendations. Lifted verbatim from a
151
249
  // `browser_blueprint` element captured during this run. Source of truth for
@@ -388,8 +486,25 @@ const issueFoundSchema = z
388
486
  .describe("1-based line number in sourceFile. Optional and advisory. " +
389
487
  "Give the line of the offending statement itself, not the line of the enclosing function's signature — review views place the objection on exactly this line. " +
390
488
  "The tool does not check that the line still holds the cited code, because line numbers drift."),
489
+ environmentFamily: environmentFamilyField
490
+ .optional()
491
+ .describe("Only for category 'config'. Not allowed for the other categories. " +
492
+ "When this config issue is a test-environment gap, name the gap. Omit it for a config issue that is not a test-environment gap. " +
493
+ ENVIRONMENT_FAMILY_MEANINGS),
391
494
  })
392
495
  .superRefine((issue, ctx) => {
496
+ // SKYR-4555: the family names a test-environment gap, which only a config
497
+ // entry reports. On a bug, lint or type entry it would put the issue in two
498
+ // places at once.
499
+ if (issue.environmentFamily !== undefined &&
500
+ issue.category !== IssueFoundCategory.Config) {
501
+ ctx.addIssue({
502
+ code: "custom",
503
+ path: ["environmentFamily"],
504
+ message: `environmentFamily is allowed only when category is 'config'. It names the test-environment gap behind a config issue. ` +
505
+ `Remove environmentFamily from this '${issue.category}' entry, or set category to 'config' if the issue is a test-environment gap.`,
506
+ });
507
+ }
393
508
  // A `bug` entry asserts that product code is wrong, so it has to name the code:
394
509
  // without a file the entry is prose a reviewer cannot act on, and nothing
395
510
  // downstream can place it in the diff. Only the file is required — a config file
@@ -2750,7 +2865,10 @@ export function registerSubmitReportTool(server) {
2750
2865
  // these objects and don't expect it).
2751
2866
  testResults: params.testResults.map((row) => {
2752
2867
  const { testFilePath: _tfp, ...wire } = attachVideoPath(row, stateData.executionVideos);
2753
- return wire;
2868
+ // SKYR-4555: the basename is the join key to newTestsCreated and
2869
+ // testMaintenance (consumers compare it with the basename of their
2870
+ // fileName, which may be a relative path); the path stays internal.
2871
+ return { ...wire, fileName: path.basename(row.testFilePath) };
2754
2872
  }),
2755
2873
  issuesFound,
2756
2874
  executionCoverage,
@@ -13,6 +13,26 @@ export declare enum IssueFoundCategory {
13
13
  Type = "type",
14
14
  Config = "config"
15
15
  }
16
+ /** Why a Fail or Skipped testResults row did not pass. Application is the code
17
+ * under test (or a test written to prove a defect, failing as intended);
18
+ * Environment is the test environment stopping the test before it reached the
19
+ * behaviour; TestDefect is the generated test itself being wrong. */
20
+ export declare enum FailureCauseKind {
21
+ Application = "application",
22
+ Environment = "environment",
23
+ TestDefect = "test_defect"
24
+ }
25
+ /** Which test-environment gap stopped a test: the `family` of an Environment
26
+ * failureCause, and the `environmentFamily` of a config issuesFound entry. */
27
+ export declare enum EnvironmentFamily {
28
+ AuthToken = "auth_token",
29
+ Secret = "secret",
30
+ DataConnection = "data_connection",
31
+ AppNotStarted = "app_not_started",
32
+ Login = "login",
33
+ ExternalDependency = "external_dependency",
34
+ RecordingReplay = "recording_replay"
35
+ }
16
36
  /**
17
37
  * Shape of the JSON report written by skyramp_submit_report and read by testbot
18
38
  * for rendering as Markdown. All fields mirror the corresponding Zod schemas in
@@ -104,6 +124,19 @@ export interface TestbotReport {
104
124
  endpoint: string;
105
125
  status: "Pass" | "Fail" | "Skipped";
106
126
  details: string;
127
+ /** Server-derived: the basename of the `testFilePath` the agent submitted,
128
+ * which never reaches the report. The join key to `newTestsCreated` and
129
+ * `testMaintenance`: match it against the basename of their `fileName`
130
+ * (a `newTestsCreated` row may carry a repository-relative path), plus
131
+ * `repository`. */
132
+ fileName: string;
133
+ /** Why a Fail or Skipped row did not pass. The submit schema requires it on
134
+ * those rows and refuses it on a Pass row; `family` is set exactly when
135
+ * `kind` is Environment. Absent in reports from MCP versions predating it. */
136
+ failureCause?: {
137
+ kind: FailureCauseKind;
138
+ family?: EnvironmentFamily;
139
+ };
107
140
  videoPath?: string;
108
141
  /** See newTestsCreated[].repository. */
109
142
  repository: string;
@@ -158,6 +191,10 @@ export interface TestbotReport {
158
191
  sourceFile?: string;
159
192
  sourceSymbol?: string;
160
193
  sourceLine?: number;
194
+ /** On a `config` entry that is a test-environment gap, which gap. The submit
195
+ * schema refuses it on any other category. Optional: a config entry need
196
+ * not be an environment gap, and older MCP versions never set it. */
197
+ environmentFamily?: EnvironmentFamily;
161
198
  /** See newTestsCreated[].repository. */
162
199
  repository: string;
163
200
  }[];
@@ -8,3 +8,25 @@ export var IssueFoundCategory;
8
8
  IssueFoundCategory["Type"] = "type";
9
9
  IssueFoundCategory["Config"] = "config";
10
10
  })(IssueFoundCategory || (IssueFoundCategory = {}));
11
+ /** Why a Fail or Skipped testResults row did not pass. Application is the code
12
+ * under test (or a test written to prove a defect, failing as intended);
13
+ * Environment is the test environment stopping the test before it reached the
14
+ * behaviour; TestDefect is the generated test itself being wrong. */
15
+ export var FailureCauseKind;
16
+ (function (FailureCauseKind) {
17
+ FailureCauseKind["Application"] = "application";
18
+ FailureCauseKind["Environment"] = "environment";
19
+ FailureCauseKind["TestDefect"] = "test_defect";
20
+ })(FailureCauseKind || (FailureCauseKind = {}));
21
+ /** Which test-environment gap stopped a test: the `family` of an Environment
22
+ * failureCause, and the `environmentFamily` of a config issuesFound entry. */
23
+ export var EnvironmentFamily;
24
+ (function (EnvironmentFamily) {
25
+ EnvironmentFamily["AuthToken"] = "auth_token";
26
+ EnvironmentFamily["Secret"] = "secret";
27
+ EnvironmentFamily["DataConnection"] = "data_connection";
28
+ EnvironmentFamily["AppNotStarted"] = "app_not_started";
29
+ EnvironmentFamily["Login"] = "login";
30
+ EnvironmentFamily["ExternalDependency"] = "external_dependency";
31
+ EnvironmentFamily["RecordingReplay"] = "recording_replay";
32
+ })(EnvironmentFamily || (EnvironmentFamily = {}));
@@ -2,7 +2,7 @@ export { TestExecutionStatus } from "./TestExecution.js";
2
2
  export { DriftAction } from "./TestAnalysis.js";
3
3
  export { TestType, HttpMethod } from "./TestTypes.js";
4
4
  export type { TestbotReport } from "./TestbotReport.js";
5
- export { IssueFoundCategory } from "./TestbotReport.js";
5
+ export { IssueFoundCategory, FailureCauseKind, EnvironmentFamily, } from "./TestbotReport.js";
6
6
  export { ReuseDeclinedBy, ReuseVerificationOutcome, HelperVerificationOutcome, } from "./ReuseOutcome.js";
7
7
  export type { ReuseOutcome, ReuseSkippedEntry, ReuseMissedEntry, HelperReuseOutcome, RetrofitOutcome, ChangedHelperEntry, ChangedHelperCaller, OrphanedHelper, } from "./ReuseOutcome.js";
8
8
  export type { RelatedRepository, TestbotPromptOptions, } from "./TestbotPromptOptions.js";
@@ -1,5 +1,5 @@
1
1
  export { TestExecutionStatus } from "./TestExecution.js";
2
2
  export { DriftAction } from "./TestAnalysis.js";
3
3
  export { TestType, HttpMethod } from "./TestTypes.js";
4
- export { IssueFoundCategory } from "./TestbotReport.js";
4
+ export { IssueFoundCategory, FailureCauseKind, EnvironmentFamily, } from "./TestbotReport.js";
5
5
  export { ReuseDeclinedBy, ReuseVerificationOutcome, HelperVerificationOutcome, } from "./ReuseOutcome.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyramp/mcp",
3
- "version": "0.4.4-rc.2",
3
+ "version": "0.4.4",
4
4
  "main": "build/index.js",
5
5
  "exports": {
6
6
  ".": "./build/index.js",
@@ -65,7 +65,7 @@ Log in once via the credentials in your <ui-credentials> context before you navi
65
65
  **One state file, per-repo sections.** There is exactly ONE state file for the whole run (created automatically and reused across calls). Each `skyramp_analyze_changes` call writes its repo's section into it, keyed by `repository`. The run declares its primary when it starts, so the root section is the primary's whichever call runs first. When you maintain or act on a related repo's tests, pass that repo's `repository` value on the call, exactly as step {analyzeStep} says to pass `{primaryRepo}` for the primary repo's tests.
66
66
 
67
67
  **Cross-repo test generation ({generateTask} spans all repos).** Draft your planned tests from the analysis of the primary AND every related repo together, and register them as ONE plan. When you generate a test for a related repo's endpoint:
68
- - **Execute it only if that repo's service is already running and reachable.** The workflow's setup may have started multiple services; before generating an API test for a related repo, confirm its `base_url` (from that repo's workspace/Execution Plan) responds. If the service is unreachable, still GENERATE the test but mark its `testResults` status as `Skipped` with details "service not running in this run" — do NOT count an unreachable service as a failure.
68
+ - **Execute it only if that repo's service is already running and reachable.** The workflow's setup may have started multiple services; before generating an API test for a related repo, confirm its `base_url` (from that repo's workspace/Execution Plan) responds. If the service is unreachable, still GENERATE the test but mark its `testResults` status as `Skipped` with details "service not running in this run" and `failureCause` `{ "kind": "environment", "family": "app_not_started" }` — do NOT count an unreachable service as a failure.
69
69
  - **Write the test file into that service's own `testDirectory`** — the one declared for the service in the unified workspace.yml (the related repo's services were registered there in {registerServicesRef}, each with its `repository`). The `testDirectory` is interpreted relative to the **single delivery root** (the configured test repo if set, otherwise the primary repo), so all generated tests are delivered together by the existing single-target delivery. Do NOT invent a per-source-repo subdirectory, and do NOT write into the related repo's own checkout — it is read-only context. (If two repos happen to declare the same `testDirectory`, their files coexist there; the `repository` field on each report item — below — is what attributes ownership, not the path.)
70
70
  - **Set the `repository` field** (`owner/repo`) on every such `newTestsCreated` / `testResults` item so the report attributes it to the originating repo (see Report Guidelines).
71
71