@aifabrix/builder 2.52.0 → 2.52.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 (134) hide show
  1. package/jest.isolated-projects.js +25 -2
  2. package/jest.projects.js +4 -1
  3. package/lib/api/datasources-extended.api.js +8 -8
  4. package/lib/api/keycloak.api.js +92 -0
  5. package/lib/api/token-providers.api.js +77 -0
  6. package/lib/api/types/keycloak.types.js +23 -0
  7. package/lib/api/types/token-providers.types.js +65 -0
  8. package/lib/app/down.js +16 -1
  9. package/lib/channels/add-channel.js +126 -19
  10. package/lib/channels/approval-guides/chatgpt.js +1 -1
  11. package/lib/channels/approval-guides/microsoft-copilot.js +16 -12
  12. package/lib/channels/channel-add-ensure.js +128 -0
  13. package/lib/channels/channel-artifact-generator.js +11 -1
  14. package/lib/channels/channel-branding.js +94 -0
  15. package/lib/channels/channel-credential.js +8 -0
  16. package/lib/channels/channel-discovery.js +330 -0
  17. package/lib/channels/channel-external-system.js +1 -1
  18. package/lib/channels/channel-runtime-resolver.js +103 -14
  19. package/lib/channels/channel-status-display.js +7 -1
  20. package/lib/channels/microsoft-copilot-channel-add.js +138 -0
  21. package/lib/channels/microsoft-copilot-entra-sso-form.js +180 -0
  22. package/lib/channels/microsoft-copilot-entra-sso.js +264 -0
  23. package/lib/channels/microsoft-copilot-icons.js +74 -0
  24. package/lib/channels/microsoft-copilot-limits.js +84 -0
  25. package/lib/channels/microsoft-copilot-package.js +130 -49
  26. package/lib/channels/microsoft-copilot-validate.js +197 -0
  27. package/lib/cli/index.js +2 -0
  28. package/lib/cli/infra-guided.js +13 -6
  29. package/lib/cli/setup-infra.js +31 -4
  30. package/lib/commands/channel-add-setup.js +28 -9
  31. package/lib/commands/channel.js +14 -10
  32. package/lib/commands/integration-definition.js +158 -0
  33. package/lib/commands/repair-auth-apply.js +19 -5
  34. package/lib/commands/repair-datasource-sync-operations.js +4 -1
  35. package/lib/commands/role-assistant-test-output.js +200 -0
  36. package/lib/commands/role-assistant.js +125 -166
  37. package/lib/datasource/capability/capability-resolve.js +15 -5
  38. package/lib/datasource/capability/copy-operations.js +14 -4
  39. package/lib/infrastructure/index.js +19 -4
  40. package/lib/integration-definition/apply.js +478 -0
  41. package/lib/integration-definition/canonical-json.js +51 -0
  42. package/lib/integration-definition/emit-suite-bid.js +94 -0
  43. package/lib/integration-definition/enrich-crm-field-mappings.js +240 -0
  44. package/lib/integration-definition/enrich-openapi-operations.js +434 -0
  45. package/lib/integration-definition/from-discovery.js +339 -0
  46. package/lib/integration-definition/hash.js +37 -0
  47. package/lib/integration-definition/index.js +37 -0
  48. package/lib/integration-definition/openapi-enrichment/hubspot-crm-companies.json +205 -0
  49. package/lib/integration-definition/resource-type-catalog.js +59 -0
  50. package/lib/integration-definition/system-kind-enum.js +40 -0
  51. package/lib/integration-definition/validate.js +148 -0
  52. package/lib/programmatic/builder-api-request.js +5 -1
  53. package/lib/programmatic/builder-help-governance.js +3 -2
  54. package/lib/programmatic/capability-registry.js +19 -0
  55. package/lib/programmatic/openapi-component-schemas.js +6 -1
  56. package/lib/programmatic/openapi-descriptions.js +40 -0
  57. package/lib/programmatic/openapi-envelope-examples.js +269 -0
  58. package/lib/programmatic/openapi-operation-bindings.js +45 -0
  59. package/lib/programmatic/openapi-request-schemas.js +14 -0
  60. package/lib/programmatic/openapi-result-schemas-overflow.js +101 -0
  61. package/lib/programmatic/openapi-result-schemas.js +46 -99
  62. package/lib/programmatic/openapi-schema-descriptions-core.js +7 -4
  63. package/lib/programmatic/openapi-schema-descriptions-help.js +1 -1
  64. package/lib/programmatic/openapi-schema-descriptions-operations.js +24 -0
  65. package/lib/programmatic/openapi-schema-helpers.js +9 -9
  66. package/lib/programmatic/openapi-spec.js +11 -1
  67. package/lib/programmatic/orchestration-openapi-meta.js +10 -0
  68. package/lib/programmatic/route-handler-map.js +29 -1
  69. package/lib/programmatic/run-channel-package.js +236 -0
  70. package/lib/programmatic/run-integration-definition.js +69 -0
  71. package/lib/role-assistant/knowledge-sync-logging.js +58 -0
  72. package/lib/role-assistant/knowledge-sync.js +88 -93
  73. package/lib/role-assistant/test-cases-bid.js +155 -0
  74. package/lib/role-assistant/test-cases-evaluate.js +9 -1
  75. package/lib/role-assistant/test-cases-honesty.js +24 -10
  76. package/lib/role-assistant/test-cases.js +28 -0
  77. package/lib/role-assistant/test-runner-bid-apply.js +122 -0
  78. package/lib/role-assistant/test-runner-bid-document.js +278 -0
  79. package/lib/role-assistant/test-runner-workhub-answers.js +245 -20
  80. package/lib/role-assistant/test-runner-workhub.js +100 -18
  81. package/lib/role-assistant/test-suite.js +119 -0
  82. package/lib/schema/application-schema.json +56 -6
  83. package/lib/schema/external-datasource.schema.json +103 -17
  84. package/lib/schema/external-system.schema.json +54 -3
  85. package/lib/schema/integration-definition.schema.json +195 -0
  86. package/lib/utils/authentication-shape.js +6 -0
  87. package/lib/utils/docker-build.js +3 -3
  88. package/lib/utils/help-builder.js +1 -0
  89. package/lib/utils/promote-dev-docker-image.js +97 -18
  90. package/lib/utils/transient-developer-id.js +28 -0
  91. package/package.json +1 -1
  92. package/scripts/pnpm/af.mjs +24 -9
  93. package/scripts/pnpm/help.mjs +1 -1
  94. package/templates/applications/dataplane/application.yaml +3 -2
  95. package/templates/applications/dataplane/env.template +7 -6
  96. package/templates/applications/miso-controller/application.yaml +23 -9
  97. package/templates/applications/miso-controller/env.template +8 -2
  98. package/templates/channels/README.md +11 -1
  99. package/templates/channels/brand/color-192.png +0 -0
  100. package/templates/channels/brand/dark/128.png +0 -0
  101. package/templates/channels/brand/dark/192.png +0 -0
  102. package/templates/channels/brand/dark/256.png +0 -0
  103. package/templates/channels/brand/dark/32.png +0 -0
  104. package/templates/channels/brand/dark/512.png +0 -0
  105. package/templates/channels/brand/dark/64.png +0 -0
  106. package/templates/channels/brand/light/128.png +0 -0
  107. package/templates/channels/brand/light/192.png +0 -0
  108. package/templates/channels/brand/light/256.png +0 -0
  109. package/templates/channels/brand/light/32.png +0 -0
  110. package/templates/channels/brand/light/512.png +0 -0
  111. package/templates/channels/brand/light/64.png +0 -0
  112. package/templates/channels/brand/mono-white/128.png +0 -0
  113. package/templates/channels/brand/mono-white/192.png +0 -0
  114. package/templates/channels/brand/mono-white/256.png +0 -0
  115. package/templates/channels/brand/mono-white/32.png +0 -0
  116. package/templates/channels/brand/mono-white/512.png +0 -0
  117. package/templates/channels/brand/mono-white/64.png +0 -0
  118. package/templates/channels/brand/outline-32.png +0 -0
  119. package/templates/channels/chatgpt/install.template.md.hbs +6 -4
  120. package/templates/channels/chatgpt/integration/README.md +4 -5
  121. package/templates/channels/chatgpt/integration/chatgpt-system.json +1 -16
  122. package/templates/channels/microsoft-copilot/ai-plugin.template.json.hbs +10 -16
  123. package/templates/channels/microsoft-copilot/declarative-agent.template.json.hbs +5 -3
  124. package/templates/channels/microsoft-copilot/entra-sso-registration.template.md.hbs +27 -0
  125. package/templates/channels/microsoft-copilot/install.template.md.hbs +39 -20
  126. package/templates/channels/microsoft-copilot/integration/README.md +2 -3
  127. package/templates/channels/microsoft-copilot/integration/microsoft-copilot-system.json +1 -22
  128. package/templates/channels/microsoft-copilot/manifest.template.json.hbs +7 -7
  129. package/templates/channels/microsoft-copilot/oauth-plugin-vault-required.template.md.hbs +11 -0
  130. package/templates/channels/slack/integration/README.md +2 -4
  131. package/templates/channels/teams-bot/install.template.md.hbs +4 -0
  132. package/templates/channels/teams-bot/integration/README.md +3 -5
  133. package/templates/channels/web-embed/integration/web-embed-system.json +1 -16
  134. /package/.cursor/plans/{Done → Archive}/157.1-security-quality-violations-by-type.csv +0 -0
