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 +23 -0
- package/build/tools/shared/gql-queries.js +22 -2
- package/build/tools/shared/helpers.js +7 -6
- package/build/tools/wopee_create_github_issue/format.js +21 -0
- package/build/tools/wopee_create_github_issue/index.js +16 -5
- package/build/tools/wopee_fetch_recent_executions/index.js +2 -0
- package/build/tools/wopee_fetch_test_inventory/index.js +12 -5
- package/build/tools/wopee_fetch_test_inventory/logic.js +13 -2
- package/package.json +1 -1
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(
|
|
294
|
-
|
|
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
|
-
.
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
42
|
-
//
|
|
43
|
-
//
|
|
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
|
-
|
|
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