sap-ai-dev-toolkit 0.5.0 → 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 +17 -11
  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 +17 -18
  25. package/src/bas-destination-relay.mjs +58 -22
  26. package/src/bas-discovery.mjs +24 -8
  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 +22 -19
  33. package/src/mcp-config.mjs +41 -7
  34. package/src/mcp-proxy.mjs +392 -104
  35. package/src/redact.mjs +28 -0
  36. package/src/setup.mjs +33 -3
  37. package/tools.md +4 -2
  38. package/test/bas-destination-relay.test.mjs +0 -374
  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 -257
  46. package/test/fixtures/fake-vsp.mjs +0 -263
  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 -399
  51. package/test/live-s4h.test.mjs +0 -202
  52. package/test/mcp-config-cf.test.mjs +0 -407
  53. package/test/mcp-proxy.test.mjs +0 -409
  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,417 +0,0 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { chmod, mkdir, mkdtemp, readFile, readdir, rm, stat, symlink, writeFile } from 'node:fs/promises';
4
- import { createServer } from 'node:http';
5
- import { spawn } from 'node:child_process';
6
- import { stripVTControlCharacters } from 'node:util';
7
- import { PassThrough } from 'node:stream';
8
- import { tmpdir } from 'node:os';
9
- import { join } from 'node:path';
10
- import { pathToFileURL } from 'node:url';
11
- import { installMcpConfig } from '../src/mcp-config.mjs';
12
- import { runSetup } from '../src/setup.mjs';
13
- import { spawnWithPty } from './pty.mjs';
14
-
15
- const destinations = [
16
- { name: 'alpha-system', client: '100', authentication: 'Basic', probe: { status: 'available', available: true } },
17
- { name: 'beta-system', client: '200', authentication: 'PrincipalPropagation', probe: { status: 'auth-required', available: true } },
18
- { name: 'offline-system', client: '300', authentication: 'Basic', probe: { status: 'network-error', available: false } }
19
- ];
20
-
21
- function outputStream() {
22
- const output = new PassThrough();
23
- const chunks = [];
24
- output.isTTY = true;
25
- output.on('data', chunk => chunks.push(chunk.toString()));
26
- output.text = () => chunks.join('');
27
- output.resume();
28
- return output;
29
- }
30
-
31
- function runSetupVisibilityInPty(fixture, env, keys = '\r') {
32
- return new Promise((resolve, reject) => {
33
- const command = `stty cols 48 rows 12; ${JSON.stringify(process.execPath)} ${JSON.stringify(fixture)}`;
34
- const child = spawnWithPty(command, { env, stdio: ['pipe', 'pipe', 'pipe'] });
35
- let stdout = '';
36
- let stderr = '';
37
- let selectionSent = false;
38
- const timeout = setTimeout(() => child.kill('SIGKILL'), 15000);
39
- child.stdout.on('data', chunk => {
40
- stdout += chunk.toString();
41
- if (!selectionSent && stdout.includes('Select destinations')) {
42
- selectionSent = true;
43
- setTimeout(() => child.stdin.write(keys), 500);
44
- }
45
- });
46
- child.stderr.on('data', chunk => { stderr += chunk.toString(); });
47
- child.on('error', error => {
48
- clearTimeout(timeout);
49
- reject(error);
50
- });
51
- child.on('exit', (code, signal) => {
52
- clearTimeout(timeout);
53
- resolve({ code, signal, stdout, stderr, selectionSent });
54
- });
55
- });
56
- }
57
-
58
- function runPostinstallInPty(env, keys, assetsAnswer = '\r') {
59
- return new Promise((resolve, reject) => {
60
- const command = `${JSON.stringify(process.execPath)} scripts/postinstall.mjs </dev/null | cat`;
61
- const child = spawnWithPty(command, { env, stdio: ['pipe', 'pipe', 'pipe'] });
62
- let stdout = '';
63
- let stderr = '';
64
- let selectionSent = false;
65
- let assetsAnswerSent = false;
66
- const timeout = setTimeout(() => child.kill('SIGKILL'), 15000);
67
- child.stdout.on('data', chunk => {
68
- stdout += chunk.toString();
69
- if (!selectionSent && stdout.includes('Select destinations')) {
70
- selectionSent = true;
71
- child.stdin.write(keys);
72
- }
73
- if (!assetsAnswerSent && stdout.includes('Install the bundled agents and all skills')) {
74
- assetsAnswerSent = true;
75
- child.stdin.write(assetsAnswer);
76
- }
77
- });
78
- child.stderr.on('data', chunk => { stderr += chunk.toString(); });
79
- child.on('error', error => {
80
- clearTimeout(timeout);
81
- reject(error);
82
- });
83
- child.on('exit', (code, signal) => {
84
- clearTimeout(timeout);
85
- resolve({ code, signal, stdout, stderr, selectionSent, assetsAnswerSent });
86
- });
87
- });
88
- }
89
-
90
- async function assertUserCopilotAssets(home) {
91
- const copilotRoot = join(home, '.copilot');
92
- assert.equal((await stat(join(copilotRoot, 'agents', 'abap-developer.agent.md'))).isFile(), true);
93
- assert.equal((await stat(join(copilotRoot, 'agents', 'hana-cloud-hdi-specialist.agent.md'))).isFile(), true);
94
- const skillNames = (await readdir(join(copilotRoot, 'skills'))).sort();
95
- assert.deepEqual(skillNames, [
96
- 'abap-debugging',
97
- 'abap-development',
98
- 'abap-runtime-analysis',
99
- 'abap-testing-quality',
100
- 'cds-development',
101
- 'clean-core-extensibility',
102
- 'hana-cloud-inspection',
103
- 'hana-cloud-native-development',
104
- 'hana-cloud-validation',
105
- 'rap-development',
106
- 'rap-service-delivery',
107
- 'sap-sdlc-orchestration',
108
- 'sap-standard-api-analysis',
109
- 'sap-transport-release'
110
- ]);
111
- for (const skillName of skillNames) {
112
- assert.equal((await stat(join(copilotRoot, 'skills', skillName, 'SKILL.md'))).isFile(), true);
113
- }
114
- }
115
-
116
- test('reconciles generated entries while preserving unrelated MCP config', async () => {
117
- const directory = await mkdtemp(join(tmpdir(), 'bas-mcp-config-'));
118
- const path = join(directory, 'mcp.json');
119
- await writeFile(path, JSON.stringify({
120
- inputs: [{ id: 'keep-me' }],
121
- custom: true,
122
- servers: {
123
- unrelated: { type: 'stdio', command: 'other' },
124
- sapAiDev_stale: { type: 'stdio', command: 'old' }
125
- }
126
- }));
127
- try {
128
- await installMcpConfig(destinations.slice(0, 2), { env: { H2O_URL: 'http://new-h2o' }, path });
129
- const config = JSON.parse(await readFile(path, 'utf8'));
130
- const generated = Object.entries(config.servers).filter(([, entry]) => entry.BAS_EXT === 'true');
131
- assert.equal(config.custom, true);
132
- assert.deepEqual(config.inputs, [{ id: 'keep-me' }]);
133
- assert.deepEqual(Object.keys(config.servers).sort(), ['alpha-system', 'beta-system', 'unrelated']);
134
- assert.deepEqual(generated.map(([name]) => name), ['alpha-system', 'beta-system']);
135
- assert.equal(generated.length, 2);
136
- assert.deepEqual(generated.map(([, entry]) => entry.env.SAP_AI_DEV_TOOLKIT_DESTINATION), ['alpha-system', 'beta-system']);
137
- assert.deepEqual(generated.map(([, entry]) => entry.env.H2O_URL), ['http://new-h2o', 'http://new-h2o']);
138
- assert.deepEqual(generated.map(([, entry]) => entry.command), ['sap-ai-dev', 'sap-ai-dev']);
139
- assert.deepEqual(generated.map(([, entry]) => entry.env.SAP_ALLOW_TRANSPORTABLE_EDITS), ['true', 'true']);
140
- assert.equal(new Set(generated.map(([name]) => name)).size, 2);
141
-
142
- await installMcpConfig([], { env: { H2O_URL: 'http://new-h2o' }, path });
143
- const cleared = JSON.parse(await readFile(path, 'utf8'));
144
- assert.deepEqual(Object.keys(cleared.servers), ['unrelated']);
145
- } finally {
146
- await rm(directory, { recursive: true, force: true });
147
- }
148
- });
149
- test('does not overwrite an unrelated server with the destination name', async () => {
150
- const directory = await mkdtemp(join(tmpdir(), 'bas-mcp-config-collision-'));
151
- const path = join(directory, 'mcp.json');
152
- const unrelated = { type: 'stdio', command: 'other' };
153
- await writeFile(path, JSON.stringify({ servers: { 'alpha-system': unrelated } }));
154
- try {
155
- await assert.rejects(
156
- () => installMcpConfig(destinations.slice(0, 1), { env: { H2O_URL: 'http://new-h2o' }, path }),
157
- /already exists and is not managed/
158
- );
159
- const config = JSON.parse(await readFile(path, 'utf8'));
160
- assert.deepEqual(config.servers['alpha-system'], unrelated);
161
- } finally {
162
- await rm(directory, { recursive: true, force: true });
163
- }
164
- });
165
-
166
-
167
- test('does not overwrite malformed MCP JSON', async () => {
168
- const directory = await mkdtemp(join(tmpdir(), 'bas-mcp-config-'));
169
- const path = join(directory, 'mcp.json');
170
- const original = '{ malformed';
171
- await writeFile(path, original);
172
- try {
173
- await assert.rejects(() => installMcpConfig([], { env: { H2O_URL: 'http://h2o.example' }, path }), /invalid JSON/);
174
- assert.equal(await readFile(path, 'utf8'), original);
175
- } finally {
176
- await rm(directory, { recursive: true, force: true });
177
- }
178
- });
179
-
180
- test('setup checkbox remains visible in a narrow live TTY without spinner artifacts', async () => {
181
- const directory = await mkdtemp(join(tmpdir(), 'bas-setup-visibility-'));
182
- const fixture = join(directory, 'visibility-fixture.mjs');
183
- const bin = join(directory, 'bin');
184
- await mkdir(bin);
185
- await writeFile(join(bin, 'cf'), '#!/bin/sh\nexit 1\n', { mode: 0o755 });
186
- await writeFile(fixture, `
187
- import { runSetup } from ${JSON.stringify(pathToFileURL(join(process.cwd(), 'src/setup.mjs')).href)};
188
- const destinations = [
189
- { name: 'S4H', client: '100', authentication: 'Basic', probe: { status: 'available', available: true } },
190
- { name: 'VeryLongDestinationNameForWrapping', client: '200', authentication: 'Basic', probe: { status: 'available', available: true } }
191
- ];
192
- await runSetup({
193
- env: { ...process.env, H2O_URL: 'http://h2o.example', PATH: ${JSON.stringify(`${bin}:${process.env.PATH || ''}`)} },
194
- discover: async () => destinations,
195
- install: async selected => ({ path: process.env.SAP_AI_DEV_TOOLKIT_MCP_CONFIG, servers: Object.fromEntries(selected.map(destination => [destination.name, { env: { SAP_AI_DEV_TOOLKIT_DESTINATION: destination.name } }])) })
196
- });
197
- `);
198
- try {
199
- const env = {
200
- ...process.env,
201
- SAP_AI_DEV_TOOLKIT_MCP_CONFIG: join(directory, 'mcp.json'),
202
- FORCE_COLOR: '1',
203
- PATH: `${bin}:${process.env.PATH || ''}`
204
- };
205
- delete env.NO_COLOR;
206
- const result = await runSetupVisibilityInPty(fixture, env, '\r');
207
- const logs = `${result.stdout}\n${result.stderr}`;
208
- const visible = stripVTControlCharacters(logs);
209
- assert.equal(result.code, 0, logs);
210
- assert.equal(result.selectionSent, true, logs);
211
- assert.match(visible, /[❯>]\s*◯ S4H \(BAS, client 100, ok:available\)/u, visible);
212
- assert.match(visible, /Configured 0 MCP servers/u, visible);
213
- assert.doesNotMatch(logs, /[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]/u, logs);
214
- assert.doesNotMatch(logs, /⏳/u, logs);
215
- } finally {
216
- await rm(directory, { recursive: true, force: true });
217
- }
218
- });
219
-
220
- test('non-TTY setup skips without writing config', async () => {
221
- const input = new PassThrough();
222
- input.isTTY = false;
223
- const output = outputStream();
224
- let installCalls = 0;
225
- const result = await runSetup({
226
- env: { H2O_URL: 'http://h2o.example' },
227
- input,
228
- output,
229
- discover: async () => destinations,
230
- install: async () => { installCalls += 1; }
231
- });
232
- assert.equal(result.skipped, true);
233
- assert.equal(result.reason, 'non-tty');
234
- assert.equal(installCalls, 0);
235
- });
236
-
237
- test('global postinstall completes BAS selection before optional Copilot assets', async t => {
238
- const directory = await mkdtemp(join(tmpdir(), 'bas-postinstall-wizard-'));
239
- const config = join(directory, 'mcp.json');
240
- const bin = join(directory, 'bin');
241
- await mkdir(bin);
242
- const cfCli = join(bin, 'cf');
243
- await writeFile(cfCli, '#!/bin/sh\nexit 1\n');
244
- await chmod(cfCli, 0o755);
245
- const server = createServer((request, response) => {
246
- if (request.url === '/api/listDestinations') {
247
- response.writeHead(200, { 'content-type': 'application/json' });
248
- response.end(JSON.stringify([
249
- { Name: 'alpha-system', Client: '100', Authentication: 'Basic' },
250
- { Name: 'beta-system', Client: '200', Authentication: 'PrincipalPropagation' },
251
- { Name: 'gamma-system', Client: '300', Authentication: 'Basic' }
252
- ]));
253
- return;
254
- }
255
- response.writeHead(404);
256
- response.end();
257
- });
258
- await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
259
- const env = {
260
- ...process.env,
261
- HOME: directory,
262
- PATH: `${bin}:${process.env.PATH || ''}`,
263
- BAS_VSP_BINARY: '/bin/true',
264
- SAP_AI_DEV_TOOLKIT_MCP_CONFIG: config,
265
- SAP_AI_DEV_TOOLKIT_SKIP_PROBE: 'true',
266
- H2O_URL: `http://127.0.0.1:${server.address().port}`,
267
- HTTP_PROXY: '',
268
- http_proxy: '',
269
- NO_PROXY: '127.0.0.1,localhost',
270
- FORCE_COLOR: '1'
271
- };
272
- delete env.NO_COLOR;
273
- delete env.npm_config_ignore_scripts;
274
- t.after(async () => {
275
- await new Promise(resolve => server.close(resolve));
276
- await rm(directory, { recursive: true, force: true });
277
- });
278
-
279
- const declined = await runPostinstallInPty(env, '\r', 'n\r');
280
- assert.equal(declined.code, 0, `${declined.stdout}\n${declined.stderr}`);
281
- assert.equal(declined.selectionSent, true, declined.stdout);
282
- assert.equal(declined.assetsAnswerSent, true, declined.stdout);
283
- const declineLogs = `${declined.stdout}\n${declined.stderr}`;
284
- assert.match(declineLogs, /Configured 0 MCP servers/);
285
- assert.match(declineLogs, /\[Y\/n\]/);
286
- assert.match(declineLogs, /Press Enter to install the bundled agents and all skills in the path shown below; type n then press Enter to skip/);
287
- assert.match(declineLogs, /🤖 sap-ai-dev-toolkit/);
288
- assert.match(declineLogs, /\u001b\[1;35m/);
289
- assert.match(declineLogs, /Optional Copilot setup is waiting for your choice/);
290
- assert.match(declineLogs, /Copilot agent and skills were skipped\. Your files were not changed/);
291
- assert.ok(declineLogs.indexOf('Optional Copilot setup is waiting for your choice') > declineLogs.indexOf('Configured 0 MCP servers'), declineLogs);
292
- assert.ok(declineLogs.indexOf('Install the bundled agents and all skills') > declineLogs.indexOf('Optional Copilot setup is waiting for your choice'), declineLogs);
293
- assert.ok(declineLogs.indexOf('No MCP server entries are configured for this add-on.') > declineLogs.indexOf('Copilot agent and skills were skipped'), declineLogs);
294
- assert.ok(declineLogs.includes(`MCP config file: ${config}`), declineLogs);
295
- const configAfterDecline = JSON.parse(await readFile(config, 'utf8'));
296
- assert.equal(Object.values(configAfterDecline.servers).filter(entry => entry.BAS_EXT === 'true').length, 0);
297
- await assert.rejects(stat(join(directory, '.copilot')), { code: 'ENOENT' });
298
-
299
- const accepted = await runPostinstallInPty(env, ' \r', '\r');
300
- assert.equal(accepted.code, 0, `${accepted.stdout}\n${accepted.stderr}`);
301
- assert.equal(accepted.selectionSent, true, accepted.stdout);
302
- assert.equal(accepted.assetsAnswerSent, true, accepted.stdout);
303
- const acceptLogs = `${accepted.stdout}\n${accepted.stderr}`;
304
- assert.ok(acceptLogs.indexOf('Install the bundled agents and all skills') > acceptLogs.indexOf('Configured 1 MCP server'), acceptLogs);
305
- assert.match(acceptLogs, /Installed 19 Copilot files/);
306
- assert.ok(acceptLogs.indexOf('Installation configuration summary') > acceptLogs.indexOf('Installed 19 Copilot files'), acceptLogs);
307
- assert.ok(acceptLogs.includes(`MCP config file: ${config}`), acceptLogs);
308
- assert.ok(acceptLogs.includes('Destination: BAS · alpha-system · client 100 · Basic'), acceptLogs);
309
- assert.match(acceptLogs, /Launch: stdio · (?:[^\r\n]*[\\/])?sap-ai-dev(?:\.cmd)?(?:\r?\n|$)/, acceptLogs);
310
- assert.ok(acceptLogs.includes('Environment keys: H2O_URL, SAP_AI_DEV_TOOLKIT_DESTINATION, SAP_ALLOW_TRANSPORTABLE_EDITS'), acceptLogs);
311
- const configAfterAccept = JSON.parse(await readFile(config, 'utf8'));
312
- assert.deepEqual(Object.values(configAfterAccept.servers)
313
- .filter(entry => entry.BAS_EXT === 'true')
314
- .map(entry => entry.env.SAP_AI_DEV_TOOLKIT_DESTINATION), ['alpha-system']);
315
- await assertUserCopilotAssets(directory);
316
- });
317
-
318
-
319
-
320
- test('non-TTY postinstall skips setup and leaves MCP config untouched', async () => {
321
- const directory = await mkdtemp(join(tmpdir(), 'bas-postinstall-non-tty-'));
322
- const env = {
323
- ...process.env,
324
- HOME: directory,
325
- SAP_AI_DEV_TOOLKIT_BINARY: '/bin/true',
326
- H2O_URL: 'http://bas.example'
327
- };
328
- delete env.npm_config_ignore_scripts;
329
-
330
- try {
331
- for (const original of [undefined, JSON.stringify({ servers: { unrelated: { command: 'other' } } })]) {
332
- const config = join(directory, original === undefined ? 'new.json' : 'existing.json');
333
- env.SAP_AI_DEV_TOOLKIT_MCP_CONFIG = config;
334
- if (original !== undefined) await writeFile(config, original);
335
- const result = await new Promise((resolve, reject) => {
336
- const child = spawn(process.execPath, ['scripts/postinstall.mjs'], { env, stdio: ['ignore', 'pipe', 'pipe'] });
337
- let stdout = '';
338
- let stderr = '';
339
- child.stdout.on('data', chunk => { stdout += chunk; });
340
- child.stderr.on('data', chunk => { stderr += chunk; });
341
- child.on('error', reject);
342
- child.on('exit', (code, signal) => resolve({ code, signal, stdout, stderr }));
343
- });
344
- assert.equal(result.code, 0, `${result.stdout}\n${result.stderr}`);
345
- if (original === undefined) {
346
- await assert.rejects(readFile(config), { code: 'ENOENT' });
347
- } else {
348
- assert.equal(await readFile(config, 'utf8'), original);
349
- }
350
- }
351
- await assert.rejects(stat(join(env.HOME, '.copilot')), { code: 'ENOENT' });
352
- } finally {
353
- await rm(directory, { recursive: true, force: true });
354
- }
355
- });
356
-
357
- test('postinstall skips optional Copilot assets without an interactive terminal', async () => {
358
- const directory = await mkdtemp(join(tmpdir(), 'bas-postinstall-'));
359
- const config = join(directory, 'mcp.json');
360
- const env = {
361
- ...process.env,
362
- HOME: join(directory, 'home'),
363
- SAP_AI_DEV_TOOLKIT_BINARY: '/bin/true',
364
- SAP_AI_DEV_TOOLKIT_MCP_CONFIG: config
365
- };
366
- delete env.H2O_URL;
367
- delete env.npm_config_ignore_scripts;
368
- try {
369
- const result = await new Promise((resolve, reject) => {
370
- const child = spawn(process.execPath, ['scripts/postinstall.mjs'], { env, stdio: ['ignore', 'pipe', 'pipe'] });
371
- let stdout = '';
372
- let stderr = '';
373
- child.stdout.on('data', chunk => { stdout += chunk; });
374
- child.stderr.on('data', chunk => { stderr += chunk; });
375
- child.on('error', reject);
376
- child.on('exit', (code, signal) => resolve({ code, signal, stdout, stderr }));
377
- });
378
- const logs = `${result.stdout}\n${result.stderr}`;
379
- assert.equal(result.code, 0, logs);
380
- assert.match(logs, /Optional Copilot installation skipped because no interactive terminal/);
381
- await assert.rejects(stat(join(env.HOME, '.copilot')), { code: 'ENOENT' });
382
- await assert.rejects(stat(join(directory, '.github')), { code: 'ENOENT' });
383
- await assert.rejects(readFile(config), { code: 'ENOENT' });
384
- } finally {
385
- await rm(directory, { recursive: true, force: true });
386
- }
387
- });
388
-
389
- test('postinstall runs when invoked through a symlinked install path', async () => {
390
- const directory = await mkdtemp(join(tmpdir(), 'sap-ai-postinstall-symlink-'));
391
- const alias = join(directory, 'postinstall.mjs');
392
- await symlink(join(process.cwd(), 'scripts', 'postinstall.mjs'), alias);
393
- const env = {
394
- ...process.env,
395
- HOME: directory,
396
- SAP_AI_DEV_TOOLKIT_BINARY: '/bin/true'
397
- };
398
- delete env.H2O_URL;
399
- delete env.npm_config_ignore_scripts;
400
- delete env.NPM_CONFIG_IGNORE_SCRIPTS;
401
- try {
402
- const result = await new Promise((resolve, reject) => {
403
- const child = spawn(process.execPath, [alias], { env, stdio: ['ignore', 'pipe', 'pipe'] });
404
- let stdout = '';
405
- let stderr = '';
406
- child.stdout.on('data', chunk => { stdout += chunk; });
407
- child.stderr.on('data', chunk => { stderr += chunk; });
408
- child.on('error', reject);
409
- child.on('exit', (code, signal) => resolve({ code, signal, stdout, stderr }));
410
- });
411
- const logs = `${result.stdout}\n${result.stderr}`;
412
- assert.equal(result.code, 0, logs);
413
- assert.match(logs, /BAS destination setup was skipped because H2O_URL is not set/);
414
- } finally {
415
- await rm(directory, { recursive: true, force: true });
416
- }
417
- });
@@ -1,47 +0,0 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { Writable } from 'node:stream';
4
- import { formatStatus, promptOutput, quietSpinnerTheme } from '../src/terminal-ui.mjs';
5
-
6
- test('progress status uses a stable single-column glyph instead of a wide emoji', () => {
7
- const rendered = formatStatus('Loading destinations', 'progress', false, 'Setup');
8
- assert.equal(rendered, '… Setup Loading destinations');
9
- assert.doesNotMatch(rendered, /⏳/u);
10
- });
11
-
12
- test('prompt output proxy preserves TTY sizing and color capabilities for Inquirer rendering', async () => {
13
- const chunks = [];
14
- const target = new Writable({
15
- write(chunk, encoding, callback) {
16
- chunks.push(chunk.toString());
17
- callback();
18
- }
19
- });
20
- target.isTTY = true;
21
- target.columns = 42;
22
- target.rows = 9;
23
- target.getColorDepth = () => 8;
24
- target.hasColors = count => count <= 256;
25
-
26
- const proxy = promptOutput(target);
27
- assert.equal(proxy.isTTY, true);
28
- assert.equal(proxy.columns, 42);
29
- assert.equal(proxy.rows, 9);
30
- assert.equal(proxy.getColorDepth(), 8);
31
- assert.equal(proxy.hasColors(256), true);
32
-
33
- for (let index = 0; index < 20; index += 1) {
34
- target.columns = 20 + index;
35
- target.rows = 5 + (index % 4);
36
- assert.equal(proxy.columns, target.columns);
37
- assert.equal(proxy.rows, target.rows);
38
- }
39
-
40
- await new Promise(resolve => proxy.write('visible line', resolve));
41
- assert.equal(chunks.join(''), 'visible line');
42
- });
43
-
44
- test('quiet spinner theme disables animated Braille loading frames', () => {
45
- assert.deepEqual(quietSpinnerTheme.spinner.frames, ['']);
46
- assert.equal(quietSpinnerTheme.spinner.interval, 80);
47
- });