@boyingliu01/xp-gate 0.18.4 → 0.19.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 (248) hide show
  1. package/adapter-common.sh +27 -0
  2. package/adapters/powershell.sh +85 -33
  3. package/adapters/typescript.sh +8 -8
  4. package/build-integrity/__tests__/import-resolver.test.ts +22 -0
  5. package/build-integrity/__tests__/orchestrator.test.ts +1 -2
  6. package/build-integrity/gate-10.ts +7 -0
  7. package/gate-12-file-hygiene.sh +230 -0
  8. package/hooks/adapter-common.sh +27 -0
  9. package/hooks/gate-10.sh +105 -0
  10. package/hooks/lib/validate-code-walkthrough.cjs +125 -0
  11. package/hooks/pre-commit +130 -33
  12. package/hooks/pre-push +15 -91
  13. package/lib/__tests__/check-version.test.js +67 -46
  14. package/lib/__tests__/doctor.test.js +7 -6
  15. package/lib/__tests__/gate-runner.test.js +267 -0
  16. package/lib/__tests__/init.test.js +32 -0
  17. package/lib/__tests__/npm-package.test.js +13 -0
  18. package/lib/__tests__/phase-transition.test.js +604 -12
  19. package/lib/__tests__/sprint-discovery.test.ts +0 -1
  20. package/lib/__tests__/sprint-status-rework.test.js +100 -1
  21. package/lib/__tests__/update-hooks.test.js +143 -10
  22. package/lib/__tests__/upgrade-exec.test.js +73 -45
  23. package/lib/__tests__/upgrade-skill-sync.test.js +1 -1
  24. package/lib/__tests__/upgrade.test.js +12 -11
  25. package/lib/doctor.js +2 -1
  26. package/lib/gate-runner.js +68 -23
  27. package/lib/init.js +34 -5
  28. package/lib/install-skill.js +1 -0
  29. package/lib/mattpocock-license-verdict.json +1 -1
  30. package/lib/phase-transition.js +203 -23
  31. package/lib/shared-paths.js +2 -0
  32. package/lib/sprint-status.js +15 -4
  33. package/lib/test-alignment.ts +3 -13
  34. package/lib/update-hooks.js +129 -15
  35. package/mock-policy/AGENTS.md +5 -5
  36. package/mock-policy/__tests__/gate-m3.test.ts +0 -5
  37. package/mock-policy/__tests__/scope-scanner.test.ts +8 -2
  38. package/mutation/AGENTS.md +4 -4
  39. package/mutation/__tests__/mutmut-runner.test.ts +443 -2
  40. package/mutation/__tests__/update-baseline.test.ts +0 -2
  41. package/mutation/runners/mutmut-runner.ts +67 -17
  42. package/package.json +9 -3
  43. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  44. package/plugins/claude-code/skills/clipboard-vision/SKILL.md +47 -0
  45. package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +65 -0
  46. package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +185 -0
  47. package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +138 -0
  48. package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +273 -0
  49. package/plugins/claude-code/skills/clipboard-vision/describe-clipboard.ps1 +115 -0
  50. package/plugins/claude-code/skills/clipboard-vision/describe-clipboard.sh +161 -0
  51. package/plugins/claude-code/skills/clipboard-vision/scripts/clipboard-vision.ps1 +86 -0
  52. package/plugins/claude-code/skills/clipboard-vision/scripts/clipboard-vision.sh +137 -0
  53. package/plugins/claude-code/skills/delphi-review/.delphi-config.json.example +24 -27
  54. package/plugins/claude-code/skills/delphi-review/AGENTS.md +13 -12
  55. package/plugins/claude-code/skills/delphi-review/INSTALL.md +15 -17
  56. package/plugins/claude-code/skills/delphi-review/SKILL.md +30 -29
  57. package/plugins/claude-code/skills/delphi-review/opencode.json.delphi.example +6 -10
  58. package/plugins/claude-code/skills/delphi-review/references/code-walkthrough.md +33 -49
  59. package/plugins/claude-code/skills/delphi-review/references/requirements.md +66 -54
  60. package/plugins/claude-code/skills/delphi-review/references/round-templates.md +7 -7
  61. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +5 -5
  62. package/plugins/claude-code/skills/sprint-flow/SKILL.md +4 -4
  63. package/plugins/claude-code/skills/sprint-flow/references/components/skill-invocations.md +2 -2
  64. package/plugins/claude-code/skills/sprint-flow/references/force-levels.md +37 -35
  65. package/plugins/claude-code/skills/sprint-flow/references/orchestration-rules.md +1 -1
  66. package/plugins/claude-code/skills/sprint-flow/references/phase-1-prep.md +1 -1
  67. package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +24 -14
  68. package/plugins/claude-code/skills/sprint-flow/references/phase-4-verify.md +6 -6
  69. package/plugins/claude-code/skills/sprint-flow/references/phase-overview.md +2 -2
  70. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
  71. package/plugins/dsh/README.md +25 -0
  72. package/plugins/dsh/cordis.patch.yml +7 -0
  73. package/plugins/dsh/package.json +70 -0
  74. package/plugins/dsh/scripts/prepack.cjs +82 -0
  75. package/plugins/dsh/skills/admin-template-guidelines/SKILL.md +113 -0
  76. package/plugins/dsh/skills/admin-template-guidelines/references/rule-1.md +91 -0
  77. package/plugins/dsh/skills/admin-template-guidelines/references/rule-2.md +155 -0
  78. package/plugins/dsh/skills/admin-template-guidelines/references/rule-3.md +70 -0
  79. package/plugins/dsh/skills/admin-template-guidelines/references/rule-4.md +154 -0
  80. package/plugins/dsh/skills/admin-template-guidelines/references/rule-5.md +126 -0
  81. package/plugins/dsh/skills/admin-template-guidelines/references/rule-6.md +180 -0
  82. package/plugins/dsh/skills/batch-grill-me/SKILL.md +15 -0
  83. package/plugins/dsh/skills/clipboard-vision/SKILL.md +47 -0
  84. package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +65 -0
  85. package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +185 -0
  86. package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +138 -0
  87. package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +273 -0
  88. package/plugins/dsh/skills/clipboard-vision/describe-clipboard.ps1 +115 -0
  89. package/plugins/dsh/skills/clipboard-vision/describe-clipboard.sh +161 -0
  90. package/plugins/dsh/skills/clipboard-vision/scripts/clipboard-vision.ps1 +86 -0
  91. package/plugins/dsh/skills/clipboard-vision/scripts/clipboard-vision.sh +137 -0
  92. package/plugins/dsh/skills/delphi-review/.delphi-config.json.example +70 -0
  93. package/plugins/dsh/skills/delphi-review/AGENTS.md +67 -0
  94. package/plugins/dsh/skills/delphi-review/INSTALL.md +150 -0
  95. package/plugins/dsh/skills/delphi-review/SKILL.md +559 -0
  96. package/plugins/dsh/skills/delphi-review/hooks.md +19 -0
  97. package/plugins/dsh/skills/delphi-review/opencode.json.delphi.example +52 -0
  98. package/plugins/dsh/skills/delphi-review/references/code-walkthrough.md +470 -0
  99. package/plugins/dsh/skills/delphi-review/references/orchestrator-dispatch.md +64 -0
  100. package/plugins/dsh/skills/delphi-review/references/requirements.md +402 -0
  101. package/plugins/dsh/skills/delphi-review/references/round-templates.md +134 -0
  102. package/plugins/dsh/skills/delphi-review/scripts/verify-consensus.sh +43 -0
  103. package/plugins/dsh/skills/delphi-review/tools.md +29 -0
  104. package/plugins/dsh/skills/domain-modeling/ADR-FORMAT.md +64 -0
  105. package/plugins/dsh/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  106. package/plugins/dsh/skills/domain-modeling/SKILL.md +74 -0
  107. package/plugins/dsh/skills/grill-with-docs/SKILL.md +7 -0
  108. package/plugins/dsh/skills/grilling/SKILL.md +12 -0
  109. package/plugins/dsh/skills/improve-codebase-architecture/SKILL.md +156 -0
  110. package/plugins/dsh/skills/ralph-loop/SKILL.md +198 -0
  111. package/plugins/dsh/skills/ralph-loop/references/components.md +113 -0
  112. package/plugins/dsh/skills/ralph-loop/references/phase-2-build-ralph.md +89 -0
  113. package/plugins/dsh/skills/ralph-loop/references/workflow-details.md +99 -0
  114. package/plugins/dsh/skills/ralph-loop/templates/progress-log.md +36 -0
  115. package/plugins/dsh/skills/sprint-flow/AGENTS.md +103 -0
  116. package/plugins/dsh/skills/sprint-flow/SKILL.md +411 -0
  117. package/plugins/dsh/skills/sprint-flow/__tests__/sprint-flow.test.cjs +457 -0
  118. package/plugins/dsh/skills/sprint-flow/hooks.md +18 -0
  119. package/plugins/dsh/skills/sprint-flow/references/components/memory.md +87 -0
  120. package/plugins/dsh/skills/sprint-flow/references/components/middleware.md +75 -0
  121. package/plugins/dsh/skills/sprint-flow/references/components/skill-invocations.md +105 -0
  122. package/plugins/dsh/skills/sprint-flow/references/components/system-prompt.md +27 -0
  123. package/plugins/dsh/skills/sprint-flow/references/components/tool-descriptions.md +95 -0
  124. package/plugins/dsh/skills/sprint-flow/references/force-levels.md +204 -0
  125. package/plugins/dsh/skills/sprint-flow/references/orchestration-rules.md +236 -0
  126. package/plugins/dsh/skills/sprint-flow/references/phase-1-prep.md +294 -0
  127. package/plugins/dsh/skills/sprint-flow/references/phase-2-design.md +273 -0
  128. package/plugins/dsh/skills/sprint-flow/references/phase-3-build.md +348 -0
  129. package/plugins/dsh/skills/sprint-flow/references/phase-4-verify.md +225 -0
  130. package/plugins/dsh/skills/sprint-flow/references/phase-5-ship.md +222 -0
  131. package/plugins/dsh/skills/sprint-flow/references/phase-6-close.md +266 -0
  132. package/plugins/dsh/skills/sprint-flow/references/phase-overview.md +486 -0
  133. package/plugins/dsh/skills/sprint-flow/scripts/verify-sprint-state.sh +38 -0
  134. package/plugins/dsh/skills/sprint-flow/templates/auto-estimate-learning-log.md +136 -0
  135. package/plugins/dsh/skills/sprint-flow/templates/auto-estimate-output-template.md +110 -0
  136. package/plugins/dsh/skills/sprint-flow/templates/emergent-issues-template.md +120 -0
  137. package/plugins/dsh/skills/sprint-flow/templates/pain-document-template.md +115 -0
  138. package/plugins/dsh/skills/sprint-flow/templates/sprint-progress-template.md +140 -0
  139. package/plugins/dsh/skills/sprint-flow/templates/sprint-summary-template.md +121 -0
  140. package/plugins/dsh/skills/sprint-flow/tools.md +29 -0
  141. package/plugins/dsh/skills/test-driven-development/SKILL.md +371 -0
  142. package/plugins/dsh/skills/test-driven-development/testing-anti-patterns.md +299 -0
  143. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +56 -0
  144. package/plugins/dsh/skills/test-specification-alignment/SKILL.md +205 -0
  145. package/plugins/dsh/skills/test-specification-alignment/references/alignment-verification-algorithm.md +493 -0
  146. package/plugins/dsh/skills/test-specification-alignment/references/phase2-constraint-enforcement.md +431 -0
  147. package/plugins/dsh/skills/test-specification-alignment/references/specification-format.md +348 -0
  148. package/plugins/dsh/skills/to-issues/SKILL.md +277 -0
  149. package/plugins/dsh/src/command.test.ts +102 -0
  150. package/plugins/dsh/src/command.ts +75 -0
  151. package/plugins/dsh/src/contract.test.ts +62 -0
  152. package/plugins/dsh/src/gate-runner.test.ts +100 -0
  153. package/plugins/dsh/src/gate-runner.ts +58 -0
  154. package/plugins/dsh/src/index.test.ts +97 -0
  155. package/plugins/dsh/src/index.ts +94 -0
  156. package/plugins/dsh/src/skills.test.ts +58 -0
  157. package/plugins/dsh/tsconfig.build.json +13 -0
  158. package/plugins/dsh/tsconfig.json +19 -0
  159. package/plugins/dsh/vitest.config.ts +8 -0
  160. package/plugins/opencode/package.json +1 -1
  161. package/plugins/opencode/scripts/prepack.cjs +1 -0
  162. package/plugins/opencode/skills/clipboard-vision/SKILL.md +47 -0
  163. package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +65 -0
  164. package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +185 -0
  165. package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +138 -0
  166. package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +273 -0
  167. package/plugins/opencode/skills/clipboard-vision/describe-clipboard.ps1 +115 -0
  168. package/plugins/opencode/skills/clipboard-vision/describe-clipboard.sh +161 -0
  169. package/plugins/opencode/skills/clipboard-vision/scripts/clipboard-vision.ps1 +86 -0
  170. package/plugins/opencode/skills/clipboard-vision/scripts/clipboard-vision.sh +137 -0
  171. package/plugins/opencode/skills/delphi-review/.delphi-config.json.example +24 -27
  172. package/plugins/opencode/skills/delphi-review/AGENTS.md +13 -12
  173. package/plugins/opencode/skills/delphi-review/INSTALL.md +15 -17
  174. package/plugins/opencode/skills/delphi-review/SKILL.md +30 -29
  175. package/plugins/opencode/skills/delphi-review/opencode.json.delphi.example +6 -10
  176. package/plugins/opencode/skills/delphi-review/references/code-walkthrough.md +33 -49
  177. package/plugins/opencode/skills/delphi-review/references/requirements.md +66 -54
  178. package/plugins/opencode/skills/delphi-review/references/round-templates.md +7 -7
  179. package/plugins/opencode/skills/sprint-flow/AGENTS.md +5 -5
  180. package/plugins/opencode/skills/sprint-flow/SKILL.md +4 -4
  181. package/plugins/opencode/skills/sprint-flow/references/components/skill-invocations.md +2 -2
  182. package/plugins/opencode/skills/sprint-flow/references/force-levels.md +37 -35
  183. package/plugins/opencode/skills/sprint-flow/references/orchestration-rules.md +1 -1
  184. package/plugins/opencode/skills/sprint-flow/references/phase-1-prep.md +1 -1
  185. package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +24 -14
  186. package/plugins/opencode/skills/sprint-flow/references/phase-4-verify.md +6 -6
  187. package/plugins/opencode/skills/sprint-flow/references/phase-overview.md +2 -2
  188. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
  189. package/plugins/qoder/plugin.json +1 -1
  190. package/plugins/qoder/skills/clipboard-vision/SKILL.md +47 -0
  191. package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +65 -0
  192. package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +185 -0
  193. package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +138 -0
  194. package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +273 -0
  195. package/plugins/qoder/skills/clipboard-vision/clipboard-vision/SKILL.md +47 -0
  196. package/plugins/qoder/skills/clipboard-vision/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +65 -0
  197. package/plugins/qoder/skills/clipboard-vision/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +185 -0
  198. package/plugins/qoder/skills/clipboard-vision/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +138 -0
  199. package/plugins/qoder/skills/clipboard-vision/clipboard-vision/__tests__/clipboard-vision.test.cjs +273 -0
  200. package/plugins/qoder/skills/clipboard-vision/clipboard-vision/scripts/clipboard-vision.ps1 +86 -0
  201. package/plugins/qoder/skills/clipboard-vision/clipboard-vision/scripts/clipboard-vision.sh +137 -0
  202. package/plugins/qoder/skills/clipboard-vision/scripts/clipboard-vision.ps1 +86 -0
  203. package/plugins/qoder/skills/clipboard-vision/scripts/clipboard-vision.sh +137 -0
  204. package/plugins/qoder/skills/delphi-review/.delphi-config.json.example +3 -11
  205. package/plugins/qoder/skills/delphi-review/AGENTS.md +15 -17
  206. package/plugins/qoder/skills/delphi-review/INSTALL.md +12 -12
  207. package/plugins/qoder/skills/delphi-review/SKILL.md +58 -25
  208. package/plugins/qoder/skills/delphi-review/opencode.json.delphi.example +1 -1
  209. package/plugins/qoder/skills/delphi-review/references/code-walkthrough.md +33 -49
  210. package/plugins/qoder/skills/sprint-flow/AGENTS.md +5 -5
  211. package/plugins/qoder/skills/sprint-flow/SKILL.md +2 -2
  212. package/plugins/qoder/skills/sprint-flow/references/force-levels.md +38 -35
  213. package/plugins/qoder/skills/sprint-flow/references/phase-1-prep.md +1 -1
  214. package/plugins/qoder/skills/sprint-flow/references/phase-2-design.md +48 -6
  215. package/plugins/qoder/skills/sprint-flow/references/phase-4-verify.md +5 -6
  216. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
  217. package/principles/AGENTS.md +4 -4
  218. package/scripts/__tests__/sync-package-content.test.js +30 -0
  219. package/scripts/delphi-external-review.cjs +194 -63
  220. package/scripts/sync-package-content.js +9 -2
  221. package/skills/clipboard-vision/SKILL.md +47 -0
  222. package/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +65 -0
  223. package/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +185 -0
  224. package/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +138 -0
  225. package/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +273 -0
  226. package/skills/clipboard-vision/describe-clipboard.ps1 +115 -0
  227. package/skills/clipboard-vision/describe-clipboard.sh +161 -0
  228. package/skills/clipboard-vision/scripts/clipboard-vision.ps1 +86 -0
  229. package/skills/clipboard-vision/scripts/clipboard-vision.sh +137 -0
  230. package/skills/delphi-review/.delphi-config.json.example +24 -27
  231. package/skills/delphi-review/AGENTS.md +13 -12
  232. package/skills/delphi-review/INSTALL.md +15 -17
  233. package/skills/delphi-review/SKILL.md +30 -29
  234. package/skills/delphi-review/opencode.json.delphi.example +6 -10
  235. package/skills/delphi-review/references/code-walkthrough.md +33 -49
  236. package/skills/delphi-review/references/requirements.md +66 -54
  237. package/skills/delphi-review/references/round-templates.md +7 -7
  238. package/skills/sprint-flow/AGENTS.md +5 -5
  239. package/skills/sprint-flow/SKILL.md +4 -4
  240. package/skills/sprint-flow/references/components/skill-invocations.md +2 -2
  241. package/skills/sprint-flow/references/force-levels.md +37 -35
  242. package/skills/sprint-flow/references/orchestration-rules.md +1 -1
  243. package/skills/sprint-flow/references/phase-1-prep.md +1 -1
  244. package/skills/sprint-flow/references/phase-2-design.md +24 -14
  245. package/skills/sprint-flow/references/phase-4-verify.md +6 -6
  246. package/skills/sprint-flow/references/phase-overview.md +2 -2
  247. package/skills/test-specification-alignment/AGENTS.md +4 -4
  248. package/sprint-gate.sh +291 -0
