@t1mmen/srtd 0.3.0 → 0.4.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 (186) hide show
  1. package/README.md +85 -118
  2. package/dist/package.json +97 -0
  3. package/dist/src/__tests__/apply.test.js +69 -0
  4. package/dist/src/__tests__/apply.test.js.map +1 -0
  5. package/dist/src/__tests__/build.test.js +67 -0
  6. package/dist/src/__tests__/build.test.js.map +1 -0
  7. package/dist/{__tests__ → src/__tests__}/vitest.setup.js +41 -25
  8. package/dist/src/__tests__/vitest.setup.js.map +1 -0
  9. package/dist/{__tests__ → src/__tests__}/watch.test.js +9 -3
  10. package/dist/src/__tests__/watch.test.js.map +1 -0
  11. package/dist/src/cli.js.map +1 -0
  12. package/dist/src/commands/_app.js +29 -0
  13. package/dist/src/commands/_app.js.map +1 -0
  14. package/dist/{commands → src/commands}/apply.d.ts +2 -1
  15. package/dist/src/commands/apply.js +30 -0
  16. package/dist/src/commands/apply.js.map +1 -0
  17. package/dist/{commands → src/commands}/build.d.ts +5 -1
  18. package/dist/src/commands/build.js +41 -0
  19. package/dist/src/commands/build.js.map +1 -0
  20. package/dist/src/commands/clear.d.ts +2 -0
  21. package/dist/src/commands/clear.js +45 -0
  22. package/dist/src/commands/clear.js.map +1 -0
  23. package/dist/{commands → src/commands}/index.js +11 -5
  24. package/dist/src/commands/index.js.map +1 -0
  25. package/dist/src/commands/init.js.map +1 -0
  26. package/dist/{commands → src/commands}/register.js +6 -5
  27. package/dist/src/commands/register.js.map +1 -0
  28. package/dist/{commands → src/commands}/watch.js +61 -33
  29. package/dist/src/commands/watch.js.map +1 -0
  30. package/dist/{components → src/components}/Branding.js +7 -7
  31. package/dist/src/components/Branding.js.map +1 -0
  32. package/dist/src/components/Debug.d.ts +2 -0
  33. package/dist/src/components/Debug.js +160 -0
  34. package/dist/src/components/Debug.js.map +1 -0
  35. package/dist/src/components/ProcessingResults.d.ts +9 -0
  36. package/dist/src/components/ProcessingResults.js +32 -0
  37. package/dist/src/components/ProcessingResults.js.map +1 -0
  38. package/dist/{components → src/components}/Quittable.js +1 -1
  39. package/dist/src/components/Quittable.js.map +1 -0
  40. package/dist/src/components/TimeSince.js.map +1 -0
  41. package/dist/src/components/customTheme.d.ts +10 -0
  42. package/dist/src/components/customTheme.js +66 -0
  43. package/dist/src/components/customTheme.js.map +1 -0
  44. package/dist/src/constants.js.map +1 -0
  45. package/dist/src/hooks/useDatabaseConnection.js +57 -0
  46. package/dist/src/hooks/useDatabaseConnection.js.map +1 -0
  47. package/dist/{hooks → src/hooks}/useTemplateManager.d.ts +6 -8
  48. package/dist/src/hooks/useTemplateManager.js +124 -0
  49. package/dist/src/hooks/useTemplateManager.js.map +1 -0
  50. package/dist/src/hooks/useTemplateProcessor.d.ts +11 -0
  51. package/dist/src/hooks/useTemplateProcessor.js +71 -0
  52. package/dist/src/hooks/useTemplateProcessor.js.map +1 -0
  53. package/dist/src/hooks/useTemplateState.js.map +1 -0
  54. package/dist/{lib → src/lib}/templateManager.d.ts +13 -2
  55. package/dist/{lib → src/lib}/templateManager.js +172 -50
  56. package/dist/src/lib/templateManager.js.map +1 -0
  57. package/dist/src/lib/templateManager.test.js +729 -0
  58. package/dist/src/lib/templateManager.test.js.map +1 -0
  59. package/dist/{types.d.ts → src/types.d.ts} +3 -0
  60. package/dist/src/types.js.map +1 -0
  61. package/dist/src/utils/applyMigration.js.map +1 -0
  62. package/dist/src/utils/applyMigrations.test.js.map +1 -0
  63. package/dist/src/utils/calculateMD5.js.map +1 -0
  64. package/dist/{utils → src/utils}/config.d.ts +2 -0
  65. package/dist/src/utils/config.js +65 -0
  66. package/dist/src/utils/config.js.map +1 -0
  67. package/dist/src/utils/config.test.js.map +1 -0
  68. package/dist/src/utils/createEmptyBuildLog.js.map +1 -0
  69. package/dist/{utils → src/utils}/databaseConnection.d.ts +5 -0
  70. package/dist/{utils → src/utils}/databaseConnection.js +40 -13
  71. package/dist/src/utils/databaseConnection.js.map +1 -0
  72. package/dist/src/utils/databaseConnection.test.d.ts +1 -0
  73. package/dist/src/utils/databaseConnection.test.js.map +1 -0
  74. package/dist/src/utils/ensureDirectories.js.map +1 -0
  75. package/dist/src/utils/fileExists.js.map +1 -0
  76. package/dist/src/utils/getNextTimestamp.js.map +1 -0
  77. package/dist/src/utils/isWipTemplate.js.map +1 -0
  78. package/dist/src/utils/loadBuildLog.js.map +1 -0
  79. package/dist/src/utils/loadBuildLog.test.d.ts +1 -0
  80. package/dist/src/utils/loadBuildLog.test.js.map +1 -0
  81. package/dist/src/utils/logger.js.map +1 -0
  82. package/dist/src/utils/registerTemplate.js.map +1 -0
  83. package/dist/src/utils/safeCreate.js.map +1 -0
  84. package/dist/src/utils/saveBuildLog.js.map +1 -0
  85. package/dist/src/utils/store.d.ts +5 -0
  86. package/dist/src/utils/store.js +10 -0
  87. package/dist/src/utils/store.js.map +1 -0
  88. package/package.json +4 -3
  89. package/dist/__tests__/vitest.setup.js.map +0 -1
  90. package/dist/__tests__/watch.test.js.map +0 -1
  91. package/dist/cli.js.map +0 -1
  92. package/dist/commands/_app.js +0 -27
  93. package/dist/commands/_app.js.map +0 -1
  94. package/dist/commands/apply.js +0 -32
  95. package/dist/commands/apply.js.map +0 -1
  96. package/dist/commands/build.js +0 -25
  97. package/dist/commands/build.js.map +0 -1
  98. package/dist/commands/index.js.map +0 -1
  99. package/dist/commands/init.js.map +0 -1
  100. package/dist/commands/register.js.map +0 -1
  101. package/dist/commands/watch.js.map +0 -1
  102. package/dist/components/Branding.js.map +0 -1
  103. package/dist/components/Quittable.js.map +0 -1
  104. package/dist/components/TimeSince.js.map +0 -1
  105. package/dist/constants.js.map +0 -1
  106. package/dist/hooks/useDatabaseConnection.js +0 -68
  107. package/dist/hooks/useDatabaseConnection.js.map +0 -1
  108. package/dist/hooks/useTemplateManager.js +0 -141
  109. package/dist/hooks/useTemplateManager.js.map +0 -1
  110. package/dist/hooks/useTemplateState.js.map +0 -1
  111. package/dist/lib/templateManager.js.map +0 -1
  112. package/dist/lib/templateManager.test.js +0 -289
  113. package/dist/lib/templateManager.test.js.map +0 -1
  114. package/dist/types.js.map +0 -1
  115. package/dist/utils/applyMigration.js.map +0 -1
  116. package/dist/utils/applyMigrations.test.js.map +0 -1
  117. package/dist/utils/calculateMD5.js.map +0 -1
  118. package/dist/utils/config.js +0 -79
  119. package/dist/utils/config.js.map +0 -1
  120. package/dist/utils/config.test.js.map +0 -1
  121. package/dist/utils/createEmptyBuildLog.js.map +0 -1
  122. package/dist/utils/databaseConnection.js.map +0 -1
  123. package/dist/utils/databaseConnection.test.js.map +0 -1
  124. package/dist/utils/ensureDirectories.js.map +0 -1
  125. package/dist/utils/fileExists.js.map +0 -1
  126. package/dist/utils/getNextTimestamp.js.map +0 -1
  127. package/dist/utils/isWipTemplate.js.map +0 -1
  128. package/dist/utils/loadBuildLog.js.map +0 -1
  129. package/dist/utils/loadBuildLog.test.js.map +0 -1
  130. package/dist/utils/logger.js.map +0 -1
  131. package/dist/utils/registerTemplate.js.map +0 -1
  132. package/dist/utils/safeCreate.js.map +0 -1
  133. package/dist/utils/saveBuildLog.js.map +0 -1
  134. /package/dist/{__tests__/watch.test.d.ts → src/__tests__/apply.test.d.ts} +0 -0
  135. /package/dist/{lib/templateManager.test.d.ts → src/__tests__/build.test.d.ts} +0 -0
  136. /package/dist/{__tests__ → src/__tests__}/vitest.setup.d.ts +0 -0
  137. /package/dist/{utils/applyMigrations.test.d.ts → src/__tests__/watch.test.d.ts} +0 -0
  138. /package/dist/{cli.d.ts → src/cli.d.ts} +0 -0
  139. /package/dist/{cli.js → src/cli.js} +0 -0
  140. /package/dist/{commands → src/commands}/_app.d.ts +0 -0
  141. /package/dist/{commands → src/commands}/index.d.ts +0 -0
  142. /package/dist/{commands → src/commands}/init.d.ts +0 -0
  143. /package/dist/{commands → src/commands}/init.js +0 -0
  144. /package/dist/{commands → src/commands}/register.d.ts +0 -0
  145. /package/dist/{commands → src/commands}/watch.d.ts +0 -0
  146. /package/dist/{components → src/components}/Branding.d.ts +0 -0
  147. /package/dist/{components → src/components}/Quittable.d.ts +0 -0
  148. /package/dist/{components → src/components}/TimeSince.d.ts +0 -0
  149. /package/dist/{components → src/components}/TimeSince.js +0 -0
  150. /package/dist/{constants.d.ts → src/constants.d.ts} +0 -0
  151. /package/dist/{constants.js → src/constants.js} +0 -0
  152. /package/dist/{hooks → src/hooks}/useDatabaseConnection.d.ts +0 -0
  153. /package/dist/{hooks → src/hooks}/useTemplateState.d.ts +0 -0
  154. /package/dist/{hooks → src/hooks}/useTemplateState.js +0 -0
  155. /package/dist/{utils/config.test.d.ts → src/lib/templateManager.test.d.ts} +0 -0
  156. /package/dist/{types.js → src/types.js} +0 -0
  157. /package/dist/{utils → src/utils}/applyMigration.d.ts +0 -0
  158. /package/dist/{utils → src/utils}/applyMigration.js +0 -0
  159. /package/dist/{utils/databaseConnection.test.d.ts → src/utils/applyMigrations.test.d.ts} +0 -0
  160. /package/dist/{utils → src/utils}/applyMigrations.test.js +0 -0
  161. /package/dist/{utils → src/utils}/calculateMD5.d.ts +0 -0
  162. /package/dist/{utils → src/utils}/calculateMD5.js +0 -0
  163. /package/dist/{utils/loadBuildLog.test.d.ts → src/utils/config.test.d.ts} +0 -0
  164. /package/dist/{utils → src/utils}/config.test.js +0 -0
  165. /package/dist/{utils → src/utils}/createEmptyBuildLog.d.ts +0 -0
  166. /package/dist/{utils → src/utils}/createEmptyBuildLog.js +0 -0
  167. /package/dist/{utils → src/utils}/databaseConnection.test.js +0 -0
  168. /package/dist/{utils → src/utils}/ensureDirectories.d.ts +0 -0
  169. /package/dist/{utils → src/utils}/ensureDirectories.js +0 -0
  170. /package/dist/{utils → src/utils}/fileExists.d.ts +0 -0
  171. /package/dist/{utils → src/utils}/fileExists.js +0 -0
  172. /package/dist/{utils → src/utils}/getNextTimestamp.d.ts +0 -0
  173. /package/dist/{utils → src/utils}/getNextTimestamp.js +0 -0
  174. /package/dist/{utils → src/utils}/isWipTemplate.d.ts +0 -0
  175. /package/dist/{utils → src/utils}/isWipTemplate.js +0 -0
  176. /package/dist/{utils → src/utils}/loadBuildLog.d.ts +0 -0
  177. /package/dist/{utils → src/utils}/loadBuildLog.js +0 -0
  178. /package/dist/{utils → src/utils}/loadBuildLog.test.js +0 -0
  179. /package/dist/{utils → src/utils}/logger.d.ts +0 -0
  180. /package/dist/{utils → src/utils}/logger.js +0 -0
  181. /package/dist/{utils → src/utils}/registerTemplate.d.ts +0 -0
  182. /package/dist/{utils → src/utils}/registerTemplate.js +0 -0
  183. /package/dist/{utils → src/utils}/safeCreate.d.ts +0 -0
  184. /package/dist/{utils → src/utils}/safeCreate.js +0 -0
  185. /package/dist/{utils → src/utils}/saveBuildLog.d.ts +0 -0
  186. /package/dist/{utils → src/utils}/saveBuildLog.js +0 -0
