Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions docs/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -729,13 +729,19 @@ The tool's chip-level `title` — `Generate Test Case (full steps in one call)`

**Argument XML conventions** (automatically applied by the generator):

| Argument key / value pattern | Emitted XML class | API context |
| ------------------------------------ | ----------------------------------- | ----------------------- |
| `target` key | `class="uiTarget"` | UiWithScreen, UiWithRow |
| `locator` key | `class="uiLocator"` | UiDoAction, UiAssert |
| Value matches `{VarName}` or `{A.B}` | `class="variable"` + `<path>` | Any step |
| SetValues attributes | `class="valueList"/<namedValues>` | SetValues only |
| All other values | `class="value" valueClass="string"` | Any step |
| Argument key / value pattern | Emitted XML class | API context |
| --------------------------------------- | ------------------------------------- | ----------------------- |
| `target` key | `class="uiTarget"` | UiWithScreen, UiWithRow |
| `locator` key | `class="uiLocator"` | UiDoAction, UiAssert |
| Value matches `{VarName}` or `{A.B}` | `class="variable"` + `<path>` | Any step |
| SetValues attributes | `class="valueList"/<namedValues>` | SetValues only |
| Value `YYYY-MM-DDTHH:MM:SS…` (ISO-8601) | `class="value" valueClass="datetime"` | Any step |
| Value `YYYY-MM-DD` (ISO-8601) | `class="value" valueClass="date"` | Any step |
| Value `true` / `false` | `class="value" valueClass="boolean"` | Any step |
| Value matches `^-?\d+$` | `class="value" valueClass="integer"` | Any step |
| All other values | `class="value" valueClass="string"` | Any step |

`valueClass` is inferred automatically by `inferSalesforceValueClass(key, val, fieldTypeHint?)`. Detection order: explicit `fieldTypeHint` (wired in a follow-up tool surface — `field_type_hints` param) → ISO-8601 datetime → ISO-8601 date → boolean → integer → string. Provar runtime silently discards date fields emitted as `valueClass="string"`, so always pass date / datetime values in ISO-8601 form.
Comment on lines +741 to +744

AssertValues uses **flat** argument structure (`expectedValue`, `actualValue`, `comparisonType`) — not the `valueList`/namedValues format.

Expand Down
39 changes: 38 additions & 1 deletion src/mcp/tools/testCaseGenerate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ const TOOL_DESCRIPTION = [
'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="...".',
'valueClass auto-detection: argument values are typed automatically before XML emission. ' +
'ISO-8601 date "YYYY-MM-DD" → valueClass="date"; ISO-8601 datetime "YYYY-MM-DDTHH:MM:SS" → "datetime"; ' +
'"true"/"false" → "boolean"; integer-only string (e.g. "42", "-5") → "integer"; otherwise "string". ' +
'Pass dates / booleans / integers in those formats — Provar runtime silently discards date fields emitted as valueClass="string".',
Comment on lines +155 to +156
'Edit page objects: action=Edit targets require a compiled page object for the SF object. ' +
'If none exists in the project page-objects directory, the locator binding will fail at runtime. ' +
'For objects without a compiled Edit page object, use inline edit instead: sfIleActivate to activate the field, ' +
Expand Down Expand Up @@ -374,6 +378,35 @@ export function registerTestCaseGenerate(server: McpServer, config: ServerConfig

// ── XML builder ───────────────────────────────────────────────────────────────

// PDX-493 (H3): infer the Salesforce `valueClass` attribute that should be emitted on a
// `<value class="value" valueClass="..."/>` element from an argument's key + string value.
//
// Detection order:
// 1. Explicit `fieldTypeHint` (from `field_type_hints` param or `provar_org_describe` cache — wired
// in PDX-492 H2b) wins if provided.
// 2. ISO-8601 datetime → 'datetime' (e.g. "2026-05-19T10:30:00", with optional fractional/zone).
// 3. ISO-8601 date → 'date' (e.g. "2026-05-19").
// 4. Literal 'true' / 'false' → 'boolean'.
// 5. Integer-only string (optional leading '-') → 'integer'.
// 6. Else → 'string'.
//
// The `key` argument is reserved for future heuristics (e.g. SF naming conventions like *__c)
// but is intentionally not consulted today — explicit hints + value-shape regexes are the
// safer signal until org-describe is wired.
export function inferSalesforceValueClass(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
key: string,
val: string,
fieldTypeHint?: 'date' | 'datetime' | 'boolean' | 'integer' | 'string'
): 'date' | 'datetime' | 'boolean' | 'integer' | 'string' {
if (fieldTypeHint) return fieldTypeHint;
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(val)) return 'datetime';
if (/^\d{4}-\d{2}-\d{2}$/.test(val)) return 'date';
if (val === 'true' || val === 'false') return 'boolean';
if (/^-?\d+$/.test(val)) return 'integer';
return 'string';
}

// F1/F3: build class="compound" for strings that mix literal text with {VarName} tokens.
function buildCompoundValue(val: string, indent: string): string {
const i = `${indent} `;
Expand Down Expand Up @@ -423,7 +456,11 @@ function buildArgumentValue(key: string, val: string, indent: string, inNamedVal
return `${indent}<value class="uiLocator" uri="${escapeXmlAttr(val)}"/>`;
}
}
return `${indent}<value class="value" valueClass="string">${escapeXmlContent(val)}</value>`;
// PDX-493 (H3): infer valueClass for date / datetime / boolean / integer / string. The
// `fieldTypeHint` parameter on `inferSalesforceValueClass` is intentionally not threaded
// through here yet — it lands in PDX-492 (H2b) along with the `field_type_hints` tool input.
const inferred = inferSalesforceValueClass(key, val);
return `${indent}<value class="value" valueClass="${inferred}">${escapeXmlContent(val)}</value>`;
}

