abapgit-agent 1.17.9 → 1.18.1

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.
@@ -91,29 +91,32 @@ class DebugSession {
91
91
  }
92
92
 
93
93
  /**
94
- * Restore the pinned SAP_SESSIONID into the HTTP client's cookie jar.
94
+ * Restore the pinned session-affinity cookies into the HTTP client's cookie jar.
95
95
  *
96
96
  * SAP ADT debug sessions are bound to a specific frozen ABAP work process
97
- * via the SAP_SESSIONID cookie. AdtHttp updates this cookie automatically
98
- * from every response's Set-Cookie header and replaces it on CSRF refresh
99
- * (401/403 retry). When the cookie rotates mid-session the next request
100
- * routes to a different ABAP session that has no debug state, causing
101
- * HTTP 400 "Service cannot be reached".
97
+ * via SAP_SESSIONID_{SID}_{CLIENT} or sap-contextid (depending on the system).
98
+ * AdtHttp updates cookies automatically from every Set-Cookie response header.
99
+ * When the session cookie rotates mid-session the next request routes to a
100
+ * different ABAP work process that has no debug state, causing HTTP 400.
102
101
  *
103
- * This method reverts any rotation that occurred since attach() by replacing
104
- * the current SAP_SESSIONID value with the one captured at attach time.
102
+ * This method restores all pinned cookies before every ADT call.
105
103
  * It is a no-op when called before attach() (pinnedSessionId is null).
106
104
  */