@@ -0,0 +1,729 @@
1
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
2
+ if (value !== null && value !== void 0) {
3
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
4
+ var dispose, inner;
5
+ if (async) {
6
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
7
+ dispose = value[Symbol.asyncDispose];
8
+ }
9
+ if (dispose === void 0) {
10
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
11
+ dispose = value[Symbol.dispose];
12
+ if (async) inner = dispose;
13
+ }
14
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
15
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
16
+ env.stack.push({ value: value, dispose: dispose, async: async });
17
+ }
18
+ else if (async) {
19
+ env.stack.push({ async: true });
20
+ }
21
+ return value;
22
+ };
23
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
24
+ return function (env) {
25
+ function fail(e) {
26
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
27
+ env.hasError = true;
28
+ }
29
+ var r, s = 0;
30
+ function next() {
31
+ while (r = env.stack.pop()) {
32
+ try {
33
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
34
+ if (r.dispose) {
35
+ var result = r.dispose.call(r.value);
36
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
37
+ }
38
+ else s |= 1;
39
+ }
40
+ catch (e) {
41
+ fail(e);
42
+ }
43
+ }
44
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
45
+ if (env.hasError) throw env.error;
46
+ }
47
+ return next();
48
+ };
49
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
50
+ var e = new Error(message);
51
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
+ });
53
+ import fs from 'node:fs/promises';
54
+ import { tmpdir } from 'node:os';
55
+ import { default as path, join, relative } from 'node:path';
56
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
57
+ import { TEST_FN_PREFIX } from '../__tests__/vitest.setup.js';
58
+ import { calculateMD5 } from '../utils/calculateMD5.js';
59
+ import { connect, disconnect } from '../utils/databaseConnection.js';
60
+ import { ensureDirectories } from '../utils/ensureDirectories.js';
61
+ import { TemplateManager } from './templateManager.js';
62
+ describe('TemplateManager', () => {
63
+ const testContext = {
64
+ timestamp: Date.now(),
65
+ testDir: '',
66
+ testFunctionName: '',
67
+ };
68
+ beforeEach(async () => {
69
+ testContext.testDir = join(tmpdir(), `srtd-test-${testContext.timestamp}`);
70
+ testContext.testFunctionName = `${TEST_FN_PREFIX}${testContext.timestamp}`;
71
+ await ensureDirectories(testContext.testDir);
72
+ const client = await connect();
73
+ try {
74
+ await client.query('BEGIN');
75
+ await client.query(`DROP FUNCTION IF EXISTS ${testContext.testFunctionName}()`);
76
+ await client.query('COMMIT');
77
+ }
78
+ catch (e) {
79
+ await client.query('ROLLBACK');
80
+ throw e;
81
+ }
82
+ finally {
83
+ client.release();
84
+ }
85
+ });
86
+ afterEach(async () => {
87
+ const client = await connect();
88
+ try {
89
+ await client.query('BEGIN');
90
+ await client.query(`DROP FUNCTION IF EXISTS ${testContext.testFunctionName}()`);
91
+ await client.query('COMMIT');
92
+ }
93
+ catch (_) {
94
+ await client.query('ROLLBACK');
95
+ }
96
+ finally {
97
+ client.release();
98
+ }
99
+ await fs.rm(testContext.testDir, { recursive: true, force: true });
100
+ disconnect();
101
+ });
102
+ const createTemplate = async (name, content, dir) => {
103
+ const fullPath = dir
104
+ ? join(testContext.testDir, 'test-templates', dir, name)
105
+ : join(testContext.testDir, 'test-templates', name);
106
+ await fs.mkdir(path.dirname(fullPath), { recursive: true });
107
+ await fs.writeFile(fullPath, content);
108
+ return fullPath;
109
+ };
110
+ const createTemplateWithFunc = async (name, funcSuffix = '', dir) => {
111
+ const funcName = `${testContext.testFunctionName}${funcSuffix}`;
112
+ const content = `CREATE OR REPLACE FUNCTION ${funcName}() RETURNS void AS $$ BEGIN NULL; END; $$ LANGUAGE plpgsql;`;
113
+ return createTemplate(name, content, dir);
114
+ };
115
+ it('should create migration file when template changes', async () => {
116
+ await createTemplateWithFunc(`test-${testContext.timestamp}.sql`);
117
+ const manager = await TemplateManager.create(testContext.testDir);
118
+ await manager.processTemplates({ generateFiles: true });
119
+ const migrations = await fs.readdir(join(testContext.testDir, 'test-migrations'));
120
+ const relevantMigrations = migrations.filter(m => m.includes(`test-${testContext.timestamp}`));
121
+ expect(relevantMigrations).toHaveLength(1);
122
+ });
123
+ it('should not allow building WIP templates', async () => {
124
+ await createTemplateWithFunc(`test-${testContext.timestamp}.wip.sql`);
125
+ const manager = await TemplateManager.create(testContext.testDir);
126
+ await manager.processTemplates({ generateFiles: true });
127
+ const migrations = await fs.readdir(join(testContext.testDir, 'test-migrations'));
128
+ expect(migrations.filter(m => m.includes(`test-${testContext.timestamp}`))).toHaveLength(0);
129
+ });
130
+ it('should maintain separate build and local logs', async () => {
131
+ const templatePath = join(testContext.testDir, 'test-templates', `test-${testContext.timestamp}.sql`);
132
+ const templateContent = `CREATE FUNCTION ${testContext.testFunctionName}() RETURNS void AS $$ BEGIN NULL; END; $$ LANGUAGE plpgsql;`;
133
+ await fs.writeFile(templatePath, templateContent);
134
+ const manager = await TemplateManager.create(testContext.testDir);
135
+ // Build writes to build log
136
+ await manager.processTemplates({ generateFiles: true });
137
+ const buildLog = JSON.parse(await fs.readFile(join(testContext.testDir, '.buildlog-test.json'), 'utf-8'));
138
+ const relPath = relative(testContext.testDir, templatePath);
139
+ expect(buildLog.templates[relPath].lastBuildHash).toBeDefined();
140
+ // Apply writes to local log
141
+ await manager.processTemplates({ apply: true });
142
+ const localLog = JSON.parse(await fs.readFile(join(testContext.testDir, '.buildlog-test.local.json'), 'utf-8'));
143
+ expect(localLog.templates[relPath].lastAppliedHash).toBeDefined();
144
+ });
145
+ it('should track template state correctly', async () => {
146
+ const templatePath = join(testContext.testDir, 'test-templates', `test-${testContext.timestamp}.sql`);
147
+ const templateContent = `CREATE FUNCTION ${testContext.testFunctionName}() RETURNS void AS $$ BEGIN NULL; END; $$ LANGUAGE plpgsql;`;
148
+ await fs.writeFile(templatePath, templateContent);
149
+ const manager = await TemplateManager.create(testContext.testDir);
150
+ // Initially no state
151
+ let status = await manager.getTemplateStatus(templatePath);
152
+ expect(status.buildState.lastBuildHash).toBeUndefined();
153
+ expect(status.buildState.lastAppliedHash).toBeUndefined();
154
+ // After build
155
+ await manager.processTemplates({ generateFiles: true });
156
+ status = await manager.getTemplateStatus(templatePath);
157
+ expect(status.buildState.lastBuildHash).toBeDefined();
158
+ expect(status.buildState.lastBuildDate).toBeDefined();
159
+ // After apply
160
+ await manager.processTemplates({ apply: true });
161
+ status = await manager.getTemplateStatus(templatePath);
162
+ expect(status.buildState.lastAppliedHash).toBeDefined();
163
+ expect(status.buildState.lastAppliedDate).toBeDefined();
164
+ });
165
+ it('should handle rapid template changes', async () => {
166
+ const templatePath = join(testContext.testDir, 'test-templates', `test-${testContext.timestamp}.sql`);
167
+ const baseContent = `CREATE OR REPLACE FUNCTION ${testContext.testFunctionName}() RETURNS void AS $$ BEGIN NULL; END; $$ LANGUAGE plpgsql;`;
168
+ await fs.writeFile(templatePath, baseContent);
169
+ const manager = await TemplateManager.create(testContext.testDir);
170
+ const changes = [];
171
+ manager.on('templateChanged', async (template) => {
172
+ changes.push(template.currentHash);
173
+ });
174
+ const watcher = await manager.watch();
175
+ await new Promise(resolve => setTimeout(resolve, 100));
176
+ // Make rapid changes
177
+ for (let i = 0; i < 5; i++) {
178
+ await fs.writeFile(templatePath, `${baseContent}\n-- Change ${i}`);
179
+ await new Promise(resolve => setTimeout(resolve, 51));
180
+ }
181
+ await new Promise(resolve => setTimeout(resolve, 500));
182
+ watcher.close();
183
+ expect(changes.length).toBeGreaterThanOrEqual(1);
184
+ expect(new Set(changes).size).toBe(changes.length); // All changes should be unique
185
+ }, 10000);
186
+ it('should apply WIP templates directly to database', async () => {
187
+ const templatePath = join(testContext.testDir, 'test-templates', `test-${testContext.timestamp}.wip.sql`);
188
+ const templateContent = `CREATE FUNCTION ${testContext.testFunctionName}() RETURNS void AS $$ BEGIN NULL; END; $$ LANGUAGE plpgsql;`;
189
+ await fs.writeFile(templatePath, templateContent);
190
+ const manager = await TemplateManager.create(testContext.testDir);
191
+ const result = await manager.processTemplates({ apply: true });
192
+ expect(result.errors).toHaveLength(0);
193
+ const client = await connect();
194
+ try {
195
+ const res = await client.query(`SELECT COUNT(*) FROM pg_proc WHERE proname = $1`, [
196
+ testContext.testFunctionName,
197
+ ]);
198
+ expect(Number.parseInt(res.rows[0].count)).toBe(1);
199
+ }
200
+ finally {
201
+ client.release();
202
+ }
203
+ });
204
+ it('should handle sequential template operations', async () => {
205
+ const templates = await Promise.all([...Array(5)].map(async (_, i) => {
206
+ await new Promise(resolve => setTimeout(resolve, 5));
207
+ return createTemplateWithFunc(`sequencetest-${i}-${testContext.timestamp}.sql`, `_sequence_test_${i}`);
208
+ }));
209
+ const manager = await TemplateManager.create(testContext.testDir);
210
+ // Apply templates one by one
211
+ for (const _templatePath of templates) {
212
+ const result = await manager.processTemplates({ apply: true });
213
+ expect(result.errors).toHaveLength(0);
214
+ }
215
+ await new Promise(resolve => setTimeout(resolve, 100));
216
+ const client = await connect();
217
+ try {
218
+ for (let i = 0; i < 5; i++) {
219
+ const res = await client.query(`SELECT proname FROM pg_proc WHERE proname = $1`, [
220
+ `${testContext.testFunctionName}_sequence_test_${i}`,
221
+ ]);
222
+ expect(res.rows).toHaveLength(1);
223
+ }
224
+ }
225
+ finally {
226
+ client.release();
227
+ }
228
+ });
229
+ it('should generate unique timestamps for multiple templates', async () => {
230
+ const templates = await Promise.all([...Array(10)].map((_, i) => createTemplateWithFunc(`timestamptest-${i}-${testContext.timestamp}.sql`, `_${i}`)));
231
+ const manager = await TemplateManager.create(testContext.testDir);
232
+ await manager.processTemplates({ generateFiles: true });
233
+ const migrations = await fs.readdir(join(testContext.testDir, 'test-migrations'));
234
+ const timestamps = migrations.map(m => m.split('_')[0]);
235
+ const uniqueTimestamps = new Set(timestamps);
236
+ expect(uniqueTimestamps.size).toBe(templates.length);
237
+ expect(timestamps).toEqual([...timestamps].sort());
238
+ });
239
+ it('should handle mix of working and broken templates', async () => {
240
+ await createTemplateWithFunc(`a-test-good-${testContext.timestamp}.sql`, '_good');
241
+ await createTemplate(`a-test-bad-${testContext.timestamp}.sql`, 'INVALID SQL SYNTAX;');
242
+ const manager = await TemplateManager.create(testContext.testDir);
243
+ const result = await manager.processTemplates({ apply: true });
244
+ expect(result.errors).toHaveLength(1);
245
+ expect(result.applied).toHaveLength(1);
246
+ const client = await connect();
247
+ try {
248
+ const res = await client.query(`SELECT COUNT(*) FROM pg_proc WHERE proname = $1`, [
249
+ `${testContext.testFunctionName}_good`,
250
+ ]);
251
+ expect(Number.parseInt(res.rows[0].count)).toBe(1);
252
+ }
253
+ finally {
254
+ client.release();
255
+ }
256
+ });
257
+ it('should handle database errors gracefully', async () => {
258
+ const manager = await TemplateManager.create(testContext.testDir);
259
+ await createTemplate(`test-error-${testContext.timestamp}.sql`, 'SELECT 1/0;'); // Division by zero error
260
+ const result = await manager.processTemplates({ apply: true });
261
+ expect(result.errors).toHaveLength(1);
262
+ expect(result.errors[0]?.error).toMatch(/division by zero/i);
263
+ });
264
+ it('should handle file system errors', async () => {
265
+ const errorPath = join(testContext.testDir, 'test-templates', `test-error-${testContext.timestamp}.sql`);
266
+ try {
267
+ await createTemplate(`test-error-${testContext.timestamp}.sql`, 'SELECT 1;');
268
+ await fs.chmod(errorPath, 0o000);
269
+ const manager = await TemplateManager.create(testContext.testDir);
270
+ await manager.processTemplates({ generateFiles: true });
271
+ // Cleanup for afterEach
272
+ await fs.chmod(errorPath, 0o644);
273
+ }
274
+ catch (error) {
275
+ expect(error).toBeDefined();
276
+ expect(error).toMatchObject({
277
+ errno: -13,
278
+ code: 'EACCES',
279
+ syscall: 'open',
280
+ path: expect.stringContaining('test-error'),
281
+ });
282
+ // expect(error.length).toBeGreaterThan(0);
283
+ }
284
+ });
285
+ it('should handle large batches of templates', async () => {
286
+ // Create 50 templates
287
+ await Promise.all([...Array(50)].map((_, i) => createTemplateWithFunc(`test-${i}-${testContext.timestamp}.sql`, `_${i}`)));
288
+ const manager = await TemplateManager.create(testContext.testDir);
289
+ const result = await manager.processTemplates({ generateFiles: true });
290
+ expect(result.errors).toHaveLength(0);
291
+ const migrations = await fs.readdir(join(testContext.testDir, 'test-migrations'));
292
+ expect(migrations.length).toBe(50);
293
+ });
294
+ it('should handle templates with complex SQL', async () => {
295
+ const complexSQL = `
296
+ CREATE OR REPLACE FUNCTION ${testContext.testFunctionName}(
297
+ param1 integer DEFAULT 100,
298
+ OUT result1 integer,
299
+ OUT result2 text
300
+ ) RETURNS record AS $$
301
+ DECLARE
302
+ temp_var integer;
303
+ BEGIN
304
+ -- Complex logic with multiple statements
305
+ SELECT CASE
306
+ WHEN param1 > 100 THEN param1 * 2
307
+ ELSE param1 / 2
308
+ END INTO temp_var;
309
+
310
+ result1 := temp_var;
311
+ result2 := 'Processed: ' || temp_var::text;
312
+
313
+ -- Exception handling
314
+ EXCEPTION WHEN OTHERS THEN
315
+ result1 := -1;
316
+ result2 := SQLERRM;
317
+ END;
318
+ $$ LANGUAGE plpgsql;
319
+ `;
320
+ await createTemplate(`test-complex-${testContext.timestamp}.sql`, complexSQL);
321
+ const manager = await TemplateManager.create(testContext.testDir);
322
+ const result = await manager.processTemplates({ apply: true });
323
+ expect(result.errors).toHaveLength(0);
324
+ const client = await connect();
325
+ try {
326
+ const res = await client.query(`
327
+ SELECT proname, pronargs, prorettype::regtype::text as return_type
328
+ FROM pg_proc
329
+ WHERE proname = $1
330
+ `, [testContext.testFunctionName]);
331
+ expect(res.rows).toHaveLength(1);
332
+ expect(res.rows[0].return_type).toBe('record');
333
+ }
334
+ finally {
335
+ client.release();
336
+ }
337
+ });
338
+ it('should maintain template state across manager instances', async () => {
339
+ const template = await createTemplateWithFunc(`test-${testContext.timestamp}.sql`);
340
+ // First manager instance
341
+ const manager1 = await TemplateManager.create(testContext.testDir);
342
+ await manager1.processTemplates({ generateFiles: true });
343
+ // Second manager instance should see the state
344
+ const manager2 = await TemplateManager.create(testContext.testDir);
345
+ const status = await manager2.getTemplateStatus(template);
346
+ expect(status.buildState.lastBuildHash).toBeDefined();
347
+ });
348
+ it('should handle template additions in watch mode', async () => {
349
+ const manager = await TemplateManager.create(testContext.testDir);
350
+ const changes = [];
351
+ manager.on('templateChanged', template => {
352
+ changes.push(template.name);
353
+ });
354
+ const watcher = await manager.watch();
355
+ // Add new template after watch started
356
+ await createTemplateWithFunc(`test-new-${testContext.timestamp}.sql`);
357
+ await new Promise(resolve => setTimeout(resolve, 150));
358
+ watcher.close();
359
+ expect(changes).toContain(`test-new-${testContext.timestamp}`);
360
+ });
361
+ it('should handle templates in deep subdirectories', async () => {
362
+ // Create nested directory structure
363
+ const depth = 5;
364
+ const templatePaths = [];
365
+ for (let i = 1; i <= depth; i++) {
366
+ const dir = [...Array(i)].map((_, idx) => `level${idx + 1}`).join('/');
367
+ const templatePath = await createTemplateWithFunc(`depth-test-${i}-${testContext.timestamp}.sql`, `_depth_${i}`, dir);
368
+ templatePaths.push(templatePath);
369
+ }
370
+ const manager = await TemplateManager.create(testContext.testDir);
371
+ const changes = [];
372
+ manager.on('templateChanged', template => {
373
+ changes.push(template.name);
374
+ });
375
+ const watcher = await manager.watch();
376
+ await new Promise(resolve => setTimeout(resolve, depth * 100 * 1.1));
377
+ watcher.close();
378
+ expect(changes.length).toBe(depth);
379
+ // Verify each template was detected
380
+ for (let i = 1; i <= depth; i++) {
381
+ expect(changes).toContain(`depth-test-${i}-${testContext.timestamp}`);
382
+ }
383
+ });
384
+ it('should only watch SQL files', async () => {
385
+ const manager = await TemplateManager.create(testContext.testDir);
386
+ const changes = [];
387
+ manager.on('templateChanged', template => {
388
+ changes.push(template.name);
389
+ });
390
+ const watcher = await manager.watch();
391
+ await new Promise(resolve => setTimeout(resolve, 100));
392
+ // Create various file types
393
+ await fs.writeFile(join(testContext.testDir, 'test-templates/test.txt'), 'not sql');
394
+ await fs.writeFile(join(testContext.testDir, 'test-templates/test.md'), 'not sql');
395
+ await createTemplateWithFunc(`test-sql-${testContext.timestamp}.sql`);
396
+ await new Promise(resolve => setTimeout(resolve, 500));
397
+ watcher.close();
398
+ expect(changes).toHaveLength(1);
399
+ expect(changes[0]).toBe(`test-sql-${testContext.timestamp}`);
400
+ });
401
+ it('should handle multiple template changes simultaneously', async () => {
402
+ const manager = await TemplateManager.create(testContext.testDir);
403
+ const changes = new Set();
404
+ const count = 5;
405
+ manager.on('templateChanged', template => {
406
+ changes.add(template.name);
407
+ });
408
+ const watcher = await manager.watch();
409
+ await new Promise(resolve => setTimeout(resolve, 100));
410
+ // Create multiple templates simultaneously
411
+ await Promise.all([
412
+ createTemplateWithFunc(`rapid_test-1-${testContext.timestamp}.sql`, '_batch_changes_1'),
413
+ createTemplateWithFunc(`rapid_test-2-${testContext.timestamp}.sql`, '_batch_changes_2'),
414
+ createTemplateWithFunc(`rapid_test-3-${testContext.timestamp}.sql`, '_batch_changes_3'),
415
+ createTemplateWithFunc(`rapid_test-4-${testContext.timestamp}.sql`, '_batch_changes_4', 'deep'),
416
+ createTemplateWithFunc(`rapid_test-5-${testContext.timestamp}.sql`, '_batch_changes_5', 'deep/nested'),
417
+ ]);
418
+ // Give enough time for all changes to be detected
419
+ await new Promise(resolve => setTimeout(resolve, count * 100 * 1.1));
420
+ watcher.close();
421
+ expect(changes.size).toBe(count); // Should detect all 5 templates
422
+ for (let i = 1; i <= count; i++) {
423
+ expect(changes.has(`rapid_test-${i}-${testContext.timestamp}`)).toBe(true);
424
+ }
425
+ // Verify all templates were processed
426
+ const client = await connect();
427
+ try {
428
+ for (let i = 1; i <= count; i++) {
429
+ const res = await client.query(`SELECT proname FROM pg_proc WHERE proname = $1`, [
430
+ `${testContext.testFunctionName}_batch_changes_${i}`,
431
+ ]);
432
+ expect(res.rows).toHaveLength(1);
433
+ }
434
+ }
435
+ finally {
436
+ client.release();
437
+ }
438
+ }, 15000);
439
+ it('should handle rapid bulk template creation realistically', async () => {
440
+ const TEMPLATE_COUNT = 50;
441
+ const manager = await TemplateManager.create(testContext.testDir);
442
+ const processed = new Set();
443
+ const failed = new Set();
444
+ const inProgress = new Set();
445
+ const events = [];
446
+ let resolveProcessing;
447
+ const processingComplete = new Promise(resolve => {
448
+ resolveProcessing = resolve;
449
+ });
450
+ manager.on('templateChanged', ({ name }) => {
451
+ events.push({ event: 'changed', template: name, time: Date.now() });
452
+ inProgress.add(name);
453
+ });
454
+ manager.on('templateApplied', ({ name }) => {
455
+ events.push({ event: 'applied', template: name, time: Date.now() });
456
+ processed.add(name);
457
+ inProgress.delete(name);
458
+ if (processed.size + failed.size === TEMPLATE_COUNT) {
459
+ resolveProcessing();
460
+ }
461
+ });
462
+ manager.on('templateError', ({ template: { name }, error }) => {
463
+ events.push({ event: 'error', template: name, time: Date.now() });
464
+ failed.add(name);
465
+ inProgress.delete(name);
466
+ console.error('Template error:', { name, error });
467
+ if (processed.size + failed.size === TEMPLATE_COUNT) {
468
+ resolveProcessing();
469
+ }
470
+ });
471
+ const watcher = await manager.watch();
472
+ // Create all templates
473
+ await Promise.all(Array.from({ length: TEMPLATE_COUNT }, (_, i) => createTemplateWithFunc(`bulk_created_template_${i + 1}.sql`, `_bulk_${i + 1}`)));
474
+ await processingComplete;
475
+ watcher.close();
476
+ expect(processed.size + failed.size).toBe(TEMPLATE_COUNT);
477
+ expect(inProgress.size).toBe(0);
478
+ expect(failed.size).toBe(0);
479
+ });
480
+ it('should cleanup resources when disposed', async () => {
481
+ const manager = await TemplateManager.create(testContext.testDir);
482
+ const changes = [];
483
+ manager.on('templateChanged', template => {
484
+ changes.push(template.name);
485
+ });
486
+ await manager.watch();
487
+ // Create template before disposal
488
+ await createTemplateWithFunc(`test-before-dispose-${testContext.timestamp}.sql`);
489
+ await new Promise(resolve => setTimeout(resolve, 100));
490
+ // Dispose and verify cleanup
491
+ manager[Symbol.dispose]();
492
+ // Try creating template after disposal
493
+ await createTemplateWithFunc(`test-after-dispose-${testContext.timestamp}.sql`);
494
+ await new Promise(resolve => setTimeout(resolve, 100));
495
+ expect(changes).toHaveLength(1);
496
+ expect(changes[0]).toBe(`test-before-dispose-${testContext.timestamp}`);
497
+ });
498
+ it('should auto-cleanup with using statement', async () => {
499
+ const changes = [];
500
+ await (async () => {
501
+ const env_1 = { stack: [], error: void 0, hasError: false };
502
+ try {
503
+ const manager = __addDisposableResource(env_1, await TemplateManager.create(testContext.testDir), false);
504
+ manager.on('templateChanged', template => {
505
+ changes.push(template.name);
506
+ });
507
+ await manager.watch();
508
+ await createTemplateWithFunc(`test-during-scope-${testContext.timestamp}.sql`);
509
+ await new Promise(resolve => setTimeout(resolve, 100));
510
+ }
511
+ catch (e_1) {
512
+ env_1.error = e_1;
513
+ env_1.hasError = true;
514
+ }
515
+ finally {
516
+ __disposeResources(env_1);
517
+ }
518
+ })();
519
+ // After scope exit, create another template
520
+ await createTemplateWithFunc(`test-after-scope-${testContext.timestamp}.sql`);
521
+ await new Promise(resolve => setTimeout(resolve, 100));
522
+ expect(changes).toHaveLength(1);
523
+ expect(changes[0]).toBe(`test-during-scope-${testContext.timestamp}`);
524
+ });
525
+ it('should not process unchanged templates', async () => {
526
+ const templatePath = await createTemplateWithFunc(`test-unchanged-${testContext.timestamp}.sql`);
527
+ const manager = await TemplateManager.create(testContext.testDir);
528
+ await manager.watch();
529
+ // First processing
530
+ await manager.processTemplates({ apply: true });
531
+ // Get the status after first processing
532
+ const statusAfterFirstRun = await manager.getTemplateStatus(templatePath);
533
+ const changes = [];
534
+ manager.on('templateChanged', template => {
535
+ changes.push(template.name);
536
+ });
537
+ // Process again without changes
538
+ await manager.processTemplates({ apply: true });
539
+ // Get status after second run
540
+ const statusAfterSecondRun = await manager.getTemplateStatus(templatePath);
541
+ expect(changes).toHaveLength(0);
542
+ expect(statusAfterSecondRun.buildState.lastBuildHash).toBe(statusAfterFirstRun.buildState.lastBuildHash);
543
+ expect(statusAfterSecondRun.buildState.lastAppliedHash).toBe(statusAfterFirstRun.buildState.lastAppliedHash);
544
+ });
545
+ it('should only process modified templates in batch', async () => {
546
+ // Create two templates
547
+ const template1 = await createTemplateWithFunc(`modified_tmpl_1-${testContext.timestamp}.sql`, 'mod_1');
548
+ await createTemplateWithFunc(`modified_tmpl_2-${testContext.timestamp}.sql`, 'mod_2');
549
+ const manager = await TemplateManager.create(testContext.testDir);
550
+ // First processing of both
551
+ await manager.processTemplates({ apply: true });
552
+ const changes = [];
553
+ manager.on('templateChanged', template => {
554
+ changes.push(template.name);
555
+ });
556
+ // Modify only template1
557
+ await fs.writeFile(template1, `${await fs.readFile(template1, 'utf-8')}\n-- Modified`);
558
+ // Process both templates again
559
+ await manager.processTemplates({ apply: true });
560
+ expect(changes).toHaveLength(1);
561
+ expect(changes[0]).toBe(`modified_tmpl_1-${testContext.timestamp}`);
562
+ });
563
+ it('should correctly update local buildlog on apply', async () => {
564
+ const templatePath = await createTemplateWithFunc(`test-buildlog-${testContext.timestamp}.sql`);
565
+ const manager = await TemplateManager.create(testContext.testDir);
566
+ const localBuildlogPath = join(testContext.testDir, '.buildlog-test.local.json');
567
+ // Initial apply
568
+ await manager.processTemplates({ apply: true });
569
+ const initialLog = JSON.parse(await fs.readFile(localBuildlogPath, 'utf-8'));
570
+ const relPath = relative(testContext.testDir, templatePath);
571
+ const initialHash = initialLog.templates[relPath].lastAppliedHash;
572
+ const initialContent = await fs.readFile(templatePath, 'utf-8');
573
+ expect(initialHash).toBeDefined();
574
+ // Modify template
575
+ await fs.writeFile(templatePath, `${initialContent}\n-- Modified`);
576
+ await new Promise(resolve => setTimeout(resolve, 100));
577
+ const changedContent = await fs.readFile(templatePath, 'utf-8');
578
+ // Second apply
579
+ await manager.processTemplates({ apply: true });
580
+ await new Promise(resolve => setTimeout(resolve, 100));
581
+ const updatedLog = JSON.parse(await fs.readFile(localBuildlogPath, 'utf-8'));
582
+ const newHash = updatedLog.templates[relPath].lastAppliedHash;
583
+ const manualMd5 = await calculateMD5(changedContent);
584
+ expect(newHash).toBeDefined();
585
+ expect(newHash).toBe(manualMd5);
586
+ expect(newHash).not.toBe(initialHash);
587
+ });
588
+ it('should skip apply if template hash matches local buildlog', async () => {
589
+ const templatePath = await createTemplateWithFunc(`test-skip-${testContext.timestamp}.sql`);
590
+ const manager = await TemplateManager.create(testContext.testDir);
591
+ const localBuildlogPath = join(testContext.testDir, '.buildlog-test.local.json');
592
+ // Initial apply
593
+ await manager.processTemplates({ apply: true });
594
+ const initialLog = JSON.parse(await fs.readFile(localBuildlogPath, 'utf-8'));
595
+ const relPath = relative(testContext.testDir, templatePath);
596
+ const initialHash = initialLog.templates[relPath].lastAppliedHash;
597
+ const initialDate = initialLog.templates[relPath].lastAppliedDate;
598
+ // Wait a bit to ensure timestamp would be different
599
+ await new Promise(resolve => setTimeout(resolve, 100));
600
+ // Apply again without changes
601
+ await manager.processTemplates({ apply: true });
602
+ const updatedLog = JSON.parse(await fs.readFile(localBuildlogPath, 'utf-8'));
603
+ // Hash and date should remain exactly the same since no changes were made
604
+ expect(updatedLog.templates[relPath].lastAppliedHash).toBe(initialHash);
605
+ expect(updatedLog.templates[relPath].lastAppliedDate).toBe(initialDate);
606
+ });
607
+ it('should not reapply unchanged templates in watch mode', async () => {
608
+ // Create multiple templates
609
+ const templates = await Promise.all([
610
+ createTemplateWithFunc(`watch-stable-1-${testContext.timestamp}.sql`, '_watch_1'),
611
+ createTemplateWithFunc(`watch-stable-2-${testContext.timestamp}.sql`, '_watch_2'),
612
+ ]);
613
+ const manager = await TemplateManager.create(testContext.testDir);
614
+ const applied = [];
615
+ const changed = [];
616
+ manager.on('templateChanged', template => {
617
+ changed.push(template.name);
618
+ });
619
+ manager.on('templateApplied', template => {
620
+ applied.push(template.name);
621
+ });
622
+ // First watch session
623
+ const watcher1 = await manager.watch();
624
+ await new Promise(resolve => setTimeout(resolve, 100));
625
+ await watcher1.close();
626
+ // Record initial state
627
+ const initialApplied = [...applied];
628
+ const initialChanged = [...changed];
629
+ applied.length = 0;
630
+ changed.length = 0;
631
+ // Second watch session without any changes
632
+ const watcher2 = await manager.watch();
633
+ await new Promise(resolve => setTimeout(resolve, 100));
634
+ await watcher2.close();
635
+ expect(initialApplied).toHaveLength(2); // First run should apply both
636
+ expect(initialChanged).toHaveLength(2); // First run should detect both
637
+ expect(applied).toHaveLength(0); // Second run should apply none
638
+ expect(changed).toHaveLength(0); // Second run should detect none
639
+ // Verify the buildlog state
640
+ const localBuildlogPath = join(testContext.testDir, '.buildlog-test.local.json');
641
+ const buildLog = JSON.parse(await fs.readFile(localBuildlogPath, 'utf-8'));
642
+ for (const templatePath of templates) {
643
+ const relPath = relative(testContext.testDir, templatePath);
644
+ const content = await fs.readFile(templatePath, 'utf-8');
645
+ const hash = await calculateMD5(content);
646
+ expect(buildLog.templates[relPath].lastAppliedHash).toBe(hash);
647
+ }
648
+ });
649
+ it('should process unapplied templates on startup', async () => {
650
+ // Create template but don't process it
651
+ await createTemplateWithFunc(`startup-test-${testContext.timestamp}.sql`);
652
+ // Create a new manager instance
653
+ const manager = await TemplateManager.create(testContext.testDir);
654
+ const changes = [];
655
+ const applied = [];
656
+ manager.on('templateChanged', t => changes.push(t.name));
657
+ manager.on('templateApplied', t => applied.push(t.name));
658
+ // Start watching - this should process the template
659
+ await manager.watch();
660
+ await new Promise(resolve => setTimeout(resolve, 100));
661
+ expect(changes).toHaveLength(1);
662
+ expect(applied).toHaveLength(1);
663
+ expect(changes[0]).toBe(`startup-test-${testContext.timestamp}`);
664
+ expect(applied[0]).toBe(`startup-test-${testContext.timestamp}`);
665
+ });
666
+ it('should handle error state transitions correctly', async () => {
667
+ const templatePath = await createTemplateWithFunc(`error-state-${testContext.timestamp}.sql`, '_error_test');
668
+ const manager = await TemplateManager.create(testContext.testDir);
669
+ const states = [];
670
+ manager.on('templateChanged', () => states.push({ type: 'changed' }));
671
+ manager.on('templateApplied', () => states.push({ type: 'applied' }));
672
+ manager.on('templateError', ({ error }) => states.push({ type: 'error', error: String(error) }));
673
+ // First apply should succeed
674
+ await manager.processTemplates({ apply: true });
675
+ // Modify template to be invalid
676
+ await fs.writeFile(templatePath, 'INVALID SQL;');
677
+ await manager.processTemplates({ apply: true });
678
+ // Fix template with valid SQL
679
+ await fs.writeFile(templatePath, `CREATE OR REPLACE FUNCTION ${testContext.testFunctionName}() RETURNS void AS $$ BEGIN NULL; END; $$ LANGUAGE plpgsql;`);
680
+ await manager.processTemplates({ apply: true });
681
+ expect(states).toEqual([
682
+ { type: 'changed' },
683
+ { type: 'applied' },
684
+ { type: 'changed' },
685
+ { type: 'error', error: expect.stringMatching(/syntax error/) },
686
+ { type: 'changed' },
687
+ { type: 'applied' },
688
+ ]);
689
+ });
690
+ it('should maintain correct state through manager restarts', async () => {
691
+ const templatePath = await createTemplateWithFunc(`restart-test-${testContext.timestamp}.sql`);
692
+ // First manager instance
693
+ const manager1 = await TemplateManager.create(testContext.testDir);
694
+ await manager1.processTemplates({ apply: true });
695
+ // Get initial state
696
+ const status1 = await manager1.getTemplateStatus(templatePath);
697
+ const initialHash = status1.buildState.lastAppliedHash;
698
+ // Modify template
699
+ await fs.writeFile(templatePath, `${await fs.readFile(templatePath, 'utf-8')}\n-- Modified`);
700
+ // Create new manager instance
701
+ const manager2 = await TemplateManager.create(testContext.testDir);
702
+ const changes = [];
703
+ const applied = [];
704
+ manager2.on('templateChanged', t => changes.push(t.name));
705
+ manager2.on('templateApplied', t => applied.push(t.name));
706
+ await manager2.watch();
707
+ await new Promise(resolve => setTimeout(resolve, 100));
708
+ // Verify state was maintained and change was detected
709
+ const status2 = await manager2.getTemplateStatus(templatePath);
710
+ expect(status2.buildState.lastAppliedHash).not.toBe(initialHash);
711
+ expect(changes).toContain(`restart-test-${testContext.timestamp}`);
712
+ expect(applied).toContain(`restart-test-${testContext.timestamp}`);
713
+ });
714
+ it('should properly format and propagate error messages', async () => {
715
+ const templatePath = await createTemplateWithFunc(`error-format-${testContext.timestamp}.sql`);
716
+ const manager = await TemplateManager.create(testContext.testDir);
717
+ const errors = [];
718
+ manager.on('templateError', err => errors.push(err));
719
+ // Create invalid SQL
720
+ await fs.writeFile(templatePath, 'SELECT * FROM nonexistent_table;');
721
+ await manager.processTemplates({ apply: true });
722
+ expect(errors).toHaveLength(1);
723
+ const error = errors[0]?.error;
724
+ expect(typeof error).toBe('string');
725
+ expect(error).not.toMatch(/\[object Object\]/);
726
+ expect(error).toMatch(/relation.*does not exist/i);
727
+ });
728
+ });
729
+ //# sourceMappingURL=templateManager.test.js.map