wopee-mcp 1.30.1 → 1.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -344,6 +344,26 @@ Fetch test results for suite <suiteUuid>
344
344
  Show me the executed test cases for my latest analysis suite
345
345
  ```
346
346
 
347
+ ### Reporting
348
+
349
+ #### `wopee_create_github_issue`
350
+
351
+ Files a GitHub issue into the repository linked to the current project. Given a failed run's `executedTestCaseUuid`, Wopee.io adds what it knows about that run below your text: the failing step, expected vs actual, the failing step's screenshot and a link to the run in Commander.
352
+
353
+ - **Parameters:**
354
+ - `title` - The issue title
355
+ - `body` _(optional)_ - Markdown, kept verbatim above the run evidence
356
+ - `labels` _(optional)_ - Labels to apply, e.g. `["bug"]`. GitHub may skip labels the repository doesn't have; the result says which were applied
357
+ - `executedTestCaseUuid` _(optional)_ - The run to attach evidence from, as shown by `wopee_fetch_recent_executions` or `wopee_dispatch_agent`
358
+ - **Returns:** The issue number and URL, plus notes on anything left out (e.g. a screenshot that could not be uploaded)
359
+ - **Requirements:** The issue is created by the Wopee.io GitHub App and names you in its body. The App's installation needs the **Issues: Read & write** repository permission, and **Contents: Read & write** for the screenshot, which is committed to a separate `wopee-issue-assets` branch. That branch has no shared history with your code and is safe to delete; deleting it only breaks the images in existing issues.
360
+
361
+ **Example Usage:**
362
+
363
+ ```
364
+ File the failed checkout test as a GitHub issue
365
+ ```
366
+
347
367
  ## Typical Workflow
348
368
 
349
369
  1. **Start with a suite:**
@@ -369,6 +389,9 @@ Show me the executed test cases for my latest analysis suite
369
389
  - Use `wopee_fetch_executed_test_cases` to check detailed status and reports for a specific suite
370
390
  - Or use the `fetch-test-results` prompt for a formatted summary of all test results
371
391
 
392
+ 6. **Report failures:**
393
+ - Use `wopee_create_github_issue` with the failed run's `executedTestCaseUuid` to file it into your repository
394
+
372
395
  ## Available Prompts
373
396
 
374
397
  ### `fetch-project-summary`
@@ -192,6 +192,9 @@ export const FetchExecutedTestCases = `
192
192
  agentReportStatus
193
193
  codeReport
194
194
  codeReportStatus
195
+ verdictIntegrity
196
+ integrityFlagged
197
+ finishedAt
195
198
  createdAt
196
199
  updatedAt
197
200
  }
@@ -284,17 +287,34 @@ export const FetchRecentExecutedTestCases = `
284
287
  agentReportStatus
285
288
  codeReport
286
289
  codeReportStatus
290
+ verdictIntegrity
291
+ integrityFlagged
287
292
  createdAt
288
293
  updatedAt
289
294
  }
290
295
  }
291
296
  `;
292
297
  export const CreateGitHubIssue = `
293
- mutation CreateGitHubIssue($projectUuid: ID!, $title: String!, $body: String!, $labels: [String!]) {
294
- createGitHubIssue(projectUuid: $projectUuid, title: $title, body: $body, labels: $labels) {
298
+ mutation CreateGitHubIssue(
299
+ $projectUuid: ID!
300
+ $title: String!
301
+ $body: String!
302
+ $labels: [String!]
303
+ $executedTestCaseUuid: ID
304
+ ) {
305
+ createGitHubIssue(
306
+ projectUuid: $projectUuid
307
+ title: $title
308
+ body: $body
309
+ labels: $labels
310
+ executedTestCaseUuid: $executedTestCaseUuid
311
+ ) {
295
312
  url
296
313
  number
297
314
  title
315
+ screenshotAttached
316
+ labelsApplied
317
+ warnings
298
318
  }
299
319
  }
300
320
  `;
@@ -69,16 +69,17 @@ export function _parseGenerateArtifactType(type) {
69
69
  };
70
70
  }
71
71
  }
