@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.
- package/dist/maestro-ai/README.md +187 -7
- package/dist/maestro-ai/mcp-server.js +1118 -10
- package/package.json +1 -1
|
@@ -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(/ /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
|
+
}
|
|
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:
|
|
19775
|
-
When a user asks to "generate and run tests" or "run Maestro tests",
|
|
19776
|
-
1. FIRST
|
|
19777
|
-
2.
|
|
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.
|
|
19824
|
-
2.
|
|
19825
|
-
3.
|
|
19826
|
-
4.
|
|
19827
|
-
5.
|
|
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}
|
|
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
|
}
|