@aifabrix/builder 2.59.0 → 2.60.0

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 (55) hide show
  1. package/lib/agent-kit/git-identity.js +180 -0
  2. package/lib/agent-kit/setup.js +3 -0
  3. package/lib/agent-kit/start.js +45 -1
  4. package/lib/api/role-assistant-test-job.api.js +60 -0
  5. package/lib/api/work-search.api.js +13 -6
  6. package/lib/app/deploy.js +8 -1
  7. package/lib/app/show-display.js +1 -0
  8. package/lib/app/show-online.js +15 -0
  9. package/lib/build/standard-docker-build.js +4 -1
  10. package/lib/cli/setup-app.js +14 -1
  11. package/lib/cli/setup-utility.js +24 -1
  12. package/lib/commands/role-assistant.js +7 -0
  13. package/lib/core/env-platform-expand.js +93 -0
  14. package/lib/core/secrets-env-content.js +37 -3
  15. package/lib/deployment/installation/azure-infra-stage.js +3 -1
  16. package/lib/deployment/installation/infra-catalog.js +2 -5
  17. package/lib/generator/builders.js +17 -0
  18. package/lib/generator/helpers.js +23 -2
  19. package/lib/generator/index.js +10 -6
  20. package/lib/role-assistant/test-cases-search.js +3 -0
  21. package/lib/role-assistant/test-job-runner.js +125 -0
  22. package/lib/role-assistant/test-runner-search.js +44 -2
  23. package/lib/schema/application-schema.json +193 -2
  24. package/lib/schema/infra-parameter.schema.json +139 -33
  25. package/lib/schema/infra.parameter.yaml +28 -28
  26. package/lib/schema/infrastructure-schema.json +10 -35
  27. package/lib/utils/compose-generate-docker-compose.js +12 -0
  28. package/lib/utils/docker-build.js +29 -8
  29. package/lib/utils/docker-manifest-public-port.js +36 -0
  30. package/lib/utils/platform-resolution.js +226 -0
  31. package/lib/utils/resolve-docker-image-ref.js +8 -3
  32. package/package.json +2 -2
  33. package/templates/README.md +2 -1
  34. package/templates/agent-kit/agent-kit.yaml +4 -1
  35. package/templates/agent-kit/instructions/AGENTKIT.md +1 -1
  36. package/templates/agent-kit/instructions/root.AGENTS.md +2 -0
  37. package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +33 -1
  38. package/templates/agent-kit/skills/aifabrix-connected-system/scripts/delivery-verdict.js +176 -0
  39. package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +3 -0
  40. package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +10 -3
  41. package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +22 -0
  42. package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +21 -0
  43. package/templates/agent-kit/skills/shared/feedback.md +31 -0
  44. package/templates/agent-kit/skills/shared/hosts.md +13 -3
  45. package/templates/agent-kit/skills/shared/status.md +85 -0
  46. package/templates/agent-kit/workspace/BUILDER_IMPROVEMENT_FINDINGS.md +27 -0
  47. package/templates/applications/builder-api/application.yaml +1 -1
  48. package/templates/applications/dataplane/application.yaml +30 -2
  49. package/templates/applications/dataplane/env.template +37 -4
  50. package/templates/applications/miso-controller/application.yaml +24 -1
  51. package/templates/applications/miso-controller/env.template +21 -34
  52. package/templates/marketplace/main.json +70 -222
  53. package/templates/python/Dockerfile.hbs +2 -0
  54. package/templates/typescript/Dockerfile.hbs +2 -0
  55. package/templates/typescript/docker-compose.hbs +12 -0
