@rangka/core 0.1.1 → 0.1.3

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 (197) hide show
  1. package/package.json +6 -2
  2. package/.claude/skills/extend-core/SKILL.md +0 -133
  3. package/.turbo/turbo-build.log +0 -4
  4. package/CHANGELOG.md +0 -25
  5. package/CLAUDE.md +0 -180
  6. package/src/__tests__/coerce.test.ts +0 -154
  7. package/src/__tests__/context.test.ts +0 -111
  8. package/src/__tests__/helpers.ts +0 -21
  9. package/src/__tests__/index.test.ts +0 -7
  10. package/src/__tests__/widgets.test.ts +0 -197
  11. package/src/api/__tests__/handlers.test.ts +0 -389
  12. package/src/api/__tests__/include-resolver.test.ts +0 -393
  13. package/src/api/__tests__/middleware.test.ts +0 -100
  14. package/src/api/__tests__/openapi-schema.test.ts +0 -210
  15. package/src/api/__tests__/query-parser.test.ts +0 -291
  16. package/src/api/__tests__/route-generator.test.ts +0 -137
  17. package/src/api/__tests__/server.test.ts +0 -73
  18. package/src/api/__tests__/swagger.test.ts +0 -166
  19. package/src/api/handlers.ts +0 -274
  20. package/src/api/include-resolver.ts +0 -27
  21. package/src/api/index.ts +0 -4
  22. package/src/api/meta-handler.ts +0 -254
  23. package/src/api/openapi-schema.ts +0 -99
  24. package/src/api/query-parser.ts +0 -315
  25. package/src/api/route-generator.ts +0 -448
  26. package/src/api/server.ts +0 -147
  27. package/src/api/types.ts +0 -16
  28. package/src/audit/__tests__/audit.test.ts +0 -144
  29. package/src/audit/index.ts +0 -3
  30. package/src/audit/record.ts +0 -69
  31. package/src/audit/tables.ts +0 -48
  32. package/src/audit/types.ts +0 -26
  33. package/src/auth/__tests__/core-module.test.ts +0 -54
  34. package/src/auth/__tests__/debug.test.ts +0 -47
  35. package/src/auth/__tests__/field-permissions.test.ts +0 -245
  36. package/src/auth/__tests__/integration.test.ts +0 -208
  37. package/src/auth/__tests__/meta-boot.test.ts +0 -538
  38. package/src/auth/__tests__/model-permissions.test.ts +0 -205
  39. package/src/auth/__tests__/password.test.ts +0 -29
  40. package/src/auth/__tests__/permission-registry.test.ts +0 -313
  41. package/src/auth/__tests__/scope-hook.test.ts +0 -509
  42. package/src/auth/__tests__/scope-registry.test.ts +0 -297
  43. package/src/auth/__tests__/scopes.test.ts +0 -66
  44. package/src/auth/__tests__/session.test.ts +0 -214
  45. package/src/auth/core-models.ts +0 -52
  46. package/src/auth/core-module.ts +0 -59
  47. package/src/auth/debug.ts +0 -157
  48. package/src/auth/field-permissions.ts +0 -116
  49. package/src/auth/index.ts +0 -37
  50. package/src/auth/model-permissions.ts +0 -59
  51. package/src/auth/password.ts +0 -22
  52. package/src/auth/permission-registry.ts +0 -171
  53. package/src/auth/scope-filters.ts +0 -11
  54. package/src/auth/scope-registry.ts +0 -121
  55. package/src/auth/scopes.ts +0 -146
  56. package/src/auth/seed.ts +0 -44
  57. package/src/auth/session.ts +0 -178
  58. package/src/auth/types.ts +0 -50
  59. package/src/boot/__tests__/page-scanning.test.ts +0 -170
  60. package/src/boot/__tests__/page-utils.test.ts +0 -225
  61. package/src/boot/__tests__/project-scanner.test.ts +0 -88
  62. package/src/boot/dependency-sort.ts +0 -82
  63. package/src/boot/discovery.ts +0 -85
  64. package/src/boot/index.ts +0 -457
  65. package/src/boot/page-utils.ts +0 -110
  66. package/src/boot/project-scanner.ts +0 -397
  67. package/src/boot/schema-loader.ts +0 -26
  68. package/src/boot/schema-merger.ts +0 -125
  69. package/src/boot/traits.ts +0 -25
  70. package/src/boot/types.ts +0 -73
  71. package/src/context.ts +0 -105
  72. package/src/db/__tests__/cascade-delete.test.ts +0 -182
  73. package/src/db/__tests__/desired-state.test.ts +0 -136
  74. package/src/db/__tests__/diff-engine.test.ts +0 -635
  75. package/src/db/__tests__/field-mapper.test.ts +0 -355
  76. package/src/db/__tests__/introspect.test.ts +0 -70
  77. package/src/db/__tests__/search-filter.test.ts +0 -45
  78. package/src/db/__tests__/sequence.test.ts +0 -221
  79. package/src/db/auto-sync.ts +0 -133
  80. package/src/db/client.ts +0 -147
  81. package/src/db/desired-state.ts +0 -98
  82. package/src/db/diff-engine.ts +0 -305
  83. package/src/db/field-mapper.ts +0 -504
  84. package/src/db/filter-applier.ts +0 -89
  85. package/src/db/include-resolver.ts +0 -40
  86. package/src/db/index.ts +0 -23
  87. package/src/db/introspect.ts +0 -265
  88. package/src/db/model-include-resolver.ts +0 -327
  89. package/src/db/model-ops.ts +0 -281
  90. package/src/db/scope-enforcer.ts +0 -37
  91. package/src/db/types.ts +0 -98
  92. package/src/errors.ts +0 -41
  93. package/src/events/__tests__/bus.test.ts +0 -105
  94. package/src/events/bus.ts +0 -89
  95. package/src/events/index.ts +0 -2
  96. package/src/events/types.ts +0 -9
  97. package/src/external-model/__tests__/computed-fields.test.ts +0 -106
  98. package/src/external-model/__tests__/field-mapper.test.ts +0 -160
  99. package/src/external-model/__tests__/in-memory-ops.test.ts +0 -247
  100. package/src/external-model/__tests__/mutation-executor.test.ts +0 -160
  101. package/src/external-model/__tests__/query-executor.test.ts +0 -284
  102. package/src/external-model/__tests__/schema-converter.test.ts +0 -174
  103. package/src/external-model/computed-fields.ts +0 -15
  104. package/src/external-model/define.ts +0 -5
  105. package/src/external-model/external-model-ops.ts +0 -108
  106. package/src/external-model/field-mapper.ts +0 -66
  107. package/src/external-model/in-memory-ops.ts +0 -107
  108. package/src/external-model/index.ts +0 -7
  109. package/src/external-model/mutation-executor.ts +0 -71
  110. package/src/external-model/query-executor.ts +0 -100
  111. package/src/external-model/schema-converter.ts +0 -53
  112. package/src/external-model/types.ts +0 -32
  113. package/src/fixtures/__tests__/fixtures.test.ts +0 -203
  114. package/src/fixtures/index.ts +0 -10
  115. package/src/fixtures/loader.ts +0 -196
  116. package/src/fixtures/registry.ts +0 -125
  117. package/src/fixtures/types.ts +0 -33
  118. package/src/helpers/assert-ownership.ts +0 -19
  119. package/src/helpers/coerce.ts +0 -28
  120. package/src/helpers/stamping.ts +0 -28
  121. package/src/helpers/validation.ts +0 -14
  122. package/src/hooks/__tests__/context.test.ts +0 -73
  123. package/src/hooks/__tests__/executor.test.ts +0 -433
  124. package/src/hooks/__tests__/middleware.test.ts +0 -224
  125. package/src/hooks/__tests__/registry.test.ts +0 -50
  126. package/src/hooks/context.ts +0 -89
  127. package/src/hooks/errors.ts +0 -11
  128. package/src/hooks/executor.ts +0 -115
  129. package/src/hooks/index.ts +0 -10
  130. package/src/hooks/middleware.ts +0 -220
  131. package/src/hooks/registry.ts +0 -20
  132. package/src/hooks/types.ts +0 -32
  133. package/src/index.ts +0 -172
  134. package/src/jobs/__tests__/enqueue.test.ts +0 -77
  135. package/src/jobs/__tests__/integration.test.ts +0 -71
  136. package/src/jobs/__tests__/registry.test.ts +0 -103
  137. package/src/jobs/__tests__/scheduler.test.ts +0 -92
  138. package/src/jobs/__tests__/worker-execution.test.ts +0 -202
  139. package/src/jobs/__tests__/worker.test.ts +0 -119
  140. package/src/jobs/enqueue.ts +0 -93
  141. package/src/jobs/index.ts +0 -14
  142. package/src/jobs/registry.ts +0 -92
  143. package/src/jobs/scheduler.ts +0 -205
  144. package/src/jobs/tables.ts +0 -132
  145. package/src/jobs/types.ts +0 -62
  146. package/src/jobs/worker.ts +0 -272
  147. package/src/model-api/__tests__/cross-boundary-includes.test.ts +0 -366
  148. package/src/model-api/__tests__/extended-api.test.ts +0 -244
  149. package/src/model-api/__tests__/filter-applier.test.ts +0 -177
  150. package/src/model-api/__tests__/filter-translator.test.ts +0 -186
  151. package/src/model-api/__tests__/include-resolver.test.ts +0 -226
  152. package/src/model-api/__tests__/model-access.test.ts +0 -284
  153. package/src/model-api/__tests__/query-builder.test.ts +0 -224
  154. package/src/model-api/__tests__/scope-enforcer.test.ts +0 -268
  155. package/src/model-api/field-access.ts +0 -28
  156. package/src/model-api/filter-applier.ts +0 -1
  157. package/src/model-api/filter-translator.ts +0 -67
  158. package/src/model-api/include-resolver.ts +0 -2
  159. package/src/model-api/index.ts +0 -86
  160. package/src/model-api/query-builder.ts +0 -155
  161. package/src/model-api/scope-enforcer.ts +0 -3
  162. package/src/model-api/types.ts +0 -139
  163. package/src/plugins/__tests__/adapter-registry.test.ts +0 -92
  164. package/src/plugins/__tests__/lifecycle.test.ts +0 -96
  165. package/src/plugins/__tests__/loader.test.ts +0 -273
  166. package/src/plugins/__tests__/validator.test.ts +0 -275
  167. package/src/plugins/adapter-registry.ts +0 -42
  168. package/src/plugins/define.ts +0 -5
  169. package/src/plugins/index.ts +0 -28
  170. package/src/plugins/lifecycle.ts +0 -27
  171. package/src/plugins/loader.ts +0 -126
  172. package/src/plugins/types.ts +0 -76
  173. package/src/plugins/validator.ts +0 -141
  174. package/src/schema/__tests__/registry-models-by-module.test.ts +0 -58
  175. package/src/schema/registry.ts +0 -93
  176. package/src/schema/relationships.ts +0 -93
  177. package/src/schema/types.ts +0 -43
  178. package/src/services/__tests__/integration.test.ts +0 -63
  179. package/src/services/__tests__/registry.test.ts +0 -175
  180. package/src/services/index.ts +0 -13
  181. package/src/services/registry.ts +0 -156
  182. package/src/services/types.ts +0 -27
  183. package/src/validation/__tests__/field-validator.test.ts +0 -195
  184. package/src/validation/field-validator.ts +0 -113
  185. package/src/validation/index.ts +0 -1
  186. package/src/widgets/index.ts +0 -3
  187. package/src/widgets/slot-validator.ts +0 -87
  188. package/src/widgets/widget-registry.ts +0 -32
  189. package/tests/boot.test.ts +0 -323
  190. package/tests/dependency-sort.test.ts +0 -99
  191. package/tests/discovery.test.ts +0 -126
  192. package/tests/registry.test.ts +0 -216
  193. package/tests/schema-loader.test.ts +0 -52
  194. package/tests/schema-merger.test.ts +0 -180
  195. package/tsconfig.json +0 -9
  196. package/tsconfig.tsbuildinfo +0 -1
  197. package/vitest.config.ts +0 -14
