@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,382 @@
1
+ /**
2
+ * KGEN Atomic Writer Tests
3
+ *
4
+ * Tests for atomic file operations, backup creation,
5
+ * and transactional multi-file operations.
6
+ */
7
+
8
+ import { describe, test, expect, beforeEach, afterEach } from 'vitest';
9
+ import { promises as fs } from 'fs';
10
+ import { join } from 'path';
11
+ import { tmpdir } from 'os';
12
+
13
+ import { AtomicWriter } from '../atomic-writer.js';
14
+
15
+ describe('AtomicWriter', () => {
16
+ let tempDir;
17
+ let writer;
18
+ let testFile;
19
+
20
+ beforeEach(async () => {
21
+ tempDir = await fs.mkdtemp(join(tmpdir(), 'atomic-writer-test-'));
22
+ writer = new AtomicWriter({
23
+ backupEnabled: true,
24
+ preservePermissions: true
25
+ });
26
+ testFile = join(tempDir, 'test.txt');
27
+ });
28
+
29
+ afterEach(async () => {
30
+ await fs.rm(tempDir, { recursive: true, force: true });
31
+ });
32
+
33
+ describe('Atomic Write Operations', () => {
34
+ test('should write file atomically', async () => {
35
+ const content = 'Hello, atomic world!';
36
+
37
+ const result = await writer.writeAtomic(testFile, content);
38
+
39
+ expect(result.success).toBe(true);
40
+ expect(result.filePath).toBe(testFile);
41
+ expect(result.checksum).toMatch(/^[a-f0-9]{64}$/);
42
+
43
+ const actualContent = await fs.readFile(testFile, 'utf8');
44
+ expect(actualContent).toBe(content);
45
+ });
46
+
47
+ test('should create backup when file exists', async () => {
48
+ // Setup existing file
49
+ const originalContent = 'Original content';
50
+ await fs.writeFile(testFile, originalContent);
51
+
52
+ const newContent = 'New content';
53
+ const operationId = 'test-operation';
54
+
55
+ const result = await writer.writeAtomic(testFile, newContent, {
56
+ backup: true,
57
+ operationId
58
+ });
59
+
60
+ expect(result.success).toBe(true);
61
+ expect(result.backupPath).toBeDefined();
62
+
63
+ // Verify backup exists and contains original content
64
+ const backupExists = await fs.access(result.backupPath)
65
+ .then(() => true, () => false);
66
+ expect(backupExists).toBe(true);
67
+
68
+ const backupContent = await fs.readFile(result.backupPath, 'utf8');
69
+ expect(backupContent).toBe(originalContent);
70
+
71
+ // Verify main file has new content
72
+ const mainContent = await fs.readFile(testFile, 'utf8');
73
+ expect(mainContent).toBe(newContent);
74
+ });
75
+
76
+ test('should preserve file permissions', async () => {
77
+ // Setup file with specific permissions
78
+ await fs.writeFile(testFile, 'test content');
79
+ await fs.chmod(testFile, 0o755);
80
+
81
+ const newContent = 'new content';
82
+
83
+ await writer.writeAtomic(testFile, newContent, {
84
+ preserveMetadata: true
85
+ });
86
+
87
+ // Check permissions were preserved
88
+ const stats = await fs.stat(testFile);
89
+ expect(stats.mode & 0o777).toBe(0o755);
90
+ });
91
+
92
+ test('should handle concurrent writes with file locking', async () => {
93
+ const promises = [];
94
+
95
+ // Attempt multiple concurrent writes
96
+ for (let i = 0; i < 5; i++) {
97
+ const promise = writer.writeAtomic(testFile, `Content ${i}`);
98
+ promises.push(promise);
99
+ }
100
+
101
+ const results = await Promise.all(promises);
102
+
103
+ // All writes should succeed
104
+ results.forEach(result => {
105
+ expect(result.success).toBe(true);
106
+ });
107
+
108
+ // File should contain one of the contents
109
+ const finalContent = await fs.readFile(testFile, 'utf8');
110
+ expect(finalContent).toMatch(/^Content \d$/);
111
+ });
112
+
113
+ test('should clean up temp file on failure', async () => {
114
+ // Mock a write failure
115
+ const originalWriteFile = fs.writeFile;
116
+ fs.writeFile = async (path, content) => {
117
+ if (path.includes('.kgen-temp-')) {
118
+ throw new Error('Simulated write failure');
119
+ }
120
+ return originalWriteFile(path, content);
121
+ };
122
+
123
+ try {
124
+ await writer.writeAtomic(testFile, 'test content');
125
+ expect(true).toBe(false); // Should not reach here
126
+ } catch (error) {
127
+ expect(error.message).toBe('Simulated write failure');
128
+ }
129
+
130
+ // Check no temp files were left behind
131
+ const files = await fs.readdir(tempDir);
132
+ const tempFiles = files.filter(f => f.includes('.kgen-temp-'));
133
+ expect(tempFiles).toHaveLength(0);
134
+
135
+ // Restore original function
136
+ fs.writeFile = originalWriteFile;
137
+ });
138
+
139
+ test('should restore from backup on write failure', async () => {
140
+ // Setup existing file
141
+ const originalContent = 'Original content';
142
+ await fs.writeFile(testFile, originalContent);
143
+
144
+ // Mock a failure during the atomic rename
145
+ const originalRename = fs.rename;
146
+ fs.rename = async () => {
147
+ throw new Error('Simulated rename failure');
148
+ };
149
+
150
+ const result = await writer.writeAtomic(testFile, 'New content', {
151
+ backup: true
152
+ }).catch(error => error);
153
+
154
+ expect(result).toBeInstanceOf(Error);
155
+
156
+ // File should be restored to original content
157
+ const restoredContent = await fs.readFile(testFile, 'utf8');
158
+ expect(restoredContent).toBe(originalContent);
159
+
160
+ // Restore original function
161
+ fs.rename = originalRename;
162
+ });
163
+ });
164
+
165
+ describe('Transaction Operations', () => {
166
+ test('should handle multi-file atomic transaction', async () => {
167
+ const files = [
168
+ join(tempDir, 'file1.txt'),
169
+ join(tempDir, 'file2.txt'),
170
+ join(tempDir, 'file3.txt')
171
+ ];
172
+
173
+ const transaction = await writer.beginTransaction('test-transaction');
174
+
175
+ // Prepare writes for all files
176
+ await transaction.prepareWrite(files[0], 'Content 1');
177
+ await transaction.prepareWrite(files[1], 'Content 2');
178
+ await transaction.prepareWrite(files[2], 'Content 3');
179
+
180
+ // Commit transaction
181
+ const result = await transaction.commit();
182
+
183
+ expect(result.success).toBe(true);
184
+ expect(result.filesWritten).toBe(3);
185
+
186
+ // Verify all files were written
187
+ for (let i = 0; i < files.length; i++) {
188
+ const content = await fs.readFile(files[i], 'utf8');
189
+ expect(content).toBe(`Content ${i + 1}`);
190
+ }
191
+ });
192
+
193
+ test('should rollback transaction on failure', async () => {
194
+ const files = [
195
+ join(tempDir, 'file1.txt'),
196
+ join(tempDir, 'file2.txt')
197
+ ];
198
+
199
+ // Create existing files with content
200
+ await fs.writeFile(files[0], 'Original 1');
201
+ await fs.writeFile(files[1], 'Original 2');
202
+
203
+ const transaction = await writer.beginTransaction('test-transaction');
204
+
205
+ // Prepare first write
206
+ await transaction.prepareWrite(files[0], 'New Content 1');
207
+
208
+ // Mock failure on second write
209
+ const originalWriteFile = fs.writeFile;
210
+ let writeCount = 0;
211
+ fs.writeFile = async (path, content, options) => {
212
+ writeCount++;
213
+ if (writeCount === 2 && path.includes('file2')) {
214
+ throw new Error('Simulated failure');
215
+ }
216
+ return originalWriteFile(path, content, options);
217
+ };
218
+
219
+ try {
220
+ await transaction.prepareWrite(files[1], 'New Content 2');
221
+ await transaction.commit();
222
+ expect(true).toBe(false); // Should not reach here
223
+ } catch (error) {
224
+ // Transaction should rollback automatically
225
+ }
226
+
227
+ // Verify original content is preserved
228
+ const content1 = await fs.readFile(files[0], 'utf8');
229
+ const content2 = await fs.readFile(files[1], 'utf8');
230
+ expect(content1).toBe('Original 1');
231
+ expect(content2).toBe('Original 2');
232
+
233
+ // Restore original function
234
+ fs.writeFile = originalWriteFile;
235
+ });
236
+
237
+ test('should handle transaction rollback explicitly', async () => {
238
+ const files = [
239
+ join(tempDir, 'file1.txt'),
240
+ join(tempDir, 'file2.txt')
241
+ ];
242
+
243
+ // Create existing files
244
+ await fs.writeFile(files[0], 'Original 1');
245
+ await fs.writeFile(files[1], 'Original 2');
246
+
247
+ const transaction = await writer.beginTransaction('test-transaction');
248
+
249
+ // Prepare writes
250
+ await transaction.prepareWrite(files[0], 'New Content 1');
251
+ await transaction.prepareWrite(files[1], 'New Content 2');
252
+
253
+ // Explicitly rollback
254
+ await transaction.rollback();
255
+
256
+ // Verify files are restored
257
+ const content1 = await fs.readFile(files[0], 'utf8');
258
+ const content2 = await fs.readFile(files[1], 'utf8');
259
+ expect(content1).toBe('Original 1');
260
+ expect(content2).toBe('Original 2');
261
+ });
262
+
263
+ test('should prevent double commit', async () => {
264
+ const transaction = await writer.beginTransaction('test-transaction');
265
+
266
+ await transaction.prepareWrite(testFile, 'Test content');
267
+ await transaction.commit();
268
+
269
+ // Second commit should fail
270
+ await expect(transaction.commit())
271
+ .rejects.toThrow('Transaction already committed');
272
+ });
273
+
274
+ test('should validate temp files before commit', async () => {
275
+ const transaction = await writer.beginTransaction('test-transaction');
276
+
277
+ await transaction.prepareWrite(testFile, 'Test content');
278
+
279
+ // Manually remove temp file to simulate corruption
280
+ const tempFile = transaction.preparedWrites[0].tempPath;
281
+ await fs.unlink(tempFile);
282
+
283
+ // Commit should fail
284
+ await expect(transaction.commit())
285
+ .rejects.toThrow();
286
+ });
287
+ });
288
+
289
+ describe('File Locking', () => {
290
+ test('should acquire and release locks properly', async () => {
291
+ const lockFile = `${testFile}.kgen-lock`;
292
+
293
+ // Start first operation
294
+ const promise1 = writer.writeAtomic(testFile, 'Content 1');
295
+
296
+ // Try to start second operation immediately
297
+ const promise2 = writer.writeAtomic(testFile, 'Content 2');
298
+
299
+ // Both should complete successfully (one waits for the other)
300
+ const results = await Promise.all([promise1, promise2]);
301
+
302
+ expect(results[0].success).toBe(true);
303
+ expect(results[1].success).toBe(true);
304
+
305
+ // Lock file should be cleaned up
306
+ const lockExists = await fs.access(lockFile)
307
+ .then(() => true, () => false);
308
+ expect(lockExists).toBe(false);
309
+ });
310
+
311
+ test('should handle stale locks', async () => {
312
+ const lockFile = `${testFile}.kgen-lock`;
313
+
314
+ // Create stale lock file (old timestamp)
315
+ await fs.writeFile(lockFile, 'stale-lock-id');
316
+ const oldTime = Date.now() - 15000; // 15 seconds ago
317
+ await fs.utimes(lockFile, new Date(oldTime), new Date(oldTime));
318
+
319
+ // Should still be able to write (stale lock removed)
320
+ const result = await writer.writeAtomic(testFile, 'Test content');
321
+
322
+ expect(result.success).toBe(true);
323
+ });
324
+
325
+ test('should timeout on persistent locks', async () => {
326
+ // Create a persistent lock
327
+ const lockFile = `${testFile}.kgen-lock`;
328
+ await fs.writeFile(lockFile, 'persistent-lock');
329
+
330
+ // Set very short timeout for testing
331
+ const shortTimeoutWriter = new AtomicWriter({
332
+ backupEnabled: false,
333
+ lockConfig: { TIMEOUT: 100, RETRY_DELAY: 10 }
334
+ });
335
+
336
+ // Update lock timestamp continuously to prevent stale detection
337
+ const updateInterval = setInterval(async () => {
338
+ try {
339
+ await fs.utimes(lockFile, new Date(), new Date());
340
+ } catch {
341
+ // Ignore errors
342
+ }
343
+ }, 50);
344
+
345
+ try {
346
+ await expect(shortTimeoutWriter.writeAtomic(testFile, 'Test'))
347
+ .rejects.toThrow(/Failed to acquire lock.*within timeout/);
348
+ } finally {
349
+ clearInterval(updateInterval);
350
+ try {
351
+ await fs.unlink(lockFile);
352
+ } catch {
353
+ // Ignore cleanup errors
354
+ }
355
+ }
356
+ }, 10000); // Increase test timeout
357
+ });
358
+
359
+ describe('Checksum Calculation', () => {
360
+ test('should calculate consistent checksums', async () => {
361
+ const content = 'Test content for checksum';
362
+
363
+ const result1 = await writer.writeAtomic(testFile, content);
364
+
365
+ // Delete and recreate with same content
366
+ await fs.unlink(testFile);
367
+ const result2 = await writer.writeAtomic(testFile, content);
368
+
369
+ // Checksums should be identical
370
+ expect(result1.checksum).toBe(result2.checksum);
371
+ });
372
+
373
+ test('should have different checksums for different content', async () => {
374
+ const result1 = await writer.writeAtomic(testFile, 'Content 1');
375
+
376
+ await fs.unlink(testFile);
377
+ const result2 = await writer.writeAtomic(testFile, 'Content 2');
378
+
379
+ expect(result1.checksum).not.toBe(result2.checksum);
380
+ });
381
+ });
382
+ });