sap-ai-dev-toolkit 0.9.9918 → 0.9.9921

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 (59) hide show
  1. package/.github/agents/sap-integration-architect.agent.md +32 -18
  2. package/.github/agents/sap-integration-developer.agent.md +28 -12
  3. package/.github/skills/integration-suite-delivery/SKILL.md +94 -37
  4. package/.github/skills/sap-integration-architect/SKILL.md +49 -25
  5. package/README.md +72 -15
  6. package/dist/checksums.txt +5 -5
  7. package/dist/vsp-darwin-arm64 +0 -0
  8. package/dist/vsp-darwin-x64 +0 -0
  9. package/dist/vsp-linux-arm64 +0 -0
  10. package/dist/vsp-linux-x64 +0 -0
  11. package/dist/vsp-win32-x64.exe +0 -0
  12. package/inventory.md +97 -57
  13. package/package.json +1 -1
  14. package/scripts/check-live-integration.mjs +151 -0
  15. package/scripts/install-local.mjs +11 -4
  16. package/scripts/run-live-integration.mjs +55 -63
  17. package/src/apim-bundle.mjs +416 -13
  18. package/src/apim-cleanup-tools.mjs +145 -0
  19. package/src/apim-content.mjs +157 -0
  20. package/src/apim-delete-tools.mjs +139 -0
  21. package/src/apim-documentation.mjs +162 -0
  22. package/src/apim-inspection.mjs +141 -0
  23. package/src/apim-monitoring.mjs +140 -0
  24. package/src/apim-policy-steps.mjs +31 -0
  25. package/src/apim-runtime.mjs +262 -0
  26. package/src/apim-tools.mjs +202 -109
  27. package/src/cf-integration.mjs +46 -7
  28. package/src/cpi-async-monitoring.mjs +177 -0
  29. package/src/cpi-content.mjs +92 -0
  30. package/src/cpi-monitoring.mjs +225 -0
  31. package/src/cpi-operational.mjs +129 -0
  32. package/src/cpi-release.mjs +273 -0
  33. package/src/cpi-security.mjs +48 -0
  34. package/src/cpi-tools.mjs +184 -319
  35. package/src/devhub-retirement.mjs +345 -0
  36. package/src/devhub-tools.mjs +177 -0
  37. package/src/edge-monitoring.mjs +101 -0
  38. package/src/iflow-bundle.mjs +295 -7
  39. package/src/integration-alerts.mjs +248 -0
  40. package/src/integration-analysis.mjs +488 -0
  41. package/src/integration-capabilities.mjs +181 -0
  42. package/src/integration-collections.mjs +203 -0
  43. package/src/integration-common.mjs +140 -35
  44. package/src/integration-config.mjs +20 -14
  45. package/src/integration-credentials.mjs +16 -10
  46. package/src/integration-http.mjs +255 -67
  47. package/src/integration-messaging.mjs +118 -0
  48. package/src/integration-optional-config.mjs +122 -0
  49. package/src/integration-optional-tools.mjs +153 -0
  50. package/src/integration-release-store.mjs +410 -0
  51. package/src/integration-server.mjs +34 -16
  52. package/src/integration-state.mjs +31 -0
  53. package/src/integration-tools.mjs +44 -8
  54. package/src/integration-transport.mjs +184 -0
  55. package/src/integration-validation.mjs +26 -0
  56. package/src/integration-verify.mjs +143 -62
  57. package/src/open-connectors-tools.mjs +143 -0
  58. package/src/setup.mjs +86 -52
  59. package/tools.md +193 -26
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env node
2
+ // Separate from run-live-integration: fixed metadata tools, GET-only management transport.
3
+ import { fetch as undiciFetch } from 'undici';
4
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
5
+ import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
6
+ import { createIntegrationServer } from '../src/integration-server.mjs';
7
+ import { resolveIntegrationConfig } from '../src/integration-config.mjs';
8
+ import { readStoredIntegrationKeys } from '../src/integration-credentials.mjs';
9
+
10
+ function options(argv) {
11
+ const result = { window_minutes: 60, top: 3, json: false };
12
+ for (let i = 0; i < argv.length; i += 1) {
13
+ const option = argv[i];
14
+ if (option === '--json') result.json = true;
15
+ else if (option === '--window-minutes' || option === '--top') {
16
+ const value = Number(argv[++i]);
17
+ const maximum = option === '--top' ? 5 : 31 * 24 * 60;
18
+ if (!Number.isInteger(value) || value < 1 || value > maximum) throw new Error(`${option} must be an integer from 1 to ${maximum}`);
19
+ result[option === '--top' ? 'top' : 'window_minutes'] = value;
20
+ } else throw new Error(`Unsupported argument ${option}`);
21
+ }
22
+ return result;
23
+ }
24
+ const pick = (value, names) => Object.fromEntries(names.filter(name => value?.[name] !== undefined).map(name => [name, value[name]]));
25
+ const page = (value, itemFields) => ({ ...pick(value, ['returned', 'total', 'complete', 'stop_reason', 'consistency', 'source_result', 'source_results', 'outcome', 'window']),
26
+ resumable: typeof value.next_cursor === 'string', items: (value.items ?? []).map(item => pick(item, itemFields)) });
27
+ const sourceFailure = value => {
28
+ if (!value || typeof value !== 'object') return false;
29
+ if (['unauthenticated', 'forbidden', 'unavailable', 'error'].includes(value.result)) return true;
30
+ return Object.values(value).some(entry => entry && typeof entry === 'object' && sourceFailure(entry));
31
+ };
32
+
33
+ async function main() {
34
+ const args = options(process.argv.slice(2));
35
+ const now = Date.now();
36
+ const window = { from: new Date(now - args.window_minutes * 60000).toISOString(), to: new Date(now).toISOString() };
37
+ const env = process.env;
38
+ const config = resolveIntegrationConfig(env, { stored: readStoredIntegrationKeys(env) });
39
+ const sides = Object.entries(config).filter(([, side]) => side && typeof side === 'object' && typeof side.url === 'string');
40
+ const tokenUrls = new Set(sides.map(([, side]) => side.tokenUrl).filter(Boolean));
41
+ const origins = new Set(sides.map(([, side]) => new URL(side.url).origin));
42
+ // AEM uses one fixed public cloud origin; no caller-supplied origin is accepted.
43
+ if (config.aem) origins.add('https://api.solace.cloud');
44
+ const requests = { management_gets: 0, token_posts: 0 };
45
+ const guardedFetch = async (input, init = {}) => {
46
+ const target = new URL(typeof input === 'string' || input instanceof URL ? input : input.url);
47
+ const method = (init.method ?? 'GET').toUpperCase();
48
+ if (method === 'POST' && tokenUrls.has(target.href)) requests.token_posts += 1;
49
+ else if (method === 'GET' && origins.has(target.origin)) requests.management_gets += 1;
50
+ else throw new Error('Read-only checker rejected a non-GET management request or unbound origin');
51
+ if (target.username || target.password || target.hash) throw new Error('Read-only checker rejected an unsafe request URL');
52
+ if (init.redirect && init.redirect !== 'manual') throw new Error('Read-only checker rejected redirect following');
53
+ return undiciFetch(input, { ...init, redirect: 'manual' });
54
+ };
55
+ const runtime = createIntegrationServer({ env, fetchImpl: guardedFetch });
56
+ const [a, b] = InMemoryTransport.createLinkedPair();
57
+ const client = new Client({ name: 'read-only-integration-check', version: '1.0.0' });
58
+ const report = { mode: 'read_only', environment: runtime.connectionSummary.environment ?? 'default', window,
59
+ meaning: 'Exit 0 means requested configured core reads/schema checks executed, not a healthy landscape.', checks: [], optional_live_verification: {}, requests };
60
+ let coreFailed = false;
61
+ try {
62
+ await runtime.server.connect(b); await client.connect(a);
63
+ const names = new Set((await client.listTools()).tools.map(tool => tool.name));
64
+ const call = async (name, input = {}, { core = true, project = value => value, validate = value => value && typeof value === 'object' } = {}) => {
65
+ if (!names.has(name)) {
66
+ report.checks.push({ tool: name, result: 'skipped', reason: 'not_configured_or_not_registered' });
67
+ return null;
68
+ }
69
+ try {
70
+ const response = await client.callTool({ name, arguments: input });
71
+ if (response.isError) {
72
+ const status = /\bHTTP (\d{3})\b/.exec(response.content?.[0]?.text ?? '')?.[1];
73
+ report.checks.push({ tool: name, result: 'failed', ...(status ? { http_status: Number(status) } : {}), error: 'Tool read failed; response body withheld.' });
74
+ if (core) coreFailed = true;
75
+ return null;
76
+ }
77
+ const value = JSON.parse(response.content[0].text);
78
+ if (!validate(value)) throw new Error('contract_error');
79
+ const failed = sourceFailure(value);
80
+ report.checks.push({ tool: name, result: failed ? 'partial_or_failed_source' : 'executed', evidence: project(value) });
81
+ if (core && (failed || value.outcome === 'partial' && value.metrics_complete === false)) coreFailed = true;
82
+ return value;
83
+ } catch {
84
+ report.checks.push({ tool: name, result: 'failed', error: 'Read or contract failure; response body withheld.' });
85
+ if (core) coreFailed = true;
86
+ return null;
87
+ }
88
+ };
89
+ const isPage = value => Array.isArray(value.items) && Number.isInteger(value.returned) && value.returned === value.items.length
90
+ && (value.total === null || Number.isFinite(value.total) && value.total >= 0) && typeof value.complete === 'boolean'
91
+ && (value.next_cursor === null || typeof value.next_cursor === 'string') && typeof value.stop_reason === 'string';
92
+ const connection = await call('integration_connection_info', { verify: true }, { core: false, validate: value => value.verification && typeof value.verification.ok === 'boolean' });
93
+ if (!connection || ['cpi', 'apim', 'devhub'].some(side => config[side] && sourceFailure(connection.verification[side]))) coreFailed = true;
94
+ if (config.cpi) {
95
+ const logs = await call('cpi_list_message_logs', { ...window, top: args.top }, { validate: isPage,
96
+ project: value => page(value, ['MessageGuid', 'CorrelationId', 'ApplicationMessageId', 'Status', 'LogStart', 'LogEnd', 'IntegrationArtifact']) });
97
+ await call('cpi_get_message_statistics', window, { validate: value => value.count_by_status && typeof value.complete === 'boolean' });
98
+ const guid = logs?.items?.[0]?.MessageGuid;
99
+ if (guid) {
100
+ await call('cpi_list_message_log_runs', { message_guid: guid, top: args.top }, { validate: isPage, project: value => page(value, ['Id', 'RunStart', 'RunStop', 'OverallState']) });
101
+ await call('cpi_get_message_log_steps', { message_guid: guid, top: args.top }, { validate: isPage,
102
+ project: value => ({ ...page(value, ['StepId', 'ModelStepId', 'Activity', 'Status', 'StepStart', 'StepStop']), run: pick(value.run, ['Id', 'RunStart', 'RunStop', 'OverallState']) }) });
103
+ } else report.checks.push({ tool: 'cpi_get_message_log_steps', result: 'untested', reason: 'No message fixture in the fixed window.' });
104
+ await call('cpi_list_runtime_artifacts', { top: args.top }, { validate: isPage, project: value => page(value, ['Id', 'Version', 'Status', 'Type', 'DeployedOn']) });
105
+ await call('cpi_get_jms_broker_status');
106
+ await call('cpi_list_jms_queues', { top: args.top }, { validate: isPage, project: value => page(value, ['Name', 'State', 'Active', 'NumbOfMsgs', 'Size', 'FillGrade']) });
107
+ const stores = await call('cpi_list_data_stores', { overdue_only: true, top: args.top }, { validate: isPage,
108
+ project: value => page(value, ['DataStoreName', 'IntegrationFlow', 'Type', 'Visibility', 'NumberOfMessages', 'NumberOfOverdueMessages']) });
109
+ const store = stores?.items?.[0];
110
+ if (store) await call('cpi_list_data_store_entries', { data_store: store.DataStoreName, integration_flow: store.IntegrationFlow, type: store.Type ?? '', top: args.top },
111
+ { validate: isPage, project: value => ({ ...page(value, ['Id', 'Status', 'MessageId', 'DueAt', 'CreatedAt', 'RetainUntil']), store: value.store }) });
112
+ await call('cpi_get_certificate_health', { top: args.top }, { validate: isPage,
113
+ project: value => ({ ...page(value, ['Alias', 'Type', 'KeyType', 'ValidNotBefore', 'ValidNotAfter', 'days_remaining', 'validity']), evaluated_at: value.evaluated_at }) });
114
+ await call('cpi_get_operational_summary', window);
115
+ }
116
+ if (config.apim) {
117
+ await call('apim_get_analytics', window, { validate: value => typeof value.complete === 'boolean' && typeof value.outcome === 'string' });
118
+ await call('apim_get_analytics_series', { ...window, granularity: 'hour', top: args.top }, { validate: isPage });
119
+ await call('apim_get_api_error_breakdown', { ...window, top: args.top }, { validate: isPage });
120
+ }
121
+ // Fixed optional read catalog. Missing credentials are unverified, not a failed entitlement check.
122
+ const optionalReads = [
123
+ ['ans', 'ans_list_subscriptions'], ['ans', 'ans_list_conditions'], ['ans', 'ans_list_action_metadata'],
124
+ ['ctms', 'ctms_list_nodes'], ['aem', 'aem_list_brokers'], ['aem_semp', 'aem_list_queues'],
125
+ ['open_connectors', 'open_connectors_list_instances'], ['edge', 'edge_list_message_logs']
126
+ ];
127
+ for (const [side, name] of optionalReads) {
128
+ if (!config[side] || !names.has(name)) { report.optional_live_verification[side] = 'live_unverified'; continue; }
129
+ const value = await call(name, { ...(side === 'edge' ? window : {}), top: args.top }, { core: false, validate: isPage });
130
+ report.optional_live_verification[side] = value && !sourceFailure(value) ? 'read_exercised' : 'live_unverified';
131
+ }
132
+ if (config.edge) await call('edge_get_api_metadata', {}, { core: false });
133
+ if (config.event_mesh?.probeQueueName) {
134
+ const value = await call('event_mesh_get_queue', { name: config.event_mesh.probeQueueName }, { core: false });
135
+ report.optional_live_verification.event_mesh = value && !sourceFailure(value) ? 'read_exercised' : 'live_unverified';
136
+ } else report.optional_live_verification.event_mesh = 'live_unverified';
137
+ report.core_reads_executed = !coreFailed;
138
+ if (args.json) console.log(JSON.stringify(report, null, 2));
139
+ else {
140
+ console.log(`Read-only Integration Suite check: ${report.core_reads_executed ? 'core reads executed' : 'core read/contract failures'}`);
141
+ console.log(`Window ${window.from} to ${window.to}; ${requests.management_gets} management GETs, ${requests.token_posts} token POSTs.`);
142
+ for (const check of report.checks) console.log(`${check.tool}: ${check.result}`);
143
+ console.log('This is not a health sign-off; inspect degraded/unknown signals and optional live_unverified entries with --json.');
144
+ }
145
+ } finally { await client.close().catch(() => {}); await runtime.close(); }
146
+ if (coreFailed) process.exitCode = 1;
147
+ }
148
+ main().catch(() => {
149
+ console.error('Read-only Integration Suite check could not initialize or complete. Credential values and response bodies are withheld; check host configuration and service access.');
150
+ process.exitCode = 1;
151
+ });
@@ -1,6 +1,5 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { lstat } from 'node:fs/promises';
3
- import { readFile } from 'node:fs/promises';
2
+ import { lstat, readFile, realpath } from 'node:fs/promises';
4
3
  import { dirname, join, resolve as resolvePath } from 'node:path';
