sap-ai-dev-toolkit 0.9.9923 β†’ 0.9.9924

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/NOTICE CHANGED
@@ -3,7 +3,8 @@ Third-party notices for sap-ai-dev-toolkit
3
3
  This package includes patched VSP (vibing-steampunk) binaries built from upstream commit
4
4
  9886d2727f47506368b0a3c2f1c1766f1200f747. The BAS proxy-auth change is documented in
5
5
  patches/vsp-bas-proxy-auth.patch; the namespaced class-include EditSource fix is documented in
6
- patches/vsp-namespaced-class-include-edit.patch. The following upstream notice is reproduced from VSP:
6
+ patches/vsp-namespaced-class-include-edit.patch; the shared-logon fix for concurrent ADT requests is
7
+ documented in patches/vsp-single-logon.patch. The following upstream notice is reproduced from VSP:
7
8
 
8
9
  vibing-steampunk (vsp)
9
10
  Copyright (c) 2025-2026 Alice Vinogradova and contributors
package/README.md CHANGED
@@ -75,6 +75,16 @@ Use the attached destination-prefixed tools directly in chat. Do not launch `sap
75
75
 
76
76
  **Prerequisite:** Node.js 20 or newer. The pinned runtime ships with the package; no Go toolchain is installed or required.
77
77
 
78
+ ## πŸ†• What's new
79
+
80
+ | Version | Change |
81
+ | --- | --- |
82
+ | 0.9.9924 | **One SAP logon per runtime, shared by parallel tool calls.** AI assistants fire many tool calls at once; each used to reach SAP before the first security-session cookie came back, so every call logged on again and SM04 / the SAP GUI "already logged on" list filled with dozens of sessions. Now the first request logs on, the parallel ones wait for it and reuse its `SAP_SESSIONID` cookie (Basic, SSO and local SAP GUI entries alike); after an expired session the next burst again shares one fresh logon. |
83
+ | 0.9.9923 | **Oh My Pi (OMP) receives all twelve agents** as task agents (`~/.omp/agent/agents/<id>.md`, dispatched by kebab-case id) next to the twenty-two skills. **Four new companion MCP servers:** SAP Datasphere (`sap-ai-datasphere`, incl. BW β†’ Datasphere migration), SAP Build Process Automation (`sap-ai-process-automation`, all eight BPA APIs), SAP BTP platform administration (`sap-ai-btp`, on your own `btp login --sso`), and SAP Cloud ALM (`sap-ai-cloud-alm`), each with its own agent, skills and `--setup-*` wizard. |
84
+ | 0.9.9922 | `EditSource` no longer double-encodes namespaced class names (`/NS/CL_…`) on class-include writes. |
85
+ | 0.9.9921 | Simpler Integration Suite setup wizard: shorter intro, clearer status lines, and a single credential menu. |
86
+ | 0.9.9920 | **Integration Suite companion** (`sap-ai-integration`, since 0.9.9914) expanded across the lifecycle: API Management content, documentation, runtime and monitoring reads, confirmation-gated cleanup and delete tools, and asynchronous Cloud Integration monitoring. |
87
+
78
88
  ## πŸ€– Available agents and skills
79
89
 
80
90
  **Agents:** Twelve user-invocable custom agents are included. Select the best fit from the agent picker in your harness's chat (GitHub Copilot, Claude Code, or opencode), dispatch the matching OMP task agent by its kebab-case id, or use the matching Pi prompt command, as shown in Quick start. Harnesses without custom agents (OpenAI Codex, Cursor, Gemini CLI, Cline, Roo Code, Windsurf / Devin Desktop) get the same workflows as skills, for example `sap-integration-architect` and `integration-suite-delivery`.
@@ -100,11 +110,11 @@ Agents and skills install for the harnesses you pick during global installation:
100
110
 
101
111
  ## ✨ Your SAP development cockpit, inside your AI assistant
102
112
 
103
- `sap-ai-dev-toolkit` installs `sap-ai-dev`, discovers your BAS destinations or local SAP GUI systems, and exposes a curated SAP development toolset through MCP. Instead of manually switching between chat, terminal commands, repository searches, ADT screens, and SAP checks, describe the outcome you want and let the appropriate bundled agent coordinate the available tools.
113
+ `sap-ai-dev-toolkit` installs `sap-ai-dev`, discovers your BAS destinations or local SAP GUI systems, and exposes a curated SAP development toolset through MCP. Optional companion servers from the same package reach the rest of the landscape: HANA Cloud, Integration Suite, Datasphere, Build Process Automation, the BTP platform, and Cloud ALM. Instead of manually switching between chat, terminal commands, repository searches, ADT screens, cockpits, and SAP checks, describe the outcome you want and let the appropriate bundled agent coordinate the available tools.
104
114
 
105
115
  <p align="center"><strong>πŸ’¬ Request β†’ πŸ”Ž Inspect β†’ 🧠 Reason β†’ πŸ§‘β€πŸ’» Implement β†’ πŸ§ͺ Verify β†’ πŸ“‹ Report</strong></p>
106
116
 
107
- The workflow covers ABAP, CDS, RAP, repository analysis, table and query access, testing, ATC, application logs, transports, and controlled source changes. The live MCP tool list remains the source of truth for what your connected SAP system and active runtime mode expose.
117
+ The workflow covers ABAP, CDS, RAP, repository analysis, table and query access, testing, ATC, application logs, transports, and controlled source changes, plus integration, data, process and platform operations through the companions. The live MCP tool list remains the source of truth for what your connected SAP system and active runtime mode expose.
108
118
 
109
119
  ## 🌟 Feature highlights
110
120
 
@@ -116,6 +126,13 @@ The workflow covers ABAP, CDS, RAP, repository analysis, table and query access,
116
126
  | 🧩 | **CDS + RAP development** | Explore models and dependencies, then build RAP business objects and services with system context. |
117
127
  | πŸ—ƒοΈ | **Ground decisions in SAP data** | Inspect DDIC structures, read table contents, and run controlled ABAP SQL queries. |
118
128
  | ☁️ | **Read-only HANA Cloud inspection** | Inspect an environment-bound HDI schema, object metadata, and capped query results without exposing deployment credentials. |
129
+ | πŸ”Œ | **Integration Suite lifecycle** | Build, release, deploy, monitor, diagnose and recover Cloud Integration and API Management content with retained releases and confirmation-gated writes. |
130
+ | πŸ—„οΈ | **Datasphere and BW migration** | Model, deploy, load and administer Datasphere content; inventory BW models and reconcile BW against Datasphere after migration. |
131
+ | βš™οΈ | **Build Process Automation operations** | Inspect definitions and instances, unstick failed workflows, work user-task queues, test decisions, and trace business events across all eight BPA APIs. |
132
+ | πŸ›οΈ | **BTP platform administration** | Audit account hierarchy, entitlements and quotas, service instances and keys, destinations, trust and Cloud Connectors on your own `btp` CLI login. |
133
+ | πŸ“‘ | **Cloud ALM observability** | Find exception and alert hotspots per DEV/TEST/PROD tier and correlate them with live CPI message logs and SLG1. |
134
+ | 🧩 | **Agents in eleven AI harnesses** | Copilot, Claude Code, opencode and OMP get native agents, Pi gets prompt commands, and every harness gets the skills. |
135
+ | πŸ” | **One logon per runtime** | Parallel tool calls share a single SAP security session instead of opening one per call. |
119
136
  | ✍️ | **Edit ABAP safely** | Update supported source, stage reviewed change sets, create transport requests, and run syntax checks before requested activation. |
120
137
  | βœ… | **Quality built into the flow** | Pair local ABAP linting with SAP syntax checks, ABAP Unit, ATC, editor diagnostics, and formatting. |
121
138
  | 🐞 | **Debug with system context** | Investigate dumps, traces, application logs, runtime failures, capabilities, and installed components. |
@@ -197,17 +214,17 @@ The package ships with twelve custom agents plus twenty-two task-focused Agent S
197
214
 
198
215
  ## πŸ—ΊοΈ How it fits together
199
216
 
200
- Each selected BAS destination becomes its own isolated MCP server identity. The MCP client discovers the live tools and schemas, the add-on routes calls to the correct runtime process, and the process reaches SAP ADT through the selected BAS destination.
217
+ Each selected BAS destination or local SAP GUI system becomes its own isolated MCP server identity. The MCP client discovers the live tools and schemas, the add-on routes calls to the correct runtime process, and that process reaches SAP ADT through the selected route over one shared SAP logon. Companion servers (HANA, Integration Suite, Datasphere, Process Automation, BTP, Cloud ALM) are separate stdio MCP processes with their own user-only credentials, so an agent can combine them β€” for example a Cloud ALM alert, the matching CPI message log, and the ABAP SLG1 entry.
201
218
 
202
219
  ## 🧰 Optional full-stack companion MCP servers
203
220
 
204
- ABAP/RAP backend access is provided by this add-on's BAS proxy. For end-to-end SAP development, setup can also add managed companion MCP entries for frontend, CAP, browser validation, and read-only HANA inspection:
221
+ ABAP/RAP backend access is provided by this add-on's destination-scoped runtime. For end-to-end SAP development, setup can also add managed companion MCP entries for frontend, CAP, browser validation, read-only HANA inspection, Integration Suite, Datasphere, Build Process Automation, BTP platform administration, and Cloud ALM:
205
222
 
206
223
  ```sh
