@marcomigozzi/trailhead 0.14.0 → 0.16.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 (156) hide show
  1. package/README.it.md +158 -34
  2. package/README.md +147 -33
  3. package/bin/auto-intro-docs.test.js +72 -0
  4. package/bin/auto-merge-docs.test.js +33 -0
  5. package/bin/auto-merge-ticket-docs.test.js +32 -0
  6. package/bin/auto-review-rounds-docs.test.js +45 -0
  7. package/bin/auto-run-docs.test.js +60 -0
  8. package/bin/auto-scope-docs.test.js +33 -0
  9. package/bin/auto-slots-docs.test.js +45 -0
  10. package/bin/auto-uat-docs.test.js +47 -0
  11. package/bin/base-preflight-docs.test.js +65 -0
  12. package/bin/execute-split-docs.test.js +146 -0
  13. package/bin/grilling-holistic-review-docs.test.js +90 -0
  14. package/bin/isolation-answer-docs.test.js +41 -0
  15. package/bin/isolation-copy-docs.test.js +28 -0
  16. package/bin/lib/codex-projection.js +6 -6
  17. package/bin/lib/codex-projection.test.js +43 -2
  18. package/bin/needs-you-docs.test.js +41 -0
  19. package/bin/pick-stress-test-docs.test.js +105 -0
  20. package/bin/report-status-docs.test.js +195 -0
  21. package/bin/resolve-helpers-docs.test.js +36 -0
  22. package/bin/scope-confirm-skip-docs.test.js +70 -0
  23. package/bin/trailhead.js +6 -6
  24. package/bin/trailhead.test.js +355 -16
  25. package/bin/uat-setup-docs.test.js +65 -0
  26. package/package.json +1 -1
  27. package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
  28. package/plugins/trailhead/agents/trailhead-code-review.md +15 -2
  29. package/plugins/trailhead/agents/trailhead-codebase-map.md +20 -1
  30. package/plugins/trailhead/agents/trailhead-debug.md +20 -1
  31. package/plugins/trailhead/agents/trailhead-executor.md +29 -2
  32. package/plugins/trailhead/agents/trailhead-fix.md +29 -2
  33. package/plugins/trailhead/agents/trailhead-plan.md +20 -1
  34. package/plugins/trailhead/agents/trailhead-research.md +22 -3
  35. package/plugins/trailhead/agents/trailhead-verify.md +15 -2
  36. package/plugins/trailhead/commands/auto.md +2 -2
  37. package/plugins/trailhead/hooks/lib/auto-merge.test.js +467 -0
  38. package/plugins/trailhead/hooks/lib/auto-queue.test.js +245 -0
  39. package/plugins/trailhead/hooks/lib/auto-run.test.js +318 -0
  40. package/plugins/trailhead/hooks/lib/auto-slot.test.js +278 -0
  41. package/plugins/trailhead/hooks/lib/base-preflight.test.js +513 -0
  42. package/plugins/trailhead/hooks/lib/checkout-in.test.js +173 -0
  43. package/plugins/trailhead/hooks/lib/commit-message-check.js +1 -1
  44. package/plugins/trailhead/hooks/lib/commit-message-check.test.js +3 -2
  45. package/plugins/trailhead/hooks/lib/commit-msg-module-repo.test.js +116 -0
  46. package/plugins/trailhead/hooks/lib/commit-msg-sync.test.js +100 -9
  47. package/plugins/trailhead/hooks/lib/dashboard.test.js +472 -0
  48. package/plugins/trailhead/hooks/lib/fixtures/commit-msg-legacy-js +104 -0
  49. package/plugins/trailhead/hooks/lib/isolation-answer.test.js +258 -0
  50. package/plugins/trailhead/hooks/lib/isolation-copy.test.js +298 -0
  51. package/plugins/trailhead/hooks/lib/issue-body.test.js +470 -0
  52. package/plugins/trailhead/hooks/lib/merge-ticket.test.js +394 -0
  53. package/plugins/trailhead/hooks/lib/needs-you.test.js +177 -0
  54. package/plugins/trailhead/hooks/lib/plan-review-rounds.test.js +319 -0
  55. package/plugins/trailhead/hooks/lib/report-status.test.js +366 -0
  56. package/plugins/trailhead/hooks/lib/shell-scan.js +36 -1
  57. package/plugins/trailhead/hooks/lib/shell-scan.test.js +14 -0
  58. package/plugins/trailhead/hooks/lib/statusline-context.test.js +140 -0
  59. package/plugins/trailhead/hooks/lib/uat-backfill.test.js +150 -0
  60. package/plugins/trailhead/hooks/lib/uat-followup.test.js +169 -0
  61. package/plugins/trailhead/hooks/lib/uat-setup.test.js +340 -0
  62. package/plugins/trailhead/hooks/lib/unblock.test.js +207 -0
  63. package/plugins/trailhead/hooks/lib/work-state.test.js +45 -0
  64. package/plugins/trailhead/hooks/mod/README.md +209 -36
  65. package/plugins/trailhead/hooks/mod/auto-run-view.ts +93 -0
  66. package/plugins/trailhead/hooks/mod/auto-run.ts +165 -0
  67. package/plugins/trailhead/hooks/mod/map-pane-view.ts +304 -26
  68. package/plugins/trailhead/hooks/mod/map-pane.tsx +763 -160
  69. package/plugins/trailhead/hooks/mod/register.tsx +312 -40
  70. package/plugins/trailhead/hooks/mod/repo-pin.ts +167 -0
  71. package/plugins/trailhead/hooks/mod/session-registry.ts +65 -0
  72. package/plugins/trailhead/hooks/mod/turn-state.ts +14 -0
  73. package/plugins/trailhead/hooks/mod/whiteboard-follow.ts +98 -0
  74. package/plugins/trailhead/hooks/mod/work-state-locate.ts +191 -0
  75. package/plugins/trailhead/hooks/mod/work-state-prompt.ts +13 -15
  76. package/plugins/trailhead/hooks/mod/work-state-view.ts +38 -5
  77. package/plugins/trailhead/hooks/mod/worktree-porcelain.ts +17 -0
  78. package/plugins/trailhead/hooks/trailhead-bash-guards.js +8 -8
  79. package/plugins/trailhead/hooks/trailhead-bash-guards.test.js +22 -16
  80. package/plugins/trailhead/hooks/trailhead-install-guard.js +169 -34
  81. package/plugins/trailhead/hooks/trailhead-install-guard.test.js +180 -1
  82. package/plugins/trailhead/hooks/trailhead-labels-guard.js +231 -0
  83. package/plugins/trailhead/hooks/trailhead-labels-guard.test.js +212 -0
  84. package/plugins/trailhead/hooks/trailhead-search-guard.js +43 -7
  85. package/plugins/trailhead/hooks/trailhead-search-guard.test.js +66 -2
  86. package/plugins/trailhead/hooks/trailhead-secret-read-guard.js +296 -22
  87. package/plugins/trailhead/hooks/trailhead-secret-read-guard.test.js +253 -0
  88. package/plugins/trailhead/skills/_shared/choices.md +11 -4
  89. package/plugins/trailhead/skills/_shared/configuration-reference.md +17 -5
  90. package/plugins/trailhead/skills/_shared/load-first.md +3 -3
  91. package/plugins/trailhead/skills/_shared/multi-map.md +2 -2
  92. package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
  93. package/plugins/trailhead/skills/_shared/substrate-commands.md +47 -14
  94. package/plugins/trailhead/skills/_shared/substrate.md +9 -4
  95. package/plugins/trailhead/skills/_shared/teamwork.md +17 -8
  96. package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +18 -10
  97. package/plugins/trailhead/skills/_shared/techniques/code-review.md +1 -1
  98. package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +1 -1
  99. package/plugins/trailhead/skills/_shared/techniques/debug.md +2 -0
  100. package/plugins/trailhead/skills/_shared/techniques/grilling.md +4 -2
  101. package/plugins/trailhead/skills/_shared/techniques/plan-review.md +3 -1
  102. package/plugins/trailhead/skills/_shared/techniques/research.md +4 -2
  103. package/plugins/trailhead/skills/_shared/techniques/verify.md +1 -1
  104. package/plugins/trailhead/skills/_shared/techniques.md +12 -2
  105. package/plugins/trailhead/skills/trailhead/SKILL.md +1 -1
  106. package/plugins/trailhead/skills/trailhead-capture/SKILL.md +1 -1
  107. package/plugins/trailhead/skills/trailhead-capture/references/capture.md +3 -3
  108. package/plugins/trailhead/skills/trailhead-chart/SKILL.md +2 -2
  109. package/plugins/trailhead/skills/trailhead-chart/references/charting.md +5 -5
  110. package/plugins/trailhead/skills/trailhead-chart/references/inbox.md +2 -2
  111. package/plugins/trailhead/skills/trailhead-manage/SKILL.md +1 -1
  112. package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +1 -1
  113. package/plugins/trailhead/skills/trailhead-view/SKILL.md +9 -6
  114. package/plugins/trailhead/skills/trailhead-work/SKILL.md +31 -18
  115. package/plugins/trailhead/skills/trailhead-work/references/auto.md +70 -22
  116. package/plugins/trailhead/skills/trailhead-work/references/executor-brief.md +29 -0
  117. package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +29 -13
  118. package/plugins/trailhead/templates/statusline-trailhead.sh +77 -12
  119. package/plugins/trailhead/templates/trailhead-auto-merge.js +318 -0
  120. package/plugins/trailhead/templates/trailhead-auto-queue.js +186 -0
  121. package/plugins/trailhead/templates/trailhead-auto-run.js +294 -0
  122. package/plugins/trailhead/templates/trailhead-auto-slot.js +239 -0
  123. package/plugins/trailhead/templates/trailhead-base-preflight.js +305 -0
  124. package/plugins/trailhead/templates/trailhead-checkout-in.js +88 -0
  125. package/plugins/trailhead/templates/trailhead-commit-msg +38 -102
  126. package/plugins/trailhead/templates/trailhead-commit-msg-sync.js +58 -27
  127. package/plugins/trailhead/templates/trailhead-commit-msg.cjs +114 -0
  128. package/plugins/trailhead/templates/trailhead-dashboard.js +360 -0
  129. package/plugins/trailhead/templates/trailhead-isolation-answer.js +227 -0
  130. package/plugins/trailhead/templates/trailhead-isolation-copy.js +239 -0
  131. package/plugins/trailhead/templates/trailhead-issue-body.js +459 -0
  132. package/plugins/trailhead/templates/trailhead-merge-ticket.js +366 -0
  133. package/plugins/trailhead/templates/trailhead-needs-you.js +193 -0
  134. package/plugins/trailhead/templates/trailhead-plan-review-rounds.js +289 -0
  135. package/plugins/trailhead/templates/trailhead-report-status.js +274 -0
  136. package/plugins/trailhead/templates/trailhead-uat-backfill.js +114 -0
  137. package/plugins/trailhead/templates/trailhead-uat-followup.js +128 -0
  138. package/plugins/trailhead/templates/trailhead-uat-setup.js +385 -0
  139. package/plugins/trailhead/templates/trailhead-unblock.js +111 -0
  140. package/plugins/trailhead/templates/trailhead-work-state.js +25 -7
  141. package/plugins/trailhead/tests/auto-run-ui.test.ts +444 -0
  142. package/plugins/trailhead/tests/auto-run-view.test.ts +111 -0
  143. package/plugins/trailhead/tests/map-pane-ui.test.ts +2519 -382
  144. package/plugins/trailhead/tests/map-pane-view.test.ts +600 -18
  145. package/plugins/trailhead/tests/map-pane-write.test.ts +51 -0
  146. package/plugins/trailhead/tests/repo-pin-late-repo.test.ts +77 -0
  147. package/plugins/trailhead/tests/repo-pin.test.ts +123 -0
  148. package/plugins/trailhead/tests/session-registry.test.ts +50 -0
  149. package/plugins/trailhead/tests/whiteboard-follow.test.ts +90 -0
  150. package/plugins/trailhead/tests/work-state-locate.test.ts +33 -0
  151. package/plugins/trailhead/tests/work-state-prompt-hook.test.ts +276 -2
  152. package/plugins/trailhead/tests/work-state-prompt.test.ts +29 -35
  153. package/plugins/trailhead/tests/work-state-ui.test.ts +73 -1
  154. package/plugins/trailhead/tests/work-state-view.test.ts +44 -1
  155. package/plugins/trailhead/tests/work-state-worktree.test.ts +310 -0
  156. package/plugins/trailhead/types/index.d.ts +62 -3
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env node
2
+ // Tests for templates/trailhead-checkout-in.js. Run: node checkout-in.test.js
3
+ // No framework: plain asserts, same style as uat-backfill.test.js.
4
+ //
5
+ // Layers covered:
6
+ // 1. The pure token helpers: encode / decode round-trip.
7
+ // 2. The CLI against real temp git repos: invalid, here, move, the `cd` field.
8
+ // 3. The protocol contract: the work skill moves before it loads or preflights.
9
+ const assert = require('assert');
10
+ const { spawnSync } = require('child_process');
11
+ const path = require('path');
12
+ const fs = require('fs');
13
+ const os = require('os');
14
+
15
+ const scriptPath = path.resolve(__dirname, '..', '..', 'templates', 'trailhead-checkout-in.js');
16
+ const skillPath = path.resolve(__dirname, '..', '..', 'skills', 'trailhead-work', 'SKILL.md');
17
+ const { encodeIn, decodeIn } = require(scriptPath);
18
+
19
+ let passed = 0;
20
+ const ok = (name, cond) => { assert.ok(cond, name); passed++; };
21
+ const eq = (name, a, b) => { assert.deepStrictEqual(a, b, name); passed++; };
22
+
23
+ const tmp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'checkout-in-')));
24
+ const sh = (cwd, ...args) => {
25
+ const r = spawnSync('git', ['-C', cwd, ...args], { encoding: 'utf8' });
26
+ assert.strictEqual(r.status, 0, `git ${args.join(' ')}: ${r.stderr}`);
27
+ return r.stdout;
28
+ };
29
+ function makeRepo(dir) {
30
+ fs.mkdirSync(dir, { recursive: true });
31
+ sh(dir, 'init', '-q', '-b', 'main');
32
+ sh(dir, '-c', 'user.name=t', '-c', 'user.email=t@t', 'commit', '-q', '--allow-empty', '-m', 'init');
33
+ return fs.realpathSync(dir);
34
+ }
35
+ const run = (inArg, cwd) => {
36
+ const r = spawnSync(process.execPath, [scriptPath, '--in', inArg, '--cwd', cwd], { encoding: 'utf8' });
37
+ return { code: r.status, out: r.stdout.trim() ? JSON.parse(r.stdout.trim()) : null, err: r.stderr };
38
+ };
39
+ const tokenOf = (p) => encodeIn(p).slice('--in='.length);
40
+
41
+ // --- 1. pure helpers ---------------------------------------------------------------
42
+ const odd = [
43
+ '/tmp/with space/repo',
44
+ "/tmp/it's/repo",
45
+ '/tmp/"dq"/repo',
46
+ '/tmp/-leading/repo',
47
+ '-dash-first',
48
+ '/tmp/uniè中文/repo',
49
+ "/tmp/a'b'c d/$HOME/`x`",
50
+ ];
51
+ for (const p of odd) {
52
+ const enc = encodeIn(p);
53
+ ok(`token shape ${p}`, /^--in=[A-Za-z0-9_-]+$/.test(enc));
54
+ eq(`round-trip ${p}`, decodeIn(enc.slice(5)), p);
55
+ }
56
+ eq('decode of garbage', decodeIn('!!!'), null);
57
+ eq('decode of empty', decodeIn(''), null);
58
+
59
+ // --- 2. CLI ------------------------------------------------------------------------
60
+ const main = makeRepo(path.join(tmp, 'main repo'));
61
+ const other = makeRepo(path.join(tmp, 'other'));
62
+ const plain = path.join(tmp, 'plain');
63
+ fs.mkdirSync(plain);
64
+
65
+ {
66
+ const r = run(tokenOf(main), main);
67
+ eq('here in the root', [r.code, r.out.status, r.out.path], [0, 'here', main]);
68
+ }
69
+ {
70
+ fs.mkdirSync(path.join(main, 'sub'));
71
+ const r = run(tokenOf(main), path.join(main, 'sub'));
72
+ eq('here from a subdir of the same checkout', r.out.status, 'here');
73
+ }
74
+ {
75
+ const r = run(tokenOf(main), other);
76
+ eq('move from another repo', [r.code, r.out.status, r.out.path], [0, 'move', main]);
77
+ ok('move carries a cd', r.out.cd === `cd -- '${main}'`);
78
+ }
79
+ {
80
+ const r = run(tokenOf(main), plain);
81
+ eq('move from outside any repo', r.out.status, 'move');
82
+ }
83
+ {
84
+ const r = run(tokenOf(main), path.join(tmp, 'missing-cwd'));
85
+ eq('move from a cwd that does not exist', r.out.status, 'move');
86
+ }
87
+
88
+ // A worktree root is its own checkout: here only for itself.
89
+ {
90
+ const wt = path.join(tmp, 'wt');
91
+ sh(main, 'worktree', 'add', '-q', '-b', 'wt-branch', wt);
92
+ const wtReal = fs.realpathSync(wt);
93
+ eq('worktree root is here for itself', run(tokenOf(wtReal), wtReal).out.status, 'here');
94
+ eq('worktree root is move from the main checkout', run(tokenOf(wtReal), main).out.status, 'move');
95
+ eq('main checkout is move from the worktree', run(tokenOf(main), wtReal).out.status, 'move');
96
+ }
97
+
98
+ // A symlinked path compares canonical.
99
+ {
100
+ const link = path.join(tmp, 'link-to-main');
101
+ fs.symlinkSync(main, link);
102
+ const r = run(tokenOf(link), main);
103
+ eq('symlinked path is here by canonical compare', r.out.status, 'here');
104
+ eq('symlinked cwd is here too', run(tokenOf(main), link).out.status, 'here');
105
+ }
106
+
107
+ // invalid: never searches for another checkout.
108
+ {
109
+ eq('relative path is invalid', run(tokenOf('relative/dir'), main).out.status, 'invalid');
110
+ eq('missing path is invalid', run(tokenOf(path.join(tmp, 'nope')), main).out.status, 'invalid');
111
+ eq('non-repo dir is invalid', run(tokenOf(plain), main).out.status, 'invalid');
112
+ eq('subdir of a repo is invalid (not a root)', run(tokenOf(path.join(main, 'sub')), other).out.status, 'invalid');
113
+ const bad = run('!!!', main);
114
+ eq('undecodable token is invalid', [bad.code, bad.out.status], [0, 'invalid']);
115
+ ok('invalid has a reason', typeof bad.out.reason === 'string' && bad.out.reason.length > 0);
116
+ ok('invalid has no cd', !('cd' in run(tokenOf(plain), main).out));
117
+ }
118
+
119
+ // usage errors
120
+ {
121
+ const r = spawnSync(process.execPath, [scriptPath, '--cwd', main], { encoding: 'utf8' });
122
+ eq('missing --in is a usage error', r.status, 2);
123
+ const r2 = spawnSync(process.execPath, [scriptPath, '--in', tokenOf(main)], { encoding: 'utf8' });
124
+ eq('missing --cwd is a usage error', r2.status, 2);
125
+ }
126
+
127
+ // The cd field, run by a shell, lands in the path for every pathological case.
128
+ {
129
+ let n = 0;
130
+ for (const p of odd) {
131
+ if (!path.isAbsolute(p)) continue;
132
+ const dir = path.join(tmp, `odd${n++}`, p.replace(/^\/tmp\//, '').replace(/\//g, '_'));
133
+ const real = makeRepo(dir);
134
+ const r = run(tokenOf(real), plain);
135
+ eq(`move for ${dir}`, r.out.status, 'move');
136
+ const cd = spawnSync('sh', ['-c', `${r.out.cd} && pwd -P`], { encoding: 'utf8', cwd: plain });
137
+ eq(`cd lands in ${dir}`, cd.stdout.trim(), real);
138
+ }
139
+ // A quote and a leading dash in the actual directory names.
140
+ for (const name of ["it's here", '-dash', 'sp ace "dq" $x `y`']) {
141
+ const real = makeRepo(path.join(tmp, 'pathological', name));
142
+ const r = run(tokenOf(real), plain);
143
+ const cd = spawnSync('sh', ['-c', `${r.out.cd} && pwd -P`], { encoding: 'utf8', cwd: plain });
144
+ eq(`cd lands in ${name}`, cd.stdout.trim(), real);
145
+ }
146
+ }
147
+
148
+ // --- 3. protocol contract: the move comes before anything else -----------------------
149
+ {
150
+ const skill = fs.readFileSync(skillPath, 'utf8');
151
+ const move = skill.indexOf('## Move into the handed checkout');
152
+ ok('move section exists', move >= 0);
153
+ const load = skill.indexOf('## Load first');
154
+ const pre = skill.indexOf('## Preflight');
155
+ ok('move section sits before Load first', load > move);
156
+ ok('move section sits before the Preflight', pre > move);
157
+ const section = skill.slice(move, load);
158
+ ok('names the helper', section.includes('trailhead-checkout-in.js'));
159
+ ok('names the --in= word', section.includes('--in='));
160
+ ok('names work and resume', /`work`/.test(section) && /`resume`/.test(section));
161
+ ok('runs the cd field verbatim', /`cd` field[^.]*verbatim/.test(section));
162
+ ok('cd is its own Bash call', /own Bash call/.test(section));
163
+ ok('post-move re-check requires here', /re-run the helper/.test(section) && /`here`/.test(section));
164
+ ok('stops on invalid', /`invalid`[^.]*stop/.test(section));
165
+ ok('stops on a failed cd', /failed `cd`[^.]*stop|`cd` fails[^.]*stop|stop[^.]*`cd` fail/i.test(section));
166
+ ok('stops when the post-check is not here', /could not move/.test(section));
167
+ ok('stops when the helper is missing', /missing[^.]*stop|stop[^.]*missing/i.test(section));
168
+ ok('never moves by hand', /by hand/.test(section));
169
+ ok('Routing points at the move section', /Routing[\s\S]*Move into the handed checkout/.test(skill.slice(skill.indexOf('## Routing'))));
170
+ }
171
+
172
+ fs.rmSync(tmp, { recursive: true, force: true });
173
+ console.log(`checkout-in: ${passed} assertions passed`);
@@ -2,7 +2,7 @@
2
2
  // commit-message-check.js: the single source of truth for trailhead's commit
3
3
  // message rules, shared by two guardrails so they can never drift:
4
4
  // - trailhead-commit-guard.js (Claude Code PreToolUse(Bash) hook)
5
- // - templates/trailhead-commit-msg (host-independent git commit-msg hook,
5
+ // - templates/trailhead-commit-msg.cjs (host-independent git commit-msg hook logic,
6
6
  // installed into .git/hooks at repo first-use so it runs on ANY host)
7
7
  // Two rules: NEVER a Co-Authored-By trailer; a Conventional Commits subject
8
8
  // (<type>(scope)?: subject) of at most 72 chars. Plus a conditional third rule:
@@ -4,7 +4,8 @@
4
4
  //
5
5
  // Drives the SAME fixtures through two paths:
6
6
  // 1. checkCommitMessage() directly (the source of truth).
7
- // 2. the template hook (templates/trailhead-commit-msg) end-to-end via
7
+ // 2. the template hook (templates/trailhead-commit-msg sh wrapper, which runs
8
+ // the sibling trailhead-commit-msg.cjs logic) end-to-end via
8
9
  // execFileSync, feeding it the message as a temp file, run from a fresh
9
10
  // mkdtemp cwd (so the template's own .trailhead/session-ticket lookup is
10
11
  // independent of this repo's real marker).
@@ -60,7 +61,7 @@ function runTemplateHook(message, marker) {
60
61
  let allowed = true;
61
62
  let stderr = '';
62
63
  try {
63
- execFileSync('node', [templatePath, tmpFile], { cwd: tmp, stdio: 'pipe' });
64
+ execFileSync('sh', [templatePath, tmpFile], { cwd: tmp, stdio: 'pipe' });
64
65
  } catch (e) {
65
66
  allowed = false;
66
67
  stderr = e.stderr ? e.stderr.toString() : '';
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+ // Regression for #257: the git commit-msg hook must work in a repo whose root
3
+ // package.json says "type": "module". The hook is installed by the sync script
4
+ // into a fixture repo and exercised through real `git commit` calls.
5
+ // Run: node commit-msg-module-repo.test.js
6
+ const assert = require('assert');
7
+ const { execFileSync, spawnSync } = require('child_process');
8
+ const path = require('path');
9
+ const fs = require('fs');
10
+ const os = require('os');
11
+
12
+ const syncScript = path.resolve(__dirname, '..', '..', 'templates', 'trailhead-commit-msg-sync.js');
13
+
14
+ // Prefisso della riga che il wrapper stampa quando non può applicare le regole (#262).
15
+ const INACTIVE = 'trailhead: commit-msg hook inactive (';
16
+
17
+ let passed = 0;
18
+ const ok = (name, cond) => { assert.ok(cond, name); passed++; };
19
+
20
+ function gitEnv(extra) {
21
+ return Object.assign({}, process.env, {
22
+ GIT_CONFIG_GLOBAL: '/dev/null',
23
+ GIT_CONFIG_NOSYSTEM: '1',
24
+ GIT_AUTHOR_NAME: 'Trailhead Test',
25
+ GIT_AUTHOR_EMAIL: 'trailhead-test@example.com',
26
+ GIT_COMMITTER_NAME: 'Trailhead Test',
27
+ GIT_COMMITTER_EMAIL: 'trailhead-test@example.com',
28
+ }, extra || {});
29
+ }
30
+
31
+ // Repo fixture: package.json "type": "module" alla radice, hook installato dallo
32
+ // script di sync, marker session-ticket = #1.
33
+ function makeRepo() {
34
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'trailhead-commit-msg-esm-'));
35
+ execFileSync('git', ['init', '-q'], { cwd: dir, env: gitEnv() });
36
+ fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ type: 'module' }));
37
+ const out = execFileSync(process.execPath, [syncScript, '--repo', dir], { env: gitEnv(), encoding: 'utf8' });
38
+ assert.strictEqual(JSON.parse(out).status, 'installed');
39
+ fs.mkdirSync(path.join(dir, '.trailhead'));
40
+ fs.writeFileSync(path.join(dir, '.trailhead', 'session-ticket'), '#1 x\n');
41
+ return { dir, hooksDir: path.join(dir, '.git', 'hooks') };
42
+ }
43
+
44
+ function commit(dir, message) {
45
+ const r = spawnSync('git', ['commit', '--allow-empty', '-q', '-m', message], { cwd: dir, env: gitEnv(), encoding: 'utf8' });
46
+ return { status: r.status, stderr: r.stderr || '' };
47
+ }
48
+
49
+ // (a) commit convenzionale con Refs: #1 passa
50
+ {
51
+ const { dir } = makeRepo();
52
+ const r = commit(dir, 'fix: thing\n\nRefs: #1');
53
+ ok('module repo: conventional commit with Refs passes', r.status === 0);
54
+ ok('module repo: clean commit stays silent', !r.stderr.includes('trailhead:'));
55
+ fs.rmSync(dir, { recursive: true, force: true });
56
+ }
57
+
58
+ // (b) senza trailer: rifiutato col messaggio di trailhead
59
+ {
60
+ const { dir } = makeRepo();
61
+ const r = commit(dir, 'fix: thing');
62
+ ok('module repo: missing Refs is rejected with exit 1', r.status === 1);
63
+ ok('module repo: rejection carries trailhead message', r.stderr.includes('trailhead:') && r.stderr.includes('Refs: #1'));
64
+ ok('module repo: no ESM load error leaks', !r.stderr.includes('require is not defined'));
65
+ ok('module repo: a violation carries no inactive notice', !r.stderr.includes(INACTIVE));
66
+ fs.rmSync(dir, { recursive: true, force: true });
67
+ }
68
+
69
+ // (c) errori di avvio: mai bloccare un commit, ma segnalarlo su stderr (#262)
70
+ {
71
+ const { dir, hooksDir } = makeRepo();
72
+ fs.rmSync(path.join(hooksDir, 'trailhead-commit-msg.cjs'));
73
+ const r = commit(dir, 'not conventional at all');
74
+ ok('module repo: missing .cjs lets the commit through', r.status === 0);
75
+ ok('module repo: missing .cjs prints the inactive notice',
76
+ r.stderr.includes(INACTIVE + 'trailhead-commit-msg.cjs missing or unreadable)'));
77
+ fs.rmSync(dir, { recursive: true, force: true });
78
+ }
79
+ // .cjs presente ma illeggibile: da root chmod non toglie la lettura, quindi si salta.
80
+ if (typeof process.getuid !== 'function' || process.getuid() !== 0) {
81
+ const { dir, hooksDir } = makeRepo();
82
+ fs.chmodSync(path.join(hooksDir, 'trailhead-commit-msg.cjs'), 0o000);
83
+ const r = commit(dir, 'not conventional at all');
84
+ ok('module repo: unreadable .cjs lets the commit through', r.status === 0);
85
+ ok('module repo: unreadable .cjs prints the inactive notice',
86
+ r.stderr.includes(INACTIVE + 'trailhead-commit-msg.cjs missing or unreadable)'));
87
+ fs.rmSync(dir, { recursive: true, force: true });
88
+ }
89
+ {
90
+ const { dir, hooksDir } = makeRepo();
91
+ fs.writeFileSync(path.join(hooksDir, 'trailhead-commit-msg.cjs'), "throw new Error('boom at load');\n");
92
+ const r = commit(dir, 'not conventional at all');
93
+ ok('module repo: .cjs crashing at load lets the commit through', r.status === 0);
94
+ ok('module repo: .cjs crashing at load prints the inactive notice',
95
+ r.stderr.includes(INACTIVE + 'trailhead-commit-msg.cjs failed to run, exit '));
96
+ fs.rmSync(dir, { recursive: true, force: true });
97
+ }
98
+ {
99
+ // node assente dal PATH: il wrapper deve uscire 0 anche con un messaggio che
100
+ // altrimenti sarebbe una violazione (controllo negativo con PATH normale).
101
+ const { dir, hooksDir } = makeRepo();
102
+ const msgFile = path.join(dir, 'MSG');
103
+ fs.writeFileSync(msgFile, 'not conventional at all\n');
104
+ const emptyBin = fs.mkdtempSync(path.join(os.tmpdir(), 'trailhead-empty-bin-'));
105
+ const hook = path.join(hooksDir, 'commit-msg');
106
+ const withNode = spawnSync('/bin/sh', [hook, msgFile], { cwd: dir, env: gitEnv(), encoding: 'utf8' });
107
+ ok('wrapper control: violation exits 1 with node on PATH', withNode.status === 1);
108
+ const noNode = spawnSync('/bin/sh', [hook, msgFile], { cwd: dir, env: { PATH: emptyBin }, encoding: 'utf8' });
109
+ ok('wrapper: node absent from PATH exits 0', noNode.status === 0);
110
+ ok('wrapper: node absent from PATH prints the inactive notice',
111
+ (noNode.stderr || '').includes(INACTIVE + 'node not found on PATH)'));
112
+ fs.rmSync(emptyBin, { recursive: true, force: true });
113
+ fs.rmSync(dir, { recursive: true, force: true });
114
+ }
115
+
116
+ console.log(`✓ commit-msg module repo (#257): ${passed} assertions passed`);
@@ -16,7 +16,10 @@ const os = require('os');
16
16
 