@@ -25,20 +25,58 @@ function cleanupDir(dir) {
25
25
  describe('check-version.js — REQ-001-01', () => {
26
26
  let mod;
27
27
  let origHome;
28
+ let origCacheDir;
28
29
  let tmpHome;
30
+ let tmpCacheDir;
29
31
  let origReadFileSync;
32
+ let origHttpsGet;
33
+
34
+ function installFakeHttpsGet(latestVersion) {
35
+ const https = require('https');
36
+ const saved = https.get;
37
+ https.get = (_url, options, callbackArg) => {
38
+ const callback = typeof options === 'function' ? options : callbackArg;
39
+ if (!callback) return { on: () => undefined, destroy: () => undefined };
40
+ const response = {
41
+ statusCode: 200,
42
+ on: (event, handler) => {
43
+ const eventArgs = {
44
+ data: [JSON.stringify({ latest: latestVersion })],
45
+ end: [],
46
+ };
47
+ if (Object.hasOwn(eventArgs, event)) handler(...eventArgs[event]);
48
+ return response;
49
+ },
50
+ };
51
+ callback(response);
52
+ return { on: () => undefined, destroy: () => undefined };
53
+ };
54
+ return () => {
55
+ https.get = saved;
56
+ };
57
+ }
30
58
 
31
59
  beforeEach(() => {
32
60
  vi.resetModules();
33
61
  origHome = process.env.HOME;
34
- tmpHome = fakeHome();
62
+ origCacheDir = process.env.XP_GATE_CACHE_DIR;
35
63
  origReadFileSync = fs.readFileSync;
64
+ origHttpsGet = require('https').get;
65
+ tmpHome = undefined;
66
+ tmpCacheDir = undefined;
67
+ tmpHome = fakeHome();
68
+ tmpCacheDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ckv-cache-'));
69
+ process.env.XP_GATE_CACHE_DIR = tmpCacheDir;
36
70
  });
37
71
 
38
72
  afterEach(() => {
39
73
  process.env.HOME = origHome;
74
+ if (origCacheDir === undefined) delete process.env.XP_GATE_CACHE_DIR;
75
+ else process.env.XP_GATE_CACHE_DIR = origCacheDir;
40
76
  cleanupDir(tmpHome);
41
- fs.readFileSync = origReadFileSync;
77
+ cleanupDir(tmpCacheDir);
78
+ if (origReadFileSync) fs.readFileSync = origReadFileSync;
79
+ if (origHttpsGet) require('https').get = origHttpsGet;
42
80
  });
43
81
 
44
82
  // ──────────────────────────────────────────
@@ -159,6 +197,14 @@ describe('check-version.js — REQ-001-01', () => {
159
197
  expect(() => mod.clearCache()).not.toThrow();
160
198
  });
161
199
 
200
+ it('does not touch the home-directory cache when XP_GATE_CACHE_DIR is set', () => {
201
+ const homeCache = path.join(tmpHome, '.xp-gate', 'version-cache.json');
202
+ fs.mkdirSync(path.dirname(homeCache), { recursive: true });
203
+ fs.writeFileSync(homeCache, 'sentinel');
204
+ mod.clearCache();
205
+ expect(fs.readFileSync(homeCache, 'utf8')).toBe('sentinel');
206
+ });
207
+
162
208
  it('cachePath returns null when XP_GATE_DIR is inaccessible', () => {
163
209
  // Verify clearCache handles null cachePath gracefully
164
210
  expect(() => mod.clearCache()).not.toThrow();
@@ -216,31 +262,14 @@ describe('check-version.js — REQ-001-01', () => {
216
262
  delete require.cache[resolved];
217
263
  }
218
264
 
219
- function withMockedHttps(latestVersion, fn) {
265
+ async function withMockedHttps(latestVersion, fn) {
220
266
  const fsm = require('fs');
221
- const osm = require('os');
222
- const cpPath = require('path').join(osm.homedir(), '.xp-gate', 'version-cache.json');
267
+ const cpPath = require('path').join(process.env.XP_GATE_CACHE_DIR, 'version-cache.json');
223
268
  if (fsm.existsSync(cpPath)) {
224
269
  try { fsm.unlinkSync(cpPath); } catch { }
225
270
  }
226
271
  evictCache();
227
- const https = require('https');
228
- const saved = https.get;
229
- const body = JSON.stringify({ latest: latestVersion });
230
- https.get = (_url, _opts, cb) => {
231
- const callback = typeof _opts === 'function' ? _opts : cb;
232
- if (!callback) return { on: () => undefined, destroy: () => undefined };
233
- const mockRes = {
234
- statusCode: 200,
235
- on: (evt, handler) => {
236
- if (evt === 'data') handler(body);
237
- if (evt === 'end') handler();
238
- return mockRes;
239
- },
240
- };
241
- callback(mockRes);
242
- return { on: () => undefined, destroy: () => undefined };
243
- };
272
+ const restoreHttpsGet = installFakeHttpsGet(latestVersion);
244
273
  // Mock getLocalVersion to return a fixed version so tests don't
245
274
  // depend on the real package.json version (which drifts over time).
246
275
  const savedReadFileSync = fsm.readFileSync;
@@ -256,9 +285,9 @@ describe('check-version.js — REQ-001-01', () => {
256
285
  };
257
286
  try {
258
287
  const m = require('../check-version');
259
- return fn(m);
288
+ return await fn(m);
260
289
  } finally {
261
- https.get = saved;
290
+ restoreHttpsGet();
262
291
  fsm.readFileSync = savedReadFileSync;
263
292
  }
264
293
  }
@@ -301,39 +330,31 @@ describe('check-version.js — REQ-001-01', () => {
301
330
  delete require.cache[resolved];
302
331
  }
303
332
 
304
- function withMockedHttpsBox(latestVersion, fn) {
333
+ async function withMockedHttpsBox(latestVersion, fn) {
305
334
  const fsm = require('fs');
306
- const osm = require('os');
307
- const cpPath = require('path').join(osm.homedir(), '.xp-gate', 'version-cache.json');
335
+ const cpPath = require('path').join(process.env.XP_GATE_CACHE_DIR, 'version-cache.json');
308
336
  if (fsm.existsSync(cpPath)) {
309
337
  try { fsm.unlinkSync(cpPath); } catch { }
310
338
  }
311
339
  evictCache();
312
- const https = require('https');
313
- const saved = https.get;
314
- const body = JSON.stringify({ latest: latestVersion });
315
- https.get = (_url, _opts, cb) => {
316
- const callback = typeof _opts === 'function' ? _opts : cb;
317
- if (!callback) return { on: () => undefined, destroy: () => undefined };
318
- const mockRes = {
319
- statusCode: 200,
320
- on: (evt, handler) => {
321
- if (evt === 'data') handler(body);
322
- if (evt === 'end') handler();
323
- return mockRes;
324
- },
325
- };
326
- callback(mockRes);
327
- return { on: () => undefined, destroy: () => undefined };
328
- };
340
+ const restoreHttpsGet = installFakeHttpsGet(latestVersion);
329
341
  try {
330
342
  const m = require('../check-version');
331
- return fn(m, cpPath);
343
+ return await fn(m, cpPath);
332
344
  } finally {
333
- https.get = saved;
345
+ restoreHttpsGet();
334
346
  }
335
347
  }
336
348
 
349
+ it('restores HTTPS after an asynchronous callback rejects', async () => {
350
+ const savedGet = require('https').get;
351
+ await expect(withMockedHttpsBox('0.18.0', async () => {
352
+ await Promise.resolve();
353
+ throw new Error('callback failed');
354
+ })).rejects.toThrow('callback failed');
355
+ expect(require('https').get).toBe(savedGet);
356
+ });
357
+
337
358
  it('rejects null version from registry — cache NOT written', async () => {
338
359
  await withMockedHttpsBox(null, async (m, cpPath) => {
339
360
  const r = await m.getRemoteVersion('@test/pkg');
@@ -66,7 +66,7 @@ describe('doctor', () => {
66
66
  // and reloading it would restore the full tool list, breaking the test mock setup.
67
67
  const modulesToInvalidate = [
68
68
  '../doctor', '../uninstall', '../init', '../shared-paths',
69
- '../check-version.js',
69
+ '../check-version.js', '../doctor-tui',
70
70
  ];
71
71
  for (const mod of modulesToInvalidate) {
72
72
  try { delete require.cache[require.resolve(mod)]; } catch { /* first load */ }
@@ -126,7 +126,7 @@ describe('doctor', () => {
126
126
  fs.mkdirSync(dir, { recursive: true });
127
127
  fs.writeFileSync(
128
128
  path.join(dir, 'pre-push'),
129
- '#!/bin/bash\n# Pre-push Hook - Code Walkthrough Result Validator\n'
129
+ '#!/bin/bash\n# Pre-push Hook — 8 gates:\n'
130
130
  );
131
131
  }
132
132
 
@@ -148,7 +148,7 @@ describe('doctor', () => {
148
148
  path.join(dir, 'adapters', 'python.sh'),
149
149
  '#!/usr/bin/env bash\necho "py adapter"\n'
150
150
  );
151
- const gateScripts = ['gate-3.sh', 'gate-4.sh', 'gate-7.sh', 'gate-8.sh', 'gate-9.sh'];
151
+ const gateScripts = ['gate-3.sh', 'gate-4.sh', 'gate-7.sh', 'gate-8.sh', 'gate-9.sh', 'gate-10.sh'];
152
152
  for (const script of gateScripts) {
153
153
  fs.writeFileSync(path.join(dir, 'adapters', script), `#!/bin/bash\n# ${script}\n`);
154
154
  }
@@ -187,7 +187,7 @@ describe('doctor', () => {
187
187
  fs.mkdirSync(globalAdaptersDir(), { recursive: true });
188
188
  fs.writeFileSync(path.join(globalAdaptersDir(), 'typescript.sh'), '#!/usr/bin/env bash\necho "ts adapter"\n');
189
189
  fs.writeFileSync(path.join(globalAdaptersDir(), 'python.sh'), '#!/usr/bin/env bash\necho "py adapter"\n');
190
- const gateScripts = ['gate-3.sh', 'gate-4.sh', 'gate-7.sh', 'gate-8.sh', 'gate-9.sh'];
190
+ const gateScripts = ['gate-3.sh', 'gate-4.sh', 'gate-7.sh', 'gate-8.sh', 'gate-9.sh', 'gate-10.sh'];
191
191
  for (const script of gateScripts) {
192
192
  fs.writeFileSync(path.join(globalAdaptersDir(), script), `#!/bin/bash\n# ${script}\n`);
193
193
  }
@@ -941,7 +941,7 @@ describe('doctor', () => {
941
941
 
942
942
  function createGateScripts(dir) {
943
943
  fs.mkdirSync(dir, { recursive: true });
944
- const gateScripts = ['gate-3.sh', 'gate-4.sh', 'gate-7.sh', 'gate-8.sh', 'gate-9.sh'];
944
+ const gateScripts = ['gate-3.sh', 'gate-4.sh', 'gate-7.sh', 'gate-8.sh', 'gate-9.sh', 'gate-10.sh'];
945
945
  for (const script of gateScripts) {
946
946
  fs.writeFileSync(path.join(dir, script), `#!/bin/bash\n# ${script}\n`);
947
947
  }
@@ -962,7 +962,7 @@ describe('doctor', () => {
962
962
  expect.stringContaining('Gate scripts')
963
963
  );
964
964
  expect(logSpy).toHaveBeenCalledWith(
965
- expect.stringContaining('5 gate script(s)')
965
+ expect.stringContaining('6 gate script(s)')
966
966
  );
967
967
  });
968
968
 
@@ -1005,6 +1005,7 @@ describe('doctor', () => {
1005
1005
  expect(fs.existsSync(path.join(projectAdaptersDir(), 'gate-7.sh'))).toBe(true);
1006
1006
  expect(fs.existsSync(path.join(projectAdaptersDir(), 'gate-8.sh'))).toBe(true);
1007
1007
  expect(fs.existsSync(path.join(projectAdaptersDir(), 'gate-9.sh'))).toBe(true);
1008
+ expect(fs.existsSync(path.join(projectAdaptersDir(), 'gate-10.sh'))).toBe(true);
1008
1009
  expect(logSpy).toHaveBeenCalledWith(
1009
1010
  expect.stringContaining('Restored')
1010
1011
  );
@@ -0,0 +1,267 @@
1
+ /**
2
+ * @test REQ-standalone-gates gate-runner.js
3
+ * @intent 验证 runGateAdapter 独立运行 gate 适配器片段时能提供钩子上下文
4
+ * (gate_start_ms / record_gate_audit / now_ms / PROJECT_LANG / CHANGED_FILES),
5
+ * 避免 "command not found" 误报。
6
+ * @covers AC-STANDALONE-GATES-01, AC-STANDALONE-GATES-02, AC-STANDALONE-GATES-03, AC-STANDALONE-GATES-04, AC-STANDALONE-GATES-05
7
+ */
8
+ const fs = require('node:fs');
9
+ const path = require('node:path');
10
+ const os = require('node:os');
11
+ const { execFileSync, spawnSync } = require('node:child_process');
12
+
13
+ let tmpProject;
14
+ let tmpAdapters;
15
+
16
+ beforeEach(() => {
17
+ tmpProject = fs.mkdtempSync(path.join(os.tmpdir(), 'xpgate-runner-'));
18
+ tmpAdapters = path.join(tmpProject, 'githooks', 'adapters');
19
+ fs.mkdirSync(tmpAdapters, { recursive: true });
20
+ });
21
+
22
+ afterEach(() => {
23
+ if (tmpProject && fs.existsSync(tmpProject)) {
24
+ fs.rmSync(tmpProject, { recursive: true, force: true });
25
+ }
26
+ });
27
+
28
+ function writeAdapterCommon() {
29
+ fs.writeFileSync(
30
+ path.join(tmpAdapters, 'adapter-common.sh'),
31
+ 'detect_project_lang() { echo "typescript"; }'
32
+ );
33
+ }
34
+
35
+ function writeGateFragment(fragmentLines) {
36
+ fs.writeFileSync(
37
+ path.join(tmpAdapters, 'gate-9.sh'),
38
+ ['# gate-9 fragment', ...fragmentLines, 'exit 0'].join('\n')
39
+ );
40
+ }
41
+
42
+ function shellParameter(expression) {
43
+ return ['$', '{', expression, '}'].join('');
44
+ }
45
+
46
+ function initializeGitProject() {
47
+ execFileSync('git', ['init', '-q'], { cwd: tmpProject });
48
+ execFileSync('git', ['config', 'user.email', 'test@example.com'], { cwd: tmpProject });
49
+ execFileSync('git', ['config', 'user.name', 'XP-Gate Test'], { cwd: tmpProject });
50
+ fs.writeFileSync(path.join(tmpProject, 'tracked.ts'), 'const value = 1;\n');
51
+ execFileSync('git', ['add', 'tracked.ts'], { cwd: tmpProject });
52
+ execFileSync('git', ['-c', 'core.hooksPath=/dev/null', 'commit', '-qm', 'initial'], {
53
+ cwd: tmpProject,
54
+ });
55
+ }
56
+
57
+ // Runs runGateAdapter in a child node process (worker threads forbid process
58
+ // chdir, and runGateAdapter execs bash), capturing stdout/exit status.
59
+ function runChildAdapter(gateRunnerAbsPath, fragmentPath, cwd = tmpProject) {
60
+ const script = [
61
+ `const { runGateAdapter } = require(${JSON.stringify(gateRunnerAbsPath)});`,
62
+ `runGateAdapter(${JSON.stringify(fragmentPath)}, ${JSON.stringify(cwd)});`,
63
+ "console.log('__ADAPTER_DONE__');",
64
+ ].join('\n');
65
+ try {
66
+ const stdout = execFileSync(process.execPath, ['-e', script], {
67
+ cwd,
68
+ encoding: 'utf8',
69
+ timeout: 30000,
70
+ stdio: ['ignore', 'pipe', 'pipe'],
71
+ });
72
+ return { status: 0, stdout };
73
+ } catch (err) {
74
+ return { status: err.status ?? 1, stdout: (err.stdout || '').toString() };
75
+ }
76
+ }
77
+
78
+ describe('gate-runner.js — runGateAdapter', () => {
79
+ it('sources the fragment with hook helpers available (REQ-standalone-gates-01)', () => {
80
+ const runnerAbsPath = path.resolve(__dirname, '../gate-runner.js');
81
+ writeAdapterCommon();
82
+ writeGateFragment([
83
+ 'USES_START=$(gate_start_ms)',
84
+ `if [ -z "${shellParameter('PROJECT_LANG:-')}" ]; then exit 99; fi`,
85
+ `if [ -z "${shellParameter('CHANGED_FILES+x')}" ]; then exit 98; fi`,
86
+ 'record_gate_audit "gate-9" "sast" "PASS" "0" "$USES_START"',
87
+ ]);
88
+ const fragPath = path.join(tmpAdapters, 'gate-9.sh');
89
+ const { status, stdout } = runChildAdapter(runnerAbsPath, fragPath);
90
+ expect(status).toBe(0);
91
+ expect(stdout).toContain('__ADAPTER_DONE__');
92
+ }, 30000);
93
+
94
+ it('defines now_ms/gate_start_ms/record_gate_audit/PROJECT_LANG/CHANGED_FILES before sourcing (REQ-standalone-gates-02)', () => {
95
+ const runnerAbsPath = path.resolve(__dirname, '../gate-runner.js');
96
+ writeAdapterCommon();
97
+ writeGateFragment([
98
+ 'if ! command -v gate_start_ms >/dev/null 2>&1; then exit 97; fi',
99
+ 'if ! command -v record_gate_audit >/dev/null 2>&1; then exit 96; fi',
100
+ 'if ! command -v now_ms >/dev/null 2>&1; then exit 95; fi',
101
+ `if [ -z "${shellParameter('PROJECT_LANG:-')}" ]; then exit 94; fi`,
102
+ `if [ -z "${shellParameter('CHANGED_FILES+x')}" ]; then exit 93; fi`,
103
+ ]);
104
+ const fragPath = path.join(tmpAdapters, 'gate-9.sh');
105
+ const { status, stdout } = runChildAdapter(runnerAbsPath, fragPath);
106
+ expect(status).toBe(0);
107
+ expect(stdout).toContain('__ADAPTER_DONE__');
108
+ }, 30000);
109
+
110
+ it('sources adapter-common.sh to provide detect_project_lang (REQ-standalone-gates-03)', () => {
111
+ const runnerAbsPath = path.resolve(__dirname, '../gate-runner.js');
112
+ writeAdapterCommon();
113
+ writeGateFragment([
114
+ `if [ "${shellParameter('PROJECT_LANG:-')}" != "typescript" ]; then exit 92; fi`,
115
+ ]);
116
+ const fragPath = path.join(tmpAdapters, 'gate-9.sh');
117
+ const { status, stdout } = runChildAdapter(runnerAbsPath, fragPath);
118
+ expect(status).toBe(0);
119
+ expect(stdout).toContain('__ADAPTER_DONE__');
120
+ }, 30000);
121
+
122
+ it('propagates changed files from the project git working tree (REQ-standalone-gates-04)', () => {
123
+ const runnerAbsPath = path.resolve(__dirname, '../gate-runner.js');
124
+ writeAdapterCommon();
125
+ initializeGitProject();
126
+ fs.writeFileSync(path.join(tmpProject, 'tracked.ts'), 'const value = 2;\n');
127
+ writeGateFragment([
128
+ `if [ "${shellParameter('CHANGED_FILES')}" != "tracked.ts" ]; then exit 91; fi`,
129
+ ]);
130
+
131
+ const fragPath = path.join(tmpAdapters, 'gate-9.sh');
132
+ const { status, stdout } = runChildAdapter(runnerAbsPath, fragPath);
133
+
134
+ expect(status).toBe(0);
135
+ expect(stdout).toContain('__ADAPTER_DONE__');
136
+ }, 30000);
137
+
138
+ it('runs a standalone gate bash fallback through the public CLI (REQ-standalone-gates-05)', () => {
139
+ const packageFixture = path.join(tmpProject, 'package');
140
+ fs.cpSync(path.resolve(__dirname, '../..'), packageFixture, { recursive: true });
141
+ const cliPath = path.join(packageFixture, 'bin', 'xp-gate.js');
142
+ const gatesDir = path.join(tmpProject, 'githooks', 'gates');
143
+ fs.mkdirSync(gatesDir, { recursive: true });
144
+ fs.writeFileSync(
145
+ path.join(gatesDir, 'gate-9-public-fallback.sh'),
146
+ [
147
+ 'if ! command -v gate_start_ms >/dev/null 2>&1; then exit 90; fi',
148
+ `if [ -z "${shellParameter('CHANGED_FILES+x')}" ]; then exit 89; fi`,
149
+ 'echo "__PUBLIC_FALLBACK__"',
150
+ ].join('\n')
151
+ );
152
+
153
+ const result = spawnSync(process.execPath, [cliPath, 'check', '9'], {
154
+ cwd: tmpProject,
155
+ encoding: 'utf8',
156
+ timeout: 30000,
157
+ });
158
+
159
+ expect(result.status).toBe(0);
160
+ expect(result.stdout).toContain('__PUBLIC_FALLBACK__');
161
+ }, 30000);
162
+
163
+ it('isolates fallback resolution and git context to the requested target path (REQ-standalone-gates-06)', () => {
164
+ const packageFixture = path.join(tmpProject, 'package');
165
+ const callerProject = path.join(tmpProject, 'caller');
166
+ const targetProject = path.join(tmpProject, 'target');
167
+ fs.cpSync(path.resolve(__dirname, '../..'), packageFixture, { recursive: true });
168
+ fs.mkdirSync(path.join(callerProject, 'githooks', 'gates'), { recursive: true });
169
+ fs.mkdirSync(path.join(targetProject, 'githooks', 'gates'), { recursive: true });
170
+ fs.writeFileSync(
171
+ path.join(callerProject, 'githooks', 'gates', 'gate-9-caller.sh'),
172
+ 'echo "__CALLER_REPO__"\n'
173
+ );
174
+ fs.writeFileSync(
175
+ path.join(targetProject, 'githooks', 'gates', 'gate-9-target.sh'),
176
+ [
177
+ 'if [ "$PWD" != "$EXPECTED_TARGET" ]; then exit 88; fi',
178
+ 'echo "__TARGET_REPO__"',
179
+ ].join('\n')
180
+ );
181
+
182
+ const result = spawnSync(
183
+ process.execPath,
184
+ [path.join(packageFixture, 'bin', 'xp-gate.js'), 'check', '9', targetProject],
185
+ {
186
+ cwd: callerProject,
187
+ env: { ...process.env, EXPECTED_TARGET: targetProject },
188
+ encoding: 'utf8',
189
+ timeout: 30000,
190
+ }
191
+ );
192
+
193
+ expect(result.status).toBe(0);
194
+ expect(result.stdout).toContain('__TARGET_REPO__');
195
+ expect(result.stdout).not.toContain('__CALLER_REPO__');
196
+ }, 30000);
197
+
198
+ it('sources fragment and helper paths containing shell metacharacters literally (REQ-standalone-gates-07)', () => {
199
+ const runnerAbsPath = path.resolve(__dirname, '../gate-runner.js');
200
+ const hostileRoot = path.join(tmpProject, 'space $HOME `marker` "quoted"');
201
+ tmpAdapters = path.join(hostileRoot, 'githooks', 'adapters');
202
+ fs.mkdirSync(tmpAdapters, { recursive: true });
203
+ writeAdapterCommon();
204
+ writeGateFragment([
205
+ `if [ "${shellParameter('PROJECT_LANG')}" != "typescript" ]; then exit 87; fi`,
206
+ 'echo "__HOSTILE_PATH_OK__"',
207
+ ]);
208
+
209
+ const result = runChildAdapter(
210
+ runnerAbsPath,
211
+ path.join(tmpAdapters, 'gate-9.sh'),
212
+ hostileRoot
213
+ );
214
+
215
+ expect(result.status).toBe(0);
216
+ expect(result.stdout).toContain('__HOSTILE_PATH_OK__');
217
+ }, 30000);
218
+
219
+ it('loads parent-layout adapter-common and packaged now-ms through the public CLI (REQ-standalone-gates-08)', () => {
220
+ const packageFixture = path.join(tmpProject, 'package');
221
+ fs.cpSync(path.resolve(__dirname, '../..'), packageFixture, { recursive: true });
222
+ const gatesDir = path.join(tmpProject, 'githooks', 'gates');
223
+ fs.mkdirSync(gatesDir, { recursive: true });
224
+ fs.writeFileSync(
225
+ path.join(tmpProject, 'githooks', 'adapter-common.sh'),
226
+ 'detect_project_lang() { echo "parent-layout-language"; }\n'
227
+ );
228
+ fs.writeFileSync(
229
+ path.join(gatesDir, 'gate-9-parent-helper.sh'),
230
+ [
231
+ `if [ "${shellParameter('PROJECT_LANG')}" != "parent-layout-language" ]; then exit 86; fi`,
232
+ 'if [ "$(now_ms)" != "424242" ]; then exit 85; fi',
233
+ 'echo "__PARENT_HELPER_OK__"',
234
+ ].join('\n')
235
+ );
236
+ fs.writeFileSync(
237
+ path.join(packageFixture, 'hooks', 'lib', 'now-ms.sh'),
238
+ 'now_ms() { echo 424242; }\n'
239
+ );
240
+
241
+ const result = spawnSync(
242
+ process.execPath,
243
+ [path.join(packageFixture, 'bin', 'xp-gate.js'), 'check', '9'],
244
+ { cwd: tmpProject, encoding: 'utf8', timeout: 30000 }
245
+ );
246
+
247
+ expect(result.status).toBe(0);
248
+ expect(result.stdout).toContain('__PARENT_HELPER_OK__');
249
+ }, 30000);
250
+
251
+ it('propagates a failing fragment exit through the public CLI (REQ-standalone-gates-09)', () => {
252
+ const packageFixture = path.join(tmpProject, 'package');
253
+ const targetProject = path.join(tmpProject, 'failing-target');
254
+ fs.cpSync(path.resolve(__dirname, '../..'), packageFixture, { recursive: true });
255
+ const gatesDir = path.join(targetProject, 'githooks', 'gates');
256
+ fs.mkdirSync(gatesDir, { recursive: true });
257
+ fs.writeFileSync(path.join(gatesDir, 'gate-9-failure.sh'), 'exit 23\n');
258
+
259
+ const result = spawnSync(
260
+ process.execPath,
261
+ [path.join(packageFixture, 'bin', 'xp-gate.js'), 'check', '9', targetProject],
262
+ { cwd: tmpProject, encoding: 'utf8', timeout: 30000 }
263
+ );
264
+
265
+ expect(result.status).toBe(1);
266
+ }, 30000);
267
+ });
@@ -447,4 +447,36 @@ describe('init', () => {
447
447
  expect(fs.existsSync(path.join(tmpProject, '.qoder', 'agents'))).toBe(false);
448
448
  });
449
449
  });
450
+
451
+ describe('installModuleRuntimeDeps', () => {
452
+ it('writes a package.json with module runtime deps and runs npm install', () => {
453
+ let execArgs = null;
454
+ vi.spyOn(childProcess, 'execSync').mockImplementation((cmd, opts) => {
455
+ execArgs = { cmd, cwd: opts && opts.cwd };
456
+ return Buffer.from('');
457
+ });
458
+
459
+ const { installModuleRuntimeDeps } = require('../init');
460
+ installModuleRuntimeDeps(tmpHome);
461
+
462
+ const pkg = JSON.parse(fs.readFileSync(path.join(tmpHome, 'package.json'), 'utf8'));
463
+ expect(pkg.dependencies['js-yaml']).toBeDefined();
464
+ expect(pkg.dependencies['zod']).toBeDefined();
465
+ expect(execArgs).not.toBeNull();
466
+ expect(execArgs.cmd).toContain('npm install');
467
+ expect(execArgs.cwd).toBe(tmpHome);
468
+ });
469
+
470
+ it('fails gracefully (warning, no throw) when npm install fails', () => {
471
+ vi.spyOn(childProcess, 'execSync').mockImplementation(() => {
472
+ throw new Error('registry offline');
473
+ });
474
+ const warnBefore = console.warn;
475
+
476
+ const { installModuleRuntimeDeps } = require('../init');
477
+ expect(() => installModuleRuntimeDeps(tmpHome)).not.toThrow();
478
+
479
+ expect(console.warn).toHaveBeenCalled();
480
+ });
481
+ });
450
482
  });
@@ -97,3 +97,16 @@ describe('publishConfig for scoped packages', () => {
97
97
  expect(pkg.publishConfig?.access).toBe('public');
98
98
  });
99
99
  });
100
+
101
+ describe('published adapter-common.sh', () => {
102
+ it('matches the canonical Git hook adapter helper', () => {
103
+ const canonical = fs.readFileSync(
104
+ path.join(pkgDir, '..', '..', 'githooks', 'adapter-common.sh'),
105
+ 'utf8'
106
+ );
107
+ const published = fs.readFileSync(path.join(pkgDir, 'adapter-common.sh'), 'utf8');
108
+
109
+ expect(published).toBe(canonical);
110
+ expect(published).toContain('run_without_git_context');
111
+ });
112
+ });