@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
@@ -7,6 +7,7 @@
7
7
  const chalk = require('chalk');
8
8
  const logger = require('../utils/logger');
9
9
  const { handleCommandError } = require('../utils/cli-utils');
10
+ const { cliOptsSkipCertSync } = require('../certification/cli-cert-sync-skip');
10
11
  const { TEST_HELP_AFTER, TEST_E2E_HELP_AFTER } = require('./setup-app.help');
11
12
 
12
13
  function setupTestCommand(program) {
@@ -72,22 +73,31 @@ async function runTestE2ECommand(appName, options) {
72
73
  sync: options.sync === true
73
74
  });
74
75
  const { displayIntegrationTestResults } = require('../utils/external-system-display');
76
+ const datasourceResults = results.map(r => ({
77
+ key: r.key,
78
+ success: r.success,
79
+ error: r.error,
80
+ skipped: false,
81
+ datasourceTestRun: r.datasourceTestRun
82
+ }));
75
83
  displayIntegrationTestResults(
76
84
  {
77
85
  systemKey: appName,
78
86
  success,
79
- datasourceResults: results.map(r => ({
80
- key: r.key,
81
- success: r.success,
82
- error: r.error,
83
- skipped: false,
84
- datasourceTestRun: r.datasourceTestRun
85
- }))
87
+ datasourceResults
86
88
  },
87
89
  options.verbose,
88
90
  { debug: options.debug, runType: 'e2e' }
89
91
  );
90
- if (!success) process.exit(1);
92
+ const { computeSystemExitCodeFromDatasourceRows } = require('../utils/datasource-test-run-exit');
93
+ const exitCode = computeSystemExitCodeFromDatasourceRows(datasourceResults, {
94
+ warningsAsErrors: options.warningsAsErrors === true,
95
+ requireCert: options.requireCert === true
96
+ });
97
+ if (exitCode !== 0) process.exit(exitCode);
98
+ if (cliOptsSkipCertSync(options)) return;
99
+ const { trySyncCertificationFromDataplaneForExternalApp } = require('../certification/sync-after-external-command');
100
+ await trySyncCertificationFromDataplaneForExternalApp(appName, 'test-e2e');
91
101
  return;
92
102
  }
93
103
  const { runAppTestE2e } = require('../commands/app-test');
@@ -127,6 +137,12 @@ function setupTestE2eCommand(program) {
127
137
  '--sync',
128
138
  'Publish local system and datasource files to the dataplane before running E2E (same as aifabrix upload <systemKey>; external integration only)'
129
139
  )
140
+ .option('--warnings-as-errors', 'Treat aggregate warn as failure (exit 1)')
141
+ .option('--require-cert', 'Require certification passed on every datasource (exit 2 if not)')
142
+ .option(
143
+ '--no-cert-sync',
144
+ 'Skip updating the system file certification block from the dataplane after a successful run'
145
+ )
130
146
  .addHelpText('after', TEST_E2E_HELP_AFTER)
