wopee-mcp 1.28.0 → 1.29.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.
- package/build/tools/index.js +2 -0
- package/build/tools/shared/artifactContent.js +24 -0
- package/build/tools/shared/handlers.js +2 -2
- package/build/tools/shared/helpers.js +11 -0
- package/build/tools/shared/types.js +1 -0
- package/build/tools/wopee_fetch_test_inventory/index.js +83 -0
- package/build/tools/wopee_fetch_test_inventory/logic.js +115 -0
- package/build/tools/wopee_fetch_test_inventory/schema.js +8 -0
- package/package.json +3 -2
package/build/tools/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { wopeeCreateBlankSuite } from "./wopee_create_blank_suite/index.js";
|
|
|
7
7
|
import { wopeeFetchAnalysisSuites } from "./wopee_fetch_analysis_suites/index.js";
|
|
8
8
|
import { wopeeFetchExecutedTestCases } from "./wopee_fetch_executed_test_cases/index.js";
|
|
9
9
|
import { wopeeFetchRecentExecutions } from "./wopee_fetch_recent_executions/index.js";
|
|
10
|
+
import { wopeeFetchTestInventory } from "./wopee_fetch_test_inventory/index.js";
|
|
10
11
|
import { wopeeSendChatMessage } from "./wopee_send_chat_message/index.js";
|
|
11
12
|
import { wopeeReadChatHistory } from "./wopee_read_chat_history/index.js";
|
|
12
13
|
import { wopeeCreateGithubIssue } from "./wopee_create_github_issue/index.js";
|
|
@@ -17,6 +18,7 @@ export const TOOLS = [
|
|
|
17
18
|
wopeeFetchAnalysisSuites,
|
|
18
19
|
wopeeFetchExecutedTestCases,
|
|
19
20
|
wopeeFetchRecentExecutions,
|
|
21
|
+
wopeeFetchTestInventory,
|
|
20
22
|
wopeeFetchVariables,
|
|
21
23
|
wopeeUpdateVariables,
|
|
22
24
|
wopeeDispatchAnalysis,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip pretty-printing from `tests.json` before it enters the model's context.
|
|
3
|
+
*
|
|
4
|
+
* The api commits the USER_STORIES artifact indented so the diff of a test-case
|
|
5
|
+
* edit is reviewable in a GitHub PR (autonomous-testing/backlog#4252). That
|
|
6
|
+
* formatting is for humans reading a diff — `fetchArtifact` hands the file
|
|
7
|
+
* straight to the model, where the extra whitespace is pure token cost and buys
|
|
8
|
+
* nothing. `fetch_project_summary` pulls this artifact for every suite in a
|
|
9
|
+
* project, so the cost is multiplied.
|
|
10
|
+
*
|
|
11
|
+
* Fails open: content that does not parse is returned untouched. A broken
|
|
12
|
+
* artifact is still worth showing the model — it can say so.
|
|
13
|
+
*
|
|
14
|
+
* Kept free of enum imports so `node --test` (strip-only TS) can cover it; the
|
|
15
|
+
* artifact-type gate lives in `_compactArtifactContentForModel`.
|
|
16
|
+
*/
|
|
17
|
+
export function compactUserStoriesContent(content) {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.stringify(JSON.parse(content));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return content;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -2,7 +2,7 @@ import { FetchArtifactInputSchema, UpdateArtifactInputSchema, GenerateAIDataInpu
|
|
|
2
2
|
import { FetchArtifact, UpdateArtifact } from "./gql-queries.js";
|
|
3
3
|
import { createFetchArtifactInput, createUpdateArtifactInput, createGenerateAIDataInput, } from "./factories.js";
|
|
4
4
|
import { requestClient } from "../../utils/requestClient.js";
|
|
5
|
-
import { _convertToArtifactType, _parseError, _parseGenerateArtifactType, } from "./helpers.js";
|
|
5
|
+
import { _compactArtifactContentForModel, _convertToArtifactType, _parseError, _parseGenerateArtifactType, } from "./helpers.js";
|
|
6
6
|
export async function fetchArtifact(input) {
|
|
7
7
|
try {
|
|
8
8
|
const { type, suiteUuid, identifier } = input;
|
|
@@ -28,7 +28,7 @@ export async function fetchArtifact(input) {
|
|
|
28
28
|
content: [
|
|
29
29
|
{
|
|
30
30
|
type: "text",
|
|
31
|
-
text: result.fetchArtifact.content,
|
|
31
|
+
text: _compactArtifactContentForModel(type, result.fetchArtifact.content),
|
|
32
32
|
},
|
|
33
33
|
],
|
|
34
34
|
};
|
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { GenerateAppContext, GenerateGeneralUserStories, GenerateReusableTestCases, GenerateReusableTestCaseSteps, GenerateTestCases, GenerateTestCaseSteps, GenerateUserStoriesWithTestCases, } from "./gql-queries.js";
|
|
3
3
|
import { ArtifactType, GenerateArtifactType } from "./types.js";
|
|
4
4
|
import { RequestError } from "../../utils/requestClient.js";
|
|
5
|
+
import { compactUserStoriesContent } from "./artifactContent.js";
|
|
5
6
|
export function _convertToArtifactType(type) {
|
|
6
7
|
switch (type) {
|
|
7
8
|
case GenerateArtifactType.APP_CONTEXT:
|
|
@@ -95,3 +96,13 @@ export function _parseError(error) {
|
|
|
95
96
|
],
|
|
96
97
|
};
|
|
97
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Only USER_STORIES is JSON: APP_CONTEXT / GENERAL_USER_STORIES are markdown and
|
|
101
|
+
* PLAYWRIGHT_CODE is TypeScript, where collapsing whitespace would corrupt the
|
|
102
|
+
* file. See `compactUserStoriesContent` for why the artifact is compacted at all.
|
|
103
|
+
*/
|
|
104
|
+
export function _compactArtifactContentForModel(type, content) {
|
|
105
|
+
if (type !== ArtifactType.USER_STORIES)
|
|
106
|
+
return content;
|
|
107
|
+
return compactUserStoriesContent(content);
|
|
108
|
+
}
|
|
@@ -4,6 +4,7 @@ export var ToolName;
|
|
|
4
4
|
ToolName["WOPEE_FETCH_ANALYSIS_SUITES"] = "wopee_fetch_analysis_suites";
|
|
5
5
|
ToolName["WOPEE_FETCH_EXECUTED_TEST_CASES"] = "wopee_fetch_executed_test_cases";
|
|
6
6
|
ToolName["WOPEE_FETCH_RECENT_EXECUTIONS"] = "wopee_fetch_recent_executions";
|
|
7
|
+
ToolName["WOPEE_FETCH_TEST_INVENTORY"] = "wopee_fetch_test_inventory";
|
|
7
8
|
ToolName["WOPEE_DISPATCH_ANALYSIS"] = "wopee_dispatch_analysis";
|
|
8
9
|
ToolName["WOPEE_DISPATCH_AGENT"] = "wopee_dispatch_agent";
|
|
9
10
|
ToolName["WOPEE_FETCH_ARTIFACT"] = "wopee_fetch_artifact";
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { getConfig } from "../../utils/getConfig.js";
|
|
2
|
+
import { requestClient } from "../../utils/requestClient.js";
|
|
3
|
+
import { _parseError } from "../shared/helpers.js";
|
|
4
|
+
import { ArtifactType, ToolName, } from "../shared/types.js";
|
|
5
|
+
import { FetchAnalysisSuites, FetchArtifact, FetchExecutedTestCases, } from "../shared/gql-queries.js";
|
|
6
|
+
import { WopeeFetchTestInventoryInputSchema, } from "./schema.js";
|
|
7
|
+
import { buildTestInventory } from "./logic.js";
|
|
8
|
+
export const wopeeFetchTestInventory = {
|
|
9
|
+
name: ToolName.WOPEE_FETCH_TEST_INVENTORY,
|
|
10
|
+
config: {
|
|
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.",
|
|
13
|
+
inputSchema: WopeeFetchTestInventoryInputSchema.shape,
|
|
14
|
+
},
|
|
15
|
+
handler: async (input) => {
|
|
16
|
+
try {
|
|
17
|
+
const { WOPEE_PROJECT_UUID } = getConfig();
|
|
18
|
+
if (!WOPEE_PROJECT_UUID)
|
|
19
|
+
return {
|
|
20
|
+
content: [
|
|
21
|
+
{ type: "text", text: "WOPEE_PROJECT_UUID is not set" },
|
|
22
|
+
],
|
|
23
|
+
};
|
|
24
|
+
const parsed = WopeeFetchTestInventoryInputSchema.parse(input ?? {});
|
|
25
|
+
const suitesResult = await requestClient(FetchAnalysisSuites, { projectUuid: WOPEE_PROJECT_UUID });
|
|
26
|
+
let suites = suitesResult?.fetchAnalysisSuites ?? [];
|
|
27
|
+
if (parsed.analysisIdentifier)
|
|
28
|
+
suites = suites.filter((s) => s.analysisIdentifier === parsed.analysisIdentifier);
|
|
29
|
+
if (suites.length === 0)
|
|
30
|
+
return {
|
|
31
|
+
content: [
|
|
32
|
+
{
|
|
33
|
+
type: "text",
|
|
34
|
+
text: parsed.analysisIdentifier
|
|
35
|
+
? `No analysis found with identifier ${parsed.analysisIdentifier}.`
|
|
36
|
+
: "No analyses found in this project yet.",
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
};
|
|
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.
|
|
44
|
+
const inputs = await Promise.all(suites.map(async (suite) => {
|
|
45
|
+
const [artifactRes, execRes] = await Promise.allSettled([
|
|
46
|
+
requestClient(FetchArtifact, {
|
|
47
|
+
input: {
|
|
48
|
+
projectUuid: WOPEE_PROJECT_UUID,
|
|
49
|
+
suiteUuid: suite.uuid,
|
|
50
|
+
type: ArtifactType.USER_STORIES,
|
|
51
|
+
ref: suite.analysisIdentifier ?? undefined,
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
54
|
+
requestClient(FetchExecutedTestCases, {
|
|
55
|
+
input: {
|
|
56
|
+
projectUuid: WOPEE_PROJECT_UUID,
|
|
57
|
+
analysisSuiteUuid: suite.uuid,
|
|
58
|
+
analysisIdentifier: suite.analysisIdentifier ?? "",
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
]);
|
|
62
|
+
return {
|
|
63
|
+
suite,
|
|
64
|
+
artifactContent: artifactRes.status === "fulfilled"
|
|
65
|
+
? (artifactRes.value?.fetchArtifact?.content ?? null)
|
|
66
|
+
: null,
|
|
67
|
+
execGroups: execRes.status === "fulfilled"
|
|
68
|
+
? (execRes.value?.fetchExecutedTestCases ?? [])
|
|
69
|
+
: [],
|
|
70
|
+
};
|
|
71
|
+
}));
|
|
72
|
+
const inventory = buildTestInventory(inputs);
|
|
73
|
+
return {
|
|
74
|
+
content: [
|
|
75
|
+
{ type: "text", text: JSON.stringify(inventory, null, 2) },
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
return _parseError(error);
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// The special user story that holds reusable blocks (building blocks referenced
|
|
2
|
+
// by other test cases' steps, not independently runnable). Forced onto reusable
|
|
3
|
+
// test cases at generation time (api generateReusableTestCases.ts).
|
|
4
|
+
export const REUSABLE_USER_STORY_ID = "R001";
|
|
5
|
+
export const NOT_RUN = "NOT_RUN";
|
|
6
|
+
/** Parse the USER_STORIES artifact JSON; tolerant of null/empty/malformed. */
|
|
7
|
+
export function parseUserStories(content) {
|
|
8
|
+
if (!content)
|
|
9
|
+
return [];
|
|
10
|
+
try {
|
|
11
|
+
const parsed = JSON.parse(content);
|
|
12
|
+
const stories = parsed?.userStories;
|
|
13
|
+
return Array.isArray(stories) ? stories : [];
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Index every execution by `${userStoryId}::${testCaseId}`, newest first.
|
|
21
|
+
* Sorted by createdAt descending so index[key][0] is the latest run.
|
|
22
|
+
*/
|
|
23
|
+
export function indexExecutions(groups) {
|
|
24
|
+
const map = new Map();
|
|
25
|
+
for (const grp of groups ?? []) {
|
|
26
|
+
for (const etc of grp.executedTestCases ?? []) {
|
|
27
|
+
const key = `${etc.userStoryId}::${etc.testCaseId}`;
|
|
28
|
+
const list = map.get(key);
|
|
29
|
+
if (list)
|
|
30
|
+
list.push(etc);
|
|
31
|
+
else
|
|
32
|
+
map.set(key, [etc]);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
for (const list of map.values())
|
|
36
|
+
list.sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1));
|
|
37
|
+
return map;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Latest-run verdict, matching CMD rowVerdict precedence: the agent report is
|
|
41
|
+
* the trace-verified verdict, then the code report, then the raw execution
|
|
42
|
+
* status, else "not run". Report statuses (PASSED/FAILED/INCOMPLETE) win over
|
|
43
|
+
* a bare FINISHED — a FINISHED run can still be a FAILED verdict.
|
|
44
|
+
*/
|
|
45
|
+
export function resolveStatus(exec) {
|
|
46
|
+
if (!exec)
|
|
47
|
+
return NOT_RUN;
|
|
48
|
+
const report = exec.agentReportStatus || exec.codeReportStatus;
|
|
49
|
+
if (report)
|
|
50
|
+
return report;
|
|
51
|
+
return exec.executionStatus ?? NOT_RUN;
|
|
52
|
+
}
|
|
53
|
+
function buildAnalysisInventory({ suite, artifactContent, execGroups, }) {
|
|
54
|
+
const userStories = parseUserStories(artifactContent);
|
|
55
|
+
const execIndex = indexExecutions(execGroups);
|
|
56
|
+
const testCases = [];
|
|
57
|
+
const statusSummary = {};
|
|
58
|
+
let regularTestCount = 0;
|
|
59
|
+
let reusableBlockCount = 0;
|
|
60
|
+
for (const story of userStories) {
|
|
61
|
+
const isReusable = story.userStoryId === REUSABLE_USER_STORY_ID;
|
|
62
|
+
for (const tc of story.testCases ?? []) {
|
|
63
|
+
if (isReusable)
|
|
64
|
+
reusableBlockCount += 1;
|
|
65
|
+
else
|
|
66
|
+
regularTestCount += 1;
|
|
67
|
+
// Reusable blocks are not independently runnable, so they never carry an
|
|
68
|
+
// execution status.
|
|
69
|
+
const exec = isReusable
|
|
70
|
+
? undefined
|
|
71
|
+
: execIndex.get(`${story.userStoryId}::${tc.testCaseId}`)?.[0];
|
|
72
|
+
const status = resolveStatus(exec);
|
|
73
|
+
if (!isReusable)
|
|
74
|
+
statusSummary[status] = (statusSummary[status] ?? 0) + 1;
|
|
75
|
+
testCases.push({
|
|
76
|
+
id: `${story.userStoryId}:${tc.testCaseId}`,
|
|
77
|
+
userStoryId: story.userStoryId,
|
|
78
|
+
userStoryName: story.userStoryName ?? "",
|
|
79
|
+
userStoryCategory: story.userStoryCategory ?? "",
|
|
80
|
+
testCaseId: tc.testCaseId,
|
|
81
|
+
name: tc.name ?? "",
|
|
82
|
+
kind: isReusable ? "REUSABLE" : "REGULAR",
|
|
83
|
+
status,
|
|
84
|
+
executedAt: exec?.updatedAt ?? exec?.createdAt ?? null,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
analysisIdentifier: suite.analysisIdentifier,
|
|
90
|
+
suiteUuid: suite.uuid,
|
|
91
|
+
name: suite.name,
|
|
92
|
+
suiteRunningStatus: suite.suiteRunningStatus,
|
|
93
|
+
regularTestCount,
|
|
94
|
+
reusableBlockCount,
|
|
95
|
+
statusSummary,
|
|
96
|
+
testCases,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Assemble the full test inventory across analyses: regular vs reusable counts,
|
|
101
|
+
* and every authored test case with its latest execution status (never-run =
|
|
102
|
+
* NOT_RUN). This is the single deterministic answer for "how many tests",
|
|
103
|
+
* "list scenarios", and "executed + not-run in one list".
|
|
104
|
+
*/
|
|
105
|
+
export function buildTestInventory(inputs) {
|
|
106
|
+
const analyses = inputs.map(buildAnalysisInventory);
|
|
107
|
+
return {
|
|
108
|
+
totals: {
|
|
109
|
+
analyses: analyses.length,
|
|
110
|
+
regularTests: analyses.reduce((n, a) => n + a.regularTestCount, 0),
|
|
111
|
+
reusableBlocks: analyses.reduce((n, a) => n + a.reusableBlockCount, 0),
|
|
112
|
+
},
|
|
113
|
+
analyses,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const WopeeFetchTestInventoryInputSchema = z.object({
|
|
3
|
+
analysisIdentifier: z
|
|
4
|
+
.string({
|
|
5
|
+
description: "Optional analysis identifier (e.g. A001) to scope the inventory to a single analysis. Omit to include every analysis in the project.",
|
|
6
|
+
})
|
|
7
|
+
.optional(),
|
|
8
|
+
});
|
package/package.json
CHANGED
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
"bin": {
|
|
5
5
|
"wopee-mcp": "./build/index.js"
|
|
6
6
|
},
|
|
7
|
-
"version": "1.
|
|
7
|
+
"version": "1.29.1",
|
|
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",
|
|
11
11
|
"scripts": {
|
|
12
12
|
"build": "tsc && chmod 755 build/index.js",
|
|
13
|
-
"build:clean": "rm -rf build && npm run build"
|
|
13
|
+
"build:clean": "rm -rf build && npm run build",
|
|
14
|
+
"test": "node --test \"src/**/*.test.ts\""
|
|
14
15
|
},
|
|
15
16
|
"files": [
|
|
16
17
|
"build",
|