17
17
  const scriptPath = path.resolve(__dirname, '..', '..', 'templates', 'trailhead-commit-msg-sync.js');
18
18
  const templatePath = path.resolve(__dirname, '..', '..', 'templates', 'trailhead-commit-msg');
19
+ const cjsTemplatePath = path.resolve(__dirname, '..', '..', 'templates', 'trailhead-commit-msg.cjs');
19
20
  const oldFixturePath = path.join(__dirname, 'fixtures', 'commit-msg-a319156');
21
+ // Hook JS monofile pre-#257 (extensionless CommonJS), da migrare al nuovo layout.
22
+ const legacyJsFixturePath = path.join(__dirname, 'fixtures', 'commit-msg-legacy-js');
20
23
 
21
24
  const {
22
25
  resolveHooksTarget,
@@ -29,7 +32,10 @@ let passed = 0;
29
32
  const ok = (name, cond) => { assert.ok(cond, name); passed++; };
30
33
 
31
34
  const templateText = fs.readFileSync(templatePath, 'utf8');
35
+ const cjsText = fs.readFileSync(cjsTemplatePath, 'utf8');
32
36
  const oldHookText = fs.readFileSync(oldFixturePath, 'utf8');
37
+ const legacyJsText = fs.readFileSync(legacyJsFixturePath, 'utf8');
38
+ const CJS_NAME = 'trailhead-commit-msg.cjs';
33
39
 
34
40
  function mktmp() {
35
41
  return fs.mkdtempSync(path.join(os.tmpdir(), 'trailhead-commit-msg-sync-test-'));
@@ -38,6 +44,9 @@ function mktmp() {
38
44
  // --- isTrailheadHook() unit cases -------------------------------------------
39
45
  ok('isTrailheadHook: current template is recognised', isTrailheadHook(templateText));
40
46
  ok('isTrailheadHook: old (a319156) hook is recognised', isTrailheadHook(oldHookText));
47
+ ok('isTrailheadHook: legacy single-file JS hook is recognised', isTrailheadHook(legacyJsText));
48
+ ok('isTrailheadHook: wrapper header is on a "#" line within the first 5', isTrailheadHook(templateText));
49
+ ok('isTrailheadHook: wrapper is a sh script', templateText.startsWith('#!/bin/sh\n'));
41
50
  ok('isTrailheadHook: plain foreign script is not', !isTrailheadHook('#!/bin/sh\necho hi\n'));
42
51
  ok('isTrailheadHook: mention outside the first 5 lines, not line-start, is not',
43
52
  !isTrailheadHook('#!/bin/sh\necho hi\necho more\necho stuff\necho again\n# trailhead commit-msg hook (git). fake\n'));
@@ -55,13 +64,14 @@ function modeOf(p) { return fs.statSync(p).mode & 0o777; }
55
64
  // absent -> installed, 0755
56
65
  {
57
66
  const dir = mktmp();
58
- const result = syncCommitMsgHook({ hooksDir: dir, templateText, hooksPathSet: false, fs });
67
+ const result = syncCommitMsgHook({ hooksDir: dir, templateText, hooksPathSet: false, cjsText, fs });
59
68
  const hookPath = path.join(dir, 'commit-msg');
60
69
  ok('syncCommitMsgHook: absent -> installed', result.status === 'installed');
61
70
  ok('syncCommitMsgHook: absent -> hook path returned', result.hook === hookPath);
62
71
  ok('syncCommitMsgHook: absent -> file written', fs.existsSync(hookPath));
63
72
  ok('syncCommitMsgHook: absent -> content matches template', fs.readFileSync(hookPath, 'utf8') === templateText);
64
73
  ok('syncCommitMsgHook: absent -> mode 0755', modeOf(hookPath) === 0o755);
74
+ ok('syncCommitMsgHook: absent -> .cjs written beside it', fs.readFileSync(path.join(dir, CJS_NAME), 'utf8') === cjsText);
65
75
  fs.rmSync(dir, { recursive: true, force: true });
66
76
  }
67
77
 
@@ -71,21 +81,77 @@ function modeOf(p) { return fs.statSync(p).mode & 0o777; }
71
81
  const hookPath = path.join(dir, 'commit-msg');
72
82
  fs.writeFileSync(hookPath, oldHookText);
73
83
  fs.chmodSync(hookPath, 0o755);
74
- const result = syncCommitMsgHook({ hooksDir: dir, templateText, hooksPathSet: false, fs });
84
+ const result = syncCommitMsgHook({ hooksDir: dir, templateText, hooksPathSet: false, cjsText, fs });
75
85
  ok('syncCommitMsgHook: old -> upgraded', result.status === 'upgraded');
76
86
  ok('syncCommitMsgHook: old -> bytes now match template', fs.readFileSync(hookPath, 'utf8') === templateText);
77
87
  ok('syncCommitMsgHook: old -> mode 0755', modeOf(hookPath) === 0o755);
78
88
  fs.rmSync(dir, { recursive: true, force: true });
79
89
  }
80
90
 
91
+ // legacy single-file JS hook -> upgraded to wrapper + .cjs
92
+ {
93
+ const dir = mktmp();
94
+ const hookPath = path.join(dir, 'commit-msg');
95
+ fs.writeFileSync(hookPath, legacyJsText);
96
+ fs.chmodSync(hookPath, 0o755);
97
+ const result = syncCommitMsgHook({ hooksDir: dir, templateText, cjsText, hooksPathSet: false, fs });
98
+ ok('syncCommitMsgHook: legacy JS -> upgraded', result.status === 'upgraded');
99
+ ok('syncCommitMsgHook: legacy JS -> wrapper now matches template', fs.readFileSync(hookPath, 'utf8') === templateText);
100
+ ok('syncCommitMsgHook: legacy JS -> .cjs written', fs.readFileSync(path.join(dir, CJS_NAME), 'utf8') === cjsText);
101
+ ok('syncCommitMsgHook: legacy JS -> wrapper mode 0755', modeOf(hookPath) === 0o755);
102
+ const again = syncCommitMsgHook({ hooksDir: dir, templateText, cjsText, hooksPathSet: false, fs });
103
+ ok('syncCommitMsgHook: legacy JS -> second run is current', again.status === 'current');
104
+ ok('syncCommitMsgHook: legacy JS -> no temp files left', fs.readdirSync(dir).sort().join(',') === `commit-msg,${CJS_NAME}`);
105
+ fs.rmSync(dir, { recursive: true, force: true });
106
+ }
107
+
108
+ // current wrapper but outdated .cjs -> upgraded, .cjs rewritten
109
+ {
110
+ const dir = mktmp();
111
+ const hookPath = path.join(dir, 'commit-msg');
112
+ fs.writeFileSync(hookPath, templateText);
113
+ fs.chmodSync(hookPath, 0o755);
114
+ fs.writeFileSync(path.join(dir, CJS_NAME), '// stale\n');
115
+ const result = syncCommitMsgHook({ hooksDir: dir, templateText, cjsText, hooksPathSet: false, fs });
116
+ ok('syncCommitMsgHook: outdated .cjs -> upgraded', result.status === 'upgraded');
117
+ ok('syncCommitMsgHook: outdated .cjs -> rewritten', fs.readFileSync(path.join(dir, CJS_NAME), 'utf8') === cjsText);
118
+ fs.rmSync(dir, { recursive: true, force: true });
119
+ }
120
+
121
+ // current wrapper but .cjs missing -> upgraded, .cjs restored
122
+ {
123
+ const dir = mktmp();
124
+ const hookPath = path.join(dir, 'commit-msg');
125
+ fs.writeFileSync(hookPath, templateText);
126
+ fs.chmodSync(hookPath, 0o755);
127
+ const result = syncCommitMsgHook({ hooksDir: dir, templateText, cjsText, hooksPathSet: false, fs });
128
+ ok('syncCommitMsgHook: missing .cjs -> upgraded', result.status === 'upgraded');
129
+ ok('syncCommitMsgHook: missing .cjs -> restored', fs.readFileSync(path.join(dir, CJS_NAME), 'utf8') === cjsText);
130
+ fs.rmSync(dir, { recursive: true, force: true });
131
+ }
132
+
133
+ // outdated wrapper (older sh wrapper text) + current .cjs -> upgraded wrapper
134
+ {
135
+ const dir = mktmp();
136
+ const hookPath = path.join(dir, 'commit-msg');
137
+ fs.writeFileSync(hookPath, '#!/bin/sh\n# trailhead commit-msg hook (git). older wrapper\nexit 0\n');
138
+ fs.chmodSync(hookPath, 0o755);
139
+ fs.writeFileSync(path.join(dir, CJS_NAME), cjsText);
140
+ const result = syncCommitMsgHook({ hooksDir: dir, templateText, cjsText, hooksPathSet: false, fs });
141
+ ok('syncCommitMsgHook: outdated wrapper -> upgraded', result.status === 'upgraded');
142
+ ok('syncCommitMsgHook: outdated wrapper -> now matches template', fs.readFileSync(hookPath, 'utf8') === templateText);
143
+ fs.rmSync(dir, { recursive: true, force: true });
144
+ }
145
+
81
146
  // CRLF current -> current, untouched
82
147
  {
83
148
  const dir = mktmp();
84
149
  const hookPath = path.join(dir, 'commit-msg');
85
150
  const crlfTemplate = templateText.replace(/\n/g, '\r\n');
151
+ fs.writeFileSync(path.join(dir, CJS_NAME), cjsText);
86
152
  fs.writeFileSync(hookPath, crlfTemplate);
87
153
  fs.chmodSync(hookPath, 0o755);
88
- const result = syncCommitMsgHook({ hooksDir: dir, templateText, hooksPathSet: false, fs });
154
+ const result = syncCommitMsgHook({ hooksDir: dir, templateText, hooksPathSet: false, cjsText, fs });
89
155
  ok('syncCommitMsgHook: CRLF current -> current', result.status === 'current');
90
156
  ok('syncCommitMsgHook: CRLF current -> untouched bytes', fs.readFileSync(hookPath, 'utf8') === crlfTemplate);
91
157
  fs.rmSync(dir, { recursive: true, force: true });
@@ -96,8 +162,9 @@ function modeOf(p) { return fs.statSync(p).mode & 0o777; }
96
162
  const dir = mktmp();
97
163
  const hookPath = path.join(dir, 'commit-msg');
98
164
  fs.writeFileSync(hookPath, templateText);
165
+ fs.writeFileSync(path.join(dir, CJS_NAME), cjsText);
99
166
  fs.chmodSync(hookPath, 0o644);
100
- const result = syncCommitMsgHook({ hooksDir: dir, templateText, hooksPathSet: false, fs });
167
+ const result = syncCommitMsgHook({ hooksDir: dir, templateText, hooksPathSet: false, cjsText, fs });
101
168
  ok('syncCommitMsgHook: current 0644 -> status current', result.status === 'current');
102
169
  ok('syncCommitMsgHook: current 0644 -> mode fixed to 0755', modeOf(hookPath) === 0o755);
103
170
  fs.rmSync(dir, { recursive: true, force: true });
@@ -110,10 +177,11 @@ function modeOf(p) { return fs.statSync(p).mode & 0o777; }
110
177
  const foreign = '#!/bin/sh\necho hi\n';
111
178
  fs.writeFileSync(hookPath, foreign);
112
179
  fs.chmodSync(hookPath, 0o755);
113
- const result = syncCommitMsgHook({ hooksDir: dir, templateText, hooksPathSet: false, fs });
180
+ const result = syncCommitMsgHook({ hooksDir: dir, templateText, hooksPathSet: false, cjsText, fs });
114
181
  ok('syncCommitMsgHook: foreign shell -> foreign', result.status === 'foreign');
115
182
  ok('syncCommitMsgHook: foreign shell -> bytes untouched', fs.readFileSync(hookPath, 'utf8') === foreign);
116
183
  ok('syncCommitMsgHook: foreign shell -> mode untouched', modeOf(hookPath) === 0o755);
184
+ ok('syncCommitMsgHook: foreign shell -> no .cjs written', !fs.existsSync(path.join(dir, CJS_NAME)));
117
185
  fs.rmSync(dir, { recursive: true, force: true });
118
186
  }
119
187
 
@@ -124,7 +192,7 @@ function modeOf(p) { return fs.statSync(p).mode & 0o777; }
124
192
  const foreign = '#!/bin/sh\necho "trailhead commit-msg hook (git). not really"\nexit 0\n';
125
193
  fs.writeFileSync(hookPath, foreign);
126
194
  fs.chmodSync(hookPath, 0o755);
127
- const result = syncCommitMsgHook({ hooksDir: dir, templateText, hooksPathSet: false, fs });
195
+ const result = syncCommitMsgHook({ hooksDir: dir, templateText, hooksPathSet: false, cjsText, fs });
128
196
  ok('syncCommitMsgHook: foreign wrapper mentioning phrase -> foreign', result.status === 'foreign');
129
197
  ok('syncCommitMsgHook: foreign wrapper -> bytes untouched', fs.readFileSync(hookPath, 'utf8') === foreign);
130
198
  fs.rmSync(dir, { recursive: true, force: true });
@@ -138,10 +206,11 @@ function modeOf(p) { return fs.statSync(p).mode & 0o777; }
138
206
  const targetContent = '#!/bin/sh\necho real\n';
139
207
  fs.writeFileSync(targetPath, targetContent);
140
208
  fs.symlinkSync(targetPath, hookPath);
141
- const result = syncCommitMsgHook({ hooksDir: dir, templateText, hooksPathSet: false, fs });
209
+ const result = syncCommitMsgHook({ hooksDir: dir, templateText, hooksPathSet: false, cjsText, fs });
142
210
  ok('syncCommitMsgHook: symlink -> foreign', result.status === 'foreign');
143
211
  ok('syncCommitMsgHook: symlink -> still a symlink', fs.lstatSync(hookPath).isSymbolicLink());
144
212
  ok('syncCommitMsgHook: symlink target untouched', fs.readFileSync(targetPath, 'utf8') === targetContent);
213
+ ok('syncCommitMsgHook: symlink -> no .cjs written', !fs.existsSync(path.join(dir, CJS_NAME)));
145
214
  fs.rmSync(dir, { recursive: true, force: true });
146
215
  }
147
216
 
@@ -149,9 +218,9 @@ function modeOf(p) { return fs.statSync(p).mode & 0o777; }
149
218
  {
150
219
  const dir = mktmp();
151
220
  const hookPath = path.join(dir, 'commit-msg');
152
- const result = syncCommitMsgHook({ hooksDir: dir, templateText, hooksPathSet: true, fs });
221
+ const result = syncCommitMsgHook({ hooksDir: dir, templateText, hooksPathSet: true, cjsText, fs });
153
222
  ok('syncCommitMsgHook: absent + hooksPathSet -> skipped', result.status === 'skipped');
154
- ok('syncCommitMsgHook: absent + hooksPathSet -> nothing created', !fs.existsSync(hookPath));
223
+ ok('syncCommitMsgHook: absent + hooksPathSet -> nothing created', !fs.existsSync(hookPath) && !fs.existsSync(path.join(dir, CJS_NAME)));
155
224
  fs.rmSync(dir, { recursive: true, force: true });
156
225
  }
157
226
 
@@ -226,6 +295,7 @@ function assertSingleJsonLine(out, label) {
226
295
  const hookPath = path.join(dir, '.git', 'hooks', 'commit-msg');
227
296
  ok('e2e fresh repo: hook file exists', fs.existsSync(hookPath));
228
297
  ok('e2e fresh repo: hook is executable', (modeOf(hookPath) & 0o111) !== 0);
298
+ ok('e2e fresh repo: .cjs installed beside it', fs.readFileSync(path.join(dir, '.git', 'hooks', CJS_NAME), 'utf8') === cjsText);
229
299
 
230
300
  const out2 = runScript(['--repo', dir], { env: gitEnv() });
231
301
  const parsed2 = assertSingleJsonLine(out2, 'e2e fresh repo (2nd run)');
@@ -246,6 +316,24 @@ function assertSingleJsonLine(out, label) {
246
316
  const parsed = assertSingleJsonLine(out, 'e2e seeded old hook');
247
317
  ok('e2e seeded old hook: status upgraded', parsed.status === 'upgraded');
248
318
  ok('e2e seeded old hook: content now matches template', fs.readFileSync(hookPath, 'utf8') === templateText);
319
+ ok('e2e seeded old hook: .cjs installed', fs.readFileSync(path.join(hooksDir, CJS_NAME), 'utf8') === cjsText);
320
+ fs.rmSync(dir, { recursive: true, force: true });
321
+ }
322
+
323
+ // seeded legacy single-file JS hook -> upgraded to wrapper + .cjs
324
+ {
325
+ const dir = mktmp();
326
+ execFileSync('git', ['init', '-q'], { cwd: dir, env: gitEnv() });
327
+ const hooksDir = path.join(dir, '.git', 'hooks');
328
+ fs.mkdirSync(hooksDir, { recursive: true });
329
+ const hookPath = path.join(hooksDir, 'commit-msg');
330
+ fs.writeFileSync(hookPath, legacyJsText);
331
+ fs.chmodSync(hookPath, 0o755);
332
+ const out = runScript(['--repo', dir], { env: gitEnv() });
333
+ const parsed = assertSingleJsonLine(out, 'e2e seeded legacy JS hook');
334
+ ok('e2e seeded legacy JS hook: status upgraded', parsed.status === 'upgraded');
335
+ ok('e2e seeded legacy JS hook: wrapper installed', fs.readFileSync(hookPath, 'utf8') === templateText);
336
+ ok('e2e seeded legacy JS hook: .cjs installed', fs.readFileSync(path.join(hooksDir, CJS_NAME), 'utf8') === cjsText);
249
337
  fs.rmSync(dir, { recursive: true, force: true });
250
338
  }
251
339
 
@@ -263,6 +351,7 @@ function assertSingleJsonLine(out, label) {
263
351
  const parsed = assertSingleJsonLine(out, 'e2e seeded foreign hook');
264
352
  ok('e2e seeded foreign hook: status foreign', parsed.status === 'foreign');
265
353
  ok('e2e seeded foreign hook: bytes untouched', fs.readFileSync(hookPath, 'utf8') === foreign);
354
+ ok('e2e seeded foreign hook: no .cjs written', !fs.existsSync(path.join(hooksDir, CJS_NAME)));
266
355
  fs.rmSync(dir, { recursive: true, force: true });
267
356
  }
268
357
 
@@ -293,6 +382,7 @@ function assertSingleJsonLine(out, label) {
293
382
  const parsed = assertSingleJsonLine(out, 'e2e core.hooksPath old hook');
294
383
  ok('e2e core.hooksPath old hook: status upgraded', parsed.status === 'upgraded');
295
384
  ok('e2e core.hooksPath old hook: upgraded in .githooks', fs.readFileSync(hookPath, 'utf8') === templateText);
385
+ ok('e2e core.hooksPath old hook: .cjs lands in .githooks', fs.readFileSync(path.join(githooksDir, CJS_NAME), 'utf8') === cjsText);
296
386
  fs.rmSync(dir, { recursive: true, force: true });
297
387
  }
298
388
 
@@ -311,6 +401,7 @@ function assertSingleJsonLine(out, label) {
311
401
  const parsed = assertSingleJsonLine(out, 'e2e core.hooksPath foreign hook');
312
402
  ok('e2e core.hooksPath foreign hook: status foreign', parsed.status === 'foreign');
313
403
  ok('e2e core.hooksPath foreign hook: untouched', fs.readFileSync(hookPath, 'utf8') === foreign);
404
+ ok('e2e core.hooksPath foreign hook: no .cjs written', !fs.existsSync(path.join(githooksDir, CJS_NAME)));
314
405
  fs.rmSync(dir, { recursive: true, force: true });
315
406
  }
316
407