sap-ai-dev-toolkit 0.2.5
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/.github/agents/abap-developer.agent.md +25 -0
- package/.github/agents/abap-runtime-debugger.agent.md +27 -0
- package/.github/agents/rap-service-developer.agent.md +27 -0
- package/.github/agents/sap-solution-architect.agent.md +31 -0
- package/.github/skills/abap-debugging/SKILL.md +15 -0
- package/.github/skills/abap-development/SKILL.md +16 -0
- package/.github/skills/abap-runtime-analysis/SKILL.md +17 -0
- package/.github/skills/abap-testing-quality/SKILL.md +19 -0
- package/.github/skills/cds-development/SKILL.md +16 -0
- package/.github/skills/clean-core-extensibility/SKILL.md +16 -0
- package/.github/skills/rap-development/SKILL.md +16 -0
- package/.github/skills/rap-service-delivery/SKILL.md +17 -0
- package/.github/skills/sap-sdlc-orchestration/SKILL.md +16 -0
- package/.github/skills/sap-standard-api-analysis/SKILL.md +16 -0
- package/.github/skills/sap-transport-release/SKILL.md +14 -0
- package/LICENSE +22 -0
- package/LICENSE-APACHE-2.0.txt +201 -0
- package/NOTICE +33 -0
- package/README.md +723 -0
- package/dist/checksums.txt +5 -0
- package/dist/vsp-darwin-arm64 +0 -0
- package/dist/vsp-darwin-x64 +0 -0
- package/dist/vsp-linux-arm64 +0 -0
- package/dist/vsp-linux-x64 +0 -0
- package/dist/vsp-win32-x64.exe +0 -0
- package/package.json +52 -0
- package/patches/vsp-bas-proxy-auth.patch +310 -0
- package/scripts/build-vsp.mjs +52 -0
- package/scripts/ensure-go.mjs +59 -0
- package/scripts/install-user-copilot-assets.mjs +110 -0
- package/scripts/mutation-terminal-ui.mjs +69 -0
- package/scripts/postinstall.mjs +269 -0
- package/scripts/publish-npm.mjs +97 -0
- package/src/abaplint.mjs +105 -0
- package/src/bas-discovery.mjs +210 -0
- package/src/binary.mjs +99 -0
- package/src/cf-connectivity.mjs +317 -0
- package/src/cf-destination.mjs +793 -0
- package/src/launcher.mjs +144 -0
- package/src/mcp-config.mjs +253 -0
- package/src/mcp-proxy.mjs +388 -0
- package/src/setup.mjs +275 -0
- package/src/terminal-ui.mjs +67 -0
- package/test/cf-connectivity.test.mjs +287 -0
- package/test/cf-destination.test.mjs +307 -0
- package/test/cf-runtime.test.mjs +505 -0
- package/test/copilot-assets.test.mjs +37 -0
- package/test/copilot-content.test.mjs +145 -0
- package/test/discovery.test.mjs +199 -0
- package/test/fixtures/fake-vsp.mjs +244 -0
- package/test/launcher.test.mjs +164 -0
- package/test/mcp-config-cf.test.mjs +149 -0
- package/test/mcp-proxy.test.mjs +337 -0
- package/test/setup-cf.test.mjs +362 -0
- package/test/setup.test.mjs +376 -0
- package/test/terminal-ui.test.mjs +47 -0
- package/tools.md +68 -0
|
@@ -0,0 +1,362 @@
|
|
|
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 { spawn } from 'node:child_process';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { installMcpConfig } from '../src/mcp-config.mjs';
|
|
9
|
+
import { destinationTable } from '../scripts/postinstall.mjs';
|
|
10
|
+
|
|
11
|
+
const setupModule = fileURLToPath(new URL('../src/setup.mjs', import.meta.url));
|
|
12
|
+
|
|
13
|
+
function cloudFoundryDestination({ name, serverName, instanceGuid, instanceName, keyName }) {
|
|
14
|
+
return {
|
|
15
|
+
source: 'cloud-foundry',
|
|
16
|
+
name,
|
|
17
|
+
serverName,
|
|
18
|
+
client: '100',
|
|
19
|
+
authentication: 'Basic',
|
|
20
|
+
proxyType: 'Internet',
|
|
21
|
+
probe: { status: 'available', available: true, httpStatus: 401 },
|
|
22
|
+
cf: {
|
|
23
|
+
spaceGuid: 'space-one',
|
|
24
|
+
destinationInstanceGuid: instanceGuid,
|
|
25
|
+
destinationInstanceName: instanceName,
|
|
26
|
+
destinationKeyName: keyName
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function makeFixture(t, { version = 'cf version 8.18.0', space = 'space-one', deleteMode = 'success' } = {}) {
|
|
32
|
+
const directory = await mkdtemp(join(tmpdir(), 'bas-cf-setup-'));
|
|
33
|
+
const bin = join(directory, 'bin');
|
|
34
|
+
await mkdir(bin);
|
|
35
|
+
const configPath = join(directory, 'mcp.json');
|
|
36
|
+
const callLog = join(directory, 'cf-calls.jsonl');
|
|
37
|
+
const discoveryLog = join(directory, 'cf-discovery.json');
|
|
38
|
+
const cfPath = join(bin, 'cf');
|
|
39
|
+
await writeFile(cfPath, `#!/usr/bin/env node
|
|
40
|
+
import { appendFileSync } from 'node:fs';
|
|
41
|
+
const args = process.argv.slice(2);
|
|
42
|
+
appendFileSync(process.env.CF_CALL_LOG, JSON.stringify(args) + '\\n');
|
|
43
|
+
if (args[0] === 'version') process.stdout.write(process.env.TEST_CF_VERSION + '\\n');
|
|
44
|
+
else if (args[0] === 'target') {
|
|
45
|
+
process.stdout.write(process.env.TEST_CF_SPACE === 'not-targeted' ? 'space: <none>\\n' : 'space: ' + process.env.TEST_CF_SPACE + '\\n');
|
|
46
|
+
} else if (args[0] === 'space' && args.at(-1) === '--guid') {
|
|
47
|
+
if (process.env.TEST_CF_SPACE === 'not-targeted') { process.stderr.write('No targeted space\\n'); process.exitCode = 1; }
|
|
48
|
+
else process.stdout.write(process.env.TEST_CF_SPACE + '\\n');
|
|
49
|
+
} else if (args[0] === 'delete-service-key' && process.env.TEST_CF_DELETE_MODE === 'missing') {
|
|
50
|
+
process.stderr.write('Service key not found\\n');
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
} else if (args[0] === 'delete-service-key') process.stdout.write('deleted\\n');
|
|
53
|
+
else process.exitCode = 1;
|
|
54
|
+
`);
|
|
55
|
+
await chmod(cfPath, 0o755);
|
|
56
|
+
const harness = join(directory, 'setup-runner.mjs');
|
|
57
|
+
await writeFile(harness, `import { writeFile } from 'node:fs/promises';
|
|
58
|
+
import { runSetup } from ${JSON.stringify(setupModule)};
|
|
59
|
+
await runSetup({
|
|
60
|
+
env: process.env,
|
|
61
|
+
discover: async () => JSON.parse(process.env.TEST_BAS_DESTINATIONS),
|
|
62
|
+
discoverCf: async ({ spaceGuid, managedKeys }) => {
|
|
63
|
+
await writeFile(process.env.CF_DISCOVERY_LOG, JSON.stringify({ spaceGuid, managedKeys }));
|
|
64
|
+
return JSON.parse(process.env.TEST_CF_RESULT);
|
|
65
|
+
},
|
|
66
|
+
...(process.env.TEST_INSTALL_FAIL === 'true' ? {
|
|
67
|
+
install: async () => { throw new Error('simulated config write failure'); }
|
|
68
|
+
} : {})
|
|
69
|
+
});
|
|
70
|
+
`);
|
|
71
|
+
const env = {
|
|
72
|
+
...process.env,
|
|
73
|
+
HOME: directory,
|
|
74
|
+
PATH: `${bin}:${process.env.PATH || ''}`,
|
|
75
|
+
H2O_URL: 'http://h2o.example',
|
|
76
|
+
BAS_VSP_MCP_CONFIG: configPath,
|
|
77
|
+
CF_CALL_LOG: callLog,
|
|
78
|
+
CF_DISCOVERY_LOG: discoveryLog,
|
|
79
|
+
TEST_CF_VERSION: version,
|
|
80
|
+
TEST_CF_SPACE: space,
|
|
81
|
+
TEST_CF_DELETE_MODE: deleteMode,
|
|
82
|
+
TEST_INSTALL_FAIL: 'false',
|
|
83
|
+
TEST_BAS_DESTINATIONS: JSON.stringify([]),
|
|
84
|
+
TEST_CF_RESULT: JSON.stringify({ destinations: [], createdKeys: [], warnings: [] })
|
|
85
|
+
};
|
|
86
|
+
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
87
|
+
return { directory, configPath, callLog, discoveryLog, harness, env };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function runSetupInPty(fixture, { importAnswer = 'y\r', selection = ' \r' } = {}) {
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
const command = `${JSON.stringify(process.execPath)} ${JSON.stringify(fixture.harness)}`;
|
|
93
|
+
const child = spawn('script', ['-qec', command, '/dev/null'], { env: fixture.env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
94
|
+
let stdout = '';
|
|
95
|
+
let stderr = '';
|
|
96
|
+
let importSent = false;
|
|
97
|
+
let selectionSent = false;
|
|
98
|
+
const timeout = setTimeout(() => child.kill('SIGKILL'), 15000);
|
|
99
|
+
child.stdout.on('data', chunk => {
|
|
100
|
+
stdout += chunk.toString();
|
|
101
|
+
if (!importSent && stdout.includes("Include destinations from the current CF space's Destination service?")) {
|
|
102
|
+
importSent = true;
|
|
103
|
+
child.stdin.write(importAnswer);
|
|
104
|
+
}
|
|
105
|
+
if (!selectionSent && stdout.includes('Select destinations')) {
|
|
106
|
+
selectionSent = true;
|
|
107
|
+
child.stdin.end(selection);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
child.stderr.on('data', chunk => { stderr += chunk.toString(); });
|
|
111
|
+
child.on('error', error => {
|
|
112
|
+
clearTimeout(timeout);
|
|
113
|
+
reject(error);
|
|
114
|
+
});
|
|
115
|
+
child.on('exit', (code, signal) => {
|
|
116
|
+
clearTimeout(timeout);
|
|
117
|
+
resolve({ code, signal, stdout, stderr, importSent, selectionSent });
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
test('imports selected CF destinations, passes managed key references, and removes only orphaned keys', async t => {
|
|
123
|
+
const fixture = await makeFixture(t);
|
|
124
|
+
const previous = cloudFoundryDestination({
|
|
125
|
+
name: 'old-destination', serverName: 'cf:space-one:old-instance:old-destination',
|
|
126
|
+
instanceGuid: 'old-instance', instanceName: 'old-destination-service', keyName: 'old-key'
|
|
127
|
+
});
|
|
128
|
+
await installMcpConfig([previous], { env: fixture.env, path: fixture.configPath });
|
|
129
|
+
const created = {
|
|
130
|
+
kind: 'destination', spaceGuid: 'space-one', instanceGuid: 'new-instance',
|
|
131
|
+
instanceName: 'new-destination-service', keyName: 'new-key'
|
|
132
|
+
};
|
|
133
|
+
fixture.env.TEST_CF_RESULT = JSON.stringify({
|
|
134
|
+
destinations: [cloudFoundryDestination({
|
|
135
|
+
name: 'new-destination', serverName: 'cf:space-one:new-instance:new-destination',
|
|
136
|
+
instanceGuid: 'new-instance', instanceName: 'new-destination-service', keyName: 'new-key'
|
|
137
|
+
})],
|
|
138
|
+
createdKeys: [created],
|
|
139
|
+
warnings: []
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const result = await runSetupInPty(fixture);
|
|
143
|
+
assert.equal(result.code, 0, `${result.stdout}\n${result.stderr}`);
|
|
144
|
+
assert.equal(result.importSent, true, result.stdout);
|
|
145
|
+
assert.equal(result.selectionSent, true, result.stdout);
|
|
146
|
+
assert.match(result.stdout, /CF new-destination-service/);
|
|
147
|
+
assert.match(result.stdout, /Configured 1 MCP server/);
|
|
148
|
+
assert.doesNotMatch(`${result.stdout}\n${result.stderr}`, /SECRET|clientSecret|Password/);
|
|
149
|
+
|
|
150
|
+
const snapshot = JSON.parse(await readFile(fixture.discoveryLog, 'utf8'));
|
|
151
|
+
assert.equal(snapshot.spaceGuid, 'space-one');
|
|
152
|
+
assert.deepEqual(snapshot.managedKeys, [{
|
|
153
|
+
kind: 'destination', spaceGuid: 'space-one', instanceGuid: 'old-instance',
|
|
154
|
+
instanceName: 'old-destination-service', keyName: 'old-key'
|
|
155
|
+
}]);
|
|
156
|
+
const config = JSON.parse(await readFile(fixture.configPath, 'utf8'));
|
|
157
|
+
assert.deepEqual(Object.keys(config.servers), ['cf:space-one:new-instance:new-destination']);
|
|
158
|
+
assert.equal(config.servers['cf:space-one:new-instance:new-destination'].env.BAS_CF_DESTINATION_KEY, 'new-key');
|
|
159
|
+
const calls = (await readFile(fixture.callLog, 'utf8')).trim().split('\n').map(line => JSON.parse(line));
|
|
160
|
+
assert.deepEqual(calls.filter(args => args[0] === 'delete-service-key'), [[
|
|
161
|
+
'delete-service-key', '-f', '--wait', 'old-destination-service', 'old-key'
|
|
162
|
+
]]);
|
|
163
|
+
assert.equal(calls.some(args => args.at(-1) === 'new-key'), false);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('declining CF import keeps BAS setup and does not create CF keys', async t => {
|
|
167
|
+
const fixture = await makeFixture(t);
|
|
168
|
+
fixture.env.TEST_BAS_DESTINATIONS = JSON.stringify([{
|
|
169
|
+
name: 'bas-system', url: 'https://bas.example', client: '100', authentication: 'Basic',
|
|
170
|
+
probe: { status: 'available', available: true }
|
|
171
|
+
}]);
|
|
172
|
+
fixture.env.TEST_CF_RESULT = JSON.stringify({
|
|
173
|
+
destinations: [], createdKeys: [{ kind: 'destination', spaceGuid: 'space-one', instanceGuid: 'unused', instanceName: 'unused', keyName: 'unused' }], warnings: []
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const result = await runSetupInPty(fixture, { importAnswer: 'n\r' });
|
|
177
|
+
assert.equal(result.code, 0, `${result.stdout}\n${result.stderr}`);
|
|
178
|
+
assert.equal(result.importSent, true, result.stdout);
|
|
179
|
+
assert.equal(result.selectionSent, true, result.stdout);
|
|
180
|
+
assert.match(result.stdout, /import was not confirmed/);
|
|
181
|
+
assert.doesNotMatch(result.stdout, /Select Destination service instances/);
|
|
182
|
+
assert.equal(await readFile(fixture.discoveryLog, 'utf8').then(() => true, () => false), false);
|
|
183
|
+
const config = JSON.parse(await readFile(fixture.configPath, 'utf8'));
|
|
184
|
+
assert.deepEqual(Object.keys(config.servers), ['bas-system']);
|
|
185
|
+
const calls = (await readFile(fixture.callLog, 'utf8')).trim().split('\n').map(line => JSON.parse(line));
|
|
186
|
+
assert.equal(calls.some(args => args[0] === 'delete-service-key'), false);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('old CF keys are preserved when the CLI targets a different space', async t => {
|
|
190
|
+
const fixture = await makeFixture(t, { space: 'space-two' });
|
|
191
|
+
const previous = cloudFoundryDestination({
|
|
192
|
+
name: 'old-destination', serverName: 'cf:space-one:old-instance:old-destination',
|
|
193
|
+
instanceGuid: 'old-instance', instanceName: 'old-destination-service', keyName: 'old-key'
|
|
194
|
+
});
|
|
195
|
+
await installMcpConfig([previous], { env: fixture.env, path: fixture.configPath });
|
|
196
|
+
fixture.env.TEST_BAS_DESTINATIONS = JSON.stringify([{
|
|
197
|
+
name: 'bas-system', url: 'https://bas.example', client: '100', authentication: 'Basic', probe: { status: 'available' }
|
|
198
|
+
}]);
|
|
199
|
+
|
|
200
|
+
const result = await runSetupInPty(fixture, { importAnswer: 'n\r' });
|
|
201
|
+
assert.equal(result.code, 0, `${result.stdout}\n${result.stderr}`);
|
|
202
|
+
assert.match(result.stdout, /requires targeting CF space space-one; it was left untouched/);
|
|
203
|
+
const calls = (await readFile(fixture.callLog, 'utf8')).trim().split('\n').map(line => JSON.parse(line));
|
|
204
|
+
assert.equal(calls.some(args => args[0] === 'delete-service-key'), false);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('old CF CLI version skips import before prompting', async t => {
|
|
208
|
+
const fixture = await makeFixture(t, { version: 'cf version 8.17.0' });
|
|
209
|
+
fixture.env.TEST_BAS_DESTINATIONS = JSON.stringify([{
|
|
210
|
+
name: 'bas-system', url: 'https://bas.example', client: '100', authentication: 'Basic', probe: { status: 'available' }
|
|
211
|
+
}]);
|
|
212
|
+
|
|
213
|
+
const result = await runSetupInPty(fixture);
|
|
214
|
+
assert.equal(result.code, 0, `${result.stdout}\n${result.stderr}`);
|
|
215
|
+
assert.equal(result.importSent, false, result.stdout);
|
|
216
|
+
assert.match(result.stdout, /Cloud Foundry CLI 8\.18 or newer is required/);
|
|
217
|
+
const calls = (await readFile(fixture.callLog, 'utf8')).trim().split('\n').map(line => JSON.parse(line));
|
|
218
|
+
assert.deepEqual(calls.map(args => args[0]), ['version']);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('untargeted CF CLI skips the import prompt', async t => {
|
|
222
|
+
const fixture = await makeFixture(t, { space: 'not-targeted' });
|
|
223
|
+
fixture.env.TEST_BAS_DESTINATIONS = JSON.stringify([{
|
|
224
|
+
name: 'bas-system', url: 'https://bas.example', client: '100', authentication: 'Basic', probe: { status: 'available' }
|
|
225
|
+
}]);
|
|
226
|
+
|
|
227
|
+
const result = await runSetupInPty(fixture);
|
|
228
|
+
assert.equal(result.code, 0, `${result.stdout}\n${result.stderr}`);
|
|
229
|
+
assert.equal(result.importSent, false, result.stdout);
|
|
230
|
+
assert.match(result.stdout, /not authenticated to a targeted space/);
|
|
231
|
+
const calls = (await readFile(fixture.callLog, 'utf8')).trim().split('\n').map(line => JSON.parse(line));
|
|
232
|
+
assert.deepEqual(calls.map(args => args[0]), ['version', 'target']);
|
|
233
|
+
await assert.rejects(readFile(fixture.discoveryLog, 'utf8'), { code: 'ENOENT' });
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('missing service keys are treated as removed without exposing CLI stderr', async t => {
|
|
237
|
+
const fixture = await makeFixture(t, { deleteMode: 'missing' });
|
|
238
|
+
const previous = cloudFoundryDestination({
|
|
239
|
+
name: 'old-destination', serverName: 'cf:space-one:old-instance:old-destination',
|
|
240
|
+
instanceGuid: 'old-instance', instanceName: 'old-destination-service', keyName: 'old-key'
|
|
241
|
+
});
|
|
242
|
+
await installMcpConfig([previous], { env: fixture.env, path: fixture.configPath });
|
|
243
|
+
fixture.env.TEST_BAS_DESTINATIONS = JSON.stringify([{
|
|
244
|
+
name: 'bas-system', url: 'https://bas.example', client: '100', authentication: 'Basic', probe: { status: 'available' }
|
|
245
|
+
}]);
|
|
246
|
+
|
|
247
|
+
const result = await runSetupInPty(fixture, { importAnswer: 'n\r' });
|
|
248
|
+
assert.equal(result.code, 0, `${result.stdout}\n${result.stderr}`);
|
|
249
|
+
assert.doesNotMatch(`${result.stdout}\n${result.stderr}`, /Service key not found/);
|
|
250
|
+
assert.doesNotMatch(result.stdout, /Could not delete managed destination service key/);
|
|
251
|
+
const calls = (await readFile(fixture.callLog, 'utf8')).trim().split('\n').map(line => JSON.parse(line));
|
|
252
|
+
assert.deepEqual(calls.filter(args => args[0] === 'delete-service-key'), [[
|
|
253
|
+
'delete-service-key', '-f', '--wait', 'old-destination-service', 'old-key'
|
|
254
|
+
]]);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test('no selectable records leave MCP config unchanged and remove only new keys', async t => {
|
|
258
|
+
const fixture = await makeFixture(t);
|
|
259
|
+
const previous = cloudFoundryDestination({
|
|
260
|
+
name: 'old-destination', serverName: 'cf:space-one:old-instance:old-destination',
|
|
261
|
+
instanceGuid: 'old-instance', instanceName: 'old-destination-service', keyName: 'old-key'
|
|
262
|
+
});
|
|
263
|
+
await installMcpConfig([previous], { env: fixture.env, path: fixture.configPath });
|
|
264
|
+
const created = {
|
|
265
|
+
kind: 'destination', spaceGuid: 'space-one', instanceGuid: 'new-instance',
|
|
266
|
+
instanceName: 'new-destination-service', keyName: 'new-key'
|
|
267
|
+
};
|
|
268
|
+
fixture.env.TEST_CF_RESULT = JSON.stringify({
|
|
269
|
+
destinations: [], createdKeys: [created], warnings: ['No Destination service instance exists in the current CF space; CF import was skipped.']
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const result = await runSetupInPty(fixture);
|
|
273
|
+
assert.equal(result.code, 0, `${result.stdout}\n${result.stderr}`);
|
|
274
|
+
assert.equal(result.selectionSent, false, result.stdout);
|
|
275
|
+
assert.match(result.stdout, /No Destination service instance exists/);
|
|
276
|
+
const config = JSON.parse(await readFile(fixture.configPath, 'utf8'));
|
|
277
|
+
assert.deepEqual(Object.keys(config.servers), [previous.serverName]);
|
|
278
|
+
const calls = (await readFile(fixture.callLog, 'utf8')).trim().split('\n').map(line => JSON.parse(line));
|
|
279
|
+
assert.deepEqual(calls.filter(args => args[0] === 'delete-service-key'), [[
|
|
280
|
+
'delete-service-key', '-f', '--wait', 'new-destination-service', 'new-key'
|
|
281
|
+
]]);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('failed config writes remove only unreferenced keys created by that attempt', async t => {
|
|
285
|
+
const fixture = await makeFixture(t);
|
|
286
|
+
const previous = cloudFoundryDestination({
|
|
287
|
+
name: 'old-destination', serverName: 'cf:space-one:old-instance:old-destination',
|
|
288
|
+
instanceGuid: 'old-instance', instanceName: 'old-destination-service', keyName: 'old-key'
|
|
289
|
+
});
|
|
290
|
+
await installMcpConfig([previous], { env: fixture.env, path: fixture.configPath });
|
|
291
|
+
const created = {
|
|
292
|
+
kind: 'destination', spaceGuid: 'space-one', instanceGuid: 'new-instance',
|
|
293
|
+
instanceName: 'new-destination-service', keyName: 'new-key'
|
|
294
|
+
};
|
|
295
|
+
fixture.env.TEST_CF_RESULT = JSON.stringify({
|
|
296
|
+
destinations: [cloudFoundryDestination({
|
|
297
|
+
name: 'new-destination', serverName: 'cf:space-one:new-instance:new-destination',
|
|
298
|
+
instanceGuid: 'new-instance', instanceName: 'new-destination-service', keyName: 'new-key'
|
|
299
|
+
})],
|
|
300
|
+
createdKeys: [created],
|
|
301
|
+
warnings: []
|
|
302
|
+
});
|
|
303
|
+
fixture.env.TEST_INSTALL_FAIL = 'true';
|
|
304
|
+
|
|
305
|
+
const result = await runSetupInPty(fixture);
|
|
306
|
+
assert.equal(result.code, 1, `${result.stdout}\n${result.stderr}`);
|
|
307
|
+
assert.match(`${result.stdout}\n${result.stderr}`, /MCP config writing failed: simulated config write failure/);
|
|
308
|
+
const config = JSON.parse(await readFile(fixture.configPath, 'utf8'));
|
|
309
|
+
assert.deepEqual(Object.keys(config.servers), [previous.serverName]);
|
|
310
|
+
const calls = (await readFile(fixture.callLog, 'utf8')).trim().split('\n').map(line => JSON.parse(line));
|
|
311
|
+
assert.deepEqual(calls.filter(args => args[0] === 'delete-service-key'), [[
|
|
312
|
+
'delete-service-key', '-f', '--wait', 'new-destination-service', 'new-key'
|
|
313
|
+
]]);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test('preserves old CF keys referenced by an unrelated remaining MCP entry', async t => {
|
|
317
|
+
const fixture = await makeFixture(t);
|
|
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
|
+
const config = JSON.parse(await readFile(fixture.configPath, 'utf8'));
|
|
324
|
+
config.servers.customServer = {
|
|
325
|
+
command: 'custom-launcher',
|
|
326
|
+
env: { ...config.servers[previous.serverName].env, BAS_VSP_DESTINATION_SOURCE: undefined }
|
|
327
|
+
};
|
|
328
|
+
await writeFile(fixture.configPath, JSON.stringify(config));
|
|
329
|
+
fixture.env.TEST_BAS_DESTINATIONS = JSON.stringify([{
|
|
330
|
+
name: 'bas-system', url: 'https://bas.example', client: '100', authentication: 'Basic', probe: { status: 'available' }
|
|
331
|
+
}]);
|
|
332
|
+
|
|
333
|
+
const result = await runSetupInPty(fixture, { importAnswer: 'n\r' });
|
|
334
|
+
assert.equal(result.code, 0, `${result.stdout}\n${result.stderr}`);
|
|
335
|
+
const written = JSON.parse(await readFile(fixture.configPath, 'utf8'));
|
|
336
|
+
assert.equal(written.servers.customServer.env.BAS_CF_DESTINATION_KEY, 'old-key');
|
|
337
|
+
const calls = (await readFile(fixture.callLog, 'utf8')).trim().split('\n').map(line => JSON.parse(line));
|
|
338
|
+
assert.equal(calls.some(args => args[0] === 'delete-service-key'), false);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test('setup report labels CF source and matches registration by generated server name without leaking diagnostics', () => {
|
|
342
|
+
const report = destinationTable([{
|
|
343
|
+
source: 'cloud-foundry',
|
|
344
|
+
name: 'friendly-destination',
|
|
345
|
+
serverName: 'cf:space-one:destination-instance:friendly-destination',
|
|
346
|
+
client: '100',
|
|
347
|
+
authentication: 'Basic',
|
|
348
|
+
cf: { destinationInstanceName: 'destination-service' },
|
|
349
|
+
probe: {
|
|
350
|
+
status: 'network-error',
|
|
351
|
+
available: false,
|
|
352
|
+
error: 'https://user:secret@backend.example/path token=SECRET'
|
|
353
|
+
}
|
|
354
|
+
}], new Set(['cf:space-one:destination-instance:friendly-destination']));
|
|
355
|
+
const row = report.split('\n').find(line => line.includes('friendly-destination'));
|
|
356
|
+
assert.match(report, /Destination.*Source/);
|
|
357
|
+
assert.match(row, /CF destination-service/);
|
|
358
|
+
assert.match(row, /REGISTERED/);
|
|
359
|
+
assert.doesNotMatch(row, /NOT REGISTERED/);
|
|
360
|
+
assert.equal(report.includes('https://'), false);
|
|
361
|
+
assert.doesNotMatch(report, /user:secret|SECRET/);
|
|
362
|
+
});
|