@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.
- package/.cursor/rules/cli-layout.mdc +75 -0
- package/.cursor/rules/project-rules.mdc +8 -0
- package/.npmrc.token +1 -0
- package/.nyc_output/55e9d034-ddab-4579-a706-e02a91d75c91.json +1 -0
- package/.nyc_output/processinfo/55e9d034-ddab-4579-a706-e02a91d75c91.json +1 -0
- package/.nyc_output/processinfo/index.json +1 -0
- package/jest.projects.js +15 -2
- package/lib/api/certificates.api.js +62 -0
- package/lib/api/index.js +11 -2
- package/lib/api/types/certificates.types.js +48 -0
- package/lib/api/validation-run.api.js +16 -4
- package/lib/api/validation-runner.js +13 -3
- package/lib/app/certification-show-enrich.js +129 -0
- package/lib/app/certification-verify-rows.js +60 -0
- package/lib/app/show-display.js +43 -0
- package/lib/app/show.js +92 -8
- package/lib/certification/cli-cert-sync-skip.js +21 -0
- package/lib/certification/merge-certification-from-artifact.js +185 -0
- package/lib/certification/post-unified-cert-sync.js +33 -0
- package/lib/certification/sync-after-external-command.js +52 -0
- package/lib/certification/sync-system-certification.js +197 -0
- package/lib/cli/setup-app.js +4 -0
- package/lib/cli/setup-app.test-commands.js +24 -8
- package/lib/cli/setup-external-system.js +22 -1
- package/lib/cli/setup-secrets.js +34 -13
- package/lib/cli/setup-utility.js +18 -2
- package/lib/commands/app.js +10 -1
- package/lib/commands/datasource-unified-test-cli.js +50 -117
- package/lib/commands/datasource-unified-test-cli.options.js +44 -2
- package/lib/commands/datasource-unified-test-e2e-cli-helpers.js +106 -0
- package/lib/commands/datasource-validation-cli.js +15 -1
- package/lib/commands/datasource.js +25 -2
- package/lib/commands/upload.js +17 -6
- package/lib/core/secrets.js +47 -13
- package/lib/datasource/log-viewer.js +135 -20
- package/lib/datasource/test-e2e.js +35 -17
- package/lib/datasource/unified-validation-run-body.js +3 -0
- package/lib/datasource/unified-validation-run.js +2 -1
- package/lib/external-system/deploy.js +53 -18
- package/lib/infrastructure/compose.js +12 -3
- package/lib/infrastructure/helpers-docker-check.js +67 -0
- package/lib/infrastructure/helpers.js +47 -58
- package/lib/infrastructure/index.js +3 -1
- package/lib/infrastructure/services.js +4 -56
- package/lib/schema/external-system.schema.json +25 -3
- package/lib/schema/type/document-storage.json +15 -2
- package/lib/utils/api.js +28 -3
- package/lib/utils/app-service-env-from-builder.js +47 -6
- package/lib/utils/config-paths.js +4 -0
- package/lib/utils/configuration-env-resolver.js +11 -8
- package/lib/utils/credential-secrets-env.js +5 -5
- package/lib/utils/datasource-test-run-certificate-tty.js +82 -0
- package/lib/utils/datasource-test-run-display.js +19 -2
- package/lib/utils/datasource-test-run-exit.js +25 -0
- package/lib/utils/external-system-display.js +8 -0
- package/lib/utils/external-system-system-test-tty-overview.js +120 -0
- package/lib/utils/external-system-system-test-tty.js +417 -0
- package/lib/utils/paths.js +14 -0
- package/lib/utils/url-declarative-resolve-load-doc.js +50 -20
- package/lib/utils/urls-local-registry.js +36 -12
- package/lib/utils/validation-run-poll.js +28 -5
- package/lib/utils/validation-run-post-retry.js +20 -8
- package/lib/utils/validation-run-request.js +18 -0
- package/lib/validation/validate-external-cert-sync.js +23 -0
- package/lib/validation/validate.js +4 -1
- package/package.json +4 -3
- package/scripts/install-local.js +4 -1
- package/scripts/pnpm-global-remove.js +48 -0
- package/templates/applications/dataplane/env.template +4 -0
- package/templates/infra/compose.yaml.hbs +15 -14
- package/templates/infra/servers.json.hbs +3 -1
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview System-level TTY renderer for DatasourceTestRun fan-out results (plan §17).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
const logger = require('./logger');
|
|
9
|
+
const {
|
|
10
|
+
SEP,
|
|
11
|
+
statusGlyph
|
|
12
|
+
} = require('./datasource-test-run-display');
|
|
13
|
+
const {
|
|
14
|
+
sectionTitle,
|
|
15
|
+
headerKeyValue,
|
|
16
|
+
formatStatusKeyValue,
|
|
17
|
+
integrationFooterLine,
|
|
18
|
+
colorRollupPrefixedLine,
|
|
19
|
+
metadata: metaGray
|
|
20
|
+
} = require('./cli-test-layout-chalk');
|
|
21
|
+
const {
|
|
22
|
+
formatDataQualityLines,
|
|
23
|
+
readinessLineFromDataReadiness,
|
|
24
|
+
verdictLineFromEnvelope
|
|
25
|
+
} = require('./validation-report-tty-kit');
|
|
26
|
+
const {
|
|
27
|
+
logCapabilitiesOverview: logCapabilitiesOverviewSection,
|
|
28
|
+
logIntegrationHealthSection: logIntegrationHealthSectionBlock
|
|
29
|
+
} = require('./external-system-system-test-tty-overview');
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {'ok'|'warn'|'fail'|'skipped'|null} st
|
|
33
|
+
* @returns {number}
|
|
34
|
+
*/
|
|
35
|
+
function statusRank(st) {
|
|
36
|
+
if (st === 'fail') return 0;
|
|
37
|
+
if (st === 'warn') return 1;
|
|
38
|
+
if (st === 'ok') return 2;
|
|
39
|
+
if (st === 'skipped') return 3;
|
|
40
|
+
return 4;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Per-row status for system rollup and tables: CLI/transport failure overrides envelope-only OK.
|
|
45
|
+
* @param {{ skipped?: boolean, success?: boolean, datasourceTestRun?: { status?: string }|null }} r
|
|
46
|
+
* @returns {'ok'|'warn'|'fail'|'skipped'}
|
|
47
|
+
*/
|
|
48
|
+
function rollupRowStatus(r) {
|
|
49
|
+
if (r && r.skipped) return 'skipped';
|
|
50
|
+
if (r && r.success === false) return 'fail';
|
|
51
|
+
const env = r && r.datasourceTestRun;
|
|
52
|
+
return env && typeof env.status === 'string' ? env.status : 'ok';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {Array<{ key: string, skipped?: boolean, datasourceTestRun?: Object|null, success?: boolean }>} rows
|
|
57
|
+
* @returns {'ok'|'warn'|'fail'|'skipped'}
|
|
58
|
+
*/
|
|
59
|
+
function deriveSystemStatus(rows) {
|
|
60
|
+
if (!Array.isArray(rows) || rows.length === 0) return 'ok';
|
|
61
|
+
const statuses = rows.map(rollupRowStatus);
|
|
62
|
+
if (statuses.some(s => s === 'fail')) return 'fail';
|
|
63
|
+
if (statuses.some(s => s === 'warn')) return 'warn';
|
|
64
|
+
if (statuses.every(s => s === 'skipped')) return 'skipped';
|
|
65
|
+
// Mixed ok/skipped => warn per plan.
|
|
66
|
+
return statuses.every(s => s === 'ok') ? 'ok' : 'warn';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function bucketIssueSeverity(issue) {
|
|
70
|
+
const sev = issue && issue.severity ? String(issue.severity).toLowerCase() : '';
|
|
71
|
+
if (sev === 'error' || sev === 'critical' || sev === 'high' || sev === 'fatal') return 'fail';
|
|
72
|
+
if (sev === 'warn' || sev === 'warning' || sev === 'medium') return 'warn';
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Minimal heuristic rollups backed by envelope fields (no engine codenames).
|
|
78
|
+
* @param {Object|null|undefined} env
|
|
79
|
+
* @returns {'ok'|'warn'|'fail'}
|
|
80
|
+
*/
|
|
81
|
+
function dqFromEnvelope(env) {
|
|
82
|
+
const v = env && env.validation;
|
|
83
|
+
const st = v && typeof v.status === 'string' ? String(v.status) : null;
|
|
84
|
+
if (st === 'fail') return 'fail';
|
|
85
|
+
if (st === 'warn') return 'warn';
|
|
86
|
+
|
|
87
|
+
const issues = v && Array.isArray(v.issues) ? v.issues : [];
|
|
88
|
+
if (issues.some(i => bucketIssueSeverity(i) === 'fail')) return 'fail';
|
|
89
|
+
if (issues.some(i => bucketIssueSeverity(i) === 'warn')) return 'warn';
|
|
90
|
+
return 'ok';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {Array} rows
|
|
95
|
+
* @returns {{ schema: 'ok'|'warn'|'fail', consistency: 'ok'|'warn'|'fail', reliability: 'ok'|'warn'|'fail' }}
|
|
96
|
+
*/
|
|
97
|
+
function deriveSystemDataQuality(rows) {
|
|
98
|
+
const envs = rows
|
|
99
|
+
.map(r => (r && r.datasourceTestRun && typeof r.datasourceTestRun === 'object' ? r.datasourceTestRun : null))
|
|
100
|
+
.filter(Boolean);
|
|
101
|
+
if (envs.length === 0) {
|
|
102
|
+
return { schema: 'warn', consistency: 'warn', reliability: 'warn' };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const picks = envs.map(dqFromEnvelope);
|
|
106
|
+
const agg = picks.some(x => x === 'fail') ? 'fail' : picks.some(x => x === 'warn') ? 'warn' : 'ok';
|
|
107
|
+
return { schema: agg, consistency: agg, reliability: agg };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {Array} rows
|
|
112
|
+
* @returns {'ready'|'partial'|'not_ready'|null}
|
|
113
|
+
*/
|
|
114
|
+
function deriveSystemReadiness(rows) {
|
|
115
|
+
const envs = rows
|
|
116
|
+
.map(r => (r && r.datasourceTestRun && typeof r.datasourceTestRun === 'object' ? r.datasourceTestRun : null))
|
|
117
|
+
.filter(Boolean);
|
|
118
|
+
const drs = envs
|
|
119
|
+
.map(e => e.validation && e.validation.dataReadiness)
|
|
120
|
+
.filter(Boolean);
|
|
121
|
+
if (drs.length === 0) return null;
|
|
122
|
+
if (drs.some(x => x === 'not_ready')) return 'not_ready';
|
|
123
|
+
if (drs.some(x => x === 'partial')) return 'partial';
|
|
124
|
+
return 'ready';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function countByStatus(rows) {
|
|
128
|
+
const counts = { ok: 0, warn: 0, fail: 0, skipped: 0 };
|
|
129
|
+
for (const r of rows) {
|
|
130
|
+
const s = rollupRowStatus(r);
|
|
131
|
+
if (counts[s] !== undefined) counts[s] += 1;
|
|
132
|
+
}
|
|
133
|
+
return counts;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function pickBlockingDatasourceKey(rows) {
|
|
137
|
+
const keys = rows
|
|
138
|
+
.map(r => {
|
|
139
|
+
const env = r && r.datasourceTestRun;
|
|
140
|
+
const st = rollupRowStatus(r);
|
|
141
|
+
const key =
|
|
142
|
+
env && env.datasourceKey
|
|
143
|
+
? String(env.datasourceKey)
|
|
144
|
+
: r && r.key
|
|
145
|
+
? String(r.key)
|
|
146
|
+
: '';
|
|
147
|
+
return { key, st };
|
|
148
|
+
})
|
|
149
|
+
.filter(x => x.key);
|
|
150
|
+
keys.sort((a, b) => {
|
|
151
|
+
const d = statusRank(a.st) - statusRank(b.st);
|
|
152
|
+
if (d !== 0) return d;
|
|
153
|
+
return a.key.localeCompare(b.key);
|
|
154
|
+
});
|
|
155
|
+
return keys.length ? keys[0].key : null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function issueKey(issue) {
|
|
159
|
+
const code = issue && issue.code ? String(issue.code) : '';
|
|
160
|
+
const msg = issue && issue.message ? String(issue.message) : '';
|
|
161
|
+
return `${code}::${msg}`.toLowerCase();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function issueSortKey(it) {
|
|
165
|
+
return `${statusRank(it.severity)}::${it.datasourceKey}::${it.message}`.toLowerCase();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function extractRowIssues(row) {
|
|
169
|
+
const env = row && row.datasourceTestRun;
|
|
170
|
+
const datasourceKey = (env && env.datasourceKey) || (row && row.key) || 'datasource';
|
|
171
|
+
const issues = env && env.validation && Array.isArray(env.validation.issues) ? env.validation.issues : [];
|
|
172
|
+
const envStatus = env && typeof env.status === 'string' ? env.status : null;
|
|
173
|
+
return { datasourceKey: String(datasourceKey), issues, envStatus };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function toIssueItem(datasourceKey, envStatus, iss) {
|
|
177
|
+
const sev =
|
|
178
|
+
bucketIssueSeverity(iss) ||
|
|
179
|
+
(envStatus === 'fail' ? 'fail' : envStatus === 'warn' ? 'warn' : 'ok');
|
|
180
|
+
const msg = iss && iss.message ? String(iss.message) : iss && iss.code ? String(iss.code) : 'Issue';
|
|
181
|
+
return { datasourceKey, message: msg, severity: sev };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function collectKeyIssues(rows, cap) {
|
|
185
|
+
/** @type {{ datasourceKey: string, message: string, severity: 'fail'|'warn'|'ok' }[]} */
|
|
186
|
+
const out = [];
|
|
187
|
+
const seen = new Set();
|
|
188
|
+
for (const row of rows) {
|
|
189
|
+
const { datasourceKey, issues, envStatus } = extractRowIssues(row);
|
|
190
|
+
for (const iss of issues) {
|
|
191
|
+
const k = `${datasourceKey}::${issueKey(iss)}`;
|
|
192
|
+
if (seen.has(k)) continue;
|
|
193
|
+
seen.add(k);
|
|
194
|
+
out.push(toIssueItem(datasourceKey, envStatus, iss));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
out.sort((a, b) => issueSortKey(a).localeCompare(issueSortKey(b)));
|
|
198
|
+
return out.slice(0, Math.max(0, cap));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function certificateBucket(env) {
|
|
202
|
+
const cert = env && env.certificate;
|
|
203
|
+
if (!cert || typeof cert !== 'object') return { status: null, level: null };
|
|
204
|
+
return {
|
|
205
|
+
status: cert.status ? String(cert.status) : null,
|
|
206
|
+
level: cert.level ? String(cert.level) : null
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function systemCertStatus(rows) {
|
|
211
|
+
const certs = rows
|
|
212
|
+
.map(r => (r && r.datasourceTestRun ? certificateBucket(r.datasourceTestRun) : { status: null, level: null }))
|
|
213
|
+
.filter(c => c.status !== null);
|
|
214
|
+
if (certs.length === 0) return null;
|
|
215
|
+
if (certs.some(c => c.status === 'not_passed')) return 'not_passed';
|
|
216
|
+
return 'passed';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function drillDownCommand(runType, datasourceKey) {
|
|
220
|
+
if (!datasourceKey) return null;
|
|
221
|
+
if (runType === 'e2e') return `aifabrix datasource test-e2e ${datasourceKey}`;
|
|
222
|
+
if (runType === 'integration') return `aifabrix datasource test-integration ${datasourceKey}`;
|
|
223
|
+
return `aifabrix datasource test ${datasourceKey}`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function logSystemHeader(results, runType, systemStatus) {
|
|
227
|
+
logger.log('');
|
|
228
|
+
logger.log(sectionTitle('Server test results'));
|
|
229
|
+
logger.log('');
|
|
230
|
+
logger.log(headerKeyValue('System:', results.systemKey));
|
|
231
|
+
logger.log(
|
|
232
|
+
headerKeyValue(
|
|
233
|
+
'Run:',
|
|
234
|
+
runType === 'e2e' ? 'test-e2e (dataplane)' : 'test-integration (dataplane)'
|
|
235
|
+
)
|
|
236
|
+
);
|
|
237
|
+
logger.log(formatStatusKeyValue(systemStatus, statusGlyph(systemStatus)));
|
|
238
|
+
logger.log('');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function logVerdictAndSummary(runType, systemStatus, certStatus, counts) {
|
|
242
|
+
logger.log(sectionTitle('Verdict:'));
|
|
243
|
+
logger.log(chalk.white(verdictLineFromEnvelope(systemStatus, certStatus, runType)));
|
|
244
|
+
logger.log('');
|
|
245
|
+
logger.log(sectionTitle('Summary:'));
|
|
246
|
+
logger.log(
|
|
247
|
+
chalk.white(
|
|
248
|
+
`${counts.ok + counts.warn + counts.fail} datasource(s): ${counts.ok} ok, ${counts.warn} warn, ${counts.fail} fail${counts.skipped ? `, ${counts.skipped} skipped` : ''}`
|
|
249
|
+
)
|
|
250
|
+
);
|
|
251
|
+
logger.log('');
|
|
252
|
+
logger.log(metaGray(SEP));
|
|
253
|
+
logger.log('');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function logDataQualityAndReadiness(rows) {
|
|
257
|
+
logger.log(sectionTitle('Data Quality:'));
|
|
258
|
+
const dq = deriveSystemDataQuality(rows);
|
|
259
|
+
const dqLines = formatDataQualityLines(dq, {
|
|
260
|
+
schema: 'structural coverage aggregated across datasources.',
|
|
261
|
+
consistency: 'issues aggregated across datasources.',
|
|
262
|
+
reliability: 'issues aggregated across datasources.'
|
|
263
|
+
});
|
|
264
|
+
dqLines.map(colorRollupPrefixedLine).forEach(l => logger.log(l));
|
|
265
|
+
|
|
266
|
+
const readiness = deriveSystemReadiness(rows);
|
|
267
|
+
const readinessLine = readinessLineFromDataReadiness(readiness);
|
|
268
|
+
if (readinessLine) {
|
|
269
|
+
logger.log('');
|
|
270
|
+
logger.log(colorRollupPrefixedLine(readinessLine));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
logger.log('');
|
|
274
|
+
logger.log(metaGray(SEP));
|
|
275
|
+
logger.log('');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function logDatasourceTable(rows, counts, verbose) {
|
|
279
|
+
logger.log(sectionTitle('Datasources:'));
|
|
280
|
+
logger.log('');
|
|
281
|
+
if (!verbose && counts.ok > 0) {
|
|
282
|
+
logger.log(chalk.gray(`✔ ${counts.ok} datasource(s) fully ready`));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function rowStatus(r) {
|
|
286
|
+
return rollupRowStatus(r);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function readinessLabel(env, st) {
|
|
290
|
+
const ready = env && env.validation ? env.validation.dataReadiness : null;
|
|
291
|
+
if (ready === 'not_ready') return 'Not ready';
|
|
292
|
+
if (ready === 'partial') return 'Partial';
|
|
293
|
+
if (ready === 'ready') return 'Ready';
|
|
294
|
+
if (st === 'fail') return 'Not ready';
|
|
295
|
+
if (st === 'warn') return 'Partial';
|
|
296
|
+
return 'Ready';
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function shouldListRow(r) {
|
|
300
|
+
if (verbose) return true;
|
|
301
|
+
const st = rowStatus(r);
|
|
302
|
+
return st === 'warn' || st === 'fail';
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const listRows = rows.filter(shouldListRow);
|
|
306
|
+
for (const r of listRows) {
|
|
307
|
+
const env = r && r.datasourceTestRun;
|
|
308
|
+
const key = (env && env.datasourceKey) || (r && r.key) || 'datasource';
|
|
309
|
+
const st = rowStatus(r);
|
|
310
|
+
const readyLabel = readinessLabel(env, st);
|
|
311
|
+
logger.log(`${statusGlyph(st)} ${chalk.white(String(key).padEnd(22))} (${readyLabel})`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function logBlockingDatasource(blocking) {
|
|
316
|
+
if (!blocking) return;
|
|
317
|
+
logger.log('');
|
|
318
|
+
logger.log(chalk.white(`Blocking datasource: ${blocking}`));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function logKeyIssuesSection(rows) {
|
|
322
|
+
const issues = collectKeyIssues(rows, 5);
|
|
323
|
+
if (issues.length === 0) return false;
|
|
324
|
+
logger.log('');
|
|
325
|
+
logger.log(metaGray(SEP));
|
|
326
|
+
logger.log('');
|
|
327
|
+
logger.log(sectionTitle('Key issues:'));
|
|
328
|
+
logger.log('');
|
|
329
|
+
let cur = null;
|
|
330
|
+
for (const it of issues) {
|
|
331
|
+
if (cur !== it.datasourceKey) {
|
|
332
|
+
cur = it.datasourceKey;
|
|
333
|
+
logger.log(chalk.white(cur));
|
|
334
|
+
}
|
|
335
|
+
logger.log(chalk.white(`- ${it.message}`));
|
|
336
|
+
}
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function logCertificationSection(rows) {
|
|
341
|
+
const certSt = systemCertStatus(rows);
|
|
342
|
+
if (!certSt) return;
|
|
343
|
+
logger.log('');
|
|
344
|
+
logger.log(metaGray(SEP));
|
|
345
|
+
logger.log('');
|
|
346
|
+
logger.log(sectionTitle('Certification:'));
|
|
347
|
+
logger.log('');
|
|
348
|
+
logger.log(chalk.white(`System level: ${certSt === 'passed' ? '✔ Achieved' : '✖ Not achieved'}`));
|
|
349
|
+
logger.log('');
|
|
350
|
+
logger.log(chalk.white('Breakdown:'));
|
|
351
|
+
for (const r of rows) {
|
|
352
|
+
const env = r && r.datasourceTestRun;
|
|
353
|
+
if (!env) continue;
|
|
354
|
+
const cert = certificateBucket(env);
|
|
355
|
+
if (!cert.status) continue;
|
|
356
|
+
const g = cert.status === 'passed' ? '✔' : '✖';
|
|
357
|
+
const tier = cert.level ? cert.level : '(no level)';
|
|
358
|
+
logger.log(chalk.white(`- ${env.datasourceKey}: ${g} ${tier}`));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function logUseAndFooter(results, runType, systemStatus, blocking) {
|
|
363
|
+
logger.log('');
|
|
364
|
+
logger.log(metaGray(SEP));
|
|
365
|
+
logger.log('');
|
|
366
|
+
logger.log(sectionTitle('Use:'));
|
|
367
|
+
const cmd = drillDownCommand(runType, blocking);
|
|
368
|
+
logger.log(chalk.white(cmd || 'aifabrix datasource test <datasourceKey>'));
|
|
369
|
+
logger.log(
|
|
370
|
+
integrationFooterLine(
|
|
371
|
+
results.success,
|
|
372
|
+
systemStatus,
|
|
373
|
+
'All server tests passed.',
|
|
374
|
+
'Server tests completed with warnings.',
|
|
375
|
+
'Some server tests failed.'
|
|
376
|
+
)
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Render system-level aggregate (plan §17) for DatasourceTestRun wrapper results.
|
|
382
|
+
* @param {Object} results
|
|
383
|
+
* @param {Object} opts
|
|
384
|
+
* @param {'integration'|'e2e'} opts.runType
|
|
385
|
+
* @param {boolean} opts.verbose
|
|
386
|
+
*/
|
|
387
|
+
function displaySystemAggregateDatasourceTestRuns(results, opts) {
|
|
388
|
+
const rows = Array.isArray(results.datasourceResults) ? results.datasourceResults : [];
|
|
389
|
+
const runType = opts.runType === 'e2e' ? 'e2e' : 'integration';
|
|
390
|
+
const systemStatus = deriveSystemStatus(rows);
|
|
391
|
+
const counts = countByStatus(rows);
|
|
392
|
+
const blocking = pickBlockingDatasourceKey(rows);
|
|
393
|
+
logSystemHeader(results, runType, systemStatus);
|
|
394
|
+
logVerdictAndSummary(runType, systemStatus, systemCertStatus(rows), counts);
|
|
395
|
+
logDataQualityAndReadiness(rows);
|
|
396
|
+
logDatasourceTable(rows, counts, Boolean(opts.verbose));
|
|
397
|
+
logBlockingDatasource(blocking);
|
|
398
|
+
logKeyIssuesSection(rows);
|
|
399
|
+
const ttyIo = { log: logger.log.bind(logger), chalk, metaGray, sectionTitle, statusGlyph, SEP };
|
|
400
|
+
logCapabilitiesOverviewSection(rows, ttyIo);
|
|
401
|
+
logIntegrationHealthSectionBlock(rows, runType, ttyIo);
|
|
402
|
+
logCertificationSection(rows);
|
|
403
|
+
logUseAndFooter(results, runType, systemStatus, blocking);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
module.exports = {
|
|
407
|
+
displaySystemAggregateDatasourceTestRuns,
|
|
408
|
+
deriveSystemStatus,
|
|
409
|
+
deriveSystemDataQuality,
|
|
410
|
+
deriveSystemReadiness,
|
|
411
|
+
rollupRowStatus,
|
|
412
|
+
pickBlockingDatasourceKey,
|
|
413
|
+
collectKeyIssues,
|
|
414
|
+
systemCertStatus,
|
|
415
|
+
drillDownCommand
|
|
416
|
+
};
|
|
417
|
+
|
package/lib/utils/paths.js
CHANGED
|
@@ -332,9 +332,23 @@ function getAppPath(appName, appType) {
|
|
|
332
332
|
|
|
333
333
|
/**
|
|
334
334
|
* Base directory for integration/builder: project root when cwd is inside project, else cwd.
|
|
335
|
+
* When `aifabrix-work` / `AIFABRIX_WORK` points at a repo that contains `integration/`, use that
|
|
336
|
+
* root even if cwd is elsewhere (e.g. global CLI install + cwd under `integration/<app>/`).
|
|
335
337
|
* @returns {string} Directory to resolve integration/ and builder/ from
|
|
336
338
|
*/
|
|
337
339
|
function getIntegrationBuilderBaseDir() {
|
|
340
|
+
const work = getAifabrixWork();
|
|
341
|
+
if (work) {
|
|
342
|
+
const workNorm = path.resolve(work);
|
|
343
|
+
const integrationUnderWork = path.join(workNorm, 'integration');
|
|
344
|
+
try {
|
|
345
|
+
if (fs.existsSync(integrationUnderWork)) {
|
|
346
|
+
return workNorm;
|
|
347
|
+
}
|
|
348
|
+
} catch {
|
|
349
|
+
// ignore fs errors
|
|
350
|
+
}
|
|
351
|
+
}
|
|
338
352
|
const root = getProjectRoot();
|
|
339
353
|
const cwd = path.resolve(process.cwd());
|
|
340
354
|
const rootNorm = path.resolve(root);
|
|
@@ -8,10 +8,50 @@ const path = require('path');
|
|
|
8
8
|
const yaml = require('js-yaml');
|
|
9
9
|
const fsRealSync = require('../internal/fs-real-sync');
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} cfgPath
|
|
13
|
+
* @returns {object|null}
|
|
14
|
+
*/
|
|
15
|
+
function tryReadApplicationYamlAt(cfgPath) {
|
|
16
|
+
try {
|
|
17
|
+
if (!fsRealSync.existsSync(cfgPath)) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const raw = fsRealSync.readFileSync(cfgPath, 'utf8');
|
|
21
|
+
const doc = yaml.load(raw);
|
|
22
|
+
return doc && typeof doc === 'object' ? doc : null;
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Prefer projectRoot/builder (fixtures, tests); then {@link pathsUtil.getBuilderPath} (global npm).
|
|
30
|
+
* @param {string} appKey
|
|
31
|
+
* @param {object} pathsUtil
|
|
32
|
+
* @returns {string[]}
|
|
33
|
+
*/
|
|
34
|
+
function collectApplicationYamlPathsForUrlResolve(appKey, pathsUtil) {
|
|
35
|
+
const list = [];
|
|
36
|
+
const root = pathsUtil.getProjectRoot();
|
|
37
|
+
if (root) {
|
|
38
|
+
list.push(path.resolve(path.join(root, 'builder', appKey, 'application.yaml')));
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const viaBuilder = path.resolve(path.join(pathsUtil.getBuilderPath(appKey), 'application.yaml'));
|
|
42
|
+
if (!list.length || path.resolve(list[0]) !== viaBuilder) {
|
|
43
|
+
list.push(viaBuilder);
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
/* ignore getBuilderPath errors */
|
|
47
|
+
}
|
|
48
|
+
return list;
|
|
49
|
+
}
|
|
50
|
+
|
|
11
51
|
/**
|
|
12
52
|
* @param {string} appKey
|
|
13
53
|
* @param {Object} ctx
|
|
14
|
-
* @param {object} pathsUtil - paths module (getProjectRoot)
|
|
54
|
+
* @param {object} pathsUtil - paths module (getProjectRoot, getBuilderPath)
|
|
15
55
|
* @returns {object|null}
|
|
16
56
|
*/
|
|
17
57
|
function loadApplicationYamlDocForUrlResolve(appKey, ctx, pathsUtil) {
|
|
@@ -19,28 +59,18 @@ function loadApplicationYamlDocForUrlResolve(appKey, ctx, pathsUtil) {
|
|
|
19
59
|
const current = ctx.currentAppKey || '';
|
|
20
60
|
if (appKey === current && ctx.variablesPath) {
|
|
21
61
|
const vp = path.resolve(String(ctx.variablesPath));
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (doc && typeof doc === 'object') {
|
|
26
|
-
return doc;
|
|
27
|
-
}
|
|
28
|
-
} catch {
|
|
29
|
-
// Fall through to builder-relative resolution
|
|
62
|
+
const fromVars = tryReadApplicationYamlAt(vp);
|
|
63
|
+
if (fromVars) {
|
|
64
|
+
return fromVars;
|
|
30
65
|
}
|
|
31
66
|
}
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
const raw = fsRealSync.readFileSync(cfgPath, 'utf8');
|
|
39
|
-
const doc = yaml.load(raw);
|
|
40
|
-
return doc && typeof doc === 'object' ? doc : null;
|
|
41
|
-
} catch {
|
|
42
|
-
return null;
|
|
67
|
+
for (const cfgPath of collectApplicationYamlPathsForUrlResolve(appKey, pathsUtil)) {
|
|
68
|
+
const doc = tryReadApplicationYamlAt(cfgPath);
|
|
69
|
+
if (doc) {
|
|
70
|
+
return doc;
|
|
71
|
+
}
|
|
43
72
|
}
|
|
73
|
+
return null;
|
|
44
74
|
} catch {
|
|
45
75
|
return null;
|
|
46
76
|
}
|
|
@@ -156,19 +156,12 @@ function mergeDocIntoRegistry(merged, doc, folderName) {
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
/**
|
|
159
|
-
*
|
|
160
|
-
* @param {string
|
|
161
|
-
* @returns {Object} Updated registry
|
|
159
|
+
* @param {Object} merged
|
|
160
|
+
* @param {string} builderDir
|
|
162
161
|
*/
|
|
163
|
-
function
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (!root) {
|
|
167
|
-
return writeMergedRegistry(merged);
|
|
168
|
-
}
|
|
169
|
-
const builderDir = path.join(root, 'builder');
|
|
170
|
-
if (!fsRealSync.existsSync(builderDir) || !fsRealSync.statSync(builderDir).isDirectory()) {
|
|
171
|
-
return writeMergedRegistry(merged);
|
|
162
|
+
function mergeBuilderDirIntoRegistry(merged, builderDir) {
|
|
163
|
+
if (!builderDir || !fsRealSync.existsSync(builderDir) || !fsRealSync.statSync(builderDir).isDirectory()) {
|
|
164
|
+
return;
|
|
172
165
|
}
|
|
173
166
|
for (const ent of fsRealSync.readdirSync(builderDir, { withFileTypes: true })) {
|
|
174
167
|
if (!ent.isDirectory()) {
|
|
@@ -180,6 +173,37 @@ function refreshUrlsLocalRegistryFromBuilder(projectRoot) {
|
|
|
180
173
|
}
|
|
181
174
|
mergeDocIntoRegistry(merged, doc, ent.name);
|
|
182
175
|
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Merge scan results into registry (does not remove stale keys).
|
|
180
|
+
* @param {string|null} projectRoot - getProjectRoot() or null (same semantics as projectRoot || getProjectRoot())
|
|
181
|
+
* @returns {Object} Updated registry
|
|
182
|
+
*/
|
|
183
|
+
function refreshUrlsLocalRegistryFromBuilder(projectRoot) {
|
|
184
|
+
const root = projectRoot || pathsUtil.getProjectRoot();
|
|
185
|
+
const merged = { ...readUrlsLocalRegistrySync() };
|
|
186
|
+
if (!root) {
|
|
187
|
+
return writeMergedRegistry(merged);
|
|
188
|
+
}
|
|
189
|
+
// Published npm tarball omits builder/ under the package root (.npmignore). Global installs must
|
|
190
|
+
// still refresh from the real builder tree (AIFABRIX_BUILDER_DIR or integration base + builder).
|
|
191
|
+
const legacyBuilderDir = path.join(root, 'builder');
|
|
192
|
+
const effectiveBuilderDir = pathsUtil.getBuilderRoot();
|
|
193
|
+
const builderDirs = [legacyBuilderDir];
|
|
194
|
+
try {
|
|
195
|
+
if (
|
|
196
|
+
effectiveBuilderDir &&
|
|
197
|
+
path.resolve(effectiveBuilderDir) !== path.resolve(legacyBuilderDir)
|
|
198
|
+
) {
|
|
199
|
+
builderDirs.push(effectiveBuilderDir);
|
|
200
|
+
}
|
|
201
|
+
} catch {
|
|
202
|
+
/* ignore path resolution errors */
|
|
203
|
+
}
|
|
204
|
+
for (const builderDir of builderDirs) {
|
|
205
|
+
mergeBuilderDirIntoRegistry(merged, builderDir);
|
|
206
|
+
}
|
|
183
207
|
return writeMergedRegistry(merged);
|
|
184
208
|
}
|
|
185
209
|
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* @version 2.0.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
const logger = require('./logger');
|
|
7
9
|
const { getValidationRunWithTransportRetry } = require('./validation-run-post-retry');
|
|
8
10
|
|
|
9
11
|
const INITIAL_INTERVAL_MS = 2000;
|
|
@@ -23,6 +25,19 @@ function sleep(ms) {
|
|
|
23
25
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
function maybeLogPollProgress(envelope, verbosePoll, lastProgressLogAtRef) {
|
|
29
|
+
if (!verbosePoll || !envelope || typeof envelope !== 'object') return;
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
if (now - lastProgressLogAtRef[0] < 5000) return;
|
|
32
|
+
lastProgressLogAtRef[0] = now;
|
|
33
|
+
const st = envelope.status !== undefined && envelope.status !== null ? String(envelope.status) : '?';
|
|
34
|
+
const c =
|
|
35
|
+
envelope.reportCompleteness !== undefined && envelope.reportCompleteness !== null
|
|
36
|
+
? String(envelope.reportCompleteness)
|
|
37
|
+
: '?';
|
|
38
|
+
logger.log(chalk.gray(` Polling validation run… completeness=${c} status=${st}`));
|
|
39
|
+
}
|
|
40
|
+
|
|
26
41
|
/**
|
|
27
42
|
* Whether polling should stop on this envelope.
|
|
28
43
|
* @param {Object} envelope - DatasourceTestRun-like
|
|
@@ -42,6 +57,8 @@ function isTerminalReportCompleteness(envelope) {
|
|
|
42
57
|
* @param {string} opts.testRunId
|
|
43
58
|
* @param {number} opts.budgetMs - Remaining wall-clock budget for polls only (POST excluded)
|
|
44
59
|
* @param {typeof getValidationRunWithTransportRetry} [opts.fetchRun] - Inject for tests (default: GET with transport retry)
|
|
60
|
+
* @param {boolean} [opts.verbosePoll] - Log throttled progress (plan §3.13)
|
|
61
|
+
* @param {number} [opts.pollRequestTimeoutMs] - Per-GET HTTP timeout (match validation aggregate budget)
|
|
45
62
|
* @returns {Promise<{ envelope: Object|null, timedOut: boolean, lastApiResult: Object|null }>}
|
|
46
63
|
*/
|
|
47
64
|
async function pollValidationRunUntilComplete(opts) {
|
|
@@ -50,18 +67,22 @@ async function pollValidationRunUntilComplete(opts) {
|
|
|
50
67
|
authConfig,
|
|
51
68
|
testRunId,
|
|
52
69
|
budgetMs,
|
|
53
|
-
fetchRun = getValidationRunWithTransportRetry
|
|
70
|
+
fetchRun = getValidationRunWithTransportRetry,
|
|
71
|
+
verbosePoll = false,
|
|
72
|
+
pollRequestTimeoutMs
|
|
54
73
|
} = opts;
|
|
74
|
+
const pollTransportOpts =
|
|
75
|
+
Number.isFinite(pollRequestTimeoutMs) && pollRequestTimeoutMs > 0
|
|
76
|
+
? { timeoutMs: pollRequestTimeoutMs }
|
|
77
|
+
: {};
|
|
55
78
|
const deadline = Date.now() + Math.max(0, budgetMs);
|
|
56
79
|
let attempt = 0;
|
|
57
80
|
let lastApiResult = null;
|
|
58
81
|
let envelope = null;
|
|
82
|
+
const lastProgressLogAtRef = [0];
|
|
59
83
|
|
|
60
84
|
while (Date.now() < deadline) {
|
|
61
|
-
|
|
62
|
-
if (remaining <= 0) break;
|
|
63
|
-
|
|
64
|
-
lastApiResult = await fetchRun(dataplaneUrl, authConfig, testRunId);
|
|
85
|
+
lastApiResult = await fetchRun(dataplaneUrl, authConfig, testRunId, pollTransportOpts);
|
|
65
86
|
if (!lastApiResult.success) {
|
|
66
87
|
return { envelope: null, timedOut: false, lastApiResult };
|
|
67
88
|
}
|
|
@@ -70,6 +91,8 @@ async function pollValidationRunUntilComplete(opts) {
|
|
|
70
91
|
return { envelope, timedOut: false, lastApiResult };
|
|
71
92
|
}
|
|
72
93
|
|
|
94
|
+
maybeLogPollProgress(envelope, verbosePoll, lastProgressLogAtRef);
|
|
95
|
+
|
|
73
96
|
const delay = Math.min(nextPollDelayMs(attempt), Math.max(0, deadline - Date.now()));
|
|
74
97
|
attempt += 1;
|
|
75
98
|
if (delay > 0) {
|