5
4
  import { fileURLToPath } from 'node:url';
6
5
 
@@ -18,8 +17,8 @@ function usage() {
18
17
  ' for the selected harnesses).',
19
18
  ' --link Installs with npm link instead of a tarball; edits to src/',
20
19
  ' and scripts/ take effect immediately without reinstalling.',
21
- ' --verify Verifies the global sap-ai-dev command resolves to this',
22
- ' workspace installation.'
20
+ ' --verify Verifies the global command, installed package version,',
21
+ ' and workspace target when using npm link.'
23
22
  ].join('\n');
24
23
  }
25
24
 
@@ -88,10 +87,18 @@ async function verify() {
88
87
  console.log(`Expected location: ${expected}`);
89
88
  throw new Error('Global installation does not match the expected npm layout');
90
89
  }
90
+ const installedPackage = JSON.parse(await readFile(join(installedRoot, 'package.json'), 'utf8'));
91
+ if (installedPackage.name !== pkg.name || installedPackage.version !== pkg.version) {
92
+ throw new Error(`Installed ${installedPackage.name}@${installedPackage.version} does not match workspace ${pkg.name}@${pkg.version}; run npm run install:local`);
93
+ }
91
94
  if (!await lstat(serverCommand).catch(() => null)) {
92
95
  throw new Error(`Global sap-ai-dev command is missing at ${serverCommand}`);
93
96
  }