107
105
  _restorePinnedSession() {
108
106
  if (!this.pinnedSessionId || !this.http.cookies) return;
109
107
 
110
- // Replace whatever SAP_SESSIONID= value is currently in the cookie jar
111
- // with the pinned one. The cookie jar is a semicolon-separated string,
112
- // e.g. "SAP_SESSIONID=ABC123; sap-usercontext=xyz".
113
- this.http.cookies = this.http.cookies.replace(
114
- /SAP_SESSIONID=[^;]*/,
115
- `SAP_SESSIONID=${this.pinnedSessionId}`
116
- );
108
+ // Replace the session-affinity cookie(s) in the jar with the pinned ones.
109
+ // SAP uses either SAP_SESSIONID_{SID}_{CLIENT} or sap-contextid (or both)
110
+ // depending on the system configuration.
111
+ for (const pinned of this.pinnedSessionId) {
112
+ const cookieName = pinned.split('=')[0];
113
+ const re = new RegExp(cookieName.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&') + '=[^;]*');
114
+ if (re.test(this.http.cookies)) {
115
+ this.http.cookies = this.http.cookies.replace(re, pinned);
116
+ } else {
117
+ this.http.cookies = this.http.cookies + '; ' + pinned;
118
+ }
119
+ }
117
120
  }
118
121
 
119
122
  /**
@@ -131,7 +134,7 @@ class DebugSession {
131
134
  `&debuggingMode=user` +
132
135
  `&requestUser=${encodeURIComponent(requestUser)}`;
133
136
 
134
- const { body } = await this.http.post(url, '', {
137
+ const { body, headers: respHeaders } = await this.http.post(url, '', {
135
138
  contentType: 'application/vnd.sap.as+xml',
136
139
  headers: STATEFUL_HEADER
137
140
  });
@@ -145,14 +148,18 @@ class DebugSession {
145
148
  this.sessionId = debugSessionId;
146
149
  }
147
150
 
148
- // Pin the SAP_SESSIONID cookie that was active when we attached.
149
- // All subsequent stateful operations must present this exact cookie so
150
- // that SAP routes them to the same frozen ABAP work process.
151
+ // Pin the session-affinity cookie(s) captured after the attach POST.
152
+ // SAP uses SAP_SESSIONID_{SID}_{CLIENT} on some systems and sap-contextid
153
+ // on others (or both). Capture all candidates and restore before every call.
154
+ this.pinnedSessionId = [];
151
155
  if (this.http.cookies) {
152
- const match = this.http.cookies.match(/SAP_SESSIONID=([^;]*)/);
153
- if (match) this.pinnedSessionId = match[1];
156
+ for (const pair of this.http.cookies.split(';')) {
157
+ const name = pair.trim().split('=')[0].trim();
158
+ if (/^SAP_SESSIONID/i.test(name) || name === 'sap-contextid') {
159
+ this.pinnedSessionId.push(pair.trim());
160
+ }
161
+ }
154
162
  }
155
-
156
163
  return this.sessionId;
157
164
  }
158
165
 
@@ -298,6 +305,57 @@ class DebugSession {
298
305
  }
299
306
 
300
307
  const variables = parseVariables(varsXml);
308
+
309
+ // ── Step 3: field symbols — ADT never lists them in getChildVariables ─────
310
+ // getChildVariables silently omits FIELD-SYMBOLS declarations from the
311
+ // variable hierarchy. However, getVariables *does* resolve them when given
312
+ // the symbol name directly as the ID (e.g. ID=<LS>).
313
+ //
314
+ // Strategy: fetch the full source of the active stack frame, extract every
315
+ // FIELD-SYMBOLS <name> declaration, then request those names via getVariables
316
+ // and merge any non-empty results into the variable list.
317
+ //
318
+ // Uses GET-only stack fetch (no POST fallback) to avoid disturbing the POST
319
+ // mock sequence in unit tests. If the GET returns no frames, enrichment is skipped.
320
+ try {
321
+ const { body: stackBody } = await this.http.get(
322
+ '/sap/bc/adt/debugger/stack?emode=_&semanticURIs=true',
323
+ { accept: 'application/xml', headers: STATEFUL_HEADER }
324
+ );
325
+ const frames = parseStack(stackBody || '');
326
+ const frame = frames[0];
327
+ if (frame && frame.adtUri) {
328
+ const adtPath = frame.adtUri.split('#')[0];
329
+ const { body: srcBody } = await this.http.get(adtPath, { accept: 'text/plain' });
330
+ const fsNames = extractFieldSymbolNames(srcBody || '');
331
+ if (fsNames.length > 0) {
332
+ const fsBody =
333
+ `<?xml version="1.0" encoding="UTF-8" ?>` +
334
+ `<asx:abap xmlns:asx="http://www.sap.com/abapxml" version="1.0">` +
335
+ `<asx:values><DATA>` +
336
+ fsNames.map(n => `<STPDA_ADT_VARIABLE><ID>${escapeXml(n)}</ID></STPDA_ADT_VARIABLE>`).join('') +
337
+ `</DATA></asx:values></asx:abap>`;
338
+ const fsResp = await this.http.post(
339
+ '/sap/bc/adt/debugger?method=getVariables', fsBody, {
340
+ contentType: CT_VARS,
341
+ headers: { ...STATEFUL_HEADER, 'Accept': CT_VARS }
342
+ }
343
+ );
344
+ const fsVars = parseVariables(fsResp.body || '');
345
+ // Merge: add field symbols not already in the list
346
+ const existing = new Set(variables.map(v => v.name.toUpperCase()));
347
+ for (const fv of fsVars) {
348
+ if (fv.name && !existing.has(fv.name.toUpperCase())) {
349
+ variables.push(fv);
350
+ existing.add(fv.name.toUpperCase());
351
+ }
352
+ }
353
+ }
354
+ }
355
+ } catch (e) {
356
+ // Field symbol enrichment is best-effort — don't fail the whole call
357
+ }
358
+
301
359
  if (name) {
302
360
  return variables.filter(v => v.name.toUpperCase() === name.toUpperCase());
303
361
  }
@@ -601,7 +659,8 @@ class DebugSession {
601
659
  }
602
660
  const rowMeta = { metaType: fieldVar.metaType || '', tableLines: fieldVar.tableLines || 0 };
603
661
  const rows = await this.getVariableChildren(fieldVar.id, rowMeta);
604
- const rowTarget = rows.find(r => r.name === `[${rowIndex}]`);
662
+ // Row names may be '[N]' (nested) or 'VARNAME[N]' (top-level table rows)
663
+ const rowTarget = rows.find(r => r.name === `[${rowIndex}]` || r.name.endsWith(`[${rowIndex}]`));
605
664
  if (!rowTarget) {
606
665
  throw new Error(
607
666
  `Row [${rowIndex}] not found in '${fieldName}'. Table has ${rows.length} rows.`
@@ -613,9 +672,11 @@ class DebugSession {
613
672
 
614
673
  // Pattern 2: [N] — take row N from current (a table expanded in previous step)
615
674
  // Supports: x LO_FACTORY->MT_COMMAND_MAP->[1]
675
+ // Row names may be '[1]' (nested) or 'VARNAME[1]' (top-level table rows).
616
676
  const rowOnlyMatch = segment.match(/^\[(\d+)\]$/);
617
677
  if (rowOnlyMatch) {
618
- const rowTarget = children.find(r => r.name === `[${rowOnlyMatch[1]}]`);
678
+ const rowN = rowOnlyMatch[1];
679
+ const rowTarget = children.find(r => r.name === `[${rowN}]` || r.name.endsWith(`[${rowN}]`));
619
680
  if (!rowTarget) {
620
681
  throw new Error(
621
682
  `Row [${rowOnlyMatch[1]}] not found. Table has ${children.length} rows.`
@@ -737,6 +798,13 @@ function extractFriendlyName(rawName) {
737
798
  if (derefFieldMatch && derefFieldMatch[1] !== '*') return derefFieldMatch[1].toUpperCase();
738
799
  }
739
800
 
801
+ // Dash-separated struct field: VARNAME-FIELDNAME → FIELDNAME (plain local struct)
802
+ // This is the simple case: no opaque OO prefix, no dereference arrow.
803
+ // Must be checked after the -> case above to avoid matching deref IDs that also
804
+ // contain a dash (e.g. LR_REQUEST->*-FIELD would have been matched already).
805
+ const dashFieldMatch = rawName.match(/^[A-Z0-9_@]+(?:\[\d+\])?-([A-Z0-9_@]+)$/i);
806
+ if (dashFieldMatch) return dashFieldMatch[1].toUpperCase();
807
+
740
808
  return null;
741
809
  }
742
810
 
@@ -964,4 +1032,37 @@ function extractChildIds(xml, parents) {
964
1032
  return [...new Set(ids)];
965
1033
  }
966
1034
 
967
- module.exports = { DebugSession, parseVariables, parseStack, extractFriendlyName };
1035
+ /**
1036
+ * Extract field symbol names from ABAP source text.
1037
+ * Matches: FIELD-SYMBOLS <NAME> TYPE ... and FIELD-SYMBOLS: <A>, <B> ...
1038
+ * Returns names with angle brackets, e.g. ['<LS>', '<WA>'].
1039
+ */
1040
+ function extractFieldSymbolNames(source) {
1041
+ const names = [];
1042
+ // Match all <NAME> tokens that follow FIELD-SYMBOLS keyword (possibly on same or next line)
1043
+ // e.g. FIELD-SYMBOLS <ls> TYPE ...
1044
+ // FIELD-SYMBOLS: <ls> TYPE ..., <wa> TYPE ...
1045
+ const re = /field-symbols\s*:?\s*((?:<[^>]+>\s*(?:type[^,.\n]*)?,?\s*)+)/gi;
1046
+ let m;
1047
+ while ((m = re.exec(source)) !== null) {
1048
+ const segment = m[1];
1049
+ const nameRe = /<([^>]+)>/g;
1050
+ let nm;
1051
+ while ((nm = nameRe.exec(segment)) !== null) {
1052
+ names.push(`<${nm[1].toUpperCase()}>`);
1053
+ }
1054
+ }
1055
+ return [...new Set(names)];
1056
+ }
1057
+
1058
+ /**
1059
+ * Escape a string for safe embedding in XML text content.
1060
+ */
1061
+ function escapeXml(str) {
1062
+ return str
1063
+ .replace(/&/g, '&amp;')
1064
+ .replace(/</g, '&lt;')
1065
+ .replace(/>/g, '&gt;');
1066
+ }
1067
+
1068
+ module.exports = { DebugSession, parseVariables, parseStack, extractFriendlyName, extractFieldSymbolNames };