@deriv-com/fe-mcp-servers 0.0.12 → 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.
@@ -19748,6 +19748,893 @@ function runAllTests(options = {}) {
19748
19748
  execution
19749
19749
  };
19750
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(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/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
+ }
19751
20638
 
19752
20639
  // maestro-ai/src/mcp-server.js
19753
20640
  var transport = new StdioServerTransport();
@@ -19771,10 +20658,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
19771
20658
 
19772
20659
  Checks if Maestro CLI is installed and automatically installs it if missing.
19773
20660
 
19774
- ## \u{1F6A8} CRITICAL: Always Call First
19775
- When a user asks to "generate and run tests" or "run Maestro tests", you MUST:
19776
- 1. FIRST call maestro_ensure_installed() to verify/install Maestro CLI
19777
- 2. THEN proceed with test generation/execution tools
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.
19778
20668
 
19779
20669
  ## Purpose
19780
20670
  Ensures Maestro CLI is available before running any Maestro commands. This tool:
@@ -19816,15 +20706,23 @@ Uses curl installer: \`curl -fsSL "https://get.maestro.mobile.dev" | bash\`
19816
20706
  name: "maestro_generate_test",
19817
20707
  description: `Generates comprehensive Maestro test instructions for web applications.
19818
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
+
19819
20716
  ## Purpose
19820
20717
  Creates test planning instructions with guidelines, checklists, and best practices.
19821
20718
 
19822
20719
  ## CRITICAL Rules
19823
- 1. Only test CHANGED functionality (from git diff)
19824
- 2. Never test existing/unchanged UI elements
19825
- 3. Use extendedWaitUntil (not fixed timeouts)
19826
- 4. Text selectors first, id fallback
19827
- 5. Reuse existing flows via runFlow
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
19828
20726
 
19829
20727
  ## Flow Types (Optional)
19830
20728
  If flowType is provided, includes a starter template:
@@ -20005,6 +20903,13 @@ Each pattern includes "when to use" guidance.`,
20005
20903
  name: "maestro_analyze_changes",
20006
20904
  description: `Analyzes actual git changes in a repository and provides test recommendations.
20007
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
+
20008
20913
  ## Purpose
20009
20914
  Automatically runs git commands to:
20010
20915
  - Detect changed files (unstaged, staged, or committed)
@@ -20053,13 +20958,21 @@ Automatically runs git commands to:
20053
20958
  name: "maestro_write_test",
20054
20959
  description: `Writes a Maestro test YAML file to disk and automatically executes it.
20055
20960
 
20056
- ## \u{1F6A8} PREREQUISITE: Call maestro_ensure_installed() FIRST before using this tool!
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
20057
20969
 
20058
20970
  ## Purpose
20059
20971
  Saves generated test YAML to a file and AUTOMATICALLY RUNS IT. This is the primary tool for "generate and run" workflows.
20060
20972
 
20061
20973
  ## IMPORTANT
20062
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!
20063
20976
  - This tool ALREADY executes the test by default - do NOT call maestro_run_all_tests after this!
20064
20977
  - Use this for running newly generated tests
20065
20978
  - The execution result is included in the response
@@ -20240,6 +21153,166 @@ Discovers and executes ALL existing .yaml test files in the maestro/ directory.
20240
21153
  },
20241
21154
  required: []
20242
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
+ }
20243
21316
  }
20244
21317
  ]
20245
21318
  };
@@ -20359,6 +21432,41 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
20359
21432
  });
20360
21433
  break;
20361
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
+ }
20362
21470
  default:
20363
21471
  throw new Error(`Unknown tool: ${name}`);
20364
21472
  }