94
97
  const stats = await lstat(installedRoot).catch(() => null);
98
+ if (stats?.isSymbolicLink() && await realpath(installedRoot) !== await realpath(root)) {
99
+ throw new Error('Global npm link points to a different workspace');
100
+ }
101
+ console.log(`Installed version: ${installedPackage.version}`);
95
102
  console.log(`Install mode: ${stats?.isSymbolicLink() ? 'npm link (live workspace)' : 'tarball (snapshot)'}`);
96
103
  return 0;
97
104
  }
@@ -1,14 +1,14 @@
1
- // Live end-to-end scenarios (S01–S94) for the Integration Suite companion against a real CPI + APIM tenant.
1
+ // Live end-to-end scenarios for the Integration Suite companion against a real CPI + APIM tenant.
2
2
  // WRITES TO THE TENANT: creates a package, iFlows (deployed, one undeployed again), a user credential, a number
3
3
  // range, a Partner Directory parameter, API providers, API proxies (with policies), an API product and a key value
4
- // map, all named with the RUN suffix. There are no delete tools; remove the artifacts in the Integration Suite UI.
4
+ // map, all named with the RUN suffix. APIM products/proxies can be removed with explicit-set deletion tools;
5
+ // CPI design-time artifacts, providers and KVMs still require separate authorized cleanup.
5
6
  // Usage: CPI_SERVICE_KEY=… APIM_SERVICE_KEY=… RUN=<unique suffix> npm run test:live:integration
6
7
  // ONLY=S01,S02 limits the run to selected scenarios (later scenarios may depend on earlier ones).
7
8
  import { readFile } from 'node:fs/promises';
8
9
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
9
10
  import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
10
11
  import { createIntegrationServer } from '../src/integration-server.mjs';
11
- import { INTEGRATION_TOOL_NAMES } from '../src/integration-tools.mjs';
12
12
  import { resolveIntegrationConfig } from '../src/integration-config.mjs';
13
13
  import { readStoredIntegrationKeys } from '../src/integration-credentials.mjs';
14
14
 
