@muggleai/mcp 1.0.17 → 1.0.18
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/dist/chunk-RXCZWOOD.js +4823 -0
- package/dist/chunk-RXCZWOOD.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/index.js +2 -2
- package/dist/local-qa/contracts/project-schemas.d.ts +164 -1019
- package/dist/local-qa/contracts/project-schemas.d.ts.map +1 -1
- package/dist/local-qa/index.d.ts +9 -8
- package/dist/local-qa/index.d.ts.map +1 -1
- package/dist/local-qa/services/execution-service.d.ts +45 -24
- package/dist/local-qa/services/execution-service.d.ts.map +1 -1
- package/dist/local-qa/services/index.d.ts +1 -1
- package/dist/local-qa/services/index.d.ts.map +1 -1
- package/dist/local-qa/services/run-result-storage-service.d.ts +144 -0
- package/dist/local-qa/services/run-result-storage-service.d.ts.map +1 -0
- package/dist/local-qa/tools/tool-registry.d.ts +5 -1
- package/dist/local-qa/tools/tool-registry.d.ts.map +1 -1
- package/dist/qa/contracts/index.d.ts +52 -0
- package/dist/qa/contracts/index.d.ts.map +1 -1
- package/dist/qa/tools/tool-registry.d.ts.map +1 -1
- package/package.json +1 -1
- package/scripts/postinstall.mjs +13 -2
- package/dist/chunk-XKXZH7PD.js +0 -7804
- package/dist/chunk-XKXZH7PD.js.map +0 -1
- package/dist/local-qa/services/project-storage-service.d.ts +0 -262
- package/dist/local-qa/services/project-storage-service.d.ts.map +0 -1
|
@@ -0,0 +1,4823 @@
|
|
|
1
|
+
import * as fs4 from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import { platform } from 'os';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import winston from 'winston';
|
|
7
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
8
|
+
import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
9
|
+
import { v4 } from 'uuid';
|
|
10
|
+
import { z, ZodError } from 'zod';
|
|
11
|
+
import axios, { AxiosError } from 'axios';
|
|
12
|
+
import { exec } from 'child_process';
|
|
13
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
14
|
+
import * as fs6 from 'fs/promises';
|
|
15
|
+
|
|
16
|
+
var __defProp = Object.defineProperty;
|
|
17
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
18
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
19
|
+
}) : x)(function(x) {
|
|
20
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
21
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
22
|
+
});
|
|
23
|
+
var __export = (target, all) => {
|
|
24
|
+
for (var name in all)
|
|
25
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
26
|
+
};
|
|
27
|
+
var DEFAULT_PROMPT_SERVICE_URL = "https://promptservice.muggle-ai.com";
|
|
28
|
+
var DEFAULT_WEB_SERVICE_URL = "http://localhost:3001";
|
|
29
|
+
var DATA_DIR_NAME = ".muggle-ai";
|
|
30
|
+
var ELECTRON_APP_DIR = "electron-app";
|
|
31
|
+
var CREDENTIALS_FILE = "credentials.json";
|
|
32
|
+
var DEFAULT_AUTH0_DOMAIN = "login.muggle-ai.com";
|
|
33
|
+
var DEFAULT_AUTH0_CLIENT_ID = "UgG5UjoyLksxMciWWKqVpwfWrJ4rFvtT";
|
|
34
|
+
var DEFAULT_AUTH0_AUDIENCE = "https://muggleai.us.auth0.com/api/v2/";
|
|
35
|
+
var DEFAULT_AUTH0_SCOPE = "openid profile email offline_access";
|
|
36
|
+
var configInstance = null;
|
|
37
|
+
var muggleConfigCache = null;
|
|
38
|
+
function getPackageRoot() {
|
|
39
|
+
const currentFilePath = fileURLToPath(import.meta.url);
|
|
40
|
+
const currentDir = path.dirname(currentFilePath);
|
|
41
|
+
if (currentDir.includes(path.join("dist", "shared"))) {
|
|
42
|
+
return path.resolve(currentDir, "..", "..");
|
|
43
|
+
}
|
|
44
|
+
if (currentDir.endsWith("dist")) {
|
|
45
|
+
return path.resolve(currentDir, "..");
|
|
46
|
+
}
|
|
47
|
+
if (currentDir.includes(path.join("src", "shared"))) {
|
|
48
|
+
return path.resolve(currentDir, "..", "..");
|
|
49
|
+
}
|
|
50
|
+
return path.dirname(currentDir);
|
|
51
|
+
}
|
|
52
|
+
function getMuggleConfig() {
|
|
53
|
+
if (muggleConfigCache) {
|
|
54
|
+
return muggleConfigCache;
|
|
55
|
+
}
|
|
56
|
+
const packageRoot = getPackageRoot();
|
|
57
|
+
const packageJsonPath = path.join(packageRoot, "package.json");
|
|
58
|
+
let packageJson;
|
|
59
|
+
try {
|
|
60
|
+
const content = fs4.readFileSync(packageJsonPath, "utf-8");
|
|
61
|
+
packageJson = JSON.parse(content);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Failed to read package.json for muggleConfig.
|
|
66
|
+
Path: ${packageJsonPath}
|
|
67
|
+
Package root: ${packageRoot}
|
|
68
|
+
Error: ${errorMessage}
|
|
69
|
+
This is a bug - please report it.`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
const config = packageJson.muggleConfig;
|
|
73
|
+
if (!config) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Missing muggleConfig in package.json.
|
|
76
|
+
Path: ${packageJsonPath}
|
|
77
|
+
This is a bug - please report it.`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
if (!config.electronAppVersion || typeof config.electronAppVersion !== "string") {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`Missing or invalid muggleConfig.electronAppVersion in package.json.
|
|
83
|
+
Path: ${packageJsonPath}
|
|
84
|
+
Value: ${JSON.stringify(config.electronAppVersion)}
|
|
85
|
+
This is a bug - please report it.`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (!config.downloadBaseUrl || typeof config.downloadBaseUrl !== "string") {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Missing or invalid muggleConfig.downloadBaseUrl in package.json.
|
|
91
|
+
Path: ${packageJsonPath}
|
|
92
|
+
Value: ${JSON.stringify(config.downloadBaseUrl)}
|
|
93
|
+
This is a bug - please report it.`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
muggleConfigCache = {
|
|
97
|
+
electronAppVersion: config.electronAppVersion,
|
|
98
|
+
downloadBaseUrl: config.downloadBaseUrl,
|
|
99
|
+
checksums: config.checksums || {}
|
|
100
|
+
};
|
|
101
|
+
return muggleConfigCache;
|
|
102
|
+
}
|
|
103
|
+
function getDataDir() {
|
|
104
|
+
return path.join(os.homedir(), DATA_DIR_NAME);
|
|
105
|
+
}
|
|
106
|
+
function getDownloadedElectronAppPath() {
|
|
107
|
+
const platform3 = os.platform();
|
|
108
|
+
const config = getMuggleConfig();
|
|
109
|
+
const version = config.electronAppVersion;
|
|
110
|
+
const baseDir = path.join(getDataDir(), ELECTRON_APP_DIR, version);
|
|
111
|
+
let binaryPath;
|
|
112
|
+
switch (platform3) {
|
|
113
|
+
case "darwin":
|
|
114
|
+
binaryPath = path.join(baseDir, "MuggleAI.app", "Contents", "MacOS", "MuggleAI");
|
|
115
|
+
break;
|
|
116
|
+
case "win32":
|
|
117
|
+
binaryPath = path.join(baseDir, "MuggleAI.exe");
|
|
118
|
+
break;
|
|
119
|
+
case "linux":
|
|
120
|
+
binaryPath = path.join(baseDir, "MuggleAI");
|
|
121
|
+
break;
|
|
122
|
+
default:
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
if (fs4.existsSync(binaryPath)) {
|
|
126
|
+
return binaryPath;
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
function getSystemElectronAppPath() {
|
|
131
|
+
const platform3 = os.platform();
|
|
132
|
+
const homeDir = os.homedir();
|
|
133
|
+
let binaryPath;
|
|
134
|
+
switch (platform3) {
|
|
135
|
+
case "darwin":
|
|
136
|
+
binaryPath = path.join(homeDir, "Applications", "MuggleAI.app", "Contents", "MacOS", "MuggleAI");
|
|
137
|
+
break;
|
|
138
|
+
case "win32":
|
|
139
|
+
binaryPath = path.join(homeDir, "AppData", "Local", "Programs", "MuggleAI", "MuggleAI.exe");
|
|
140
|
+
break;
|
|
141
|
+
case "linux":
|
|
142
|
+
binaryPath = path.join(homeDir, ".local", "share", "muggle-ai", "MuggleAI");
|
|
143
|
+
break;
|
|
144
|
+
default:
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
if (fs4.existsSync(binaryPath)) {
|
|
148
|
+
return binaryPath;
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
function resolveElectronAppPathOrNull() {
|
|
153
|
+
const customPath = process.env.ELECTRON_APP_PATH;
|
|
154
|
+
if (customPath && fs4.existsSync(customPath)) {
|
|
155
|
+
return customPath;
|
|
156
|
+
}
|
|
157
|
+
const downloadedPath = getDownloadedElectronAppPath();
|
|
158
|
+
if (downloadedPath) {
|
|
159
|
+
return downloadedPath;
|
|
160
|
+
}
|
|
161
|
+
const systemPath = getSystemElectronAppPath();
|
|
162
|
+
if (systemPath) {
|
|
163
|
+
return systemPath;
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
function resolveWebServicePath() {
|
|
168
|
+
const customPath = process.env.WEB_SERVICE_PATH;
|
|
169
|
+
if (customPath && fs4.existsSync(customPath)) {
|
|
170
|
+
return customPath;
|
|
171
|
+
}
|
|
172
|
+
const packageRoot = getPackageRoot();
|
|
173
|
+
const siblingPath = path.resolve(packageRoot, "..", "web-service", "dist", "src", "index.js");
|
|
174
|
+
if (fs4.existsSync(siblingPath)) {
|
|
175
|
+
return siblingPath;
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
function parseInteger(value, defaultValue) {
|
|
180
|
+
if (value === void 0 || value === "") {
|
|
181
|
+
return defaultValue;
|
|
182
|
+
}
|
|
183
|
+
const parsed = parseInt(value, 10);
|
|
184
|
+
return isNaN(parsed) ? defaultValue : parsed;
|
|
185
|
+
}
|
|
186
|
+
function buildAuth0Config() {
|
|
187
|
+
return {
|
|
188
|
+
domain: process.env.AUTH0_DOMAIN ?? DEFAULT_AUTH0_DOMAIN,
|
|
189
|
+
clientId: process.env.AUTH0_CLIENT_ID ?? DEFAULT_AUTH0_CLIENT_ID,
|
|
190
|
+
audience: process.env.AUTH0_AUDIENCE ?? DEFAULT_AUTH0_AUDIENCE,
|
|
191
|
+
scope: process.env.AUTH0_SCOPE ?? DEFAULT_AUTH0_SCOPE
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function buildQaConfig() {
|
|
195
|
+
return {
|
|
196
|
+
promptServiceBaseUrl: process.env.PROMPT_SERVICE_BASE_URL ?? DEFAULT_PROMPT_SERVICE_URL,
|
|
197
|
+
requestTimeoutMs: parseInteger(process.env.REQUEST_TIMEOUT_MS, 3e4),
|
|
198
|
+
workflowTimeoutMs: parseInteger(process.env.WORKFLOW_TIMEOUT_MS, 12e4)
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function buildLocalQaConfig() {
|
|
202
|
+
const dataDir = getDataDir();
|
|
203
|
+
const auth0Scopes = (process.env.AUTH0_SCOPE ?? DEFAULT_AUTH0_SCOPE).split(" ");
|
|
204
|
+
return {
|
|
205
|
+
webServiceUrl: process.env.WEB_SERVICE_URL ?? DEFAULT_WEB_SERVICE_URL,
|
|
206
|
+
promptServiceUrl: process.env.PROMPT_SERVICE_BASE_URL ?? DEFAULT_PROMPT_SERVICE_URL,
|
|
207
|
+
dataDir,
|
|
208
|
+
sessionsDir: path.join(dataDir, "sessions"),
|
|
209
|
+
projectsDir: path.join(dataDir, "projects"),
|
|
210
|
+
tempDir: path.join(dataDir, "temp"),
|
|
211
|
+
credentialsFilePath: path.join(dataDir, CREDENTIALS_FILE),
|
|
212
|
+
authFilePath: path.join(dataDir, "auth.json"),
|
|
213
|
+
electronAppPath: resolveElectronAppPathOrNull(),
|
|
214
|
+
webServicePath: resolveWebServicePath(),
|
|
215
|
+
webServicePidFile: path.join(dataDir, "web-service.pid"),
|
|
216
|
+
auth0: {
|
|
217
|
+
domain: process.env.AUTH0_DOMAIN ?? DEFAULT_AUTH0_DOMAIN,
|
|
218
|
+
clientId: process.env.AUTH0_CLIENT_ID ?? DEFAULT_AUTH0_CLIENT_ID,
|
|
219
|
+
audience: process.env.AUTH0_AUDIENCE ?? DEFAULT_AUTH0_AUDIENCE,
|
|
220
|
+
scopes: auth0Scopes
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function getConfig() {
|
|
225
|
+
if (configInstance) {
|
|
226
|
+
return configInstance;
|
|
227
|
+
}
|
|
228
|
+
configInstance = {
|
|
229
|
+
serverName: "muggle-mcp",
|
|
230
|
+
serverVersion: "1.0.0",
|
|
231
|
+
logLevel: process.env.LOG_LEVEL ?? "info",
|
|
232
|
+
auth0: buildAuth0Config(),
|
|
233
|
+
qa: buildQaConfig(),
|
|
234
|
+
localQa: buildLocalQaConfig()
|
|
235
|
+
};
|
|
236
|
+
return configInstance;
|
|
237
|
+
}
|
|
238
|
+
function resetConfig() {
|
|
239
|
+
configInstance = null;
|
|
240
|
+
muggleConfigCache = null;
|
|
241
|
+
}
|
|
242
|
+
var VERSION_OVERRIDE_FILE = "electron-app-version-override.json";
|
|
243
|
+
var ELECTRON_APP_VERSION_ENV = "ELECTRON_APP_VERSION";
|
|
244
|
+
function getElectronAppVersion() {
|
|
245
|
+
const envVersion = process.env[ELECTRON_APP_VERSION_ENV];
|
|
246
|
+
if (envVersion && /^\d+\.\d+\.\d+$/.test(envVersion)) {
|
|
247
|
+
return envVersion;
|
|
248
|
+
}
|
|
249
|
+
const overridePath = path.join(getDataDir(), VERSION_OVERRIDE_FILE);
|
|
250
|
+
if (fs4.existsSync(overridePath)) {
|
|
251
|
+
try {
|
|
252
|
+
const content = JSON.parse(fs4.readFileSync(overridePath, "utf-8"));
|
|
253
|
+
if (content.version && typeof content.version === "string") {
|
|
254
|
+
return content.version;
|
|
255
|
+
}
|
|
256
|
+
} catch {
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return getMuggleConfig().electronAppVersion;
|
|
260
|
+
}
|
|
261
|
+
function getElectronAppVersionSource() {
|
|
262
|
+
const envVersion = process.env[ELECTRON_APP_VERSION_ENV];
|
|
263
|
+
if (envVersion && /^\d+\.\d+\.\d+$/.test(envVersion)) {
|
|
264
|
+
return "env";
|
|
265
|
+
}
|
|
266
|
+
const overridePath = path.join(getDataDir(), VERSION_OVERRIDE_FILE);
|
|
267
|
+
if (fs4.existsSync(overridePath)) {
|
|
268
|
+
try {
|
|
269
|
+
const content = JSON.parse(fs4.readFileSync(overridePath, "utf-8"));
|
|
270
|
+
if (content.version && typeof content.version === "string") {
|
|
271
|
+
return "override";
|
|
272
|
+
}
|
|
273
|
+
} catch {
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return "bundled";
|
|
277
|
+
}
|
|
278
|
+
function getBundledElectronAppVersion() {
|
|
279
|
+
return getMuggleConfig().electronAppVersion;
|
|
280
|
+
}
|
|
281
|
+
function getDownloadBaseUrl() {
|
|
282
|
+
return getMuggleConfig().downloadBaseUrl;
|
|
283
|
+
}
|
|
284
|
+
function getElectronAppChecksums() {
|
|
285
|
+
return getMuggleConfig().checksums;
|
|
286
|
+
}
|
|
287
|
+
function isElectronAppInstalled() {
|
|
288
|
+
return getDownloadedElectronAppPath() !== null;
|
|
289
|
+
}
|
|
290
|
+
function getElectronAppDir(version) {
|
|
291
|
+
const ver = version ?? getElectronAppVersion();
|
|
292
|
+
return path.join(getDataDir(), ELECTRON_APP_DIR, ver);
|
|
293
|
+
}
|
|
294
|
+
var loggerInstance = null;
|
|
295
|
+
function createLogger() {
|
|
296
|
+
const config = getConfig();
|
|
297
|
+
const format = winston.format.combine(
|
|
298
|
+
winston.format.timestamp(),
|
|
299
|
+
winston.format.json()
|
|
300
|
+
);
|
|
301
|
+
return winston.createLogger({
|
|
302
|
+
level: config.logLevel,
|
|
303
|
+
format,
|
|
304
|
+
defaultMeta: {
|
|
305
|
+
service: config.serverName,
|
|
306
|
+
version: config.serverVersion
|
|
307
|
+
},
|
|
308
|
+
transports: [
|
|
309
|
+
// Log to stderr to avoid interfering with MCP stdio transport
|
|
310
|
+
new winston.transports.Console({
|
|
311
|
+
stderrLevels: ["error", "warn", "info", "http", "verbose", "debug", "silly"]
|
|
312
|
+
})
|
|
313
|
+
]
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
function getLogger() {
|
|
317
|
+
if (!loggerInstance) {
|
|
318
|
+
loggerInstance = createLogger();
|
|
319
|
+
}
|
|
320
|
+
return loggerInstance;
|
|
321
|
+
}
|
|
322
|
+
function createChildLogger(correlationId) {
|
|
323
|
+
const logger5 = getLogger();
|
|
324
|
+
return logger5.child({ correlationId });
|
|
325
|
+
}
|
|
326
|
+
function resetLogger() {
|
|
327
|
+
loggerInstance = null;
|
|
328
|
+
}
|
|
329
|
+
var CREDENTIALS_FILE2 = "credentials.json";
|
|
330
|
+
function getCredentialsFilePath() {
|
|
331
|
+
return path.join(getDataDir(), CREDENTIALS_FILE2);
|
|
332
|
+
}
|
|
333
|
+
function ensureDataDir() {
|
|
334
|
+
const dataDir = getDataDir();
|
|
335
|
+
if (!fs4.existsSync(dataDir)) {
|
|
336
|
+
fs4.mkdirSync(dataDir, { recursive: true });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
function loadCredentials() {
|
|
340
|
+
const logger5 = getLogger();
|
|
341
|
+
const credentialsPath = getCredentialsFilePath();
|
|
342
|
+
try {
|
|
343
|
+
if (!fs4.existsSync(credentialsPath)) {
|
|
344
|
+
logger5.debug("No credentials file found", { path: credentialsPath });
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
const content = fs4.readFileSync(credentialsPath, "utf-8");
|
|
348
|
+
const credentials = JSON.parse(content);
|
|
349
|
+
if (!credentials.accessToken || !credentials.expiresAt) {
|
|
350
|
+
logger5.warn("Invalid credentials file - missing required fields");
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
return credentials;
|
|
354
|
+
} catch (error) {
|
|
355
|
+
logger5.warn("Failed to load credentials", {
|
|
356
|
+
error: error instanceof Error ? error.message : String(error)
|
|
357
|
+
});
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function saveCredentials(credentials) {
|
|
362
|
+
const logger5 = getLogger();
|
|
363
|
+
const credentialsPath = getCredentialsFilePath();
|
|
364
|
+
try {
|
|
365
|
+
ensureDataDir();
|
|
366
|
+
const content = JSON.stringify(credentials, null, 2);
|
|
367
|
+
fs4.writeFileSync(credentialsPath, content, { mode: 384 });
|
|
368
|
+
logger5.info("Credentials saved", { path: credentialsPath });
|
|
369
|
+
} catch (error) {
|
|
370
|
+
logger5.error("Failed to save credentials", {
|
|
371
|
+
error: error instanceof Error ? error.message : String(error)
|
|
372
|
+
});
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
function deleteCredentials() {
|
|
377
|
+
const logger5 = getLogger();
|
|
378
|
+
const credentialsPath = getCredentialsFilePath();
|
|
379
|
+
try {
|
|
380
|
+
if (fs4.existsSync(credentialsPath)) {
|
|
381
|
+
fs4.unlinkSync(credentialsPath);
|
|
382
|
+
logger5.info("Credentials deleted", { path: credentialsPath });
|
|
383
|
+
}
|
|
384
|
+
} catch (error) {
|
|
385
|
+
logger5.warn("Failed to delete credentials", {
|
|
386
|
+
error: error instanceof Error ? error.message : String(error)
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function isCredentialsExpired(credentials) {
|
|
391
|
+
const expiresAt = new Date(credentials.expiresAt);
|
|
392
|
+
const now = /* @__PURE__ */ new Date();
|
|
393
|
+
const bufferMs = 5 * 60 * 1e3;
|
|
394
|
+
return now.getTime() >= expiresAt.getTime() - bufferMs;
|
|
395
|
+
}
|
|
396
|
+
function getValidCredentials() {
|
|
397
|
+
const credentials = loadCredentials();
|
|
398
|
+
if (!credentials) {
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
if (isCredentialsExpired(credentials)) {
|
|
402
|
+
getLogger().info("Credentials expired", { expiresAt: credentials.expiresAt });
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
return credentials;
|
|
406
|
+
}
|
|
407
|
+
function getAuthStatus() {
|
|
408
|
+
const credentials = loadCredentials();
|
|
409
|
+
if (!credentials) {
|
|
410
|
+
return {
|
|
411
|
+
authenticated: false,
|
|
412
|
+
hasApiKey: false
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
const expired = isCredentialsExpired(credentials);
|
|
416
|
+
return {
|
|
417
|
+
authenticated: !expired,
|
|
418
|
+
email: credentials.email,
|
|
419
|
+
userId: credentials.userId,
|
|
420
|
+
expiresAt: credentials.expiresAt,
|
|
421
|
+
hasApiKey: !!credentials.apiKey
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
var logger = getLogger();
|
|
425
|
+
function getOpenCommand(url) {
|
|
426
|
+
const plat = platform();
|
|
427
|
+
switch (plat) {
|
|
428
|
+
case "darwin":
|
|
429
|
+
return `open "${url}"`;
|
|
430
|
+
case "win32":
|
|
431
|
+
return `start "" "${url}"`;
|
|
432
|
+
case "linux":
|
|
433
|
+
return `xdg-open "${url}"`;
|
|
434
|
+
default:
|
|
435
|
+
throw new Error(`Unsupported platform: ${plat}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
async function openBrowserUrl(options) {
|
|
439
|
+
return new Promise((resolve2) => {
|
|
440
|
+
try {
|
|
441
|
+
const command = getOpenCommand(options.url);
|
|
442
|
+
logger.debug("[Browser] Opening URL", { url: options.url, command });
|
|
443
|
+
exec(command, (error) => {
|
|
444
|
+
if (error) {
|
|
445
|
+
logger.warn("[Browser] Failed to open URL", {
|
|
446
|
+
url: options.url,
|
|
447
|
+
error: error.message
|
|
448
|
+
});
|
|
449
|
+
resolve2({
|
|
450
|
+
opened: false,
|
|
451
|
+
error: error.message
|
|
452
|
+
});
|
|
453
|
+
} else {
|
|
454
|
+
logger.info("[Browser] URL opened successfully", { url: options.url });
|
|
455
|
+
resolve2({ opened: true });
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
} catch (error) {
|
|
459
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
460
|
+
logger.warn("[Browser] Failed to open URL", {
|
|
461
|
+
url: options.url,
|
|
462
|
+
error: errorMessage
|
|
463
|
+
});
|
|
464
|
+
resolve2({
|
|
465
|
+
opened: false,
|
|
466
|
+
error: errorMessage
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// src/shared/auth.ts
|
|
473
|
+
var logger2 = getLogger();
|
|
474
|
+
async function startDeviceCodeFlow(config) {
|
|
475
|
+
const deviceCodeUrl = `https://${config.domain}/oauth/device/code`;
|
|
476
|
+
try {
|
|
477
|
+
logger2.info("[Auth] Starting device code flow", {
|
|
478
|
+
domain: config.domain,
|
|
479
|
+
clientId: config.clientId
|
|
480
|
+
});
|
|
481
|
+
const response = await axios.post(
|
|
482
|
+
deviceCodeUrl,
|
|
483
|
+
new URLSearchParams({
|
|
484
|
+
client_id: config.clientId,
|
|
485
|
+
scope: config.scope,
|
|
486
|
+
audience: config.audience
|
|
487
|
+
}).toString(),
|
|
488
|
+
{
|
|
489
|
+
headers: {
|
|
490
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
);
|
|
494
|
+
const data = response.data;
|
|
495
|
+
logger2.info("[Auth] Device code flow started successfully", {
|
|
496
|
+
userCode: data.user_code,
|
|
497
|
+
expiresIn: data.expires_in
|
|
498
|
+
});
|
|
499
|
+
const browserOpenResult = await openBrowserUrl({
|
|
500
|
+
url: data.verification_uri_complete
|
|
501
|
+
});
|
|
502
|
+
if (browserOpenResult.opened) {
|
|
503
|
+
logger2.info("[Auth] Browser opened for device code login");
|
|
504
|
+
} else {
|
|
505
|
+
logger2.warn("[Auth] Failed to open browser", {
|
|
506
|
+
error: browserOpenResult.error,
|
|
507
|
+
verificationUriComplete: data.verification_uri_complete
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
return {
|
|
511
|
+
deviceCode: data.device_code,
|
|
512
|
+
userCode: data.user_code,
|
|
513
|
+
verificationUri: data.verification_uri,
|
|
514
|
+
verificationUriComplete: data.verification_uri_complete,
|
|
515
|
+
expiresIn: data.expires_in,
|
|
516
|
+
interval: data.interval || 5,
|
|
517
|
+
browserOpened: browserOpenResult.opened,
|
|
518
|
+
browserOpenError: browserOpenResult.error
|
|
519
|
+
};
|
|
520
|
+
} catch (error) {
|
|
521
|
+
if (error instanceof AxiosError) {
|
|
522
|
+
logger2.error("[Auth] Failed to start device code flow", {
|
|
523
|
+
status: error.response?.status,
|
|
524
|
+
data: error.response?.data
|
|
525
|
+
});
|
|
526
|
+
throw new Error(
|
|
527
|
+
`Failed to start device code flow: ${error.response?.data?.error_description || error.message}`
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
throw error;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
async function pollDeviceCode(config, deviceCode) {
|
|
534
|
+
const tokenUrl = `https://${config.domain}/oauth/token`;
|
|
535
|
+
try {
|
|
536
|
+
const response = await axios.post(
|
|
537
|
+
tokenUrl,
|
|
538
|
+
new URLSearchParams({
|
|
539
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
540
|
+
device_code: deviceCode,
|
|
541
|
+
client_id: config.clientId
|
|
542
|
+
}).toString(),
|
|
543
|
+
{
|
|
544
|
+
headers: {
|
|
545
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
);
|
|
549
|
+
logger2.info("[Auth] Authorization successful");
|
|
550
|
+
return {
|
|
551
|
+
status: "authorized",
|
|
552
|
+
accessToken: response.data.access_token,
|
|
553
|
+
tokenType: response.data.token_type,
|
|
554
|
+
expiresIn: response.data.expires_in
|
|
555
|
+
};
|
|
556
|
+
} catch (error) {
|
|
557
|
+
if (error instanceof AxiosError && error.response) {
|
|
558
|
+
const data = error.response.data;
|
|
559
|
+
const errorCode = data.error;
|
|
560
|
+
if (errorCode === "authorization_pending") {
|
|
561
|
+
return {
|
|
562
|
+
status: "authorization_pending",
|
|
563
|
+
error: errorCode,
|
|
564
|
+
errorDescription: data.error_description || "User has not yet authorized"
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
if (errorCode === "slow_down") {
|
|
568
|
+
return {
|
|
569
|
+
status: "slow_down",
|
|
570
|
+
error: errorCode,
|
|
571
|
+
errorDescription: data.error_description || "Too many requests, slow down"
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
if (errorCode === "expired_token") {
|
|
575
|
+
return {
|
|
576
|
+
status: "expired_token",
|
|
577
|
+
error: errorCode,
|
|
578
|
+
errorDescription: data.error_description || "Device code expired, please restart flow"
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
if (errorCode === "access_denied") {
|
|
582
|
+
return {
|
|
583
|
+
status: "access_denied",
|
|
584
|
+
error: errorCode,
|
|
585
|
+
errorDescription: data.error_description || "User denied access"
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
logger2.error("[Auth] Unexpected error during poll", {
|
|
589
|
+
status: error.response.status,
|
|
590
|
+
error: errorCode,
|
|
591
|
+
description: data.error_description
|
|
592
|
+
});
|
|
593
|
+
throw new Error(
|
|
594
|
+
`Device code poll failed: ${data.error_description || data.error || "Unknown error"}`
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
throw error;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
async function createApiKeyWithToken(accessToken, keyName, expiry = "90d") {
|
|
601
|
+
const config = getConfig();
|
|
602
|
+
const apiKeyUrl = `${config.qa.promptServiceBaseUrl}/v1/protected/api-keys`;
|
|
603
|
+
try {
|
|
604
|
+
logger2.info("[Auth] Creating API key", {
|
|
605
|
+
keyName,
|
|
606
|
+
expiry
|
|
607
|
+
});
|
|
608
|
+
const response = await axios.post(
|
|
609
|
+
apiKeyUrl,
|
|
610
|
+
{
|
|
611
|
+
name: keyName || "MCP Auto-Generated",
|
|
612
|
+
expiry
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
headers: {
|
|
616
|
+
Authorization: `Bearer ${accessToken}`,
|
|
617
|
+
"Content-Type": "application/json"
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
);
|
|
621
|
+
logger2.info("[Auth] API key created successfully", {
|
|
622
|
+
keyId: response.data.id
|
|
623
|
+
});
|
|
624
|
+
return response.data;
|
|
625
|
+
} catch (error) {
|
|
626
|
+
if (error instanceof AxiosError) {
|
|
627
|
+
logger2.error("[Auth] Failed to create API key", {
|
|
628
|
+
status: error.response?.status,
|
|
629
|
+
data: error.response?.data
|
|
630
|
+
});
|
|
631
|
+
throw new Error(
|
|
632
|
+
`Failed to create API key: ${error.response?.data?.message || error.message}`
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
throw error;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
async function performLogin(keyName, keyExpiry = "90d", timeoutMs = 12e4) {
|
|
639
|
+
const config = getConfig();
|
|
640
|
+
try {
|
|
641
|
+
const deviceCodeResponse = await startDeviceCodeFlow(config.auth0);
|
|
642
|
+
const startTime = Date.now();
|
|
643
|
+
const interval = deviceCodeResponse.interval * 1e3;
|
|
644
|
+
let currentInterval = interval;
|
|
645
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
646
|
+
await new Promise((resolve2) => setTimeout(resolve2, currentInterval));
|
|
647
|
+
const pollResult = await pollDeviceCode(config.auth0, deviceCodeResponse.deviceCode);
|
|
648
|
+
if (pollResult.status === "authorized" && pollResult.accessToken) {
|
|
649
|
+
const expiresAt = new Date(
|
|
650
|
+
Date.now() + (pollResult.expiresIn || 86400) * 1e3
|
|
651
|
+
).toISOString();
|
|
652
|
+
const apiKeyResult = await createApiKeyWithToken(
|
|
653
|
+
pollResult.accessToken,
|
|
654
|
+
keyName,
|
|
655
|
+
keyExpiry
|
|
656
|
+
);
|
|
657
|
+
const credentials = {
|
|
658
|
+
accessToken: pollResult.accessToken,
|
|
659
|
+
expiresAt,
|
|
660
|
+
apiKey: apiKeyResult.key,
|
|
661
|
+
apiKeyId: apiKeyResult.id
|
|
662
|
+
};
|
|
663
|
+
saveCredentials(credentials);
|
|
664
|
+
return {
|
|
665
|
+
success: true,
|
|
666
|
+
deviceCodeResponse,
|
|
667
|
+
credentials
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
if (pollResult.status === "slow_down") {
|
|
671
|
+
currentInterval = Math.min(currentInterval + 1e3, 15e3);
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
if (pollResult.status === "expired_token") {
|
|
675
|
+
return {
|
|
676
|
+
success: false,
|
|
677
|
+
error: "Device code expired. Please try again."
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
if (pollResult.status === "access_denied") {
|
|
681
|
+
return {
|
|
682
|
+
success: false,
|
|
683
|
+
error: "Access denied. User did not authorize the request."
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return {
|
|
688
|
+
success: false,
|
|
689
|
+
deviceCodeResponse,
|
|
690
|
+
error: "Timeout waiting for user authorization"
|
|
691
|
+
};
|
|
692
|
+
} catch (error) {
|
|
693
|
+
return {
|
|
694
|
+
success: false,
|
|
695
|
+
error: error instanceof Error ? error.message : String(error)
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
function performLogout() {
|
|
700
|
+
deleteCredentials();
|
|
701
|
+
logger2.info("[Auth] Logged out successfully");
|
|
702
|
+
}
|
|
703
|
+
function getCallerCredentials() {
|
|
704
|
+
const credentials = getValidCredentials();
|
|
705
|
+
if (!credentials) {
|
|
706
|
+
return {};
|
|
707
|
+
}
|
|
708
|
+
if (credentials.apiKey) {
|
|
709
|
+
return { apiKey: credentials.apiKey };
|
|
710
|
+
}
|
|
711
|
+
return { bearerToken: credentials.accessToken };
|
|
712
|
+
}
|
|
713
|
+
function toolRequiresAuth(toolName) {
|
|
714
|
+
const noAuthTools = [
|
|
715
|
+
// Auth tools
|
|
716
|
+
"muggle_auth_status",
|
|
717
|
+
"muggle_auth_login",
|
|
718
|
+
"muggle_auth_poll",
|
|
719
|
+
"muggle_auth_logout",
|
|
720
|
+
// Local project tools (no cloud)
|
|
721
|
+
"muggle_project_create",
|
|
722
|
+
"muggle_project_list",
|
|
723
|
+
"muggle_project_get",
|
|
724
|
+
"muggle_project_update",
|
|
725
|
+
"muggle_project_delete",
|
|
726
|
+
"muggle_use_case_save",
|
|
727
|
+
"muggle_use_case_list",
|
|
728
|
+
"muggle_use_case_get",
|
|
729
|
+
"muggle_use_case_update",
|
|
730
|
+
"muggle_use_case_delete",
|
|
731
|
+
"muggle_test_case_save",
|
|
732
|
+
"muggle_test_case_list",
|
|
733
|
+
"muggle_test_case_get",
|
|
734
|
+
"muggle_test_case_update",
|
|
735
|
+
"muggle_test_case_delete",
|
|
736
|
+
"muggle_test_script_save",
|
|
737
|
+
"muggle_test_script_list",
|
|
738
|
+
"muggle_test_script_get",
|
|
739
|
+
"muggle_test_script_delete",
|
|
740
|
+
"muggle_execute_test_generation",
|
|
741
|
+
"muggle_execute_replay",
|
|
742
|
+
"muggle_cancel_execution",
|
|
743
|
+
"muggle_check_status",
|
|
744
|
+
"muggle_list_sessions",
|
|
745
|
+
"muggle_cleanup_sessions",
|
|
746
|
+
"muggle_get_page_state",
|
|
747
|
+
"muggle_run_test",
|
|
748
|
+
"muggle_explore_page",
|
|
749
|
+
"muggle_execute_action",
|
|
750
|
+
"muggle_get_screenshot",
|
|
751
|
+
"muggle_run_result_list",
|
|
752
|
+
"muggle_run_result_get",
|
|
753
|
+
"muggle_secret_create",
|
|
754
|
+
"muggle_secret_list",
|
|
755
|
+
"muggle_secret_get",
|
|
756
|
+
"muggle_secret_update",
|
|
757
|
+
"muggle_secret_delete",
|
|
758
|
+
"muggle_workflow_file_create",
|
|
759
|
+
"muggle_workflow_file_list",
|
|
760
|
+
"muggle_workflow_file_list_available",
|
|
761
|
+
"muggle_workflow_file_get",
|
|
762
|
+
"muggle_workflow_file_update",
|
|
763
|
+
"muggle_workflow_file_delete"
|
|
764
|
+
];
|
|
765
|
+
return !noAuthTools.includes(toolName);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// src/server/mcp-server.ts
|
|
769
|
+
var registeredTools = [];
|
|
770
|
+
function registerTools(tools) {
|
|
771
|
+
registeredTools = [...registeredTools, ...tools];
|
|
772
|
+
}
|
|
773
|
+
function getAllTools() {
|
|
774
|
+
return registeredTools;
|
|
775
|
+
}
|
|
776
|
+
function clearTools() {
|
|
777
|
+
registeredTools = [];
|
|
778
|
+
}
|
|
779
|
+
function zodToJsonSchema(schema) {
|
|
780
|
+
try {
|
|
781
|
+
const zodSchema = schema;
|
|
782
|
+
if (zodSchema._def) {
|
|
783
|
+
return convertZodDef(zodSchema);
|
|
784
|
+
}
|
|
785
|
+
} catch {
|
|
786
|
+
}
|
|
787
|
+
return {
|
|
788
|
+
type: "object",
|
|
789
|
+
properties: {},
|
|
790
|
+
additionalProperties: true
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
function convertZodDef(schema) {
|
|
794
|
+
const zodSchema = schema;
|
|
795
|
+
if (!zodSchema._def) {
|
|
796
|
+
return { type: "object" };
|
|
797
|
+
}
|
|
798
|
+
const def = zodSchema._def;
|
|
799
|
+
const typeName = def.typeName;
|
|
800
|
+
switch (typeName) {
|
|
801
|
+
case "ZodObject": {
|
|
802
|
+
const shape = def.shape ? def.shape() : zodSchema.shape || {};
|
|
803
|
+
const properties = {};
|
|
804
|
+
const required = [];
|
|
805
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
806
|
+
properties[key] = convertZodDef(value);
|
|
807
|
+
const valueDef = value._def;
|
|
808
|
+
if (valueDef?.typeName !== "ZodOptional") {
|
|
809
|
+
required.push(key);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
const result = {
|
|
813
|
+
type: "object",
|
|
814
|
+
properties
|
|
815
|
+
};
|
|
816
|
+
if (required.length > 0) {
|
|
817
|
+
result.required = required;
|
|
818
|
+
}
|
|
819
|
+
return result;
|
|
820
|
+
}
|
|
821
|
+
case "ZodString": {
|
|
822
|
+
const result = { type: "string" };
|
|
823
|
+
if (def.description) result.description = def.description;
|
|
824
|
+
if (def.checks) {
|
|
825
|
+
for (const check of def.checks) {
|
|
826
|
+
if (check.kind === "min") result.minLength = check.value;
|
|
827
|
+
if (check.kind === "max") result.maxLength = check.value;
|
|
828
|
+
if (check.kind === "url") result.format = "uri";
|
|
829
|
+
if (check.kind === "email") result.format = "email";
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
return result;
|
|
833
|
+
}
|
|
834
|
+
case "ZodNumber": {
|
|
835
|
+
const result = { type: "number" };
|
|
836
|
+
if (def.description) result.description = def.description;
|
|
837
|
+
if (def.checks) {
|
|
838
|
+
for (const check of def.checks) {
|
|
839
|
+
if (check.kind === "int") result.type = "integer";
|
|
840
|
+
if (check.kind === "min") result.minimum = check.value;
|
|
841
|
+
if (check.kind === "max") result.maximum = check.value;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
return result;
|
|
845
|
+
}
|
|
846
|
+
case "ZodBoolean": {
|
|
847
|
+
const result = { type: "boolean" };
|
|
848
|
+
if (def.description) result.description = def.description;
|
|
849
|
+
return result;
|
|
850
|
+
}
|
|
851
|
+
case "ZodArray": {
|
|
852
|
+
const result = {
|
|
853
|
+
type: "array",
|
|
854
|
+
items: def.innerType ? convertZodDef(def.innerType) : {}
|
|
855
|
+
};
|
|
856
|
+
if (def.description) result.description = def.description;
|
|
857
|
+
return result;
|
|
858
|
+
}
|
|
859
|
+
case "ZodEnum": {
|
|
860
|
+
const result = {
|
|
861
|
+
type: "string",
|
|
862
|
+
enum: def.values || []
|
|
863
|
+
};
|
|
864
|
+
if (def.description) result.description = def.description;
|
|
865
|
+
return result;
|
|
866
|
+
}
|
|
867
|
+
case "ZodOptional": {
|
|
868
|
+
const inner = def.innerType ? convertZodDef(def.innerType) : {};
|
|
869
|
+
if (def.description) inner.description = def.description;
|
|
870
|
+
return inner;
|
|
871
|
+
}
|
|
872
|
+
case "ZodUnion": {
|
|
873
|
+
const options = def.options || [];
|
|
874
|
+
return {
|
|
875
|
+
oneOf: options.map((opt) => convertZodDef(opt))
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
default:
|
|
879
|
+
return { type: "object" };
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
async function handleJitAuth(toolName, correlationId) {
|
|
883
|
+
const childLogger = createChildLogger(correlationId);
|
|
884
|
+
if (!toolRequiresAuth(toolName)) {
|
|
885
|
+
return { credentials: {}, authTriggered: false };
|
|
886
|
+
}
|
|
887
|
+
const credentials = getCallerCredentials();
|
|
888
|
+
if (credentials.apiKey || credentials.bearerToken) {
|
|
889
|
+
return { credentials, authTriggered: false };
|
|
890
|
+
}
|
|
891
|
+
childLogger.info("No credentials found, triggering JIT auth", { tool: toolName });
|
|
892
|
+
return { credentials: {}, authTriggered: false };
|
|
893
|
+
}
|
|
894
|
+
function createUnifiedMcpServer(options) {
|
|
895
|
+
const logger5 = getLogger();
|
|
896
|
+
const config = getConfig();
|
|
897
|
+
const server = new Server(
|
|
898
|
+
{
|
|
899
|
+
name: config.serverName,
|
|
900
|
+
version: config.serverVersion
|
|
901
|
+
},
|
|
902
|
+
{
|
|
903
|
+
capabilities: {
|
|
904
|
+
tools: {},
|
|
905
|
+
resources: {}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
);
|
|
909
|
+
server.setRequestHandler(ListToolsRequestSchema, () => {
|
|
910
|
+
const tools = getAllTools();
|
|
911
|
+
logger5.debug("Listing tools", { count: tools.length });
|
|
912
|
+
const toolDefinitions = tools.map((tool) => {
|
|
913
|
+
const jsonSchema = zodToJsonSchema(tool.inputSchema);
|
|
914
|
+
return {
|
|
915
|
+
name: tool.name,
|
|
916
|
+
description: tool.description,
|
|
917
|
+
inputSchema: jsonSchema
|
|
918
|
+
};
|
|
919
|
+
});
|
|
920
|
+
return { tools: toolDefinitions };
|
|
921
|
+
});
|
|
922
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
923
|
+
const correlationId = v4();
|
|
924
|
+
const childLogger = createChildLogger(correlationId);
|
|
925
|
+
const toolName = request.params.name;
|
|
926
|
+
const toolInput = request.params.arguments || {};
|
|
927
|
+
childLogger.info("Tool call received", {
|
|
928
|
+
tool: toolName,
|
|
929
|
+
hasArguments: Object.keys(toolInput).length > 0
|
|
930
|
+
});
|
|
931
|
+
try {
|
|
932
|
+
const tool = getAllTools().find((t) => t.name === toolName);
|
|
933
|
+
if (!tool) {
|
|
934
|
+
return {
|
|
935
|
+
content: [
|
|
936
|
+
{
|
|
937
|
+
type: "text",
|
|
938
|
+
text: JSON.stringify({
|
|
939
|
+
error: "NOT_FOUND",
|
|
940
|
+
message: `Unknown tool: ${toolName}`
|
|
941
|
+
})
|
|
942
|
+
}
|
|
943
|
+
],
|
|
944
|
+
isError: true
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
const { authTriggered } = await handleJitAuth(toolName, correlationId);
|
|
948
|
+
if (authTriggered) {
|
|
949
|
+
return {
|
|
950
|
+
content: [
|
|
951
|
+
{
|
|
952
|
+
type: "text",
|
|
953
|
+
text: JSON.stringify({
|
|
954
|
+
message: "Authentication required. Please complete login in your browser."
|
|
955
|
+
})
|
|
956
|
+
}
|
|
957
|
+
]
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
const startTime = Date.now();
|
|
961
|
+
const result = await tool.execute({
|
|
962
|
+
input: toolInput,
|
|
963
|
+
correlationId
|
|
964
|
+
});
|
|
965
|
+
const latency = Date.now() - startTime;
|
|
966
|
+
childLogger.info("Tool call completed", {
|
|
967
|
+
tool: toolName,
|
|
968
|
+
latencyMs: latency,
|
|
969
|
+
isError: result.isError
|
|
970
|
+
});
|
|
971
|
+
return {
|
|
972
|
+
content: [
|
|
973
|
+
{
|
|
974
|
+
type: "text",
|
|
975
|
+
text: result.content
|
|
976
|
+
}
|
|
977
|
+
],
|
|
978
|
+
isError: result.isError
|
|
979
|
+
};
|
|
980
|
+
} catch (error) {
|
|
981
|
+
if (error instanceof ZodError) {
|
|
982
|
+
childLogger.warn("Tool call failed with validation error", {
|
|
983
|
+
tool: toolName,
|
|
984
|
+
errors: error.errors
|
|
985
|
+
});
|
|
986
|
+
const issueMessages = error.errors.slice(0, 3).map((issue) => {
|
|
987
|
+
const path7 = issue.path.join(".");
|
|
988
|
+
return path7 ? `'${path7}': ${issue.message}` : issue.message;
|
|
989
|
+
});
|
|
990
|
+
return {
|
|
991
|
+
content: [
|
|
992
|
+
{
|
|
993
|
+
type: "text",
|
|
994
|
+
text: JSON.stringify({
|
|
995
|
+
error: "INVALID_ARGUMENT",
|
|
996
|
+
message: `Invalid input: ${issueMessages.join("; ")}`
|
|
997
|
+
})
|
|
998
|
+
}
|
|
999
|
+
],
|
|
1000
|
+
isError: true
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
childLogger.error("Tool call failed with error", {
|
|
1004
|
+
tool: toolName,
|
|
1005
|
+
error: String(error)
|
|
1006
|
+
});
|
|
1007
|
+
return {
|
|
1008
|
+
content: [
|
|
1009
|
+
{
|
|
1010
|
+
type: "text",
|
|
1011
|
+
text: JSON.stringify({
|
|
1012
|
+
error: "INTERNAL_ERROR",
|
|
1013
|
+
message: error instanceof Error ? error.message : "An unexpected error occurred"
|
|
1014
|
+
})
|
|
1015
|
+
}
|
|
1016
|
+
],
|
|
1017
|
+
isError: true
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
server.setRequestHandler(ListResourcesRequestSchema, () => {
|
|
1022
|
+
logger5.debug("Listing resources");
|
|
1023
|
+
return { resources: [] };
|
|
1024
|
+
});
|
|
1025
|
+
server.setRequestHandler(ReadResourceRequestSchema, (request) => {
|
|
1026
|
+
const uri = request.params.uri;
|
|
1027
|
+
logger5.debug("Reading resource", { uri });
|
|
1028
|
+
return {
|
|
1029
|
+
contents: [
|
|
1030
|
+
{
|
|
1031
|
+
uri,
|
|
1032
|
+
mimeType: "text/plain",
|
|
1033
|
+
text: `Resource not found: ${uri}`
|
|
1034
|
+
}
|
|
1035
|
+
]
|
|
1036
|
+
};
|
|
1037
|
+
});
|
|
1038
|
+
logger5.info("Unified MCP server configured", {
|
|
1039
|
+
tools: getAllTools().length,
|
|
1040
|
+
enableQaTools: options.enableQaTools,
|
|
1041
|
+
enableLocalTools: options.enableLocalTools
|
|
1042
|
+
});
|
|
1043
|
+
return server;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// src/server/index.ts
|
|
1047
|
+
var server_exports = {};
|
|
1048
|
+
__export(server_exports, {
|
|
1049
|
+
clearTools: () => clearTools,
|
|
1050
|
+
createUnifiedMcpServer: () => createUnifiedMcpServer,
|
|
1051
|
+
getAllTools: () => getAllTools,
|
|
1052
|
+
registerTools: () => registerTools,
|
|
1053
|
+
startStdioServer: () => startStdioServer
|
|
1054
|
+
});
|
|
1055
|
+
var logger3 = getLogger();
|
|
1056
|
+
async function startStdioServer(server) {
|
|
1057
|
+
logger3.info("Starting stdio server transport");
|
|
1058
|
+
const transport = new StdioServerTransport();
|
|
1059
|
+
await server.connect(transport);
|
|
1060
|
+
logger3.info("Stdio server connected");
|
|
1061
|
+
const shutdown = (signal) => {
|
|
1062
|
+
logger3.info(`Received ${signal}, shutting down...`);
|
|
1063
|
+
process.exit(0);
|
|
1064
|
+
};
|
|
1065
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
1066
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// src/qa/index.ts
|
|
1070
|
+
var qa_exports = {};
|
|
1071
|
+
__export(qa_exports, {
|
|
1072
|
+
ApiKeyCreateInputSchema: () => ApiKeyCreateInputSchema,
|
|
1073
|
+
ApiKeyGetInputSchema: () => ApiKeyGetInputSchema,
|
|
1074
|
+
ApiKeyListInputSchema: () => ApiKeyListInputSchema,
|
|
1075
|
+
ApiKeyRevokeInputSchema: () => ApiKeyRevokeInputSchema,
|
|
1076
|
+
AuthLoginInputSchema: () => AuthLoginInputSchema,
|
|
1077
|
+
AuthPollInputSchema: () => AuthPollInputSchema,
|
|
1078
|
+
EmptyInputSchema: () => EmptyInputSchema,
|
|
1079
|
+
GatewayError: () => GatewayError,
|
|
1080
|
+
IdSchema: () => IdSchema,
|
|
1081
|
+
McpErrorCode: () => McpErrorCode,
|
|
1082
|
+
PaginationInputSchema: () => PaginationInputSchema,
|
|
1083
|
+
PrdFileDeleteInputSchema: () => PrdFileDeleteInputSchema,
|
|
1084
|
+
PrdFileListInputSchema: () => PrdFileListInputSchema,
|
|
1085
|
+
PrdFileProcessLatestRunInputSchema: () => PrdFileProcessLatestRunInputSchema,
|
|
1086
|
+
PrdFileProcessStartInputSchema: () => PrdFileProcessStartInputSchema,
|
|
1087
|
+
PrdFileUploadInputSchema: () => PrdFileUploadInputSchema,
|
|
1088
|
+
ProjectCreateInputSchema: () => ProjectCreateInputSchema,
|
|
1089
|
+
ProjectDeleteInputSchema: () => ProjectDeleteInputSchema,
|
|
1090
|
+
ProjectGetInputSchema: () => ProjectGetInputSchema,
|
|
1091
|
+
ProjectListInputSchema: () => ProjectListInputSchema,
|
|
1092
|
+
ProjectTestResultsSummaryInputSchema: () => ProjectTestResultsSummaryInputSchema,
|
|
1093
|
+
ProjectTestRunsSummaryInputSchema: () => ProjectTestRunsSummaryInputSchema,
|
|
1094
|
+
ProjectTestScriptsSummaryInputSchema: () => ProjectTestScriptsSummaryInputSchema,
|
|
1095
|
+
ProjectUpdateInputSchema: () => ProjectUpdateInputSchema,
|
|
1096
|
+
PromptServiceClient: () => PromptServiceClient,
|
|
1097
|
+
RecommendCicdSetupInputSchema: () => RecommendCicdSetupInputSchema,
|
|
1098
|
+
RecommendScheduleInputSchema: () => RecommendScheduleInputSchema,
|
|
1099
|
+
ReportCostQueryInputSchema: () => ReportCostQueryInputSchema,
|
|
1100
|
+
ReportFinalGenerateInputSchema: () => ReportFinalGenerateInputSchema,
|
|
1101
|
+
ReportPreferencesUpsertInputSchema: () => ReportPreferencesUpsertInputSchema,
|
|
1102
|
+
ReportStatsSummaryInputSchema: () => ReportStatsSummaryInputSchema,
|
|
1103
|
+
SecretCreateInputSchema: () => SecretCreateInputSchema,
|
|
1104
|
+
SecretDeleteInputSchema: () => SecretDeleteInputSchema,
|
|
1105
|
+
SecretGetInputSchema: () => SecretGetInputSchema,
|
|
1106
|
+
SecretListInputSchema: () => SecretListInputSchema,
|
|
1107
|
+
SecretUpdateInputSchema: () => SecretUpdateInputSchema,
|
|
1108
|
+
TestCaseCreateInputSchema: () => TestCaseCreateInputSchema,
|
|
1109
|
+
TestCaseGenerateFromPromptInputSchema: () => TestCaseGenerateFromPromptInputSchema,
|
|
1110
|
+
TestCaseGetInputSchema: () => TestCaseGetInputSchema,
|
|
1111
|
+
TestCaseListByUseCaseInputSchema: () => TestCaseListByUseCaseInputSchema,
|
|
1112
|
+
TestCaseListInputSchema: () => TestCaseListInputSchema,
|
|
1113
|
+
TestScriptGetInputSchema: () => TestScriptGetInputSchema,
|
|
1114
|
+
TestScriptListInputSchema: () => TestScriptListInputSchema,
|
|
1115
|
+
TestScriptListPaginatedInputSchema: () => TestScriptListPaginatedInputSchema,
|
|
1116
|
+
UseCaseCandidatesApproveInputSchema: () => UseCaseCandidatesApproveInputSchema,
|
|
1117
|
+
UseCaseCreateFromPromptsInputSchema: () => UseCaseCreateFromPromptsInputSchema,
|
|
1118
|
+
UseCaseDiscoveryMemoryGetInputSchema: () => UseCaseDiscoveryMemoryGetInputSchema,
|
|
1119
|
+
UseCaseGetInputSchema: () => UseCaseGetInputSchema,
|
|
1120
|
+
UseCaseListInputSchema: () => UseCaseListInputSchema,
|
|
1121
|
+
UseCasePromptPreviewInputSchema: () => UseCasePromptPreviewInputSchema,
|
|
1122
|
+
UseCaseUpdateFromPromptInputSchema: () => UseCaseUpdateFromPromptInputSchema,
|
|
1123
|
+
WalletAutoTopUpSetPaymentMethodInputSchema: () => WalletAutoTopUpSetPaymentMethodInputSchema,
|
|
1124
|
+
WalletAutoTopUpUpdateInputSchema: () => WalletAutoTopUpUpdateInputSchema,
|
|
1125
|
+
WalletPaymentMethodCreateSetupSessionInputSchema: () => WalletPaymentMethodCreateSetupSessionInputSchema,
|
|
1126
|
+
WalletPaymentMethodListInputSchema: () => WalletPaymentMethodListInputSchema,
|
|
1127
|
+
WalletTopUpInputSchema: () => WalletTopUpInputSchema,
|
|
1128
|
+
WorkflowCancelRunInputSchema: () => WorkflowCancelRunInputSchema,
|
|
1129
|
+
WorkflowCancelRuntimeInputSchema: () => WorkflowCancelRuntimeInputSchema,
|
|
1130
|
+
WorkflowGetLatestRunInputSchema: () => WorkflowGetLatestRunInputSchema,
|
|
1131
|
+
WorkflowGetLatestScriptGenByTestCaseInputSchema: () => WorkflowGetLatestScriptGenByTestCaseInputSchema,
|
|
1132
|
+
WorkflowGetReplayBulkBatchSummaryInputSchema: () => WorkflowGetReplayBulkBatchSummaryInputSchema,
|
|
1133
|
+
WorkflowListRuntimesInputSchema: () => WorkflowListRuntimesInputSchema,
|
|
1134
|
+
WorkflowParamsSchema: () => WorkflowParamsSchema,
|
|
1135
|
+
WorkflowStartTestCaseDetectionInputSchema: () => WorkflowStartTestCaseDetectionInputSchema,
|
|
1136
|
+
WorkflowStartTestScriptGenerationInputSchema: () => WorkflowStartTestScriptGenerationInputSchema,
|
|
1137
|
+
WorkflowStartTestScriptReplayBulkInputSchema: () => WorkflowStartTestScriptReplayBulkInputSchema,
|
|
1138
|
+
WorkflowStartTestScriptReplayInputSchema: () => WorkflowStartTestScriptReplayInputSchema,
|
|
1139
|
+
WorkflowStartWebsiteScanInputSchema: () => WorkflowStartWebsiteScanInputSchema,
|
|
1140
|
+
allQaToolDefinitions: () => allQaToolDefinitions,
|
|
1141
|
+
executeQaTool: () => executeQaTool,
|
|
1142
|
+
getPromptServiceClient: () => getPromptServiceClient,
|
|
1143
|
+
getQaToolByName: () => getQaToolByName,
|
|
1144
|
+
getQaTools: () => getQaTools
|
|
1145
|
+
});
|
|
1146
|
+
var PaginationInputSchema = z.object({
|
|
1147
|
+
page: z.number().int().positive().optional().describe("Page number (1-based)"),
|
|
1148
|
+
pageSize: z.number().int().positive().max(100).optional().describe("Number of items per page")
|
|
1149
|
+
});
|
|
1150
|
+
var IdSchema = z.string().min(1).describe("Unique identifier");
|
|
1151
|
+
var WorkflowParamsSchema = z.record(z.unknown()).optional().describe("Optional workflow parameters for memory configuration overrides");
|
|
1152
|
+
var ProjectCreateInputSchema = z.object({
|
|
1153
|
+
projectName: z.string().min(1).max(255).describe("Name of the project"),
|
|
1154
|
+
description: z.string().min(1).describe("Project description"),
|
|
1155
|
+
url: z.string().url().describe("Target website URL to test")
|
|
1156
|
+
});
|
|
1157
|
+
var ProjectGetInputSchema = z.object({
|
|
1158
|
+
projectId: IdSchema.describe("Project ID to retrieve")
|
|
1159
|
+
});
|
|
1160
|
+
var ProjectDeleteInputSchema = z.object({
|
|
1161
|
+
projectId: IdSchema.describe("Project ID to delete")
|
|
1162
|
+
});
|
|
1163
|
+
var ProjectUpdateInputSchema = z.object({
|
|
1164
|
+
projectId: IdSchema.describe("Project ID to update"),
|
|
1165
|
+
projectName: z.string().min(1).max(255).optional().describe("New project name"),
|
|
1166
|
+
description: z.string().optional().describe("Updated description"),
|
|
1167
|
+
url: z.string().url().optional().describe("Updated target URL")
|
|
1168
|
+
});
|
|
1169
|
+
var ProjectListInputSchema = PaginationInputSchema.extend({});
|
|
1170
|
+
var PrdFileUploadInputSchema = z.object({
|
|
1171
|
+
projectId: IdSchema.describe("Project ID to associate the PRD file with"),
|
|
1172
|
+
fileName: z.string().min(1).describe("Name of the file"),
|
|
1173
|
+
contentBase64: z.string().min(1).describe("Base64-encoded file content"),
|
|
1174
|
+
contentType: z.string().optional().describe("MIME type of the file")
|
|
1175
|
+
});
|
|
1176
|
+
var PrdFileListInputSchema = z.object({
|
|
1177
|
+
projectId: IdSchema.describe("Project ID to list PRD files for")
|
|
1178
|
+
});
|
|
1179
|
+
var PrdFileDeleteInputSchema = z.object({
|
|
1180
|
+
projectId: IdSchema.describe("Project ID"),
|
|
1181
|
+
prdFileId: IdSchema.describe("PRD file ID to delete")
|
|
1182
|
+
});
|
|
1183
|
+
var PrdFileProcessStartInputSchema = z.object({
|
|
1184
|
+
projectId: IdSchema.describe("Project ID to process PRD files for"),
|
|
1185
|
+
name: z.string().min(1).describe("Workflow name"),
|
|
1186
|
+
description: z.string().min(1).describe("Description of the PRD processing workflow"),
|
|
1187
|
+
prdFilePath: z.string().min(1).describe("Storage path of the uploaded PRD file (from upload response)"),
|
|
1188
|
+
originalFileName: z.string().min(1).describe("Original file name of the PRD document"),
|
|
1189
|
+
url: z.string().url().describe("Target website URL for context"),
|
|
1190
|
+
contentChecksum: z.string().min(1).describe("SHA-256 checksum of the PRD file content (from upload response)"),
|
|
1191
|
+
fileSize: z.number().int().min(0).describe("Size of the PRD file in bytes (from upload response)")
|
|
1192
|
+
});
|
|
1193
|
+
var PrdFileProcessLatestRunInputSchema = z.object({
|
|
1194
|
+
workflowRuntimeId: IdSchema.describe("PRD processing workflow runtime ID")
|
|
1195
|
+
});
|
|
1196
|
+
var SecretListInputSchema = z.object({
|
|
1197
|
+
projectId: IdSchema.describe("Project ID to list secrets for")
|
|
1198
|
+
});
|
|
1199
|
+
var SecretCreateInputSchema = z.object({
|
|
1200
|
+
projectId: IdSchema.describe("Project ID to create the secret for"),
|
|
1201
|
+
name: z.string().min(1).describe("Secret name/key"),
|
|
1202
|
+
value: z.string().min(1).describe("Secret value"),
|
|
1203
|
+
description: z.string().min(1).describe("Human-readable description for selection guidance"),
|
|
1204
|
+
source: z.enum(["user", "agent"]).optional().describe("Source of the secret: 'user' for user-provided credentials, 'agent' for agent-generated credentials")
|
|
1205
|
+
});
|
|
1206
|
+
var SecretGetInputSchema = z.object({
|
|
1207
|
+
secretId: IdSchema.describe("Secret ID to retrieve")
|
|
1208
|
+
});
|
|
1209
|
+
var SecretUpdateInputSchema = z.object({
|
|
1210
|
+
secretId: IdSchema.describe("Secret ID to update"),
|
|
1211
|
+
name: z.string().min(1).optional().describe("Updated secret name"),
|
|
1212
|
+
value: z.string().min(1).optional().describe("Updated secret value"),
|
|
1213
|
+
description: z.string().optional().describe("Updated description")
|
|
1214
|
+
});
|
|
1215
|
+
var SecretDeleteInputSchema = z.object({
|
|
1216
|
+
secretId: IdSchema.describe("Secret ID to delete")
|
|
1217
|
+
});
|
|
1218
|
+
var UseCaseDiscoveryMemoryGetInputSchema = z.object({
|
|
1219
|
+
projectId: IdSchema.describe("Project ID to get use case discovery memory for")
|
|
1220
|
+
});
|
|
1221
|
+
var UseCaseCandidatesApproveInputSchema = z.object({
|
|
1222
|
+
projectId: IdSchema.describe("Project ID"),
|
|
1223
|
+
approvedCandidateIds: z.array(IdSchema).min(1).describe("IDs of candidates to approve/graduate")
|
|
1224
|
+
});
|
|
1225
|
+
var UseCaseListInputSchema = z.object({
|
|
1226
|
+
projectId: IdSchema.describe("Project ID to list use cases for")
|
|
1227
|
+
}).merge(PaginationInputSchema);
|
|
1228
|
+
var UseCaseGetInputSchema = z.object({
|
|
1229
|
+
useCaseId: IdSchema.describe("Use case ID to retrieve")
|
|
1230
|
+
});
|
|
1231
|
+
var UseCasePromptPreviewInputSchema = z.object({
|
|
1232
|
+
projectId: IdSchema.describe("Project ID to generate use case for"),
|
|
1233
|
+
instruction: z.string().min(1).describe("Natural language instruction describing the use case (e.g., 'As a logged-in user, I can add items to cart')")
|
|
1234
|
+
});
|
|
1235
|
+
var UseCaseCreateFromPromptsInputSchema = z.object({
|
|
1236
|
+
projectId: IdSchema.describe("Project ID to create use cases for"),
|
|
1237
|
+
prompts: z.array(z.object({
|
|
1238
|
+
instruction: z.string().min(1).describe("Natural language instruction describing the use case")
|
|
1239
|
+
})).min(1).describe("Array of prompts to generate use cases from")
|
|
1240
|
+
});
|
|
1241
|
+
var UseCaseUpdateFromPromptInputSchema = z.object({
|
|
1242
|
+
projectId: IdSchema.describe("Project ID"),
|
|
1243
|
+
useCaseId: IdSchema.describe("Use case ID to update"),
|
|
1244
|
+
instruction: z.string().min(1).describe("Natural language instruction to regenerate the use case from")
|
|
1245
|
+
});
|
|
1246
|
+
var TestCaseListInputSchema = z.object({
|
|
1247
|
+
projectId: IdSchema.describe("Project ID to list test cases for")
|
|
1248
|
+
}).merge(PaginationInputSchema);
|
|
1249
|
+
var TestCaseGetInputSchema = z.object({
|
|
1250
|
+
testCaseId: IdSchema.describe("Test case ID to retrieve")
|
|
1251
|
+
});
|
|
1252
|
+
var TestCaseListByUseCaseInputSchema = z.object({
|
|
1253
|
+
useCaseId: IdSchema.describe("Use case ID to list test cases for")
|
|
1254
|
+
});
|
|
1255
|
+
var TestCaseGenerateFromPromptInputSchema = z.object({
|
|
1256
|
+
projectId: IdSchema.describe("Project ID"),
|
|
1257
|
+
useCaseId: IdSchema.describe("Use case ID to generate test cases for"),
|
|
1258
|
+
instruction: z.string().min(1).describe("Natural language instruction describing the test cases to generate")
|
|
1259
|
+
});
|
|
1260
|
+
var TestCaseCreateInputSchema = z.object({
|
|
1261
|
+
projectId: IdSchema.describe("Project ID"),
|
|
1262
|
+
useCaseId: IdSchema.describe("Use case ID to associate the test case with"),
|
|
1263
|
+
title: z.string().min(1).describe("Test case title"),
|
|
1264
|
+
description: z.string().min(1).describe("Detailed description of what the test case validates"),
|
|
1265
|
+
goal: z.string().min(1).describe("Concise, measurable goal of the test"),
|
|
1266
|
+
precondition: z.string().optional().describe("Initial state/setup required before test execution"),
|
|
1267
|
+
expectedResult: z.string().min(1).describe("Expected outcome after test execution"),
|
|
1268
|
+
url: z.string().url().describe("Target URL for the test case"),
|
|
1269
|
+
status: z.enum(["DRAFT", "ACTIVE", "DEPRECATED", "ARCHIVED"]).optional().describe("Test case status"),
|
|
1270
|
+
priority: z.enum(["HIGH", "MEDIUM", "LOW"]).optional().describe("Test case priority"),
|
|
1271
|
+
tags: z.array(z.string()).optional().describe("Tags for categorization"),
|
|
1272
|
+
category: z.string().optional().describe("Test case category"),
|
|
1273
|
+
automated: z.boolean().optional().describe("Whether this test case is automated (default: true)")
|
|
1274
|
+
});
|
|
1275
|
+
var TestScriptListInputSchema = z.object({
|
|
1276
|
+
projectId: IdSchema.describe("Project ID to list test scripts for")
|
|
1277
|
+
}).merge(PaginationInputSchema);
|
|
1278
|
+
var TestScriptGetInputSchema = z.object({
|
|
1279
|
+
testScriptId: IdSchema.describe("Test script ID to retrieve")
|
|
1280
|
+
});
|
|
1281
|
+
var TestScriptListPaginatedInputSchema = z.object({
|
|
1282
|
+
projectId: IdSchema.describe("Project ID to list test scripts for")
|
|
1283
|
+
}).merge(PaginationInputSchema);
|
|
1284
|
+
var WorkflowStartWebsiteScanInputSchema = z.object({
|
|
1285
|
+
projectId: IdSchema.describe("Project ID to scan"),
|
|
1286
|
+
url: z.string().url().describe("Website URL to scan"),
|
|
1287
|
+
description: z.string().min(1).describe("Description of what to scan/discover"),
|
|
1288
|
+
archiveUnapproved: z.boolean().optional().describe("Whether to archive unapproved candidates before scanning"),
|
|
1289
|
+
workflowParams: WorkflowParamsSchema
|
|
1290
|
+
});
|
|
1291
|
+
var WorkflowListRuntimesInputSchema = z.object({
|
|
1292
|
+
projectId: IdSchema.optional().describe("Filter by project ID")
|
|
1293
|
+
});
|
|
1294
|
+
var WorkflowGetLatestRunInputSchema = z.object({
|
|
1295
|
+
workflowRuntimeId: IdSchema.describe("Workflow runtime ID")
|
|
1296
|
+
});
|
|
1297
|
+
var WorkflowStartTestCaseDetectionInputSchema = z.object({
|
|
1298
|
+
projectId: IdSchema.describe("Project ID"),
|
|
1299
|
+
useCaseId: IdSchema.describe("Use case ID to detect test cases for"),
|
|
1300
|
+
name: z.string().min(1).describe("Workflow name"),
|
|
1301
|
+
description: z.string().min(1).describe("Workflow description"),
|
|
1302
|
+
url: z.string().url().describe("Target website URL"),
|
|
1303
|
+
workflowParams: WorkflowParamsSchema
|
|
1304
|
+
});
|
|
1305
|
+
var WorkflowStartTestScriptGenerationInputSchema = z.object({
|
|
1306
|
+
projectId: IdSchema.describe("Project ID"),
|
|
1307
|
+
useCaseId: IdSchema.describe("Use case ID"),
|
|
1308
|
+
testCaseId: IdSchema.describe("Test case ID"),
|
|
1309
|
+
name: z.string().min(1).describe("Workflow name"),
|
|
1310
|
+
url: z.string().url().describe("Target website URL"),
|
|
1311
|
+
goal: z.string().min(1).describe("Test goal"),
|
|
1312
|
+
precondition: z.string().min(1).describe("Preconditions"),
|
|
1313
|
+
instructions: z.string().min(1).describe("Step-by-step instructions"),
|
|
1314
|
+
expectedResult: z.string().min(1).describe("Expected result"),
|
|
1315
|
+
workflowParams: WorkflowParamsSchema
|
|
1316
|
+
});
|
|
1317
|
+
var WorkflowGetLatestScriptGenByTestCaseInputSchema = z.object({
|
|
1318
|
+
testCaseId: IdSchema.describe("Test case ID")
|
|
1319
|
+
});
|
|
1320
|
+
var WorkflowStartTestScriptReplayInputSchema = z.object({
|
|
1321
|
+
projectId: IdSchema.describe("Project ID"),
|
|
1322
|
+
useCaseId: IdSchema.describe("Use case ID"),
|
|
1323
|
+
testCaseId: IdSchema.describe("Test case ID"),
|
|
1324
|
+
testScriptId: IdSchema.describe("Test script ID to replay"),
|
|
1325
|
+
name: z.string().min(1).describe("Workflow name"),
|
|
1326
|
+
workflowParams: WorkflowParamsSchema
|
|
1327
|
+
});
|
|
1328
|
+
var WorkflowStartTestScriptReplayBulkInputSchema = z.object({
|
|
1329
|
+
projectId: IdSchema.describe("Project ID"),
|
|
1330
|
+
name: z.string().min(1).describe("Workflow name"),
|
|
1331
|
+
intervalSec: z.number().int().describe("Interval in seconds (-1 for one-time / on-demand)"),
|
|
1332
|
+
useCaseId: IdSchema.optional().describe("Optional: only replay test cases under this use case"),
|
|
1333
|
+
namePrefix: z.string().optional().describe("Optional: prefix for generated workflow names"),
|
|
1334
|
+
limit: z.number().int().optional().describe("Optional: limit number of test cases to replay"),
|
|
1335
|
+
testCaseIds: z.array(IdSchema).optional().describe("Optional: targeted test cases to replay"),
|
|
1336
|
+
repeatPerTestCase: z.number().int().optional().describe("Optional: repeat count per test case"),
|
|
1337
|
+
workflowParams: WorkflowParamsSchema
|
|
1338
|
+
});
|
|
1339
|
+
var WorkflowGetReplayBulkBatchSummaryInputSchema = z.object({
|
|
1340
|
+
runBatchId: IdSchema.describe("Run batch ID")
|
|
1341
|
+
});
|
|
1342
|
+
var WorkflowCancelRunInputSchema = z.object({
|
|
1343
|
+
workflowRunId: IdSchema.describe("Workflow run ID to cancel")
|
|
1344
|
+
});
|
|
1345
|
+
var WorkflowCancelRuntimeInputSchema = z.object({
|
|
1346
|
+
workflowRuntimeId: IdSchema.describe("Workflow runtime ID to cancel")
|
|
1347
|
+
});
|
|
1348
|
+
var ProjectTestResultsSummaryInputSchema = z.object({
|
|
1349
|
+
projectId: IdSchema.describe("Project ID to get test results summary for")
|
|
1350
|
+
});
|
|
1351
|
+
var ProjectTestScriptsSummaryInputSchema = z.object({
|
|
1352
|
+
projectId: IdSchema.describe("Project ID to get test scripts summary for")
|
|
1353
|
+
});
|
|
1354
|
+
var ProjectTestRunsSummaryInputSchema = z.object({
|
|
1355
|
+
projectId: IdSchema.describe("Project ID to get test runs summary for")
|
|
1356
|
+
});
|
|
1357
|
+
var ReportStatsSummaryInputSchema = z.object({
|
|
1358
|
+
projectId: IdSchema.describe("Project ID to get report stats for")
|
|
1359
|
+
});
|
|
1360
|
+
var ReportCostQueryInputSchema = z.object({
|
|
1361
|
+
projectId: IdSchema.describe("Project ID"),
|
|
1362
|
+
startDateKey: z.string().optional().describe("Start date key (YYYYMMDD)"),
|
|
1363
|
+
endDateKey: z.string().optional().describe("End date key (YYYYMMDD)"),
|
|
1364
|
+
filterType: z.string().optional().describe("Filter type for cost breakdown"),
|
|
1365
|
+
filterIds: z.array(z.unknown()).optional().describe("Filter IDs")
|
|
1366
|
+
});
|
|
1367
|
+
var ReportPreferencesUpsertInputSchema = z.object({
|
|
1368
|
+
projectId: IdSchema.describe("Project ID"),
|
|
1369
|
+
channels: z.array(z.unknown()).describe("Delivery channels to enable"),
|
|
1370
|
+
emails: z.array(z.unknown()).optional().describe("Email addresses for delivery"),
|
|
1371
|
+
phones: z.array(z.unknown()).optional().describe("Phone numbers for SMS delivery"),
|
|
1372
|
+
webhookUrl: z.string().url().optional().describe("Webhook URL for delivery"),
|
|
1373
|
+
defaultExportFormat: z.string().optional().describe("Default export format (pdf, html, etc.)")
|
|
1374
|
+
});
|
|
1375
|
+
var ReportFinalGenerateInputSchema = z.object({
|
|
1376
|
+
projectId: IdSchema.describe("Project ID to generate report for"),
|
|
1377
|
+
exportFormat: z.enum(["pdf", "html", "markdown"]).describe("Export format for the report")
|
|
1378
|
+
});
|
|
1379
|
+
var WalletTopUpInputSchema = z.object({
|
|
1380
|
+
packageId: IdSchema.describe("Token package ID to purchase"),
|
|
1381
|
+
checkoutSuccessCallback: z.string().url().describe("URL to redirect to when checkout succeeds"),
|
|
1382
|
+
checkoutCancelCallback: z.string().url().describe("URL to redirect to when checkout is canceled")
|
|
1383
|
+
});
|
|
1384
|
+
var WalletPaymentMethodCreateSetupSessionInputSchema = z.object({
|
|
1385
|
+
checkoutSuccessCallback: z.string().url().describe("URL to redirect to when payment method setup succeeds"),
|
|
1386
|
+
checkoutCancelCallback: z.string().url().describe("URL to redirect to when payment method setup is canceled")
|
|
1387
|
+
});
|
|
1388
|
+
var WalletAutoTopUpSetPaymentMethodInputSchema = z.object({
|
|
1389
|
+
paymentMethodId: IdSchema.describe("Saved Stripe payment method ID (e.g., pm_xxx)")
|
|
1390
|
+
});
|
|
1391
|
+
var WalletPaymentMethodListInputSchema = z.object({});
|
|
1392
|
+
var WalletAutoTopUpUpdateInputSchema = z.object({
|
|
1393
|
+
enabled: z.boolean().describe("Whether auto top-up is enabled"),
|
|
1394
|
+
topUpTriggerTokenThreshold: z.number().int().min(0).describe("Token balance threshold to trigger auto top-up"),
|
|
1395
|
+
packageId: IdSchema.describe("Token package ID to purchase when auto top-up triggers")
|
|
1396
|
+
});
|
|
1397
|
+
var RecommendScheduleInputSchema = z.object({
|
|
1398
|
+
projectId: IdSchema.optional().describe("Project ID for context"),
|
|
1399
|
+
testFrequency: z.enum(["daily", "weekly", "onDemand"]).optional().describe("Desired test frequency"),
|
|
1400
|
+
timezone: z.string().optional().describe("Timezone for scheduling")
|
|
1401
|
+
});
|
|
1402
|
+
var RecommendCicdSetupInputSchema = z.object({
|
|
1403
|
+
projectId: IdSchema.optional().describe("Project ID for context"),
|
|
1404
|
+
repositoryProvider: z.enum(["github", "azureDevOps", "gitlab", "other"]).optional().describe("Git repository provider"),
|
|
1405
|
+
cadence: z.enum(["onPullRequest", "nightly", "onDemand"]).optional().describe("CI/CD trigger cadence")
|
|
1406
|
+
});
|
|
1407
|
+
var ApiKeyCreateInputSchema = z.object({
|
|
1408
|
+
name: z.string().optional().describe("Name for the API key (helps identify the key later)"),
|
|
1409
|
+
expiry: z.enum(["30d", "90d", "1y", "never"]).optional().describe("Key expiry period (default: 90d)")
|
|
1410
|
+
});
|
|
1411
|
+
var ApiKeyListInputSchema = z.object({});
|
|
1412
|
+
var ApiKeyGetInputSchema = z.object({
|
|
1413
|
+
apiKeyId: IdSchema.describe("ID of the API key to retrieve")
|
|
1414
|
+
});
|
|
1415
|
+
var ApiKeyRevokeInputSchema = z.object({
|
|
1416
|
+
apiKeyId: IdSchema.describe("ID of the API key to revoke")
|
|
1417
|
+
});
|
|
1418
|
+
var AuthLoginInputSchema = z.object({
|
|
1419
|
+
waitForCompletion: z.boolean().optional().describe("Whether to wait for browser login completion before returning. Default: true"),
|
|
1420
|
+
timeoutMs: z.number().int().positive().min(1e3).max(9e5).optional().describe("Maximum time to wait for login completion in milliseconds. Default: 120000")
|
|
1421
|
+
});
|
|
1422
|
+
var AuthPollInputSchema = z.object({
|
|
1423
|
+
deviceCode: z.string().optional().describe("Device code from the login response. Optional if a login was recently started.")
|
|
1424
|
+
});
|
|
1425
|
+
var EmptyInputSchema = z.object({});
|
|
1426
|
+
|
|
1427
|
+
// src/qa/types.ts
|
|
1428
|
+
var McpErrorCode = /* @__PURE__ */ ((McpErrorCode2) => {
|
|
1429
|
+
McpErrorCode2["UNAUTHORIZED"] = "UNAUTHORIZED";
|
|
1430
|
+
McpErrorCode2["FORBIDDEN"] = "FORBIDDEN";
|
|
1431
|
+
McpErrorCode2["NOT_FOUND"] = "NOT_FOUND";
|
|
1432
|
+
McpErrorCode2["INVALID_ARGUMENT"] = "INVALID_ARGUMENT";
|
|
1433
|
+
McpErrorCode2["UPSTREAM_ERROR"] = "UPSTREAM_ERROR";
|
|
1434
|
+
McpErrorCode2["INTERNAL_ERROR"] = "INTERNAL_ERROR";
|
|
1435
|
+
return McpErrorCode2;
|
|
1436
|
+
})(McpErrorCode || {});
|
|
1437
|
+
var GatewayError = class extends Error {
|
|
1438
|
+
/** MCP error code. */
|
|
1439
|
+
code;
|
|
1440
|
+
/** HTTP status code (if applicable). */
|
|
1441
|
+
statusCode;
|
|
1442
|
+
/** Additional error details. */
|
|
1443
|
+
details;
|
|
1444
|
+
constructor(params) {
|
|
1445
|
+
super(params.message);
|
|
1446
|
+
this.name = "GatewayError";
|
|
1447
|
+
this.code = params.code;
|
|
1448
|
+
this.statusCode = params.statusCode;
|
|
1449
|
+
this.details = params.details;
|
|
1450
|
+
}
|
|
1451
|
+
};
|
|
1452
|
+
var ALLOWED_UPSTREAM_PREFIXES = [
|
|
1453
|
+
"/v1/protected/muggle-test/",
|
|
1454
|
+
"/v1/protected/wallet/",
|
|
1455
|
+
"/v1/protected/api-keys"
|
|
1456
|
+
];
|
|
1457
|
+
var PromptServiceClient = class {
|
|
1458
|
+
httpClient;
|
|
1459
|
+
baseUrl;
|
|
1460
|
+
requestTimeoutMs;
|
|
1461
|
+
constructor() {
|
|
1462
|
+
const config = getConfig();
|
|
1463
|
+
this.baseUrl = config.qa.promptServiceBaseUrl;
|
|
1464
|
+
this.requestTimeoutMs = config.qa.requestTimeoutMs;
|
|
1465
|
+
this.httpClient = axios.create({
|
|
1466
|
+
baseURL: this.baseUrl,
|
|
1467
|
+
timeout: this.requestTimeoutMs,
|
|
1468
|
+
validateStatus: () => true
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* Validate that the upstream path is within the allowed prefix.
|
|
1473
|
+
* @param path - Path to validate.
|
|
1474
|
+
* @throws GatewayError if path is not allowed.
|
|
1475
|
+
*/
|
|
1476
|
+
validatePath(path7) {
|
|
1477
|
+
const isAllowed = ALLOWED_UPSTREAM_PREFIXES.some((prefix) => path7.startsWith(prefix));
|
|
1478
|
+
if (!isAllowed) {
|
|
1479
|
+
const logger5 = getLogger();
|
|
1480
|
+
logger5.error("Path not in allowlist", {
|
|
1481
|
+
path: path7,
|
|
1482
|
+
allowedPrefixes: ALLOWED_UPSTREAM_PREFIXES
|
|
1483
|
+
});
|
|
1484
|
+
throw new GatewayError({
|
|
1485
|
+
code: "FORBIDDEN" /* FORBIDDEN */,
|
|
1486
|
+
message: `Path '${path7}' is not allowed`
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* Build headers for upstream request with credential forwarding.
|
|
1492
|
+
* @param credentials - Caller credentials to forward.
|
|
1493
|
+
* @param correlationId - Request correlation ID.
|
|
1494
|
+
* @returns Headers object.
|
|
1495
|
+
*/
|
|
1496
|
+
buildHeaders(credentials, correlationId) {
|
|
1497
|
+
const headers = {
|
|
1498
|
+
"X-Correlation-Id": correlationId
|
|
1499
|
+
};
|
|
1500
|
+
if (credentials.bearerToken) {
|
|
1501
|
+
headers["Authorization"] = credentials.bearerToken.startsWith("Bearer ") ? credentials.bearerToken : `Bearer ${credentials.bearerToken}`;
|
|
1502
|
+
}
|
|
1503
|
+
if (credentials.apiKey) {
|
|
1504
|
+
headers["x-api-key"] = credentials.apiKey;
|
|
1505
|
+
}
|
|
1506
|
+
return headers;
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Build query string from parameters.
|
|
1510
|
+
* @param params - Query parameters.
|
|
1511
|
+
* @returns Query string (without leading '?').
|
|
1512
|
+
*/
|
|
1513
|
+
buildQueryString(params) {
|
|
1514
|
+
if (!params) {
|
|
1515
|
+
return "";
|
|
1516
|
+
}
|
|
1517
|
+
const entries = Object.entries(params).filter(([, value]) => value !== void 0).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
|
1518
|
+
return entries.length > 0 ? `?${entries.join("&")}` : "";
|
|
1519
|
+
}
|
|
1520
|
+
/**
|
|
1521
|
+
* Map HTTP status code to MCP error code.
|
|
1522
|
+
* @param statusCode - HTTP status code.
|
|
1523
|
+
* @returns MCP error code.
|
|
1524
|
+
*/
|
|
1525
|
+
mapStatusToErrorCode(statusCode) {
|
|
1526
|
+
if (statusCode === 401) {
|
|
1527
|
+
return "UNAUTHORIZED" /* UNAUTHORIZED */;
|
|
1528
|
+
}
|
|
1529
|
+
if (statusCode === 403) {
|
|
1530
|
+
return "FORBIDDEN" /* FORBIDDEN */;
|
|
1531
|
+
}
|
|
1532
|
+
if (statusCode === 404) {
|
|
1533
|
+
return "NOT_FOUND" /* NOT_FOUND */;
|
|
1534
|
+
}
|
|
1535
|
+
if (statusCode >= 400 && statusCode < 500) {
|
|
1536
|
+
return "INVALID_ARGUMENT" /* INVALID_ARGUMENT */;
|
|
1537
|
+
}
|
|
1538
|
+
return "UPSTREAM_ERROR" /* UPSTREAM_ERROR */;
|
|
1539
|
+
}
|
|
1540
|
+
/**
|
|
1541
|
+
* Format upstream error response into a user-friendly message.
|
|
1542
|
+
* @param statusCode - HTTP status code.
|
|
1543
|
+
* @param data - Response data from upstream.
|
|
1544
|
+
* @returns User-friendly error message.
|
|
1545
|
+
*/
|
|
1546
|
+
formatUpstreamErrorMessage(statusCode, data) {
|
|
1547
|
+
const responseData = data;
|
|
1548
|
+
const rawMessage = responseData?.message || responseData?.error || responseData?.detail || responseData?.details;
|
|
1549
|
+
if (rawMessage && typeof rawMessage === "string") {
|
|
1550
|
+
let cleaned = rawMessage.replace(/^Error:\s*/i, "").replace(/^INVALID_ARGUMENT:\s*/i, "").replace(/^NOT_FOUND:\s*/i, "").replace(/^FORBIDDEN:\s*/i, "");
|
|
1551
|
+
cleaned = cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
|
|
1552
|
+
return cleaned;
|
|
1553
|
+
}
|
|
1554
|
+
switch (statusCode) {
|
|
1555
|
+
case 400:
|
|
1556
|
+
return "Invalid request. Please check your input parameters.";
|
|
1557
|
+
case 401:
|
|
1558
|
+
return "Authentication required. Please check your credentials.";
|
|
1559
|
+
case 403:
|
|
1560
|
+
return "You don't have permission to perform this action.";
|
|
1561
|
+
case 404:
|
|
1562
|
+
return "The requested resource was not found.";
|
|
1563
|
+
case 409:
|
|
1564
|
+
return "A conflict occurred. The resource may already exist.";
|
|
1565
|
+
case 429:
|
|
1566
|
+
return "Too many requests. Please wait and try again.";
|
|
1567
|
+
case 500:
|
|
1568
|
+
return "The service encountered an error. Please try again later.";
|
|
1569
|
+
case 502:
|
|
1570
|
+
case 503:
|
|
1571
|
+
case 504:
|
|
1572
|
+
return "The service is temporarily unavailable. Please try again later.";
|
|
1573
|
+
default:
|
|
1574
|
+
return `Request failed with status ${statusCode}.`;
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
/**
|
|
1578
|
+
* Execute an upstream call to prompt-service.
|
|
1579
|
+
* @param call - Upstream call specification.
|
|
1580
|
+
* @param credentials - Caller credentials to forward.
|
|
1581
|
+
* @param correlationId - Request correlation ID.
|
|
1582
|
+
* @returns Upstream response.
|
|
1583
|
+
* @throws GatewayError on validation or upstream errors.
|
|
1584
|
+
*/
|
|
1585
|
+
async execute(call, credentials, correlationId) {
|
|
1586
|
+
const logger5 = getLogger();
|
|
1587
|
+
if (!credentials.bearerToken && !credentials.apiKey) {
|
|
1588
|
+
throw new GatewayError({
|
|
1589
|
+
code: "UNAUTHORIZED" /* UNAUTHORIZED */,
|
|
1590
|
+
message: "Missing authentication. Please run 'muggle-mcp login' to authenticate."
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
this.validatePath(call.path);
|
|
1594
|
+
const url = call.path + this.buildQueryString(call.queryParams);
|
|
1595
|
+
const headers = this.buildHeaders(credentials, correlationId);
|
|
1596
|
+
const timeout = call.timeoutMs || this.requestTimeoutMs;
|
|
1597
|
+
const startTime = Date.now();
|
|
1598
|
+
logger5.info("Upstream request", {
|
|
1599
|
+
correlationId,
|
|
1600
|
+
method: call.method,
|
|
1601
|
+
path: call.path,
|
|
1602
|
+
hasBody: !!call.body
|
|
1603
|
+
});
|
|
1604
|
+
try {
|
|
1605
|
+
const requestConfig = {
|
|
1606
|
+
method: call.method,
|
|
1607
|
+
url,
|
|
1608
|
+
headers,
|
|
1609
|
+
timeout
|
|
1610
|
+
};
|
|
1611
|
+
if (call.body && ["POST", "PUT", "PATCH"].includes(call.method)) {
|
|
1612
|
+
requestConfig.data = call.body;
|
|
1613
|
+
requestConfig.headers = {
|
|
1614
|
+
...headers,
|
|
1615
|
+
"Content-Type": "application/json"
|
|
1616
|
+
};
|
|
1617
|
+
} else {
|
|
1618
|
+
requestConfig.headers = {
|
|
1619
|
+
...headers,
|
|
1620
|
+
"Content-Type": "application/json"
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
const response = await this.httpClient.request(requestConfig);
|
|
1624
|
+
const latency = Date.now() - startTime;
|
|
1625
|
+
logger5.info("Upstream response", {
|
|
1626
|
+
correlationId,
|
|
1627
|
+
statusCode: response.status,
|
|
1628
|
+
latencyMs: latency
|
|
1629
|
+
});
|
|
1630
|
+
if (response.status >= 400) {
|
|
1631
|
+
const errorCode = this.mapStatusToErrorCode(response.status);
|
|
1632
|
+
const errorMessage = this.formatUpstreamErrorMessage(response.status, response.data);
|
|
1633
|
+
throw new GatewayError({
|
|
1634
|
+
code: errorCode,
|
|
1635
|
+
message: errorMessage,
|
|
1636
|
+
statusCode: response.status
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
const responseHeaders = {};
|
|
1640
|
+
for (const [key, value] of Object.entries(response.headers)) {
|
|
1641
|
+
if (typeof value === "string") {
|
|
1642
|
+
responseHeaders[key.toLowerCase()] = value;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
return {
|
|
1646
|
+
statusCode: response.status,
|
|
1647
|
+
data: response.data,
|
|
1648
|
+
headers: responseHeaders
|
|
1649
|
+
};
|
|
1650
|
+
} catch (error) {
|
|
1651
|
+
const latency = Date.now() - startTime;
|
|
1652
|
+
if (error instanceof GatewayError) {
|
|
1653
|
+
throw error;
|
|
1654
|
+
}
|
|
1655
|
+
if (error instanceof AxiosError) {
|
|
1656
|
+
logger5.error("Upstream request failed", {
|
|
1657
|
+
correlationId,
|
|
1658
|
+
error: error.message,
|
|
1659
|
+
code: error.code,
|
|
1660
|
+
latencyMs: latency
|
|
1661
|
+
});
|
|
1662
|
+
if (error.code === "ECONNABORTED" || error.code === "ETIMEDOUT") {
|
|
1663
|
+
throw new GatewayError({
|
|
1664
|
+
code: "UPSTREAM_ERROR" /* UPSTREAM_ERROR */,
|
|
1665
|
+
message: `Request timeout after ${timeout}ms`,
|
|
1666
|
+
details: { upstreamPath: call.path }
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
throw new GatewayError({
|
|
1670
|
+
code: "UPSTREAM_ERROR" /* UPSTREAM_ERROR */,
|
|
1671
|
+
message: `Upstream connection error: ${error.message}`,
|
|
1672
|
+
details: { upstreamPath: call.path }
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
logger5.error("Unknown upstream error", {
|
|
1676
|
+
correlationId,
|
|
1677
|
+
error: String(error),
|
|
1678
|
+
latencyMs: latency
|
|
1679
|
+
});
|
|
1680
|
+
throw new GatewayError({
|
|
1681
|
+
code: "INTERNAL_ERROR" /* INTERNAL_ERROR */,
|
|
1682
|
+
message: "Internal gateway error"
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
};
|
|
1687
|
+
var clientInstance = null;
|
|
1688
|
+
function getPromptServiceClient() {
|
|
1689
|
+
if (!clientInstance) {
|
|
1690
|
+
clientInstance = new PromptServiceClient();
|
|
1691
|
+
}
|
|
1692
|
+
return clientInstance;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
// src/local-qa/types/enums.ts
|
|
1696
|
+
var DeviceCodePollStatus = /* @__PURE__ */ ((DeviceCodePollStatus2) => {
|
|
1697
|
+
DeviceCodePollStatus2["Pending"] = "pending";
|
|
1698
|
+
DeviceCodePollStatus2["Complete"] = "complete";
|
|
1699
|
+
DeviceCodePollStatus2["Expired"] = "expired";
|
|
1700
|
+
DeviceCodePollStatus2["Error"] = "error";
|
|
1701
|
+
return DeviceCodePollStatus2;
|
|
1702
|
+
})(DeviceCodePollStatus || {});
|
|
1703
|
+
var SessionStatus = /* @__PURE__ */ ((SessionStatus2) => {
|
|
1704
|
+
SessionStatus2["Running"] = "running";
|
|
1705
|
+
SessionStatus2["Completed"] = "completed";
|
|
1706
|
+
SessionStatus2["Failed"] = "failed";
|
|
1707
|
+
return SessionStatus2;
|
|
1708
|
+
})(SessionStatus || {});
|
|
1709
|
+
var LocalRunStatus = /* @__PURE__ */ ((LocalRunStatus2) => {
|
|
1710
|
+
LocalRunStatus2["PENDING"] = "pending";
|
|
1711
|
+
LocalRunStatus2["RUNNING"] = "running";
|
|
1712
|
+
LocalRunStatus2["PASSED"] = "passed";
|
|
1713
|
+
LocalRunStatus2["FAILED"] = "failed";
|
|
1714
|
+
LocalRunStatus2["CANCELLED"] = "cancelled";
|
|
1715
|
+
return LocalRunStatus2;
|
|
1716
|
+
})(LocalRunStatus || {});
|
|
1717
|
+
var LocalRunType = /* @__PURE__ */ ((LocalRunType2) => {
|
|
1718
|
+
LocalRunType2["GENERATION"] = "generation";
|
|
1719
|
+
LocalRunType2["REPLAY"] = "replay";
|
|
1720
|
+
return LocalRunType2;
|
|
1721
|
+
})(LocalRunType || {});
|
|
1722
|
+
var LocalTestScriptStatus = /* @__PURE__ */ ((LocalTestScriptStatus2) => {
|
|
1723
|
+
LocalTestScriptStatus2["DRAFT"] = "draft";
|
|
1724
|
+
LocalTestScriptStatus2["GENERATED"] = "generated";
|
|
1725
|
+
LocalTestScriptStatus2["VALIDATED"] = "validated";
|
|
1726
|
+
LocalTestScriptStatus2["FAILED"] = "failed";
|
|
1727
|
+
return LocalTestScriptStatus2;
|
|
1728
|
+
})(LocalTestScriptStatus || {});
|
|
1729
|
+
var LocalWorkflowRunStatus = /* @__PURE__ */ ((LocalWorkflowRunStatus2) => {
|
|
1730
|
+
LocalWorkflowRunStatus2["PENDING"] = "pending";
|
|
1731
|
+
LocalWorkflowRunStatus2["RUNNING"] = "running";
|
|
1732
|
+
LocalWorkflowRunStatus2["COMPLETED"] = "completed";
|
|
1733
|
+
LocalWorkflowRunStatus2["FAILED"] = "failed";
|
|
1734
|
+
LocalWorkflowRunStatus2["CANCELLED"] = "cancelled";
|
|
1735
|
+
return LocalWorkflowRunStatus2;
|
|
1736
|
+
})(LocalWorkflowRunStatus || {});
|
|
1737
|
+
var CloudMappingEntityType = /* @__PURE__ */ ((CloudMappingEntityType2) => {
|
|
1738
|
+
CloudMappingEntityType2["PROJECT"] = "project";
|
|
1739
|
+
CloudMappingEntityType2["USE_CASE"] = "use_case";
|
|
1740
|
+
CloudMappingEntityType2["TEST_CASE"] = "test_case";
|
|
1741
|
+
CloudMappingEntityType2["TEST_SCRIPT"] = "test_script";
|
|
1742
|
+
return CloudMappingEntityType2;
|
|
1743
|
+
})(CloudMappingEntityType || {});
|
|
1744
|
+
var LocalWorkflowFileEntityType = /* @__PURE__ */ ((LocalWorkflowFileEntityType2) => {
|
|
1745
|
+
LocalWorkflowFileEntityType2["PROJECT"] = "project";
|
|
1746
|
+
LocalWorkflowFileEntityType2["USE_CASE"] = "use_case";
|
|
1747
|
+
LocalWorkflowFileEntityType2["TEST_CASE"] = "test_case";
|
|
1748
|
+
return LocalWorkflowFileEntityType2;
|
|
1749
|
+
})(LocalWorkflowFileEntityType || {});
|
|
1750
|
+
var ExecutionStatus = /* @__PURE__ */ ((ExecutionStatus2) => {
|
|
1751
|
+
ExecutionStatus2["Pending"] = "pending";
|
|
1752
|
+
ExecutionStatus2["Running"] = "running";
|
|
1753
|
+
ExecutionStatus2["Completed"] = "completed";
|
|
1754
|
+
ExecutionStatus2["Failed"] = "failed";
|
|
1755
|
+
ExecutionStatus2["Cancelled"] = "cancelled";
|
|
1756
|
+
return ExecutionStatus2;
|
|
1757
|
+
})(ExecutionStatus || {});
|
|
1758
|
+
var HealthStatus = /* @__PURE__ */ ((HealthStatus2) => {
|
|
1759
|
+
HealthStatus2["Healthy"] = "healthy";
|
|
1760
|
+
HealthStatus2["Degraded"] = "degraded";
|
|
1761
|
+
HealthStatus2["Unhealthy"] = "unhealthy";
|
|
1762
|
+
return HealthStatus2;
|
|
1763
|
+
})(HealthStatus || {});
|
|
1764
|
+
var TestResultStatus = /* @__PURE__ */ ((TestResultStatus2) => {
|
|
1765
|
+
TestResultStatus2["Passed"] = "passed";
|
|
1766
|
+
TestResultStatus2["Failed"] = "failed";
|
|
1767
|
+
TestResultStatus2["Skipped"] = "skipped";
|
|
1768
|
+
return TestResultStatus2;
|
|
1769
|
+
})(TestResultStatus || {});
|
|
1770
|
+
|
|
1771
|
+
// src/local-qa/services/auth-service.ts
|
|
1772
|
+
var DEFAULT_LOGIN_WAIT_TIMEOUT_MS = 12e4;
|
|
1773
|
+
var AuthService = class {
|
|
1774
|
+
/** Path to the auth file. */
|
|
1775
|
+
authFilePath;
|
|
1776
|
+
/** Path to the pending device code file. */
|
|
1777
|
+
pendingDeviceCodePath;
|
|
1778
|
+
/**
|
|
1779
|
+
* Create a new AuthService.
|
|
1780
|
+
*/
|
|
1781
|
+
constructor() {
|
|
1782
|
+
const config = getConfig();
|
|
1783
|
+
this.authFilePath = config.localQa.authFilePath;
|
|
1784
|
+
this.pendingDeviceCodePath = path.join(
|
|
1785
|
+
path.dirname(config.localQa.authFilePath),
|
|
1786
|
+
"pending-device-code.json"
|
|
1787
|
+
);
|
|
1788
|
+
}
|
|
1789
|
+
/**
|
|
1790
|
+
* Get current authentication status.
|
|
1791
|
+
*/
|
|
1792
|
+
getAuthStatus() {
|
|
1793
|
+
const logger5 = getLogger();
|
|
1794
|
+
const storedAuth = this.loadStoredAuth();
|
|
1795
|
+
if (!storedAuth) {
|
|
1796
|
+
logger5.debug("No stored auth found");
|
|
1797
|
+
return { authenticated: false };
|
|
1798
|
+
}
|
|
1799
|
+
const now = /* @__PURE__ */ new Date();
|
|
1800
|
+
const expiresAt = new Date(storedAuth.expiresAt);
|
|
1801
|
+
const isExpired = now >= expiresAt;
|
|
1802
|
+
logger5.debug("Auth status checked", {
|
|
1803
|
+
email: storedAuth.email,
|
|
1804
|
+
isExpired,
|
|
1805
|
+
expiresAt: storedAuth.expiresAt
|
|
1806
|
+
});
|
|
1807
|
+
return {
|
|
1808
|
+
authenticated: !isExpired,
|
|
1809
|
+
email: storedAuth.email,
|
|
1810
|
+
userId: storedAuth.userId,
|
|
1811
|
+
expiresAt: storedAuth.expiresAt,
|
|
1812
|
+
isExpired
|
|
1813
|
+
};
|
|
1814
|
+
}
|
|
1815
|
+
/**
|
|
1816
|
+
* Start the device code flow.
|
|
1817
|
+
*/
|
|
1818
|
+
async startDeviceCodeFlow() {
|
|
1819
|
+
const logger5 = getLogger();
|
|
1820
|
+
const config = getConfig();
|
|
1821
|
+
const { domain, clientId, audience, scopes } = config.localQa.auth0;
|
|
1822
|
+
logger5.info("Starting device code flow");
|
|
1823
|
+
const url = `https://${domain}/oauth/device/code`;
|
|
1824
|
+
const body = new URLSearchParams({
|
|
1825
|
+
client_id: clientId,
|
|
1826
|
+
scope: scopes.join(" "),
|
|
1827
|
+
audience
|
|
1828
|
+
});
|
|
1829
|
+
const response = await fetch(url, {
|
|
1830
|
+
method: "POST",
|
|
1831
|
+
headers: {
|
|
1832
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
1833
|
+
},
|
|
1834
|
+
body: body.toString()
|
|
1835
|
+
});
|
|
1836
|
+
if (!response.ok) {
|
|
1837
|
+
const errorText = await response.text();
|
|
1838
|
+
logger5.error("Device code request failed", {
|
|
1839
|
+
status: response.status,
|
|
1840
|
+
error: errorText
|
|
1841
|
+
});
|
|
1842
|
+
throw new Error(`Failed to start device code flow: ${response.status} ${errorText}`);
|
|
1843
|
+
}
|
|
1844
|
+
const data = await response.json();
|
|
1845
|
+
logger5.info("Device code flow started", {
|
|
1846
|
+
userCode: data.user_code,
|
|
1847
|
+
verificationUri: data.verification_uri,
|
|
1848
|
+
expiresIn: data.expires_in
|
|
1849
|
+
});
|
|
1850
|
+
this.storePendingDeviceCode({
|
|
1851
|
+
deviceCode: data.device_code,
|
|
1852
|
+
userCode: data.user_code,
|
|
1853
|
+
expiresAt: new Date(Date.now() + data.expires_in * 1e3).toISOString()
|
|
1854
|
+
});
|
|
1855
|
+
const browserOpenResult = await openBrowserUrl({
|
|
1856
|
+
url: data.verification_uri_complete
|
|
1857
|
+
});
|
|
1858
|
+
if (browserOpenResult.opened) {
|
|
1859
|
+
logger5.info("Browser opened for device code login");
|
|
1860
|
+
} else {
|
|
1861
|
+
logger5.warn("Failed to open browser for device code login", {
|
|
1862
|
+
error: browserOpenResult.error,
|
|
1863
|
+
verificationUriComplete: data.verification_uri_complete
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
return {
|
|
1867
|
+
deviceCode: data.device_code,
|
|
1868
|
+
userCode: data.user_code,
|
|
1869
|
+
verificationUri: data.verification_uri,
|
|
1870
|
+
verificationUriComplete: data.verification_uri_complete,
|
|
1871
|
+
expiresIn: data.expires_in,
|
|
1872
|
+
interval: data.interval,
|
|
1873
|
+
browserOpened: browserOpenResult.opened,
|
|
1874
|
+
browserOpenError: browserOpenResult.error
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
/**
|
|
1878
|
+
* Store a pending device code for later retrieval.
|
|
1879
|
+
*/
|
|
1880
|
+
storePendingDeviceCode(params) {
|
|
1881
|
+
const logger5 = getLogger();
|
|
1882
|
+
const dir = path.dirname(this.pendingDeviceCodePath);
|
|
1883
|
+
if (!fs4.existsSync(dir)) {
|
|
1884
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
1885
|
+
}
|
|
1886
|
+
fs4.writeFileSync(this.pendingDeviceCodePath, JSON.stringify(params, null, 2), {
|
|
1887
|
+
encoding: "utf-8",
|
|
1888
|
+
mode: 384
|
|
1889
|
+
});
|
|
1890
|
+
logger5.debug("Pending device code stored", { userCode: params.userCode });
|
|
1891
|
+
}
|
|
1892
|
+
/**
|
|
1893
|
+
* Get the pending device code if one exists and is not expired.
|
|
1894
|
+
*/
|
|
1895
|
+
getPendingDeviceCode() {
|
|
1896
|
+
const logger5 = getLogger();
|
|
1897
|
+
if (!fs4.existsSync(this.pendingDeviceCodePath)) {
|
|
1898
|
+
logger5.debug("No pending device code found");
|
|
1899
|
+
return null;
|
|
1900
|
+
}
|
|
1901
|
+
try {
|
|
1902
|
+
const content = fs4.readFileSync(this.pendingDeviceCodePath, "utf-8");
|
|
1903
|
+
const data = JSON.parse(content);
|
|
1904
|
+
const now = /* @__PURE__ */ new Date();
|
|
1905
|
+
const expiresAt = new Date(data.expiresAt);
|
|
1906
|
+
if (now >= expiresAt) {
|
|
1907
|
+
logger5.debug("Pending device code expired");
|
|
1908
|
+
this.clearPendingDeviceCode();
|
|
1909
|
+
return null;
|
|
1910
|
+
}
|
|
1911
|
+
return data.deviceCode;
|
|
1912
|
+
} catch (error) {
|
|
1913
|
+
logger5.warn("Failed to read pending device code", {
|
|
1914
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1915
|
+
});
|
|
1916
|
+
return null;
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
/**
|
|
1920
|
+
* Clear the pending device code file.
|
|
1921
|
+
*/
|
|
1922
|
+
clearPendingDeviceCode() {
|
|
1923
|
+
const logger5 = getLogger();
|
|
1924
|
+
if (fs4.existsSync(this.pendingDeviceCodePath)) {
|
|
1925
|
+
try {
|
|
1926
|
+
fs4.unlinkSync(this.pendingDeviceCodePath);
|
|
1927
|
+
logger5.debug("Pending device code cleared");
|
|
1928
|
+
} catch (error) {
|
|
1929
|
+
logger5.warn("Failed to clear pending device code", {
|
|
1930
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
/**
|
|
1936
|
+
* Poll for device code authorization completion.
|
|
1937
|
+
*/
|
|
1938
|
+
async pollDeviceCode(deviceCode) {
|
|
1939
|
+
const logger5 = getLogger();
|
|
1940
|
+
const config = getConfig();
|
|
1941
|
+
const { domain, clientId } = config.localQa.auth0;
|
|
1942
|
+
logger5.debug("Polling for device code authorization");
|
|
1943
|
+
const url = `https://${domain}/oauth/token`;
|
|
1944
|
+
const body = new URLSearchParams({
|
|
1945
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
1946
|
+
client_id: clientId,
|
|
1947
|
+
device_code: deviceCode
|
|
1948
|
+
});
|
|
1949
|
+
try {
|
|
1950
|
+
const response = await fetch(url, {
|
|
1951
|
+
method: "POST",
|
|
1952
|
+
headers: {
|
|
1953
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
1954
|
+
},
|
|
1955
|
+
body: body.toString()
|
|
1956
|
+
});
|
|
1957
|
+
if (response.ok) {
|
|
1958
|
+
const tokenData = await response.json();
|
|
1959
|
+
const tokenResponse = {
|
|
1960
|
+
accessToken: tokenData.access_token,
|
|
1961
|
+
refreshToken: tokenData.refresh_token,
|
|
1962
|
+
tokenType: tokenData.token_type,
|
|
1963
|
+
expiresIn: tokenData.expires_in
|
|
1964
|
+
};
|
|
1965
|
+
const userInfo = await this.getUserInfo(tokenResponse.accessToken);
|
|
1966
|
+
await this.storeAuth({
|
|
1967
|
+
tokenResponse,
|
|
1968
|
+
email: userInfo.email,
|
|
1969
|
+
userId: userInfo.sub
|
|
1970
|
+
});
|
|
1971
|
+
this.clearPendingDeviceCode();
|
|
1972
|
+
logger5.info("Device code authorization complete", { email: userInfo.email });
|
|
1973
|
+
return {
|
|
1974
|
+
status: "complete" /* Complete */,
|
|
1975
|
+
message: "Authentication successful!",
|
|
1976
|
+
email: userInfo.email
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
const errorData = await response.json();
|
|
1980
|
+
if (errorData.error === "authorization_pending") {
|
|
1981
|
+
logger5.debug("Authorization pending");
|
|
1982
|
+
return {
|
|
1983
|
+
status: "pending" /* Pending */,
|
|
1984
|
+
message: "Waiting for user to complete authorization..."
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
if (errorData.error === "slow_down") {
|
|
1988
|
+
logger5.debug("Polling too fast");
|
|
1989
|
+
return {
|
|
1990
|
+
status: "pending" /* Pending */,
|
|
1991
|
+
message: "Polling too fast, slowing down..."
|
|
1992
|
+
};
|
|
1993
|
+
}
|
|
1994
|
+
if (errorData.error === "expired_token") {
|
|
1995
|
+
logger5.warn("Device code expired");
|
|
1996
|
+
return {
|
|
1997
|
+
status: "expired" /* Expired */,
|
|
1998
|
+
message: "The authorization code has expired. Please start again."
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
2001
|
+
if (errorData.error === "access_denied") {
|
|
2002
|
+
logger5.warn("Access denied");
|
|
2003
|
+
return {
|
|
2004
|
+
status: "error" /* Error */,
|
|
2005
|
+
message: "Access was denied by the user.",
|
|
2006
|
+
error: errorData.error_description ?? errorData.error
|
|
2007
|
+
};
|
|
2008
|
+
}
|
|
2009
|
+
logger5.error("Unexpected error during polling", { error: errorData });
|
|
2010
|
+
return {
|
|
2011
|
+
status: "error" /* Error */,
|
|
2012
|
+
message: errorData.error_description ?? errorData.error,
|
|
2013
|
+
error: errorData.error
|
|
2014
|
+
};
|
|
2015
|
+
} catch (error) {
|
|
2016
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2017
|
+
logger5.error("Poll request failed", { error: errorMessage });
|
|
2018
|
+
return {
|
|
2019
|
+
status: "error" /* Error */,
|
|
2020
|
+
message: `Poll request failed: ${errorMessage}`,
|
|
2021
|
+
error: errorMessage
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
/**
|
|
2026
|
+
* Poll for device code authorization until completion or timeout.
|
|
2027
|
+
*/
|
|
2028
|
+
async waitForDeviceCodeAuthorization(params) {
|
|
2029
|
+
const logger5 = getLogger();
|
|
2030
|
+
const timeoutMs = params.timeoutMs ?? DEFAULT_LOGIN_WAIT_TIMEOUT_MS;
|
|
2031
|
+
const pollIntervalMs = Math.max(params.intervalSeconds, 1) * 1e3;
|
|
2032
|
+
const startedAt = Date.now();
|
|
2033
|
+
logger5.info("Waiting for device code authorization", {
|
|
2034
|
+
timeoutMs,
|
|
2035
|
+
pollIntervalMs
|
|
2036
|
+
});
|
|
2037
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
2038
|
+
const result = await this.pollDeviceCode(params.deviceCode);
|
|
2039
|
+
if (result.status !== "pending" /* Pending */) {
|
|
2040
|
+
return result;
|
|
2041
|
+
}
|
|
2042
|
+
const remainingMs = timeoutMs - (Date.now() - startedAt);
|
|
2043
|
+
if (remainingMs <= 0) {
|
|
2044
|
+
break;
|
|
2045
|
+
}
|
|
2046
|
+
await sleep({ durationMs: Math.min(pollIntervalMs, remainingMs) });
|
|
2047
|
+
}
|
|
2048
|
+
return {
|
|
2049
|
+
status: "pending" /* Pending */,
|
|
2050
|
+
message: "Timed out waiting for browser login confirmation. Please finish login in your browser, then call `muggle_auth_poll` to continue."
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
2053
|
+
/**
|
|
2054
|
+
* Get user info from Auth0.
|
|
2055
|
+
*/
|
|
2056
|
+
async getUserInfo(accessToken) {
|
|
2057
|
+
const logger5 = getLogger();
|
|
2058
|
+
const config = getConfig();
|
|
2059
|
+
const { domain } = config.localQa.auth0;
|
|
2060
|
+
const url = `https://${domain}/userinfo`;
|
|
2061
|
+
try {
|
|
2062
|
+
const response = await fetch(url, {
|
|
2063
|
+
method: "GET",
|
|
2064
|
+
headers: {
|
|
2065
|
+
Authorization: `Bearer ${accessToken}`
|
|
2066
|
+
}
|
|
2067
|
+
});
|
|
2068
|
+
if (!response.ok) {
|
|
2069
|
+
logger5.warn("Failed to get user info", { status: response.status });
|
|
2070
|
+
return {};
|
|
2071
|
+
}
|
|
2072
|
+
const data = await response.json();
|
|
2073
|
+
return data;
|
|
2074
|
+
} catch (error) {
|
|
2075
|
+
logger5.warn("User info request failed", {
|
|
2076
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2077
|
+
});
|
|
2078
|
+
return {};
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
/**
|
|
2082
|
+
* Store authentication tokens.
|
|
2083
|
+
*/
|
|
2084
|
+
async storeAuth(params) {
|
|
2085
|
+
const { tokenResponse, email, userId } = params;
|
|
2086
|
+
const logger5 = getLogger();
|
|
2087
|
+
const expiresAt = new Date(Date.now() + tokenResponse.expiresIn * 1e3).toISOString();
|
|
2088
|
+
const storedAuth = {
|
|
2089
|
+
accessToken: tokenResponse.accessToken,
|
|
2090
|
+
refreshToken: tokenResponse.refreshToken,
|
|
2091
|
+
expiresAt,
|
|
2092
|
+
email,
|
|
2093
|
+
userId
|
|
2094
|
+
};
|
|
2095
|
+
const dir = path.dirname(this.authFilePath);
|
|
2096
|
+
if (!fs4.existsSync(dir)) {
|
|
2097
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
2098
|
+
}
|
|
2099
|
+
fs4.writeFileSync(this.authFilePath, JSON.stringify(storedAuth, null, 2), {
|
|
2100
|
+
encoding: "utf-8",
|
|
2101
|
+
mode: 384
|
|
2102
|
+
});
|
|
2103
|
+
logger5.info("Auth stored successfully", { email, expiresAt });
|
|
2104
|
+
}
|
|
2105
|
+
/**
|
|
2106
|
+
* Load stored authentication.
|
|
2107
|
+
*/
|
|
2108
|
+
loadStoredAuth() {
|
|
2109
|
+
const logger5 = getLogger();
|
|
2110
|
+
if (!fs4.existsSync(this.authFilePath)) {
|
|
2111
|
+
return null;
|
|
2112
|
+
}
|
|
2113
|
+
try {
|
|
2114
|
+
const content = fs4.readFileSync(this.authFilePath, "utf-8");
|
|
2115
|
+
return JSON.parse(content);
|
|
2116
|
+
} catch (error) {
|
|
2117
|
+
logger5.error("Failed to load stored auth", {
|
|
2118
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2119
|
+
});
|
|
2120
|
+
return null;
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
/**
|
|
2124
|
+
* Get the current access token (if valid).
|
|
2125
|
+
*/
|
|
2126
|
+
getAccessToken() {
|
|
2127
|
+
const storedAuth = this.loadStoredAuth();
|
|
2128
|
+
if (!storedAuth) {
|
|
2129
|
+
return null;
|
|
2130
|
+
}
|
|
2131
|
+
const now = /* @__PURE__ */ new Date();
|
|
2132
|
+
const expiresAt = new Date(storedAuth.expiresAt);
|
|
2133
|
+
if (now >= expiresAt) {
|
|
2134
|
+
return null;
|
|
2135
|
+
}
|
|
2136
|
+
return storedAuth.accessToken;
|
|
2137
|
+
}
|
|
2138
|
+
/**
|
|
2139
|
+
* Clear stored authentication (logout).
|
|
2140
|
+
*/
|
|
2141
|
+
logout() {
|
|
2142
|
+
const logger5 = getLogger();
|
|
2143
|
+
if (!fs4.existsSync(this.authFilePath)) {
|
|
2144
|
+
logger5.debug("No auth to clear");
|
|
2145
|
+
return false;
|
|
2146
|
+
}
|
|
2147
|
+
try {
|
|
2148
|
+
fs4.unlinkSync(this.authFilePath);
|
|
2149
|
+
logger5.info("Auth cleared successfully");
|
|
2150
|
+
return true;
|
|
2151
|
+
} catch (error) {
|
|
2152
|
+
logger5.error("Failed to clear auth", {
|
|
2153
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2154
|
+
});
|
|
2155
|
+
return false;
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
};
|
|
2159
|
+
var serviceInstance = null;
|
|
2160
|
+
function getAuthService() {
|
|
2161
|
+
serviceInstance ??= new AuthService();
|
|
2162
|
+
return serviceInstance;
|
|
2163
|
+
}
|
|
2164
|
+
function resetAuthService() {
|
|
2165
|
+
serviceInstance = null;
|
|
2166
|
+
}
|
|
2167
|
+
function sleep(params) {
|
|
2168
|
+
return new Promise((resolve2) => {
|
|
2169
|
+
setTimeout(() => {
|
|
2170
|
+
resolve2();
|
|
2171
|
+
}, params.durationMs);
|
|
2172
|
+
});
|
|
2173
|
+
}
|
|
2174
|
+
var DEFAULT_SESSION_MAX_AGE_DAYS = 30;
|
|
2175
|
+
var StorageService = class {
|
|
2176
|
+
/** Base data directory. */
|
|
2177
|
+
dataDir;
|
|
2178
|
+
/** Sessions directory. */
|
|
2179
|
+
sessionsDir;
|
|
2180
|
+
/**
|
|
2181
|
+
* Create a new StorageService.
|
|
2182
|
+
*/
|
|
2183
|
+
constructor() {
|
|
2184
|
+
const config = getConfig();
|
|
2185
|
+
this.dataDir = config.localQa.dataDir;
|
|
2186
|
+
this.sessionsDir = config.localQa.sessionsDir;
|
|
2187
|
+
}
|
|
2188
|
+
/**
|
|
2189
|
+
* Ensure the base directories exist.
|
|
2190
|
+
*/
|
|
2191
|
+
ensureDirectories() {
|
|
2192
|
+
const logger5 = getLogger();
|
|
2193
|
+
if (!fs4.existsSync(this.dataDir)) {
|
|
2194
|
+
fs4.mkdirSync(this.dataDir, { recursive: true });
|
|
2195
|
+
logger5.info("Created data directory", { path: this.dataDir });
|
|
2196
|
+
}
|
|
2197
|
+
if (!fs4.existsSync(this.sessionsDir)) {
|
|
2198
|
+
fs4.mkdirSync(this.sessionsDir, { recursive: true });
|
|
2199
|
+
logger5.info("Created sessions directory", { path: this.sessionsDir });
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
/**
|
|
2203
|
+
* Create a new session directory.
|
|
2204
|
+
* @param sessionId - Unique session ID.
|
|
2205
|
+
* @returns Path to the session directory.
|
|
2206
|
+
*/
|
|
2207
|
+
createSessionDirectory(sessionId) {
|
|
2208
|
+
const logger5 = getLogger();
|
|
2209
|
+
this.ensureDirectories();
|
|
2210
|
+
const sessionDir = path.join(this.sessionsDir, sessionId);
|
|
2211
|
+
if (!fs4.existsSync(sessionDir)) {
|
|
2212
|
+
fs4.mkdirSync(sessionDir, { recursive: true });
|
|
2213
|
+
fs4.mkdirSync(path.join(sessionDir, "screenshots"), { recursive: true });
|
|
2214
|
+
fs4.mkdirSync(path.join(sessionDir, "logs"), { recursive: true });
|
|
2215
|
+
logger5.info("Created session directory", { sessionId, path: sessionDir });
|
|
2216
|
+
}
|
|
2217
|
+
return sessionDir;
|
|
2218
|
+
}
|
|
2219
|
+
/**
|
|
2220
|
+
* Save session metadata.
|
|
2221
|
+
* @param metadata - Session metadata to save.
|
|
2222
|
+
*/
|
|
2223
|
+
saveSessionMetadata(metadata) {
|
|
2224
|
+
const logger5 = getLogger();
|
|
2225
|
+
const sessionDir = this.createSessionDirectory(metadata.sessionId);
|
|
2226
|
+
const metadataPath = path.join(sessionDir, "metadata.json");
|
|
2227
|
+
fs4.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
|
|
2228
|
+
logger5.debug("Saved session metadata", { sessionId: metadata.sessionId });
|
|
2229
|
+
}
|
|
2230
|
+
/**
|
|
2231
|
+
* Load session metadata.
|
|
2232
|
+
* @param sessionId - Session ID to load.
|
|
2233
|
+
* @returns Session metadata, or null if not found.
|
|
2234
|
+
*/
|
|
2235
|
+
loadSessionMetadata(sessionId) {
|
|
2236
|
+
const logger5 = getLogger();
|
|
2237
|
+
const metadataPath = path.join(this.sessionsDir, sessionId, "metadata.json");
|
|
2238
|
+
if (!fs4.existsSync(metadataPath)) {
|
|
2239
|
+
return null;
|
|
2240
|
+
}
|
|
2241
|
+
try {
|
|
2242
|
+
const content = fs4.readFileSync(metadataPath, "utf-8");
|
|
2243
|
+
return JSON.parse(content);
|
|
2244
|
+
} catch (error) {
|
|
2245
|
+
logger5.error("Failed to load session metadata", {
|
|
2246
|
+
sessionId,
|
|
2247
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2248
|
+
});
|
|
2249
|
+
return null;
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
/**
|
|
2253
|
+
* List all sessions.
|
|
2254
|
+
* @returns Array of session IDs.
|
|
2255
|
+
*/
|
|
2256
|
+
listSessions() {
|
|
2257
|
+
if (!fs4.existsSync(this.sessionsDir)) {
|
|
2258
|
+
return [];
|
|
2259
|
+
}
|
|
2260
|
+
return fs4.readdirSync(this.sessionsDir).filter((entry) => {
|
|
2261
|
+
const entryPath = path.join(this.sessionsDir, entry);
|
|
2262
|
+
return fs4.statSync(entryPath).isDirectory();
|
|
2263
|
+
});
|
|
2264
|
+
}
|
|
2265
|
+
/**
|
|
2266
|
+
* Get the current session ID (if any).
|
|
2267
|
+
* @returns Current session ID, or null.
|
|
2268
|
+
*/
|
|
2269
|
+
getCurrentSessionId() {
|
|
2270
|
+
const currentPath = path.join(this.sessionsDir, "current");
|
|
2271
|
+
if (!fs4.existsSync(currentPath)) {
|
|
2272
|
+
return null;
|
|
2273
|
+
}
|
|
2274
|
+
try {
|
|
2275
|
+
const target = fs4.readlinkSync(currentPath);
|
|
2276
|
+
return path.basename(target);
|
|
2277
|
+
} catch {
|
|
2278
|
+
return null;
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
/**
|
|
2282
|
+
* Set the current session.
|
|
2283
|
+
* @param sessionId - Session ID to set as current.
|
|
2284
|
+
*/
|
|
2285
|
+
setCurrentSession(sessionId) {
|
|
2286
|
+
const logger5 = getLogger();
|
|
2287
|
+
const currentPath = path.join(this.sessionsDir, "current");
|
|
2288
|
+
const targetPath = path.join(this.sessionsDir, sessionId);
|
|
2289
|
+
if (fs4.existsSync(currentPath)) {
|
|
2290
|
+
fs4.unlinkSync(currentPath);
|
|
2291
|
+
}
|
|
2292
|
+
fs4.symlinkSync(targetPath, currentPath);
|
|
2293
|
+
logger5.info("Set current session", { sessionId });
|
|
2294
|
+
}
|
|
2295
|
+
/**
|
|
2296
|
+
* Save a screenshot to the session directory.
|
|
2297
|
+
* @param params - Screenshot save parameters.
|
|
2298
|
+
*/
|
|
2299
|
+
saveScreenshot(params) {
|
|
2300
|
+
const { sessionId, filename, data } = params;
|
|
2301
|
+
const logger5 = getLogger();
|
|
2302
|
+
const sessionDir = this.createSessionDirectory(sessionId);
|
|
2303
|
+
const screenshotPath = path.join(sessionDir, "screenshots", filename);
|
|
2304
|
+
fs4.writeFileSync(screenshotPath, data);
|
|
2305
|
+
logger5.debug("Saved screenshot", { sessionId, filename });
|
|
2306
|
+
return screenshotPath;
|
|
2307
|
+
}
|
|
2308
|
+
/**
|
|
2309
|
+
* Append to the results markdown file.
|
|
2310
|
+
* @param params - Results append parameters.
|
|
2311
|
+
*/
|
|
2312
|
+
appendToResults(params) {
|
|
2313
|
+
const { sessionId, content } = params;
|
|
2314
|
+
const logger5 = getLogger();
|
|
2315
|
+
const sessionDir = this.createSessionDirectory(sessionId);
|
|
2316
|
+
const resultsPath = path.join(sessionDir, "results.md");
|
|
2317
|
+
fs4.appendFileSync(resultsPath, content + "\n", "utf-8");
|
|
2318
|
+
logger5.debug("Appended to results", { sessionId });
|
|
2319
|
+
}
|
|
2320
|
+
/**
|
|
2321
|
+
* Get the results markdown content.
|
|
2322
|
+
* @param sessionId - Session ID.
|
|
2323
|
+
* @returns Results content, or null if not found.
|
|
2324
|
+
*/
|
|
2325
|
+
getResults(sessionId) {
|
|
2326
|
+
const resultsPath = path.join(this.sessionsDir, sessionId, "results.md");
|
|
2327
|
+
if (!fs4.existsSync(resultsPath)) {
|
|
2328
|
+
return null;
|
|
2329
|
+
}
|
|
2330
|
+
return fs4.readFileSync(resultsPath, "utf-8");
|
|
2331
|
+
}
|
|
2332
|
+
/**
|
|
2333
|
+
* Get the data directory path.
|
|
2334
|
+
* @returns Data directory path.
|
|
2335
|
+
*/
|
|
2336
|
+
getDataDir() {
|
|
2337
|
+
return this.dataDir;
|
|
2338
|
+
}
|
|
2339
|
+
/**
|
|
2340
|
+
* Get the sessions directory path.
|
|
2341
|
+
* @returns Sessions directory path.
|
|
2342
|
+
*/
|
|
2343
|
+
getSessionsDir() {
|
|
2344
|
+
return this.sessionsDir;
|
|
2345
|
+
}
|
|
2346
|
+
/**
|
|
2347
|
+
* Create a new session with metadata.
|
|
2348
|
+
* @param params - Session creation parameters.
|
|
2349
|
+
* @returns Path to the session directory.
|
|
2350
|
+
*/
|
|
2351
|
+
createSession(params) {
|
|
2352
|
+
const { sessionId, targetUrl, testInstructions } = params;
|
|
2353
|
+
const logger5 = getLogger();
|
|
2354
|
+
const sessionDir = this.createSessionDirectory(sessionId);
|
|
2355
|
+
const metadata = {
|
|
2356
|
+
sessionId,
|
|
2357
|
+
workflowRunId: sessionId,
|
|
2358
|
+
status: "running" /* Running */,
|
|
2359
|
+
startTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2360
|
+
targetUrl,
|
|
2361
|
+
testInstructions
|
|
2362
|
+
};
|
|
2363
|
+
this.saveSessionMetadata(metadata);
|
|
2364
|
+
this.setCurrentSession(sessionId);
|
|
2365
|
+
logger5.info("Created session", { sessionId, targetUrl });
|
|
2366
|
+
return sessionDir;
|
|
2367
|
+
}
|
|
2368
|
+
/**
|
|
2369
|
+
* Update the status of an existing session.
|
|
2370
|
+
* @param params - Status update parameters.
|
|
2371
|
+
*/
|
|
2372
|
+
updateSessionStatus(params) {
|
|
2373
|
+
const { sessionId, status } = params;
|
|
2374
|
+
const logger5 = getLogger();
|
|
2375
|
+
const metadata = this.loadSessionMetadata(sessionId);
|
|
2376
|
+
if (!metadata) {
|
|
2377
|
+
logger5.warn("Session not found for status update", { sessionId });
|
|
2378
|
+
return;
|
|
2379
|
+
}
|
|
2380
|
+
metadata.status = status;
|
|
2381
|
+
if (status === "completed" /* Completed */ || status === "failed" /* Failed */) {
|
|
2382
|
+
metadata.endTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
2383
|
+
}
|
|
2384
|
+
this.saveSessionMetadata(metadata);
|
|
2385
|
+
logger5.debug("Updated session status", { sessionId, status });
|
|
2386
|
+
}
|
|
2387
|
+
/**
|
|
2388
|
+
* Initialize the results.md file with a header.
|
|
2389
|
+
* @param params - Initialization parameters.
|
|
2390
|
+
*/
|
|
2391
|
+
initializeResults(params) {
|
|
2392
|
+
const { sessionId, targetUrl, testInstructions } = params;
|
|
2393
|
+
const logger5 = getLogger();
|
|
2394
|
+
const sessionDir = this.createSessionDirectory(sessionId);
|
|
2395
|
+
const resultsPath = path.join(sessionDir, "results.md");
|
|
2396
|
+
const header = [
|
|
2397
|
+
`# Test Results: ${sessionId}`,
|
|
2398
|
+
"",
|
|
2399
|
+
`**Target URL:** ${targetUrl}`,
|
|
2400
|
+
`**Started:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
2401
|
+
`**Instructions:** ${testInstructions}`,
|
|
2402
|
+
"",
|
|
2403
|
+
"---",
|
|
2404
|
+
"",
|
|
2405
|
+
"## Test Steps",
|
|
2406
|
+
""
|
|
2407
|
+
].join("\n");
|
|
2408
|
+
fs4.writeFileSync(resultsPath, header, "utf-8");
|
|
2409
|
+
logger5.debug("Initialized results.md", { sessionId });
|
|
2410
|
+
}
|
|
2411
|
+
/**
|
|
2412
|
+
* Append a test step to the results.md file.
|
|
2413
|
+
* @param params - Step parameters.
|
|
2414
|
+
*/
|
|
2415
|
+
appendStepToResults(params) {
|
|
2416
|
+
const { sessionId, step } = params;
|
|
2417
|
+
const logger5 = getLogger();
|
|
2418
|
+
const sessionDir = this.createSessionDirectory(sessionId);
|
|
2419
|
+
const resultsPath = path.join(sessionDir, "results.md");
|
|
2420
|
+
const statusIcon = step.success ? "\u2713" : "\u2717";
|
|
2421
|
+
const stepContent = [
|
|
2422
|
+
`### Step ${step.stepNumber}: ${step.action}`,
|
|
2423
|
+
"",
|
|
2424
|
+
step.target ? `- **Target:** ${step.target}` : "",
|
|
2425
|
+
`- **Result:** ${step.result} ${statusIcon}`,
|
|
2426
|
+
step.screenshotPath ? `- **Screenshot:** [step-${String(step.stepNumber).padStart(3, "0")}.png](screenshots/step-${String(step.stepNumber).padStart(3, "0")}.png)` : "",
|
|
2427
|
+
""
|
|
2428
|
+
].filter(Boolean).join("\n");
|
|
2429
|
+
fs4.appendFileSync(resultsPath, stepContent + "\n", "utf-8");
|
|
2430
|
+
logger5.debug("Appended step to results", { sessionId, stepNumber: step.stepNumber });
|
|
2431
|
+
const metadata = this.loadSessionMetadata(sessionId);
|
|
2432
|
+
if (metadata) {
|
|
2433
|
+
metadata.stepsCount = (metadata.stepsCount ?? 0) + 1;
|
|
2434
|
+
this.saveSessionMetadata(metadata);
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
/**
|
|
2438
|
+
* Finalize the results.md file with a summary.
|
|
2439
|
+
* @param params - Finalization parameters.
|
|
2440
|
+
*/
|
|
2441
|
+
finalizeResults(params) {
|
|
2442
|
+
const { sessionId, status, summary } = params;
|
|
2443
|
+
const logger5 = getLogger();
|
|
2444
|
+
const sessionDir = path.join(this.sessionsDir, sessionId);
|
|
2445
|
+
const resultsPath = path.join(sessionDir, "results.md");
|
|
2446
|
+
if (!fs4.existsSync(resultsPath)) {
|
|
2447
|
+
logger5.warn("Results file not found for finalization", { sessionId });
|
|
2448
|
+
return;
|
|
2449
|
+
}
|
|
2450
|
+
const metadata = this.loadSessionMetadata(sessionId);
|
|
2451
|
+
const endTime = /* @__PURE__ */ new Date();
|
|
2452
|
+
const startTime = metadata?.startTime ? new Date(metadata.startTime) : endTime;
|
|
2453
|
+
const durationMs = endTime.getTime() - startTime.getTime();
|
|
2454
|
+
const durationSeconds = (durationMs / 1e3).toFixed(2);
|
|
2455
|
+
const statusDisplay = status === "completed" /* Completed */ ? "\u2713 Passed" : status === "failed" /* Failed */ ? "\u2717 Failed" : "\u2014 Running";
|
|
2456
|
+
const footer = [
|
|
2457
|
+
"",
|
|
2458
|
+
"---",
|
|
2459
|
+
"",
|
|
2460
|
+
"## Summary",
|
|
2461
|
+
"",
|
|
2462
|
+
`**Status:** ${statusDisplay}`,
|
|
2463
|
+
`**Duration:** ${durationSeconds}s`,
|
|
2464
|
+
`**Steps:** ${metadata?.stepsCount ?? 0}`,
|
|
2465
|
+
`**Completed:** ${endTime.toISOString()}`,
|
|
2466
|
+
"",
|
|
2467
|
+
summary ? summary : ""
|
|
2468
|
+
].join("\n");
|
|
2469
|
+
fs4.appendFileSync(resultsPath, footer, "utf-8");
|
|
2470
|
+
logger5.debug("Finalized results.md", { sessionId, status });
|
|
2471
|
+
if (metadata) {
|
|
2472
|
+
metadata.durationMs = durationMs;
|
|
2473
|
+
metadata.endTime = endTime.toISOString();
|
|
2474
|
+
metadata.status = status;
|
|
2475
|
+
this.saveSessionMetadata(metadata);
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
/**
|
|
2479
|
+
* List all sessions with their metadata.
|
|
2480
|
+
* @returns Array of session summaries.
|
|
2481
|
+
*/
|
|
2482
|
+
listSessionsWithMetadata() {
|
|
2483
|
+
const sessionIds = this.listSessions();
|
|
2484
|
+
const summaries = [];
|
|
2485
|
+
for (const sessionId of sessionIds) {
|
|
2486
|
+
if (sessionId === "current") {
|
|
2487
|
+
continue;
|
|
2488
|
+
}
|
|
2489
|
+
const metadata = this.loadSessionMetadata(sessionId);
|
|
2490
|
+
if (metadata) {
|
|
2491
|
+
summaries.push({
|
|
2492
|
+
sessionId: metadata.sessionId,
|
|
2493
|
+
status: metadata.status,
|
|
2494
|
+
startTime: metadata.startTime,
|
|
2495
|
+
endTime: metadata.endTime,
|
|
2496
|
+
targetUrl: metadata.targetUrl,
|
|
2497
|
+
durationMs: metadata.durationMs,
|
|
2498
|
+
stepsCount: metadata.stepsCount
|
|
2499
|
+
});
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
summaries.sort((a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime());
|
|
2503
|
+
return summaries;
|
|
2504
|
+
}
|
|
2505
|
+
/**
|
|
2506
|
+
* Cleanup old sessions beyond the specified age.
|
|
2507
|
+
* @param params - Cleanup parameters.
|
|
2508
|
+
* @returns Number of sessions deleted.
|
|
2509
|
+
*/
|
|
2510
|
+
cleanupOldSessions(params) {
|
|
2511
|
+
const maxAgeDays = params?.maxAgeDays ?? DEFAULT_SESSION_MAX_AGE_DAYS;
|
|
2512
|
+
const logger5 = getLogger();
|
|
2513
|
+
const cutoffDate = /* @__PURE__ */ new Date();
|
|
2514
|
+
cutoffDate.setDate(cutoffDate.getDate() - maxAgeDays);
|
|
2515
|
+
const sessionIds = this.listSessions();
|
|
2516
|
+
let deletedCount = 0;
|
|
2517
|
+
for (const sessionId of sessionIds) {
|
|
2518
|
+
if (sessionId === "current") {
|
|
2519
|
+
continue;
|
|
2520
|
+
}
|
|
2521
|
+
const metadata = this.loadSessionMetadata(sessionId);
|
|
2522
|
+
if (!metadata) {
|
|
2523
|
+
continue;
|
|
2524
|
+
}
|
|
2525
|
+
const sessionDate = new Date(metadata.startTime);
|
|
2526
|
+
if (sessionDate < cutoffDate) {
|
|
2527
|
+
const sessionDir = path.join(this.sessionsDir, sessionId);
|
|
2528
|
+
try {
|
|
2529
|
+
fs4.rmSync(sessionDir, { recursive: true, force: true });
|
|
2530
|
+
deletedCount++;
|
|
2531
|
+
logger5.info("Deleted old session", {
|
|
2532
|
+
sessionId,
|
|
2533
|
+
age: Math.floor((Date.now() - sessionDate.getTime()) / (1e3 * 60 * 60 * 24))
|
|
2534
|
+
});
|
|
2535
|
+
} catch (error) {
|
|
2536
|
+
logger5.error("Failed to delete session", {
|
|
2537
|
+
sessionId,
|
|
2538
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2539
|
+
});
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
if (deletedCount > 0) {
|
|
2544
|
+
logger5.info("Session cleanup completed", {
|
|
2545
|
+
deletedCount,
|
|
2546
|
+
maxAgeDays
|
|
2547
|
+
});
|
|
2548
|
+
}
|
|
2549
|
+
return deletedCount;
|
|
2550
|
+
}
|
|
2551
|
+
/**
|
|
2552
|
+
* Get a session directory path.
|
|
2553
|
+
* @param sessionId - Session ID.
|
|
2554
|
+
* @returns Session directory path.
|
|
2555
|
+
*/
|
|
2556
|
+
getSessionPath(sessionId) {
|
|
2557
|
+
return path.join(this.sessionsDir, sessionId);
|
|
2558
|
+
}
|
|
2559
|
+
/**
|
|
2560
|
+
* Delete a specific session.
|
|
2561
|
+
* @param sessionId - Session ID to delete.
|
|
2562
|
+
* @returns Whether deletion succeeded.
|
|
2563
|
+
*/
|
|
2564
|
+
deleteSession(sessionId) {
|
|
2565
|
+
const logger5 = getLogger();
|
|
2566
|
+
const sessionDir = path.join(this.sessionsDir, sessionId);
|
|
2567
|
+
if (!fs4.existsSync(sessionDir)) {
|
|
2568
|
+
logger5.warn("Session not found for deletion", { sessionId });
|
|
2569
|
+
return false;
|
|
2570
|
+
}
|
|
2571
|
+
try {
|
|
2572
|
+
const currentId = this.getCurrentSessionId();
|
|
2573
|
+
if (currentId === sessionId) {
|
|
2574
|
+
const currentPath = path.join(this.sessionsDir, "current");
|
|
2575
|
+
if (fs4.existsSync(currentPath)) {
|
|
2576
|
+
fs4.unlinkSync(currentPath);
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
fs4.rmSync(sessionDir, { recursive: true, force: true });
|
|
2580
|
+
logger5.info("Deleted session", { sessionId });
|
|
2581
|
+
return true;
|
|
2582
|
+
} catch (error) {
|
|
2583
|
+
logger5.error("Failed to delete session", {
|
|
2584
|
+
sessionId,
|
|
2585
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2586
|
+
});
|
|
2587
|
+
return false;
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
};
|
|
2591
|
+
var serviceInstance2 = null;
|
|
2592
|
+
function getStorageService() {
|
|
2593
|
+
serviceInstance2 ??= new StorageService();
|
|
2594
|
+
return serviceInstance2;
|
|
2595
|
+
}
|
|
2596
|
+
function resetStorageService() {
|
|
2597
|
+
serviceInstance2 = null;
|
|
2598
|
+
}
|
|
2599
|
+
var logger4 = getLogger();
|
|
2600
|
+
var RunResultStorageService = class {
|
|
2601
|
+
/** Base directory for run results. */
|
|
2602
|
+
runResultsDir;
|
|
2603
|
+
/** Base directory for test scripts. */
|
|
2604
|
+
testScriptsDir;
|
|
2605
|
+
constructor() {
|
|
2606
|
+
const storageService = getStorageService();
|
|
2607
|
+
const dataDir = storageService.getDataDir();
|
|
2608
|
+
this.runResultsDir = path.join(dataDir, "run-results");
|
|
2609
|
+
this.testScriptsDir = path.join(dataDir, "test-scripts");
|
|
2610
|
+
this.ensureDirectories();
|
|
2611
|
+
}
|
|
2612
|
+
/**
|
|
2613
|
+
* Ensure storage directories exist.
|
|
2614
|
+
*/
|
|
2615
|
+
ensureDirectories() {
|
|
2616
|
+
if (!fs4.existsSync(this.runResultsDir)) {
|
|
2617
|
+
fs4.mkdirSync(this.runResultsDir, { recursive: true });
|
|
2618
|
+
}
|
|
2619
|
+
if (!fs4.existsSync(this.testScriptsDir)) {
|
|
2620
|
+
fs4.mkdirSync(this.testScriptsDir, { recursive: true });
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
// ========================================
|
|
2624
|
+
// Run Results
|
|
2625
|
+
// ========================================
|
|
2626
|
+
/**
|
|
2627
|
+
* List all run results.
|
|
2628
|
+
*/
|
|
2629
|
+
listRunResults() {
|
|
2630
|
+
try {
|
|
2631
|
+
const files = fs4.readdirSync(this.runResultsDir).filter((f) => f.endsWith(".json"));
|
|
2632
|
+
const results = [];
|
|
2633
|
+
for (const file of files) {
|
|
2634
|
+
try {
|
|
2635
|
+
const content = fs4.readFileSync(path.join(this.runResultsDir, file), "utf-8");
|
|
2636
|
+
results.push(JSON.parse(content));
|
|
2637
|
+
} catch {
|
|
2638
|
+
logger4.warn("Failed to read run result file", { file });
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
return results.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
2642
|
+
} catch {
|
|
2643
|
+
return [];
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
/**
|
|
2647
|
+
* Get a run result by ID.
|
|
2648
|
+
*/
|
|
2649
|
+
getRunResult(runId) {
|
|
2650
|
+
const filePath = path.join(this.runResultsDir, `${runId}.json`);
|
|
2651
|
+
if (!fs4.existsSync(filePath)) {
|
|
2652
|
+
return void 0;
|
|
2653
|
+
}
|
|
2654
|
+
try {
|
|
2655
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
2656
|
+
return JSON.parse(content);
|
|
2657
|
+
} catch {
|
|
2658
|
+
return void 0;
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
/**
|
|
2662
|
+
* Save a run result.
|
|
2663
|
+
*/
|
|
2664
|
+
saveRunResult(result) {
|
|
2665
|
+
const filePath = path.join(this.runResultsDir, `${result.id}.json`);
|
|
2666
|
+
fs4.writeFileSync(filePath, JSON.stringify(result, null, 2));
|
|
2667
|
+
}
|
|
2668
|
+
/**
|
|
2669
|
+
* Create a new run result.
|
|
2670
|
+
*/
|
|
2671
|
+
createRunResult(params) {
|
|
2672
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2673
|
+
const id = `run_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
|
|
2674
|
+
const result = {
|
|
2675
|
+
id,
|
|
2676
|
+
runType: params.runType,
|
|
2677
|
+
status: "pending",
|
|
2678
|
+
cloudTestCaseId: params.cloudTestCaseId,
|
|
2679
|
+
localUrl: params.localUrl,
|
|
2680
|
+
createdAt: now,
|
|
2681
|
+
updatedAt: now
|
|
2682
|
+
};
|
|
2683
|
+
this.saveRunResult(result);
|
|
2684
|
+
return result;
|
|
2685
|
+
}
|
|
2686
|
+
/**
|
|
2687
|
+
* Update a run result.
|
|
2688
|
+
*/
|
|
2689
|
+
updateRunResult(runId, updates) {
|
|
2690
|
+
const result = this.getRunResult(runId);
|
|
2691
|
+
if (!result) {
|
|
2692
|
+
return void 0;
|
|
2693
|
+
}
|
|
2694
|
+
const updated = {
|
|
2695
|
+
...result,
|
|
2696
|
+
...updates,
|
|
2697
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2698
|
+
};
|
|
2699
|
+
this.saveRunResult(updated);
|
|
2700
|
+
return updated;
|
|
2701
|
+
}
|
|
2702
|
+
// ========================================
|
|
2703
|
+
// Test Scripts
|
|
2704
|
+
// ========================================
|
|
2705
|
+
/**
|
|
2706
|
+
* List all test scripts.
|
|
2707
|
+
*/
|
|
2708
|
+
listTestScripts() {
|
|
2709
|
+
try {
|
|
2710
|
+
const files = fs4.readdirSync(this.testScriptsDir).filter((f) => f.endsWith(".json"));
|
|
2711
|
+
const scripts = [];
|
|
2712
|
+
for (const file of files) {
|
|
2713
|
+
try {
|
|
2714
|
+
const content = fs4.readFileSync(path.join(this.testScriptsDir, file), "utf-8");
|
|
2715
|
+
scripts.push(JSON.parse(content));
|
|
2716
|
+
} catch {
|
|
2717
|
+
logger4.warn("Failed to read test script file", { file });
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
return scripts.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
2721
|
+
} catch {
|
|
2722
|
+
return [];
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
/**
|
|
2726
|
+
* Get a test script by ID.
|
|
2727
|
+
*/
|
|
2728
|
+
getTestScript(testScriptId) {
|
|
2729
|
+
const filePath = path.join(this.testScriptsDir, `${testScriptId}.json`);
|
|
2730
|
+
if (!fs4.existsSync(filePath)) {
|
|
2731
|
+
return void 0;
|
|
2732
|
+
}
|
|
2733
|
+
try {
|
|
2734
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
2735
|
+
return JSON.parse(content);
|
|
2736
|
+
} catch {
|
|
2737
|
+
return void 0;
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
/**
|
|
2741
|
+
* Save a test script.
|
|
2742
|
+
*/
|
|
2743
|
+
saveTestScript(script) {
|
|
2744
|
+
const filePath = path.join(this.testScriptsDir, `${script.id}.json`);
|
|
2745
|
+
fs4.writeFileSync(filePath, JSON.stringify(script, null, 2));
|
|
2746
|
+
}
|
|
2747
|
+
/**
|
|
2748
|
+
* Create a new test script.
|
|
2749
|
+
*/
|
|
2750
|
+
createTestScript(params) {
|
|
2751
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2752
|
+
const id = `ts_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
|
|
2753
|
+
const script = {
|
|
2754
|
+
id,
|
|
2755
|
+
name: params.name,
|
|
2756
|
+
url: params.url,
|
|
2757
|
+
status: "pending",
|
|
2758
|
+
cloudTestCaseId: params.cloudTestCaseId,
|
|
2759
|
+
goal: params.goal,
|
|
2760
|
+
createdAt: now,
|
|
2761
|
+
updatedAt: now
|
|
2762
|
+
};
|
|
2763
|
+
this.saveTestScript(script);
|
|
2764
|
+
return script;
|
|
2765
|
+
}
|
|
2766
|
+
/**
|
|
2767
|
+
* Update a test script.
|
|
2768
|
+
*/
|
|
2769
|
+
updateTestScript(testScriptId, updates) {
|
|
2770
|
+
const script = this.getTestScript(testScriptId);
|
|
2771
|
+
if (!script) {
|
|
2772
|
+
return void 0;
|
|
2773
|
+
}
|
|
2774
|
+
const updated = {
|
|
2775
|
+
...script,
|
|
2776
|
+
...updates,
|
|
2777
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2778
|
+
};
|
|
2779
|
+
this.saveTestScript(updated);
|
|
2780
|
+
return updated;
|
|
2781
|
+
}
|
|
2782
|
+
};
|
|
2783
|
+
var instance = null;
|
|
2784
|
+
function getRunResultStorageService() {
|
|
2785
|
+
if (!instance) {
|
|
2786
|
+
instance = new RunResultStorageService();
|
|
2787
|
+
}
|
|
2788
|
+
return instance;
|
|
2789
|
+
}
|
|
2790
|
+
function resetRunResultStorageService() {
|
|
2791
|
+
instance = null;
|
|
2792
|
+
}
|
|
2793
|
+
var activeProcesses = /* @__PURE__ */ new Map();
|
|
2794
|
+
function getAuthenticatedUserId() {
|
|
2795
|
+
const authService = getAuthService();
|
|
2796
|
+
const authStatus = authService.getAuthStatus();
|
|
2797
|
+
if (!authStatus.authenticated) {
|
|
2798
|
+
throw new Error("Not authenticated. Please run qa_auth_login first.");
|
|
2799
|
+
}
|
|
2800
|
+
if (!authStatus.userId) {
|
|
2801
|
+
throw new Error("User ID not found in auth. Please re-authenticate.");
|
|
2802
|
+
}
|
|
2803
|
+
return authStatus.userId;
|
|
2804
|
+
}
|
|
2805
|
+
function buildStudioAuthContent() {
|
|
2806
|
+
const authService = getAuthService();
|
|
2807
|
+
const authStatus = authService.getAuthStatus();
|
|
2808
|
+
const storedAuth = authService.loadStoredAuth();
|
|
2809
|
+
if (!authStatus.authenticated || !storedAuth) {
|
|
2810
|
+
throw new Error("Not authenticated. Please run qa_auth_login first.");
|
|
2811
|
+
}
|
|
2812
|
+
if (!storedAuth.email || !storedAuth.userId) {
|
|
2813
|
+
throw new Error("Auth data incomplete. Please re-authenticate.");
|
|
2814
|
+
}
|
|
2815
|
+
return {
|
|
2816
|
+
accessToken: storedAuth.accessToken,
|
|
2817
|
+
email: storedAuth.email,
|
|
2818
|
+
userId: storedAuth.userId
|
|
2819
|
+
};
|
|
2820
|
+
}
|
|
2821
|
+
async function ensureTempDir() {
|
|
2822
|
+
const config = getConfig();
|
|
2823
|
+
const tempDir = path.join(config.localQa.dataDir, "temp");
|
|
2824
|
+
await fs6.mkdir(tempDir, { recursive: true });
|
|
2825
|
+
return tempDir;
|
|
2826
|
+
}
|
|
2827
|
+
async function writeTempFile(params) {
|
|
2828
|
+
const tempDir = await ensureTempDir();
|
|
2829
|
+
const filePath = path.join(tempDir, params.filename);
|
|
2830
|
+
await fs6.writeFile(filePath, JSON.stringify(params.data, null, 2));
|
|
2831
|
+
return filePath;
|
|
2832
|
+
}
|
|
2833
|
+
async function cleanupTempFiles(params) {
|
|
2834
|
+
for (const filePath of params.filePaths) {
|
|
2835
|
+
try {
|
|
2836
|
+
await fs6.unlink(filePath);
|
|
2837
|
+
} catch {
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
async function executeTestGeneration(params) {
|
|
2842
|
+
const { testCase, localUrl } = params;
|
|
2843
|
+
getAuthenticatedUserId();
|
|
2844
|
+
const authContent = buildStudioAuthContent();
|
|
2845
|
+
const storage = getRunResultStorageService();
|
|
2846
|
+
const runResult = storage.createRunResult({
|
|
2847
|
+
runType: "generation",
|
|
2848
|
+
cloudTestCaseId: testCase.id,
|
|
2849
|
+
localUrl
|
|
2850
|
+
});
|
|
2851
|
+
try {
|
|
2852
|
+
const localTestScript = storage.createTestScript({
|
|
2853
|
+
name: `Script for ${testCase.title}`,
|
|
2854
|
+
url: localUrl,
|
|
2855
|
+
cloudTestCaseId: testCase.id,
|
|
2856
|
+
goal: testCase.goal
|
|
2857
|
+
});
|
|
2858
|
+
const actionScript = {
|
|
2859
|
+
steps: [
|
|
2860
|
+
{
|
|
2861
|
+
type: "navigate",
|
|
2862
|
+
url: localUrl
|
|
2863
|
+
},
|
|
2864
|
+
{
|
|
2865
|
+
type: "explore",
|
|
2866
|
+
goal: testCase.goal,
|
|
2867
|
+
instructions: testCase.instructions,
|
|
2868
|
+
expectedResult: testCase.expectedResult
|
|
2869
|
+
}
|
|
2870
|
+
]
|
|
2871
|
+
};
|
|
2872
|
+
const runId = runResult.id;
|
|
2873
|
+
const startedAt = Date.now();
|
|
2874
|
+
const inputFilePath = await writeTempFile({
|
|
2875
|
+
filename: `${runId}_input.json`,
|
|
2876
|
+
data: actionScript
|
|
2877
|
+
});
|
|
2878
|
+
const authFilePath = await writeTempFile({
|
|
2879
|
+
filename: `${runId}_auth.json`,
|
|
2880
|
+
data: authContent
|
|
2881
|
+
});
|
|
2882
|
+
const completedAt = Date.now();
|
|
2883
|
+
const executionTimeMs = completedAt - startedAt;
|
|
2884
|
+
storage.updateRunResult(runId, {
|
|
2885
|
+
status: "failed",
|
|
2886
|
+
testScriptId: localTestScript.id,
|
|
2887
|
+
executionTimeMs,
|
|
2888
|
+
errorMessage: "Electron-app execution not yet implemented."
|
|
2889
|
+
});
|
|
2890
|
+
await cleanupTempFiles({ filePaths: [inputFilePath, authFilePath] });
|
|
2891
|
+
return {
|
|
2892
|
+
id: runId,
|
|
2893
|
+
testScriptId: localTestScript.id,
|
|
2894
|
+
status: "failed",
|
|
2895
|
+
executionTimeMs,
|
|
2896
|
+
errorMessage: "Electron-app execution not yet implemented."
|
|
2897
|
+
};
|
|
2898
|
+
} catch (error) {
|
|
2899
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2900
|
+
storage.updateRunResult(runResult.id, {
|
|
2901
|
+
status: "failed",
|
|
2902
|
+
errorMessage
|
|
2903
|
+
});
|
|
2904
|
+
return {
|
|
2905
|
+
id: runResult.id,
|
|
2906
|
+
status: "failed",
|
|
2907
|
+
executionTimeMs: 0,
|
|
2908
|
+
errorMessage
|
|
2909
|
+
};
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
async function executeReplay(params) {
|
|
2913
|
+
const { testScript, localUrl } = params;
|
|
2914
|
+
getAuthenticatedUserId();
|
|
2915
|
+
const authContent = buildStudioAuthContent();
|
|
2916
|
+
const storage = getRunResultStorageService();
|
|
2917
|
+
const runResult = storage.createRunResult({
|
|
2918
|
+
runType: "replay",
|
|
2919
|
+
cloudTestCaseId: testScript.testCaseId,
|
|
2920
|
+
localUrl
|
|
2921
|
+
});
|
|
2922
|
+
try {
|
|
2923
|
+
const runId = runResult.id;
|
|
2924
|
+
const startedAt = Date.now();
|
|
2925
|
+
const rewrittenActionScript = rewriteActionScriptUrls({
|
|
2926
|
+
actionScript: testScript.actionScript,
|
|
2927
|
+
originalUrl: testScript.url,
|
|
2928
|
+
localUrl
|
|
2929
|
+
});
|
|
2930
|
+
const inputFilePath = await writeTempFile({
|
|
2931
|
+
filename: `${runId}_input.json`,
|
|
2932
|
+
data: rewrittenActionScript
|
|
2933
|
+
});
|
|
2934
|
+
const authFilePath = await writeTempFile({
|
|
2935
|
+
filename: `${runId}_auth.json`,
|
|
2936
|
+
data: authContent
|
|
2937
|
+
});
|
|
2938
|
+
const completedAt = Date.now();
|
|
2939
|
+
const executionTimeMs = completedAt - startedAt;
|
|
2940
|
+
storage.updateRunResult(runId, {
|
|
2941
|
+
status: "failed",
|
|
2942
|
+
executionTimeMs,
|
|
2943
|
+
errorMessage: "Electron-app execution not yet implemented."
|
|
2944
|
+
});
|
|
2945
|
+
await cleanupTempFiles({ filePaths: [inputFilePath, authFilePath] });
|
|
2946
|
+
return {
|
|
2947
|
+
id: runId,
|
|
2948
|
+
status: "failed",
|
|
2949
|
+
executionTimeMs,
|
|
2950
|
+
errorMessage: "Electron-app execution not yet implemented."
|
|
2951
|
+
};
|
|
2952
|
+
} catch (error) {
|
|
2953
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2954
|
+
storage.updateRunResult(runResult.id, {
|
|
2955
|
+
status: "failed",
|
|
2956
|
+
errorMessage
|
|
2957
|
+
});
|
|
2958
|
+
return {
|
|
2959
|
+
id: runResult.id,
|
|
2960
|
+
status: "failed",
|
|
2961
|
+
executionTimeMs: 0,
|
|
2962
|
+
errorMessage
|
|
2963
|
+
};
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
function rewriteActionScriptUrls(params) {
|
|
2967
|
+
const { actionScript, originalUrl, localUrl } = params;
|
|
2968
|
+
if (!originalUrl) {
|
|
2969
|
+
return actionScript;
|
|
2970
|
+
}
|
|
2971
|
+
const serialized = JSON.stringify(actionScript);
|
|
2972
|
+
const rewritten = serialized.replace(new RegExp(escapeRegex(originalUrl), "g"), localUrl);
|
|
2973
|
+
return JSON.parse(rewritten);
|
|
2974
|
+
}
|
|
2975
|
+
function escapeRegex(str) {
|
|
2976
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2977
|
+
}
|
|
2978
|
+
function cancelExecution(params) {
|
|
2979
|
+
const process2 = activeProcesses.get(params.runId);
|
|
2980
|
+
if (!process2) {
|
|
2981
|
+
return false;
|
|
2982
|
+
}
|
|
2983
|
+
process2.process.kill("SIGTERM");
|
|
2984
|
+
process2.status = "cancelled";
|
|
2985
|
+
activeProcesses.delete(params.runId);
|
|
2986
|
+
const storage = getRunResultStorageService();
|
|
2987
|
+
storage.updateRunResult(params.runId, {
|
|
2988
|
+
status: "cancelled",
|
|
2989
|
+
errorMessage: "Execution cancelled by user."
|
|
2990
|
+
});
|
|
2991
|
+
return true;
|
|
2992
|
+
}
|
|
2993
|
+
function listActiveExecutions() {
|
|
2994
|
+
return Array.from(activeProcesses.entries()).map(([runId, process2]) => ({
|
|
2995
|
+
runId,
|
|
2996
|
+
status: process2.status
|
|
2997
|
+
}));
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
// src/qa/tools/tool-registry.ts
|
|
3001
|
+
var MUGGLE_TEST_PREFIX = "/v1/protected/muggle-test";
|
|
3002
|
+
var getWorkflowTimeoutMs = () => getConfig().qa.workflowTimeoutMs;
|
|
3003
|
+
var projectTools = [
|
|
3004
|
+
{
|
|
3005
|
+
name: "qa_project_create",
|
|
3006
|
+
description: "Create a new QA testing project. Projects organize use cases, test cases, and test scripts.",
|
|
3007
|
+
inputSchema: ProjectCreateInputSchema,
|
|
3008
|
+
mapToUpstream: (input) => {
|
|
3009
|
+
const data = input;
|
|
3010
|
+
return {
|
|
3011
|
+
method: "POST",
|
|
3012
|
+
path: `${MUGGLE_TEST_PREFIX}/projects`,
|
|
3013
|
+
body: {
|
|
3014
|
+
name: data.projectName,
|
|
3015
|
+
description: data.description,
|
|
3016
|
+
url: data.url
|
|
3017
|
+
}
|
|
3018
|
+
};
|
|
3019
|
+
}
|
|
3020
|
+
},
|
|
3021
|
+
{
|
|
3022
|
+
name: "qa_project_get",
|
|
3023
|
+
description: "Get details of a specific project by ID.",
|
|
3024
|
+
inputSchema: ProjectGetInputSchema,
|
|
3025
|
+
mapToUpstream: (input) => {
|
|
3026
|
+
const data = input;
|
|
3027
|
+
return {
|
|
3028
|
+
method: "GET",
|
|
3029
|
+
path: `${MUGGLE_TEST_PREFIX}/projects/${data.projectId}`
|
|
3030
|
+
};
|
|
3031
|
+
}
|
|
3032
|
+
},
|
|
3033
|
+
{
|
|
3034
|
+
name: "qa_project_update",
|
|
3035
|
+
description: "Update an existing project's details.",
|
|
3036
|
+
inputSchema: ProjectUpdateInputSchema,
|
|
3037
|
+
mapToUpstream: (input) => {
|
|
3038
|
+
const data = input;
|
|
3039
|
+
const body = { id: data.projectId };
|
|
3040
|
+
if (data.projectName !== void 0) body.name = data.projectName;
|
|
3041
|
+
if (data.description !== void 0) body.description = data.description;
|
|
3042
|
+
if (data.url !== void 0) body.url = data.url;
|
|
3043
|
+
return {
|
|
3044
|
+
method: "PUT",
|
|
3045
|
+
path: `${MUGGLE_TEST_PREFIX}/projects/${data.projectId}`,
|
|
3046
|
+
body
|
|
3047
|
+
};
|
|
3048
|
+
}
|
|
3049
|
+
},
|
|
3050
|
+
{
|
|
3051
|
+
name: "qa_project_list",
|
|
3052
|
+
description: "List all projects accessible to the authenticated user.",
|
|
3053
|
+
inputSchema: ProjectListInputSchema,
|
|
3054
|
+
mapToUpstream: (input) => {
|
|
3055
|
+
const data = input;
|
|
3056
|
+
return {
|
|
3057
|
+
method: "GET",
|
|
3058
|
+
path: `${MUGGLE_TEST_PREFIX}/projects`,
|
|
3059
|
+
queryParams: { page: data.page, pageSize: data.pageSize }
|
|
3060
|
+
};
|
|
3061
|
+
}
|
|
3062
|
+
},
|
|
3063
|
+
{
|
|
3064
|
+
name: "qa_project_delete",
|
|
3065
|
+
description: "Delete a project and all associated entities. This is a soft delete.",
|
|
3066
|
+
inputSchema: ProjectDeleteInputSchema,
|
|
3067
|
+
mapToUpstream: (input) => {
|
|
3068
|
+
const data = input;
|
|
3069
|
+
return {
|
|
3070
|
+
method: "DELETE",
|
|
3071
|
+
path: `${MUGGLE_TEST_PREFIX}/projects/${data.projectId}`
|
|
3072
|
+
};
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
];
|
|
3076
|
+
var useCaseTools = [
|
|
3077
|
+
{
|
|
3078
|
+
name: "qa_use_case_discovery_memory_get",
|
|
3079
|
+
description: "Get the use case discovery memory for a project, including all discovered use case candidates.",
|
|
3080
|
+
inputSchema: UseCaseDiscoveryMemoryGetInputSchema,
|
|
3081
|
+
mapToUpstream: (input) => {
|
|
3082
|
+
const data = input;
|
|
3083
|
+
return {
|
|
3084
|
+
method: "GET",
|
|
3085
|
+
path: `${MUGGLE_TEST_PREFIX}/projects/${data.projectId}/use-case-discovery-memory`
|
|
3086
|
+
};
|
|
3087
|
+
}
|
|
3088
|
+
},
|
|
3089
|
+
{
|
|
3090
|
+
name: "qa_use_case_candidates_approve",
|
|
3091
|
+
description: "Approve (graduate) selected use case candidates into actual use cases.",
|
|
3092
|
+
inputSchema: UseCaseCandidatesApproveInputSchema,
|
|
3093
|
+
mapToUpstream: (input) => {
|
|
3094
|
+
const data = input;
|
|
3095
|
+
return {
|
|
3096
|
+
method: "POST",
|
|
3097
|
+
path: `${MUGGLE_TEST_PREFIX}/projects/${data.projectId}/use-case-discovery-memory/graduate`,
|
|
3098
|
+
body: { approveIds: data.approvedCandidateIds }
|
|
3099
|
+
};
|
|
3100
|
+
}
|
|
3101
|
+
},
|
|
3102
|
+
{
|
|
3103
|
+
name: "qa_use_case_list",
|
|
3104
|
+
description: "List all use cases for a project.",
|
|
3105
|
+
inputSchema: UseCaseListInputSchema,
|
|
3106
|
+
mapToUpstream: (input) => {
|
|
3107
|
+
const data = input;
|
|
3108
|
+
return {
|
|
3109
|
+
method: "GET",
|
|
3110
|
+
path: `${MUGGLE_TEST_PREFIX}/use-cases`,
|
|
3111
|
+
queryParams: { projectId: data.projectId, page: data.page, pageSize: data.pageSize }
|
|
3112
|
+
};
|
|
3113
|
+
}
|
|
3114
|
+
},
|
|
3115
|
+
{
|
|
3116
|
+
name: "qa_use_case_get",
|
|
3117
|
+
description: "Get details of a specific use case by ID.",
|
|
3118
|
+
inputSchema: UseCaseGetInputSchema,
|
|
3119
|
+
mapToUpstream: (input) => {
|
|
3120
|
+
const data = input;
|
|
3121
|
+
return {
|
|
3122
|
+
method: "GET",
|
|
3123
|
+
path: `${MUGGLE_TEST_PREFIX}/use-cases/${data.useCaseId}`
|
|
3124
|
+
};
|
|
3125
|
+
}
|
|
3126
|
+
},
|
|
3127
|
+
{
|
|
3128
|
+
name: "qa_use_case_prompt_preview",
|
|
3129
|
+
description: "Preview a use case generated from a natural language instruction without saving.",
|
|
3130
|
+
inputSchema: UseCasePromptPreviewInputSchema,
|
|
3131
|
+
mapToUpstream: (input) => {
|
|
3132
|
+
const data = input;
|
|
3133
|
+
return {
|
|
3134
|
+
method: "POST",
|
|
3135
|
+
path: `${MUGGLE_TEST_PREFIX}/projects/${data.projectId}/use-cases/prompt/preview`,
|
|
3136
|
+
body: { instruction: data.instruction }
|
|
3137
|
+
};
|
|
3138
|
+
}
|
|
3139
|
+
},
|
|
3140
|
+
{
|
|
3141
|
+
name: "qa_use_case_create_from_prompts",
|
|
3142
|
+
description: "Create one or more use cases from natural language instructions.",
|
|
3143
|
+
inputSchema: UseCaseCreateFromPromptsInputSchema,
|
|
3144
|
+
mapToUpstream: (input) => {
|
|
3145
|
+
const data = input;
|
|
3146
|
+
return {
|
|
3147
|
+
method: "POST",
|
|
3148
|
+
path: `${MUGGLE_TEST_PREFIX}/projects/${data.projectId}/use-cases/prompts/bulk`,
|
|
3149
|
+
body: { projectId: data.projectId, prompts: data.prompts }
|
|
3150
|
+
};
|
|
3151
|
+
}
|
|
3152
|
+
},
|
|
3153
|
+
{
|
|
3154
|
+
name: "qa_use_case_update_from_prompt",
|
|
3155
|
+
description: "Update an existing use case by regenerating its fields from a new instruction.",
|
|
3156
|
+
inputSchema: UseCaseUpdateFromPromptInputSchema,
|
|
3157
|
+
mapToUpstream: (input) => {
|
|
3158
|
+
const data = input;
|
|
3159
|
+
return {
|
|
3160
|
+
method: "POST",
|
|
3161
|
+
path: `${MUGGLE_TEST_PREFIX}/projects/${data.projectId}/use-cases/${data.useCaseId}/prompt`,
|
|
3162
|
+
body: { instruction: data.instruction }
|
|
3163
|
+
};
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
];
|
|
3167
|
+
var testCaseTools = [
|
|
3168
|
+
{
|
|
3169
|
+
name: "qa_test_case_list",
|
|
3170
|
+
description: "List test cases for a project.",
|
|
3171
|
+
inputSchema: TestCaseListInputSchema,
|
|
3172
|
+
mapToUpstream: (input) => {
|
|
3173
|
+
const data = input;
|
|
3174
|
+
return {
|
|
3175
|
+
method: "GET",
|
|
3176
|
+
path: `${MUGGLE_TEST_PREFIX}/test-cases`,
|
|
3177
|
+
queryParams: { projectId: data.projectId, page: data.page, pageSize: data.pageSize }
|
|
3178
|
+
};
|
|
3179
|
+
}
|
|
3180
|
+
},
|
|
3181
|
+
{
|
|
3182
|
+
name: "qa_test_case_get",
|
|
3183
|
+
description: "Get details of a specific test case.",
|
|
3184
|
+
inputSchema: TestCaseGetInputSchema,
|
|
3185
|
+
mapToUpstream: (input) => {
|
|
3186
|
+
const data = input;
|
|
3187
|
+
return {
|
|
3188
|
+
method: "GET",
|
|
3189
|
+
path: `${MUGGLE_TEST_PREFIX}/test-cases/${data.testCaseId}`
|
|
3190
|
+
};
|
|
3191
|
+
}
|
|
3192
|
+
},
|
|
3193
|
+
{
|
|
3194
|
+
name: "qa_test_case_list_by_use_case",
|
|
3195
|
+
description: "List test cases for a specific use case.",
|
|
3196
|
+
inputSchema: TestCaseListByUseCaseInputSchema,
|
|
3197
|
+
mapToUpstream: (input) => {
|
|
3198
|
+
const data = input;
|
|
3199
|
+
return {
|
|
3200
|
+
method: "GET",
|
|
3201
|
+
path: `${MUGGLE_TEST_PREFIX}/use-cases/${data.useCaseId}/test-cases`
|
|
3202
|
+
};
|
|
3203
|
+
}
|
|
3204
|
+
},
|
|
3205
|
+
{
|
|
3206
|
+
name: "qa_test_case_generate_from_prompt",
|
|
3207
|
+
description: "Generate test cases from a natural language prompt. Returns preview test cases.",
|
|
3208
|
+
inputSchema: TestCaseGenerateFromPromptInputSchema,
|
|
3209
|
+
mapToUpstream: (input) => {
|
|
3210
|
+
const data = input;
|
|
3211
|
+
return {
|
|
3212
|
+
method: "POST",
|
|
3213
|
+
path: `${MUGGLE_TEST_PREFIX}/projects/${data.projectId}/use-cases/${data.useCaseId}/test-cases/prompt/preview`,
|
|
3214
|
+
body: { instruction: data.instruction }
|
|
3215
|
+
};
|
|
3216
|
+
}
|
|
3217
|
+
},
|
|
3218
|
+
{
|
|
3219
|
+
name: "qa_test_case_create",
|
|
3220
|
+
description: "Create a new test case for a use case.",
|
|
3221
|
+
inputSchema: TestCaseCreateInputSchema,
|
|
3222
|
+
mapToUpstream: (input) => {
|
|
3223
|
+
const data = input;
|
|
3224
|
+
return {
|
|
3225
|
+
method: "POST",
|
|
3226
|
+
path: `${MUGGLE_TEST_PREFIX}/test-cases`,
|
|
3227
|
+
body: {
|
|
3228
|
+
projectId: data.projectId,
|
|
3229
|
+
useCaseId: data.useCaseId,
|
|
3230
|
+
title: data.title,
|
|
3231
|
+
description: data.description,
|
|
3232
|
+
goal: data.goal,
|
|
3233
|
+
precondition: data.precondition,
|
|
3234
|
+
expectedResult: data.expectedResult,
|
|
3235
|
+
url: data.url,
|
|
3236
|
+
status: data.status || "DRAFT",
|
|
3237
|
+
priority: data.priority || "MEDIUM",
|
|
3238
|
+
tags: data.tags || [],
|
|
3239
|
+
category: data.category || "Functional",
|
|
3240
|
+
automated: data.automated ?? true
|
|
3241
|
+
}
|
|
3242
|
+
};
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
];
|
|
3246
|
+
var testScriptTools = [
|
|
3247
|
+
{
|
|
3248
|
+
name: "qa_test_script_list",
|
|
3249
|
+
description: "List test scripts for a project.",
|
|
3250
|
+
inputSchema: TestScriptListInputSchema,
|
|
3251
|
+
mapToUpstream: (input) => {
|
|
3252
|
+
const data = input;
|
|
3253
|
+
return {
|
|
3254
|
+
method: "GET",
|
|
3255
|
+
path: `${MUGGLE_TEST_PREFIX}/test-scripts`,
|
|
3256
|
+
queryParams: { projectId: data.projectId, page: data.page, pageSize: data.pageSize }
|
|
3257
|
+
};
|
|
3258
|
+
}
|
|
3259
|
+
},
|
|
3260
|
+
{
|
|
3261
|
+
name: "qa_test_script_get",
|
|
3262
|
+
description: "Get details of a specific test script.",
|
|
3263
|
+
inputSchema: TestScriptGetInputSchema,
|
|
3264
|
+
mapToUpstream: (input) => {
|
|
3265
|
+
const data = input;
|
|
3266
|
+
return {
|
|
3267
|
+
method: "GET",
|
|
3268
|
+
path: `${MUGGLE_TEST_PREFIX}/test-scripts/${data.testScriptId}`
|
|
3269
|
+
};
|
|
3270
|
+
}
|
|
3271
|
+
},
|
|
3272
|
+
{
|
|
3273
|
+
name: "qa_test_script_list_paginated",
|
|
3274
|
+
description: "List test scripts with full pagination support.",
|
|
3275
|
+
inputSchema: TestScriptListPaginatedInputSchema,
|
|
3276
|
+
mapToUpstream: (input) => {
|
|
3277
|
+
const data = input;
|
|
3278
|
+
return {
|
|
3279
|
+
method: "GET",
|
|
3280
|
+
path: `${MUGGLE_TEST_PREFIX}/test-scripts/paginated`,
|
|
3281
|
+
queryParams: { projectId: data.projectId, page: data.page, pageSize: data.pageSize }
|
|
3282
|
+
};
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
];
|
|
3286
|
+
var workflowTools = [
|
|
3287
|
+
{
|
|
3288
|
+
name: "qa_workflow_start_website_scan",
|
|
3289
|
+
description: "Start a website scan workflow to discover use cases from a URL.",
|
|
3290
|
+
inputSchema: WorkflowStartWebsiteScanInputSchema,
|
|
3291
|
+
mapToUpstream: (input) => {
|
|
3292
|
+
const data = input;
|
|
3293
|
+
return {
|
|
3294
|
+
method: "POST",
|
|
3295
|
+
path: `${MUGGLE_TEST_PREFIX}/workflow/use-case/website-scan`,
|
|
3296
|
+
body: {
|
|
3297
|
+
projectId: data.projectId,
|
|
3298
|
+
url: data.url,
|
|
3299
|
+
description: data.description,
|
|
3300
|
+
archiveUnapproved: data.archiveUnapproved,
|
|
3301
|
+
...data.workflowParams && { workflowParams: data.workflowParams }
|
|
3302
|
+
},
|
|
3303
|
+
timeoutMs: getWorkflowTimeoutMs()
|
|
3304
|
+
};
|
|
3305
|
+
}
|
|
3306
|
+
},
|
|
3307
|
+
{
|
|
3308
|
+
name: "qa_workflow_list_website_scan_runtimes",
|
|
3309
|
+
description: "List website scan workflow runtimes.",
|
|
3310
|
+
inputSchema: WorkflowListRuntimesInputSchema,
|
|
3311
|
+
mapToUpstream: (input) => {
|
|
3312
|
+
const data = input;
|
|
3313
|
+
return {
|
|
3314
|
+
method: "GET",
|
|
3315
|
+
path: `${MUGGLE_TEST_PREFIX}/workflow/use-case/website-scan/workflowRuntimes`,
|
|
3316
|
+
queryParams: { projectId: data.projectId }
|
|
3317
|
+
};
|
|
3318
|
+
}
|
|
3319
|
+
},
|
|
3320
|
+
{
|
|
3321
|
+
name: "qa_workflow_get_website_scan_latest_run",
|
|
3322
|
+
description: "Get the latest run status for a website scan workflow runtime.",
|
|
3323
|
+
inputSchema: WorkflowGetLatestRunInputSchema,
|
|
3324
|
+
mapToUpstream: (input) => {
|
|
3325
|
+
const data = input;
|
|
3326
|
+
return {
|
|
3327
|
+
method: "GET",
|
|
3328
|
+
path: `${MUGGLE_TEST_PREFIX}/workflow/use-case/website-scan/${data.workflowRuntimeId}/run/latest`
|
|
3329
|
+
};
|
|
3330
|
+
}
|
|
3331
|
+
},
|
|
3332
|
+
{
|
|
3333
|
+
name: "qa_workflow_start_test_case_detection",
|
|
3334
|
+
description: "Start a test case detection workflow to generate test cases from use cases.",
|
|
3335
|
+
inputSchema: WorkflowStartTestCaseDetectionInputSchema,
|
|
3336
|
+
mapToUpstream: (input) => {
|
|
3337
|
+
const data = input;
|
|
3338
|
+
return {
|
|
3339
|
+
method: "POST",
|
|
3340
|
+
path: `${MUGGLE_TEST_PREFIX}/workflow/test-case/test-case-detection`,
|
|
3341
|
+
body: {
|
|
3342
|
+
projectId: data.projectId,
|
|
3343
|
+
useCaseId: data.useCaseId,
|
|
3344
|
+
name: data.name,
|
|
3345
|
+
description: data.description,
|
|
3346
|
+
url: data.url,
|
|
3347
|
+
...data.workflowParams && { workflowParams: data.workflowParams }
|
|
3348
|
+
},
|
|
3349
|
+
timeoutMs: getWorkflowTimeoutMs()
|
|
3350
|
+
};
|
|
3351
|
+
}
|
|
3352
|
+
},
|
|
3353
|
+
{
|
|
3354
|
+
name: "qa_workflow_list_test_case_detection_runtimes",
|
|
3355
|
+
description: "List test case detection workflow runtimes.",
|
|
3356
|
+
inputSchema: WorkflowListRuntimesInputSchema,
|
|
3357
|
+
mapToUpstream: (input) => {
|
|
3358
|
+
const data = input;
|
|
3359
|
+
return {
|
|
3360
|
+
method: "GET",
|
|
3361
|
+
path: `${MUGGLE_TEST_PREFIX}/workflow/test-case/test-case-detection/workflowRuntimes`,
|
|
3362
|
+
queryParams: { projectId: data.projectId }
|
|
3363
|
+
};
|
|
3364
|
+
}
|
|
3365
|
+
},
|
|
3366
|
+
{
|
|
3367
|
+
name: "qa_workflow_get_test_case_detection_latest_run",
|
|
3368
|
+
description: "Get the latest run status for a test case detection workflow runtime.",
|
|
3369
|
+
inputSchema: WorkflowGetLatestRunInputSchema,
|
|
3370
|
+
mapToUpstream: (input) => {
|
|
3371
|
+
const data = input;
|
|
3372
|
+
return {
|
|
3373
|
+
method: "GET",
|
|
3374
|
+
path: `${MUGGLE_TEST_PREFIX}/workflow/test-case/test-case-detection/${data.workflowRuntimeId}/run/latest`
|
|
3375
|
+
};
|
|
3376
|
+
}
|
|
3377
|
+
},
|
|
3378
|
+
{
|
|
3379
|
+
name: "qa_workflow_start_test_script_generation",
|
|
3380
|
+
description: "Start a test script generation workflow.",
|
|
3381
|
+
inputSchema: WorkflowStartTestScriptGenerationInputSchema,
|
|
3382
|
+
mapToUpstream: (input) => {
|
|
3383
|
+
const data = input;
|
|
3384
|
+
return {
|
|
3385
|
+
method: "POST",
|
|
3386
|
+
path: `${MUGGLE_TEST_PREFIX}/workflow/test-script/test-script-generation`,
|
|
3387
|
+
body: {
|
|
3388
|
+
projectId: data.projectId,
|
|
3389
|
+
testCaseId: data.testCaseId,
|
|
3390
|
+
useCaseId: data.useCaseId,
|
|
3391
|
+
name: data.name,
|
|
3392
|
+
url: data.url,
|
|
3393
|
+
goal: data.goal,
|
|
3394
|
+
precondition: data.precondition,
|
|
3395
|
+
instructions: data.instructions,
|
|
3396
|
+
expectedResult: data.expectedResult,
|
|
3397
|
+
...data.workflowParams && { workflowParams: data.workflowParams }
|
|
3398
|
+
},
|
|
3399
|
+
timeoutMs: getWorkflowTimeoutMs()
|
|
3400
|
+
};
|
|
3401
|
+
}
|
|
3402
|
+
},
|
|
3403
|
+
{
|
|
3404
|
+
name: "qa_workflow_get_test_script_generation_latest_run",
|
|
3405
|
+
description: "Get the latest run status for a test script generation workflow runtime.",
|
|
3406
|
+
inputSchema: WorkflowGetLatestRunInputSchema,
|
|
3407
|
+
mapToUpstream: (input) => {
|
|
3408
|
+
const data = input;
|
|
3409
|
+
return {
|
|
3410
|
+
method: "GET",
|
|
3411
|
+
path: `${MUGGLE_TEST_PREFIX}/workflow/test-script/test-script-generation/${data.workflowRuntimeId}/run/latest`
|
|
3412
|
+
};
|
|
3413
|
+
}
|
|
3414
|
+
},
|
|
3415
|
+
{
|
|
3416
|
+
name: "qa_workflow_get_latest_test_script_generation_runtime_by_test_case",
|
|
3417
|
+
description: "Get the latest test script generation runtime for a specific test case.",
|
|
3418
|
+
inputSchema: WorkflowGetLatestScriptGenByTestCaseInputSchema,
|
|
3419
|
+
mapToUpstream: (input) => {
|
|
3420
|
+
const data = input;
|
|
3421
|
+
return {
|
|
3422
|
+
method: "GET",
|
|
3423
|
+
path: `${MUGGLE_TEST_PREFIX}/workflow/test-script/test-script-generation/testcases/${data.testCaseId}/runtime/latest`
|
|
3424
|
+
};
|
|
3425
|
+
}
|
|
3426
|
+
},
|
|
3427
|
+
{
|
|
3428
|
+
name: "qa_workflow_start_test_script_replay",
|
|
3429
|
+
description: "Start a test script replay workflow to execute a single test script.",
|
|
3430
|
+
inputSchema: WorkflowStartTestScriptReplayInputSchema,
|
|
3431
|
+
mapToUpstream: (input) => {
|
|
3432
|
+
const data = input;
|
|
3433
|
+
return {
|
|
3434
|
+
method: "POST",
|
|
3435
|
+
path: `${MUGGLE_TEST_PREFIX}/workflow/test-script/test-script-replay`,
|
|
3436
|
+
body: {
|
|
3437
|
+
projectId: data.projectId,
|
|
3438
|
+
useCaseId: data.useCaseId,
|
|
3439
|
+
testCaseId: data.testCaseId,
|
|
3440
|
+
testScriptId: data.testScriptId,
|
|
3441
|
+
name: data.name,
|
|
3442
|
+
...data.workflowParams && { workflowParams: data.workflowParams }
|
|
3443
|
+
},
|
|
3444
|
+
timeoutMs: getWorkflowTimeoutMs()
|
|
3445
|
+
};
|
|
3446
|
+
}
|
|
3447
|
+
},
|
|
3448
|
+
{
|
|
3449
|
+
name: "qa_workflow_get_test_script_replay_latest_run",
|
|
3450
|
+
description: "Get the latest run status for a test script replay workflow runtime.",
|
|
3451
|
+
inputSchema: WorkflowGetLatestRunInputSchema,
|
|
3452
|
+
mapToUpstream: (input) => {
|
|
3453
|
+
const data = input;
|
|
3454
|
+
return {
|
|
3455
|
+
method: "GET",
|
|
3456
|
+
path: `${MUGGLE_TEST_PREFIX}/workflow/test-script/test-script-replay/${data.workflowRuntimeId}/run/latest`
|
|
3457
|
+
};
|
|
3458
|
+
}
|
|
3459
|
+
},
|
|
3460
|
+
{
|
|
3461
|
+
name: "qa_workflow_start_test_script_replay_bulk",
|
|
3462
|
+
description: "Start a bulk test script replay workflow to execute multiple test scripts.",
|
|
3463
|
+
inputSchema: WorkflowStartTestScriptReplayBulkInputSchema,
|
|
3464
|
+
mapToUpstream: (input) => {
|
|
3465
|
+
const data = input;
|
|
3466
|
+
return {
|
|
3467
|
+
method: "POST",
|
|
3468
|
+
path: `${MUGGLE_TEST_PREFIX}/workflow/test-script/test-script-replay/bulk/workflowRuntimes`,
|
|
3469
|
+
body: {
|
|
3470
|
+
projectId: data.projectId,
|
|
3471
|
+
name: data.name,
|
|
3472
|
+
intervalSec: data.intervalSec,
|
|
3473
|
+
useCaseId: data.useCaseId,
|
|
3474
|
+
namePrefix: data.namePrefix,
|
|
3475
|
+
limit: data.limit,
|
|
3476
|
+
testCaseIds: data.testCaseIds,
|
|
3477
|
+
repeatPerTestCase: data.repeatPerTestCase,
|
|
3478
|
+
...data.workflowParams && { workflowParams: data.workflowParams }
|
|
3479
|
+
},
|
|
3480
|
+
timeoutMs: getWorkflowTimeoutMs()
|
|
3481
|
+
};
|
|
3482
|
+
}
|
|
3483
|
+
},
|
|
3484
|
+
{
|
|
3485
|
+
name: "qa_workflow_list_test_script_replay_bulk_runtimes",
|
|
3486
|
+
description: "List bulk test script replay workflow runtimes.",
|
|
3487
|
+
inputSchema: WorkflowListRuntimesInputSchema,
|
|
3488
|
+
mapToUpstream: (input) => {
|
|
3489
|
+
const data = input;
|
|
3490
|
+
return {
|
|
3491
|
+
method: "GET",
|
|
3492
|
+
path: `${MUGGLE_TEST_PREFIX}/workflow/test-script/test-script-replay/bulk/workflowRuntimes`,
|
|
3493
|
+
queryParams: { projectId: data.projectId }
|
|
3494
|
+
};
|
|
3495
|
+
}
|
|
3496
|
+
},
|
|
3497
|
+
{
|
|
3498
|
+
name: "qa_workflow_get_test_script_replay_bulk_latest_run",
|
|
3499
|
+
description: "Get the latest run status for a bulk test script replay workflow runtime.",
|
|
3500
|
+
inputSchema: WorkflowGetLatestRunInputSchema,
|
|
3501
|
+
mapToUpstream: (input) => {
|
|
3502
|
+
const data = input;
|
|
3503
|
+
return {
|
|
3504
|
+
method: "GET",
|
|
3505
|
+
path: `${MUGGLE_TEST_PREFIX}/workflow/test-script/test-script-replay/bulk/${data.workflowRuntimeId}/run/latest`
|
|
3506
|
+
};
|
|
3507
|
+
}
|
|
3508
|
+
},
|
|
3509
|
+
{
|
|
3510
|
+
name: "qa_workflow_get_replay_bulk_run_batch_summary",
|
|
3511
|
+
description: "Get the summary of a bulk replay run batch.",
|
|
3512
|
+
inputSchema: WorkflowGetReplayBulkBatchSummaryInputSchema,
|
|
3513
|
+
mapToUpstream: (input) => {
|
|
3514
|
+
const data = input;
|
|
3515
|
+
return {
|
|
3516
|
+
method: "GET",
|
|
3517
|
+
path: `${MUGGLE_TEST_PREFIX}/workflow/test-script/test-script-replay/bulk/run-batch/${data.runBatchId}/summary`
|
|
3518
|
+
};
|
|
3519
|
+
}
|
|
3520
|
+
},
|
|
3521
|
+
{
|
|
3522
|
+
name: "qa_workflow_cancel_run",
|
|
3523
|
+
description: "Cancel a running workflow run.",
|
|
3524
|
+
inputSchema: WorkflowCancelRunInputSchema,
|
|
3525
|
+
mapToUpstream: (input) => {
|
|
3526
|
+
const data = input;
|
|
3527
|
+
return {
|
|
3528
|
+
method: "POST",
|
|
3529
|
+
path: `${MUGGLE_TEST_PREFIX}/workflow/runs/${data.workflowRunId}/cancel`
|
|
3530
|
+
};
|
|
3531
|
+
}
|
|
3532
|
+
},
|
|
3533
|
+
{
|
|
3534
|
+
name: "qa_workflow_cancel_runtime",
|
|
3535
|
+
description: "Cancel a workflow runtime and all its runs.",
|
|
3536
|
+
inputSchema: WorkflowCancelRuntimeInputSchema,
|
|
3537
|
+
mapToUpstream: (input) => {
|
|
3538
|
+
const data = input;
|
|
3539
|
+
return {
|
|
3540
|
+
method: "POST",
|
|
3541
|
+
path: `${MUGGLE_TEST_PREFIX}/workflow/runtimes/${data.workflowRuntimeId}/cancel`
|
|
3542
|
+
};
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
];
|
|
3546
|
+
var reportTools = [
|
|
3547
|
+
{
|
|
3548
|
+
name: "qa_project_test_results_summary_get",
|
|
3549
|
+
description: "Get a summary of test results for a project.",
|
|
3550
|
+
inputSchema: ProjectTestResultsSummaryInputSchema,
|
|
3551
|
+
mapToUpstream: (input) => {
|
|
3552
|
+
const data = input;
|
|
3553
|
+
return {
|
|
3554
|
+
method: "GET",
|
|
3555
|
+
path: `${MUGGLE_TEST_PREFIX}/projects/${data.projectId}/testResults`
|
|
3556
|
+
};
|
|
3557
|
+
}
|
|
3558
|
+
},
|
|
3559
|
+
{
|
|
3560
|
+
name: "qa_project_test_scripts_summary_get",
|
|
3561
|
+
description: "Get a summary of test scripts for a project.",
|
|
3562
|
+
inputSchema: ProjectTestScriptsSummaryInputSchema,
|
|
3563
|
+
mapToUpstream: (input) => {
|
|
3564
|
+
const data = input;
|
|
3565
|
+
return {
|
|
3566
|
+
method: "GET",
|
|
3567
|
+
path: `${MUGGLE_TEST_PREFIX}/projects/${data.projectId}/test-scripts/summary`
|
|
3568
|
+
};
|
|
3569
|
+
}
|
|
3570
|
+
},
|
|
3571
|
+
{
|
|
3572
|
+
name: "qa_project_test_runs_summary_get",
|
|
3573
|
+
description: "Get a summary of test runs for a project.",
|
|
3574
|
+
inputSchema: ProjectTestRunsSummaryInputSchema,
|
|
3575
|
+
mapToUpstream: (input) => {
|
|
3576
|
+
const data = input;
|
|
3577
|
+
return {
|
|
3578
|
+
method: "GET",
|
|
3579
|
+
path: `${MUGGLE_TEST_PREFIX}/projects/${data.projectId}/test-runs/summary`
|
|
3580
|
+
};
|
|
3581
|
+
}
|
|
3582
|
+
},
|
|
3583
|
+
{
|
|
3584
|
+
name: "qa_report_stats_summary_get",
|
|
3585
|
+
description: "Get report statistics summary for a project.",
|
|
3586
|
+
inputSchema: ReportStatsSummaryInputSchema,
|
|
3587
|
+
mapToUpstream: (input) => {
|
|
3588
|
+
const data = input;
|
|
3589
|
+
return {
|
|
3590
|
+
method: "GET",
|
|
3591
|
+
path: `${MUGGLE_TEST_PREFIX}/report/stats-summary`,
|
|
3592
|
+
queryParams: { projectId: data.projectId }
|
|
3593
|
+
};
|
|
3594
|
+
}
|
|
3595
|
+
},
|
|
3596
|
+
{
|
|
3597
|
+
name: "qa_report_cost_query",
|
|
3598
|
+
description: "Query cost/usage data for a project over a date range.",
|
|
3599
|
+
inputSchema: ReportCostQueryInputSchema,
|
|
3600
|
+
mapToUpstream: (input) => {
|
|
3601
|
+
const data = input;
|
|
3602
|
+
return {
|
|
3603
|
+
method: "POST",
|
|
3604
|
+
path: `${MUGGLE_TEST_PREFIX}/report/cost/query`,
|
|
3605
|
+
body: {
|
|
3606
|
+
projectId: data.projectId,
|
|
3607
|
+
startDateKey: data.startDateKey,
|
|
3608
|
+
endDateKey: data.endDateKey,
|
|
3609
|
+
filterType: data.filterType,
|
|
3610
|
+
filterIds: data.filterIds
|
|
3611
|
+
}
|
|
3612
|
+
};
|
|
3613
|
+
}
|
|
3614
|
+
},
|
|
3615
|
+
{
|
|
3616
|
+
name: "qa_report_preferences_upsert",
|
|
3617
|
+
description: "Update report delivery preferences for a project.",
|
|
3618
|
+
inputSchema: ReportPreferencesUpsertInputSchema,
|
|
3619
|
+
mapToUpstream: (input) => {
|
|
3620
|
+
const data = input;
|
|
3621
|
+
return {
|
|
3622
|
+
method: "PUT",
|
|
3623
|
+
path: `${MUGGLE_TEST_PREFIX}/report/preferences`,
|
|
3624
|
+
body: {
|
|
3625
|
+
projectId: data.projectId,
|
|
3626
|
+
channels: data.channels,
|
|
3627
|
+
emails: data.emails,
|
|
3628
|
+
phones: data.phones,
|
|
3629
|
+
webhookUrl: data.webhookUrl,
|
|
3630
|
+
defaultExportFormat: data.defaultExportFormat
|
|
3631
|
+
}
|
|
3632
|
+
};
|
|
3633
|
+
}
|
|
3634
|
+
},
|
|
3635
|
+
{
|
|
3636
|
+
name: "qa_report_final_generate",
|
|
3637
|
+
description: "Generate a final test report for a project.",
|
|
3638
|
+
inputSchema: ReportFinalGenerateInputSchema,
|
|
3639
|
+
mapToUpstream: (input) => {
|
|
3640
|
+
const data = input;
|
|
3641
|
+
return {
|
|
3642
|
+
method: "POST",
|
|
3643
|
+
path: `${MUGGLE_TEST_PREFIX}/report/final/generate`,
|
|
3644
|
+
body: {
|
|
3645
|
+
projectId: data.projectId,
|
|
3646
|
+
exportFormat: data.exportFormat
|
|
3647
|
+
},
|
|
3648
|
+
timeoutMs: getWorkflowTimeoutMs()
|
|
3649
|
+
};
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3652
|
+
];
|
|
3653
|
+
var secretTools = [
|
|
3654
|
+
{
|
|
3655
|
+
name: "qa_secret_list",
|
|
3656
|
+
description: "List all secrets for a project. Secret values are not returned for security.",
|
|
3657
|
+
inputSchema: SecretListInputSchema,
|
|
3658
|
+
mapToUpstream: (input) => {
|
|
3659
|
+
const data = input;
|
|
3660
|
+
return {
|
|
3661
|
+
method: "GET",
|
|
3662
|
+
path: `${MUGGLE_TEST_PREFIX}/secrets`,
|
|
3663
|
+
queryParams: { projectId: data.projectId }
|
|
3664
|
+
};
|
|
3665
|
+
}
|
|
3666
|
+
},
|
|
3667
|
+
{
|
|
3668
|
+
name: "qa_secret_create",
|
|
3669
|
+
description: "Create a new secret (credential) for a project.",
|
|
3670
|
+
inputSchema: SecretCreateInputSchema,
|
|
3671
|
+
mapToUpstream: (input) => {
|
|
3672
|
+
const data = input;
|
|
3673
|
+
return {
|
|
3674
|
+
method: "POST",
|
|
3675
|
+
path: `${MUGGLE_TEST_PREFIX}/secrets`,
|
|
3676
|
+
body: {
|
|
3677
|
+
projectId: data.projectId,
|
|
3678
|
+
secretName: data.name,
|
|
3679
|
+
value: data.value,
|
|
3680
|
+
description: data.description,
|
|
3681
|
+
source: data.source
|
|
3682
|
+
}
|
|
3683
|
+
};
|
|
3684
|
+
}
|
|
3685
|
+
},
|
|
3686
|
+
{
|
|
3687
|
+
name: "qa_secret_get",
|
|
3688
|
+
description: "Get details of a specific secret. The secret value is not returned for security.",
|
|
3689
|
+
inputSchema: SecretGetInputSchema,
|
|
3690
|
+
mapToUpstream: (input) => {
|
|
3691
|
+
const data = input;
|
|
3692
|
+
return {
|
|
3693
|
+
method: "GET",
|
|
3694
|
+
path: `${MUGGLE_TEST_PREFIX}/secrets/${data.secretId}`
|
|
3695
|
+
};
|
|
3696
|
+
}
|
|
3697
|
+
},
|
|
3698
|
+
{
|
|
3699
|
+
name: "qa_secret_update",
|
|
3700
|
+
description: "Update an existing secret.",
|
|
3701
|
+
inputSchema: SecretUpdateInputSchema,
|
|
3702
|
+
mapToUpstream: (input) => {
|
|
3703
|
+
const data = input;
|
|
3704
|
+
const body = {};
|
|
3705
|
+
if (data.name !== void 0) body.name = data.name;
|
|
3706
|
+
if (data.value !== void 0) body.value = data.value;
|
|
3707
|
+
if (data.description !== void 0) body.description = data.description;
|
|
3708
|
+
return {
|
|
3709
|
+
method: "PUT",
|
|
3710
|
+
path: `${MUGGLE_TEST_PREFIX}/secrets/${data.secretId}`,
|
|
3711
|
+
body
|
|
3712
|
+
};
|
|
3713
|
+
}
|
|
3714
|
+
},
|
|
3715
|
+
{
|
|
3716
|
+
name: "qa_secret_delete",
|
|
3717
|
+
description: "Delete a secret from a project.",
|
|
3718
|
+
inputSchema: SecretDeleteInputSchema,
|
|
3719
|
+
mapToUpstream: (input) => {
|
|
3720
|
+
const data = input;
|
|
3721
|
+
return {
|
|
3722
|
+
method: "DELETE",
|
|
3723
|
+
path: `${MUGGLE_TEST_PREFIX}/secrets/${data.secretId}`
|
|
3724
|
+
};
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
];
|
|
3728
|
+
var prdFileTools = [
|
|
3729
|
+
{
|
|
3730
|
+
name: "qa_prd_file_upload",
|
|
3731
|
+
description: "Upload a PRD file to a project. File content should be base64-encoded.",
|
|
3732
|
+
inputSchema: PrdFileUploadInputSchema,
|
|
3733
|
+
mapToUpstream: (input) => {
|
|
3734
|
+
const data = input;
|
|
3735
|
+
return {
|
|
3736
|
+
method: "POST",
|
|
3737
|
+
path: `${MUGGLE_TEST_PREFIX}/workflow/use-case/prd-file-upload`,
|
|
3738
|
+
multipartFormData: {
|
|
3739
|
+
fileFieldName: "file",
|
|
3740
|
+
fileName: data.fileName,
|
|
3741
|
+
contentType: data.contentType || "application/octet-stream",
|
|
3742
|
+
fileBase64: data.contentBase64
|
|
3743
|
+
}
|
|
3744
|
+
};
|
|
3745
|
+
}
|
|
3746
|
+
},
|
|
3747
|
+
{
|
|
3748
|
+
name: "qa_prd_file_list_by_project",
|
|
3749
|
+
description: "List all PRD files associated with a project.",
|
|
3750
|
+
inputSchema: PrdFileListInputSchema,
|
|
3751
|
+
mapToUpstream: (input) => {
|
|
3752
|
+
const data = input;
|
|
3753
|
+
return {
|
|
3754
|
+
method: "GET",
|
|
3755
|
+
path: `${MUGGLE_TEST_PREFIX}/projects/${data.projectId}/prd-files`
|
|
3756
|
+
};
|
|
3757
|
+
}
|
|
3758
|
+
},
|
|
3759
|
+
{
|
|
3760
|
+
name: "qa_prd_file_delete",
|
|
3761
|
+
description: "Delete a PRD file from a project.",
|
|
3762
|
+
inputSchema: PrdFileDeleteInputSchema,
|
|
3763
|
+
mapToUpstream: (input) => {
|
|
3764
|
+
const data = input;
|
|
3765
|
+
return {
|
|
3766
|
+
method: "DELETE",
|
|
3767
|
+
path: `${MUGGLE_TEST_PREFIX}/projects/${data.projectId}/prd-files/${data.prdFileId}`
|
|
3768
|
+
};
|
|
3769
|
+
}
|
|
3770
|
+
},
|
|
3771
|
+
{
|
|
3772
|
+
name: "qa_workflow_start_prd_file_process",
|
|
3773
|
+
description: "Start a PRD file processing workflow to extract use cases.",
|
|
3774
|
+
inputSchema: PrdFileProcessStartInputSchema,
|
|
3775
|
+
mapToUpstream: (input) => {
|
|
3776
|
+
const data = input;
|
|
3777
|
+
return {
|
|
3778
|
+
method: "POST",
|
|
3779
|
+
path: `${MUGGLE_TEST_PREFIX}/workflow/use-case/prd-file-process`,
|
|
3780
|
+
body: {
|
|
3781
|
+
projectId: data.projectId,
|
|
3782
|
+
name: data.name,
|
|
3783
|
+
description: data.description,
|
|
3784
|
+
prdFilePath: data.prdFilePath,
|
|
3785
|
+
originalFileName: data.originalFileName,
|
|
3786
|
+
url: data.url,
|
|
3787
|
+
contentChecksum: data.contentChecksum,
|
|
3788
|
+
fileSize: data.fileSize
|
|
3789
|
+
}
|
|
3790
|
+
};
|
|
3791
|
+
}
|
|
3792
|
+
},
|
|
3793
|
+
{
|
|
3794
|
+
name: "qa_workflow_get_prd_file_process_latest_run",
|
|
3795
|
+
description: "Get the latest run status of a PRD file processing workflow.",
|
|
3796
|
+
inputSchema: PrdFileProcessLatestRunInputSchema,
|
|
3797
|
+
mapToUpstream: (input) => {
|
|
3798
|
+
const data = input;
|
|
3799
|
+
return {
|
|
3800
|
+
method: "GET",
|
|
3801
|
+
path: `${MUGGLE_TEST_PREFIX}/workflow/use-case/prd-file-process/${data.workflowRuntimeId}/run/latest`
|
|
3802
|
+
};
|
|
3803
|
+
}
|
|
3804
|
+
}
|
|
3805
|
+
];
|
|
3806
|
+
var walletTools = [
|
|
3807
|
+
{
|
|
3808
|
+
name: "qa_wallet_topup",
|
|
3809
|
+
description: "Create a Stripe checkout session to purchase a token package.",
|
|
3810
|
+
inputSchema: WalletTopUpInputSchema,
|
|
3811
|
+
mapToUpstream: (input) => {
|
|
3812
|
+
const data = input;
|
|
3813
|
+
return {
|
|
3814
|
+
method: "POST",
|
|
3815
|
+
path: "/v1/protected/wallet/topup",
|
|
3816
|
+
body: {
|
|
3817
|
+
packageId: data.packageId,
|
|
3818
|
+
checkoutSuccessCallback: data.checkoutSuccessCallback,
|
|
3819
|
+
checkoutCancelCallback: data.checkoutCancelCallback
|
|
3820
|
+
}
|
|
3821
|
+
};
|
|
3822
|
+
}
|
|
3823
|
+
},
|
|
3824
|
+
{
|
|
3825
|
+
name: "qa_wallet_payment_method_create_setup_session",
|
|
3826
|
+
description: "Create a Stripe setup session to add a payment method.",
|
|
3827
|
+
inputSchema: WalletPaymentMethodCreateSetupSessionInputSchema,
|
|
3828
|
+
mapToUpstream: (input) => {
|
|
3829
|
+
const data = input;
|
|
3830
|
+
return {
|
|
3831
|
+
method: "POST",
|
|
3832
|
+
path: "/v1/protected/wallet/payment-methods/setup",
|
|
3833
|
+
body: {
|
|
3834
|
+
checkoutSuccessCallback: data.checkoutSuccessCallback,
|
|
3835
|
+
checkoutCancelCallback: data.checkoutCancelCallback
|
|
3836
|
+
}
|
|
3837
|
+
};
|
|
3838
|
+
}
|
|
3839
|
+
},
|
|
3840
|
+
{
|
|
3841
|
+
name: "qa_wallet_auto_topup_set_payment_method",
|
|
3842
|
+
description: "Set the saved payment method used by wallet auto top-up.",
|
|
3843
|
+
inputSchema: WalletAutoTopUpSetPaymentMethodInputSchema,
|
|
3844
|
+
mapToUpstream: (input) => {
|
|
3845
|
+
const data = input;
|
|
3846
|
+
return {
|
|
3847
|
+
method: "PUT",
|
|
3848
|
+
path: "/v1/protected/wallet/auto-topup/payment-method",
|
|
3849
|
+
body: { paymentMethodId: data.paymentMethodId }
|
|
3850
|
+
};
|
|
3851
|
+
}
|
|
3852
|
+
},
|
|
3853
|
+
{
|
|
3854
|
+
name: "qa_wallet_payment_method_list",
|
|
3855
|
+
description: "List saved payment methods.",
|
|
3856
|
+
inputSchema: WalletPaymentMethodListInputSchema,
|
|
3857
|
+
mapToUpstream: () => {
|
|
3858
|
+
return {
|
|
3859
|
+
method: "GET",
|
|
3860
|
+
path: "/v1/protected/wallet/payment-methods"
|
|
3861
|
+
};
|
|
3862
|
+
}
|
|
3863
|
+
},
|
|
3864
|
+
{
|
|
3865
|
+
name: "qa_wallet_auto_topup_update",
|
|
3866
|
+
description: "Update wallet auto-topup settings.",
|
|
3867
|
+
inputSchema: WalletAutoTopUpUpdateInputSchema,
|
|
3868
|
+
mapToUpstream: (input) => {
|
|
3869
|
+
const data = input;
|
|
3870
|
+
return {
|
|
3871
|
+
method: "PUT",
|
|
3872
|
+
path: "/v1/protected/wallet/auto-topup",
|
|
3873
|
+
body: {
|
|
3874
|
+
enabled: data.enabled,
|
|
3875
|
+
topUpTriggerTokenThreshold: data.topUpTriggerTokenThreshold,
|
|
3876
|
+
packageId: data.packageId
|
|
3877
|
+
}
|
|
3878
|
+
};
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3881
|
+
];
|
|
3882
|
+
var recommendationTools = [
|
|
3883
|
+
{
|
|
3884
|
+
name: "qa_recommend_schedule",
|
|
3885
|
+
description: "Get recommendations for test scheduling based on project needs.",
|
|
3886
|
+
inputSchema: RecommendScheduleInputSchema,
|
|
3887
|
+
requiresAuth: false,
|
|
3888
|
+
mapToUpstream: () => {
|
|
3889
|
+
throw new Error("RECOMMENDATION_ONLY");
|
|
3890
|
+
},
|
|
3891
|
+
mapFromUpstream: () => {
|
|
3892
|
+
return {
|
|
3893
|
+
recommendations: [
|
|
3894
|
+
{
|
|
3895
|
+
title: "Nightly Regression Tests",
|
|
3896
|
+
rationale: "Running tests every night catches regressions quickly.",
|
|
3897
|
+
schedule: "0 2 * * *",
|
|
3898
|
+
timezone: "UTC"
|
|
3899
|
+
},
|
|
3900
|
+
{
|
|
3901
|
+
title: "On-Demand with Smoke Tests",
|
|
3902
|
+
rationale: "Run smoke tests on every PR, full regression on merge.",
|
|
3903
|
+
schedule: "Pull Request trigger + main branch merge"
|
|
3904
|
+
},
|
|
3905
|
+
{
|
|
3906
|
+
title: "Continuous Monitoring",
|
|
3907
|
+
rationale: "Run tests every 4 hours for production monitoring.",
|
|
3908
|
+
schedule: "0 */4 * * *"
|
|
3909
|
+
}
|
|
3910
|
+
]
|
|
3911
|
+
};
|
|
3912
|
+
},
|
|
3913
|
+
localHandler: async () => {
|
|
3914
|
+
return {
|
|
3915
|
+
recommendations: [
|
|
3916
|
+
{
|
|
3917
|
+
title: "Nightly Regression Tests",
|
|
3918
|
+
rationale: "Running tests every night catches regressions quickly.",
|
|
3919
|
+
schedule: "0 2 * * *",
|
|
3920
|
+
timezone: "UTC"
|
|
3921
|
+
},
|
|
3922
|
+
{
|
|
3923
|
+
title: "On-Demand with Smoke Tests",
|
|
3924
|
+
rationale: "Run smoke tests on every PR, full regression on merge.",
|
|
3925
|
+
schedule: "Pull Request trigger + main branch merge"
|
|
3926
|
+
},
|
|
3927
|
+
{
|
|
3928
|
+
title: "Continuous Monitoring",
|
|
3929
|
+
rationale: "Run tests every 4 hours for production monitoring.",
|
|
3930
|
+
schedule: "0 */4 * * *"
|
|
3931
|
+
}
|
|
3932
|
+
]
|
|
3933
|
+
};
|
|
3934
|
+
}
|
|
3935
|
+
},
|
|
3936
|
+
{
|
|
3937
|
+
name: "qa_recommend_cicd_setup",
|
|
3938
|
+
description: "Get recommendations and templates for CI/CD integration.",
|
|
3939
|
+
inputSchema: RecommendCicdSetupInputSchema,
|
|
3940
|
+
requiresAuth: false,
|
|
3941
|
+
mapToUpstream: () => {
|
|
3942
|
+
throw new Error("RECOMMENDATION_ONLY");
|
|
3943
|
+
},
|
|
3944
|
+
localHandler: async (input) => {
|
|
3945
|
+
const data = input;
|
|
3946
|
+
const provider = data?.repositoryProvider || "github";
|
|
3947
|
+
const recommendations = [];
|
|
3948
|
+
if (provider === "github" || provider === "other") {
|
|
3949
|
+
recommendations.push({
|
|
3950
|
+
title: "GitHub Actions Integration",
|
|
3951
|
+
rationale: "Native GitHub integration with minimal setup.",
|
|
3952
|
+
steps: [
|
|
3953
|
+
"Create .github/workflows/muggle-test.yml",
|
|
3954
|
+
"Add MUGGLE_AI_API_KEY as a repository secret",
|
|
3955
|
+
"Configure workflow trigger"
|
|
3956
|
+
]
|
|
3957
|
+
});
|
|
3958
|
+
}
|
|
3959
|
+
if (provider === "azureDevOps" || provider === "other") {
|
|
3960
|
+
recommendations.push({
|
|
3961
|
+
title: "Azure DevOps Pipelines Integration",
|
|
3962
|
+
rationale: "Native Azure DevOps integration with pipeline triggers.",
|
|
3963
|
+
steps: [
|
|
3964
|
+
"Create azure-pipelines.yml",
|
|
3965
|
+
"Add MUGGLE_AI_API_KEY to variable group",
|
|
3966
|
+
"Configure triggers"
|
|
3967
|
+
]
|
|
3968
|
+
});
|
|
3969
|
+
}
|
|
3970
|
+
if (provider === "gitlab" || provider === "other") {
|
|
3971
|
+
recommendations.push({
|
|
3972
|
+
title: "GitLab CI Integration",
|
|
3973
|
+
rationale: "Native GitLab CI integration with merge request pipelines.",
|
|
3974
|
+
steps: [
|
|
3975
|
+
"Add .gitlab-ci.yml",
|
|
3976
|
+
"Add MUGGLE_AI_API_KEY as CI/CD variable",
|
|
3977
|
+
"Configure pipeline rules"
|
|
3978
|
+
]
|
|
3979
|
+
});
|
|
3980
|
+
}
|
|
3981
|
+
return { recommendations };
|
|
3982
|
+
}
|
|
3983
|
+
}
|
|
3984
|
+
];
|
|
3985
|
+
var API_KEY_PREFIX = "/v1/protected/api-keys";
|
|
3986
|
+
var apiKeyTools = [
|
|
3987
|
+
{
|
|
3988
|
+
name: "qa_auth_api_key_create",
|
|
3989
|
+
description: "Create a new API key for the authenticated user. Requires existing authentication.",
|
|
3990
|
+
inputSchema: ApiKeyCreateInputSchema,
|
|
3991
|
+
mapToUpstream: (input) => {
|
|
3992
|
+
const data = input;
|
|
3993
|
+
return {
|
|
3994
|
+
method: "POST",
|
|
3995
|
+
path: API_KEY_PREFIX,
|
|
3996
|
+
body: {
|
|
3997
|
+
name: data.name || "MCP Gateway Key",
|
|
3998
|
+
expiry: data.expiry || "90d"
|
|
3999
|
+
}
|
|
4000
|
+
};
|
|
4001
|
+
},
|
|
4002
|
+
mapFromUpstream: (response) => {
|
|
4003
|
+
const data = response.data;
|
|
4004
|
+
const maskedKey = `${data.prefix}...${data.lastFour}`;
|
|
4005
|
+
const expiresAt = data.expiresAt ? new Date(data.expiresAt).toISOString() : "never";
|
|
4006
|
+
return {
|
|
4007
|
+
success: true,
|
|
4008
|
+
message: "API key created.",
|
|
4009
|
+
apiKey: {
|
|
4010
|
+
id: data.id,
|
|
4011
|
+
key: data.key,
|
|
4012
|
+
hint: maskedKey,
|
|
4013
|
+
name: data.name,
|
|
4014
|
+
status: data.status,
|
|
4015
|
+
createdAt: new Date(data.createdAt).toISOString(),
|
|
4016
|
+
expiresAt
|
|
4017
|
+
},
|
|
4018
|
+
note: "The full API key is returned only once. Store it securely."
|
|
4019
|
+
};
|
|
4020
|
+
}
|
|
4021
|
+
},
|
|
4022
|
+
{
|
|
4023
|
+
name: "qa_auth_api_key_list",
|
|
4024
|
+
description: "List all API keys for the authenticated user. Shows key metadata but not the secret values.",
|
|
4025
|
+
inputSchema: ApiKeyListInputSchema,
|
|
4026
|
+
mapToUpstream: () => {
|
|
4027
|
+
return {
|
|
4028
|
+
method: "GET",
|
|
4029
|
+
path: API_KEY_PREFIX
|
|
4030
|
+
};
|
|
4031
|
+
},
|
|
4032
|
+
mapFromUpstream: (response) => {
|
|
4033
|
+
const keys = response.data;
|
|
4034
|
+
return {
|
|
4035
|
+
success: true,
|
|
4036
|
+
count: keys.length,
|
|
4037
|
+
apiKeys: keys.map((key) => ({
|
|
4038
|
+
id: key.id,
|
|
4039
|
+
name: key.name,
|
|
4040
|
+
status: key.status,
|
|
4041
|
+
hint: `${key.prefix}...${key.lastFour}`,
|
|
4042
|
+
createdAt: new Date(key.createdAt).toISOString(),
|
|
4043
|
+
expiresAt: key.expiresAt ? new Date(key.expiresAt).toISOString() : "never",
|
|
4044
|
+
revokedAt: key.revokedAt ? new Date(key.revokedAt).toISOString() : null
|
|
4045
|
+
}))
|
|
4046
|
+
};
|
|
4047
|
+
}
|
|
4048
|
+
},
|
|
4049
|
+
{
|
|
4050
|
+
name: "qa_auth_api_key_get",
|
|
4051
|
+
description: "Get details of a specific API key by ID.",
|
|
4052
|
+
inputSchema: ApiKeyGetInputSchema,
|
|
4053
|
+
mapToUpstream: (input) => {
|
|
4054
|
+
const data = input;
|
|
4055
|
+
return {
|
|
4056
|
+
method: "GET",
|
|
4057
|
+
path: `${API_KEY_PREFIX}/${data.apiKeyId}`
|
|
4058
|
+
};
|
|
4059
|
+
},
|
|
4060
|
+
mapFromUpstream: (response) => {
|
|
4061
|
+
const key = response.data;
|
|
4062
|
+
return {
|
|
4063
|
+
success: true,
|
|
4064
|
+
apiKey: {
|
|
4065
|
+
id: key.id,
|
|
4066
|
+
name: key.name,
|
|
4067
|
+
status: key.status,
|
|
4068
|
+
hint: `${key.prefix}...${key.lastFour}`,
|
|
4069
|
+
createdAt: new Date(key.createdAt).toISOString(),
|
|
4070
|
+
expiresAt: key.expiresAt ? new Date(key.expiresAt).toISOString() : "never",
|
|
4071
|
+
revokedAt: key.revokedAt ? new Date(key.revokedAt).toISOString() : null
|
|
4072
|
+
}
|
|
4073
|
+
};
|
|
4074
|
+
}
|
|
4075
|
+
},
|
|
4076
|
+
{
|
|
4077
|
+
name: "qa_auth_api_key_revoke",
|
|
4078
|
+
description: "Revoke an API key. The key will immediately stop working. Use qa_auth_api_key_list to find the key ID first.",
|
|
4079
|
+
inputSchema: ApiKeyRevokeInputSchema,
|
|
4080
|
+
mapToUpstream: (input) => {
|
|
4081
|
+
const data = input;
|
|
4082
|
+
return {
|
|
4083
|
+
method: "DELETE",
|
|
4084
|
+
path: `${API_KEY_PREFIX}/${data.apiKeyId}`
|
|
4085
|
+
};
|
|
4086
|
+
},
|
|
4087
|
+
mapFromUpstream: () => {
|
|
4088
|
+
return {
|
|
4089
|
+
success: true,
|
|
4090
|
+
message: "API key revoked successfully. It will no longer work for authentication."
|
|
4091
|
+
};
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
4094
|
+
];
|
|
4095
|
+
var authTools = [
|
|
4096
|
+
{
|
|
4097
|
+
name: "qa_auth_status",
|
|
4098
|
+
description: "Check current authentication status. Shows if you're logged in and when your session expires.",
|
|
4099
|
+
inputSchema: EmptyInputSchema,
|
|
4100
|
+
requiresAuth: false,
|
|
4101
|
+
mapToUpstream: () => {
|
|
4102
|
+
throw new Error("LOCAL_HANDLER_ONLY");
|
|
4103
|
+
},
|
|
4104
|
+
localHandler: async () => {
|
|
4105
|
+
const authService = getAuthService();
|
|
4106
|
+
const status = authService.getAuthStatus();
|
|
4107
|
+
if (!status.authenticated) {
|
|
4108
|
+
return {
|
|
4109
|
+
authenticated: false,
|
|
4110
|
+
message: "Not authenticated. Use qa_auth_login to authenticate."
|
|
4111
|
+
};
|
|
4112
|
+
}
|
|
4113
|
+
return {
|
|
4114
|
+
authenticated: true,
|
|
4115
|
+
email: status.email,
|
|
4116
|
+
userId: status.userId,
|
|
4117
|
+
expiresAt: status.expiresAt,
|
|
4118
|
+
isExpired: status.isExpired
|
|
4119
|
+
};
|
|
4120
|
+
}
|
|
4121
|
+
},
|
|
4122
|
+
{
|
|
4123
|
+
name: "qa_auth_login",
|
|
4124
|
+
description: "Start authentication with the Muggle Test service. Opens a browser-based login flow and waits for confirmation by default. If login is still pending after the wait timeout, use qa_auth_poll to finish authentication.",
|
|
4125
|
+
inputSchema: AuthLoginInputSchema,
|
|
4126
|
+
requiresAuth: false,
|
|
4127
|
+
mapToUpstream: () => {
|
|
4128
|
+
throw new Error("LOCAL_HANDLER_ONLY");
|
|
4129
|
+
},
|
|
4130
|
+
localHandler: async (input) => {
|
|
4131
|
+
const data = input;
|
|
4132
|
+
const authService = getAuthService();
|
|
4133
|
+
const deviceCodeResponse = await authService.startDeviceCodeFlow();
|
|
4134
|
+
const waitForCompletion = data.waitForCompletion ?? true;
|
|
4135
|
+
if (!waitForCompletion) {
|
|
4136
|
+
return {
|
|
4137
|
+
status: "pending",
|
|
4138
|
+
deviceCode: deviceCodeResponse.deviceCode,
|
|
4139
|
+
userCode: deviceCodeResponse.userCode,
|
|
4140
|
+
verificationUri: deviceCodeResponse.verificationUri,
|
|
4141
|
+
browserOpened: deviceCodeResponse.browserOpened,
|
|
4142
|
+
message: "Login started. Complete authentication in your browser, then call qa_auth_poll."
|
|
4143
|
+
};
|
|
4144
|
+
}
|
|
4145
|
+
const pollResult = await authService.waitForDeviceCodeAuthorization({
|
|
4146
|
+
deviceCode: deviceCodeResponse.deviceCode,
|
|
4147
|
+
intervalSeconds: deviceCodeResponse.interval,
|
|
4148
|
+
timeoutMs: data.timeoutMs
|
|
4149
|
+
});
|
|
4150
|
+
if (pollResult.status === "complete" /* Complete */) {
|
|
4151
|
+
return {
|
|
4152
|
+
status: "complete",
|
|
4153
|
+
success: true,
|
|
4154
|
+
email: pollResult.email,
|
|
4155
|
+
message: "Login successful. You are now authenticated."
|
|
4156
|
+
};
|
|
4157
|
+
}
|
|
4158
|
+
return {
|
|
4159
|
+
status: pollResult.status,
|
|
4160
|
+
message: pollResult.message
|
|
4161
|
+
};
|
|
4162
|
+
}
|
|
4163
|
+
},
|
|
4164
|
+
{
|
|
4165
|
+
name: "qa_auth_poll",
|
|
4166
|
+
description: "Poll for login completion after starting the login flow with qa_auth_login. Call this after the user completes authentication in their browser.",
|
|
4167
|
+
inputSchema: AuthPollInputSchema,
|
|
4168
|
+
requiresAuth: false,
|
|
4169
|
+
mapToUpstream: () => {
|
|
4170
|
+
throw new Error("LOCAL_HANDLER_ONLY");
|
|
4171
|
+
},
|
|
4172
|
+
localHandler: async (input) => {
|
|
4173
|
+
const data = input;
|
|
4174
|
+
const authService = getAuthService();
|
|
4175
|
+
const deviceCode = data.deviceCode ?? authService.getPendingDeviceCode();
|
|
4176
|
+
if (!deviceCode) {
|
|
4177
|
+
return {
|
|
4178
|
+
error: "NO_PENDING_LOGIN",
|
|
4179
|
+
message: "No pending login found. Please start a new login with qa_auth_login."
|
|
4180
|
+
};
|
|
4181
|
+
}
|
|
4182
|
+
const result = await authService.pollDeviceCode(deviceCode);
|
|
4183
|
+
if (result.status === "complete" /* Complete */) {
|
|
4184
|
+
return {
|
|
4185
|
+
status: "complete",
|
|
4186
|
+
success: true,
|
|
4187
|
+
email: result.email,
|
|
4188
|
+
message: "Login complete. You are now authenticated."
|
|
4189
|
+
};
|
|
4190
|
+
}
|
|
4191
|
+
return {
|
|
4192
|
+
status: result.status,
|
|
4193
|
+
message: result.message
|
|
4194
|
+
};
|
|
4195
|
+
}
|
|
4196
|
+
},
|
|
4197
|
+
{
|
|
4198
|
+
name: "qa_auth_logout",
|
|
4199
|
+
description: "Log out and clear stored credentials.",
|
|
4200
|
+
inputSchema: EmptyInputSchema,
|
|
4201
|
+
requiresAuth: false,
|
|
4202
|
+
mapToUpstream: () => {
|
|
4203
|
+
throw new Error("LOCAL_HANDLER_ONLY");
|
|
4204
|
+
},
|
|
4205
|
+
localHandler: async () => {
|
|
4206
|
+
const authService = getAuthService();
|
|
4207
|
+
const result = authService.logout();
|
|
4208
|
+
if (result) {
|
|
4209
|
+
return { success: true, message: "Successfully logged out." };
|
|
4210
|
+
}
|
|
4211
|
+
return { success: false, message: "No active session to log out from." };
|
|
4212
|
+
}
|
|
4213
|
+
}
|
|
4214
|
+
];
|
|
4215
|
+
var allQaToolDefinitions = [
|
|
4216
|
+
...projectTools,
|
|
4217
|
+
...useCaseTools,
|
|
4218
|
+
...testCaseTools,
|
|
4219
|
+
...testScriptTools,
|
|
4220
|
+
...workflowTools,
|
|
4221
|
+
...reportTools,
|
|
4222
|
+
...secretTools,
|
|
4223
|
+
...prdFileTools,
|
|
4224
|
+
...walletTools,
|
|
4225
|
+
...recommendationTools,
|
|
4226
|
+
...apiKeyTools,
|
|
4227
|
+
...authTools
|
|
4228
|
+
];
|
|
4229
|
+
function getQaToolByName(name) {
|
|
4230
|
+
return allQaToolDefinitions.find((tool) => tool.name === name);
|
|
4231
|
+
}
|
|
4232
|
+
function defaultResponseMapper(response) {
|
|
4233
|
+
return response.data;
|
|
4234
|
+
}
|
|
4235
|
+
async function executeQaTool(toolName, input, correlationId) {
|
|
4236
|
+
const logger5 = createChildLogger(correlationId);
|
|
4237
|
+
const tool = getQaToolByName(toolName);
|
|
4238
|
+
if (!tool) {
|
|
4239
|
+
return {
|
|
4240
|
+
content: JSON.stringify({ error: "NOT_FOUND", message: `Unknown tool: ${toolName}` }),
|
|
4241
|
+
isError: true
|
|
4242
|
+
};
|
|
4243
|
+
}
|
|
4244
|
+
try {
|
|
4245
|
+
const validatedInput = tool.inputSchema.parse(input);
|
|
4246
|
+
if (tool.localHandler) {
|
|
4247
|
+
const result = await tool.localHandler(validatedInput);
|
|
4248
|
+
return {
|
|
4249
|
+
content: JSON.stringify(result, null, 2),
|
|
4250
|
+
isError: false
|
|
4251
|
+
};
|
|
4252
|
+
}
|
|
4253
|
+
const credentials = getCallerCredentials();
|
|
4254
|
+
try {
|
|
4255
|
+
const upstreamCall = tool.mapToUpstream(validatedInput);
|
|
4256
|
+
const client = getPromptServiceClient();
|
|
4257
|
+
const response = await client.execute(upstreamCall, credentials, correlationId);
|
|
4258
|
+
const mapper = tool.mapFromUpstream || defaultResponseMapper;
|
|
4259
|
+
const result = mapper(response, validatedInput);
|
|
4260
|
+
return {
|
|
4261
|
+
content: JSON.stringify(result, null, 2),
|
|
4262
|
+
isError: false
|
|
4263
|
+
};
|
|
4264
|
+
} catch (error) {
|
|
4265
|
+
if (error instanceof Error && error.message === "RECOMMENDATION_ONLY") {
|
|
4266
|
+
const mapper = tool.mapFromUpstream || defaultResponseMapper;
|
|
4267
|
+
const result = mapper({ statusCode: 200, data: {}, headers: {} }, validatedInput);
|
|
4268
|
+
return {
|
|
4269
|
+
content: JSON.stringify(result, null, 2),
|
|
4270
|
+
isError: false
|
|
4271
|
+
};
|
|
4272
|
+
}
|
|
4273
|
+
throw error;
|
|
4274
|
+
}
|
|
4275
|
+
} catch (error) {
|
|
4276
|
+
if (error instanceof GatewayError) {
|
|
4277
|
+
logger5.warn("Tool call failed with gateway error", {
|
|
4278
|
+
tool: toolName,
|
|
4279
|
+
code: error.code,
|
|
4280
|
+
message: error.message
|
|
4281
|
+
});
|
|
4282
|
+
return {
|
|
4283
|
+
content: JSON.stringify({ error: error.code, message: error.message }),
|
|
4284
|
+
isError: true
|
|
4285
|
+
};
|
|
4286
|
+
}
|
|
4287
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4288
|
+
logger5.error("Tool call failed", { tool: toolName, error: errorMessage });
|
|
4289
|
+
return {
|
|
4290
|
+
content: JSON.stringify({ error: "INTERNAL_ERROR", message: errorMessage }),
|
|
4291
|
+
isError: true
|
|
4292
|
+
};
|
|
4293
|
+
}
|
|
4294
|
+
}
|
|
4295
|
+
|
|
4296
|
+
// src/qa/index.ts
|
|
4297
|
+
function getQaTools() {
|
|
4298
|
+
return allQaToolDefinitions.map((tool) => ({
|
|
4299
|
+
name: tool.name,
|
|
4300
|
+
description: tool.description,
|
|
4301
|
+
inputSchema: tool.inputSchema,
|
|
4302
|
+
requiresAuth: tool.requiresAuth !== false,
|
|
4303
|
+
execute: async (params) => {
|
|
4304
|
+
return executeQaTool(tool.name, params.input, params.correlationId);
|
|
4305
|
+
}
|
|
4306
|
+
}));
|
|
4307
|
+
}
|
|
4308
|
+
|
|
4309
|
+
// src/local-qa/index.ts
|
|
4310
|
+
var local_qa_exports = {};
|
|
4311
|
+
__export(local_qa_exports, {
|
|
4312
|
+
AuthLoginInputSchema: () => AuthLoginInputSchema2,
|
|
4313
|
+
AuthPollInputSchema: () => AuthPollInputSchema2,
|
|
4314
|
+
AuthService: () => AuthService,
|
|
4315
|
+
CancelExecutionInputSchema: () => CancelExecutionInputSchema,
|
|
4316
|
+
CleanupSessionsInputSchema: () => CleanupSessionsInputSchema,
|
|
4317
|
+
CloudMappingEntityType: () => CloudMappingEntityType,
|
|
4318
|
+
DeviceCodePollStatus: () => DeviceCodePollStatus,
|
|
4319
|
+
EmptyInputSchema: () => EmptyInputSchema2,
|
|
4320
|
+
ExecuteReplayInputSchema: () => ExecuteReplayInputSchema,
|
|
4321
|
+
ExecuteTestGenerationInputSchema: () => ExecuteTestGenerationInputSchema,
|
|
4322
|
+
ExecutionStatus: () => ExecutionStatus,
|
|
4323
|
+
HealthStatus: () => HealthStatus,
|
|
4324
|
+
ListSessionsInputSchema: () => ListSessionsInputSchema,
|
|
4325
|
+
LocalRunStatus: () => LocalRunStatus,
|
|
4326
|
+
LocalRunType: () => LocalRunType,
|
|
4327
|
+
LocalTestScriptStatus: () => LocalTestScriptStatus,
|
|
4328
|
+
LocalWorkflowFileEntityType: () => LocalWorkflowFileEntityType,
|
|
4329
|
+
LocalWorkflowRunStatus: () => LocalWorkflowRunStatus,
|
|
4330
|
+
PublishTestScriptInputSchema: () => PublishTestScriptInputSchema,
|
|
4331
|
+
RunResultGetInputSchema: () => RunResultGetInputSchema,
|
|
4332
|
+
RunResultListInputSchema: () => RunResultListInputSchema,
|
|
4333
|
+
RunResultStorageService: () => RunResultStorageService,
|
|
4334
|
+
SessionStatus: () => SessionStatus,
|
|
4335
|
+
StorageService: () => StorageService,
|
|
4336
|
+
TestCaseDetailsSchema: () => TestCaseDetailsSchema,
|
|
4337
|
+
TestResultStatus: () => TestResultStatus,
|
|
4338
|
+
TestScriptDetailsSchema: () => TestScriptDetailsSchema,
|
|
4339
|
+
TestScriptGetInputSchema: () => TestScriptGetInputSchema2,
|
|
4340
|
+
TestScriptListInputSchema: () => TestScriptListInputSchema2,
|
|
4341
|
+
allLocalQaTools: () => allLocalQaTools,
|
|
4342
|
+
cancelExecution: () => cancelExecution,
|
|
4343
|
+
executeReplay: () => executeReplay,
|
|
4344
|
+
executeTestGeneration: () => executeTestGeneration,
|
|
4345
|
+
executeTool: () => executeTool,
|
|
4346
|
+
getAuthService: () => getAuthService,
|
|
4347
|
+
getLocalQaTools: () => getLocalQaTools,
|
|
4348
|
+
getRunResultStorageService: () => getRunResultStorageService,
|
|
4349
|
+
getStorageService: () => getStorageService,
|
|
4350
|
+
getTool: () => getTool,
|
|
4351
|
+
listActiveExecutions: () => listActiveExecutions,
|
|
4352
|
+
resetAuthService: () => resetAuthService,
|
|
4353
|
+
resetRunResultStorageService: () => resetRunResultStorageService,
|
|
4354
|
+
resetStorageService: () => resetStorageService
|
|
4355
|
+
});
|
|
4356
|
+
var AuthLoginInputSchema2 = z.object({
|
|
4357
|
+
waitForCompletion: z.boolean().optional().describe("Whether to wait for browser login completion before returning. Default: true"),
|
|
4358
|
+
timeoutMs: z.number().int().positive().min(1e3).max(9e5).optional().describe("Maximum time to wait for login completion in milliseconds. Default: 120000")
|
|
4359
|
+
});
|
|
4360
|
+
var AuthPollInputSchema2 = z.object({
|
|
4361
|
+
deviceCode: z.string().optional().describe("Device code from the login response. Optional if a login was recently started.")
|
|
4362
|
+
});
|
|
4363
|
+
var EmptyInputSchema2 = z.object({});
|
|
4364
|
+
var TestCaseDetailsSchema = z.object({
|
|
4365
|
+
/** Cloud test case ID. */
|
|
4366
|
+
id: z.string().min(1).describe("Cloud test case ID"),
|
|
4367
|
+
/** Test case title. */
|
|
4368
|
+
title: z.string().min(1).describe("Test case title"),
|
|
4369
|
+
/** Test goal. */
|
|
4370
|
+
goal: z.string().min(1).describe("Test goal - what the test should verify"),
|
|
4371
|
+
/** Expected result. */
|
|
4372
|
+
expectedResult: z.string().min(1).describe("Expected outcome after test execution"),
|
|
4373
|
+
/** Preconditions (optional). */
|
|
4374
|
+
precondition: z.string().optional().describe("Initial state/setup required before test execution"),
|
|
4375
|
+
/** Step-by-step instructions (optional). */
|
|
4376
|
+
instructions: z.string().optional().describe("Step-by-step instructions for the test"),
|
|
4377
|
+
/** Original cloud URL (for reference, replaced by localUrl). */
|
|
4378
|
+
url: z.string().url().optional().describe("Original cloud URL (replaced by localUrl during execution)")
|
|
4379
|
+
});
|
|
4380
|
+
var TestScriptDetailsSchema = z.object({
|
|
4381
|
+
/** Cloud test script ID. */
|
|
4382
|
+
id: z.string().min(1).describe("Cloud test script ID"),
|
|
4383
|
+
/** Script name. */
|
|
4384
|
+
name: z.string().min(1).describe("Test script name"),
|
|
4385
|
+
/** Cloud test case ID this script belongs to. */
|
|
4386
|
+
testCaseId: z.string().min(1).describe("Cloud test case ID this script was generated from"),
|
|
4387
|
+
/** Action script steps. */
|
|
4388
|
+
actionScript: z.array(z.unknown()).describe("Action script steps to replay"),
|
|
4389
|
+
/** Original cloud URL (for reference, replaced by localUrl). */
|
|
4390
|
+
url: z.string().url().optional().describe("Original cloud URL (replaced by localUrl during execution)")
|
|
4391
|
+
});
|
|
4392
|
+
var ExecuteTestGenerationInputSchema = z.object({
|
|
4393
|
+
/** Test case details from qa_test_case_get. */
|
|
4394
|
+
testCase: TestCaseDetailsSchema.describe("Test case details obtained from qa_test_case_get"),
|
|
4395
|
+
/** Local URL to test against. */
|
|
4396
|
+
localUrl: z.string().url().describe("Local URL to test against (e.g., http://localhost:3000)"),
|
|
4397
|
+
/** Explicit approval to launch electron-app. */
|
|
4398
|
+
approveElectronAppLaunch: z.boolean().describe("Set to true after the user explicitly approves launching electron-app"),
|
|
4399
|
+
/** Optional timeout. */
|
|
4400
|
+
timeoutMs: z.number().int().positive().optional().describe("Timeout in milliseconds (default: 300000 = 5 min)")
|
|
4401
|
+
});
|
|
4402
|
+
var ExecuteReplayInputSchema = z.object({
|
|
4403
|
+
/** Test script details from qa_test_script_get. */
|
|
4404
|
+
testScript: TestScriptDetailsSchema.describe("Test script details obtained from qa_test_script_get"),
|
|
4405
|
+
/** Local URL to test against. */
|
|
4406
|
+
localUrl: z.string().url().describe("Local URL to test against (e.g., http://localhost:3000)"),
|
|
4407
|
+
/** Explicit approval to launch electron-app. */
|
|
4408
|
+
approveElectronAppLaunch: z.boolean().describe("Set to true after the user explicitly approves launching electron-app"),
|
|
4409
|
+
/** Optional timeout. */
|
|
4410
|
+
timeoutMs: z.number().int().positive().optional().describe("Timeout in milliseconds (default: 180000 = 3 min)")
|
|
4411
|
+
});
|
|
4412
|
+
var CancelExecutionInputSchema = z.object({
|
|
4413
|
+
runId: z.string().min(1).describe("Run ID to cancel")
|
|
4414
|
+
});
|
|
4415
|
+
var RunResultListInputSchema = z.object({
|
|
4416
|
+
cloudTestCaseId: z.string().optional().describe("Optional cloud test case ID to filter by"),
|
|
4417
|
+
limit: z.number().int().positive().optional().describe("Maximum results to return (default: 20)")
|
|
4418
|
+
});
|
|
4419
|
+
var RunResultGetInputSchema = z.object({
|
|
4420
|
+
runId: z.string().min(1).describe("Run result ID to retrieve")
|
|
4421
|
+
});
|
|
4422
|
+
var TestScriptListInputSchema2 = z.object({
|
|
4423
|
+
cloudTestCaseId: z.string().optional().describe("Optional cloud test case ID to filter by")
|
|
4424
|
+
});
|
|
4425
|
+
var TestScriptGetInputSchema2 = z.object({
|
|
4426
|
+
testScriptId: z.string().min(1).describe("Test script ID to retrieve")
|
|
4427
|
+
});
|
|
4428
|
+
var PublishTestScriptInputSchema = z.object({
|
|
4429
|
+
runId: z.string().min(1).describe("Local run result ID from muggle_execute_test_generation"),
|
|
4430
|
+
cloudTestCaseId: z.string().min(1).describe("Cloud test case ID to publish the script under")
|
|
4431
|
+
});
|
|
4432
|
+
var ListSessionsInputSchema = z.object({
|
|
4433
|
+
limit: z.number().int().positive().optional().describe("Maximum number of sessions to return. Defaults to 10.")
|
|
4434
|
+
});
|
|
4435
|
+
var CleanupSessionsInputSchema = z.object({
|
|
4436
|
+
max_age_days: z.number().int().min(0).optional().describe("Maximum age of sessions to keep (in days). Sessions older than this will be deleted. Defaults to 30.")
|
|
4437
|
+
});
|
|
4438
|
+
|
|
4439
|
+
// src/local-qa/tools/tool-registry.ts
|
|
4440
|
+
function createChildLogger2(correlationId) {
|
|
4441
|
+
const logger5 = getLogger();
|
|
4442
|
+
return {
|
|
4443
|
+
info: (msg, meta) => logger5.info(msg, { ...meta, correlationId }),
|
|
4444
|
+
error: (msg, meta) => logger5.error(msg, { ...meta, correlationId }),
|
|
4445
|
+
warn: (msg, meta) => logger5.warn(msg, { ...meta, correlationId }),
|
|
4446
|
+
debug: (msg, meta) => logger5.debug(msg, { ...meta, correlationId })
|
|
4447
|
+
};
|
|
4448
|
+
}
|
|
4449
|
+
var checkStatusTool = {
|
|
4450
|
+
name: "muggle_check_status",
|
|
4451
|
+
description: "Check the status of Muggle Test Local. This verifies the connection to web-service and shows current session information.",
|
|
4452
|
+
inputSchema: EmptyInputSchema2,
|
|
4453
|
+
execute: async (ctx) => {
|
|
4454
|
+
const logger5 = createChildLogger2(ctx.correlationId);
|
|
4455
|
+
logger5.info("Executing muggle_check_status");
|
|
4456
|
+
const authService = getAuthService();
|
|
4457
|
+
const storageService = getStorageService();
|
|
4458
|
+
const authStatus = authService.getAuthStatus();
|
|
4459
|
+
const content = [
|
|
4460
|
+
"## Muggle Test Local Status",
|
|
4461
|
+
"",
|
|
4462
|
+
`**Data Directory:** ${storageService.getDataDir()}`,
|
|
4463
|
+
`**Sessions Directory:** ${storageService.getSessionsDir()}`,
|
|
4464
|
+
"",
|
|
4465
|
+
"### Authentication",
|
|
4466
|
+
`**Authenticated:** ${authStatus.authenticated ? "Yes" : "No"}`,
|
|
4467
|
+
authStatus.email ? `**Email:** ${authStatus.email}` : "",
|
|
4468
|
+
authStatus.expiresAt ? `**Expires:** ${authStatus.expiresAt}` : ""
|
|
4469
|
+
].filter(Boolean).join("\n");
|
|
4470
|
+
return { content, isError: false };
|
|
4471
|
+
}
|
|
4472
|
+
};
|
|
4473
|
+
var listSessionsTool = {
|
|
4474
|
+
name: "muggle_list_sessions",
|
|
4475
|
+
description: "List all stored testing sessions. Shows session IDs, status, and metadata for each session.",
|
|
4476
|
+
inputSchema: ListSessionsInputSchema,
|
|
4477
|
+
execute: async (ctx) => {
|
|
4478
|
+
const logger5 = createChildLogger2(ctx.correlationId);
|
|
4479
|
+
logger5.info("Executing muggle_list_sessions");
|
|
4480
|
+
const input = ListSessionsInputSchema.parse(ctx.input);
|
|
4481
|
+
const storageService = getStorageService();
|
|
4482
|
+
const sessions = storageService.listSessionsWithMetadata();
|
|
4483
|
+
const limit = input.limit ?? 10;
|
|
4484
|
+
const limited = sessions.slice(0, limit);
|
|
4485
|
+
if (limited.length === 0) {
|
|
4486
|
+
return { content: "No sessions found.", isError: false, data: { sessions: [] } };
|
|
4487
|
+
}
|
|
4488
|
+
const lines = limited.map((s) => {
|
|
4489
|
+
return `- **${s.sessionId}** - ${s.status} - ${s.targetUrl} (${s.stepsCount ?? 0} steps)`;
|
|
4490
|
+
});
|
|
4491
|
+
const content = [
|
|
4492
|
+
"## Sessions",
|
|
4493
|
+
"",
|
|
4494
|
+
...lines,
|
|
4495
|
+
"",
|
|
4496
|
+
sessions.length > limit ? `Showing ${limit} of ${sessions.length} sessions.` : ""
|
|
4497
|
+
].filter(Boolean).join("\n");
|
|
4498
|
+
return { content, isError: false, data: { sessions: limited } };
|
|
4499
|
+
}
|
|
4500
|
+
};
|
|
4501
|
+
var runResultListTool = {
|
|
4502
|
+
name: "muggle_run_result_list",
|
|
4503
|
+
description: "List run results (test generation and replay history), optionally filtered by cloud test case ID.",
|
|
4504
|
+
inputSchema: RunResultListInputSchema,
|
|
4505
|
+
execute: async (ctx) => {
|
|
4506
|
+
const logger5 = createChildLogger2(ctx.correlationId);
|
|
4507
|
+
logger5.info("Executing muggle_run_result_list");
|
|
4508
|
+
const input = RunResultListInputSchema.parse(ctx.input);
|
|
4509
|
+
const storage = getRunResultStorageService();
|
|
4510
|
+
let results = storage.listRunResults();
|
|
4511
|
+
if (input.cloudTestCaseId) {
|
|
4512
|
+
results = results.filter((r) => r.cloudTestCaseId === input.cloudTestCaseId);
|
|
4513
|
+
}
|
|
4514
|
+
const limit = input.limit ?? 20;
|
|
4515
|
+
results = results.slice(0, limit);
|
|
4516
|
+
if (results.length === 0) {
|
|
4517
|
+
return { content: "No run results found.", isError: false, data: { results: [] } };
|
|
4518
|
+
}
|
|
4519
|
+
const lines = results.map((r) => {
|
|
4520
|
+
return `- **${r.id}** - ${r.runType} - ${r.status} (${r.executionTimeMs ?? 0}ms)`;
|
|
4521
|
+
});
|
|
4522
|
+
const content = ["## Run Results", "", ...lines].join("\n");
|
|
4523
|
+
return { content, isError: false, data: { results } };
|
|
4524
|
+
}
|
|
4525
|
+
};
|
|
4526
|
+
var runResultGetTool = {
|
|
4527
|
+
name: "muggle_run_result_get",
|
|
4528
|
+
description: "Get detailed information about a run result including screenshots and action script output.",
|
|
4529
|
+
inputSchema: RunResultGetInputSchema,
|
|
4530
|
+
execute: async (ctx) => {
|
|
4531
|
+
const logger5 = createChildLogger2(ctx.correlationId);
|
|
4532
|
+
logger5.info("Executing muggle_run_result_get");
|
|
4533
|
+
const input = RunResultGetInputSchema.parse(ctx.input);
|
|
4534
|
+
const storage = getRunResultStorageService();
|
|
4535
|
+
const result = storage.getRunResult(input.runId);
|
|
4536
|
+
if (!result) {
|
|
4537
|
+
return { content: `Run result not found: ${input.runId}`, isError: true };
|
|
4538
|
+
}
|
|
4539
|
+
const content = [
|
|
4540
|
+
"## Run Result Details",
|
|
4541
|
+
"",
|
|
4542
|
+
`**ID:** ${result.id}`,
|
|
4543
|
+
`**Type:** ${result.runType}`,
|
|
4544
|
+
`**Status:** ${result.status}`,
|
|
4545
|
+
`**Cloud Test Case:** ${result.cloudTestCaseId}`,
|
|
4546
|
+
`**Duration:** ${result.executionTimeMs ?? 0}ms`,
|
|
4547
|
+
result.errorMessage ? `**Error:** ${result.errorMessage}` : ""
|
|
4548
|
+
].filter(Boolean).join("\n");
|
|
4549
|
+
return { content, isError: false, data: result };
|
|
4550
|
+
}
|
|
4551
|
+
};
|
|
4552
|
+
var testScriptListTool = {
|
|
4553
|
+
name: "muggle_test_script_list",
|
|
4554
|
+
description: "List locally generated test scripts, optionally filtered by cloud test case ID.",
|
|
4555
|
+
inputSchema: TestScriptListInputSchema2,
|
|
4556
|
+
execute: async (ctx) => {
|
|
4557
|
+
const logger5 = createChildLogger2(ctx.correlationId);
|
|
4558
|
+
logger5.info("Executing muggle_test_script_list");
|
|
4559
|
+
const input = TestScriptListInputSchema2.parse(ctx.input);
|
|
4560
|
+
const storage = getRunResultStorageService();
|
|
4561
|
+
let scripts = storage.listTestScripts();
|
|
4562
|
+
if (input.cloudTestCaseId) {
|
|
4563
|
+
scripts = scripts.filter((s) => s.cloudTestCaseId === input.cloudTestCaseId);
|
|
4564
|
+
}
|
|
4565
|
+
if (scripts.length === 0) {
|
|
4566
|
+
return { content: "No test scripts found.", isError: false, data: { testScripts: [] } };
|
|
4567
|
+
}
|
|
4568
|
+
const lines = scripts.map((ts) => `- **${ts.name}** (${ts.id}) - ${ts.status}`);
|
|
4569
|
+
const content = ["## Test Scripts", "", ...lines].join("\n");
|
|
4570
|
+
return { content, isError: false, data: { testScripts: scripts } };
|
|
4571
|
+
}
|
|
4572
|
+
};
|
|
4573
|
+
var testScriptGetTool = {
|
|
4574
|
+
name: "muggle_test_script_get",
|
|
4575
|
+
description: "Get details of a locally generated test script including action script steps.",
|
|
4576
|
+
inputSchema: TestScriptGetInputSchema2,
|
|
4577
|
+
execute: async (ctx) => {
|
|
4578
|
+
const logger5 = createChildLogger2(ctx.correlationId);
|
|
4579
|
+
logger5.info("Executing muggle_test_script_get");
|
|
4580
|
+
const input = TestScriptGetInputSchema2.parse(ctx.input);
|
|
4581
|
+
const storage = getRunResultStorageService();
|
|
4582
|
+
const testScript = storage.getTestScript(input.testScriptId);
|
|
4583
|
+
if (!testScript) {
|
|
4584
|
+
return { content: `Test script not found: ${input.testScriptId}`, isError: true };
|
|
4585
|
+
}
|
|
4586
|
+
const content = [
|
|
4587
|
+
"## Test Script Details",
|
|
4588
|
+
"",
|
|
4589
|
+
`**ID:** ${testScript.id}`,
|
|
4590
|
+
`**Name:** ${testScript.name}`,
|
|
4591
|
+
`**URL:** ${testScript.url}`,
|
|
4592
|
+
`**Status:** ${testScript.status}`,
|
|
4593
|
+
testScript.goal ? `**Goal:** ${testScript.goal}` : "",
|
|
4594
|
+
testScript.actionScript ? `**Steps:** ${testScript.actionScript.length}` : ""
|
|
4595
|
+
].filter(Boolean).join("\n");
|
|
4596
|
+
return { content, isError: false, data: testScript };
|
|
4597
|
+
}
|
|
4598
|
+
};
|
|
4599
|
+
var executeTestGenerationTool = {
|
|
4600
|
+
name: "muggle_execute_test_generation",
|
|
4601
|
+
description: "Execute test script generation for a test case. First call qa_test_case_get to get test case details, then pass them here along with the localhost URL. Requires explicit approval before launching electron-app in explore mode.",
|
|
4602
|
+
inputSchema: ExecuteTestGenerationInputSchema,
|
|
4603
|
+
execute: async (ctx) => {
|
|
4604
|
+
const logger5 = createChildLogger2(ctx.correlationId);
|
|
4605
|
+
logger5.info("Executing muggle_execute_test_generation");
|
|
4606
|
+
const input = ExecuteTestGenerationInputSchema.parse(ctx.input);
|
|
4607
|
+
if (!input.approveElectronAppLaunch) {
|
|
4608
|
+
return {
|
|
4609
|
+
content: [
|
|
4610
|
+
"## Electron App Launch Required",
|
|
4611
|
+
"",
|
|
4612
|
+
"This tool will launch the electron-app to generate a test script.",
|
|
4613
|
+
"Please set `approveElectronAppLaunch: true` to proceed.",
|
|
4614
|
+
"",
|
|
4615
|
+
`**Test Case:** ${input.testCase.title}`,
|
|
4616
|
+
`**Local URL:** ${input.localUrl}`,
|
|
4617
|
+
"",
|
|
4618
|
+
"**Note:** The electron-app will open a browser window and navigate to your test URL."
|
|
4619
|
+
].join("\n"),
|
|
4620
|
+
isError: false,
|
|
4621
|
+
data: { requiresApproval: true }
|
|
4622
|
+
};
|
|
4623
|
+
}
|
|
4624
|
+
try {
|
|
4625
|
+
const result = await executeTestGeneration({
|
|
4626
|
+
testCase: input.testCase,
|
|
4627
|
+
localUrl: input.localUrl,
|
|
4628
|
+
timeoutMs: input.timeoutMs
|
|
4629
|
+
});
|
|
4630
|
+
const content = [
|
|
4631
|
+
"## Test Generation " + (result.status === "passed" ? "Successful" : "Failed"),
|
|
4632
|
+
"",
|
|
4633
|
+
`**Run ID:** ${result.id}`,
|
|
4634
|
+
`**Test Script ID:** ${result.testScriptId}`,
|
|
4635
|
+
`**Status:** ${result.status}`,
|
|
4636
|
+
`**Duration:** ${result.executionTimeMs}ms`,
|
|
4637
|
+
result.errorMessage ? `**Error:** ${result.errorMessage}` : ""
|
|
4638
|
+
].filter(Boolean).join("\n");
|
|
4639
|
+
return {
|
|
4640
|
+
content,
|
|
4641
|
+
isError: result.status !== "passed",
|
|
4642
|
+
data: result
|
|
4643
|
+
};
|
|
4644
|
+
} catch (error) {
|
|
4645
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4646
|
+
logger5.error("Test generation failed", { error: errorMessage });
|
|
4647
|
+
return { content: `Test generation failed: ${errorMessage}`, isError: true };
|
|
4648
|
+
}
|
|
4649
|
+
}
|
|
4650
|
+
};
|
|
4651
|
+
var executeReplayTool = {
|
|
4652
|
+
name: "muggle_execute_replay",
|
|
4653
|
+
description: "Execute test script replay. First call qa_test_script_get to get test script details (including actionScript), then pass them here along with the localhost URL. Requires explicit approval before launching electron-app in engine mode.",
|
|
4654
|
+
inputSchema: ExecuteReplayInputSchema,
|
|
4655
|
+
execute: async (ctx) => {
|
|
4656
|
+
const logger5 = createChildLogger2(ctx.correlationId);
|
|
4657
|
+
logger5.info("Executing muggle_execute_replay");
|
|
4658
|
+
const input = ExecuteReplayInputSchema.parse(ctx.input);
|
|
4659
|
+
if (!input.approveElectronAppLaunch) {
|
|
4660
|
+
return {
|
|
4661
|
+
content: [
|
|
4662
|
+
"## Electron App Launch Required",
|
|
4663
|
+
"",
|
|
4664
|
+
"This tool will launch the electron-app to replay a test script.",
|
|
4665
|
+
"Please set `approveElectronAppLaunch: true` to proceed.",
|
|
4666
|
+
"",
|
|
4667
|
+
`**Test Script:** ${input.testScript.name}`,
|
|
4668
|
+
`**Local URL:** ${input.localUrl}`,
|
|
4669
|
+
`**Steps:** ${input.testScript.actionScript.length}`,
|
|
4670
|
+
"",
|
|
4671
|
+
"**Note:** The electron-app will open a browser window and execute the test steps."
|
|
4672
|
+
].join("\n"),
|
|
4673
|
+
isError: false,
|
|
4674
|
+
data: { requiresApproval: true }
|
|
4675
|
+
};
|
|
4676
|
+
}
|
|
4677
|
+
try {
|
|
4678
|
+
const result = await executeReplay({
|
|
4679
|
+
testScript: input.testScript,
|
|
4680
|
+
localUrl: input.localUrl,
|
|
4681
|
+
timeoutMs: input.timeoutMs
|
|
4682
|
+
});
|
|
4683
|
+
const content = [
|
|
4684
|
+
"## Test Replay " + (result.status === "passed" ? "Successful" : "Failed"),
|
|
4685
|
+
"",
|
|
4686
|
+
`**Run ID:** ${result.id}`,
|
|
4687
|
+
`**Test Script ID:** ${result.testScriptId}`,
|
|
4688
|
+
`**Status:** ${result.status}`,
|
|
4689
|
+
`**Duration:** ${result.executionTimeMs}ms`,
|
|
4690
|
+
result.errorMessage ? `**Error:** ${result.errorMessage}` : ""
|
|
4691
|
+
].filter(Boolean).join("\n");
|
|
4692
|
+
return {
|
|
4693
|
+
content,
|
|
4694
|
+
isError: result.status !== "passed",
|
|
4695
|
+
data: result
|
|
4696
|
+
};
|
|
4697
|
+
} catch (error) {
|
|
4698
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4699
|
+
logger5.error("Test replay failed", { error: errorMessage });
|
|
4700
|
+
return { content: `Test replay failed: ${errorMessage}`, isError: true };
|
|
4701
|
+
}
|
|
4702
|
+
}
|
|
4703
|
+
};
|
|
4704
|
+
var cancelExecutionTool = {
|
|
4705
|
+
name: "muggle_cancel_execution",
|
|
4706
|
+
description: "Cancel an active test generation or replay execution.",
|
|
4707
|
+
inputSchema: CancelExecutionInputSchema,
|
|
4708
|
+
execute: async (ctx) => {
|
|
4709
|
+
const logger5 = createChildLogger2(ctx.correlationId);
|
|
4710
|
+
logger5.info("Executing muggle_cancel_execution");
|
|
4711
|
+
const input = CancelExecutionInputSchema.parse(ctx.input);
|
|
4712
|
+
const cancelled = cancelExecution({ runId: input.runId });
|
|
4713
|
+
if (cancelled) {
|
|
4714
|
+
return { content: `Execution cancelled: ${input.runId}`, isError: false };
|
|
4715
|
+
}
|
|
4716
|
+
return { content: `No active execution found with ID: ${input.runId}`, isError: true };
|
|
4717
|
+
}
|
|
4718
|
+
};
|
|
4719
|
+
var publishTestScriptTool = {
|
|
4720
|
+
name: "muggle_publish_test_script",
|
|
4721
|
+
description: "Publish a locally generated test script to the cloud. Uses the run ID from muggle_execute_test_generation to find the script and uploads it to the specified cloud test case.",
|
|
4722
|
+
inputSchema: PublishTestScriptInputSchema,
|
|
4723
|
+
execute: async (ctx) => {
|
|
4724
|
+
const logger5 = createChildLogger2(ctx.correlationId);
|
|
4725
|
+
logger5.info("Executing muggle_publish_test_script");
|
|
4726
|
+
const input = PublishTestScriptInputSchema.parse(ctx.input);
|
|
4727
|
+
const storage = getRunResultStorageService();
|
|
4728
|
+
const runResult = storage.getRunResult(input.runId);
|
|
4729
|
+
if (!runResult) {
|
|
4730
|
+
return { content: `Run result not found: ${input.runId}`, isError: true };
|
|
4731
|
+
}
|
|
4732
|
+
if (!runResult.testScriptId) {
|
|
4733
|
+
return { content: `Run result ${input.runId} does not have an associated test script`, isError: true };
|
|
4734
|
+
}
|
|
4735
|
+
const testScript = storage.getTestScript(runResult.testScriptId);
|
|
4736
|
+
if (!testScript) {
|
|
4737
|
+
return { content: `Test script not found: ${runResult.testScriptId}`, isError: true };
|
|
4738
|
+
}
|
|
4739
|
+
return {
|
|
4740
|
+
content: [
|
|
4741
|
+
"## Test Script Publishing",
|
|
4742
|
+
"",
|
|
4743
|
+
"Publishing test scripts to cloud is not yet implemented.",
|
|
4744
|
+
"",
|
|
4745
|
+
`**Run ID:** ${input.runId}`,
|
|
4746
|
+
`**Test Script ID:** ${runResult.testScriptId}`,
|
|
4747
|
+
`**Target Cloud Test Case:** ${input.cloudTestCaseId}`
|
|
4748
|
+
].join("\n"),
|
|
4749
|
+
isError: true
|
|
4750
|
+
};
|
|
4751
|
+
}
|
|
4752
|
+
};
|
|
4753
|
+
var allLocalQaTools = [
|
|
4754
|
+
// Status tools
|
|
4755
|
+
checkStatusTool,
|
|
4756
|
+
listSessionsTool,
|
|
4757
|
+
// Run result tools
|
|
4758
|
+
runResultListTool,
|
|
4759
|
+
runResultGetTool,
|
|
4760
|
+
// Test script tools (read-only)
|
|
4761
|
+
testScriptListTool,
|
|
4762
|
+
testScriptGetTool,
|
|
4763
|
+
// Execution tools
|
|
4764
|
+
executeTestGenerationTool,
|
|
4765
|
+
executeReplayTool,
|
|
4766
|
+
cancelExecutionTool,
|
|
4767
|
+
// Publishing tools
|
|
4768
|
+
publishTestScriptTool
|
|
4769
|
+
];
|
|
4770
|
+
var toolMap = new Map(
|
|
4771
|
+
allLocalQaTools.map((tool) => [tool.name, tool])
|
|
4772
|
+
);
|
|
4773
|
+
function getTool(name) {
|
|
4774
|
+
return toolMap.get(name);
|
|
4775
|
+
}
|
|
4776
|
+
async function executeTool(name, input, correlationId) {
|
|
4777
|
+
const tool = getTool(name);
|
|
4778
|
+
if (!tool) {
|
|
4779
|
+
return {
|
|
4780
|
+
content: `Unknown tool: ${name}. Available tools: ${allLocalQaTools.map((t) => t.name).join(", ")}`,
|
|
4781
|
+
isError: true
|
|
4782
|
+
};
|
|
4783
|
+
}
|
|
4784
|
+
return tool.execute({ input, correlationId });
|
|
4785
|
+
}
|
|
4786
|
+
|
|
4787
|
+
// src/local-qa/index.ts
|
|
4788
|
+
function getLocalQaTools() {
|
|
4789
|
+
return allLocalQaTools.map((tool) => ({
|
|
4790
|
+
name: tool.name,
|
|
4791
|
+
description: tool.description,
|
|
4792
|
+
inputSchema: tool.inputSchema,
|
|
4793
|
+
requiresAuth: !isLocalOnlyTool(tool.name),
|
|
4794
|
+
execute: async (params) => {
|
|
4795
|
+
const result = await tool.execute({
|
|
4796
|
+
input: params.input,
|
|
4797
|
+
correlationId: params.correlationId
|
|
4798
|
+
});
|
|
4799
|
+
return {
|
|
4800
|
+
content: result.content,
|
|
4801
|
+
isError: result.isError,
|
|
4802
|
+
data: result.data
|
|
4803
|
+
};
|
|
4804
|
+
}
|
|
4805
|
+
}));
|
|
4806
|
+
}
|
|
4807
|
+
function isLocalOnlyTool(toolName) {
|
|
4808
|
+
const localOnlyTools = [
|
|
4809
|
+
// Status and session tools (no auth needed)
|
|
4810
|
+
"muggle_check_status",
|
|
4811
|
+
"muggle_list_sessions",
|
|
4812
|
+
// Run result and test script viewing (local storage only)
|
|
4813
|
+
"muggle_run_result_list",
|
|
4814
|
+
"muggle_run_result_get",
|
|
4815
|
+
"muggle_test_script_list",
|
|
4816
|
+
"muggle_test_script_get"
|
|
4817
|
+
];
|
|
4818
|
+
return localOnlyTools.includes(toolName);
|
|
4819
|
+
}
|
|
4820
|
+
|
|
4821
|
+
export { __export, __require, createApiKeyWithToken, createChildLogger, createUnifiedMcpServer, deleteCredentials, getAuthStatus, getBundledElectronAppVersion, getCallerCredentials, getConfig, getCredentialsFilePath, getDataDir, getDownloadBaseUrl, getElectronAppChecksums, getElectronAppDir, getElectronAppVersion, getElectronAppVersionSource, getLocalQaTools, getLogger, getQaTools, getValidCredentials, isCredentialsExpired, isElectronAppInstalled, loadCredentials, local_qa_exports, openBrowserUrl, performLogin, performLogout, pollDeviceCode, qa_exports, registerTools, resetConfig, resetLogger, saveCredentials, server_exports, startDeviceCodeFlow, startStdioServer, toolRequiresAuth };
|
|
4822
|
+
//# sourceMappingURL=chunk-RXCZWOOD.js.map
|
|
4823
|
+
//# sourceMappingURL=chunk-RXCZWOOD.js.map
|