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.
Files changed (48) hide show
  1. package/dist/adapters/remoteMcp/adapter.js +1 -1
  2. package/dist/adapters/simplifyqa/adapter.d.ts +48 -4
  3. package/dist/adapters/simplifyqa/adapter.d.ts.map +1 -1
  4. package/dist/adapters/simplifyqa/adapter.js +585 -63
  5. package/dist/adapters/simplifyqa/adapter.js.map +1 -1
  6. package/dist/adapters/simplifyqa/mapper.d.ts +2 -2
  7. package/dist/adapters/simplifyqa/mapper.d.ts.map +1 -1
  8. package/dist/adapters/simplifyqa/mapper.js +257 -64
  9. package/dist/adapters/simplifyqa/mapper.js.map +1 -1
  10. package/dist/adapters/types.d.ts +10 -1
  11. package/dist/adapters/types.d.ts.map +1 -1
  12. package/dist/domain/types.d.ts +46 -4
  13. package/dist/domain/types.d.ts.map +1 -1
  14. package/dist/domain/validate.d.ts.map +1 -1
  15. package/dist/domain/validate.js +5 -1
  16. package/dist/domain/validate.js.map +1 -1
  17. package/dist/drafts/store.d.ts +9 -0
  18. package/dist/drafts/store.d.ts.map +1 -1
  19. package/dist/drafts/store.js +12 -0
  20. package/dist/drafts/store.js.map +1 -1
  21. package/dist/initAgents.d.ts.map +1 -1
  22. package/dist/initAgents.js +48 -16
  23. package/dist/initAgents.js.map +1 -1
  24. package/dist/server.d.ts.map +1 -1
  25. package/dist/server.js +6 -1
  26. package/dist/server.js.map +1 -1
  27. package/dist/tools/recording.tools.d.ts.map +1 -1
  28. package/dist/tools/recording.tools.js +234 -0
  29. package/dist/tools/recording.tools.js.map +1 -1
  30. package/dist/tools/run.tools.d.ts +4 -0
  31. package/dist/tools/run.tools.d.ts.map +1 -1
  32. package/dist/tools/run.tools.js +360 -114
  33. package/dist/tools/run.tools.js.map +1 -1
  34. package/dist/tools/schemas.d.ts +41 -49
  35. package/dist/tools/schemas.d.ts.map +1 -1
  36. package/dist/tools/schemas.js +19 -1
  37. package/dist/tools/schemas.js.map +1 -1
  38. package/dist/tools/tcEditPublisher.d.ts +20 -0
  39. package/dist/tools/tcEditPublisher.d.ts.map +1 -0
  40. package/dist/tools/tcEditPublisher.js +46 -0
  41. package/dist/tools/tcEditPublisher.js.map +1 -0
  42. package/dist/tools/testcase.tools.d.ts.map +1 -1
  43. package/dist/tools/testcase.tools.js +303 -10
  44. package/dist/tools/testcase.tools.js.map +1 -1
  45. package/dist/tools/testdata.tools.d.ts.map +1 -1
  46. package/dist/tools/testdata.tools.js +60 -6
  47. package/dist/tools/testdata.tools.js.map +1 -1
  48. 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":"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"}
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
- ...(tc.description ? { description: tc.description } : {}),
706
- ...(tc.preconditions ? { prerequisite: tc.preconditions } : {}),
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
- originalExecutinType: 'AUTOMATION',
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
- reviewApproveStatus: '',
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
- if (!resolution) {
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: union of description ${tokens} + keys of step.recordedParam. `recordedParam`
821
- // on the wire is keyed by NUMERIC paramId (JSON keys → strings), NOT by name.
822
- const paramIds = [];
823
- const recordedParamOut = {};
824
- const seen = new Set();
825
- const addParam = (name, value) => {
826
- if (seen.has(name))
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
- seen.add(name);
949
+ seenNames.add(name);
829
950
  const id = paramIdByName.get(name);
830
951
  if (id == null)
831
952
  return;
832
- paramIds.push(id);
953
+ registerId(id);
833
954
  if (value !== undefined)
834
- recordedParamOut[String(id)] = value;
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
- addParam(name, tc.data?.[name] ?? s.value);
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
- addParam(name, value);
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
- moduleId,
845
- type: 'TC',
846
- stepType: 'NORMAL_STATEMENT',
847
- seq: i + 1,
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
- technologyType,
852
- actionId: resolution.actionId,
853
- objectId: resolution.objectId,
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
- attachments: [],
858
- conditions: { ifstring: '', children: [] },
859
- loop: { loopType: 'for', loopIteration: null },
1047
+ seq: i + 1,
860
1048
  stepLevelActions: {
861
- skip: s.flags?.skip ?? false,
862
- captureScreenshot: s.flags?.screenshot ?? false,
863
- continueOnFailure: s.flags?.continueOnFailure ?? false,
864
- skippedReport: false,
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
- ...(technologyType === 'WEB' ? { browserType: s.browserType ?? 'CHROME' } : {}),
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
  });