@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.
- package/lib/agent-kit/git-identity.js +180 -0
- package/lib/agent-kit/setup.js +3 -0
- package/lib/agent-kit/start.js +45 -1
- package/lib/api/role-assistant-test-job.api.js +60 -0
- package/lib/api/work-search.api.js +13 -6
- package/lib/app/deploy.js +8 -1
- package/lib/app/show-display.js +1 -0
- package/lib/app/show-online.js +15 -0
- package/lib/build/standard-docker-build.js +4 -1
- package/lib/cli/setup-app.js +14 -1
- package/lib/cli/setup-utility.js +24 -1
- package/lib/commands/role-assistant.js +7 -0
- package/lib/core/env-platform-expand.js +93 -0
- package/lib/core/secrets-env-content.js +37 -3
- package/lib/deployment/installation/azure-infra-stage.js +3 -1
- package/lib/deployment/installation/infra-catalog.js +2 -5
- package/lib/generator/builders.js +17 -0
- package/lib/generator/helpers.js +23 -2
- package/lib/generator/index.js +10 -6
- package/lib/role-assistant/test-cases-search.js +3 -0
- package/lib/role-assistant/test-job-runner.js +125 -0
- package/lib/role-assistant/test-runner-search.js +44 -2
- package/lib/schema/application-schema.json +193 -2
- package/lib/schema/infra-parameter.schema.json +139 -33
- package/lib/schema/infra.parameter.yaml +28 -28
- package/lib/schema/infrastructure-schema.json +10 -35
- package/lib/utils/compose-generate-docker-compose.js +12 -0
- package/lib/utils/docker-build.js +29 -8
- package/lib/utils/docker-manifest-public-port.js +36 -0
- package/lib/utils/platform-resolution.js +226 -0
- package/lib/utils/resolve-docker-image-ref.js +8 -3
- package/package.json +2 -2
- package/templates/README.md +2 -1
- package/templates/agent-kit/agent-kit.yaml +4 -1
- package/templates/agent-kit/instructions/AGENTKIT.md +1 -1
- package/templates/agent-kit/instructions/root.AGENTS.md +2 -0
- package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +33 -1
- package/templates/agent-kit/skills/aifabrix-connected-system/scripts/delivery-verdict.js +176 -0
- package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +3 -0
- package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +10 -3
- package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +22 -0
- package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +21 -0
- package/templates/agent-kit/skills/shared/feedback.md +31 -0
- package/templates/agent-kit/skills/shared/hosts.md +13 -3
- package/templates/agent-kit/skills/shared/status.md +85 -0
- package/templates/agent-kit/workspace/BUILDER_IMPROVEMENT_FINDINGS.md +27 -0
- package/templates/applications/builder-api/application.yaml +1 -1
- package/templates/applications/dataplane/application.yaml +30 -2
- package/templates/applications/dataplane/env.template +37 -4
- package/templates/applications/miso-controller/application.yaml +24 -1
- package/templates/applications/miso-controller/env.template +21 -34
- package/templates/marketplace/main.json +70 -222
- package/templates/python/Dockerfile.hbs +2 -0
- package/templates/typescript/Dockerfile.hbs +2 -0
- 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
|
|
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,
|
|
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
|
-
|
|
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', '
|
|
156
|
-
DOCKER: ['ADMIN_PASSWORD', 'ENCRYPTION_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) {
|
package/lib/generator/helpers.js
CHANGED
|
@@ -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);
|
package/lib/generator/index.js
CHANGED
|
@@ -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
|
-
|
|
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, {
|
|
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
|
|
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(
|
|
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,
|