@aifabrix/builder 2.59.0 → 2.61.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -11
- package/docs/README.md +80 -0
- package/docs/builder-help/evidence-patterns.json +155 -0
- package/docs/builder-help/golden-examples/crm-company.json +30 -0
- package/docs/builder-help/golden-examples/crm-deal.json +29 -0
- package/docs/builder-help/golden-examples/document-storage-keyed-get.json +85 -0
- package/docs/builder-help/golden-examples/document-storage.json +30 -0
- package/docs/builder-help/golden-examples/meeting-transcript.json +29 -0
- package/docs/builder-help/golden-examples/repository-template.json +29 -0
- package/docs/builder-help/golden-examples/service-ticket.json +29 -0
- package/docs/builder-help/platform-roles.json +98 -0
- package/docs/builder-help/resource-type-catalog.json +402 -0
- 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/configuration.api.js +131 -0
- package/lib/api/role-assistant-test-job.api.js +60 -0
- package/lib/api/system-secrets.api.js +72 -0
- package/lib/api/work-search.api.js +13 -6
- package/lib/app/deploy.js +8 -1
- package/lib/app/run-docker-fallback.js +6 -1
- package/lib/app/run-helpers.js +2 -1
- package/lib/app/run-parameter-sync.js +142 -0
- package/lib/app/show-display.js +1 -0
- package/lib/app/show-online.js +15 -0
- package/lib/build/docker-build-args.js +5 -2
- package/lib/build/index.js +3 -2
- package/lib/build/standard-docker-build.js +6 -3
- package/lib/cli/setup-app.js +19 -1
- package/lib/cli/setup-environment.js +156 -0
- package/lib/cli/setup-utility.js +24 -1
- package/lib/commands/datasource-capability-upsert-cli.js +112 -0
- package/lib/commands/datasource-capability.js +4 -2
- package/lib/commands/env-secret-context.js +113 -0
- package/lib/commands/env-secret-list.js +200 -0
- package/lib/commands/env-secret-push-confirm.js +54 -0
- package/lib/commands/env-secret-push-run.js +227 -0
- package/lib/commands/env-secret-push.js +168 -0
- package/lib/commands/repair-datasource-apply.js +2 -0
- package/lib/commands/repair-datasource-keyed-document.js +122 -0
- package/lib/commands/repair-datasource-run.js +1 -0
- package/lib/commands/role-assistant.js +7 -0
- package/lib/commands/setup-modes.js +1 -8
- package/lib/commands/setup-prompts.js +2 -180
- package/lib/commands/verify-operations-skip-e2e.js +25 -1
- package/lib/commands/verify-operations-steps.js +13 -1
- package/lib/commands/wizard-config-normalizer.js +7 -4
- package/lib/commands/wizard-core.js +5 -157
- package/lib/commands/wizard-file-saving.js +163 -0
- package/lib/core/env-platform-expand.js +97 -0
- package/lib/core/secrets-env-content.js +42 -4
- package/lib/core/secrets-env-write.js +10 -3
- package/lib/core/secrets-load.js +4 -2
- package/lib/datasource/binary-documents-validator.js +190 -0
- package/lib/datasource/capability/run-capability-upsert.js +202 -0
- package/lib/datasource/capability/upsert-ingredients.js +291 -0
- package/lib/datasource/capability/upsert-operations.js +138 -0
- package/lib/datasource/capability/upsert-test-scaffold.js +189 -0
- package/lib/datasource/validate.js +12 -5
- 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 +13 -6
- package/lib/lifecycle/product-model.js +4 -3
- package/lib/lifecycle/report-display.js +3 -2
- package/lib/parameters/infra-parameter-catalog.js +1 -1
- package/lib/programmatic/builder-help-enterprise-sync-fabrix.js +1 -1
- package/lib/programmatic/builder-help-governance.js +1 -1
- package/lib/programmatic/builder-help.js +1 -1
- 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/role-assistant/test-runner-workhub-answers.js +4 -1
- package/lib/role-assistant/test-runner-workhub-missing-fields.js +42 -0
- package/lib/role-assistant/test-runner-workhub-wait-stop.js +94 -0
- package/lib/role-assistant/test-runner-workhub.js +28 -19
- package/lib/schema/application-schema.json +205 -2
- package/lib/schema/external-datasource.schema.json +23 -3
- package/lib/schema/infra-parameter.schema.json +139 -33
- package/lib/schema/infra.parameter.yaml +416 -66
- package/lib/schema/infrastructure-schema.json +10 -35
- package/lib/utils/compose-generate-docker-compose.js +16 -9
- package/lib/utils/datasource-binary-evidence.js +92 -0
- package/lib/utils/datasource-test-run-capability-scope.js +44 -1
- package/lib/utils/datasource-test-run-debug-display.js +2 -0
- package/lib/utils/datasource-test-run-display.js +8 -2
- package/lib/utils/datasource-test-run-issue-guidance.js +176 -0
- package/lib/utils/datasource-test-run-tty-log.js +2 -0
- package/lib/utils/docker-build.js +29 -8
- package/lib/utils/docker-manifest-public-port.js +36 -0
- package/lib/utils/env-copy.js +11 -10
- package/lib/utils/external-system-system-test-tty.js +3 -2
- package/lib/utils/image-tags.js +2 -2
- package/lib/utils/platform-kv-ref.js +1 -1
- package/lib/utils/platform-resolution.js +226 -0
- package/lib/utils/prepare-local-data-mount.js +58 -0
- package/lib/utils/resolve-docker-image-ref.js +8 -3
- package/lib/utils/secrets-helpers.js +0 -1
- package/lib/utils/system-secret-mapping.js +125 -0
- package/lib/utils/test-log-writer.js +2 -1
- package/lib/validation/external-manifest-validator.js +5 -0
- package/lib/validation/openapi-contract-surface-validator.js +3 -1
- package/lib/validation/validate-external-file.js +5 -1
- package/package.json +5 -4
- 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 +35 -2
- package/templates/agent-kit/skills/aifabrix-connected-system/references/delivery-gates.md +15 -0
- package/templates/agent-kit/skills/aifabrix-connected-system/scripts/delivery-verdict.js +176 -0
- package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +6 -2
- package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +28 -2
- package/templates/agent-kit/skills/aifabrix-prove/references/evidence-lifecycle.md +22 -0
- package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +28 -1
- package/templates/agent-kit/skills/aifabrix-role-assistant/references/testing-playbook.md +117 -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/interaction.md +83 -0
- package/templates/agent-kit/skills/shared/status.md +76 -0
- package/templates/agent-kit/workspace/BUILDER_IMPROVEMENT_FINDINGS.md +27 -0
- package/templates/applications/builder-api/application.yaml +1 -1
- package/templates/applications/builder-api/env.template +5 -1
- package/templates/applications/dataplane/application.yaml +30 -2
- package/templates/applications/dataplane/env.template +36 -5
- package/templates/applications/keycloak/application.yaml +6 -1
- package/templates/applications/miso-controller/application.yaml +96 -1
- package/templates/applications/miso-controller/env.template +63 -37
- package/templates/applications/miso-controller/rbac.yaml +17 -0
- package/templates/external-system/external-datasource.yaml.hbs +7 -1
- package/templates/marketplace/main.json +73 -225
- package/templates/python/Dockerfile.hbs +2 -0
- package/templates/python/docker-compose.hbs +1 -1
- package/templates/typescript/Dockerfile.hbs +2 -0
- package/templates/typescript/docker-compose.hbs +12 -0
- package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +0 -34
- /package/{lib/programmatic/help-content → docs/builder-help/content}/channel-onboarding.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/cip-overview.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/connected-system-ui.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/dimensions-guide.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/enterprise-sync-fabrix.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/overview.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/subscription-guide.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/workflow.md +0 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a no-duplicate upsert proof from author-owned scenario facts.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Safe testPayload scaffolding for datasource capability upsert
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const { deepClone } = require('./copy-operations');
|
|
10
|
+
|
|
11
|
+
const RUN_TOKEN = /\{\{\s*testRunId\s*\}\}/i;
|
|
12
|
+
|
|
13
|
+
function scenarioInput(scenario) {
|
|
14
|
+
const input = scenario?.input;
|
|
15
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) return null;
|
|
16
|
+
const properties = input.properties;
|
|
17
|
+
return properties && typeof properties === 'object' && !Array.isArray(properties)
|
|
18
|
+
? properties
|
|
19
|
+
: input;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function scalar(value) {
|
|
23
|
+
return value !== null && ['string', 'number', 'boolean'].includes(typeof value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function identityFromInput(input, identityFields) {
|
|
27
|
+
if (!input) return null;
|
|
28
|
+
const entries = identityFields.map((field) => [field, input[field]]);
|
|
29
|
+
if (entries.some(([, value]) => !scalar(value) || value === '')) return null;
|
|
30
|
+
return Object.fromEntries(entries);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sameIdentity(first, second, identityFields) {
|
|
34
|
+
return identityFields.every((field) => first[field] === second[field]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isCollisionSafe(identity) {
|
|
38
|
+
return Object.values(identity).some((value) =>
|
|
39
|
+
typeof value === 'string' && RUN_TOKEN.test(value)
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function profileFields(doc, targetKey) {
|
|
44
|
+
const profile = doc.exposed?.profiles?.[targetKey];
|
|
45
|
+
if (Array.isArray(profile)) return profile;
|
|
46
|
+
return Array.isArray(profile?.attributes) ? profile.attributes : [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function changedPublishedField(doc, targetKey, first, second, identityFields) {
|
|
50
|
+
const identity = new Set(identityFields);
|
|
51
|
+
const published = new Set(profileFields(doc, targetKey));
|
|
52
|
+
return Object.keys(first).find((field) =>
|
|
53
|
+
!identity.has(field) && published.has(field) && scalar(first[field]) &&
|
|
54
|
+
scalar(second[field]) && first[field] !== second[field]
|
|
55
|
+
) || null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function analyzeWritePair(doc, targetKey, identityFields, firstRow, secondRow) {
|
|
59
|
+
const first = scenarioInput(firstRow);
|
|
60
|
+
const second = scenarioInput(secondRow);
|
|
61
|
+
const firstIdentity = identityFromInput(first, identityFields);
|
|
62
|
+
const secondIdentity = identityFromInput(second, identityFields);
|
|
63
|
+
if (!firstIdentity || !secondIdentity) return { reason: 'both writes must contain every business identity field' };
|
|
64
|
+
if (!sameIdentity(firstIdentity, secondIdentity, identityFields)) {
|
|
65
|
+
return { reason: 'the two writes must use the same business identity values' };
|
|
66
|
+
}
|
|
67
|
+
if (!isCollisionSafe(firstIdentity)) {
|
|
68
|
+
return { reason: 'the business identity must include {{testRunId}} to isolate each live run' };
|
|
69
|
+
}
|
|
70
|
+
const changedField = changedPublishedField(
|
|
71
|
+
doc, targetKey, first, second, identityFields
|
|
72
|
+
);
|
|
73
|
+
if (!changedField) {
|
|
74
|
+
return { reason: 'the second write must change one published non-identity scalar field' };
|
|
75
|
+
}
|
|
76
|
+
return { first, second, identity: firstIdentity, changedField };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function exactLookup(operation, input, expectedRows, identityKey) {
|
|
80
|
+
return {
|
|
81
|
+
operation,
|
|
82
|
+
input: deepClone(input),
|
|
83
|
+
identityKey,
|
|
84
|
+
matchMode: 'exactIdentities',
|
|
85
|
+
expectedRows: deepClone(expectedRows)
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function expectedRow(identity, changedField, value) {
|
|
90
|
+
return { ...deepClone(identity), [changedField]: value };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function hasExactProof(scenarios, index, targetKey, lookupKey, facts) {
|
|
94
|
+
const rows = scenarios.slice(index, index + 5);
|
|
95
|
+
if (rows.length !== 5) return false;
|
|
96
|
+
const operations = rows.map((row) => row?.operation);
|
|
97
|
+
if (operations.join('|') !== [lookupKey, targetKey, lookupKey, targetKey, lookupKey].join('|')) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
const [before, , afterFirst, , afterSecond] = rows;
|
|
101
|
+
return [before, afterFirst, afterSecond].every((row) =>
|
|
102
|
+
row?.matchMode === 'exactIdentities' && row?.identityKey === facts.changedField
|
|
103
|
+
) && Array.isArray(before.expectedRows) && before.expectedRows.length === 0 &&
|
|
104
|
+
afterFirst.expectedRows?.length === 1 && afterSecond.expectedRows?.length === 1 &&
|
|
105
|
+
afterFirst.expectedRows?.[0]?.[facts.changedField] === facts.first[facts.changedField] &&
|
|
106
|
+
afterSecond.expectedRows?.[0]?.[facts.changedField] === facts.second[facts.changedField];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function findExistingProof(doc, scenarios, targetKey, lookupKey, identityFields) {
|
|
110
|
+
for (let index = 0; index <= scenarios.length - 5; index += 1) {
|
|
111
|
+
const facts = analyzeWritePair(
|
|
112
|
+
doc, targetKey, identityFields, scenarios[index + 1], scenarios[index + 3]
|
|
113
|
+
);
|
|
114
|
+
if (!facts.reason && hasExactProof(scenarios, index, targetKey, lookupKey, facts)) return true;
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function findAdjacentWrites(doc, scenarios, targetKey, identityFields) {
|
|
120
|
+
let lastReason = 'two adjacent enabled upsert scenarios are required';
|
|
121
|
+
for (let index = 0; index < scenarios.length - 1; index += 1) {
|
|
122
|
+
const first = scenarios[index];
|
|
123
|
+
const second = scenarios[index + 1];
|
|
124
|
+
if (first?.operation !== targetKey || second?.operation !== targetKey) continue;
|
|
125
|
+
if (first.enabled === false || second.enabled === false) continue;
|
|
126
|
+
const facts = analyzeWritePair(doc, targetKey, identityFields, first, second);
|
|
127
|
+
if (!facts.reason) return { index, facts };
|
|
128
|
+
lastReason = facts.reason;
|
|
129
|
+
}
|
|
130
|
+
return { reason: lastReason };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function insufficientIssue(reason) {
|
|
134
|
+
return {
|
|
135
|
+
code: 'UPSERT-TEST-DATA-INSUFFICIENT',
|
|
136
|
+
severity: 'blocking',
|
|
137
|
+
ownership: 'test-payload',
|
|
138
|
+
aiFixable: false,
|
|
139
|
+
message: `Safe --test scaffolding was not generated: ${reason}; no values were fabricated`,
|
|
140
|
+
consequence: 'A weaker generated case could provide false no-duplicate confidence.',
|
|
141
|
+
remediation: ['Author two adjacent upsert inputs with the same isolated identity and one changed published field, then rerun --test.'],
|
|
142
|
+
prevention: 'Keep collision-safe two-write facts with the datasource test payload.',
|
|
143
|
+
residualRisk: 'Upsert remains unproven until the exact live chain passes.'
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function buildProofRows(lookupKey, facts) {
|
|
148
|
+
const firstExpected = expectedRow(
|
|
149
|
+
facts.identity, facts.changedField, facts.first[facts.changedField]
|
|
150
|
+
);
|
|
151
|
+
const secondExpected = expectedRow(
|
|
152
|
+
facts.identity, facts.changedField, facts.second[facts.changedField]
|
|
153
|
+
);
|
|
154
|
+
return [
|
|
155
|
+
exactLookup(lookupKey, facts.identity, [], facts.changedField),
|
|
156
|
+
exactLookup(lookupKey, facts.identity, [firstExpected], facts.changedField),
|
|
157
|
+
exactLookup(lookupKey, facts.identity, [secondExpected], facts.changedField)
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function scaffoldUpsertTests(originalDoc, analysis, targetKey = 'upsert') {
|
|
162
|
+
const scenarios = originalDoc.testPayload?.scenarios;
|
|
163
|
+
const lookupKey = analysis?.selectedLookup?.operationKey;
|
|
164
|
+
const identities = Array.isArray(analysis?.identityFields) ? analysis.identityFields : [];
|
|
165
|
+
if (!Array.isArray(scenarios) || !lookupKey || identities.length === 0) {
|
|
166
|
+
return { changed: false, issue: insufficientIssue('required scenarios or exact lookup facts are missing') };
|
|
167
|
+
}
|
|
168
|
+
if (findExistingProof(originalDoc, scenarios, targetKey, lookupKey, identities)) {
|
|
169
|
+
return { changed: false, doc: originalDoc, patchOperations: [], updatedSections: [] };
|
|
170
|
+
}
|
|
171
|
+
const pair = findAdjacentWrites(originalDoc, scenarios, targetKey, identities);
|
|
172
|
+
if (pair.reason) return { changed: false, issue: insufficientIssue(pair.reason) };
|
|
173
|
+
const [before, afterFirst, afterSecond] = buildProofRows(lookupKey, pair.facts);
|
|
174
|
+
const doc = deepClone(originalDoc);
|
|
175
|
+
doc.testPayload.scenarios.splice(pair.index, 2,
|
|
176
|
+
before, scenarios[pair.index], afterFirst, scenarios[pair.index + 1], afterSecond);
|
|
177
|
+
return {
|
|
178
|
+
changed: true,
|
|
179
|
+
doc,
|
|
180
|
+
patchOperations: [{ op: 'replace', path: '/testPayload/scenarios', value: doc.testPayload.scenarios }],
|
|
181
|
+
updatedSections: ['testPayload.scenarios: upsert no-duplicate proof']
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = {
|
|
186
|
+
scaffoldUpsertTests,
|
|
187
|
+
analyzeWritePair,
|
|
188
|
+
insufficientIssue
|
|
189
|
+
};
|
|
@@ -23,6 +23,8 @@ const { validateFieldReferences } = require('./field-reference-validator');
|
|
|
23
23
|
const { validateAbac } = require('./abac-validator');
|
|
24
24
|
const { buildDatasourceValidateSummary } = require('./datasource-validate-summary');
|
|
25
25
|
|
|
26
|
+
const { validateBinaryDocumentsContract } = require('./binary-documents-validator');
|
|
27
|
+
|
|
26
28
|
const DATASOURCE_CONFIG_EXTENSIONS = new Set(['.json', '.yaml', '.yml']);
|
|
27
29
|
const EXCLUDE_CONFIG_NAMES = new Set([
|
|
28
30
|
'application.json',
|
|
@@ -242,12 +244,14 @@ async function validateDatasourceFile(filePathOrKey) {
|
|
|
242
244
|
};
|
|
243
245
|
}
|
|
244
246
|
|
|
247
|
+
const binary = validateBinaryDocumentsContract(parsed);
|
|
248
|
+
const warnings = binary.warnings.map(issue => `${issue.code}: ${issue.message}`);
|
|
245
249
|
const postSchemaErrors = collectPostSchemaErrors(parsed);
|
|
246
250
|
if (postSchemaErrors.length > 0) {
|
|
247
251
|
return {
|
|
248
252
|
valid: false,
|
|
249
253
|
errors: postSchemaErrors,
|
|
250
|
-
warnings
|
|
254
|
+
warnings,
|
|
251
255
|
resolvedPath: filePath,
|
|
252
256
|
summary
|
|
253
257
|
};
|
|
@@ -256,7 +260,7 @@ async function validateDatasourceFile(filePathOrKey) {
|
|
|
256
260
|
return {
|
|
257
261
|
valid: true,
|
|
258
262
|
errors: [],
|
|
259
|
-
warnings
|
|
263
|
+
warnings,
|
|
260
264
|
resolvedPath: filePath,
|
|
261
265
|
summary
|
|
262
266
|
};
|
|
@@ -269,7 +273,8 @@ async function validateDatasourceFile(filePathOrKey) {
|
|
|
269
273
|
function collectPostSchemaErrors(parsed) {
|
|
270
274
|
return [
|
|
271
275
|
...validateFieldReferences(parsed),
|
|
272
|
-
...validateAbac(parsed)
|
|
276
|
+
...validateAbac(parsed),
|
|
277
|
+
...validateBinaryDocumentsContract(parsed).errors.map(issue => `${issue.code}: ${issue.message}`)
|
|
273
278
|
];
|
|
274
279
|
}
|
|
275
280
|
|
|
@@ -303,12 +308,14 @@ function validateDatasourceParsed(parsed) {
|
|
|
303
308
|
};
|
|
304
309
|
}
|
|
305
310
|
|
|
311
|
+
const binary = validateBinaryDocumentsContract(parsed);
|
|
312
|
+
const warnings = binary.warnings.map(issue => `${issue.code}: ${issue.message}`);
|
|
306
313
|
const postSchemaErrors = collectPostSchemaErrors(parsed);
|
|
307
314
|
if (postSchemaErrors.length > 0) {
|
|
308
315
|
return {
|
|
309
316
|
valid: false,
|
|
310
317
|
errors: postSchemaErrors,
|
|
311
|
-
warnings
|
|
318
|
+
warnings,
|
|
312
319
|
summary
|
|
313
320
|
};
|
|
314
321
|
}
|
|
@@ -316,7 +323,7 @@ function validateDatasourceParsed(parsed) {
|
|
|
316
323
|
return {
|
|
317
324
|
valid: true,
|
|
318
325
|
errors: [],
|
|
319
|
-
warnings
|
|
326
|
+
warnings,
|
|
320
327
|
summary
|
|
321
328
|
};
|
|
322
329
|
}
|
|
@@ -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
|
@@ -22,6 +22,7 @@ const { generateControllerManifest } = require('./external-controller-manifest')
|
|
|
22
22
|
const { resolveVersionForApp } = require('../utils/image-version');
|
|
23
23
|
const { getContainerPort } = require('../utils/port-resolver');
|
|
24
24
|
const { buildEnvVarMap } = require('../utils/env-map');
|
|
25
|
+
const { mergeAdditionalPublishedPorts } = require('../utils/docker-manifest-public-port');
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* Generates deployment JSON from application configuration files
|
|
@@ -175,8 +176,9 @@ function substituteEnvVarsInDeployment(obj, envVarMap, fieldPath = '') {
|
|
|
175
176
|
* @throws {Error} If validation fails
|
|
176
177
|
*/
|
|
177
178
|
function buildAndValidateDeployment(appName, variables, envTemplate, rbac, options = null) {
|
|
178
|
-
// Parse environment variables from template and merge portalInput from application config
|
|
179
|
-
|
|
179
|
+
// Parse environment variables from template and merge portalInput from application config.
|
|
180
|
+
// A known deployment target resolves manifest {NAME} values; otherwise they stay for the Controller.
|
|
181
|
+
const configuration = parseEnvironmentVariables(envTemplate, variables, options && options.target);
|
|
180
182
|
|
|
181
183
|
// Build deployment manifest (Controller computes deploymentKey from schema)
|
|
182
184
|
const deployment = builders.buildManifestStructure(appName, variables, configuration, rbac);
|
|
@@ -236,7 +238,7 @@ async function buildDeploymentManifestInMemory(appName, options = {}) {
|
|
|
236
238
|
...variables,
|
|
237
239
|
app: { ...variables.app, version: resolved.version }
|
|
238
240
|
};
|
|
239
|
-
const configuration = parseEnvironmentVariables(envTemplate, variables);
|
|
241
|
+
const configuration = parseEnvironmentVariables(envTemplate, variables, options && options.target);
|
|
240
242
|
const deployment = builders.buildManifestStructure(appName, variablesWithVersion, configuration, rbac);
|
|
241
243
|
|
|
242
244
|
const effectivePort = getEffectivePortForSubstitution(variablesWithVersion, 3000);
|
|
@@ -246,6 +248,7 @@ async function buildDeploymentManifestInMemory(appName, options = {}) {
|
|
|
246
248
|
? parseInt(deployment.port, 10) : (typeof deployment.port === 'number' ? deployment.port : effectivePort);
|
|
247
249
|
}
|
|
248
250
|
const envVarMap = await buildEnvVarMap('docker', null, null, { appPort: effectivePort });
|
|
251
|
+
await mergeAdditionalPublishedPorts(envVarMap, variablesWithVersion);
|
|
249
252
|
substituteEnvVarsInDeployment(deployment, envVarMap);
|
|
250
253
|
_validator.validateNoUnresolvedVariablesInDeployment(deployment);
|
|
251
254
|
return { deployment, appPath };
|
|
@@ -290,7 +293,7 @@ async function writeExternalDeployJson(appName, appPath, options) {
|
|
|
290
293
|
* @param {string} appType - Application type
|
|
291
294
|
* @returns {Promise<string>} Path to written deploy JSON
|
|
292
295
|
*/
|
|
293
|
-
async function writeRegularDeployJson(appName, appPath, appType) {
|
|
296
|
+
async function writeRegularDeployJson(appName, appPath, appType, options = {}) {
|
|
294
297
|
const { variables, envTemplate, rbac, jsonPath } = loadDeploymentConfigFiles(appPath, appType, appName);
|
|
295
298
|
const resolved = await resolveVersionForApp(appName, variables, { updateBuilder: false });
|
|
296
299
|
const variablesWithVersion = {
|
|
@@ -299,7 +302,11 @@ async function writeRegularDeployJson(appName, appPath, appType) {
|
|
|
299
302
|
};
|
|
300
303
|
const effectivePort = getEffectivePortForSubstitution(variablesWithVersion, 3000);
|
|
301
304
|
const envVarMap = await buildEnvVarMap('docker', null, null, { appPort: effectivePort });
|
|
302
|
-
|
|
305
|
+
await mergeAdditionalPublishedPorts(envVarMap, variablesWithVersion);
|
|
306
|
+
const deployment = buildAndValidateDeployment(appName, variablesWithVersion, envTemplate, rbac, {
|
|
307
|
+
envVarMap,
|
|
308
|
+
target: options.target
|
|
309
|
+
});
|
|
303
310
|
const jsonContent = JSON.stringify(deployment, null, 2);
|
|
304
311
|
fs.writeFileSync(jsonPath, jsonContent, { mode: 0o644 });
|
|
305
312
|
return jsonPath;
|
|
@@ -333,7 +340,7 @@ async function generateDeployJson(appName, options = {}) {
|
|
|
333
340
|
if (isExternal) {
|
|
334
341
|
return writeExternalDeployJson(appName, appPath, options);
|
|
335
342
|
}
|
|
336
|
-
return await writeRegularDeployJson(appName, appPath, appType);
|
|
343
|
+
return await writeRegularDeployJson(appName, appPath, appType, options);
|
|
337
344
|
}
|
|
338
345
|
|
|
339
346
|
async function generateDeployJsonWithValidation(appName, options = {}) {
|
|
@@ -134,6 +134,7 @@ function computeOperationalReadinessPercent(checks) {
|
|
|
134
134
|
let earned = 0;
|
|
135
135
|
let total = 0;
|
|
136
136
|
for (const [key, weight] of Object.entries(OPERATIONS_WEIGHTS)) {
|
|
137
|
+
if (checks[key] === null) continue;
|
|
137
138
|
total += weight;
|
|
138
139
|
if (checks[key] === true) {
|
|
139
140
|
earned += weight;
|
|
@@ -159,9 +160,9 @@ function buildOperationsChecksFromSteps(steps) {
|
|
|
159
160
|
authorization: integration || e2e,
|
|
160
161
|
openApi: validation,
|
|
161
162
|
mappings: validation,
|
|
162
|
-
sync: e2e,
|
|
163
|
-
crud: e2e,
|
|
164
|
-
execution: e2e,
|
|
163
|
+
sync: steps.keyedDocumentGet === true ? null : e2e,
|
|
164
|
+
crud: steps.keyedDocumentGet === true ? steps.documentPersistence === true : e2e,
|
|
165
|
+
execution: steps.keyedDocumentGet === true ? steps.documentPersistence === true : e2e,
|
|
165
166
|
validation,
|
|
166
167
|
unitTests: unit,
|
|
167
168
|
integrationTests: integration,
|
|
@@ -132,8 +132,9 @@ function displayOperationsVerificationSteps(detailsGrouped) {
|
|
|
132
132
|
for (const [groupName, keys] of groups) {
|
|
133
133
|
logger.log(chalk.white(groupName));
|
|
134
134
|
for (const key of keys) {
|
|
135
|
-
const
|
|
136
|
-
const
|
|
135
|
+
const group = detailsGrouped[groupName.toLowerCase()];
|
|
136
|
+
const passed = group && Object.hasOwn(group, key) ? group[key] : detailsGrouped[key];
|
|
137
|
+
const glyph = passed === null ? chalk.gray('⏭ Not applicable') : passed ? successGlyph() : failureGlyph();
|
|
137
138
|
logger.log(` ${labels[key] || key}`.padEnd(24) + glyph);
|
|
138
139
|
}
|
|
139
140
|
}
|
|
@@ -135,7 +135,7 @@ function createCatalogApi(doc, exact, patterns) {
|
|
|
135
135
|
const entry = findEntryForKey(key);
|
|
136
136
|
const gen = entry && entry.generator;
|
|
137
137
|
// emptyAllowed: e.g. Redis password when no requirepass.
|
|
138
|
-
// emptyString: optional user-supplied keys
|
|
138
|
+
// emptyString: optional user-supplied catalog keys — absent resolves like ''.
|
|
139
139
|
return Boolean(gen && (gen.type === 'emptyAllowed' || gen.type === 'emptyString'));
|
|
140
140
|
}
|
|
141
141
|
|
|
@@ -10,7 +10,7 @@ const fs = require('fs');
|
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const { buildHelpEnvelope } = require('./builder-help-envelope');
|
|
12
12
|
|
|
13
|
-
const HELP_MD_DIR = path.join(__dirname, 'help
|
|
13
|
+
const HELP_MD_DIR = path.join(__dirname, '..', '..', 'docs', 'builder-help', 'content');
|
|
14
14
|
|
|
15
15
|
/** @type {ReadonlyArray<string>} */
|
|
16
16
|
const REPAIR_COMMANDS = Object.freeze([
|
|
@@ -15,7 +15,7 @@ const { listPlatformRoles } = require('./platform-rbac-catalog');
|
|
|
15
15
|
const { CHANNEL_TARGET_KEYS, CHANNEL_TARGETS } = require('../channels/channel-targets');
|
|
16
16
|
|
|
17
17
|
const BUILDER_HELP_DIR = path.join(__dirname, '..', '..', 'docs', 'builder-help');
|
|
18
|
-
const HELP_MD_DIR = path.join(
|
|
18
|
+
const HELP_MD_DIR = path.join(BUILDER_HELP_DIR, 'content');
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* @param {string} fileName
|
|
@@ -65,7 +65,7 @@ const PROSE_TOPIC_FILES = Object.freeze({
|
|
|
65
65
|
enterpriseSyncFabrix: 'enterprise-sync-fabrix.md'
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
const HELP_MD_DIR = path.join(__dirname, 'help
|
|
68
|
+
const HELP_MD_DIR = path.join(__dirname, '..', '..', 'docs', 'builder-help', 'content');
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
71
|
* @param {string} message
|
|
@@ -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
|
+
};
|