function buildArgumentsXml(attributes: Record<string, string>, baseIndent = ' ', apiId = ''): string {
Expand Down
185 changes: 184 additions & 1 deletion test/unit/mcp/testCaseGenerate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { describe, it, beforeEach, afterEach } from 'mocha';
import { registerTestCaseGenerate } from '../../../src/mcp/tools/testCaseGenerate.js';
import { registerTestCaseGenerate, inferSalesforceValueClass } from '../../../src/mcp/tools/testCaseGenerate.js';
import type { ServerConfig } from '../../../src/mcp/server.js';

// ── Minimal McpServer mock ─────────────────────────────────────────────────────
Expand Down Expand Up @@ -699,6 +699,189 @@ describe('provar_testcase_generate', () => {
});
});

// PDX-493 (H3): date/datetime/boolean/integer valueClass dispatch via inferSalesforceValueClass.
describe('PDX-493 — inferSalesforceValueClass helper', () => {
it('returns "datetime" for ISO-8601 datetime string', () => {
assert.equal(inferSalesforceValueClass('CloseDate', '2026-05-19T10:30:00'), 'datetime');
});

it('returns "datetime" for ISO-8601 datetime with fractional seconds + zone', () => {
assert.equal(inferSalesforceValueClass('CloseDate', '2026-05-19T10:30:00.123Z'), 'datetime');
});

it('returns "date" for ISO-8601 date string', () => {
assert.equal(inferSalesforceValueClass('CloseDate', '2026-05-19'), 'date');
});

it('returns "boolean" for "true"', () => {
assert.equal(inferSalesforceValueClass('IsActive', 'true'), 'boolean');
});

it('returns "boolean" for "false"', () => {
assert.equal(inferSalesforceValueClass('IsActive', 'false'), 'boolean');
});

it('returns "integer" for positive integer string', () => {
assert.equal(inferSalesforceValueClass('Quantity', '42'), 'integer');
});

it('returns "integer" for negative integer string', () => {
assert.equal(inferSalesforceValueClass('Delta', '-5'), 'integer');
});

it('returns "string" for plain text', () => {
assert.equal(inferSalesforceValueClass('Name', 'Acme Corp'), 'string');
});

it('returns "integer" not "date" for a short numeric string like "12"', () => {
// Edge case: the date regex requires the full ISO yyyy-mm-dd form, so a bare "12"
// is integer, not date. Guards against false-positive date detection on numeric IDs.
assert.equal(inferSalesforceValueClass('Code', '12'), 'integer');
Comment on lines +724 to +739
});

it('returns "string" for date-looking strings that miss the ISO format', () => {
// Confirms the regex is strict: month/day shape matters.
assert.equal(inferSalesforceValueClass('CloseDate', '2026/05/19'), 'string');
assert.equal(inferSalesforceValueClass('CloseDate', '05-19-2026'), 'string');
});

it('explicit fieldTypeHint wins over format detection', () => {
// Value looks like a string but the hint says it's a date — hint wins.
assert.equal(inferSalesforceValueClass('CloseDate', 'today', 'date'), 'date');
// Value looks like a date but the hint says string — hint wins (e.g. an external
// ID that happens to look like a date).
assert.equal(inferSalesforceValueClass('ExternalId', '2026-05-19', 'string'), 'string');
// Value is integer, hint says boolean — hint wins.
assert.equal(inferSalesforceValueClass('IsActive', '1', 'boolean'), 'boolean');
});
});