131
147
  .action(async(appName, options, cmd) => {
132
148
  try {
@@ -15,6 +15,7 @@
15
15
  */
16
16
 
17
17
  const { handleCommandError } = require('../utils/cli-utils');
18
+ const { cliOptsSkipCertSync } = require('../certification/cli-cert-sync-skip');
18
19
  const { TEST_INTEGRATION_HELP_AFTER } = require('./setup-app.help');
19
20
 
20
21
  function setupDownloadCommand(program) {
@@ -47,6 +48,10 @@ function setupUploadCommand(program) {
47
48
  .option('--probe', 'After publish, run dataplane runtime checks (validation/run); slower')
48
49
  .option('--minimal', 'Print only a short readiness summary after upload')
49
50
  .option('--probe-timeout <ms>', 'Timeout for --probe (default: 120000)', '120000')
51
+ .option(
52
+ '--no-cert-sync',
53
+ 'Skip updating the system file certification block from the dataplane active certificate after publish'
54
+ )
50
55
  .action(async(systemKey, options) => {
51
56
  try {
52
57
  const upload = require('../commands/upload');
@@ -56,6 +61,7 @@ function setupUploadCommand(program) {
56
61
  : Number(options.probeTimeout);
57
62
  await upload.uploadExternalSystem(systemKey, {
58
63
  ...options,
64
+ noCertSync: cliOptsSkipCertSync(options),
59
65
  probeTimeout: Number.isFinite(probeTimeout) ? probeTimeout : 120000
60
66
  });
61
67
  } catch (error) {
@@ -132,7 +138,16 @@ async function runExternalSystemTestIntegration(appName, options) {
132
138
  };
133
139
  const results = await test.testExternalSystemIntegration(appName, opts);
134
140
  test.displayIntegrationTestResults(results, options.verbose, { debug: options.debug, runType: 'integration' });
135
- if (!results.success) process.exit(1);
141
+ const { computeSystemExitCodeFromDatasourceRows } = require('../utils/datasource-test-run-exit');
142
+ const rows = Array.isArray(results.datasourceResults) ? results.datasourceResults : [];
143
+ const exitCode = computeSystemExitCodeFromDatasourceRows(rows, {
144
+ warningsAsErrors: options.warningsAsErrors === true,
145
+ requireCert: options.requireCert === true
146
+ });
147
+ if (exitCode !== 0) process.exit(exitCode);
148
+ if (cliOptsSkipCertSync(options)) return;
149
+ const { trySyncCertificationFromDataplaneForExternalApp } = require('../certification/sync-after-external-command');
150
+ await trySyncCertificationFromDataplaneForExternalApp(appName, 'test-integration');
136
151
  }
137
152
 
138
153
  /**
@@ -180,6 +195,12 @@ function setupExternalSystemTestCommands(program) {
180
195
  '--sync',
181
196
  'Publish local system and datasource files to the dataplane before running tests (same as aifabrix upload <systemKey>)'
182
197
  )
198
+ .option('--warnings-as-errors', 'Treat aggregate warn as failure (exit 1)')
199
+ .option('--require-cert', 'Require certification passed on every datasource (exit 2 if not)')
200
+ .option(
201
+ '--no-cert-sync',
202
+ 'Skip updating the system file certification block from the dataplane after a successful run'
203
+ )
183
204
  .addHelpText('after', TEST_INTEGRATION_HELP_AFTER)
184
205
  .action(async(appName, options, cmd) => {
185
206
  try {
@@ -24,6 +24,10 @@ Subcommands:
24
24
 
25
25
  Also: aifabrix secure Encrypt secrets.local.yaml (ISO 27001)
26
26
 
27
+ Default "secret set" (no --shared): writes only to your user secrets.local.yaml next to
28
+ config.yaml (typically ~/.aifabrix/secrets.local.yaml). Use --shared to write the
29
+ shared store from config aifabrix-secrets (another file or https), not that user file.
30
+
27
31
  Examples:
28
32
  $ aifabrix secret list
29
33
  $ aifabrix secret set myapp/clientSecret "your-value"
@@ -41,6 +45,14 @@ Examples:
41
45
  `;
42
46
 
43
47
  const SECRET_SET_HELP_AFTER = `
48
+ Where the value is stored:
49
+ No --shared User file secrets.local.yaml in your aifabrix config directory (same
50
+ folder as config.yaml; often ~/.aifabrix/). Used for app/integration
51
+ secrets and kv:// resolution on your machine.
52
+ --shared The store set in config.yaml as aifabrix-secrets: a YAML file path or
53
+ an https secrets API (never the user-only file above unless you point
54
+ aifabrix-secrets at that path deliberately).
55
+
44
56
  Examples:
45
57
  $ aifabrix secret set myapp/clientSecret "your-secret"
46
58
  $ aifabrix secret set hubspot/apiKey "$HUBSPOT_KEY"
@@ -144,22 +156,13 @@ function setupSecureCommand(program) {
144
156
  });
145
157
  }
146
158
 
147
- /**
148
- * Sets up secrets and security commands
149
- * @param {Command} program - Commander program instance
150
- */
151
- function setupSecretsCommands(program) {
152
- const secretCmd = program
153
- .command('secret')
154
- .description('User and shared secrets (list, set, remove, remove-all, validate)')
155
- .addHelpText('after', SECRET_GROUP_HELP_AFTER);
156
-
159
+ function setupSecretListCommand(secretCmd) {
157
160
  secretCmd
158
161
  .command('list')
159
162
  .description('List secret keys (--shared for shared/remote)')
160
163
  .addHelpText('after', SECRET_LIST_HELP_AFTER)
161
164
  .option('--shared', 'List shared secrets (from config aifabrix-secrets or remote API)')
162
- .action(async(options) => {
165
+ .action(async options => {
163
166
  try {
164
167
  await config.ensureSecretsEncryptionKey();
165
168
  await handleSecretsList(options);
@@ -168,12 +171,30 @@ function setupSecretsCommands(program) {
168
171
  process.exit(1);
169
172
  }
170
173
  });
174
+ }
175
+
176
+ /**
177
+ * Sets up secrets and security commands
178
+ * @param {Command} program - Commander program instance
179
+ */
180
+ function setupSecretsCommands(program) {
181
+ const secretCmd = program
182
+ .command('secret')
183
+ .description('User and shared secrets (list, set, remove, remove-all, validate)')
184
+ .addHelpText('after', SECRET_GROUP_HELP_AFTER);
185
+
186
+ setupSecretListCommand(secretCmd);
171
187
 
172
188
  secretCmd
173
189
  .command('set <key> <value>')
174
- .description('Set a secret value')
190
+ .description(
191
+ 'Set a secret (default: user secrets.local.yaml beside config; --shared → aifabrix-secrets store)'
192
+ )
175
193
  .addHelpText('after', SECRET_SET_HELP_AFTER)
176
- .option('--shared', 'Save to general secrets file (from config.yaml aifabrix-secrets) instead of user secrets')
194
+ .option(
195
+ '--shared',
196
+ 'Write to shared secrets (config aifabrix-secrets: YAML path or https), not user secrets.local.yaml'
197
+ )
177
198
  .action(async(key, value, options) => {
178
199
  try {
179
200
  await config.ensureSecretsEncryptionKey();
@@ -25,6 +25,7 @@ Generates *-deploy.json (or application-schema.json) for commit before deploy.
25
25
  const VALIDATE_HELP_AFTER = `
26
26
  Examples:
27
27
  $ aifabrix validate myapp
28
+ $ aifabrix validate myapp --cert-sync
28
29
  $ aifabrix validate --integration
29
30
  $ aifabrix validate --builder
30
31
  `;
@@ -262,10 +263,18 @@ function setupShowCommand(program) {
262
263
  .description('Show app from local tree (default) or controller (--online)')
263
264
  .option('--online', 'Fetch application data from the controller')
264
265
  .option('--json', 'Output as JSON')
266
+ .option(
267
+ '--verify-cert',
268
+ 'For external integrations, verify trust state on the dataplane when logged in (with --online uses current session; offline attempts the same if a controller URL is configured)'
269
+ )
265
270
  .action(async(appKey, options) => {
266
271
  try {
267
272
  const { showApp } = require('../app/show');
268
- await showApp(appKey, { online: options.online, json: options.json });
273
+ await showApp(appKey, {
274
+ online: options.online,
275
+ json: options.json,
276
+ verifyCert: options.verifyCert === true
277
+ });
269
278
  } catch (error) {
270
279
  logger.error(chalk.red(`Error: ${error.message}`));
271
280
  process.exit(1);
@@ -307,7 +316,10 @@ async function runValidateCommand(appOrFile, options) {
307
316
  process.exit(1);
308
317
  }
309
318
 
310
- const result = await validate.validateAppOrFile(appOrFile, opts);
319
+ const result = await validate.validateAppOrFile(appOrFile, {
320
+ ...opts,
321
+ certSync: opts.certSync === true
322
+ });
311
323
  if (outFormat === 'json') {
312
324
  logger.log(JSON.stringify(result, null, 2));
313
325
  } else {
@@ -323,6 +335,10 @@ function setupValidateDiffCommands(program) {
323
335
  .option('--format <format>', 'Output format: json | default (human-readable)')
324
336
  .option('--integration', 'Validate all applications under integration/')
325
337
  .option('--builder', 'Validate all applications under builder/')
338
+ .option(
339
+ '--cert-sync',
340
+ 'After successful validation of an external integration, refresh the system file certification block from the dataplane (requires login)'
341
+ )
326
342
  .action((appOrFile, options) => {
327
343
  runValidateCommand(appOrFile, options).catch((error) => {
328
344
  handleCommandError(error, 'validate');
@@ -78,9 +78,18 @@ function setupAppShowCommand(app) {
78
78
  .option('--online', 'Fetch from controller (default for this command)')
79
79
  .option('--json', 'Output as JSON')
80
80
  .option('--permissions', 'Show only list of permissions')
81
+ .option(
82
+ '--verify-cert',
83
+ 'For external integrations, verify trust state on the dataplane (requires login)'
84
+ )
81
85
  .action(async(appKey, options) => {
82
86
  try {
83
- await showApp(appKey, { online: true, json: !!options.json, permissions: !!options.permissions });
87
+ await showApp(appKey, {
88
+ online: true,
89
+ json: !!options.json,
90
+ permissions: !!options.permissions,
91
+ verifyCert: options.verifyCert === true
92
+ });
84
93
  } catch (error) {
85
94
  logger.error(chalk.red(`Error: ${error.message}`));
86
95
  process.exit(1);
@@ -10,34 +10,31 @@ const logger = require('../utils/logger');
10
10
  const { runDatasourceTestIntegration } = require('../datasource/test-integration');
11
11
  const { runDatasourceTestE2E } = require('../datasource/test-e2e');
12
12
  const { runUnifiedDatasourceValidation } = require('../datasource/unified-validation-run');
13
- const { displayIntegrationTestResults, displayE2EResults } = require('../utils/external-system-display');
13
+ const { displayIntegrationTestResults } = require('../utils/external-system-display');
14
14
  const path = require('path');
15
15
  const { getIntegrationPath } = require('../utils/paths');
16
16
  const { writeTestLog } = require('../utils/test-log-writer');
17
17
  const { includeDebugForRequest } = require('../utils/validation-run-request');
18
18
  const {
19
- exitFromUnifiedValidationResult,
20
19
  finalizeUnifiedValidationResult,
21
20
  unifiedCliResultFromIntegrationReturn,
22
- exitAfterIntegrationDisplay,
23
- finalizeAfterIntegrationDisplay,
24
- emitCapabilityScopeDiagnostics
21
+ finalizeAfterIntegrationDisplay
25
22
  } = require('./datasource-validation-cli');
23
+ const { afterUnifiedValidationCertSync } = require('../certification/post-unified-cert-sync');
26
24
  const { resolveAppKeyForDatasource } = require('../datasource/resolve-app');
27
25
  const { runDatasourceValidationWatchLoop } = require('../utils/datasource-validation-watch');
28
- const { computeExitCodeFromDatasourceTestRun } = require('../utils/datasource-test-run-exit');
29
- const { analyzeCapabilityScope } = require('../utils/datasource-test-run-capability-scope');
30
26
  const {
31
- resolveDebugDisplayMode,
32
- formatDatasourceTestRunDebugBlock
33
- } = require('../utils/datasource-test-run-debug-display');
34
- const { formatCapabilityFocusSection } = require('../utils/datasource-test-run-display');
27
+ exitCodeFromDatasourceTestRunEnvelope,
28
+ finalizeDatasourceTestE2ELegacyPath,
29
+ displayDatasourceTestE2EEnvelopeResults
30
+ } = require('./datasource-unified-test-e2e-cli-helpers');
35
31
  const {
36
32
  datasourceTestHelpAfter,
37
33
  datasourceTestIntegrationHelpAfter,
38
34
  datasourceTestE2eHelpAfter,
39
35
  attachDatasourceWatchOptions,
40
- attachDatasourceTestCommonOptions
36
+ attachDatasourceTestCommonOptions,
37
+ attachDatasourceTestE2eExclusiveOptions
41
38
  } = require('./datasource-unified-test-cli.options');
42
39
 
43
40
  function integrationBaseDirForLogs(appKey) {
@@ -68,88 +65,6 @@ async function writeDatasourceTestDebugLogIfRequested(appKey, datasourceKey, res
68
65
  logger.log(chalk.gray(` Debug log: ${logPath}`));
69
66
  }
70
67
 
71
- function logDatasourceTestRunDebugAppendix(envelope, debugOpt) {
72
- const mode = resolveDebugDisplayMode(debugOpt);
73
- if (!mode || !envelope) return;
74
- const block = formatDatasourceTestRunDebugBlock(envelope, mode, process.stdout.isTTY);
75
- if (block) logger.log(block);
76
- }
77
-
78
- function logE2eCapabilityFocusFromEnvelope(env, capabilityOpt) {
79
- if (!env) return;
80
- const capKey =
81
- capabilityOpt !== undefined && capabilityOpt !== null
82
- ? String(capabilityOpt).trim()
83
- : '';
84
- if (!capKey) return;
85
- const sec = formatCapabilityFocusSection(env, capKey);
86
- if (sec) logger.log(sec);
87
- }
88
-
89
- /**
90
- * Exit code from envelope only (no stderr diagnostics; use when TTY output already ran emit via logEnvelope).
91
- * @param {Object|null|undefined} env
92
- * @param {Object} options
93
- * @returns {number|null} null if no envelope
94
- */
95
- function exitCodeFromDatasourceTestRunEnvelope(env, options) {
96
- if (!env || typeof env !== 'object') return null;
97
- let code = computeExitCodeFromDatasourceTestRun(env, {
98
- warningsAsErrors: false,
99
- requireCert: false
100
- });
101
- const scope = analyzeCapabilityScope(env, options.capability);
102
- if (options.strictCapabilityScope === true && scope.violated) {
103
- code = Math.max(code, 1);
104
- }
105
- return code;
106
- }
107
-
108
- /**
109
- * Legacy E2E display + exit code (no process.exit; watch mode).
110
- * @param {Object} data
111
- * @param {Object} options
112
- * @returns {number}
113
- */
114
- function finalizeDatasourceTestE2ELegacyPath(data, options) {
115
- displayE2EResults(data, options.verbose);
116
- logDatasourceTestRunDebugAppendix(data.datasourceTestRun, options.debug);
117
- logE2eCapabilityFocusFromEnvelope(data.datasourceTestRun, options.capability);
118
- const env = data.datasourceTestRun;
119
- if (env) {
120
- emitCapabilityScopeDiagnostics(env, { requestedCapabilityKey: options.capability });
121
- const code = exitCodeFromDatasourceTestRunEnvelope(env, options);
122
- return code === null ? 1 : code;
123
- }
124
- const steps = data.steps || data.completedActions || [];
125
- const failed = data.success === false || steps.some(s => s.success === false || s.error);
126
- return failed ? 1 : 0;
127
- }
128
-
129
- /**
130
- * Human TTY for single-datasource E2E when DatasourceTestRun envelope is present.
131
- * @param {string} datasourceKey
132
- * @param {Object} env
133
- * @param {Object} options
134
- */
135
- function displayDatasourceTestE2EEnvelopeResults(datasourceKey, env, options) {
136
- const success = env.status !== 'fail';
137
- displayIntegrationTestResults(
138
- {
139
- systemKey: env.systemKey || 'unknown',
140
- success,
141
- datasourceResults: [{ key: datasourceKey, success, datasourceTestRun: env }]
142
- },
143
- options.verbose,
144
- {
145
- debug: options.debug,
146
- runType: 'e2e',
147
- requestedCapabilityKey: options.capability
148
- }
149
- );
150
- logE2eCapabilityFocusFromEnvelope(env, options.capability);
151
- }
152
-
153
68
  function buildDatasourceTestRunOpts(options) {
154
69
  return {
155
70
  app: options.app,
@@ -175,11 +90,13 @@ function buildDatasourceTestDisplayOpts(options) {
175
90
  };
176
91
  }
177
92
 
178
- async function runDatasourceUnifiedTestOnceForWatch(datasourceKey, runOpts, displayOpts) {
93
+ async function runDatasourceUnifiedTestOnceForWatch(datasourceKey, runOpts, displayOpts, certCliOptions = {}) {
179
94
  try {
180
95
  const result = await runUnifiedDatasourceValidation(datasourceKey, runOpts);
96
+ const exitCode = finalizeUnifiedValidationResult(result, displayOpts);
97
+ await afterUnifiedValidationCertSync(exitCode, datasourceKey, certCliOptions, 'datasource test');
181
98
  return {
182
- exitCode: finalizeUnifiedValidationResult(result, displayOpts),
99
+ exitCode,
183
100
  envelope: result.envelope
184
101
  };
185
102
  } catch (err) {
@@ -203,8 +120,10 @@ async function datasourceTestCommandAction(datasourceKey, options) {
203
120
  runOnce: async() => {
204
121
  const result = await runUnifiedDatasourceValidation(datasourceKey, runOpts);
205
122
  await writeDatasourceTestDebugLogIfRequested(appKey, datasourceKey, result, options);
123
+ const exitCode = finalizeUnifiedValidationResult(result, displayOpts);
124
+ await afterUnifiedValidationCertSync(exitCode, datasourceKey, options, 'datasource test (watch)');
206
125
  return {
207
- exitCode: finalizeUnifiedValidationResult(result, displayOpts),
126
+ exitCode,
208
127
  envelope: result.envelope
209
128
  };
210
129
  }
@@ -220,7 +139,9 @@ async function datasourceTestCommandAction(datasourceKey, options) {
220
139
  logger.warn(chalk.yellow(`⚠ Could not write debug log: ${e.message}`));
221
140
  }
222
141
  }
223
- exitFromUnifiedValidationResult(result, displayOpts);
142
+ const exitCode = finalizeUnifiedValidationResult(result, displayOpts);
143
+ await afterUnifiedValidationCertSync(exitCode, datasourceKey, options, 'datasource test');
144
+ process.exit(exitCode);
224
145
  } catch (error) {
225
146
  logger.error(formatBlockingError('Datasource test failed:'), error.message);
226
147
  process.exit(4);
@@ -274,6 +195,7 @@ async function runIntegrationOnceForWatch(datasourceKey, integOpts, options, uni
274
195
  if (unifiedModes) {
275
196
  const uni = unifiedCliResultFromIntegrationReturn(result);
276
197
  const exitCode = finalizeUnifiedValidationResult(uni, unifiedDisplayOpts);
198
+ await afterUnifiedValidationCertSync(exitCode, datasourceKey, options, 'datasource test-integration (watch)');
277
199
  return { exitCode, envelope: uni.envelope };
278
200
  }
279
201
  displayIntegrationTestResults(
@@ -285,7 +207,11 @@ async function runIntegrationOnceForWatch(datasourceKey, integOpts, options, uni
285
207
  options.verbose,
286
208
  { debug: options.debug, runType: 'integration' }
287
209
  );
288
- const exitCode = finalizeAfterIntegrationDisplay(result, {});
210
+ const exitCode = finalizeAfterIntegrationDisplay(result, {
211
+ warningsAsErrors: unifiedDisplayOpts.warningsAsErrors === true,
212
+ requireCert: unifiedDisplayOpts.requireCert === true
213
+ });
214
+ await afterUnifiedValidationCertSync(exitCode, datasourceKey, options, 'datasource test-integration (watch)');
289
215
  return { exitCode, envelope: result.datasourceTestRun || null };
290
216
  } catch (err) {
291
217
  logger.error(formatBlockingError('Integration test failed:'), err.message);
@@ -313,7 +239,12 @@ async function integrationTestCommandAction(datasourceKey, options) {
313
239
  const unifiedModes =
314
240
  options.json || options.summary || options.warningsAsErrors || options.requireCert;
315
241
  if (unifiedModes) {
316
- exitFromUnifiedValidationResult(unifiedCliResultFromIntegrationReturn(result), unifiedDisplayOpts);
242
+ const exitCode = finalizeUnifiedValidationResult(
243
+ unifiedCliResultFromIntegrationReturn(result),
244
+ unifiedDisplayOpts
245
+ );
246
+ await afterUnifiedValidationCertSync(exitCode, datasourceKey, options, 'datasource test-integration');
247
+ process.exit(exitCode);
317
248
  return;
318
249
  }
319
250
  displayIntegrationTestResults(
@@ -325,7 +256,12 @@ async function integrationTestCommandAction(datasourceKey, options) {
325
256
  options.verbose,
326
257
  { debug: options.debug, runType: 'integration' }
327
258
  );
328
- exitAfterIntegrationDisplay(result, {});
259
+ const integExit = finalizeAfterIntegrationDisplay(result, {
260
+ warningsAsErrors: unifiedDisplayOpts.warningsAsErrors === true,
261
+ requireCert: unifiedDisplayOpts.requireCert === true
262
+ });
263
+ await afterUnifiedValidationCertSync(integExit, datasourceKey, options, 'datasource test-integration');
264
+ process.exit(integExit);
329
265
  } catch (error) {
330
266
  logger.error(formatBlockingError('Integration test failed:'), error.message);
331
267
  process.exit(4);
@@ -364,6 +300,9 @@ async function runDatasourceTestE2ECliOnce(datasourceKey, options) {
364
300
  recordId: options.recordId,
365
301
  cleanup: options.cleanup,
366
302
  primaryKeyValue: options.primaryKeyValue,
303
+ minVectorHits: options.minVectorHits,
304
+ minProcessed: options.minProcessed,
305
+ minRecordCount: options.minRecordCount,
367
306
  timeout: options.timeout,
368
307
  capabilityKey: options.capability,
369
308
  sync: options.sync === true
@@ -402,6 +341,7 @@ async function runDatasourceTestE2ECliOnce(datasourceKey, options) {
402
341
 
403
342
  async function runDatasourceTestE2ECliAction(datasourceKey, options) {
404
343
  const { exitCode } = await runDatasourceTestE2ECliOnce(datasourceKey, options);
344
+ await afterUnifiedValidationCertSync(exitCode, datasourceKey, options, 'datasource test-e2e');
405
345
  process.exit(exitCode);
406
346
  }
407
347
 
@@ -430,7 +370,9 @@ async function e2eTestCommandAction(datasourceKey, capabilityKey, options) {
430
370
  watchFullDiff: mergedOptions.watchFullDiff === true,
431
371
  runOnce: async() => {
432
372
  try {
433
- return await runDatasourceTestE2ECliOnce(datasourceKey, mergedOptions);
373
+ const r = await runDatasourceTestE2ECliOnce(datasourceKey, mergedOptions);
374
+ await afterUnifiedValidationCertSync(r.exitCode, datasourceKey, mergedOptions, 'datasource test-e2e (watch)');
375
+ return r;
434
376
  } catch (err) {
435
377
  logger.error(formatBlockingError('E2E test failed:'), err.message);
436
378
  return { exitCode: 3, envelope: null };
@@ -456,23 +398,14 @@ function chainDatasourceTestE2ECommand(datasource) {
456
398
  appHelp: 'Integration folder name (default: resolve from cwd if inside integration/<systemKey>/)',
457
399
  verboseHelp: 'Audit / explain-oriented request flags where applicable',
458
400
  debugHelp: 'includeDebug + log under integration/<systemKey>/logs/; TTY appendix: summary, full, or raw',
459
- timeoutHelp: 'Aggregate timeout for POST + polls (default 15m)',
401
+ timeoutHelp:
402
+ 'Wall-clock budget for validation (ms); also raises per-request HTTP wait for slow E2E POST/polls (default 15m)',
460
403
  timeoutDefault: String(15 * 60 * 1000)
461
404
  });
462
- return cmd
463
- .option('--test-crud', 'Enable CRUD lifecycle test (e2eOptions.testCrud)')
464
- .option('--record-id <id>', 'Record ID for test (e2eOptions.recordId)')
465
- .option('--no-cleanup', 'Disable cleanup after test (e2eOptions.cleanup: false)')
466
- .option(
467
- '--primary-key-value <value|@path>',
468
- 'Primary key value or path to JSON file (e.g. @pk.json) for e2eOptions.primaryKeyValue'
469
- )
470
- .option('--capability <key>', 'Capability drill-down (deprecated; use positional [capabilityKey])')
471
- .option(
472
- '--strict-capability-scope',
473
- 'Exit 1 if a capability drill-down is requested but the report lists more than one capabilities[] row (plan §2.3)'
474
- )
475
- .addHelpText('after', datasourceTestE2eHelpAfter());
405
+ return attachDatasourceTestE2eExclusiveOptions(cmd).addHelpText(
406
+ 'after',
407
+ datasourceTestE2eHelpAfter()
408
+ );
476
409
  }
477
410
 
478
411
  function setupDatasourceTestE2ECommand(datasource) {
@@ -42,6 +42,7 @@ function datasourceTestE2eHelpAfter() {
42
42
  Examples:
43
43
  $ aifabrix datasource test-e2e hubspot-users
44
44
  $ aifabrix datasource test-e2e hubspot-users --app test-e2e-hubspot -v --debug
45
+ $ aifabrix datasource test-e2e my-documents-ds --min-vector-hits 7 -v --debug
45
46
  $ aifabrix datasource test-e2e hubspot-users -a test-e2e-hubspot --no-async
46
47
  $ aifabrix datasource test-e2e hubspot-users read
47
48
 
@@ -129,7 +130,11 @@ function attachDatasourceTestCommonOptions(cmd, opts) {
129
130
  .option('--json', 'Print raw DatasourceTestRun JSON to stdout')
130
131
  .option('--summary', 'Print compact summary line')
131
132
  .option('--warnings-as-errors', 'Exit 1 when root status is warn')
132
- .option('--require-cert', 'Exit 2 when certificate missing or not_passed');
133
+ .option('--require-cert', 'Exit 2 when certificate missing or not_passed')
134
+ .option(
135
+ '--no-cert-sync',
136
+ 'Skip updating system file certification from the dataplane after a successful run'
137
+ );
133
138
 
134
139
  if (includeNoAsync) {
135
140
  cmd.option('--no-async', 'Do not poll; fail if report is not complete in first response');
@@ -139,11 +144,48 @@ function attachDatasourceTestCommonOptions(cmd, opts) {
139
144
  return cmd;
140
145
  }
141
146
 
147
+ /**
148
+ * E2E-only Commander flags (kept out of datasource-unified-test-cli.js for file size limits).
149
+ * @param {object} cmd
150
+ * @returns {object}
151
+ */
152
+ function attachDatasourceTestE2eExclusiveOptions(cmd) {
153
+ return cmd
154
+ .option(
155
+ '--min-vector-hits <n>',
156
+ 'Assert at least N vector search hits after sync (e2eOptions.minVectorHits)',
157
+ (v) => parseInt(String(v), 10)
158
+ )
159
+ .option(
160
+ '--min-processed <n>',
161
+ 'Minimum records processed in sync step (e2eOptions.minProcessed)',
162
+ (v) => parseInt(String(v), 10)
163
+ )
164
+ .option(
165
+ '--min-record-count <n>',
166
+ 'Minimum record count assertion (e2eOptions.minRecordCount)',
167
+ (v) => parseInt(String(v), 10)
168
+ )
169
+ .option('--test-crud', 'Enable CRUD lifecycle test (e2eOptions.testCrud)')
170
+ .option('--record-id <id>', 'Record ID for test (e2eOptions.recordId)')
171
+ .option('--no-cleanup', 'Disable cleanup after test (e2eOptions.cleanup: false)')
172
+ .option(
173
+ '--primary-key-value <value|@path>',
174
+ 'Primary key value or path to JSON file (e.g. @pk.json) for e2eOptions.primaryKeyValue'
175
+ )
176
+ .option('--capability <key>', 'Capability drill-down (deprecated; use positional [capabilityKey])')
177
+ .option(
178
+ '--strict-capability-scope',
179
+ 'Exit 1 if a capability drill-down is requested but the report lists more than one capabilities[] row (plan §2.3)'
180
+ );
181
+ }
182
+
142
183
  module.exports = {
143
184
  datasourceTestHelpAfter,
144
185
  datasourceTestIntegrationHelpAfter,
145
186
  datasourceTestE2eHelpAfter,
146
187
  attachDatasourceWatchOptions,
147
- attachDatasourceTestCommonOptions
188
+ attachDatasourceTestCommonOptions,
189
+ attachDatasourceTestE2eExclusiveOptions
148
190
  };
149
191