@aifabrix/builder 2.44.0 → 2.44.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/.cursor/rules/cli-layout.mdc +75 -0
  2. package/.cursor/rules/project-rules.mdc +8 -0
  3. package/.npmrc.token +1 -0
  4. package/.nyc_output/55e9d034-ddab-4579-a706-e02a91d75c91.json +1 -0
  5. package/.nyc_output/processinfo/55e9d034-ddab-4579-a706-e02a91d75c91.json +1 -0
  6. package/.nyc_output/processinfo/index.json +1 -0
  7. package/jest.projects.js +15 -2
  8. package/lib/api/certificates.api.js +62 -0
  9. package/lib/api/index.js +11 -2
  10. package/lib/api/types/certificates.types.js +48 -0
  11. package/lib/api/validation-run.api.js +16 -4
  12. package/lib/api/validation-runner.js +13 -3
  13. package/lib/app/certification-show-enrich.js +129 -0
  14. package/lib/app/certification-verify-rows.js +60 -0
  15. package/lib/app/show-display.js +43 -0
  16. package/lib/app/show.js +92 -8
  17. package/lib/certification/cli-cert-sync-skip.js +21 -0
  18. package/lib/certification/merge-certification-from-artifact.js +185 -0
  19. package/lib/certification/post-unified-cert-sync.js +33 -0
  20. package/lib/certification/sync-after-external-command.js +52 -0
  21. package/lib/certification/sync-system-certification.js +197 -0
  22. package/lib/cli/setup-app.js +4 -0
  23. package/lib/cli/setup-app.test-commands.js +24 -8
  24. package/lib/cli/setup-external-system.js +22 -1
  25. package/lib/cli/setup-secrets.js +34 -13
  26. package/lib/cli/setup-utility.js +18 -2
  27. package/lib/commands/app.js +10 -1
  28. package/lib/commands/datasource-unified-test-cli.js +50 -117
  29. package/lib/commands/datasource-unified-test-cli.options.js +44 -2
  30. package/lib/commands/datasource-unified-test-e2e-cli-helpers.js +106 -0
  31. package/lib/commands/datasource-validation-cli.js +15 -1
  32. package/lib/commands/datasource.js +25 -2
  33. package/lib/commands/upload.js +17 -6
  34. package/lib/core/secrets.js +47 -13
  35. package/lib/datasource/log-viewer.js +135 -20
  36. package/lib/datasource/test-e2e.js +35 -17
  37. package/lib/datasource/unified-validation-run-body.js +3 -0
  38. package/lib/datasource/unified-validation-run.js +2 -1
  39. package/lib/external-system/deploy.js +53 -18
  40. package/lib/infrastructure/compose.js +12 -3
  41. package/lib/infrastructure/helpers-docker-check.js +67 -0
  42. package/lib/infrastructure/helpers.js +47 -58
  43. package/lib/infrastructure/index.js +3 -1
  44. package/lib/infrastructure/services.js +4 -56
  45. package/lib/schema/external-system.schema.json +25 -3
  46. package/lib/schema/type/document-storage.json +15 -2
  47. package/lib/utils/api.js +28 -3
  48. package/lib/utils/app-service-env-from-builder.js +47 -6
  49. package/lib/utils/config-paths.js +4 -0
  50. package/lib/utils/configuration-env-resolver.js +11 -8
  51. package/lib/utils/credential-secrets-env.js +5 -5
  52. package/lib/utils/datasource-test-run-certificate-tty.js +82 -0
  53. package/lib/utils/datasource-test-run-display.js +19 -2
  54. package/lib/utils/datasource-test-run-exit.js +25 -0
  55. package/lib/utils/external-system-display.js +8 -0
  56. package/lib/utils/external-system-system-test-tty-overview.js +120 -0
  57. package/lib/utils/external-system-system-test-tty.js +417 -0
  58. package/lib/utils/paths.js +14 -0
  59. package/lib/utils/url-declarative-resolve-load-doc.js +50 -20
  60. package/lib/utils/urls-local-registry.js +36 -12
  61. package/lib/utils/validation-run-poll.js +28 -5
  62. package/lib/utils/validation-run-post-retry.js +20 -8
  63. package/lib/utils/validation-run-request.js +18 -0
  64. package/lib/validation/validate-external-cert-sync.js +23 -0
  65. package/lib/validation/validate.js +4 -1
  66. package/package.json +4 -3
  67. package/scripts/install-local.js +4 -1
  68. package/scripts/pnpm-global-remove.js +48 -0
  69. package/templates/applications/dataplane/env.template +4 -0
  70. package/templates/infra/compose.yaml.hbs +15 -14
  71. package/templates/infra/servers.json.hbs +3 -1
