@flowcodex/core 0.3.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.
Files changed (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +9 -0
  3. package/dist/index-LbxYtxxS.d.ts +560 -0
  4. package/dist/index.d.ts +995 -0
  5. package/dist/index.js +3840 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/kernel/index.d.ts +1 -0
  8. package/dist/kernel/index.js +551 -0
  9. package/dist/kernel/index.js.map +1 -0
  10. package/package.json +39 -0
  11. package/src/agent/agent-loop.ts +254 -0
  12. package/src/agent/context.ts +99 -0
  13. package/src/agent/conversation-state.ts +44 -0
  14. package/src/agent/provider-runner.ts +241 -0
  15. package/src/agent/system-prompt-builder.ts +193 -0
  16. package/src/execution/compactor.ts +256 -0
  17. package/src/execution/index.ts +7 -0
  18. package/src/execution/output-serializer.ts +90 -0
  19. package/src/execution/schema-validator.ts +124 -0
  20. package/src/execution/tool-executor.ts +276 -0
  21. package/src/execution/tool-registry.ts +104 -0
  22. package/src/index.ts +215 -0
  23. package/src/infrastructure/catalog-parser.ts +218 -0
  24. package/src/infrastructure/index.ts +16 -0
  25. package/src/infrastructure/path-resolver.ts +123 -0
  26. package/src/infrastructure/provider-factory.ts +116 -0
  27. package/src/infrastructure/provider-presets.ts +19 -0
  28. package/src/infrastructure/retry-policy.ts +50 -0
  29. package/src/infrastructure/secret-scrubber.ts +67 -0
  30. package/src/infrastructure/token-counter.ts +156 -0
  31. package/src/infrastructure/tracer.ts +23 -0
  32. package/src/kernel/container.ts +166 -0
  33. package/src/kernel/events.ts +323 -0
  34. package/src/kernel/index.ts +18 -0
  35. package/src/kernel/pipeline.ts +152 -0
  36. package/src/kernel/run-controller.ts +85 -0
  37. package/src/kernel/tokens.ts +21 -0
  38. package/src/security/index.ts +13 -0
  39. package/src/security/permission-policy.ts +273 -0
  40. package/src/session/audit-log.ts +201 -0
  41. package/src/session/auth-service.ts +178 -0
  42. package/src/session/index.ts +26 -0
  43. package/src/session/secret-vault.ts +183 -0
  44. package/src/session/session-store.ts +339 -0
  45. package/src/session/types.ts +100 -0
  46. package/src/types/blocks.ts +56 -0
  47. package/src/types/context.ts +54 -0
  48. package/src/types/errors.ts +359 -0
  49. package/src/types/index.ts +34 -0
  50. package/src/types/provider.ts +58 -0
  51. package/src/types/tool.ts +39 -0
  52. package/src/utils/error.ts +3 -0
  53. package/src/utils/fs.ts +185 -0
  54. package/src/utils/image-resize.ts +76 -0
  55. package/src/utils/ssrf-guard.ts +133 -0
  56. package/src/utils/ulid.ts +72 -0
  57. package/src/utils/version-check.ts +59 -0
  58. package/tests/agent-loop.test.ts +490 -0
  59. package/tests/audit-log.test.ts +199 -0
  60. package/tests/auth-service.test.ts +170 -0
  61. package/tests/blocks.test.ts +79 -0
  62. package/tests/catalog-parser.test.ts +174 -0
  63. package/tests/compactor.test.ts +180 -0
  64. package/tests/container.test.ts +224 -0
  65. package/tests/conversation-state.test.ts +75 -0
  66. package/tests/errors.test.ts +429 -0
  67. package/tests/events-v021.test.ts +60 -0
  68. package/tests/events-v022.test.ts +75 -0
  69. package/tests/events.test.ts +340 -0
  70. package/tests/fixtures/large-image.png +0 -0
  71. package/tests/fixtures/small-image.png +0 -0
  72. package/tests/fs-utils.test.ts +164 -0
  73. package/tests/image-resize.test.ts +51 -0
  74. package/tests/output-serializer.test.ts +79 -0
  75. package/tests/path-resolver.test.ts +91 -0
  76. package/tests/permission-policy.test.ts +174 -0
  77. package/tests/pipeline.test.ts +193 -0
  78. package/tests/provider-factory.test.ts +245 -0
  79. package/tests/provider-runner.test.ts +535 -0
  80. package/tests/retry-policy.test.ts +104 -0
  81. package/tests/run-controller.test.ts +115 -0
  82. package/tests/sanity.test.ts +26 -0
  83. package/tests/schema-validator.test.ts +109 -0
  84. package/tests/secret-scrubber.test.ts +133 -0
  85. package/tests/secret-vault.test.ts +130 -0
  86. package/tests/session-store.test.ts +429 -0
  87. package/tests/ssrf-guard.test.ts +112 -0
  88. package/tests/system-prompt-builder.test.ts +116 -0
  89. package/tests/token-counter.test.ts +163 -0
  90. package/tests/tokens.test.ts +42 -0
  91. package/tests/tool-executor.test.ts +452 -0
  92. package/tests/tool-registry.test.ts +143 -0
  93. package/tests/tracer.test.ts +32 -0
  94. package/tests/ulid.test.ts +53 -0
  95. package/tests/version-check.test.ts +57 -0
  96. package/tsconfig.json +11 -0
  97. package/tsup.config.ts +16 -0
@@ -0,0 +1,429 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ FlowCodexError,
4
+ ToolError,
5
+ ConfigError,
6
+ PluginError,
7
+ AgentError,
8
+ PermissionError,
9
+ SessionError,
10
+ FsError,
11
+ toFlowCodexError,
12
+ isFlowCodexError,
13
+ isToolError,
14
+ isConfigError,
15
+ isPluginError,
16
+ isSessionError,
17
+ isAgentError,
18
+ isPermissionError,
19
+ isFsError,
20
+ ERROR_CODES,
21
+ } from '../src/types/errors.js';
22
+ import { toErrorMessage } from '../src/utils/error.js';
23
+
24
+ describe('toErrorMessage', () => {
25
+ it('extracts message from Error', () => {
26
+ expect(toErrorMessage(new Error('test msg'))).toBe('test msg');
27
+ });
28
+
29
+ it('stringifies non-Error values', () => {
30
+ expect(toErrorMessage('plain string')).toBe('plain string');
31
+ expect(toErrorMessage(42)).toBe('42');
32
+ expect(toErrorMessage(null)).toBe('null');
33
+ expect(toErrorMessage(undefined)).toBe('undefined');
34
+ });
35
+ });
36
+
37
+ describe('FlowCodexError', () => {
38
+ it('creates with all fields', () => {
39
+ const err = new FlowCodexError({
40
+ message: 'something broke',
41
+ code: ERROR_CODES.UNKNOWN,
42
+ subsystem: 'general',
43
+ severity: 'error',
44
+ recoverable: true,
45
+ context: { key: 'value' },
46
+ });
47
+ expect(err.message).toBe('something broke');
48
+ expect(err.code).toBe('UNKNOWN');
49
+ expect(err.subsystem).toBe('general');
50
+ expect(err.severity).toBe('error');
51
+ expect(err.recoverable).toBe(true);
52
+ expect(err.context).toEqual({ key: 'value' });
53
+ expect(err.name).toBe('FlowCodexError');
54
+ expect(err instanceof Error).toBe(true);
55
+ });
56
+
57
+ it('defaults severity to error', () => {
58
+ const err = new FlowCodexError({
59
+ message: 'x',
60
+ code: ERROR_CODES.UNKNOWN,
61
+ subsystem: 'general',
62
+ });
63
+ expect(err.severity).toBe('error');
64
+ expect(err.recoverable).toBe(false);
65
+ expect(err.context).toBeUndefined();
66
+ });
67
+
68
+ it('describe() includes code, message, and context', () => {
69
+ const err = new FlowCodexError({
70
+ message: 'failed',
71
+ code: ERROR_CODES.PROVIDER_NETWORK_ERROR,
72
+ subsystem: 'provider',
73
+ context: { url: 'https://example.com', retry: 3 },
74
+ });
75
+ const d = err.describe();
76
+ expect(d).toContain('PROVIDER_NETWORK_ERROR');
77
+ expect(d).toContain('failed');
78
+ expect(d).toContain('url=https://example.com');
79
+ expect(d).toContain('retry=3');
80
+ });
81
+
82
+ it('describe() with no context omits brackets', () => {
83
+ const err = new FlowCodexError({
84
+ message: 'plain',
85
+ code: ERROR_CODES.UNKNOWN,
86
+ subsystem: 'general',
87
+ });
88
+ expect(err.describe()).toBe('UNKNOWN: plain');
89
+ });
90
+
91
+ it('preserves cause', () => {
92
+ const cause = new Error('root cause');
93
+ const err = new FlowCodexError({
94
+ message: 'wrapper',
95
+ code: ERROR_CODES.AGENT_RUN_FAILED,
96
+ subsystem: 'agent',
97
+ cause,
98
+ });
99
+ expect(err.cause).toBe(cause);
100
+ });
101
+ });
102
+
103
+ describe('ToolError', () => {
104
+ it('sets subsystem to tool and includes toolName in context', () => {
105
+ const err = new ToolError({
106
+ message: 'not found',
107
+ code: ERROR_CODES.TOOL_NOT_FOUND,
108
+ toolName: 'bash',
109
+ });
110
+ expect(err.subsystem).toBe('tool');
111
+ expect(err.toolName).toBe('bash');
112
+ expect(err.context).toEqual({ tool: 'bash' });
113
+ expect(err.name).toBe('ToolError');
114
+ expect(isFlowCodexError(err)).toBe(true);
115
+ });
116
+
117
+ it('merges extra context with tool name', () => {
118
+ const err = new ToolError({
119
+ message: 'timeout',
120
+ code: ERROR_CODES.TOOL_TIMEOUT,
121
+ toolName: 'read',
122
+ context: { duration: 5000 },
123
+ });
124
+ expect(err.context).toEqual({ tool: 'read', duration: 5000 });
125
+ });
126
+ });
127
+
128
+ describe('ConfigError', () => {
129
+ it('is always fatal and non-recoverable', () => {
130
+ const err = new ConfigError({
131
+ message: 'bad config',
132
+ code: ERROR_CODES.CONFIG_INVALID,
133
+ });
134
+ expect(err.severity).toBe('fatal');
135
+ expect(err.recoverable).toBe(false);
136
+ expect(err.subsystem).toBe('config');
137
+ expect(err.name).toBe('ConfigError');
138
+ });
139
+ });
140
+
141
+ describe('PluginError', () => {
142
+ it('missing dependency is recoverable', () => {
143
+ const err = new PluginError({
144
+ message: 'missing dep',
145
+ code: ERROR_CODES.PLUGIN_MISSING_DEPENDENCY,
146
+ pluginName: 'my-plugin',
147
+ });
148
+ expect(err.recoverable).toBe(true);
149
+ expect(err.pluginName).toBe('my-plugin');
150
+ });
151
+
152
+ it('load failed is not recoverable', () => {
153
+ const err = new PluginError({
154
+ message: 'load fail',
155
+ code: ERROR_CODES.PLUGIN_LOAD_FAILED,
156
+ pluginName: 'bad-plugin',
157
+ });
158
+ expect(err.recoverable).toBe(false);
159
+ });
160
+ });
161
+
162
+ describe('AgentError', () => {
163
+ it('aborted is warning severity', () => {
164
+ const err = new AgentError({
165
+ message: 'user aborted',
166
+ code: ERROR_CODES.AGENT_ABORTED,
167
+ });
168
+ expect(err.severity).toBe('warning');
169
+ });
170
+
171
+ it('iteration limit is recoverable by default', () => {
172
+ const err = new AgentError({
173
+ message: 'limit reached',
174
+ code: ERROR_CODES.AGENT_ITERATION_LIMIT,
175
+ });
176
+ expect(err.recoverable).toBe(true);
177
+ });
178
+
179
+ it('budget exceeded is not recoverable by default', () => {
180
+ const err = new AgentError({
181
+ message: 'cost cap hit',
182
+ code: ERROR_CODES.AGENT_BUDGET_EXCEEDED,
183
+ });
184
+ expect(err.recoverable).toBe(false);
185
+ expect(err.severity).toBe('error');
186
+ });
187
+
188
+ it('accepts COMPACTION_FAILED', () => {
189
+ const err = new AgentError({
190
+ message: 'could not reduce context below hard threshold',
191
+ code: ERROR_CODES.COMPACTION_FAILED,
192
+ });
193
+ expect(err.code).toBe('COMPACTION_FAILED');
194
+ expect(err.subsystem).toBe('agent');
195
+ });
196
+
197
+ it('accepts PROMPT_BUDGET_EXCEEDED', () => {
198
+ const err = new AgentError({
199
+ message: 'system prompt layers exceeded budget',
200
+ code: ERROR_CODES.PROMPT_BUDGET_EXCEEDED,
201
+ });
202
+ expect(err.code).toBe('PROMPT_BUDGET_EXCEEDED');
203
+ expect(err.subsystem).toBe('agent');
204
+ });
205
+ });
206
+
207
+ describe('PermissionError', () => {
208
+ it('reuses TOOL_PERMISSION_DENIED code with tool subsystem', () => {
209
+ const err = new PermissionError({
210
+ message: 'denied by policy',
211
+ tool: 'bash',
212
+ pattern: 'rm -rf *',
213
+ source: 'trust',
214
+ });
215
+ expect(err.code).toBe('TOOL_PERMISSION_DENIED');
216
+ expect(err.subsystem).toBe('tool');
217
+ expect(err.name).toBe('PermissionError');
218
+ expect(err.recoverable).toBe(false);
219
+ expect(err.tool).toBe('bash');
220
+ expect(err.pattern).toBe('rm -rf *');
221
+ expect(err.source).toBe('trust');
222
+ });
223
+
224
+ it('carries tool, pattern, and source in context', () => {
225
+ const err = new PermissionError({
226
+ message: 'user denied',
227
+ tool: 'edit',
228
+ source: 'user',
229
+ });
230
+ expect(err.context).toEqual({ tool: 'edit', pattern: undefined, source: 'user' });
231
+ });
232
+
233
+ it('is a FlowCodexError but not a ToolError', () => {
234
+ const err = new PermissionError({
235
+ message: 'x',
236
+ tool: 'bash',
237
+ source: 'deny',
238
+ });
239
+ expect(isFlowCodexError(err)).toBe(true);
240
+ expect(isPermissionError(err)).toBe(true);
241
+ expect(isToolError(err)).toBe(false);
242
+ });
243
+
244
+ it('isPermissionError rejects plain errors', () => {
245
+ expect(isPermissionError(new Error('plain'))).toBe(false);
246
+ });
247
+ });
248
+
249
+ describe('SessionError', () => {
250
+ it('corrupted is not recoverable', () => {
251
+ const err = new SessionError({
252
+ message: 'jsonl malformed',
253
+ code: ERROR_CODES.SESSION_CORRUPTED,
254
+ sessionId: '01ABC',
255
+ });
256
+ expect(err.recoverable).toBe(false);
257
+ expect(err.sessionId).toBe('01ABC');
258
+ expect(err.context).toEqual({ sessionId: '01ABC' });
259
+ });
260
+
261
+ it('write failed is warning severity', () => {
262
+ const err = new SessionError({
263
+ message: 'disk full',
264
+ code: ERROR_CODES.SESSION_WRITE_FAILED,
265
+ });
266
+ expect(err.severity).toBe('error');
267
+ });
268
+ });
269
+
270
+ describe('FsError', () => {
271
+ it('read failed is not recoverable', () => {
272
+ const err = new FsError({
273
+ message: 'no such file',
274
+ code: ERROR_CODES.FS_READ_FAILED,
275
+ path: '/missing.txt',
276
+ });
277
+ expect(err.recoverable).toBe(false);
278
+ expect(err.path).toBe('/missing.txt');
279
+ });
280
+
281
+ it('path escape sets path in context', () => {
282
+ const err = new FsError({
283
+ message: 'outside project root',
284
+ code: ERROR_CODES.FS_PATH_ESCAPE,
285
+ path: '/etc/passwd',
286
+ });
287
+ expect(err.code).toBe('FS_PATH_ESCAPE');
288
+ expect(err.context).toEqual({ path: '/etc/passwd' });
289
+ });
290
+ });
291
+
292
+ describe('toFlowCodexError', () => {
293
+ it('passes through FlowCodexError unchanged', () => {
294
+ const original = new ToolError({
295
+ message: 'orig',
296
+ code: ERROR_CODES.TOOL_NOT_FOUND,
297
+ toolName: 'x',
298
+ });
299
+ const wrapped = toFlowCodexError(original);
300
+ expect(wrapped).toBe(original);
301
+ });
302
+
303
+ it('wraps plain Error into AgentError', () => {
304
+ const wrapped = toFlowCodexError(new Error('boom'));
305
+ expect(wrapped).toBeInstanceOf(AgentError);
306
+ expect(wrapped.code).toBe('AGENT_RUN_FAILED');
307
+ expect(wrapped.message).toBe('boom');
308
+ expect(wrapped.cause).toBeInstanceOf(Error);
309
+ });
310
+
311
+ it('wraps non-Error values', () => {
312
+ const wrapped = toFlowCodexError('string error');
313
+ expect(wrapped).toBeInstanceOf(AgentError);
314
+ expect(wrapped.message).toBe('string error');
315
+ });
316
+
317
+ it('respects custom code', () => {
318
+ const wrapped = toFlowCodexError(new Error('abort'), ERROR_CODES.AGENT_ABORTED);
319
+ expect(wrapped.code).toBe('AGENT_ABORTED');
320
+ });
321
+ });
322
+
323
+ describe('type guards', () => {
324
+ it('isFlowCodexError', () => {
325
+ expect(isFlowCodexError(new FlowCodexError({ message: 'x', code: ERROR_CODES.UNKNOWN, subsystem: 'general' }))).toBe(true);
326
+ expect(isFlowCodexError(new Error('plain'))).toBe(false);
327
+ });
328
+
329
+ it('isToolError', () => {
330
+ expect(isToolError(new ToolError({ message: 'x', code: ERROR_CODES.TOOL_TIMEOUT, toolName: 't' }))).toBe(true);
331
+ expect(isToolError(new Error('plain'))).toBe(false);
332
+ });
333
+
334
+ it('isConfigError', () => {
335
+ expect(isConfigError(new ConfigError({ message: 'x', code: ERROR_CODES.CONFIG_INVALID }))).toBe(true);
336
+ });
337
+
338
+ it('isPluginError', () => {
339
+ expect(isPluginError(new PluginError({ message: 'x', code: ERROR_CODES.PLUGIN_LOAD_FAILED, pluginName: 'p' }))).toBe(true);
340
+ });
341
+
342
+ it('isSessionError', () => {
343
+ expect(isSessionError(new SessionError({ message: 'x', code: ERROR_CODES.SESSION_NOT_FOUND }))).toBe(true);
344
+ });
345
+
346
+ it('isAgentError', () => {
347
+ expect(isAgentError(new AgentError({ message: 'x', code: ERROR_CODES.AGENT_RUN_FAILED }))).toBe(true);
348
+ });
349
+
350
+ it('isPermissionError', () => {
351
+ expect(isPermissionError(new PermissionError({ message: 'x', tool: 'bash', source: 'deny' }))).toBe(true);
352
+ expect(isPermissionError(new Error('plain'))).toBe(false);
353
+ });
354
+
355
+ it('isFsError', () => {
356
+ expect(isFsError(new FsError({ message: 'x', code: ERROR_CODES.FS_READ_FAILED }))).toBe(true);
357
+ });
358
+
359
+ it('subclasses pass isFlowCodexError', () => {
360
+ expect(isFlowCodexError(new ToolError({ message: 'x', code: ERROR_CODES.TOOL_TIMEOUT, toolName: 't' }))).toBe(true);
361
+ expect(isFlowCodexError(new ConfigError({ message: 'x', code: ERROR_CODES.CONFIG_INVALID }))).toBe(true);
362
+ });
363
+ });
364
+
365
+ describe('PROVIDER_UNSUPPORTED error code', () => {
366
+ it('PROVIDER_UNSUPPORTED error code exists', () => {
367
+ expect(ERROR_CODES.PROVIDER_UNSUPPORTED).toBe('PROVIDER_UNSUPPORTED');
368
+ });
369
+
370
+ it('PROVIDER_NOT_WIRED error code exists', () => {
371
+ expect(ERROR_CODES.PROVIDER_NOT_WIRED).toBe('PROVIDER_NOT_WIRED');
372
+ });
373
+
374
+ it('FlowCodexError with PROVIDER_UNSUPPORTED has provider subsystem', () => {
375
+ const err = new FlowCodexError({
376
+ message: 'unsupported',
377
+ code: ERROR_CODES.PROVIDER_UNSUPPORTED,
378
+ subsystem: 'provider',
379
+ });
380
+ expect(err.subsystem).toBe('provider');
381
+ expect(err.recoverable).toBe(false);
382
+ });
383
+ });
384
+
385
+ describe('v0.2.1 error codes', () => {
386
+ it('SSRF_BLOCKED exists', () => {
387
+ expect(ERROR_CODES.SSRF_BLOCKED).toBe('SSRF_BLOCKED');
388
+ });
389
+ it('WEBFETCH_FAILED exists', () => {
390
+ expect(ERROR_CODES.WEBFETCH_FAILED).toBe('WEBFETCH_FAILED');
391
+ });
392
+ it('WEBSEARCH_FAILED exists', () => {
393
+ expect(ERROR_CODES.WEBSEARCH_FAILED).toBe('WEBSEARCH_FAILED');
394
+ });
395
+ it('DIFF_FILE_NOT_FOUND exists', () => {
396
+ expect(ERROR_CODES.DIFF_FILE_NOT_FOUND).toBe('DIFF_FILE_NOT_FOUND');
397
+ });
398
+ it('PATCH_HUNK_FAILED exists', () => {
399
+ expect(ERROR_CODES.PATCH_HUNK_FAILED).toBe('PATCH_HUNK_FAILED');
400
+ });
401
+ it('DEV_TOOL_NOT_FOUND exists', () => {
402
+ expect(ERROR_CODES.DEV_TOOL_NOT_FOUND).toBe('DEV_TOOL_NOT_FOUND');
403
+ });
404
+ it('REPLAY_MISS exists', () => {
405
+ expect(ERROR_CODES.REPLAY_MISS).toBe('REPLAY_MISS');
406
+ });
407
+ it('SESSION_EXPORT_FAILED exists', () => {
408
+ expect(ERROR_CODES.SESSION_EXPORT_FAILED).toBe('SESSION_EXPORT_FAILED');
409
+ });
410
+ it('SESSION_NOT_FOUND already exists', () => {
411
+ expect(ERROR_CODES.SESSION_NOT_FOUND).toBe('SESSION_NOT_FOUND');
412
+ });
413
+ it('FlowCodexError accepts SSRF_BLOCKED', () => {
414
+ const err = new FlowCodexError({
415
+ message: 'blocked',
416
+ code: ERROR_CODES.SSRF_BLOCKED,
417
+ subsystem: 'general',
418
+ });
419
+ expect(err.code).toBe('SSRF_BLOCKED');
420
+ });
421
+ it('FlowCodexError accepts REPLAY_MISS', () => {
422
+ const err = new FlowCodexError({
423
+ message: 'no recorded response',
424
+ code: ERROR_CODES.REPLAY_MISS,
425
+ subsystem: 'provider',
426
+ });
427
+ expect(err.code).toBe('REPLAY_MISS');
428
+ });
429
+ });
@@ -0,0 +1,60 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { EventBus } from '../src/kernel/events.js';
3
+
4
+ describe('v0.2.1 events', () => {
5
+ it('emits session.resumed', () => {
6
+ const bus = new EventBus();
7
+ let received: { sessionId: string; forkedFrom?: string | undefined } | undefined;
8
+ bus.on('session.resumed', (p) => {
9
+ received = p;
10
+ });
11
+ bus.emit('session.resumed', { sessionId: 'abc', forkedFrom: 'parent' });
12
+ expect(received?.sessionId).toBe('abc');
13
+ expect(received?.forkedFrom).toBe('parent');
14
+ });
15
+
16
+ it('emits session.resumed without forkedFrom', () => {
17
+ const bus = new EventBus();
18
+ let received: { sessionId: string; forkedFrom?: string | undefined } | undefined;
19
+ bus.on('session.resumed', (p) => {
20
+ received = p;
21
+ });
22
+ bus.emit('session.resumed', { sessionId: 'xyz' });
23
+ expect(received?.sessionId).toBe('xyz');
24
+ expect(received?.forkedFrom).toBeUndefined();
25
+ });
26
+
27
+ it('emits session.summary_written', () => {
28
+ const bus = new EventBus();
29
+ let received: { sessionId: string; summary: unknown } | undefined;
30
+ bus.on('session.summary_written', (p) => {
31
+ received = p;
32
+ });
33
+ const summary = { id: 's1', title: 'test' };
34
+ bus.emit('session.summary_written', { sessionId: 's1', summary });
35
+ expect(received?.sessionId).toBe('s1');
36
+ expect(received?.summary).toBe(summary);
37
+ });
38
+
39
+ it('emits tool.tier_filtered', () => {
40
+ const bus = new EventBus();
41
+ let received: { total: number; kept: number } | undefined;
42
+ bus.on('tool.tier_filtered', (p) => {
43
+ received = p;
44
+ });
45
+ bus.emit('tool.tier_filtered', { total: 15, kept: 10 });
46
+ expect(received?.total).toBe(15);
47
+ expect(received?.kept).toBe(10);
48
+ });
49
+
50
+ it('emits ssrf.blocked', () => {
51
+ const bus = new EventBus();
52
+ let received: { url: string; ip: string; reason: string } | undefined;
53
+ bus.on('ssrf.blocked', (p) => {
54
+ received = p;
55
+ });
56
+ bus.emit('ssrf.blocked', { url: 'http://10.0.0.1', ip: '10.0.0.1', reason: 'private' });
57
+ expect(received?.url).toBe('http://10.0.0.1');
58
+ expect(received?.reason).toBe('private');
59
+ });
60
+ });
@@ -0,0 +1,75 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { EventBus } from '../src/kernel/events.js';
3
+ import { ERROR_CODES, FlowCodexError } from '../src/types/errors.js';
4
+
5
+ describe('v0.2.2 error codes', () => {
6
+ it('PROVIDER_UNSUPPORTED_MODALITY exists', () => {
7
+ expect(ERROR_CODES.PROVIDER_UNSUPPORTED_MODALITY).toBe('PROVIDER_UNSUPPORTED_MODALITY');
8
+ });
9
+ it('TOOL_BATCH_FAILED exists', () => {
10
+ expect(ERROR_CODES.TOOL_BATCH_FAILED).toBe('TOOL_BATCH_FAILED');
11
+ });
12
+ it('TOOL_BATCH_TOO_LARGE exists', () => {
13
+ expect(ERROR_CODES.TOOL_BATCH_TOO_LARGE).toBe('TOOL_BATCH_TOO_LARGE');
14
+ });
15
+ it('TOOL_NESTED_BATCH exists', () => {
16
+ expect(ERROR_CODES.TOOL_NESTED_BATCH).toBe('TOOL_NESTED_BATCH');
17
+ });
18
+ it('TOOL_INVALID_ATTACHMENT exists', () => {
19
+ expect(ERROR_CODES.TOOL_INVALID_ATTACHMENT).toBe('TOOL_INVALID_ATTACHMENT');
20
+ });
21
+ it('TOOL_UNSUPPORTED_PROVIDER exists', () => {
22
+ expect(ERROR_CODES.TOOL_UNSUPPORTED_PROVIDER).toBe('TOOL_UNSUPPORTED_PROVIDER');
23
+ });
24
+ it('AGENT_STRUCTURED_OUTPUT_NOT_PRODUCED exists', () => {
25
+ expect(ERROR_CODES.AGENT_STRUCTURED_OUTPUT_NOT_PRODUCED).toBe('AGENT_STRUCTURED_OUTPUT_NOT_PRODUCED');
26
+ });
27
+ it('FlowCodexError accepts PROVIDER_UNSUPPORTED_MODALITY', () => {
28
+ const err = new FlowCodexError({
29
+ message: 'no vision',
30
+ code: ERROR_CODES.PROVIDER_UNSUPPORTED_MODALITY,
31
+ subsystem: 'provider',
32
+ });
33
+ expect(err.code).toBe('PROVIDER_UNSUPPORTED_MODALITY');
34
+ expect(err.subsystem).toBe('provider');
35
+ });
36
+ it('FlowCodexError accepts TOOL_NESTED_BATCH', () => {
37
+ const err = new FlowCodexError({
38
+ message: 'nested',
39
+ code: ERROR_CODES.TOOL_NESTED_BATCH,
40
+ subsystem: 'tool',
41
+ });
42
+ expect(err.code).toBe('TOOL_NESTED_BATCH');
43
+ });
44
+ });
45
+
46
+ describe('v0.2.2 events', () => {
47
+ it('emits tool.batch_started', () => {
48
+ const bus = new EventBus();
49
+ let captured: unknown = null;
50
+ bus.on('tool.batch_started', (p) => { captured = p; });
51
+ bus.emit('tool.batch_started', { count: 3, names: ['read', 'read', 'glob'] });
52
+ expect(captured).toEqual({ count: 3, names: ['read', 'read', 'glob'] });
53
+ });
54
+ it('emits tool.batch_completed', () => {
55
+ const bus = new EventBus();
56
+ let captured: unknown = null;
57
+ bus.on('tool.batch_completed', (p) => { captured = p; });
58
+ bus.emit('tool.batch_completed', { count: 3, succeeded: 2, failed: 1, durationMs: 42 });
59
+ expect(captured).toEqual({ count: 3, succeeded: 2, failed: 1, durationMs: 42 });
60
+ });
61
+ it('emits provider.structured_output', () => {
62
+ const bus = new EventBus();
63
+ let captured: unknown = null;
64
+ bus.on('provider.structured_output', (p) => { captured = p; });
65
+ bus.emit('provider.structured_output', { name: 'structured_output', valid: true });
66
+ expect(captured).toEqual({ name: 'structured_output', valid: true });
67
+ });
68
+ it('emits provider.modality_rejected', () => {
69
+ const bus = new EventBus();
70
+ let captured: unknown = null;
71
+ bus.on('provider.modality_rejected', (p) => { captured = p; });
72
+ bus.emit('provider.modality_rejected', { provider: 'openai', model: 'gpt-4', modality: 'pdf' });
73
+ expect(captured).toEqual({ provider: 'openai', model: 'gpt-4', modality: 'pdf' });
74
+ });
75
+ });