207
224
  sap-ai-dev --setup --tools
208
225
  ```
209
226
 
210
- The companion entries are optional and are launched through `npx` only when the MCP client starts them. They are marked as managed by `sap-ai-dev-toolkit`, so rerunning setup can update or remove them without touching unrelated MCP servers. The HANA, Integration Suite and Datasphere companions use `--ignore-scripts` so the toolkit postinstall wizard cannot run during MCP startup.
227
+ The companion entries are optional and are launched through `npx` only when the MCP client starts them. They are marked as managed by `sap-ai-dev-toolkit`, so rerunning setup can update or remove them without touching unrelated MCP servers. The companions shipped in this package (HANA, Integration Suite, Datasphere, Process Automation, BTP, Cloud ALM) use `--ignore-scripts` so the toolkit postinstall wizard cannot run during MCP startup. Each has a dedicated wizard: `sap-ai-dev --setup-integration`, `--setup-datasphere`, `--setup-process-automation`, `--setup-btp`, and `--setup-cloud-alm`.
211
228
 
212
229
  | MCP entry | Package | Launches | Use when the agent needs to |
213
230
  | --- | --- | --- | --- |
@@ -219,6 +236,7 @@ The companion entries are optional and are launched through `npx` only when the
219
236
  | `integration-suite` | `sap-ai-dev-toolkit` | `sap-ai-integration` | Build, deploy, undeploy, and monitor Cloud Integration content (iFlows, value mappings, script collections, security material, Partner Directory) and manage API Management providers, proxies, policies, products, and key value maps |
220
237
  | `datasphere` | `sap-ai-dev-toolkit` | `sap-ai-datasphere` | Model and administer SAP Datasphere: spaces, users, roles, database users, connections, modeling objects and packages, task chains, replication flows, Data Marketplace, consumption queries and Open SQL |
221
238
  | `process-automation` | `sap-ai-dev-toolkit` | `sap-ai-process-automation` | Inspect and operate SAP Build Process Automation across all eight BPA APIs: process definitions, workflow instances with contexts, logs and errors, stuck instances, user tasks and the user inbox, decisions, API triggers, automations, process visibility, administration jobs, process starts and business events |
239
+ | `sap-btp` | `sap-ai-dev-toolkit` | `sap-ai-btp` | Audit and operate the BTP core platform through your own `btp login --sso`: account hierarchy, entitlements and quotas, Service Manager instances and service keys, destinations, trust, and Cloud Connectors |
222
240
  | `cloud-alm` | `sap-ai-dev-toolkit` | `sap-ai-cloud-alm` | Observe and triage SAP landscapes with SAP Cloud ALM: health, exception and alert hotspots per tier, Integration & Exception Monitoring records, monitoring events with CPI and SLG1 handoffs, analytics, feature and transport status, and confirmation-gated alert closing |
223
241
 
224
242
  Recommended profiles:
@@ -1094,6 +1112,8 @@ If `GetSystemInfo` or `RunQuery` reports a CSRF-token fetch failure, the runtime
1094
1112
 
1095
1113
  **`MCP config ... contains invalid JSON`** usually means one of three things, all healed automatically by setup: the file starts with a byte-order mark (Windows PowerShell `>` redirection, `Set-Content`, or Notepad's "UTF-8 with BOM" mode β€” invisible in editors and terminals), it contains comments or trailing commas (VS Code reads `mcp.json` as JSONC), or setup raced an editor save and read a torn file (retried once before healing). Unrecoverable content is backed up as `mcp.json.<kind>-<timestamp>.bak` and replaced; the backup path is printed in the setup warning. On older versions, re-save the file as UTF-8 without BOM (VS Code: "Change File Encoding β†’ Save with Encoding β†’ UTF-8") and rerun `sap-ai-dev --setup`.
1096
1114
 
1115
+ **SAP shows "You are already logged on to the system in the following sessions" with dozens of entries** (or SM04 fills with HTTP sessions of your user): runtimes before 0.9.9924 opened one SAP security session per parallel tool call until the first session cookie came back. Upgrade and restart the MCP server; each runtime process now logs on once and shares that session. Sessions left by older processes expire with the system's HTTP session timeout, or end them with **Cancel all existing logons** at the next SAP GUI logon. Every MCP server start (editor reload, `/mcp reload`, a second editor) is still one process with one logon of its own.
1116
+
1097
1117
  If automatic runtime provisioning fails, check network access and the package's supported platform. You can install Go manually or set `GO_BINARY` as an explicit fallback. A trusted prebuilt executable can be supplied with `SAP_AI_DEV_TOOLKIT_BINARY`.
1098
1118
 
1099
1119
  ## πŸ§‘β€πŸ’» Development
@@ -1106,6 +1126,8 @@ npm test
1106
1126
 
1107
1127
  Install the checkout globally with `npm run install:local`, or use `npm run install:local -- --link` for a live workspace link. Verify without reinstalling using `npm run install:local -- --verify`: it checks the installed package version and, for a link, the actual workspace target, and refuses an outdated snapshot or a different checkout. After installation, exercise the installed executable with `sap-ai-integration --check`; npm-linked commands resolve their real entrypoint before starting the server.
1108
1128
 
1129
+ The ABAP runtime is upstream VSP pinned to one commit and patched with `patches/*.patch` (applied in the order listed in `scripts/build-vsp.mjs`). `npm run build:vsp` clones it into `.build/vibing-steampunk`, applies the patches, and builds `dist/`; run `go test ./pkg/adt/ ./internal/...` there to exercise a patch. To change a patch, commit the patched tree in `.build`, edit, and write `git diff` back to the patch file.
1130
+
1109
1131
  The package repository is [grknylmz/sap-ai-dev-toolkit](https://github.com/grknylmz/sap-ai-dev-toolkit).
1110
1132
 
1111
1133
  ## πŸš€ Publishing to npm
@@ -1,5 +1,5 @@
1
- 31902921b0d2b5349be8e392aae3632ca8c3dee26950ea30c412ec83271722f3 vsp-linux-x64
2
- df39b324fe419348f5157954d56c5f8d60e5bac28edc04a56e90c380b67de10d vsp-linux-arm64
3
- 754338fbbbe66260cac722677753b16a0a2f23127b947bf1f3a189044fa66c32 vsp-darwin-x64
4
- ac153052b5d02b2f19a536623c957369af3412f5bbe2eed0f6a43e8597ed0f1b vsp-darwin-arm64
5
- 722c6de352719dd9374932b56546189a75c29800e74da3bdcb0308b2034248ac vsp-win32-x64.exe
1
+ 8666b8126f460a6b1b3c64c5523eb5b942d2c1a795f406936cfb9e0f1c62e68c vsp-linux-x64
2
+ e920aa3fda3b3b5020893e0869737c8a2ca049db9eafdd9e19dfca021cefe24b vsp-linux-arm64
3
+ e56bf5a23cdbf889bff499a399b53fd247b2ef26b44e0d4affa6a20382303abd vsp-darwin-x64
4
+ 3c794b489af29237480a27353cb3e98af35de1d81ad03761304da10fdb5df89d vsp-darwin-arm64
5
+ 0c35ddd18c2edfb10486a056143ad438d79a737f55839e3adfb882b29438fa1a vsp-win32-x64.exe
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sap-ai-dev-toolkit",
3
- "version": "0.9.9923",
3
+ "version": "0.9.9924",
4
4
  "description": "SAP AI development toolkit for BAS, ABAP, RAP, CAP, HANA Cloud, Fiori, UI5, and MCP",
5
5
  "author": "Gurkan Yilmaz",
6
6
  "type": "module",
@@ -0,0 +1,258 @@
1
+ diff --git a/pkg/adt/http.go b/pkg/adt/http.go
2
+ index e420573..5386a26 100755
3
+ --- a/pkg/adt/http.go
4
+ +++ b/pkg/adt/http.go
5
+ @@ -12,6 +12,7 @@ import (
6
+ "os"
7
+ "strings"
8
+ "sync"
9
+ + "sync/atomic"
10
+ "time"
11
+ )
12
+
13
+ @@ -135,6 +136,10 @@ type Transport struct {
14
+ // from triggering simultaneous SAML dances.
15
+ reauthMu sync.Mutex
16
+ lastReauth time.Time
17
+ +
18
+ + // Logon single-flight: see http_logon_gate.go.
19
+ + logonSlot chan struct{}
20
+ + logonSettled atomic.Bool
21
+ }
22
+
23
+ // NewTransport creates a new Transport with the given configuration.
24
+ @@ -149,6 +154,7 @@ func NewTransportWithClient(cfg *Config, client HTTPDoer) *Transport {
25
+ t := &Transport{
26
+ config: cfg,
27
+ httpClient: client,
28
+ + logonSlot: make(chan struct{}, 1),
29
+ }
30
+ if cfg.Cache {
31
+ t.cache = newResponseCache(cfg.CacheStore, cfg.CacheTTL)
32
+ @@ -212,10 +218,10 @@ func (t *Transport) Request(ctx context.Context, path string, opts *RequestOptio
33
+ opts.Method = http.MethodGet
34
+ }
35
+ if t.cache == nil {
36
+ - return t.request(ctx, path, opts)
37
+ + return t.gatedRequest(ctx, path, opts)
38
+ }
39
+ if !cacheable(path, opts) {
40
+ - resp, err := t.request(ctx, path, opts)
41
+ + resp, err := t.gatedRequest(ctx, path, opts)
42
+ if isModifyingMethod(opts.Method) && !strings.HasPrefix(path, "/sap/bc/adt/datapreview/") {
43
+ t.cache.invalidate()
44
+ }
45
+ @@ -229,7 +235,7 @@ func (t *Transport) Request(ctx context.Context, path string, opts *RequestOptio
46
+ if resp, ok := t.cache.get(key); ok {
47
+ return resp, nil
48
+ }
49
+ - resp, err := t.request(ctx, path, opts)
50
+ + resp, err := t.gatedRequest(ctx, path, opts)
51
+ if err == nil && resp != nil && resp.StatusCode >= 200 && resp.StatusCode < 300 {
52
+ t.cache.put(key, resp)
53
+ }
54
+ @@ -1062,6 +1068,8 @@ func (t *Transport) resetCookieJar() {
55
+ }
56
+ if jar, err := cookiejar.New(nil); err == nil {
57
+ client.Jar = jar
58
+ + // The new jar holds no session: the next requests must share one logon again.
59
+ + t.logonSettled.Store(false)
60
+ }
61
+ }
62
+
63
+ diff --git a/pkg/adt/http_logon_gate.go b/pkg/adt/http_logon_gate.go
64
+ new file mode 100644
65
+ index 0000000..9e71c60
66
+ --- /dev/null
67
+ +++ b/pkg/adt/http_logon_gate.go
68
+ @@ -0,0 +1,76 @@
69
+ +package adt
70
+ +
71
+ +import (
72
+ + "context"
73
+ + "net/http"
74
+ + "net/url"
75
+ + "strings"
76
+ +)
77
+ +
78
+ +// Every request that reaches SAP without a security-session cookie is a new
79
+ +// logon: SAP authenticates it and opens a security session of its own, which
80
+ +// lingers in SM04 until it times out. MCP clients fan tool calls out in
81
+ +// parallel, so a cold transport used to send each of them without a cookie
82
+ +// and opened one session per call (dozens per burst for one user).
83
+ +//
84
+ +// gatedRequest lets exactly one request log on while the jar holds no session
85
+ +// cookie; the others wait for it and then reuse the cookie it brought back.
86
+ +// When that request succeeds without the server issuing a session cookie
87
+ +// (security sessions off, or a proxy chain that keeps them), the gate settles
88
+ +// open so requests are never serialized for nothing. Dropping the jar after an
89
+ +// expired session re-arms it.
90
+ +func (t *Transport) gatedRequest(ctx context.Context, path string, opts *RequestOptions) (*Response, error) {
91
+ + if t.logonSlot == nil || t.logonSettled.Load() || t.hasSessionCookie() {
92
+ + return t.request(ctx, path, opts)
93
+ + }
94
+ + select {
95
+ + case t.logonSlot <- struct{}{}:
96
+ + case <-ctx.Done():
97
+ + return nil, ctx.Err()
98
+ + }
99
+ + if t.logonSettled.Load() || t.hasSessionCookie() {
100
+ + <-t.logonSlot
101
+ + return t.request(ctx, path, opts)
102
+ + }
103
+ + resp, err := t.request(ctx, path, opts)
104
+ + if err == nil && !t.hasSessionCookie() {
105
+ + t.logonSettled.Store(true)
106
+ + }
107
+ + <-t.logonSlot
108
+ + return resp, err
109
+ +}
110
+ +
111
+ +// hasSessionCookie reports whether requests to the SAP system already carry
112
+ +// a security session: one the jar learned from the server, or one the caller
113
+ +// supplied for cookie authentication.
114
+ +func (t *Transport) hasSessionCookie() bool {
115
+ + t.cookiesMu.RLock()
116
+ + for name := range t.config.Cookies {
117
+ + if isSessionCookieName(name) {
118
+ + t.cookiesMu.RUnlock()
119
+ + return true
120
+ + }
121
+ + }
122
+ + t.cookiesMu.RUnlock()
123
+ + client, ok := t.httpClient.(*http.Client)
124
+ + if !ok || client.Jar == nil {
125
+ + return false
126
+ + }
127
+ + base, err := url.Parse(strings.TrimRight(t.config.BaseURL, "/") + "/sap/bc/adt/")
128
+ + if err != nil {
129
+ + return false
130
+ + }
131
+ + for _, cookie := range client.Jar.Cookies(base) {
132
+ + if isSessionCookieName(cookie.Name) {
133
+ + return true
134
+ + }
135
+ + }
136
+ + return false
137
+ +}
138
+ +
139
+ +// isSessionCookieName matches the ICF security-session cookie
140
+ +// (SAP_SESSIONID_<SID>_<client>). A MYSAPSSO2 logon ticket does not count:
141
+ +// every request presenting only the ticket opens another session.
142
+ +func isSessionCookieName(name string) bool {
143
+ + return strings.HasPrefix(name, "SAP_SESSIONID")
144
+ +}
145
+ diff --git a/pkg/adt/http_logon_gate_test.go b/pkg/adt/http_logon_gate_test.go
146
+ new file mode 100644
147
+ index 0000000..e973790
148
+ --- /dev/null
149
+ +++ b/pkg/adt/http_logon_gate_test.go
150
+ @@ -0,0 +1,108 @@
151
+ +package adt
152
+ +
153
+ +import (
154
+ + "context"
155
+ + "fmt"
156
+ + "net/http"
157
+ + "net/http/httptest"
158
+ + "sync"
159
+ + "sync/atomic"
160
+ + "testing"
161
+ + "time"
162
+ +)
163
+ +
164
+ +// sapSessionServer mimics ICF security sessions: a request without a live
165
+ +// SAP_SESSIONID cookie is a logon and, when issueCookies, gets a new one.
166
+ +type sapSessionServer struct {
167
+ + issueCookies bool
168
+ + logons atomic.Int32
169
+ + inFlight atomic.Int32
170
+ + peak atomic.Int32
171
+ + mu sync.Mutex
172
+ + live map[string]bool
173
+ +}
174
+ +
175
+ +func (s *sapSessionServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
176
+ + now := s.inFlight.Add(1)
177
+ + defer s.inFlight.Add(-1)
178
+ + for {
179
+ + peak := s.peak.Load()
180
+ + if now <= peak || s.peak.CompareAndSwap(peak, now) {
181
+ + break
182
+ + }
183
+ + }
184
+ + s.mu.Lock()
185
+ + cookie, err := r.Cookie("SAP_SESSIONID_T4D_100")
186
+ + if err != nil || !s.live[cookie.Value] {
187
+ + id := fmt.Sprintf("S%d", s.logons.Add(1))
188
+ + if s.issueCookies {
189
+ + s.live[id] = true
190
+ + http.SetCookie(w, &http.Cookie{Name: "SAP_SESSIONID_T4D_100", Value: id, Path: "/"})
191
+ + }
192
+ + }
193
+ + s.mu.Unlock()
194
+ + w.Header().Add("Set-Cookie", "sap-usercontext=sap-client=100; path=/")
195
+ + time.Sleep(30 * time.Millisecond)
196
+ + fmt.Fprint(w, "ok")
197
+ +}
198
+ +
199
+ +func newSessionTransport(t *testing.T, issueCookies bool) (*Transport, *sapSessionServer) {
200
+ + t.Helper()
201
+ + sap := &sapSessionServer{issueCookies: issueCookies, live: map[string]bool{}}
202
+ + server := httptest.NewServer(sap)
203
+ + t.Cleanup(server.Close)
204
+ + return NewTransport(NewConfig(server.URL, "EMSDI0845", "secret", WithClient("100"))), sap
205
+ +}
206
+ +
207
+ +func burst(t *testing.T, transport *Transport, n int) {
208
+ + t.Helper()
209
+ + var wg sync.WaitGroup
210
+ + errs := make(chan error, n)
211
+ + for i := range n {
212
+ + wg.Add(1)
213
+ + go func() {
214
+ + defer wg.Done()
215
+ + if _, err := transport.Request(context.Background(), fmt.Sprintf("/sap/bc/adt/oo/classes/ZCL_%d/source/main", i), nil); err != nil {
216
+ + errs <- err
217
+ + }
218
+ + }()
219
+ + }
220
+ + wg.Wait()
221
+ + close(errs)
222
+ + for err := range errs {
223
+ + t.Fatalf("request failed: %v", err)
224
+ + }
225
+ +}
226
+ +
227
+ +func TestTransport_ConcurrentColdRequestsShareOneLogon(t *testing.T) {
228
+ + transport, sap := newSessionTransport(t, true)
229
+ +
230
+ + burst(t, transport, 12)
231
+ + burst(t, transport, 12)
232
+ +
233
+ + if got := sap.logons.Load(); got != 1 {
234
+ + t.Fatalf("logons = %d, want 1: parallel tool calls must reuse one SAP security session", got)
235
+ + }
236
+ +}
237
+ +
238
+ +func TestTransport_LogonGateOpensWhenServerKeepsNoSession(t *testing.T) {
239
+ + transport, sap := newSessionTransport(t, false)
240
+ +
241
+ + burst(t, transport, 12)
242
+ +
243
+ + if peak := sap.peak.Load(); peak < 2 {
244
+ + t.Fatalf("peak concurrency = %d: without session cookies the gate must not serialize every request", peak)
245
+ + }
246
+ +}
247
+ +
248
+ +func TestTransport_LogonGateRearmsAfterJarReset(t *testing.T) {
249
+ + transport, sap := newSessionTransport(t, true)
250
+ +
251
+ + burst(t, transport, 6)
252
+ + transport.resetCookieJar()
253
+ + burst(t, transport, 6)
254
+ +
255
+ + if got := sap.logons.Load(); got != 2 {
256
+ + t.Fatalf("logons = %d, want 2 (one per session)", got)
257
+ + }
258
+ +}
@@ -33,7 +33,7 @@ async function main() {
33
33
  await mkdir(dirname(source), { recursive: true });
34
34
  await run('git', ['clone', '--filter=blob:none', upstream, source]);
35
35
  await run('git', ['-C', source, 'checkout', '--detach', commit]);
36
- for (const patchFile of ['vsp-bas-proxy-auth.patch', 'vsp-namespaced-class-include-edit.patch']) {
36
+ for (const patchFile of ['vsp-bas-proxy-auth.patch', 'vsp-namespaced-class-include-edit.patch', 'vsp-single-logon.patch']) {
37
37
  await run('patch', ['-p1', '-i', join(root, 'patches', patchFile)], { cwd: source });
38
38
  }
39
39
  await mkdir(dist, { recursive: true });