@unrdf/kgn 5.0.1

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 (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +210 -0
  3. package/package.json +90 -0
  4. package/src/MIGRATION_COMPLETE.md +186 -0
  5. package/src/PORT-MAP.md +302 -0
  6. package/src/base/filter-templates.js +479 -0
  7. package/src/base/index.js +92 -0
  8. package/src/base/injection-targets.js +583 -0
  9. package/src/base/macro-templates.js +298 -0
  10. package/src/base/macro-templates.js.bak +461 -0
  11. package/src/base/shacl-templates.js +617 -0
  12. package/src/base/template-base.js +388 -0
  13. package/src/core/attestor.js +381 -0
  14. package/src/core/filters.js +518 -0
  15. package/src/core/index.js +21 -0
  16. package/src/core/kgen-engine.js +372 -0
  17. package/src/core/parser.js +447 -0
  18. package/src/core/post-processor.js +313 -0
  19. package/src/core/renderer.js +469 -0
  20. package/src/doc-generator/cli.mjs +122 -0
  21. package/src/doc-generator/index.mjs +28 -0
  22. package/src/doc-generator/mdx-generator.mjs +71 -0
  23. package/src/doc-generator/nav-generator.mjs +136 -0
  24. package/src/doc-generator/parser.mjs +291 -0
  25. package/src/doc-generator/rdf-builder.mjs +306 -0
  26. package/src/doc-generator/scanner.mjs +189 -0
  27. package/src/engine/index.js +42 -0
  28. package/src/engine/pipeline.js +448 -0
  29. package/src/engine/renderer.js +604 -0
  30. package/src/engine/template-engine.js +566 -0
  31. package/src/filters/array.js +436 -0
  32. package/src/filters/data.js +479 -0
  33. package/src/filters/index.js +270 -0
  34. package/src/filters/rdf.js +264 -0
  35. package/src/filters/text.js +369 -0
  36. package/src/index.js +109 -0
  37. package/src/inheritance/index.js +40 -0
  38. package/src/injection/api.js +260 -0
  39. package/src/injection/atomic-writer.js +327 -0
  40. package/src/injection/constants.js +136 -0
  41. package/src/injection/idempotency-manager.js +295 -0
  42. package/src/injection/index.js +28 -0
  43. package/src/injection/injection-engine.js +378 -0
  44. package/src/injection/integration.js +339 -0
  45. package/src/injection/modes/index.js +341 -0
  46. package/src/injection/rollback-manager.js +373 -0
  47. package/src/injection/target-resolver.js +323 -0
  48. package/src/injection/tests/atomic-writer.test.js +382 -0
  49. package/src/injection/tests/injection-engine.test.js +611 -0
  50. package/src/injection/tests/integration.test.js +392 -0
  51. package/src/injection/tests/run-tests.js +283 -0
  52. package/src/injection/validation-engine.js +547 -0
  53. package/src/linter/determinism-linter.js +473 -0
  54. package/src/linter/determinism.js +410 -0
  55. package/src/linter/index.js +6 -0
  56. package/src/linter/test-doubles.js +475 -0
  57. package/src/parser/frontmatter.js +228 -0
  58. package/src/parser/variables.js +344 -0
  59. package/src/renderer/deterministic.js +245 -0
  60. package/src/renderer/index.js +6 -0
  61. package/src/templates/latex/academic-paper.njk +186 -0
  62. package/src/templates/latex/index.js +104 -0
  63. package/src/templates/nextjs/app-page.njk +66 -0
  64. package/src/templates/nextjs/index.js +80 -0
  65. package/src/templates/office/docx/document.njk +368 -0
  66. package/src/templates/office/index.js +79 -0
  67. package/src/templates/office/word-report.njk +129 -0
  68. package/src/utils/template-utils.js +426 -0
@@ -0,0 +1,611 @@
1
+ /**
2
+ * KGEN Injection Engine Tests
3
+ *
4
+ * Comprehensive tests for the injection engine covering all
5
+ * atomic operations, idempotency, and error handling.
6
+ */
7
+
8
+ import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest';
9
+ import { promises as fs } from 'fs';
10
+ import { join, dirname } from 'path';
11
+ import { tmpdir } from 'os';
12
+
13
+ import { InjectionEngine } from '../injection-engine.js';
14
+ import { INJECTION_MODES, ERROR_CODES } from '../constants.js';
15
+
16
+ describe('InjectionEngine', () => {
17
+ let tempDir;
18
+ let engine;
19
+ let testFiles;
20
+
21
+ beforeEach(async () => {
22
+ // Create temporary directory for tests
23
+ tempDir = await fs.mkdtemp(join(tmpdir(), 'kgen-injection-test-'));
24
+
25
+ // Initialize injection engine
26
+ engine = new InjectionEngine({
27
+ projectRoot: tempDir,
28
+ backupEnabled: true,
29
+ atomicWrites: true
30
+ });
31
+
32
+ testFiles = {
33
+ routes: join(tempDir, 'src', 'routes.ts'),
34
+ config: join(tempDir, 'src', 'config.ts'),
35
+ index: join(tempDir, 'src', 'index.ts')
36
+ };
37
+
38
+ // Create test directory structure
39
+ await fs.mkdir(join(tempDir, 'src'), { recursive: true });
40
+ });
41
+
42
+ afterEach(async () => {
43
+ // Clean up temporary directory
44
+ await fs.rm(tempDir, { recursive: true, force: true });
45
+ });
46
+
47
+ describe('Basic Injection Operations', () => {
48
+ test('should append content to file', async () => {
49
+ // Setup
50
+ const initialContent = 'export { Router } from "./router";\n';
51
+ await fs.writeFile(testFiles.index, initialContent);
52
+
53
+ const templateConfig = {
54
+ to: 'src/index.ts',
55
+ inject: true,
56
+ mode: INJECTION_MODES.APPEND
57
+ };
58
+
59
+ const injectionContent = 'export { UserService } from "./services/user";';
60
+ const variables = {};
61
+
62
+ // Execute
63
+ const result = await engine.inject(templateConfig, injectionContent, variables);
64
+
65
+ // Verify
66
+ expect(result.success).toBe(true);
67
+ expect(result.operationId).toMatch(/^injection-[a-f0-9]{16}$/);
68
+
69
+ const finalContent = await fs.readFile(testFiles.index, 'utf8');
70
+ expect(finalContent).toContain(initialContent.trim());
71
+ expect(finalContent).toContain(injectionContent);
72
+ });
73
+
74
+ test('should prepend content to file', async () => {
75
+ // Setup
76
+ const initialContent = 'import express from "express";\n\nconst app = express();';
77
+ await fs.writeFile(testFiles.routes, initialContent);
78
+
79
+ const templateConfig = {
80
+ to: 'src/routes.ts',
81
+ inject: true,
82
+ mode: INJECTION_MODES.PREPEND
83
+ };
84
+
85
+ const injectionContent = 'import { cors } from "cors";';
86
+ const variables = {};
87
+
88
+ // Execute
89
+ const result = await engine.inject(templateConfig, injectionContent, variables);
90
+
91
+ // Verify
92
+ expect(result.success).toBe(true);
93
+
94
+ const finalContent = await fs.readFile(testFiles.routes, 'utf8');
95
+ expect(finalContent).toMatch(/^import { cors } from "cors";\nimport express from "express";/);
96
+ });
97
+
98
+ test('should inject before specific pattern', async () => {
99
+ // Setup
100
+ const initialContent = `import { Router } from 'express';
101
+
102
+ const router = Router();
103
+
104
+ // Existing routes
105
+ router.get('/health', healthCheck);
106
+
107
+ export default router;`;
108
+ await fs.writeFile(testFiles.routes, initialContent);
109
+
110
+ const templateConfig = {
111
+ to: 'src/routes.ts',
112
+ inject: true,
113
+ mode: INJECTION_MODES.BEFORE,
114
+ target: 'export default router;'
115
+ };
116
+
117
+ const injectionContent = "router.get('/users', getUsers);";
118
+ const variables = {};
119
+
120
+ // Execute
121
+ const result = await engine.inject(templateConfig, injectionContent, variables);
122
+
123
+ // Verify
124
+ expect(result.success).toBe(true);
125
+
126
+ const finalContent = await fs.readFile(testFiles.routes, 'utf8');
127
+ expect(finalContent).toContain("router.get('/users', getUsers);");
128
+ expect(finalContent.indexOf("router.get('/users', getUsers);"))
129
+ .toBeLessThan(finalContent.indexOf('export default router;'));
130
+ });
131
+
132
+ test('should inject after specific pattern', async () => {
133
+ // Setup
134
+ const initialContent = `const router = Router();
135
+
136
+ // Existing routes
137
+ router.get('/health', healthCheck);
138
+
139
+ export default router;`;
140
+ await fs.writeFile(testFiles.routes, initialContent);
141
+
142
+ const templateConfig = {
143
+ to: 'src/routes.ts',
144
+ inject: true,
145
+ mode: INJECTION_MODES.AFTER,
146
+ target: '// Existing routes'
147
+ };
148
+
149
+ const injectionContent = "router.post('/users', createUser);";
150
+ const variables = {};
151
+
152
+ // Execute
153
+ const result = await engine.inject(templateConfig, injectionContent, variables);
154
+
155
+ // Verify
156
+ expect(result.success).toBe(true);
157
+
158
+ const finalContent = await fs.readFile(testFiles.routes, 'utf8');
159
+ expect(finalContent).toContain("router.post('/users', createUser);");
160
+ expect(finalContent.indexOf('// Existing routes'))
161
+ .toBeLessThan(finalContent.indexOf("router.post('/users', createUser);"));
162
+ });
163
+
164
+ test('should replace specific content', async () => {
165
+ // Setup
166
+ const initialContent = `export const config = {
167
+ port: 3000,
168
+ host: 'localhost'
169
+ };`;
170
+ await fs.writeFile(testFiles.config, initialContent);
171
+
172
+ const templateConfig = {
173
+ to: 'src/config.ts',
174
+ inject: true,
175
+ mode: INJECTION_MODES.REPLACE,
176
+ target: 'port: 3000',
177
+ exact: true
178
+ };
179
+
180
+ const injectionContent = 'port: 8080';
181
+ const variables = {};
182
+
183
+ // Execute
184
+ const result = await engine.inject(templateConfig, injectionContent, variables);
185
+
186
+ // Verify
187
+ expect(result.success).toBe(true);
188
+
189
+ const finalContent = await fs.readFile(testFiles.config, 'utf8');
190
+ expect(finalContent).toContain('port: 8080');
191
+ expect(finalContent).not.toContain('port: 3000');
192
+ });
193
+
194
+ test('should create new file when mode is create', async () => {
195
+ const templateConfig = {
196
+ to: 'src/models/User.ts',
197
+ inject: true,
198
+ mode: INJECTION_MODES.CREATE,
199
+ createIfMissing: true,
200
+ createDirectories: true
201
+ };
202
+
203
+ const injectionContent = `export interface User {
204
+ id: string;
205
+ name: string;
206
+ }`;
207
+ const variables = {};
208
+
209
+ // Execute
210
+ const result = await engine.inject(templateConfig, injectionContent, variables);
211
+
212
+ // Verify
213
+ expect(result.success).toBe(true);
214
+
215
+ const userModelPath = join(tempDir, 'src', 'models', 'User.ts');
216
+ const exists = await fs.access(userModelPath).then(() => true, () => false);
217
+ expect(exists).toBe(true);
218
+
219
+ const content = await fs.readFile(userModelPath, 'utf8');
220
+ expect(content).toContain('export interface User');
221
+ });
222
+ });
223
+
224
+ describe('Idempotency', () => {
225
+ test('should skip injection when content already exists', async () => {
226
+ // Setup
227
+ const initialContent = `export const config = {
228
+ port: 3000,
229
+ database: 'mongodb://localhost:27017'
230
+ };`;
231
+ await fs.writeFile(testFiles.config, initialContent);
232
+
233
+ const templateConfig = {
234
+ to: 'src/config.ts',
235
+ inject: true,
236
+ mode: INJECTION_MODES.BEFORE,
237
+ target: '};',
238
+ skipIf: 'database:'
239
+ };
240
+
241
+ const injectionContent = " database: 'mongodb://localhost:27017'";
242
+ const variables = {};
243
+
244
+ // Execute
245
+ const result = await engine.inject(templateConfig, injectionContent, variables);
246
+
247
+ // Verify
248
+ expect(result.success).toBe(true);
249
+ expect(result.skipped).toBe(true);
250
+
251
+ const finalContent = await fs.readFile(testFiles.config, 'utf8');
252
+ // Should not have duplicate database entries
253
+ const databaseMatches = (finalContent.match(/database:/g) || []).length;
254
+ expect(databaseMatches).toBe(1);
255
+ });
256
+
257
+ test('should skip injection with regex pattern', async () => {
258
+ // Setup
259
+ const initialContent = `import express from 'express';
260
+ import cors from 'cors';`;
261
+ await fs.writeFile(testFiles.routes, initialContent);
262
+
263
+ const templateConfig = {
264
+ to: 'src/routes.ts',
265
+ inject: true,
266
+ mode: INJECTION_MODES.PREPEND,
267
+ skipIf: '/import.*cors/'
268
+ };
269
+
270
+ const injectionContent = 'import cors from "cors";';
271
+ const variables = {};
272
+
273
+ // Execute
274
+ const result = await engine.inject(templateConfig, injectionContent, variables);
275
+
276
+ // Verify
277
+ expect(result.success).toBe(true);
278
+ expect(result.skipped).toBe(true);
279
+ });
280
+
281
+ test('should handle multiple skipIf conditions with OR logic', async () => {
282
+ // Setup
283
+ const initialContent = 'router.get("/users", getUsers);';
284
+ await fs.writeFile(testFiles.routes, initialContent);
285
+
286
+ const templateConfig = {
287
+ to: 'src/routes.ts',
288
+ inject: true,
289
+ mode: INJECTION_MODES.APPEND,
290
+ skipIf: ['/users', 'getUsers'],
291
+ skipIfLogic: 'OR'
292
+ };
293
+
294
+ const injectionContent = 'router.post("/users", createUser);';
295
+ const variables = {};
296
+
297
+ // Execute
298
+ const result = await engine.inject(templateConfig, injectionContent, variables);
299
+
300
+ // Verify - should skip because either condition matches
301
+ expect(result.success).toBe(true);
302
+ expect(result.skipped).toBe(true);
303
+ });
304
+
305
+ test('should handle force flag to override skipIf', async () => {
306
+ // Setup
307
+ const initialContent = 'router.get("/users", getUsers);';
308
+ await fs.writeFile(testFiles.routes, initialContent);
309
+
310
+ const templateConfig = {
311
+ to: 'src/routes.ts',
312
+ inject: true,
313
+ mode: INJECTION_MODES.APPEND,
314
+ skipIf: '/users',
315
+ force: true
316
+ };
317
+
318
+ const injectionContent = 'router.post("/users", createUser);';
319
+ const variables = {};
320
+
321
+ // Execute
322
+ const result = await engine.inject(templateConfig, injectionContent, variables);
323
+
324
+ // Verify - should proceed despite skipIf because of force
325
+ expect(result.success).toBe(true);
326
+ expect(result.skipped).toBe(false);
327
+
328
+ const finalContent = await fs.readFile(testFiles.routes, 'utf8');
329
+ expect(finalContent).toContain('router.post("/users", createUser);');
330
+ });
331
+ });
332
+
333
+ describe('Atomic Operations', () => {
334
+ test('should rollback changes on validation failure', async () => {
335
+ // Setup
336
+ const initialContent = 'const validCode = true;';
337
+ await fs.writeFile(testFiles.config, initialContent);
338
+
339
+ const templateConfig = {
340
+ to: 'src/config.ts',
341
+ inject: true,
342
+ mode: INJECTION_MODES.APPEND,
343
+ validateSyntax: true
344
+ };
345
+
346
+ // Invalid JavaScript that should fail validation
347
+ const injectionContent = 'const invalid = {';
348
+ const variables = {};
349
+
350
+ // Execute & Verify
351
+ await expect(engine.inject(templateConfig, injectionContent, variables))
352
+ .rejects.toThrow();
353
+
354
+ // Original content should be preserved
355
+ const finalContent = await fs.readFile(testFiles.config, 'utf8');
356
+ expect(finalContent).toBe(initialContent);
357
+ });
358
+
359
+ test('should handle concurrent operations safely', async () => {
360
+ // Setup
361
+ const initialContent = 'let counter = 0;\n';
362
+ await fs.writeFile(testFiles.config, initialContent);
363
+
364
+ const templateConfig = {
365
+ to: 'src/config.ts',
366
+ inject: true,
367
+ mode: INJECTION_MODES.APPEND
368
+ };
369
+
370
+ // Execute multiple concurrent injections
371
+ const promises = Array.from({ length: 5 }, (_, i) => {
372
+ return engine.inject(templateConfig, `// Injection ${i}\n`, {});
373
+ });
374
+
375
+ const results = await Promise.all(promises);
376
+
377
+ // Verify all operations succeeded
378
+ results.forEach(result => {
379
+ expect(result.success).toBe(true);
380
+ });
381
+
382
+ // Verify final content has all injections
383
+ const finalContent = await fs.readFile(testFiles.config, 'utf8');
384
+ for (let i = 0; i < 5; i++) {
385
+ expect(finalContent).toContain(`// Injection ${i}`);
386
+ }
387
+ });
388
+
389
+ test('should create backups when enabled', async () => {
390
+ // Setup
391
+ const initialContent = 'const original = true;';
392
+ await fs.writeFile(testFiles.config, initialContent);
393
+
394
+ const templateConfig = {
395
+ to: 'src/config.ts',
396
+ inject: true,
397
+ mode: INJECTION_MODES.APPEND
398
+ };
399
+
400
+ const injectionContent = 'const added = true;';
401
+ const variables = {};
402
+
403
+ // Execute
404
+ const result = await engine.inject(templateConfig, injectionContent, variables);
405
+
406
+ // Verify
407
+ expect(result.success).toBe(true);
408
+ expect(result.results[0].backup).toBeDefined();
409
+
410
+ // Check backup file exists
411
+ const backupExists = await fs.access(result.results[0].backup)
412
+ .then(() => true, () => false);
413
+ expect(backupExists).toBe(true);
414
+
415
+ // Check backup contains original content
416
+ const backupContent = await fs.readFile(result.results[0].backup, 'utf8');
417
+ expect(backupContent).toBe(initialContent);
418
+ });
419
+ });
420
+
421
+ describe('Variable Interpolation', () => {
422
+ test('should interpolate variables in injection content', async () => {
423
+ // Setup
424
+ await fs.writeFile(testFiles.routes, 'const router = Router();\n');
425
+
426
+ const templateConfig = {
427
+ to: 'src/routes.ts',
428
+ inject: true,
429
+ mode: INJECTION_MODES.APPEND
430
+ };
431
+
432
+ const injectionContent = 'router.{{method}}("{{path}}", {{handler}});';
433
+ const variables = {
434
+ method: 'get',
435
+ path: '/users',
436
+ handler: 'getUsers'
437
+ };
438
+
439
+ // Execute
440
+ const result = await engine.inject(templateConfig, injectionContent, variables);
441
+
442
+ // Verify
443
+ expect(result.success).toBe(true);
444
+
445
+ const finalContent = await fs.readFile(testFiles.routes, 'utf8');
446
+ expect(finalContent).toContain('router.get("/users", getUsers);');
447
+ });
448
+
449
+ test('should interpolate variables in skipIf conditions', async () => {
450
+ // Setup
451
+ const initialContent = 'router.get("/users", getUsers);';
452
+ await fs.writeFile(testFiles.routes, initialContent);
453
+
454
+ const templateConfig = {
455
+ to: 'src/routes.ts',
456
+ inject: true,
457
+ mode: INJECTION_MODES.APPEND,
458
+ skipIf: '{{path}}'
459
+ };
460
+
461
+ const injectionContent = 'router.post("{{path}}", createUser);';
462
+ const variables = { path: '/users' };
463
+
464
+ // Execute
465
+ const result = await engine.inject(templateConfig, injectionContent, variables);
466
+
467
+ // Verify - should skip because /users already exists
468
+ expect(result.success).toBe(true);
469
+ expect(result.skipped).toBe(true);
470
+ });
471
+ });
472
+
473
+ describe('Error Handling', () => {
474
+ test('should handle target file not found', async () => {
475
+ const templateConfig = {
476
+ to: 'src/nonexistent.ts',
477
+ inject: true,
478
+ mode: INJECTION_MODES.BEFORE,
479
+ target: 'some pattern'
480
+ };
481
+
482
+ const injectionContent = 'some content';
483
+ const variables = {};
484
+
485
+ // Execute & Verify
486
+ await expect(engine.inject(templateConfig, injectionContent, variables))
487
+ .rejects.toThrow(/ENOENT|Target file does not exist/);
488
+ });
489
+
490
+ test('should handle injection pattern not found', async () => {
491
+ // Setup
492
+ const initialContent = 'const existing = true;';
493
+ await fs.writeFile(testFiles.config, initialContent);
494
+
495
+ const templateConfig = {
496
+ to: 'src/config.ts',
497
+ inject: true,
498
+ mode: INJECTION_MODES.BEFORE,
499
+ target: 'NONEXISTENT_PATTERN'
500
+ };
501
+
502
+ const injectionContent = 'const added = true;';
503
+ const variables = {};
504
+
505
+ // Execute & Verify
506
+ await expect(engine.inject(templateConfig, injectionContent, variables))
507
+ .rejects.toThrow('Target pattern not found');
508
+ });
509
+
510
+ test('should handle read-only files', async () => {
511
+ // Setup
512
+ const initialContent = 'const readonly = true;';
513
+ await fs.writeFile(testFiles.config, initialContent);
514
+ await fs.chmod(testFiles.config, 0o444); // Read-only
515
+
516
+ const templateConfig = {
517
+ to: 'src/config.ts',
518
+ inject: true,
519
+ mode: INJECTION_MODES.APPEND
520
+ };
521
+
522
+ const injectionContent = 'const added = true;';
523
+ const variables = {};
524
+
525
+ // Execute & Verify
526
+ await expect(engine.inject(templateConfig, injectionContent, variables))
527
+ .rejects.toThrow();
528
+
529
+ // Restore permissions for cleanup
530
+ await fs.chmod(testFiles.config, 0o644);
531
+ });
532
+ });
533
+
534
+ describe('Dry Run', () => {
535
+ test('should perform dry run without modifying files', async () => {
536
+ // Setup
537
+ const initialContent = 'const original = true;';
538
+ await fs.writeFile(testFiles.config, initialContent);
539
+
540
+ const templateConfig = {
541
+ to: 'src/config.ts',
542
+ inject: true,
543
+ mode: INJECTION_MODES.APPEND
544
+ };
545
+
546
+ const injectionContent = 'const added = true;';
547
+ const variables = {};
548
+
549
+ // Execute dry run
550
+ const dryRunResult = await engine.dryRun(templateConfig, injectionContent, variables);
551
+
552
+ // Verify dry run results
553
+ expect(dryRunResult.targets).toHaveLength(1);
554
+ expect(dryRunResult.targets[0].path).toBe(testFiles.config);
555
+ expect(dryRunResult.targets[0].valid).toBe(true);
556
+
557
+ // Verify file was not modified
558
+ const finalContent = await fs.readFile(testFiles.config, 'utf8');
559
+ expect(finalContent).toBe(initialContent);
560
+ });
561
+ });
562
+
563
+ describe('Operation History', () => {
564
+ test('should maintain operation history', async () => {
565
+ // Setup
566
+ await fs.writeFile(testFiles.config, 'const original = true;\n');
567
+
568
+ const templateConfig = {
569
+ to: 'src/config.ts',
570
+ inject: true,
571
+ mode: INJECTION_MODES.APPEND
572
+ };
573
+
574
+ const injectionContent = 'const added = true;';
575
+ const variables = {};
576
+
577
+ // Execute
578
+ await engine.inject(templateConfig, injectionContent, variables);
579
+
580
+ // Verify history
581
+ const history = engine.getOperationHistory();
582
+ expect(history).toHaveLength(1);
583
+ expect(history[0].phase).toBe('committed');
584
+ expect(history[0].results).toBeDefined();
585
+ expect(history[0].metadata.timestamp).toBeDefined();
586
+ });
587
+
588
+ test('should record failed operations in history', async () => {
589
+ const templateConfig = {
590
+ to: 'src/nonexistent.ts',
591
+ inject: true,
592
+ mode: INJECTION_MODES.APPEND
593
+ };
594
+
595
+ const injectionContent = 'const added = true;';
596
+ const variables = {};
597
+
598
+ // Execute & expect failure
599
+ try {
600
+ await engine.inject(templateConfig, injectionContent, variables);
601
+ } catch (error) {
602
+ // Expected failure
603
+ }
604
+
605
+ // Verify failed operation is in history
606
+ const history = engine.getOperationHistory();
607
+ expect(history).toHaveLength(1);
608
+ expect(history[0].error).toBeDefined();
609
+ });
610
+ });
611
+ });