@@ -44,6 +44,7 @@ const { computeEffectiveEnvironmentScopedResources, redisDbIndexForScopedRunEnv
44
44
  const { applyRedisDbIndexToEnvContent } = require('../utils/redis-env-scope');
45
45
  const { expandDeclarativeUrlsIfPresent } = require('./secrets-env-declarative-expand');
46
46
  const {
47
+ mergeAdditionalPublishedPorts,
47
48
  mergeDockerManifestPublishedPort,
48
49
  rewriteDockerManifestPublicPortEnvLine
49
50
  } = require('../utils/docker-manifest-public-port');
@@ -154,6 +155,12 @@ async function getDockerRedisDbEndpoints() {
154
155
  return { redisHost, redisPort, dbHost, dbPort };
155
156
  }
156
157
 
158
+ /** Host ports clients connect to: the manifest's own, plus any additionalPorts. */
159
+ async function mergePublishedPorts(envVars, appDoc) {
160
+ await mergeDockerManifestPublishedPort(envVars, appDoc);
161
+ await mergeAdditionalPublishedPorts(envVars, appDoc);
162
+ }
163
+
157
164
  /** Docker env transformations: ports, infra endpoints, PORT. */
158
165
  async function applyDockerTransformations(resolved, variablesPath) {
159
166
  resolved = await resolveServicePortsInEnvContent(resolved, 'docker');
@@ -164,7 +171,7 @@ async function applyDockerTransformations(resolved, variablesPath) {
164
171
  const containerPort = getContainerPortFromPath(variablesPath) ?? getContainerPortFromDockerEnv(dockerEnv) ?? 3000;
165
172
  const envVars = await buildEnvVarMap('docker', null, null, { appPort: containerPort });
166
173
  const appDoc = loadVariablesFromPath(variablesPath);
167
- await mergeDockerManifestPublishedPort(envVars, appDoc);
174
+ await mergePublishedPorts(envVars, appDoc);
168
175
  envVars.REDIS_HOST = redisHost;
169
176
  envVars.REDIS_PORT = String(redisPort);
170
177
  envVars.DB_HOST = dbHost;
@@ -187,6 +194,32 @@ async function applyEnvironmentTransformations(resolved, environment, variablesP
187
194
  return resolved;
188
195
  }
189
196
 
197
+ /**
198
+ * Expands manifest `{NAME}` values when the application declares platforms/environments.
199
+ * Local Builder runs pass no resource group, so they resolve as the local platform.
200
+ * @param {string} content - Env content after kv:// resolution
201
+ * @param {string|null} variablesPath - Application manifest path (null in env-only mode)
202
+ * @param {string} runEnvKey - Deployment environment key
203
+ * @param {Object} [options] - `resourceGroup` when a deployment target is known
204
+ * @returns {string}
205
+ */
206
+ function expandPlatformNamesForApp(content, variablesPath, runEnvKey, options = {}) {
207
+ if (!variablesPath) return content;
208
+ const { expandPlatformNamesIfPresent } = require('./env-platform-expand');
209
+ const { loadConfigFile } = require('../utils/config-format');
210
+ let manifest = null;
211
+ try {
212
+ manifest = loadConfigFile(variablesPath);
213
+ } catch {
214
+ return content;
215
+ }
216
+ return expandPlatformNamesIfPresent(content, {
217
+ manifest,
218
+ environment: runEnvKey,
219
+ resourceGroup: options.resourceGroup
220
+ });
221
+ }
222
+
190
223
  /**
191
224
  * Generate .env content from template and secrets (no disk write).
192
225
  * When options.envOnly is true, variablesPath is null (no application config).
@@ -210,15 +243,16 @@ async function generateEnvContent(appName, secretsPath, environment = 'local', f
210
243
  }
211
244
  const secrets = await loadSecrets(secretsPath, appName);
212
245
  const { runEnvKey, effective } = await buildScopedKvContext(appPath, options);
213
- const scopedKv = { envKey: runEnvKey, effective };
214
246
  let resolved = await resolveKvReferences(template, secrets, environment, secretsPaths, appName, {
215
247
  envKey: runEnvKey,
216
248
  effective,
217
249
  envTemplatePath: templatePath
218
250
  });
219
251
  if (!secretsPath) {
220
- await materializeResolvedKvSecretsToUserLocal(template, secrets, scopedKv, options);
252
+ await materializeResolvedKvSecretsToUserLocal(template, secrets, { envKey: runEnvKey, effective }, options);
221
253
  }
254
+ // Platform/environment {NAME} before url://, so a resolved value may carry a declarative URL.
255
+ resolved = expandPlatformNamesForApp(resolved, variablesPath, runEnvKey, options);
222
256
  resolved = await expandDeclarativeUrlsIfPresent(
223
257
  resolved,
224
258
  appName,
@@ -24,7 +24,9 @@ const preflight = require('./azure-preflight');
24
24
  const readiness = require('./azure-readiness');
25
25
  const registryAuth = require('./registry-auth-mode');
26
26
 
27
- const INFRA_SECRETS = Object.freeze(['ADMIN_PASSWORD', 'ENCRYPTION_KEY', 'MORI_CALLBACK_API_KEY']);
27
+ // The Marketplace template stopped accepting moriCallbackApiKey: the installation receives its
28
+ // Mori key at licence registration instead, so infra no longer provisions or projects one.
29
+ const INFRA_SECRETS = Object.freeze(['ADMIN_PASSWORD', 'ENCRYPTION_KEY']);
28
30
 
29
31
  function missingInputError(file, fields) {
30
32
  const error = new Error(
@@ -100,8 +100,6 @@ const PARAMETER_CATALOG = Object.freeze({
100
100
  moriBaseUrl: { kind: 'file', source: 'registration', required: true, description: 'Mori service base URL.' },
101
101
  moriLicensePublicKey: { kind: 'file', source: 'registration', required: false, description: 'Mori licence issuer public key (SPKI PEM); empty leaves the installation unpinned and it then refuses every licence.' },
102
102
  moriLicenseIssuer: { kind: 'file', source: 'registration', required: false, description: 'Expected iss claim on a Mori licence.' },
103
- moriCallbackApiKey: { kind: 'secretEnv', env: 'MORI_CALLBACK_API_KEY', required: true, description: 'Mori callback authentication key.' },
104
- entraCallbackStateSecret: { kind: 'deferred', description: 'Not collected or sent by infra; issued/configured during later Entra provider onboarding.' },
105
103
  entraIdClientId: { kind: 'deferred', description: 'Not collected or sent by infra; issued/configured during later Entra provider onboarding.' },
106
104
  entraIdClientSecret: { kind: 'deferred', description: 'Not collected or sent by infra; issued/configured during later Entra provider onboarding.' },
107
105
  entraIdTenantId: { kind: 'deferred', description: 'Customer Entra provider tenant; distinct from the Azure subscription directory tenant and configured later.' },
@@ -146,14 +144,13 @@ const MORI_SERVICE_KEY = 'mori-controller-api-keyKeyVault';
146
144
  const SECRET_BINDINGS = Object.freeze({
147
145
  ADMIN_PASSWORD: { kv: 'miso-controller-admin-passwordKeyVault', required: true, generate: false, comment: 'Required installation administrator password; same canonical secret used by Miso onboarding.' },
148
146
  ENCRYPTION_KEY: { kv: 'miso-controller-secrets-encryptionKeyVault', required: true, generate: true, comment: 'Application/data encryption key.' },
149
- MORI_CALLBACK_API_KEY: { kv: 'mori-callback-api-keyKeyVault', required: true, generate: true, comment: 'Mori callback credential.' },
150
147
  MISO_API_KEY: { kv: 'miso-controller-secrets-apiKeyVault', required: false, generate: false, comment: 'Temporary Miso deployment key; an existing login may be used instead.' },
151
148
  MORI_API_KEY: { kv: MORI_SERVICE_KEY, required: true, generate: false, comment: 'Mori installation service key stored by DOCKER registration.' }
152
149
  });
153
150
 
154
151
  const ENV_TEMPLATE_KEYS = Object.freeze({
155
- AZURE: ['ADMIN_PASSWORD', 'ENCRYPTION_KEY', 'MORI_CALLBACK_API_KEY', 'MISO_API_KEY'],
156
- DOCKER: ['ADMIN_PASSWORD', 'ENCRYPTION_KEY', 'MORI_CALLBACK_API_KEY', 'MORI_API_KEY']
152
+ AZURE: ['ADMIN_PASSWORD', 'ENCRYPTION_KEY', 'MISO_API_KEY'],
153
+ DOCKER: ['ADMIN_PASSWORD', 'ENCRYPTION_KEY', 'MORI_API_KEY']
157
154
  });
158
155
 
159
156
  /**
@@ -362,6 +362,23 @@ function addSimpleOptionalFields(deployment, variables) {
362
362
  if (variables.required !== undefined && !(Array.isArray(variables.required) && variables.required.length === 0)) {
363
363
  deployment.required = variables.required;
364
364
  }
365
+ addPlatformSections(deployment, variables);
366
+ }
367
+
368
+ /**
369
+ * Carries the platform/environment declarations into the deployment manifest so the Controller can
370
+ * resolve `{NAME}` values per installation, which is the case Builder cannot resolve: one uploaded
371
+ * application serving many customer resource groups.
372
+ * @param {Object} deployment - Deployment manifest (mutated)
373
+ * @param {Object} variables - Application manifest
374
+ */
375
+ function addPlatformSections(deployment, variables) {
376
+ if (Array.isArray(variables.platforms) && variables.platforms.length > 0) {
377
+ deployment.platforms = variables.platforms;
378
+ }
379
+ if (Array.isArray(variables.environments) && variables.environments.length > 0) {
380
+ deployment.environments = variables.environments;
381
+ }
365
382
  }
366
383
 
367
384
  function buildOptionalFields(deployment, variables, rbac) {
@@ -243,6 +243,27 @@ function determineVariableLocation(value) {
243
243
  return value.startsWith('kv://') ? 'keyvault' : 'variable';
244
244
  }
245
245
 
246
+ /**
247
+ * Expands manifest `{NAME}` values into the env template before it becomes configuration items.
248
+ * Lenient by design: with no deployment target the tokens stay in the manifest for the Controller
249
+ * to resolve per installation, which is what the SaaS case needs. A known target resolves fully.
250
+ * @param {string} envTemplate - Raw env template
251
+ * @param {Object|null} variablesConfig - Application manifest
252
+ * @param {Object|null} target - `environment` and `resourceGroup` when known
253
+ * @returns {string}
254
+ */
255
+ function expandManifestNames(envTemplate, variablesConfig, target) {
256
+ const { expandPlatformNamesIfPresent } = require('../core/env-platform-expand');
257
+ const known = target && (target.resourceGroup || target.environment);
258
+ if (!known) return envTemplate;
259
+ return expandPlatformNamesIfPresent(envTemplate, {
260
+ manifest: variablesConfig,
261
+ environment: target.environment,
262
+ resourceGroup: target.resourceGroup,
263
+ strict: Boolean(target.resourceGroup)
264
+ });
265
+ }
266
+
246
267
  /**
247
268
  * Creates a configuration item from parsed variable
248
269
  * @function createConfigItem
@@ -282,9 +303,9 @@ function createConfigItem(key, value, location, applicationConfigMap) {
282
303
  return configItem;
283
304
  }
284
305
 
285
- function parseEnvironmentVariables(envTemplate, variablesConfig = null) {
306
+ function parseEnvironmentVariables(envTemplate, variablesConfig = null, target = null) {
286
307
  const configuration = [];
287
- const lines = envTemplate.split('\n');
308
+ const lines = expandManifestNames(envTemplate, variablesConfig, target).split('\n');
288
309
  const applicationConfigMap = createApplicationConfigMap(variablesConfig);
289
310
  for (const line of lines) {
290
311
  const parsed = parseEnvironmentVariableLine(line);
@@ -175,8 +175,9 @@ function substituteEnvVarsInDeployment(obj, envVarMap, fieldPath = '') {
175
175
  * @throws {Error} If validation fails
176
176
  */
177
177
  function buildAndValidateDeployment(appName, variables, envTemplate, rbac, options = null) {
178
- // Parse environment variables from template and merge portalInput from application config
179
- const configuration = parseEnvironmentVariables(envTemplate, variables);
178
+ // Parse environment variables from template and merge portalInput from application config.
179
+ // A known deployment target resolves manifest {NAME} values; otherwise they stay for the Controller.
180
+ const configuration = parseEnvironmentVariables(envTemplate, variables, options && options.target);
180
181
 
181
182
  // Build deployment manifest (Controller computes deploymentKey from schema)
182
183
  const deployment = builders.buildManifestStructure(appName, variables, configuration, rbac);
@@ -236,7 +237,7 @@ async function buildDeploymentManifestInMemory(appName, options = {}) {
236
237
  ...variables,
237
238
  app: { ...variables.app, version: resolved.version }
238
239
  };
239
- const configuration = parseEnvironmentVariables(envTemplate, variables);
240
+ const configuration = parseEnvironmentVariables(envTemplate, variables, options && options.target);
240
241
  const deployment = builders.buildManifestStructure(appName, variablesWithVersion, configuration, rbac);
241
242
 
242
243
  const effectivePort = getEffectivePortForSubstitution(variablesWithVersion, 3000);
@@ -290,7 +291,7 @@ async function writeExternalDeployJson(appName, appPath, options) {
290
291
  * @param {string} appType - Application type
291
292
  * @returns {Promise<string>} Path to written deploy JSON
292
293
  */
293
- async function writeRegularDeployJson(appName, appPath, appType) {
294
+ async function writeRegularDeployJson(appName, appPath, appType, options = {}) {
294
295
  const { variables, envTemplate, rbac, jsonPath } = loadDeploymentConfigFiles(appPath, appType, appName);
295
296
  const resolved = await resolveVersionForApp(appName, variables, { updateBuilder: false });
296
297
  const variablesWithVersion = {
@@ -299,7 +300,10 @@ async function writeRegularDeployJson(appName, appPath, appType) {
299
300
  };
300
301
  const effectivePort = getEffectivePortForSubstitution(variablesWithVersion, 3000);
301
302
  const envVarMap = await buildEnvVarMap('docker', null, null, { appPort: effectivePort });
302
- const deployment = buildAndValidateDeployment(appName, variablesWithVersion, envTemplate, rbac, { envVarMap });
303
+ const deployment = buildAndValidateDeployment(appName, variablesWithVersion, envTemplate, rbac, {
304
+ envVarMap,
305
+ target: options.target
306
+ });
303
307
  const jsonContent = JSON.stringify(deployment, null, 2);
304
308
  fs.writeFileSync(jsonPath, jsonContent, { mode: 0o644 });
305
309
  return jsonPath;
@@ -333,7 +337,7 @@ async function generateDeployJson(appName, options = {}) {
333
337
  if (isExternal) {
334
338
  return writeExternalDeployJson(appName, appPath, options);
335
339
  }
336
- return await writeRegularDeployJson(appName, appPath, appType);
340
+ return await writeRegularDeployJson(appName, appPath, appType, options);
337
341
  }
338
342
 
339
343
  async function generateDeployJsonWithValidation(appName, options = {}) {
@@ -142,6 +142,9 @@ function evaluateSearchExpectations(expectedSearch, observation) {
142
142
 
143
143
  function skillsResourceEntry(skillsPayload, resourceType) {
144
144
  const root = skillsEnvelope(skillsPayload) || asObject(skillsPayload);
145
+ if (String(root.resourceType || '').trim() === resourceType) {
146
+ return root;
147
+ }
145
148
  const rts = asObject(asObject(root).search).resourceTypes;
146
149
  return asObject(asObject(rts)[resourceType]);
147
150
  }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Run one published Role Assistant case as an explicitly selected subject.
3
+ *
4
+ * @fileoverview Async test-job isolation for the Role Assistant CLI
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const { resolveControllerUrl } = require('../utils/controller-url');
10
+ const { resolveUserIdOrEmail } = require('../identity/resolve-user-id');
11
+ const {
12
+ enqueueRoleAssistantTestJob,
13
+ getRoleAssistantTestJob
14
+ } = require('../api/role-assistant-test-job.api');
15
+ const { roleAssistantSystemKey } = require('../utils/role-assistant-paths');
16
+
17
+ function selectedSubject(opts) {
18
+ const ids = Array.isArray(opts.subjectUserId)
19
+ ? opts.subjectUserId.map((value) => String(value || '').trim()).filter(Boolean)
20
+ : [];
21
+ const emails = Array.isArray(opts.subjectEmail)
22
+ ? opts.subjectEmail.map((value) => String(value || '').trim()).filter(Boolean)
23
+ : [];
24
+ if (ids.length + emails.length > 1) {
25
+ throw new Error('Role Assistant case execution accepts exactly one isolation subject.');
26
+ }
27
+ return { userId: ids[0] || '', email: emails[0] || '' };
28
+ }
29
+
30
+ function hasRoleAssistantTestSubject(opts) {
31
+ const subject = selectedSubject(opts);
32
+ return Boolean(subject.userId || subject.email);
33
+ }
34
+
35
+ async function resolveSubjectUserId(opts, authConfig) {
36
+ const subject = selectedSubject(opts);
37
+ if (subject.userId) {
38
+ return subject.userId;
39
+ }
40
+ const controllerUrl = await resolveControllerUrl();
41
+ return resolveUserIdOrEmail(controllerUrl, authConfig, subject.email);
42
+ }
43
+
44
+ function waitFor(milliseconds) {
45
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
46
+ }
47
+
48
+ async function pollJob(roleKey, authContext, jobId, opts) {
49
+ const timeoutMs = Number(opts.pollTimeoutMs) || 180000;
50
+ const intervalMs = Number(opts.pollIntervalMs) || 750;
51
+ const startedAt = Date.now();
52
+ let job = null;
53
+ while (Date.now() - startedAt < timeoutMs) {
54
+ job = await getRoleAssistantTestJob(
55
+ authContext.dataplaneUrl,
56
+ authContext.authConfig,
57
+ roleAssistantSystemKey(roleKey),
58
+ jobId
59
+ );
60
+ const status = String(job?.status || '').trim().toLowerCase();
61
+ if (['completed', 'failed', 'cancelled'].includes(status)) {
62
+ return job;
63
+ }
64
+ await waitFor(intervalMs);
65
+ }
66
+ throw new Error(`Role Assistant test job ${jobId} did not finish within ${timeoutMs} ms.`);
67
+ }
68
+
69
+ function evaluationFromJobCase(roleCase, job, row, fallbackJobId = null) {
70
+ const assertions = Array.isArray(row?.assertions)
71
+ ? row.assertions.map((assertion) => ({
72
+ ...assertion,
73
+ passed: String(assertion?.kind || '').toLowerCase() !== 'fail'
74
+ }))
75
+ : [];
76
+ return {
77
+ caseId: row?.caseId || roleCase.caseId,
78
+ sourceFile: roleCase.sourceFile || null,
79
+ passed: row?.passed === true,
80
+ lifecycleState: row?.lifecycleState || String(job?.status || 'failed'),
81
+ assertions,
82
+ failedAssertions: assertions.filter((assertion) => assertion.passed === false),
83
+ warnings: assertions.filter((assertion) => assertion.kind === 'warn'),
84
+ execution: {
85
+ processExecutionId: row?.processExecutionId || null,
86
+ finalStatus: row?.lifecycleState || job?.status || null,
87
+ waitReason: null,
88
+ interactions: [],
89
+ workResultError: job?.errorMessage || null,
90
+ testJobId: job?.jobId || job?.id || fallbackJobId || null
91
+ }
92
+ };
93
+ }
94
+
95
+ async function runPublishedCaseAsSubject(roleKey, roleCase, opts, authContext) {
96
+ const subjectUserId = await resolveSubjectUserId(opts, authContext.authConfig);
97
+ const accepted = await enqueueRoleAssistantTestJob(
98
+ authContext.dataplaneUrl,
99
+ authContext.authConfig,
100
+ roleAssistantSystemKey(roleKey),
101
+ { caseId: roleCase.caseId, subjectUserId }
102
+ );
103
+ const jobId = String(accepted?.jobId || '').trim();
104
+ if (!jobId) {
105
+ throw new Error('Role Assistant test job enqueue did not return jobId.');
106
+ }
107
+ const job = await pollJob(roleKey, authContext, jobId, opts);
108
+ const row = Array.isArray(job?.cases)
109
+ ? job.cases.find((candidate) => candidate?.caseId === roleCase.caseId)
110
+ : null;
111
+ if (!row) {
112
+ throw new Error(
113
+ job?.errorMessage ||
114
+ `Published Role Assistant case '${roleCase.caseId}' returned no result in job ${jobId}.`
115
+ );
116
+ }
117
+ return evaluationFromJobCase(roleCase, job, row, jobId);
118
+ }
119
+
120
+ module.exports = {
121
+ hasRoleAssistantTestSubject,
122
+ runPublishedCaseAsSubject,
123
+ evaluationFromJobCase,
124
+ selectedSubject
125
+ };
@@ -17,7 +17,7 @@ const {
17
17
  wantsSkillsOnlyPath
18
18
  } = require('./test-runner-workhub-helpers');
19
19
 
20
- function operationKeys(spec) {
20
+ function directOperationKeys(spec) {
21
21
  const raw = Array.isArray(spec && spec.operations) ? spec.operations : [];
22
22
  const keys = [];
23
23
  for (const item of raw) {
@@ -33,6 +33,42 @@ function operationKeys(spec) {
33
33
  return keys;
34
34
  }
35
35
 
36
+ function operationClassKeys(spec) {
37
+ const classes = spec && typeof spec.operationClasses === 'object'
38
+ ? spec.operationClasses
39
+ : {};
40
+ const keys = [];
41
+ for (const rows of Object.values(classes)) {
42
+ if (!Array.isArray(rows)) {
43
+ continue;
44
+ }
45
+ for (const row of rows) {
46
+ const key = String(row || '').trim();
47
+ if (key && !keys.includes(key)) {
48
+ keys.push(key);
49
+ }
50
+ }
51
+ }
52
+ return keys;
53
+ }
54
+
55
+ function operationKeys(spec) {
56
+ return [...new Set([...directOperationKeys(spec), ...operationClassKeys(spec)])];
57
+ }
58
+
59
+ function skillsRequestOptions(testCase) {
60
+ if (!wantsSkillsOnlyPath(testCase)) {
61
+ return undefined;
62
+ }
63
+ const input = resolveInput(testCase);
64
+ const resourceType = String(input.resourceType || '').trim();
65
+ const operation = String(input.operation || '').trim();
66
+ if (operation) {
67
+ return { detail: 'operation', resourceType, operation };
68
+ }
69
+ return resourceType ? { detail: 'resource', resourceType } : undefined;
70
+ }
71
+
36
72
  function pickResourceType(skillsPayload, operation) {
37
73
  const rts = resourceTypesMap(skillsPayload);
38
74
  const keys = Object.keys(rts);
@@ -139,7 +175,12 @@ async function runSearchCaseLifecycle(roleKey, testCase, runtimeContext) {
139
175
  throw new Error('Role assistant search case requires dataplane auth context.');
140
176
  }
141
177
  const roleRef = resolveRoleRef(roleKey, testCase);
142
- const skillsRes = await getWorkSkills(dataplaneUrl, authConfig, roleRef);
178
+ const skillsRes = await getWorkSkills(
179
+ dataplaneUrl,
180
+ authConfig,
181
+ roleRef,
182
+ skillsRequestOptions(testCase)
183
+ );
143
184
  if (wantsSkillsOnlyPath(testCase)) {
144
185
  return searchLifecycleResult(roleKey, testCase, roleRef, skillsRes, null, null);
145
186
  }
@@ -157,6 +198,7 @@ module.exports = {
157
198
  wantsSearchPath,
158
199
  wantsSkillsOnlyPath,
159
200
  operationKeys,
201
+ skillsRequestOptions,
160
202
  pickResourceType,
161
203
  resolveRoleRef,
162
204
  buildSearchArguments,