@aifabrix/builder 2.44.0 → 2.44.2

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 (71) hide show
  1. package/.cursor/rules/cli-layout.mdc +75 -0
  2. package/.cursor/rules/project-rules.mdc +8 -0
  3. package/.npmrc.token +1 -0
  4. package/.nyc_output/55e9d034-ddab-4579-a706-e02a91d75c91.json +1 -0
  5. package/.nyc_output/processinfo/55e9d034-ddab-4579-a706-e02a91d75c91.json +1 -0
  6. package/.nyc_output/processinfo/index.json +1 -0
  7. package/jest.projects.js +15 -2
  8. package/lib/api/certificates.api.js +62 -0
  9. package/lib/api/index.js +11 -2
  10. package/lib/api/types/certificates.types.js +48 -0
  11. package/lib/api/validation-run.api.js +16 -4
  12. package/lib/api/validation-runner.js +13 -3
  13. package/lib/app/certification-show-enrich.js +129 -0
  14. package/lib/app/certification-verify-rows.js +60 -0
  15. package/lib/app/show-display.js +43 -0
  16. package/lib/app/show.js +92 -8
  17. package/lib/certification/cli-cert-sync-skip.js +21 -0
  18. package/lib/certification/merge-certification-from-artifact.js +185 -0
  19. package/lib/certification/post-unified-cert-sync.js +33 -0
  20. package/lib/certification/sync-after-external-command.js +52 -0
  21. package/lib/certification/sync-system-certification.js +197 -0
  22. package/lib/cli/setup-app.js +4 -0
  23. package/lib/cli/setup-app.test-commands.js +24 -8
  24. package/lib/cli/setup-external-system.js +22 -1
  25. package/lib/cli/setup-secrets.js +34 -13
  26. package/lib/cli/setup-utility.js +18 -2
  27. package/lib/commands/app.js +10 -1
  28. package/lib/commands/datasource-unified-test-cli.js +50 -117
  29. package/lib/commands/datasource-unified-test-cli.options.js +44 -2
  30. package/lib/commands/datasource-unified-test-e2e-cli-helpers.js +106 -0
  31. package/lib/commands/datasource-validation-cli.js +15 -1
  32. package/lib/commands/datasource.js +25 -2
  33. package/lib/commands/upload.js +17 -6
  34. package/lib/core/secrets.js +47 -13
  35. package/lib/datasource/log-viewer.js +135 -20
  36. package/lib/datasource/test-e2e.js +35 -17
  37. package/lib/datasource/unified-validation-run-body.js +3 -0
  38. package/lib/datasource/unified-validation-run.js +2 -1
  39. package/lib/external-system/deploy.js +53 -18
  40. package/lib/infrastructure/compose.js +12 -3
  41. package/lib/infrastructure/helpers-docker-check.js +67 -0
  42. package/lib/infrastructure/helpers.js +47 -58
  43. package/lib/infrastructure/index.js +3 -1
  44. package/lib/infrastructure/services.js +4 -56
  45. package/lib/schema/external-system.schema.json +25 -3
  46. package/lib/schema/type/document-storage.json +15 -2
  47. package/lib/utils/api.js +28 -3
  48. package/lib/utils/app-service-env-from-builder.js +47 -6
  49. package/lib/utils/config-paths.js +4 -0
  50. package/lib/utils/configuration-env-resolver.js +11 -8
  51. package/lib/utils/credential-secrets-env.js +5 -5
  52. package/lib/utils/datasource-test-run-certificate-tty.js +82 -0
  53. package/lib/utils/datasource-test-run-display.js +19 -2
  54. package/lib/utils/datasource-test-run-exit.js +25 -0
  55. package/lib/utils/external-system-display.js +8 -0
  56. package/lib/utils/external-system-system-test-tty-overview.js +120 -0
  57. package/lib/utils/external-system-system-test-tty.js +417 -0
  58. package/lib/utils/paths.js +14 -0
  59. package/lib/utils/url-declarative-resolve-load-doc.js +50 -20
  60. package/lib/utils/urls-local-registry.js +36 -12
  61. package/lib/utils/validation-run-poll.js +28 -5
  62. package/lib/utils/validation-run-post-retry.js +20 -8
  63. package/lib/utils/validation-run-request.js +18 -0
  64. package/lib/validation/validate-external-cert-sync.js +23 -0
  65. package/lib/validation/validate.js +4 -1
  66. package/package.json +4 -3
  67. package/scripts/install-local.js +4 -1
  68. package/scripts/pnpm-global-remove.js +48 -0
  69. package/templates/applications/dataplane/env.template +4 -0
  70. package/templates/infra/compose.yaml.hbs +15 -14
  71. package/templates/infra/servers.json.hbs +3 -1