@@ -108,7 +108,10 @@ function ensureSyncOperationScaffold(parsed) {
108
108
  parsed.execution = { engine: 'cip' };
109
109
  }
110
110
  if (!parsed.execution.cip || typeof parsed.execution.cip !== 'object') {
111
- parsed.execution.cip = { operations: {} };
111
+ parsed.execution.cip = { version: '1.0', operations: {} };
112
+ }
113
+ if (!parsed.execution.cip.version) {
114
+ parsed.execution.cip.version = '1.0';
112
115
  }
113
116
  if (!parsed.execution.cip.operations || typeof parsed.execution.cip.operations !== 'object') {
114
117
  parsed.execution.cip.operations = {};
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Role Assistant test CLI envelope + human summary output.
3
+ * @fileoverview Split from role-assistant.js for max-lines / complexity
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const logger = require('../utils/logger');
9
+ const { VERDICT } = require('../lifecycle/product-model');
10
+ const {
11
+ sectionTitle,
12
+ headerKeyValue,
13
+ formatSuccessLine,
14
+ formatSuccessParagraph,
15
+ formatWarningLine,
16
+ formatBlockingError,
17
+ metadata
18
+ } = require('../utils/cli-test-layout-chalk');
19
+
20
+ /**
21
+ * Bucket a lifecycle step status into ok | failed | skipped | other.
22
+ * @param {string} status
23
+ * @returns {'ok'|'failed'|'skipped'|'other'}
24
+ */
25
+ function bucketStepStatus(status) {
26
+ const normalized = String(status || 'ok').toLowerCase();
27
+ if (normalized === 'ok' || normalized === 'verified' || normalized === 'passed') {
28
+ return 'ok';
29
+ }
30
+ if (normalized === 'failed' || normalized === 'error') {
31
+ return 'failed';
32
+ }
33
+ if (normalized === 'skipped' || normalized === 'n/a' || normalized === 'not_applicable') {
34
+ return 'skipped';
35
+ }
36
+ return 'other';
37
+ }
38
+
39
+ /**
40
+ * Summarize lifecycle stepsRun rows (API uses step/status/datasourceKey, not pillar).
41
+ * @param {Array<Object|string>} steps
42
+ * @returns {string}
43
+ */
44
+ function formatStepsRunSummary(steps) {
45
+ const byStep = Object.create(null);
46
+ for (const row of steps) {
47
+ if (!row || typeof row !== 'object') {
48
+ const label = String(row);
49
+ byStep[label] = byStep[label] || { ok: 0, failed: 0, skipped: 0, other: 0 };
50
+ byStep[label].other += 1;
51
+ continue;
52
+ }
53
+ const stepName = String(row.step || row.pillar || row.name || 'step').trim() || 'step';
54
+ const status = row.status || (row.success === false ? 'failed' : 'ok');
55
+ byStep[stepName] = byStep[stepName] || { ok: 0, failed: 0, skipped: 0, other: 0 };
56
+ byStep[stepName][bucketStepStatus(status)] += 1;
57
+ }
58
+ return Object.keys(byStep)
59
+ .map((stepName) => {
60
+ const counts = byStep[stepName];
61
+ const parts = [];
62
+ if (counts.ok) parts.push(`${counts.ok} ok`);
63
+ if (counts.failed) parts.push(`${counts.failed} failed`);
64
+ if (counts.skipped) parts.push(`${counts.skipped} skipped`);
65
+ if (counts.other) parts.push(`${counts.other} other`);
66
+ return parts.length > 0 ? `${stepName} (${parts.join(', ')})` : stepName;
67
+ })
68
+ .join('; ');
69
+ }
70
+
71
+ function mapCaseEnvelopeRow(row) {
72
+ return {
73
+ caseId: row.caseId,
74
+ sourceFile: row.sourceFile,
75
+ passed: row.passed,
76
+ lifecycleState: row.lifecycleState,
77
+ assertions: row.assertions,
78
+ failedAssertions: row.failedAssertions,
79
+ warnings: Array.isArray(row.warnings) ? row.warnings : [],
80
+ execution: row.execution || null
81
+ };
82
+ }
83
+
84
+ function attachEnvelopeCases(envelope, caseResult) {
85
+ if (Array.isArray(caseResult)) {
86
+ envelope.cases = caseResult.map(mapCaseEnvelopeRow);
87
+ const passed = envelope.cases.filter((c) => c.passed).length;
88
+ envelope.summary = {
89
+ total: envelope.cases.length,
90
+ passed,
91
+ failed: envelope.cases.length - passed
92
+ };
93
+ envelope.case = envelope.cases.length === 1 ? envelope.cases[0] : null;
94
+ return;
95
+ }
96
+ if (caseResult) {
97
+ envelope.case = mapCaseEnvelopeRow(caseResult);
98
+ }
99
+ }
100
+
101
+ function buildRoleAssistantTestEnvelope(roleKey, verdict, portalUrl, result, opts, caseResult = null) {
102
+ const report = result.report || {};
103
+ const multiCase = Array.isArray(caseResult) && caseResult.length > 1;
104
+ const envelope = {
105
+ roleKey,
106
+ command: 'role-assistant test',
107
+ mode: opts.suite || multiCase ? 'suite' : 'single',
108
+ suite: opts.suite || null,
109
+ verdict,
110
+ portalUrl,
111
+ openApiPublished: result.openApiPublished === true,
112
+ certification: report.certification || null,
113
+ recommendations: Array.isArray(report.recommendations) ? report.recommendations : [],
114
+ warnings: Array.isArray(report.warnings) ? report.warnings : [],
115
+ stepsRun: result.stepsRun || [],
116
+ case: null,
117
+ cases: null,
118
+ summary: null
119
+ };
120
+ attachEnvelopeCases(envelope, caseResult);
121
+ if (opts.verbose || opts.debug) {
122
+ envelope.report = report;
123
+ }
124
+ if (opts.debug) {
125
+ envelope.debug = {
126
+ observation: caseResult && !Array.isArray(caseResult) ? caseResult.observation : null,
127
+ lifecycleResult: result
128
+ };
129
+ }
130
+ return envelope;
131
+ }
132
+
133
+ function displayCaseResultSummary(caseResult) {
134
+ if (!caseResult) {
135
+ return;
136
+ }
137
+ logger.log(headerKeyValue('Case:', caseResult.caseId));
138
+ logger.log(headerKeyValue('Case file:', caseResult.sourceFile));
139
+ logger.log(headerKeyValue('Observed state:', caseResult.lifecycleState));
140
+ if (caseResult.execution) {
141
+ logger.log(headerKeyValue('Execution id:', caseResult.execution.processExecutionId || 'n/a'));
142
+ logger.log(headerKeyValue('Final runtime status:', caseResult.execution.finalStatus || 'n/a'));
143
+ }
144
+ if (caseResult.passed) {
145
+ logger.log(formatSuccessLine('Case assertions passed.'));
146
+ const warnings = Array.isArray(caseResult.warnings) ? caseResult.warnings : [];
147
+ for (const row of warnings) {
148
+ logger.log(formatWarningLine(row.message || String(row)));
149
+ }
150
+ return;
151
+ }
152
+ logger.log(formatBlockingError('Case assertions failed.'));
153
+ for (const row of caseResult.failedAssertions) {
154
+ logger.log(formatWarningLine(row.message));
155
+ }
156
+ }
157
+
158
+ function displayPortalAndVerdict(portalUrl, verdict) {
159
+ if (portalUrl) {
160
+ logger.log(headerKeyValue('Portal:', portalUrl));
161
+ } else {
162
+ logger.log(formatWarningLine('Portal link unavailable. Configure dataplane public URL and retry.'));
163
+ }
164
+ const verdictMessage =
165
+ verdict === VERDICT.VERIFIED
166
+ ? 'Lifecycle checks passed. Open portal for full execution steps and logs.'
167
+ : 'Lifecycle checks are not fully verified. Open portal to inspect blocking steps.';
168
+ if (verdict === VERDICT.VERIFIED) {
169
+ logger.log(formatSuccessParagraph(verdictMessage));
170
+ } else {
171
+ logger.log(formatWarningLine(verdictMessage));
172
+ }
173
+ }
174
+
175
+ function displayRoleAssistantTestSummary(roleKey, verdict, portalUrl, result, caseResult = null) {
176
+ logger.log('');
177
+ logger.log(sectionTitle(`${String(roleKey).toUpperCase()} ROLE ASSISTANT TEST`));
178
+ logger.log('');
179
+ logger.log(headerKeyValue('Role key:', roleKey));
180
+ displayPortalAndVerdict(portalUrl, verdict);
181
+ const steps = Array.isArray(result.stepsRun) ? result.stepsRun : [];
182
+ if (steps.length > 0) {
183
+ logger.log(metadata(`Steps run: ${formatStepsRunSummary(steps)}`));
184
+ }
185
+ if (Array.isArray(caseResult)) {
186
+ for (const row of caseResult) {
187
+ displayCaseResultSummary(row);
188
+ }
189
+ const passed = caseResult.filter((c) => c.passed).length;
190
+ logger.log(headerKeyValue('Suite summary:', `${passed}/${caseResult.length} passed`));
191
+ return;
192
+ }
193
+ displayCaseResultSummary(caseResult);
194
+ }
195
+
196
+ module.exports = {
197
+ formatStepsRunSummary,
198
+ buildRoleAssistantTestEnvelope,
199
+ displayRoleAssistantTestSummary
200
+ };
@@ -8,6 +8,7 @@
8
8
 
9
9
  'use strict';
10
10
 
11
+ const path = require('path');
11
12
  const logger = require('../utils/logger');
12
13
  const { handleCommandError } = require('../utils/cli-utils');
13
14
  const {
@@ -22,6 +23,11 @@ const {
22
23
  loadRoleAssistantTestCase,
23
24
  evaluateRoleAssistantTestCase
24
25
  } = require('../role-assistant/test-cases');
26
+ const {
27
+ resolveCasesRoot,
28
+ discoverSuiteCaseFiles,
29
+ filterDiscoveredCases
30
+ } = require('../role-assistant/test-suite');
25
31
  const {
26
32
  isExecutableRoleAssistantCase,
27
33
  runRoleAssistantCaseLifecycle,
@@ -32,68 +38,18 @@ const { collectString } = require('./governance');
32
38
  const {
33
39
  sectionTitle,
34
40
  headerKeyValue,
35
- formatSuccessLine,
36
- formatSuccessParagraph,
37
- formatWarningLine,
38
- formatBlockingError,
39
41
  metadata
40
42
  } = require('../utils/cli-test-layout-chalk');
43
+ const {
44
+ formatStepsRunSummary,
45
+ buildRoleAssistantTestEnvelope,
46
+ displayRoleAssistantTestSummary
47
+ } = require('./role-assistant-test-output');
41
48
  const {
42
49
  bareRoleKey,
43
50
  roleAssistantSystemKey
44
51
  } = require('../utils/role-assistant-paths');
45
52
 
46
- /**
47
- * Bucket a lifecycle step status into ok | failed | skipped | other.
48
- * @param {string} status
49
- * @returns {'ok'|'failed'|'skipped'|'other'}
50
- */
51
- function bucketStepStatus(status) {
52
- const normalized = String(status || 'ok').toLowerCase();
53
- if (normalized === 'ok' || normalized === 'verified' || normalized === 'passed') {
54
- return 'ok';
55
- }
56
- if (normalized === 'failed' || normalized === 'error') {
57
- return 'failed';
58
- }
59
- if (normalized === 'skipped' || normalized === 'n/a' || normalized === 'not_applicable') {
60
- return 'skipped';
61
- }
62
- return 'other';
63
- }
64
-
65
- /**
66
- * Summarize lifecycle stepsRun rows (API uses step/status/datasourceKey, not pillar).
67
- * @param {Array<Object|string>} steps
68
- * @returns {string}
69
- */
70
- function formatStepsRunSummary(steps) {
71
- const byStep = Object.create(null);
72
- for (const row of steps) {
73
- if (!row || typeof row !== 'object') {
74
- const label = String(row);
75
- byStep[label] = byStep[label] || { ok: 0, failed: 0, skipped: 0, other: 0 };
76
- byStep[label].other += 1;
77
- continue;
78
- }
79
- const stepName = String(row.step || row.pillar || row.name || 'step').trim() || 'step';
80
- const status = row.status || (row.success === false ? 'failed' : 'ok');
81
- byStep[stepName] = byStep[stepName] || { ok: 0, failed: 0, skipped: 0, other: 0 };
82
- byStep[stepName][bucketStepStatus(status)] += 1;
83
- }
84
- return Object.keys(byStep)
85
- .map((stepName) => {
86
- const counts = byStep[stepName];
87
- const parts = [];
88
- if (counts.ok) parts.push(`${counts.ok} ok`);
89
- if (counts.failed) parts.push(`${counts.failed} failed`);
90
- if (counts.skipped) parts.push(`${counts.skipped} skipped`);
91
- if (counts.other) parts.push(`${counts.other} other`);
92
- return parts.length > 0 ? `${stepName} (${parts.join(', ')})` : stepName;
93
- })
94
- .join('; ');
95
- }
96
-
97
53
  function roleAssistantHelpAfter() {
98
54
  return `
99
55
  Examples:
@@ -101,8 +57,9 @@ Examples:
101
57
  $ aifabrix role-assistant test customer-support-bot --case smoke
102
58
  $ aifabrix role-assistant test customer-support-bot --case tests/customer-address-change.yaml
103
59
  $ aifabrix role-assistant test customer-support-bot --subject-email ops@example.com
104
- $ aifabrix role-assistant test customer-support-bot --json
105
- $ aifabrix role-assistant test customer-support-bot --publish --force
60
+ $ aifabrix role-assistant test customer-support-bot --suite 001
61
+ $ aifabrix role-assistant test customer-support-bot --list-cases
62
+ $ aifabrix role-assistant test customer-support-bot --suite 001 --fail-fast --json
106
63
 
107
64
  Notes:
108
65
  - Uses the public/running Role Assistant on the dataplane by default (does not publish local datasources).
@@ -110,6 +67,7 @@ Notes:
110
67
  - Runs lifecycle checks, then prints a portal deep link for full step-by-step status.
111
68
  - Case files with input.message/input.content execute Work Hub plan→accept (or work.start when executionPath: intent).
112
69
  - answers[] and approvals[] are matched to runtime wait payloads (questionKey/requestKey/waitReason).
70
+ - --suite runs all YAML under tests/role-assistant/ra-<roleKey>/<suite>/ (or --cases-root).
113
71
  - Use the portal URL for detailed human review while automation reads CLI/JSON output.
114
72
  `;
115
73
  }
@@ -146,96 +104,123 @@ function normalizeRoleAssistantTestOptions(options, cmd) {
146
104
  certificationProfile: options.certificationProfile || 'bronze',
147
105
  case: trimOptionalString(options.case),
148
106
  testsDir: trimOptionalString(options.testsDir),
107
+ suite: trimOptionalString(options.suite),
108
+ casesRoot: trimOptionalString(options.casesRoot),
109
+ listCases: optionOrFlag(options, 'listCases', cmd, '--list-cases'),
110
+ failFast: optionOrFlag(options, 'failFast', cmd, '--fail-fast'),
149
111
  run: true
150
112
  };
151
113
  }
152
114
 
153
- function buildRoleAssistantTestEnvelope(roleKey, verdict, portalUrl, result, opts, caseResult = null) {
154
- const report = result.report || {};
155
- const envelope = {
156
- roleKey,
157
- command: 'role-assistant test',
158
- verdict,
159
- portalUrl,
160
- openApiPublished: result.openApiPublished === true,
161
- certification: report.certification || null,
162
- recommendations: Array.isArray(report.recommendations) ? report.recommendations : [],
163
- warnings: Array.isArray(report.warnings) ? report.warnings : [],
164
- stepsRun: result.stepsRun || [],
165
- case: caseResult
166
- ? {
167
- caseId: caseResult.caseId,
168
- sourceFile: caseResult.sourceFile,
169
- passed: caseResult.passed,
170
- lifecycleState: caseResult.lifecycleState,
171
- assertions: caseResult.assertions,
172
- failedAssertions: caseResult.failedAssertions,
173
- warnings: Array.isArray(caseResult.warnings) ? caseResult.warnings : [],
174
- execution: caseResult.execution || null
175
- }
176
- : null
177
- };
178
- if (opts.verbose || opts.debug) {
179
- envelope.report = report;
115
+ function computeRoleAssistantExitCode(verdict, caseResult) {
116
+ if (Array.isArray(caseResult)) {
117
+ if (caseResult.some((row) => row && row.passed === false)) {
118
+ return 2;
119
+ }
120
+ return 0;
180
121
  }
181
- if (opts.debug) {
182
- envelope.debug = {
183
- observation: caseResult ? caseResult.observation : null,
184
- lifecycleResult: result
185
- };
122
+ if (caseResult) {
123
+ return caseResult.passed ? 0 : 2;
124
+ }
125
+ if (verdict === VERDICT.VERIFIED) {
126
+ return 0;
186
127
  }
187
- return envelope;
128
+ return exitCodeFromVerdict(verdict, { partial: verdict === VERDICT.NOT_VERIFIED });
188
129
  }
189
130
 
190
- function displayCaseResultSummary(caseResult) {
191
- if (!caseResult) {
192
- return;
131
+ function listRoleAssistantCases(roleKey, opts) {
132
+ const root = resolveCasesRoot(roleKey, { casesRoot: opts.casesRoot || undefined });
133
+ const rows = discoverSuiteCaseFiles(root, { suite: opts.suite || undefined });
134
+ const filtered = filterDiscoveredCases(rows, opts.case);
135
+ if (opts.json) {
136
+ logger.log(
137
+ JSON.stringify(
138
+ {
139
+ roleKey: bareRoleKey(roleKey),
140
+ command: 'role-assistant test',
141
+ mode: 'list-cases',
142
+ casesRoot: root,
143
+ cases: filtered.map((r) => ({
144
+ suite: r.suite,
145
+ file: r.file,
146
+ path: r.absPath,
147
+ caseIdHint: r.caseIdHint
148
+ }))
149
+ },
150
+ null,
151
+ 2
152
+ )
153
+ );
154
+ } else {
155
+ logger.log(sectionTitle('ROLE ASSISTANT CASES'));
156
+ logger.log(headerKeyValue('Cases root:', root));
157
+ for (const row of filtered) {
158
+ logger.log(`${row.suite}\t${row.file}\t${row.absPath}`);
159
+ }
160
+ logger.log(metadata(`total ${filtered.length}`));
193
161
  }
194
- logger.log(headerKeyValue('Case:', caseResult.caseId));
195
- logger.log(headerKeyValue('Case file:', caseResult.sourceFile));
196
- logger.log(headerKeyValue('Observed state:', caseResult.lifecycleState));
197
- if (caseResult.execution) {
198
- logger.log(headerKeyValue('Execution id:', caseResult.execution.processExecutionId || 'n/a'));
199
- logger.log(headerKeyValue('Final runtime status:', caseResult.execution.finalStatus || 'n/a'));
162
+ return 0;
163
+ }
164
+
165
+ async function resolveSuiteCaseResults(systemKey, opts, result, verdict, portalUrl) {
166
+ const root = resolveCasesRoot(systemKey, { casesRoot: opts.casesRoot || undefined });
167
+ const discovered = discoverSuiteCaseFiles(root, { suite: opts.suite || undefined });
168
+ const selected = filterDiscoveredCases(discovered, opts.case);
169
+ if (selected.length === 0) {
170
+ throw new Error(
171
+ `No role-assistant cases found under ${root}` +
172
+ (opts.suite ? ` suite=${opts.suite}` : '')
173
+ );
200
174
  }
201
- if (caseResult.passed) {
202
- logger.log(formatSuccessLine('Case assertions passed.'));
203
- const warnings = Array.isArray(caseResult.warnings) ? caseResult.warnings : [];
204
- for (const row of warnings) {
205
- logger.log(formatWarningLine(row.message || String(row)));
175
+ const outcomes = [];
176
+ for (const row of selected) {
177
+ const caseOpts = { ...opts, case: row.absPath, testsDir: path.dirname(row.absPath) };
178
+ const one = await resolveRoleAssistantCaseResult(systemKey, caseOpts, result, verdict, portalUrl);
179
+ outcomes.push(one);
180
+ if (opts.failFast && one && one.passed === false) {
181
+ break;
206
182
  }
207
- return;
208
- }
209
- logger.log(formatBlockingError('Case assertions failed.'));
210
- for (const row of caseResult.failedAssertions) {
211
- logger.log(formatWarningLine(row.message));
212
183
  }
184
+ return outcomes;
213
185
  }
214
186
 
215
- function displayRoleAssistantTestSummary(roleKey, verdict, portalUrl, result, caseResult = null) {
216
- logger.log('');
217
- logger.log(sectionTitle(`${String(roleKey).toUpperCase()} ROLE ASSISTANT TEST`));
218
- logger.log('');
219
- logger.log(headerKeyValue('Role key:', roleKey));
220
- if (portalUrl) {
221
- logger.log(headerKeyValue('Portal:', portalUrl));
187
+ async function runRoleAssistantTestAction(roleKey, options, cmd) {
188
+ const opts = normalizeRoleAssistantTestOptions(options, cmd);
189
+ const systemKey = roleAssistantSystemKey(roleKey);
190
+ const displayKey = bareRoleKey(roleKey);
191
+
192
+ if (opts.listCases) {
193
+ return listRoleAssistantCases(systemKey, opts);
194
+ }
195
+
196
+ const result = await runLifecycleForExternalSystem(systemKey, opts);
197
+ const verdict = lifecycleVerdictFromReport(result.report || {});
198
+ const portalUrl = buildExternalSystemUiUrl(result.dataplaneUiBaseUrl, systemKey);
199
+
200
+ let caseResult = null;
201
+ if (opts.suite) {
202
+ caseResult = await resolveSuiteCaseResults(systemKey, opts, result, verdict, portalUrl);
222
203
  } else {
223
- logger.log(formatWarningLine('Portal link unavailable. Configure dataplane public URL and retry.'));
204
+ caseResult = await resolveRoleAssistantCaseResult(systemKey, opts, result, verdict, portalUrl);
224
205
  }
225
- const verdictMessage =
226
- verdict === VERDICT.VERIFIED
227
- ? 'Lifecycle checks passed. Open portal for full execution steps and logs.'
228
- : 'Lifecycle checks are not fully verified. Open portal to inspect blocking steps.';
229
- if (verdict === VERDICT.VERIFIED) {
230
- logger.log(formatSuccessParagraph(verdictMessage));
206
+
207
+ if (opts.json) {
208
+ logger.log(
209
+ JSON.stringify(
210
+ buildRoleAssistantTestEnvelope(displayKey, verdict, portalUrl, result, opts, caseResult),
211
+ null,
212
+ 2
213
+ )
214
+ );
231
215
  } else {
232
- logger.log(formatWarningLine(verdictMessage));
216
+ displayRoleAssistantTestSummary(displayKey, verdict, portalUrl, result, caseResult);
233
217
  }
234
- const steps = Array.isArray(result.stepsRun) ? result.stepsRun : [];
235
- if (steps.length > 0) {
236
- logger.log(metadata(`Steps run: ${formatStepsRunSummary(steps)}`));
218
+
219
+ const exitCode = computeRoleAssistantExitCode(verdict, caseResult);
220
+ if (exitCode !== 0) {
221
+ process.exit(exitCode);
237
222
  }
238
- displayCaseResultSummary(caseResult);
223
+ return exitCode;
239
224
  }
240
225
 
241
226
  async function resolveRoleAssistantCaseResult(roleKey, opts, result, verdict, portalUrl) {
@@ -272,44 +257,6 @@ async function resolveRoleAssistantCaseResult(roleKey, opts, result, verdict, po
272
257
  return evaluation;
273
258
  }
274
259
 
275
- function computeRoleAssistantExitCode(verdict, caseResult) {
276
- if (caseResult) {
277
- return caseResult.passed ? 0 : 2;
278
- }
279
- if (verdict === VERDICT.VERIFIED) {
280
- return 0;
281
- }
282
- return exitCodeFromVerdict(verdict, { partial: verdict === VERDICT.NOT_VERIFIED });
283
- }
284
-
285
- async function runRoleAssistantTestAction(roleKey, options, cmd) {
286
- const opts = normalizeRoleAssistantTestOptions(options, cmd);
287
- // Dataplane ExternalSystem key is always ra-{roleKey}; bare roleKey is display/settings only.
288
- const systemKey = roleAssistantSystemKey(roleKey);
289
- const displayKey = bareRoleKey(roleKey);
290
- const result = await runLifecycleForExternalSystem(systemKey, opts);
291
- const verdict = lifecycleVerdictFromReport(result.report || {});
292
- const portalUrl = buildExternalSystemUiUrl(result.dataplaneUiBaseUrl, systemKey);
293
- const caseResult = await resolveRoleAssistantCaseResult(systemKey, opts, result, verdict, portalUrl);
294
-
295
- if (opts.json) {
296
- logger.log(
297
- JSON.stringify(
298
- buildRoleAssistantTestEnvelope(displayKey, verdict, portalUrl, result, opts, caseResult),
299
- null,
300
- 2
301
- )
302
- );
303
- } else {
304
- displayRoleAssistantTestSummary(displayKey, verdict, portalUrl, result, caseResult);
305
- }
306
-
307
- const exitCode = computeRoleAssistantExitCode(verdict, caseResult);
308
- if (exitCode !== 0) {
309
- process.exit(exitCode);
310
- }
311
- }
312
-
313
260
  async function roleAssistantTestHandler(roleKey, options, cmd) {
314
261
  try {
315
262
  await runRoleAssistantTestAction(roleKey, options, cmd);
@@ -330,6 +277,16 @@ function setupRoleAssistantCommands(program) {
330
277
  .option('-e, --env <env>', 'Environment: dev, tst, or pro', 'dev')
331
278
  .option('--case <caseIdOrPath>', 'Run and evaluate one role-assistant test case')
332
279
  .option('--tests-dir <dir>', 'Override role-assistant test case directory')
280
+ .option(
281
+ '--suite <suiteId>',
282
+ 'Run all YAML cases under tests/role-assistant/ra-<roleKey>/<suiteId>/'
283
+ )
284
+ .option(
285
+ '--cases-root <dir>',
286
+ 'Override cases root (default: tests/role-assistant/ra-<roleKey>/)'
287
+ )
288
+ .option('--list-cases', 'List discoverable case files and exit')
289
+ .option('--fail-fast', 'Stop suite run after the first failing case')
333
290
  .option('-v, --verbose', 'Include full lifecycle report payload in --json output')
334
291
  .option('-d, --debug', 'Write debug logs under role-assistant/<roleKey>/logs/')
335
292
  .option(
@@ -367,5 +324,7 @@ module.exports = {
367
324
  buildRoleAssistantTestEnvelope,
368
325
  computeRoleAssistantExitCode,
369
326
  resolveRoleAssistantCaseResult,
327
+ resolveSuiteCaseResults,
328
+ listRoleAssistantCases,
370
329
  formatStepsRunSummary
371
330
  };
@@ -6,7 +6,7 @@
6
6
  * @version 2.0.0
7
7
  */
8
8
 
9
- const { capabilitiesIncludes } = require('./capability-manifest');
9
+ const { capabilitiesIncludes, capabilityKeyFromEntry } = require('./capability-manifest');
10
10
 
11
11
  /**
12
12
  * @param {object|undefined} operations
@@ -44,6 +44,16 @@ function resolveSingleOpsKey(operations, logicalKey, label) {
44
44
  );
45
45
  }
46
46
 
47
+ /**
48
+ * @param {unknown} entry
49
+ * @param {string} userKey
50
+ * @returns {boolean}
51
+ */
52
+ function capabilityEntryMatches(entry, userKey) {
53
+ const key = capabilityKeyFromEntry(entry);
54
+ return Boolean(key) && key.toLowerCase() === String(userKey).toLowerCase();
55
+ }
56
+
47
57
  /**
48
58
  * For --from on copy: when capabilities[] is non-empty, the name must appear there (case-insensitive).
49
59
  *
@@ -56,14 +66,14 @@ function resolveSourceCapabilityForCopy(doc, userKey) {
56
66
  const u = String(userKey).trim();
57
67
  const caps = doc.capabilities;
58
68
  if (Array.isArray(caps) && caps.length > 0) {
59
- const hit = caps.find((c) => String(c).toLowerCase() === u.toLowerCase());
69
+ const hit = caps.find((c) => capabilityEntryMatches(c, u));
60
70
  if (hit === undefined) {
61
71
  throw new Error(
62
72
  `Source capability "${userKey}" not found in capabilities[]. ` +
63
73
  'Use a name that appears in capabilities[] (case-insensitive match is allowed).'
64
74
  );
65
75
  }
66
- return String(hit);
76
+ return capabilityKeyFromEntry(hit) || u;
67
77
  }
68
78
  return u;
69
79
  }
@@ -79,9 +89,9 @@ function resolveLogicalNameForRemove(doc, userKey) {
79
89
  const u = String(userKey).trim();
80
90
  const caps = doc.capabilities;
81
91
  if (Array.isArray(caps) && caps.length > 0) {
82
- const hit = caps.find((c) => String(c).toLowerCase() === u.toLowerCase());
92
+ const hit = caps.find((c) => capabilityEntryMatches(c, u));
83
93
  if (hit !== undefined) {
84
- return String(hit);
94
+ return capabilityKeyFromEntry(hit) || u;
85
95
  }
86
96
  }
87
97
  return u;
@@ -7,7 +7,11 @@
7
7
  */
8
8
 
9
9
  const { rewriteCapabilityReferences } = require('./reference-rewrite');
10
- const { capabilityKeyFromEntry } = require('./capability-manifest');
10
+ const {
11
+ capabilityKeyFromEntry,
12
+ capabilitiesIncludes,
13
+ buildCapabilityObject
14
+ } = require('./capability-manifest');
11
15
  const { jsonPointerPath } = require('./json-pointer');
12
16
  const { buildBasicExposureProfileArray } = require('./basic-exposure');
13
17
  const {
@@ -256,14 +260,20 @@ function appendCapabilityList(doc, resolvedAs, patchOperations, updatedSections)
256
260
  if (!Array.isArray(doc.capabilities)) {
257
261
  doc.capabilities = [];
258
262
  }
259
- if (doc.capabilities.includes(resolvedAs)) {
263
+ if (capabilitiesIncludes(doc.capabilities, resolvedAs)) {
260
264
  return;
261
265
  }
262
- doc.capabilities.push(resolvedAs);
266
+ const usesObjects = doc.capabilities.some(
267
+ (entry) => entry && typeof entry === 'object' && !Array.isArray(entry)
268
+ );
269
+ const value = usesObjects
270
+ ? buildCapabilityObject(resolvedAs, doc.displayName || doc.key || 'records')
271
+ : resolvedAs;
272
+ doc.capabilities.push(value);
263
273
  patchOperations.push({
264
274
  op: 'add',
265
275
  path: '/capabilities/-',
266
- value: resolvedAs
276
+ value
267
277
  });
268
278
  updatedSections.push(`capabilities[]: ${resolvedAs}`);
269
279
  }