@intentius/chant-lexicon-terraform 0.57.0 → 0.59.0

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 (408) hide show
  1. package/README.md +9 -0
  2. package/dist/composites/terraform-adopt-op.d.ts +131 -0
  3. package/dist/composites/terraform-adopt-op.d.ts.map +1 -0
  4. package/dist/composites/terraform-apply-op.d.ts +70 -17
  5. package/dist/composites/terraform-apply-op.d.ts.map +1 -1
  6. package/dist/composites/terraform-watch-op.d.ts +104 -37
  7. package/dist/composites/terraform-watch-op.d.ts.map +1 -1
  8. package/dist/config.d.ts +49 -2
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/describe-resources.d.ts +256 -16
  11. package/dist/describe-resources.d.ts.map +1 -1
  12. package/dist/hcl/descend.d.ts +98 -0
  13. package/dist/hcl/descend.d.ts.map +1 -0
  14. package/dist/hcl/marker.d.ts +130 -0
  15. package/dist/hcl/marker.d.ts.map +1 -0
  16. package/dist/hcl/parse.d.ts +156 -9
  17. package/dist/hcl/parse.d.ts.map +1 -1
  18. package/dist/hcl/references.d.ts +76 -0
  19. package/dist/hcl/references.d.ts.map +1 -0
  20. package/dist/hcl/roots.d.ts +22 -3
  21. package/dist/hcl/roots.d.ts.map +1 -1
  22. package/dist/hcl/suppressions.d.ts +54 -0
  23. package/dist/hcl/suppressions.d.ts.map +1 -0
  24. package/dist/hcl/value.d.ts +77 -0
  25. package/dist/hcl/value.d.ts.map +1 -0
  26. package/dist/index.d.ts +5 -2
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/integrity.json +34 -5
  29. package/dist/lint/audit-catalog.d.ts +3 -2
  30. package/dist/lint/audit-catalog.d.ts.map +1 -1
  31. package/dist/lint/audit-lineage.d.ts +95 -0
  32. package/dist/lint/audit-lineage.d.ts.map +1 -1
  33. package/dist/lint/post-synth/blocks.d.ts +53 -0
  34. package/dist/lint/post-synth/blocks.d.ts.map +1 -0
  35. package/dist/lint/post-synth/fixtures/load.d.ts +37 -0
  36. package/dist/lint/post-synth/fixtures/load.d.ts.map +1 -0
  37. package/dist/lint/post-synth/index.d.ts.map +1 -1
  38. package/dist/lint/post-synth/module-source.d.ts +38 -0
  39. package/dist/lint/post-synth/module-source.d.ts.map +1 -0
  40. package/dist/lint/post-synth/scope.d.ts +65 -0
  41. package/dist/lint/post-synth/scope.d.ts.map +1 -0
  42. package/dist/lint/post-synth/tf001.d.ts +58 -6
  43. package/dist/lint/post-synth/tf001.d.ts.map +1 -1
  44. package/dist/lint/post-synth/tf002.d.ts +32 -0
  45. package/dist/lint/post-synth/tf002.d.ts.map +1 -0
  46. package/dist/lint/post-synth/tf003.d.ts +22 -0
  47. package/dist/lint/post-synth/tf003.d.ts.map +1 -0
  48. package/dist/lint/post-synth/tf004.d.ts +22 -0
  49. package/dist/lint/post-synth/tf004.d.ts.map +1 -0
  50. package/dist/lint/post-synth/tf005.d.ts +28 -0
  51. package/dist/lint/post-synth/tf005.d.ts.map +1 -0
  52. package/dist/lint/post-synth/tf006.d.ts +21 -0
  53. package/dist/lint/post-synth/tf006.d.ts.map +1 -0
  54. package/dist/lint/post-synth/tf007.d.ts +21 -0
  55. package/dist/lint/post-synth/tf007.d.ts.map +1 -0
  56. package/dist/lint/post-synth/tf008.d.ts +22 -0
  57. package/dist/lint/post-synth/tf008.d.ts.map +1 -0
  58. package/dist/lint/post-synth/tf009.d.ts +24 -0
  59. package/dist/lint/post-synth/tf009.d.ts.map +1 -0
  60. package/dist/lint/post-synth/tf010.d.ts +19 -0
  61. package/dist/lint/post-synth/tf010.d.ts.map +1 -0
  62. package/dist/lint/post-synth/tf011.d.ts +18 -0
  63. package/dist/lint/post-synth/tf011.d.ts.map +1 -0
  64. package/dist/lint/post-synth/tf012.d.ts +17 -0
  65. package/dist/lint/post-synth/tf012.d.ts.map +1 -0
  66. package/dist/lint/post-synth/tf013.d.ts +21 -0
  67. package/dist/lint/post-synth/tf013.d.ts.map +1 -0
  68. package/dist/lint/post-synth/tf014.d.ts +25 -0
  69. package/dist/lint/post-synth/tf014.d.ts.map +1 -0
  70. package/dist/lint/post-synth/tf015.d.ts +30 -0
  71. package/dist/lint/post-synth/tf015.d.ts.map +1 -0
  72. package/dist/lint/post-synth/tf016.d.ts +30 -0
  73. package/dist/lint/post-synth/tf016.d.ts.map +1 -0
  74. package/dist/lint/post-synth/tf017.d.ts +19 -0
  75. package/dist/lint/post-synth/tf017.d.ts.map +1 -0
  76. package/dist/lint/post-synth/tf018.d.ts +20 -0
  77. package/dist/lint/post-synth/tf018.d.ts.map +1 -0
  78. package/dist/lint/post-synth/tf019.d.ts +25 -0
  79. package/dist/lint/post-synth/tf019.d.ts.map +1 -0
  80. package/dist/lint/post-synth/tf020.d.ts +38 -0
  81. package/dist/lint/post-synth/tf020.d.ts.map +1 -0
  82. package/dist/lint/post-synth/tf021.d.ts +41 -0
  83. package/dist/lint/post-synth/tf021.d.ts.map +1 -0
  84. package/dist/lint/post-synth/tf022.d.ts +24 -0
  85. package/dist/lint/post-synth/tf022.d.ts.map +1 -0
  86. package/dist/lint/post-synth/tf024.d.ts +24 -0
  87. package/dist/lint/post-synth/tf024.d.ts.map +1 -0
  88. package/dist/lint/post-synth/tf025.d.ts +29 -0
  89. package/dist/lint/post-synth/tf025.d.ts.map +1 -0
  90. package/dist/lint/post-synth/tf026.d.ts +28 -0
  91. package/dist/lint/post-synth/tf026.d.ts.map +1 -0
  92. package/dist/lint/post-synth/tf027.d.ts +30 -0
  93. package/dist/lint/post-synth/tf027.d.ts.map +1 -0
  94. package/dist/lint/post-synth/tf028.d.ts +33 -0
  95. package/dist/lint/post-synth/tf028.d.ts.map +1 -0
  96. package/dist/lint/rules/fixtures/load.d.ts +13 -0
  97. package/dist/lint/rules/fixtures/load.d.ts.map +1 -0
  98. package/dist/lint/rules/plan-before-apply.d.ts +11 -0
  99. package/dist/lint/rules/plan-before-apply.d.ts.map +1 -1
  100. package/dist/lint/secret-shape.d.ts +68 -0
  101. package/dist/lint/secret-shape.d.ts.map +1 -0
  102. package/dist/live-ownership.d.ts +55 -0
  103. package/dist/live-ownership.d.ts.map +1 -0
  104. package/dist/lsp/option-keys.d.ts.map +1 -1
  105. package/dist/manifest.json +1 -1
  106. package/dist/okf/index.md +26 -0
  107. package/dist/okf/rules/TF002.md +11 -0
  108. package/dist/okf/rules/TF003.md +11 -0
  109. package/dist/okf/rules/TF004.md +11 -0
  110. package/dist/okf/rules/TF005.md +11 -0
  111. package/dist/okf/rules/TF006.md +11 -0
  112. package/dist/okf/rules/TF007.md +11 -0
  113. package/dist/okf/rules/TF008.md +11 -0
  114. package/dist/okf/rules/TF009.md +11 -0
  115. package/dist/okf/rules/TF010.md +11 -0
  116. package/dist/okf/rules/TF011.md +11 -0
  117. package/dist/okf/rules/TF012.md +11 -0
  118. package/dist/okf/rules/TF013.md +11 -0
  119. package/dist/okf/rules/TF014.md +11 -0
  120. package/dist/okf/rules/TF015.md +11 -0
  121. package/dist/okf/rules/TF016.md +11 -0
  122. package/dist/okf/rules/TF017.md +11 -0
  123. package/dist/okf/rules/TF018.md +11 -0
  124. package/dist/okf/rules/TF019.md +11 -0
  125. package/dist/okf/rules/TF020.md +11 -0
  126. package/dist/okf/rules/TF021.md +11 -0
  127. package/dist/okf/rules/TF022.md +11 -0
  128. package/dist/okf/rules/TF024.md +11 -0
  129. package/dist/okf/rules/TF025.md +11 -0
  130. package/dist/okf/rules/TF026.md +11 -0
  131. package/dist/okf/rules/TF027.md +11 -0
  132. package/dist/okf/rules/TF028.md +11 -0
  133. package/dist/op/activities/index.d.ts +13 -6
  134. package/dist/op/activities/index.d.ts.map +1 -1
  135. package/dist/op/activities/live-detect.d.ts +51 -0
  136. package/dist/op/activities/live-detect.d.ts.map +1 -0
  137. package/dist/op/activities/terraform.d.ts +466 -17
  138. package/dist/op/activities/terraform.d.ts.map +1 -1
  139. package/dist/op/activity-contracts.d.ts +148 -0
  140. package/dist/op/activity-contracts.d.ts.map +1 -0
  141. package/dist/op/adoption.d.ts +118 -0
  142. package/dist/op/adoption.d.ts.map +1 -0
  143. package/dist/op/builders.d.ts +48 -6
  144. package/dist/op/builders.d.ts.map +1 -1
  145. package/dist/op/resolve-root-mode.d.ts +59 -0
  146. package/dist/op/resolve-root-mode.d.ts.map +1 -0
  147. package/dist/plugin.d.ts.map +1 -1
  148. package/dist/rules/blocks.ts +109 -0
  149. package/dist/rules/module-source.ts +121 -0
  150. package/dist/rules/plan-before-apply.ts +11 -0
  151. package/dist/rules/scope.ts +107 -0
  152. package/dist/rules/tf001.ts +98 -18
  153. package/dist/rules/tf002.ts +151 -0
  154. package/dist/rules/tf003.ts +71 -0
  155. package/dist/rules/tf004.ts +63 -0
  156. package/dist/rules/tf005.ts +76 -0
  157. package/dist/rules/tf006.ts +53 -0
  158. package/dist/rules/tf007.ts +78 -0
  159. package/dist/rules/tf008.ts +75 -0
  160. package/dist/rules/tf009.ts +59 -0
  161. package/dist/rules/tf010.ts +51 -0
  162. package/dist/rules/tf011.ts +49 -0
  163. package/dist/rules/tf012.ts +47 -0
  164. package/dist/rules/tf013.ts +63 -0
  165. package/dist/rules/tf014.ts +72 -0
  166. package/dist/rules/tf015.ts +72 -0
  167. package/dist/rules/tf016.ts +75 -0
  168. package/dist/rules/tf017.ts +50 -0
  169. package/dist/rules/tf018.ts +72 -0
  170. package/dist/rules/tf019.ts +72 -0
  171. package/dist/rules/tf020.ts +123 -0
  172. package/dist/rules/tf021.ts +126 -0
  173. package/dist/rules/tf022.ts +61 -0
  174. package/dist/rules/tf024.ts +70 -0
  175. package/dist/rules/tf025.ts +101 -0
  176. package/dist/rules/tf026.ts +84 -0
  177. package/dist/rules/tf027.ts +74 -0
  178. package/dist/rules/tf028.ts +96 -0
  179. package/dist/skills/chant-terraform.md +1 -1
  180. package/dist/validate.d.ts.map +1 -1
  181. package/package.json +7 -2
  182. package/src/__fixtures__/ACCEPTANCE.md +181 -0
  183. package/src/__fixtures__/live/main.tf +28 -0
  184. package/src/__fixtures__/live-adopt/estate.chdf.hcl +6 -0
  185. package/src/__fixtures__/live-adopt/main.tf +21 -0
  186. package/src/__fixtures__/live-estate/README.md +58 -0
  187. package/src/__fixtures__/live-estate/compute.tf +10 -0
  188. package/src/__fixtures__/live-estate/estate.chdf.hcl +1 -0
  189. package/src/__fixtures__/live-estate/logs.tf +8 -0
  190. package/src/__fixtures__/live-estate/network.tf +24 -0
  191. package/src/__fixtures__/live-estate/rule.tf +11 -0
  192. package/src/__fixtures__/live-estate/unowned.tf +21 -0
  193. package/src/__fixtures__/live-estate/versions.tf +31 -0
  194. package/src/__fixtures__/live-ls.json +98 -0
  195. package/src/__fixtures__/live-plan.json +285 -0
  196. package/src/__fixtures__/live-sidecar/estate.chdf.hcl +3 -0
  197. package/src/__fixtures__/live-sidecar/main.tf +24 -0
  198. package/src/__fixtures__/module-tree/README.md +16 -0
  199. package/src/__fixtures__/module-tree/cycle/main.tf +3 -0
  200. package/src/__fixtures__/module-tree/cycle/modules/a/main.tf +9 -0
  201. package/src/__fixtures__/module-tree/cycle/modules/b/main.tf +9 -0
  202. package/src/__fixtures__/module-tree/missing/main.tf +9 -0
  203. package/src/__fixtures__/module-tree/outside/root/main.tf +9 -0
  204. package/src/__fixtures__/module-tree/root/main.tf +28 -0
  205. package/src/__fixtures__/module-tree/root/modules/cdn/main.tf +15 -0
  206. package/src/__fixtures__/module-tree/root/modules/cdn/modules/bucket/main.tf +11 -0
  207. package/src/__fixtures__/module-tree/shared/main.tf +5 -0
  208. package/src/__fixtures__/references/main.tf +56 -0
  209. package/src/__fixtures__/remote-backend/main.tf +34 -0
  210. package/src/__fixtures__/with-backend/main.tf +7 -0
  211. package/src/codegen/lint-rules-page.test.ts +45 -0
  212. package/src/composites/terraform-adopt-op.acceptance.test.ts +193 -0
  213. package/src/composites/terraform-adopt-op.test.ts +175 -0
  214. package/src/composites/terraform-adopt-op.ts +202 -0
  215. package/src/composites/terraform-apply-op.acceptance.test.ts +188 -5
  216. package/src/composites/terraform-apply-op.test.ts +179 -19
  217. package/src/composites/terraform-apply-op.ts +123 -30
  218. package/src/composites/terraform-watch-op.test.ts +169 -29
  219. package/src/composites/terraform-watch-op.ts +183 -75
  220. package/src/config.ts +32 -3
  221. package/src/describe-resources.live.test.ts +598 -0
  222. package/src/describe-resources.test.ts +53 -0
  223. package/src/describe-resources.ts +811 -28
  224. package/src/hcl/descend.test.ts +162 -0
  225. package/src/hcl/descend.ts +273 -0
  226. package/src/hcl/marker.test.ts +147 -0
  227. package/src/hcl/marker.ts +235 -0
  228. package/src/hcl/parse.test.ts +69 -0
  229. package/src/hcl/parse.ts +269 -27
  230. package/src/hcl/references.test.ts +116 -0
  231. package/src/hcl/references.ts +172 -0
  232. package/src/hcl/roots.ts +59 -5
  233. package/src/hcl/suppressions.test.ts +147 -0
  234. package/src/hcl/suppressions.ts +204 -0
  235. package/src/hcl/value.test.ts +73 -0
  236. package/src/hcl/value.ts +106 -0
  237. package/src/index.ts +31 -1
  238. package/src/lint/audit-catalog.ts +288 -3
  239. package/src/lint/audit-lineage.ts +384 -1
  240. package/src/lint/audit.test.ts +210 -3
  241. package/src/lint/post-synth/blocks.ts +109 -0
  242. package/src/lint/post-synth/fixtures/TF001/negative-cloud.tf +28 -0
  243. package/src/lint/post-synth/fixtures/TF001/negative.tf +30 -0
  244. package/src/lint/post-synth/fixtures/TF001/positive-local.tf +31 -0
  245. package/src/lint/post-synth/fixtures/TF001/positive.tf +24 -0
  246. package/src/lint/post-synth/fixtures/TF002/negative.tf +17 -0
  247. package/src/lint/post-synth/fixtures/TF002/positive-legacy.tf +10 -0
  248. package/src/lint/post-synth/fixtures/TF002/positive-no-version.tf +12 -0
  249. package/src/lint/post-synth/fixtures/TF002/positive.tf +7 -0
  250. package/src/lint/post-synth/fixtures/TF003/negative.tf +10 -0
  251. package/src/lint/post-synth/fixtures/TF003/positive.tf +8 -0
  252. package/src/lint/post-synth/fixtures/TF004/negative-git.tf +3 -0
  253. package/src/lint/post-synth/fixtures/TF004/negative-local.tf +3 -0
  254. package/src/lint/post-synth/fixtures/TF004/negative.tf +4 -0
  255. package/src/lint/post-synth/fixtures/TF004/positive.tf +3 -0
  256. package/src/lint/post-synth/fixtures/TF005/negative-local.tf +3 -0
  257. package/src/lint/post-synth/fixtures/TF005/negative-registry.tf +3 -0
  258. package/src/lint/post-synth/fixtures/TF005/negative-sha.tf +3 -0
  259. package/src/lint/post-synth/fixtures/TF005/negative.tf +3 -0
  260. package/src/lint/post-synth/fixtures/TF005/positive-branch.tf +3 -0
  261. package/src/lint/post-synth/fixtures/TF005/positive-non-semver.tf +3 -0
  262. package/src/lint/post-synth/fixtures/TF005/positive.tf +3 -0
  263. package/src/lint/post-synth/fixtures/TF006/negative.tf +5 -0
  264. package/src/lint/post-synth/fixtures/TF006/positive-empty-default.tf +11 -0
  265. package/src/lint/post-synth/fixtures/TF006/positive.tf +6 -0
  266. package/src/lint/post-synth/fixtures/TF007/negative.tf +10 -0
  267. package/src/lint/post-synth/fixtures/TF007/positive.tf +9 -0
  268. package/src/lint/post-synth/fixtures/TF008/negative-variable.tf +10 -0
  269. package/src/lint/post-synth/fixtures/TF008/negative.tf +8 -0
  270. package/src/lint/post-synth/fixtures/TF008/positive-other-provider.tf +10 -0
  271. package/src/lint/post-synth/fixtures/TF008/positive.tf +5 -0
  272. package/src/lint/post-synth/fixtures/TF009/negative.tf +10 -0
  273. package/src/lint/post-synth/fixtures/TF009/positive.tf +4 -0
  274. package/src/lint/post-synth/fixtures/TF010/negative.tf +5 -0
  275. package/src/lint/post-synth/fixtures/TF010/positive.tf +4 -0
  276. package/src/lint/post-synth/fixtures/TF011/negative.tf +5 -0
  277. package/src/lint/post-synth/fixtures/TF011/positive-blank.tf +5 -0
  278. package/src/lint/post-synth/fixtures/TF011/positive.tf +4 -0
  279. package/src/lint/post-synth/fixtures/TF012/negative.tf +4 -0
  280. package/src/lint/post-synth/fixtures/TF012/positive.tf +3 -0
  281. package/src/lint/post-synth/fixtures/TF013/negative.tf +8 -0
  282. package/src/lint/post-synth/fixtures/TF013/positive-data.tf +8 -0
  283. package/src/lint/post-synth/fixtures/TF013/positive.tf +8 -0
  284. package/src/lint/post-synth/fixtures/TF014/negative/main.tf +26 -0
  285. package/src/lint/post-synth/fixtures/TF014/negative/modules/cdn/main.tf +19 -0
  286. package/src/lint/post-synth/fixtures/TF014/positive/main.tf +16 -0
  287. package/src/lint/post-synth/fixtures/TF014/positive/modules/cdn/main.tf +15 -0
  288. package/src/lint/post-synth/fixtures/TF015/negative/main.tf +13 -0
  289. package/src/lint/post-synth/fixtures/TF015/negative/modules/cdn/main.tf +16 -0
  290. package/src/lint/post-synth/fixtures/TF015/positive/main.tf +13 -0
  291. package/src/lint/post-synth/fixtures/TF015/positive/modules/cdn/main.tf +14 -0
  292. package/src/lint/post-synth/fixtures/TF016/negative-template.tf +7 -0
  293. package/src/lint/post-synth/fixtures/TF016/negative.tf +16 -0
  294. package/src/lint/post-synth/fixtures/TF016/positive-shared-file.tf +12 -0
  295. package/src/lint/post-synth/fixtures/TF016/positive.tf +13 -0
  296. package/src/lint/post-synth/fixtures/TF017/negative.tf +4 -0
  297. package/src/lint/post-synth/fixtures/TF017/positive.tf +5 -0
  298. package/src/lint/post-synth/fixtures/TF018/negative.tf +9 -0
  299. package/src/lint/post-synth/fixtures/TF018/positive-data.tf +9 -0
  300. package/src/lint/post-synth/fixtures/TF018/positive.tf +4 -0
  301. package/src/lint/post-synth/fixtures/TF019/negative.tf +14 -0
  302. package/src/lint/post-synth/fixtures/TF019/positive-output.tf +5 -0
  303. package/src/lint/post-synth/fixtures/TF019/positive.tf +16 -0
  304. package/src/lint/post-synth/fixtures/TF020/negative.tf +43 -0
  305. package/src/lint/post-synth/fixtures/TF020/positive.tf +32 -0
  306. package/src/lint/post-synth/fixtures/TF020/scoped/main.tf +21 -0
  307. package/src/lint/post-synth/fixtures/TF020/scoped/modules/cdn/main.tf +13 -0
  308. package/src/lint/post-synth/fixtures/TF021/negative.tf +31 -0
  309. package/src/lint/post-synth/fixtures/TF021/positive-length.tf +8 -0
  310. package/src/lint/post-synth/fixtures/TF021/positive.tf +8 -0
  311. package/src/lint/post-synth/fixtures/TF022/negative-locator.tf +7 -0
  312. package/src/lint/post-synth/fixtures/TF022/negative.tf +10 -0
  313. package/src/lint/post-synth/fixtures/TF022/positive-nested.tf +10 -0
  314. package/src/lint/post-synth/fixtures/TF022/positive.tf +6 -0
  315. package/src/lint/post-synth/fixtures/TF023/negative/main.tf +15 -0
  316. package/src/lint/post-synth/fixtures/TF023/positive/main.tf +9 -0
  317. package/src/lint/post-synth/fixtures/TF023/positive/state.json +24 -0
  318. package/src/lint/post-synth/fixtures/TF024/live-clean/main.tf +9 -0
  319. package/src/lint/post-synth/fixtures/TF024/live-with-backend/main.tf +14 -0
  320. package/src/lint/post-synth/fixtures/TF024/live-with-cloud/main.tf +16 -0
  321. package/src/lint/post-synth/fixtures/TF025/live-clean/main.tf +13 -0
  322. package/src/lint/post-synth/fixtures/TF025/live-with-workspace-ref/main.tf +13 -0
  323. package/src/lint/post-synth/fixtures/TF026/live-default/main.tf +9 -0
  324. package/src/lint/post-synth/fixtures/TF026/live-explicit-delete/main.tf +13 -0
  325. package/src/lint/post-synth/fixtures/TF026/live-keep/main.tf +13 -0
  326. package/src/lint/post-synth/fixtures/TF027/live-default/main.tf +9 -0
  327. package/src/lint/post-synth/fixtures/TF027/live-untagged-delete/main.tf +13 -0
  328. package/src/lint/post-synth/fixtures/TF027/live-untagged-delete-scoped/main.tf +17 -0
  329. package/src/lint/post-synth/fixtures/TF027/live-untagged-report/main.tf +13 -0
  330. package/src/lint/post-synth/fixtures/TF028/live/main.tf +9 -0
  331. package/src/lint/post-synth/fixtures/TF028/stock/main.tf +10 -0
  332. package/src/lint/post-synth/fixtures/load.ts +108 -0
  333. package/src/lint/post-synth/index.ts +52 -0
  334. package/src/lint/post-synth/live-refusals-config-format.test.ts +143 -0
  335. package/src/lint/post-synth/module-rules.test.ts +153 -0
  336. package/src/lint/post-synth/module-source.test.ts +75 -0
  337. package/src/lint/post-synth/module-source.ts +121 -0
  338. package/src/lint/post-synth/post-synth.test.ts +73 -16
  339. package/src/lint/post-synth/scope.ts +107 -0
  340. package/src/lint/post-synth/single-block.test.ts +263 -0
  341. package/src/lint/post-synth/suppression.test.ts +110 -0
  342. package/src/lint/post-synth/tf001.ts +98 -18
  343. package/src/lint/post-synth/tf002.test.ts +93 -0
  344. package/src/lint/post-synth/tf002.ts +151 -0
  345. package/src/lint/post-synth/tf003.test.ts +75 -0
  346. package/src/lint/post-synth/tf003.ts +71 -0
  347. package/src/lint/post-synth/tf004.test.ts +47 -0
  348. package/src/lint/post-synth/tf004.ts +63 -0
  349. package/src/lint/post-synth/tf005.test.ts +57 -0
  350. package/src/lint/post-synth/tf005.ts +76 -0
  351. package/src/lint/post-synth/tf006.ts +53 -0
  352. package/src/lint/post-synth/tf007.ts +78 -0
  353. package/src/lint/post-synth/tf008.ts +75 -0
  354. package/src/lint/post-synth/tf009.ts +59 -0
  355. package/src/lint/post-synth/tf010.ts +51 -0
  356. package/src/lint/post-synth/tf011.ts +49 -0
  357. package/src/lint/post-synth/tf012.ts +47 -0
  358. package/src/lint/post-synth/tf013.ts +63 -0
  359. package/src/lint/post-synth/tf014.ts +72 -0
  360. package/src/lint/post-synth/tf015.ts +72 -0
  361. package/src/lint/post-synth/tf016.ts +75 -0
  362. package/src/lint/post-synth/tf017.ts +50 -0
  363. package/src/lint/post-synth/tf018.ts +72 -0
  364. package/src/lint/post-synth/tf019.ts +72 -0
  365. package/src/lint/post-synth/tf020.ts +123 -0
  366. package/src/lint/post-synth/tf021.ts +126 -0
  367. package/src/lint/post-synth/tf022.ts +61 -0
  368. package/src/lint/post-synth/tf024.test.ts +42 -0
  369. package/src/lint/post-synth/tf024.ts +70 -0
  370. package/src/lint/post-synth/tf025.test.ts +46 -0
  371. package/src/lint/post-synth/tf025.ts +101 -0
  372. package/src/lint/post-synth/tf026.test.ts +56 -0
  373. package/src/lint/post-synth/tf026.ts +84 -0
  374. package/src/lint/post-synth/tf027.test.ts +62 -0
  375. package/src/lint/post-synth/tf027.ts +74 -0
  376. package/src/lint/post-synth/tf028.test.ts +92 -0
  377. package/src/lint/post-synth/tf028.ts +96 -0
  378. package/src/lint/presets.test.ts +72 -0
  379. package/src/lint/rules/fixtures/TF101/negative-op.ts +2 -0
  380. package/src/lint/rules/fixtures/TF101/positive-op.ts +2 -0
  381. package/src/lint/rules/fixtures/load.ts +53 -0
  382. package/src/lint/rules/plan-before-apply.test.ts +112 -7
  383. package/src/lint/rules/plan-before-apply.ts +11 -0
  384. package/src/lint/secret-shape.test.ts +150 -0
  385. package/src/lint/secret-shape.ts +159 -0
  386. package/src/live-ownership.ts +60 -0
  387. package/src/lsp/completions.test.ts +3 -2
  388. package/src/lsp/option-keys.ts +12 -6
  389. package/src/op/activities/choudoufu.acceptance.test.ts +127 -0
  390. package/src/op/activities/choudoufu.test.ts +725 -0
  391. package/src/op/activities/index.ts +40 -4
  392. package/src/op/activities/live-detect.test.ts +143 -0
  393. package/src/op/activities/live-detect.ts +162 -0
  394. package/src/op/activities/registry.test.ts +11 -3
  395. package/src/op/activities/terraform.test.ts +7 -3
  396. package/src/op/activities/terraform.ts +861 -69
  397. package/src/op/activity-contracts.test.ts +147 -0
  398. package/src/op/activity-contracts.ts +178 -0
  399. package/src/op/adoption.test.ts +170 -0
  400. package/src/op/adoption.ts +241 -0
  401. package/src/op/builders.test.ts +60 -5
  402. package/src/op/builders.ts +95 -5
  403. package/src/op/resolve-root-mode.test.ts +85 -0
  404. package/src/op/resolve-root-mode.ts +86 -0
  405. package/src/plugin.test.ts +106 -0
  406. package/src/plugin.ts +186 -29
  407. package/src/skills/chant-terraform.md +1 -1
  408. package/src/validate.ts +10 -1
