@aifabrix/builder 2.44.0 → 2.44.1
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/datasource/log-viewer.js +105 -14
- 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/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/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,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview E2E / envelope exit helpers for datasource unified test CLI (keeps main CLI file under max-lines).
|
|
3
|
+
* @author AI Fabrix Team
|
|
4
|
+
* @version 2.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const logger = require('../utils/logger');
|
|
10
|
+
const { displayIntegrationTestResults, displayE2EResults } = require('../utils/external-system-display');
|
|
11
|
+
const { computeExitCodeFromDatasourceTestRun } = require('../utils/datasource-test-run-exit');
|
|
12
|
+
const { analyzeCapabilityScope } = require('../utils/datasource-test-run-capability-scope');
|
|
13
|
+
const {
|
|
14
|
+
resolveDebugDisplayMode,
|
|
15
|
+
formatDatasourceTestRunDebugBlock
|
|
16
|
+
} = require('../utils/datasource-test-run-debug-display');
|
|
17
|
+
const { formatCapabilityFocusSection } = require('../utils/datasource-test-run-display');
|
|
18
|
+
const { emitCapabilityScopeDiagnostics } = require('./datasource-validation-cli');
|
|
19
|
+
|
|
20
|
+
function logDatasourceTestRunDebugAppendix(envelope, debugOpt) {
|
|
21
|
+
const mode = resolveDebugDisplayMode(debugOpt);
|
|
22
|
+
if (!mode || !envelope) return;
|
|
23
|
+
const block = formatDatasourceTestRunDebugBlock(envelope, mode, process.stdout.isTTY);
|
|
24
|
+
if (block) logger.log(block);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function logE2eCapabilityFocusFromEnvelope(env, capabilityOpt) {
|
|
28
|
+
if (!env) return;
|
|
29
|
+
const capKey =
|
|
30
|
+
capabilityOpt !== undefined && capabilityOpt !== null
|
|
31
|
+
? String(capabilityOpt).trim()
|
|
32
|
+
: '';
|
|
33
|
+
if (!capKey) return;
|
|
34
|
+
const sec = formatCapabilityFocusSection(env, capKey);
|
|
35
|
+
if (sec) logger.log(sec);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {Object|null|undefined} env
|
|
40
|
+
* @param {Object} options
|
|
41
|
+
* @returns {number|null} null if no envelope
|
|
42
|
+
*/
|
|
43
|
+
function exitCodeFromDatasourceTestRunEnvelope(env, options) {
|
|
44
|
+
if (!env || typeof env !== 'object') return null;
|
|
45
|
+
let code = computeExitCodeFromDatasourceTestRun(env, {
|
|
46
|
+
warningsAsErrors: options.warningsAsErrors === true,
|
|
47
|
+
requireCert: options.requireCert === true
|
|
48
|
+
});
|
|
49
|
+
const scope = analyzeCapabilityScope(env, options.capability);
|
|
50
|
+
if (options.strictCapabilityScope === true && scope.violated) {
|
|
51
|
+
code = Math.max(code, 1);
|
|
52
|
+
}
|
|
53
|
+
return code;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Legacy E2E display + exit code (no process.exit; watch mode).
|
|
58
|
+
* @param {Object} data
|
|
59
|
+
* @param {Object} options
|
|
60
|
+
* @returns {number}
|
|
61
|
+
*/
|
|
62
|
+
function finalizeDatasourceTestE2ELegacyPath(data, options) {
|
|
63
|
+
displayE2EResults(data, options.verbose);
|
|
64
|
+
logDatasourceTestRunDebugAppendix(data.datasourceTestRun, options.debug);
|
|
65
|
+
logE2eCapabilityFocusFromEnvelope(data.datasourceTestRun, options.capability);
|
|
66
|
+
const env = data.datasourceTestRun;
|
|
67
|
+
if (env) {
|
|
68
|
+
emitCapabilityScopeDiagnostics(env, { requestedCapabilityKey: options.capability });
|
|
69
|
+
const code = exitCodeFromDatasourceTestRunEnvelope(env, options);
|
|
70
|
+
return code === null ? 1 : code;
|
|
71
|
+
}
|
|
72
|
+
const steps = data.steps || data.completedActions || [];
|
|
73
|
+
const failed = data.success === false || steps.some(s => s.success === false || s.error);
|
|
74
|
+
return failed ? 1 : 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {string} datasourceKey
|
|
79
|
+
* @param {Object} env
|
|
80
|
+
* @param {Object} options
|
|
81
|
+
*/
|
|
82
|
+
function displayDatasourceTestE2EEnvelopeResults(datasourceKey, env, options) {
|
|
83
|
+
const success = env.status !== 'fail';
|
|
84
|
+
displayIntegrationTestResults(
|
|
85
|
+
{
|
|
86
|
+
systemKey: env.systemKey || 'unknown',
|
|
87
|
+
success,
|
|
88
|
+
datasourceResults: [{ key: datasourceKey, success, datasourceTestRun: env }]
|
|
89
|
+
},
|
|
90
|
+
options.verbose,
|
|
91
|
+
{
|
|
92
|
+
debug: options.debug,
|
|
93
|
+
runType: 'e2e',
|
|
94
|
+
requestedCapabilityKey: options.capability
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
logE2eCapabilityFocusFromEnvelope(env, options.capability);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = {
|
|
101
|
+
logDatasourceTestRunDebugAppendix,
|
|
102
|
+
logE2eCapabilityFocusFromEnvelope,
|
|
103
|
+
exitCodeFromDatasourceTestRunEnvelope,
|
|
104
|
+
finalizeDatasourceTestE2ELegacyPath,
|
|
105
|
+
displayDatasourceTestE2EEnvelopeResults
|
|
106
|
+
};
|
|
@@ -103,10 +103,24 @@ function finalizeAfterIntegrationDisplay(integrationResult, exitOpts = {}) {
|
|
|
103
103
|
if (!env) {
|
|
104
104
|
return integrationResult.success ? 0 : 1;
|
|
105
105
|
}
|
|
106
|
-
|
|
106
|
+
let exitCode = computeExitCodeFromDatasourceTestRun(env, {
|
|
107
107
|
warningsAsErrors: exitOpts.warningsAsErrors === true,
|
|
108
108
|
requireCert: exitOpts.requireCert === true
|
|
109
109
|
});
|
|
110
|
+
if (exitOpts.strictCapabilityScope === true) {
|
|
111
|
+
const scope = analyzeCapabilityScope(env, exitOpts.requestedCapabilityKey);
|
|
112
|
+
if (scope.violated) {
|
|
113
|
+
exitCode = Math.max(exitCode, 1);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (
|
|
117
|
+
exitCode === 2 &&
|
|
118
|
+
exitOpts.requireCert &&
|
|
119
|
+
!env.certificate
|
|
120
|
+
) {
|
|
121
|
+
logger.error(formatBlockingError('Certification not returned; cannot verify.'));
|
|
122
|
+
}
|
|
123
|
+
return exitCode;
|
|
110
124
|
}
|
|
111
125
|
|
|
112
126
|
/**
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* AI Fabrix Builder - Datasource Commands
|
|
3
3
|
*
|
|
4
4
|
* Handles datasource validation, listing, comparison, deployment, and online validation runs.
|
|
5
|
-
* Subcommands `test`, `test-integration`, and `test-e2e` call the dataplane unified validation API;
|
|
5
|
+
* Subcommands `test`, `test-integration`, and `test-e2e` call the dataplane unified validation API; `log-test` / `log-integration` / `log-e2e` read saved debug JSON locally. Permissions are summarized in `docs/commands/permissions.md`.
|
|
6
6
|
*
|
|
7
7
|
* @fileoverview Datasource management commands for AI Fabrix Builder
|
|
8
8
|
* @author AI Fabrix Team
|
|
@@ -32,7 +32,7 @@ Subcommands:
|
|
|
32
32
|
diff Compare two datasource JSON files
|
|
33
33
|
test <key> Structural/policy validation via unified dataplane API (DatasourceTestRun)
|
|
34
34
|
test-integration / test-e2e Integration or E2E run via the same unified validation API
|
|
35
|
-
log-integration / log-e2e
|
|
35
|
+
log-test / log-integration / log-e2e Show saved debug logs (structural, integration, E2E)
|
|
36
36
|
`;
|
|
37
37
|
|
|
38
38
|
const DATASOURCE_VALIDATE_HELP_AFTER = `
|
|
@@ -187,6 +187,28 @@ function setupDatasourceLogIntegrationCommand(datasource) {
|
|
|
187
187
|
});
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
function setupDatasourceLogTestCommand(datasource) {
|
|
191
|
+
datasource.command('log-test <datasourceKey>')
|
|
192
|
+
.description('Show structural validation log from datasource test (latest test-*.json or --file)')
|
|
193
|
+
.option(
|
|
194
|
+
'-a, --app <app>',
|
|
195
|
+
'Integration folder name (optional: resolve from cwd or datasource key if single match)'
|
|
196
|
+
)
|
|
197
|
+
.option('-f, --file <path>', 'Path to log file (default: latest structural log in app logs folder)')
|
|
198
|
+
.action(async(datasourceKey, options) => {
|
|
199
|
+
try {
|
|
200
|
+
await runLogViewer(datasourceKey, {
|
|
201
|
+
app: options.app,
|
|
202
|
+
file: options.file,
|
|
203
|
+
logType: 'test'
|
|
204
|
+
});
|
|
205
|
+
} catch (error) {
|
|
206
|
+
logger.error(formatBlockingError('log-test failed:'), error.message);
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
190
212
|
/**
|
|
191
213
|
* Setup datasource management commands
|
|
192
214
|
* @param {Command} program - Commander program instance
|
|
@@ -208,6 +230,7 @@ function setupDatasourceCommands(program) {
|
|
|
208
230
|
setupDatasourceTestE2ECommand(datasource);
|
|
209
231
|
setupDatasourceLogE2ECommand(datasource);
|
|
210
232
|
setupDatasourceLogIntegrationCommand(datasource);
|
|
233
|
+
setupDatasourceLogTestCommand(datasource);
|
|
211
234
|
}
|
|
212
235
|
|
|
213
236
|
module.exports = { setupDatasourceCommands };
|
package/lib/commands/upload.js
CHANGED
|
@@ -35,6 +35,8 @@ const {
|
|
|
35
35
|
logServerValidationWarnings,
|
|
36
36
|
logProbeRuntimeBlock
|
|
37
37
|
} = require('../utils/external-system-readiness-display');
|
|
38
|
+
const { maybeSyncSystemCertificationFromDataplane } = require('../certification/sync-system-certification');
|
|
39
|
+
const { cliOptsSkipCertSync } = require('../certification/cli-cert-sync-skip');
|
|
38
40
|
|
|
39
41
|
/**
|
|
40
42
|
* Validates system-key format (same as download).
|
|
@@ -192,13 +194,12 @@ async function maybeRunVerboseServerValidation(dataplaneUrl, authConfig, payload
|
|
|
192
194
|
}
|
|
193
195
|
|
|
194
196
|
/**
|
|
195
|
-
*
|
|
196
|
-
* (
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
* still uses the engine path without payloadTemplate).
|
|
197
|
+
* Omit payloadTemplate so POST /validation/run uses the **validation-engine** path
|
|
198
|
+
* (same as deploy --probe). Needed for certification and dataplane auto-issue of integration
|
|
199
|
+
* certificates after a successful run. If RBAC/live checks fail right after upload until the
|
|
200
|
+
* controller syncs permissions, skip --probe or retry once permissions are visible.
|
|
200
201
|
*/
|
|
201
|
-
const UPLOAD_PROBE_TEST_DATA = {
|
|
202
|
+
const UPLOAD_PROBE_TEST_DATA = {};
|
|
202
203
|
|
|
203
204
|
/**
|
|
204
205
|
* Optional POST validation/run after successful upload.
|
|
@@ -281,6 +282,16 @@ async function runUploadPublishAndSummary(systemKey, options, manifest, payload)
|
|
|
281
282
|
if (options.probe) {
|
|
282
283
|
await maybeRunUploadProbe(dataplaneUrl, systemKey, authConfig, options.probeTimeout);
|
|
283
284
|
}
|
|
285
|
+
|
|
286
|
+
const dsKeys = (payload.dataSources || []).map((ds) => ds && ds.key).filter(Boolean);
|
|
287
|
+
await maybeSyncSystemCertificationFromDataplane({
|
|
288
|
+
label: 'upload',
|
|
289
|
+
noCertSync: cliOptsSkipCertSync(options),
|
|
290
|
+
systemKey,
|
|
291
|
+
dataplaneUrl,
|
|
292
|
+
authConfig,
|
|
293
|
+
datasourceKeys: dsKeys
|
|
294
|
+
});
|
|
284
295
|
}
|
|
285
296
|
|
|
286
297
|
/**
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Log viewer for E2E
|
|
3
|
-
* @fileoverview Read and format
|
|
2
|
+
* Log viewer for E2E, integration, and structural (`datasource test`) debug logs.
|
|
3
|
+
* @fileoverview Read and format saved JSON logs under integration/<app>/logs/
|
|
4
4
|
* @author AI Fabrix Team
|
|
5
5
|
* @version 2.0.0
|
|
6
6
|
*/
|
|
7
7
|
/* eslint-disable max-statements, complexity, max-depth -- Formatter functions; display branches by design */
|
|
8
8
|
|
|
9
9
|
const path = require('path');
|
|
10
|
-
|
|
10
|
+
// Use node:fs so suites that jest.mock('fs') do not break real-disk log resolution (structural tests, CI).
|
|
11
|
+
const fsp = require('node:fs').promises;
|
|
11
12
|
const chalk = require('chalk');
|
|
12
13
|
const logger = require('../utils/logger');
|
|
13
14
|
const { resolveAppKeyForDatasource } = require('./resolve-app');
|
|
@@ -23,7 +24,7 @@ const { sectionTitle, headerKeyValue, formatBlockingError, successGlyph, failure
|
|
|
23
24
|
async function getLatestLogPath(logsDir, pattern) {
|
|
24
25
|
let entries;
|
|
25
26
|
try {
|
|
26
|
-
entries = await
|
|
27
|
+
entries = await fsp.readdir(logsDir, { withFileTypes: true });
|
|
27
28
|
} catch (err) {
|
|
28
29
|
if (err.code === 'ENOENT') return null;
|
|
29
30
|
throw err;
|
|
@@ -34,12 +35,46 @@ async function getLatestLogPath(logsDir, pattern) {
|
|
|
34
35
|
.map(e => path.join(logsDir, e.name));
|
|
35
36
|
if (files.length === 0) return null;
|
|
36
37
|
const withStats = await Promise.all(
|
|
37
|
-
files.map(async f => ({ path: f, mtime: (await
|
|
38
|
+
files.map(async f => ({ path: f, mtime: (await fsp.stat(f)).mtimeMs }))
|
|
38
39
|
);
|
|
39
40
|
withStats.sort((a, b) => b.mtime - a.mtime);
|
|
40
41
|
return withStats[0].path;
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Structural validation logs use prefix `test-` but must not pick up `test-e2e-` or `test-integration-`.
|
|
46
|
+
* @param {string} name - File name only
|
|
47
|
+
* @returns {boolean}
|
|
48
|
+
*/
|
|
49
|
+
function isStructuralTestLogFileName(name) {
|
|
50
|
+
if (!name || typeof name !== 'string' || !name.endsWith('.json')) return false;
|
|
51
|
+
if (name.startsWith('test-e2e-') || name.startsWith('test-integration-')) return false;
|
|
52
|
+
return name.startsWith('test-');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Latest structural `datasource test --debug` log in logsDir.
|
|
57
|
+
* @param {string} logsDir
|
|
58
|
+
* @returns {Promise<string|null>}
|
|
59
|
+
*/
|
|
60
|
+
async function getLatestStructuralTestLogPath(logsDir) {
|
|
61
|
+
let entries;
|
|
62
|
+
try {
|
|
63
|
+
entries = await fsp.readdir(logsDir, { withFileTypes: true });
|
|
64
|
+
} catch (err) {
|
|
65
|
+
if (err.code === 'ENOENT') return null;
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
const names = entries
|
|
69
|
+
.filter(e => e.isFile() && isStructuralTestLogFileName(e.name))
|
|
70
|
+
.map(e => e.name);
|
|
71
|
+
if (names.length === 0) return null;
|
|
72
|
+
// Structural logs embed a sortable timestamp in the filename (`test-YYYY-...Z.json`).
|
|
73
|
+
// Prefer lexicographic ordering to avoid filesystem timestamp resolution issues in CI.
|
|
74
|
+
names.sort((a, b) => b.localeCompare(a));
|
|
75
|
+
return path.join(logsDir, names[0]);
|
|
76
|
+
}
|
|
77
|
+
|
|
43
78
|
/**
|
|
44
79
|
* Truncate string for display
|
|
45
80
|
* @param {string} s - String
|
|
@@ -185,13 +220,57 @@ function formatIntegrationLog(data, fileName) {
|
|
|
185
220
|
}
|
|
186
221
|
}
|
|
187
222
|
|
|
223
|
+
function structuralEnvelopeStatusLine(status) {
|
|
224
|
+
if (status === 'ok' || status === 'skipped') return `${successGlyph()} ${chalk.gray('status:')} ${status}`;
|
|
225
|
+
if (status === 'warn') return `${chalk.yellow('⚠')} ${chalk.gray('status:')} ${status}`;
|
|
226
|
+
if (status === 'fail') return `${failureGlyph()} ${chalk.gray('status:')} ${status}`;
|
|
227
|
+
return `${chalk.gray('?')} ${chalk.gray('status:')} ${status ?? '—'}`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Format structural validation log (`datasource test --debug` → `test-*.json`).
|
|
232
|
+
* @param {Object} data - Parsed JSON { request, response?, error? }
|
|
233
|
+
* @param {string} [fileName] - Log file name for header
|
|
234
|
+
*/
|
|
235
|
+
function formatStructuralTestLog(data, fileName) {
|
|
236
|
+
logger.log('');
|
|
237
|
+
logger.log(sectionTitle('Structural validation log'));
|
|
238
|
+
if (fileName) logger.log(chalk.gray(fileName));
|
|
239
|
+
const req = data.request || {};
|
|
240
|
+
logger.log('');
|
|
241
|
+
logger.log(sectionTitle('Request:'));
|
|
242
|
+
logger.log(headerKeyValue('datasourceKey:', String(req.datasourceKey ?? '—')));
|
|
243
|
+
if (req.runType) logger.log(headerKeyValue('runType:', String(req.runType)));
|
|
244
|
+
if (req.includeDebug !== undefined) {
|
|
245
|
+
logger.log(chalk.gray(` includeDebug: ${req.includeDebug}`));
|
|
246
|
+
}
|
|
247
|
+
if (data.error) {
|
|
248
|
+
logger.log('');
|
|
249
|
+
logger.log(formatBlockingError(`Error: ${data.error}`));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const res = data.response || {};
|
|
253
|
+
logger.log('');
|
|
254
|
+
logger.log(sectionTitle('Response (envelope):'));
|
|
255
|
+
logger.log(` ${structuralEnvelopeStatusLine(res.status)}`);
|
|
256
|
+
if (res.reportCompleteness) {
|
|
257
|
+
logger.log(chalk.gray(` reportCompleteness: ${res.reportCompleteness}`));
|
|
258
|
+
}
|
|
259
|
+
if (res.runId) logger.log(chalk.gray(` runId: ${res.runId}`));
|
|
260
|
+
if (res.systemKey) logger.log(chalk.gray(` systemKey: ${res.systemKey}`));
|
|
261
|
+
}
|
|
262
|
+
|
|
188
263
|
/**
|
|
189
264
|
* Format log content by type
|
|
190
265
|
* @param {Object} parsed - Parsed JSON log
|
|
191
|
-
* @param {'test-e2e'|'test-integration'} logType - Log type
|
|
266
|
+
* @param {'test'|'test-e2e'|'test-integration'} logType - Log type
|
|
192
267
|
* @param {string} [fileName] - File name for header
|
|
193
268
|
*/
|
|
194
269
|
function formatLogContent(parsed, logType, fileName) {
|
|
270
|
+
if (logType === 'test') {
|
|
271
|
+
formatStructuralTestLog(parsed, fileName);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
195
274
|
if (logType === 'test-e2e') {
|
|
196
275
|
formatE2ELog(parsed, fileName);
|
|
197
276
|
} else {
|
|
@@ -206,7 +285,7 @@ function formatLogContent(parsed, logType, fileName) {
|
|
|
206
285
|
* @param {Object} options - Options
|
|
207
286
|
* @param {string} [options.app] - App key (optional, resolved from key if omitted)
|
|
208
287
|
* @param {string} [options.file] - Path to log file (overrides app resolution)
|
|
209
|
-
* @param {'test-e2e'|'test-integration'} options.logType - Log type
|
|
288
|
+
* @param {'test'|'test-e2e'|'test-integration'} options.logType - Log type
|
|
210
289
|
* @throws {Error} When file not found or invalid JSON
|
|
211
290
|
*/
|
|
212
291
|
/* eslint-disable-next-line max-statements -- Resolve path, read, parse, format */
|
|
@@ -224,16 +303,25 @@ async function runLogViewer(datasourceKey, options) {
|
|
|
224
303
|
const { appKey } = await resolveAppKeyForDatasource(datasourceKey.trim(), app);
|
|
225
304
|
const appPath = getIntegrationPath(appKey);
|
|
226
305
|
const logsDir = path.join(appPath, 'logs');
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
306
|
+
if (logType === 'test') {
|
|
307
|
+
logPath = await getLatestStructuralTestLogPath(logsDir);
|
|
308
|
+
if (!logPath) {
|
|
309
|
+
throw new Error(
|
|
310
|
+
`No structural validation log found in ${logsDir}. Run: aifabrix datasource test <key> --debug`
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
} else {
|
|
314
|
+
const pattern = logType === 'test-e2e' ? 'test-e2e-' : 'test-integration-';
|
|
315
|
+
logPath = await getLatestLogPath(logsDir, pattern);
|
|
316
|
+
if (!logPath) {
|
|
317
|
+
throw new Error(
|
|
318
|
+
`No ${logType} log found in ${logsDir}. Run the test with --debug first.`
|
|
319
|
+
);
|
|
320
|
+
}
|
|
233
321
|
}
|
|
234
322
|
fileName = path.basename(logPath);
|
|
235
323
|
}
|
|
236
|
-
const content = await
|
|
324
|
+
const content = await fsp.readFile(logPath, 'utf8');
|
|
237
325
|
let parsed;
|
|
238
326
|
try {
|
|
239
327
|
parsed = JSON.parse(content);
|
|
@@ -245,8 +333,11 @@ async function runLogViewer(datasourceKey, options) {
|
|
|
245
333
|
|
|
246
334
|
module.exports = {
|
|
247
335
|
getLatestLogPath,
|
|
336
|
+
getLatestStructuralTestLogPath,
|
|
337
|
+
isStructuralTestLogFileName,
|
|
248
338
|
formatLogContent,
|
|
249
339
|
formatE2ELog,
|
|
250
340
|
formatIntegrationLog,
|
|
341
|
+
formatStructuralTestLog,
|
|
251
342
|
runLogViewer
|
|
252
343
|
};
|
|
@@ -19,6 +19,33 @@ const { writeTestLog } = require('../utils/test-log-writer');
|
|
|
19
19
|
|
|
20
20
|
const DEFAULT_POLL_TIMEOUT_MS = 15 * 60 * 1000;
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* @param {Object} options
|
|
24
|
+
* @param {string|number} timeoutMs
|
|
25
|
+
* @param {string|Object|null} pk
|
|
26
|
+
*/
|
|
27
|
+
function buildUnifiedE2eRunOptions(options, timeoutMs, pk) {
|
|
28
|
+
return {
|
|
29
|
+
app: options.app,
|
|
30
|
+
environment: options.environment,
|
|
31
|
+
runType: 'e2e',
|
|
32
|
+
debug: options.debug,
|
|
33
|
+
verbose: options.verbose,
|
|
34
|
+
async: options.async !== false,
|
|
35
|
+
noAsync: options.async === false,
|
|
36
|
+
testCrud: options.testCrud,
|
|
37
|
+
recordId: options.recordId,
|
|
38
|
+
cleanup: options.cleanup,
|
|
39
|
+
primaryKeyValue: pk,
|
|
40
|
+
minVectorHits: options.minVectorHits,
|
|
41
|
+
minProcessed: options.minProcessed,
|
|
42
|
+
minRecordCount: options.minRecordCount,
|
|
43
|
+
capabilityKey: options.capabilityKey,
|
|
44
|
+
timeout: timeoutMs,
|
|
45
|
+
sync: options.sync === true
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
22
49
|
function logE2eDatasourceBanner(datasourceKey, verbose) {
|
|
23
50
|
if (!verbose) return;
|
|
24
51
|
logger.log('');
|
|
@@ -115,25 +142,16 @@ async function runDatasourceTestE2E(datasourceKey, options = {}) {
|
|
|
115
142
|
testCrud: options.testCrud,
|
|
116
143
|
recordId: options.recordId,
|
|
117
144
|
cleanup: options.cleanup,
|
|
118
|
-
primaryKeyValue: pk !== undefined && pk !== null
|
|
145
|
+
primaryKeyValue: pk !== undefined && pk !== null,
|
|
146
|
+
minVectorHits: options.minVectorHits,
|
|
147
|
+
minProcessed: options.minProcessed,
|
|
148
|
+
minRecordCount: options.minRecordCount
|
|
119
149
|
};
|
|
120
150
|
|
|
121
|
-
const unifiedResult = await runUnifiedDatasourceValidation(
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
debug: options.debug,
|
|
126
|
-
verbose: options.verbose,
|
|
127
|
-
async: options.async !== false,
|
|
128
|
-
noAsync: options.async === false,
|
|
129
|
-
testCrud: options.testCrud,
|
|
130
|
-
recordId: options.recordId,
|
|
131
|
-
cleanup: options.cleanup,
|
|
132
|
-
primaryKeyValue: pk,
|
|
133
|
-
capabilityKey: options.capabilityKey,
|
|
134
|
-
timeout: timeoutMs,
|
|
135
|
-
sync: options.sync === true
|
|
136
|
-
});
|
|
151
|
+
const unifiedResult = await runUnifiedDatasourceValidation(
|
|
152
|
+
datasourceKey,
|
|
153
|
+
buildUnifiedE2eRunOptions(options, timeoutMs, pk)
|
|
154
|
+
);
|
|
137
155
|
|
|
138
156
|
await throwIfUnifiedE2EBlocked(unifiedResult, appKey, options, requestMeta);
|
|
139
157
|
|
|
@@ -46,6 +46,9 @@ async function buildUnifiedValidationBody(params) {
|
|
|
46
46
|
recordId: options.recordId,
|
|
47
47
|
cleanup: options.cleanup,
|
|
48
48
|
primaryKeyValue: options.primaryKeyValue,
|
|
49
|
+
minVectorHits: options.minVectorHits,
|
|
50
|
+
minProcessed: options.minProcessed,
|
|
51
|
+
minRecordCount: options.minRecordCount,
|
|
49
52
|
e2eOptionsExtra: e2eExtra
|
|
50
53
|
})
|
|
51
54
|
: undefined;
|
|
@@ -82,7 +82,8 @@ async function runUnifiedDatasourceValidation(datasourceKey, options) {
|
|
|
82
82
|
body,
|
|
83
83
|
timeoutMs,
|
|
84
84
|
useAsync,
|
|
85
|
-
noAsync: options.noAsync === true || options.async === false
|
|
85
|
+
noAsync: options.noAsync === true || options.async === false,
|
|
86
|
+
verbosePoll: options.verbose === true
|
|
86
87
|
});
|
|
87
88
|
}
|
|
88
89
|
|
|
@@ -27,6 +27,8 @@ const { parseControllerDeploymentOutcome } = require('../utils/controller-deploy
|
|
|
27
27
|
const { generateControllerManifest } = require('../generator/external-controller-manifest');
|
|
28
28
|
const { validateExternalSystemComplete } = require('../validation/validate');
|
|
29
29
|
const { displayValidationResults } = require('../validation/validate-display');
|
|
30
|
+
const { maybeSyncSystemCertificationFromDataplane } = require('../certification/sync-system-certification');
|
|
31
|
+
const { cliOptsSkipCertSync } = require('../certification/cli-cert-sync-skip');
|
|
30
32
|
|
|
31
33
|
/**
|
|
32
34
|
* Lists datasources for a system and loads system record for docs URLs.
|
|
@@ -97,6 +99,54 @@ async function fetchDataplaneDeployReadiness(controllerUrl, environment, authCon
|
|
|
97
99
|
/**
|
|
98
100
|
* @param {Object} deploymentOutcome - from parseControllerDeploymentOutcome
|
|
99
101
|
*/
|
|
102
|
+
/**
|
|
103
|
+
* Optional dataplane validation/run after external deploy (--probe).
|
|
104
|
+
* @async
|
|
105
|
+
* @param {{ dataplaneUrl: string|null, error: Error|null }} ctx
|
|
106
|
+
* @param {string} systemKey
|
|
107
|
+
* @param {Object} authConfig
|
|
108
|
+
* @param {Object} options
|
|
109
|
+
* @returns {Promise<Object|null>} Unwrapped probe payload or null
|
|
110
|
+
*/
|
|
111
|
+
async function runOptionalExternalDeployProbe(ctx, systemKey, authConfig, options) {
|
|
112
|
+
if (!options.probe || !ctx.dataplaneUrl || ctx.error) return null;
|
|
113
|
+
logger.log(chalk.blue('\nRunning runtime checks (--probe)...'));
|
|
114
|
+
try {
|
|
115
|
+
const pr = await testSystemViaPipeline(ctx.dataplaneUrl, systemKey, authConfig, {}, {
|
|
116
|
+
timeout: options.probeTimeout || 120000
|
|
117
|
+
});
|
|
118
|
+
if (pr.success === false) {
|
|
119
|
+
logger.log(chalk.yellow(`⚠ Probe request failed: ${pr.formattedError || pr.error || 'unknown error'}`));
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
return unwrapApiData(pr);
|
|
123
|
+
} catch (e) {
|
|
124
|
+
logger.log(chalk.yellow(`⚠ Probe failed: ${e.message}`));
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @param {Object} deploymentOutcome
|
|
131
|
+
* @param {{ dataplaneUrl: string|null, error: Error|null }} ctx
|
|
132
|
+
* @param {Object} manifest
|
|
133
|
+
* @param {Object} options
|
|
134
|
+
* @param {Object} authConfig
|
|
135
|
+
* @returns {Promise<void>}
|
|
136
|
+
*/
|
|
137
|
+
async function maybeSyncCertificationAfterExternalDeploy(deploymentOutcome, ctx, manifest, options, authConfig) {
|
|
138
|
+
if (!deploymentOutcome.ok || !ctx.dataplaneUrl || ctx.error) return;
|
|
139
|
+
const dsKeys = (manifest.dataSources || []).map((ds) => ds && ds.key).filter(Boolean);
|
|
140
|
+
await maybeSyncSystemCertificationFromDataplane({
|
|
141
|
+
label: 'deploy',
|
|
142
|
+
noCertSync: cliOptsSkipCertSync(options),
|
|
143
|
+
systemKey: manifest.key,
|
|
144
|
+
dataplaneUrl: ctx.dataplaneUrl,
|
|
145
|
+
authConfig,
|
|
146
|
+
datasourceKeys: dsKeys
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
100
150
|
function logImmediateControllerDeploymentOutcome(deploymentOutcome) {
|
|
101
151
|
if (deploymentOutcome.ok) {
|
|
102
152
|
logger.log(formatSuccessParagraph('Controller deployment OK'));
|
|
@@ -148,24 +198,7 @@ async function executeDeployAndDisplay(manifest, controllerUrl, environment, aut
|
|
|
148
198
|
manifest.key
|
|
149
199
|
);
|
|
150
200
|
|
|
151
|
-
|
|
152
|
-
if (options.probe && ctx.dataplaneUrl && !ctx.error) {
|
|
153
|
-
logger.log(chalk.blue('\nRunning runtime checks (--probe)...'));
|
|
154
|
-
try {
|
|
155
|
-
const pr = await testSystemViaPipeline(ctx.dataplaneUrl, manifest.key, authConfig, {}, {
|
|
156
|
-
timeout: options.probeTimeout || 120000
|
|
157
|
-
});
|
|
158
|
-
if (pr.success === false) {
|
|
159
|
-
logger.log(
|
|
160
|
-
chalk.yellow(`⚠ Probe request failed: ${pr.formattedError || pr.error || 'unknown error'}`)
|
|
161
|
-
);
|
|
162
|
-
} else {
|
|
163
|
-
probeData = unwrapApiData(pr);
|
|
164
|
-
}
|
|
165
|
-
} catch (e) {
|
|
166
|
-
logger.log(chalk.yellow(`⚠ Probe failed: ${e.message}`));
|
|
167
|
-
}
|
|
168
|
-
}
|
|
201
|
+
const probeData = await runOptionalExternalDeployProbe(ctx, manifest.key, authConfig, options);
|
|
169
202
|
|
|
170
203
|
logDeployReadinessSummary({
|
|
171
204
|
environment,
|
|
@@ -179,6 +212,8 @@ async function executeDeployAndDisplay(manifest, controllerUrl, environment, aut
|
|
|
179
212
|
probeData
|
|
180
213
|
});
|
|
181
214
|
|
|
215
|
+
await maybeSyncCertificationAfterExternalDeploy(deploymentOutcome, ctx, manifest, options, authConfig);
|
|
216
|
+
|
|
182
217
|
return result;
|
|
183
218
|
}
|
|
184
219
|
|
|
@@ -109,13 +109,23 @@ function generateComposeFile(templatePath, devId, idNum, ports, infraDir, option
|
|
|
109
109
|
const template = handlebars.compile(templateContent);
|
|
110
110
|
const networkName = idNum === 0 ? 'infra-aifabrix-network' : `infra-dev${devId}-aifabrix-network`;
|
|
111
111
|
const serversJsonPath = path.join(infraDir, 'servers.json');
|
|
112
|
+
const serversJsonBind = toDockerBindMountSource(serversJsonPath);
|
|
112
113
|
const pgpassPath = path.join(infraDir, 'pgpass');
|
|
113
114
|
const traefikConfig = typeof options.traefik === 'object'
|
|
114
115
|
? options.traefik
|
|
115
116
|
: buildTraefikConfig(!!options.traefik);
|
|
116
|
-
const
|
|
117
|
+
const pgadminConfigRaw = options.pgadmin && typeof options.pgadmin.enabled === 'boolean'
|
|
117
118
|
? options.pgadmin
|
|
118
119
|
: { enabled: true };
|
|
120
|
+
const pgadminEnabled = !!pgadminConfigRaw.enabled;
|
|
121
|
+
const pgpassBootstrapPath = path.join(infraDir, '.pgpass.bootstrap');
|
|
122
|
+
const pgadminConfig = {
|
|
123
|
+
...pgadminConfigRaw,
|
|
124
|
+
enabled: pgadminEnabled,
|
|
125
|
+
...(pgadminEnabled
|
|
126
|
+
? { pgpassBootstrapBind: toDockerBindMountSource(pgpassBootstrapPath) }
|
|
127
|
+
: {})
|
|
128
|
+
};
|
|
119
129
|
const redisCommanderConfig = options.redisCommander && typeof options.redisCommander.enabled === 'boolean'
|
|
120
130
|
? options.redisCommander
|
|
121
131
|
: { enabled: true };
|
|
@@ -132,7 +142,6 @@ function generateComposeFile(templatePath, devId, idNum, ports, infraDir, option
|
|
|
132
142
|
}
|
|
133
143
|
: traefikConfig;
|
|
134
144
|
const initScriptsBind = toDockerBindMountSource(path.join(infraDir, 'init-scripts'));
|
|
135
|
-
const infraDirBind = toDockerBindMountSource(infraDir);
|
|
136
145
|
const composeContent = template({
|
|
137
146
|
devId: devId,
|
|
138
147
|
postgresPort: ports.postgres,
|
|
@@ -143,10 +152,10 @@ function generateComposeFile(templatePath, devId, idNum, ports, infraDir, option
|
|
|
143
152
|
traefikHttpsPort: ports.traefikHttps,
|
|
144
153
|
networkName: networkName,
|
|
145
154
|
serversJsonPath: serversJsonPath,
|
|
155
|
+
serversJsonBind: serversJsonBind,
|
|
146
156
|
pgpassPath: pgpassPath,
|
|
147
157
|
infraDir: infraDir,
|
|
148
158
|
initScriptsBind: initScriptsBind,
|
|
149
|
-
infraDirBind: infraDirBind,
|
|
150
159
|
traefik: traefikForCompose,
|
|
151
160
|
pgadmin: pgadminConfig,
|
|
152
161
|
redisCommander: redisCommanderConfig
|