@alternative-path/qa-path-mcp 1.0.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/LICENSE +23 -0
- package/QUICK_INSTALL.md +133 -0
- package/README.md +226 -0
- package/TOOLS_DOCUMENTATION.md +675 -0
- package/dist/__tests__/tools/module-tools.test.d.ts +2 -0
- package/dist/__tests__/tools/module-tools.test.d.ts.map +1 -0
- package/dist/__tests__/tools/module-tools.test.js +145 -0
- package/dist/__tests__/tools/module-tools.test.js.map +1 -0
- package/dist/__tests__/tools/project-tools.test.d.ts +2 -0
- package/dist/__tests__/tools/project-tools.test.d.ts.map +1 -0
- package/dist/__tests__/tools/project-tools.test.js +674 -0
- package/dist/__tests__/tools/project-tools.test.js.map +1 -0
- package/dist/__tests__/tools/query-tools.test.d.ts +2 -0
- package/dist/__tests__/tools/query-tools.test.d.ts.map +1 -0
- package/dist/__tests__/tools/query-tools.test.js +225 -0
- package/dist/__tests__/tools/query-tools.test.js.map +1 -0
- package/dist/__tests__/tools/testgroup-launch-tools.test.d.ts +2 -0
- package/dist/__tests__/tools/testgroup-launch-tools.test.d.ts.map +1 -0
- package/dist/__tests__/tools/testgroup-launch-tools.test.js +553 -0
- package/dist/__tests__/tools/testgroup-launch-tools.test.js.map +1 -0
- package/dist/__tests__/utils/mcp-error-mapper.test.d.ts +2 -0
- package/dist/__tests__/utils/mcp-error-mapper.test.d.ts.map +1 -0
- package/dist/__tests__/utils/mcp-error-mapper.test.js +240 -0
- package/dist/__tests__/utils/mcp-error-mapper.test.js.map +1 -0
- package/dist/__tests__/utils/mcp-response.test.d.ts +2 -0
- package/dist/__tests__/utils/mcp-response.test.d.ts.map +1 -0
- package/dist/__tests__/utils/mcp-response.test.js +72 -0
- package/dist/__tests__/utils/mcp-response.test.js.map +1 -0
- package/dist/agents/test-planner-context.d.ts +7 -0
- package/dist/agents/test-planner-context.d.ts.map +1 -0
- package/dist/agents/test-planner-context.js +283 -0
- package/dist/agents/test-planner-context.js.map +1 -0
- package/dist/agents/test-planner-prompt.d.ts +34 -0
- package/dist/agents/test-planner-prompt.d.ts.map +1 -0
- package/dist/agents/test-planner-prompt.js +82 -0
- package/dist/agents/test-planner-prompt.js.map +1 -0
- package/dist/api-client.d.ts +52 -0
- package/dist/api-client.d.ts.map +1 -0
- package/dist/api-client.js +285 -0
- package/dist/api-client.js.map +1 -0
- package/dist/constants.d.ts +7 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +7 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +175 -0
- package/dist/index.js.map +1 -0
- package/dist/services/project-context-service.d.ts +15 -0
- package/dist/services/project-context-service.d.ts.map +1 -0
- package/dist/services/project-context-service.js +36 -0
- package/dist/services/project-context-service.js.map +1 -0
- package/dist/tools/auth-tools.d.ts +16 -0
- package/dist/tools/auth-tools.d.ts.map +1 -0
- package/dist/tools/auth-tools.js +66 -0
- package/dist/tools/auth-tools.js.map +1 -0
- package/dist/tools/automation-tools.d.ts +28 -0
- package/dist/tools/automation-tools.d.ts.map +1 -0
- package/dist/tools/automation-tools.js +541 -0
- package/dist/tools/automation-tools.js.map +1 -0
- package/dist/tools/export-import-tools.d.ts +18 -0
- package/dist/tools/export-import-tools.d.ts.map +1 -0
- package/dist/tools/export-import-tools.js +61 -0
- package/dist/tools/export-import-tools.js.map +1 -0
- package/dist/tools/module-tools.d.ts +43 -0
- package/dist/tools/module-tools.d.ts.map +1 -0
- package/dist/tools/module-tools.js +289 -0
- package/dist/tools/module-tools.js.map +1 -0
- package/dist/tools/project-context-tools.d.ts +19 -0
- package/dist/tools/project-context-tools.d.ts.map +1 -0
- package/dist/tools/project-context-tools.js +133 -0
- package/dist/tools/project-context-tools.js.map +1 -0
- package/dist/tools/project-tools.d.ts +47 -0
- package/dist/tools/project-tools.d.ts.map +1 -0
- package/dist/tools/project-tools.js +362 -0
- package/dist/tools/project-tools.js.map +1 -0
- package/dist/tools/query-tools.d.ts +22 -0
- package/dist/tools/query-tools.d.ts.map +1 -0
- package/dist/tools/query-tools.js +127 -0
- package/dist/tools/query-tools.js.map +1 -0
- package/dist/tools/testcase-tools.d.ts +135 -0
- package/dist/tools/testcase-tools.d.ts.map +1 -0
- package/dist/tools/testcase-tools.js +845 -0
- package/dist/tools/testcase-tools.js.map +1 -0
- package/dist/tools/testgroup-launch-tools.d.ts +37 -0
- package/dist/tools/testgroup-launch-tools.d.ts.map +1 -0
- package/dist/tools/testgroup-launch-tools.js +727 -0
- package/dist/tools/testgroup-launch-tools.js.map +1 -0
- package/dist/utils/mcp-error-mapper.d.ts +27 -0
- package/dist/utils/mcp-error-mapper.d.ts.map +1 -0
- package/dist/utils/mcp-error-mapper.js +79 -0
- package/dist/utils/mcp-error-mapper.js.map +1 -0
- package/dist/utils/mcp-response.d.ts +26 -0
- package/dist/utils/mcp-response.d.ts.map +1 -0
- package/dist/utils/mcp-response.js +34 -0
- package/dist/utils/mcp-response.js.map +1 -0
- package/package.json +60 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { jsonResponse } from "../utils/mcp-response.js";
|
|
2
|
+
import { API_KEY } from "../constants.js";
|
|
3
|
+
export class AuthTools {
|
|
4
|
+
apiClient;
|
|
5
|
+
constructor(apiClient) {
|
|
6
|
+
this.apiClient = apiClient;
|
|
7
|
+
}
|
|
8
|
+
getTools() {
|
|
9
|
+
return [
|
|
10
|
+
{
|
|
11
|
+
name: "check_auth_status",
|
|
12
|
+
description: "Check if the MCP server is currently authenticated with the API. When authenticated, returns the logged-in user (id, username, user_email).",
|
|
13
|
+
inputSchema: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
handles(name) {
|
|
21
|
+
return name === "check_auth_status";
|
|
22
|
+
}
|
|
23
|
+
async handle(name, args) {
|
|
24
|
+
if (name === "check_auth_status") {
|
|
25
|
+
return await this.checkAuthStatus(args);
|
|
26
|
+
}
|
|
27
|
+
throw new Error(`Unknown auth tool: ${name}`);
|
|
28
|
+
}
|
|
29
|
+
async checkAuthStatus(args) {
|
|
30
|
+
const isAuthenticated = this.apiClient.getIsAuthenticated();
|
|
31
|
+
if (!isAuthenticated) {
|
|
32
|
+
return jsonResponse({
|
|
33
|
+
authenticated: false,
|
|
34
|
+
message: `MCP server is not authenticated. Set ${API_KEY} and restart the MCP server.`,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const response = await this.apiClient.get("auth/getCurrentLoggedInUser");
|
|
39
|
+
const user = response.data?.user;
|
|
40
|
+
const roles = user?.roles;
|
|
41
|
+
const roleNames = Array.isArray(roles)
|
|
42
|
+
? roles.map((r) => (typeof r === "string" ? r : r.name ?? String(r)))
|
|
43
|
+
: undefined;
|
|
44
|
+
return jsonResponse({
|
|
45
|
+
authenticated: true,
|
|
46
|
+
message: "MCP server is authenticated",
|
|
47
|
+
user: user
|
|
48
|
+
? {
|
|
49
|
+
id: user.id,
|
|
50
|
+
username: user.username,
|
|
51
|
+
user_email: user.user_email,
|
|
52
|
+
roles: roleNames ?? user.roles,
|
|
53
|
+
}
|
|
54
|
+
: undefined,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
return jsonResponse({
|
|
59
|
+
authenticated: false,
|
|
60
|
+
message: `Session may have expired or could not retrieve user. Set ${API_KEY} and restart the MCP server.`,
|
|
61
|
+
error: error?.message || String(error),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=auth-tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-tools.js","sourceRoot":"","sources":["../../src/tools/auth-tools.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAE1C,MAAM,OAAO,SAAS;IACA;IAApB,YAAoB,SAAoB;QAApB,cAAS,GAAT,SAAS,CAAW;IAAG,CAAC;IAE5C,QAAQ;QACN,OAAO;YACL;gBACE,IAAI,EAAE,mBAAmB;gBACzB,WAAW,EACT,6IAA6I;gBAC/I,WAAW,EAAE;oBACX,IAAI,EAAE,QAAQ;oBACd,UAAU,EAAE,EAAE;iBACf;aACF;SACF,CAAC;IACJ,CAAC;IAED,OAAO,CAAC,IAAY;QAClB,OAAO,IAAI,KAAK,mBAAmB,CAAC;IACtC,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,IAAY,EAAE,IAAS;QAClC,IAAI,IAAI,KAAK,mBAAmB,EAAE,CAAC;YACjC,OAAO,MAAM,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;QAC1C,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,sBAAsB,IAAI,EAAE,CAAC,CAAC;IAChD,CAAC;IAEO,KAAK,CAAC,eAAe,CAAC,IAAS;QACrC,MAAM,eAAe,GAAG,IAAI,CAAC,SAAS,CAAC,kBAAkB,EAAE,CAAC;QAE5D,IAAI,CAAC,eAAe,EAAE,CAAC;YACrB,OAAO,YAAY,CAAC;gBAClB,aAAa,EAAE,KAAK;gBACpB,OAAO,EACL,wCAAwC,OAAO,8BAA8B;aAChF,CAAC,CAAC;QACL,CAAC;QAED,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,GAAG,CAYtC,6BAA6B,CAAC,CAAC;YAElC,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;YACjC,MAAM,KAAK,GAAG,IAAI,EAAE,KAAK,CAAC;YAC1B,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;gBACpC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAE,CAAuB,CAAC,IAAI,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC5F,CAAC,CAAC,SAAS,CAAC;YACd,OAAO,YAAY,CAAC;gBAClB,aAAa,EAAE,IAAI;gBACnB,OAAO,EAAE,6BAA6B;gBACtC,IAAI,EAAE,IAAI;oBACR,CAAC,CAAC;wBACE,EAAE,EAAE,IAAI,CAAC,EAAE;wBACX,QAAQ,EAAE,IAAI,CAAC,QAAQ;wBACvB,UAAU,EAAE,IAAI,CAAC,UAAU;wBAC3B,KAAK,EAAE,SAAS,IAAI,IAAI,CAAC,KAAK;qBAC/B;oBACH,CAAC,CAAC,SAAS;aACd,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAU,EAAE,CAAC;YACpB,OAAO,YAAY,CAAC;gBAClB,aAAa,EAAE,KAAK;gBACpB,OAAO,EACL,4DAA4D,OAAO,8BAA8B;gBACnG,KAAK,EAAE,KAAK,EAAE,OAAO,IAAI,MAAM,CAAC,KAAK,CAAC;aACvC,CAAC,CAAC;QACL,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import { ApiClient } from "../api-client.js";
|
|
3
|
+
import { ProjectContextService } from "../services/project-context-service.js";
|
|
4
|
+
export declare class AutomationTools {
|
|
5
|
+
private apiClient;
|
|
6
|
+
private projectContext;
|
|
7
|
+
constructor(apiClient: ApiClient, projectContext: ProjectContextService);
|
|
8
|
+
getTools(): Tool[];
|
|
9
|
+
handles(name: string): boolean;
|
|
10
|
+
handle(name: string, args: any): Promise<{
|
|
11
|
+
content: {
|
|
12
|
+
type: "text";
|
|
13
|
+
text: string;
|
|
14
|
+
}[];
|
|
15
|
+
}>;
|
|
16
|
+
private getCurrentUserInfo;
|
|
17
|
+
private getOrganizationServiceUrl;
|
|
18
|
+
private formatInstructions;
|
|
19
|
+
private runExecutionOverWebSocket;
|
|
20
|
+
private generateNlpScript;
|
|
21
|
+
private formatToolOutput;
|
|
22
|
+
private parseExecutionTime;
|
|
23
|
+
private parseStatus;
|
|
24
|
+
private sanitizeForJSON;
|
|
25
|
+
private formattedResponse;
|
|
26
|
+
private jsonResponse;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=automation-tools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"automation-tools.d.ts","sourceRoot":"","sources":["../../src/tools/automation-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,oCAAoC,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,qBAAqB,EAAE,MAAM,wCAAwC,CAAC;AAO/E,qBAAa,eAAe;IAExB,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,cAAc;gBADd,SAAS,EAAE,SAAS,EACpB,cAAc,EAAE,qBAAqB;IAG/C,QAAQ,IAAI,IAAI,EAAE;IAqClB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAIxB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG;;;;;;YAOtB,kBAAkB;YAkBlB,yBAAyB;IAgBvC,OAAO,CAAC,kBAAkB;YASZ,yBAAyB;YAqKzB,iBAAiB;IA6M/B,OAAO,CAAC,gBAAgB;IAuDxB,OAAO,CAAC,kBAAkB;IAW1B,OAAO,CAAC,WAAW;IAanB,OAAO,CAAC,eAAe;IAyBvB,OAAO,CAAC,iBAAiB;IAyBzB,OAAO,CAAC,YAAY;CAGrB"}
|
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
import { textResponse } from "../utils/mcp-response.js";
|
|
2
|
+
import { io } from "socket.io-client";
|
|
3
|
+
const WS_PATH = "/test-automation/socket.io/";
|
|
4
|
+
const EXECUTION_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
5
|
+
export class AutomationTools {
|
|
6
|
+
apiClient;
|
|
7
|
+
projectContext;
|
|
8
|
+
constructor(apiClient, projectContext) {
|
|
9
|
+
this.apiClient = apiClient;
|
|
10
|
+
this.projectContext = projectContext;
|
|
11
|
+
}
|
|
12
|
+
getTools() {
|
|
13
|
+
return [
|
|
14
|
+
{
|
|
15
|
+
name: "generate_nlp_script",
|
|
16
|
+
description: "Generate an automation script from NLP instructions using the Script Creator flow: runs the AI agent to interpret and execute the instructions, then saves the generated script to the specified test case. Returns execution logs and test steps in the response (not streamed in real time). Requires modelId (UUID). If the user provided a model by name, call list_llm_models first, map the name to the id from the response, then call this tool with that modelId.",
|
|
17
|
+
inputSchema: {
|
|
18
|
+
type: "object",
|
|
19
|
+
properties: {
|
|
20
|
+
nlpInstructions: {
|
|
21
|
+
type: "string",
|
|
22
|
+
description: "NLP instructions (one per line or newline-separated). Example: 'open https://example.com\\nClick on Login\\nEnter username and password'",
|
|
23
|
+
},
|
|
24
|
+
modelId: {
|
|
25
|
+
type: "string",
|
|
26
|
+
description: "ID of the AI model to use for automation (required). Get ids from list_llm_models. If the user gave a model by name, call list_llm_models and map the name to the id, then pass it here.",
|
|
27
|
+
},
|
|
28
|
+
testCaseId: {
|
|
29
|
+
type: "string",
|
|
30
|
+
description: "UUID of the test case where the generated automation script will be saved (required for save step).",
|
|
31
|
+
},
|
|
32
|
+
projectId: {
|
|
33
|
+
type: "string",
|
|
34
|
+
description: "Optional project ID (UUID). If not provided, uses the active project set via 'set_active_project'. " +
|
|
35
|
+
"If no active project is set, the tool will return an error with instructions on how to set one.",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
required: ["nlpInstructions", "testCaseId"],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
handles(name) {
|
|
44
|
+
return name === "generate_nlp_script";
|
|
45
|
+
}
|
|
46
|
+
async handle(name, args) {
|
|
47
|
+
if (name === "generate_nlp_script") {
|
|
48
|
+
return await this.generateNlpScript(args);
|
|
49
|
+
}
|
|
50
|
+
throw new Error(`Unknown automation tool: ${name}`);
|
|
51
|
+
}
|
|
52
|
+
async getCurrentUserInfo() {
|
|
53
|
+
const response = await this.apiClient.get("auth/getCurrentLoggedInUser");
|
|
54
|
+
const user = response.data?.user;
|
|
55
|
+
if (!user?.id) {
|
|
56
|
+
throw new Error("Could not get current user. Please ensure you are logged in.");
|
|
57
|
+
}
|
|
58
|
+
if (!user.org_id) {
|
|
59
|
+
throw new Error("Organization ID is required but not found in user session. Please ensure you are logged in with a valid user account.");
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
userId: user.id,
|
|
63
|
+
organizationId: user.org_id,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
async getOrganizationServiceUrl(projectId) {
|
|
67
|
+
try {
|
|
68
|
+
const url = projectId
|
|
69
|
+
? `organization/getServiceUrl?projectId=${projectId}`
|
|
70
|
+
: `organization/getServiceUrl`;
|
|
71
|
+
const response = await this.apiClient.get(url);
|
|
72
|
+
return response.data?.agentServiceUrl;
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
// If organization service URL fetch fails, return undefined to use default
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
formatInstructions(nlpInstructions) {
|
|
80
|
+
// Match frontend format: split into array of {id, text} objects
|
|
81
|
+
const lines = nlpInstructions
|
|
82
|
+
.split(/\r?\n/)
|
|
83
|
+
.map((s) => s.trim())
|
|
84
|
+
.filter(Boolean);
|
|
85
|
+
return lines.map((text, i) => ({ id: i + 1, text }));
|
|
86
|
+
}
|
|
87
|
+
async runExecutionOverWebSocket(params) {
|
|
88
|
+
const { instructionsArray, modelId, projectId, userId, organizationId, wsUrl: providedWsUrl } = params;
|
|
89
|
+
const wsUrl = providedWsUrl;
|
|
90
|
+
if (!wsUrl || typeof wsUrl !== "string" || wsUrl.trim() === "") {
|
|
91
|
+
throw new Error("WebSocket URL is required for automation. Ensure the organization service returns agentServiceUrl.");
|
|
92
|
+
}
|
|
93
|
+
let origin;
|
|
94
|
+
try {
|
|
95
|
+
origin = new URL(wsUrl).origin;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
throw new Error("Could not derive Origin from WebSocket URL. Ensure the organization service returns a valid agentServiceUrl.");
|
|
99
|
+
}
|
|
100
|
+
const uniqueId = `mcp_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
101
|
+
const executionLogs = [];
|
|
102
|
+
const testSteps = [];
|
|
103
|
+
let tokenUsage = null;
|
|
104
|
+
const urlsToTry = [wsUrl];
|
|
105
|
+
try {
|
|
106
|
+
const baseUrl = this.apiClient.client?.defaults?.baseURL;
|
|
107
|
+
if (baseUrl)
|
|
108
|
+
urlsToTry.push(baseUrl);
|
|
109
|
+
}
|
|
110
|
+
catch (_) { }
|
|
111
|
+
let cookieString = "";
|
|
112
|
+
for (const u of urlsToTry) {
|
|
113
|
+
try {
|
|
114
|
+
const c = await this.apiClient.getCookieStringForUrl(u);
|
|
115
|
+
if (c && c.length > cookieString.length)
|
|
116
|
+
cookieString = c;
|
|
117
|
+
}
|
|
118
|
+
catch (_) { }
|
|
119
|
+
}
|
|
120
|
+
return new Promise((resolve) => {
|
|
121
|
+
let socket = null;
|
|
122
|
+
let replayPlan = null;
|
|
123
|
+
let testCompleteResult = null;
|
|
124
|
+
let resolved = false;
|
|
125
|
+
const finish = (err) => {
|
|
126
|
+
if (resolved)
|
|
127
|
+
return;
|
|
128
|
+
resolved = true;
|
|
129
|
+
if (socket) {
|
|
130
|
+
socket.removeAllListeners();
|
|
131
|
+
socket.disconnect();
|
|
132
|
+
}
|
|
133
|
+
if (err) {
|
|
134
|
+
resolve({ plan: null, error: err, executionLogs, testSteps, tokenUsage });
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
resolve({
|
|
138
|
+
plan: replayPlan || testCompleteResult?.testPlan,
|
|
139
|
+
error: testCompleteResult?.error,
|
|
140
|
+
executionLogs,
|
|
141
|
+
testSteps,
|
|
142
|
+
tokenUsage,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const timeout = setTimeout(() => {
|
|
147
|
+
finish("Execution timed out (5 minutes). The automation agent may still be running.");
|
|
148
|
+
}, EXECUTION_TIMEOUT_MS);
|
|
149
|
+
try {
|
|
150
|
+
const socketOptions = {
|
|
151
|
+
path: WS_PATH,
|
|
152
|
+
transports: ["polling", "websocket"],
|
|
153
|
+
reconnection: false,
|
|
154
|
+
forceNew: true,
|
|
155
|
+
withCredentials: true,
|
|
156
|
+
extraHeaders: {
|
|
157
|
+
Origin: origin,
|
|
158
|
+
...(cookieString && { Cookie: cookieString }),
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
// Log connection attempt for debugging
|
|
162
|
+
socket = io(wsUrl, socketOptions);
|
|
163
|
+
socket.on("connect", () => {
|
|
164
|
+
// Match frontend format exactly: pass instructions array directly
|
|
165
|
+
socket.emit("executeTest", {
|
|
166
|
+
testId: uniqueId,
|
|
167
|
+
instructions: instructionsArray,
|
|
168
|
+
model: modelId,
|
|
169
|
+
environmentId: null,
|
|
170
|
+
executionContext: {
|
|
171
|
+
projectId,
|
|
172
|
+
organizationId,
|
|
173
|
+
userId,
|
|
174
|
+
},
|
|
175
|
+
projectId,
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
socket.on("connect_error", (err) => {
|
|
179
|
+
clearTimeout(timeout);
|
|
180
|
+
const msg = err?.message || "Connection failed";
|
|
181
|
+
const extra = [err?.description, err?.context].filter(Boolean).join("; ");
|
|
182
|
+
finish(extra ? `${msg}. ${extra}` : msg);
|
|
183
|
+
});
|
|
184
|
+
socket.on("disconnect", (reason) => {
|
|
185
|
+
// Disconnect handled silently
|
|
186
|
+
});
|
|
187
|
+
socket.on("log", (data) => {
|
|
188
|
+
if (data && typeof data === "object") {
|
|
189
|
+
const logMessage = data.message ?? data.text ?? JSON.stringify(data);
|
|
190
|
+
executionLogs.push({
|
|
191
|
+
...data,
|
|
192
|
+
timestamp: data.timestamp ?? Date.now(),
|
|
193
|
+
message: logMessage,
|
|
194
|
+
level: data.level ?? "info",
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
socket.on("testStep", (step) => {
|
|
199
|
+
if (step && typeof step === "object") {
|
|
200
|
+
testSteps.push({
|
|
201
|
+
...step,
|
|
202
|
+
timestamp: step.timestamp ?? Date.now(),
|
|
203
|
+
id: step.id ?? `step_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
socket.on("tokenUsage", (usage) => {
|
|
208
|
+
if (usage && typeof usage === "object")
|
|
209
|
+
tokenUsage = usage;
|
|
210
|
+
});
|
|
211
|
+
socket.on("replayPlan", (data) => {
|
|
212
|
+
if (data)
|
|
213
|
+
replayPlan = data;
|
|
214
|
+
});
|
|
215
|
+
socket.on("testComplete", (result) => {
|
|
216
|
+
testCompleteResult = result;
|
|
217
|
+
clearTimeout(timeout);
|
|
218
|
+
finish(result?.error);
|
|
219
|
+
});
|
|
220
|
+
socket.on("error", (err) => {
|
|
221
|
+
clearTimeout(timeout);
|
|
222
|
+
finish(err?.message || String(err));
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
clearTimeout(timeout);
|
|
227
|
+
finish(err?.message || String(err));
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
async generateNlpScript(args) {
|
|
232
|
+
const { nlpInstructions, modelId, testCaseId } = args;
|
|
233
|
+
const projectId = this.projectContext.resolveProjectId(args, "generate_nlp_script");
|
|
234
|
+
if (!nlpInstructions || typeof nlpInstructions !== "string") {
|
|
235
|
+
return this.jsonResponse({
|
|
236
|
+
success: false,
|
|
237
|
+
message: "nlpInstructions (string) is required.",
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
if (!testCaseId) {
|
|
241
|
+
return this.jsonResponse({
|
|
242
|
+
success: false,
|
|
243
|
+
message: "testCaseId is required.",
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
if (!modelId) {
|
|
247
|
+
return this.jsonResponse({
|
|
248
|
+
success: false,
|
|
249
|
+
error_code: "MODEL_REQUIRED",
|
|
250
|
+
message: "modelId is required.",
|
|
251
|
+
ACTION_REQUIRED: [
|
|
252
|
+
"Call list_llm_models to fetch the available LLM models.",
|
|
253
|
+
"If the user provided a model by name, map it to the id from the list (e.g. match by name).",
|
|
254
|
+
"Ask the user to choose one if needed, then retry generate_nlp_script with modelId set to the chosen id.",
|
|
255
|
+
],
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
// Format instructions to match frontend format: array of {id, text} objects
|
|
259
|
+
const instructionsArray = this.formatInstructions(nlpInstructions);
|
|
260
|
+
const { userId, organizationId } = await this.getCurrentUserInfo();
|
|
261
|
+
// Get organization-specific WebSocket URL
|
|
262
|
+
const orgServiceUrl = await this.getOrganizationServiceUrl(projectId);
|
|
263
|
+
const { plan, error: executionError, executionLogs, testSteps, tokenUsage, } = await this.runExecutionOverWebSocket({
|
|
264
|
+
instructionsArray,
|
|
265
|
+
modelId,
|
|
266
|
+
projectId,
|
|
267
|
+
userId,
|
|
268
|
+
organizationId,
|
|
269
|
+
wsUrl: orgServiceUrl,
|
|
270
|
+
});
|
|
271
|
+
// Helper to sanitize arrays for JSON serialization
|
|
272
|
+
const sanitizeArray = (arr, fields) => {
|
|
273
|
+
return arr.map(item => {
|
|
274
|
+
const sanitized = {};
|
|
275
|
+
for (const field of fields) {
|
|
276
|
+
if (item[field] !== undefined && item[field] !== null) {
|
|
277
|
+
sanitized[field] = item[field];
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return sanitized;
|
|
281
|
+
});
|
|
282
|
+
};
|
|
283
|
+
if (executionError) {
|
|
284
|
+
const sanitizedLogs = sanitizeArray(executionLogs.slice(-100), ['message', 'level', 'timestamp']);
|
|
285
|
+
const sanitizedSteps = sanitizeArray(testSteps.slice(-50), ['stepDescription', 'passed', 'timestamp', 'id']);
|
|
286
|
+
return this.jsonResponse({
|
|
287
|
+
success: false,
|
|
288
|
+
message: "Automation execution failed.",
|
|
289
|
+
error: executionError,
|
|
290
|
+
executionLogs: sanitizedLogs,
|
|
291
|
+
testSteps: sanitizedSteps,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
if (!plan || !plan.steps) {
|
|
295
|
+
const sanitizedLogs = sanitizeArray(executionLogs.slice(-100), ['message', 'level', 'timestamp']);
|
|
296
|
+
const sanitizedSteps = sanitizeArray(testSteps.slice(-50), ['stepDescription', 'passed', 'timestamp', 'id']);
|
|
297
|
+
return this.jsonResponse({
|
|
298
|
+
success: false,
|
|
299
|
+
message: "No automation plan was generated. The agent may not have produced steps.",
|
|
300
|
+
executionLogs: sanitizedLogs,
|
|
301
|
+
testSteps: sanitizedSteps,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
await this.apiClient.put(`${projectId}/auto-execution/updateExecutionRecord`, {
|
|
306
|
+
testCaseId,
|
|
307
|
+
plan,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
return this.jsonResponse({
|
|
312
|
+
success: false,
|
|
313
|
+
message: "Failed to save execution record to test case.",
|
|
314
|
+
error: err?.message || String(err),
|
|
315
|
+
executionLogs: executionLogs.slice(-100),
|
|
316
|
+
testSteps: testSteps.slice(-50),
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
// instructionsArray is already in the correct format: Array<{id: number, text: string}>
|
|
320
|
+
// Ensure all text fields are non-empty strings and ids are integers
|
|
321
|
+
const automationInstruction = instructionsArray
|
|
322
|
+
.filter(item => item.text && item.text.trim().length > 0)
|
|
323
|
+
.map(item => ({
|
|
324
|
+
id: Number(item.id),
|
|
325
|
+
text: String(item.text).trim(),
|
|
326
|
+
}));
|
|
327
|
+
if (automationInstruction.length === 0) {
|
|
328
|
+
// No valid automation instructions to save
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
try {
|
|
332
|
+
await this.apiClient.put(`${projectId}/auto-execution/updateAutomationInstruction`, { testCaseId, automationInstruction });
|
|
333
|
+
}
|
|
334
|
+
catch (err) {
|
|
335
|
+
const sanitizedTestSteps = testSteps.slice(-50).map(step => ({
|
|
336
|
+
stepDescription: step.stepDescription,
|
|
337
|
+
expectedOutcome: step.expectedOutcome,
|
|
338
|
+
actualOutcome: step.actualOutcome,
|
|
339
|
+
confidence: step.confidence,
|
|
340
|
+
passed: step.passed,
|
|
341
|
+
reason: step.reason,
|
|
342
|
+
isFinal: step.isFinal,
|
|
343
|
+
timestamp: step.timestamp,
|
|
344
|
+
screenshotPath: step.screenshotPath,
|
|
345
|
+
executionId: step.executionId,
|
|
346
|
+
id: step.id,
|
|
347
|
+
}));
|
|
348
|
+
const sanitizedExecutionLogs = executionLogs.slice(-100).map(log => ({
|
|
349
|
+
message: log.message,
|
|
350
|
+
level: log.level,
|
|
351
|
+
timestamp: log.timestamp,
|
|
352
|
+
}));
|
|
353
|
+
return this.jsonResponse({
|
|
354
|
+
success: true,
|
|
355
|
+
message: "Execution record saved, but automation instruction text could not be updated.",
|
|
356
|
+
testCaseId,
|
|
357
|
+
executionRecordSaved: true,
|
|
358
|
+
error: err?.message || String(err),
|
|
359
|
+
executionLogs: sanitizedExecutionLogs,
|
|
360
|
+
testSteps: sanitizedTestSteps,
|
|
361
|
+
stepsCount: Array.isArray(plan.steps) ? plan.steps.length : 0,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const sanitizedTestSteps = testSteps.slice(-50).map(step => ({
|
|
366
|
+
stepDescription: step.stepDescription,
|
|
367
|
+
expectedOutcome: step.expectedOutcome,
|
|
368
|
+
actualOutcome: step.actualOutcome,
|
|
369
|
+
confidence: step.confidence,
|
|
370
|
+
passed: step.passed,
|
|
371
|
+
reason: step.reason,
|
|
372
|
+
isFinal: step.isFinal,
|
|
373
|
+
timestamp: step.timestamp,
|
|
374
|
+
screenshotPath: step.screenshotPath,
|
|
375
|
+
executionId: step.executionId,
|
|
376
|
+
locator: step.locator,
|
|
377
|
+
code: step.code,
|
|
378
|
+
duration: step.duration,
|
|
379
|
+
id: step.id,
|
|
380
|
+
playwrightAssertion: step.playwrightAssertion ? {
|
|
381
|
+
code: step.playwrightAssertion.code,
|
|
382
|
+
type: step.playwrightAssertion.type,
|
|
383
|
+
description: step.playwrightAssertion.description,
|
|
384
|
+
passed: step.playwrightAssertion.passed,
|
|
385
|
+
comment: step.playwrightAssertion.comment,
|
|
386
|
+
} : null,
|
|
387
|
+
}));
|
|
388
|
+
const sanitizedExecutionLogs = executionLogs.slice(-100).map(log => ({
|
|
389
|
+
message: log.message,
|
|
390
|
+
level: log.level,
|
|
391
|
+
timestamp: log.timestamp,
|
|
392
|
+
}));
|
|
393
|
+
return this.jsonResponse({
|
|
394
|
+
success: true,
|
|
395
|
+
message: "NLP script generated and saved to the test case successfully.",
|
|
396
|
+
testCaseId,
|
|
397
|
+
stepsCount: Array.isArray(plan.steps) ? plan.steps.length : 0,
|
|
398
|
+
executionLogs: sanitizedExecutionLogs,
|
|
399
|
+
testSteps: sanitizedTestSteps,
|
|
400
|
+
tokenUsage: tokenUsage ? {
|
|
401
|
+
input: tokenUsage.input,
|
|
402
|
+
output: tokenUsage.output,
|
|
403
|
+
total: tokenUsage.total,
|
|
404
|
+
iterations: tokenUsage.iterations?.length || 0,
|
|
405
|
+
} : undefined,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
formatToolOutput(obj) {
|
|
409
|
+
const success = obj.success === true;
|
|
410
|
+
const message = obj.message || "";
|
|
411
|
+
if (success && obj.models?.length) {
|
|
412
|
+
const names = obj.models.map((m) => m.name ?? m.id).join(", ");
|
|
413
|
+
return `**${message}**\n\nAvailable models: ${names}`;
|
|
414
|
+
}
|
|
415
|
+
if (!success && !obj.executionLogs?.length) {
|
|
416
|
+
let out = `**${message}**`;
|
|
417
|
+
if (obj.error)
|
|
418
|
+
out += `\n\nError: ${obj.error}`;
|
|
419
|
+
if (obj.models?.length) {
|
|
420
|
+
out += `\n\nAvailable models: ${obj.models.map((m) => m.name ?? m.id).join(", ")}`;
|
|
421
|
+
}
|
|
422
|
+
return out;
|
|
423
|
+
}
|
|
424
|
+
if (!success) {
|
|
425
|
+
let out = `**${message}**`;
|
|
426
|
+
if (obj.error)
|
|
427
|
+
out += `\n\nError: ${obj.error}`;
|
|
428
|
+
const passed = obj.testSteps?.filter((s) => s?.passed).length ?? 0;
|
|
429
|
+
const total = obj.testSteps?.length ?? 0;
|
|
430
|
+
if (total > 0)
|
|
431
|
+
out += `\n\nSteps executed: ${passed}/${total}`;
|
|
432
|
+
return out;
|
|
433
|
+
}
|
|
434
|
+
const stepsCount = obj.stepsCount ?? 0;
|
|
435
|
+
const testSteps = Array.isArray(obj.testSteps) ? obj.testSteps : [];
|
|
436
|
+
const passed = testSteps.filter((s) => s?.passed).length;
|
|
437
|
+
const total = testSteps.length || stepsCount;
|
|
438
|
+
const executionTime = this.parseExecutionTime(obj.executionLogs);
|
|
439
|
+
const status = this.parseStatus(obj.executionLogs) ?? (passed === total && total > 0 ? "PASSED" : null);
|
|
440
|
+
let out = "**The NLP automation script ran successfully and was saved to the test case.**\n\n";
|
|
441
|
+
out += "| | |\n|---|---|\n";
|
|
442
|
+
out += `| **Test case** | ${obj.testCaseId ?? "—"} |\n`;
|
|
443
|
+
out += `| **Status** | ${status ?? "—"} |\n`;
|
|
444
|
+
out += `| **Steps in plan** | ${stepsCount} |\n`;
|
|
445
|
+
out += `| **Passed** | ${total > 0 ? `${passed}/${total}` : "—"} |\n`;
|
|
446
|
+
if (executionTime)
|
|
447
|
+
out += `| **Execution time** | ${executionTime} |\n`;
|
|
448
|
+
if (testSteps.length > 0) {
|
|
449
|
+
out += "\n**What was generated**\n\n";
|
|
450
|
+
testSteps.forEach((s, i) => {
|
|
451
|
+
const desc = s.stepDescription ?? s.code ?? `Step ${i + 1}`;
|
|
452
|
+
out += `- ${desc}\n`;
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
out += "\nThe plan includes Playwright steps and assertions. Both the execution record (plan) and the automation instruction text were saved to the test case. You can open the test case in QA Path to view or run the script.";
|
|
456
|
+
return out;
|
|
457
|
+
}
|
|
458
|
+
parseExecutionTime(logs) {
|
|
459
|
+
if (!Array.isArray(logs))
|
|
460
|
+
return null;
|
|
461
|
+
const line = logs.find((l) => {
|
|
462
|
+
const msg = l?.message ?? l?.text ?? "";
|
|
463
|
+
return typeof msg === "string" && msg.includes("Execution Time:");
|
|
464
|
+
});
|
|
465
|
+
const msg = line?.message ?? line?.text ?? "";
|
|
466
|
+
const match = typeof msg === "string" && msg.match(/Execution Time:\s*([\d.]+s?)/);
|
|
467
|
+
return match ? `~${match[1]}` : null;
|
|
468
|
+
}
|
|
469
|
+
parseStatus(logs) {
|
|
470
|
+
if (!Array.isArray(logs))
|
|
471
|
+
return null;
|
|
472
|
+
const line = logs.find((l) => {
|
|
473
|
+
const msg = l?.message ?? l?.text ?? "";
|
|
474
|
+
return typeof msg === "string" && (msg.includes("Status: PASSED") || msg.includes("Status: FAILED"));
|
|
475
|
+
});
|
|
476
|
+
const msg = line?.message ?? line?.text ?? "";
|
|
477
|
+
if (typeof msg !== "string")
|
|
478
|
+
return null;
|
|
479
|
+
if (msg.includes("Status: PASSED"))
|
|
480
|
+
return "PASSED";
|
|
481
|
+
if (msg.includes("Status: FAILED"))
|
|
482
|
+
return "FAILED";
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
sanitizeForJSON(obj) {
|
|
486
|
+
if (obj === null || obj === undefined)
|
|
487
|
+
return obj;
|
|
488
|
+
if (typeof obj !== 'object')
|
|
489
|
+
return obj;
|
|
490
|
+
if (Array.isArray(obj)) {
|
|
491
|
+
return obj.map(item => this.sanitizeForJSON(item));
|
|
492
|
+
}
|
|
493
|
+
const sanitized = {};
|
|
494
|
+
for (const key in obj) {
|
|
495
|
+
if (obj.hasOwnProperty(key)) {
|
|
496
|
+
const value = obj[key];
|
|
497
|
+
// Skip functions, undefined, and circular references
|
|
498
|
+
if (typeof value === 'function' || value === undefined) {
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
try {
|
|
502
|
+
sanitized[key] = this.sanitizeForJSON(value);
|
|
503
|
+
}
|
|
504
|
+
catch (e) {
|
|
505
|
+
// Skip circular references
|
|
506
|
+
sanitized[key] = '[Circular]';
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return sanitized;
|
|
511
|
+
}
|
|
512
|
+
formattedResponse(obj) {
|
|
513
|
+
const summary = this.formatToolOutput(obj);
|
|
514
|
+
// Sanitize the object to remove circular references and non-serializable values
|
|
515
|
+
const sanitized = this.sanitizeForJSON(obj);
|
|
516
|
+
let jsonString;
|
|
517
|
+
try {
|
|
518
|
+
jsonString = JSON.stringify(sanitized, null, 2);
|
|
519
|
+
}
|
|
520
|
+
catch (error) {
|
|
521
|
+
// Fallback: create a simplified version if JSON.stringify still fails
|
|
522
|
+
const simplified = {
|
|
523
|
+
success: sanitized.success,
|
|
524
|
+
message: sanitized.message,
|
|
525
|
+
testCaseId: sanitized.testCaseId,
|
|
526
|
+
stepsCount: sanitized.stepsCount,
|
|
527
|
+
error: sanitized.error,
|
|
528
|
+
executionRecordSaved: sanitized.executionRecordSaved,
|
|
529
|
+
executionLogs: sanitized.executionLogs?.length || 0,
|
|
530
|
+
testSteps: sanitized.testSteps?.length || 0,
|
|
531
|
+
};
|
|
532
|
+
jsonString = JSON.stringify(simplified, null, 2);
|
|
533
|
+
}
|
|
534
|
+
const text = summary + "\n\n<details>\n<summary>Full response (JSON)</summary>\n\n```json\n" + jsonString + "\n```\n</details>";
|
|
535
|
+
return textResponse(text);
|
|
536
|
+
}
|
|
537
|
+
jsonResponse(obj) {
|
|
538
|
+
return this.formattedResponse(obj);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
//# sourceMappingURL=automation-tools.js.map
|