@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.
Files changed (66) 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/datasource/log-viewer.js +105 -14
  35. package/lib/datasource/test-e2e.js +35 -17
  36. package/lib/datasource/unified-validation-run-body.js +3 -0
  37. package/lib/datasource/unified-validation-run.js +2 -1
  38. package/lib/external-system/deploy.js +53 -18
  39. package/lib/infrastructure/compose.js +12 -3
  40. package/lib/infrastructure/helpers-docker-check.js +67 -0
  41. package/lib/infrastructure/helpers.js +47 -58
  42. package/lib/infrastructure/index.js +3 -1
  43. package/lib/infrastructure/services.js +4 -56
  44. package/lib/schema/external-system.schema.json +25 -3
  45. package/lib/schema/type/document-storage.json +15 -2
  46. package/lib/utils/api.js +28 -3
  47. package/lib/utils/configuration-env-resolver.js +11 -8
  48. package/lib/utils/credential-secrets-env.js +5 -5
  49. package/lib/utils/datasource-test-run-certificate-tty.js +82 -0
  50. package/lib/utils/datasource-test-run-display.js +19 -2
  51. package/lib/utils/datasource-test-run-exit.js +25 -0
  52. package/lib/utils/external-system-display.js +8 -0
  53. package/lib/utils/external-system-system-test-tty-overview.js +120 -0
  54. package/lib/utils/external-system-system-test-tty.js +417 -0
  55. package/lib/utils/paths.js +14 -0
  56. package/lib/utils/validation-run-poll.js +28 -5
  57. package/lib/utils/validation-run-post-retry.js +20 -8
  58. package/lib/utils/validation-run-request.js +18 -0
  59. package/lib/validation/validate-external-cert-sync.js +23 -0
  60. package/lib/validation/validate.js +4 -1
  61. package/package.json +4 -3
  62. package/scripts/install-local.js +4 -1
  63. package/scripts/pnpm-global-remove.js +48 -0
  64. package/templates/applications/dataplane/env.template +4 -0
  65. package/templates/infra/compose.yaml.hbs +15 -14
  66. 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
- return computeExitCodeFromDatasourceTestRun(env, {
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; permissions are summarized in `docs/commands/permissions.md`.
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 Show saved test logs
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 };
@@ -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
- * Empty payload template for POST /validation/run: selects dataplane **payload test** path
196
- * (per-datasource template / connectivity), not the full validation-engine run.
197
- * Avoids false RBAC failures right after upload: engine security checks autoRbac vs
198
- * `system.permissions[]` before the controller has synced permissions (deploy --probe
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 = { payloadTemplate: {} };
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 and integration test logs - format and display JSON logs.
3
- * @fileoverview Read and format test-e2e / test-integration debug logs for terminal
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
- const fs = require('fs').promises;
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 fs.readdir(logsDir, { withFileTypes: true });
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 fs.stat(f)).mtimeMs }))
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
- const pattern = logType === 'test-e2e' ? 'test-e2e-' : 'test-integration-';
228
- logPath = await getLatestLogPath(logsDir, pattern);
229
- if (!logPath) {
230
- throw new Error(
231
- `No ${logType} log found in ${logsDir}. Run the test with --debug first.`
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 fs.readFile(logPath, 'utf8');
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(datasourceKey, {
122
- app: options.app,
123
- environment: options.environment,
124
- runType: 'e2e',
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
- let probeData = null;
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 pgadminConfig = options.pgadmin && typeof options.pgadmin.enabled === 'boolean'
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