@vertesia/studio-utils 1.6.0-dev.20260923.085325Z → 1.6.0-dev.20261004.085740Z

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/lib/index.d.ts +2 -1
  2. package/lib/index.d.ts.map +1 -1
  3. package/lib/index.js +4 -1
  4. package/lib/index.js.map +1 -1
  5. package/lib/prompts/extract-vars.d.ts +44 -12
  6. package/lib/prompts/extract-vars.d.ts.map +1 -1
  7. package/lib/prompts/extract-vars.js +165 -78
  8. package/lib/prompts/extract-vars.js.map +1 -1
  9. package/lib/prompts/render.d.ts +7 -9
  10. package/lib/prompts/render.d.ts.map +1 -1
  11. package/lib/prompts/render.js +12 -15
  12. package/lib/prompts/render.js.map +1 -1
  13. package/lib/prompts/validate.d.ts +8 -2
  14. package/lib/prompts/validate.d.ts.map +1 -1
  15. package/lib/prompts/validate.js +87 -51
  16. package/lib/prompts/validate.js.map +1 -1
  17. package/lib/roles/agent-runs.d.ts +8 -0
  18. package/lib/roles/agent-runs.d.ts.map +1 -0
  19. package/lib/roles/agent-runs.js +36 -0
  20. package/lib/roles/agent-runs.js.map +1 -0
  21. package/lib/roles/classes.d.ts +15 -1
  22. package/lib/roles/classes.d.ts.map +1 -1
  23. package/lib/roles/classes.js +10 -1
  24. package/lib/roles/classes.js.map +1 -1
  25. package/lib/roles/content.d.ts.map +1 -1
  26. package/lib/roles/content.js +14 -4
  27. package/lib/roles/content.js.map +1 -1
  28. package/lib/roles/index.d.ts +4 -3
  29. package/lib/roles/index.d.ts.map +1 -1
  30. package/lib/roles/index.js +7 -5
  31. package/lib/roles/index.js.map +1 -1
  32. package/lib/vertesia-studio-utils.js +2 -1
  33. package/lib/vertesia-studio-utils.js.map +1 -1
  34. package/package.json +11 -10
  35. package/src/index.ts +20 -1
  36. package/src/prompts/extract-vars.ts +212 -71
  37. package/src/prompts/render.ts +24 -15
  38. package/src/prompts/validate.ts +113 -57
  39. package/src/roles/agent-runs.ts +41 -0
  40. package/src/roles/classes.ts +7 -0
  41. package/src/roles/content.ts +21 -5
  42. package/src/roles/index.ts +7 -5
  43. package/src/apps/index.test.ts +0 -166
  44. package/src/conditions/match.test.ts +0 -134
  45. package/src/prompts/render.test.ts +0 -110
  46. package/src/prompts/validate.test.ts +0 -309
  47. package/src/roles/index.test.ts +0 -221