@@ -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
  /**
@@ -224,6 +224,52 @@ async function loadMergedConfigAndUserSecrets() {
224
224
  }
225
225
  }
226
226
 
227
+ /**
228
+ * @returns {string[]}
229
+ */
230
+ function collectBuilderSecretsYamlPaths() {
231
+ const projectRoot = pathsUtil.getProjectRoot();
232
+ const candidates = [];
233
+ if (projectRoot) {
234
+ candidates.push(path.join(projectRoot, 'builder', 'secrets.local.yaml'));
235
+ }
236
+ try {
237
+ const alt = path.join(pathsUtil.getBuilderRoot(), 'secrets.local.yaml');
238
+ if (!candidates.length || path.resolve(candidates[0]) !== path.resolve(alt)) {
239
+ candidates.push(alt);
240
+ }
241
+ } catch {
242
+ /* ignore */
243
+ }
244
+ return candidates;
245
+ }
246
+
247
+ /**
248
+ * Merge `builder/secrets.local.yaml` from project root and from {@link pathsUtil.getBuilderRoot} when distinct.
249
+ * @param {Object|null|undefined} merged
250
+ * @returns {Object|null|undefined}
251
+ */
252
+ function mergeBuilderSecretsLocalFiles(merged) {
253
+ try {
254
+ const seen = new Set();
255
+ let out = merged;
256
+ for (const builderPath of collectBuilderSecretsYamlPaths()) {
257
+ if (!builderPath || seen.has(path.resolve(builderPath))) {
258
+ continue;
259
+ }
260
+ seen.add(path.resolve(builderPath));
261
+ if (fs.existsSync(builderPath)) {
262
+ ensureSecureFilePermissions(builderPath);
263
+ const builderSecrets = mergeUserWithConfigFile(out || {}, builderPath);
264
+ if (builderSecrets) out = builderSecrets;
265
+ }
266
+ }
267
+ return out;
268
+ } catch {
269
+ return merged;
270
+ }
271
+ }
272
+
227
273
  /**
228
274
  * Loads merged secrets using config/user cascade, builder file merge, and default fallback.
229
275
  * @async
@@ -238,19 +284,7 @@ async function loadSecretsWithFallbacks() {
238
284
  }
239
285
  merged = await applyCanonicalSecretsOverride(merged);
240
286
  }
241
- try {
242
- const projectRoot = pathsUtil.getProjectRoot();
243
- if (projectRoot) {
244
- const builderPath = path.join(projectRoot, 'builder', 'secrets.local.yaml');
245
- if (fs.existsSync(builderPath)) {
246
- ensureSecureFilePermissions(builderPath);
247
- const builderSecrets = mergeUserWithConfigFile(merged || {}, builderPath);
248
- if (builderSecrets) merged = builderSecrets;
249
- }
250
- }
251
- } catch {
252
- // Ignore (e.g. no project root or read error)
253
- }
287
+ merged = mergeBuilderSecretsLocalFiles(merged);
254
288
  if (Object.keys(merged).length === 0) {
255
289
  merged = loadDefaultSecrets();
256
290
  }
@@ -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 {
@@ -199,6 +278,34 @@ function formatLogContent(parsed, logType, fileName) {
199
278
  }
200
279
  }
201
280
 
281
+ /**
282
+ * Parse saved debug log JSON (strict). Always throws an Error whose message starts with
283
+ * "Invalid JSON" so CLI/tests get a stable contract.
284
+ * @param {string} content
285
+ * @param {string} logPath
286
+ * @returns {Object}
287
+ */
288
+ function parseDatasourceLogJson(content, logPath) {
289
+ if (content === undefined || content === null) {
290
+ throw new Error(`Invalid JSON in ${logPath}: empty content`);
291
+ }
292
+ const text = String(content).replace(/^\uFEFF/, '').trim();
293
+ if (!text) {
294
+ throw new Error(`Invalid JSON in ${logPath}: empty file`);
295
+ }
296
+ let parsed;
297
+ try {
298
+ parsed = JSON.parse(text);
299
+ } catch (err) {
300
+ const detail = err instanceof Error ? err.message : String(err);
301
+ throw new Error(`Invalid JSON in ${logPath}: ${detail}`);
302
+ }
303
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
304
+ throw new Error(`Invalid JSON in ${logPath}: expected a JSON object at the root`);
305
+ }
306
+ return parsed;
307
+ }
308
+
202
309
  /**
203
310
  * Run log viewer: resolve log file, read, parse, format and print
204
311
  * @async
@@ -206,7 +313,7 @@ function formatLogContent(parsed, logType, fileName) {
206
313
  * @param {Object} options - Options
207
314
  * @param {string} [options.app] - App key (optional, resolved from key if omitted)
208
315
  * @param {string} [options.file] - Path to log file (overrides app resolution)
209
- * @param {'test-e2e'|'test-integration'} options.logType - Log type
316
+ * @param {'test'|'test-e2e'|'test-integration'} options.logType - Log type
210
317
  * @throws {Error} When file not found or invalid JSON
211
318
  */
