sap-ai-dev-toolkit 0.5.5 → 0.5.6

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 (57) hide show
  1. package/.github/agents/abap-developer.agent.md +3 -3
  2. package/.github/agents/abap-runtime-debugger.agent.md +3 -3
  3. package/.github/agents/hana-cloud-hdi-specialist.agent.md +1 -1
  4. package/.github/agents/rap-service-developer.agent.md +1 -1
  5. package/.github/agents/sap-solution-architect.agent.md +1 -1
  6. package/.github/skills/abap-debugging/SKILL.md +3 -3
  7. package/.github/skills/abap-development/SKILL.md +1 -1
  8. package/.github/skills/abap-runtime-analysis/SKILL.md +3 -3
  9. package/.github/skills/abap-testing-quality/SKILL.md +1 -1
  10. package/.github/skills/cds-development/SKILL.md +1 -1
  11. package/.github/skills/clean-core-extensibility/SKILL.md +1 -1
  12. package/.github/skills/hana-cloud-inspection/SKILL.md +1 -1
  13. package/.github/skills/hana-cloud-native-development/SKILL.md +2 -2
  14. package/.github/skills/hana-cloud-validation/SKILL.md +1 -1
  15. package/.github/skills/rap-development/SKILL.md +1 -1
  16. package/.github/skills/rap-service-delivery/SKILL.md +1 -1
  17. package/.github/skills/sap-sdlc-orchestration/SKILL.md +1 -1
  18. package/.github/skills/sap-standard-api-analysis/SKILL.md +1 -1
  19. package/.github/skills/sap-transport-release/SKILL.md +1 -1
  20. package/README.md +6 -6
  21. package/inventory.md +179 -0
  22. package/package.json +2 -2
  23. package/scripts/build-vsp.mjs +2 -1
  24. package/scripts/postinstall.mjs +14 -16
  25. package/src/bas-destination-relay.mjs +25 -3
  26. package/src/bas-discovery.mjs +9 -3
  27. package/src/binary.mjs +101 -17
  28. package/src/cf-destination.mjs +55 -1
  29. package/src/credentials-store.mjs +0 -10
  30. package/src/engineering-tools.mjs +32 -10
  31. package/src/hana-tools.mjs +9 -2
  32. package/src/launcher.mjs +8 -11
  33. package/src/mcp-config.mjs +19 -1
  34. package/src/mcp-proxy.mjs +230 -76
  35. package/src/redact.mjs +28 -0
  36. package/src/setup.mjs +30 -1
  37. package/tools.md +1 -1
  38. package/test/bas-destination-relay.test.mjs +0 -415
  39. package/test/cf-connectivity.test.mjs +0 -287
  40. package/test/cf-destination.test.mjs +0 -307
  41. package/test/cf-runtime.test.mjs +0 -527
  42. package/test/copilot-assets.test.mjs +0 -63
  43. package/test/copilot-content.test.mjs +0 -249
  44. package/test/credentials-store.test.mjs +0 -187
  45. package/test/discovery.test.mjs +0 -282
  46. package/test/fixtures/fake-vsp.mjs +0 -266
  47. package/test/hana-config.test.mjs +0 -168
  48. package/test/hana-inspector-stdio.test.mjs +0 -44
  49. package/test/hana-tools.test.mjs +0 -206
  50. package/test/launcher.test.mjs +0 -401
  51. package/test/live-s4h.test.mjs +0 -204
  52. package/test/mcp-config-cf.test.mjs +0 -475
  53. package/test/mcp-proxy.test.mjs +0 -604
  54. package/test/pty.mjs +0 -8
  55. package/test/setup-cf.test.mjs +0 -489
  56. package/test/setup.test.mjs +0 -417
  57. package/test/terminal-ui.test.mjs +0 -47