72
+ /** One-line description of a failed request, e.g. "[GRAPHQL_ERROR] Not Authorised!". */
73
+ export function _describeError(error) {
74
+ if (error instanceof RequestError)
75
+ return `[${error.code}] ${error.message}${error.retryable ? " (retryable)" : ""}`;
76
+ return error instanceof Error ? error.message : String(error);
77
+ }
72
78
  export function _parseError(error) {
73
79
  console.error(error instanceof z.ZodError ? error.issues : error);
74
80
  if (error instanceof RequestError) {
75
81
  return {
76
- content: [
77
- {
78
- type: "text",
79
- text: `[${error.code}] ${error.message}${error.retryable ? " (retryable)" : ""}`,
80
- },
81
- ],
82
+ content: [{ type: "text", text: _describeError(error) }],
82
83
  };
83
84
  }
84
85
  return {
@@ -0,0 +1,21 @@
1
+ // The text an MCP caller reads back after filing an issue.
2
+ //
3
+ // No runtime imports, which keeps it unit-testable under `node --test`.
4
+ export function formatIssueCreated(issue, { withRunEvidence }) {
5
+ const lines = [
6
+ "GitHub issue created successfully:",
7
+ `- Title: ${issue.title}`,
8
+ `- Number: #${issue.number}`,
9
+ `- URL: ${issue.url}`,
10
+ ];
11
+ if (issue.labelsApplied?.length) {
12
+ lines.push(`- Labels: ${issue.labelsApplied.join(", ")}`);
13
+ }
14
+ if (withRunEvidence) {
15
+ lines.push(`- Run evidence: failing step, expected vs actual and the Commander report link added; screenshot ${issue.screenshotAttached ? "attached" : "not attached"}`);
16
+ }
17
+ for (const warning of issue.warnings ?? []) {
18
+ lines.push(`- Note: ${warning}`);
19
+ }
20
+ return lines.join("\n");
21
+ }
@@ -4,21 +4,27 @@ import { requestClient } from "../../utils/requestClient.js";
4
4
  import { _parseError } from "../shared/helpers.js";
5
5
  import { ToolName } from "../shared/types.js";
6
6
  import { CreateGitHubIssue } from "../shared/gql-queries.js";
7
+ import { formatIssueCreated } from "./format.js";
7
8
  const InputSchema = z.object({
8
9
  title: z.string().describe("The title of the GitHub issue"),
9
10
  body: z
10
11
  .string()
11
- .describe("The body/description of the GitHub issue (supports Markdown)"),
12
+ .optional()
13
+ .describe("The body/description of the GitHub issue (supports Markdown). Kept verbatim, above anything Wopee.io adds from the test run."),
12
14
  labels: z
13
15
  .array(z.string())
14
16
  .optional()
15
17
  .describe("Optional labels to apply to the issue (e.g., ['bug', 'testing'])"),
18
+ executedTestCaseUuid: z
19
+ .string()
20
+ .optional()
21
+ .describe("UUID of the executed test case the issue is about (the executedTestCaseUuid shown by wopee_fetch_recent_executions and wopee_dispatch_agent, or uuid in wopee_fetch_executed_test_cases). When set, Wopee.io adds the failing step, expected vs actual, the failing step's screenshot and a link to the run in Commander, so the body only needs your own analysis."),
16
22
  });
17
23
  export const wopeeCreateGithubIssue = {
18
24
  name: ToolName.WOPEE_CREATE_GITHUB_ISSUE,
19
25
  config: {
20
26
  title: "Create GitHub issue",
21
- description: "Create a new GitHub issue in the project's connected repository. Use this to report bugs found during testing, track test failures, or create action items from chat discussions. The issue will be created in the GitHub repository linked to the current project. Requires the project to have GitHub integration configured and WOPEE_PROJECT_UUID to be set.",
27
+ description: "Create a new GitHub issue in the project's connected repository. Use this to report bugs found during testing, track test failures, or create action items from chat discussions. For a failed test, pass its executedTestCaseUuid so the issue carries the run's evidence (failing step, expected vs actual, screenshot, report link). The issue is created by the Wopee.io GitHub App and names the requester in its body. Requires a GitHub repository linked to the project and WOPEE_PROJECT_UUID to be set.",
22
28
  inputSchema: InputSchema.shape,
23
29
  },
24
30
  handler: async (input) => {
@@ -33,15 +39,18 @@ export const wopeeCreateGithubIssue = {
33
39
  const result = await requestClient(CreateGitHubIssue, {
34
40
  projectUuid: WOPEE_PROJECT_UUID,
35
41
  title: input.title,
36
- body: input.body,
42
+ body: input.body ?? "",
37
43
  labels: input.labels || [],
44
+ ...(input.executedTestCaseUuid && {
45
+ executedTestCaseUuid: input.executedTestCaseUuid,
46
+ }),
38
47
  });
39
48
  if (!result?.createGitHubIssue)
40
49
  return {
41
50
  content: [
42
51
  {
43
52
  type: "text",
44
- text: "Failed to create GitHub issue. Make sure the project has GitHub integration configured.",
53
+ text: "Failed to create GitHub issue. Make sure the project has a GitHub repository linked.",
45
54
  },
46
55
  ],
47
56
  };
@@ -49,7 +58,9 @@ export const wopeeCreateGithubIssue = {
49
58
  content: [
50
59
  {
51
60
  type: "text",
52
- text: `GitHub issue created successfully:\n- Title: ${result.createGitHubIssue.title}\n- Number: #${result.createGitHubIssue.number}\n- URL: ${result.createGitHubIssue.url}`,
61
+ text: formatIssueCreated(result.createGitHubIssue, {
62
+ withRunEvidence: !!input.executedTestCaseUuid,
63
+ }),
53
64
  },
54
65
  ],
55
66
  };
@@ -52,6 +52,8 @@ export const wopeeFetchRecentExecutions = {
52
52
  // INCOMPLETE on top, and reporting it as FAILED tells the user their app is broken.
53
53
  const status = tc.agentReportStatus ?? tc.executionStatus;
54
54
  lines.push(`- ${tc.userStoryId}:${tc.testCaseId} [${tc.analysisIdentifier}] → ${status} (${tc.updatedAt})`);
55
+ // The handle wopee_create_github_issue takes to attach this run's evidence.
56
+ lines.push(` executedTestCaseUuid: ${tc.uuid}`);
55
57
  // Any report, not only a FINISHED run's: an incomplete run's report is what names the
56
58
  // infrastructure error behind it.
57
59
  if (tc.agentReport) {
@@ -1,6 +1,6 @@
1
1
  import { getConfig } from "../../utils/getConfig.js";
2
2
  import { requestClient } from "../../utils/requestClient.js";
3
- import { _parseError } from "../shared/helpers.js";
3
+ import { _describeError, _parseError } from "../shared/helpers.js";
4
4
  import { ArtifactType, ToolName, } from "../shared/types.js";
5
5
  import { FetchAnalysisSuites, FetchArtifact, FetchExecutedTestCases, } from "../shared/gql-queries.js";
6
6
  import { WopeeFetchTestInventoryInputSchema, } from "./schema.js";
@@ -9,7 +9,7 @@ export const wopeeFetchTestInventory = {
9
9
  name: ToolName.WOPEE_FETCH_TEST_INVENTORY,
10
10
  config: {
11
11
  title: "Fetch test inventory (counts + statuses)",
12
- description: "The authoritative tool for how many tests exist and their latest status. Returns, per analysis, the FULL list of authored test cases joined with their latest execution status — including never-run ones as NOT_RUN. Use this for questions like 'how many tests do I have', 'list the scenarios/test cases in A001', or 'show executed and not-run tests in one table'. Terminology: a 'scenario' is a test case; test cases are grouped under user stories (US001) and identified as US001:TC001. Reusable blocks (user story R001) are counted separately (reusableBlockCount) and are building blocks, not runnable, so they never carry an execution status. Regular tests are all non-R001 test cases. Read-only. Takes an optional analysisIdentifier (e.g. A001) to scope to one analysis; omit to cover every analysis in the project. Prefer this over wopee_fetch_recent_executions / wopee_fetch_executed_test_cases when the user asks about totals or the complete list — those return only test cases that have already run.",
12
+ description: "The authoritative tool for how many tests exist and their latest status. Returns, per analysis, the FULL list of authored test cases joined with their latest execution status — including never-run ones as NOT_RUN. Use this for questions like 'how many tests do I have', 'list the scenarios/test cases in A001', or 'show executed and not-run tests in one table'. Terminology: a 'scenario' is a test case; test cases are grouped under user stories (US001) and identified as US001:TC001. Reusable blocks (user story R001) are counted separately (reusableBlockCount) and are building blocks, not runnable, so they never carry an execution status. Regular tests are all non-R001 test cases. Read-only. Takes an optional analysisIdentifier (e.g. A001) to scope to one analysis; omit to cover every analysis in the project. Prefer this over wopee_fetch_recent_executions / wopee_fetch_executed_test_cases when the user asks about totals or the complete list — those return only test cases that have already run. Each test case carries executedTestCaseUuid (the latest run's uuid, the same one wopee_dispatch_agent returns) and verdictGateDisagreed (true when a verdict gate disagreed with a PASSED verdict — report it alongside the pass). Status UNKNOWN means the execution history could not be read (the reason is in the analysis's executionHistoryError); do not report such tests as not run.",
13
13
  inputSchema: WopeeFetchTestInventoryInputSchema.shape,
14
14
  },
15
15
  handler: async (input) => {
@@ -38,9 +38,11 @@ export const wopeeFetchTestInventory = {
38
38
  ],
39
39
  };
40
40
  // Fetch the authored test cases (USER_STORIES artifact) and execution
41
- // history for every analysis in parallel. A failure on one suite (e.g. an
42
- // analysis with no generated tests yet) degrades to an empty result for
43
- // that suite rather than failing the whole inventory.
41
+ // history for every analysis in parallel. A missing artifact (an analysis
42
+ // with no generated tests yet) degrades to an empty list rather than
43
+ // failing the whole inventory. A failed execution fetch does NOT degrade to
44
+ // "no runs": an analysis with no runs returns [], so a rejection is a real
45
+ // error (e.g. Not Authorised!) and is reported as UNKNOWN.
44
46
  const inputs = await Promise.all(suites.map(async (suite) => {
45
47
  const [artifactRes, execRes] = await Promise.allSettled([
46
48
  requestClient(FetchArtifact, {
@@ -59,6 +61,8 @@ export const wopeeFetchTestInventory = {
59
61
  },
60
62
  }),
61
63
  ]);
64
+ if (execRes.status === "rejected")
65
+ console.error(execRes.reason);
62
66
  return {
63
67
  suite,
64
68
  artifactContent: artifactRes.status === "fulfilled"
@@ -67,6 +71,9 @@ export const wopeeFetchTestInventory = {
67
71
  execGroups: execRes.status === "fulfilled"
68
72
  ? (execRes.value?.fetchExecutedTestCases ?? [])
69
73
  : [],
74
+ execError: execRes.status === "rejected"
75
+ ? _describeError(execRes.reason)
76
+ : null,
70
77
  };
71
78
  }));
72
79
  const inventory = buildTestInventory(inputs);
@@ -3,6 +3,10 @@
3
3
  // test cases at generation time (api generateReusableTestCases.ts).
4
4
  export const REUSABLE_USER_STORY_ID = "R001";
5
5
  export const NOT_RUN = "NOT_RUN";
6
+ // The execution history could not be read (e.g. the credential is not allowed to
7
+ // read executions), so whether a test ran is unknown. Never collapsed into
8
+ // NOT_RUN: "never ran" and "could not ask" must not look the same.
9
+ export const UNKNOWN = "UNKNOWN";
6
10
  /** Parse the USER_STORIES artifact JSON; tolerant of null/empty/malformed. */
7
11
  export function parseUserStories(content) {
8
12
  if (!content)
@@ -50,7 +54,11 @@ export function resolveStatus(exec) {
50
54
  return report;
51
55
  return exec.executionStatus ?? NOT_RUN;
52
56
  }
53
- function buildAnalysisInventory({ suite, artifactContent, execGroups, }) {
57
+ /** Mirrors CMD rowVerdict: only a pass is split, a flagged fail is already red. */
58
+ export function verdictGateDisagreed(exec, status) {
59
+ return !!exec?.integrityFlagged && status.toUpperCase() === "PASSED";
60
+ }
61
+ function buildAnalysisInventory({ suite, artifactContent, execGroups, execError = null, }) {
54
62
  const userStories = parseUserStories(artifactContent);
55
63
  const execIndex = indexExecutions(execGroups);
56
64
  const testCases = [];
@@ -69,7 +77,7 @@ function buildAnalysisInventory({ suite, artifactContent, execGroups, }) {
69
77
  const exec = isReusable
70
78
  ? undefined
71
79
  : execIndex.get(`${story.userStoryId}::${tc.testCaseId}`)?.[0];
72
- const status = resolveStatus(exec);
80
+ const status = execError && !isReusable ? UNKNOWN : resolveStatus(exec);
73
81
  if (!isReusable)
74
82
  statusSummary[status] = (statusSummary[status] ?? 0) + 1;
75
83
  testCases.push({
@@ -82,6 +90,8 @@ function buildAnalysisInventory({ suite, artifactContent, execGroups, }) {
82
90
  kind: isReusable ? "REUSABLE" : "REGULAR",
83
91
  status,
84
92
  executedAt: exec?.updatedAt ?? exec?.createdAt ?? null,
93
+ executedTestCaseUuid: exec?.uuid ?? null,
94
+ verdictGateDisagreed: verdictGateDisagreed(exec, status),
85
95
  });
86
96
  }
87
97
  }
@@ -93,6 +103,7 @@ function buildAnalysisInventory({ suite, artifactContent, execGroups, }) {
93
103
  regularTestCount,
94
104
  reusableBlockCount,
95
105
  statusSummary,
106
+ executionHistoryError: execError,
96
107
  testCases,
97
108
  };
98
109
  }
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "bin": {
5
5
  "wopee-mcp": "./build/index.js"
6
6
  },
7
- "version": "1.30.1",
7
+ "version": "1.31.0",
8
8
  "mcpName": "io.github.Wopee-io/wopee-mcp",
9
9
  "description": "Wopee.io MCP server for autonomous testing platform",
10
10
  "main": "./build/index.js",