@aifabrix/builder 2.33.6 → 2.36.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -121,6 +121,11 @@ Build --> Run[Run Locally]:::base
121
121
  Run --> Deploy[Deploy to Azure]:::primary
122
122
  ```
123
123
 
124
+ ## Development
125
+
126
+ - **Tests**: `npm test` (runs via wrapper; handles known Jest/Node exit issues).
127
+ - **Coverage**: `npm run test:coverage` — runs tests with coverage through the same wrapper. May take 3–5 minutes for the full suite. If the process exits with a signal after "Ran all test suites", the wrapper treats it as success and coverage is written to `coverage/`. Use `test:coverage:nyc` only if you need nyc-specific reporters.
128
+
124
129
  ## Requirements
125
130
 
126
131
  - **Docker Desktop** - For running containers
@@ -128,9 +128,56 @@ aifabrix datasource list
128
128
  aifabrix datasource validate hubspot-company
129
129
  ```
130
130
 
131
+ ## Wizard E2E tests and use case coverage
132
+
133
+ The script `integration/hubspot/test.js` runs end-to-end wizard and validation tests. It covers all wizard use cases from [Wizard Guide](../../docs/wizard.md):
134
+
135
+ | Use case | Test ID | Description |
136
+ |----------|---------|-------------|
137
+ | **Headless config (wizard.yaml)** | | |
138
+ | Required `appName` | 2.1 | Rejects config missing `appName` |
139
+ | Valid `appName` pattern (lowercase, hyphens/underscores) | 2.2 | Rejects uppercase in app name |
140
+ | Required `mode` | 2.5 | Rejects invalid `mode` enum |
141
+ | Required `source` block | 2.3 | Rejects config without `source` |
142
+ | Valid `source.type` | 2.4 | Rejects invalid source type |
143
+ | `openapi-file` requires `filePath` | 2.7 | Rejects missing/non-existent OpenAPI file |
144
+ | `openapi-url` requires `url` | 2.8 | Rejects `openapi-url` without `url` |
145
+ | `known-platform` requires `platform` | 2.6 | Rejects known-platform without `platform` |
146
+ | **Mode: add-datasource** | | |
147
+ | `systemIdOrKey` required when mode=add-datasource | 2.9 | Rejects add-datasource without systemIdOrKey |
148
+ | **Credential** | | |
149
+ | `credential.action=select` requires `credentialIdOrKey` | 2.10 | Rejects select without credentialIdOrKey |
150
+ | `credential.action=create` requires `config` | 2.11 | Rejects create without config |
151
+ | **Positive flows** | | |
152
+ | Full wizard with OpenAPI file | 1.1 | Complete flow with local OpenAPI file |
153
+ | Wizard with known platform | 1.2 | Flow using known-platform (e.g. HubSpot) |
154
+ | Wizard with env var substitution in deployment | 1.6 | `${CONTROLLER_URL}`, `${DATAPLANE_URL}` in wizard.yaml |
155
+ | Real credential creation (real-data) | 1.3 | Credential create with real OAuth2 (optional env) |
156
+ | **Post-wizard validation (external system)** | | |
157
+ | RBAC: permissions reference existing roles | 2.12 | Rejects permission referencing non-existent role |
158
+ | RBAC: rbac.yaml valid YAML and structure | 2.13 | Rejects invalid YAML in rbac.yaml |
159
+ | Datasource: dimensions required in fieldMappings | 2.14 | Rejects missing dimensions |
160
+ | Datasource: dimension key pattern | 2.15 | Rejects invalid dimension key |
161
+ | Datasource: attribute path pattern | 2.16 | Rejects invalid attribute path |
162
+ | Datasource: dimensions must be object | 2.17 | Rejects dimensions as array |
163
+
164
+ **Run tests:**
165
+
166
+ ```bash
167
+ # All tests (positive may skip if dataplane/controller unavailable)
168
+ node integration/hubspot/test.js
169
+
170
+ # Negative only (no dataplane required)
171
+ node integration/hubspot/test.js --type negative
172
+
173
+ # Specific test
174
+ node integration/hubspot/test.js --test "2.1,2.2"
175
+ ```
176
+
131
177
  ## Documentation