212
319
  /* eslint-disable-next-line max-statements -- Resolve path, read, parse, format */
@@ -224,29 +331,37 @@ async function runLogViewer(datasourceKey, options) {
224
331
  const { appKey } = await resolveAppKeyForDatasource(datasourceKey.trim(), app);
225
332
  const appPath = getIntegrationPath(appKey);
226
333
  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
- );
334
+ if (logType === 'test') {
335
+ logPath = await getLatestStructuralTestLogPath(logsDir);
336
+ if (!logPath) {
337
+ throw new Error(
338
+ `No structural validation log found in ${logsDir}. Run: aifabrix datasource test <key> --debug`
339
+ );
340
+ }
341
+ } else {
342
+ const pattern = logType === 'test-e2e' ? 'test-e2e-' : 'test-integration-';
343
+ logPath = await getLatestLogPath(logsDir, pattern);
344
+ if (!logPath) {
345
+ throw new Error(
346
+ `No ${logType} log found in ${logsDir}. Run the test with --debug first.`
347
+ );
348
+ }
233
349
  }
234
350
  fileName = path.basename(logPath);
235
351
  }
236
- const content = await fs.readFile(logPath, 'utf8');
237
- let parsed;
238
- try {
239
- parsed = JSON.parse(content);
240
- } catch (err) {
241
- throw new Error(`Invalid JSON in ${logPath}: ${err.message}`);
242
- }
352
+ const content = await fsp.readFile(logPath, 'utf8');
353
+ const parsed = parseDatasourceLogJson(content, logPath);
243
354
  formatLogContent(parsed, logType, fileName);
244
355
  }
245
356
 
246
357
  module.exports = {
247
358
  getLatestLogPath,
359
+ getLatestStructuralTestLogPath,
360
+ isStructuralTestLogFileName,
248
361
  formatLogContent,
249
362
  formatE2ELog,
250
363
  formatIntegrationLog,
364
+ formatStructuralTestLog,
365
+ parseDatasourceLogJson,
251
366
  runLogViewer
252
367
  };
@@ -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