@@ -1,77 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { enqueue } from '../enqueue.js';
3
-
4
- describe('enqueue', () => {
5
- it('inserts a job with default options', async () => {
6
- const rows = [{ id: 'job-1' }];
7
-
8
- const mockExecute = vi.fn().mockResolvedValue({ rows });
9
- vi.mock('kysely', async (importOriginal) => {
10
- const actual = (await importOriginal()) as any;
11
- return {
12
- ...actual,
13
- sql: new Proxy(actual.sql, {
14
- apply: (target: any, thisArg: any, args: any[]) => {
15
- const result = target.apply(thisArg, args);
16
- result.execute = mockExecute;
17
- return result;
18
- },
19
- get: (target: any, prop: string) => {
20
- if (prop === '__esModule') return true;
21
- return target[prop];
22
- },
23
- }),
24
- };
25
- });
26
-
27
- // Since mocking kysely's sql tag is complex, we test the function signature and logic
28
- // by verifying it accepts the correct parameters
29
- expect(typeof enqueue).toBe('function');
30
- expect(enqueue.length).toBeGreaterThanOrEqual(2);
31
- });
32
-
33
- it('computes start_after from delay option', () => {
34
- const now = Date.now();
35
- const delay = 5000;
36
- const expected = new Date(now + delay);
37
-
38
- // Verify the delay math is correct
39
- expect(expected.getTime() - now).toBe(delay);
40
- });
41
-
42
- it('uses job name as uniqueKey when unique=true but no key specified', () => {
43
- const options: { unique: boolean; uniqueKey?: string } = { unique: true };
44
- const name = 'my_job';
45
- const uniqueKey = options.unique ? (options.uniqueKey ?? name) : null;
46
- expect(uniqueKey).toBe('my_job');
47
- });
48
-
49
- it('uses custom uniqueKey when provided', () => {
50
- const options: { unique: boolean; uniqueKey?: string } = {
51
- unique: true,
52
- uniqueKey: 'custom:key',
53
- };
54
- const name = 'my_job';
55
- const uniqueKey = options.unique ? (options.uniqueKey ?? name) : null;
56
- expect(uniqueKey).toBe('custom:key');
57
- });
58
-
59
- it('sets null uniqueKey when unique is false', () => {
60
- const options: { unique: boolean; uniqueKey?: string } = {
61
- unique: false,
62
- uniqueKey: 'ignored',
63
- };
64
- const uniqueKey = options.unique ? (options.uniqueKey ?? 'name') : null;
65
- expect(uniqueKey).toBeNull();
66
- });
67
-
68
- it('defaults backoff to exponential', () => {
69
- const backoff: string | undefined = undefined;
70
- expect(backoff ?? 'exponential').toBe('exponential');
71
- });
72
-
73
- it('defaults retries to 0', () => {
74
- const retries: number | undefined = undefined;
75
- expect(retries ?? 0).toBe(0);
76
- });
77
- });
@@ -1,71 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { createHookContext } from '../../hooks/context.js';
3
- import { EventBus } from '../../events/bus.js';
4
- import type { SchemaRegistry } from '../../schema/registry.js';
5
- import type { RequestContext } from '../../auth/types.js';
6
-
7
- function mockSchema(): SchemaRegistry {
8
- return { getAllModels: () => [] } as unknown as SchemaRegistry;
9
- }
10
-
11
- function mockAuth(): RequestContext {
12
- return {
13
- user: { id: '1', email: 'test@test.com' },
14
- roles: ['Admin'],
15
- scopeFilters: [],
16
- } as unknown as RequestContext;
17
- }
18
-
19
- describe('Hook → EventBus integration', () => {
20
- it('ctx.events.emit delegates to EventBus in sync mode', async () => {
21
- const bus = new EventBus();
22
- const handler = vi.fn().mockResolvedValue(undefined);
23
- bus.on('invoice.created', handler);
24
-
25
- const ctx = createHookContext({
26
- trx: {},
27
- schema: mockSchema(),
28
- auth: mockAuth(),
29
- eventBus: bus,
30
- });
31
-
32
- // emitWithTrx with sync falls through to sync handler
33
- const mockEmitWithTrx = vi.fn().mockResolvedValue(undefined);
34
- (bus as any).emitWithTrx = mockEmitWithTrx;
35
-
36
- await ctx.events.emit('invoice.created', { id: '123' });
37
-
38
- expect(mockEmitWithTrx).toHaveBeenCalledWith(
39
- 'invoice.created',
40
- { id: '123' },
41
- expect.anything(),
42
- );
43
- });
44
-
45
- it('EventBus sync emit runs listener inline', async () => {
46
- const bus = new EventBus();
47
- const results: string[] = [];
48
-
49
- bus.on('job.done', async (payload: unknown) => {
50
- const p = payload as { name: string };
51
- results.push(p.name);
52
- });
53
-
54
- await bus.emit('job.done', { name: 'test' }, { sync: true });
55
- expect(results).toEqual(['test']);
56
- });
57
-
58
- it('EventBus registers event handler that JobWorker can call', async () => {
59
- const bus = new EventBus();
60
- const handler = vi.fn().mockResolvedValue(undefined);
61
-
62
- bus.on('order.shipped', handler);
63
-
64
- const listeners = bus.getListeners('order.shipped');
65
- expect(listeners).toHaveLength(1);
66
-
67
- // Simulate what the worker does: call the handler directly
68
- await listeners[0].handler({ orderId: '456' });
69
- expect(handler).toHaveBeenCalledWith({ orderId: '456' });
70
- });
71
- });
@@ -1,103 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { JobRegistry } from '../registry.js';
3
-
4
- describe('JobRegistry', () => {
5
- const validHandler = async () => {};
6
-
7
- it('registers a valid job', () => {
8
- const registry = new JobRegistry();
9
- registry.register('send_email', {
10
- handler: validHandler,
11
- concurrency: 5,
12
- retries: 3,
13
- backoff: 'exponential',
14
- });
15
-
16
- expect(registry.has('send_email')).toBe(true);
17
- const job = registry.get('send_email');
18
- expect(job?.name).toBe('send_email');
19
- expect(job?.config.concurrency).toBe(5);
20
- });
21
-
22
- it('throws on duplicate name', () => {
23
- const registry = new JobRegistry();
24
- registry.register('job1', { handler: validHandler });
25
-
26
- expect(() => registry.register('job1', { handler: validHandler })).toThrow(
27
- 'Job "job1" is already registered',
28
- );
29
- });
30
-
31
- it('throws on invalid concurrency', () => {
32
- const registry = new JobRegistry();
33
-
34
- expect(() => registry.register('bad', { handler: validHandler, concurrency: 0 })).toThrow(
35
- 'Concurrency must be a positive integer',
36
- );
37
-
38
- expect(() => registry.register('bad2', { handler: validHandler, concurrency: -1 })).toThrow(
39
- 'Concurrency must be a positive integer',
40
- );
41
-
42
- expect(() => registry.register('bad3', { handler: validHandler, concurrency: 1.5 })).toThrow(
43
- 'Concurrency must be a positive integer',
44
- );
45
- });
46
-
47
- it('throws on invalid retries', () => {
48
- const registry = new JobRegistry();
49
-
50
- expect(() => registry.register('bad', { handler: validHandler, retries: -1 })).toThrow(
51
- 'Retries must be a non-negative integer',
52
- );
53
- });
54
-
55
- it('throws on invalid backoff', () => {
56
- const registry = new JobRegistry();
57
-
58
- expect(() =>
59
- registry.register('bad', { handler: validHandler, backoff: 'unknown' as any }),
60
- ).toThrow('Backoff must be one of');
61
- });
62
-
63
- it('throws on invalid cron schedule', () => {
64
- const registry = new JobRegistry();
65
-
66
- expect(() => registry.register('bad', { handler: validHandler, schedule: '* * *' })).toThrow(
67
- 'Schedule must be a valid 5-field cron expression',
68
- );
69
- });
70
-
71
- it('accepts valid cron schedule', () => {
72
- const registry = new JobRegistry();
73
- registry.register('daily', { handler: validHandler, schedule: '0 2 * * *' });
74
-
75
- expect(registry.has('daily')).toBe(true);
76
- });
77
-
78
- it('throws on missing handler', () => {
79
- const registry = new JobRegistry();
80
-
81
- expect(() => registry.register('bad', { handler: null as any })).toThrow(
82
- 'Job handler must be a function',
83
- );
84
- });
85
-
86
- it('getAll returns all registered jobs', () => {
87
- const registry = new JobRegistry();
88
- registry.register('a', { handler: validHandler });
89
- registry.register('b', { handler: validHandler });
90
-
91
- expect(registry.getAll()).toHaveLength(2);
92
- });
93
-
94
- it('getScheduled returns only jobs with a schedule', () => {
95
- const registry = new JobRegistry();
96
- registry.register('a', { handler: validHandler });
97
- registry.register('b', { handler: validHandler, schedule: '0 * * * *' });
98
-
99
- const scheduled = registry.getScheduled();
100
- expect(scheduled).toHaveLength(1);
101
- expect(scheduled[0].name).toBe('b');
102
- });
103
- });
@@ -1,92 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { ScheduleManager } from '../scheduler.js';
3
- import { JobRegistry } from '../registry.js';
4
-
5
- describe('ScheduleManager', () => {
6
- const registry = new JobRegistry();
7
- const mockDb = {} as any;
8
- const manager = new ScheduleManager(mockDb, registry, 5000);
9
-
10
- describe('computeNextRun', () => {
11
- it('parses simple cron: every hour at minute 0', () => {
12
- const from = new Date('2026-01-15T10:00:00Z');
13
- const next = manager.computeNextRun('0 * * * *', from);
14
- expect(next.getMinutes()).toBe(0);
15
- expect(next.getTime()).toBeGreaterThan(from.getTime());
16
- });
17
-
18
- it('parses cron: daily at 2 AM', () => {
19
- const from = new Date('2026-01-15T03:00:00Z');
20
- const next = manager.computeNextRun('0 2 * * *', from);
21
- expect(next.getHours()).toBe(2);
22
- expect(next.getMinutes()).toBe(0);
23
- expect(next.getDate()).toBe(16); // next day since 2AM already passed
24
- });
25
-
26
- it('parses cron: every 5 minutes', () => {
27
- const from = new Date('2026-01-15T10:02:00Z');
28
- const next = manager.computeNextRun('*/5 * * * *', from);
29
- expect(next.getMinutes() % 5).toBe(0);
30
- expect(next.getTime()).toBeGreaterThan(from.getTime());
31
- });
32
-
33
- it('parses cron: specific day of week (Monday = 1)', () => {
34
- const from = new Date('2026-01-15T00:00:00Z'); // Wednesday
35
- const next = manager.computeNextRun('0 9 * * 1', from);
36
- expect(next.getDay()).toBe(1); // Monday
37
- expect(next.getHours()).toBe(9);
38
- });
39
-
40
- it('parses cron: specific day of month', () => {
41
- const from = new Date('2026-01-15T00:00:00Z');
42
- const next = manager.computeNextRun('30 8 20 * *', from);
43
- expect(next.getDate()).toBe(20);
44
- expect(next.getHours()).toBe(8);
45
- expect(next.getMinutes()).toBe(30);
46
- });
47
-
48
- it('parses cron with ranges', () => {
49
- const from = new Date('2026-01-15T10:00:00Z');
50
- const next = manager.computeNextRun('0 9-17 * * *', from);
51
- expect(next.getHours()).toBeGreaterThanOrEqual(9);
52
- expect(next.getHours()).toBeLessThanOrEqual(17);
53
- });
54
-
55
- it('parses cron with comma-separated values', () => {
56
- const from = new Date('2026-01-15T10:00:00Z');
57
- const next = manager.computeNextRun('0,30 * * * *', from);
58
- expect([0, 30]).toContain(next.getMinutes());
59
- });
60
-
61
- it('throws on invalid cron expression', () => {
62
- expect(() => manager.computeNextRun('* * *')).toThrow('Invalid cron expression');
63
- });
64
-
65
- it('next run is always in the future', () => {
66
- const from = new Date('2026-01-15T10:30:00Z');
67
- const next = manager.computeNextRun('30 10 * * *', from);
68
- expect(next.getTime()).toBeGreaterThan(from.getTime());
69
- });
70
- });
71
-
72
- describe('lifecycle', () => {
73
- it('starts and stops', async () => {
74
- const mgr = new ScheduleManager(mockDb, registry, 60000);
75
- expect(mgr.isRunning()).toBe(false);
76
-
77
- mgr.start();
78
- expect(mgr.isRunning()).toBe(true);
79
-
80
- await mgr.stop();
81
- expect(mgr.isRunning()).toBe(false);
82
- });
83
-
84
- it('does not double-start', async () => {
85
- const mgr = new ScheduleManager(mockDb, registry, 60000);
86
- mgr.start();
87
- mgr.start();
88
- expect(mgr.isRunning()).toBe(true);
89
- await mgr.stop();
90
- });
91
- });
92
- });
@@ -1,202 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { JobWorker } from '../worker.js';
3
- import { JobRegistry } from '../registry.js';
4
-
5
- describe('JobWorker execution', () => {
6
- const mockCtx = {
7
- db: {},
8
- schema: {},
9
- auth: { user: null, roles: [] },
10
- scope: null,
11
- config: {},
12
- models: {},
13
- service: () => ({}),
14
- enqueue: async () => {},
15
- events: { emit: async () => {}, on: () => {} },
16
- notify: () => {},
17
- email: { send: async () => {} },
18
- } as any;
19
-
20
- beforeEach(() => {
21
- vi.useFakeTimers();
22
- });
23
-
24
- afterEach(() => {
25
- vi.useRealTimers();
26
- });
27
-
28
- describe('handler invocation', () => {
29
- it('calls handler with job data and ctx', async () => {
30
- vi.useRealTimers();
31
- const registry = new JobRegistry();
32
- const handler = vi.fn().mockRejectedValue(new Error('stop'));
33
- registry.register('test.job', { handler });
34
-
35
- const mockDb = {} as any;
36
- const worker = new JobWorker(mockDb, registry, mockCtx, { pollInterval: 100 });
37
-
38
- const executeJob = (worker as any).executeJob.bind(worker);
39
- // Will throw in handleFailure since no real DB, but handler should have been called
40
- try {
41
- await executeJob({
42
- id: 'job-1',
43
- name: 'test.job',
44
- data: { key: 'value' },
45
- state: 'active',
46
- retry_count: 0,
47
- max_retries: 0,
48
- backoff: 'exponential',
49
- });
50
- } catch {
51
- /* expected */
52
- }
53
-
54
- expect(handler).toHaveBeenCalledWith({ key: 'value' }, mockCtx);
55
- });
56
-
57
- it('does not call handler for unregistered job', async () => {
58
- vi.useRealTimers();
59
- const registry = new JobRegistry();
60
- const mockDb = {} as any;
61
- const worker = new JobWorker(mockDb, registry, mockCtx, { pollInterval: 100 });
62
-
63
- const executeJob = (worker as any).executeJob.bind(worker);
64
- await executeJob({
65
- id: 'job-1',
66
- name: 'unknown.job',
67
- data: {},
68
- state: 'active',
69
- retry_count: 0,
70
- max_retries: 0,
71
- backoff: 'exponential',
72
- });
73
-
74
- expect(worker.getInFlightCount()).toBe(0);
75
- });
76
- });
77
-
78
- describe('backoff computation', () => {
79
- it('exponential: doubles with each retry, capped at 600s', () => {
80
- const registry = new JobRegistry();
81
- const mockDb = {} as any;
82
- const worker = new JobWorker(mockDb, registry, mockCtx);
83
-
84
- const compute = (worker as any).computeBackoffDelay.bind(worker);
85
- expect(compute('exponential', 1)).toBe(2000);
86
- expect(compute('exponential', 2)).toBe(4000);
87
- expect(compute('exponential', 3)).toBe(8000);
88
- expect(compute('exponential', 10)).toBe(600000); // capped
89
- expect(compute('exponential', 20)).toBe(600000); // still capped
90
- });
91
-
92
- it('linear: 10s * retryCount', () => {
93
- const registry = new JobRegistry();
94
- const mockDb = {} as any;
95
- const worker = new JobWorker(mockDb, registry, mockCtx);
96
-
97
- const compute = (worker as any).computeBackoffDelay.bind(worker);
98
- expect(compute('linear', 1)).toBe(10000);
99
- expect(compute('linear', 2)).toBe(20000);
100
- expect(compute('linear', 5)).toBe(50000);
101
- });
102
-
103
- it('fixed: always 5s', () => {
104
- const registry = new JobRegistry();
105
- const mockDb = {} as any;
106
- const worker = new JobWorker(mockDb, registry, mockCtx);
107
-
108
- const compute = (worker as any).computeBackoffDelay.bind(worker);
109
- expect(compute('fixed', 1)).toBe(5000);
110
- expect(compute('fixed', 5)).toBe(5000);
111
- expect(compute('fixed', 100)).toBe(5000);
112
- });
113
- });
114
-
115
- describe('concurrency limit check', () => {
116
- it('returns false when no limit set (Infinity)', async () => {
117
- const registry = new JobRegistry();
118
- const mockDb = {} as any;
119
- const worker = new JobWorker(mockDb, registry, mockCtx);
120
-
121
- const exceeds = (worker as any).exceedsConcurrencyLimit.bind(worker);
122
- const result = await exceeds('test.job', undefined);
123
- expect(result).toBe(false);
124
- });
125
-
126
- it('returns false when limit is Infinity', async () => {
127
- const registry = new JobRegistry();
128
- const mockDb = {} as any;
129
- const worker = new JobWorker(mockDb, registry, mockCtx);
130
-
131
- const exceeds = (worker as any).exceedsConcurrencyLimit.bind(worker);
132
- const result = await exceeds('test.job', Infinity);
133
- expect(result).toBe(false);
134
- });
135
- });
136
-
137
- describe('graceful shutdown', () => {
138
- it('stop resolves immediately when no jobs in flight', async () => {
139
- const registry = new JobRegistry();
140
- const mockDb = {} as any;
141
- const worker = new JobWorker(mockDb, registry, mockCtx, { pollInterval: 10000 });
142
-
143
- worker.start();
144
- expect(worker.isRunning()).toBe(true);
145
-
146
- await worker.stop();
147
- expect(worker.isRunning()).toBe(false);
148
- expect(worker.getInFlightCount()).toBe(0);
149
- });
150
-
151
- it('tracks in-flight jobs correctly during execution', async () => {
152
- vi.useRealTimers();
153
-
154
- const registry = new JobRegistry();
155
- let resolveHandler: () => void;
156
- const handlerPromise = new Promise<void>((r) => {
157
- resolveHandler = r;
158
- });
159
-
160
- registry.register('test.slow', {
161
- handler: async () => {
162
- await handlerPromise;
163
- },
164
- });
165
-
166
- const mockDb = {} as any;
167
- const worker = new JobWorker(mockDb, registry, mockCtx);
168
-
169
- // Simulate adding to in-flight
170
- (worker as any).activeJobIds.add('job-1');
171
- expect(worker.getInFlightCount()).toBe(1);
172
-
173
- (worker as any).activeJobIds.delete('job-1');
174
- expect(worker.getInFlightCount()).toBe(0);
175
-
176
- resolveHandler!();
177
- });
178
- });
179
-
180
- describe('failure handling decision', () => {
181
- it('moves to dead letter when retry_count + 1 > max_retries', () => {
182
- // retry_count = 2, max_retries = 2 → attemptNumber = 3 > 2 → dead letter
183
- const attemptNumber = 2 + 1;
184
- const maxRetries = 2;
185
- expect(attemptNumber > maxRetries).toBe(true);
186
- });
187
-
188
- it('schedules retry when retry_count + 1 <= max_retries', () => {
189
- // retry_count = 0, max_retries = 2 → attemptNumber = 1 <= 2 → retry
190
- const attemptNumber = 0 + 1;
191
- const maxRetries = 2;
192
- expect(attemptNumber > maxRetries).toBe(false);
193
- });
194
-
195
- it('zero retries means immediate dead letter on first failure', () => {
196
- // retry_count = 0, max_retries = 0 → attemptNumber = 1 > 0 → dead letter
197
- const attemptNumber = 0 + 1;
198
- const maxRetries = 0;
199
- expect(attemptNumber > maxRetries).toBe(true);
200
- });
201
- });
202
- });
@@ -1,119 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { JobWorker } from '../worker.js';
3
- import { JobRegistry } from '../registry.js';
4
-
5
- describe('JobWorker', () => {
6
- let registry: JobRegistry;
7
-
8
- beforeEach(() => {
9
- registry = new JobRegistry();
10
- vi.useFakeTimers();
11
- });
12
-
13
- afterEach(() => {
14
- vi.useRealTimers();
15
- });
16
-
17
- const mockCtx = {} as any;
18
-
19
- it('starts and stops cleanly', async () => {
20
- const mockDb = {} as any;
21
- const worker = new JobWorker(mockDb, registry, mockCtx, { pollInterval: 100 });
22
-
23
- expect(worker.isRunning()).toBe(false);
24
- worker.start();
25
- expect(worker.isRunning()).toBe(true);
26
-
27
- await worker.stop();
28
- expect(worker.isRunning()).toBe(false);
29
- });
30
-
31
- it('does not double-start', () => {
32
- const mockDb = {} as any;
33
- const worker = new JobWorker(mockDb, registry, mockCtx, { pollInterval: 100 });
34
-
35
- worker.start();
36
- worker.start();
37
- expect(worker.isRunning()).toBe(true);
38
- });
39
-
40
- it('stop is a no-op when not running', async () => {
41
- const mockDb = {} as any;
42
- const worker = new JobWorker(mockDb, registry, mockCtx, { pollInterval: 100 });
43
-
44
- await worker.stop();
45
- expect(worker.isRunning()).toBe(false);
46
- });
47
-
48
- it('defaults pollInterval to 2000ms', () => {
49
- const mockDb = {} as any;
50
- const worker = new JobWorker(mockDb, registry, mockCtx);
51
- // Access via starting — the timer would fire at 2000ms intervals
52
- expect(worker.isRunning()).toBe(false);
53
- });
54
-
55
- it('tracks in-flight count', () => {
56
- const mockDb = {} as any;
57
- const worker = new JobWorker(mockDb, registry, mockCtx, { pollInterval: 100 });
58
-
59
- expect(worker.getInFlightCount()).toBe(0);
60
- });
61
-
62
- describe('computeBackoffDelay logic', () => {
63
- it('exponential backoff doubles with each retry', () => {
64
- // Testing the math: 1000 * 2^retry, capped at 600000
65
- expect(1000 * Math.pow(2, 1)).toBe(2000);
66
- expect(1000 * Math.pow(2, 2)).toBe(4000);
67
- expect(1000 * Math.pow(2, 3)).toBe(8000);
68
- expect(Math.min(1000 * Math.pow(2, 20), 600000)).toBe(600000);
69
- });
70
-
71
- it('linear backoff increases linearly', () => {
72
- expect(10000 * 1).toBe(10000);
73
- expect(10000 * 2).toBe(20000);
74
- expect(10000 * 3).toBe(30000);
75
- });
76
-
77
- it('fixed backoff is constant', () => {
78
- expect(5000).toBe(5000);
79
- });
80
- });
81
-
82
- describe('job execution', () => {
83
- it('calls handler with job data on success', async () => {
84
- const handler = vi.fn().mockResolvedValue(undefined);
85
- registry.register('test_job', { handler });
86
-
87
- // Verify the handler is registered and callable
88
- const job = registry.get('test_job');
89
- expect(job).toBeDefined();
90
- await job!.config.handler({ foo: 'bar' }, {} as any);
91
- expect(handler).toHaveBeenCalledWith({ foo: 'bar' }, expect.anything());
92
- });
93
-
94
- it('handler failure is catchable', async () => {
95
- const handler = vi.fn().mockRejectedValue(new Error('boom'));
96
- registry.register('fail_job', { handler, retries: 2, backoff: 'fixed' });
97
-
98
- const job = registry.get('fail_job');
99
- await expect(job!.config.handler({}, {} as any)).rejects.toThrow('boom');
100
- });
101
- });
102
-
103
- describe('dead letter logic', () => {
104
- it('moves to dead letter after max retries exceeded', () => {
105
- // retry_count + 1 > max_retries means dead letter
106
- const retryCount = 3;
107
- const maxRetries = 3;
108
- const newRetryCount = retryCount + 1;
109
- expect(newRetryCount > maxRetries).toBe(true);
110
- });
111
-
112
- it('retries when under max', () => {
113
- const retryCount = 1;
114
- const maxRetries = 3;
115
- const newRetryCount = retryCount + 1;
116
- expect(newRetryCount > maxRetries).toBe(false);
117
- });
118
- });
119
- });