132
178
 
133
179
  - [External Systems Guide](../../docs/external-systems.md) - Complete guide with examples
180
+ - [Wizard Guide](../../docs/wizard.md) - Wizard workflow and headless config
134
181
  - [CLI Reference](../../docs/cli-reference.md) - All commands
135
182
  - [Configuration Reference](../../docs/CONFIGURATION.md) - Config file details
136
183
 
@@ -5,5 +5,10 @@
5
5
  CLIENTID=kv://hubspot-clientidKeyVault
6
6
  CLIENTSECRET=kv://hubspot-clientsecretKeyVault
7
7
  TOKENURL=https://api.hubapi.com/oauth/v1/token
8
- REDIRECT_URI=kv://hubspot-redirect-uriKeyVault
8
+
9
+ # Optional: test runner (integration/hubspot/test.js) reads all config from this .env
10
+ # CONTROLLER_URL=http://localhost:3110
11
+ # ENVIRONMENT=miso
12
+ # DATAPLANE_URL=
13
+ # HUBSPOT_OPENAPI_FILE=integration/hubspot/companies.json
9
14
 
@@ -61,12 +61,6 @@
61
61
  "location": "variable",
62
62
  "required": true
63
63
  },
64
- {
65
- "name": "REDIRECT_URI",
66
- "value": "hubspot-redirect-uriKeyVault",
67
- "location": "keyvault",
68
- "required": false
69
- },
70
64
  {
71
65
  "name": "HUBSPOT_API_VERSION",
72
66
  "value": "v3",
@@ -43,12 +43,6 @@
43
43
  "location": "variable",
44
44
  "required": true
45
45
  },
