abapgit-agent 1.17.8 → 1.18.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/README.md +1 -0
- package/abap/CLAUDE.md +150 -25
- package/abap/CLAUDE.slim.md +5 -4
- package/abap/guidelines/abaplint.md +2 -0
- package/abap/guidelines/cds-testing.md +12 -0
- package/abap/guidelines/cds.md +7 -0
- package/abap/guidelines/debug-dump.md +4 -0
- package/abap/guidelines/debug-session.md +27 -2
- package/abap/guidelines/run-probe-classes.md +43 -0
- package/abap/guidelines/string-template.md +66 -1
- package/bin/abapgit-agent +3 -2
- package/package.json +10 -6
- package/src/commands/debug.js +156 -119
- package/src/commands/guide.js +17 -0
- package/src/commands/inspect.js +7 -4
- package/src/commands/pull.js +32 -14
- package/src/commands/unit.js +2 -1
- package/src/commands/view.js +1 -1
- package/src/config.js +13 -1
- package/src/utils/abap-http.js +136 -252
- package/src/utils/adt-http.js +134 -216
- package/src/utils/debug-daemon.js +57 -48
- package/src/utils/debug-session.js +126 -25
|
@@ -91,29 +91,32 @@ class DebugSession {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
|
-
* Restore the pinned
|
|
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
|
|
98
|
-
* from every
|
|
99
|
-
*
|
|
100
|
-
*
|
|
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
|
|
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
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
149
|
-
//
|
|
150
|
-
//
|
|
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
|
|
153
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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, '&')
|
|
1064
|
+
.replace(/</g, '<')
|
|
1065
|
+
.replace(/>/g, '>');
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
module.exports = { DebugSession, parseVariables, parseStack, extractFriendlyName, extractFieldSymbolNames };
|