Skip to content

Commit 0cb5a54

Browse files
os-zhuangclaude
andcommitted
feat(approvals)!: rename role approver type to org_membership_level (#3133)
`ApproverType.role` was the last platform surface projecting the word ADR-0090 D3 reserved. Renaming it also unbreaks `lint.yml` on main: the role-word ratchet has been red since #3113 (approvals.mdx 5 → 9), and the docs stop needing the word once the type is spelled correctly. D3's exception does not cover this enum. It protects better-auth's own `sys_member.role` COLUMN — third-party schema we cannot rename. `ApproverType` is ours: an authoring surface, i.e. the *projection*, which D3 says is spelled `org_membership_level` and labelled "organization membership", never "role". The sentence licensing the leak is itself false. ADR-0090 D3:203 claims `sys_member.role` is "already relabelled `org_membership_level` in the platform projection (ADR-0057 D7)" — but `org_membership_level` appeared nowhere in the codebase (one comment in position.zod.ts), and ADR-0057 D7:335 lists that relabel under "Deferred (evidence-gated, P4)". The projection never landed, so the word reached authors. The name manufactured a silent failure ("hotcrm class"): every sibling surface renamed to `position` (`sys_role`, `ShareRecipientType.role`, `ctx.roles[]`), so `{ type: 'role', value: 'sales_manager' }` reads as a position's legacy spelling. It resolves against the membership tier, finds no member row, falls back to an inert `role:sales_manager` literal, and the request waits forever. Repo-wide, `type: 'role'` had ZERO real callers — only lint tests and the docs warning that exists to undo the confusion the name creates. - spec: `ApproverType` gains `org_membership_level`; `role` kept as a deprecated alias for one window so a published 15.x flow keeps loading. `DEPRECATED_APPROVER_TYPES` + `canonicalApproverType()` are the single source for the mapping (runtime and lint both read it). - plugin-approvals: resolves on the canonical type, warns on the deprecated spelling, `expandRoleUsers` → `expandMembershipTierUsers`. The `type:value` fallback literal deliberately keeps the AUTHORED spelling — 15.x wrote `role:<v>` into `sys_approval_approver` / `pending_approvers`, and canonicalising it here would orphan every stored slot. - lint: `approval-role-not-membership-tier` → `approval-approver-not-membership-tier` (the rule id carried the word too), plus `approval-approver-type-deprecated`. Mutually exclusive: a bad VALUE wins, because prescribing `org_membership_level` for a position name is wrong advice — the fix there is `position`. - docs/skill/reference + role-word baseline ratcheted DOWN (approvals.mdx 5 → 1, automation SKILL.md 3 → 1); api-surface snapshot regenerated (0 breaking, 2 added). Studio still offers "Role" and its picker calls `client.list('role')` on a metadata type D3 deleted — that picker is already dead, and the dropdown is objectui's own hardcoded copy of this enum. Tracked as objectui follow-up in #3133; the xRef map here points both spellings at the new picker kind, which degrades to free text (strictly better than a picker that lists nothing). Closes #3133. Supersedes #3120 and #3122, which only deleted the duplicated docs callout without touching the name that causes the confusion. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 4cba5a5 commit 0cb5a54

13 files changed

Lines changed: 313 additions & 78 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/plugin-approvals": minor
4+
"@objectstack/lint": minor
5+
---
6+
7+
feat(approvals): rename the `role` approver type to `org_membership_level` (#3133)
8+
9+
`ApproverType.role` was the last platform surface projecting the reserved word
10+
"role" (ADR-0090 D3). It is not covered by D3's better-auth exception: that
11+
exception protects better-auth's own `sys_member.role` **column**, which we do
12+
not own — `ApproverType` is our own enum, an authoring surface, and D3 mandates
13+
that the projection of that concept is spelled `org_membership_level` and
14+
labelled "organization membership", **never "role"**.
15+
16+
The sentence licensing the leak was also false: ADR-0090 D3 claims
17+
`sys_member.role` is "already relabelled `org_membership_level` in the platform
18+
projection", but `org_membership_level` existed nowhere in the codebase and
19+
ADR-0057 D7 lists that relabel under "Deferred (evidence-gated, P4)". The
20+
projection never landed, so the word reached authors.
21+
22+
The name manufactured a real, silent failure — "hotcrm class": every other
23+
surface renamed to `position` (`sys_role`, `ShareRecipientType.role`,
24+
`ctx.roles[]`), so `{ type: 'role', value: 'sales_manager' }` reads as the
25+
legacy spelling of a position. It resolves against the membership tier, finds
26+
no member row, falls back to an inert `role:sales_manager` literal, and the
27+
request waits forever on an approver that cannot exist.
28+
29+
- **spec**: `ApproverType` gains `org_membership_level`; `role` stays as a
30+
deprecated alias for one window (a published 15.x flow keeps loading) with
31+
`DEPRECATED_APPROVER_TYPES` + `canonicalApproverType()` as the single source
32+
for the mapping. Removed in the next major.
33+
- **plugin-approvals**: resolves on the canonical type and warns on the
34+
deprecated spelling. The `type:value` fallback literal keeps the **authored**
35+
spelling — stored `sys_approval_approver` rows and `pending_approvers` slots
36+
from 15.x carry `role:<v>`, and rewriting it would orphan them.
37+
- **lint**: `approval-role-not-membership-tier``approval-approver-not-membership-tier`
38+
(the rule id carried the reserved word too), plus a new
39+
`approval-approver-type-deprecated`. The two are mutually exclusive: a bad
40+
*value* wins, because prescribing `org_membership_level` for a position name
41+
would be wrong advice — the fix there is `position`.
42+
43+
Authoring `type: 'role'` keeps working and now says so out loud. Rewrite it as
44+
`org_membership_level`; if the value is an org position, the fix is `position`.

content/docs/automation/approvals.mdx

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,14 @@ defineFlow({
5353
```
5454

5555
<Callout type="warn">
56-
**`position` vs `role`.** `{ type: 'position', value: 'finance_manager' }` routes to the
57-
holders of a position (`sys_user_position`, ADR-0090 D3). The `role` approver type is the
58-
better-auth **org-membership tier** (`sys_member.role`: `owner`/`admin`/`member`) — a position
59-
name authored as `type: 'role'` matches nobody and the request stalls; `os lint` flags this
60-
(`approval-role-not-membership-tier`).
56+
**`position` vs `org_membership_level`.** `{ type: 'position', value: 'finance_manager' }` routes
57+
to the holders of a position (`sys_user_position`, ADR-0090 D3). `org_membership_level` is a
58+
different thing — the better-auth **org-membership tier**, whose only values are
59+
`owner`/`admin`/`member`. A position name authored there matches nobody and the request stalls;
60+
`os lint` flags it (`approval-approver-not-membership-tier`).
61+
62+
Authored `type: 'role'` on 15.x? That is the deprecated spelling of `org_membership_level`
63+
(ADR-0090 D3): it still resolves, warns at runtime, and is removed in the next major.
6164
</Callout>
6265

6366
Approving is itself a gated action — model "may approve" as a capability (`approve_invoice`) the approver's permission set grants, and gate the approve action's `requiredPermissions` on it so the gate is enforced on **both** the UI and the server (ADR-0066 D4).
@@ -130,14 +133,11 @@ record is **locked** against edits while pending (`lockRecord`, default `true`),
130133
and the flow run parks until a decision arrives.
131134

132135
Only `approvers` is required on the node; everything else has a default
133-
(`behavior: 'first_response'`, `lockRecord: true`, `maxRevisions: 3`).
134-
135-
<Callout type="warn">
136-
**`type: 'role'` is not a position.** In an approver entry, `role` means the
137-
better-auth membership tier (`owner` / `admin` / `member`). Naming a business
138-
role like `sales_manager` there matches nobody and the request silently has no
139-
approvers — use `{ type: 'position', value: 'sales_manager' }`.
140-
</Callout>
136+
(`behavior: 'first_response'`, `lockRecord: true`, `maxRevisions: 3`). An
137+
approver entry that resolves to nobody is not an error: the request opens with
138+
an empty `pending_approvers` and nothing can move it, so the run parks forever.
139+
The approver-type trap that causes this is covered under
140+
**3. The approval node** above.
141141

142142
### The approver finds it in their queue
143143

@@ -150,8 +150,10 @@ curl -b cookies.txt \
150150
"https://your-app.example.com/api/v1/approvals/requests?status=pending&approverId=usr_123"
151151
```
152152

153-
`approverId` accepts a user id, an email, or `role:<r>` — and takes several
154-
values (comma-separated or repeated) to cover a person's identities in one call.
153+
`approverId` accepts a user id, an email, or a `<type>:<value>` approver literal
154+
(`position:finance_manager` — the form an entry falls back to when it resolves
155+
to no users) — and takes several values (comma-separated or repeated) to cover
156+
a person's identities in one call.
155157
Other filters: `status`, `object`, `recordId`, `submitterId`, `q`, `limit`,
156158
`offset`.
157159

content/docs/references/automation/approval.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ const result = ApprovalDecision.parse(data);
5454

5555
| Property | Type | Required | Description |
5656
| :--- | :--- | :--- | :--- |
57-
| **type** | `Enum<'user' \| 'role' \| 'position' \| 'team' \| 'department' \| 'manager' \| 'field' \| 'queue'>` || |
57+
| **type** | `Enum<'user' \| 'org_membership_level' \| 'role' \| 'position' \| 'team' \| 'department' \| 'manager' \| 'field' \| 'queue'>` || |
5858
| **value** | `string` | optional | User id / membership tier / position / team / department / field / queue — per `type` |
5959

6060

@@ -81,6 +81,7 @@ const result = ApprovalDecision.parse(data);
8181
### Allowed Values
8282

8383
* `user`
84+
* `org_membership_level`
8485
* `role`
8586
* `position`
8687
* `team`

packages/lint/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,8 @@ export type { CapabilityRefFinding, CapabilityRefSeverity } from './validate-cap
101101

102102
export {
103103
validateApprovalApprovers,
104-
APPROVAL_ROLE_NOT_MEMBERSHIP_TIER,
104+
APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER,
105+
APPROVAL_APPROVER_TYPE_DEPRECATED,
105106
APPROVAL_APPROVER_TYPE_UNKNOWN,
106107
APPROVAL_ESCALATION_REASSIGN_NO_TARGET,
107108
} from './validate-approval-approvers.js';

packages/lint/src/validate-approval-approvers.test.ts

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
import { describe, it, expect } from 'vitest';
44
import {
55
validateApprovalApprovers,
6-
APPROVAL_ROLE_NOT_MEMBERSHIP_TIER,
6+
APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER,
7+
APPROVAL_APPROVER_TYPE_DEPRECATED,
78
APPROVAL_APPROVER_TYPE_UNKNOWN,
89
APPROVAL_ESCALATION_REASSIGN_NO_TARGET,
910
} from './validate-approval-approvers.js';
@@ -27,28 +28,54 @@ describe('validateApprovalApprovers', () => {
2728
expect(validateApprovalApprovers({ flows: [] })).toEqual([]);
2829
});
2930

30-
it('accepts membership tiers for type role (owner/admin/member/guest)', () => {
31+
it('accepts membership tiers for org_membership_level (owner/admin/member/guest)', () => {
3132
const findings = validateApprovalApprovers(stackWithApprovers([
32-
{ type: 'role', value: 'admin' },
33-
{ type: 'role', value: 'Owner' }, // case-insensitive
34-
{ type: 'role', value: 'member' },
35-
{ type: 'role', value: 'guest' },
33+
{ type: 'org_membership_level', value: 'admin' },
34+
{ type: 'org_membership_level', value: 'Owner' }, // case-insensitive
35+
{ type: 'org_membership_level', value: 'member' },
36+
{ type: 'org_membership_level', value: 'guest' },
3637
]));
3738
expect(findings).toEqual([]);
3839
});
3940

40-
it("flags a position name authored as type 'role' (the ADR-0090 D3 hotcrm class)", () => {
41+
it("flags a position name authored as a membership tier (the ADR-0090 D3 hotcrm class)", () => {
4142
const findings = validateApprovalApprovers(stackWithApprovers([
42-
{ type: 'role', value: 'sales_manager' },
43+
{ type: 'org_membership_level', value: 'sales_manager' },
4344
]));
4445
expect(findings).toHaveLength(1);
45-
expect(findings[0].rule).toBe(APPROVAL_ROLE_NOT_MEMBERSHIP_TIER);
46+
expect(findings[0].rule).toBe(APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER);
4647
expect(findings[0].severity).toBe('warning');
4748
expect(findings[0].where).toContain('expense_approval');
4849
expect(findings[0].path).toBe('flows[0].nodes[1].config.approvers[0].value');
4950
expect(findings[0].hint).toContain("type: 'position'");
5051
});
5152

53+
// ── the deprecated `role` spelling (ADR-0090 D3, #3133) ──────────────────
54+
55+
it('flags the deprecated `role` spelling even when its value is a valid tier', () => {
56+
const findings = validateApprovalApprovers(stackWithApprovers([
57+
{ type: 'role', value: 'admin' },
58+
]));
59+
expect(findings).toHaveLength(1);
60+
expect(findings[0].rule).toBe(APPROVAL_APPROVER_TYPE_DEPRECATED);
61+
expect(findings[0].path).toBe('flows[0].nodes[1].config.approvers[0].type');
62+
expect(findings[0].hint).toContain("type: 'org_membership_level'");
63+
});
64+
65+
// The two rules must not both fire: rewriting { type: 'role', value:
66+
// 'sales_manager' } as `org_membership_level` is WRONG advice — the value is
67+
// a position, so `position` is the fix and the deprecation is beside the
68+
// point. Exactly one finding, and it must be the value rule.
69+
it('prefers the value fix over the deprecation notice for a position name', () => {
70+
const findings = validateApprovalApprovers(stackWithApprovers([
71+
{ type: 'role', value: 'sales_manager' },
72+
]));
73+
expect(findings).toHaveLength(1);
74+
expect(findings[0].rule).toBe(APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER);
75+
expect(findings[0].hint).toContain("type: 'position'");
76+
expect(findings[0].hint).not.toContain('org_membership_level, value');
77+
});
78+
5279
it('accepts the position approver type and the other spec types silently', () => {
5380
const findings = validateApprovalApprovers(stackWithApprovers([
5481
{ type: 'position', value: 'sales_manager' },

packages/lint/src/validate-approval-approvers.ts

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,45 @@
33
/**
44
* Approval-node approver authoring lint (ADR-0090 D3 fallout).
55
*
6-
* The `role` approver type resolves against better-auth's org-membership tier
7-
* (`sys_member.role`: owner / admin / member) — it is NOT a position. After
8-
* ADR-0090 D3 renamed `sys_role` → `sys_position`, downstream apps that
9-
* authored `{ type: 'role', value: 'sales_manager' }` silently route the
10-
* approval to nobody: the expansion finds no member row, falls back to the
11-
* `role:sales_manager` literal, and the request waits on an approver that can
12-
* never act. This rule moves that failure from a stuck request at runtime to
13-
* a located fix-it at author time.
6+
* `org_membership_level` (and `role`, its deprecated spelling) resolves against
7+
* better-auth's org-membership tier (`sys_member.role`: owner / admin / member)
8+
* — it is NOT a position. After ADR-0090 D3 renamed `sys_role` → `sys_position`,
9+
* downstream apps that authored `{ type: 'role', value: 'sales_manager' }`
10+
* silently route the approval to nobody: the expansion finds no member row,
11+
* falls back to the `role:sales_manager` literal, and the request waits on an
12+
* approver that can never act. These rules move that failure from a stuck
13+
* request at runtime to a located fix-it at author time.
1414
*
1515
* Rules:
1616
*
1717
* | Rule | Severity | Origin |
1818
* |--------------------------------------------|----------|----------------------------|
19-
* | approval-role-not-membership-tier | warning | ADR-0090 D3 (hotcrm class) |
19+
* | approval-approver-not-membership-tier | warning | ADR-0090 D3 (hotcrm class) |
20+
* | approval-approver-type-deprecated | warning | ADR-0090 D3 (#3133) |
2021
* | approval-approver-type-unknown | warning | contract-first (PD #12) |
2122
* | approval-escalation-reassign-no-target | warning | silent notify degradation |
2223
*
24+
* The first two are mutually exclusive by construction — a bad *value* wins,
25+
* because its fix (`position`) differs from the deprecation's fix
26+
* (`org_membership_level`), and prescribing the latter for a position name
27+
* would be wrong advice.
28+
*
2329
* Warnings (not errors): a custom better-auth membership tier is legal, and
2430
* the runtime keeps its literal fallback — but both shapes are near-certainly
2531
* authoring mistakes, so say it out loud.
2632
*
2733
* Pure `(stack) => Finding[]`; accepts the NORMALIZED stack input.
2834
*/
2935

30-
import { ApproverType, APPROVAL_NODE_TYPE } from '@objectstack/spec/automation';
36+
import {
37+
ApproverType,
38+
APPROVAL_NODE_TYPE,
39+
DEPRECATED_APPROVER_TYPES,
40+
canonicalApproverType,
41+
} from '@objectstack/spec/automation';
3142

32-
export const APPROVAL_ROLE_NOT_MEMBERSHIP_TIER = 'approval-role-not-membership-tier';
43+
export const APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER = 'approval-approver-not-membership-tier';
44+
export const APPROVAL_APPROVER_TYPE_DEPRECATED = 'approval-approver-type-deprecated';
3345
export const APPROVAL_APPROVER_TYPE_UNKNOWN = 'approval-approver-type-unknown';
3446
export const APPROVAL_ESCALATION_REASSIGN_NO_TARGET = 'approval-escalation-reassign-no-target';
3547

@@ -54,7 +66,8 @@ type AnyRec = Record<string, unknown>;
5466
/**
5567
* The better-auth org-membership tiers `sys_member.role` actually stores
5668
* (see `identity/organization.zod.ts` + `mapMembershipRole`). Anything else
57-
* authored as `{ type: 'role' }` is almost certainly a position name.
69+
* authored as `{ type: 'org_membership_level' }` (or its deprecated `role`
70+
* spelling) is almost certainly a position name.
5871
*/
5972
const MEMBERSHIP_TIERS = new Set(['owner', 'admin', 'member', 'guest']);
6073

@@ -121,20 +134,39 @@ export function validateApprovalApprovers(stack: AnyRec): ApprovalApproverFindin
121134
continue;
122135
}
123136

124-
if (type === 'role' && value && !MEMBERSHIP_TIERS.has(value.toLowerCase())) {
137+
const canonical = canonicalApproverType(type);
138+
139+
// Exactly one of the two below fires. Order matters: a bad VALUE is
140+
// the more serious (and differently-fixed) defect, so it wins. Telling
141+
// an author to rewrite { type: 'role', value: 'sales_manager' } as
142+
// `org_membership_level` would be actively wrong advice — the fix is
143+
// `position`, and the deprecation is beside the point.
144+
if (canonical === 'org_membership_level' && value && !MEMBERSHIP_TIERS.has(value.toLowerCase())) {
125145
findings.push({
126146
severity: 'warning',
127-
rule: APPROVAL_ROLE_NOT_MEMBERSHIP_TIER,
147+
rule: APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER,
128148
where,
129149
path: `${path}.value`,
130150
message:
131-
`approver { type: 'role', value: '${value}' } resolves against the better-auth ` +
151+
`approver { type: '${type}', value: '${value}' } resolves against the better-auth ` +
132152
`org-membership tier (sys_member.role: owner/admin/member) — '${value}' is not a ` +
133153
`membership tier, so this approver matches nobody and the request stalls.`,
134154
hint:
135155
`If '${value}' is an org position, author { type: 'position', value: '${value}' } ` +
136-
`(resolved via sys_user_position, ADR-0090 D3). Keep type 'role' only for ` +
137-
`membership tiers (owner/admin/member).`,
156+
`(resolved via sys_user_position, ADR-0090 D3). Keep type 'org_membership_level' ` +
157+
`only for membership tiers (owner/admin/member).`,
158+
});
159+
} else if (type in DEPRECATED_APPROVER_TYPES) {
160+
const fix = canonicalApproverType(type);
161+
findings.push({
162+
severity: 'warning',
163+
rule: APPROVAL_APPROVER_TYPE_DEPRECATED,
164+
where,
165+
path: `${path}.type`,
166+
message:
167+
`approver type '${type}' is the deprecated spelling of '${fix}' (ADR-0090 D3) and ` +
168+
`is removed in the next major.`,
169+
hint: `Author { type: '${fix}', value: '${value}' }. It resolves identically today.`,
138170
});
139171
}
140172
}

packages/plugins/plugin-approvals/src/approval-service.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,57 @@ describe('ApprovalService (node era)', () => {
227227
expect(req.pending_approvers).toEqual(['position:sales_manager']);
228228
});
229229

230+
// ── approver expansion: org_membership_level + its deprecated `role` alias
231+
// (ADR-0090 D3) ────────────────────────────────────────────────────────
232+
233+
// `recordId` is parameterised: the service rejects a second pending request
234+
// on the same record, and the alias test deliberately opens two.
235+
const tierInput = (type: 'org_membership_level' | 'role', recordId = 'opp1') => ({
236+
...openInput([]),
237+
recordId,
238+
record: { id: recordId, amount: 100 },
239+
config: {
240+
approvers: [{ type: type as any, value: 'admin' }],
241+
behavior: 'first_response' as const,
242+
lockRecord: true,
243+
},
244+
});
245+
246+
it('org_membership_level approver: expands the better-auth tier, org-scoped', async () => {
247+
engine._tables['sys_member'] = [
248+
{ id: 'm1', user_id: 'u1', role: 'admin', organization_id: 't1' },
249+
{ id: 'm2', user_id: 'u2', role: 'admin', organization_id: 't1' },
250+
{ id: 'm3', user_id: 'u3', role: 'admin', organization_id: 't2' }, // other tenant
251+
{ id: 'm4', user_id: 'u4', role: 'member', organization_id: 't1' }, // other tier
252+
];
253+
const req = await svc.openNodeRequest(tierInput('org_membership_level'), CTX);
254+
expect(req.pending_approvers.sort()).toEqual(['u1', 'u2']);
255+
});
256+
257+
it('deprecated `role` alias resolves IDENTICALLY to org_membership_level', async () => {
258+
engine._tables['sys_member'] = [
259+
{ id: 'm1', user_id: 'u1', role: 'admin', organization_id: 't1' },
260+
{ id: 'm2', user_id: 'u4', role: 'member', organization_id: 't1' },
261+
];
262+
const canonical = await svc.openNodeRequest(tierInput('org_membership_level', 'opp_canon'), CTX);
263+
const deprecated = await svc.openNodeRequest(tierInput('role', 'opp_depr'), CTX);
264+
expect(deprecated.pending_approvers).toEqual(canonical.pending_approvers);
265+
expect(deprecated.pending_approvers).toEqual(['u1']);
266+
});
267+
268+
// The fallback literal keeps the AUTHORED spelling: `sys_approval_approver`
269+
// rows and `pending_approvers` slots written by 15.x carry `role:<v>`, and
270+
// canonicalising the literal here would orphan every one of them.
271+
it('deprecated `role` alias keeps its legacy literal on fallback (no orphaned slots)', async () => {
272+
const req = await svc.openNodeRequest(tierInput('role'), CTX);
273+
expect(req.pending_approvers).toEqual(['role:admin']);
274+
});
275+
276+
it('org_membership_level falls back to its own canonical literal', async () => {
277+
const req = await svc.openNodeRequest(tierInput('org_membership_level'), CTX);
278+
expect(req.pending_approvers).toEqual(['org_membership_level:admin']);
279+
});
280+
230281
it("department approver: honors the spec enum value 'department' (not just the business_unit dialect)", async () => {
231282
engine._tables['sys_business_unit'] = [
232283
{ id: 'bu1', organization_id: 't1', active: true },

0 commit comments

Comments
 (0)