Skip to content

Commit 2ddf213

Browse files
committed
PR_26152_267-custom-extension-approval-contract
1 parent aefcca1 commit 2ddf213

3 files changed

Lines changed: 279 additions & 9 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Engine V2 Custom Extension Approval Lifecycle
2+
3+
## Scope
4+
5+
- Added the Custom Extension approval lifecycle statuses: `draft`, `private`, `submitted`, `aiValidated`, `aiRejected`, `humanApproved`, `humanRejected`, and `promotedCandidate`.
6+
- Added the Admin Custom Extension Approval boundary as a contract/runtime boundary only.
7+
- Kept AI validation advisory only: `aiValidated` and `aiRejected` do not make a Custom Extension publish eligible.
8+
- Kept human approval as the publish eligibility gate: only `humanApproved` clears publish blocking.
9+
- Kept unapproved Custom Extensions creator-private and publish-blocking.
10+
11+
## Boundaries
12+
13+
- OpenAI integration implementation: Not added.
14+
- Admin UI implementation: Not added.
15+
- Marketplace, Publishing, Toolbox rebuild, and sample work: Not touched.
16+
- Runtime code execution for Custom Extensions: Not added.
17+
18+
## Validation
19+
20+
- PASS: `node --check src/engine/runtime/engineV2CustomExtensionsHookRuntime.js`
21+
- PASS: `node --check tests/engine/EngineV2CustomExtensionsHookRuntime.test.mjs`
22+
- PASS: `node tests/engine/EngineV2CustomExtensionsHookRuntime.test.mjs`
23+
24+
## Notes
25+
26+
- The approval boundary is represented by exported lifecycle/action constants and a pure resolver for valid status transitions.
27+
- AI advisory outcomes remain visible in approval metadata but cannot bypass human approval.

src/engine/runtime/engineV2CustomExtensionsHookRuntime.js

