-
Notifications
You must be signed in to change notification settings - Fork 2
feat(validate): Wave 3 — XML generator correctness + 4 new local validator rules #136
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
37213d9
c62c6ca
1ef9f6a
a3639d8
184a98a
7ba1616
ef06d73
63acc2c
e7d557b
debca78
d6d4da6
849c6b9
c6ba663
7f7ea1c
c9fa3fe
7d148f3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -69,6 +69,17 @@ function buildStepWarnings(steps: Array<{ api_id: string }>): string[] { | |
| ); | ||
| } | ||
|
|
||
| // D7: Cleanup steps placed after a potential failure point are skipped when stopOnError=false. | ||
| if (resolvedIds.includes(SHORTHAND_TO_FQID['ApexDeleteObject'] ?? '')) { | ||
| warnings.push( | ||
| 'ApexDeleteObject detected (likely cleanup): with stopOnError=false Provar skips all steps after ' + | ||
| 'the first failure, so cleanup steps placed at the end of the test will NOT run when an earlier ' + | ||
| 'step fails — leaving orphaned records in the org. ' + | ||
| 'Wrap cleanup in a Provar TearDown callable, or place create/delete inside the same UiWithScreen ' + | ||
| 'clause so both run as a unit regardless of failure.' | ||
| ); | ||
| } | ||
|
|
||
| return warnings; | ||
| } | ||
|
|
||
|
|
@@ -89,10 +100,19 @@ const StepSchema = z.object({ | |
| .record(z.string()) | ||
| .default({}) | ||
| .describe( | ||
| 'Step argument values as key/value pairs. Written as <arguments><argument id="key"><value .../></argument></arguments> ' + | ||
| 'inside the <apiCall> element — the format Provar runtime requires. ' + | ||
| 'Step argument values as key/value pairs. Written as <arguments><argument id="key"><value .../></argument></arguments>. ' + | ||
| 'Do NOT rely on XML attributes on <apiCall>; the runtime silently ignores them. ' + | ||
| 'Example: { "connectionName": "MyOrg", "objectApiName": "Opportunity" }' | ||
| 'Special value conventions (applied automatically by the generator): ' + | ||
| '(1) Variable references: wrap the name in braces, e.g. "{MyVar}" → emitted as class="variable" <path element="MyVar"/>. ' + | ||
| ' Dotted paths are also supported: "{Obj.Field}" → two <path> elements. ' + | ||
| '(2) SetValues: pass each variable name and its value as a flat key/value pair; ' + | ||
| ' the generator wraps them in <value class="valueList"><namedValues>...</namedValues></value> automatically. ' + | ||
| ' Example: { "testCaseName": "TC_New", "testType": "Acceptance testing" } ' + | ||
| '(3) AssertValues: pass assertion arguments as flat key/value pairs; emitted as flat <argument> elements, NOT wrapped in valueList/namedValues. ' + | ||
| '(4) target argument (UiWithScreen / UiWithRow): pass the sf:ui:target or ui:pageobject:target URI; ' + | ||
| ' emitted as class="uiTarget" uri="...". ' + | ||
| '(5) locator argument (UiDoAction / UiAssert): pass the locator URI; emitted as class="uiLocator" uri="...". ' + | ||
| 'All other string values use class="value" valueClass="string".' | ||
| ), | ||
| }); | ||
|
|
||
|
|
@@ -111,6 +131,14 @@ const TOOL_DESCRIPTION = [ | |
| 'ApexReadObject requires field names in attributes; omitting them produces MALFORMED_QUERY. Prefer ApexSoqlQuery.', | ||
| 'AssertValues on SOQL results: index paths like "ResultList[0].Field" are not supported.', | ||
| 'Use ForEach to iterate the result list, or SetValues to extract a field into a variable first.', | ||
| 'SetValues: pass named variable values as flat key/value pairs in attributes; ' + | ||
| 'the generator wraps them in <value class="valueList"><namedValues>...</namedValues></value> automatically.', | ||
| 'AssertValues: pass assertion values as flat key/value argument pairs; emitted as flat arguments, NOT wrapped in namedValues. ' + | ||
| 'If AssertValues uses namedValues-shaped content, validation reports warning ASSERT-001.', | ||
| 'Variable references: pass values as "{VarName}" (braces); emitted as class="variable" <path element="VarName"/>.', | ||
| 'target argument (UiWithScreen/UiWithRow): pass the URI value; emitted as class="uiTarget" uri="...".', | ||
| 'locator argument (UiDoAction/UiAssert): pass the URI value; emitted as class="uiLocator" uri="...".', | ||
| 'Cleanup warning: ApexDeleteObject steps near end of test will be skipped if an earlier step fails (stopOnError=false). Use a TearDown callable.', | ||
| 'Validation: when validate_after_edit=true (default) the response includes a validation field and returns TESTCASE_INVALID if the generated XML fails structural checks.', | ||
| 'Grounding: call provar.qualityhub.examples.retrieve before generating to get corpus examples for the scenario — correct XML structure for the step types you need.', | ||
| ].join(' '); | ||
|
|
@@ -239,28 +267,85 @@ export function registerTestCaseGenerate(server: McpServer, config: ServerConfig | |
|
|
||
| // ── XML builder ─────────────────────────────────────────────────────────────── | ||
|
|
||
| function buildArgumentsXml(attributes: Record<string, string>, baseIndent = ' '): string { | ||
| // Build the <value> element for a single argument (D2/D4 aware). | ||
| // inNamedValues: when true (inside SetValues namedValues), skip uiTarget/uiLocator dispatch. | ||
| // apiId: resolved API ID used to restrict key-name dispatch to the correct UI APIs. | ||
| function buildArgumentValue(key: string, val: string, indent: string, inNamedValues = false, apiId = ''): string { | ||
| // D4: {VarName} or {Obj.Field} → class="variable" with <path> elements. | ||
| const varMatch = /^\{([\w.]+)\}$/.exec(val); | ||
| if (varMatch) { | ||
| const pathElements = varMatch[1] | ||
| .split('.') | ||
| .map((p) => `${indent} <path element="${escapeXmlAttr(p)}"/>`) | ||
| .join('\n'); | ||
| return `${indent}<value class="variable">\n${pathElements}\n${indent}</value>`; | ||
| } | ||
| if (!inNamedValues) { | ||
| // D2: 'target' argument → class="uiTarget" (only for UiWithScreen / UiWithRow). | ||
| if (key === 'target' && (apiId.includes('UiWithScreen') || apiId.includes('UiWithRow'))) { | ||
| return `${indent}<value class="uiTarget" uri="${escapeXmlAttr(val)}"/>`; | ||
| } | ||
| // D2: 'locator' argument → class="uiLocator" (only for UiDoAction / UiAssert). | ||
| if (key === 'locator' && (apiId.includes('UiDoAction') || apiId.includes('UiAssert'))) { | ||
| return `${indent}<value class="uiLocator" uri="${escapeXmlAttr(val)}"/>`; | ||
| } | ||
|
Comment on lines
+283
to
+291
|
||
| } | ||
| return `${indent}<value class="value" valueClass="string">${escapeXmlContent(val)}</value>`; | ||
| } | ||
|
|
||
| function buildArgumentsXml(attributes: Record<string, string>, baseIndent = ' ', apiId = ''): string { | ||
| const entries = Object.entries(attributes); | ||
| if (entries.length === 0) return ''; | ||
| const argLines = entries | ||
| .map( | ||
| ([k, v]) => | ||
| .map(([k, v]) => { | ||
| const valueXml = buildArgumentValue(k, v, `${baseIndent} `, false, apiId); | ||
| return ( | ||
| `${baseIndent}<argument id="${escapeXmlAttr(k)}">\n` + | ||
| `${baseIndent} <value class="value" valueClass="string">${escapeXmlContent(v)}</value>\n` + | ||
| valueXml + '\n' + | ||
| `${baseIndent}</argument>` | ||
| ) | ||
| ); | ||
| }) | ||
| .join('\n'); | ||
| return `\n${baseIndent}<arguments>\n${argLines}\n${baseIndent}</arguments>\n${baseIndent.slice(0, -2)}`; | ||
| } | ||
|
|
||
| // D3: SetValues — all attributes become <namedValues> under a single 'values' argument. | ||
| function buildSetValuesXml(attributes: Record<string, string>, baseIndent: string): string { | ||
| const entries = Object.entries(attributes); | ||
| if (entries.length === 0) return ''; | ||
| const i = (n: number): string => baseIndent + ' '.repeat(n); | ||
| const namedValueLines = entries | ||
| .map(([name, val]) => { | ||
| const valueXml = buildArgumentValue(name, val, `${i(3)} `, true); | ||
| return `${i(3)}<namedValue name="${escapeXmlAttr(name)}">\n${valueXml}\n${i(3)}</namedValue>`; | ||
| }) | ||
| .join('\n'); | ||
| return ( | ||
| `\n${i(0)}<arguments>\n` + | ||
| `${i(0)}<argument id="values">\n` + | ||
| `${i(1)}<value class="valueList" mutable="Mutable">\n` + | ||
| `${i(2)}<namedValues>\n` + | ||
| namedValueLines + '\n' + | ||
| `${i(2)}</namedValues>\n` + | ||
| `${i(1)}</value>\n` + | ||
| `${i(0)}</argument>\n` + | ||
| `${i(0)}</arguments>\n` + | ||
| `${baseIndent.slice(0, -2)}` | ||
| ); | ||
| } | ||
|
|
||
| function buildFlatStepXml( | ||
| step: { api_id: string; name: string; attributes: Record<string, string> }, | ||
| testItemId: number, | ||
| indent: string | ||
| ): string { | ||
| const guid = randomUUID(); | ||
| const resolvedApiId = resolveApiId(step.api_id); | ||
| const argumentsXml = buildArgumentsXml(step.attributes, indent + ' '); | ||
| const baseIndent = indent + ' '; | ||
| // Use SetValues structure for any SetValues API (string-match mirrors the validator). | ||
| const argumentsXml = resolvedApiId.includes('SetValues') | ||
| ? buildSetValuesXml(step.attributes, baseIndent) | ||
| : buildArgumentsXml(step.attributes, baseIndent, resolvedApiId); | ||
| if (argumentsXml) { | ||
| return ( | ||
| `${indent}<apiCall guid="${guid}" apiId="${escapeXmlAttr(resolvedApiId)}"` + | ||
|
|
@@ -321,7 +406,7 @@ function buildUiWithScreenXml( | |
| ' </clauses>\n '; | ||
| return ( | ||
| ` <apiCall guid="${wrapperGuid}" apiId="${wrapperApiId}"` + | ||
| ` name="With page" testItemId="1">${buildArgumentsXml({ target: targetUri }).trimEnd()}${clausesXml}</apiCall>` | ||
| ` name="With page" testItemId="1">${buildArgumentsXml({ target: targetUri }, ' ', wrapperApiId).trimEnd()}${clausesXml}</apiCall>` | ||
| ); | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The argument convention table lists locator conversion as applying to UiDoAction/UiAssert only, but the validator rule UI-LOCATOR-001 also checks UiScrollToElement. To avoid confusion, consider adding UiScrollToElement to this table row (or align the validator scope).