sap-ai-dev-toolkit 0.9.9917 → 0.9.9920
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/sap-integration-architect.agent.md +32 -18
- package/.github/agents/sap-integration-developer.agent.md +28 -12
- package/.github/skills/integration-suite-delivery/SKILL.md +94 -37
- package/.github/skills/sap-integration-architect/SKILL.md +49 -25
- package/README.md +73 -14
- package/dist/checksums.txt +5 -5
- 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/inventory.md +97 -57
- package/package.json +1 -1
- package/scripts/check-live-integration.mjs +151 -0
- package/scripts/install-local.mjs +11 -4
- package/scripts/run-live-integration.mjs +52 -60
- package/src/apim-bundle.mjs +416 -13
- package/src/apim-cleanup-tools.mjs +145 -0
- package/src/apim-content.mjs +157 -0
- package/src/apim-delete-tools.mjs +139 -0
- package/src/apim-documentation.mjs +162 -0
- package/src/apim-inspection.mjs +141 -0
- package/src/apim-monitoring.mjs +140 -0
- package/src/apim-policy-steps.mjs +31 -0
- package/src/apim-runtime.mjs +262 -0
- package/src/apim-tools.mjs +202 -109
- package/src/cf-integration.mjs +46 -7
- package/src/cpi-async-monitoring.mjs +177 -0
- package/src/cpi-content.mjs +92 -0
- package/src/cpi-monitoring.mjs +225 -0
- package/src/cpi-operational.mjs +129 -0
- package/src/cpi-release.mjs +273 -0
- package/src/cpi-security.mjs +48 -0
- package/src/cpi-tools.mjs +184 -319
- package/src/devhub-retirement.mjs +345 -0
- package/src/devhub-tools.mjs +177 -0
- package/src/edge-monitoring.mjs +101 -0
- package/src/iflow-bundle.mjs +295 -7
- package/src/integration-alerts.mjs +248 -0
- package/src/integration-analysis.mjs +488 -0
- package/src/integration-capabilities.mjs +181 -0
- package/src/integration-collections.mjs +203 -0
- package/src/integration-common.mjs +140 -35
- package/src/integration-config.mjs +20 -14
- package/src/integration-credentials.mjs +16 -10
- package/src/integration-http.mjs +255 -67
- package/src/integration-messaging.mjs +118 -0
- package/src/integration-optional-config.mjs +122 -0
- package/src/integration-optional-tools.mjs +153 -0
- package/src/integration-release-store.mjs +410 -0
- package/src/integration-server.mjs +34 -16
- package/src/integration-state.mjs +31 -0
- package/src/integration-tools.mjs +44 -8
- package/src/integration-transport.mjs +184 -0
- package/src/integration-validation.mjs +26 -0
- package/src/integration-verify.mjs +143 -62
- package/src/open-connectors-tools.mjs +143 -0
- package/src/setup.mjs +66 -44
- 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
|
|
22
|
-
' workspace
|
|
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,16 @@
|
|
|
1
|
-
// Live end-to-end scenarios
|
|
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.
|
|
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 {
|
|
12
|
+
import { resolveIntegrationConfig } from '../src/integration-config.mjs';
|
|
13
|
+
import { readStoredIntegrationKeys } from '../src/integration-credentials.mjs';
|
|
12
14
|
|
|
13
15
|
const RUN = process.env.RUN;
|
|
14
16
|
if (!RUN || !/^[A-Za-z0-9]{1,8}$/.test(RUN)) {
|
|
@@ -27,10 +29,8 @@ const PROXY = `ZAI_Smoke${RUN}`;
|
|
|
27
29
|
const PROXY_URL = `ZAI_SmokeUrl${RUN}`;
|
|
28
30
|
const PROXY_ODATA = `ZAI_SmokeOData${RUN}`;
|
|
29
31
|
const PROXY_CPI = `ZAI_SmokeCpi${RUN}`;
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
return [key.oauth?.clientsecret, key.clientSecret].filter(Boolean);
|
|
33
|
-
});
|
|
32
|
+
const config = resolveIntegrationConfig(process.env, { stored: readStoredIntegrationKeys() });
|
|
33
|
+
const SECRETS = Object.values(config).filter(Boolean).map(side => side.clientSecret);
|
|
34
34
|
const PRODUCT = `ZAI_SmokeProduct${RUN}`;
|
|
35
35
|
const KVM = `ZAI_SmokeKvm${RUN}`;
|
|
36
36
|
const CREDENTIAL = `ZAI_SMOKE_USER_${RUN}`;
|
|
@@ -173,14 +173,15 @@ function timerParameters(address) {
|
|
|
173
173
|
|
|
174
174
|
|
|
175
175
|
async function deployAndWait(iflowId) {
|
|
176
|
-
|
|
177
|
-
let
|
|
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
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
413
|
-
const
|
|
414
|
-
|
|
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(', ');
|
|
@@ -612,12 +605,12 @@ 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');
|
|
613
|
+
const endpoints = ok(await call('cpi_list_service_endpoints', { iflow_id: IFLOW_POST }), 'endpoints').items;
|
|
621
614
|
ctx.postEndpoint = endpoints.flatMap(entry => entry.urls)[0] ?? ctx.cpiEndpoint.replace(`/http/ai/smoke${RUN}`, `/http/ai/post${RUN}`);
|
|
622
615
|
return `description "${listed.Description}"`;
|
|
623
616
|
});
|
|
@@ -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.
|
|
676
|
+
if (completed.items.length >= 3) break;
|
|
684
677
|
await sleep(5000);
|
|
685
678
|
}
|
|
686
|
-
expect(completed.
|
|
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.
|
|
689
|
-
const past = ok(await call('cpi_list_message_logs', { iflow_id: IFLOW_POST, to: '2020-01-01T00:00:00Z' }), 'past');
|
|
690
|
-
expect(past.
|
|
691
|
-
const ordered = completed.
|
|
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.
|
|
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.
|
|
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.
|
|
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 &&
|
|
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`;
|
|
@@ -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.
|
|
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(
|
|
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(
|
|
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',
|
|
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`;
|