Lines changed: 159 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,33 @@ export const ENGINE_V2_CUSTOM_EXTENSION_HOOKS = Object.freeze({
1818
export const ENGINE_V2_CUSTOM_EXTENSION_HOOK_LIST = Object.freeze(Object.values(ENGINE_V2_CUSTOM_EXTENSION_HOOKS));
1919

2020
export const ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS = Object.freeze({
21-
APPROVED: "approved",
22-
UNAPPROVED: "unapproved",
21+
DRAFT: "draft",
22+
PRIVATE: "private",
23+
SUBMITTED: "submitted",
24+
AI_VALIDATED: "aiValidated",
25+
AI_REJECTED: "aiRejected",
26+
HUMAN_APPROVED: "humanApproved",
27+
HUMAN_REJECTED: "humanRejected",
28+
PROMOTED_CANDIDATE: "promotedCandidate",
29+
});
30+
31+
export const ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS = Object.freeze({
32+
SUBMIT: "submit",
33+
MARK_PRIVATE: "markPrivate",
34+
RETURN_TO_DRAFT: "returnToDraft",
35+
RECORD_AI_VALIDATED: "recordAiValidated",
36+
RECORD_AI_REJECTED: "recordAiRejected",
37+
PROMOTE_CANDIDATE: "promoteCandidate",
38+
HUMAN_APPROVE: "humanApprove",
39+
HUMAN_REJECT: "humanReject",
40+
});
41+
42+
export const ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_BOUNDARY = Object.freeze({
43+
boundaryName: "Admin Custom Extension Approval",
44+
aiValidationAdvisoryOnly: true,
45+
humanApprovalRequiredForPublishEligibility: true,
46+
noOpenAiIntegrationImplementation: true,
47+
noAdminUiImplementation: true,
2348
});
2449

2550
export const ENGINE_V2_CUSTOM_EXTENSION_ALLOWED_CONTEXT_KEYS = Object.freeze([
@@ -73,9 +98,53 @@ export const ENGINE_V2_CUSTOM_EXTENSION_ERRORS = Object.freeze({
7398
FIELD_FORBIDDEN: "ENGINE_V2_CUSTOM_EXTENSION_FIELD_FORBIDDEN",
7499
RUNTIME_INVALID: "ENGINE_V2_CUSTOM_EXTENSION_RUNTIME_INVALID",
75100
HOOK_NAME_INVALID: "ENGINE_V2_CUSTOM_EXTENSION_HOOK_NAME_INVALID",
101+
APPROVAL_ACTION_INVALID: "ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_ACTION_INVALID",
102+
APPROVAL_TRANSITION_INVALID: "ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_TRANSITION_INVALID",
76103
});
77104

78105
const APPROVAL_STATUS_LIST = Object.freeze(Object.values(ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS));
106+
const APPROVAL_ACTION_LIST = Object.freeze(Object.values(ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS));
107+
const AI_ADVISORY_APPROVAL_STATUSES = new Set([
108+
ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.AI_VALIDATED,
109+
ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.AI_REJECTED,
110+
]);
111+
const PUBLISH_ELIGIBLE_APPROVAL_STATUSES = new Set([
112+
ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.HUMAN_APPROVED,
113+
]);
114+
const ADMIN_APPROVAL_TRANSITIONS = Object.freeze({
115+
[ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.DRAFT]: Object.freeze({
116+
[ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS.MARK_PRIVATE]: ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.PRIVATE,
117+
[ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS.SUBMIT]: ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.SUBMITTED,
118+
}),
119+
[ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.PRIVATE]: Object.freeze({
120+
[ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS.RETURN_TO_DRAFT]: ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.DRAFT,
121+
[ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS.SUBMIT]: ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.SUBMITTED,
122+
}),
123+
[ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.SUBMITTED]: Object.freeze({
124+
[ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS.RECORD_AI_VALIDATED]: ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.AI_VALIDATED,
125+
[ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS.RECORD_AI_REJECTED]: ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.AI_REJECTED,
126+
[ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS.HUMAN_APPROVE]: ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.HUMAN_APPROVED,
127+
[ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS.HUMAN_REJECT]: ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.HUMAN_REJECTED,
128+
}),
129+
[ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.AI_VALIDATED]: Object.freeze({
130+
[ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS.PROMOTE_CANDIDATE]: ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.PROMOTED_CANDIDATE,
131+
[ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS.HUMAN_APPROVE]: ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.HUMAN_APPROVED,
132+
[ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS.HUMAN_REJECT]: ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.HUMAN_REJECTED,
133+
}),
134+
[ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.AI_REJECTED]: Object.freeze({
135+
[ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS.SUBMIT]: ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.SUBMITTED,
136+
[ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS.HUMAN_REJECT]: ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.HUMAN_REJECTED,
137+
}),
138+
[ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.PROMOTED_CANDIDATE]: Object.freeze({
139+
[ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS.HUMAN_APPROVE]: ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.HUMAN_APPROVED,
140+
[ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS.HUMAN_REJECT]: ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.HUMAN_REJECTED,
141+
}),
142+
[ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.HUMAN_REJECTED]: Object.freeze({
143+
[ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS.MARK_PRIVATE]: ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.PRIVATE,
144+
[ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS.SUBMIT]: ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.SUBMITTED,
145+
}),
146+
[ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.HUMAN_APPROVED]: Object.freeze({}),
147+
});
79148

80149
export function registerEngineV2CustomExtensionHooks({ extensionDefinitions }) {
81150
const errors = [];
@@ -94,25 +163,29 @@ export function registerEngineV2CustomExtensionHooks({ extensionDefinitions }) {
94163
}
95164

96165
const registeredHooks = extensionDefinitions.flatMap((definition) => {
97-
const approved = definition.approvalStatus === ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.APPROVED;
166+
const approvalState = createApprovalState(definition.approvalStatus);
98167

99168
return definition.hookRegistrations.map((registration) => Object.freeze({
100169
extensionId: definition.extensionId,
101170
displayName: definition.displayName,
102171
hookName: registration.hookName,
103172
extensionMode: registration.extensionMode,
104173
allowedContextKeys: Object.freeze([...registration.allowedContextKeys]),
105-
approved,
106-
creatorPrivate: !approved,
107-
publishEligible: approved,
174+
approvalStatus: approvalState.approvalStatus,
175+
humanApproved: approvalState.humanApproved,
176+
aiValidationAdvisory: approvalState.aiValidationAdvisory,
177+
humanApprovalRequiredForPublishEligibility: approvalState.humanApprovalRequiredForPublishEligibility,
178+
creatorPrivate: approvalState.creatorPrivate,
179+
publishEligible: approvalState.publishEligible,
108180
}));
109181
});
110182
const publishEligible = registeredHooks.every((hook) => hook.publishEligible);
111183
const runtime = Object.freeze({
112184
registeredHooks: Object.freeze(registeredHooks),
113185
publishEligibility: Object.freeze({
114186
eligible: publishEligible,
115-
blockedByExtensionIds: Object.freeze(registeredHooks.filter((hook) => !hook.publishEligible).map((hook) => hook.extensionId)),
187+
humanApprovalRequired: !publishEligible,
188+
blockedByExtensionIds: freezeUnique(registeredHooks.filter((hook) => !hook.publishEligible).map((hook) => hook.extensionId)),
116189
}),
117190
});
118191

@@ -124,6 +197,52 @@ export function registerEngineV2CustomExtensionHooks({ extensionDefinitions }) {
124197
});
125198
}
126199

200+
export function resolveEngineV2AdminCustomExtensionApprovalBoundary({ extensionDefinition, approvalAction }) {
201+
const errors = [];
202+
const definitionErrors = validateExtensionDefinition(extensionDefinition, "extensionDefinition");
203+
definitionErrors.forEach((error) => errors.push(error));
204+
205+
const actionType = isRecord(approvalAction) ? approvalAction.actionType : approvalAction;
206+
207+
if (!APPROVAL_ACTION_LIST.includes(actionType)) {
208+
errors.push(createCustomExtensionError(ENGINE_V2_CUSTOM_EXTENSION_ERRORS.APPROVAL_ACTION_INVALID, "Admin Custom Extension Approval requires an approved approval action.", "approvalAction.actionType"));
209+
}
210+
211+
if (errors.length > 0) {
212+
return createApprovalBoundaryResult({
213+
previousApprovalStatus: isRecord(extensionDefinition) ? extensionDefinition.approvalStatus : null,
214+
nextApprovalStatus: null,
215+
publishEligibility: null,
216+
errors,
217+
});
218+
}
219+
220+
const nextApprovalStatus = ADMIN_APPROVAL_TRANSITIONS[extensionDefinition.approvalStatus][actionType];
221+
222+
if (!nextApprovalStatus) {
223+
errors.push(createCustomExtensionError(ENGINE_V2_CUSTOM_EXTENSION_ERRORS.APPROVAL_TRANSITION_INVALID, "Admin Custom Extension Approval action is not valid for the current approval status.", "approvalAction.actionType"));
224+
return createApprovalBoundaryResult({
225+
previousApprovalStatus: extensionDefinition.approvalStatus,
226+
nextApprovalStatus: null,
227+
publishEligibility: null,
228+
errors,
229+
});
230+
}
231+
232+
const approvalState = createApprovalState(nextApprovalStatus);
233+
234+
return createApprovalBoundaryResult({
235+
previousApprovalStatus: extensionDefinition.approvalStatus,
236+
nextApprovalStatus,
237+
publishEligibility: Object.freeze({
238+
eligible: approvalState.publishEligible,
239+
humanApprovalRequired: approvalState.humanApprovalRequiredForPublishEligibility,
240+
blockedByExtensionIds: approvalState.publishEligible ? Object.freeze([]) : Object.freeze([extensionDefinition.extensionId]),
241+
}),
242+
errors,
243+
});
244+
}
245+
127246
export function dispatchEngineV2CustomExtensionHook({ runtime, hookName, runtimeContext }) {
128247
const errors = [];
129248

@@ -200,6 +319,19 @@ function validateHookRegistration(registration, path) {
200319
return errors;
201320
}
202321

322+
function createApprovalState(approvalStatus) {
323+
const humanApproved = PUBLISH_ELIGIBLE_APPROVAL_STATUSES.has(approvalStatus);
324+
325+
return Object.freeze({
326+
approvalStatus,
327+
humanApproved,
328+
aiValidationAdvisory: AI_ADVISORY_APPROVAL_STATUSES.has(approvalStatus),
329+
humanApprovalRequiredForPublishEligibility: !humanApproved,
330+
creatorPrivate: !humanApproved,
331+
publishEligible: humanApproved,
332+
});
333+
}
334+
203335
function validateNoForbiddenFields(value, path) {
204336
return ENGINE_V2_CUSTOM_EXTENSION_FORBIDDEN_FIELDS
205337
.filter((field) => Object.hasOwn(value, field))
@@ -246,6 +378,22 @@ function createCustomExtensionDispatchResult({ hookInvocations, errors }) {
246378
});
247379
}
248380

381+
function createApprovalBoundaryResult({ previousApprovalStatus, nextApprovalStatus, publishEligibility, errors }) {
382+
const nextApprovalState = nextApprovalStatus ? createApprovalState(nextApprovalStatus) : null;
383+
384+
return Object.freeze({
385+
valid: errors.length === 0,
386+
boundary: ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_BOUNDARY,
387+
previousApprovalStatus,
388+
nextApprovalStatus,
389+
aiValidationAdvisory: nextApprovalState ? nextApprovalState.aiValidationAdvisory : false,
390+
humanApprovalRequiredForPublishEligibility: nextApprovalState ? nextApprovalState.humanApprovalRequiredForPublishEligibility : true,
391+
creatorPrivate: nextApprovalState ? nextApprovalState.creatorPrivate : true,
392+
publishEligibility,
393+
errors: Object.freeze(errors),
394+
});
395+
}
396+
249397
function createCustomExtensionError(code, message, path) {
250398
return Object.freeze({ code, message, path });
251399
}
@@ -254,6 +402,10 @@ function freezeJsonClone(value) {
254402
return Object.freeze(JSON.parse(JSON.stringify(value)));
255403
}
256404

405+
function freezeUnique(values) {
406+
return Object.freeze([...new Set(values)]);
407+
}
408+
257409
function isRecord(value) {
258410
return value !== null && typeof value === "object" && !Array.isArray(value);
259411
}

tests/engine/EngineV2CustomExtensionsHookRuntime.test.mjs

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,20 @@ EngineV2CustomExtensionsHookRuntime.test.mjs
88
import assert from "node:assert/strict";
99
import fs from "node:fs";
1010
import {
11+
ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS,
12+
ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS,
1113
ENGINE_V2_CUSTOM_EXTENSION_ERRORS,
1214
ENGINE_V2_CUSTOM_EXTENSION_HOOK_LIST,
1315
dispatchEngineV2CustomExtensionHook,
1416
registerEngineV2CustomExtensionHooks,
17+
resolveEngineV2AdminCustomExtensionApprovalBoundary,
1518
} from "../../src/engine/runtime/engineV2CustomExtensionsHookRuntime.js";
1619

1720
function createExtensionDefinition(overrides = {}) {
1821
return {
1922
extensionId: "custom-extension.approved",
2023
displayName: "Custom Extensions Weather Polish",
21-
approvalStatus: "approved",
24+
approvalStatus: ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.HUMAN_APPROVED,
2225
requestedCapabilities: [],
2326
hookRegistrations: ENGINE_V2_CUSTOM_EXTENSION_HOOK_LIST.map((hookName) => ({
2427
hookName,
@@ -31,11 +34,22 @@ function createExtensionDefinition(overrides = {}) {
3134
}
3235

3336
export function run() {
37+
assert.deepEqual(new Set(Object.values(ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS)), new Set([
38+
"draft",
39+
"private",
40+
"submitted",
41+
"aiValidated",
42+
"aiRejected",
43+
"humanApproved",
44+
"humanRejected",
45+
"promotedCandidate",
46+
]));
47+
3448
const approvedExtension = createExtensionDefinition();
3549
const privateExtension = createExtensionDefinition({
3650
extensionId: "custom-extension.private",
3751
displayName: "Creator Private Custom Extension",
38-
approvalStatus: "unapproved",
52+
approvalStatus: ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.PRIVATE,
3953
hookRegistrations: [
4054
{
4155
hookName: "onTick",
@@ -54,9 +68,86 @@ export function run() {
5468
assert.deepEqual(new Set(registrationResult.registeredHooks.map((hook) => hook.hookName)), new Set(ENGINE_V2_CUSTOM_EXTENSION_HOOK_LIST));
5569
assert.equal(registrationResult.publishEligibility.eligible, false);
5670
assert.deepEqual(registrationResult.publishEligibility.blockedByExtensionIds, ["custom-extension.private"]);
71+
assert.equal(registrationResult.publishEligibility.humanApprovalRequired, true);
72+
assert.equal(registrationResult.registeredHooks.find((hook) => hook.extensionId === "custom-extension.approved").humanApproved, true);
5773
assert.equal(registrationResult.registeredHooks.find((hook) => hook.extensionId === "custom-extension.private").creatorPrivate, true);
74+
assert.equal(registrationResult.registeredHooks.find((hook) => hook.extensionId === "custom-extension.private").humanApprovalRequiredForPublishEligibility, true);
5875
assert.equal(registrationResult.registeredHooks.find((hook) => hook.extensionId === "custom-extension.private").publishEligible, false);
5976

77+
const aiValidatedExtension = createExtensionDefinition({
78+
extensionId: "custom-extension.ai-validated",
79+
displayName: "AI Validated Advisory Custom Extension",
80+
approvalStatus: ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.AI_VALIDATED,
81+
hookRegistrations: [
82+
{
83+
hookName: "onAction",
84+
extensionMode: "enhance",
85+
allowedContextKeys: ["projectId", "sceneId", "action"],
86+
requestedCapabilities: [],
87+
},
88+
],
89+
});
90+
const aiValidatedRegistration = registerEngineV2CustomExtensionHooks({
91+
extensionDefinitions: [aiValidatedExtension],
92+
});
93+
94+
assert.equal(aiValidatedRegistration.valid, true);
95+
assert.equal(aiValidatedRegistration.publishEligibility.eligible, false);
96+
assert.deepEqual(aiValidatedRegistration.publishEligibility.blockedByExtensionIds, ["custom-extension.ai-validated"]);
97+
assert.equal(aiValidatedRegistration.registeredHooks[0].aiValidationAdvisory, true);
98+
assert.equal(aiValidatedRegistration.registeredHooks[0].creatorPrivate, true);
99+
assert.equal(aiValidatedRegistration.registeredHooks[0].publishEligible, false);
100+
101+
const aiBoundaryResult = resolveEngineV2AdminCustomExtensionApprovalBoundary({
102+
extensionDefinition: createExtensionDefinition({
103+
extensionId: "custom-extension.submitted",
104+
approvalStatus: ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.SUBMITTED,
105+
}),
106+
approvalAction: { actionType: ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS.RECORD_AI_VALIDATED },
107+
});
108+
109+
assert.equal(aiBoundaryResult.valid, true);
110+
assert.equal(aiBoundaryResult.nextApprovalStatus, ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.AI_VALIDATED);
111+
assert.equal(aiBoundaryResult.aiValidationAdvisory, true);
112+
assert.equal(aiBoundaryResult.publishEligibility.eligible, false);
113+
assert.equal(aiBoundaryResult.publishEligibility.humanApprovalRequired, true);
114+
115+
const promotedBoundaryResult = resolveEngineV2AdminCustomExtensionApprovalBoundary({
116+
extensionDefinition: createExtensionDefinition({
117+
extensionId: "custom-extension.candidate",
118+
approvalStatus: ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.AI_VALIDATED,
119+
}),
120+
approvalAction: { actionType: ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS.PROMOTE_CANDIDATE },
121+
});
122+
123+
assert.equal(promotedBoundaryResult.valid, true);
124+
assert.equal(promotedBoundaryResult.nextApprovalStatus, ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.PROMOTED_CANDIDATE);
125+
assert.equal(promotedBoundaryResult.publishEligibility.eligible, false);
126+
assert.equal(promotedBoundaryResult.creatorPrivate, true);
127+
128+
const humanApprovalBoundaryResult = resolveEngineV2AdminCustomExtensionApprovalBoundary({
129+
extensionDefinition: createExtensionDefinition({
130+
extensionId: "custom-extension.candidate",
131+
approvalStatus: ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.PROMOTED_CANDIDATE,
132+
}),
133+
approvalAction: { actionType: ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS.HUMAN_APPROVE },
134+
});
135+
136+
assert.equal(humanApprovalBoundaryResult.valid, true);
137+
assert.equal(humanApprovalBoundaryResult.nextApprovalStatus, ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.HUMAN_APPROVED);
138+
assert.equal(humanApprovalBoundaryResult.publishEligibility.eligible, true);
139+
assert.equal(humanApprovalBoundaryResult.creatorPrivate, false);
140+
141+
const invalidApprovalTransition = resolveEngineV2AdminCustomExtensionApprovalBoundary({
142+
extensionDefinition: createExtensionDefinition({
143+
approvalStatus: ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS.PRIVATE,
144+
}),
145+
approvalAction: { actionType: ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS.HUMAN_APPROVE },
146+
});
147+
148+
assert.equal(invalidApprovalTransition.valid, false);
149+
assert.deepEqual(invalidApprovalTransition.errors.map((error) => error.code), [ENGINE_V2_CUSTOM_EXTENSION_ERRORS.APPROVAL_TRANSITION_INVALID]);
150+
60151
const dispatchResult = dispatchEngineV2CustomExtensionHook({
61152
runtime: registrationResult.runtime,
62153
hookName: "onTick",

0 commit comments

Comments
 (0)