@@ -12,8 +12,21 @@
12
12
  const path = require('path');
13
13
  const yaml = require('js-yaml');
14
14
  const fsRealSync = require('../internal/fs-real-sync');
15
+ const pathsUtil = require('./paths');
15
16
  const { localHostPort } = require('./declarative-url-ports');
16
17
 
18
+ /**
19
+ * @param {string} p
20
+ * @returns {boolean}
21
+ */
22
+ function isExistingDirSync(p) {
23
+ try {
24
+ return Boolean(p && fsRealSync.existsSync(p) && fsRealSync.statSync(p).isDirectory());
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+
17
30
  /**
18
31
  * Maps application.yaml `app.key` to env var prefix (MISO_HOST, DATAPLANE_PORT, …).
19
32
  * Generic keys not listed are skipped (no guessed PREFIX).
@@ -110,12 +123,11 @@ function mergeDocIntoOverlay(overlayDocker, overlayLocal, doc, folderName) {
110
123
  }
111
124
 
112
125
  /**
113
- * @param {string} projectRoot
126
+ * @param {string} builderDir
114
127
  * @returns {Array<{ doc: object, folderName: string }>}
115
128
  */
116
- function listBuilderApplicationDocs(projectRoot) {
117
- const builderDir = path.join(projectRoot, 'builder');
118
- if (!fsRealSync.existsSync(builderDir) || !fsRealSync.statSync(builderDir).isDirectory()) {
129
+ function listBuilderApplicationDocsInDir(builderDir) {
130
+ if (!isExistingDirSync(builderDir)) {
119
131
  return [];
120
132
  }
121
133
  const out = [];
@@ -140,6 +152,33 @@ function listBuilderApplicationDocs(projectRoot) {
140
152
  return out;
141
153
  }
142
154
 
155
+ /**
156
+ * Prefer projectRoot/builder when present (unit tests, in-repo runs). When missing but projectRoot
157
+ * is the detected CLI package root (global npm install omits builder/), fall back to
158
+ * {@link pathsUtil.getBuilderRoot}.
159
+ *
160
+ * @param {string} projectRoot
161
+ * @returns {string[]}
162
+ */
163
+ function collectBuilderScanDirs(projectRoot) {
164
+ const legacy = path.join(projectRoot, 'builder');
165
+ if (isExistingDirSync(legacy)) {
166
+ return [legacy];
167
+ }
168
+ try {
169
+ const detected = pathsUtil.getProjectRoot();
170
+ if (detected && path.resolve(projectRoot) === path.resolve(detected)) {
171
+ const br = pathsUtil.getBuilderRoot();
172
+ if (isExistingDirSync(br)) {
173
+ return [br];
174
+ }
175
+ }
176
+ } catch {
177
+ /* ignore */
178
+ }
179
+ return [];
180
+ }
181
+
143
182
  /**
144
183
  * @param {string|null|undefined} projectRoot
145
184
  * @returns {{ docker: Record<string, unknown>, local: Record<string, unknown> }}
@@ -150,8 +189,10 @@ function buildAppServiceEnvOverlay(projectRoot) {
150
189
  if (!projectRoot || !fsRealSync.existsSync(projectRoot)) {
151
190
  return { docker: overlayDocker, local: overlayLocal };
152
191
  }
153
- for (const { doc, folderName } of listBuilderApplicationDocs(projectRoot)) {
154
- mergeDocIntoOverlay(overlayDocker, overlayLocal, doc, folderName);
192
+ for (const builderDir of collectBuilderScanDirs(projectRoot)) {
193
+ for (const { doc, folderName } of listBuilderApplicationDocsInDir(builderDir)) {
194
+ mergeDocIntoOverlay(overlayDocker, overlayLocal, doc, folderName);
195
+ }
155
196
  }
156
197
  return { docker: overlayDocker, local: overlayLocal };
157
198
  }
@@ -216,6 +216,10 @@ function createEnvConfigPathFunctions(getConfigFn, saveConfigFn) {
216
216
  if (fromProject) {
217
217
  return fromProject;
218
218
  }
219
+ const fromEffective = tryDir(pathsMod.getBuilderRoot());
220
+ if (fromEffective) {
221
+ return fromEffective;
222
+ }
219
223
  const work = pathsMod.getAifabrixWork();
220
224
  return tryDir(work);
221
225
  }
@@ -10,7 +10,7 @@
10
10
  const path = require('path');
11
11
  const fs = require('fs');
12
12
  const { getIntegrationPath } = require('./paths');
13
- const { parseEnvToMap, resolveKvValue } = require('./credential-secrets-env');
13
+ const { parseEnvToMap } = require('./credential-secrets-env');
14
14
  const { loadSecrets, resolveKvReferences } = require('../core/secrets');
15
15
  const { loadEnvTemplate } = require('./secrets-helpers');
16
16
  const { getActualSecretsPath } = require('./secrets-path');
@@ -81,13 +81,13 @@ function substituteVarPlaceholders(value, envMap, systemKey) {
81
81
 
82
82
  /**
83
83
  * Resolves configuration array values in place by location: variable → {{VAR}} from envMap;
84
- * keyvault → kv:// from secrets. Does not log or expose secret values.
84
+ * keyvault → leaves kv:// as-is (secret value pushed separately by CLI). Does not log or expose secret values.
85
85
  *
86
86
  * @param {Array<{ name?: string, value?: string, location?: string }>} configArray - Configuration array (mutated)
87
87
  * @param {Object.<string, string>} envMap - Resolved env map from buildResolvedEnvMapForIntegration
88
- * @param {Object} secrets - Loaded secrets for kv:// resolution
88
+ * @param {Object} secrets - Loaded secrets (unused for keyvault config values; kept for backward compatibility)
89
89
  * @param {string} [systemKey] - System key for error messages
90
- * @throws {Error} If variable env is missing or keyvault secret unresolved (message never contains secret values)
90
+ * @throws {Error} If variable env is missing or keyvault value is not a kv:// reference (message never contains secret values)
91
91
  */
92
92
  function resolveConfigurationValues(configArray, envMap, secrets, systemKey) {
93
93
  if (!Array.isArray(configArray)) return;
@@ -101,11 +101,14 @@ function resolveConfigurationValues(configArray, envMap, secrets, systemKey) {
101
101
  }
102
102
  item.value = substituteVarPlaceholders(item.value, envMap, systemKey);
103
103
  } else if (location === 'keyvault') {
104
- const resolved = resolveKvValue(secrets, item.value);
105
- if (resolved === null || resolved === undefined) {
106
- throw new Error(`Unresolved keyvault reference for configuration '${item.name || 'unknown'}'.${hint}`);
104
+ if (!item.value.trim().startsWith('kv://')) {
105
+ throw new Error(
106
+ `Configuration entry '${item.name || 'unknown'}' has location 'keyvault' but value is not kv://. ` +
107
+ `Set value to a kv:// reference and push the secret with KV_* env vars.${hint}`
108
+ );
107
109
  }
108
- item.value = resolved;
110
+ // Intentionally do not resolve kv:// here. Upload keeps kv:// references in config and
111
+ // pushes secret values separately via the credential secret API.
109
112
  }
110
113
  }
111
114
  }
@@ -132,7 +132,7 @@ function kvEnvKeyToPath(envKey, systemKey) {
132
132
  * @param {Object.<string, string>} envMap - Key-value map from .env
133
133
  * @returns {Array<{ key: string, value: string }>} Items (key = kv://..., value = raw)
134
134
  */
135
- function collectKvEnvVarsAsSecretItems(envMap) {
135
+ function collectKvEnvVarsAsSecretItems(envMap, systemKey) {
136
136
  if (!envMap || typeof envMap !== 'object') {
137
137
  return [];
138
138
  }
@@ -144,7 +144,7 @@ function collectKvEnvVarsAsSecretItems(envMap) {
144
144
  if (value.startsWith('kv://') && isValidKvPath(value)) {
145
145
  kvPath = value;
146
146
  }
147
- if (!kvPath) kvPath = kvEnvKeyToPath(envKey);
147
+ if (!kvPath) kvPath = kvEnvKeyToPath(envKey, systemKey) || kvEnvKeyToPath(envKey);
148
148
  if (!kvPath) continue;
149
149
  items.push({ key: kvPath, value });
150
150
  }
@@ -223,12 +223,12 @@ function isValidKvPath(key) {
223
223
  * @param {Object} secrets - Loaded secrets
224
224
  * @param {Map<string, string>} itemsByKey - Mutable map to add items to
225
225
  */
226
- function buildItemsFromEnv(envFilePath, secrets, itemsByKey) {
226
+ function buildItemsFromEnv(envFilePath, secrets, itemsByKey, systemKey) {
227
227
  if (!envFilePath || typeof envFilePath !== 'string' || !fs.existsSync(envFilePath)) return;
228
228
  try {
229
229
  const content = fs.readFileSync(envFilePath, 'utf8');
230
230
  const envMap = parseEnvToMap(content);
231
- const fromEnv = collectKvEnvVarsAsSecretItems(envMap);
231
+ const fromEnv = collectKvEnvVarsAsSecretItems(envMap, systemKey);
232
232
  for (const { key, value } of fromEnv) {
233
233
  const resolved = resolveKvValue(secrets, value);
234
234
  // Skip placeholder: value that equals the kv path (e.g. from env.template) must not be pushed as the secret
@@ -320,7 +320,7 @@ async function pushCredentialSecrets(dataplaneUrl, authConfig, options = {}) {
320
320
  secrets = {};
321
321
  }
322
322
  const itemsByKey = new Map();
323
- buildItemsFromEnv(envFilePath, secrets, itemsByKey);
323
+ buildItemsFromEnv(envFilePath, secrets, itemsByKey, appName);
324
324
  buildItemsFromPayload(payload, secrets, itemsByKey);
325
325
 
326
326
  const items = Array.from(itemsByKey.entries())
@@ -0,0 +1,82 @@
1
+ /**
2
+ * @fileoverview Certificate / certification tier lines for DatasourceTestRun TTY output.
3
+ * @author AI Fabrix Team
4
+ * @version 2.0.0
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const chalk = require('chalk');
10
+ const { sectionTitle } = require('./cli-test-layout-chalk');
11
+
12
+ function trimCertificateField(v) {
13
+ if (v === undefined || v === null) return '';
14
+ return String(v).trim();
15
+ }
16
+
17
+ function certificateEnvelopeGlyph(statusRaw) {
18
+ if (statusRaw === 'passed') return '✔';
19
+ if (statusRaw === 'not_passed') return '✖';
20
+ return statusRaw ? '⚠' : ' ';
21
+ }
22
+
23
+ function certificateTTYHasContent(levelRaw, stRaw, summaryRaw, blockerCount) {
24
+ return Boolean(levelRaw || stRaw || summaryRaw || blockerCount > 0);
25
+ }
26
+
27
+ /**
28
+ * @param {string[]} lines
29
+ * @param {string} stRaw
30
+ * @param {string} levelRaw
31
+ * @param {string} summaryRaw
32
+ */
33
+ function appendCertificateStatusAndSummaryLines(lines, stRaw, levelRaw, summaryRaw) {
34
+ if (stRaw || levelRaw) {
35
+ const cg = certificateEnvelopeGlyph(stRaw);
36
+ const tier = levelRaw ? ` — tier ${levelRaw}` : '';
37
+ lines.push(chalk.white(` ${cg} ${stRaw || 'unknown'}${tier}`));
38
+ }
39
+ if (summaryRaw) {
40
+ lines.push(chalk.gray(` ${summaryRaw}`));
41
+ }
42
+ }
43
+
44
+ /**
45
+ * @param {string[]} lines
46
+ * @param {Object[]} blockers
47
+ * @param {number} maxVisible
48
+ */
49
+ function appendCertificateBlockerLines(lines, blockers, maxVisible) {
50
+ const cap = Math.min(maxVisible, blockers.length);
51
+ for (let i = 0; i < cap; i += 1) {
52
+ const b = blockers[i];
53
+ const msg = b && b.message ? String(b.message) : '';
54
+ if (msg) lines.push(chalk.yellow(` • ${msg}`));
55
+ }
56
+ if (blockers.length > cap) {
57
+ lines.push(chalk.gray(` … and ${blockers.length - cap} more`));
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Certification / certificate tier (integration engine or E2E envelope after active cert attach).
63
+ * @param {string[]} lines
64
+ * @param {Object} envelope
65
+ */
66
+ function appendCertificateTTY(lines, envelope) {
67
+ const cert = envelope && envelope.certificate;
68
+ if (!cert || typeof cert !== 'object') return;
69
+ const levelRaw = trimCertificateField(cert.level);
70
+ const stRaw = trimCertificateField(cert.status);
71
+ const summaryRaw = trimCertificateField(cert.summary);
72
+ const blockers = Array.isArray(cert.blockers) ? cert.blockers : [];
73
+ if (!certificateTTYHasContent(levelRaw, stRaw, summaryRaw, blockers.length)) return;
74
+ lines.push('');
75
+ lines.push(sectionTitle('Certification:'));
76
+ appendCertificateStatusAndSummaryLines(lines, stRaw, levelRaw, summaryRaw);
77
+ appendCertificateBlockerLines(lines, blockers, 5);
78
+ }
79
+
80
+ module.exports = {
81
+ appendCertificateTTY
82
+ };
@@ -6,6 +6,7 @@
6
6
 
7
7
  const chalk = require('chalk');
8
8
  const { sectionTitle, headerKeyValue, colorAggregateGlyph, successGlyph, failureGlyph } = require('./cli-test-layout-chalk');
9
+ const { appendCertificateTTY } = require('./datasource-test-run-certificate-tty');
9
10
 
10
11
  const SEP = '────────────────────────────────';
11
12
 
@@ -235,6 +236,16 @@ function appendDebugPayloadRefLines(lines, dbg, maxRefChars) {
235
236
  */
236
237
  function appendDebugMetaLines(lines, dbg, maxRefChars) {
237
238
  let added = false;
239
+ function formatSecondsText(s) {
240
+ const txt = String(s);
241
+ // UI-only: reduce noisy float seconds to 3 decimals, e.g. "1.1713749s" -> "1.171s".
242
+ // Applies only to number tokens immediately followed by "s".
243
+ return txt.replace(/(\d+\.\d+)(?=s\b)/g, m => {
244
+ const n = Number(m);
245
+ if (!Number.isFinite(n)) return m;
246
+ return n.toFixed(3);
247
+ });
248
+ }
238
249
  if (dbg.mode) {
239
250
  lines.push(
240
251
  `${chalk.blue.bold('debug.mode:')} ${chalk.white(String(dbg.mode))}`
@@ -244,7 +255,7 @@ function appendDebugMetaLines(lines, dbg, maxRefChars) {
244
255
  if (dbg.executionSummary) {
245
256
  lines.push(
246
257
  `${chalk.blue.bold('debug.executionSummary:')} ${chalk.white(
247
- truncateRefLine(String(dbg.executionSummary), maxRefChars)
258
+ truncateRefLine(formatSecondsText(dbg.executionSummary), maxRefChars)
248
259
  )}`
249
260
  );
250
261
  added = true;
@@ -326,9 +337,14 @@ function appendIntegrationStepLines(lines, envelope) {
326
337
 
327
338
  function pickExecutiveVerdictLine(envelope) {
328
339
  const dev = envelope.developer;
340
+ const cert = envelope.certificate;
341
+ if (envelope.runType === 'e2e' && cert && typeof cert === 'object') {
342
+ const cs = cert.summary;
343
+ if (cs && String(cs).trim()) return String(cs).trim();
344
+ }
329
345
  return (
330
346
  (dev && dev.executiveSummary) ||
331
- (envelope.certificate && envelope.certificate.summary) ||
347
+ (cert && cert.summary) ||
332
348
  (envelope.validation && envelope.validation.summary) ||
333
349
  (envelope.integration && envelope.integration.summary) ||
334
350
  `Run finished with status ${envelope.status}.`
@@ -416,6 +432,7 @@ function formatDatasourceTestRunTTY(envelope, options = {}) {
416
432
  lines.push('');
417
433
  lines.push(sectionTitle('Verdict:'));
418
434
  lines.push(chalk.white(pickExecutiveVerdictLine(envelope)));
435
+ appendCertificateTTY(lines, envelope);
419
436
  lines.push('');
420
437
  lines.push(chalk.gray(SEP));
421
438
  if (appendReferenceLayoutLines(lines, envelope, { maxRefChars: 160 })) {
@@ -52,7 +52,32 @@ function exitCodeForPollTimeout(lastBody) {
52
52
  return 3;
53
53
  }
54
54
 
55
+ const {
56
+ deriveSystemStatus,
57
+ systemCertStatus
58
+ } = require('./external-system-system-test-tty');
59
+
60
+ /**
61
+ * System-level exit code from per-datasource result rows (same matrix as §3.1 on synthetic rollup).
62
+ * @param {Array<{ skipped?: boolean, success?: boolean, datasourceTestRun?: Object|null }>} rows
63
+ * @param {Object} [opts]
64
+ * @param {boolean} [opts.warningsAsErrors]
65
+ * @param {boolean} [opts.requireCert]
66
+ * @returns {number}
67
+ */
68
+ function computeSystemExitCodeFromDatasourceRows(rows, opts = {}) {
69
+ const list = Array.isArray(rows) ? rows : [];
70
+ const status = deriveSystemStatus(list);
71
+ const certAgg = systemCertStatus(list);
72
+ const body = {
73
+ status,
74
+ certificate: certAgg ? { status: certAgg === 'passed' ? 'passed' : 'not_passed' } : undefined
75
+ };
76
+ return computeExitCodeFromDatasourceTestRun(body, opts);
77
+ }
78
+
55
79
  module.exports = {
56
80
  computeExitCodeFromDatasourceTestRun,
81
+ computeSystemExitCodeFromDatasourceRows,
57
82
  exitCodeForPollTimeout
58
83
  };
@@ -24,6 +24,7 @@ const {
24
24
  successGlyph,
25
25
  failureGlyph
26
26
  } = require('./cli-test-layout-chalk');
27
+ const { displaySystemAggregateDatasourceTestRuns } = require('./external-system-system-test-tty');
27
28
 
28
29
  /**
29
30
  * Displays formatted test results (local external `aifabrix test` — structured report layout).
@@ -269,6 +270,13 @@ function displayServerDatasourceTestRunResults(results, verbose, opts = {}) {
269
270
  return;
270
271
  }
271
272
 
273
+ // Plan §17: system-level overview for multi-datasource results (no full §16 dump per datasource by default).
274
+ // Keep legacy per-datasource full envelope available only via datasource commands.
275
+ if (integrationResultsHaveEnvelope(results)) {
276
+ displaySystemAggregateDatasourceTestRuns(results, { runType, verbose: Boolean(verbose) });
277
+ return;
278
+ }
279
+
272
280
  const agg = deriveAggregateServerStatus(results);
273
281
  logServerDatasourceTestRunHeader(results, runType, agg);
274
282
 
@@ -0,0 +1,120 @@
1
+ /**
2
+ * @fileoverview Plan §17 blocks: capabilities overview and integration health (split for file-size limits).
3
+ */
4
+
5
+ 'use strict';
6
+
7
+ /**
8
+ * @param {Object|null|undefined} env
9
+ * @returns {'ok'|'warn'|'fail'}
10
+ */
11
+ function integrationRollupStatus(env) {
12
+ const integ = env && env.integration;
13
+ if (!integ || typeof integ !== 'object') return 'ok';
14
+ if (typeof integ.status === 'string') {
15
+ const s = integ.status.toLowerCase();
16
+ if (s === 'fail' || s === 'failed' || s === 'error') return 'fail';
17
+ if (s === 'warn' || s === 'warning') return 'warn';
18
+ if (s === 'ok' || s === 'passed' || s === 'success') return 'ok';
19
+ }
20
+ const steps = Array.isArray(integ.stepResults) ? integ.stepResults : [];
21
+ if (steps.some(s => s && (s.success === false || s.error))) return 'fail';
22
+ return 'ok';
23
+ }
24
+
25
+ function buildCapabilityBlocks(rows, statusGlyph) {
26
+ const blocks = [];
27
+ for (const r of rows) {
28
+ if (r && r.skipped) continue;
29
+ const env = r && r.datasourceTestRun;
30
+ if (!env || !Array.isArray(env.capabilities) || env.capabilities.length === 0) continue;
31
+ const dkey = env.datasourceKey || r.key || 'datasource';
32
+ const caps = [...env.capabilities]
33
+ .filter(c => c && c.key)
34
+ .sort((a, b) => String(a.key).localeCompare(String(b.key)))
35
+ .slice(0, 4);
36
+ const parts = caps.map(c => `${statusGlyph(c.status)} ${c.key}`);
37
+ if (parts.length === 0) continue;
38
+ blocks.push({ dkey: String(dkey), line: parts.join(' ') });
39
+ }
40
+ return blocks;
41
+ }
42
+
43
+ function emitSectionHeader(io, title) {
44
+ const { log, metaGray, sectionTitle, SEP } = io;
45
+ log('');
46
+ log(metaGray(SEP));
47
+ log('');
48
+ log(sectionTitle(title));
49
+ log('');
50
+ }
51
+
52
+ /**
53
+ * @param {Array} rows
54
+ * @param {{ log: Function, chalk: object, metaGray: Function, sectionTitle: Function, statusGlyph: Function, SEP: string }} io
55
+ * @returns {boolean}
56
+ */
57
+ function logCapabilitiesOverview(rows, io) {
58
+ const blocks = buildCapabilityBlocks(rows, io.statusGlyph);
59
+ if (blocks.length === 0) return false;
60
+ emitSectionHeader(io, 'Capabilities overview:');
61
+ for (const b of blocks) {
62
+ io.log(io.chalk.white(`${b.dkey}:`));
63
+ io.log(io.chalk.white(` ${b.line}`));
64
+ }
65
+ return true;
66
+ }
67
+
68
+ function badIntegrationSteps(integ) {
69
+ if (!integ || !Array.isArray(integ.stepResults)) return [];
70
+ return integ.stepResults.filter(s => s && (s.success === false || s.error)).slice(0, 2);
71
+ }
72
+
73
+ function buildIntegrationBlocks(rows) {
74
+ const blocks = [];
75
+ for (const r of rows) {
76
+ if (r && r.skipped) continue;
77
+ const env = r && r.datasourceTestRun;
78
+ if (!env) continue;
79
+ const dkey = env.datasourceKey || r.key || 'datasource';
80
+ const st = integrationRollupStatus(env);
81
+ const integ = env.integration && typeof env.integration === 'object' ? env.integration : null;
82
+ blocks.push({ dkey: String(dkey), st, badSteps: badIntegrationSteps(integ) });
83
+ }
84
+ return blocks;
85
+ }
86
+
87
+ function emitFailedSteps(block, io) {
88
+ if (block.st !== 'fail' && block.st !== 'warn') return;
89
+ const { log, chalk, statusGlyph } = io;
90
+ for (const stp of block.badSteps) {
91
+ const nm = stp.name || stp.step || 'step';
92
+ const hint = stp.error || stp.message || '';
93
+ log(chalk.white(` ${statusGlyph('fail')} ${nm}${hint ? `: ${hint}` : ''}`));
94
+ }
95
+ }
96
+
97
+ /**
98
+ * @param {Array} rows
99
+ * @param {'integration'|'e2e'} runType
100
+ * @param {{ log: Function, chalk: object, metaGray: Function, sectionTitle: Function, statusGlyph: Function, SEP: string }} io
101
+ * @returns {boolean}
102
+ */
103
+ function logIntegrationHealthSection(rows, runType, io) {
104
+ if (runType !== 'integration') return false;
105
+ const blocks = buildIntegrationBlocks(rows);
106
+ if (blocks.length === 0) return false;
107
+ emitSectionHeader(io, 'Integration health:');
108
+ const { log, chalk, statusGlyph } = io;
109
+ for (const b of blocks) {
110
+ log(chalk.white(`${b.dkey}: ${statusGlyph(b.st)} ${b.st}`));
111
+ emitFailedSteps(b, io);
112
+ }
113
+ return true;
114
+ }
115
+
116
+ module.exports = {
117
+ integrationRollupStatus,
118
+ logCapabilitiesOverview,
119
+ logIntegrationHealthSection
120
+ };