@@ -1,489 +0,0 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
4
- import { tmpdir } from 'node:os';
5
- import { join } from 'node:path';
6
- import { fileURLToPath } from 'node:url';
7
- import { generatedServerName, installMcpConfig } from '../src/mcp-config.mjs';
8
- import { destinationTable } from '../scripts/postinstall.mjs';
9
- import { readCredentials, resolveCredentialsPath, storeDestinationCredentials } from '../src/credentials-store.mjs';
10
- import { credentialKeyForDestination } from '../src/credential-overrides.mjs';
11
- import { spawnWithPty } from './pty.mjs';
12
-
13
- const setupModule = fileURLToPath(new URL('../src/setup.mjs', import.meta.url));
14
-
15
- function cloudFoundryDestination({ name, serverName, instanceGuid, instanceName, keyName }) {
16
- return {
17
- source: 'cloud-foundry',
18
- name,
19
- serverName,
20
- client: '100',
21
- authentication: 'Basic',
22
- proxyType: 'Internet',
23
- probe: { status: 'available', available: true, httpStatus: 401 },
24
- cf: {
25
- spaceGuid: 'space-one',
26
- destinationInstanceGuid: instanceGuid,
27
- destinationInstanceName: instanceName,
28
- destinationKeyName: keyName
29
- }
30
- };
31
- }
32
-
33
- function cloudFoundryOnPremiseDestination() {
34
- const destination = cloudFoundryDestination({
35
- name: 'shared-name',
36
- serverName: 'cf:space-one:cf-destination:shared-name',
37
- instanceGuid: 'cf-destination',
38
- instanceName: 'destination-service',
39
- keyName: 'destination-key'
40
- });
41
- destination.authentication = 'BasicAuthentication';
42
- destination.proxyType = 'OnPremise';
43
- destination.cf.connectivityInstanceGuid = 'cf-connectivity';
44
- destination.cf.connectivityInstanceName = 'connectivity-service';
45
- destination.cf.connectivityKeyName = 'connectivity-key';
46
- return destination;
47
- }
48
-
49
- async function makeFixture(t, { version = 'cf version 8.18.0', space = 'space-one', deleteMode = 'success' } = {}) {
50
- const directory = await mkdtemp(join(tmpdir(), 'bas-cf-setup-'));
51
- const bin = join(directory, 'bin');
52
- await mkdir(bin);
53
- const configPath = join(directory, 'mcp.json');
54
- const callLog = join(directory, 'cf-calls.jsonl');
55
- const discoveryLog = join(directory, 'cf-discovery.json');
56
- const cfPath = join(bin, 'cf');
57
- await writeFile(cfPath, `#!/usr/bin/env node
58
- import { appendFileSync } from 'node:fs';
59
- const args = process.argv.slice(2);
60
- appendFileSync(process.env.CF_CALL_LOG, JSON.stringify(args) + '\\n');
61
- if (args[0] === 'version') process.stdout.write(process.env.TEST_CF_VERSION + '\\n');
62
- else if (args[0] === 'target') {
63
- process.stdout.write(process.env.TEST_CF_SPACE === 'not-targeted' ? 'space: <none>\\n' : 'space: ' + process.env.TEST_CF_SPACE + '\\n');
64
- } else if (args[0] === 'space' && args.at(-1) === '--guid') {
65
- if (process.env.TEST_CF_SPACE === 'not-targeted') { process.stderr.write('No targeted space\\n'); process.exitCode = 1; }
66
- else process.stdout.write(process.env.TEST_CF_SPACE + '\\n');
67
- } else if (args[0] === 'delete-service-key' && process.env.TEST_CF_DELETE_MODE === 'missing') {
68
- process.stderr.write('Service key not found\\n');
69
- process.exitCode = 1;
70
- } else if (args[0] === 'delete-service-key') process.stdout.write('deleted\\n');
71
- else process.exitCode = 1;
72
- `);
73
- await chmod(cfPath, 0o755);
74
- const harness = join(directory, 'setup-runner.mjs');
75
- await writeFile(harness, `import { writeFile } from 'node:fs/promises';
76
- import { runSetup } from ${JSON.stringify(setupModule)};
77
- await runSetup({
78
- env: process.env,
79
- discover: async () => JSON.parse(process.env.TEST_BAS_DESTINATIONS),
80
- discoverCf: async ({ spaceGuid, managedKeys }) => {
81
- await writeFile(process.env.CF_DISCOVERY_LOG, JSON.stringify({ spaceGuid, managedKeys }));
82
- return JSON.parse(process.env.TEST_CF_RESULT);
83
- },
84
- verifyOnPremCredentialRoute: async () => JSON.parse(process.env.TEST_ONPREM_CSRF_RESULT),
85
- ...(process.env.TEST_INSTALL_FAIL === 'true' ? {
86
- install: async () => { throw new Error('simulated config write failure'); }
87
- } : {})
88
- });
89
- `);
90
- const env = {
91
- ...process.env,
92
- HOME: directory,
93
- PATH: `${bin}:${process.env.PATH || ''}`,
94
- H2O_URL: 'http://h2o.example',
95
- SAP_AI_DEV_TOOLKIT_MCP_CONFIG: configPath,
96
- CF_CALL_LOG: callLog,
97
- CF_DISCOVERY_LOG: discoveryLog,
98
- TEST_CF_VERSION: version,
99
- TEST_CF_SPACE: space,
100
- TEST_CF_DELETE_MODE: deleteMode,
101
- TEST_INSTALL_FAIL: 'false',
102
- TEST_BAS_DESTINATIONS: JSON.stringify([]),
103
- TEST_CF_RESULT: JSON.stringify({ destinations: [], createdKeys: [], warnings: [] }),
104
- TEST_ONPREM_CSRF_RESULT: JSON.stringify({ httpStatus: 200, tokenReceived: true, cookieCount: 1 })
105
- };
106
- t.after(() => rm(directory, { recursive: true, force: true }));
107
- return { directory, configPath, callLog, discoveryLog, harness, env };
108
- }
109
-
110
- function runSetupInPty(fixture, { importAnswer = 'y\r', selection = ' \r', promptAnswers = [] } = {}) {
111
- return new Promise((resolve, reject) => {
112
- const command = `${JSON.stringify(process.execPath)} ${JSON.stringify(fixture.harness)}`;
113
- const child = spawnWithPty(command, { env: fixture.env, stdio: ['pipe', 'pipe', 'pipe'] });
114
- let stdout = '';
115
- let stderr = '';
116
- let importSent = false;
117
- let selectionSent = false;
118
- let promptAnswerIndex = 0;
119
- const timeout = setTimeout(() => child.kill('SIGKILL'), 15000);
120
- child.stdout.on('data', chunk => {
121
- stdout += chunk.toString();
122
- if (!importSent && stdout.includes("Include destinations from the current CF space's Destination service?")) {
123
- importSent = true;
124
- child.stdin.write(importAnswer);
125
- }
126
- if (!selectionSent && stdout.includes('Select destinations')) {
127
- selectionSent = true;
128
- if (promptAnswers.length) child.stdin.write(selection);
129
- else child.stdin.end(selection);
130
- }
131
- if (selectionSent && promptAnswerIndex < promptAnswers.length) {
132
- const answer = promptAnswers[promptAnswerIndex];
133
- if (stdout.includes(answer.when)) {
134
- promptAnswerIndex++;
135
- child.stdin.write(answer.value);
136
- if (promptAnswerIndex === promptAnswers.length) child.stdin.end();
137
- }
138
- }
139
- });
140
- child.stderr.on('data', chunk => { stderr += chunk.toString(); });
141
- child.on('error', error => {
142
- clearTimeout(timeout);
143
- reject(error);
144
- });
145
- child.on('exit', (code, signal) => {
146
- clearTimeout(timeout);
147
- resolve({ code, signal, stdout, stderr, importSent, selectionSent, promptAnswersSent: promptAnswerIndex });
148
- });
149
- });
150
- }
151
-
152
- test('imports selected CF destinations, passes managed key references, and removes only orphaned keys', async t => {
153
- const fixture = await makeFixture(t);
154
- const previous = cloudFoundryDestination({
155
- name: 'old-destination', serverName: 'cf:space-one:old-instance:old-destination',
156
- instanceGuid: 'old-instance', instanceName: 'old-destination-service', keyName: 'old-key'
157
- });
158
- await installMcpConfig([previous], { env: fixture.env, path: fixture.configPath });
159
- const created = {
160
- kind: 'destination', spaceGuid: 'space-one', instanceGuid: 'new-instance',
161
- instanceName: 'new-destination-service', keyName: 'new-key'
162
- };
163
- fixture.env.TEST_CF_RESULT = JSON.stringify({
164
- destinations: [cloudFoundryDestination({
165
- name: 'new-destination', serverName: 'cf:space-one:new-instance:new-destination',
166
- instanceGuid: 'new-instance', instanceName: 'new-destination-service', keyName: 'new-key'
167
- })],
168
- createdKeys: [created],
169
- warnings: []
170
- });
171
-
172
- const result = await runSetupInPty(fixture);
173
- assert.equal(result.code, 0, `${result.stdout}\n${result.stderr}`);
174
- assert.equal(result.importSent, true, result.stdout);
175
- assert.equal(result.selectionSent, true, result.stdout);
176
- assert.match(result.stdout, /CF new-destination-service/);
177
- assert.match(result.stdout, /Configured 1 MCP server/);
178
- assert.match(result.stdout, /GitHub Copilot Chat.*Chat tools picker.*enable each selected destination server/s);
179
- assert.doesNotMatch(`${result.stdout}\n${result.stderr}`, /SECRET|clientSecret|Password/);
180
-
181
- const snapshot = JSON.parse(await readFile(fixture.discoveryLog, 'utf8'));
182
- assert.equal(snapshot.spaceGuid, 'space-one');
183
- assert.deepEqual(snapshot.managedKeys, [{
184
- kind: 'destination', spaceGuid: 'space-one', instanceGuid: 'old-instance',
185
- instanceName: 'old-destination-service', keyName: 'old-key'
186
- }]);
187
- const config = JSON.parse(await readFile(fixture.configPath, 'utf8'));
188
- assert.deepEqual(Object.keys(config.servers), ['cf-space-one-new-instance-new-destination']);
189
- assert.equal(config.servers['cf-space-one-new-instance-new-destination'].env.BAS_CF_DESTINATION_KEY, 'new-key');
190
- const calls = (await readFile(fixture.callLog, 'utf8')).trim().split('\n').map(line => JSON.parse(line));
191
- assert.deepEqual(calls.filter(args => args[0] === 'delete-service-key'), [[
192
- 'delete-service-key', '-f', '--wait', 'old-destination-service', 'old-key'
193
- ]]);
194
- assert.equal(calls.some(args => args.at(-1) === 'new-key'), false);
195
- });
196
-
197
- test('prompts for BAS and CF OnPremise overrides separately and keeps their routes distinct', async t => {
198
- const fixture = await makeFixture(t);
199
- const cfDestination = cloudFoundryOnPremiseDestination();
200
- fixture.env.TEST_BAS_DESTINATIONS = JSON.stringify([
201
- {
202
- name: 'internet-system', url: 'http://internet-system.dest', backendUrl: 'https://internet.example:44300',
203
- client: '100', authentication: 'BasicAuthentication', proxyType: 'Internet',
204
- probe: { status: 'available', available: true }
205
- },
206
- {
207
- name: 'shared-name', url: 'http://shared-name.dest', backendUrl: 'https://internal.example:44300',
208
- client: '101', authentication: 'BasicAuthentication', proxyType: 'OnPremise',
209
- probe: { status: 'available', available: true }
210
- },
211
- {
212
- name: 'principal-system', url: 'http://principal-system.dest',
213
- client: '102', authentication: 'PrincipalPropagation', proxyType: 'OnPremise',
214
- probe: { status: 'available', available: true }
215
- }
216
- ]);
217
- fixture.env.TEST_CF_RESULT = JSON.stringify({ destinations: [cfDestination], createdKeys: [], warnings: [] });
218
-
219
- const result = await runSetupInPty(fixture, {
220
- selection: 'a\r',
221
- promptAnswers: [
222
- { when: 'BAS Internet — internet-system — override SAP credentials?', value: 'y\r' },
223
- { when: 'BAS Internet — internet-system — SAP user', value: 'INTERNET_USER\r' },
224
- { when: 'BAS Internet — internet-system — SAP password', value: 'internet-password-secret\r' },
225
- { when: 'BAS OnPremise — shared-name — override SAP credentials?', value: 'y\r' },
226
- { when: 'BAS OnPremise — shared-name — SAP user', value: 'BAS_CC_USER\r' },
227
- { when: 'BAS OnPremise — shared-name — SAP password', value: 'bas-cc-password-secret\r' },
228
- { when: 'Cloud Foundry OnPremise — shared-name — override SAP credentials?', value: 'y\r' },
229
- { when: 'Cloud Foundry OnPremise — shared-name — SAP user', value: 'CF_CC_USER\r' },
230
- { when: 'Cloud Foundry OnPremise — shared-name — SAP password', value: 'cf-cc-password-secret\r' }
231
- ]
232
- });
233
- assert.equal(result.code, 0, `${result.stdout}\n${result.stderr}`);
234
- assert.equal(result.promptAnswersSent, 9, result.stdout);
235
- assert.doesNotMatch(result.stdout, /PrincipalPropagation — principal-system — override SAP credentials/);
236
- assert.doesNotMatch(`${result.stdout}\n${result.stderr}`, /internet-password-secret|bas-cc-password-secret|cf-cc-password-secret/);
237
-
238
- const credentialsPath = await resolveCredentialsPath(fixture.env, fixture.configPath);
239
- const stored = (await readCredentials(credentialsPath)).destinations;
240
- assert.equal(stored['internet-system'].mode, 'direct');
241
- assert.equal(stored['internet-system'].host, 'https://internet.example:44300');
242
- assert.equal(stored['shared-name'].mode, 'bas-tunnel');
243
- assert.equal(stored['shared-name'].host, undefined, 'BAS OnPremise must not store or use the backend URL');
244
- const cfKey = credentialKeyForDestination(cfDestination);
245
- assert.equal(stored[cfKey].mode, 'cf-connectivity');
246
- assert.equal(stored[cfKey].host, undefined);
247
- assert.deepEqual(
248
- [stored['shared-name'].user, stored[cfKey].user],
249
- ['BAS_CC_USER', 'CF_CC_USER'],
250
- 'same destination names from BAS and CF must remain distinct'
251
- );
252
-
253
- const mcpConfig = JSON.parse(await readFile(fixture.configPath, 'utf8'));
254
- assert.equal(JSON.stringify(mcpConfig).includes('bas-cc-password-secret'), false);
255
- assert.equal(JSON.stringify(mcpConfig).includes('cf-cc-password-secret'), false);
256
- });
257
-
258
- test('No removes a stored BAS OnPremise override and failed CSRF preflight refuses a new one', async t => {
259
- const fixture = await makeFixture(t);
260
- fixture.env.TEST_BAS_DESTINATIONS = JSON.stringify([{
261
- name: 'shared-name', url: 'http://shared-name.dest', backendUrl: 'https://internal.example:44300',
262
- client: '100', authentication: 'BasicAuthentication', proxyType: 'OnPremise',
263
- probe: { status: 'available', available: true }
264
- }]);
265
- fixture.env.TEST_CF_RESULT = JSON.stringify({ destinations: [], createdKeys: [], warnings: [] });
266
- const credentialsPath = await resolveCredentialsPath(fixture.env, fixture.configPath);
267
- await storeDestinationCredentials(credentialsPath, 'shared-name', {
268
- user: 'OLD_USER', password: 'old-password-secret', mode: 'bas-tunnel'
269
- });
270
-
271
- const declined = await runSetupInPty(fixture, {
272
- selection: ' \r',
273
- promptAnswers: [{ when: 'BAS OnPremise — shared-name — override SAP credentials?', value: 'n\r' }]
274
- });
275
- assert.equal(declined.code, 0, `${declined.stdout}\n${declined.stderr}`);
276
- assert.equal((await readCredentials(credentialsPath)).destinations['shared-name'], undefined);
277
-
278
- fixture.env.TEST_ONPREM_CSRF_RESULT = JSON.stringify({ httpStatus: 200, tokenReceived: true, cookieCount: 0 });
279
- const rejected = await runSetupInPty(fixture, {
280
- selection: ' \r',
281
- promptAnswers: [
282
- { when: 'BAS OnPremise — shared-name — override SAP credentials?', value: 'y\r' },
283
- { when: 'BAS OnPremise — shared-name — SAP user', value: 'NEW_USER\r' },
284
- { when: 'BAS OnPremise — shared-name — SAP password', value: 'new-password-secret\r' }
285
- ]
286
- });
287
- assert.equal(rejected.code, 0, `${rejected.stdout}\n${rejected.stderr}`);
288
- assert.match(rejected.stdout, /did not return both a CSRF token and session cookie/);
289
- assert.equal((await readCredentials(credentialsPath)).destinations['shared-name'], undefined);
290
- assert.doesNotMatch(`${rejected.stdout}\n${rejected.stderr}`, /new-password-secret/);
291
- });
292
-
293
- test('declining CF import keeps BAS setup and does not create CF keys', async t => {
294
- const fixture = await makeFixture(t);
295
- fixture.env.TEST_BAS_DESTINATIONS = JSON.stringify([{
296
- name: 'bas-system', url: 'https://bas.example', client: '100', authentication: 'Basic',
297
- probe: { status: 'available', available: true }
298
- }]);
299
- fixture.env.TEST_CF_RESULT = JSON.stringify({
300
- destinations: [], createdKeys: [{ kind: 'destination', spaceGuid: 'space-one', instanceGuid: 'unused', instanceName: 'unused', keyName: 'unused' }], warnings: []
301
- });
302
-
303
- const result = await runSetupInPty(fixture, { importAnswer: 'n\r' });
304
- assert.equal(result.code, 0, `${result.stdout}\n${result.stderr}`);
305
- assert.equal(result.importSent, true, result.stdout);
306
- assert.equal(result.selectionSent, true, result.stdout);
307
- assert.match(result.stdout, /import was not confirmed/);
308
- assert.doesNotMatch(result.stdout, /Select Destination service instances/);
309
- assert.equal(await readFile(fixture.discoveryLog, 'utf8').then(() => true, () => false), false);
310
- const config = JSON.parse(await readFile(fixture.configPath, 'utf8'));
311
- assert.deepEqual(Object.keys(config.servers), ['bas-system']);
312
- const calls = (await readFile(fixture.callLog, 'utf8')).trim().split('\n').map(line => JSON.parse(line));
313
- assert.equal(calls.some(args => args[0] === 'delete-service-key'), false);
314
- });
315
-
316
- test('old CF keys are preserved when the CLI targets a different space', async t => {
317
- const fixture = await makeFixture(t, { space: 'space-two' });
318
- const previous = cloudFoundryDestination({
319
- name: 'old-destination', serverName: 'cf:space-one:old-instance:old-destination',
320
- instanceGuid: 'old-instance', instanceName: 'old-destination-service', keyName: 'old-key'
321
- });
322
- await installMcpConfig([previous], { env: fixture.env, path: fixture.configPath });
323
- fixture.env.TEST_BAS_DESTINATIONS = JSON.stringify([{
324
- name: 'bas-system', url: 'https://bas.example', client: '100', authentication: 'Basic', probe: { status: 'available' }
325
- }]);
326
-
327
- const result = await runSetupInPty(fixture, { importAnswer: 'n\r' });
328
- assert.equal(result.code, 0, `${result.stdout}\n${result.stderr}`);
329
- assert.match(result.stdout, /requires targeting CF space space-one; it was left untouched/);
330
- const calls = (await readFile(fixture.callLog, 'utf8')).trim().split('\n').map(line => JSON.parse(line));
331
- assert.equal(calls.some(args => args[0] === 'delete-service-key'), false);
332
- });
333
-
334
- test('old CF CLI version skips import before prompting', async t => {
335
- const fixture = await makeFixture(t, { version: 'cf version 8.17.0' });
336
- fixture.env.TEST_BAS_DESTINATIONS = JSON.stringify([{
337
- name: 'bas-system', url: 'https://bas.example', client: '100', authentication: 'Basic', probe: { status: 'available' }
338
- }]);
339
-
340
- const result = await runSetupInPty(fixture);
341
- assert.equal(result.code, 0, `${result.stdout}\n${result.stderr}`);
342
- assert.equal(result.importSent, false, result.stdout);
343
- assert.match(result.stdout, /Cloud Foundry CLI 8\.18 or newer is required/);
344
- const calls = (await readFile(fixture.callLog, 'utf8')).trim().split('\n').map(line => JSON.parse(line));
345
- assert.deepEqual(calls.map(args => args[0]), ['version']);
346
- });
347
-
348
- test('untargeted CF CLI skips the import prompt', async t => {
349
- const fixture = await makeFixture(t, { space: 'not-targeted' });
350
- fixture.env.TEST_BAS_DESTINATIONS = JSON.stringify([{
351
- name: 'bas-system', url: 'https://bas.example', client: '100', authentication: 'Basic', probe: { status: 'available' }
352
- }]);
353
-
354
- const result = await runSetupInPty(fixture);
355
- assert.equal(result.code, 0, `${result.stdout}\n${result.stderr}`);
356
- assert.equal(result.importSent, false, result.stdout);
357
- assert.match(result.stdout, /not authenticated to a targeted space/);
358
- const calls = (await readFile(fixture.callLog, 'utf8')).trim().split('\n').map(line => JSON.parse(line));
359
- assert.deepEqual(calls.map(args => args[0]), ['version', 'target']);
360
- await assert.rejects(readFile(fixture.discoveryLog, 'utf8'), { code: 'ENOENT' });
361
- });
362
-
363
- test('missing service keys are treated as removed without exposing CLI stderr', async t => {
364
- const fixture = await makeFixture(t, { deleteMode: 'missing' });
365
- const previous = cloudFoundryDestination({
366
- name: 'old-destination', serverName: 'cf:space-one:old-instance:old-destination',
367
- instanceGuid: 'old-instance', instanceName: 'old-destination-service', keyName: 'old-key'
368
- });
369
- await installMcpConfig([previous], { env: fixture.env, path: fixture.configPath });
370
- fixture.env.TEST_BAS_DESTINATIONS = JSON.stringify([{
371
- name: 'bas-system', url: 'https://bas.example', client: '100', authentication: 'Basic', probe: { status: 'available' }
372
- }]);
373
-
374
- const result = await runSetupInPty(fixture, { importAnswer: 'n\r' });
375
- assert.equal(result.code, 0, `${result.stdout}\n${result.stderr}`);
376
- assert.doesNotMatch(`${result.stdout}\n${result.stderr}`, /Service key not found/);
377
- assert.doesNotMatch(result.stdout, /Could not delete managed destination service key/);
378
- const calls = (await readFile(fixture.callLog, 'utf8')).trim().split('\n').map(line => JSON.parse(line));
379
- assert.deepEqual(calls.filter(args => args[0] === 'delete-service-key'), [[
380
- 'delete-service-key', '-f', '--wait', 'old-destination-service', 'old-key'
381
- ]]);
382
- });
383
-
384
- test('no selectable records leave MCP config unchanged and remove only new keys', async t => {
385
- const fixture = await makeFixture(t);
386
- const previous = cloudFoundryDestination({
387
- name: 'old-destination', serverName: 'cf:space-one:old-instance:old-destination',
388
- instanceGuid: 'old-instance', instanceName: 'old-destination-service', keyName: 'old-key'
389
- });
390
- await installMcpConfig([previous], { env: fixture.env, path: fixture.configPath });
391
- const created = {
392
- kind: 'destination', spaceGuid: 'space-one', instanceGuid: 'new-instance',
393
- instanceName: 'new-destination-service', keyName: 'new-key'
394
- };
395
- fixture.env.TEST_CF_RESULT = JSON.stringify({
396
- destinations: [], createdKeys: [created], warnings: ['No Destination service instance exists in the current CF space; CF import was skipped.']
397
- });
398
-
399
- const result = await runSetupInPty(fixture);
400
- assert.equal(result.code, 0, `${result.stdout}\n${result.stderr}`);
401
- assert.equal(result.selectionSent, false, result.stdout);
402
- assert.match(result.stdout, /No Destination service instance exists/);
403
- const config = JSON.parse(await readFile(fixture.configPath, 'utf8'));
404
- assert.deepEqual(Object.keys(config.servers), [generatedServerName(previous.serverName)]);
405
- const calls = (await readFile(fixture.callLog, 'utf8')).trim().split('\n').map(line => JSON.parse(line));
406
- assert.deepEqual(calls.filter(args => args[0] === 'delete-service-key'), [[
407
- 'delete-service-key', '-f', '--wait', 'new-destination-service', 'new-key'
408
- ]]);
409
- });
410
-
411
- test('failed config writes remove only unreferenced keys created by that attempt', async t => {
412
- const fixture = await makeFixture(t);
413
- const previous = cloudFoundryDestination({
414
- name: 'old-destination', serverName: 'cf:space-one:old-instance:old-destination',
415
- instanceGuid: 'old-instance', instanceName: 'old-destination-service', keyName: 'old-key'
416
- });
417
- await installMcpConfig([previous], { env: fixture.env, path: fixture.configPath });
418
- const created = {
419
- kind: 'destination', spaceGuid: 'space-one', instanceGuid: 'new-instance',
420
- instanceName: 'new-destination-service', keyName: 'new-key'
421
- };
422
- fixture.env.TEST_CF_RESULT = JSON.stringify({
423
- destinations: [cloudFoundryDestination({
424
- name: 'new-destination', serverName: 'cf:space-one:new-instance:new-destination',
425
- instanceGuid: 'new-instance', instanceName: 'new-destination-service', keyName: 'new-key'
426
- })],
427
- createdKeys: [created],
428
- warnings: []
429
- });
430
- fixture.env.TEST_INSTALL_FAIL = 'true';
431
-
432
- const result = await runSetupInPty(fixture);
433
- assert.equal(result.code, 1, `${result.stdout}\n${result.stderr}`);
434
- assert.match(`${result.stdout}\n${result.stderr}`, /MCP config writing failed: simulated config write failure/);
435
- const config = JSON.parse(await readFile(fixture.configPath, 'utf8'));
436
- assert.deepEqual(Object.keys(config.servers), [generatedServerName(previous.serverName)]);
437
- const calls = (await readFile(fixture.callLog, 'utf8')).trim().split('\n').map(line => JSON.parse(line));
438
- assert.deepEqual(calls.filter(args => args[0] === 'delete-service-key'), [[
439
- 'delete-service-key', '-f', '--wait', 'new-destination-service', 'new-key'
440
- ]]);
441
- });
442
-
443
- test('preserves old CF keys referenced by an unrelated remaining MCP entry', async t => {
444
- const fixture = await makeFixture(t);
445
- const previous = cloudFoundryDestination({
446
- name: 'old-destination', serverName: 'cf:space-one:old-instance:old-destination',
447
- instanceGuid: 'old-instance', instanceName: 'old-destination-service', keyName: 'old-key'
448
- });
449
- await installMcpConfig([previous], { env: fixture.env, path: fixture.configPath });
450
- const config = JSON.parse(await readFile(fixture.configPath, 'utf8'));
451
- config.servers.customServer = {
452
- command: 'custom-launcher',
453
- env: { ...config.servers[generatedServerName(previous.serverName)].env, SAP_AI_DEV_TOOLKIT_DESTINATION_SOURCE: undefined }
454
- };
455
- await writeFile(fixture.configPath, JSON.stringify(config));
456
- fixture.env.TEST_BAS_DESTINATIONS = JSON.stringify([{
457
- name: 'bas-system', url: 'https://bas.example', client: '100', authentication: 'Basic', probe: { status: 'available' }
458
- }]);
459
-
460
- const result = await runSetupInPty(fixture, { importAnswer: 'n\r' });
461
- assert.equal(result.code, 0, `${result.stdout}\n${result.stderr}`);
462
- const written = JSON.parse(await readFile(fixture.configPath, 'utf8'));
463
- assert.equal(written.servers.customServer.env.BAS_CF_DESTINATION_KEY, 'old-key');
464
- const calls = (await readFile(fixture.callLog, 'utf8')).trim().split('\n').map(line => JSON.parse(line));
465
- assert.equal(calls.some(args => args[0] === 'delete-service-key'), false);
466
- });
467
-
468
- test('setup report labels CF source and matches registration by generated server name without leaking diagnostics', () => {
469
- const report = destinationTable([{
470
- source: 'cloud-foundry',
471
- name: 'friendly-destination',
472
- serverName: 'cf:space-one:destination-instance:friendly-destination',
473
- client: '100',
474
- authentication: 'Basic',
475
- cf: { destinationInstanceName: 'destination-service' },
476
- probe: {
477
- status: 'network-error',
478
- available: false,
479
- error: 'https://user:secret@backend.example/path token=SECRET'
480
- }
481
- }], new Set(['cf-space-one-destination-instance-friendly-destination']));
482
- const row = report.split('\n').find(line => line.includes('friendly-destination'));
483
- assert.match(report, /Destination.*Source/);
484
- assert.match(row, /CF destination-service/);
485
- assert.match(row, /REGISTERED/);
486
- assert.doesNotMatch(row, /NOT REGISTERED/);
487
- assert.equal(report.includes('https://'), false);
488
- assert.doesNotMatch(report, /user:secret|SECRET/);
489
- });