46
- {
47
- "name": "REDIRECT_URI",
48
- "value": "hubspot-redirect-uriKeyVault",
49
- "location": "keyvault",
50
- "required": false
51
- },
52
46
  {
53
47
  "name": "HUBSPOT_API_VERSION",
54
48
  "value": "v3",
@@ -6,4 +6,4 @@ source:
6
6
  deployment:
7
7
  controller: ${CONTROLLER_URL}
8
8
  dataplane: ${DATAPLANE_URL}
9
- environment: miso
9
+ environment: dev
@@ -217,7 +217,10 @@ function validateWizardResult(result, output) {
217
217
  'fetch failed',
218
218
  'timeout',
219
219
  'unreachable',
220
- 'failed to create wizard session'
220
+ 'failed to create wizard session',
221
+ 'authentication failed',
222
+ 'no authentication method',
223
+ 'client token unavailable'
221
224
  ];
222
225
 
223
226
  const isValid = !result.success && validateError(output, expectedPatterns);
@@ -65,13 +65,7 @@ async function testWizard() {
65
65
  'bin/aifabrix.js',
66
66
  'wizard',
67
67
  '--config',
68
- configPath,
69
- '--controller',
70
- CONTROLLER_URL,
71
- '--environment',
72
- ENVIRONMENT,
73
- '--dataplane',
74
- INVALID_DATAPLANE_URL
68
+ configPath
75
69
  ];
76
70
 
77
71
  const result = await runCommand('node', args);
@@ -107,11 +101,7 @@ async function testDownload() {
107
101
  const args = [
108
102
  'bin/aifabrix.js',
109
103
  'download',
110
- 'non-existent-system-that-should-fail',
111
- '--environment',
112
- ENVIRONMENT,
113
- '--controller',
114
- CONTROLLER_URL
104
+ 'non-existent-system-that-should-fail'
115
105
  ];
116
106
 
117
107
  const result = await runCommand('node', args);
@@ -155,10 +145,6 @@ async function testDelete() {
155
145
  'non-existent-system-that-should-fail',
156
146
  '--type',
157
147
  'external',
158
- '--environment',
159
- ENVIRONMENT,
160
- '--controller',
161
- CONTROLLER_URL,
162
148
  '--yes'
163
149
  ];
164
150
 
@@ -219,13 +205,7 @@ function buildDatasourceDeployArgs(datasourcePath) {
219
205
  'datasource',
220
206
  'deploy',
221
207
  'test-app',
222
- datasourcePath,
223
- '--environment',
224
- ENVIRONMENT,
225
- '--controller',
226
- CONTROLLER_URL,
227
- '--dataplane',
228
- INVALID_DATAPLANE_URL
208
+ datasourcePath
229
209
  ];
230
210
  }
231
211
 
@@ -321,11 +301,7 @@ async function testIntegration() {
321
301
  const args = [
322
302
  'bin/aifabrix.js',
323
303
  'test-integration',
324
- testApp,
325
- '--environment',
326
- ENVIRONMENT,
327
- '--controller',
328
- CONTROLLER_URL
304
+ testApp
329
305
  ];
330
306
 
331
307
  const result = await runCommand('node', args);
@@ -372,11 +348,7 @@ async function testDataplaneDiscovery() {
372
348
  const args = [
373
349
  'bin/aifabrix.js',
374
350
  'download',
375
- 'non-existent-system-for-discovery-test',
376
- '--environment',
377
- ENVIRONMENT,
378
- '--controller',
379
- CONTROLLER_URL
351
+ 'non-existent-system-for-discovery-test'
380
352
  ];
381
353
 
382
354
  const result = await runCommand('node', args);
@@ -11,6 +11,9 @@
11
11
  */
12
12
  'use strict';
13
13
 
14
+ const fs = require('fs').promises;
15
+ const path = require('path');
16
+ const os = require('os');
14
17
  const {
15
18
  logInfo,
16
19
  logSuccess,
@@ -27,9 +30,51 @@ const {
27
30
  } = require('./test-dataplane-down-tests');
28
31
 
29
32
  const INVALID_DATAPLANE_URL = 'http://localhost:9999';
30
- const CONTROLLER_URL = process.env.CONTROLLER_URL || 'http://localhost:3110';
31
33
  const ENVIRONMENT = process.env.ENVIRONMENT || 'miso';
32
34
 
35
+ /** Path to temp config used so CLI uses invalid controller URL and fails with connection error */
36
+ let tempConfigPath = null;
37
+ let originalAifabrixConfig = null;
38
+
39
+ /**
40
+ * Create temp config pointing controller to invalid URL so commands fail with connection error
41
+ * @async
42
+ * @returns {Promise<string>} Path to temp config file
43
+ */
44
+ async function createTempConfig() {
45
+ const dir = path.join(os.tmpdir(), `aifabrix-dataplane-down-${Date.now()}`);
46
+ await fs.mkdir(dir, { recursive: true });
47
+ const configPath = path.join(dir, 'config.yaml');
48
+ const content = `controller: "${INVALID_DATAPLANE_URL}"
49
+ environment: "${ENVIRONMENT}"
50
+ `;
51
+ await fs.writeFile(configPath, content, 'utf8');
52
+ return configPath;
53
+ }
54
+
55
+ /**
56
+ * Restore AIFABRIX_CONFIG and remove temp config
57
+ * @async
58
+ * @returns {Promise<void>} Resolves when cleanup is complete
59
+ */
60
+ async function cleanupTempConfig() {
61
+ if (originalAifabrixConfig !== undefined) {
62
+ if (originalAifabrixConfig === null) {
63
+ delete process.env.AIFABRIX_CONFIG;
64
+ } else {
65
+ process.env.AIFABRIX_CONFIG = originalAifabrixConfig;
66
+ }
67
+ }
68
+ if (tempConfigPath) {
69
+ try {
70
+ await fs.rm(path.dirname(tempConfigPath), { recursive: true, force: true }).catch(() => {});
71
+ } catch {
72
+ // Ignore cleanup errors
73
+ }
74
+ tempConfigPath = null;
75
+ }
76
+ }
77
+
33
78
  /**
34
79
  * Displays failed test details
35
80
  * @function displayFailedTestDetails
@@ -137,17 +182,24 @@ async function main() {
137
182
  logInfo('='.repeat(60));
138
183
  logInfo('Dataplane Down Error Handling Test Suite');
139
184
  logInfo('='.repeat(60));
140
- logInfo(`Invalid Dataplane URL: ${INVALID_DATAPLANE_URL}`);
141
- logInfo(`Controller URL: ${CONTROLLER_URL}`);
185
+ logInfo(`Invalid Controller URL (used for all commands): ${INVALID_DATAPLANE_URL}`);
142
186
  logInfo(`Environment: ${ENVIRONMENT}`);
143
187
  logInfo('='.repeat(60));
144
188
 
145
- const results = await runTests();
146
- displaySummary(results);
189
+ try {
190
+ tempConfigPath = await createTempConfig();
191
+ originalAifabrixConfig = process.env.AIFABRIX_CONFIG || null;
192
+ process.env.AIFABRIX_CONFIG = tempConfigPath;
147
193
 
148
- const failed = results.filter(r => !r.success).length;
149
- if (failed > 0) {
150
- process.exitCode = 1;
194
+ const results = await runTests();
195
+ displaySummary(results);
196
+
197
+ const failed = results.filter(r => !r.success).length;
198
+ if (failed > 0) {
199
+ process.exitCode = 1;
200
+ }
201
+ } finally {
202
+ await cleanupTempConfig();
151
203
  }
152
204
  }
153
205
 
@@ -18,19 +18,14 @@ const yaml = require('js-yaml');
18
18
  const chalk = require('chalk');
19
19
  const { getDeploymentAuth } = require('../../lib/utils/token-manager');
20
20
  const { discoverDataplaneUrl } = require('../../lib/commands/wizard-dataplane');
21
+ const { resolveControllerUrl } = require('../../lib/utils/controller-url');
22
+ const { resolveEnvironment } = require('../../lib/core/config');
21
23
 
22
24
  const execFileAsync = promisify(execFile);
23
25
 
24
- const DEFAULT_CONTROLLER_URL = process.env.CONTROLLER_URL || 'http://localhost:3110';
25
- const DEFAULT_ENVIRONMENT = process.env.ENVIRONMENT || 'miso';
26
- const DEFAULT_DATAPLANE_URL = process.env.DATAPLANE_URL || '';
27
- const DEFAULT_OPENAPI_FILE = process.env.HUBSPOT_OPENAPI_FILE ||
28
- '/workspace/aifabrix-dataplane/data/hubspot/openapi/companies.json';
29
- const LOCAL_ENV_PATH = path.join(process.cwd(), 'integration', 'hubspot', '.env');
30
- const DEFAULT_ENV_PATH = process.env.HUBSPOT_ENV_PATH ||
31
- (fsSync.existsSync(LOCAL_ENV_PATH) ? LOCAL_ENV_PATH : '/workspace/aifabrix-dataplane/data/hubspot/.env');
32
-
26
+ /** Single source for test config: integration/hubspot/.env */
33
27
  const HUBSPOT_DIR = path.join(process.cwd(), 'integration', 'hubspot');
28
+ const LOCAL_ENV_PATH = path.join(HUBSPOT_DIR, '.env');
34
29
  const ARTIFACT_DIR = path.join(HUBSPOT_DIR, 'test-artifacts');
35
30
  const MAX_OUTPUT_BYTES = 10 * 1024 * 1024;
36
31
  const COMMAND_TIMEOUT_MS = 10 * 60 * 1000;
@@ -271,11 +266,54 @@ async function loadEnvFile(envPath, options) {
271
266
  process.env[key] = value;
272
267
  }
273
268
  }
269
+ // Map common .env keys so tests and wizard use credentials from integration/hubspot/.env
270
+ if (process.env.CLIENTID && process.env.HUBSPOT_CLIENT_ID === undefined) {
271
+ process.env.HUBSPOT_CLIENT_ID = process.env.CLIENTID;
272
+ }
273
+ if (process.env.CLIENTSECRET && process.env.HUBSPOT_CLIENT_SECRET === undefined) {
274
+ process.env.HUBSPOT_CLIENT_SECRET = process.env.CLIENTSECRET;
275
+ }
274
276
  if (options.verbose) {
275
277
  logInfo(`Loaded env vars from: ${envPath}`);
276
278
  }
277
279
  }
278
280
 
281
+ /**
282
+ * Load test config (controller, environment, dataplane, openapi file).
283
+ * Reads integration/hubspot/.env; missing CONTROLLER_URL/ENVIRONMENT fall back to
284
+ * the same resolution as the CLI (aifx auth status) so tests use the same controller.
285
+ * @async
286
+ * @function loadTestConfigFromEnv
287
+ * @returns {Promise<Object>} Context with controllerUrl, environment, dataplaneUrl, openapiFile
288
+ */
289
+ async function loadTestConfigFromEnv() {
290
+ const defaults = {
291
+ DATAPLANE_URL: '',
292
+ HUBSPOT_OPENAPI_FILE: path.join(HUBSPOT_DIR, 'companies.json')
293
+ };
294
+ let parsed = {};
295
+ try {
296
+ if (fsSync.existsSync(LOCAL_ENV_PATH)) {
297
+ const content = await fs.readFile(LOCAL_ENV_PATH, 'utf8');
298
+ parsed = parseEnvFile(content);
299
+ }
300
+ } catch (error) {
301
+ logWarn(`Could not read ${LOCAL_ENV_PATH}: ${error.message}`);
302
+ }
303
+ const controllerUrl = parsed.CONTROLLER_URL
304
+ ? parsed.CONTROLLER_URL.trim()
305
+ : await resolveControllerUrl();
306
+ const environment = parsed.ENVIRONMENT
307
+ ? parsed.ENVIRONMENT.trim()
308
+ : await resolveEnvironment();
309
+ return {
310
+ controllerUrl,
311
+ environment,
312
+ dataplaneUrl: parsed.DATAPLANE_URL || defaults.DATAPLANE_URL,
313
+ openapiFile: parsed.HUBSPOT_OPENAPI_FILE || defaults.HUBSPOT_OPENAPI_FILE
314
+ };
315
+ }
316
+
279
317
  /**
280
318
  * Ensure dataplane URL is available for tests
281
319
  * @async
@@ -392,7 +430,8 @@ async function validateAuth(context, options) {
392
430
  throw new Error('Authentication check failed. Run: node bin/aifabrix.js login --controller <url> --method device');
393
431
  }
394
432
  const output = `${result.stdout}\n${result.stderr}`;
395
- if (output.includes('Not authenticated') || output.includes('✗')) {
433
+ // Only treat "Not authenticated" as failure; "✗ Not reachable" (dataplane) is not auth failure
434
+ if (output.includes('Not authenticated')) {
396
435
  throw new Error('Not authenticated. Run: node bin/aifabrix.js login --controller <url> --method device');
397
436
  }
398
437
  logSuccess('Authentication validated.');
@@ -1216,7 +1255,7 @@ async function corruptSystemFileWithInvalidRole(appPath) {
1216
1255
  * @returns {Promise<void>} Resolves when file is corrupted
1217
1256
  */
1218
1257
  async function corruptRbacFile(appPath) {
1219
- const rbacPath = path.join(appPath, 'rbac.yml');
1258
+ const rbacPath = path.join(appPath, 'rbac.yaml');
1220
1259
  await fs.writeFile(rbacPath, 'invalid: yaml: syntax: [', 'utf8');
1221
1260
  }
1222
1261
 
@@ -1339,7 +1378,7 @@ function buildNegativeRbacTestCases(context) {
1339
1378
  const appName = 'hubspot-test-negative-rbac-invalid-yaml';
1340
1379
  const appPath = await createSystemForNegativeTest(appName, 'wizard-valid-for-rbac-yaml-test', context, options);
1341
1380
  await corruptRbacFile(appPath);
1342
- await runValidationExpectFailure(appName, context, options, 'schema must be object or boolean');
1381
+ await runValidationExpectFailure(appName, context, options, 'Invalid YAML syntax in rbac.yaml');
1343
1382
  await cleanupAppArtifacts(appName, options);
1344
1383
  }
1345
1384
  }
@@ -1485,13 +1524,8 @@ async function main() {
1485
1524
  return;
1486
1525
  }
1487
1526
  await ensureDir(ARTIFACT_DIR);
1488
- await loadEnvFile(DEFAULT_ENV_PATH, args);
1489
- const context = {
1490
- controllerUrl: DEFAULT_CONTROLLER_URL,
1491
- environment: DEFAULT_ENVIRONMENT,
1492
- dataplaneUrl: DEFAULT_DATAPLANE_URL,
1493
- openapiFile: DEFAULT_OPENAPI_FILE
1494
- };
1527
+ await loadEnvFile(LOCAL_ENV_PATH, args);
1528
+ const context = await loadTestConfigFromEnv();
1495
1529
  setupTestContext(context);
1496
1530
  await validateAuth(context, args);
1497
1531
  const testCases = buildTestCases(context).filter(testCase => (
@@ -13,4 +13,4 @@ preferences:
13
13
  enableRBAC: false
14
14
  deployment:
15
15
  controller: http://localhost:3110
16
- environment: miso
16
+ environment: dev
@@ -326,6 +326,28 @@ async function getDeploymentDocs(dataplaneUrl, authConfig, systemKey) {
326
326
  return await client.get(`/api/v1/wizard/deployment-docs/${systemKey}`);
327
327
  }
328
328
 
329
+ /**
330
+ * Get known wizard platforms from dataplane.
331
+ * GET /api/v1/wizard/platforms
332
+ * Expected response: { platforms: [ { key: string, displayName?: string } ] } or equivalent.
333
+ * On 404 or error, returns empty array (caller should hide "Known platform" choice).
334
+ * @async
335
+ * @function getWizardPlatforms
336
+ * @param {string} dataplaneUrl - Dataplane base URL
337
+ * @param {Object} authConfig - Authentication configuration
338
+ * @returns {Promise<Array<{key: string, displayName?: string}>>} List of platforms (empty on 404/error)
339
+ */
340
+ async function getWizardPlatforms(dataplaneUrl, authConfig) {
341
+ try {
342
+ const client = new ApiClient(dataplaneUrl, authConfig);
343
+ const response = await client.get('/api/v1/wizard/platforms');
344
+ const platforms = response?.data?.platforms ?? response?.platforms ?? [];
345
+ return Array.isArray(platforms) ? platforms : [];
346
+ } catch (error) {
347
+ return [];
348
+ }
349
+ }
350
+
329
351
  module.exports = {
330
352
  createWizardSession,
331
353
  getWizardSession,
@@ -342,5 +364,6 @@ module.exports = {
342
364
  validateStep,
343
365
  getPreview,
344
366
  testMcpConnection,
345
- getDeploymentDocs
367
+ getDeploymentDocs,
368
+ getWizardPlatforms
346
369
  };
package/lib/app/config.js CHANGED
@@ -66,7 +66,6 @@ function generateExternalSystemEnvTemplate(config, appName) {
66
66
  lines.push('CLIENTID=kv://' + systemKey + '-clientidKeyVault');
67
67
  lines.push('CLIENTSECRET=kv://' + systemKey + '-clientsecretKeyVault');
68
68
  lines.push('TOKENURL=https://api.example.com/oauth/token');
69
- lines.push('REDIRECT_URI=kv://' + systemKey + '-redirect-uriKeyVault');
70
69
  } else if (authType === 'apikey') {
71
70
  lines.push('API_KEY=kv://' + systemKey + '-api-keyKeyVault');
72
71
  } else if (authType === 'basic') {