@camaradesuk/git-worktree-tools 1.8.0 → 1.10.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 (353) hide show
  1. package/README.md +48 -27
  2. package/dist/cli/cleanpr.js +74 -53
  3. package/dist/cli/cleanpr.js.map +1 -1
  4. package/dist/cli/cleanpr.test.js +2 -0
  5. package/dist/cli/cleanpr.test.js.map +1 -1
  6. package/dist/cli/lswt.js +32 -56
  7. package/dist/cli/lswt.js.map +1 -1
  8. package/dist/cli/lswt.test.js +17 -27
  9. package/dist/cli/lswt.test.js.map +1 -1
  10. package/dist/cli/newpr.d.ts +13 -1
  11. package/dist/cli/newpr.d.ts.map +1 -1
  12. package/dist/cli/newpr.js +350 -151
  13. package/dist/cli/newpr.js.map +1 -1
  14. package/dist/cli/newpr.test.js +314 -5
  15. package/dist/cli/newpr.test.js.map +1 -1
  16. package/dist/cli/prs.d.ts +3 -10
  17. package/dist/cli/prs.d.ts.map +1 -1
  18. package/dist/cli/prs.js +6 -168
  19. package/dist/cli/prs.js.map +1 -1
  20. package/dist/cli/prs.test.js +55 -0
  21. package/dist/cli/prs.test.js.map +1 -1
  22. package/dist/cli/wt/clean.d.ts +6 -2
  23. package/dist/cli/wt/clean.d.ts.map +1 -1
  24. package/dist/cli/wt/clean.js +401 -20
  25. package/dist/cli/wt/clean.js.map +1 -1
  26. package/dist/cli/wt/clean.test.d.ts +8 -0
  27. package/dist/cli/wt/clean.test.d.ts.map +1 -0
  28. package/dist/cli/wt/clean.test.js +624 -0
  29. package/dist/cli/wt/clean.test.js.map +1 -0
  30. package/dist/cli/wt/completion.d.ts +3 -0
  31. package/dist/cli/wt/completion.d.ts.map +1 -1
  32. package/dist/cli/wt/completion.js +80 -9
  33. package/dist/cli/wt/completion.js.map +1 -1
  34. package/dist/cli/wt/completion.test.js +102 -0
  35. package/dist/cli/wt/completion.test.js.map +1 -1
  36. package/dist/cli/wt/config.d.ts +3 -1
  37. package/dist/cli/wt/config.d.ts.map +1 -1
  38. package/dist/cli/wt/config.js +323 -32
  39. package/dist/cli/wt/config.js.map +1 -1
  40. package/dist/cli/wt/config.test.d.ts +2 -0
  41. package/dist/cli/wt/config.test.d.ts.map +1 -1
  42. package/dist/cli/wt/config.test.js +206 -26
  43. package/dist/cli/wt/config.test.js.map +1 -1
  44. package/dist/cli/wt/interactive-menu.d.ts +2 -0
  45. package/dist/cli/wt/interactive-menu.d.ts.map +1 -1
  46. package/dist/cli/wt/interactive-menu.js +346 -73
  47. package/dist/cli/wt/interactive-menu.js.map +1 -1
  48. package/dist/cli/wt/interactive-menu.test.d.ts +4 -2
  49. package/dist/cli/wt/interactive-menu.test.d.ts.map +1 -1
  50. package/dist/cli/wt/interactive-menu.test.js +383 -323
  51. package/dist/cli/wt/interactive-menu.test.js.map +1 -1
  52. package/dist/cli/wt/link.d.ts +3 -1
  53. package/dist/cli/wt/link.d.ts.map +1 -1
  54. package/dist/cli/wt/link.js +125 -38
  55. package/dist/cli/wt/link.js.map +1 -1
  56. package/dist/cli/wt/list.d.ts +4 -1
  57. package/dist/cli/wt/list.d.ts.map +1 -1
  58. package/dist/cli/wt/list.js +85 -16
  59. package/dist/cli/wt/list.js.map +1 -1
  60. package/dist/cli/wt/list.test.d.ts +10 -0
  61. package/dist/cli/wt/list.test.d.ts.map +1 -0
  62. package/dist/cli/wt/list.test.js +157 -0
  63. package/dist/cli/wt/list.test.js.map +1 -0
  64. package/dist/cli/wt/new.d.ts +8 -2
  65. package/dist/cli/wt/new.d.ts.map +1 -1
  66. package/dist/cli/wt/new.js +91 -46
  67. package/dist/cli/wt/new.js.map +1 -1
  68. package/dist/cli/wt/prs.d.ts +2 -1
  69. package/dist/cli/wt/prs.d.ts.map +1 -1
  70. package/dist/cli/wt/prs.js +3 -164
  71. package/dist/cli/wt/prs.js.map +1 -1
  72. package/dist/cli/wt/run-command.d.ts +4 -2
  73. package/dist/cli/wt/run-command.d.ts.map +1 -1
  74. package/dist/cli/wt/run-command.js +6 -4
  75. package/dist/cli/wt/run-command.js.map +1 -1
  76. package/dist/cli/wt/state.d.ts +3 -1
  77. package/dist/cli/wt/state.d.ts.map +1 -1
  78. package/dist/cli/wt/state.js +74 -10
  79. package/dist/cli/wt/state.js.map +1 -1
  80. package/dist/cli/wt/state.test.d.ts +9 -0
  81. package/dist/cli/wt/state.test.d.ts.map +1 -0
  82. package/dist/cli/wt/state.test.js +127 -0
  83. package/dist/cli/wt/state.test.js.map +1 -0
  84. package/dist/cli/wt/wt.test.d.ts +2 -2
  85. package/dist/cli/wt/wt.test.js +430 -212
  86. package/dist/cli/wt/wt.test.js.map +1 -1
  87. package/dist/cli/wt.d.ts.map +1 -1
  88. package/dist/cli/wt.js +50 -36
  89. package/dist/cli/wt.js.map +1 -1
  90. package/dist/cli/wt.unit.test.js +16 -38
  91. package/dist/cli/wt.unit.test.js.map +1 -1
  92. package/dist/cli/wtconfig.d.ts +1 -0
  93. package/dist/cli/wtconfig.d.ts.map +1 -1
  94. package/dist/cli/wtconfig.js +213 -21
  95. package/dist/cli/wtconfig.js.map +1 -1
  96. package/dist/cli/wtconfig.test.js +3 -0
  97. package/dist/cli/wtconfig.test.js.map +1 -1
  98. package/dist/cli/wtlink.js +116 -73
  99. package/dist/cli/wtlink.js.map +1 -1
  100. package/dist/cli/wtstate.js +21 -2
  101. package/dist/cli/wtstate.js.map +1 -1
  102. package/dist/e2e/wt/interactive-menu.e2e.test.js +17 -17
  103. package/dist/e2e/wt/interactive-menu.e2e.test.js.map +1 -1
  104. package/dist/lib/ai/types.d.ts +12 -0
  105. package/dist/lib/ai/types.d.ts.map +1 -1
  106. package/dist/lib/ai/types.js.map +1 -1
  107. package/dist/lib/cleanpr/args.d.ts.map +1 -1
  108. package/dist/lib/cleanpr/args.js +20 -0
  109. package/dist/lib/cleanpr/args.js.map +1 -1
  110. package/dist/lib/cleanpr/types.d.ts +6 -0
  111. package/dist/lib/cleanpr/types.d.ts.map +1 -1
  112. package/dist/lib/cleanpr/worktree-info.d.ts.map +1 -1
  113. package/dist/lib/cleanpr/worktree-info.js +1 -6
  114. package/dist/lib/cleanpr/worktree-info.js.map +1 -1
  115. package/dist/lib/cleanpr/worktree-info.test.js +10 -13
  116. package/dist/lib/cleanpr/worktree-info.test.js.map +1 -1
  117. package/dist/lib/colors.d.ts +5 -0
  118. package/dist/lib/colors.d.ts.map +1 -1
  119. package/dist/lib/colors.js +13 -6
  120. package/dist/lib/colors.js.map +1 -1
  121. package/dist/lib/config-editor.d.ts.map +1 -1
  122. package/dist/lib/config-editor.js.map +1 -1
  123. package/dist/lib/config-migration/detector.d.ts +25 -0
  124. package/dist/lib/config-migration/detector.d.ts.map +1 -0
  125. package/dist/lib/config-migration/detector.js +372 -0
  126. package/dist/lib/config-migration/detector.js.map +1 -0
  127. package/dist/lib/config-migration/detector.test.d.ts +5 -0
  128. package/dist/lib/config-migration/detector.test.d.ts.map +1 -0
  129. package/dist/lib/config-migration/detector.test.js +201 -0
  130. package/dist/lib/config-migration/detector.test.js.map +1 -0
  131. package/dist/lib/config-migration/index.d.ts +29 -0
  132. package/dist/lib/config-migration/index.d.ts.map +1 -0
  133. package/dist/lib/config-migration/index.js +33 -0
  134. package/dist/lib/config-migration/index.js.map +1 -0
  135. package/dist/lib/config-migration/reporter.d.ts +53 -0
  136. package/dist/lib/config-migration/reporter.d.ts.map +1 -0
  137. package/dist/lib/config-migration/reporter.js +257 -0
  138. package/dist/lib/config-migration/reporter.js.map +1 -0
  139. package/dist/lib/config-migration/reporter.test.d.ts +5 -0
  140. package/dist/lib/config-migration/reporter.test.d.ts.map +1 -0
  141. package/dist/lib/config-migration/reporter.test.js +305 -0
  142. package/dist/lib/config-migration/reporter.test.js.map +1 -0
  143. package/dist/lib/config-migration/runner.d.ts +46 -0
  144. package/dist/lib/config-migration/runner.d.ts.map +1 -0
  145. package/dist/lib/config-migration/runner.js +364 -0
  146. package/dist/lib/config-migration/runner.js.map +1 -0
  147. package/dist/lib/config-migration/runner.test.d.ts +5 -0
  148. package/dist/lib/config-migration/runner.test.d.ts.map +1 -0
  149. package/dist/lib/config-migration/runner.test.js +235 -0
  150. package/dist/lib/config-migration/runner.test.js.map +1 -0
  151. package/dist/lib/config-migration/types.d.ts +120 -0
  152. package/dist/lib/config-migration/types.d.ts.map +1 -0
  153. package/dist/lib/config-migration/types.js +70 -0
  154. package/dist/lib/config-migration/types.js.map +1 -0
  155. package/dist/lib/config-validation.d.ts.map +1 -1
  156. package/dist/lib/config-validation.js +6 -0
  157. package/dist/lib/config-validation.js.map +1 -1
  158. package/dist/lib/config-validation.test.js +25 -0
  159. package/dist/lib/config-validation.test.js.map +1 -1
  160. package/dist/lib/config.d.ts +31 -7
  161. package/dist/lib/config.d.ts.map +1 -1
  162. package/dist/lib/config.js +2 -0
  163. package/dist/lib/config.js.map +1 -1
  164. package/dist/lib/config.test.js +3 -15
  165. package/dist/lib/config.test.js.map +1 -1
  166. package/dist/lib/constants.d.ts +12 -4
  167. package/dist/lib/constants.d.ts.map +1 -1
  168. package/dist/lib/constants.js +24 -5
  169. package/dist/lib/constants.js.map +1 -1
  170. package/dist/lib/constants.test.js +88 -29
  171. package/dist/lib/constants.test.js.map +1 -1
  172. package/dist/lib/deprecation.d.ts +18 -0
  173. package/dist/lib/deprecation.d.ts.map +1 -0
  174. package/dist/lib/deprecation.js +28 -0
  175. package/dist/lib/deprecation.js.map +1 -0
  176. package/dist/lib/deprecation.test.d.ts +2 -0
  177. package/dist/lib/deprecation.test.d.ts.map +1 -0
  178. package/dist/lib/deprecation.test.js +71 -0
  179. package/dist/lib/deprecation.test.js.map +1 -0
  180. package/dist/lib/hooks/confirmation.d.ts +49 -0
  181. package/dist/lib/hooks/confirmation.d.ts.map +1 -0
  182. package/dist/lib/hooks/confirmation.js +147 -0
  183. package/dist/lib/hooks/confirmation.js.map +1 -0
  184. package/dist/lib/hooks/confirmation.test.d.ts +7 -0
  185. package/dist/lib/hooks/confirmation.test.d.ts.map +1 -0
  186. package/dist/lib/hooks/confirmation.test.js +300 -0
  187. package/dist/lib/hooks/confirmation.test.js.map +1 -0
  188. package/dist/lib/hooks/executor.d.ts +16 -1
  189. package/dist/lib/hooks/executor.d.ts.map +1 -1
  190. package/dist/lib/hooks/executor.js +53 -4
  191. package/dist/lib/hooks/executor.js.map +1 -1
  192. package/dist/lib/hooks/index.d.ts +4 -2
  193. package/dist/lib/hooks/index.d.ts.map +1 -1
  194. package/dist/lib/hooks/index.js +3 -2
  195. package/dist/lib/hooks/index.js.map +1 -1
  196. package/dist/lib/hooks/types.d.ts +16 -0
  197. package/dist/lib/hooks/types.d.ts.map +1 -1
  198. package/dist/lib/hooks/types.js +12 -0
  199. package/dist/lib/hooks/types.js.map +1 -1
  200. package/dist/lib/logger.d.ts +40 -155
  201. package/dist/lib/logger.d.ts.map +1 -1
  202. package/dist/lib/logger.js +349 -420
  203. package/dist/lib/logger.js.map +1 -1
  204. package/dist/lib/logger.test.d.ts +10 -1
  205. package/dist/lib/logger.test.d.ts.map +1 -1
  206. package/dist/lib/logger.test.js +658 -258
  207. package/dist/lib/logger.test.js.map +1 -1
  208. package/dist/lib/lswt/action-executors.d.ts +2 -0
  209. package/dist/lib/lswt/action-executors.d.ts.map +1 -1
  210. package/dist/lib/lswt/action-executors.js +4 -3
  211. package/dist/lib/lswt/action-executors.js.map +1 -1
  212. package/dist/lib/lswt/action-executors.test.js +7 -0
  213. package/dist/lib/lswt/action-executors.test.js.map +1 -1
  214. package/dist/lib/lswt/args.d.ts.map +1 -1
  215. package/dist/lib/lswt/args.js +15 -1
  216. package/dist/lib/lswt/args.js.map +1 -1
  217. package/dist/lib/lswt/environment.d.ts +21 -2
  218. package/dist/lib/lswt/environment.d.ts.map +1 -1
  219. package/dist/lib/lswt/environment.js +73 -32
  220. package/dist/lib/lswt/environment.js.map +1 -1
  221. package/dist/lib/lswt/environment.test.js +79 -1
  222. package/dist/lib/lswt/environment.test.js.map +1 -1
  223. package/dist/lib/lswt/index.d.ts +1 -0
  224. package/dist/lib/lswt/index.d.ts.map +1 -1
  225. package/dist/lib/lswt/index.js +2 -0
  226. package/dist/lib/lswt/index.js.map +1 -1
  227. package/dist/lib/lswt/table.d.ts +15 -0
  228. package/dist/lib/lswt/table.d.ts.map +1 -0
  229. package/dist/lib/lswt/table.js +61 -0
  230. package/dist/lib/lswt/table.js.map +1 -0
  231. package/dist/lib/lswt/table.test.d.ts +5 -0
  232. package/dist/lib/lswt/table.test.d.ts.map +1 -0
  233. package/dist/lib/lswt/table.test.js +262 -0
  234. package/dist/lib/lswt/table.test.js.map +1 -0
  235. package/dist/lib/lswt/types.d.ts +4 -0
  236. package/dist/lib/lswt/types.d.ts.map +1 -1
  237. package/dist/lib/lswt/worktree-info.d.ts.map +1 -1
  238. package/dist/lib/lswt/worktree-info.js +1 -6
  239. package/dist/lib/lswt/worktree-info.js.map +1 -1
  240. package/dist/lib/lswt/worktree-info.test.js +5 -17
  241. package/dist/lib/lswt/worktree-info.test.js.map +1 -1
  242. package/dist/lib/newpr/args.d.ts.map +1 -1
  243. package/dist/lib/newpr/args.js +36 -1
  244. package/dist/lib/newpr/args.js.map +1 -1
  245. package/dist/lib/newpr/hook-runner.d.ts +11 -0
  246. package/dist/lib/newpr/hook-runner.d.ts.map +1 -1
  247. package/dist/lib/newpr/hook-runner.js +49 -1
  248. package/dist/lib/newpr/hook-runner.js.map +1 -1
  249. package/dist/lib/newpr/hook-runner.test.js +121 -0
  250. package/dist/lib/newpr/hook-runner.test.js.map +1 -1
  251. package/dist/lib/newpr/plan-generator.d.ts +121 -0
  252. package/dist/lib/newpr/plan-generator.d.ts.map +1 -0
  253. package/dist/lib/newpr/plan-generator.js +185 -0
  254. package/dist/lib/newpr/plan-generator.js.map +1 -0
  255. package/dist/lib/newpr/plan-generator.test.d.ts +7 -0
  256. package/dist/lib/newpr/plan-generator.test.d.ts.map +1 -0
  257. package/dist/lib/newpr/plan-generator.test.js +387 -0
  258. package/dist/lib/newpr/plan-generator.test.js.map +1 -0
  259. package/dist/lib/newpr/types.d.ts +12 -0
  260. package/dist/lib/newpr/types.d.ts.map +1 -1
  261. package/dist/lib/prs/actions.d.ts +5 -1
  262. package/dist/lib/prs/actions.d.ts.map +1 -1
  263. package/dist/lib/prs/actions.js +12 -10
  264. package/dist/lib/prs/actions.js.map +1 -1
  265. package/dist/lib/prs/actions.test.js +48 -5
  266. package/dist/lib/prs/actions.test.js.map +1 -1
  267. package/dist/lib/prs/command.d.ts +21 -0
  268. package/dist/lib/prs/command.d.ts.map +1 -0
  269. package/dist/lib/prs/command.js +175 -0
  270. package/dist/lib/prs/command.js.map +1 -0
  271. package/dist/lib/prs/command.test.d.ts +11 -0
  272. package/dist/lib/prs/command.test.d.ts.map +1 -0
  273. package/dist/lib/prs/command.test.js +409 -0
  274. package/dist/lib/prs/command.test.js.map +1 -0
  275. package/dist/lib/prs/interactive.d.ts.map +1 -1
  276. package/dist/lib/prs/interactive.js +15 -2
  277. package/dist/lib/prs/interactive.js.map +1 -1
  278. package/dist/lib/prs/interactive.test.js +153 -0
  279. package/dist/lib/prs/interactive.test.js.map +1 -1
  280. package/dist/lib/prs/types.d.ts +15 -0
  281. package/dist/lib/prs/types.d.ts.map +1 -1
  282. package/dist/lib/ui/error.d.ts +31 -0
  283. package/dist/lib/ui/error.d.ts.map +1 -0
  284. package/dist/lib/ui/error.js +47 -0
  285. package/dist/lib/ui/error.js.map +1 -0
  286. package/dist/lib/ui/error.test.d.ts +2 -0
  287. package/dist/lib/ui/error.test.d.ts.map +1 -0
  288. package/dist/lib/ui/error.test.js +143 -0
  289. package/dist/lib/ui/error.test.js.map +1 -0
  290. package/dist/lib/ui/index.d.ts +15 -0
  291. package/dist/lib/ui/index.d.ts.map +1 -0
  292. package/dist/lib/ui/index.js +19 -0
  293. package/dist/lib/ui/index.js.map +1 -0
  294. package/dist/lib/ui/output.d.ts +18 -0
  295. package/dist/lib/ui/output.d.ts.map +1 -0
  296. package/dist/lib/ui/output.js +31 -0
  297. package/dist/lib/ui/output.js.map +1 -0
  298. package/dist/lib/ui/output.test.d.ts +2 -0
  299. package/dist/lib/ui/output.test.d.ts.map +1 -0
  300. package/dist/lib/ui/output.test.js +59 -0
  301. package/dist/lib/ui/output.test.js.map +1 -0
  302. package/dist/lib/ui/spinner.d.ts +10 -0
  303. package/dist/lib/ui/spinner.d.ts.map +1 -0
  304. package/dist/lib/ui/spinner.js +10 -0
  305. package/dist/lib/ui/spinner.js.map +1 -0
  306. package/dist/lib/ui/status.d.ts +65 -0
  307. package/dist/lib/ui/status.d.ts.map +1 -0
  308. package/dist/lib/ui/status.js +100 -0
  309. package/dist/lib/ui/status.js.map +1 -0
  310. package/dist/lib/ui/status.test.d.ts +2 -0
  311. package/dist/lib/ui/status.test.d.ts.map +1 -0
  312. package/dist/lib/ui/status.test.js +158 -0
  313. package/dist/lib/ui/status.test.js.map +1 -0
  314. package/dist/lib/ui/table.d.ts +39 -0
  315. package/dist/lib/ui/table.d.ts.map +1 -0
  316. package/dist/lib/ui/table.js +45 -0
  317. package/dist/lib/ui/table.js.map +1 -0
  318. package/dist/lib/ui/table.test.d.ts +2 -0
  319. package/dist/lib/ui/table.test.d.ts.map +1 -0
  320. package/dist/lib/ui/table.test.js +115 -0
  321. package/dist/lib/ui/table.test.js.map +1 -0
  322. package/dist/lib/ui/theme.d.ts +34 -0
  323. package/dist/lib/ui/theme.d.ts.map +1 -0
  324. package/dist/lib/ui/theme.js +37 -0
  325. package/dist/lib/ui/theme.js.map +1 -0
  326. package/dist/lib/ui/theme.test.d.ts +2 -0
  327. package/dist/lib/ui/theme.test.d.ts.map +1 -0
  328. package/dist/lib/ui/theme.test.js +76 -0
  329. package/dist/lib/ui/theme.test.js.map +1 -0
  330. package/dist/lib/wtconfig/environment.d.ts +18 -1
  331. package/dist/lib/wtconfig/environment.d.ts.map +1 -1
  332. package/dist/lib/wtconfig/environment.js +60 -24
  333. package/dist/lib/wtconfig/environment.js.map +1 -1
  334. package/dist/lib/wtconfig/environment.test.js +45 -1
  335. package/dist/lib/wtconfig/environment.test.js.map +1 -1
  336. package/dist/lib/wtlink/config-manifest.test.js +26 -0
  337. package/dist/lib/wtlink/config-manifest.test.js.map +1 -1
  338. package/dist/lib/wtlink/link-configs.js +7 -7
  339. package/dist/lib/wtlink/link-configs.js.map +1 -1
  340. package/dist/lib/wtlink/validate-manifest.d.ts.map +1 -1
  341. package/dist/lib/wtlink/validate-manifest.js +5 -5
  342. package/dist/lib/wtlink/validate-manifest.js.map +1 -1
  343. package/dist/lib/wtstate/args.d.ts.map +1 -1
  344. package/dist/lib/wtstate/args.js +2 -0
  345. package/dist/lib/wtstate/args.js.map +1 -1
  346. package/dist/mcp/server.d.ts +2 -1
  347. package/dist/mcp/server.d.ts.map +1 -1
  348. package/dist/mcp/server.js +264 -44
  349. package/dist/mcp/server.js.map +1 -1
  350. package/dist/mcp/server.test.js +111 -0
  351. package/dist/mcp/server.test.js.map +1 -1
  352. package/package.json +3 -1
  353. package/schemas/worktreerc.schema.json +23 -0