@@ -30,7 +30,7 @@ const PROXY_URL = `ZAI_SmokeUrl${RUN}`;
30
30
  const PROXY_ODATA = `ZAI_SmokeOData${RUN}`;
31
31
  const PROXY_CPI = `ZAI_SmokeCpi${RUN}`;
32
32
  const config = resolveIntegrationConfig(process.env, { stored: readStoredIntegrationKeys() });
33
- const SECRETS = Object.values(config).filter(Boolean).map(side => side.clientSecret);
33
+ const SECRETS = Object.values(config).filter(Boolean).map(side => side.clientSecret).filter(Boolean);
34
34
  const PRODUCT = `ZAI_SmokeProduct${RUN}`;
35
35
  const KVM = `ZAI_SmokeKvm${RUN}`;
36
36
  const CREDENTIAL = `ZAI_SMOKE_USER_${RUN}`;
@@ -99,7 +99,7 @@ async function invoke(url, { method = 'POST', body, headers = {}, auth = true, d
99
99
  const text = await response.text();
100
100
  let json;
101
101
  try { json = JSON.parse(text); } catch { /* not JSON */ }
102
- result = { status: response.status, text, json };
102
+ result = { status: response.status, headers: Object.fromEntries(response.headers), text, json };
103
103
  if (done(result)) return result;
104
104
  await sleep(5000);
105
105
  }
@@ -173,14 +173,15 @@ function timerParameters(address) {
173
173
 
174
174
 
175
175
  async function deployAndWait(iflowId) {
176
- const deployed = ok(await call('cpi_deploy_iflow', { iflow_id: iflowId, wait_seconds: 50, confirmation: `DEPLOY IFLOW ${iflowId}` }), `deploy ${iflowId}`);
177
- let status = { Status: deployed.runtime_status, Version: deployed.runtime_version, error_information: deployed.error_information };
178
- for (let attempt = 0; attempt < 36 && (deployed.timed_out || status.Status === 'STARTING' || !status.Status); attempt += 1) {
176
+ let deployed = ok(await call('cpi_deploy_iflow', { iflow_id: iflowId, wait_seconds: 50, confirmation: `DEPLOY IFLOW ${iflowId}` }), `deploy ${iflowId}`);
177
+ for (let attempt = 0; attempt < 36 && ['pending', 'unverified'].includes(deployed.outcome) && deployed.task_id; attempt += 1) {
179
178
  await sleep(5000);
180
- status = ok(await call('cpi_get_iflow_runtime_status', { iflow_id: iflowId }), 'status');
181
- if (status.Status && status.Status !== 'STARTING') break;
179
+ deployed = ok(await call('cpi_get_deployment_task', {
180
+ task_id: deployed.task_id, iflow_id: iflowId, expected_version: deployed.requested_version
181
+ }), 'deployment task');
182
182
  }
183
- return { deployed, runtime: status };
183
+ expect(deployed.outcome === 'succeeded' || deployed.outcome === 'failed' && deployed.runtime_status === 'ERROR', `Deployment not verified: ${JSON.stringify(deployed)}`);
184
+ return { deployed, runtime: { Status: deployed.runtime_status, Version: deployed.runtime_version, error_information: deployed.error_information } };
184
185
  }
185
186
 
186
187
  // ============================================================ shared
@@ -190,21 +191,12 @@ await scenario('S01', 'connection info reports both sides without secrets', asyn
190
191
  return `${info.cpi.host} / ${info.apim.host}`;
191
192
  });
192
193
 
193
- await scenario('S02', `tools/list exposes ${INTEGRATION_TOOL_NAMES.length} tools with write annotations`, async () => {
194
- const { tools } = await client.listTools();
195
- expect(tools.length === INTEGRATION_TOOL_NAMES.length, `expected ${INTEGRATION_TOOL_NAMES.length} tools, got ${tools.length}`);
196
- const byName = Object.fromEntries(tools.map(tool => [tool.name, tool]));
197
- expect(byName.cpi_update_iflow.annotations.destructiveHint === true, 'update must be destructive');
198
- expect(byName.cpi_undeploy_artifact.annotations.destructiveHint === true, 'undeploy must be destructive');
199
- expect(byName.cpi_list_packages.annotations.readOnlyHint === true, 'list must be read-only');
200
- expect(!tools.some(tool => /delete/i.test(tool.name)), 'no delete tools');
201
- });
202
194
 
203
195
  // ============================================================ CPI packages
204
196
  await scenario('S03', 'cpi_create_package with wrong confirmation is refused', async () => {
205
197
  const result = await call('cpi_create_package', { package_id: PKG, name: 'AI smoke', short_text: 'AI smoke', confirmation: `CREATE CPI PACKAGE WRONG` });
206
198
  expect(result.isError && result.text === `confirmation must equal CREATE CPI PACKAGE ${PKG}`, result.text);
207
- const packages = ok(await call('cpi_list_packages', { top: 200 }), 'list');
199
+ const packages = ok(await call('cpi_list_packages', { top: 200 }), 'list').items;
208
200
  expect(!packages.some(entry => entry.Id === PKG), 'package must not exist');
209
201
  });
210
202
 
@@ -220,7 +212,7 @@ await scenario('S05', 'cpi_create_package on an existing id surfaces the tenant
220
212
  });
221
213
 
222
214
  await scenario('S06', 'cpi_list_packages shows the package', async () => {
223
- const packages = ok(await call('cpi_list_packages', { top: 200 }), 'list');
215
+ const packages = ok(await call('cpi_list_packages', { top: 200 }), 'list').items;
224
216
  const entry = packages.find(item => item.Id === PKG);
225
217
  expect(entry, 'package missing');
226
218
  expect(entry.ShortText === 'Created by sap-ai-integration live scenarios', `short text ${entry.ShortText}`);
@@ -247,7 +239,7 @@ await scenario('S08', 'cpi_create_iflow refuses a duplicate id before posting',
247
239
  });
248
240
 
249
241
  await scenario('S09', 'cpi_list_iflows lists the iFlow at 1.0.0', async () => {
250
- const iflows = ok(await call('cpi_list_iflows', { package_id: PKG }), 'list');
242
+ const iflows = ok(await call('cpi_list_iflows', { package_id: PKG }), 'list').items;
251
243
  const entry = iflows.find(item => item.Id === IFLOW);
252
244
  expect(entry && entry.Version === '1.0.0', JSON.stringify(iflows));
253
245
  });
@@ -288,7 +280,7 @@ await scenario('S13', 'cpi_update_iflow_configuration reports an unknown key as
288
280
  confirmation: `CONFIGURE IFLOW ${IFLOW}`
289
281
  }), 'configure');
