@aifabrix/builder 2.44.0 → 2.44.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/rules/cli-layout.mdc +75 -0
- package/.cursor/rules/project-rules.mdc +8 -0
- package/.npmrc.token +1 -0
- package/.nyc_output/55e9d034-ddab-4579-a706-e02a91d75c91.json +1 -0
- package/.nyc_output/processinfo/55e9d034-ddab-4579-a706-e02a91d75c91.json +1 -0
- package/.nyc_output/processinfo/index.json +1 -0
- package/jest.projects.js +15 -2
- package/lib/api/certificates.api.js +62 -0
- package/lib/api/index.js +11 -2
- package/lib/api/types/certificates.types.js +48 -0
- package/lib/api/validation-run.api.js +16 -4
- package/lib/api/validation-runner.js +13 -3
- package/lib/app/certification-show-enrich.js +129 -0
- package/lib/app/certification-verify-rows.js +60 -0
- package/lib/app/show-display.js +43 -0
- package/lib/app/show.js +92 -8
- package/lib/certification/cli-cert-sync-skip.js +21 -0
- package/lib/certification/merge-certification-from-artifact.js +185 -0
- package/lib/certification/post-unified-cert-sync.js +33 -0
- package/lib/certification/sync-after-external-command.js +52 -0
- package/lib/certification/sync-system-certification.js +197 -0
- package/lib/cli/setup-app.js +4 -0
- package/lib/cli/setup-app.test-commands.js +24 -8
- package/lib/cli/setup-external-system.js +22 -1
- package/lib/cli/setup-secrets.js +34 -13
- package/lib/cli/setup-utility.js +18 -2
- package/lib/commands/app.js +10 -1
- package/lib/commands/datasource-unified-test-cli.js +50 -117
- package/lib/commands/datasource-unified-test-cli.options.js +44 -2
- package/lib/commands/datasource-unified-test-e2e-cli-helpers.js +106 -0
- package/lib/commands/datasource-validation-cli.js +15 -1
- package/lib/commands/datasource.js +25 -2
- package/lib/commands/upload.js +17 -6
- package/lib/core/secrets.js +47 -13
- package/lib/datasource/log-viewer.js +135 -20
- package/lib/datasource/test-e2e.js +35 -17
- package/lib/datasource/unified-validation-run-body.js +3 -0
- package/lib/datasource/unified-validation-run.js +2 -1
- package/lib/external-system/deploy.js +53 -18
- package/lib/infrastructure/compose.js +12 -3
- package/lib/infrastructure/helpers-docker-check.js +67 -0
- package/lib/infrastructure/helpers.js +47 -58
- package/lib/infrastructure/index.js +3 -1
- package/lib/infrastructure/services.js +4 -56
- package/lib/schema/external-system.schema.json +25 -3
- package/lib/schema/type/document-storage.json +15 -2
- package/lib/utils/api.js +28 -3
- package/lib/utils/app-service-env-from-builder.js +47 -6
- package/lib/utils/config-paths.js +4 -0
- package/lib/utils/configuration-env-resolver.js +11 -8
- package/lib/utils/credential-secrets-env.js +5 -5
- package/lib/utils/datasource-test-run-certificate-tty.js +82 -0
- package/lib/utils/datasource-test-run-display.js +19 -2
- package/lib/utils/datasource-test-run-exit.js +25 -0
- package/lib/utils/external-system-display.js +8 -0
- package/lib/utils/external-system-system-test-tty-overview.js +120 -0
- package/lib/utils/external-system-system-test-tty.js +417 -0
- package/lib/utils/paths.js +14 -0
- package/lib/utils/url-declarative-resolve-load-doc.js +50 -20
- package/lib/utils/urls-local-registry.js +36 -12
- package/lib/utils/validation-run-poll.js +28 -5
- package/lib/utils/validation-run-post-retry.js +20 -8
- package/lib/utils/validation-run-request.js +18 -0
- package/lib/validation/validate-external-cert-sync.js +23 -0
- package/lib/validation/validate.js +4 -1
- package/package.json +4 -3
- package/scripts/install-local.js +4 -1
- package/scripts/pnpm-global-remove.js +48 -0
- package/templates/applications/dataplane/env.template +4 -0
- package/templates/infra/compose.yaml.hbs +15 -14
- package/templates/infra/servers.json.hbs +3 -1
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview E2E / envelope exit helpers for datasource unified test CLI (keeps main CLI file under max-lines).
|
|
3
|
+
* @author AI Fabrix Team
|
|
4
|
+
* @version 2.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const logger = require('../utils/logger');
|
|
10
|
+
const { displayIntegrationTestResults, displayE2EResults } = require('../utils/external-system-display');
|
|
11
|
+
const { computeExitCodeFromDatasourceTestRun } = require('../utils/datasource-test-run-exit');
|
|
12
|
+
const { analyzeCapabilityScope } = require('../utils/datasource-test-run-capability-scope');
|
|
13
|
+
const {
|
|
14
|
+
resolveDebugDisplayMode,
|
|
15
|
+
formatDatasourceTestRunDebugBlock
|
|
16
|
+
} = require('../utils/datasource-test-run-debug-display');
|
|
17
|
+
const { formatCapabilityFocusSection } = require('../utils/datasource-test-run-display');
|
|
18
|
+
const { emitCapabilityScopeDiagnostics } = require('./datasource-validation-cli');
|
|
19
|
+
|
|
20
|
+
function logDatasourceTestRunDebugAppendix(envelope, debugOpt) {
|
|
21
|
+
const mode = resolveDebugDisplayMode(debugOpt);
|
|
22
|
+
if (!mode || !envelope) return;
|
|
23
|
+
const block = formatDatasourceTestRunDebugBlock(envelope, mode, process.stdout.isTTY);
|
|
24
|
+
if (block) logger.log(block);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function logE2eCapabilityFocusFromEnvelope(env, capabilityOpt) {
|
|
28
|
+
if (!env) return;
|
|
29
|
+
const capKey =
|
|
30
|
+
capabilityOpt !== undefined && capabilityOpt !== null
|
|
31
|
+
? String(capabilityOpt).trim()
|
|
32
|
+
: '';
|
|
33
|
+
if (!capKey) return;
|
|
34
|
+
const sec = formatCapabilityFocusSection(env, capKey);
|
|
35
|
+
if (sec) logger.log(sec);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {Object|null|undefined} env
|
|
40
|
+
* @param {Object} options
|
|
41
|
+
* @returns {number|null} null if no envelope
|
|
42
|
+
*/
|
|
43
|
+
function exitCodeFromDatasourceTestRunEnvelope(env, options) {
|
|
44
|
+
if (!env || typeof env !== 'object') return null;
|
|
45
|
+
let code = computeExitCodeFromDatasourceTestRun(env, {
|
|
46
|
+
warningsAsErrors: options.warningsAsErrors === true,
|
|
47
|
+
requireCert: options.requireCert === true
|
|
48
|
+
});
|
|
49
|
+
const scope = analyzeCapabilityScope(env, options.capability);
|
|
50
|
+
if (options.strictCapabilityScope === true && scope.violated) {
|
|
51
|
+
code = Math.max(code, 1);
|
|
52
|
+
}
|
|
53
|
+
return code;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Legacy E2E display + exit code (no process.exit; watch mode).
|
|
58
|
+
* @param {Object} data
|
|
59
|
+
* @param {Object} options
|
|
60
|
+
* @returns {number}
|
|
61
|
+
*/
|
|
62
|
+
function finalizeDatasourceTestE2ELegacyPath(data, options) {
|
|
63
|
+
displayE2EResults(data, options.verbose);
|
|
64
|
+
logDatasourceTestRunDebugAppendix(data.datasourceTestRun, options.debug);
|
|
65
|
+
logE2eCapabilityFocusFromEnvelope(data.datasourceTestRun, options.capability);
|
|
66
|
+
const env = data.datasourceTestRun;
|
|
67
|
+
if (env) {
|
|
68
|
+
emitCapabilityScopeDiagnostics(env, { requestedCapabilityKey: options.capability });
|
|
69
|
+
const code = exitCodeFromDatasourceTestRunEnvelope(env, options);
|
|
70
|
+
return code === null ? 1 : code;
|
|
71
|
+
}
|
|
72
|
+
const steps = data.steps || data.completedActions || [];
|
|
73
|
+
const failed = data.success === false || steps.some(s => s.success === false || s.error);
|
|
74
|
+
return failed ? 1 : 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {string} datasourceKey
|
|
79
|
+
* @param {Object} env
|
|
80
|
+
* @param {Object} options
|
|
81
|
+
*/
|
|
82
|
+
function displayDatasourceTestE2EEnvelopeResults(datasourceKey, env, options) {
|
|
83
|
+
const success = env.status !== 'fail';
|
|
84
|
+
displayIntegrationTestResults(
|
|
85
|
+
{
|
|
86
|
+
systemKey: env.systemKey || 'unknown',
|
|
87
|
+
success,
|
|
88
|
+
datasourceResults: [{ key: datasourceKey, success, datasourceTestRun: env }]
|
|
89
|
+
},
|
|
90
|
+
options.verbose,
|
|
91
|
+
{
|
|
92
|
+
debug: options.debug,
|
|
93
|
+
runType: 'e2e',
|
|
94
|
+
requestedCapabilityKey: options.capability
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
logE2eCapabilityFocusFromEnvelope(env, options.capability);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = {
|
|
101
|
+
logDatasourceTestRunDebugAppendix,
|
|
102
|
+
logE2eCapabilityFocusFromEnvelope,
|
|
103
|
+
exitCodeFromDatasourceTestRunEnvelope,
|
|
104
|
+
finalizeDatasourceTestE2ELegacyPath,
|
|
105
|
+
displayDatasourceTestE2EEnvelopeResults
|
|
106
|
+
};
|
|
@@ -103,10 +103,24 @@ function finalizeAfterIntegrationDisplay(integrationResult, exitOpts = {}) {
|
|
|
103
103
|
if (!env) {
|
|
104
104
|
return integrationResult.success ? 0 : 1;
|
|
105
105
|
}
|
|
106
|
-
|
|
106
|
+
let exitCode = computeExitCodeFromDatasourceTestRun(env, {
|
|
107
107
|
warningsAsErrors: exitOpts.warningsAsErrors === true,
|
|
108
108
|
requireCert: exitOpts.requireCert === true
|
|
109
109
|
});
|
|
110
|
+
if (exitOpts.strictCapabilityScope === true) {
|
|
111
|
+
const scope = analyzeCapabilityScope(env, exitOpts.requestedCapabilityKey);
|
|
112
|
+
if (scope.violated) {
|
|
113
|
+
exitCode = Math.max(exitCode, 1);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (
|
|
117
|
+
exitCode === 2 &&
|
|
118
|
+
exitOpts.requireCert &&
|
|
119
|
+
!env.certificate
|
|
120
|
+
) {
|
|
121
|
+
logger.error(formatBlockingError('Certification not returned; cannot verify.'));
|
|
122
|
+
}
|
|
123
|
+
return exitCode;
|
|
110
124
|
}
|
|
111
125
|
|
|
112
126
|
/**
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* AI Fabrix Builder - Datasource Commands
|
|
3
3
|
*
|
|
4
4
|
* Handles datasource validation, listing, comparison, deployment, and online validation runs.
|
|
5
|
-
* Subcommands `test`, `test-integration`, and `test-e2e` call the dataplane unified validation API;
|
|
5
|
+
* Subcommands `test`, `test-integration`, and `test-e2e` call the dataplane unified validation API; `log-test` / `log-integration` / `log-e2e` read saved debug JSON locally. Permissions are summarized in `docs/commands/permissions.md`.
|
|
6
6
|
*
|
|
7
7
|
* @fileoverview Datasource management commands for AI Fabrix Builder
|
|
8
8
|
* @author AI Fabrix Team
|
|
@@ -32,7 +32,7 @@ Subcommands:
|
|
|
32
32
|
diff Compare two datasource JSON files
|
|
33
33
|
test <key> Structural/policy validation via unified dataplane API (DatasourceTestRun)
|
|
34
34
|
test-integration / test-e2e Integration or E2E run via the same unified validation API
|
|
35
|
-
log-integration / log-e2e
|
|
35
|
+
log-test / log-integration / log-e2e Show saved debug logs (structural, integration, E2E)
|
|
36
36
|
`;
|
|
37
37
|
|
|
38
38
|
const DATASOURCE_VALIDATE_HELP_AFTER = `
|
|
@@ -187,6 +187,28 @@ function setupDatasourceLogIntegrationCommand(datasource) {
|
|
|
187
187
|
});
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
function setupDatasourceLogTestCommand(datasource) {
|
|
191
|
+
datasource.command('log-test <datasourceKey>')
|
|
192
|
+
.description('Show structural validation log from datasource test (latest test-*.json or --file)')
|
|
193
|
+
.option(
|
|
194
|
+
'-a, --app <app>',
|
|
195
|
+
'Integration folder name (optional: resolve from cwd or datasource key if single match)'
|
|
196
|
+
)
|
|
197
|
+
.option('-f, --file <path>', 'Path to log file (default: latest structural log in app logs folder)')
|
|
198
|
+
.action(async(datasourceKey, options) => {
|
|
199
|
+
try {
|
|
200
|
+
await runLogViewer(datasourceKey, {
|
|
201
|
+
app: options.app,
|
|
202
|
+
file: options.file,
|
|
203
|
+
logType: 'test'
|
|
204
|
+
});
|
|
205
|
+
} catch (error) {
|
|
206
|
+
logger.error(formatBlockingError('log-test failed:'), error.message);
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
190
212
|
/**
|
|
191
213
|
* Setup datasource management commands
|
|
192
214
|
* @param {Command} program - Commander program instance
|
|
@@ -208,6 +230,7 @@ function setupDatasourceCommands(program) {
|
|
|
208
230
|
setupDatasourceTestE2ECommand(datasource);
|
|
209
231
|
setupDatasourceLogE2ECommand(datasource);
|
|
210
232
|
setupDatasourceLogIntegrationCommand(datasource);
|
|
233
|
+
setupDatasourceLogTestCommand(datasource);
|
|
211
234
|
}
|
|
212
235
|
|
|
213
236
|
module.exports = { setupDatasourceCommands };
|
package/lib/commands/upload.js
CHANGED
|
@@ -35,6 +35,8 @@ const {
|
|
|
35
35
|
logServerValidationWarnings,
|
|
36
36
|
logProbeRuntimeBlock
|
|
37
37
|
} = require('../utils/external-system-readiness-display');
|
|
38
|
+
const { maybeSyncSystemCertificationFromDataplane } = require('../certification/sync-system-certification');
|
|
39
|
+
const { cliOptsSkipCertSync } = require('../certification/cli-cert-sync-skip');
|
|
38
40
|
|
|
39
41
|
/**
|
|
40
42
|
* Validates system-key format (same as download).
|
|
@@ -192,13 +194,12 @@ async function maybeRunVerboseServerValidation(dataplaneUrl, authConfig, payload
|
|
|
192
194
|
}
|
|
193
195
|
|
|
194
196
|
/**
|
|
195
|
-
*
|
|
196
|
-
* (
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
* still uses the engine path without payloadTemplate).
|
|
197
|
+
* Omit payloadTemplate so POST /validation/run uses the **validation-engine** path
|
|
198
|
+
* (same as deploy --probe). Needed for certification and dataplane auto-issue of integration
|
|
199
|
+
* certificates after a successful run. If RBAC/live checks fail right after upload until the
|
|
200
|
+
* controller syncs permissions, skip --probe or retry once permissions are visible.
|
|
200
201
|
*/
|
|
201
|
-
const UPLOAD_PROBE_TEST_DATA = {
|
|
202
|
+
const UPLOAD_PROBE_TEST_DATA = {};
|
|
202
203
|
|
|
203
204
|
/**
|
|
204
205
|
* Optional POST validation/run after successful upload.
|
|
@@ -281,6 +282,16 @@ async function runUploadPublishAndSummary(systemKey, options, manifest, payload)
|
|
|
281
282
|
if (options.probe) {
|
|
282
283
|
await maybeRunUploadProbe(dataplaneUrl, systemKey, authConfig, options.probeTimeout);
|
|
283
284
|
}
|
|
285
|
+
|
|
286
|
+
const dsKeys = (payload.dataSources || []).map((ds) => ds && ds.key).filter(Boolean);
|
|
287
|
+
await maybeSyncSystemCertificationFromDataplane({
|
|
288
|
+
label: 'upload',
|
|
289
|
+
noCertSync: cliOptsSkipCertSync(options),
|
|
290
|
+
systemKey,
|
|
291
|
+
dataplaneUrl,
|
|
292
|
+
authConfig,
|
|
293
|
+
datasourceKeys: dsKeys
|
|
294
|
+
});
|
|
284
295
|
}
|
|
285
296
|
|
|
286
297
|
/**
|
package/lib/core/secrets.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
3
|
-
* @fileoverview Read and format
|
|
2
|
+
* Log viewer for E2E, integration, and structural (`datasource test`) debug logs.
|
|
3
|
+
* @fileoverview Read and format saved JSON logs under integration/<app>/logs/
|
|
4
4
|
* @author AI Fabrix Team
|
|
5
5
|
* @version 2.0.0
|
|
6
6
|
*/
|
|
7
7
|
/* eslint-disable max-statements, complexity, max-depth -- Formatter functions; display branches by design */
|
|
8
8
|
|
|
9
9
|
const path = require('path');
|
|
10
|
-
|
|
10
|
+
// Use node:fs so suites that jest.mock('fs') do not break real-disk log resolution (structural tests, CI).
|
|
11
|
+
const fsp = require('node:fs').promises;
|
|
11
12
|
const chalk = require('chalk');
|
|
12
13
|
const logger = require('../utils/logger');
|
|
13
14
|
const { resolveAppKeyForDatasource } = require('./resolve-app');
|
|
@@ -23,7 +24,7 @@ const { sectionTitle, headerKeyValue, formatBlockingError, successGlyph, failure
|
|
|
23
24
|
async function getLatestLogPath(logsDir, pattern) {
|
|
24
25
|
let entries;
|
|
25
26
|
try {
|
|
26
|
-
entries = await
|
|
27
|
+
entries = await fsp.readdir(logsDir, { withFileTypes: true });
|
|
27
28
|
} catch (err) {
|
|
28
29
|
if (err.code === 'ENOENT') return null;
|
|
29
30
|
throw err;
|
|
@@ -34,12 +35,46 @@ async function getLatestLogPath(logsDir, pattern) {
|
|
|
34
35
|
.map(e => path.join(logsDir, e.name));
|
|
35
36
|
if (files.length === 0) return null;
|
|
36
37
|
const withStats = await Promise.all(
|
|
37
|
-
files.map(async f => ({ path: f, mtime: (await
|
|
38
|
+
files.map(async f => ({ path: f, mtime: (await fsp.stat(f)).mtimeMs }))
|
|
38
39
|
);
|
|
39
40
|
withStats.sort((a, b) => b.mtime - a.mtime);
|
|
40
41
|
return withStats[0].path;
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Structural validation logs use prefix `test-` but must not pick up `test-e2e-` or `test-integration-`.
|
|
46
|
+
* @param {string} name - File name only
|
|
47
|
+
* @returns {boolean}
|
|
48
|
+
*/
|
|
49
|
+
function isStructuralTestLogFileName(name) {
|
|
50
|
+
if (!name || typeof name !== 'string' || !name.endsWith('.json')) return false;
|
|
51
|
+
if (name.startsWith('test-e2e-') || name.startsWith('test-integration-')) return false;
|
|
52
|
+
return name.startsWith('test-');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Latest structural `datasource test --debug` log in logsDir.
|
|
57
|
+
* @param {string} logsDir
|
|
58
|
+
* @returns {Promise<string|null>}
|
|
59
|
+
*/
|
|
60
|
+
async function getLatestStructuralTestLogPath(logsDir) {
|
|
61
|
+
let entries;
|
|
62
|
+
try {
|
|
63
|
+
entries = await fsp.readdir(logsDir, { withFileTypes: true });
|
|
64
|
+
} catch (err) {
|
|
65
|
+
if (err.code === 'ENOENT') return null;
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
const names = entries
|
|
69
|
+
.filter(e => e.isFile() && isStructuralTestLogFileName(e.name))
|
|
70
|
+
.map(e => e.name);
|
|
71
|
+
if (names.length === 0) return null;
|
|
72
|
+
// Structural logs embed a sortable timestamp in the filename (`test-YYYY-...Z.json`).
|
|
73
|
+
// Prefer lexicographic ordering to avoid filesystem timestamp resolution issues in CI.
|
|
74
|
+
names.sort((a, b) => b.localeCompare(a));
|
|
75
|
+
return path.join(logsDir, names[0]);
|
|
76
|
+
}
|
|
77
|
+
|
|
43
78
|
/**
|
|
44
79
|
* Truncate string for display
|
|
45
80
|
* @param {string} s - String
|
|
@@ -185,13 +220,57 @@ function formatIntegrationLog(data, fileName) {
|
|
|
185
220
|
}
|
|
186
221
|
}
|
|
187
222
|
|
|
223
|
+
function structuralEnvelopeStatusLine(status) {
|
|
224
|
+
if (status === 'ok' || status === 'skipped') return `${successGlyph()} ${chalk.gray('status:')} ${status}`;
|
|
225
|
+
if (status === 'warn') return `${chalk.yellow('⚠')} ${chalk.gray('status:')} ${status}`;
|
|
226
|
+
if (status === 'fail') return `${failureGlyph()} ${chalk.gray('status:')} ${status}`;
|
|
227
|
+
return `${chalk.gray('?')} ${chalk.gray('status:')} ${status ?? '—'}`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Format structural validation log (`datasource test --debug` → `test-*.json`).
|
|
232
|
+
* @param {Object} data - Parsed JSON { request, response?, error? }
|
|
233
|
+
* @param {string} [fileName] - Log file name for header
|
|
234
|
+
*/
|
|
235
|
+
function formatStructuralTestLog(data, fileName) {
|
|
236
|
+
logger.log('');
|
|
237
|
+
logger.log(sectionTitle('Structural validation log'));
|
|
238
|
+
if (fileName) logger.log(chalk.gray(fileName));
|
|
239
|
+
const req = data.request || {};
|
|
240
|
+
logger.log('');
|
|
241
|
+
logger.log(sectionTitle('Request:'));
|
|
242
|
+
logger.log(headerKeyValue('datasourceKey:', String(req.datasourceKey ?? '—')));
|
|
243
|
+
if (req.runType) logger.log(headerKeyValue('runType:', String(req.runType)));
|
|
244
|
+
if (req.includeDebug !== undefined) {
|
|
245
|
+
logger.log(chalk.gray(` includeDebug: ${req.includeDebug}`));
|
|
246
|
+
}
|
|
247
|
+
if (data.error) {
|
|
248
|
+
logger.log('');
|
|
249
|
+
logger.log(formatBlockingError(`Error: ${data.error}`));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const res = data.response || {};
|
|
253
|
+
logger.log('');
|
|
254
|
+
logger.log(sectionTitle('Response (envelope):'));
|
|
255
|
+
logger.log(` ${structuralEnvelopeStatusLine(res.status)}`);
|
|
256
|
+
if (res.reportCompleteness) {
|
|
257
|
+
logger.log(chalk.gray(` reportCompleteness: ${res.reportCompleteness}`));
|
|
258
|
+
}
|
|
259
|
+
if (res.runId) logger.log(chalk.gray(` runId: ${res.runId}`));
|
|
260
|
+
if (res.systemKey) logger.log(chalk.gray(` systemKey: ${res.systemKey}`));
|
|
261
|
+
}
|
|
262
|
+
|
|
188
263
|
/**
|
|
189
264
|
* Format log content by type
|
|
190
265
|
* @param {Object} parsed - Parsed JSON log
|
|
191
|
-
* @param {'test-e2e'|'test-integration'} logType - Log type
|
|
266
|
+
* @param {'test'|'test-e2e'|'test-integration'} logType - Log type
|
|
192
267
|
* @param {string} [fileName] - File name for header
|
|
193
268
|
*/
|
|
194
269
|
function formatLogContent(parsed, logType, fileName) {
|
|
270
|
+
if (logType === 'test') {
|
|
271
|
+
formatStructuralTestLog(parsed, fileName);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
195
274
|
if (logType === 'test-e2e') {
|
|
196
275
|
formatE2ELog(parsed, fileName);
|
|
197
276
|
} else {
|
|
@@ -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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
237
|
-
|
|
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(
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
debug: options.debug,
|
|
126
|
-
verbose: options.verbose,
|
|
127
|
-
async: options.async !== false,
|
|
128
|
-
noAsync: options.async === false,
|
|
129
|
-
testCrud: options.testCrud,
|
|
130
|
-
recordId: options.recordId,
|
|
131
|
-
cleanup: options.cleanup,
|
|
132
|
-
primaryKeyValue: pk,
|
|
133
|
-
capabilityKey: options.capabilityKey,
|
|
134
|
-
timeout: timeoutMs,
|
|
135
|
-
sync: options.sync === true
|
|
136
|
-
});
|
|
151
|
+
const unifiedResult = await runUnifiedDatasourceValidation(
|
|
152
|
+
datasourceKey,
|
|
153
|
+
buildUnifiedE2eRunOptions(options, timeoutMs, pk)
|
|
154
|
+
);
|
|
137
155
|
|
|
138
156
|
await throwIfUnifiedE2EBlocked(unifiedResult, appKey, options, requestMeta);
|
|
139
157
|
|
|
@@ -46,6 +46,9 @@ async function buildUnifiedValidationBody(params) {
|
|
|
46
46
|
recordId: options.recordId,
|
|
47
47
|
cleanup: options.cleanup,
|
|
48
48
|
primaryKeyValue: options.primaryKeyValue,
|
|
49
|
+
minVectorHits: options.minVectorHits,
|
|
50
|
+
minProcessed: options.minProcessed,
|
|
51
|
+
minRecordCount: options.minRecordCount,
|
|
49
52
|
e2eOptionsExtra: e2eExtra
|
|
50
53
|
})
|
|
51
54
|
: undefined;
|
|
@@ -82,7 +82,8 @@ async function runUnifiedDatasourceValidation(datasourceKey, options) {
|
|
|
82
82
|
body,
|
|
83
83
|
timeoutMs,
|
|
84
84
|
useAsync,
|
|
85
|
-
noAsync: options.noAsync === true || options.async === false
|
|
85
|
+
noAsync: options.noAsync === true || options.async === false,
|
|
86
|
+
verbosePoll: options.verbose === true
|
|
86
87
|
});
|
|
87
88
|
}
|
|
88
89
|
|