@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,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
|
+
});
|