290
282
  expect(result.updated.length === 1 && result.failed?.key === 'No_Such_Key', JSON.stringify(result));
291
- return result.failed.error.slice(0, 120);
283
+ return JSON.stringify(result.failed);
292
284
  });
293
285
 
294
286
  await scenario('S14', 'cpi_update_iflow rejects a stale expected_version', async () => {
@@ -331,7 +323,7 @@ await scenario('S17', 'cpi_get_iflow_runtime_status reports STARTED at 1.0.1', a
331
323
  await scenario('S18', 'cpi_list_service_endpoints returns the iFlow URL', async () => {
332
324
  let endpoints = [];
333
325
  for (let attempt = 0; attempt < 36 && !endpoints.length; attempt += 1) {
334
- endpoints = ok(await call('cpi_list_service_endpoints', { iflow_id: IFLOW }), 'endpoints');
326
+ endpoints = ok(await call('cpi_list_service_endpoints', { iflow_id: IFLOW }), 'endpoints').items;
335
327
  if (!endpoints.length) await sleep(5000);
336
328
  }
337
329
  const url = endpoints.flatMap(entry => entry.urls).find(value => value.endsWith(`/http/ai/smoke${RUN}`));
@@ -363,7 +355,7 @@ await scenario('S20', 'custom BPMN (timer start) iFlow is created with caller pa
363
355
  const xml = timerIflw();
364
356
  ok(await call('cpi_create_iflow', { package_id: PKG, iflow_id: IFLOW_TIMER_OK, name: `AI Timer Ok ${RUN}`, template: 'custom', iflw_xml: xml, parameters: timerParameters('https://httpbin.org/anything/timer-ok'), confirmation: `CREATE IFLOW ${IFLOW_TIMER_OK}` }), 'create ok');
365
357
  ok(await call('cpi_create_iflow', { package_id: PKG, iflow_id: IFLOW_TIMER_FAIL, name: `AI Timer Fail ${RUN}`, template: 'custom', iflw_xml: xml, parameters: timerParameters('https://httpbin.org/status/500'), confirmation: `CREATE IFLOW ${IFLOW_TIMER_FAIL}` }), 'create fail');
366
- const iflows = ok(await call('cpi_list_iflows', { package_id: PKG }), 'list');
358
+ const iflows = ok(await call('cpi_list_iflows', { package_id: PKG }), 'list').items;
367
359
  expect([IFLOW_TIMER_OK, IFLOW_TIMER_FAIL].every(id => iflows.some(entry => entry.Id === id)), 'both listed');
368
360
  });
369
361
 
@@ -378,7 +370,7 @@ await scenario('S21', 'timer iFlows deploy and start', async () => {
378
370
  async function waitForLog(iflowId, status) {
379
371
  for (let attempt = 0; attempt < 24; attempt += 1) {
380
372
  const result = ok(await call('cpi_list_message_logs', { iflow_id: iflowId, status, top: 5 }), 'logs');
381
- if (result.logs.length) return result;
373
+ if (result.items.length) return result;
382
374
  await sleep(5000);
383
375
  }
384
376
  throw new Error(`no ${status} log for ${iflowId} after 120 s`);
@@ -386,7 +378,7 @@ async function waitForLog(iflowId, status) {
386
378
 
387
379
  await scenario('S22', 'cpi_list_message_logs finds the COMPLETED timer run', async () => {
388
380
  const result = await waitForLog(IFLOW_TIMER_OK, 'COMPLETED');
389
- const [log] = result.logs;
381
+ const [log] = result.items;
390
382
  expect(log.Status === 'COMPLETED' && /^\d{4}-\d\d-\d\dT/.test(log.LogStart), JSON.stringify(log));
391
383
  ctx.okGuid = log.MessageGuid;
392
384
  return `${log.MessageGuid} ${log.LogStart}`;
@@ -394,7 +386,7 @@ await scenario('S22', 'cpi_list_message_logs finds the COMPLETED timer run', asy
394
386
 
395
387
  await scenario('S23', 'cpi_list_message_logs + cpi_get_message_log return the FAILED run with error text', async () => {
396
388
  const result = await waitForLog(IFLOW_TIMER_FAIL, 'FAILED');
397
- const detail = ok(await call('cpi_get_message_log', { message_guid: result.logs[0].MessageGuid }), 'detail');
389
+ const detail = ok(await call('cpi_get_message_log', { message_guid: result.items[0].MessageGuid }), 'detail');
398
390
  expect(detail.log.Status === 'FAILED', JSON.stringify(detail.log));
399
391
  expect(detail.error_information && /500/.test(detail.error_information), `error text: ${detail.error_information}`);
400
392
  return detail.error_information.replace(/\s+/g, ' ').slice(0, 140);
@@ -408,10 +400,11 @@ await scenario('S24', 'cpi_get_message_log for a COMPLETED run has no error text
408
400
  });
409
401
 
410
402
  await scenario('S25', 'cpi_list_message_logs time window filters (from/to)', async () => {
411
- const future = ok(await call('cpi_list_message_logs', { iflow_id: IFLOW_TIMER_OK, from: '2099-01-01T00:00:00Z' }), 'future');
412
- expect(future.logs.length === 0, 'future window must be empty');
413
- const past = ok(await call('cpi_list_message_logs', { iflow_id: IFLOW_TIMER_OK, from: '2020-01-01T00:00:00Z', to: '2099-01-01T00:00:00Z' }), 'past');
414
- expect(past.logs.length >= 1, 'wide window must include the run');
403
+ const future = ok(await call('cpi_list_message_logs', { iflow_id: IFLOW_TIMER_OK, from: '2099-01-01T00:00:00Z', to: '2099-01-02T00:00:00Z' }), 'future');
404
+ expect(future.items.length === 0, 'future window must be empty');
405
+ const now = new Date();
406
+ const past = ok(await call('cpi_list_message_logs', { iflow_id: IFLOW_TIMER_OK, from: new Date(now.getTime() - 86400000).toISOString(), to: now.toISOString() }), 'past');
407
+ expect(past.items.length >= 1, 'recent window must include the run');
415
408
  return `total=${past.total}`;
416
409
  });
417
410
 
@@ -449,7 +442,7 @@ await scenario('S29', 'apim_create_api_provider creates an On-Premise provider w
449
442
  });
450
443
 
451
444
  await scenario('S30', 'apim_list_api_providers lists both providers', async () => {
452
- const providers = ok(await call('apim_list_api_providers', { top: 200 }), 'list');
445
+ const providers = ok(await call('apim_list_api_providers', { top: 200 }), 'list').items;
453
446
  expect([PROVIDER, PROVIDER_ONPREM].every(name => providers.some(entry => entry.name === name)), JSON.stringify(providers.map(entry => entry.name)));
454
447
  });
455
448
 
@@ -457,7 +450,7 @@ await scenario('S30', 'apim_list_api_providers lists both providers', async () =
457
450
  await scenario('S31', 'apim_create_api_proxy fails for a missing provider before importing', async () => {
458
451
  const result = await call('apim_create_api_proxy', { name: `${PROXY}x`, base_path: `/ai/missing${RUN}`, service_type: 'rest', target: { provider: 'ZAI_NO_SUCH_PROVIDER', relative_path: '/x' }, confirmation: `CREATE API PROXY ${PROXY}x` });
459
452
  expect(result.isError && result.text === 'API provider ZAI_NO_SUCH_PROVIDER does not exist; create it with apim_create_api_provider', result.text);
460
- const proxies = ok(await call('apim_list_api_proxies', { top: 200 }), 'list');
453
+ const proxies = ok(await call('apim_list_api_proxies', { top: 200 }), 'list').items;
461
454
  expect(!proxies.some(entry => entry.name === `${PROXY}x`), 'proxy must not exist');
462
455
  });
463
456
 
@@ -543,7 +536,7 @@ await scenario('S39', 'APIM in front of CPI: proxy to the iFlow endpoint reaches
543
536
  });
544
537
 
545
538
  await scenario('S40', 'apim_list_api_proxies shows all smoke proxies deployed', async () => {
546
- const proxies = ok(await call('apim_list_api_proxies', { top: 200 }), 'list');
539
+ const proxies = ok(await call('apim_list_api_proxies', { top: 200 }), 'list').items;
547
540
  const mine = proxies.filter(entry => [PROXY, PROXY_URL, PROXY_ODATA, PROXY_CPI].includes(entry.name));
548
541
  expect(mine.length === 4, JSON.stringify(mine));
549
542
  return mine.map(entry => `${entry.name}:${entry.state}`).join(', ');
@@ -569,7 +562,7 @@ await scenario('S41', 'runtime call to the template iFlow passes body and query
569
562
  expect(/\/anything\/configured/.test(result.json.url), `receiver url ${result.json.url}`);
570
563
  expect(result.json.args.probe === RUN, `query ${JSON.stringify(result.json.args)}`);
571
564
  expect(JSON.parse(result.json.data).kind === 'direct', `body ${result.json.data}`);
572
- ctx.directGuid = result.json.headers['Sap-Messageprocessinglogid'];
565
+ ctx.directGuid = result.headers['sap_messageprocessinglogid'];
573
566
  expect(ctx.directGuid, 'no MPL id header forwarded');
574
567
  return `MPL ${ctx.directGuid}`;
575
568
  });
@@ -586,7 +579,7 @@ await scenario('S43', 'APIM proxy in front of CPI forwards the caller bearer tok
586
579
  const result = await invoke(proxy.runtime_url, { body: JSON.stringify({ via: 'apim-url-target', run: RUN }) });
587
580
  expect(result.status === 200, `HTTP ${result.status}: ${result.text.slice(0, 200)}`);
588
581
  expect(JSON.parse(result.json.data).via === 'apim-url-target', result.json.data);
589
- ctx.apimGuid = result.json.headers['Sap-Messageprocessinglogid'];
582
+ ctx.apimGuid = result.headers['sap_messageprocessinglogid'];
590
583
  return `${proxy.runtime_url} → CPI → httpbin`;
591
584
  });
592
585
 
@@ -612,14 +605,14 @@ await scenario('S46', 'iFlow with Unicode description, POST receiver and static
612
605
  template_parameters: { sender_path: `/ai/post${RUN}`, receiver_address: 'https://httpbin.org/anything/post', receiver_query: 'static=1&mode=test', receiver_method: 'POST', receiver_proxy_type: 'internet', receiver_authentication: 'None', receiver_timeout_ms: 20000 },
613
606
  confirmation: `CREATE IFLOW ${IFLOW_POST}`
614
607
  }), 'create');
615
- const listed = ok(await call('cpi_list_iflows', { package_id: PKG }), 'list').find(entry => entry.Id === IFLOW_POST);
608
+ const listed = ok(await call('cpi_list_iflows', { package_id: PKG }), 'list').items.find(entry => entry.Id === IFLOW_POST);
616
609
  expect(listed?.Name === ASCII_NAME, `name read back ${listed?.Name}`);
617
610
  expect(listed?.Description === UNICODE_DESCRIPTION, `description read back ${listed?.Description}`);
618
611
  const { runtime } = await deployAndWait(IFLOW_POST);
619
612
  expect(runtime.Status === 'STARTED', JSON.stringify(runtime));
620
- const endpoints = ok(await call('cpi_list_service_endpoints', { iflow_id: IFLOW_POST }), 'endpoints');
621
- ctx.postEndpoint = endpoints.flatMap(entry => entry.urls)[0] ?? ctx.cpiEndpoint.replace(`/http/ai/smoke${RUN}`, `/http/ai/post${RUN}`);
622
- return `description "${listed.Description}"`;
613
+ const endpoints = ok(await call('cpi_list_service_endpoints', { iflow_id: IFLOW_POST }), 'endpoints').items;
614
+ ctx.postEndpoint = endpoints.flatMap(entry => entry.urls)[0] ?? ctx.cpiEndpoint?.replace(`/http/ai/smoke${RUN}`, `/http/ai/post${RUN}`);
615
+ expect(ctx.postEndpoint, 'no POST endpoint; run with S18 in ONLY');
623
616
  });
624
617
 
625
618
  await scenario('S47', 'POST receiver gets method, static query and body', async () => {
@@ -664,7 +657,7 @@ await scenario('S50', 'large script (57 KiB): truncated in the listing, complete
664
657
  await scenario('S51', 'rename through cpi_update_iflow keeps version chain consistent', async () => {
665
658
  const renamed = ok(await call('cpi_update_iflow', { iflow_id: IFLOW_POST, expected_version: '1.0.2', name: `${ASCII_NAME} v2`, files: [{ path: 'src/main/resources/script/marker.groovy', content: '// marker v2\n' }], confirmation: `UPDATE IFLOW ${IFLOW_POST}` }), 'update');
666
659
  expect(renamed.new_version === '1.0.3', JSON.stringify(renamed));
667
- const listed = ok(await call('cpi_list_iflows', { package_id: PKG }), 'list').find(entry => entry.Id === IFLOW_POST);
660
+ const listed = ok(await call('cpi_list_iflows', { package_id: PKG }), 'list').items.find(entry => entry.Id === IFLOW_POST);
668
661
  expect(listed.Name === `${ASCII_NAME} v2` && listed.Version === '1.0.3', JSON.stringify(listed));
669
662
  });
670
663
 
@@ -680,15 +673,15 @@ await scenario('S53', 'message log filters: COMPLETED for the POST iFlow, top=1,
680
673
  let completed;
681
674
  for (let attempt = 0; attempt < 18; attempt += 1) {
682
675
  completed = ok(await call('cpi_list_message_logs', { iflow_id: IFLOW_POST, status: 'COMPLETED', top: 50 }), 'completed');
683
- if (completed.logs.length >= 3) break;
676
+ if (completed.items.length >= 3) break;
684
677
  await sleep(5000);
685
678
  }
686
- expect(completed.logs.length >= 3 && completed.logs.every(log => log.Status === 'COMPLETED'), `${completed.logs.length} logs`);
679
+ expect(completed.items.length >= 3 && completed.items.every(log => log.Status === 'COMPLETED'), `${completed.items.length} logs`);
687
680
  const one = ok(await call('cpi_list_message_logs', { iflow_id: IFLOW_POST, top: 1 }), 'top1');
688
- expect(one.returned === 1 && one.total >= completed.logs.length, JSON.stringify({ returned: one.returned, total: one.total }));
689
- const past = ok(await call('cpi_list_message_logs', { iflow_id: IFLOW_POST, to: '2020-01-01T00:00:00Z' }), 'past');
690
- expect(past.logs.length === 0, 'past window must be empty');
691
- const ordered = completed.logs.map(log => log.LogEnd);
681
+ expect(one.returned === 1 && one.total >= completed.items.length, JSON.stringify({ returned: one.returned, total: one.total }));
682
+ const past = ok(await call('cpi_list_message_logs', { iflow_id: IFLOW_POST, from: '2019-12-31T00:00:00Z', to: '2020-01-01T00:00:00Z' }), 'past');
683
+ expect(past.items.length === 0, 'past window must be empty');
684
+ const ordered = completed.items.map(log => log.LogStart);
692
685
  expect(ordered.every((value, index) => index === 0 || ordered[index - 1] >= value), 'logs must be newest first');
693
686
  return `total=${one.total}`;
694
687
  });
@@ -700,7 +693,7 @@ await scenario('S54', 'receiver HTTP 503 → caller error and FAILED log with th
700
693
  const result = await invoke(ctx.cpiEndpoint, { body: '{}', done: response => response.status >= 500 && response.status !== 503 });
701
694
  expect(result.status === 500, `caller got HTTP ${result.status}`);
702
695
  const failed = await waitForLog(IFLOW, 'FAILED');
703
- const detail = ok(await call('cpi_get_message_log', { message_guid: failed.logs[0].MessageGuid }), 'detail');
696
+ const detail = ok(await call('cpi_get_message_log', { message_guid: failed.items[0].MessageGuid }), 'detail');
704
697
  expect(/503/.test(detail.error_information || ''), `error ${detail.error_information}`);
705
698
  return detail.error_information.replace(/\s+/g, ' ').slice(0, 120);
706
699
  });
@@ -720,7 +713,7 @@ await scenario('S55', 'missing security material → FAILED log names the creden
720
713
  const result = await invoke(endpoint, { body: '{}', done: response => response.status !== 404 && response.status !== 503 });
721
714
  expect(result.status >= 500, `caller got HTTP ${result.status}`);
722
715
  const failed = await waitForLog(IFLOW_CRED, 'FAILED');
723
- const detail = ok(await call('cpi_get_message_log', { message_guid: failed.logs[0].MessageGuid }), 'detail');
716
+ const detail = ok(await call('cpi_get_message_log', { message_guid: failed.items[0].MessageGuid }), 'detail');
724
717
  expect(/ZAI_MISSING_CRED|credential/i.test(detail.error_information || ''), `error ${detail.error_information}`);
725
718
  return detail.error_information.replace(/\s+/g, ' ').slice(0, 120);
726
719
  });
@@ -776,7 +769,7 @@ await scenario('S61', 'apim_update_api_proxy moves a proxy to a new base path',
776
769
  });
777
770
 
778
771
  await scenario('S62', 'apim_get_api_proxy runtime_url uses the default virtual host and base path', async () => {
779
- const hosts = ok(await call('apim_list_virtual_hosts'), 'hosts');
772
+ const hosts = ok(await call('apim_list_virtual_hosts'), 'hosts').items;
780
773
  const host = hosts.find(entry => entry.isDefault);
781
774
  const proxy = ok(await call('apim_get_api_proxy', { name: PROXY_CPI_PROVIDER }), 'get');
782
775
  expect(proxy.runtime_url === `https://${host.virtual_host}/ai/cpiprov${RUN}`, `${proxy.runtime_url} vs ${host.virtual_host}`);
@@ -784,7 +777,7 @@ await scenario('S62', 'apim_get_api_proxy runtime_url uses the default virtual h
784
777
 
785
778
  await scenario('S63', 'apim_list_api_proxies honours top', async () => {
786
779
  const one = ok(await call('apim_list_api_proxies', { top: 1 }), 'list');
787
- expect(one.length === 1, `${one.length} proxies`);
780
+ expect(one.returned === 1, `${one.returned} proxies`);
788
781
  });
789
782
 
790
783
  await scenario('S64', 'XML-special title/description are escaped in the bundle and read back exactly', async () => {
@@ -819,7 +812,7 @@ await scenario('S67', 'API provider input validation rejects bad combinations',
819
812
  const result = await call('apim_create_api_provider', { ...args, confirmation: `CREATE API PROVIDER ${args.name}` });
820
813
  expect(result.isError, `accepted ${JSON.stringify(args)}`);
821
814
  }
822
- const providers = ok(await call('apim_list_api_providers', { top: 200 }), 'list');
815
+ const providers = ok(await call('apim_list_api_providers', { top: 200 }), 'list').items;
823
816
  expect(!providers.some(entry => /^ZAIBAD/.test(entry.name)), 'invalid provider was created');
824
817
  });
825
818
 
@@ -873,7 +866,7 @@ await scenario('S72', 'message log lookups: malformed GUID rejected, unknown GUI
873
866
  await scenario('S73', 'deploy and configure of a missing iFlow fail cleanly', async () => {
874
867
  cleanError(await call('cpi_deploy_iflow', { iflow_id: 'Z_AI_No_Such_Flow', wait_seconds: 5, confirmation: 'DEPLOY IFLOW Z_AI_No_Such_Flow' }), /^CPI /);
875
868
  const configured = ok(await call('cpi_update_iflow_configuration', { iflow_id: 'Z_AI_No_Such_Flow', parameters: [{ key: 'A', value: 'b' }], confirmation: 'CONFIGURE IFLOW Z_AI_No_Such_Flow' }), 'configure');
876
- expect(configured.updated.length === 0 && /^CPI PUT/.test(configured.failed?.error || ''), JSON.stringify(configured));
869
+ expect(configured.updated.length === 0 && configured.failed?.status === 404, JSON.stringify(configured));
877
870
  });
878
871
 
879
872
  await scenario('S74', 'update with a missing iFlow fails without writing', async () => {
@@ -903,7 +896,7 @@ await scenario('S76', 'CPI-runtime API provider read back has no secrets and Int
903
896
  });
904
897
 
905
898
  await scenario('S77', 'service endpoints without a filter include every HTTPS iFlow of this run', async () => {
906
- const endpoints = ok(await call('cpi_list_service_endpoints'), 'endpoints');
899
+ const endpoints = ok(await call('cpi_list_service_endpoints'), 'endpoints').items;
907
900
  const urls = endpoints.flatMap(entry => entry.urls);
908
901
  for (const suffix of [`/http/ai/smoke${RUN}`, `/http/ai/post${RUN}`]) expect(urls.some(url => url.endsWith(suffix)), `missing ${suffix}`);
909
902
  return `${endpoints.length} endpoints`;
@@ -921,7 +914,7 @@ await scenario('S79', 'full chain after recovery: client → APIM (provider) →
921
914
  const proxy = ok(await call('apim_get_api_proxy', { name: PROXY_CPI_PROVIDER }), 'get');
922
915
  const result = await invoke(proxy.runtime_url, { body: '{"final":true}', done: response => response.status === 200 && /restored/.test(response.json?.url || '') });
923
916
  expect(result.status === 200 && JSON.parse(result.json.data).final === true, `HTTP ${result.status}`);
924
- const detail = await waitForGuid(result.json.headers['Sap-Messageprocessinglogid']);
917
+ const detail = await waitForGuid(result.headers['sap_messageprocessinglogid']);
925
918
  expect(detail.log.Status === 'COMPLETED', detail.log.Status);
926
919
  });
927
920
 
@@ -937,7 +930,7 @@ await scenario('S81', 'odata_get readers list entity sets from both $metadata do
937
930
 
938
931
  await scenario('S82', 'cpi_list_runtime_artifacts and cpi_list_package_artifacts show this run', async () => {
939
932
  const runtime = ok(await call('cpi_list_runtime_artifacts', { top: 200 }), 'runtime');
940
- const artifacts = runtime.artifacts ?? runtime;
933
+ const artifacts = runtime.items;
941
934
  expect(JSON.stringify(artifacts).includes(IFLOW), `${IFLOW} not deployed in runtime list`);
942
935
  const contents = ok(await call('cpi_list_package_artifacts', { package_id: PKG }), 'package');
943
936
  expect(JSON.stringify(contents).includes(IFLOW), 'package contents miss the iFlow');
@@ -946,7 +939,7 @@ await scenario('S82', 'cpi_list_runtime_artifacts and cpi_list_package_artifacts
946
939
  await scenario('S83', 'cpi_get_message_log_steps returns the steps of a runtime call', async () => {
947
940
  expect(ctx.directGuid, 'needs S41');
948
941
  const steps = ok(await call('cpi_get_message_log_steps', { message_guid: ctx.directGuid }), 'steps');
949
- expect(JSON.stringify(steps).length > 50, JSON.stringify(steps));
942
+ expect(Array.isArray(steps.items) && steps.returned === steps.items.length, JSON.stringify(steps));
950
943
  return JSON.stringify(steps).slice(0, 160);
951
944
  });
952
945
 
@@ -983,8 +976,8 @@ await scenario('S86', 'cpi_set_number_range creates a range and reads it back',
983
976
  await scenario('S87', 'cpi_set_partner_parameter creates and updates a string parameter', async () => {
984
977
  ok(await call('cpi_set_partner_parameter', { pid: PARTNER, id: 'ReceiverAddress', type: 'string', value: 'https://a.example', confirmation: `SET PARTNER PARAMETER ${PARTNER}/ReceiverAddress` }), 'create');
985
978
  ok(await call('cpi_set_partner_parameter', { pid: PARTNER, id: 'ReceiverAddress', type: 'string', value: 'https://b.example', confirmation: `SET PARTNER PARAMETER ${PARTNER}/ReceiverAddress` }), 'update');
986
- const listed = ok(await call('cpi_list_partner_parameters', { pid: PARTNER }), 'list');
987
- expect(JSON.stringify(listed).includes('https://b.example'), JSON.stringify(listed).slice(0, 300));
979
+ const listed = ok(await call('cpi_list_partner_parameters', { pid: PARTNER, type: 'string' }), 'list');
980
+ expect(listed.items.some(entry => entry.Id === 'ReceiverAddress'), JSON.stringify(listed).slice(0, 300));
988
981
  });
989
982
 
990
983
  await scenario('S88', 'apim_create_key_value_map (encrypted) and set entries; values never returned', async () => {
@@ -1065,8 +1058,7 @@ await scenario('S94', 'integration_connection_info verify: both sides sign in an
1065
1058
  return `CPI: ${summary(verification.cpi)} | APIM: ${summary(verification.apim)}`;
1066
1059
  });
1067
1060
 
1068
- await scenario('S80', `after all writes: ${INTEGRATION_TOOL_NAMES.length} tools, both sides configured, no secrets emitted`, async () => {
1069
- expect((await client.listTools()).tools.length === INTEGRATION_TOOL_NAMES.length, 'tool count changed');
1061
+ await scenario('S80', 'after all writes: core sides remain configured and tool outputs contain no secrets', async () => {
1070
1062
  const info = ok(await call('integration_connection_info'), 'info');
1071
1063
  expect(info.cpi.configured && info.apim.configured, 'side lost');
1072
1064
  return `${results.filter(result => result.status === 'PASS').length} passed so far; every tool output scanned for ${SECRETS.length} secrets`;