@elench/testkit 0.1.114 → 0.1.116

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 (212) hide show
  1. package/README.md +33 -8
  2. package/lib/cli/args.mjs +3 -3
  3. package/lib/cli/assistant/app.mjs +4 -2
  4. package/lib/cli/assistant/session.mjs +5 -1
  5. package/lib/cli/assistant/state.mjs +1 -2
  6. package/lib/cli/command-flags.mjs +4 -0
  7. package/lib/cli/commands/db/schema/refresh.mjs +21 -0
  8. package/lib/cli/commands/db/schema/verify.mjs +27 -0
  9. package/lib/cli/components/blocks/run-tree.mjs +7 -2
  10. package/lib/cli/components/hooks/use-element-layout.mjs +63 -0
  11. package/lib/cli/components/hooks/use-spinner-frame.mjs +26 -0
  12. package/lib/cli/entrypoint.mjs +1 -0
  13. package/lib/cli/operations/db/schema/refresh/operation.mjs +56 -0
  14. package/lib/cli/operations/db/{snapshot/capture → schema/verify}/operation.mjs +6 -27
  15. package/lib/cli/operations/run/operation.mjs +1 -0
  16. package/lib/cli/renderers/db-schema/text.mjs +7 -0
  17. package/lib/config/database.mjs +64 -0
  18. package/lib/config-api/index.d.ts +16 -1
  19. package/lib/config-api/index.mjs +31 -16
  20. package/lib/database/fingerprint.mjs +2 -0
  21. package/lib/database/index.mjs +142 -104
  22. package/lib/database/schema-source.mjs +295 -0
  23. package/lib/database/template-steps.mjs +158 -38
  24. package/lib/runner/orchestrator.mjs +4 -3
  25. package/lib/runner/template-steps.mjs +12 -1
  26. package/lib/runner/template.mjs +16 -1
  27. package/node_modules/@alcalzone/ansi-tokenize/README.md +0 -5
  28. package/node_modules/@alcalzone/ansi-tokenize/build/ansiCodes.d.ts +8 -0
  29. package/node_modules/@alcalzone/ansi-tokenize/build/ansiCodes.js +10 -8
  30. package/node_modules/@alcalzone/ansi-tokenize/build/ansiCodes.js.map +1 -1
  31. package/node_modules/@alcalzone/ansi-tokenize/build/tokenize.d.ts +1 -5
  32. package/node_modules/@alcalzone/ansi-tokenize/build/tokenize.js +9 -45
  33. package/node_modules/@alcalzone/ansi-tokenize/build/tokenize.js.map +1 -1
  34. package/node_modules/@alcalzone/ansi-tokenize/package.json +1 -1
  35. package/node_modules/@elench/next-analysis/package.json +1 -1
  36. package/node_modules/@elench/testkit-bridge/package.json +2 -2
  37. package/node_modules/@elench/testkit-protocol/package.json +1 -1
  38. package/node_modules/@elench/ts-analysis/package.json +1 -1
  39. package/node_modules/cli-boxes/index.d.ts +95 -90
  40. package/node_modules/cli-boxes/index.js +5 -2
  41. package/node_modules/cli-boxes/package.json +6 -13
  42. package/node_modules/cli-boxes/readme.md +15 -3
  43. package/node_modules/cli-truncate/index.d.ts +1 -1
  44. package/node_modules/cli-truncate/package.json +4 -4
  45. package/node_modules/cli-truncate/readme.md +1 -0
  46. package/node_modules/es-toolkit/CHANGELOG.md +801 -0
  47. package/node_modules/es-toolkit/src/compat/_internal/Equals.d.ts +1 -0
  48. package/node_modules/es-toolkit/src/compat/_internal/IsWritable.d.ts +3 -0
  49. package/node_modules/es-toolkit/src/compat/_internal/MutableList.d.ts +4 -0
  50. package/node_modules/es-toolkit/src/compat/_internal/RejectReadonly.d.ts +4 -0
  51. package/node_modules/esprima/ChangeLog +235 -0
  52. package/node_modules/ink/build/apply-styles.js +175 -0
  53. package/node_modules/ink/build/build-layout.js +77 -0
  54. package/node_modules/ink/build/calculate-wrapped-text.js +53 -0
  55. package/node_modules/ink/build/components/App.d.ts +1 -4
  56. package/node_modules/ink/build/components/App.js +22 -142
  57. package/node_modules/ink/build/components/App.js.map +1 -1
  58. package/node_modules/ink/build/components/AppContext.d.ts +3 -23
  59. package/node_modules/ink/build/components/AppContext.js +4 -7
  60. package/node_modules/ink/build/components/AppContext.js.map +1 -1
  61. package/node_modules/ink/build/components/Box.d.ts +3 -16
  62. package/node_modules/ink/build/components/Color.js +62 -0
  63. package/node_modules/ink/build/components/Cursor.d.ts +83 -0
  64. package/node_modules/ink/build/components/Cursor.js +53 -0
  65. package/node_modules/ink/build/components/Cursor.js.map +1 -0
  66. package/node_modules/ink/build/components/ErrorBoundary.d.ts +2 -2
  67. package/node_modules/ink/build/components/ErrorOverview.js +6 -6
  68. package/node_modules/ink/build/components/ErrorOverview.js.map +1 -1
  69. package/node_modules/ink/build/components/Static.js.map +1 -1
  70. package/node_modules/ink/build/components/StdinContext.d.ts +1 -7
  71. package/node_modules/ink/build/components/StdinContext.js +0 -1
  72. package/node_modules/ink/build/components/StdinContext.js.map +1 -1
  73. package/node_modules/ink/build/components/Text.d.ts +1 -1
  74. package/node_modules/ink/build/components/Text.js +1 -1
  75. package/node_modules/ink/build/components/Text.js.map +1 -1
  76. package/node_modules/ink/build/components/Transform.d.ts +1 -1
  77. package/node_modules/ink/build/devtools-window-polyfill.js +4 -7
  78. package/node_modules/ink/build/devtools-window-polyfill.js.map +1 -1
  79. package/node_modules/ink/build/devtools.js +6 -31
  80. package/node_modules/ink/build/devtools.js.map +1 -1
  81. package/node_modules/ink/build/dom.d.ts +1 -5
  82. package/node_modules/ink/build/dom.js +1 -20
  83. package/node_modules/ink/build/dom.js.map +1 -1
  84. package/node_modules/ink/build/experimental/apply-style.js +140 -0
  85. package/node_modules/ink/build/experimental/dom.js +123 -0
  86. package/node_modules/ink/build/experimental/output.js +91 -0
  87. package/node_modules/ink/build/experimental/reconciler.js +141 -0
  88. package/node_modules/ink/build/experimental/renderer.js +81 -0
  89. package/node_modules/ink/build/hooks/use-app.d.ts +1 -1
  90. package/node_modules/ink/build/hooks/use-app.js +1 -1
  91. package/node_modules/ink/build/hooks/use-cursor.d.ts +1 -1
  92. package/node_modules/ink/build/hooks/use-cursor.js +1 -1
  93. package/node_modules/ink/build/hooks/use-focus-manager.d.ts +2 -17
  94. package/node_modules/ink/build/hooks/use-focus-manager.js +1 -2
  95. package/node_modules/ink/build/hooks/use-focus-manager.js.map +1 -1
  96. package/node_modules/ink/build/hooks/use-focus.d.ts +1 -2
  97. package/node_modules/ink/build/hooks/use-focus.js +4 -5
  98. package/node_modules/ink/build/hooks/use-focus.js.map +1 -1
  99. package/node_modules/ink/build/hooks/use-input.d.ts +1 -2
  100. package/node_modules/ink/build/hooks/use-input.js +80 -82
  101. package/node_modules/ink/build/hooks/use-input.js.map +1 -1
  102. package/node_modules/ink/build/hooks/use-is-screen-reader-enabled.d.ts +1 -2
  103. package/node_modules/ink/build/hooks/use-is-screen-reader-enabled.js +1 -2
  104. package/node_modules/ink/build/hooks/use-is-screen-reader-enabled.js.map +1 -1
  105. package/node_modules/ink/build/hooks/use-stderr.d.ts +1 -1
  106. package/node_modules/ink/build/hooks/use-stderr.js +1 -1
  107. package/node_modules/ink/build/hooks/use-stdin.d.ts +2 -4
  108. package/node_modules/ink/build/hooks/use-stdin.js +1 -2
  109. package/node_modules/ink/build/hooks/use-stdin.js.map +1 -1
  110. package/node_modules/ink/build/hooks/use-stdout.d.ts +1 -1
  111. package/node_modules/ink/build/hooks/use-stdout.js +1 -1
  112. package/node_modules/ink/build/hooks/useInput.js +38 -0
  113. package/node_modules/ink/build/index.d.ts +1 -8
  114. package/node_modules/ink/build/index.js +0 -4
  115. package/node_modules/ink/build/index.js.map +1 -1
  116. package/node_modules/ink/build/ink.d.ts +3 -48
  117. package/node_modules/ink/build/ink.js +155 -325
  118. package/node_modules/ink/build/ink.js.map +1 -1
  119. package/node_modules/ink/build/input-parser.d.ts +1 -4
  120. package/node_modules/ink/build/input-parser.js +30 -70
  121. package/node_modules/ink/build/input-parser.js.map +1 -1
  122. package/node_modules/ink/build/instance.js +205 -0
  123. package/node_modules/ink/build/layout.d.ts +7 -0
  124. package/node_modules/ink/build/layout.js +33 -0
  125. package/node_modules/ink/build/layout.js.map +1 -0
  126. package/node_modules/ink/build/log-update.d.ts +0 -1
  127. package/node_modules/ink/build/log-update.js +1 -13
  128. package/node_modules/ink/build/log-update.js.map +1 -1
  129. package/node_modules/ink/build/measure-element.d.ts +0 -4
  130. package/node_modules/ink/build/measure-element.js +0 -4
  131. package/node_modules/ink/build/measure-element.js.map +1 -1
  132. package/node_modules/ink/build/options.d.ts +52 -0
  133. package/node_modules/ink/build/options.js +2 -0
  134. package/node_modules/ink/build/options.js.map +1 -0
  135. package/node_modules/ink/build/output.js +0 -25
  136. package/node_modules/ink/build/output.js.map +1 -1
  137. package/node_modules/ink/build/parse-keypress.d.ts +3 -1
  138. package/node_modules/ink/build/parse-keypress.js +17 -19
  139. package/node_modules/ink/build/parse-keypress.js.map +1 -1
  140. package/node_modules/ink/build/reconciler.js +27 -46
  141. package/node_modules/ink/build/reconciler.js.map +1 -1
  142. package/node_modules/ink/build/render-border.js +18 -29
  143. package/node_modules/ink/build/render-border.js.map +1 -1
  144. package/node_modules/ink/build/render-to-string.js +1 -2
  145. package/node_modules/ink/build/render-to-string.js.map +1 -1
  146. package/node_modules/ink/build/render.d.ts +2 -57
  147. package/node_modules/ink/build/render.js +11 -18
  148. package/node_modules/ink/build/render.js.map +1 -1
  149. package/node_modules/ink/build/screen-reader-update.d.ts +13 -0
  150. package/node_modules/ink/build/screen-reader-update.js +38 -0
  151. package/node_modules/ink/build/screen-reader-update.js.map +1 -0
  152. package/node_modules/ink/build/styles.d.ts +16 -78
  153. package/node_modules/ink/build/styles.js +31 -102
  154. package/node_modules/ink/build/styles.js.map +1 -1
  155. package/node_modules/ink/build/utils.d.ts +2 -9
  156. package/node_modules/ink/build/utils.js +3 -18
  157. package/node_modules/ink/build/utils.js.map +1 -1
  158. package/node_modules/ink/build/wrap-text.js +0 -7
  159. package/node_modules/ink/build/wrap-text.js.map +1 -1
  160. package/node_modules/ink/build/write-synchronized.d.ts +1 -1
  161. package/node_modules/ink/build/write-synchronized.js +2 -4
  162. package/node_modules/ink/build/write-synchronized.js.map +1 -1
  163. package/node_modules/ink/node_modules/emoji-regex/LICENSE-MIT.txt +20 -0
  164. package/node_modules/ink/node_modules/emoji-regex/README.md +107 -0
  165. package/node_modules/ink/node_modules/emoji-regex/index.d.ts +3 -0
  166. package/node_modules/ink/node_modules/emoji-regex/index.js +4 -0
  167. package/node_modules/ink/node_modules/emoji-regex/index.mjs +4 -0
  168. package/node_modules/ink/node_modules/emoji-regex/package.json +45 -0
  169. package/node_modules/{wrap-ansi → ink/node_modules/wrap-ansi}/index.d.ts +1 -1
  170. package/node_modules/ink/node_modules/wrap-ansi/index.js +222 -0
  171. package/node_modules/ink/node_modules/wrap-ansi/node_modules/string-width/index.d.ts +39 -0
  172. package/node_modules/ink/node_modules/wrap-ansi/node_modules/string-width/index.js +82 -0
  173. package/node_modules/ink/node_modules/wrap-ansi/node_modules/string-width/license +9 -0
  174. package/node_modules/ink/node_modules/wrap-ansi/node_modules/string-width/package.json +64 -0
  175. package/node_modules/ink/node_modules/wrap-ansi/node_modules/string-width/readme.md +66 -0
  176. package/node_modules/{wrap-ansi → ink/node_modules/wrap-ansi}/package.json +11 -11
  177. package/node_modules/{wrap-ansi → ink/node_modules/wrap-ansi}/readme.md +0 -2
  178. package/node_modules/ink/package.json +98 -34
  179. package/node_modules/ink/readme.md +48 -554
  180. package/node_modules/slice-ansi/index.d.ts +1 -1
  181. package/node_modules/slice-ansi/index.js +89 -146
  182. package/node_modules/slice-ansi/package.json +5 -5
  183. package/node_modules/slice-ansi/readme.md +0 -1
  184. package/node_modules/slice-ansi/tokenize-ansi.js +1 -1
  185. package/package.json +14 -10
  186. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.d.ts +188 -0
  187. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.d.ts.map +1 -0
  188. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.js +293 -0
  189. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.js.map +1 -0
  190. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/package.json +25 -0
  191. package/lib/cli/commands/db/snapshot/capture.mjs +0 -26
  192. package/lib/cli/renderers/db-snapshot-capture/text.mjs +0 -3
  193. package/node_modules/@alcalzone/ansi-tokenize/build/consts.d.ts +0 -17
  194. package/node_modules/@alcalzone/ansi-tokenize/build/consts.js +0 -28
  195. package/node_modules/@alcalzone/ansi-tokenize/build/consts.js.map +0 -1
  196. package/node_modules/ink/build/components/AnimationContext.d.ts +0 -9
  197. package/node_modules/ink/build/components/AnimationContext.js +0 -13
  198. package/node_modules/ink/build/components/AnimationContext.js.map +0 -1
  199. package/node_modules/ink/build/hooks/use-animation.d.ts +0 -49
  200. package/node_modules/ink/build/hooks/use-animation.js +0 -87
  201. package/node_modules/ink/build/hooks/use-animation.js.map +0 -1
  202. package/node_modules/ink/build/hooks/use-box-metrics.d.ts +0 -59
  203. package/node_modules/ink/build/hooks/use-box-metrics.js +0 -88
  204. package/node_modules/ink/build/hooks/use-box-metrics.js.map +0 -1
  205. package/node_modules/ink/build/hooks/use-paste.d.ts +0 -35
  206. package/node_modules/ink/build/hooks/use-paste.js +0 -62
  207. package/node_modules/ink/build/hooks/use-paste.js.map +0 -1
  208. package/node_modules/ink/build/hooks/use-window-size.d.ts +0 -18
  209. package/node_modules/ink/build/hooks/use-window-size.js +0 -22
  210. package/node_modules/ink/build/hooks/use-window-size.js.map +0 -1
  211. package/node_modules/wrap-ansi/index.js +0 -468
  212. /package/node_modules/{wrap-ansi → ink/node_modules/wrap-ansi}/license +0 -0