@@ -1,309 +0,0 @@
1
- import type { JSONSchema } from '@vertesia/common';
2
- import { TemplateType } from '@vertesia/common';
3
- import { describe, expect, it } from 'vitest';
4
- import { type PromptValidationIssue, validatePrompt } from './validate.js';
5
-
6
- function findIssue(
7
- issues: PromptValidationIssue[],
8
- type: PromptValidationIssue['type'],
9
- variable?: string,
10
- ): PromptValidationIssue | undefined {
11
- return issues.find((i) => i.type === type && (variable === undefined || i.variable === variable));
12
- }
13
-
14
- const schema = (properties: Record<string, JSONSchema>, required?: string[]): JSONSchema => ({
15
- type: 'object',
16
- properties,
17
- ...(required ? { required } : {}),
18
- });
19
-
20
- describe('validatePrompt — text (no validation)', () => {
21
- it('returns no issues for plain text content regardless of schema', () => {
22
- const r = validatePrompt({
23
- content: 'Hello, this is just text. {{not a template tag}}',
24
- contentType: TemplateType.text,
25
- inputSchema: schema({ unused: { type: 'string' } }),
26
- });
27
- expect(r.error_count).toBe(0);
28
- expect(r.warning_count).toBe(0);
29
- expect(r.issues).toHaveLength(0);
30
- });
31
- });
32
-
33
- describe('validatePrompt — handlebars', () => {
34
- describe('undeclared_template_variable (error)', () => {
35
- it('flags a top-level variable that is not in the schema', () => {
36
- const r = validatePrompt({
37
- content: 'Summarize this document: {{document}}',
38
- contentType: TemplateType.handlebars,
39
- inputSchema: schema({}),
40
- });
41
- const issue = findIssue(r.issues, 'undeclared_template_variable', 'document');
42
- expect(issue).toBeDefined();
43
- expect(issue?.severity).toBe('error');
44
- expect(r.error_count).toBeGreaterThanOrEqual(1);
45
- });
46
-
47
- it('flags multiple undeclared variables independently', () => {
48
- const r = validatePrompt({
49
- content: 'Hi {{name}}, your role is {{role}} and dept is {{dept}}',
50
- contentType: TemplateType.handlebars,
51
- inputSchema: schema({ name: { type: 'string' } }),
52
- });
53
- expect(findIssue(r.issues, 'undeclared_template_variable', 'role')).toBeDefined();
54
- expect(findIssue(r.issues, 'undeclared_template_variable', 'dept')).toBeDefined();
55
- expect(findIssue(r.issues, 'undeclared_template_variable', 'name')).toBeUndefined();
56
- });
57
-
58
- it('flags the root variable when the template uses dotted access', () => {
59
- // Handlebars `{{user.name}}` references the top-level `user` — the schema must declare `user`.
60
- const r = validatePrompt({
61
- content: 'User: {{user.name}}',
62
- contentType: TemplateType.handlebars,
63
- inputSchema: schema({}),
64
- });
65
- expect(findIssue(r.issues, 'undeclared_template_variable', 'user')).toBeDefined();
66
- });
67
-
68
- it('treats no schema as schema with no properties', () => {
69
- const r = validatePrompt({
70
- content: 'Hello {{name}}',
71
- contentType: TemplateType.handlebars,
72
- });
73
- expect(findIssue(r.issues, 'undeclared_template_variable', 'name')).toBeDefined();
74
- });
75
- });
76
-
77
- describe('unused_schema_variable (warning)', () => {
78
- it('flags a schema property that the template never references', () => {
79
- const r = validatePrompt({
80
- content: 'Hello {{name}}',
81
- contentType: TemplateType.handlebars,
82
- inputSchema: schema({
83
- name: { type: 'string' },
84
- extra: { type: 'string' },
85
- }),
86
- });
87
- const issue = findIssue(r.issues, 'unused_schema_variable', 'extra');
88
- expect(issue).toBeDefined();
89
- expect(issue?.severity).toBe('warning');
90
- expect(r.warning_count).toBeGreaterThanOrEqual(1);
91
- });
92
-
93
- it('does not block validation — only errors do', () => {
94
- const r = validatePrompt({
95
- content: 'Hello {{name}}',
96
- contentType: TemplateType.handlebars,
97
- inputSchema: schema({
98
- name: { type: 'string' },
99
- unused: { type: 'string' },
100
- }),
101
- });
102
- expect(r.error_count).toBe(0);
103
- expect(r.warning_count).toBe(1);
104
- });
105
- });
106
-
107
- describe('handlebars_render_error (error)', () => {
108
- it('flags a syntax error in the template', () => {
109
- const r = validatePrompt({
110
- // Unclosed mustache → handlebars parse error
111
- content: 'Hello {{name',
112
- contentType: TemplateType.handlebars,
113
- inputSchema: schema({ name: { type: 'string' } }),
114
- });
115
- expect(findIssue(r.issues, 'handlebars_render_error')).toBeDefined();
116
- expect(r.error_count).toBeGreaterThanOrEqual(1);
117
- });
118
-
119
- it('flags an unregistered helper call', () => {
120
- const r = validatePrompt({
121
- content: 'Tactics: {{join mitre_tactics ", "}}',
122
- contentType: TemplateType.handlebars,
123
- inputSchema: schema({ mitre_tactics: { type: 'array', items: { type: 'string' } } }),
124
- });
125
-
126
- const issue = findIssue(r.issues, 'handlebars_render_error');
127
- expect(issue).toBeDefined();
128
- expect(issue?.message).toContain('Missing helper: "join"');
129
- });
130
- });
131
-
132
- describe('happy path', () => {
133
- it('returns zero issues when every template variable is declared and every declared variable is used', () => {
134
- const r = validatePrompt({
135
- content: 'Hello {{name}}, please summarize: {{document}}',
136
- contentType: TemplateType.handlebars,
137
- inputSchema: schema(
138
- {
139
- name: { type: 'string' },
140
- document: { type: 'string' },
141
- },
142
- ['name', 'document'],
143
- ),
144
- });
145
- expect(r.error_count).toBe(0);
146
- expect(r.warning_count).toBe(0);
147
- expect(r.issues).toHaveLength(0);
148
- });
149
-
150
- it('accepts handlebars block helpers (#if/#each) on declared variables', () => {
151
- const r = validatePrompt({
152
- content: '{{#if items}}Items:{{#each items}}- {{this}}\n{{/each}}{{/if}}',
153
- contentType: TemplateType.handlebars,
154
- inputSchema: schema({ items: { type: 'array', items: { type: 'string' } } }),
155
- });
156
- expect(findIssue(r.issues, 'undeclared_template_variable', 'items')).toBeUndefined();
157
- expect(r.error_count).toBe(0);
158
- });
159
- });
160
-
161
- describe('combined errors + warnings', () => {
162
- it('reports both kinds in one pass and counts them correctly', () => {
163
- const r = validatePrompt({
164
- content: 'Hi {{name}}, document: {{document}}',
165
- contentType: TemplateType.handlebars,
166
- inputSchema: schema({
167
- name: { type: 'string' },
168
- extra: { type: 'string' }, // unused → warning
169
- // document missing → error
170
- }),
171
- });
172
- expect(findIssue(r.issues, 'undeclared_template_variable', 'document')).toBeDefined();
173
- expect(findIssue(r.issues, 'unused_schema_variable', 'extra')).toBeDefined();
174
- expect(r.error_count).toBe(1);
175
- expect(r.warning_count).toBe(1);
176
- expect(r.issues.length).toBe(2);
177
- });
178
- });
179
-
180
- describe('ordering and independence of error sources', () => {
181
- // Handlebars is non-strict — `{{undeclared}}` renders as empty string, NOT an exception.
182
- // So an undeclared variable does not cause a render error; only one error is reported.
183
- it('reports undeclared_template_variable only (render does not echo it in non-strict Handlebars)', () => {
184
- const r = validatePrompt({
185
- content: 'Hello {{undefined_var}}',
186
- contentType: TemplateType.handlebars,
187
- inputSchema: schema({}),
188
- });
189
- expect(findIssue(r.issues, 'undeclared_template_variable', 'undefined_var')).toBeDefined();
190
- expect(findIssue(r.issues, 'handlebars_render_error')).toBeUndefined();
191
- expect(r.error_count).toBe(1);
192
- });
193
-
194
- // When the template has both an undeclared variable AND a real parse error,
195
- // we must surface both — and the variable error must appear first in the list
196
- // so the LLM/user sees the precise reason before the generic render error.
197
- it('reports BOTH undeclared_template_variable and handlebars_render_error, vars first', () => {
198
- const r = validatePrompt({
199
- // {{name has no closing brace → parse error. extractHandlebarsVariables fails
200
- // silently and returns an empty set, so no undeclared error is raised by
201
- // step 1 here — but the rendering step catches the parse failure.
202
- content: 'Hello {{undefined_var}} and {{name',
203
- contentType: TemplateType.handlebars,
204
- inputSchema: schema({}),
205
- });
206
- // The renderer reports the parse problem; the var-extractor couldn't even parse the
207
- // template, so it returned no usedVars and we cannot detect the undeclared var here.
208
- // The contract is "both kinds are independent and both reported when applicable".
209
- expect(findIssue(r.issues, 'handlebars_render_error')).toBeDefined();
210
- });
211
- });
212
- });
213
-
214
- describe('validatePrompt — jst', () => {
215
- it('happy path: identifier referenced and declared', () => {
216
- const r = validatePrompt({
217
- content: 'return `hello ${name}`;',
218
- contentType: TemplateType.jst,
219
- inputSchema: schema({ name: { type: 'string' } }, ['name']),
220
- });
221
- expect(r.error_count).toBe(0);
222
- expect(r.warning_count).toBe(0);
223
- });
224
-
225
- it('flags undeclared identifier as error', () => {
226
- const r = validatePrompt({
227
- content: 'return `hello ${name}`;',
228
- contentType: TemplateType.jst,
229
- inputSchema: schema({}),
230
- });
231
- expect(findIssue(r.issues, 'undeclared_template_variable', 'name')).toBeDefined();
232
- expect(r.error_count).toBeGreaterThanOrEqual(1);
233
- });
234
-
235
- it('flags declared-but-unused as warning', () => {
236
- const r = validatePrompt({
237
- content: 'return `hello ${name}`;',
238
- contentType: TemplateType.jst,
239
- inputSchema: schema({
240
- name: { type: 'string' },
241
- unused: { type: 'string' },
242
- }),
243
- });
244
- const issue = findIssue(r.issues, 'unused_schema_variable', 'unused');
245
- expect(issue).toBeDefined();
246
- expect(issue?.severity).toBe('warning');
247
- });
248
-
249
- it('does not flag locally-bound variables as undeclared', () => {
250
- const r = validatePrompt({
251
- content: 'const greeting = `hello ${name}`; return greeting;',
252
- contentType: TemplateType.jst,
253
- inputSchema: schema({ name: { type: 'string' } }, ['name']),
254
- });
255
- expect(findIssue(r.issues, 'undeclared_template_variable', 'greeting')).toBeUndefined();
256
- expect(r.error_count).toBe(0);
257
- });
258
-
259
- it('does not flag jst auto-globals (_, Array, Set)', () => {
260
- const r = validatePrompt({
261
- content: 'return _.stringify({ count: Array.isArray(items) ? items.length : 0 });',
262
- contentType: TemplateType.jst,
263
- inputSchema: schema({ items: { type: 'array', items: { type: 'string' } } }, ['items']),
264
- });
265
- expect(findIssue(r.issues, 'undeclared_template_variable', '_')).toBeUndefined();
266
- expect(findIssue(r.issues, 'undeclared_template_variable', 'Array')).toBeUndefined();
267
- });
268
-
269
- it('flags unsafe constructs (with) as jst_unsafe_construct', () => {
270
- const r = validatePrompt({
271
- content: 'with (foo) { return bar; }',
272
- contentType: TemplateType.jst,
273
- inputSchema: schema({ foo: { type: 'object' }, bar: { type: 'string' } }, ['foo', 'bar']),
274
- });
275
- expect(findIssue(r.issues, 'jst_unsafe_construct')).toBeDefined();
276
- });
277
-
278
- it('flags for-loops as jst_unsafe_construct', () => {
279
- const r = validatePrompt({
280
- content: 'for (let i = 0; i < items.length; i++) { return items[i]; } return null;',
281
- contentType: TemplateType.jst,
282
- inputSchema: schema({ items: { type: 'array', items: { type: 'string' } } }, ['items']),
283
- });
284
- expect(findIssue(r.issues, 'jst_unsafe_construct')).toBeDefined();
285
- });
286
- });
287
-
288
- describe('Handlebars runtime values', () => {
289
- it('accepts the bare time helper with an empty schema', () => {
290
- const result = validatePrompt({
291
- content: '{{_now}}',
292
- contentType: TemplateType.handlebars,
293
- inputSchema: schema({}),
294
- });
295
- expect(result.error_count).toBe(0);
296
- });
297
-
298
- it.each(['{{this._now}}', '{{_now.foo}}'])('requires a declaration for explicit data path %s', (content) => {
299
- const result = validatePrompt({ content, contentType: TemplateType.handlebars, inputSchema: schema({}) });
300
- expect(findIssue(result.issues, 'undeclared_template_variable', '_now')).toBeDefined();
301
- });
302
-
303
- it('accepts the runtime model without accepting ordinary undeclared variables', () => {
304
- const result = validatePrompt({ content: '{{_model}} {{customer}}', contentType: TemplateType.handlebars });
305
- expect(findIssue(result.issues, 'undeclared_template_variable', '_model')).toBeUndefined();
306
- expect(findIssue(result.issues, 'undeclared_template_variable', 'customer')).toBeDefined();
307
- expect(validatePrompt({ content: '{{_model}}', contentType: TemplateType.handlebars }).error_count).toBe(0);
308
- });
309
- });
@@ -1,221 +0,0 @@
1
- import { Permission, SystemRoles } from '@vertesia/common';
2
- import { describe, expect, it } from 'vitest';
3
- import { ContentRoleNames } from './content.js';
4
- import {
5
- AbacRole,
6
- getAllRoleNames,
7
- getDelegablePermissionsForRole,
8
- getPermissionsForRoles,
9
- getRoleByName,
10
- listAbacRolesForScope,
11
- listRoles,
12
- listRolesByDomain,
13
- listSystemRoles,
14
- Role,
15
- RoleList,
16
- SystemRole,
17
- } from './index.js';
18
-
19
- describe('getRoleByName', () => {
20
- it('returns a system role by name', () => {
21
- const role = getRoleByName(SystemRoles.owner);
22
- expect(role).toBeInstanceOf(SystemRole);
23
- expect(role.name).toBe('owner');
24
- expect(role.domain).toBe('system');
25
- });
26
-
27
- it('returns an ABAC role by name', () => {
28
- const role = getRoleByName(ContentRoleNames.content_reader);
29
- expect(role).toBeInstanceOf(AbacRole);
30
- expect(role.name).toBe('content:reader');
31
- expect(role.domain).toBe('content');
32
- });
33
-
34
- it('throws on unknown role', () => {
35
- expect(() => getRoleByName('not_a_real_role')).toThrow(/Role not_a_real_role not found/);
36
- });
37
-
38
- it('queries partitions in registration order — system first', () => {
39
- // System partition is registered before content. Even if a future partition
40
- // declared a role named 'owner', the system one would still win.
41
- const role = getRoleByName(SystemRoles.owner);
42
- expect(role.domain).toBe('system');
43
- });
44
- });
45
-
46
- describe('listRoles', () => {
47
- it('returns every role across all partitions', () => {
48
- const roles = listRoles();
49
- // 17 system roles + 3 content roles
50
- expect(roles).toHaveLength(20);
51
- });
52
-
53
- it('lists system roles before content roles (partition registration order)', () => {
54
- const roles = listRoles();
55
- const systemCount = roles.filter((r) => r.domain === 'system').length;
56
- const contentCount = roles.filter((r) => r.domain === 'content').length;
57
- expect(systemCount).toBe(17);
58
- expect(contentCount).toBe(3);
59
-
60
- // First 17 are system, next 3 are content
61
- for (let i = 0; i < 17; i++) expect(roles[i].domain).toBe('system');
62
- for (let i = 17; i < 20; i++) expect(roles[i].domain).toBe('content');
63
- });
64
- });
65
-
66
- describe('listRolesByDomain', () => {
67
- it('returns only system roles for "system"', () => {
68
- const roles = listRolesByDomain('system');
69
- expect(roles).toHaveLength(17);
70
- expect(roles.every((r) => r.domain === 'system')).toBe(true);
71
- });
72
-
73
- it('returns only content roles for "content"', () => {
74
- const roles = listRolesByDomain('content');
75
- expect(roles).toHaveLength(3);
76
- expect(roles.every((r) => r.domain === 'content')).toBe(true);
77
- });
78
-
79
- it('returns empty for an unregistered domain', () => {
80
- expect(listRolesByDomain('tasks')).toEqual([]);
81
- });
82
- });
83
-
84
- describe('listSystemRoles', () => {
85
- it('returns SystemRole instances', () => {
86
- const roles = listSystemRoles();
87
- expect(roles).toHaveLength(17);
88
- expect(roles.every((r) => r instanceof SystemRole)).toBe(true);
89
- });
90
-
91
- it('excludes ABAC roles', () => {
92
- const roles = listSystemRoles();
93
- expect(roles.some((r) => r instanceof AbacRole)).toBe(false);
94
- });
95
- });
96
-
97
- describe('listAbacRolesForScope', () => {
98
- it('returns content roles for "document" scope', () => {
99
- const roles = listAbacRolesForScope('document');
100
- expect(roles).toHaveLength(3);
101
- expect(roles.map((r) => r.name).sort()).toEqual(['content:manager', 'content:reader', 'content:writer']);
102
- });
103
-
104
- it('returns content roles for "collection" scope (same roles, applicableScopes covers both)', () => {
105
- const roles = listAbacRolesForScope('collection');
106
- expect(roles).toHaveLength(3);
107
- expect(roles.every((r) => r.applicableScopes.includes('collection'))).toBe(true);
108
- });
109
-
110
- it('returns empty for "task" scope (no task partition registered)', () => {
111
- expect(listAbacRolesForScope('task')).toEqual([]);
112
- });
113
-
114
- it('returns AbacRole instances only — no system roles bleed through', () => {
115
- const roles = listAbacRolesForScope('document');
116
- expect(roles.every((r) => r instanceof AbacRole)).toBe(true);
117
- });
118
- });
119
-
120
- describe('getAllRoleNames', () => {
121
- it('returns names of every registered role', () => {
122
- const names = getAllRoleNames();
123
- expect(names).toHaveLength(20);
124
- expect(names).toContain('owner');
125
- expect(names).toContain('content:reader');
126
- });
127
-
128
- it('produces a flat list suited for mongoose enum constraints', () => {
129
- const names = getAllRoleNames();
130
- expect(names.every((n) => typeof n === 'string')).toBe(true);
131
- });
132
- });
133
-
134
- describe('getPermissionsForRoles', () => {
135
- it('merges permissions across multiple roles, deduped', () => {
136
- // content:reader: ['read']
137
- // content:writer: ['read', 'write']
138
- const merged = getPermissionsForRoles([ContentRoleNames.content_reader, ContentRoleNames.content_writer]);
139
- expect(merged.sort()).toEqual(['read', 'write']);
140
- });
141
-
142
- it('returns system Permission values for system roles', () => {
143
- const merged = getPermissionsForRoles([SystemRoles.reader]);
144
- // reader role has: int_read, run_read, content_read + account_member (via OrgMemberRole)
145
- expect(merged).toContain(Permission.int_read);
146
- expect(merged).toContain(Permission.content_read);
147
- expect(merged).toContain(Permission.account_member);
148
- });
149
- });
150
-
151
- describe('getDelegablePermissionsForRole', () => {
152
- it('returns system permission keys unchanged', () => {
153
- expect(getDelegablePermissionsForRole(SystemRoles.reader)).toContain(Permission.content_read);
154
- });
155
-
156
- it('qualifies ABAC verbs with their role domain', () => {
157
- expect(getDelegablePermissionsForRole(ContentRoleNames.content_manager).sort()).toEqual([
158
- Permission.content_delete,
159
- Permission.content_read,
160
- Permission.content_write,
161
- ]);
162
- });
163
- });
164
-
165
- describe('Role instances', () => {
166
- it('SystemRole hasPermission for granted Permission', () => {
167
- const owner = getRoleByName(SystemRoles.owner);
168
- expect(owner.hasPermission(Permission.content_read)).toBe(true);
169
- expect(owner.hasPermission(Permission.manage_billing)).toBe(true);
170
- });
171
-
172
- it('SystemRole hasPermission false for arbitrary string', () => {
173
- const reader = getRoleByName(SystemRoles.reader);
174
- expect(reader.hasPermission('not_a_real_perm')).toBe(false);
175
- });
176
-
177
- it('AbacRole carries applicableScopes', () => {
178
- const reader = getRoleByName(ContentRoleNames.content_reader) as AbacRole;
179
- expect(reader.applicableScopes).toEqual(['document', 'collection']);
180
- });
181
-
182
- it('AbacRole permissions are bare verbs, not Permission enum values', () => {
183
- const manager = getRoleByName(ContentRoleNames.content_manager);
184
- expect(Array.from(manager.permissions).sort()).toEqual(['delete', 'read', 'write']);
185
- });
186
- });
187
-
188
- describe('SystemRole vs AbacRole discrimination', () => {
189
- it('SystemRole instanceof Role', () => {
190
- const owner = getRoleByName(SystemRoles.owner);
191
- expect(owner).toBeInstanceOf(Role);
192
- expect(owner).toBeInstanceOf(SystemRole);
193
- expect(owner).not.toBeInstanceOf(AbacRole);
194
- });
195
-
196
- it('AbacRole instanceof Role', () => {
197
- const reader = getRoleByName(ContentRoleNames.content_reader);
198
- expect(reader).toBeInstanceOf(Role);
199
- expect(reader).toBeInstanceOf(AbacRole);
200
- expect(reader).not.toBeInstanceOf(SystemRole);
201
- });
202
- });
203
-
204
- describe('RoleList', () => {
205
- it('fromRoleNames composes a list checkable for permissions', () => {
206
- const list = RoleList.fromRoleNames([SystemRoles.reader, SystemRoles.executor]);
207
- expect(list.hasPermission(Permission.content_read)).toBe(true); // from reader
208
- expect(list.hasPermission(Permission.int_execute)).toBe(true); // from executor
209
- expect(list.hasPermission(Permission.manage_billing)).toBe(false); // neither has it
210
- });
211
-
212
- it('fromRoleName composes a single-role list', () => {
213
- const list = RoleList.fromRoleName(SystemRoles.billing);
214
- expect(list.hasPermission(Permission.manage_billing)).toBe(true);
215
- expect(list.hasPermission(Permission.content_write)).toBe(false);
216
- });
217
-
218
- it('throws on unknown role name', () => {
219
- expect(() => RoleList.fromRoleNames(['not_real'])).toThrow(/Role not_real not found/);
220
- });
221
- });