@@ -1,292 +1,692 @@
1
1
  /**
2
- * Tests for logger.ts
2
+ * Comprehensive tests for logger.ts (consola-based)
3
+ *
4
+ * Covers:
5
+ * - parseLogLevel mapping
6
+ * - LogLevel compatibility export
7
+ * - initializeLogger level resolution (flag precedence)
8
+ * - DEBUG=newpr deprecation path
9
+ * - AuditFileReporter (write, JSONL, directory creation, rotation)
10
+ * - ConditionalStderrReporter (verbose/non-verbose conditional output)
11
+ * - Process exit handler (synchronous audit summary)
3
12
  */
4
13
  import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
5
14
  import fs from 'fs';
6
15
  import path from 'path';
7
16
  import os from 'os';
8
- import { parseLogLevel, initializeLogger, logger, LogLevel } from './logger.js';
9
- // Get Logger class for reset
10
- const LoggerClass = logger.constructor;
11
- describe('logger', () => {
12
- let tempDir;
17
+ import { parseLogLevel, initializeLogger, logger, LogLevel, setAuditContext, _resetForTesting, } from './logger.js';
18
+ // ---------------------------------------------------------------------------
19
+ // Test-level helpers
20
+ // ---------------------------------------------------------------------------
21
+ /** Saved env vars to restore after each test */
22
+ let savedEnv;
23
+ /** Per-test temp directory */
24
+ let tempDir;
25
+ function createTempDir() {
26
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'logger-test-'));
27
+ }
28
+ function cleanupTempDir(dir) {
29
+ if (dir && fs.existsSync(dir)) {
30
+ fs.rmSync(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
31
+ }
32
+ }
33
+ /**
34
+ * Wait for an audit log file to have non-empty content.
35
+ * Polls every 50ms up to the given timeout (default 2s).
36
+ * WriteStream flush timing varies across platforms/Node versions.
37
+ */
38
+ async function waitForAuditContent(filePath, timeoutMs = 2000) {
39
+ const start = Date.now();
40
+ while (Date.now() - start < timeoutMs) {
41
+ try {
42
+ const content = fs.readFileSync(filePath, 'utf-8');
43
+ if (content.length > 0)
44
+ return content;
45
+ }
46
+ catch {
47
+ // file may not exist yet
48
+ }
49
+ await new Promise((resolve) => setTimeout(resolve, 50));
50
+ }
51
+ // Final attempt — return whatever is there (may be empty, test will fail with a clear message)
52
+ try {
53
+ return fs.readFileSync(filePath, 'utf-8');
54
+ }
55
+ catch {
56
+ return '';
57
+ }
58
+ }
59
+ // ---------------------------------------------------------------------------
60
+ // parseLogLevel
61
+ // ---------------------------------------------------------------------------
62
+ describe('parseLogLevel', () => {
63
+ it('parses "debug" to 4', () => {
64
+ expect(parseLogLevel('debug')).toBe(4);
65
+ });
66
+ it('parses "info" to 3', () => {
67
+ expect(parseLogLevel('info')).toBe(3);
68
+ });
69
+ it('parses "warn" to 1', () => {
70
+ expect(parseLogLevel('warn')).toBe(1);
71
+ });
72
+ it('parses "warning" to 1', () => {
73
+ expect(parseLogLevel('warning')).toBe(1);
74
+ });
75
+ it('parses "error" to 0', () => {
76
+ expect(parseLogLevel('error')).toBe(0);
77
+ });
78
+ it('parses "silent" to -999', () => {
79
+ expect(parseLogLevel('silent')).toBe(-999);
80
+ });
81
+ it('parses "trace" to 5', () => {
82
+ expect(parseLogLevel('trace')).toBe(5);
83
+ });
84
+ it('parses "verbose" to 4 (alias for debug)', () => {
85
+ expect(parseLogLevel('verbose')).toBe(4);
86
+ });
87
+ it('is case insensitive — "DEBUG" returns 4', () => {
88
+ expect(parseLogLevel('DEBUG')).toBe(4);
89
+ });
90
+ it('is case insensitive — mixed case "Error" returns 0', () => {
91
+ expect(parseLogLevel('Error')).toBe(0);
92
+ });
93
+ it('returns undefined for "unknown"', () => {
94
+ expect(parseLogLevel('unknown')).toBeUndefined();
95
+ });
96
+ it('returns undefined for empty string', () => {
97
+ expect(parseLogLevel('')).toBeUndefined();
98
+ });
99
+ it('returns undefined for numeric strings', () => {
100
+ expect(parseLogLevel('99')).toBeUndefined();
101
+ expect(parseLogLevel('3')).toBeUndefined();
102
+ });
103
+ it('trims whitespace', () => {
104
+ expect(parseLogLevel(' info ')).toBe(3);
105
+ expect(parseLogLevel('\tdebug\n')).toBe(4);
106
+ });
107
+ });
108
+ // ---------------------------------------------------------------------------
109
+ // LogLevel compatibility export
110
+ // ---------------------------------------------------------------------------
111
+ describe('LogLevel compatibility export', () => {
112
+ it('LogLevel.SILENT exists and equals -999', () => {
113
+ expect(LogLevel.SILENT).toBe(-999);
114
+ });
115
+ it('LogLevel.ERROR equals 0', () => {
116
+ expect(LogLevel.ERROR).toBe(0);
117
+ });
118
+ it('LogLevel.WARN equals 1', () => {
119
+ expect(LogLevel.WARN).toBe(1);
120
+ });
121
+ it('LogLevel.INFO equals 3', () => {
122
+ expect(LogLevel.INFO).toBe(3);
123
+ });
124
+ it('LogLevel.DEBUG equals 4', () => {
125
+ expect(LogLevel.DEBUG).toBe(4);
126
+ });
127
+ it('LogLevel.TRACE equals 5', () => {
128
+ expect(LogLevel.TRACE).toBe(5);
129
+ });
130
+ });
131
+ // ---------------------------------------------------------------------------
132
+ // Logger singleton
133
+ // ---------------------------------------------------------------------------
134
+ describe('Logger singleton', () => {
135
+ beforeEach(() => {
136
+ _resetForTesting();
137
+ });
138
+ it('returns same instance on repeated access', () => {
139
+ const instance1 = logger;
140
+ const instance2 = logger;
141
+ expect(instance1).toBe(instance2);
142
+ });
143
+ it('defaults to level 3 (INFO) after reset', () => {
144
+ expect(logger.level).toBe(3);
145
+ });
146
+ it('has standard logging methods', () => {
147
+ expect(typeof logger.error).toBe('function');
148
+ expect(typeof logger.warn).toBe('function');
149
+ expect(typeof logger.info).toBe('function');
150
+ expect(typeof logger.debug).toBe('function');
151
+ expect(typeof logger.trace).toBe('function');
152
+ });
153
+ });
154
+ // ---------------------------------------------------------------------------
155
+ // initializeLogger — level resolution
156
+ // ---------------------------------------------------------------------------
157
+ describe('initializeLogger level resolution', () => {
13
158
  beforeEach(() => {
14
- // Reset logger singleton
15
- LoggerClass.reset();
16
- // Clear env vars
159
+ _resetForTesting();
160
+ savedEnv = {
161
+ GWT_LOG_LEVEL: process.env.GWT_LOG_LEVEL,
162
+ DEBUG: process.env.DEBUG,
163
+ NO_COLOR: process.env.NO_COLOR,
164
+ };
17
165
  delete process.env.GWT_LOG_LEVEL;
18
- delete process.env.GWT_LOG_FILE;
19
- // Create temp dir for file tests
20
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'logger-test-'));
166
+ delete process.env.DEBUG;
21
167
  });
22
168
  afterEach(() => {
23
- // Reset logger
24
- LoggerClass.reset();
25
- // Clean up temp dir
26
- if (tempDir && fs.existsSync(tempDir)) {
27
- fs.rmSync(tempDir, { recursive: true, force: true });
169
+ _resetForTesting();
170
+ for (const [key, value] of Object.entries(savedEnv)) {
171
+ if (value === undefined) {
172
+ delete process.env[key];
173
+ }
174
+ else {
175
+ process.env[key] = value;
176
+ }
28
177
  }
29
178
  });
30
- describe('parseLogLevel', () => {
31
- it('parses string log levels', () => {
32
- expect(parseLogLevel('silent')).toBe(LogLevel.SILENT);
33
- expect(parseLogLevel('error')).toBe(LogLevel.ERROR);
34
- expect(parseLogLevel('warn')).toBe(LogLevel.WARN);
35
- expect(parseLogLevel('warning')).toBe(LogLevel.WARN);
36
- expect(parseLogLevel('info')).toBe(LogLevel.INFO);
37
- expect(parseLogLevel('debug')).toBe(LogLevel.DEBUG);
38
- expect(parseLogLevel('trace')).toBe(LogLevel.TRACE);
39
- expect(parseLogLevel('verbose')).toBe(LogLevel.DEBUG);
40
- });
41
- it('is case insensitive', () => {
42
- expect(parseLogLevel('SILENT')).toBe(LogLevel.SILENT);
43
- expect(parseLogLevel('Error')).toBe(LogLevel.ERROR);
44
- expect(parseLogLevel('DEBUG')).toBe(LogLevel.DEBUG);
45
- });
46
- it('parses numeric log levels', () => {
47
- expect(parseLogLevel('0')).toBe(LogLevel.SILENT);
48
- expect(parseLogLevel('1')).toBe(LogLevel.ERROR);
49
- expect(parseLogLevel('2')).toBe(LogLevel.WARN);
50
- expect(parseLogLevel('3')).toBe(LogLevel.INFO);
51
- expect(parseLogLevel('4')).toBe(LogLevel.DEBUG);
52
- expect(parseLogLevel('5')).toBe(LogLevel.TRACE);
53
- });
54
- it('returns undefined for invalid levels', () => {
55
- expect(parseLogLevel('invalid')).toBeUndefined();
56
- expect(parseLogLevel('99')).toBeUndefined();
57
- expect(parseLogLevel('')).toBeUndefined();
58
- });
59
- it('trims whitespace', () => {
60
- expect(parseLogLevel(' info ')).toBe(LogLevel.INFO);
61
- });
179
+ it('quiet flag sets level to 0 (error only) regardless of env', () => {
180
+ process.env.GWT_LOG_LEVEL = 'debug';
181
+ initializeLogger({ quiet: true });
182
+ expect(logger.level).toBe(0);
62
183
  });
63
- describe('Logger singleton', () => {
64
- it('returns same instance', () => {
65
- const instance1 = logger;
66
- const instance2 = logger;
67
- expect(instance1).toBe(instance2);
68
- });
69
- it('defaults to INFO level', () => {
70
- logger.initialize({});
71
- expect(logger.getLevel()).toBe(LogLevel.INFO);
72
- });
73
- it('sets level from config', () => {
74
- logger.initialize({ level: LogLevel.DEBUG });
75
- expect(logger.getLevel()).toBe(LogLevel.DEBUG);
76
- });
77
- it('respects environment variable', () => {
78
- process.env.GWT_LOG_LEVEL = 'debug';
79
- logger.initialize({});
80
- expect(logger.getLevel()).toBe(LogLevel.DEBUG);
81
- });
82
- it('prefers config over environment', () => {
83
- process.env.GWT_LOG_LEVEL = 'debug';
84
- logger.initialize({ level: LogLevel.ERROR });
85
- expect(logger.getLevel()).toBe(LogLevel.ERROR);
86
- });
184
+ it('verbose flag sets level to 4 (debug)', () => {
185
+ initializeLogger({ verbose: true });
186
+ expect(logger.level).toBe(4);
87
187
  });
88
- describe('log level checks', () => {
89
- it('isLevelEnabled works correctly', () => {
90
- logger.initialize({ level: LogLevel.WARN });
91
- expect(logger.isLevelEnabled(LogLevel.ERROR)).toBe(true);
92
- expect(logger.isLevelEnabled(LogLevel.WARN)).toBe(true);
93
- expect(logger.isLevelEnabled(LogLevel.INFO)).toBe(false);
94
- expect(logger.isLevelEnabled(LogLevel.DEBUG)).toBe(false);
95
- });
96
- it('isDebug returns true for DEBUG and higher', () => {
97
- logger.initialize({ level: LogLevel.INFO });
98
- expect(logger.isDebug()).toBe(false);
99
- logger.setLevel(LogLevel.DEBUG);
100
- expect(logger.isDebug()).toBe(true);
101
- logger.setLevel(LogLevel.TRACE);
102
- expect(logger.isDebug()).toBe(true);
103
- });
104
- it('isTrace returns true only for TRACE', () => {
105
- logger.initialize({ level: LogLevel.DEBUG });
106
- expect(logger.isTrace()).toBe(false);
107
- logger.setLevel(LogLevel.TRACE);
108
- expect(logger.isTrace()).toBe(true);
109
- });
188
+ it('GWT_LOG_LEVEL=debug with no flags sets level to 4', () => {
189
+ process.env.GWT_LOG_LEVEL = 'debug';
190
+ initializeLogger({});
191
+ expect(logger.level).toBe(4);
110
192
  });
111
- describe('logging methods', () => {
112
- let consoleLogSpy;
113
- let consoleWarnSpy;
114
- let consoleErrorSpy;
115
- beforeEach(() => {
116
- consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
117
- consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
118
- consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
119
- });
120
- afterEach(() => {
121
- consoleLogSpy.mockRestore();
122
- consoleWarnSpy.mockRestore();
123
- consoleErrorSpy.mockRestore();
124
- });
125
- it('info logs at INFO level', () => {
126
- logger.initialize({ level: LogLevel.INFO, timestamps: false, colors: false });
127
- logger.info('test message');
128
- expect(consoleLogSpy).toHaveBeenCalled();
129
- expect(consoleLogSpy.mock.calls[0][0]).toContain('test message');
130
- });
131
- it('debug logs at DEBUG level', () => {
132
- logger.initialize({ level: LogLevel.DEBUG, timestamps: false, colors: false });
133
- logger.debug('debug message');
134
- expect(consoleLogSpy).toHaveBeenCalled();
135
- expect(consoleLogSpy.mock.calls[0][0]).toContain('debug message');
136
- });
137
- it('debug does not log at INFO level', () => {
138
- logger.initialize({ level: LogLevel.INFO, timestamps: false, colors: false });
139
- logger.debug('debug message');
140
- expect(consoleLogSpy).not.toHaveBeenCalledWith(expect.stringContaining('debug message'));
141
- });
142
- it('warn uses console.warn', () => {
143
- logger.initialize({ level: LogLevel.WARN, timestamps: false, colors: false });
144
- logger.warn('warning message');
145
- expect(consoleWarnSpy).toHaveBeenCalled();
146
- expect(consoleWarnSpy.mock.calls[0][0]).toContain('warning message');
147
- });
148
- it('error uses console.error', () => {
149
- logger.initialize({ level: LogLevel.ERROR, timestamps: false, colors: false });
150
- logger.error('error message');
151
- expect(consoleErrorSpy).toHaveBeenCalled();
152
- expect(consoleErrorSpy.mock.calls[0][0]).toContain('error message');
153
- });
154
- it('formats messages with placeholders', () => {
155
- logger.initialize({ level: LogLevel.INFO, timestamps: false, colors: false });
156
- // Test basic %s placeholder
157
- logger.info('Hello %s', 'world');
158
- expect(consoleLogSpy).toHaveBeenCalled();
159
- expect(consoleLogSpy.mock.calls[0][0]).toContain('Hello world');
160
- });
161
- it('appends extra arguments', () => {
162
- logger.initialize({ level: LogLevel.INFO, timestamps: false, colors: false });
163
- logger.info('Message', 'extra1', 'extra2');
164
- expect(consoleLogSpy).toHaveBeenCalled();
165
- expect(consoleLogSpy.mock.calls[0][0]).toContain('extra1');
166
- expect(consoleLogSpy.mock.calls[0][0]).toContain('extra2');
167
- });
193
+ it('GWT_LOG_LEVEL=warn with no flags sets level to 1', () => {
194
+ process.env.GWT_LOG_LEVEL = 'warn';
195
+ initializeLogger({});
196
+ expect(logger.level).toBe(1);
168
197
  });
169
- describe('file logging', () => {
170
- it('writes to log file', async () => {
171
- const logFile = path.join(tempDir, 'test.log');
172
- logger.initialize({
173
- level: LogLevel.INFO,
174
- logFile,
175
- consoleOutput: false,
176
- });
177
- logger.info('file log message');
178
- // Wait for write to complete
179
- await logger.close();
180
- const content = fs.readFileSync(logFile, 'utf8');
181
- expect(content).toContain('file log message');
182
- });
183
- it('creates log file directory if needed', async () => {
184
- const logFile = path.join(tempDir, 'subdir', 'test.log');
185
- logger.initialize({
186
- level: LogLevel.INFO,
187
- logFile,
188
- consoleOutput: false,
189
- });
190
- logger.info('nested log message');
191
- await logger.close();
192
- expect(fs.existsSync(logFile)).toBe(true);
193
- });
194
- it('writes JSON format to file', async () => {
195
- const logFile = path.join(tempDir, 'json.log');
196
- logger.initialize({
197
- level: LogLevel.INFO,
198
- logFile,
199
- consoleOutput: false,
200
- });
201
- logger.info('json message');
202
- await logger.close();
203
- const content = fs.readFileSync(logFile, 'utf8');
204
- const entry = JSON.parse(content.trim());
205
- expect(entry.level).toBe('INFO');
206
- expect(entry.message).toBe('json message');
207
- expect(entry.timestamp).toBeDefined();
208
- });
198
+ it('quiet flag overrides GWT_LOG_LEVEL=debug (CLI flag wins)', () => {
199
+ process.env.GWT_LOG_LEVEL = 'debug';
200
+ initializeLogger({ quiet: true });
201
+ expect(logger.level).toBe(0);
209
202
  });
210
- describe('errorWithStack', () => {
211
- it('logs error message and stack trace via error method', () => {
212
- // This tests that errorWithStack calls error() internally
213
- const err = new Error('Test error');
214
- err.stack = 'Error: Test error\n at test.ts:1:1';
215
- // The errorWithStack method combines the optional message with the error message
216
- const msg = `Something went wrong: ${err.message}`;
217
- expect(msg).toBe('Something went wrong: Test error');
218
- });
219
- it('errorWithStack method exists and is callable', () => {
220
- expect(typeof logger.errorWithStack).toBe('function');
221
- const err = new Error('Test');
222
- // Should not throw
223
- expect(() => {
224
- logger.setLevel(LogLevel.SILENT); // Suppress output
225
- logger.errorWithStack(err);
226
- }).not.toThrow();
227
- });
203
+ it('no flags and no env var defaults to level 3 (INFO)', () => {
204
+ initializeLogger({});
205
+ expect(logger.level).toBe(3);
228
206
  });
229
- describe('child logger', () => {
230
- it('creates child logger with context', () => {
231
- const child = logger.child('TestContext');
232
- expect(child).toBeDefined();
233
- });
234
- it('child logger has all logging methods', () => {
235
- const child = logger.child('TestContext');
236
- expect(typeof child.info).toBe('function');
237
- expect(typeof child.debug).toBe('function');
238
- expect(typeof child.warn).toBe('function');
239
- expect(typeof child.error).toBe('function');
240
- expect(typeof child.trace).toBe('function');
241
- });
242
- it('child logger methods are callable without throwing', () => {
243
- logger.setLevel(LogLevel.SILENT); // Suppress output
244
- const child = logger.child('TestContext');
245
- expect(() => {
246
- child.info('test');
247
- child.debug('test');
248
- child.warn('test');
249
- child.error('test');
250
- child.trace('test');
251
- }).not.toThrow();
207
+ it('quiet flag takes priority over verbose when both are set', () => {
208
+ initializeLogger({ quiet: true, verbose: true });
209
+ expect(logger.level).toBe(0);
210
+ });
211
+ it('GWT_LOG_LEVEL with invalid value falls back to INFO', () => {
212
+ process.env.GWT_LOG_LEVEL = 'bananas';
213
+ initializeLogger({});
214
+ expect(logger.level).toBe(3);
215
+ });
216
+ it('verbose flag overrides GWT_LOG_LEVEL=warn (CLI flag wins)', () => {
217
+ process.env.GWT_LOG_LEVEL = 'warn';
218
+ initializeLogger({ verbose: true });
219
+ expect(logger.level).toBe(4);
220
+ });
221
+ it('sets reporters when called', () => {
222
+ initializeLogger({});
223
+ expect(logger.options.reporters).toBeDefined();
224
+ expect(logger.options.reporters.length).toBeGreaterThan(0);
225
+ });
226
+ });
227
+ // ---------------------------------------------------------------------------
228
+ // DEBUG=newpr deprecation path
229
+ // ---------------------------------------------------------------------------
230
+ describe('DEBUG=newpr deprecation path', () => {
231
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
232
+ let stderrSpy;
233
+ beforeEach(() => {
234
+ _resetForTesting();
235
+ savedEnv = {
236
+ GWT_LOG_LEVEL: process.env.GWT_LOG_LEVEL,
237
+ DEBUG: process.env.DEBUG,
238
+ };
239
+ delete process.env.GWT_LOG_LEVEL;
240
+ delete process.env.DEBUG;
241
+ stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
242
+ });
243
+ afterEach(() => {
244
+ stderrSpy.mockRestore();
245
+ _resetForTesting();
246
+ for (const [key, value] of Object.entries(savedEnv)) {
247
+ if (value === undefined) {
248
+ delete process.env[key];
249
+ }
250
+ else {
251
+ process.env[key] = value;
252
+ }
253
+ }
254
+ });
255
+ it('DEBUG=newpr sets level to 4 (debug) and prints deprecation warning', () => {
256
+ process.env.DEBUG = 'newpr';
257
+ initializeLogger({});
258
+ expect(logger.level).toBe(4);
259
+ expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('WARNING: DEBUG=newpr is deprecated'));
260
+ });
261
+ it('DEBUG=newpr deprecation warning fires exactly once across multiple calls', () => {
262
+ process.env.DEBUG = 'newpr';
263
+ initializeLogger({});
264
+ initializeLogger({});
265
+ const deprecationCalls = stderrSpy.mock.calls.filter((call) => typeof call[0] === 'string' && call[0].includes('deprecated'));
266
+ expect(deprecationCalls).toHaveLength(1);
267
+ });
268
+ it('DEBUG=* activates debug level and prints deprecation warning', () => {
269
+ process.env.DEBUG = '*';
270
+ initializeLogger({});
271
+ expect(logger.level).toBe(4);
272
+ expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('deprecated'));
273
+ });
274
+ it('DEBUG=1 activates debug level and prints deprecation warning', () => {
275
+ process.env.DEBUG = '1';
276
+ initializeLogger({});
277
+ expect(logger.level).toBe(4);
278
+ expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('deprecated'));
279
+ });
280
+ it('DEBUG=something_else does NOT activate debug — level stays at default', () => {
281
+ process.env.DEBUG = 'something_else';
282
+ initializeLogger({});
283
+ expect(logger.level).toBe(3);
284
+ const deprecationCalls = stderrSpy.mock.calls.filter((call) => typeof call[0] === 'string' && call[0].includes('deprecated'));
285
+ expect(deprecationCalls).toHaveLength(0);
286
+ });
287
+ it('GWT_LOG_LEVEL takes priority over DEBUG=newpr', () => {
288
+ process.env.GWT_LOG_LEVEL = 'warn';
289
+ process.env.DEBUG = 'newpr';
290
+ initializeLogger({});
291
+ expect(logger.level).toBe(1);
292
+ });
293
+ });
294
+ // ---------------------------------------------------------------------------
295
+ // _resetForTesting
296
+ // ---------------------------------------------------------------------------
297
+ describe('_resetForTesting', () => {
298
+ it('resets logger level to INFO (3)', () => {
299
+ logger.level = 0;
300
+ _resetForTesting();
301
+ expect(logger.level).toBe(3);
302
+ });
303
+ it('clears reporters', () => {
304
+ initializeLogger({ verbose: true });
305
+ _resetForTesting();
306
+ expect(logger.options.reporters).toEqual([]);
307
+ });
308
+ it('resets deprecation warning flag so it can fire again', () => {
309
+ const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
310
+ const origDebug = process.env.DEBUG;
311
+ const origGwt = process.env.GWT_LOG_LEVEL;
312
+ delete process.env.GWT_LOG_LEVEL;
313
+ process.env.DEBUG = 'newpr';
314
+ initializeLogger({});
315
+ _resetForTesting();
316
+ delete process.env.GWT_LOG_LEVEL;
317
+ process.env.DEBUG = 'newpr';
318
+ initializeLogger({});
319
+ const deprecationCalls = stderrSpy.mock.calls.filter((call) => typeof call[0] === 'string' && call[0].includes('deprecated'));
320
+ expect(deprecationCalls).toHaveLength(2);
321
+ stderrSpy.mockRestore();
322
+ if (origDebug === undefined)
323
+ delete process.env.DEBUG;
324
+ else
325
+ process.env.DEBUG = origDebug;
326
+ if (origGwt === undefined)
327
+ delete process.env.GWT_LOG_LEVEL;
328
+ else
329
+ process.env.GWT_LOG_LEVEL = origGwt;
330
+ });
331
+ });
332
+ // ---------------------------------------------------------------------------
333
+ // AuditFileReporter
334
+ // ---------------------------------------------------------------------------
335
+ describe('AuditFileReporter', () => {
336
+ beforeEach(() => {
337
+ _resetForTesting();
338
+ tempDir = createTempDir();
339
+ savedEnv = {
340
+ GWT_LOG_LEVEL: process.env.GWT_LOG_LEVEL,
341
+ DEBUG: process.env.DEBUG,
342
+ };
343
+ delete process.env.GWT_LOG_LEVEL;
344
+ delete process.env.DEBUG;
345
+ });
346
+ afterEach(() => {
347
+ _resetForTesting();
348
+ cleanupTempDir(tempDir);
349
+ for (const [key, value] of Object.entries(savedEnv)) {
350
+ if (value === undefined) {
351
+ delete process.env[key];
352
+ }
353
+ else {
354
+ process.env[key] = value;
355
+ }
356
+ }
357
+ });
358
+ it('writes a log entry to the audit log file', async () => {
359
+ const constantsMod = await import('./constants.js');
360
+ vi.spyOn(constantsMod, 'getGlobalDataDir').mockReturnValue(tempDir);
361
+ const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
362
+ initializeLogger({ commandName: 'test-cmd' });
363
+ logger.info('test audit message');
364
+ const auditPath = path.join(tempDir, 'audit.log');
365
+ const content = await waitForAuditContent(auditPath);
366
+ expect(content).toContain('test audit message');
367
+ stderrSpy.mockRestore();
368
+ vi.mocked(constantsMod.getGlobalDataDir).mockRestore();
369
+ });
370
+ it('audit log entries contain timestamp, level, and message', async () => {
371
+ const constantsMod = await import('./constants.js');
372
+ vi.spyOn(constantsMod, 'getGlobalDataDir').mockReturnValue(tempDir);
373
+ const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
374
+ initializeLogger({ commandName: 'test-cmd' });
375
+ logger.warn('something went wrong');
376
+ const auditPath = path.join(tempDir, 'audit.log');
377
+ const content = await waitForAuditContent(auditPath);
378
+ expect(content).toMatch(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
379
+ expect(content).toMatch(/WARN/);
380
+ expect(content).toContain('something went wrong');
381
+ stderrSpy.mockRestore();
382
+ vi.mocked(constantsMod.getGlobalDataDir).mockRestore();
383
+ });
384
+ it('writes JSONL entries when json mode is active', async () => {
385
+ const constantsMod = await import('./constants.js');
386
+ vi.spyOn(constantsMod, 'getGlobalDataDir').mockReturnValue(tempDir);
387
+ const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
388
+ initializeLogger({ json: true, commandName: 'test-cmd' });
389
+ logger.info('json test message');
390
+ const auditPath = path.join(tempDir, 'audit.log');
391
+ const content = (await waitForAuditContent(auditPath)).trim();
392
+ const lines = content.split('\n').filter((l) => l.trim().length > 0);
393
+ for (const line of lines) {
394
+ const parsed = JSON.parse(line);
395
+ expect(parsed).toHaveProperty('timestamp');
396
+ expect(parsed).toHaveProperty('level');
397
+ expect(parsed).toHaveProperty('message');
398
+ }
399
+ const hasMessage = lines.some((line) => {
400
+ const parsed = JSON.parse(line);
401
+ return parsed.message.includes('json test message');
252
402
  });
403
+ expect(hasMessage).toBe(true);
404
+ stderrSpy.mockRestore();
405
+ vi.mocked(constantsMod.getGlobalDataDir).mockRestore();
253
406
  });
254
- describe('initializeLogger', () => {
255
- let consoleLogSpy;
407
+ it('creates audit directory automatically if it does not exist', async () => {
408
+ const nestedDir = path.join(tempDir, 'nested', 'subdir');
409
+ const constantsMod = await import('./constants.js');
410
+ vi.spyOn(constantsMod, 'getGlobalDataDir').mockReturnValue(nestedDir);
411
+ const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
412
+ expect(fs.existsSync(nestedDir)).toBe(false);
413
+ initializeLogger({ commandName: 'test-cmd' });
414
+ expect(fs.existsSync(nestedDir)).toBe(true);
415
+ stderrSpy.mockRestore();
416
+ vi.mocked(constantsMod.getGlobalDataDir).mockRestore();
417
+ });
418
+ });
419
+ // ---------------------------------------------------------------------------
420
+ // Rotation
421
+ // ---------------------------------------------------------------------------
422
+ describe('Audit log rotation', () => {
423
+ beforeEach(() => {
424
+ _resetForTesting();
425
+ tempDir = createTempDir();
426
+ savedEnv = {
427
+ GWT_LOG_LEVEL: process.env.GWT_LOG_LEVEL,
428
+ DEBUG: process.env.DEBUG,
429
+ };
430
+ delete process.env.GWT_LOG_LEVEL;
431
+ delete process.env.DEBUG;
432
+ });
433
+ afterEach(() => {
434
+ _resetForTesting();
435
+ cleanupTempDir(tempDir);
436
+ for (const [key, value] of Object.entries(savedEnv)) {
437
+ if (value === undefined) {
438
+ delete process.env[key];
439
+ }
440
+ else {
441
+ process.env[key] = value;
442
+ }
443
+ }
444
+ });
445
+ it('rotates a file larger than 10MB to audit.log.1', async () => {
446
+ const constantsMod = await import('./constants.js');
447
+ vi.spyOn(constantsMod, 'getGlobalDataDir').mockReturnValue(tempDir);
448
+ const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
449
+ const auditPath = path.join(tempDir, 'audit.log');
450
+ const bigContent = 'x'.repeat(10 * 1024 * 1024 + 100);
451
+ fs.writeFileSync(auditPath, bigContent);
452
+ initializeLogger({ commandName: 'test-cmd' });
453
+ expect(fs.existsSync(path.join(tempDir, 'audit.log.1'))).toBe(true);
454
+ const rotatedContent = fs.readFileSync(path.join(tempDir, 'audit.log.1'), 'utf-8');
455
+ expect(rotatedContent).toBe(bigContent);
456
+ logger.info('post-rotation entry');
457
+ await waitForAuditContent(auditPath);
458
+ expect(fs.existsSync(auditPath)).toBe(true);
459
+ const newSize = fs.statSync(auditPath).size;
460
+ expect(newSize).toBeLessThan(10 * 1024 * 1024);
461
+ stderrSpy.mockRestore();
462
+ vi.mocked(constantsMod.getGlobalDataDir).mockRestore();
463
+ });
464
+ it('shifts existing rotated files: .1 becomes .2, original becomes .1, oldest deleted', async () => {
465
+ const constantsMod = await import('./constants.js');
466
+ vi.spyOn(constantsMod, 'getGlobalDataDir').mockReturnValue(tempDir);
467
+ const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
468
+ const auditPath = path.join(tempDir, 'audit.log');
469
+ const bigContent = 'x'.repeat(10 * 1024 * 1024 + 100);
470
+ fs.writeFileSync(auditPath, bigContent);
471
+ fs.writeFileSync(auditPath + '.1', 'rotated-1-content');
472
+ fs.writeFileSync(auditPath + '.2', 'rotated-2-content-to-be-deleted');
473
+ initializeLogger({ commandName: 'test-cmd' });
474
+ expect(fs.readFileSync(auditPath + '.2', 'utf-8')).toBe('rotated-1-content');
475
+ expect(fs.readFileSync(auditPath + '.1', 'utf-8')).toBe(bigContent);
476
+ stderrSpy.mockRestore();
477
+ vi.mocked(constantsMod.getGlobalDataDir).mockRestore();
478
+ });
479
+ it('rotation failure does not crash the tool', async () => {
480
+ const constantsMod = await import('./constants.js');
481
+ const badDir = path.join(tempDir, 'readonly-test');
482
+ fs.mkdirSync(badDir);
483
+ const auditPath = path.join(badDir, 'audit.log');
484
+ const bigContent = 'x'.repeat(10 * 1024 * 1024 + 100);
485
+ fs.writeFileSync(auditPath, bigContent);
486
+ fs.chmodSync(badDir, 0o444);
487
+ vi.spyOn(constantsMod, 'getGlobalDataDir').mockReturnValue(badDir);
488
+ const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
489
+ expect(() => initializeLogger({ commandName: 'test-cmd' })).not.toThrow();
490
+ fs.chmodSync(badDir, 0o755);
491
+ stderrSpy.mockRestore();
492
+ vi.mocked(constantsMod.getGlobalDataDir).mockRestore();
493
+ });
494
+ });
495
+ // ---------------------------------------------------------------------------
496
+ // ConditionalStderrReporter
497
+ // ---------------------------------------------------------------------------
498
+ describe('ConditionalStderrReporter', () => {
499
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
500
+ let stderrSpy;
501
+ beforeEach(() => {
502
+ _resetForTesting();
503
+ savedEnv = {
504
+ GWT_LOG_LEVEL: process.env.GWT_LOG_LEVEL,
505
+ DEBUG: process.env.DEBUG,
506
+ };
507
+ delete process.env.GWT_LOG_LEVEL;
508
+ delete process.env.DEBUG;
509
+ stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
510
+ });
511
+ afterEach(() => {
512
+ stderrSpy.mockRestore();
513
+ _resetForTesting();
514
+ for (const [key, value] of Object.entries(savedEnv)) {
515
+ if (value === undefined) {
516
+ delete process.env[key];
517
+ }
518
+ else {
519
+ process.env[key] = value;
520
+ }
521
+ }
522
+ });
523
+ describe('non-verbose mode (default)', () => {
256
524
  beforeEach(() => {
257
- consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
525
+ initializeLogger({});
258
526
  });
259
- afterEach(() => {
260
- consoleLogSpy.mockRestore();
527
+ it('error writes to stderr', () => {
528
+ logger.error('err-msg');
529
+ const errorCalls = stderrSpy.mock.calls.filter((call) => typeof call[0] === 'string' && call[0].includes('err-msg'));
530
+ expect(errorCalls.length).toBeGreaterThan(0);
261
531
  });
262
- it('sets quiet mode to SILENT', () => {
263
- initializeLogger({ quiet: true });
264
- expect(logger.getLevel()).toBe(LogLevel.SILENT);
532
+ it('warn writes to stderr', () => {
533
+ logger.warn('wrn-msg');
534
+ const warnCalls = stderrSpy.mock.calls.filter((call) => typeof call[0] === 'string' && call[0].includes('wrn-msg'));
535
+ expect(warnCalls.length).toBeGreaterThan(0);
265
536
  });
266
- it('sets debug mode to DEBUG', () => {
267
- initializeLogger({ debug: true });
268
- expect(logger.getLevel()).toBe(LogLevel.DEBUG);
537
+ it('info does NOT write to stderr', () => {
538
+ logger.info('inf-msg');
539
+ const infoCalls = stderrSpy.mock.calls.filter((call) => typeof call[0] === 'string' && call[0].includes('inf-msg'));
540
+ expect(infoCalls.length).toBe(0);
269
541
  });
270
- it('sets verbose mode to DEBUG', () => {
542
+ it('debug does NOT write to stderr', () => {
543
+ logger.debug('dbg-msg');
544
+ const debugCalls = stderrSpy.mock.calls.filter((call) => typeof call[0] === 'string' && call[0].includes('dbg-msg'));
545
+ expect(debugCalls.length).toBe(0);
546
+ });
547
+ });
548
+ describe('verbose mode', () => {
549
+ beforeEach(() => {
271
550
  initializeLogger({ verbose: true });
272
- expect(logger.getLevel()).toBe(LogLevel.DEBUG);
273
551
  });
274
- it('sets double verbose (-vv) to TRACE', () => {
275
- initializeLogger({ verbose: 2 });
276
- expect(logger.getLevel()).toBe(LogLevel.TRACE);
552
+ it('error writes to stderr', () => {
553
+ logger.error('err-verbose');
554
+ const errorCalls = stderrSpy.mock.calls.filter((call) => typeof call[0] === 'string' && call[0].includes('err-verbose'));
555
+ expect(errorCalls.length).toBeGreaterThan(0);
277
556
  });
278
- it('respects config log level', () => {
279
- initializeLogger({ configLogLevel: 'warn' });
280
- expect(logger.getLevel()).toBe(LogLevel.WARN);
557
+ it('warn writes to stderr', () => {
558
+ logger.warn('wrn-verbose');
559
+ const warnCalls = stderrSpy.mock.calls.filter((call) => typeof call[0] === 'string' && call[0].includes('wrn-verbose'));
560
+ expect(warnCalls.length).toBeGreaterThan(0);
281
561
  });
282
- it('CLI flags override config', () => {
283
- initializeLogger({ verbose: true, configLogLevel: 'warn' });
284
- expect(logger.getLevel()).toBe(LogLevel.DEBUG);
562
+ it('info writes to stderr in verbose mode', () => {
563
+ logger.info('inf-verbose');
564
+ const infoCalls = stderrSpy.mock.calls.filter((call) => typeof call[0] === 'string' && call[0].includes('inf-verbose'));
565
+ expect(infoCalls.length).toBeGreaterThan(0);
285
566
  });
286
- it('quiet overrides verbose', () => {
287
- initializeLogger({ quiet: true, verbose: true });
288
- expect(logger.getLevel()).toBe(LogLevel.SILENT);
567
+ it('debug writes to stderr in verbose mode', () => {
568
+ logger.debug('dbg-verbose');
569
+ const debugCalls = stderrSpy.mock.calls.filter((call) => typeof call[0] === 'string' && call[0].includes('dbg-verbose'));
570
+ expect(debugCalls.length).toBeGreaterThan(0);
289
571
  });
290
572
  });
291
573
  });
574
+ // ---------------------------------------------------------------------------
575
+ // Process exit handler / audit session summary
576
+ // ---------------------------------------------------------------------------
577
+ describe('Process exit handler', () => {
578
+ beforeEach(() => {
579
+ _resetForTesting();
580
+ tempDir = createTempDir();
581
+ savedEnv = {
582
+ GWT_LOG_LEVEL: process.env.GWT_LOG_LEVEL,
583
+ DEBUG: process.env.DEBUG,
584
+ };
585
+ delete process.env.GWT_LOG_LEVEL;
586
+ delete process.env.DEBUG;
587
+ });
588
+ afterEach(() => {
589
+ _resetForTesting();
590
+ cleanupTempDir(tempDir);
591
+ for (const [key, value] of Object.entries(savedEnv)) {
592
+ if (value === undefined) {
593
+ delete process.env[key];
594
+ }
595
+ else {
596
+ process.env[key] = value;
597
+ }
598
+ }
599
+ });
600
+ it('exit handler writes audit summary via fs.appendFileSync', async () => {
601
+ const constantsMod = await import('./constants.js');
602
+ vi.spyOn(constantsMod, 'getGlobalDataDir').mockReturnValue(tempDir);
603
+ const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
604
+ const appendSpy = vi.spyOn(fs, 'appendFileSync');
605
+ initializeLogger({ commandName: 'test-exit' });
606
+ setAuditContext({ prNumber: 42 });
607
+ process.emit('exit', 0);
608
+ expect(appendSpy).toHaveBeenCalled();
609
+ const lastCall = appendSpy.mock.calls.find((call) => typeof call[1] === 'string' && call[1].includes('test-exit'));
610
+ expect(lastCall).toBeDefined();
611
+ const writtenContent = lastCall[1];
612
+ expect(writtenContent).toContain('test-exit');
613
+ expect(writtenContent).toContain('exit=0');
614
+ appendSpy.mockRestore();
615
+ stderrSpy.mockRestore();
616
+ vi.mocked(constantsMod.getGlobalDataDir).mockRestore();
617
+ });
618
+ it('exit handler includes duration as a positive number', async () => {
619
+ const constantsMod = await import('./constants.js');
620
+ vi.spyOn(constantsMod, 'getGlobalDataDir').mockReturnValue(tempDir);
621
+ const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
622
+ const appendSpy = vi.spyOn(fs, 'appendFileSync');
623
+ initializeLogger({ commandName: 'duration-test' });
624
+ await new Promise((resolve) => setTimeout(resolve, 50));
625
+ process.emit('exit', 0);
626
+ const exitCall = appendSpy.mock.calls.find((call) => typeof call[1] === 'string' && call[1].includes('duration-test'));
627
+ expect(exitCall).toBeDefined();
628
+ const writtenContent = exitCall[1];
629
+ const durationMatch = writtenContent.match(/duration=(\d+)ms/);
630
+ expect(durationMatch).toBeTruthy();
631
+ const duration = parseInt(durationMatch[1], 10);
632
+ expect(duration).toBeGreaterThanOrEqual(0);
633
+ appendSpy.mockRestore();
634
+ stderrSpy.mockRestore();
635
+ vi.mocked(constantsMod.getGlobalDataDir).mockRestore();
636
+ });
637
+ it('exit handler writes JSON format when json mode is active', async () => {
638
+ const constantsMod = await import('./constants.js');
639
+ vi.spyOn(constantsMod, 'getGlobalDataDir').mockReturnValue(tempDir);
640
+ const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
641
+ const appendSpy = vi.spyOn(fs, 'appendFileSync');
642
+ initializeLogger({ json: true, commandName: 'json-exit-test' });
643
+ setAuditContext({ prNumber: 99 });
644
+ process.emit('exit', 1);
645
+ const exitCall = appendSpy.mock.calls.find((call) => typeof call[1] === 'string' && call[1].includes('json-exit-test'));
646
+ expect(exitCall).toBeDefined();
647
+ const writtenContent = exitCall[1].trim();
648
+ const parsed = JSON.parse(writtenContent);
649
+ expect(parsed.command).toBe('json-exit-test');
650
+ expect(parsed.exitCode).toBe(1);
651
+ expect(parsed.prNumber).toBe(99);
652
+ expect(parsed.type).toBe('session');
653
+ expect(parsed.durationMs).toBeGreaterThanOrEqual(0);
654
+ appendSpy.mockRestore();
655
+ stderrSpy.mockRestore();
656
+ vi.mocked(constantsMod.getGlobalDataDir).mockRestore();
657
+ });
658
+ it('exit handler fails silently if audit log path is inaccessible', async () => {
659
+ const constantsMod = await import('./constants.js');
660
+ vi.spyOn(constantsMod, 'getGlobalDataDir').mockReturnValue('/nonexistent/path/deep/nest');
661
+ const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
662
+ initializeLogger({ commandName: 'fail-silently-test' });
663
+ expect(() => process.emit('exit', 0)).not.toThrow();
664
+ stderrSpy.mockRestore();
665
+ vi.mocked(constantsMod.getGlobalDataDir).mockRestore();
666
+ });
667
+ });
668
+ // ---------------------------------------------------------------------------
669
+ // setAuditContext
670
+ // ---------------------------------------------------------------------------
671
+ describe('setAuditContext', () => {
672
+ beforeEach(() => {
673
+ _resetForTesting();
674
+ });
675
+ it('merges additional metadata into audit context', async () => {
676
+ const constantsMod = await import('./constants.js');
677
+ const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
678
+ tempDir = createTempDir();
679
+ vi.spyOn(constantsMod, 'getGlobalDataDir').mockReturnValue(tempDir);
680
+ const appendSpy = vi.spyOn(fs, 'appendFileSync');
681
+ initializeLogger({ commandName: 'context-test' });
682
+ setAuditContext({ worktreePath: '/tmp/worktree', prNumber: 123 });
683
+ process.emit('exit', 0);
684
+ const exitCall = appendSpy.mock.calls.find((call) => typeof call[1] === 'string' && call[1].includes('context-test'));
685
+ expect(exitCall).toBeDefined();
686
+ appendSpy.mockRestore();
687
+ stderrSpy.mockRestore();
688
+ vi.mocked(constantsMod.getGlobalDataDir).mockRestore();
689
+ cleanupTempDir(tempDir);
690
+ });
691
+ });
292
692
  //# sourceMappingURL=logger.test.js.map