@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.
- package/LICENSE +21 -0
- package/README.md +210 -0
- package/package.json +90 -0
- package/src/MIGRATION_COMPLETE.md +186 -0
- package/src/PORT-MAP.md +302 -0
- package/src/base/filter-templates.js +479 -0
- package/src/base/index.js +92 -0
- package/src/base/injection-targets.js +583 -0
- package/src/base/macro-templates.js +298 -0
- package/src/base/macro-templates.js.bak +461 -0
- package/src/base/shacl-templates.js +617 -0
- package/src/base/template-base.js +388 -0
- package/src/core/attestor.js +381 -0
- package/src/core/filters.js +518 -0
- package/src/core/index.js +21 -0
- package/src/core/kgen-engine.js +372 -0
- package/src/core/parser.js +447 -0
- package/src/core/post-processor.js +313 -0
- package/src/core/renderer.js +469 -0
- package/src/doc-generator/cli.mjs +122 -0
- package/src/doc-generator/index.mjs +28 -0
- package/src/doc-generator/mdx-generator.mjs +71 -0
- package/src/doc-generator/nav-generator.mjs +136 -0
- package/src/doc-generator/parser.mjs +291 -0
- package/src/doc-generator/rdf-builder.mjs +306 -0
- package/src/doc-generator/scanner.mjs +189 -0
- package/src/engine/index.js +42 -0
- package/src/engine/pipeline.js +448 -0
- package/src/engine/renderer.js +604 -0
- package/src/engine/template-engine.js +566 -0
- package/src/filters/array.js +436 -0
- package/src/filters/data.js +479 -0
- package/src/filters/index.js +270 -0
- package/src/filters/rdf.js +264 -0
- package/src/filters/text.js +369 -0
- package/src/index.js +109 -0
- package/src/inheritance/index.js +40 -0
- package/src/injection/api.js +260 -0
- package/src/injection/atomic-writer.js +327 -0
- package/src/injection/constants.js +136 -0
- package/src/injection/idempotency-manager.js +295 -0
- package/src/injection/index.js +28 -0
- package/src/injection/injection-engine.js +378 -0
- package/src/injection/integration.js +339 -0
- package/src/injection/modes/index.js +341 -0
- package/src/injection/rollback-manager.js +373 -0
- package/src/injection/target-resolver.js +323 -0
- package/src/injection/tests/atomic-writer.test.js +382 -0
- package/src/injection/tests/injection-engine.test.js +611 -0
- package/src/injection/tests/integration.test.js +392 -0
- package/src/injection/tests/run-tests.js +283 -0
- package/src/injection/validation-engine.js +547 -0
- package/src/linter/determinism-linter.js +473 -0
- package/src/linter/determinism.js +410 -0
- package/src/linter/index.js +6 -0
- package/src/linter/test-doubles.js +475 -0
- package/src/parser/frontmatter.js +228 -0
- package/src/parser/variables.js +344 -0
- package/src/renderer/deterministic.js +245 -0
- package/src/renderer/index.js +6 -0
- package/src/templates/latex/academic-paper.njk +186 -0
- package/src/templates/latex/index.js +104 -0
- package/src/templates/nextjs/app-page.njk +66 -0
- package/src/templates/nextjs/index.js +80 -0
- package/src/templates/office/docx/document.njk +368 -0
- package/src/templates/office/index.js +79 -0
- package/src/templates/office/word-report.njk +129 -0
- 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
|
+
});
|