@@ -0,0 +1,725 @@
1
+ /**
2
+ * choudoufu-specific activity tests (#2103): the three new activities, the
3
+ * `terraformApply` live-root branch, and the version check. Same
4
+ * `node:child_process` mock as `terraform.test.ts`; nothing here runs
5
+ * choudoufu or terraform.
6
+ */
7
+
8
+ import { describe, test, expect, beforeEach, afterAll, afterEach, vi } from "vitest";
9
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync, readFileSync } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { join, resolve } from "node:path";
12
+ import {
13
+ terraformApply,
14
+ choudoufuLivePlan,
15
+ choudoufuLiveLs,
16
+ choudoufuLiveCheck,
17
+ choudoufuAdopt,
18
+ terraformApplyCommand,
19
+ classifyApprovalRefusal,
20
+ choudoufuLivePlanCommand,
21
+ choudoufuLiveLsCommand,
22
+ choudoufuLiveCheckCommand,
23
+ countLivePlanUnowned,
24
+ parseChoudoufuPlanSummary,
25
+ isOlderVersion,
26
+ parseChoudoufuVersion,
27
+ __resetChoudoufuVersionCheckForTests,
28
+ DEFAULT_LIVE_PLAN_DOCUMENT_FILE,
29
+ MIN_CHOUDOUFU_VERSION,
30
+ CHOUDOUFU_APPROVAL_MISMATCH_REFUSAL,
31
+ CHOUDOUFU_WRONG_ESTATE_REFUSAL,
32
+ } from "./terraform";
33
+
34
+ // ── The child-process stub (mirrors terraform.test.ts) ──────────────────────
35
+
36
+ interface ExecCall {
37
+ cmd: string;
38
+ opts: { cwd?: string; env?: Record<string, string>; signal?: AbortSignal };
39
+ }
40
+
41
+ const execCalls: ExecCall[] = [];
42
+
43
+ type Reply = { stdout: string; stderr: string } | Error;
44
+ let replies: Array<{ match: string; reply: Reply }> = [];
45
+
46
+ function execError(code: number, stderr: string, stdout = ""): Error & { code: number; stdout: string; stderr: string } {
47
+ return Object.assign(new Error(`Command failed (exit ${code})`), { code, stdout, stderr });
48
+ }
49
+
50
+ /** Every exec call's command, excluding the `choudoufu version` check every activity call triggers once. */
51
+ function commandsRun(): string[] {
52
+ return execCalls.filter((c) => c.cmd !== "choudoufu version").map((c) => c.cmd);
53
+ }
54
+
55
+ // Partial mock: `@intentius/chant/op` reaches core's base activities
56
+ // statically now (#2117), and they need the real `execFile`/`spawn` to load.
57
+ vi.mock("node:child_process", async (importOriginal) => {
58
+ const actual = await importOriginal<typeof import("node:child_process")>();
59
+ const custom = Symbol.for("nodejs.util.promisify.custom");
60
+ const exec = ((_cmd: string, _opts: unknown, cb?: (...a: unknown[]) => void) => {
61
+ cb?.(new Error("unmocked exec path"));
62
+ }) as unknown as Record<symbol, unknown>;
63
+ exec[custom] = async (cmd: string, opts?: ExecCall["opts"]) => {
64
+ execCalls.push({ cmd, opts: opts ?? {} });
65
+ for (const { match, reply } of replies) {
66
+ if (cmd.includes(match)) {
67
+ if (reply instanceof Error) throw reply;
68
+ return reply;
69
+ }
70
+ }
71
+ return { stdout: "", stderr: "" };
72
+ };
73
+ return { ...actual, exec };
74
+ });
75
+
76
+ // ── A real project config to resolve roots against ──────────────────────────
77
+
78
+ const workspaces: string[] = [];
79
+
80
+ /** Write a throwaway project with a `terraform` namespace and a live root directory. */
81
+ function liveProject(opts: { withLiveBlock?: boolean; withSidecar?: boolean } = {}): string {
82
+ const dir = mkdtempSync(join(tmpdir(), "chant-tf-choudoufu-"));
83
+ workspaces.push(dir);
84
+ const rootDir = join(dir, "estate");
85
+ mkdirSync(rootDir, { recursive: true });
86
+
87
+ if (opts.withSidecar) {
88
+ writeFileSync(join(rootDir, "estate.chdf.hcl"), 'estate = "prod-networking"\n');
89
+ writeFileSync(join(rootDir, "main.tf"), 'resource "null_resource" "x" {}\n');
90
+ } else if (opts.withLiveBlock !== false) {
91
+ writeFileSync(
92
+ join(rootDir, "main.tf"),
93
+ ["terraform {", " live {", ' estate = "prod-networking"', " }", "}", "", 'resource "null_resource" "x" {}', ""].join(
94
+ "\n",
95
+ ),
96
+ );
97
+ } else {
98
+ writeFileSync(join(rootDir, "main.tf"), 'resource "null_resource" "x" {}\n');
99
+ }
100
+
101
+ writeFileSync(
102
+ join(dir, "chant.config.json"),
103
+ JSON.stringify({ lexicons: ["terraform"], terraform: { binary: "choudoufu", roots: { estate: { dir: "./estate" } } } }, null, 2),
104
+ );
105
+ return dir;
106
+ }
107
+
108
+ afterAll(() => {
109
+ for (const dir of workspaces) rmSync(dir, { recursive: true, force: true });
110
+ });
111
+
112
+ beforeEach(() => {
113
+ execCalls.length = 0;
114
+ replies = [];
115
+ __resetChoudoufuVersionCheckForTests();
116
+ });
117
+
118
+ afterEach(() => {
119
+ __resetChoudoufuVersionCheckForTests();
120
+ });
121
+
122
+ // ── Pure command builders ────────────────────────────────────────────────────
123
+
124
+ describe("apply on a live root is the stock command (#2106 follow-up)", () => {
125
+ test("no -auto-approve: a matched plan file applies without re-prompting, as stock's own apply does", () => {
126
+ expect(terraformApplyCommand({ binary: "choudoufu", planFile: "chant.tfplan" })).toBe(
127
+ "choudoufu apply -input=false chant.tfplan",
128
+ );
129
+ });
130
+ });
131
+
132
+ describe("classifyApprovalRefusal (#2106 follow-up)", () => {
133
+ test("names the change-set refusal", () => {
134
+ expect(classifyApprovalRefusal(`\nError: ${CHOUDOUFU_APPROVAL_MISMATCH_REFUSAL}\n\nrows...`)).toBe(
135
+ "approval-mismatch",
136
+ );
137
+ });
138
+
139
+ test("names the estate refusal", () => {
140
+ expect(classifyApprovalRefusal(`\nError: ${CHOUDOUFU_WRONG_ESTATE_REFUSAL}\n`)).toBe("wrong-estate");
141
+ });
142
+
143
+ test("undefined for output carrying neither summary line", () => {
144
+ expect(classifyApprovalRefusal("Error: something else entirely")).toBeUndefined();
145
+ });
146
+ });
147
+
148
+ describe("choudoufuLivePlanCommand (#2103)", () => {
149
+ test("json form carries -detailed-exitcode, -json and -estate", () => {
150
+ expect(choudoufuLivePlanCommand({ binary: "choudoufu", estate: "prod", json: true })).toBe(
151
+ "choudoufu live-plan -detailed-exitcode -json -estate=prod",
152
+ );
153
+ });
154
+
155
+ test("text form omits -json", () => {
156
+ expect(choudoufuLivePlanCommand({ binary: "choudoufu", estate: "prod", json: false })).toBe(
157
+ "choudoufu live-plan -detailed-exitcode -estate=prod",
158
+ );
159
+ });
160
+
161
+ test("an estate needing shell quoting is quoted", () => {
162
+ expect(choudoufuLivePlanCommand({ binary: "choudoufu", estate: "has space", json: true })).toContain(
163
+ "-estate='has space'",
164
+ );
165
+ });
166
+
167
+ // choudoufu refuses `-estate` beside a `live` block or an `estate.chdf.hcl`
168
+ // sidecar, and since v0.14.0 it does not need one there (choudoufu #894, PR
169
+ // 915). Omitting the option is how a declared root is run.
170
+ test("no estate emits no -estate, for a root that names its own (choudoufu #894)", () => {
171
+ expect(choudoufuLivePlanCommand({ binary: "choudoufu", json: true })).toBe(
172
+ "choudoufu live-plan -detailed-exitcode -json",
173
+ );
174
+ expect(choudoufuLivePlanCommand({ binary: "choudoufu", json: false, noColor: true })).toBe(
175
+ "choudoufu live-plan -detailed-exitcode -no-color",
176
+ );
177
+ });
178
+
179
+ test("-adoption-only and -no-color ride the human form (#2105)", () => {
180
+ expect(
181
+ choudoufuLivePlanCommand({ binary: "choudoufu", estate: "prod", json: false, adoptionOnly: true, noColor: true }),
182
+ ).toBe("choudoufu live-plan -detailed-exitcode -adoption-only -no-color -estate=prod");
183
+ });
184
+
185
+ test("-adoption-only with -json is refused here, as choudoufu refuses it (#2105)", () => {
186
+ expect(() =>
187
+ choudoufuLivePlanCommand({ binary: "choudoufu", estate: "prod", json: true, adoptionOnly: true }),
188
+ ).toThrow(/cannot be combined/);
189
+ });
190
+ });
191
+
192
+ describe("choudoufuLiveLsCommand (#2103)", () => {
193
+ test("carries -estate and -json", () => {
194
+ expect(choudoufuLiveLsCommand({ binary: "choudoufu", estate: "prod" })).toBe(
195
+ "choudoufu live-ls -estate=prod -json",
196
+ );
197
+ });
198
+
199
+ test("-consistent is opt-in", () => {
200
+ expect(choudoufuLiveLsCommand({ binary: "choudoufu", estate: "prod", consistent: true })).toBe(
201
+ "choudoufu live-ls -estate=prod -json -consistent",
202
+ );
203
+ });
204
+ });
205
+
206
+ describe("choudoufuLiveCheckCommand (#2103)", () => {
207
+ test("live-check -json, no DIR argument", () => {
208
+ expect(choudoufuLiveCheckCommand({ binary: "choudoufu" })).toBe("choudoufu live-check -json");
209
+ });
210
+ });
211
+
212
+ describe("countLivePlanUnowned (#2103)", () => {
213
+ test("counts unowned entries and the adoptable subset", () => {
214
+ const doc = {
215
+ unowned: [
216
+ { addr: "aws_vpc.a", adopt_tofu_estate: "prod", adopt_tofu_address: "aws_vpc.a" },
217
+ { addr: "aws_vpc.b" },
218
+ { addr: "aws_vpc.c", adopt_tofu_address: "aws_vpc.c" },
219
+ ],
220
+ };
221
+ expect(countLivePlanUnowned(doc)).toEqual({ unowned: 3, adoptable: 2 });
222
+ });
223
+
224
+ test("zero when the document has no unowned array", () => {
225
+ expect(countLivePlanUnowned({})).toEqual({ unowned: 0, adoptable: 0 });
226
+ expect(countLivePlanUnowned(null)).toEqual({ unowned: 0, adoptable: 0 });
227
+ });
228
+ });
229
+
230
+ describe("parseChoudoufuPlanSummary (#2106)", () => {
231
+ test("parses the ordinary Plan: N to add, N to change, N to destroy. line", () => {
232
+ expect(parseChoudoufuPlanSummary("Plan: 3 to add, 1 to change, 2 to destroy.\n")).toEqual({
233
+ adds: 3,
234
+ changes: 1,
235
+ destroys: 2,
236
+ });
237
+ });
238
+
239
+ test("parses the import-prefixed form", () => {
240
+ expect(parseChoudoufuPlanSummary("Plan: 4 to import, 1 to add, 1 to change, 2 to destroy.\n")).toEqual({
241
+ adds: 1,
242
+ changes: 1,
243
+ destroys: 2,
244
+ });
245
+ });
246
+
247
+ test("zero counts for No changes.", () => {
248
+ expect(parseChoudoufuPlanSummary("No changes. Your infrastructure still matches the configuration.\n")).toEqual({
249
+ adds: 0,
250
+ changes: 0,
251
+ destroys: 0,
252
+ });
253
+ });
254
+
255
+ test("zero counts for a shape it does not recognize", () => {
256
+ expect(parseChoudoufuPlanSummary("")).toEqual({ adds: 0, changes: 0, destroys: 0 });
257
+ });
258
+ });
259
+
260
+ describe("isOlderVersion / parseChoudoufuVersion (#2103)", () => {
261
+ test("compares major.minor.patch numerically", () => {
262
+ expect(isOlderVersion("0.12.9", "0.13.0")).toBe(true);
263
+ expect(isOlderVersion("0.13.0", "0.13.0")).toBe(false);
264
+ expect(isOlderVersion("0.13.1", "0.13.0")).toBe(false);
265
+ expect(isOlderVersion("1.0.0", "0.13.0")).toBe(false);
266
+ });
267
+
268
+ test("parses the release version out of the human `version` output", () => {
269
+ expect(parseChoudoufuVersion("choudoufu v0.13.0 (based on OpenTofu v1.13.0)\non darwin_arm64")).toBe("0.13.0");
270
+ });
271
+
272
+ test("undefined for a dev build with no release tag baked in", () => {
273
+ expect(parseChoudoufuVersion("OpenTofu v1.13.0-dev\non darwin_arm64")).toBeUndefined();
274
+ });
275
+ });
276
+
277
+ // ── The version check, wired into every activity via resolveRoot ───────────
278
+
279
+ describe("choudoufu version check (#2103)", () => {
280
+ test("refuses a binary older than the floor, which the -json document moved to v0.14.0", async () => {
281
+ const dir = liveProject();
282
+ replies.push({ match: "version", reply: { stdout: "choudoufu v0.13.0 (based on OpenTofu v1.13.0)\n", stderr: "" } });
283
+ await expect(choudoufuLiveCheck({ root: "estate", cwd: dir })).rejects.toThrow(/older than.*v0\.14\.0/);
284
+ });
285
+
286
+ test("passes at exactly the minimum version", async () => {
287
+ const dir = liveProject();
288
+ replies.push({
289
+ match: "version",
290
+ reply: { stdout: `choudoufu v${MIN_CHOUDOUFU_VERSION} (based on OpenTofu v1.13.0)\n`, stderr: "" },
291
+ });
292
+ replies.push({ match: "live-check", reply: { stdout: "{}", stderr: "" } });
293
+ await expect(choudoufuLiveCheck({ root: "estate", cwd: dir })).resolves.toBeDefined();
294
+ });
295
+
296
+ test("runs once per module load, not once per activity call", async () => {
297
+ const dir = liveProject();
298
+ replies.push({
299
+ match: "version",
300
+ reply: { stdout: `choudoufu v${MIN_CHOUDOUFU_VERSION} (based on OpenTofu v1.13.0)\n`, stderr: "" },
301
+ });
302
+ replies.push({ match: "live-check", reply: { stdout: "{}", stderr: "" } });
303
+ await choudoufuLiveCheck({ root: "estate", cwd: dir });
304
+ await choudoufuLiveCheck({ root: "estate", cwd: dir });
305
+ const versionCalls = execCalls.filter((c) => c.cmd === "choudoufu version");
306
+ expect(versionCalls).toHaveLength(1);
307
+ });
308
+
309
+ test("a dev build with no parsable release version is never refused", async () => {
310
+ const dir = liveProject();
311
+ replies.push({ match: "version", reply: { stdout: "OpenTofu v1.13.0-dev\non darwin_arm64\n", stderr: "" } });
312
+ replies.push({ match: "live-check", reply: { stdout: "{}", stderr: "" } });
313
+ await expect(choudoufuLiveCheck({ root: "estate", cwd: dir })).resolves.toBeDefined();
314
+ });
315
+ });
316
+
317
+ // ── terraformApply on a live root ───────────────────────────────────────────
318
+
319
+ describe("terraformApply on a live root (#2106 follow-up)", () => {
320
+ /** The version reply every activity call needs before it reaches the apply. */
321
+ function atMinimumVersion(): void {
322
+ replies.push({
323
+ match: "version",
324
+ reply: { stdout: `choudoufu v${MIN_CHOUDOUFU_VERSION} (based on OpenTofu v1.13.0)\n`, stderr: "" },
325
+ });
326
+ }
327
+
328
+ test("applies the plan file, the same command a stock root runs", async () => {
329
+ const dir = liveProject();
330
+ atMinimumVersion();
331
+ const result = await terraformApply({ root: "estate", cwd: dir, planFile: "chant.tfplan" });
332
+ expect(commandsRun()).toEqual(["choudoufu apply -input=false chant.tfplan"]);
333
+ expect(result).toEqual({ planFile: "chant.tfplan", dir: resolve(dir, "estate"), applied: true });
334
+ });
335
+
336
+ test("exit 3 with the change-set refusal comes back as a named result, not a throw", async () => {
337
+ const dir = liveProject();
338
+ atMinimumVersion();
339
+ replies.push({
340
+ match: "apply",
341
+ reply: execError(3, `\nError: ${CHOUDOUFU_APPROVAL_MISMATCH_REFUSAL}\n\n null_resource.second Create -\n`),
342
+ });
343
+ const result = await terraformApply({ root: "estate", cwd: dir, planFile: "chant.tfplan" });
344
+ expect(result.applied).toBe(false);
345
+ expect(result.refused).toBe("approval-mismatch");
346
+ expect(result.refusal).toContain(CHOUDOUFU_APPROVAL_MISMATCH_REFUSAL);
347
+ expect(result.planFile).toBe("chant.tfplan");
348
+ });
349
+
350
+ test("exit 3 with the estate refusal is the sibling named result", async () => {
351
+ const dir = liveProject();
352
+ atMinimumVersion();
353
+ replies.push({ match: "apply", reply: execError(3, `\nError: ${CHOUDOUFU_WRONG_ESTATE_REFUSAL}\n`) });
354
+ const result = await terraformApply({ root: "estate", cwd: dir, planFile: "chant.tfplan" });
355
+ expect(result.refused).toBe("wrong-estate");
356
+ });
357
+
358
+ test("exit 1 stays a failure, so an ordinary breakage is never routed to a reviewer", async () => {
359
+ const dir = liveProject();
360
+ atMinimumVersion();
361
+ replies.push({ match: "apply", reply: execError(1, "Error: no valid credential sources found") });
362
+ await expect(terraformApply({ root: "estate", cwd: dir, planFile: "chant.tfplan" })).rejects.toThrow(
363
+ /Command failed \(exit 1\)/,
364
+ );
365
+ });
366
+
367
+ test("exit 3 carrying neither refusal line stays a failure", async () => {
368
+ const dir = liveProject();
369
+ atMinimumVersion();
370
+ replies.push({ match: "apply", reply: execError(3, "Error: something nobody has named yet") });
371
+ await expect(terraformApply({ root: "estate", cwd: dir, planFile: "chant.tfplan" })).rejects.toThrow(
372
+ /Command failed \(exit 3\)/,
373
+ );
374
+ });
375
+
376
+ test("reads the estate from the sidecar just as well", async () => {
377
+ const dir = liveProject({ withSidecar: true });
378
+ atMinimumVersion();
379
+ await terraformApply({ root: "estate", cwd: dir, planFile: "chant.tfplan" });
380
+ expect(commandsRun()).toEqual(["choudoufu apply -input=false chant.tfplan"]);
381
+ });
382
+
383
+ test("a missing plan file is refused on a live root too", async () => {
384
+ const dir = liveProject();
385
+ atMinimumVersion();
386
+ await expect(terraformApply({ root: "estate", cwd: dir })).rejects.toThrow(/planFile is required/);
387
+ expect(commandsRun()).toEqual([]);
388
+ });
389
+
390
+ test("a choudoufu root with no live declaration reads exit 3 as an ordinary failure", async () => {
391
+ const dir = liveProject({ withLiveBlock: false });
392
+ atMinimumVersion();
393
+ replies.push({ match: "apply", reply: execError(3, `\nError: ${CHOUDOUFU_APPROVAL_MISMATCH_REFUSAL}\n`) });
394
+ await expect(terraformApply({ root: "estate", cwd: dir, planFile: "chant.tfplan" })).rejects.toThrow(
395
+ /Command failed \(exit 3\)/,
396
+ );
397
+ });
398
+ });
399
+
400
+ // ── choudoufuLivePlan ────────────────────────────────────────────────────────
401
+
402
+ describe("choudoufuLivePlan (#2103)", () => {
403
+ const DOC = JSON.stringify({
404
+ estate: "prod-networking",
405
+ bound: [],
406
+ omissions: [],
407
+ unowned: [{ addr: "aws_vpc.solo", adopt_tofu_address: "aws_vpc.solo" }],
408
+ });
409
+
410
+ test("exit 0: no drift, the document is captured and written, the human plan comes from a second call", async () => {
411
+ const dir = liveProject();
412
+ replies.push({ match: "live-plan -detailed-exitcode -json", reply: { stdout: DOC, stderr: "" } });
413
+ replies.push({ match: "live-plan -detailed-exitcode -no-color", reply: { stdout: "No changes.\n", stderr: "" } });
414
+
415
+ const result = await choudoufuLivePlan({ root: "estate", cwd: dir });
416
+ expect(result.drift).toBe(false);
417
+ expect(result.estate).toBe("prod-networking");
418
+ expect(result.json).toEqual(JSON.parse(DOC));
419
+ expect(result.text).toBe("No changes.\n");
420
+ expect(result.unowned).toBe(1);
421
+ expect(result.adoptable).toBe(1);
422
+ expect(result.documentPath).toBe(DEFAULT_LIVE_PLAN_DOCUMENT_FILE);
423
+ expect(result.adds).toBe(0);
424
+ expect(result.changes).toBe(0);
425
+ expect(result.destroys).toBe(0);
426
+
427
+ const written = readFileSync(join(result.dir, DEFAULT_LIVE_PLAN_DOCUMENT_FILE), "utf-8");
428
+ expect(JSON.parse(written)).toEqual(JSON.parse(DOC));
429
+
430
+ // The root names its own estate, so neither call carries `-estate`:
431
+ // choudoufu refuses the flag beside a declaration, and since v0.14.0 it
432
+ // reads the declared name itself (choudoufu #894, PR 915).
433
+ expect(commandsRun()).toEqual([
434
+ "choudoufu live-plan -detailed-exitcode -json",
435
+ "choudoufu live-plan -detailed-exitcode -no-color",
436
+ ]);
437
+ });
438
+
439
+ test("exit 2: drift, and the document is still captured", async () => {
440
+ const dir = liveProject();
441
+ replies.push({ match: "live-plan -detailed-exitcode -json", reply: execError(2, "", DOC) });
442
+ replies.push({
443
+ match: "live-plan -detailed-exitcode -no-color",
444
+ reply: { stdout: "Plan: 2 to add, 0 to change, 1 to destroy.\n", stderr: "" },
445
+ });
446
+
447
+ const result = await choudoufuLivePlan({ root: "estate", cwd: dir });
448
+ expect(result.drift).toBe(true);
449
+ expect(result.json).toEqual(JSON.parse(DOC));
450
+ // Parsed off the human render's own summary line, not off the document
451
+ // (#788's JSON carries no resource-change diff of its own — see
452
+ // ChoudoufuLivePlanResult's doc comment).
453
+ expect(result.adds).toBe(2);
454
+ expect(result.changes).toBe(0);
455
+ expect(result.destroys).toBe(1);
456
+ });
457
+
458
+ test("exit 1: throws with choudoufu's stderr attached", async () => {
459
+ const dir = liveProject();
460
+ replies.push({ match: "live-plan -detailed-exitcode -json", reply: execError(1, "Error: something refused") });
461
+ await expect(choudoufuLivePlan({ root: "estate", cwd: dir })).rejects.toThrow(
462
+ /live-plan failed.*exit 1[\s\S]*something refused/,
463
+ );
464
+ });
465
+
466
+ test("a root declaring no estate carries the caller's as -estate", async () => {
467
+ const dir = liveProject({ withLiveBlock: false });
468
+ replies.push({ match: "live-plan -detailed-exitcode -json", reply: { stdout: DOC, stderr: "" } });
469
+ replies.push({ match: "live-plan -detailed-exitcode -no-color", reply: { stdout: "", stderr: "" } });
470
+ await choudoufuLivePlan({ root: "estate", cwd: dir, estate: "other-estate" });
471
+ expect(commandsRun()[0]).toContain("-estate=other-estate");
472
+ });
473
+
474
+ // The declaration wins on the wire whatever the caller passes, because
475
+ // choudoufu refuses `-estate` beside one. The result still reports the
476
+ // estate the document names.
477
+ test("a root that declares its own estate emits no -estate, even when one is passed", async () => {
478
+ const dir = liveProject();
479
+ replies.push({ match: "live-plan -detailed-exitcode -json", reply: { stdout: DOC, stderr: "" } });
480
+ replies.push({ match: "live-plan -detailed-exitcode -no-color", reply: { stdout: "", stderr: "" } });
481
+ await choudoufuLivePlan({ root: "estate", cwd: dir, estate: "other-estate" });
482
+ expect(commandsRun().every((c) => !c.includes("-estate"))).toBe(true);
483
+ });
484
+
485
+ test("no estate anywhere: refused, and the plan itself never runs", async () => {
486
+ const dir = liveProject({ withLiveBlock: false });
487
+ await expect(choudoufuLivePlan({ root: "estate", cwd: dir })).rejects.toThrow(/no estate to run against/);
488
+ expect(commandsRun()).toHaveLength(0);
489
+ });
490
+ });
491
+
492
+ // ── choudoufuLivePlan's adoption ledger (#2105) ──────────────────────────────
493
+
494
+ describe("choudoufuLivePlan adoption ledger (#2105)", () => {
495
+ const ADOPTABLE = JSON.stringify({
496
+ estate: "prod-networking",
497
+ bound: [{ addr: "aws_subnet.app", identity: "subnet-99" }],
498
+ omissions: [],
499
+ unowned: [
500
+ {
501
+ addr: "aws_vpc.main",
502
+ type: "aws_vpc",
503
+ identity: "vpc-0abc",
504
+ adopt_tofu_estate: "prod-networking",
505
+ adopt_tofu_address: "aws_vpc.main",
506
+ },
507
+ ],
508
+ });
509
+
510
+ const LEDGER_RENDER = [
511
+ "Adoptable now: 1 resource instance",
512
+ "",
513
+ " aws_vpc.main <- aws_vpc vpc-0abc",
514
+ " adopt with: aws ec2 create-tags --resources 'vpc-0abc' --tags 'Key=tofu-estate,Value=prod-networking'",
515
+ " or write: tofu-estate=prod-networking tofu-address=aws_vpc.main",
516
+ "",
517
+ ].join("\n");
518
+
519
+ test("every run projects the document into a ledger, adoptions and contested", async () => {
520
+ const dir = liveProject();
521
+ replies.push({ match: "live-plan -detailed-exitcode -json", reply: { stdout: ADOPTABLE, stderr: "" } });
522
+ replies.push({
523
+ match: "live-plan -detailed-exitcode -no-color",
524
+ reply: { stdout: "Plan: 1 to add.\n", stderr: "" },
525
+ });
526
+
527
+ const result = await choudoufuLivePlan({ root: "estate", cwd: dir });
528
+ expect(result.adoptions.map((c) => c.addr)).toEqual(["aws_vpc.main"]);
529
+ expect(result.contested).toEqual([]);
530
+ expect(result.ambiguous).toBe(0);
531
+ expect(result.ledger).toContain(
532
+ " aws_vpc.main <- aws_vpc vpc-0abc write: tofu-estate=prod-networking tofu-address=aws_vpc.main",
533
+ );
534
+ });
535
+
536
+ test("`finding` is the plan text and then the ledger, and carries no JSON", async () => {
537
+ const dir = liveProject();
538
+ replies.push({ match: "live-plan -detailed-exitcode -json", reply: { stdout: ADOPTABLE, stderr: "" } });
539
+ replies.push({
540
+ match: "live-plan -detailed-exitcode -no-color",
541
+ reply: { stdout: "Plan: 1 to add.\n", stderr: "" },
542
+ });
543
+
544
+ const result = await choudoufuLivePlan({ root: "estate", cwd: dir });
545
+ expect(result.finding.startsWith("Plan: 1 to add.")).toBe(true);
546
+ expect(result.finding).toContain("Adoptable now: 1 live resource");
547
+ // The document's bound section names a resource the ledger has no reason
548
+ // to mention; if any of the JSON leaked into `finding`, this is where.
549
+ expect(result.finding).not.toContain("subnet-99");
550
+ expect(result.finding).not.toContain("adopt_tofu_address");
551
+ });
552
+
553
+ test("adoptionOnly renders the human half as choudoufu's ledger and reads its commands", async () => {
554
+ const dir = liveProject();
555
+ replies.push({ match: "live-plan -detailed-exitcode -json", reply: { stdout: ADOPTABLE, stderr: "" } });
556
+ replies.push({ match: "-adoption-only", reply: { stdout: LEDGER_RENDER, stderr: "" } });
557
+
558
+ const result = await choudoufuLivePlan({ root: "estate", cwd: dir, adoptionOnly: true });
559
+ expect(commandsRun()).toEqual([
560
+ "choudoufu live-plan -detailed-exitcode -json",
561
+ "choudoufu live-plan -detailed-exitcode -adoption-only -no-color",
562
+ ]);
563
+ expect(result.adoptions[0].command).toBe(
564
+ "aws ec2 create-tags --resources 'vpc-0abc' --tags 'Key=tofu-estate,Value=prod-networking'",
565
+ );
566
+ });
567
+
568
+ test("without adoptionOnly a candidate carries the marker values and no command", async () => {
569
+ const dir = liveProject();
570
+ replies.push({ match: "live-plan -detailed-exitcode -json", reply: { stdout: ADOPTABLE, stderr: "" } });
571
+ replies.push({ match: "live-plan -detailed-exitcode -no-color", reply: { stdout: "", stderr: "" } });
572
+
573
+ const result = await choudoufuLivePlan({ root: "estate", cwd: dir });
574
+ expect(result.adoptions[0].command).toBeUndefined();
575
+ expect(result.adoptions[0].markerAddress).toBe("aws_vpc.main");
576
+ });
577
+ });
578
+
579
+ // ── choudoufuAdopt (#2105) ───────────────────────────────────────────────────
580
+
581
+ describe("choudoufuAdopt (#2105)", () => {
582
+ const candidate = (addr: string, identity: string, command?: string) => ({
583
+ addr,
584
+ type: "aws_vpc",
585
+ identity,
586
+ markerEstate: "prod-networking",
587
+ markerAddress: addr,
588
+ ...(command ? { command } : {}),
589
+ });
590
+
591
+ test("runs each candidate's own tagging command, in the root directory", async () => {
592
+ const dir = liveProject();
593
+ const result = await choudoufuAdopt({
594
+ root: "estate",
595
+ cwd: dir,
596
+ adoptions: [
597
+ candidate("aws_vpc.main", "vpc-0abc", "aws ec2 create-tags --resources 'vpc-0abc'"),
598
+ candidate("aws_subnet.app", "subnet-01", "aws ec2 create-tags --resources 'subnet-01'"),
599
+ ],
600
+ });
601
+ expect(result.mechanism).toBe("tag-write");
602
+ expect(result.adopted).toEqual(["aws_vpc.main", "aws_subnet.app"]);
603
+ expect(result.adoptedCount).toBe(2);
604
+ expect(commandsRun()).toEqual([
605
+ "aws ec2 create-tags --resources 'vpc-0abc'",
606
+ "aws ec2 create-tags --resources 'subnet-01'",
607
+ ]);
608
+ expect(
609
+ execCalls.filter((c) => c.cmd !== "choudoufu version").every((c) => c.opts.cwd === resolve(dir, "estate")),
610
+ ).toBe(true);
611
+ });
612
+
613
+ test("a candidate with no printed command is refused, naming both marker values", async () => {
614
+ const dir = liveProject();
615
+ const result = await choudoufuAdopt({
616
+ root: "estate",
617
+ cwd: dir,
618
+ adoptions: [candidate("aws_iam_role.app", "app-role")],
619
+ });
620
+ expect(result.adopted).toEqual([]);
621
+ expect(result.refused).toHaveLength(1);
622
+ expect(result.refused[0].reason).toContain("tofu-estate=prod-networking");
623
+ expect(result.refused[0].reason).toContain("tofu-address=aws_iam_role.app");
624
+ expect(commandsRun()).toEqual([]);
625
+ });
626
+
627
+ test("a contested candidate is reported and never written", async () => {
628
+ const dir = liveProject();
629
+ const result = await choudoufuAdopt({
630
+ root: "estate",
631
+ cwd: dir,
632
+ adoptions: [],
633
+ contested: [
634
+ candidate("aws_vpc.main", "vpc-0abc", "aws ec2 create-tags --resources 'vpc-0abc'"),
635
+ candidate("aws_vpc.main", "vpc-0def", "aws ec2 create-tags --resources 'vpc-0def'"),
636
+ ],
637
+ });
638
+ expect(result.adopted).toEqual([]);
639
+ expect(result.ambiguous).toBe(1);
640
+ expect(result.refused.map((r) => r.identity)).toEqual(["vpc-0abc", "vpc-0def"]);
641
+ expect(result.refused[0].reason).toContain("more than one live resource");
642
+ // A contested candidate carries a command; running it anyway is the whole
643
+ // thing this refuses to do.
644
+ expect(commandsRun()).toEqual([]);
645
+ });
646
+
647
+ test("nothing to adopt runs nothing and fails nothing", async () => {
648
+ const dir = liveProject();
649
+ const result = await choudoufuAdopt({ root: "estate", cwd: dir, adoptions: [] });
650
+ expect(result).toMatchObject({ adopted: [], adoptedCount: 0, refused: [], ambiguous: 0 });
651
+ expect(commandsRun()).toEqual([]);
652
+ });
653
+ });
654
+
655
+ // ── choudoufuLiveLs ──────────────────────────────────────────────────────────
656
+
657
+ describe("choudoufuLiveLs (#2103)", () => {
658
+ test("lists the estate as JSON", async () => {
659
+ const dir = liveProject();
660
+ const listing = JSON.stringify([{ arn: "arn:aws:ec2:...:vpc/vpc-1", type: "aws_vpc", address: "aws_vpc.main" }]);
661
+ replies.push({ match: "live-ls", reply: { stdout: listing, stderr: "" } });
662
+
663
+ const result = await choudoufuLiveLs({ root: "estate", cwd: dir });
664
+ expect(result.estate).toBe("prod-networking");
665
+ expect(result.json).toEqual(JSON.parse(listing));
666
+ expect(commandsRun()).toEqual(["choudoufu live-ls -estate=prod-networking -json"]);
667
+ });
668
+
669
+ test("-consistent is forwarded", async () => {
670
+ const dir = liveProject();
671
+ replies.push({ match: "live-ls", reply: { stdout: "[]", stderr: "" } });
672
+ await choudoufuLiveLs({ root: "estate", cwd: dir, consistent: true });
673
+ expect(commandsRun()[0]).toContain("-consistent");
674
+ });
675
+ });
676
+
677
+ // ── choudoufuLiveCheck ───────────────────────────────────────────────────────
678
+
679
+ describe("choudoufuLiveCheck (#2103)", () => {
680
+ test("exit 0: not refused", async () => {
681
+ const dir = liveProject();
682
+ replies.push({ match: "live-check", reply: { stdout: '{"instances":[]}', stderr: "" } });
683
+ const result = await choudoufuLiveCheck({ root: "estate", cwd: dir });
684
+ expect(result.refused).toBe(false);
685
+ expect(result.json).toEqual({ instances: [] });
686
+ expect(commandsRun()).toEqual(["choudoufu live-check -json"]);
687
+ });
688
+
689
+ test("non-zero exit: refused, not thrown, and the output is still captured", async () => {
690
+ const dir = liveProject();
691
+ replies.push({ match: "live-check", reply: execError(1, "", '{"instances":[{"refused":true}]}') });
692
+ const result = await choudoufuLiveCheck({ root: "estate", cwd: dir });
693
+ expect(result.refused).toBe(true);
694
+ expect(result.json).toEqual({ instances: [{ refused: true }] });
695
+ });
696
+
697
+ test("runs with no estate at all: live-check needs none", async () => {
698
+ const dir = liveProject({ withLiveBlock: false });
699
+ replies.push({ match: "live-check", reply: { stdout: "{}", stderr: "" } });
700
+ const result = await choudoufuLiveCheck({ root: "estate", cwd: dir });
701
+ expect(result.refused).toBe(false);
702
+ });
703
+ });
704
+
705
+ // Defensive, and no longer describing a live defect: choudoufu v0.14.0 keeps
706
+ // `-json` stdout to the document, so the slice is the identity on every
707
+ // supported binary. These cases pin the behaviour that makes keeping it cheap:
708
+ // a stray writer ahead of the document costs a slice rather than a parse
709
+ // failure, and a stdout with no document at all still reaches `JSON.parse`
710
+ // with its real text.
711
+ describe("liveDocumentFrom (defensive, choudoufu #894 fixed in v0.14.0)", () => {
712
+ test("skips any line that precedes the document, if a writer ever puts one there", async () => {
713
+ const { liveDocumentFrom } = await import("./terraform");
714
+ const stdout = 'aws_vpc.main: Refreshing state... [id=vpc-1]\naws_subnet.a: Refreshing state...\n{\n "bound": []\n}\n';
715
+ expect(JSON.parse(liveDocumentFrom(stdout))).toEqual({ bound: [] });
716
+ });
717
+ test("returns a clean document unchanged", async () => {
718
+ const { liveDocumentFrom } = await import("./terraform");
719
+ expect(liveDocumentFrom('{\n "bound": []\n}\n')).toBe('{\n "bound": []\n}\n');
720
+ });
721
+ test("returns the input unchanged when no line opens a document, so JSON.parse reports the real text", async () => {
722
+ const { liveDocumentFrom } = await import("./terraform");
723
+ expect(liveDocumentFrom("Error: boom\n")).toBe("Error: boom\n");
724
+ });
725
+ });