common-qa-mcp 0.8.0 → 0.9.0
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/adapters/remoteMcp/adapter.js +1 -1
- package/dist/adapters/simplifyqa/adapter.d.ts +48 -4
- package/dist/adapters/simplifyqa/adapter.d.ts.map +1 -1
- package/dist/adapters/simplifyqa/adapter.js +585 -63
- package/dist/adapters/simplifyqa/adapter.js.map +1 -1
- package/dist/adapters/simplifyqa/mapper.d.ts +2 -2
- package/dist/adapters/simplifyqa/mapper.d.ts.map +1 -1
- package/dist/adapters/simplifyqa/mapper.js +257 -64
- package/dist/adapters/simplifyqa/mapper.js.map +1 -1
- package/dist/adapters/types.d.ts +10 -1
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/domain/types.d.ts +46 -4
- package/dist/domain/types.d.ts.map +1 -1
- package/dist/domain/validate.d.ts.map +1 -1
- package/dist/domain/validate.js +5 -1
- package/dist/domain/validate.js.map +1 -1
- package/dist/drafts/store.d.ts +9 -0
- package/dist/drafts/store.d.ts.map +1 -1
- package/dist/drafts/store.js +12 -0
- package/dist/drafts/store.js.map +1 -1
- package/dist/initAgents.d.ts.map +1 -1
- package/dist/initAgents.js +48 -16
- package/dist/initAgents.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +6 -1
- package/dist/server.js.map +1 -1
- package/dist/tools/recording.tools.d.ts.map +1 -1
- package/dist/tools/recording.tools.js +234 -0
- package/dist/tools/recording.tools.js.map +1 -1
- package/dist/tools/run.tools.d.ts +4 -0
- package/dist/tools/run.tools.d.ts.map +1 -1
- package/dist/tools/run.tools.js +360 -114
- package/dist/tools/run.tools.js.map +1 -1
- package/dist/tools/schemas.d.ts +41 -49
- package/dist/tools/schemas.d.ts.map +1 -1
- package/dist/tools/schemas.js +19 -1
- package/dist/tools/schemas.js.map +1 -1
- package/dist/tools/tcEditPublisher.d.ts +20 -0
- package/dist/tools/tcEditPublisher.d.ts.map +1 -0
- package/dist/tools/tcEditPublisher.js +46 -0
- package/dist/tools/tcEditPublisher.js.map +1 -0
- package/dist/tools/testcase.tools.d.ts.map +1 -1
- package/dist/tools/testcase.tools.js +303 -10
- package/dist/tools/testcase.tools.js.map +1 -1
- package/dist/tools/testdata.tools.d.ts.map +1 -1
- package/dist/tools/testdata.tools.js +60 -6
- package/dist/tools/testdata.tools.js.map +1 -1
- package/package.json +1 -1
|
@@ -70,8 +70,8 @@ export type BuildTestcasePayloadInputs = {
|
|
|
70
70
|
* `objectId` the adapter resolved before this call. `objectId` is null when the action does
|
|
71
71
|
* not need one. Ignored on manual saves. */
|
|
72
72
|
stepResolutions?: Array<{
|
|
73
|
-
actionId: number;
|
|
74
|
-
objectId: number | null;
|
|
73
|
+
actionId: number | null;
|
|
74
|
+
objectId: number | null | undefined;
|
|
75
75
|
}>;
|
|
76
76
|
};
|
|
77
77
|
/** Neutral Step.stepType → SQA `technologyType` on the step + `tcType` on the TC.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mapper.d.ts","sourceRoot":"","sources":["../../../src/adapters/simplifyqa/mapper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,cAAc,EACzD,eAAe,EAAE,WAAW,EAAE,aAAa,EAAE,UAAU,EAAE,cAAc,EAAE,QAAQ,EAAE,aAAa,EAAE,IAAI,EACtG,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAChG,MAAM,uBAAuB,CAAC;AAU/B,wBAAgB,SAAS,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,GAAG,EAAE,CAM1D;AAED;;6FAE6F;AAC7F,wBAAgB,SAAS,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,GAAG,EAAE,GAAG,GAAG,CAAC,EAAE,GAAG;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAI/E;AAED,wBAAgB,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAQjE;AAED;+FAC+F;AAC/F,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAI5C;AAED;;oGAEoG;AACpG,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,GAAG,EAAE,EAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,EAAE,CAc3F;AAED,uFAAuF;AACvF,wBAAgB,WAAW,CAAC,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC,UAAU,CAAC,CAI5D;AAWD,eAAO,MAAM,oBAAoB,EAAE,WAAW,CAAC,MAAM,CAGnD,CAAC;AAEH,eAAO,MAAM,qBAAqB,EAAE,WAAW,CAAC,MAAM,CAEpD,CAAC;
|
|
1
|
+
{"version":3,"file":"mapper.d.ts","sourceRoot":"","sources":["../../../src/adapters/simplifyqa/mapper.ts"],"names":[],"mappings":"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"}
|
|
@@ -109,6 +109,32 @@ const SWITCH_KINDS = new Set([
|
|
|
109
109
|
'SWITCH_TO_API', 'SWITCH_TO_DB', 'SWITCH_TO_WEB', 'SWITCH_TO_MAINFRAME',
|
|
110
110
|
'SWITCH_TO_DESKTOP', 'SWITCH_TO_SAP', 'SWITCH_TO_IOS', 'SWITCH_TO_ANDROID',
|
|
111
111
|
]);
|
|
112
|
+
/** Round-trip whitelist: raw backend fields we preserve verbatim on the neutral Step so save
|
|
113
|
+
* re-emits control-flow / function / switch / api structure intact. Subset of the backend's
|
|
114
|
+
* step joi allow-list (testcaseValidator.js:244-306) MINUS fields the mapper always computes
|
|
115
|
+
* fresh (id, parentId, versionId, seq, actionId, objectId, description, expectedResult,
|
|
116
|
+
* technologyType, projectId, moduleId, type, deleted, recordedParam, parameterIds,
|
|
117
|
+
* objectParameterIds, browserType, stepLevelActions). Duplicated (not imported from adapter.ts)
|
|
118
|
+
* to avoid a circular import — mapper is a leaf module. */
|
|
119
|
+
const STEP_ROUND_TRIP_KEYS = [
|
|
120
|
+
'sharedFnProjectId', 'apiData', 'lastActiveTab', 'userstoryId',
|
|
121
|
+
'stepType', 'functionId', 'fnVersionId', 'sharedFnProjectName',
|
|
122
|
+
'parameterizedParamIds', 'attachments', 'compareFiles', 'compareFileType',
|
|
123
|
+
'deviceId', 'ignoreObject', 'captureScreenshot', 'screenshotEnabled',
|
|
124
|
+
'conditions', 'skipParamInReport', 'paramLoopduration', 'showGroups',
|
|
125
|
+
'isConditional', 'if', 'else', 'loop', 'isLoop', 'loopType', 'startFor', 'endFor',
|
|
126
|
+
'loopDuration', 'switchToAndroidData', 'switchToIOSData',
|
|
127
|
+
'isDebug', 'manualStepsTopreview', 'isMigrated', 'customFields',
|
|
128
|
+
];
|
|
129
|
+
function pickRoundTrip(s) {
|
|
130
|
+
const out = {};
|
|
131
|
+
if (!s || typeof s !== 'object')
|
|
132
|
+
return out;
|
|
133
|
+
for (const k of STEP_ROUND_TRIP_KEYS)
|
|
134
|
+
if (s[k] !== undefined)
|
|
135
|
+
out[k] = s[k];
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
112
138
|
/** Squash a step's `recordedParam: { <paramId>: value }` map into a single readable cell
|
|
113
139
|
* ("packageName=us.zoom, activity=..."). Names would be nicer than ids, but the id→name
|
|
114
140
|
* resolution requires the project parameter dictionary — deferred; the raw id is unambiguous
|
|
@@ -152,6 +178,29 @@ export function fromSqaStep(s, fallbackSeq) {
|
|
|
152
178
|
objectId = str(s.object, 'id');
|
|
153
179
|
}
|
|
154
180
|
const objectName = s?.object && typeof s.object === 'object' ? str(s.object, 'name') : undefined;
|
|
181
|
+
// ponytail: preserve backend parameterIds so round-trip save doesn't wipe FK linkages that
|
|
182
|
+
// weren't derived from description ${tokens} (e.g. a "Go to URL" step with parameterIds:[841]
|
|
183
|
+
// even though the description has no token).
|
|
184
|
+
const sourceParameterIds = Array.isArray(s?.parameterIds)
|
|
185
|
+
? s.parameterIds.filter((n) => typeof n === 'number' && Number.isFinite(n))
|
|
186
|
+
: [];
|
|
187
|
+
const sourceObjectParameterIds = Array.isArray(s?.objectParameterIds)
|
|
188
|
+
? s.objectParameterIds.filter((n) => typeof n === 'number' && Number.isFinite(n))
|
|
189
|
+
: [];
|
|
190
|
+
// ponytail: preserve backend recordedParam as-is (id-keyed) so a save that doesn't touch values
|
|
191
|
+
// re-emits them intact. LLM patches via Step.recordedParam (name-keyed) override at save time.
|
|
192
|
+
const sourceRecordedParam = {};
|
|
193
|
+
if (s?.recordedParam && typeof s.recordedParam === 'object') {
|
|
194
|
+
for (const [k, v] of Object.entries(s.recordedParam)) {
|
|
195
|
+
if (typeof v === 'string')
|
|
196
|
+
sourceRecordedParam[k] = v;
|
|
197
|
+
else if (typeof v === 'number' || typeof v === 'boolean')
|
|
198
|
+
sourceRecordedParam[k] = String(v);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// ponytail: preserve every backend field that's on the joi allow-list but that the mapper
|
|
202
|
+
// doesn't recompute. This is what makes IF/FOR/WHILE/FUNCTION/SWITCH/API steps round-trip.
|
|
203
|
+
const sourceRaw = pickRoundTrip(s);
|
|
155
204
|
const step = {
|
|
156
205
|
n: seq,
|
|
157
206
|
description,
|
|
@@ -159,6 +208,10 @@ export function fromSqaStep(s, fallbackSeq) {
|
|
|
159
208
|
stepType,
|
|
160
209
|
...(sid ? { sourceId: sid } : {}),
|
|
161
210
|
...(expectedResult ? { expectedResult } : {}),
|
|
211
|
+
...(sourceParameterIds.length ? { sourceParameterIds } : {}),
|
|
212
|
+
...(sourceObjectParameterIds.length ? { sourceObjectParameterIds } : {}),
|
|
213
|
+
...(Object.keys(sourceRecordedParam).length ? { sourceRecordedParam } : {}),
|
|
214
|
+
...(Object.keys(sourceRaw).length ? { sourceRaw } : {}),
|
|
162
215
|
};
|
|
163
216
|
if (objectId && objectId.toLowerCase() !== 'null') {
|
|
164
217
|
step.objectRef = { id: objectId, ...(objectName ? { name: objectName } : {}) };
|
|
@@ -252,6 +305,7 @@ export function toTestCase(doc) {
|
|
|
252
305
|
steps,
|
|
253
306
|
...(versions.length ? { versions } : {}),
|
|
254
307
|
...(currentVersionId ? { currentVersionId } : {}),
|
|
308
|
+
...(typeof doc?.updateVersion === 'number' ? { updateVersion: doc.updateVersion } : {}),
|
|
255
309
|
};
|
|
256
310
|
}
|
|
257
311
|
/** Overwrite `tc.steps` with the mapped collection-endpoint rows when non-empty. */
|
|
@@ -670,6 +724,17 @@ export function deriveTcType(tc) {
|
|
|
670
724
|
/** SQA-empty `mailersList` shape — the frontend and joi validator expect the three arrays, not
|
|
671
725
|
* a bare `{}`. Verified against the real create payload for TC-584. */
|
|
672
726
|
const EMPTY_MAILERS = { users: [], nonUsers: [], groups: [] };
|
|
727
|
+
/** Locate the version entry the neutral TestCase currently points at, so update payloads carry
|
|
728
|
+
* the REAL versionId (not `''`, which the backend interprets as "create a new version" —
|
|
729
|
+
* see tcfn.service.js:233 — and produces the empty-steps-after-save symptom). Returns an empty
|
|
730
|
+
* object when unresolved; callers then fall back to `id:''` (correct for CREATE). */
|
|
731
|
+
function currentVersionFor(tc) {
|
|
732
|
+
const id = tc.currentVersionId;
|
|
733
|
+
if (!id || !Array.isArray(tc.versions))
|
|
734
|
+
return {};
|
|
735
|
+
const v = tc.versions.find((entry) => entry.id === id);
|
|
736
|
+
return v ? { id: v.id, name: v.name, ...(typeof v.updateVersion === 'number' ? { updateVersion: v.updateVersion } : {}) } : {};
|
|
737
|
+
}
|
|
673
738
|
/**
|
|
674
739
|
* Assemble the `testcaseObj` payload for POST/PUT /tm/testcase (multipart). Branches on
|
|
675
740
|
* `tc.category`: manual → `steps.manualSteps` (legacy behavior), automated → `steps.automationSteps`
|
|
@@ -691,30 +756,65 @@ export function buildTestcasePayload(inputs) {
|
|
|
691
756
|
*/
|
|
692
757
|
function buildManualPayload(inputs) {
|
|
693
758
|
const { projectId, moduleId, userstoryId, existingId, tc, paramIdByName } = inputs;
|
|
759
|
+
// ponytail: emit the same field set as the SQA UI payload, always — validators + $set on the
|
|
760
|
+
// backend depend on presence, even for empty defaults. Loader-mutator-saver semantics.
|
|
694
761
|
const tcfnObj = {
|
|
695
762
|
...(existingId ? { id: Number.parseInt(existingId, 10) } : {}),
|
|
696
763
|
name: tc.title ?? '',
|
|
764
|
+
userstoryId: userstoryId ?? null,
|
|
765
|
+
defectId: [],
|
|
766
|
+
suiteId: [],
|
|
767
|
+
description: tc.description ?? '',
|
|
697
768
|
projectId,
|
|
698
|
-
moduleId,
|
|
699
|
-
...(userstoryId != null ? { userstoryId } : {}),
|
|
700
|
-
obsolete: false,
|
|
701
|
-
...(existingId ? { deleted: false } : {}),
|
|
702
769
|
tcType: 'WEB',
|
|
703
770
|
executionType: 'MANUAL',
|
|
771
|
+
obsolete: false,
|
|
772
|
+
linkedIssues: [],
|
|
773
|
+
moduleId,
|
|
774
|
+
prerequisite: tc.preconditions ?? '',
|
|
704
775
|
customFields: {},
|
|
705
|
-
|
|
706
|
-
|
|
776
|
+
tags: [],
|
|
777
|
+
mailersList: EMPTY_MAILERS,
|
|
778
|
+
updateVersion: tc.updateVersion ?? 0,
|
|
779
|
+
...(existingId ? { deleted: false } : {}),
|
|
707
780
|
};
|
|
781
|
+
// On UPDATE, use the CURRENT version's real id/name/updateVersion so the backend updates
|
|
782
|
+
// that version row instead of minting a new one (empty `id` triggers create at tcfn.service.js:233).
|
|
783
|
+
const curVer = existingId ? currentVersionFor(tc) : {};
|
|
708
784
|
const versionsDetails = {
|
|
709
|
-
name: 'v1.0',
|
|
710
|
-
id: '',
|
|
785
|
+
name: curVer.name ?? 'v1.0',
|
|
786
|
+
id: curVer.id ?? '',
|
|
787
|
+
syncONOFF: '',
|
|
788
|
+
manualStepsTopreview: false,
|
|
789
|
+
updateVersion: curVer.updateVersion ?? 0,
|
|
790
|
+
executionType: 'MANUAL',
|
|
791
|
+
reviewApproveStatus: '',
|
|
792
|
+
functionProjectIds: [projectId],
|
|
711
793
|
...(existingId ? { deleted: false } : {}),
|
|
712
794
|
};
|
|
713
795
|
const manualSteps = [];
|
|
714
796
|
const steps = Array.isArray(tc.steps) ? tc.steps : [];
|
|
715
797
|
steps.forEach((s, i) => {
|
|
716
798
|
const desc = s.description ?? '';
|
|
799
|
+
const paramIds = [];
|
|
800
|
+
const recordedParam = {};
|
|
801
|
+
for (const name of extractParamNames(desc)) {
|
|
802
|
+
const id = paramIdByName.get(name);
|
|
803
|
+
if (id == null)
|
|
804
|
+
continue;
|
|
805
|
+
paramIds.push(id);
|
|
806
|
+
recordedParam[String(id)] = paramDefault(name, tc.data, undefined);
|
|
807
|
+
}
|
|
717
808
|
const step = {
|
|
809
|
+
deleted: false,
|
|
810
|
+
// ponytail: pass through the backend step uuid on round-trip. Missing `id` sends every step
|
|
811
|
+
// down the backend's CREATE branch (tcfn.service.js:355), which mints a fresh uuid and
|
|
812
|
+
// orphans the original row — duplication instead of update.
|
|
813
|
+
...(s.sourceId ? { id: s.sourceId } : {}),
|
|
814
|
+
// ponytail: same rationale as buildAutomationPayload — parentId + versionId are part of the
|
|
815
|
+
// update filter at tcfn.service.js:406; omitting them causes the upsert to insert an orphan.
|
|
816
|
+
...(existingId ? { parentId: Number.parseInt(existingId, 10) } : {}),
|
|
817
|
+
...(existingId && curVer.id ? { versionId: curVer.id } : {}),
|
|
718
818
|
projectId,
|
|
719
819
|
moduleId,
|
|
720
820
|
type: 'TC',
|
|
@@ -723,20 +823,11 @@ function buildManualPayload(inputs) {
|
|
|
723
823
|
description: desc,
|
|
724
824
|
expectedResult: s.expectedResult ?? '',
|
|
725
825
|
executionType: 'MANUAL',
|
|
826
|
+
// ponytail: always emit — matches UI payload shape. UI sends `[]`/`{}` on every step, backend
|
|
827
|
+
// joi validators + $set semantics assume presence.
|
|
828
|
+
parameterIds: paramIds,
|
|
829
|
+
recordedParam,
|
|
726
830
|
};
|
|
727
|
-
const paramIds = [];
|
|
728
|
-
const recordedParam = {};
|
|
729
|
-
for (const name of extractParamNames(desc)) {
|
|
730
|
-
const id = paramIdByName.get(name);
|
|
731
|
-
if (id == null)
|
|
732
|
-
continue;
|
|
733
|
-
paramIds.push(id);
|
|
734
|
-
recordedParam[String(id)] = paramDefault(name, tc.data, undefined);
|
|
735
|
-
}
|
|
736
|
-
if (paramIds.length > 0) {
|
|
737
|
-
step['parameterIds'] = paramIds;
|
|
738
|
-
step['recordedParam'] = recordedParam;
|
|
739
|
-
}
|
|
740
831
|
manualSteps.push(step);
|
|
741
832
|
});
|
|
742
833
|
return {
|
|
@@ -777,32 +868,42 @@ function buildAutomationPayload(inputs) {
|
|
|
777
868
|
`tc.executionPlatform:{id:<deviceSerial>, name, technologyType:'${tcType}', type:'REAL_DEVICE'|'CLOUD_DEVICE'}. ` +
|
|
778
869
|
`For Android, call mobile_list_devices to see the available devices.`);
|
|
779
870
|
}
|
|
871
|
+
// ponytail: mirror the real UI payload field-for-field (see the working PUT captured on 2026-09-17
|
|
872
|
+
// for TC-635). Loader-mutator-saver semantics — always emit, empty defaults included.
|
|
780
873
|
const tcfnObj = {
|
|
781
874
|
...(existingId ? { id: Number.parseInt(existingId, 10) } : {}),
|
|
782
875
|
name: tc.title ?? '',
|
|
876
|
+
userstoryId: userstoryId ?? null,
|
|
877
|
+
defectId: [],
|
|
878
|
+
suiteId: [],
|
|
879
|
+
description: tc.description ?? '',
|
|
783
880
|
projectId,
|
|
784
|
-
moduleId,
|
|
785
|
-
...(userstoryId != null ? { userstoryId } : {}),
|
|
786
|
-
obsolete: false,
|
|
787
|
-
...(existingId ? { deleted: false } : {}),
|
|
788
881
|
tcType,
|
|
789
882
|
executionType: 'AUTOMATION',
|
|
790
|
-
|
|
883
|
+
obsolete: false,
|
|
884
|
+
linkedIssues: [],
|
|
885
|
+
moduleId,
|
|
886
|
+
prerequisite: tc.preconditions ?? '',
|
|
791
887
|
customFields: {},
|
|
792
|
-
mailersList: EMPTY_MAILERS,
|
|
793
|
-
suiteId: [],
|
|
794
|
-
defectId: [],
|
|
795
888
|
tags: [],
|
|
796
|
-
|
|
889
|
+
mailersList: EMPTY_MAILERS,
|
|
890
|
+
originalExecutinType: 'AUTOMATION',
|
|
891
|
+
updateVersion: tc.updateVersion ?? 0,
|
|
892
|
+
...(existingId ? { deleted: false } : {}),
|
|
797
893
|
...(tc.executionPlatform ? { executionPlatform: tc.executionPlatform } : {}),
|
|
798
|
-
...(tc.description ? { description: tc.description } : {}),
|
|
799
|
-
...(tc.preconditions ? { prerequisite: tc.preconditions } : {}),
|
|
800
894
|
};
|
|
895
|
+
// On UPDATE, use the CURRENT version's real id/name/updateVersion so the backend updates
|
|
896
|
+
// that version row instead of minting a new one (empty `id` triggers create at tcfn.service.js:233,
|
|
897
|
+
// which orphans all saved steps under a fresh version and leaves the UI's view empty).
|
|
898
|
+
const curVer = existingId ? currentVersionFor(tc) : {};
|
|
801
899
|
const versionsDetails = {
|
|
802
|
-
name: 'v1.0',
|
|
803
|
-
id: '',
|
|
900
|
+
name: curVer.name ?? 'v1.0',
|
|
901
|
+
id: curVer.id ?? '',
|
|
804
902
|
syncONOFF: '',
|
|
903
|
+
manualStepsTopreview: false,
|
|
904
|
+
updateVersion: curVer.updateVersion ?? 0,
|
|
805
905
|
executionType: 'AUTOMATION',
|
|
906
|
+
reviewApproveStatus: '',
|
|
806
907
|
functionProjectIds: [projectId],
|
|
807
908
|
...(existingId ? { deleted: false } : {}),
|
|
808
909
|
};
|
|
@@ -812,58 +913,150 @@ function buildAutomationPayload(inputs) {
|
|
|
812
913
|
const desc = s.description ?? '';
|
|
813
914
|
const technologyType = TECH_BY_STEPTYPE[s.stepType] ?? tcType;
|
|
814
915
|
const resolution = stepResolutions?.[i];
|
|
815
|
-
|
|
916
|
+
// ponytail: control-flow / function / switch / manual steps have a null-actionId slot from
|
|
917
|
+
// resolveAutomationSteps — they don't need action resolution to be valid.
|
|
918
|
+
const isControlFlow = s.stepType === 'control_flow' || s.stepType === 'function' || s.stepType === 'function_end' || s.stepType === 'switch' || s.stepType === 'manual';
|
|
919
|
+
if (!isControlFlow && !resolution) {
|
|
816
920
|
throw new Error(`Automation TC "${tc.title}" step ${i + 1}: missing action resolution — the adapter did ` +
|
|
817
921
|
'not resolve `actionId` for this step. Check that the neutral Step.actionType maps to a ' +
|
|
818
922
|
'known SQA action for this technology.');
|
|
819
923
|
}
|
|
820
|
-
// parameterIds
|
|
821
|
-
//
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
924
|
+
// parameterIds + recordedParam resolution order:
|
|
925
|
+
// 1. Seed from backend-preserved sourceParameterIds / sourceRecordedParam (round-trip
|
|
926
|
+
// intact — a save that doesn't touch anything re-emits the same values).
|
|
927
|
+
// 2. Description ${tokens}: register the param id + a DEFAULT value from tc.data. Never
|
|
928
|
+
// overrides an already-seeded id — a token default must not wipe a real recorded value.
|
|
929
|
+
// 3. LLM patches (s.recordedParam, name-keyed): resolve name → id and OVERRIDE. This is
|
|
930
|
+
// how "change the URL parameter to X" via tc_draft_update flows through to the wire.
|
|
931
|
+
const paramIds = [...(s.sourceParameterIds ?? [])];
|
|
932
|
+
const seenIds = new Set(paramIds);
|
|
933
|
+
const recordedParamOut = { ...(s.sourceRecordedParam ?? {}) };
|
|
934
|
+
const registerId = (id) => {
|
|
935
|
+
if (!seenIds.has(id)) {
|
|
936
|
+
paramIds.push(id);
|
|
937
|
+
seenIds.add(id);
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
const setValue = (id, value, override) => {
|
|
941
|
+
const key = String(id);
|
|
942
|
+
if (override || !(key in recordedParamOut))
|
|
943
|
+
recordedParamOut[key] = value;
|
|
944
|
+
};
|
|
945
|
+
const seenNames = new Set();
|
|
946
|
+
const addByName = (name, value, override) => {
|
|
947
|
+
if (seenNames.has(name))
|
|
827
948
|
return;
|
|
828
|
-
|
|
949
|
+
seenNames.add(name);
|
|
829
950
|
const id = paramIdByName.get(name);
|
|
830
951
|
if (id == null)
|
|
831
952
|
return;
|
|
832
|
-
|
|
953
|
+
registerId(id);
|
|
833
954
|
if (value !== undefined)
|
|
834
|
-
|
|
955
|
+
setValue(id, value, override);
|
|
835
956
|
};
|
|
957
|
+
// Description tokens seed defaults but don't override
|
|
836
958
|
for (const name of extractParamNames(desc))
|
|
837
|
-
|
|
959
|
+
addByName(name, tc.data?.[name] ?? s.value, false);
|
|
960
|
+
// LLM patches override
|
|
838
961
|
if (s.recordedParam) {
|
|
839
962
|
for (const [name, value] of Object.entries(s.recordedParam))
|
|
840
|
-
|
|
963
|
+
addByName(name, value, true);
|
|
964
|
+
}
|
|
965
|
+
const objectParamIds = [...(s.sourceObjectParameterIds ?? [])];
|
|
966
|
+
// ponytail: spread the round-tripped raw fields FIRST so control-flow / function / switch /
|
|
967
|
+
// api / attachments / conditions / loop / functionId / fnVersionId / apiData / etc. all
|
|
968
|
+
// survive. Explicit computed fields below override where they overlap.
|
|
969
|
+
const rawBase = (s.sourceRaw ?? {});
|
|
970
|
+
const rawStepType = typeof rawBase['stepType'] === 'string' ? rawBase['stepType'] : undefined;
|
|
971
|
+
// stepLevelActions: merge — keep any extra flags from source, our four win where they exist.
|
|
972
|
+
const rawSLA = rawBase['stepLevelActions'] ?? {};
|
|
973
|
+
// ponytail: wire stepType resolution ladder for FRESH steps (no sourceRaw):
|
|
974
|
+
// 1. Round-trip sourceRaw.stepType wins if present (IF/FOR/FUNCTION preserved from DB)
|
|
975
|
+
// 2. Function step → 'FUNCTION' | 'FUNCTION_END'
|
|
976
|
+
// 3. Control-flow step → uppercase(controlFlow.kind) (e.g. "IF", "FOR_START", "WHILE")
|
|
977
|
+
// 4. Everything else → 'NORMAL_STATEMENT'
|
|
978
|
+
// This is what makes `add_step({stepType: 'control_flow', controlFlow: {kind: 'IF'}})` and
|
|
979
|
+
// `add_step({stepType: 'function', functionCall: {testCaseId: '99'}})` actually work.
|
|
980
|
+
let wireStepType = rawStepType;
|
|
981
|
+
if (!wireStepType) {
|
|
982
|
+
if (s.stepType === 'function')
|
|
983
|
+
wireStepType = 'FUNCTION';
|
|
984
|
+
else if (s.stepType === 'function_end')
|
|
985
|
+
wireStepType = 'FUNCTION_END';
|
|
986
|
+
else if (s.stepType === 'control_flow' && s.controlFlow?.kind)
|
|
987
|
+
wireStepType = String(s.controlFlow.kind).toUpperCase();
|
|
988
|
+
else
|
|
989
|
+
wireStepType = 'NORMAL_STATEMENT';
|
|
841
990
|
}
|
|
991
|
+
// FUNCTION step: derive functionId + fnVersionId from neutral functionCall when no sourceRaw
|
|
992
|
+
// (LLM authored a fresh function-call step via add_step).
|
|
993
|
+
const wireFunctionId = (typeof rawBase['functionId'] === 'number' ? rawBase['functionId'] : undefined)
|
|
994
|
+
?? (s.functionCall?.testCaseId ? Number.parseInt(s.functionCall.testCaseId, 10) : undefined);
|
|
995
|
+
const wireFnVersionId = (typeof rawBase['fnVersionId'] === 'string' ? rawBase['fnVersionId'] : undefined)
|
|
996
|
+
?? s.functionCall?.versionId;
|
|
997
|
+
const wireSharedFnProjectId = (typeof rawBase['sharedFnProjectId'] === 'number' ? rawBase['sharedFnProjectId'] : undefined)
|
|
998
|
+
?? (s.functionCall?.sharedProjectId ? Number.parseInt(s.functionCall.sharedProjectId, 10) : undefined);
|
|
999
|
+
// Control-flow step: derive conditions.ifstring + loopDuration from neutral controlFlow.
|
|
1000
|
+
const wireConditions = rawBase['conditions']
|
|
1001
|
+
?? (s.controlFlow?.conditionText ? { ifstring: s.controlFlow.conditionText, children: [] } : { ifstring: '', children: [] });
|
|
1002
|
+
const wireLoopDuration = (typeof rawBase['loopDuration'] === 'number' ? rawBase['loopDuration'] : undefined)
|
|
1003
|
+
?? s.controlFlow?.maxIterations;
|
|
842
1004
|
const step = {
|
|
1005
|
+
...rawBase,
|
|
1006
|
+
deleted: false,
|
|
1007
|
+
// ponytail: pass through the backend step uuid on round-trip. Without `id`, every step
|
|
1008
|
+
// routes to tcfn.service.js:355 CREATE branch, mints a new uuid, orphans the original row.
|
|
1009
|
+
...(s.sourceId ? { id: s.sourceId } : {}),
|
|
1010
|
+
// ponytail: parentId + versionId REQUIRED on update — tcfn.service.js:406 builds the update
|
|
1011
|
+
// filter `{id, customerId, projectId, parentId, type}` and without parentId it becomes
|
|
1012
|
+
// `{parentId: undefined}` → misses the existing row → upsert inserts a new orphan step
|
|
1013
|
+
// with no parentId. UI query `{parentId: tcId, versionId, deleted:false}` then finds nothing.
|
|
1014
|
+
...(existingId ? { parentId: Number.parseInt(existingId, 10) } : {}),
|
|
1015
|
+
...(existingId && curVer.id ? { versionId: curVer.id } : {}),
|
|
843
1016
|
projectId,
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
1017
|
+
// Only emit actionId/objectId for steps that resolve to an action (straight-line automation).
|
|
1018
|
+
// Control-flow / function / switch steps have no action; the sourceRaw spread carries their
|
|
1019
|
+
// functionId / conditions / loop instead.
|
|
1020
|
+
...(resolution && resolution.actionId != null
|
|
1021
|
+
? {
|
|
1022
|
+
actionId: resolution.actionId,
|
|
1023
|
+
// ponytail: undefined objectId means "preserve DB value" (existing step with no round-trip
|
|
1024
|
+
// objectRef.id). null is a valid emitted value (fresh step where the action doesn't need one).
|
|
1025
|
+
objectId: resolution.objectId === undefined ? null : resolution.objectId,
|
|
1026
|
+
}
|
|
1027
|
+
: {}),
|
|
1028
|
+
// Round-tripped attachments (source) preserved via spread above; default to [] if fresh.
|
|
1029
|
+
attachments: Array.isArray(rawBase['attachments']) ? rawBase['attachments'] : [],
|
|
1030
|
+
conditions: wireConditions,
|
|
848
1031
|
description: desc,
|
|
849
|
-
expectedResult: s.expectedResult ?? '',
|
|
850
1032
|
executionType: 'AUTOMATION',
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
1033
|
+
expectedResult: s.expectedResult ?? '',
|
|
1034
|
+
loop: rawBase['loop'] ?? { loopType: 'for', loopIteration: null },
|
|
1035
|
+
...(wireLoopDuration !== undefined ? { loopDuration: wireLoopDuration } : {}),
|
|
1036
|
+
moduleId,
|
|
1037
|
+
// Function-call fields — emitted only for FUNCTION steps (rawBase spread already carries
|
|
1038
|
+
// them for round-tripped functions; these derivations cover fresh add_step callers).
|
|
1039
|
+
...(wireFunctionId !== undefined && Number.isFinite(wireFunctionId) ? { functionId: wireFunctionId } : {}),
|
|
1040
|
+
...(wireFnVersionId ? { fnVersionId: wireFnVersionId } : {}),
|
|
1041
|
+
...(wireSharedFnProjectId !== undefined && Number.isFinite(wireSharedFnProjectId) ? { sharedFnProjectId: wireSharedFnProjectId } : {}),
|
|
1042
|
+
objectParameterIds: objectParamIds,
|
|
1043
|
+
// ponytail: always emit — matches UI payload shape. Sending `[]` / `{}` is how UI signals
|
|
1044
|
+
// "this step has no params/values"; the joi validator + backend $set semantics assume presence.
|
|
854
1045
|
parameterIds: paramIds,
|
|
855
|
-
objectParameterIds: [],
|
|
856
1046
|
recordedParam: recordedParamOut,
|
|
857
|
-
|
|
858
|
-
conditions: { ifstring: '', children: [] },
|
|
859
|
-
loop: { loopType: 'for', loopIteration: null },
|
|
1047
|
+
seq: i + 1,
|
|
860
1048
|
stepLevelActions: {
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
1049
|
+
...rawSLA,
|
|
1050
|
+
skip: s.flags?.skip ?? rawSLA['skip'] ?? false,
|
|
1051
|
+
captureScreenshot: s.flags?.screenshot ?? rawSLA['captureScreenshot'] ?? false,
|
|
1052
|
+
continueOnFailure: s.flags?.continueOnFailure ?? rawSLA['continueOnFailure'] ?? false,
|
|
1053
|
+
skippedReport: rawSLA['skippedReport'] ?? false,
|
|
865
1054
|
},
|
|
866
|
-
|
|
1055
|
+
// Wire stepType: sourceRaw wins on round-trip; for fresh steps, translated from neutral
|
|
1056
|
+
// stepType + controlFlow.kind + function marker (see wireStepType computation above).
|
|
1057
|
+
stepType: wireStepType,
|
|
1058
|
+
technologyType,
|
|
1059
|
+
type: 'TC',
|
|
867
1060
|
};
|
|
868
1061
|
automationSteps.push(step);
|
|
869
1062
|
});
|