@deriv-com/fe-mcp-servers 0.0.11 → 0.0.13
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/maestro-ai/README.md +450 -1
- package/dist/maestro-ai/mcp-server.js +1991 -124
- package/package.json +1 -1
|
@@ -18492,7 +18492,17 @@ var StdioServerTransport = class {
|
|
|
18492
18492
|
};
|
|
18493
18493
|
|
|
18494
18494
|
// maestro-ai/src/mcp.js
|
|
18495
|
-
import { execSync } from "child_process";
|
|
18495
|
+
import { execSync, spawn } from "child_process";
|
|
18496
|
+
import {
|
|
18497
|
+
writeFileSync,
|
|
18498
|
+
existsSync,
|
|
18499
|
+
mkdirSync,
|
|
18500
|
+
readFileSync,
|
|
18501
|
+
openSync,
|
|
18502
|
+
closeSync,
|
|
18503
|
+
fsyncSync
|
|
18504
|
+
} from "fs";
|
|
18505
|
+
import { join, dirname } from "path";
|
|
18496
18506
|
var FLOW_TYPES = {
|
|
18497
18507
|
auth: {
|
|
18498
18508
|
description: "Login, signup, or password reset flows",
|
|
@@ -18520,6 +18530,156 @@ var FLOW_TYPES = {
|
|
|
18520
18530
|
requiresOnboarding: false
|
|
18521
18531
|
}
|
|
18522
18532
|
};
|
|
18533
|
+
function isMaestroInstalled() {
|
|
18534
|
+
try {
|
|
18535
|
+
execSync("which maestro", { encoding: "utf-8", stdio: "pipe" });
|
|
18536
|
+
return true;
|
|
18537
|
+
} catch {
|
|
18538
|
+
return false;
|
|
18539
|
+
}
|
|
18540
|
+
}
|
|
18541
|
+
function installMaestro() {
|
|
18542
|
+
const installCmd = 'curl -fsSL "https://get.maestro.mobile.dev" | bash';
|
|
18543
|
+
try {
|
|
18544
|
+
try {
|
|
18545
|
+
execSync(installCmd, {
|
|
18546
|
+
encoding: "utf-8",
|
|
18547
|
+
stdio: "pipe",
|
|
18548
|
+
shell: "/bin/bash",
|
|
18549
|
+
timeout: 12e4
|
|
18550
|
+
// 2 minute timeout for install
|
|
18551
|
+
});
|
|
18552
|
+
} catch (curlError) {
|
|
18553
|
+
const curlOutput = curlError.stderr || curlError.stdout || curlError.message;
|
|
18554
|
+
throw new Error(`Installation failed: ${curlOutput}`);
|
|
18555
|
+
}
|
|
18556
|
+
if (isMaestroInstalled()) {
|
|
18557
|
+
return {
|
|
18558
|
+
success: true,
|
|
18559
|
+
message: "\u2705 Maestro CLI installed successfully!",
|
|
18560
|
+
method: "curl"
|
|
18561
|
+
};
|
|
18562
|
+
}
|
|
18563
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
18564
|
+
const maestroBin = join(homeDir, ".maestro", "bin");
|
|
18565
|
+
process.env.PATH = `${maestroBin}:${process.env.PATH}`;
|
|
18566
|
+
try {
|
|
18567
|
+
execSync(`${join(maestroBin, "maestro")} --version`, {
|
|
18568
|
+
encoding: "utf-8",
|
|
18569
|
+
stdio: "pipe"
|
|
18570
|
+
});
|
|
18571
|
+
return {
|
|
18572
|
+
success: true,
|
|
18573
|
+
message: `\u2705 Maestro CLI installed successfully! Added ${maestroBin} to PATH.`,
|
|
18574
|
+
method: "curl"
|
|
18575
|
+
};
|
|
18576
|
+
} catch {
|
|
18577
|
+
return {
|
|
18578
|
+
success: false,
|
|
18579
|
+
message: `\u26A0\uFE0F Maestro installed but not in PATH. Add ${maestroBin} to your PATH and restart your terminal.`,
|
|
18580
|
+
method: "curl"
|
|
18581
|
+
};
|
|
18582
|
+
}
|
|
18583
|
+
} catch (error2) {
|
|
18584
|
+
return {
|
|
18585
|
+
success: false,
|
|
18586
|
+
message: `\u274C Failed to install Maestro: ${error2.message}
|
|
18587
|
+
|
|
18588
|
+
Manual install:
|
|
18589
|
+
${installCmd}`,
|
|
18590
|
+
method: "curl"
|
|
18591
|
+
};
|
|
18592
|
+
}
|
|
18593
|
+
}
|
|
18594
|
+
function ensureMaestroAvailable() {
|
|
18595
|
+
if (isMaestroInstalled()) {
|
|
18596
|
+
return {
|
|
18597
|
+
available: true,
|
|
18598
|
+
message: "Maestro CLI is available",
|
|
18599
|
+
installed: false
|
|
18600
|
+
};
|
|
18601
|
+
}
|
|
18602
|
+
const installResult = installMaestro();
|
|
18603
|
+
return {
|
|
18604
|
+
available: installResult.success,
|
|
18605
|
+
message: installResult.message,
|
|
18606
|
+
installed: installResult.success
|
|
18607
|
+
};
|
|
18608
|
+
}
|
|
18609
|
+
function getMaestroVersion() {
|
|
18610
|
+
try {
|
|
18611
|
+
const version2 = execSync("maestro --version", {
|
|
18612
|
+
encoding: "utf-8",
|
|
18613
|
+
stdio: "pipe"
|
|
18614
|
+
}).trim();
|
|
18615
|
+
return version2;
|
|
18616
|
+
} catch {
|
|
18617
|
+
return null;
|
|
18618
|
+
}
|
|
18619
|
+
}
|
|
18620
|
+
function ensureMaestroInstalled(options = {}) {
|
|
18621
|
+
const { autoInstall = true, forceReinstall = false } = options;
|
|
18622
|
+
const result = {
|
|
18623
|
+
installed: false,
|
|
18624
|
+
version: null,
|
|
18625
|
+
wasInstalled: false,
|
|
18626
|
+
message: "",
|
|
18627
|
+
details: {
|
|
18628
|
+
installMethod: null,
|
|
18629
|
+
path: null
|
|
18630
|
+
}
|
|
18631
|
+
};
|
|
18632
|
+
const alreadyInstalled = isMaestroInstalled();
|
|
18633
|
+
const currentVersion = alreadyInstalled ? getMaestroVersion() : null;
|
|
18634
|
+
if (alreadyInstalled && !forceReinstall) {
|
|
18635
|
+
result.installed = true;
|
|
18636
|
+
result.version = currentVersion;
|
|
18637
|
+
result.wasInstalled = false;
|
|
18638
|
+
result.message = `\u2705 Maestro CLI is already installed (${currentVersion || "version unknown"})`;
|
|
18639
|
+
try {
|
|
18640
|
+
const path = execSync("which maestro", {
|
|
18641
|
+
encoding: "utf-8",
|
|
18642
|
+
stdio: "pipe"
|
|
18643
|
+
}).trim();
|
|
18644
|
+
result.details.path = path;
|
|
18645
|
+
} catch {
|
|
18646
|
+
}
|
|
18647
|
+
return result;
|
|
18648
|
+
}
|
|
18649
|
+
if (!autoInstall) {
|
|
18650
|
+
result.installed = false;
|
|
18651
|
+
result.version = null;
|
|
18652
|
+
result.wasInstalled = false;
|
|
18653
|
+
result.message = `\u274C Maestro CLI is not installed. Set autoInstall: true to install automatically.`;
|
|
18654
|
+
result.details.installCommand = 'curl -fsSL "https://get.maestro.mobile.dev" | bash';
|
|
18655
|
+
return result;
|
|
18656
|
+
}
|
|
18657
|
+
const installResult = installMaestro();
|
|
18658
|
+
if (installResult.success) {
|
|
18659
|
+
const newVersion = getMaestroVersion();
|
|
18660
|
+
result.installed = true;
|
|
18661
|
+
result.version = newVersion;
|
|
18662
|
+
result.wasInstalled = true;
|
|
18663
|
+
result.message = installResult.message;
|
|
18664
|
+
result.details.installMethod = installResult.method;
|
|
18665
|
+
try {
|
|
18666
|
+
const path = execSync("which maestro", {
|
|
18667
|
+
encoding: "utf-8",
|
|
18668
|
+
stdio: "pipe"
|
|
18669
|
+
}).trim();
|
|
18670
|
+
result.details.path = path;
|
|
18671
|
+
} catch {
|
|
18672
|
+
}
|
|
18673
|
+
} else {
|
|
18674
|
+
result.installed = false;
|
|
18675
|
+
result.version = null;
|
|
18676
|
+
result.wasInstalled = false;
|
|
18677
|
+
result.message = installResult.message;
|
|
18678
|
+
result.details.installMethod = installResult.method;
|
|
18679
|
+
result.details.installCommand = 'curl -fsSL "https://get.maestro.mobile.dev" | bash';
|
|
18680
|
+
}
|
|
18681
|
+
return result;
|
|
18682
|
+
}
|
|
18523
18683
|
function getMaestroCheatSheet() {
|
|
18524
18684
|
const commands = [
|
|
18525
18685
|
{
|
|
@@ -19289,138 +19449,1382 @@ ${analysis.shouldCreateTest ? `1. Review the changed UI files above
|
|
|
19289
19449
|
}
|
|
19290
19450
|
};
|
|
19291
19451
|
}
|
|
19292
|
-
|
|
19293
|
-
|
|
19294
|
-
|
|
19295
|
-
|
|
19296
|
-
|
|
19297
|
-
|
|
19298
|
-
|
|
19299
|
-
|
|
19300
|
-
|
|
19301
|
-
|
|
19302
|
-
|
|
19452
|
+
function writeTestFile(options = {}) {
|
|
19453
|
+
const {
|
|
19454
|
+
yaml,
|
|
19455
|
+
fileName,
|
|
19456
|
+
directory = "maestro",
|
|
19457
|
+
basePath = process.cwd(),
|
|
19458
|
+
execute = true,
|
|
19459
|
+
deviceId = null,
|
|
19460
|
+
env = {},
|
|
19461
|
+
feature = "",
|
|
19462
|
+
action = "",
|
|
19463
|
+
flowType = null,
|
|
19464
|
+
changedElements = [],
|
|
19465
|
+
existingTests = []
|
|
19466
|
+
} = options;
|
|
19467
|
+
const generation = generateMaestroTest({
|
|
19468
|
+
feature,
|
|
19469
|
+
action,
|
|
19470
|
+
flowType,
|
|
19471
|
+
changedElements,
|
|
19472
|
+
existingTests
|
|
19473
|
+
});
|
|
19474
|
+
if (!yaml || !fileName) {
|
|
19475
|
+
return {
|
|
19476
|
+
success: false,
|
|
19477
|
+
filePath: null,
|
|
19478
|
+
message: "\u274C Missing required parameters: yaml and fileName are required"
|
|
19479
|
+
};
|
|
19480
|
+
}
|
|
19481
|
+
const normalizedFileName = fileName.endsWith(".yaml") ? fileName : `${fileName}.yaml`;
|
|
19482
|
+
const targetDir = join(basePath, directory);
|
|
19483
|
+
const filePath = join(targetDir, normalizedFileName);
|
|
19484
|
+
try {
|
|
19485
|
+
if (!existsSync(targetDir)) {
|
|
19486
|
+
mkdirSync(targetDir, { recursive: true });
|
|
19487
|
+
}
|
|
19488
|
+
writeFileSync(filePath, yaml, "utf-8");
|
|
19489
|
+
const fd = openSync(filePath, "r");
|
|
19490
|
+
fsyncSync(fd);
|
|
19491
|
+
closeSync(fd);
|
|
19492
|
+
const result = {
|
|
19493
|
+
success: true,
|
|
19494
|
+
filePath,
|
|
19495
|
+
message: `\u2705 Test file written successfully to: ${filePath}`,
|
|
19496
|
+
generation
|
|
19497
|
+
// Include test generation guidelines and instructions
|
|
19498
|
+
};
|
|
19499
|
+
if (execute) {
|
|
19500
|
+
const sleep = (ms) => {
|
|
19501
|
+
const end = Date.now() + ms;
|
|
19502
|
+
while (Date.now() < end) {
|
|
19503
|
+
}
|
|
19504
|
+
};
|
|
19505
|
+
sleep(500);
|
|
19506
|
+
const execution = executeTest({
|
|
19507
|
+
flowFile: filePath,
|
|
19508
|
+
deviceId,
|
|
19509
|
+
env
|
|
19510
|
+
});
|
|
19511
|
+
result.execution = execution;
|
|
19512
|
+
result.message += execution.success ? "\n\u2705 Test executed successfully!" : "\n\u274C Test execution failed.";
|
|
19303
19513
|
}
|
|
19514
|
+
return result;
|
|
19515
|
+
} catch (error2) {
|
|
19516
|
+
return {
|
|
19517
|
+
success: false,
|
|
19518
|
+
filePath: null,
|
|
19519
|
+
message: `\u274C Failed to write test file: ${error2.message}`
|
|
19520
|
+
};
|
|
19304
19521
|
}
|
|
19305
|
-
|
|
19306
|
-
|
|
19307
|
-
|
|
19308
|
-
|
|
19309
|
-
|
|
19310
|
-
|
|
19311
|
-
|
|
19312
|
-
|
|
19313
|
-
|
|
19314
|
-
|
|
19522
|
+
}
|
|
19523
|
+
function executeTest(options = {}) {
|
|
19524
|
+
const { flowFile, deviceId = null, env = {}, timeout = 12e4 } = options;
|
|
19525
|
+
if (!flowFile) {
|
|
19526
|
+
return {
|
|
19527
|
+
success: false,
|
|
19528
|
+
output: "\u274C Missing required parameter: flowFile",
|
|
19529
|
+
exitCode: 1,
|
|
19530
|
+
duration: 0
|
|
19531
|
+
};
|
|
19532
|
+
}
|
|
19533
|
+
const maestroCheck = ensureMaestroAvailable();
|
|
19534
|
+
if (!maestroCheck.available) {
|
|
19535
|
+
return {
|
|
19536
|
+
success: false,
|
|
19537
|
+
output: maestroCheck.message,
|
|
19538
|
+
exitCode: 1,
|
|
19539
|
+
duration: 0,
|
|
19540
|
+
maestroInstalled: false
|
|
19541
|
+
};
|
|
19542
|
+
}
|
|
19543
|
+
const args = ["test"];
|
|
19544
|
+
if (deviceId) {
|
|
19545
|
+
args.push("--device", deviceId);
|
|
19546
|
+
}
|
|
19547
|
+
Object.entries(env).forEach(([key, value]) => {
|
|
19548
|
+
args.push("-e", `${key}=${value}`);
|
|
19549
|
+
});
|
|
19550
|
+
args.push(flowFile);
|
|
19551
|
+
const command = `maestro ${args.join(" ")}`;
|
|
19552
|
+
const startTime = Date.now();
|
|
19553
|
+
try {
|
|
19554
|
+
const output = execSync(command, {
|
|
19555
|
+
encoding: "utf-8",
|
|
19556
|
+
timeout,
|
|
19557
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
19558
|
+
env: { ...process.env, ...env }
|
|
19559
|
+
});
|
|
19560
|
+
const duration3 = Date.now() - startTime;
|
|
19561
|
+
return {
|
|
19562
|
+
success: true,
|
|
19563
|
+
output: `${maestroCheck.installed ? "\u{1F4E6} " + maestroCheck.message + "\n\n" : ""}\u2705 Test passed!
|
|
19315
19564
|
|
|
19316
|
-
|
|
19317
|
-
1. Only test CHANGED functionality (from git diff)
|
|
19318
|
-
2. Never test existing/unchanged UI elements
|
|
19319
|
-
3. Use extendedWaitUntil (not fixed timeouts)
|
|
19320
|
-
4. Text selectors first, id fallback
|
|
19321
|
-
5. Reuse existing flows via runFlow
|
|
19565
|
+
Command: ${command}
|
|
19322
19566
|
|
|
19323
|
-
|
|
19324
|
-
|
|
19325
|
-
|
|
19326
|
-
|
|
19327
|
-
|
|
19328
|
-
|
|
19329
|
-
|
|
19330
|
-
|
|
19567
|
+
${output}`,
|
|
19568
|
+
exitCode: 0,
|
|
19569
|
+
duration: duration3,
|
|
19570
|
+
command,
|
|
19571
|
+
maestroInstalled: maestroCheck.installed
|
|
19572
|
+
};
|
|
19573
|
+
} catch (error2) {
|
|
19574
|
+
const duration3 = Date.now() - startTime;
|
|
19575
|
+
const output = error2.stdout || error2.stderr || error2.message;
|
|
19576
|
+
return {
|
|
19577
|
+
success: false,
|
|
19578
|
+
output: `${maestroCheck.installed ? "\u{1F4E6} " + maestroCheck.message + "\n\n" : ""}\u274C Test failed!
|
|
19331
19579
|
|
|
19332
|
-
|
|
19580
|
+
Command: ${command}
|
|
19333
19581
|
|
|
19334
|
-
|
|
19335
|
-
|
|
19336
|
-
|
|
19337
|
-
|
|
19338
|
-
|
|
19339
|
-
|
|
19340
|
-
|
|
19341
|
-
|
|
19342
|
-
|
|
19343
|
-
|
|
19344
|
-
|
|
19345
|
-
|
|
19346
|
-
|
|
19347
|
-
|
|
19348
|
-
|
|
19349
|
-
|
|
19350
|
-
|
|
19351
|
-
|
|
19352
|
-
|
|
19353
|
-
|
|
19354
|
-
|
|
19355
|
-
|
|
19356
|
-
|
|
19357
|
-
|
|
19358
|
-
|
|
19359
|
-
|
|
19360
|
-
|
|
19361
|
-
|
|
19362
|
-
|
|
19363
|
-
|
|
19364
|
-
|
|
19365
|
-
|
|
19366
|
-
|
|
19367
|
-
|
|
19368
|
-
|
|
19369
|
-
|
|
19370
|
-
|
|
19371
|
-
|
|
19372
|
-
|
|
19373
|
-
|
|
19374
|
-
|
|
19375
|
-
|
|
19582
|
+
${output}`,
|
|
19583
|
+
exitCode: error2.status || 1,
|
|
19584
|
+
duration: duration3,
|
|
19585
|
+
command,
|
|
19586
|
+
maestroInstalled: maestroCheck.installed
|
|
19587
|
+
};
|
|
19588
|
+
}
|
|
19589
|
+
}
|
|
19590
|
+
function executeTestsSequentially(options = {}) {
|
|
19591
|
+
const {
|
|
19592
|
+
flowFiles = [],
|
|
19593
|
+
deviceId = null,
|
|
19594
|
+
env = {},
|
|
19595
|
+
stopOnFailure = false
|
|
19596
|
+
} = options;
|
|
19597
|
+
if (!flowFiles || flowFiles.length === 0) {
|
|
19598
|
+
return {
|
|
19599
|
+
success: false,
|
|
19600
|
+
results: [],
|
|
19601
|
+
summary: {
|
|
19602
|
+
total: 0,
|
|
19603
|
+
passed: 0,
|
|
19604
|
+
failed: 0,
|
|
19605
|
+
skipped: 0,
|
|
19606
|
+
totalDuration: 0
|
|
19607
|
+
}
|
|
19608
|
+
};
|
|
19609
|
+
}
|
|
19610
|
+
const results = [];
|
|
19611
|
+
let stopped = false;
|
|
19612
|
+
for (const flowFile of flowFiles) {
|
|
19613
|
+
if (stopped) {
|
|
19614
|
+
results.push({
|
|
19615
|
+
flowFile,
|
|
19616
|
+
success: false,
|
|
19617
|
+
output: "\u23ED\uFE0F Skipped due to previous failure",
|
|
19618
|
+
exitCode: -1,
|
|
19619
|
+
duration: 0,
|
|
19620
|
+
skipped: true
|
|
19621
|
+
});
|
|
19622
|
+
continue;
|
|
19623
|
+
}
|
|
19624
|
+
const result = executeTest({ flowFile, deviceId, env });
|
|
19625
|
+
results.push({
|
|
19626
|
+
flowFile,
|
|
19627
|
+
...result,
|
|
19628
|
+
skipped: false
|
|
19629
|
+
});
|
|
19630
|
+
if (!result.success && stopOnFailure) {
|
|
19631
|
+
stopped = true;
|
|
19632
|
+
}
|
|
19633
|
+
}
|
|
19634
|
+
const passed = results.filter((r) => r.success).length;
|
|
19635
|
+
const failed = results.filter((r) => !r.success && !r.skipped).length;
|
|
19636
|
+
const skipped = results.filter((r) => r.skipped).length;
|
|
19637
|
+
const totalDuration = results.reduce((sum, r) => sum + r.duration, 0);
|
|
19638
|
+
return {
|
|
19639
|
+
success: failed === 0,
|
|
19640
|
+
results,
|
|
19641
|
+
summary: {
|
|
19642
|
+
total: flowFiles.length,
|
|
19643
|
+
passed,
|
|
19644
|
+
failed,
|
|
19645
|
+
skipped,
|
|
19646
|
+
totalDuration,
|
|
19647
|
+
report: `
|
|
19648
|
+
## \u{1F4CA} Test Execution Summary
|
|
19376
19649
|
|
|
19377
|
-
|
|
19378
|
-
|
|
19379
|
-
|
|
19380
|
-
|
|
19381
|
-
|
|
19382
|
-
|
|
19650
|
+
| Metric | Value |
|
|
19651
|
+
|--------|-------|
|
|
19652
|
+
| Total Tests | ${flowFiles.length} |
|
|
19653
|
+
| \u2705 Passed | ${passed} |
|
|
19654
|
+
| \u274C Failed | ${failed} |
|
|
19655
|
+
| \u23ED\uFE0F Skipped | ${skipped} |
|
|
19656
|
+
| \u23F1\uFE0F Duration | ${(totalDuration / 1e3).toFixed(2)}s |
|
|
19383
19657
|
|
|
19384
|
-
|
|
19385
|
-
|
|
19386
|
-
|
|
19387
|
-
|
|
19388
|
-
|
|
19658
|
+
### Results by Flow:
|
|
19659
|
+
${results.map(
|
|
19660
|
+
(r) => `- ${r.success ? "\u2705" : r.skipped ? "\u23ED\uFE0F" : "\u274C"} \`${r.flowFile}\` (${(r.duration / 1e3).toFixed(2)}s)`
|
|
19661
|
+
).join("\n")}
|
|
19662
|
+
`
|
|
19663
|
+
}
|
|
19664
|
+
};
|
|
19665
|
+
}
|
|
19666
|
+
function discoverTestFiles(options = {}) {
|
|
19667
|
+
const { directory = "maestro", basePath = process.cwd() } = options;
|
|
19668
|
+
const targetDir = join(basePath, directory);
|
|
19669
|
+
if (!existsSync(targetDir)) {
|
|
19670
|
+
return {
|
|
19671
|
+
files: [],
|
|
19672
|
+
count: 0,
|
|
19673
|
+
message: `\u274C Directory not found: ${targetDir}`
|
|
19674
|
+
};
|
|
19675
|
+
}
|
|
19676
|
+
try {
|
|
19677
|
+
const result = execSync(`find "${targetDir}" -name "*.yaml" -type f`, {
|
|
19678
|
+
encoding: "utf-8"
|
|
19679
|
+
});
|
|
19680
|
+
const files = result.trim().split("\n").filter(Boolean);
|
|
19681
|
+
return {
|
|
19682
|
+
files,
|
|
19683
|
+
count: files.length,
|
|
19684
|
+
message: `Found ${files.length} test file(s) in ${targetDir}`
|
|
19685
|
+
};
|
|
19686
|
+
} catch (error2) {
|
|
19687
|
+
return {
|
|
19688
|
+
files: [],
|
|
19689
|
+
count: 0,
|
|
19690
|
+
message: `\u274C Error discovering files: ${error2.message}`
|
|
19691
|
+
};
|
|
19692
|
+
}
|
|
19693
|
+
}
|
|
19694
|
+
function runAllTests(options = {}) {
|
|
19695
|
+
const {
|
|
19696
|
+
directory = "maestro",
|
|
19697
|
+
basePath = process.cwd(),
|
|
19698
|
+
deviceId = null,
|
|
19699
|
+
env = {},
|
|
19700
|
+
stopOnFailure = false,
|
|
19701
|
+
files = null
|
|
19702
|
+
} = options;
|
|
19703
|
+
let discovery;
|
|
19704
|
+
let filesToRun;
|
|
19705
|
+
if (files && Array.isArray(files) && files.length > 0) {
|
|
19706
|
+
filesToRun = files.map((f) => {
|
|
19707
|
+
if (f.startsWith("/")) {
|
|
19708
|
+
return f;
|
|
19709
|
+
}
|
|
19710
|
+
return join(basePath, f);
|
|
19711
|
+
});
|
|
19712
|
+
discovery = {
|
|
19713
|
+
files: filesToRun,
|
|
19714
|
+
count: filesToRun.length,
|
|
19715
|
+
message: `Using ${filesToRun.length} specified test file(s)`,
|
|
19716
|
+
mode: "specified"
|
|
19717
|
+
};
|
|
19718
|
+
} else {
|
|
19719
|
+
discovery = discoverTestFiles({ directory, basePath });
|
|
19720
|
+
discovery.mode = "discovered";
|
|
19721
|
+
filesToRun = discovery.files;
|
|
19722
|
+
}
|
|
19723
|
+
if (!filesToRun || filesToRun.length === 0) {
|
|
19724
|
+
return {
|
|
19725
|
+
discovery,
|
|
19726
|
+
execution: {
|
|
19727
|
+
success: false,
|
|
19728
|
+
results: [],
|
|
19729
|
+
summary: {
|
|
19730
|
+
total: 0,
|
|
19731
|
+
passed: 0,
|
|
19732
|
+
failed: 0,
|
|
19733
|
+
skipped: 0,
|
|
19734
|
+
totalDuration: 0,
|
|
19735
|
+
report: "No test files found to execute."
|
|
19389
19736
|
}
|
|
19390
|
-
}
|
|
19391
|
-
|
|
19392
|
-
|
|
19393
|
-
|
|
19394
|
-
|
|
19395
|
-
|
|
19396
|
-
|
|
19397
|
-
|
|
19398
|
-
|
|
19399
|
-
|
|
19400
|
-
|
|
19401
|
-
|
|
19402
|
-
|
|
19403
|
-
|
|
19404
|
-
|
|
19405
|
-
|
|
19406
|
-
|
|
19407
|
-
|
|
19408
|
-
|
|
19409
|
-
|
|
19410
|
-
|
|
19411
|
-
|
|
19412
|
-
|
|
19413
|
-
|
|
19414
|
-
|
|
19415
|
-
|
|
19416
|
-
|
|
19417
|
-
|
|
19418
|
-
|
|
19419
|
-
|
|
19420
|
-
|
|
19421
|
-
|
|
19422
|
-
|
|
19423
|
-
|
|
19737
|
+
}
|
|
19738
|
+
};
|
|
19739
|
+
}
|
|
19740
|
+
const execution = executeTestsSequentially({
|
|
19741
|
+
flowFiles: filesToRun,
|
|
19742
|
+
deviceId,
|
|
19743
|
+
env,
|
|
19744
|
+
stopOnFailure
|
|
19745
|
+
});
|
|
19746
|
+
return {
|
|
19747
|
+
discovery,
|
|
19748
|
+
execution
|
|
19749
|
+
};
|
|
19750
|
+
}
|
|
19751
|
+
function extractTaskIdFromUrl(url2) {
|
|
19752
|
+
if (!url2 || typeof url2 !== "string") {
|
|
19753
|
+
return {
|
|
19754
|
+
success: false,
|
|
19755
|
+
taskId: null,
|
|
19756
|
+
message: "No URL provided"
|
|
19757
|
+
};
|
|
19758
|
+
}
|
|
19759
|
+
const trimmedUrl = url2.trim();
|
|
19760
|
+
const directPattern = /app\.clickup\.com\/t\/([a-zA-Z0-9]+)/;
|
|
19761
|
+
const directMatch = trimmedUrl.match(directPattern);
|
|
19762
|
+
if (directMatch) {
|
|
19763
|
+
return {
|
|
19764
|
+
success: true,
|
|
19765
|
+
taskId: directMatch[1],
|
|
19766
|
+
message: `Extracted task ID: ${directMatch[1]}`
|
|
19767
|
+
};
|
|
19768
|
+
}
|
|
19769
|
+
const queryPattern = /[?&]p=([a-zA-Z0-9]+)/;
|
|
19770
|
+
const queryMatch = trimmedUrl.match(queryPattern);
|
|
19771
|
+
if (queryMatch) {
|
|
19772
|
+
return {
|
|
19773
|
+
success: true,
|
|
19774
|
+
taskId: queryMatch[1],
|
|
19775
|
+
message: `Extracted task ID from query: ${queryMatch[1]}`
|
|
19776
|
+
};
|
|
19777
|
+
}
|
|
19778
|
+
const listViewPattern = /\/li\/\d+\/([a-zA-Z0-9]+)/;
|
|
19779
|
+
const listViewMatch = trimmedUrl.match(listViewPattern);
|
|
19780
|
+
if (listViewMatch) {
|
|
19781
|
+
return {
|
|
19782
|
+
success: true,
|
|
19783
|
+
taskId: listViewMatch[1],
|
|
19784
|
+
message: `Extracted task ID from list view: ${listViewMatch[1]}`
|
|
19785
|
+
};
|
|
19786
|
+
}
|
|
19787
|
+
const taskIdPattern = /^[a-zA-Z0-9]{6,12}$/;
|
|
19788
|
+
if (taskIdPattern.test(trimmedUrl)) {
|
|
19789
|
+
return {
|
|
19790
|
+
success: true,
|
|
19791
|
+
taskId: trimmedUrl,
|
|
19792
|
+
message: `Using provided value as task ID: ${trimmedUrl}`
|
|
19793
|
+
};
|
|
19794
|
+
}
|
|
19795
|
+
return {
|
|
19796
|
+
success: false,
|
|
19797
|
+
taskId: null,
|
|
19798
|
+
message: `Could not extract task ID from URL: ${trimmedUrl}`
|
|
19799
|
+
};
|
|
19800
|
+
}
|
|
19801
|
+
async function fetchClickUpTask(options = {}) {
|
|
19802
|
+
const { taskUrl = null } = options;
|
|
19803
|
+
const apiToken = process.env.CLICKUP_API_TOKEN;
|
|
19804
|
+
if (!taskUrl) {
|
|
19805
|
+
return {
|
|
19806
|
+
success: false,
|
|
19807
|
+
skipped: false,
|
|
19808
|
+
needsInput: true,
|
|
19809
|
+
prompt: "Do you have a ClickUp task URL for this feature? Paste the URL to generate tests based on acceptance criteria, or type 'skip' to continue with git-based analysis only.",
|
|
19810
|
+
message: "No ClickUp task URL provided. Provide a URL or skip to continue."
|
|
19811
|
+
};
|
|
19812
|
+
}
|
|
19813
|
+
const skipCommands = ["skip", "no", "none", "n", ""];
|
|
19814
|
+
if (skipCommands.includes(taskUrl.toLowerCase().trim())) {
|
|
19815
|
+
return {
|
|
19816
|
+
success: true,
|
|
19817
|
+
skipped: true,
|
|
19818
|
+
needsInput: false,
|
|
19819
|
+
message: "Skipping ClickUp integration. Continuing with git-based analysis.",
|
|
19820
|
+
task: null
|
|
19821
|
+
};
|
|
19822
|
+
}
|
|
19823
|
+
const extraction = extractTaskIdFromUrl(taskUrl);
|
|
19824
|
+
if (!extraction.success) {
|
|
19825
|
+
return {
|
|
19826
|
+
success: false,
|
|
19827
|
+
skipped: false,
|
|
19828
|
+
needsInput: false,
|
|
19829
|
+
message: extraction.message,
|
|
19830
|
+
error: "Invalid ClickUp URL format"
|
|
19831
|
+
};
|
|
19832
|
+
}
|
|
19833
|
+
const taskId = extraction.taskId;
|
|
19834
|
+
if (!apiToken) {
|
|
19835
|
+
return {
|
|
19836
|
+
success: false,
|
|
19837
|
+
skipped: false,
|
|
19838
|
+
needsInput: false,
|
|
19839
|
+
message: "\u274C CLICKUP_API_TOKEN environment variable is not set. Please configure it in your MCP settings.",
|
|
19840
|
+
error: "Missing API token",
|
|
19841
|
+
taskId
|
|
19842
|
+
};
|
|
19843
|
+
}
|
|
19844
|
+
try {
|
|
19845
|
+
const response = await fetch(
|
|
19846
|
+
`https://api.clickup.com/api/v2/task/${taskId}`,
|
|
19847
|
+
{
|
|
19848
|
+
method: "GET",
|
|
19849
|
+
headers: {
|
|
19850
|
+
Authorization: apiToken,
|
|
19851
|
+
"Content-Type": "application/json"
|
|
19852
|
+
}
|
|
19853
|
+
}
|
|
19854
|
+
);
|
|
19855
|
+
if (!response.ok) {
|
|
19856
|
+
const errorText = await response.text();
|
|
19857
|
+
return {
|
|
19858
|
+
success: false,
|
|
19859
|
+
skipped: false,
|
|
19860
|
+
needsInput: false,
|
|
19861
|
+
message: `\u274C ClickUp API error (${response.status}): ${errorText}`,
|
|
19862
|
+
error: `API returned ${response.status}`,
|
|
19863
|
+
taskId
|
|
19864
|
+
};
|
|
19865
|
+
}
|
|
19866
|
+
const task = await response.json();
|
|
19867
|
+
return {
|
|
19868
|
+
success: true,
|
|
19869
|
+
skipped: false,
|
|
19870
|
+
needsInput: false,
|
|
19871
|
+
message: `\u2705 Successfully fetched task: ${task.name}`,
|
|
19872
|
+
task,
|
|
19873
|
+
taskId,
|
|
19874
|
+
taskName: task.name,
|
|
19875
|
+
taskStatus: task.status?.status,
|
|
19876
|
+
taskUrl: task.url
|
|
19877
|
+
};
|
|
19878
|
+
} catch (error2) {
|
|
19879
|
+
return {
|
|
19880
|
+
success: false,
|
|
19881
|
+
skipped: false,
|
|
19882
|
+
needsInput: false,
|
|
19883
|
+
message: `\u274C Failed to fetch ClickUp task: ${error2.message}`,
|
|
19884
|
+
error: error2.message,
|
|
19885
|
+
taskId
|
|
19886
|
+
};
|
|
19887
|
+
}
|
|
19888
|
+
}
|
|
19889
|
+
function parseAcceptanceCriteria(options = {}) {
|
|
19890
|
+
const { task, acSource = null } = options;
|
|
19891
|
+
if (!task) {
|
|
19892
|
+
return {
|
|
19893
|
+
success: false,
|
|
19894
|
+
acItems: [],
|
|
19895
|
+
source: null,
|
|
19896
|
+
message: "No task provided for parsing acceptance criteria"
|
|
19897
|
+
};
|
|
19898
|
+
}
|
|
19899
|
+
const sourceConfig = acSource || process.env.CLICKUP_AC_SOURCE || "checklist:Acceptance Criteria";
|
|
19900
|
+
const acItems = [];
|
|
19901
|
+
const sources = sourceConfig.split(",").map((s) => s.trim());
|
|
19902
|
+
const usedSources = [];
|
|
19903
|
+
for (const source of sources) {
|
|
19904
|
+
const [sourceType, sourceName] = source.includes(":") ? source.split(":", 2) : [source, null];
|
|
19905
|
+
switch (sourceType.toLowerCase()) {
|
|
19906
|
+
case "checklist": {
|
|
19907
|
+
const checklists = task.checklists || [];
|
|
19908
|
+
for (const checklist of checklists) {
|
|
19909
|
+
if (!sourceName || checklist.name.toLowerCase().includes(sourceName.toLowerCase())) {
|
|
19910
|
+
const items = checklist.items || [];
|
|
19911
|
+
for (const item of items) {
|
|
19912
|
+
acItems.push({
|
|
19913
|
+
id: item.id,
|
|
19914
|
+
text: item.name,
|
|
19915
|
+
completed: item.resolved || false,
|
|
19916
|
+
source: `checklist:${checklist.name}`,
|
|
19917
|
+
type: "checklist_item",
|
|
19918
|
+
order: item.orderindex
|
|
19919
|
+
});
|
|
19920
|
+
}
|
|
19921
|
+
usedSources.push(`checklist:${checklist.name}`);
|
|
19922
|
+
}
|
|
19923
|
+
}
|
|
19924
|
+
break;
|
|
19925
|
+
}
|
|
19926
|
+
case "custom_field": {
|
|
19927
|
+
const customFields = task.custom_fields || [];
|
|
19928
|
+
for (const field of customFields) {
|
|
19929
|
+
if (!sourceName || field.name.toLowerCase().includes(sourceName.toLowerCase())) {
|
|
19930
|
+
if (field.value) {
|
|
19931
|
+
if (Array.isArray(field.value)) {
|
|
19932
|
+
field.value.forEach((v, idx) => {
|
|
19933
|
+
acItems.push({
|
|
19934
|
+
id: `${field.id}_${idx}`,
|
|
19935
|
+
text: typeof v === "object" ? v.name || v.value : v,
|
|
19936
|
+
completed: false,
|
|
19937
|
+
source: `custom_field:${field.name}`,
|
|
19938
|
+
type: "custom_field_item",
|
|
19939
|
+
order: idx
|
|
19940
|
+
});
|
|
19941
|
+
});
|
|
19942
|
+
} else if (typeof field.value === "string") {
|
|
19943
|
+
const lines = field.value.split("\n").filter((l) => l.trim());
|
|
19944
|
+
lines.forEach((line, idx) => {
|
|
19945
|
+
const checkboxMatch = line.match(/^[\s]*[-*\[\]xX✓✗]\s*(.*)/);
|
|
19946
|
+
acItems.push({
|
|
19947
|
+
id: `${field.id}_${idx}`,
|
|
19948
|
+
text: checkboxMatch ? checkboxMatch[1].trim() : line.trim(),
|
|
19949
|
+
completed: /^[\s]*[\[x\]✓]/.test(line),
|
|
19950
|
+
source: `custom_field:${field.name}`,
|
|
19951
|
+
type: "custom_field_item",
|
|
19952
|
+
order: idx
|
|
19953
|
+
});
|
|
19954
|
+
});
|
|
19955
|
+
}
|
|
19956
|
+
usedSources.push(`custom_field:${field.name}`);
|
|
19957
|
+
}
|
|
19958
|
+
}
|
|
19959
|
+
}
|
|
19960
|
+
break;
|
|
19961
|
+
}
|
|
19962
|
+
case "description": {
|
|
19963
|
+
let description = task.description || task.text_content || task.content || "";
|
|
19964
|
+
const rawDescription = description;
|
|
19965
|
+
if (description) {
|
|
19966
|
+
description = description.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<\/div>/gi, "\n").replace(/<\/li>/gi, "\n").replace(/<[^>]*>/g, "").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"');
|
|
19967
|
+
description = description.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
19968
|
+
const acSectionPattern = /(?:acceptance\s*criteria|ac|requirements?)[\s:]*\n([\s\S]*?)(?:\n\n|\n#|$)/i;
|
|
19969
|
+
const sectionMatch = description.match(acSectionPattern);
|
|
19970
|
+
const textToParse = sectionMatch ? sectionMatch[1] : description;
|
|
19971
|
+
const lines = textToParse.split("\n").filter((l) => l.trim());
|
|
19972
|
+
lines.forEach((line, idx) => {
|
|
19973
|
+
const itemMatch = line.match(
|
|
19974
|
+
/^[\s]*(?:[-–—*•◦▪▸►‣⁃]|\d+[.)\]:]|\[[\sx✓✗☐☑]?\])\s*(.*)/
|
|
19975
|
+
);
|
|
19976
|
+
if (itemMatch && itemMatch[1] && itemMatch[1].trim().length > 0) {
|
|
19977
|
+
acItems.push({
|
|
19978
|
+
id: `desc_${idx}`,
|
|
19979
|
+
text: itemMatch[1].trim(),
|
|
19980
|
+
completed: /\[[x✓☑]\]/i.test(line),
|
|
19981
|
+
source: "description",
|
|
19982
|
+
type: "description_item",
|
|
19983
|
+
order: idx
|
|
19984
|
+
});
|
|
19985
|
+
}
|
|
19986
|
+
});
|
|
19987
|
+
if (!acItems.some((item) => item.source === "description")) {
|
|
19988
|
+
const headerMatch = description.match(
|
|
19989
|
+
/acceptance\s*criteria\s*:?\s*\n?([\s\S]*)/i
|
|
19990
|
+
);
|
|
19991
|
+
if (headerMatch) {
|
|
19992
|
+
const afterHeader = headerMatch[1].trim();
|
|
19993
|
+
const criteriaLines = afterHeader.split("\n").filter((l) => l.trim());
|
|
19994
|
+
criteriaLines.forEach((line, idx) => {
|
|
19995
|
+
const trimmedLine = line.trim();
|
|
19996
|
+
if (trimmedLine.startsWith("-") || trimmedLine.startsWith("*") || trimmedLine.startsWith("\u2022")) {
|
|
19997
|
+
acItems.push({
|
|
19998
|
+
id: `desc_${idx}`,
|
|
19999
|
+
text: trimmedLine.replace(/^[-*•]\s*/, "").trim(),
|
|
20000
|
+
completed: false,
|
|
20001
|
+
source: "description",
|
|
20002
|
+
type: "description_item",
|
|
20003
|
+
order: idx
|
|
20004
|
+
});
|
|
20005
|
+
} else if (trimmedLine.length > 10 && idx < 10) {
|
|
20006
|
+
acItems.push({
|
|
20007
|
+
id: `desc_fallback_${idx}`,
|
|
20008
|
+
text: trimmedLine,
|
|
20009
|
+
completed: false,
|
|
20010
|
+
source: "description",
|
|
20011
|
+
type: "description_item_fallback",
|
|
20012
|
+
order: idx
|
|
20013
|
+
});
|
|
20014
|
+
}
|
|
20015
|
+
});
|
|
20016
|
+
}
|
|
20017
|
+
}
|
|
20018
|
+
if (acItems.some((item) => item.source === "description")) {
|
|
20019
|
+
usedSources.push("description");
|
|
20020
|
+
}
|
|
20021
|
+
}
|
|
20022
|
+
if (!acItems.some((item) => item.source === "description")) {
|
|
20023
|
+
if (!task._debug) task._debug = {};
|
|
20024
|
+
task._debug.rawDescription = rawDescription?.substring(0, 500);
|
|
20025
|
+
task._debug.processedDescription = description?.substring(0, 500);
|
|
20026
|
+
task._debug.descriptionLength = description?.length || 0;
|
|
20027
|
+
}
|
|
20028
|
+
break;
|
|
20029
|
+
}
|
|
20030
|
+
default:
|
|
20031
|
+
break;
|
|
20032
|
+
}
|
|
20033
|
+
}
|
|
20034
|
+
const result = {
|
|
20035
|
+
success: acItems.length > 0,
|
|
20036
|
+
acItems,
|
|
20037
|
+
sources: usedSources,
|
|
20038
|
+
totalCount: acItems.length,
|
|
20039
|
+
completedCount: acItems.filter((item) => item.completed).length,
|
|
20040
|
+
message: acItems.length > 0 ? `\u2705 Found ${acItems.length} acceptance criteria items from: ${usedSources.join(", ")}` : `\u26A0\uFE0F No acceptance criteria found in sources: ${sourceConfig}`
|
|
20041
|
+
};
|
|
20042
|
+
if (acItems.length === 0 && task._debug) {
|
|
20043
|
+
result.debug = {
|
|
20044
|
+
rawDescriptionPreview: task._debug.rawDescription,
|
|
20045
|
+
processedDescriptionPreview: task._debug.processedDescription,
|
|
20046
|
+
descriptionLength: task._debug.descriptionLength,
|
|
20047
|
+
hint: "Check if the description contains bullet points (-, *, \u2022) or is formatted differently"
|
|
20048
|
+
};
|
|
20049
|
+
}
|
|
20050
|
+
if (acItems.length === 0) {
|
|
20051
|
+
result.taskInfo = {
|
|
20052
|
+
hasDescription: !!(task.description || task.text_content || task.content),
|
|
20053
|
+
hasChecklists: !!(task.checklists && task.checklists.length > 0),
|
|
20054
|
+
checklistNames: task.checklists?.map((c) => c.name) || [],
|
|
20055
|
+
hasCustomFields: !!(task.custom_fields && task.custom_fields.length > 0),
|
|
20056
|
+
customFieldNames: task.custom_fields?.map((f) => f.name) || []
|
|
20057
|
+
};
|
|
20058
|
+
}
|
|
20059
|
+
return result;
|
|
20060
|
+
}
|
|
20061
|
+
function validateAcceptanceCriteria(options = {}) {
|
|
20062
|
+
const { acItems = [] } = options;
|
|
20063
|
+
if (!acItems || acItems.length === 0) {
|
|
20064
|
+
return {
|
|
20065
|
+
success: false,
|
|
20066
|
+
validatedItems: [],
|
|
20067
|
+
testableCount: 0,
|
|
20068
|
+
message: "No acceptance criteria items provided for validation"
|
|
20069
|
+
};
|
|
20070
|
+
}
|
|
20071
|
+
const validatedItems = acItems.map((item) => {
|
|
20072
|
+
const validation = {
|
|
20073
|
+
...item,
|
|
20074
|
+
isTestable: true,
|
|
20075
|
+
testabilityScore: 0,
|
|
20076
|
+
testabilityReasons: [],
|
|
20077
|
+
suggestedFlowType: null,
|
|
20078
|
+
uiKeywords: []
|
|
20079
|
+
};
|
|
20080
|
+
const text = item.text.toLowerCase();
|
|
20081
|
+
const uiKeywords = {
|
|
20082
|
+
button: ["click", "button", "tap", "press"],
|
|
20083
|
+
form: ["input", "enter", "fill", "type", "form", "field"],
|
|
20084
|
+
navigation: ["navigate", "go to", "open", "redirect", "page", "screen"],
|
|
20085
|
+
visibility: ["see", "display", "show", "visible", "appear"],
|
|
20086
|
+
modal: ["modal", "dialog", "popup", "overlay"],
|
|
20087
|
+
validation: ["error", "valid", "invalid", "required", "message"]
|
|
20088
|
+
};
|
|
20089
|
+
for (const [category, keywords] of Object.entries(uiKeywords)) {
|
|
20090
|
+
for (const keyword of keywords) {
|
|
20091
|
+
if (text.includes(keyword)) {
|
|
20092
|
+
validation.uiKeywords.push(keyword);
|
|
20093
|
+
validation.testabilityScore += 10;
|
|
20094
|
+
validation.testabilityReasons.push(
|
|
20095
|
+
`Contains UI keyword: "${keyword}"`
|
|
20096
|
+
);
|
|
20097
|
+
if (!validation.suggestedFlowType) {
|
|
20098
|
+
if (category === "form") validation.suggestedFlowType = "form";
|
|
20099
|
+
else if (category === "navigation")
|
|
20100
|
+
validation.suggestedFlowType = "navigation";
|
|
20101
|
+
else if (category === "modal")
|
|
20102
|
+
validation.suggestedFlowType = "modal";
|
|
20103
|
+
else if (category === "button" || category === "visibility")
|
|
20104
|
+
validation.suggestedFlowType = "navigation";
|
|
20105
|
+
}
|
|
20106
|
+
}
|
|
20107
|
+
}
|
|
20108
|
+
}
|
|
20109
|
+
const authKeywords = [
|
|
20110
|
+
"login",
|
|
20111
|
+
"logout",
|
|
20112
|
+
"sign in",
|
|
20113
|
+
"sign out",
|
|
20114
|
+
"password",
|
|
20115
|
+
"authenticate"
|
|
20116
|
+
];
|
|
20117
|
+
if (authKeywords.some((k) => text.includes(k))) {
|
|
20118
|
+
validation.suggestedFlowType = "auth";
|
|
20119
|
+
validation.testabilityScore += 15;
|
|
20120
|
+
validation.testabilityReasons.push("Contains auth-related keyword");
|
|
20121
|
+
}
|
|
20122
|
+
const sidebarKeywords = ["sidebar", "menu", "panel", "drawer"];
|
|
20123
|
+
if (sidebarKeywords.some((k) => text.includes(k))) {
|
|
20124
|
+
validation.suggestedFlowType = "sidebar";
|
|
20125
|
+
validation.testabilityScore += 10;
|
|
20126
|
+
validation.testabilityReasons.push("Contains sidebar-related keyword");
|
|
20127
|
+
}
|
|
20128
|
+
const nonTestablePatterns = [
|
|
20129
|
+
"backend",
|
|
20130
|
+
"api",
|
|
20131
|
+
"database",
|
|
20132
|
+
"performance",
|
|
20133
|
+
"security",
|
|
20134
|
+
"code review",
|
|
20135
|
+
"documentation"
|
|
20136
|
+
];
|
|
20137
|
+
if (nonTestablePatterns.some((p) => text.includes(p))) {
|
|
20138
|
+
validation.testabilityScore -= 20;
|
|
20139
|
+
validation.testabilityReasons.push("Contains non-UI pattern");
|
|
20140
|
+
}
|
|
20141
|
+
validation.isTestable = validation.testabilityScore > 0;
|
|
20142
|
+
return validation;
|
|
20143
|
+
});
|
|
20144
|
+
const testableItems = validatedItems.filter((item) => item.isTestable);
|
|
20145
|
+
return {
|
|
20146
|
+
success: testableItems.length > 0,
|
|
20147
|
+
validatedItems,
|
|
20148
|
+
testableCount: testableItems.length,
|
|
20149
|
+
nonTestableCount: validatedItems.length - testableItems.length,
|
|
20150
|
+
message: testableItems.length > 0 ? `\u2705 ${testableItems.length}/${validatedItems.length} items are testable via Maestro` : "\u26A0\uFE0F No testable acceptance criteria found",
|
|
20151
|
+
suggestedFlowTypes: [
|
|
20152
|
+
...new Set(
|
|
20153
|
+
testableItems.map((item) => item.suggestedFlowType).filter(Boolean)
|
|
20154
|
+
)
|
|
20155
|
+
]
|
|
20156
|
+
};
|
|
20157
|
+
}
|
|
20158
|
+
function extractUITextFromFiles(repoPath, files) {
|
|
20159
|
+
const uiTexts = [];
|
|
20160
|
+
for (const file of files) {
|
|
20161
|
+
try {
|
|
20162
|
+
const filePath = join(repoPath, file);
|
|
20163
|
+
if (!existsSync(filePath)) continue;
|
|
20164
|
+
const content = readFileSync(filePath, "utf-8");
|
|
20165
|
+
const jsxTextPattern = />([^<>{}\n]{2,100})</g;
|
|
20166
|
+
let match;
|
|
20167
|
+
while ((match = jsxTextPattern.exec(content)) !== null) {
|
|
20168
|
+
const text = match[1].trim();
|
|
20169
|
+
if (text && !text.startsWith("{") && !text.includes("className")) {
|
|
20170
|
+
uiTexts.push(text);
|
|
20171
|
+
}
|
|
20172
|
+
}
|
|
20173
|
+
const stringLiteralPattern = /['"`]([^'"`\n]{2,50})['"`]/g;
|
|
20174
|
+
while ((match = stringLiteralPattern.exec(content)) !== null) {
|
|
20175
|
+
const text = match[1].trim();
|
|
20176
|
+
if (text && !text.includes("./") && !text.includes("../") && !text.startsWith("http") && !text.includes("className") && !text.match(/^[a-z_]+$/) && // snake_case identifiers
|
|
20177
|
+
!text.match(/^[a-zA-Z]+\.[a-zA-Z]+/)) {
|
|
20178
|
+
uiTexts.push(text);
|
|
20179
|
+
}
|
|
20180
|
+
}
|
|
20181
|
+
const attrPattern = /(?:label|title|placeholder|value|text)=["'`]([^"'`\n]{2,100})["'`]/gi;
|
|
20182
|
+
while ((match = attrPattern.exec(content)) !== null) {
|
|
20183
|
+
uiTexts.push(match[1].trim());
|
|
20184
|
+
}
|
|
20185
|
+
} catch {
|
|
20186
|
+
}
|
|
20187
|
+
}
|
|
20188
|
+
return [...new Set(uiTexts)];
|
|
20189
|
+
}
|
|
20190
|
+
function mapACToUIElements(options = {}) {
|
|
20191
|
+
const { acItems = [], repoPath = process.cwd(), changedFiles = [] } = options;
|
|
20192
|
+
if (!acItems || acItems.length === 0) {
|
|
20193
|
+
const analysis = analyzeChangesForTest({ repoPath });
|
|
20194
|
+
return {
|
|
20195
|
+
success: true,
|
|
20196
|
+
mode: "git_only",
|
|
20197
|
+
mappings: [],
|
|
20198
|
+
gitAnalysis: analysis.analysis,
|
|
20199
|
+
changedUIFiles: analysis.analysis.changedUIFiles,
|
|
20200
|
+
interactiveElements: analysis.analysis.interactiveElements,
|
|
20201
|
+
suggestedFlowType: analysis.analysis.suggestedFlowType,
|
|
20202
|
+
message: "No AC items provided. Using git-based analysis for test generation.",
|
|
20203
|
+
recommendations: analysis.recommendations
|
|
20204
|
+
};
|
|
20205
|
+
}
|
|
20206
|
+
const gitAnalysis = analyzeChangesForTest({ repoPath });
|
|
20207
|
+
const filesToCheck = changedFiles.length > 0 ? changedFiles : gitAnalysis.analysis.changedUIFiles;
|
|
20208
|
+
const actualUITexts = extractUITextFromFiles(repoPath, filesToCheck);
|
|
20209
|
+
const sourceCode = getSourceCodeContent(repoPath, filesToCheck);
|
|
20210
|
+
const validationErrors = [];
|
|
20211
|
+
const validationWarnings = [];
|
|
20212
|
+
const validationInfo = [];
|
|
20213
|
+
const mappings = acItems.map((item) => {
|
|
20214
|
+
const mapping = {
|
|
20215
|
+
...item,
|
|
20216
|
+
matchedFiles: [],
|
|
20217
|
+
matchedElements: [],
|
|
20218
|
+
matchedUITexts: [],
|
|
20219
|
+
relatedUIElements: [],
|
|
20220
|
+
confidence: 0,
|
|
20221
|
+
validated: false,
|
|
20222
|
+
validationStatus: "pending",
|
|
20223
|
+
validationMessage: "",
|
|
20224
|
+
intent: extractIntent(item.text)
|
|
20225
|
+
};
|
|
20226
|
+
const acText = item.text;
|
|
20227
|
+
const intent = mapping.intent;
|
|
20228
|
+
const intentMatch = validateIntent(
|
|
20229
|
+
intent,
|
|
20230
|
+
sourceCode,
|
|
20231
|
+
actualUITexts,
|
|
20232
|
+
gitAnalysis
|
|
20233
|
+
);
|
|
20234
|
+
if (intentMatch.found) {
|
|
20235
|
+
mapping.validated = true;
|
|
20236
|
+
mapping.confidence = intentMatch.confidence;
|
|
20237
|
+
mapping.validationStatus = "passed";
|
|
20238
|
+
mapping.validationMessage = intentMatch.message;
|
|
20239
|
+
mapping.matchedUITexts.push(...intentMatch.matches);
|
|
20240
|
+
validationInfo.push({
|
|
20241
|
+
acItem: acText,
|
|
20242
|
+
intent: intent.action,
|
|
20243
|
+
message: `\u2705 Intent validated: ${intentMatch.message}`
|
|
20244
|
+
});
|
|
20245
|
+
} else {
|
|
20246
|
+
const keywordMatch = findRelatedUIElements(
|
|
20247
|
+
acText,
|
|
20248
|
+
actualUITexts,
|
|
20249
|
+
sourceCode
|
|
20250
|
+
);
|
|
20251
|
+
const specificValueCheck = validateSpecificValues(
|
|
20252
|
+
acText,
|
|
20253
|
+
keywordMatch.elements
|
|
20254
|
+
);
|
|
20255
|
+
if (specificValueCheck.hasMismatch) {
|
|
20256
|
+
mapping.validated = false;
|
|
20257
|
+
mapping.validationStatus = "failed";
|
|
20258
|
+
mapping.confidence = 0;
|
|
20259
|
+
mapping.validationMessage = specificValueCheck.message;
|
|
20260
|
+
mapping.relatedUIElements = keywordMatch.elements;
|
|
20261
|
+
validationErrors.push({
|
|
20262
|
+
acItem: acText,
|
|
20263
|
+
expected: specificValueCheck.expected,
|
|
20264
|
+
found: specificValueCheck.found,
|
|
20265
|
+
message: `\u274C ${specificValueCheck.message}`
|
|
20266
|
+
});
|
|
20267
|
+
} else if (keywordMatch.found) {
|
|
20268
|
+
mapping.relatedUIElements = keywordMatch.elements;
|
|
20269
|
+
mapping.confidence = keywordMatch.confidence;
|
|
20270
|
+
if (keywordMatch.confidence >= 70) {
|
|
20271
|
+
mapping.validated = true;
|
|
20272
|
+
mapping.validationStatus = "passed";
|
|
20273
|
+
mapping.validationMessage = `Found related UI elements: ${keywordMatch.elements.slice(0, 3).join(", ")}`;
|
|
20274
|
+
validationInfo.push({
|
|
20275
|
+
acItem: acText,
|
|
20276
|
+
message: `\u2705 Found related UI elements for "${acText}"`,
|
|
20277
|
+
elements: keywordMatch.elements
|
|
20278
|
+
});
|
|
20279
|
+
} else {
|
|
20280
|
+
mapping.validationStatus = "soft_match";
|
|
20281
|
+
mapping.validationMessage = `Possible match with confidence ${keywordMatch.confidence}%`;
|
|
20282
|
+
validationWarnings.push({
|
|
20283
|
+
acItem: acText,
|
|
20284
|
+
message: `\u26A0\uFE0F Weak match for "${acText}" - confidence ${keywordMatch.confidence}%`,
|
|
20285
|
+
elements: keywordMatch.elements
|
|
20286
|
+
});
|
|
20287
|
+
}
|
|
20288
|
+
}
|
|
20289
|
+
}
|
|
20290
|
+
for (const file of filesToCheck) {
|
|
20291
|
+
const fileName = file.toLowerCase();
|
|
20292
|
+
const textWords = acText.toLowerCase().split(/\s+/).filter((w) => w.length > 3);
|
|
20293
|
+
const matchingWords = textWords.filter((word) => fileName.includes(word));
|
|
20294
|
+
if (matchingWords.length > 0) {
|
|
20295
|
+
mapping.matchedFiles.push({
|
|
20296
|
+
file,
|
|
20297
|
+
matchingWords,
|
|
20298
|
+
confidence: Math.min(matchingWords.length * 25, 100)
|
|
20299
|
+
});
|
|
20300
|
+
if (!mapping.validated) {
|
|
20301
|
+
mapping.confidence = Math.max(
|
|
20302
|
+
mapping.confidence,
|
|
20303
|
+
matchingWords.length * 25
|
|
20304
|
+
);
|
|
20305
|
+
}
|
|
20306
|
+
}
|
|
20307
|
+
}
|
|
20308
|
+
for (const element of gitAnalysis.analysis.interactiveElements) {
|
|
20309
|
+
const elementLower = element.toLowerCase();
|
|
20310
|
+
const acLower = acText.toLowerCase();
|
|
20311
|
+
if (acLower.includes(elementLower) || item.uiKeywords?.some((k) => k.toLowerCase() === elementLower)) {
|
|
20312
|
+
mapping.matchedElements.push(element);
|
|
20313
|
+
if (!mapping.validated) {
|
|
20314
|
+
mapping.confidence = Math.max(mapping.confidence, 60);
|
|
20315
|
+
}
|
|
20316
|
+
}
|
|
20317
|
+
}
|
|
20318
|
+
if (mapping.validationStatus === "pending") {
|
|
20319
|
+
if (mapping.confidence >= 50 || mapping.matchedFiles.length > 0 || mapping.matchedElements.length > 0) {
|
|
20320
|
+
mapping.validationStatus = "soft_match";
|
|
20321
|
+
mapping.validationMessage = "Found related code changes but could not verify exact implementation";
|
|
20322
|
+
} else {
|
|
20323
|
+
mapping.validationStatus = "unmatched";
|
|
20324
|
+
mapping.validationMessage = "No matching UI elements or code changes found";
|
|
20325
|
+
validationWarnings.push({
|
|
20326
|
+
acItem: acText,
|
|
20327
|
+
message: `\u26A0\uFE0F Could not find implementation for "${acText}"`,
|
|
20328
|
+
suggestion: "This AC may not be implemented yet, or may be in unchanged files"
|
|
20329
|
+
});
|
|
20330
|
+
}
|
|
20331
|
+
}
|
|
20332
|
+
mapping.confidence = Math.min(mapping.confidence, 100);
|
|
20333
|
+
return mapping;
|
|
20334
|
+
});
|
|
20335
|
+
const passedMappings = mappings.filter(
|
|
20336
|
+
(m) => m.validationStatus === "passed"
|
|
20337
|
+
);
|
|
20338
|
+
const failedMappings = mappings.filter(
|
|
20339
|
+
(m) => m.validationStatus === "failed"
|
|
20340
|
+
);
|
|
20341
|
+
const softMatchMappings = mappings.filter(
|
|
20342
|
+
(m) => m.validationStatus === "soft_match"
|
|
20343
|
+
);
|
|
20344
|
+
const unmatchedMappings = mappings.filter(
|
|
20345
|
+
(m) => m.validationStatus === "unmatched"
|
|
20346
|
+
);
|
|
20347
|
+
const validatedCount = passedMappings.length + softMatchMappings.length;
|
|
20348
|
+
const validationRate = mappings.length > 0 ? passedMappings.length / mappings.length : 0;
|
|
20349
|
+
const success = failedMappings.length === 0 && validationRate >= 0.7;
|
|
20350
|
+
return {
|
|
20351
|
+
success,
|
|
20352
|
+
mode: "ac_with_git",
|
|
20353
|
+
mappings,
|
|
20354
|
+
passedCount: passedMappings.length,
|
|
20355
|
+
softMatchCount: softMatchMappings.length,
|
|
20356
|
+
unmatchedCount: unmatchedMappings.length,
|
|
20357
|
+
validationRate: Math.round(validationRate * 100),
|
|
20358
|
+
validationErrors,
|
|
20359
|
+
validationWarnings,
|
|
20360
|
+
validationInfo,
|
|
20361
|
+
actualUITexts: actualUITexts.slice(0, 50),
|
|
20362
|
+
diagnostics: {
|
|
20363
|
+
filesScanned: filesToCheck.length,
|
|
20364
|
+
uiTextsExtracted: actualUITexts.length,
|
|
20365
|
+
acItemsProcessed: acItems.length,
|
|
20366
|
+
validationApproach: "intent-based"
|
|
20367
|
+
},
|
|
20368
|
+
gitAnalysis: gitAnalysis.analysis,
|
|
20369
|
+
changedUIFiles: gitAnalysis.analysis.changedUIFiles,
|
|
20370
|
+
interactiveElements: gitAnalysis.analysis.interactiveElements,
|
|
20371
|
+
suggestedFlowType: acItems[0]?.suggestedFlowType || gitAnalysis.analysis.suggestedFlowType,
|
|
20372
|
+
message: generateValidationMessage(
|
|
20373
|
+
success,
|
|
20374
|
+
passedMappings,
|
|
20375
|
+
softMatchMappings,
|
|
20376
|
+
failedMappings,
|
|
20377
|
+
unmatchedMappings
|
|
20378
|
+
),
|
|
20379
|
+
recommendations: generateRecommendations(
|
|
20380
|
+
mappings,
|
|
20381
|
+
actualUITexts,
|
|
20382
|
+
gitAnalysis
|
|
20383
|
+
)
|
|
20384
|
+
};
|
|
20385
|
+
}
|
|
20386
|
+
function extractIntent(acText) {
|
|
20387
|
+
const text = acText.toLowerCase();
|
|
20388
|
+
const intentPatterns = [
|
|
20389
|
+
{
|
|
20390
|
+
pattern: /(?:should|can|must)\s+(?:be able to\s+)?(click|tap|press|select)/i,
|
|
20391
|
+
action: "click"
|
|
20392
|
+
},
|
|
20393
|
+
{
|
|
20394
|
+
pattern: /(?:should|can|must)\s+(?:be able to\s+)?(see|view|display|show)/i,
|
|
20395
|
+
action: "display"
|
|
20396
|
+
},
|
|
20397
|
+
{
|
|
20398
|
+
pattern: /(?:should|can|must)\s+(?:be able to\s+)?(enter|input|type|fill)/i,
|
|
20399
|
+
action: "input"
|
|
20400
|
+
},
|
|
20401
|
+
{
|
|
20402
|
+
pattern: /(?:should|can|must)\s+(?:be able to\s+)?(navigate|go to|redirect)/i,
|
|
20403
|
+
action: "navigate"
|
|
20404
|
+
},
|
|
20405
|
+
{
|
|
20406
|
+
pattern: /(?:should|can|must)\s+(?:be able to\s+)?(validate|check|verify)/i,
|
|
20407
|
+
action: "validate"
|
|
20408
|
+
},
|
|
20409
|
+
{
|
|
20410
|
+
pattern: /(?:should|can|must)\s+(?:be able to\s+)?(submit|save|send)/i,
|
|
20411
|
+
action: "submit"
|
|
20412
|
+
},
|
|
20413
|
+
{
|
|
20414
|
+
pattern: /(?:should|can|must)\s+(?:be able to\s+)?(open|close|toggle)/i,
|
|
20415
|
+
action: "toggle"
|
|
20416
|
+
}
|
|
20417
|
+
];
|
|
20418
|
+
for (const { pattern, action } of intentPatterns) {
|
|
20419
|
+
if (pattern.test(text)) {
|
|
20420
|
+
const match = text.match(pattern);
|
|
20421
|
+
const afterAction = text.substring(
|
|
20422
|
+
text.indexOf(match[0]) + match[0].length
|
|
20423
|
+
);
|
|
20424
|
+
const target = afterAction.split(/\s+/).filter((w) => w.length > 2).slice(0, 5).join(" ");
|
|
20425
|
+
return { action, target, text: acText };
|
|
20426
|
+
}
|
|
20427
|
+
}
|
|
20428
|
+
const keywords = text.split(/\s+/).filter((w) => w.length > 3);
|
|
20429
|
+
return { action: "unknown", target: keywords.join(" "), text: acText };
|
|
20430
|
+
}
|
|
20431
|
+
function validateIntent(intent, sourceCode, uiTexts, gitAnalysis) {
|
|
20432
|
+
const { action, target } = intent;
|
|
20433
|
+
const matches = [];
|
|
20434
|
+
let confidence = 0;
|
|
20435
|
+
const actionPatterns = {
|
|
20436
|
+
click: ["onClick", "onPress", "onTap", "button", "Button", "clickable"],
|
|
20437
|
+
display: ["visible", "show", "display", "render", "return"],
|
|
20438
|
+
input: ["input", "Input", "onChange", "value", "setValue"],
|
|
20439
|
+
navigate: ["navigate", "redirect", "push", "route", "Router"],
|
|
20440
|
+
validate: ["validate", "error", "required", "check", "verify"],
|
|
20441
|
+
submit: ["onSubmit", "submit", "handleSubmit", "post", "send"],
|
|
20442
|
+
toggle: ["toggle", "open", "close", "setState", "setOpen"]
|
|
20443
|
+
};
|
|
20444
|
+
const patterns = actionPatterns[action] || [];
|
|
20445
|
+
const foundPatterns = patterns.filter((p) => sourceCode.includes(p));
|
|
20446
|
+
if (foundPatterns.length > 0) {
|
|
20447
|
+
confidence += 30;
|
|
20448
|
+
matches.push({ type: "code_pattern", patterns: foundPatterns });
|
|
20449
|
+
}
|
|
20450
|
+
const targetWords = target.toLowerCase().split(/\s+/).filter((w) => w.length > 2);
|
|
20451
|
+
const foundTexts = uiTexts.filter(
|
|
20452
|
+
(text) => targetWords.some((word) => text.toLowerCase().includes(word))
|
|
20453
|
+
);
|
|
20454
|
+
if (foundTexts.length > 0) {
|
|
20455
|
+
confidence += 40;
|
|
20456
|
+
matches.push({ type: "ui_text", texts: foundTexts.slice(0, 5) });
|
|
20457
|
+
}
|
|
20458
|
+
const hasInteractiveElements = gitAnalysis.analysis.interactiveElements.some(
|
|
20459
|
+
(el) => patterns.some((p) => el.includes(p))
|
|
20460
|
+
);
|
|
20461
|
+
if (hasInteractiveElements) {
|
|
20462
|
+
confidence += 30;
|
|
20463
|
+
matches.push({ type: "interactive_element" });
|
|
20464
|
+
}
|
|
20465
|
+
return {
|
|
20466
|
+
found: confidence >= 50,
|
|
20467
|
+
confidence: Math.min(confidence, 100),
|
|
20468
|
+
matches,
|
|
20469
|
+
message: confidence >= 50 ? `${action} action detected with ${confidence}% confidence` : `Could not validate ${action} action`
|
|
20470
|
+
};
|
|
20471
|
+
}
|
|
20472
|
+
function validateSpecificValues(acText, foundUIElements) {
|
|
20473
|
+
const quotedPattern = /["']([^"']+)["']/g;
|
|
20474
|
+
const quotedValues = [];
|
|
20475
|
+
let match;
|
|
20476
|
+
while ((match = quotedPattern.exec(acText)) !== null) {
|
|
20477
|
+
quotedValues.push(match[1]);
|
|
20478
|
+
}
|
|
20479
|
+
if (quotedValues.length === 0) {
|
|
20480
|
+
return { hasMismatch: false };
|
|
20481
|
+
}
|
|
20482
|
+
for (const quotedValue of quotedValues) {
|
|
20483
|
+
const quotedLower = quotedValue.toLowerCase();
|
|
20484
|
+
const exactMatch = foundUIElements.some(
|
|
20485
|
+
(el) => el.toLowerCase() === quotedLower || el.toLowerCase().includes(quotedLower)
|
|
20486
|
+
);
|
|
20487
|
+
if (!exactMatch) {
|
|
20488
|
+
const relatedElements = foundUIElements.filter((el) => {
|
|
20489
|
+
const quotedWords = quotedLower.split(/\s+/).filter((w) => w.length > 2);
|
|
20490
|
+
const elLower = el.toLowerCase();
|
|
20491
|
+
return quotedWords.some((word) => elLower.includes(word));
|
|
20492
|
+
});
|
|
20493
|
+
if (relatedElements.length > 0) {
|
|
20494
|
+
return {
|
|
20495
|
+
hasMismatch: true,
|
|
20496
|
+
expected: quotedValue,
|
|
20497
|
+
found: relatedElements,
|
|
20498
|
+
message: `AC requires "${quotedValue}" but only found: ${relatedElements.slice(0, 5).join(", ")}. The specific value "${quotedValue}" does NOT exist in the UI.`
|
|
20499
|
+
};
|
|
20500
|
+
} else {
|
|
20501
|
+
return {
|
|
20502
|
+
hasMismatch: true,
|
|
20503
|
+
expected: quotedValue,
|
|
20504
|
+
found: [],
|
|
20505
|
+
message: `AC requires "${quotedValue}" but this value was NOT found in any UI element.`
|
|
20506
|
+
};
|
|
20507
|
+
}
|
|
20508
|
+
}
|
|
20509
|
+
}
|
|
20510
|
+
return { hasMismatch: false };
|
|
20511
|
+
}
|
|
20512
|
+
function findRelatedUIElements(acText, uiTexts, sourceCode) {
|
|
20513
|
+
const elements = [];
|
|
20514
|
+
let confidence = 0;
|
|
20515
|
+
const stopWords = /* @__PURE__ */ new Set([
|
|
20516
|
+
"the",
|
|
20517
|
+
"a",
|
|
20518
|
+
"an",
|
|
20519
|
+
"is",
|
|
20520
|
+
"should",
|
|
20521
|
+
"can",
|
|
20522
|
+
"must",
|
|
20523
|
+
"be",
|
|
20524
|
+
"to",
|
|
20525
|
+
"of",
|
|
20526
|
+
"and",
|
|
20527
|
+
"or"
|
|
20528
|
+
]);
|
|
20529
|
+
const cleanText = acText.replace(/["'`]/g, "");
|
|
20530
|
+
const keywords = cleanText.toLowerCase().split(/\s+/).filter((w) => w.length > 2 && !stopWords.has(w));
|
|
20531
|
+
for (const uiText of uiTexts) {
|
|
20532
|
+
const uiTextLower = uiText.toLowerCase();
|
|
20533
|
+
const matchingKeywords = keywords.filter((kw) => uiTextLower.includes(kw));
|
|
20534
|
+
if (matchingKeywords.length > 0) {
|
|
20535
|
+
elements.push(uiText);
|
|
20536
|
+
confidence += matchingKeywords.length * 15;
|
|
20537
|
+
}
|
|
20538
|
+
}
|
|
20539
|
+
const codeMatches = keywords.filter(
|
|
20540
|
+
(kw) => sourceCode.toLowerCase().includes(kw)
|
|
20541
|
+
);
|
|
20542
|
+
if (codeMatches.length > 0) {
|
|
20543
|
+
confidence += codeMatches.length * 10;
|
|
20544
|
+
}
|
|
20545
|
+
return {
|
|
20546
|
+
found: elements.length > 0,
|
|
20547
|
+
elements,
|
|
20548
|
+
confidence: Math.min(confidence, 100)
|
|
20549
|
+
};
|
|
20550
|
+
}
|
|
20551
|
+
function getSourceCodeContent(repoPath, files) {
|
|
20552
|
+
let content = "";
|
|
20553
|
+
for (const file of files) {
|
|
20554
|
+
try {
|
|
20555
|
+
const filePath = join(repoPath, file);
|
|
20556
|
+
if (existsSync(filePath)) {
|
|
20557
|
+
content += readFileSync(filePath, "utf-8") + "\n";
|
|
20558
|
+
}
|
|
20559
|
+
} catch {
|
|
20560
|
+
}
|
|
20561
|
+
}
|
|
20562
|
+
return content;
|
|
20563
|
+
}
|
|
20564
|
+
function generateValidationMessage(success, passed, softMatch, failed, unmatched) {
|
|
20565
|
+
const total = passed.length + softMatch.length + failed.length + unmatched.length;
|
|
20566
|
+
if (failed.length > 0) {
|
|
20567
|
+
return `\u274C VALIDATION FAILED: ${failed.length} AC item(s) have SPECIFIC VALUE MISMATCHES. Workflow stopped.`;
|
|
20568
|
+
}
|
|
20569
|
+
const validationRate = Math.round(passed.length / total * 100);
|
|
20570
|
+
if (success) {
|
|
20571
|
+
return `\u2705 Validated ${passed.length} AC items with high confidence, ${softMatch.length} with partial evidence (${validationRate}% validation rate)`;
|
|
20572
|
+
} else {
|
|
20573
|
+
return `\u274C VALIDATION FAILED: Only ${validationRate}% validation rate (${passed.length}/${total} items). Workflow stopped - review unmatched items before proceeding.`;
|
|
20574
|
+
}
|
|
20575
|
+
}
|
|
20576
|
+
function generateRecommendations(mappings, uiTexts, gitAnalysis) {
|
|
20577
|
+
const passed = mappings.filter((m) => m.validationStatus === "passed");
|
|
20578
|
+
const failed = mappings.filter((m) => m.validationStatus === "failed");
|
|
20579
|
+
const softMatch = mappings.filter((m) => m.validationStatus === "soft_match");
|
|
20580
|
+
const unmatched = mappings.filter((m) => m.validationStatus === "unmatched");
|
|
20581
|
+
const total = mappings.length;
|
|
20582
|
+
const validatedCount = passed.length + softMatch.length;
|
|
20583
|
+
const validationRate = total > 0 ? validatedCount / total : 0;
|
|
20584
|
+
const success = failed.length === 0 && validationRate >= 0.7;
|
|
20585
|
+
return `
|
|
20586
|
+
## \u{1F3AF} AC Validation Results (Intent-Based + Strict Value Checking)
|
|
20587
|
+
|
|
20588
|
+
### \u274C FAILED - Specific Value Mismatches (${failed.length}):
|
|
20589
|
+
${failed.length > 0 ? failed.map(
|
|
20590
|
+
(m) => `- **${m.text}**
|
|
20591
|
+
\u274C ${m.validationMessage}
|
|
20592
|
+
Expected: "${m.intent.target}"
|
|
20593
|
+
Found: ${m.relatedUIElements.slice(0, 5).join(", ")}`
|
|
20594
|
+
).join("\n") : "_None_"}
|
|
20595
|
+
|
|
20596
|
+
### \u2705 Validated (${passed.length}):
|
|
20597
|
+
${passed.length > 0 ? passed.map(
|
|
20598
|
+
(m) => `- **${m.text}** (${m.confidence}% confidence)
|
|
20599
|
+
${m.validationMessage}`
|
|
20600
|
+
).join("\n") : "_None_"}
|
|
20601
|
+
|
|
20602
|
+
### \u26A0\uFE0F Partial Matches (${softMatch.length}):
|
|
20603
|
+
${softMatch.length > 0 ? softMatch.map(
|
|
20604
|
+
(m) => `- **${m.text}** (${m.confidence}% confidence)
|
|
20605
|
+
${m.validationMessage}`
|
|
20606
|
+
).join("\n") : "_None_"}
|
|
20607
|
+
|
|
20608
|
+
### \u2753 Unmatched (${unmatched.length}):
|
|
20609
|
+
${unmatched.length > 0 ? unmatched.map((m) => `- **${m.text}**
|
|
20610
|
+
${m.validationMessage}`).join("\n") : "_None_"}
|
|
20611
|
+
|
|
20612
|
+
### \u{1F4CB} UI Elements Found (sample):
|
|
20613
|
+
${uiTexts.slice(0, 15).map((t) => `- "${t}"`).join("\n")}
|
|
20614
|
+
|
|
20615
|
+
### \u{1F4A1} Recommendation:
|
|
20616
|
+
${failed.length > 0 ? `\u274C **WORKFLOW STOPPED** - ${failed.length} AC item(s) have SPECIFIC VALUE MISMATCHES
|
|
20617
|
+
|
|
20618
|
+
**Critical Issues:**
|
|
20619
|
+
${failed.map((m) => `- "${m.text}" expects values that DON'T EXIST in the UI`).join("\n")}
|
|
20620
|
+
|
|
20621
|
+
**Action Required:**
|
|
20622
|
+
1. Check if the AC has the correct expected values (typos, outdated requirements?)
|
|
20623
|
+
2. Verify if the implementation is missing these specific values
|
|
20624
|
+
3. Update EITHER the AC OR the implementation to match
|
|
20625
|
+
4. Re-run validation after fixes
|
|
20626
|
+
|
|
20627
|
+
**Do not proceed with test generation until ALL validations pass.**` : !success ? `\u274C **WORKFLOW STOPPED** - Validation rate too low (${Math.round(validationRate * 100)}%)
|
|
20628
|
+
|
|
20629
|
+
**Action Required:**
|
|
20630
|
+
1. Review the unmatched AC items above
|
|
20631
|
+
2. Verify if they are implemented in unchanged files
|
|
20632
|
+
3. Update the AC to match actual implementation, OR
|
|
20633
|
+
4. Complete the implementation to match AC requirements
|
|
20634
|
+
|
|
20635
|
+
**Do not proceed with test generation until validation passes (\u226570%).**` : passed.length === mappings.length ? "\u2705 All AC items validated - proceed with test generation" : "\u2705 Sufficient validation - safe to proceed with test generation"}
|
|
20636
|
+
`;
|
|
20637
|
+
}
|
|
20638
|
+
|
|
20639
|
+
// maestro-ai/src/mcp-server.js
|
|
20640
|
+
var transport = new StdioServerTransport();
|
|
20641
|
+
var server = new Server(
|
|
20642
|
+
{
|
|
20643
|
+
name: "Maestro AI MCP Server",
|
|
20644
|
+
version: "0.0.1"
|
|
20645
|
+
},
|
|
20646
|
+
{
|
|
20647
|
+
capabilities: {
|
|
20648
|
+
tools: {}
|
|
20649
|
+
}
|
|
20650
|
+
}
|
|
20651
|
+
);
|
|
20652
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
20653
|
+
return {
|
|
20654
|
+
tools: [
|
|
20655
|
+
{
|
|
20656
|
+
name: "maestro_ensure_installed",
|
|
20657
|
+
description: `\u26A0\uFE0F PREREQUISITE: Call this FIRST before using maestro_write_test or maestro_run_all_tests!
|
|
20658
|
+
|
|
20659
|
+
Checks if Maestro CLI is installed and automatically installs it if missing.
|
|
20660
|
+
|
|
20661
|
+
## \u{1F6A8} CRITICAL: Workflow Order
|
|
20662
|
+
When a user asks to "generate and run tests" or "run Maestro tests", follow this exact sequence:
|
|
20663
|
+
1. FIRST: maestro_ensure_installed() \u2190 YOU ARE HERE
|
|
20664
|
+
2. NEXT: clickup_fetch_task() \u2190 Call this immediately after (asks user for ClickUp URL)
|
|
20665
|
+
3. THEN: Based on user response, continue with ClickUp or git-based flow
|
|
20666
|
+
|
|
20667
|
+
After this tool succeeds, your NEXT action must be to call clickup_fetch_task() without parameters.
|
|
20668
|
+
|
|
20669
|
+
## Purpose
|
|
20670
|
+
Ensures Maestro CLI is available before running any Maestro commands. This tool:
|
|
20671
|
+
- Checks if Maestro CLI is installed on the system
|
|
20672
|
+
- Automatically installs it if missing (via Homebrew or curl)
|
|
20673
|
+
- Returns version and path information
|
|
20674
|
+
|
|
20675
|
+
## When to Use
|
|
20676
|
+
- ALWAYS before maestro_write_test (if execute=true)
|
|
20677
|
+
- ALWAYS before maestro_run_all_tests
|
|
20678
|
+
- When setting up a new development environment
|
|
20679
|
+
- To troubleshoot Maestro installation issues
|
|
20680
|
+
|
|
20681
|
+
## Installation Method
|
|
20682
|
+
Uses curl installer: \`curl -fsSL "https://get.maestro.mobile.dev" | bash\`
|
|
20683
|
+
|
|
20684
|
+
## Output
|
|
20685
|
+
- installed: Whether Maestro is now installed
|
|
20686
|
+
- version: Installed version (if available)
|
|
20687
|
+
- wasInstalled: Whether this call performed the installation
|
|
20688
|
+
- message: Status message
|
|
20689
|
+
- details: Additional info (path, install method)`,
|
|
20690
|
+
inputSchema: {
|
|
20691
|
+
type: "object",
|
|
20692
|
+
properties: {
|
|
20693
|
+
autoInstall: {
|
|
20694
|
+
type: "boolean",
|
|
20695
|
+
description: "Whether to automatically install Maestro if not found (default: true)"
|
|
20696
|
+
},
|
|
20697
|
+
forceReinstall: {
|
|
20698
|
+
type: "boolean",
|
|
20699
|
+
description: "Whether to force reinstall even if already installed (default: false)"
|
|
20700
|
+
}
|
|
20701
|
+
},
|
|
20702
|
+
required: []
|
|
20703
|
+
}
|
|
20704
|
+
},
|
|
20705
|
+
{
|
|
20706
|
+
name: "maestro_generate_test",
|
|
20707
|
+
description: `Generates comprehensive Maestro test instructions for web applications.
|
|
20708
|
+
|
|
20709
|
+
## \u26D4 PREREQUISITE: Validation Must Pass First!
|
|
20710
|
+
|
|
20711
|
+
If you called map_AC_to_UI and it returned success: false:
|
|
20712
|
+
- **DO NOT call this tool!**
|
|
20713
|
+
- Report validation errors to user first
|
|
20714
|
+
- Get user confirmation before proceeding
|
|
20715
|
+
|
|
20716
|
+
## Purpose
|
|
20717
|
+
Creates test planning instructions with guidelines, checklists, and best practices.
|
|
20718
|
+
|
|
20719
|
+
## CRITICAL Rules
|
|
20720
|
+
1. **Validation must pass** - Don't generate tests for mismatched AC
|
|
20721
|
+
2. Only test CHANGED functionality (from git diff)
|
|
20722
|
+
3. Never test existing/unchanged UI elements
|
|
20723
|
+
4. Use extendedWaitUntil (not fixed timeouts)
|
|
20724
|
+
5. Text selectors first, id fallback
|
|
20725
|
+
6. Reuse existing flows via runFlow
|
|
20726
|
+
|
|
20727
|
+
## Flow Types (Optional)
|
|
20728
|
+
If flowType is provided, includes a starter template:
|
|
20729
|
+
- auth: Login, signup flows
|
|
20730
|
+
- sidebar: Sidebar panel interactions
|
|
20731
|
+
- form: Form fill and submit
|
|
20732
|
+
- modal: Modal/dialog interactions
|
|
20733
|
+
- navigation: Page-to-page navigation
|
|
20734
|
+
- extended: Build on existing flow
|
|
20735
|
+
|
|
20736
|
+
If flowType is omitted, provides guidance to use other tools (cheat_sheet, pattern, ui_inspection).
|
|
20737
|
+
|
|
20738
|
+
## Output
|
|
20739
|
+
Returns guidelines checklist, suggested filename, and optionally a template.`,
|
|
20740
|
+
inputSchema: {
|
|
20741
|
+
type: "object",
|
|
20742
|
+
properties: {
|
|
20743
|
+
feature: {
|
|
20744
|
+
type: "string",
|
|
20745
|
+
description: 'The feature being tested (e.g., "User Settings", "Login")'
|
|
20746
|
+
},
|
|
20747
|
+
action: {
|
|
20748
|
+
type: "string",
|
|
20749
|
+
description: 'The action being tested (e.g., "Reset defaults", "Submit form")'
|
|
20750
|
+
},
|
|
20751
|
+
flowType: {
|
|
20752
|
+
type: "string",
|
|
20753
|
+
enum: [
|
|
20754
|
+
"auth",
|
|
20755
|
+
"sidebar",
|
|
20756
|
+
"form",
|
|
20757
|
+
"modal",
|
|
20758
|
+
"navigation",
|
|
20759
|
+
"extended"
|
|
20760
|
+
],
|
|
20761
|
+
description: "Type of flow template (optional - omit for template-free generation)"
|
|
20762
|
+
},
|
|
20763
|
+
changedElements: {
|
|
20764
|
+
type: "array",
|
|
20765
|
+
items: { type: "string" },
|
|
20766
|
+
description: "List of UI elements that were changed (from git diff)"
|
|
20767
|
+
},
|
|
20768
|
+
existingTests: {
|
|
20769
|
+
type: "array",
|
|
20770
|
+
items: { type: "string" },
|
|
20771
|
+
description: "List of existing related test files to potentially extend"
|
|
20772
|
+
}
|
|
20773
|
+
},
|
|
20774
|
+
required: ["feature", "action"]
|
|
20775
|
+
}
|
|
20776
|
+
},
|
|
20777
|
+
{
|
|
20778
|
+
name: "maestro_cheat_sheet",
|
|
20779
|
+
description: `Returns Maestro commands quick reference.
|
|
20780
|
+
|
|
20781
|
+
## Includes
|
|
20782
|
+
- All core commands (launchApp, tapOn, inputText, etc.)
|
|
20783
|
+
- Selector strategy (text \u2192 id \u2192 index \u2192 repeat)
|
|
20784
|
+
- Waiting strategy (extendedWaitUntil)
|
|
20785
|
+
- File naming conventions
|
|
20786
|
+
- Environment variable setup
|
|
20787
|
+
|
|
20788
|
+
Use this before writing any Maestro YAML to ensure correct syntax.`,
|
|
20789
|
+
inputSchema: {
|
|
20790
|
+
type: "object",
|
|
20791
|
+
properties: {},
|
|
20792
|
+
required: []
|
|
20793
|
+
}
|
|
20794
|
+
},
|
|
20795
|
+
{
|
|
20796
|
+
name: "maestro_flow_template",
|
|
20797
|
+
description: `Returns a specific Maestro flow template by type.
|
|
20798
|
+
|
|
20799
|
+
## Available Types
|
|
20800
|
+
- auth: Login/signup with onboarding dismissal
|
|
20801
|
+
- sidebar: Sidebar panel interactions
|
|
20802
|
+
- form: Form fill and submit
|
|
20803
|
+
- modal: Modal/dialog interactions
|
|
20804
|
+
- navigation: Page-to-page navigation
|
|
20805
|
+
- extended: Build on existing flow with runFlow
|
|
20806
|
+
|
|
20807
|
+
## Customization Options
|
|
20808
|
+
- name: Flow name
|
|
20809
|
+
- baseUrl: Override default localhost:8443
|
|
20810
|
+
- waitForElement: Initial element to wait for
|
|
20811
|
+
- baseFlow: For extended type, the base flow to extend`,
|
|
20812
|
+
inputSchema: {
|
|
20813
|
+
type: "object",
|
|
20814
|
+
properties: {
|
|
20815
|
+
flowType: {
|
|
20816
|
+
type: "string",
|
|
20817
|
+
enum: [
|
|
20818
|
+
"auth",
|
|
20819
|
+
"sidebar",
|
|
20820
|
+
"form",
|
|
20821
|
+
"modal",
|
|
20822
|
+
"navigation",
|
|
20823
|
+
"extended"
|
|
20824
|
+
],
|
|
20825
|
+
description: "Type of flow template to return"
|
|
20826
|
+
},
|
|
20827
|
+
name: {
|
|
19424
20828
|
type: "string",
|
|
19425
20829
|
description: "Custom name for the flow"
|
|
19426
20830
|
},
|
|
@@ -19499,6 +20903,13 @@ Each pattern includes "when to use" guidance.`,
|
|
|
19499
20903
|
name: "maestro_analyze_changes",
|
|
19500
20904
|
description: `Analyzes actual git changes in a repository and provides test recommendations.
|
|
19501
20905
|
|
|
20906
|
+
## Workflow Position: Step 3
|
|
20907
|
+
Call this AFTER the clickup_fetch_task() prompt has been resolved:
|
|
20908
|
+
1. maestro_ensure_installed() \u2190 Step 1
|
|
20909
|
+
2. clickup_fetch_task() \u2190 Step 2 (ask user for ClickUp URL)
|
|
20910
|
+
3. maestro_analyze_changes() \u2190 YOU ARE HERE - Step 3
|
|
20911
|
+
4. map_AC_to_UI() \u2192 maestro_generate_test() \u2192 maestro_write_test()
|
|
20912
|
+
|
|
19502
20913
|
## Purpose
|
|
19503
20914
|
Automatically runs git commands to:
|
|
19504
20915
|
- Detect changed files (unstaged, staged, or committed)
|
|
@@ -19542,6 +20953,366 @@ Automatically runs git commands to:
|
|
|
19542
20953
|
},
|
|
19543
20954
|
required: ["repoPath"]
|
|
19544
20955
|
}
|
|
20956
|
+
},
|
|
20957
|
+
{
|
|
20958
|
+
name: "maestro_write_test",
|
|
20959
|
+
description: `Writes a Maestro test YAML file to disk and automatically executes it.
|
|
20960
|
+
|
|
20961
|
+
## \u{1F6A8} PREREQUISITES - Check BOTH before using this tool!
|
|
20962
|
+
|
|
20963
|
+
1. **maestro_ensure_installed()** - Must be called first
|
|
20964
|
+
2. **map_AC_to_UI validation** - If called with ClickUp AC, validation must pass (success: true)
|
|
20965
|
+
|
|
20966
|
+
## \u26D4 DO NOT USE IF:
|
|
20967
|
+
- map_AC_to_UI returned success: false (validation failed)
|
|
20968
|
+
- User hasn't confirmed how to handle validation mismatches
|
|
20969
|
+
|
|
20970
|
+
## Purpose
|
|
20971
|
+
Saves generated test YAML to a file and AUTOMATICALLY RUNS IT. This is the primary tool for "generate and run" workflows.
|
|
20972
|
+
|
|
20973
|
+
## IMPORTANT
|
|
20974
|
+
- \u26A0\uFE0F ALWAYS call maestro_ensure_installed() first to ensure Maestro CLI is available!
|
|
20975
|
+
- \u26A0\uFE0F If using ClickUp AC, validation MUST pass before generating tests!
|
|
20976
|
+
- This tool ALREADY executes the test by default - do NOT call maestro_run_all_tests after this!
|
|
20977
|
+
- Use this for running newly generated tests
|
|
20978
|
+
- The execution result is included in the response
|
|
20979
|
+
- This tool internally calls maestro_generate_test to provide guidelines and instructions
|
|
20980
|
+
|
|
20981
|
+
## Parameters
|
|
20982
|
+
- yaml: The YAML content to write (required)
|
|
20983
|
+
- fileName: Name of the file, e.g., "login_test.yaml" (required)
|
|
20984
|
+
- directory: Target directory (default: "maestro")
|
|
20985
|
+
- basePath: Project base path (default: current directory)
|
|
20986
|
+
- execute: Whether to auto-execute after writing (default: true)
|
|
20987
|
+
- deviceId: Device ID for execution (optional)
|
|
20988
|
+
- env: Environment variables for execution (optional)
|
|
20989
|
+
- feature: Feature being tested (for test generation guidelines)
|
|
20990
|
+
- action: Action being tested (for test generation guidelines)
|
|
20991
|
+
- flowType: Type of flow template (auth, sidebar, form, modal, navigation, extended)
|
|
20992
|
+
- changedElements: List of UI elements that were changed
|
|
20993
|
+
- existingTests: List of existing related test files
|
|
20994
|
+
|
|
20995
|
+
## Output
|
|
20996
|
+
- success: boolean
|
|
20997
|
+
- filePath: Path where file was written
|
|
20998
|
+
- message: Status message
|
|
20999
|
+
- generation: Test generation guidelines and instructions (from maestro_generate_test)
|
|
21000
|
+
- execution: Test execution results (if execute=true)`,
|
|
21001
|
+
inputSchema: {
|
|
21002
|
+
type: "object",
|
|
21003
|
+
properties: {
|
|
21004
|
+
yaml: {
|
|
21005
|
+
type: "string",
|
|
21006
|
+
description: "The YAML content to write"
|
|
21007
|
+
},
|
|
21008
|
+
fileName: {
|
|
21009
|
+
type: "string",
|
|
21010
|
+
description: 'Name of the file (e.g., "login_test.yaml")'
|
|
21011
|
+
},
|
|
21012
|
+
directory: {
|
|
21013
|
+
type: "string",
|
|
21014
|
+
description: 'Target directory (default: "maestro")'
|
|
21015
|
+
},
|
|
21016
|
+
basePath: {
|
|
21017
|
+
type: "string",
|
|
21018
|
+
description: "Project base path (default: current directory)"
|
|
21019
|
+
},
|
|
21020
|
+
execute: {
|
|
21021
|
+
type: "boolean",
|
|
21022
|
+
description: "Whether to auto-execute the test after writing (default: true)"
|
|
21023
|
+
},
|
|
21024
|
+
deviceId: {
|
|
21025
|
+
type: "string",
|
|
21026
|
+
description: "Device ID to run on (optional)"
|
|
21027
|
+
},
|
|
21028
|
+
env: {
|
|
21029
|
+
type: "object",
|
|
21030
|
+
description: "Environment variables to pass",
|
|
21031
|
+
additionalProperties: { type: "string" }
|
|
21032
|
+
},
|
|
21033
|
+
feature: {
|
|
21034
|
+
type: "string",
|
|
21035
|
+
description: 'Feature being tested for generation guidelines (e.g., "User Settings")'
|
|
21036
|
+
},
|
|
21037
|
+
action: {
|
|
21038
|
+
type: "string",
|
|
21039
|
+
description: 'Action being tested for generation guidelines (e.g., "Reset defaults")'
|
|
21040
|
+
},
|
|
21041
|
+
flowType: {
|
|
21042
|
+
type: "string",
|
|
21043
|
+
enum: [
|
|
21044
|
+
"auth",
|
|
21045
|
+
"sidebar",
|
|
21046
|
+
"form",
|
|
21047
|
+
"modal",
|
|
21048
|
+
"navigation",
|
|
21049
|
+
"extended"
|
|
21050
|
+
],
|
|
21051
|
+
description: "Type of flow template for generation guidelines"
|
|
21052
|
+
},
|
|
21053
|
+
changedElements: {
|
|
21054
|
+
type: "array",
|
|
21055
|
+
items: { type: "string" },
|
|
21056
|
+
description: "List of UI elements that were changed (from git diff)"
|
|
21057
|
+
},
|
|
21058
|
+
existingTests: {
|
|
21059
|
+
type: "array",
|
|
21060
|
+
items: { type: "string" },
|
|
21061
|
+
description: "List of existing related test files to potentially extend"
|
|
21062
|
+
}
|
|
21063
|
+
},
|
|
21064
|
+
required: ["yaml", "fileName"]
|
|
21065
|
+
}
|
|
21066
|
+
},
|
|
21067
|
+
{
|
|
21068
|
+
name: "maestro_discover_tests",
|
|
21069
|
+
description: `Discovers all Maestro test files in a directory.
|
|
21070
|
+
|
|
21071
|
+
## Purpose
|
|
21072
|
+
Lists all .yaml test files in the maestro/ directory.
|
|
21073
|
+
|
|
21074
|
+
## Parameters
|
|
21075
|
+
- directory: Directory to search (default: "maestro")
|
|
21076
|
+
- basePath: Project base path
|
|
21077
|
+
|
|
21078
|
+
## Output
|
|
21079
|
+
- files: Array of file paths
|
|
21080
|
+
- count: Number of files found`,
|
|
21081
|
+
inputSchema: {
|
|
21082
|
+
type: "object",
|
|
21083
|
+
properties: {
|
|
21084
|
+
directory: {
|
|
21085
|
+
type: "string",
|
|
21086
|
+
description: 'Directory to search (default: "maestro")'
|
|
21087
|
+
},
|
|
21088
|
+
basePath: {
|
|
21089
|
+
type: "string",
|
|
21090
|
+
description: "Project base path"
|
|
21091
|
+
}
|
|
21092
|
+
},
|
|
21093
|
+
required: []
|
|
21094
|
+
}
|
|
21095
|
+
},
|
|
21096
|
+
{
|
|
21097
|
+
name: "maestro_run_all_tests",
|
|
21098
|
+
description: `Runs the ENTIRE Maestro test suite.
|
|
21099
|
+
|
|
21100
|
+
## \u{1F6A8} PREREQUISITE: Call maestro_ensure_installed() FIRST before using this tool!
|
|
21101
|
+
|
|
21102
|
+
## \u26A0\uFE0F ONLY use when user EXPLICITLY asks to "run all tests" or "run the test suite"
|
|
21103
|
+
|
|
21104
|
+
## DO NOT USE for:
|
|
21105
|
+
- "generate and run test cases" \u2192 use maestro_write_test instead (it auto-executes)
|
|
21106
|
+
- Running a newly generated test \u2192 maestro_write_test already does this
|
|
21107
|
+
- Any workflow that involves generating new tests
|
|
21108
|
+
|
|
21109
|
+
## Purpose
|
|
21110
|
+
Discovers and executes ALL existing .yaml test files in the maestro/ directory.
|
|
21111
|
+
|
|
21112
|
+
## Parameters
|
|
21113
|
+
- files: Specific test file paths (optional)
|
|
21114
|
+
- directory: Directory containing tests (default: "maestro")
|
|
21115
|
+
- basePath: Project base path
|
|
21116
|
+
- deviceId: Target device ID
|
|
21117
|
+
- env: Environment variables
|
|
21118
|
+
- stopOnFailure: Stop on first failure (default: false)
|
|
21119
|
+
|
|
21120
|
+
## Output
|
|
21121
|
+
- discovery: List of test files found
|
|
21122
|
+
- execution: Results with pass/fail status for each test
|
|
21123
|
+
- summary: Aggregated statistics`,
|
|
21124
|
+
inputSchema: {
|
|
21125
|
+
type: "object",
|
|
21126
|
+
properties: {
|
|
21127
|
+
files: {
|
|
21128
|
+
type: "array",
|
|
21129
|
+
items: { type: "string" },
|
|
21130
|
+
description: "Specific test file paths to run (if provided, only these files are executed instead of discovering all)"
|
|
21131
|
+
},
|
|
21132
|
+
directory: {
|
|
21133
|
+
type: "string",
|
|
21134
|
+
description: 'Directory containing tests (default: "maestro") - only used if files is not provided'
|
|
21135
|
+
},
|
|
21136
|
+
basePath: {
|
|
21137
|
+
type: "string",
|
|
21138
|
+
description: "Project base path"
|
|
21139
|
+
},
|
|
21140
|
+
deviceId: {
|
|
21141
|
+
type: "string",
|
|
21142
|
+
description: "Device ID to run on"
|
|
21143
|
+
},
|
|
21144
|
+
env: {
|
|
21145
|
+
type: "object",
|
|
21146
|
+
description: "Environment variables",
|
|
21147
|
+
additionalProperties: { type: "string" }
|
|
21148
|
+
},
|
|
21149
|
+
stopOnFailure: {
|
|
21150
|
+
type: "boolean",
|
|
21151
|
+
description: "Stop on first failure (default: false)"
|
|
21152
|
+
}
|
|
21153
|
+
},
|
|
21154
|
+
required: []
|
|
21155
|
+
}
|
|
21156
|
+
},
|
|
21157
|
+
{
|
|
21158
|
+
name: "clickup_fetch_task",
|
|
21159
|
+
description: `Fetches a ClickUp task by URL to extract acceptance criteria for test generation.
|
|
21160
|
+
|
|
21161
|
+
## \u{1F6A8} ASK FIRST Pattern
|
|
21162
|
+
When called WITHOUT a taskUrl, this tool returns a prompt asking the user:
|
|
21163
|
+
> "Do you have a ClickUp task URL for this feature? Paste the URL to generate tests based on acceptance criteria, or type 'skip' to continue with git-based analysis only."
|
|
21164
|
+
|
|
21165
|
+
## Workflow Position: Step 2
|
|
21166
|
+
This is the MANDATORY second step after maestro_ensure_installed():
|
|
21167
|
+
1. maestro_ensure_installed() \u2190 Step 1 (completed)
|
|
21168
|
+
2. clickup_fetch_task() \u2190 YOU ARE HERE - Step 2
|
|
21169
|
+
3. Wait for user response, then:
|
|
21170
|
+
- If URL provided \u2192 validate_acceptance_criteria \u2192 maestro_analyze_changes \u2192 map_AC_to_UI
|
|
21171
|
+
- If skipped \u2192 maestro_analyze_changes \u2192 map_AC_to_UI (git-only mode)
|
|
21172
|
+
|
|
21173
|
+
## How to Use
|
|
21174
|
+
1. Call WITHOUT parameters first: clickup_fetch_task()
|
|
21175
|
+
2. Show the returned prompt to the user
|
|
21176
|
+
3. Based on user's response:
|
|
21177
|
+
- URL provided: Call again with clickup_fetch_task(taskUrl: "<user's url>")
|
|
21178
|
+
- User says "skip"/"no": Call with clickup_fetch_task(taskUrl: "skip")
|
|
21179
|
+
|
|
21180
|
+
## URL Parsing
|
|
21181
|
+
Extracts task ID from various ClickUp URL formats:
|
|
21182
|
+
- https://app.clickup.com/t/abc123
|
|
21183
|
+
- https://app.clickup.com/t/86abc123
|
|
21184
|
+
- https://app.clickup.com/{workspace}/v/...?p=abc123
|
|
21185
|
+
|
|
21186
|
+
## Environment Variables
|
|
21187
|
+
Requires CLICKUP_API_TOKEN to be set in MCP configuration.
|
|
21188
|
+
|
|
21189
|
+
## Output
|
|
21190
|
+
- If no URL: Returns prompt for user input (needsInput: true)
|
|
21191
|
+
- If URL provided: Returns full task object with checklists, custom fields, description
|
|
21192
|
+
- If "skip": Returns { skipped: true } to signal continue without ClickUp`,
|
|
21193
|
+
inputSchema: {
|
|
21194
|
+
type: "object",
|
|
21195
|
+
properties: {
|
|
21196
|
+
taskUrl: {
|
|
21197
|
+
type: "string",
|
|
21198
|
+
description: 'ClickUp task URL (optional - if omitted, returns a prompt asking for URL). Use "skip" to bypass ClickUp integration.'
|
|
21199
|
+
}
|
|
21200
|
+
},
|
|
21201
|
+
required: []
|
|
21202
|
+
}
|
|
21203
|
+
},
|
|
21204
|
+
{
|
|
21205
|
+
name: "validate_acceptance_criteria",
|
|
21206
|
+
description: `Parses and validates acceptance criteria from a ClickUp task for testability.
|
|
21207
|
+
|
|
21208
|
+
## Purpose
|
|
21209
|
+
Extracts AC items from multiple sources and validates them for Maestro test generation.
|
|
21210
|
+
|
|
21211
|
+
## AC Sources (configured via CLICKUP_AC_SOURCE env var)
|
|
21212
|
+
- checklist:Checklist Name - Parse from a specific checklist
|
|
21213
|
+
- custom_field:Field Name - Parse from a custom field
|
|
21214
|
+
- description - Parse from task description
|
|
21215
|
+
- Multiple sources: "checklist:Acceptance Criteria,custom_field:AC,description"
|
|
21216
|
+
|
|
21217
|
+
## Validation
|
|
21218
|
+
Each AC item is analyzed for:
|
|
21219
|
+
- UI-related keywords (click, button, form, input, navigate, etc.)
|
|
21220
|
+
- Testability score (0-100)
|
|
21221
|
+
- Suggested flow type (auth, form, sidebar, modal, navigation)
|
|
21222
|
+
- Whether it's testable via Maestro
|
|
21223
|
+
|
|
21224
|
+
## Output
|
|
21225
|
+
- validatedItems: Array of AC items with testability analysis
|
|
21226
|
+
- testableCount: Number of items that can be tested
|
|
21227
|
+
- suggestedFlowTypes: Recommended flow types based on AC content`,
|
|
21228
|
+
inputSchema: {
|
|
21229
|
+
type: "object",
|
|
21230
|
+
properties: {
|
|
21231
|
+
task: {
|
|
21232
|
+
type: "object",
|
|
21233
|
+
description: "ClickUp task object (from clickup_fetch_task)"
|
|
21234
|
+
},
|
|
21235
|
+
acSource: {
|
|
21236
|
+
type: "string",
|
|
21237
|
+
description: 'AC source specification (optional, uses CLICKUP_AC_SOURCE env var if not provided). Format: "checklist:Name" or "custom_field:Name" or "description"'
|
|
21238
|
+
}
|
|
21239
|
+
},
|
|
21240
|
+
required: ["task"]
|
|
21241
|
+
}
|
|
21242
|
+
},
|
|
21243
|
+
{
|
|
21244
|
+
name: "map_AC_to_UI",
|
|
21245
|
+
description: `Maps acceptance criteria items to UI elements based on git changes.
|
|
21246
|
+
|
|
21247
|
+
## \u{1F6D1} BLOCKING VALIDATION - STOP ON FAILURE!
|
|
21248
|
+
|
|
21249
|
+
This tool performs **strict validation** of AC against actual UI implementation.
|
|
21250
|
+
## Purpose
|
|
21251
|
+
Cross-references AC items with git diff and source code to:
|
|
21252
|
+
- Extract actual UI text from
|
|
21253
|
+
I source files
|
|
21254
|
+
- Compare AC text against real UI strings
|
|
21255
|
+
- **FAIL if values don't match** (e.g., AC: "20 days" vs UI: "30 days")
|
|
21256
|
+
- Report validation errors that must be resolved
|
|
21257
|
+
|
|
21258
|
+
## \u26D4 CRITICAL: When success=false, STOP THE WORKFLOW!
|
|
21259
|
+
|
|
21260
|
+
## Modes
|
|
21261
|
+
- ac with_git: When AC items are provided, performs HARD validation
|
|
21262
|
+
- git_only: When no AC items, uses pure git-based analysis (soft mode)
|
|
21263
|
+
|
|
21264
|
+
## Output
|
|
21265
|
+
- success: **false** if any AC doesn't match UI, **true** if all validated
|
|
21266
|
+
- validationErrors: Array of mismatches (AC expected vs UI actual)
|
|
21267
|
+
- validationWarnings: Items that couldn't be fully verified
|
|
21268
|
+
- actualUITexts: Sample of UI text strings found in source code
|
|
21269
|
+
|
|
21270
|
+
If this tool returns success: false:
|
|
21271
|
+
1. **STOP IMMEDIATELY** - Do NOT call any more tools
|
|
21272
|
+
3. **END your response** - Do not continue, do not ask questions
|
|
21273
|
+
|
|
21274
|
+
## Output Format When Validation Fails:
|
|
21275
|
+
## \u2705 CORRECT BEHAVIOR:
|
|
21276
|
+
When validation fails, respond with:
|
|
21277
|
+
"\u274C Validation failed. The acceptance criteria don't match the UI:
|
|
21278
|
+
- [list mismatches]
|
|
21279
|
+
|
|
21280
|
+
**Recommendation:** Update the ClickUp task acceptance criteria to match the actual UI implementation, OR fix the UI to match the AC requirements.
|
|
21281
|
+
|
|
21282
|
+
**Actual UI values available:** [list what exists]
|
|
21283
|
+
|
|
21284
|
+
*Workflow stopped. Re-run after resolving the mismatches.*"
|
|
21285
|
+
|
|
21286
|
+
## \u274C FORBIDDEN ACTIONS AFTER FAILURE:
|
|
21287
|
+
- Calling maestro_generate_test
|
|
21288
|
+
- Calling maestro_write_test
|
|
21289
|
+
- Saying "I'll generate a test based on what actually exists"
|
|
21290
|
+
- Asking follow-up questions
|
|
21291
|
+
- Continuing the workflow in any way
|
|
21292
|
+
|
|
21293
|
+
## Output
|
|
21294
|
+
- success: **false** = STOP WORKFLOW, output recommendation, END RESPONSE
|
|
21295
|
+
- success: **true** = OK to proceed with test generation`,
|
|
21296
|
+
inputSchema: {
|
|
21297
|
+
type: "object",
|
|
21298
|
+
properties: {
|
|
21299
|
+
acItems: {
|
|
21300
|
+
type: "array",
|
|
21301
|
+
items: { type: "object" },
|
|
21302
|
+
description: "Validated AC items from validate_acceptance_criteria (optional - if omitted, uses git-only analysis)"
|
|
21303
|
+
},
|
|
21304
|
+
repoPath: {
|
|
21305
|
+
type: "string",
|
|
21306
|
+
description: "Path to the git repository (default: current directory)"
|
|
21307
|
+
},
|
|
21308
|
+
changedFiles: {
|
|
21309
|
+
type: "array",
|
|
21310
|
+
items: { type: "string" },
|
|
21311
|
+
description: "List of changed files to match against (optional - auto-detected from git if omitted)"
|
|
21312
|
+
}
|
|
21313
|
+
},
|
|
21314
|
+
required: []
|
|
21315
|
+
}
|
|
19545
21316
|
}
|
|
19546
21317
|
]
|
|
19547
21318
|
};
|
|
@@ -19550,6 +21321,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
19550
21321
|
const { name, arguments: args } = request.params;
|
|
19551
21322
|
let result;
|
|
19552
21323
|
switch (name) {
|
|
21324
|
+
case "maestro_ensure_installed": {
|
|
21325
|
+
const { autoInstall, forceReinstall } = args || {};
|
|
21326
|
+
result = ensureMaestroInstalled({
|
|
21327
|
+
autoInstall: autoInstall !== false,
|
|
21328
|
+
// defaults to true
|
|
21329
|
+
forceReinstall: forceReinstall || false
|
|
21330
|
+
});
|
|
21331
|
+
break;
|
|
21332
|
+
}
|
|
19553
21333
|
case "maestro_generate_test": {
|
|
19554
21334
|
const { feature, action, flowType, changedElements, existingTests } = args || {};
|
|
19555
21335
|
result = generateMaestroTest({
|
|
@@ -19600,6 +21380,93 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
19600
21380
|
});
|
|
19601
21381
|
break;
|
|
19602
21382
|
}
|
|
21383
|
+
case "maestro_write_test": {
|
|
21384
|
+
const {
|
|
21385
|
+
yaml,
|
|
21386
|
+
fileName,
|
|
21387
|
+
directory,
|
|
21388
|
+
basePath,
|
|
21389
|
+
execute,
|
|
21390
|
+
deviceId,
|
|
21391
|
+
env,
|
|
21392
|
+
feature,
|
|
21393
|
+
action,
|
|
21394
|
+
flowType,
|
|
21395
|
+
changedElements,
|
|
21396
|
+
existingTests
|
|
21397
|
+
} = args || {};
|
|
21398
|
+
result = writeTestFile({
|
|
21399
|
+
yaml,
|
|
21400
|
+
fileName,
|
|
21401
|
+
directory: directory || "maestro",
|
|
21402
|
+
basePath: basePath || process.cwd(),
|
|
21403
|
+
execute: execute !== false,
|
|
21404
|
+
// defaults to true
|
|
21405
|
+
deviceId: deviceId || null,
|
|
21406
|
+
env: env || {},
|
|
21407
|
+
feature: feature || "",
|
|
21408
|
+
action: action || "",
|
|
21409
|
+
flowType: flowType || null,
|
|
21410
|
+
changedElements: changedElements || [],
|
|
21411
|
+
existingTests: existingTests || []
|
|
21412
|
+
});
|
|
21413
|
+
break;
|
|
21414
|
+
}
|
|
21415
|
+
case "maestro_discover_tests": {
|
|
21416
|
+
const { directory, basePath } = args || {};
|
|
21417
|
+
result = discoverTestFiles({
|
|
21418
|
+
directory: directory || "maestro",
|
|
21419
|
+
basePath: basePath || process.cwd()
|
|
21420
|
+
});
|
|
21421
|
+
break;
|
|
21422
|
+
}
|
|
21423
|
+
case "maestro_run_all_tests": {
|
|
21424
|
+
const { files, directory, basePath, deviceId, env, stopOnFailure } = args || {};
|
|
21425
|
+
result = runAllTests({
|
|
21426
|
+
files: files || null,
|
|
21427
|
+
directory: directory || "maestro",
|
|
21428
|
+
basePath: basePath || process.cwd(),
|
|
21429
|
+
deviceId: deviceId || null,
|
|
21430
|
+
env: env || {},
|
|
21431
|
+
stopOnFailure: stopOnFailure || false
|
|
21432
|
+
});
|
|
21433
|
+
break;
|
|
21434
|
+
}
|
|
21435
|
+
case "clickup_fetch_task": {
|
|
21436
|
+
const { taskUrl } = args || {};
|
|
21437
|
+
result = await fetchClickUpTask({
|
|
21438
|
+
taskUrl: taskUrl || null
|
|
21439
|
+
});
|
|
21440
|
+
break;
|
|
21441
|
+
}
|
|
21442
|
+
case "validate_acceptance_criteria": {
|
|
21443
|
+
const { task, acSource } = args || {};
|
|
21444
|
+
const parsed = parseAcceptanceCriteria({
|
|
21445
|
+
task,
|
|
21446
|
+
acSource: acSource || null
|
|
21447
|
+
});
|
|
21448
|
+
if (parsed.success) {
|
|
21449
|
+
const validated = validateAcceptanceCriteria({
|
|
21450
|
+
acItems: parsed.acItems
|
|
21451
|
+
});
|
|
21452
|
+
result = {
|
|
21453
|
+
...parsed,
|
|
21454
|
+
...validated
|
|
21455
|
+
};
|
|
21456
|
+
} else {
|
|
21457
|
+
result = parsed;
|
|
21458
|
+
}
|
|
21459
|
+
break;
|
|
21460
|
+
}
|
|
21461
|
+
case "map_AC_to_UI": {
|
|
21462
|
+
const { acItems, repoPath, changedFiles } = args || {};
|
|
21463
|
+
result = mapACToUIElements({
|
|
21464
|
+
acItems: acItems || [],
|
|
21465
|
+
repoPath: repoPath || process.cwd(),
|
|
21466
|
+
changedFiles: changedFiles || []
|
|
21467
|
+
});
|
|
21468
|
+
break;
|
|
21469
|
+
}
|
|
19603
21470
|
default:
|
|
19604
21471
|
throw new Error(`Unknown tool: ${name}`);
|
|
19605
21472
|
}
|