@plures/praxis 1.2.41 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/{chunk-BBP2F7TT.js → chunk-MJK3IYTJ.js} +123 -5
- package/dist/browser/{chunk-FCEH7WMH.js → chunk-N63K4KWS.js} +1 -1
- package/dist/browser/{engine-65QDGCAN.js → engine-YIEGSX7U.js} +1 -1
- package/dist/browser/index.d.ts +2 -2
- package/dist/browser/index.js +10 -5
- package/dist/browser/integrations/svelte.d.ts +2 -2
- package/dist/browser/integrations/svelte.js +2 -2
- package/dist/browser/{reactive-engine.svelte-Cqd8Mod2.d.ts → reactive-engine.svelte-DjynI82A.d.ts} +83 -4
- package/dist/node/chunk-2IUFZBH3.js +87 -0
- package/dist/node/{chunk-WZ6B3LZ6.js → chunk-7CSWBDFL.js} +3 -56
- package/dist/node/{chunk-32YFEEML.js → chunk-7M3HV4XR.js} +4 -4
- package/dist/node/{chunk-PTH6MD6P.js → chunk-FWOXU4MM.js} +1 -1
- package/dist/node/{chunk-BBP2F7TT.js → chunk-KMJWAFZV.js} +128 -5
- package/dist/node/chunk-PGVSB6NR.js +59 -0
- package/dist/node/cli/index.cjs +1078 -211
- package/dist/node/cli/index.js +21 -2
- package/dist/node/cloud/index.d.cts +1 -1
- package/dist/node/cloud/index.d.ts +1 -1
- package/dist/node/{engine-7CXQV6RC.js → engine-FEN5IYZ5.js} +1 -1
- package/dist/node/index.cjs +1633 -59
- package/dist/node/index.d.cts +769 -5
- package/dist/node/index.d.ts +769 -5
- package/dist/node/index.js +1375 -45
- package/dist/node/integrations/svelte.cjs +123 -5
- package/dist/node/integrations/svelte.d.cts +3 -3
- package/dist/node/integrations/svelte.d.ts +3 -3
- package/dist/node/integrations/svelte.js +3 -3
- package/dist/node/{protocol-BocKczNv.d.ts → protocol-DcyGMmWY.d.cts} +7 -0
- package/dist/node/{protocol-BocKczNv.d.cts → protocol-DcyGMmWY.d.ts} +7 -0
- package/dist/node/{reactive-engine.svelte-CGe8SpVE.d.cts → reactive-engine.svelte-Cg0Yc2Hs.d.cts} +90 -6
- package/dist/node/{reactive-engine.svelte-D-xTDxT5.d.ts → reactive-engine.svelte-DekxqFu0.d.ts} +90 -6
- package/dist/node/{reverse-W7THPV45.js → reverse-YD3CWIGM.js} +3 -2
- package/dist/node/rules-4DAJ4Z4N.js +7 -0
- package/dist/node/server-SYZPDULV.js +361 -0
- package/dist/node/{validate-EN3M4FUR.js → validate-TQGVIG7G.js} +4 -3
- package/package.json +29 -3
- package/src/__tests__/engine-v2.test.ts +532 -0
- package/src/__tests__/expectations.test.ts +364 -0
- package/src/__tests__/factory.test.ts +426 -0
- package/src/__tests__/mcp-server.test.ts +310 -0
- package/src/__tests__/project.test.ts +396 -0
- package/src/cli/index.ts +28 -0
- package/src/core/completeness.ts +274 -0
- package/src/core/engine.ts +47 -5
- package/src/core/pluresdb/store.ts +9 -3
- package/src/core/protocol.ts +7 -0
- package/src/core/rule-result.ts +130 -0
- package/src/core/rules.ts +12 -5
- package/src/core/ui-rules.ts +340 -0
- package/src/expectations/expectations.ts +471 -0
- package/src/expectations/index.ts +29 -0
- package/src/expectations/types.ts +95 -0
- package/src/factory/factory.ts +634 -0
- package/src/factory/index.ts +27 -0
- package/src/factory/types.ts +64 -0
- package/src/index.ts +84 -0
- package/src/mcp/index.ts +33 -0
- package/src/mcp/server.ts +485 -0
- package/src/mcp/types.ts +161 -0
- package/src/project/index.ts +31 -0
- package/src/project/project.ts +423 -0
- package/src/project/types.ts +87 -0
- package/src/vite/completeness-plugin.ts +72 -0
- /package/dist/node/{chunk-R2PSBPKQ.js → chunk-TEMFJOIH.js} +0 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Rules Factory
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
6
|
+
import { inputRules, toastRules, formRules, navigationRules, dataRules } from '../factory/factory.js';
|
|
7
|
+
import { PraxisRegistry } from '../core/rules.js';
|
|
8
|
+
import { LogicEngine } from '../core/engine.js';
|
|
9
|
+
|
|
10
|
+
// ─── Input Rules ────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
describe('Rules Factory', () => {
|
|
13
|
+
describe('inputRules', () => {
|
|
14
|
+
it('should create a module with sanitization rules', () => {
|
|
15
|
+
const mod = inputRules({ sanitize: ['xss', 'sql-injection'] });
|
|
16
|
+
expect(mod.rules).toHaveLength(1);
|
|
17
|
+
expect(mod.rules![0].id).toContain('sanitize');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should detect XSS in input', () => {
|
|
21
|
+
const mod = inputRules({ sanitize: ['xss'] });
|
|
22
|
+
const registry = new PraxisRegistry({ compliance: { enabled: false } });
|
|
23
|
+
registry.registerModule(mod);
|
|
24
|
+
|
|
25
|
+
const engine = new LogicEngine({
|
|
26
|
+
initialContext: { input: { value: '<script>alert("xss")</script>' } },
|
|
27
|
+
registry,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const result = engine.step([{ tag: 'input.submit', payload: { value: '<script>alert("xss")</script>' } }]);
|
|
31
|
+
const violation = result.state.facts.find(f => f.tag === 'input.violation');
|
|
32
|
+
expect(violation).toBeDefined();
|
|
33
|
+
expect((violation!.payload as { violations: string[] }).violations).toContain('xss');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should pass clean input', () => {
|
|
37
|
+
const mod = inputRules({ sanitize: ['xss', 'sql-injection'] });
|
|
38
|
+
const registry = new PraxisRegistry({ compliance: { enabled: false } });
|
|
39
|
+
registry.registerModule(mod);
|
|
40
|
+
|
|
41
|
+
const engine = new LogicEngine({
|
|
42
|
+
initialContext: { input: { value: 'Hello world' } },
|
|
43
|
+
registry,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const result = engine.step([{ tag: 'input.submit', payload: { value: 'Hello world' } }]);
|
|
47
|
+
const valid = result.state.facts.find(f => f.tag === 'input.valid');
|
|
48
|
+
expect(valid).toBeDefined();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should detect SQL injection', () => {
|
|
52
|
+
const mod = inputRules({ sanitize: ['sql-injection'] });
|
|
53
|
+
const registry = new PraxisRegistry({ compliance: { enabled: false } });
|
|
54
|
+
registry.registerModule(mod);
|
|
55
|
+
|
|
56
|
+
const engine = new LogicEngine({
|
|
57
|
+
initialContext: {},
|
|
58
|
+
registry,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const result = engine.step([{ tag: 'input.submit', payload: { value: "'; DROP TABLE users; --" } }]);
|
|
62
|
+
const violation = result.state.facts.find(f => f.tag === 'input.violation');
|
|
63
|
+
expect(violation).toBeDefined();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should add max length constraint', () => {
|
|
67
|
+
const mod = inputRules({ maxLength: 10 });
|
|
68
|
+
expect(mod.constraints).toHaveLength(1);
|
|
69
|
+
expect(mod.constraints![0].id).toContain('max-length');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should add required constraint', () => {
|
|
73
|
+
const mod = inputRules({ required: true });
|
|
74
|
+
expect(mod.constraints).toHaveLength(1);
|
|
75
|
+
expect(mod.constraints![0].id).toContain('required');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should support custom field names', () => {
|
|
79
|
+
const mod = inputRules({ sanitize: ['xss'], fieldName: 'search' });
|
|
80
|
+
expect(mod.rules![0].id).toContain('search');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should include contracts on all rules', () => {
|
|
84
|
+
const mod = inputRules({ sanitize: ['xss'], maxLength: 100, required: true });
|
|
85
|
+
for (const rule of mod.rules ?? []) {
|
|
86
|
+
expect(rule.contract).toBeDefined();
|
|
87
|
+
expect(rule.contract!.behavior).toBeTruthy();
|
|
88
|
+
}
|
|
89
|
+
for (const constraint of mod.constraints ?? []) {
|
|
90
|
+
expect(constraint.contract).toBeDefined();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// ─── Toast Rules ────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
describe('toastRules', () => {
|
|
98
|
+
it('should create toast show rule', () => {
|
|
99
|
+
const mod = toastRules();
|
|
100
|
+
expect(mod.rules).toHaveLength(1);
|
|
101
|
+
expect(mod.rules![0].id).toBe('factory/toast.show');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should emit toast on request', () => {
|
|
105
|
+
const mod = toastRules();
|
|
106
|
+
const registry = new PraxisRegistry({ compliance: { enabled: false } });
|
|
107
|
+
registry.registerModule(mod);
|
|
108
|
+
|
|
109
|
+
const engine = new LogicEngine({
|
|
110
|
+
initialContext: {},
|
|
111
|
+
registry,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const result = engine.step([{
|
|
115
|
+
tag: 'toast.request',
|
|
116
|
+
payload: { message: 'Settings saved!' },
|
|
117
|
+
}]);
|
|
118
|
+
|
|
119
|
+
const toast = result.state.facts.find(f => f.tag === 'toast.show');
|
|
120
|
+
expect(toast).toBeDefined();
|
|
121
|
+
expect((toast!.payload as { message: string }).message).toBe('Settings saved!');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should skip toast when requireDiff and no diff', () => {
|
|
125
|
+
const mod = toastRules({ requireDiff: true });
|
|
126
|
+
const registry = new PraxisRegistry({ compliance: { enabled: false } });
|
|
127
|
+
registry.registerModule(mod);
|
|
128
|
+
|
|
129
|
+
const engine = new LogicEngine({
|
|
130
|
+
initialContext: { diff: null },
|
|
131
|
+
registry,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const result = engine.step([{
|
|
135
|
+
tag: 'toast.request',
|
|
136
|
+
payload: { message: 'Settings saved!' },
|
|
137
|
+
}]);
|
|
138
|
+
|
|
139
|
+
const toast = result.state.facts.find(f => f.tag === 'toast.show');
|
|
140
|
+
expect(toast).toBeUndefined();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should show toast when requireDiff and diff exists', () => {
|
|
144
|
+
const mod = toastRules({ requireDiff: true });
|
|
145
|
+
const registry = new PraxisRegistry({ compliance: { enabled: false } });
|
|
146
|
+
registry.registerModule(mod);
|
|
147
|
+
|
|
148
|
+
const engine = new LogicEngine({
|
|
149
|
+
initialContext: { diff: { theme: 'dark' } },
|
|
150
|
+
registry,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const result = engine.step([{
|
|
154
|
+
tag: 'toast.request',
|
|
155
|
+
payload: { message: 'Theme updated!' },
|
|
156
|
+
}]);
|
|
157
|
+
|
|
158
|
+
const toast = result.state.facts.find(f => f.tag === 'toast.show');
|
|
159
|
+
expect(toast).toBeDefined();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should add deduplicate constraint', () => {
|
|
163
|
+
const mod = toastRules({ deduplicate: true });
|
|
164
|
+
expect(mod.constraints).toHaveLength(1);
|
|
165
|
+
expect(mod.constraints![0].id).toBe('factory/toast.no-duplicates');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should include autoDismissMs in toast payload', () => {
|
|
169
|
+
const mod = toastRules({ autoDismissMs: 3000 });
|
|
170
|
+
const registry = new PraxisRegistry({ compliance: { enabled: false } });
|
|
171
|
+
registry.registerModule(mod);
|
|
172
|
+
|
|
173
|
+
const engine = new LogicEngine({
|
|
174
|
+
initialContext: {},
|
|
175
|
+
registry,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const result = engine.step([{
|
|
179
|
+
tag: 'toast.request',
|
|
180
|
+
payload: { message: 'Quick toast' },
|
|
181
|
+
}]);
|
|
182
|
+
|
|
183
|
+
const toast = result.state.facts.find(f => f.tag === 'toast.show');
|
|
184
|
+
expect((toast!.payload as { autoDismissMs: number }).autoDismissMs).toBe(3000);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// ─── Form Rules ─────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
describe('formRules', () => {
|
|
191
|
+
it('should always include dirty tracking rule', () => {
|
|
192
|
+
const mod = formRules();
|
|
193
|
+
expect(mod.rules!.some(r => r.id.includes('dirty-tracking'))).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should add validate-on-blur rule when configured', () => {
|
|
197
|
+
const mod = formRules({ validateOnBlur: true });
|
|
198
|
+
expect(mod.rules!.some(r => r.id.includes('validate-on-blur'))).toBe(true);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should add submit gate constraint when configured', () => {
|
|
202
|
+
const mod = formRules({ submitGate: true });
|
|
203
|
+
expect(mod.constraints!.some(c => c.id.includes('submit-gate'))).toBe(true);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should track dirty state on change events', () => {
|
|
207
|
+
const mod = formRules();
|
|
208
|
+
const registry = new PraxisRegistry({ compliance: { enabled: false } });
|
|
209
|
+
registry.registerModule(mod);
|
|
210
|
+
|
|
211
|
+
const engine = new LogicEngine({
|
|
212
|
+
initialContext: {},
|
|
213
|
+
registry,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const result = engine.step([{
|
|
217
|
+
tag: 'form.change',
|
|
218
|
+
payload: { field: 'name', value: 'Alice' },
|
|
219
|
+
}]);
|
|
220
|
+
|
|
221
|
+
const dirty = result.state.facts.find(f => f.tag === 'form.dirty');
|
|
222
|
+
expect(dirty).toBeDefined();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should clear dirty state on reset', () => {
|
|
226
|
+
const mod = formRules();
|
|
227
|
+
const registry = new PraxisRegistry({ compliance: { enabled: false } });
|
|
228
|
+
registry.registerModule(mod);
|
|
229
|
+
|
|
230
|
+
const engine = new LogicEngine({
|
|
231
|
+
initialContext: {},
|
|
232
|
+
registry,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const result = engine.step([{ tag: 'form.reset', payload: {} }]);
|
|
236
|
+
// Should retract form.dirty
|
|
237
|
+
const dirty = result.state.facts.find(f => f.tag === 'form.dirty');
|
|
238
|
+
expect(dirty).toBeUndefined();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should support custom form names', () => {
|
|
242
|
+
const mod = formRules({ formName: 'signup' });
|
|
243
|
+
expect(mod.rules![0].id).toContain('signup');
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// ─── Navigation Rules ─────────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
describe('navigationRules', () => {
|
|
250
|
+
it('should create navigation handler rule', () => {
|
|
251
|
+
const mod = navigationRules();
|
|
252
|
+
expect(mod.rules!.some(r => r.id === 'factory/navigation.handle')).toBe(true);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should allow navigation when no guards active', () => {
|
|
256
|
+
const mod = navigationRules();
|
|
257
|
+
const registry = new PraxisRegistry({ compliance: { enabled: false } });
|
|
258
|
+
registry.registerModule(mod);
|
|
259
|
+
|
|
260
|
+
const engine = new LogicEngine({
|
|
261
|
+
initialContext: { dirty: false, authenticated: true },
|
|
262
|
+
registry,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const result = engine.step([{
|
|
266
|
+
tag: 'navigation.request',
|
|
267
|
+
payload: { target: '/settings' },
|
|
268
|
+
}]);
|
|
269
|
+
|
|
270
|
+
expect(result.state.facts.some(f => f.tag === 'navigation.allowed')).toBe(true);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('should block navigation when dirty and dirtyGuard enabled', () => {
|
|
274
|
+
const mod = navigationRules({ dirtyGuard: true });
|
|
275
|
+
const registry = new PraxisRegistry({ compliance: { enabled: false } });
|
|
276
|
+
registry.registerModule(mod);
|
|
277
|
+
|
|
278
|
+
const engine = new LogicEngine({
|
|
279
|
+
initialContext: { dirty: true, authenticated: true },
|
|
280
|
+
registry,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const result = engine.step([{
|
|
284
|
+
tag: 'navigation.request',
|
|
285
|
+
payload: { target: '/other' },
|
|
286
|
+
}]);
|
|
287
|
+
|
|
288
|
+
const blocked = result.state.facts.find(f => f.tag === 'navigation.blocked');
|
|
289
|
+
expect(blocked).toBeDefined();
|
|
290
|
+
expect((blocked!.payload as { reasons: string[] }).reasons).toContain('Unsaved changes will be lost');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should block navigation when not authenticated and authRequired', () => {
|
|
294
|
+
const mod = navigationRules({ authRequired: true });
|
|
295
|
+
const registry = new PraxisRegistry({ compliance: { enabled: false } });
|
|
296
|
+
registry.registerModule(mod);
|
|
297
|
+
|
|
298
|
+
const engine = new LogicEngine({
|
|
299
|
+
initialContext: { dirty: false, authenticated: false },
|
|
300
|
+
registry,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const result = engine.step([{
|
|
304
|
+
tag: 'navigation.request',
|
|
305
|
+
payload: { target: '/admin' },
|
|
306
|
+
}]);
|
|
307
|
+
|
|
308
|
+
const blocked = result.state.facts.find(f => f.tag === 'navigation.blocked');
|
|
309
|
+
expect(blocked).toBeDefined();
|
|
310
|
+
expect((blocked!.payload as { reasons: string[] }).reasons).toContain('Authentication required');
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('should add dirty guard constraint', () => {
|
|
314
|
+
const mod = navigationRules({ dirtyGuard: true });
|
|
315
|
+
expect(mod.constraints!.some(c => c.id === 'factory/navigation.dirty-guard')).toBe(true);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// ─── Data Rules ─────────────────────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
describe('dataRules', () => {
|
|
322
|
+
it('should create optimistic update rule', () => {
|
|
323
|
+
const mod = dataRules({ optimisticUpdate: true });
|
|
324
|
+
expect(mod.rules!.some(r => r.id.includes('optimistic-update'))).toBe(true);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('should emit optimistic update on mutate', () => {
|
|
328
|
+
const mod = dataRules({ optimisticUpdate: true });
|
|
329
|
+
const registry = new PraxisRegistry({ compliance: { enabled: false } });
|
|
330
|
+
registry.registerModule(mod);
|
|
331
|
+
|
|
332
|
+
const engine = new LogicEngine({
|
|
333
|
+
initialContext: {},
|
|
334
|
+
registry,
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const result = engine.step([{
|
|
338
|
+
tag: 'data.mutate',
|
|
339
|
+
payload: { id: 'item-1', data: { name: 'Updated' } },
|
|
340
|
+
}]);
|
|
341
|
+
|
|
342
|
+
const optimistic = result.state.facts.find(f => f.tag === 'data.optimistic');
|
|
343
|
+
expect(optimistic).toBeDefined();
|
|
344
|
+
expect((optimistic!.payload as { pending: boolean }).pending).toBe(true);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should create rollback rule', () => {
|
|
348
|
+
const mod = dataRules({ rollbackOnError: true });
|
|
349
|
+
expect(mod.rules!.some(r => r.id.includes('rollback'))).toBe(true);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('should create cache invalidation rule', () => {
|
|
353
|
+
const mod = dataRules({ cacheInvalidation: true });
|
|
354
|
+
expect(mod.rules!.some(r => r.id.includes('cache-invalidate'))).toBe(true);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('should emit cache invalidation on confirmation', () => {
|
|
358
|
+
const mod = dataRules({ cacheInvalidation: true });
|
|
359
|
+
const registry = new PraxisRegistry({ compliance: { enabled: false } });
|
|
360
|
+
registry.registerModule(mod);
|
|
361
|
+
|
|
362
|
+
const engine = new LogicEngine({
|
|
363
|
+
initialContext: {},
|
|
364
|
+
registry,
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
const result = engine.step([{
|
|
368
|
+
tag: 'data.confirmed',
|
|
369
|
+
payload: { id: 'item-1' },
|
|
370
|
+
}]);
|
|
371
|
+
|
|
372
|
+
expect(result.state.facts.some(f => f.tag === 'data.cache-invalidate')).toBe(true);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('should always include integrity constraint', () => {
|
|
376
|
+
const mod = dataRules();
|
|
377
|
+
expect(mod.constraints!.some(c => c.id.includes('integrity'))).toBe(true);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('should support custom entity names', () => {
|
|
381
|
+
const mod = dataRules({ entityName: 'products', optimisticUpdate: true });
|
|
382
|
+
expect(mod.rules![0].id).toContain('products');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('should include contracts on all rules and constraints', () => {
|
|
386
|
+
const mod = dataRules({
|
|
387
|
+
optimisticUpdate: true,
|
|
388
|
+
rollbackOnError: true,
|
|
389
|
+
cacheInvalidation: true,
|
|
390
|
+
});
|
|
391
|
+
for (const rule of mod.rules ?? []) {
|
|
392
|
+
expect(rule.contract).toBeDefined();
|
|
393
|
+
expect(rule.contract!.invariants.length).toBeGreaterThan(0);
|
|
394
|
+
}
|
|
395
|
+
for (const constraint of mod.constraints ?? []) {
|
|
396
|
+
expect(constraint.contract).toBeDefined();
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// ─── Integration ──────────────────────────────────────────────────────
|
|
402
|
+
|
|
403
|
+
describe('integration', () => {
|
|
404
|
+
it('should register multiple factory modules in one registry', () => {
|
|
405
|
+
const registry = new PraxisRegistry({ compliance: { enabled: false } });
|
|
406
|
+
|
|
407
|
+
registry.registerModule(inputRules({ sanitize: ['xss'], required: true }));
|
|
408
|
+
registry.registerModule(toastRules({ requireDiff: true, deduplicate: true }));
|
|
409
|
+
registry.registerModule(formRules({ validateOnBlur: true, submitGate: true }));
|
|
410
|
+
registry.registerModule(navigationRules({ dirtyGuard: true, authRequired: true }));
|
|
411
|
+
registry.registerModule(dataRules({ optimisticUpdate: true, rollbackOnError: true }));
|
|
412
|
+
|
|
413
|
+
const ruleIds = registry.getRuleIds();
|
|
414
|
+
expect(ruleIds.length).toBeGreaterThanOrEqual(5);
|
|
415
|
+
|
|
416
|
+
const constraintIds = registry.getConstraintIds();
|
|
417
|
+
expect(constraintIds.length).toBeGreaterThanOrEqual(3);
|
|
418
|
+
|
|
419
|
+
// All should have contracts
|
|
420
|
+
const rules = registry.getAllRules();
|
|
421
|
+
for (const rule of rules) {
|
|
422
|
+
expect(rule.contract).toBeDefined();
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
});
|