describe('PDX-493 — valueClass emission in generated XML', () => {
it('emits valueClass="date" for an ISO-8601 date string', () => {
const result = server.call('provar_testcase_generate', {
test_case_name: 'DateField',
steps: [
{
api_id: 'ApexCreateObject',
name: 'Create Opp',
attributes: { CloseDate: '2026-05-19' },
},
],
dry_run: true,
overwrite: false,
});

const xml = parseText(result)['xml_content'] as string;
assert.ok(
xml.includes('valueClass="date">2026-05-19</value>'),
`Expected valueClass="date" for ISO date; got: ${xml}`
);
});

it('emits valueClass="datetime" for an ISO-8601 datetime string', () => {
const result = server.call('provar_testcase_generate', {
test_case_name: 'DatetimeField',
steps: [
{
api_id: 'ApexCreateObject',
name: 'Create Event',
attributes: { StartTime: '2026-05-19T10:30:00' },
},
],
dry_run: true,
overwrite: false,
});

const xml = parseText(result)['xml_content'] as string;
assert.ok(
xml.includes('valueClass="datetime">2026-05-19T10:30:00</value>'),
`Expected valueClass="datetime" for ISO datetime; got: ${xml}`
);
});

it('emits valueClass="boolean" for "true" / "false" literals', () => {
const result = server.call('provar_testcase_generate', {
test_case_name: 'BoolField',
steps: [
{
api_id: 'ApexCreateObject',
name: 'Create Account',
attributes: { IsActive: 'true', IsDeleted: 'false' },
},
],
dry_run: true,
overwrite: false,
});

const xml = parseText(result)['xml_content'] as string;
assert.ok(
xml.includes('valueClass="boolean">true</value>'),
`Expected valueClass="boolean" for "true"; got: ${xml}`
);
assert.ok(
xml.includes('valueClass="boolean">false</value>'),
`Expected valueClass="boolean" for "false"; got: ${xml}`
);
});

it('emits valueClass="integer" for an integer-only string', () => {
const result = server.call('provar_testcase_generate', {
test_case_name: 'IntField',
steps: [
{
api_id: 'ApexCreateObject',
name: 'Create Opp',
attributes: { Quantity: '42' },
},
],
dry_run: true,
overwrite: false,
});

const xml = parseText(result)['xml_content'] as string;
assert.ok(xml.includes('valueClass="integer">42</value>'), `Expected valueClass="integer" for "42"; got: ${xml}`);
});

it('emits valueClass="integer" for a negative integer string', () => {
const result = server.call('provar_testcase_generate', {
test_case_name: 'NegIntField',
steps: [
{
api_id: 'ApexCreateObject',
name: 'Create Adjustment',
attributes: { Delta: '-5' },
},
],
dry_run: true,
overwrite: false,
});

const xml = parseText(result)['xml_content'] as string;
assert.ok(xml.includes('valueClass="integer">-5</value>'), `Expected valueClass="integer" for "-5"; got: ${xml}`);
});

it('emits valueClass="string" for plain text', () => {
const result = server.call('provar_testcase_generate', {
test_case_name: 'StringField',
steps: [
{
api_id: 'ApexCreateObject',
name: 'Create Account',
attributes: { Name: 'Acme Corp' },
},
],
dry_run: true,
overwrite: false,
});

const xml = parseText(result)['xml_content'] as string;
assert.ok(
xml.includes('valueClass="string">Acme Corp</value>'),
`Expected valueClass="string" for "Acme Corp"; got: ${xml}`
);
});
});

describe('target_uri — non-SF page object (ui:) nesting', () => {
it('wraps steps in UiWithScreen when target_uri uses ?pageId= format', () => {
const result = server.call('provar_testcase_generate', {
Expand Down
Loading