@@ -34,19 +34,36 @@ function postgresDatabase(options = {}) {
34
34
  }
35
35
 
36
36
  function buildDatabaseTemplateConfig(options = {}) {
37
+ for (const legacyKey of ["schema"]) {
38
+ if (Object.prototype.hasOwnProperty.call(options, legacyKey)) {
39
+ throw new Error(
40
+ `database.postgres({ template }) no longer accepts "${legacyKey}". Use database.postgres({ sourceSchema: database.schema.fromEnv(...) }) instead.`
41
+ );
42
+ }
43
+ }
37
44
  const migrate = normalizeTemplateStepList(options.migrate);
38
45
  const seed = normalizeTemplateStepList(options.seed);
39
46
  const verify = normalizeTemplateStepList(options.verify);
40
- const schema = normalizeSchemaStep(options.schema);
41
47
 
42
48
  return {
43
49
  inputs: Array.isArray(options.inputs) ? [...options.inputs] : undefined,
44
- migrate: schema ? [schema, ...migrate] : migrate,
50
+ migrate,
45
51
  seed,
46
52
  verify,
47
53
  };
48
54
  }
49
55
 
56
+ function sourceSchemaFromEnv(envName, options = {}) {
57
+ if (typeof envName !== "string" || envName.trim().length === 0) {
58
+ throw new Error("database.schema.fromEnv(...) requires a non-empty env var name");
59
+ }
60
+ return {
61
+ kind: "env",
62
+ env: envName.trim(),
63
+ ...options,
64
+ };
65
+ }
66
+
50
67
  function postgresFixture(options = {}) {
51
68
  const { discovery, envFiles, template, ...databaseOptions } = options;
52
69
  for (const legacyKey of ["inputs", "schema", "migrate", "seed", "verify"]) {
@@ -241,8 +258,11 @@ export const app = {
241
258
  };
242
259
 
243
260
  export const database = {
261
+ schema: {
262
+ fromEnv: sourceSchemaFromEnv,
263
+ },
244
264
  postgres(options = {}) {
245
- const { template, ...databaseOptions } = options;
265
+ const { template, sourceSchema, ...databaseOptions } = options;
246
266
  for (const legacyKey of ["inputs", "schema", "migrate", "seed", "verify"]) {
247
267
  if (Object.prototype.hasOwnProperty.call(options, legacyKey)) {
248
268
  throw new Error(
@@ -250,13 +270,19 @@ export const database = {
250
270
  );
251
271
  }
252
272
  }
273
+ const normalizedDatabaseOptions = sourceSchema === undefined
274
+ ? databaseOptions
275
+ : {
276
+ ...databaseOptions,
277
+ sourceSchema,
278
+ };
253
279
  return postgresDatabase(
254
280
  template
255
281
  ? {
256
- ...databaseOptions,
282
+ ...normalizedDatabaseOptions,
257
283
  template: buildDatabaseTemplateConfig(template),
258
284
  }
259
- : databaseOptions
285
+ : normalizedDatabaseOptions
260
286
  );
261
287
  },
262
288
  fixture: postgresFixture,
@@ -365,17 +391,6 @@ function normalizeTemplateStepList(value) {
365
391
  return Array.isArray(value) ? [...value] : [value];
366
392
  }
367
393
 
368
- function normalizeSchemaStep(value) {
369
- if (value == null) return null;
370
- if (typeof value === "string") {
371
- return {
372
- kind: "sql-file",
373
- path: value,
374
- };
375
- }
376
- return value;
377
- }
378
-
379
394
  function compiledEntryFromSource(entry, outDir) {
380
395
  const normalized = String(entry || "src/index.ts").replaceAll("\\", "/");
381
396
  const compiled = normalized.replace(/\.[cm]?[jt]sx?$/i, ".js");
@@ -3,6 +3,7 @@ import fs from "fs";
3
3
  import path from "path";
4
4
  import { resolveServiceCwd } from "../config/paths.mjs";
5
5
  import { collectTemplateInputs } from "./template-steps.mjs";
6
+ import { appendSourceSchemaCacheToHash } from "./schema-source.mjs";
6
7
 
7
8
  const LOCAL_IMAGE = "pgvector/pgvector:pg16";
8
9
  const LOCAL_USER = "testkit";
@@ -24,6 +25,7 @@ export async function computeTemplateFingerprint(config) {
24
25
  for (const input of collectTemplateInputs(config.productDir, db.template || {})) {
25
26
  appendResolvedInputToHash(hash, config.productDir, input);
26
27
  }
28
+ appendSourceSchemaCacheToHash(hash, config);
27
29
 
28
30
  return hash.digest("hex");
29
31
  }
@@ -25,7 +25,17 @@ import {
25
25
  readStateValue as readStateValueModel,
26
26
  visitDirs as visitDirsModel,
27
27
  } from "./state.mjs";
28
- import { captureTemplateSnapshot, runTemplateStage } from "./template-steps.mjs";
28
+ import {
29
+ runTemplateStage,
30
+ runTemplateStep,
31
+ } from "./template-steps.mjs";
32
+ import {
33
+ applySourceSchemaCache,
34
+ createSourceSchemaMismatchError,
35
+ forceRefreshSourceSchemaCache,
36
+ prepareSourceSchemaCache,
37
+ verifyLocalSchemaMatchesSource,
38
+ } from "./schema-source.mjs";
29
39
  import { collectStateDirLines } from "../runner/state-io.mjs";
30
40
 
31
41
  const LOCAL_IMAGE = "pgvector/pgvector:pg16";
@@ -50,60 +60,6 @@ export async function prepareDatabaseRuntime(config, options = {}) {
50
60
  throw new Error(`Unsupported database provider "${db.provider}"`);
51
61
  }
52
62
 
53
- export async function captureDatabaseTemplateSnapshot(config, outputPath, options = {}) {
54
- if (!config.testkit.database || config.testkit.database.provider !== "local") {
55
- throw new Error(`Service "${config.name}" does not use a local testkit database`);
56
- }
57
-
58
- await prepareDatabaseRuntime(config, options);
59
- const cacheDir = getLocalServiceCacheDir(config.productDir, config.name);
60
- const templateDbName = readStateValue(path.join(cacheDir, "template_database_name"));
61
- if (!templateDbName) {
62
- throw new Error(`Missing template database for service "${config.name}"`);
63
- }
64
-
65
- const infra = await loadExistingLocalContainer(config.productDir);
66
- if (!infra) {
67
- throw new Error(`Missing local database container for service "${config.name}"`);
68
- }
69
-
70
- const snapshotOperation = options.setupRegistry?.start({
71
- config,
72
- stage: "template:snapshot",
73
- kind: "database-snapshot",
74
- summary: `snapshot: ${path.relative(config.productDir, outputPath)}`,
75
- });
76
- try {
77
- const output = await captureTemplateSnapshot(
78
- config,
79
- outputPath,
80
- buildDatabaseUrl(infra, templateDbName),
81
- {
82
- reporter: options.reporter || null,
83
- logRecord: snapshotOperation?._logRecord || null,
84
- }
85
- );
86
- const finished = snapshotOperation
87
- ? options.setupRegistry.finish(snapshotOperation, {
88
- status: "passed",
89
- summary: `snapshot: ${path.relative(config.productDir, outputPath)}`,
90
- })
91
- : null;
92
- if (finished) options.reporter?.setupOperationFinished?.(finished);
93
- return output;
94
- } catch (error) {
95
- const finished = snapshotOperation
96
- ? options.setupRegistry.finish(snapshotOperation, {
97
- status: "failed",
98
- summary: `snapshot: ${path.relative(config.productDir, outputPath)}`,
99
- error: error?.message || error,
100
- })
101
- : null;
102
- if (finished) options.reporter?.setupOperationFinished?.(finished);
103
- throw error;
104
- }
105
- }
106
-
107
63
  export async function destroyRuntimeDatabase({ productDir, stateDir }) {
108
64
  const backend = readStateValue(path.join(stateDir, "database_backend"));
109
65
  if (backend === "local") {
@@ -184,13 +140,24 @@ async function prepareLocalDatabase(config, options = {}) {
184
140
  fs.mkdirSync(lockDir, { recursive: true });
185
141
  fs.mkdirSync(cacheDir, { recursive: true });
186
142
 
187
- const templateFingerprint = await computeTemplateFingerprint(config);
143
+ let templateFingerprint = null;
188
144
  const infra = await withLock(path.join(lockDir, "container.lock"), () =>
189
145
  ensureLocalContainer(productDir, db)
190
146
  );
191
147
 
192
148
  await withLock(path.join(lockDir, `template-${serviceName}.lock`), async () => {
193
- await ensureTemplateDatabase(config, infra, cacheDir, templateFingerprint, options);
149
+ const sourceSchemaState = await prepareSourceSchemaCache(config, options);
150
+ templateFingerprint = await computeTemplateFingerprint(config);
151
+ templateFingerprint = await ensureTemplateDatabase(
152
+ config,
153
+ infra,
154
+ cacheDir,
155
+ templateFingerprint,
156
+ {
157
+ ...options,
158
+ sourceSchemaState,
159
+ }
160
+ );
194
161
  });
195
162
 
196
163
  await withLock(path.join(lockDir, `runtime-${serviceName}-${hashString(bindingKey, 10)}.lock`), async () => {
@@ -200,34 +167,58 @@ async function prepareLocalDatabase(config, options = {}) {
200
167
 
201
168
  async function ensureTemplateDatabase(config, infra, cacheDir, templateFingerprint, options = {}) {
202
169
  const serviceName = config.name;
203
- const existingFingerprint = readStateValue(path.join(cacheDir, "template_fingerprint"));
204
- const existingDbName = readStateValue(path.join(cacheDir, "template_database_name"));
205
- const desiredDbName = buildTemplateDatabaseName(serviceName, templateFingerprint);
206
-
207
- if (
208
- existingFingerprint === templateFingerprint &&
209
- existingDbName &&
210
- (await databaseExists(infra, existingDbName))
211
- ) {
212
- options.setupRegistry?.recordCached({
213
- config,
214
- stage: "template",
215
- kind: "database-template",
216
- summary: "template cache hit",
170
+ let activeFingerprint = templateFingerprint;
171
+ let sourceSchemaState = options.sourceSchemaState || null;
172
+ let refreshedSourceAfterMismatch = false;
173
+
174
+ while (true) {
175
+ const existingFingerprint = readStateValue(path.join(cacheDir, "template_fingerprint"));
176
+ const existingDbName = readStateValue(path.join(cacheDir, "template_database_name"));
177
+ const desiredDbName = buildTemplateDatabaseName(serviceName, activeFingerprint);
178
+
179
+ if (
180
+ existingFingerprint === activeFingerprint &&
181
+ existingDbName &&
182
+ (await databaseExists(infra, existingDbName))
183
+ ) {
184
+ options.setupRegistry?.recordCached({
185
+ config,
186
+ stage: "template",
187
+ kind: "database-template",
188
+ summary: "template cache hit",
189
+ });
190
+ writeLocalCacheState(cacheDir, infra, existingDbName, activeFingerprint);
191
+ return activeFingerprint;
192
+ }
193
+
194
+ if (existingDbName && existingDbName !== desiredDbName) {
195
+ await dropDatabaseIfExists(infra, existingDbName);
196
+ }
197
+ if (await databaseExists(infra, desiredDbName)) {
198
+ await dropDatabaseIfExists(infra, desiredDbName);
199
+ }
200
+
201
+ const templateUrl = buildDatabaseUrl(infra, desiredDbName);
202
+ await createEmptyDatabase(infra, desiredDbName);
203
+ const buildResult = await rebuildTemplateDatabase(config, infra, desiredDbName, templateUrl, {
204
+ ...options,
205
+ sourceSchemaState,
206
+ refreshedSourceAfterMismatch,
217
207
  });
218
- writeLocalCacheState(cacheDir, infra, existingDbName, templateFingerprint);
219
- return;
220
- }
208
+ if (buildResult.refreshSourceSchema) {
209
+ await dropDatabaseIfExists(infra, desiredDbName);
210
+ sourceSchemaState = await forceRefreshSourceSchemaCache(config, sourceSchemaState, options);
211
+ refreshedSourceAfterMismatch = true;
212
+ activeFingerprint = await computeTemplateFingerprint(config);
213
+ continue;
214
+ }
221
215
 
222
- if (existingDbName && existingDbName !== desiredDbName) {
223
- await dropDatabaseIfExists(infra, existingDbName);
224
- }
225
- if (await databaseExists(infra, desiredDbName)) {
226
- await dropDatabaseIfExists(infra, desiredDbName);
216
+ writeLocalCacheState(cacheDir, infra, desiredDbName, activeFingerprint);
217
+ return activeFingerprint;
227
218
  }
219
+ }
228
220
 
229
- const templateUrl = buildDatabaseUrl(infra, desiredDbName);
230
- await createEmptyDatabase(infra, desiredDbName);
221
+ async function rebuildTemplateDatabase(config, infra, templateDbName, templateUrl, options = {}) {
231
222
  const templateOperation = options.setupRegistry?.start({
232
223
  config,
233
224
  stage: "template",
@@ -236,42 +227,89 @@ async function ensureTemplateDatabase(config, infra, cacheDir, templateFingerpri
236
227
  recordLog: false,
237
228
  });
238
229
  try {
239
- await runTemplateStage(config, "migrate", templateUrl, {
240
- reporter: options.reporter || null,
241
- setupRegistry: options.setupRegistry || null,
242
- parentOperation: templateOperation,
243
- });
244
- await runTemplateStage(config, "seed", templateUrl, {
230
+ await applySourceSchemaCache(config, templateUrl, options.sourceSchemaState, {
245
231
  reporter: options.reporter || null,
246
232
  setupRegistry: options.setupRegistry || null,
247
233
  parentOperation: templateOperation,
248
234
  });
235
+
236
+ const verifySchema = async () => {
237
+ const verification = await verifyLocalSchemaMatchesSource(
238
+ config,
239
+ templateUrl,
240
+ options.sourceSchemaState,
241
+ {
242
+ reporter: options.reporter || null,
243
+ setupRegistry: options.setupRegistry || null,
244
+ skipSchemaSourceVerify: options.skipSchemaSourceVerify,
245
+ }
246
+ );
247
+ if (verification.status !== "mismatch") return null;
248
+ if (options.refreshedSourceAfterMismatch) {
249
+ throw createSourceSchemaMismatchError(config, options.sourceSchemaState, verification);
250
+ }
251
+ return verification;
252
+ };
253
+
254
+ const runStageWithDriftChecks = async (stageName) => {
255
+ const steps = config.testkit.database?.template?.[stageName] || [];
256
+ for (const [index, step] of steps.entries()) {
257
+ await runTemplateStep(config, stageName, step, index, templateUrl, {
258
+ reporter: options.reporter || null,
259
+ setupRegistry: options.setupRegistry || null,
260
+ parentOperation: templateOperation,
261
+ });
262
+ if (await verifySchema()) return true;
263
+ }
264
+ return false;
265
+ };
266
+
267
+ if (await runStageWithDriftChecks("migrate")) {
268
+ finishTemplateOperation(templateOperation, options, {
269
+ status: "passed",
270
+ summary: "source schema refresh requested",
271
+ });
272
+ return { refreshSourceSchema: true };
273
+ }
274
+ if (await runStageWithDriftChecks("seed")) {
275
+ finishTemplateOperation(templateOperation, options, {
276
+ status: "passed",
277
+ summary: "source schema refresh requested",
278
+ });
279
+ return { refreshSourceSchema: true };
280
+ }
281
+ if (await verifySchema()) {
282
+ finishTemplateOperation(templateOperation, options, {
283
+ status: "passed",
284
+ summary: "source schema refresh requested",
285
+ });
286
+ return { refreshSourceSchema: true };
287
+ }
288
+
249
289
  await runTemplateStage(config, "verify", templateUrl, {
250
290
  reporter: options.reporter || null,
251
291
  setupRegistry: options.setupRegistry || null,
252
292
  parentOperation: templateOperation,
253
293
  });
254
- const finished = templateOperation
255
- ? options.setupRegistry.finish(templateOperation, {
256
- status: "passed",
257
- summary: "template rebuild",
258
- })
259
- : null;
260
- if (finished) options.reporter?.setupOperationFinished?.(finished);
294
+ finishTemplateOperation(templateOperation, options, {
295
+ status: "passed",
296
+ summary: "template rebuild",
297
+ });
298
+ return { refreshSourceSchema: false };
261
299
  } catch (error) {
262
- const finished = templateOperation
263
- ? options.setupRegistry.finish(templateOperation, {
264
- status: "failed",
265
- summary: "template rebuild",
266
- error: error?.message || error,
267
- })
268
- : null;
269
- if (finished) options.reporter?.setupOperationFinished?.(finished);
270
- await dropDatabaseIfExists(infra, desiredDbName);
300
+ finishTemplateOperation(templateOperation, options, {
301
+ status: "failed",
302
+ summary: "template rebuild",
303
+ error: error?.message || error,
304
+ });
305
+ await dropDatabaseIfExists(infra, templateDbName);
271
306
  throw error;
272
307
  }
308
+ }
273
309
 
274
- writeLocalCacheState(cacheDir, infra, desiredDbName, templateFingerprint);
310
+ function finishTemplateOperation(templateOperation, options, result) {
311
+ const finished = templateOperation ? options.setupRegistry.finish(templateOperation, result) : null;
312
+ if (finished) options.reporter?.setupOperationFinished?.(finished);
275
313
  }
276
314
 
277
315
  async function ensureRuntimeClone(config, infra, cacheDir, templateFingerprint, bindingKey) {