pi-harness-runtime 1.1.440 → 1.1.441

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 (349) hide show
  1. package/harness/e2e/auto-test-loop.ts +2 -0
  2. package/harness/e2e/browser-scrapers/base-browser-scraper.ts +2 -0
  3. package/harness/e2e/browser-scrapers/chrome-profile-manager.ts +2 -0
  4. package/harness/e2e/browser-scrapers/codex-browser-scraper.ts +2 -0
  5. package/harness/e2e/tools-detector.ts +2 -0
  6. package/harness/key-loader.ts +2 -0
  7. package/harness/langchain/checkpointer.ts +2 -0
  8. package/harness/langchain/cron-cli.ts +2 -0
  9. package/harness/langchain/graph.ts +2 -0
  10. package/harness/notification-center.ts +2 -0
  11. package/harness/secret-provider.ts +2 -0
  12. package/index.ts +2 -0
  13. package/package.json +7 -1
  14. package/packages/a2a-adapter/package.json +1 -1
  15. package/packages/agent-policy/dist/extension.d.ts.map +1 -1
  16. package/packages/agent-policy/dist/extension.js +2 -0
  17. package/packages/agent-policy/dist/extension.js.map +1 -1
  18. package/packages/agent-policy/package.json +1 -1
  19. package/packages/agent-policy/src/extension.ts +2 -0
  20. package/packages/architecture-generator/package.json +1 -1
  21. package/packages/auth/dist/minimax-browser-auth.d.ts.map +1 -1
  22. package/packages/auth/dist/minimax-browser-auth.js +23 -1
  23. package/packages/auth/dist/minimax-browser-auth.js.map +1 -1
  24. package/packages/auth/package.json +1 -1
  25. package/packages/auth/src/minimax-browser-auth.ts +30 -5
  26. package/packages/autonomous-refactor/package.json +1 -1
  27. package/packages/autonomous-runtime/dist/inbox.d.ts.map +1 -1
  28. package/packages/autonomous-runtime/dist/inbox.js +2 -0
  29. package/packages/autonomous-runtime/dist/inbox.js.map +1 -1
  30. package/packages/autonomous-runtime/package.json +1 -1
  31. package/packages/autonomous-runtime/src/inbox.ts +2 -0
  32. package/packages/cache-strategy/package.json +1 -1
  33. package/packages/capability-registry/package.json +1 -1
  34. package/packages/checkpoint/package.json +1 -1
  35. package/packages/cli-plugin-sdk/package.json +1 -1
  36. package/packages/clipboard/dist/src/clipboard.d.ts.map +1 -1
  37. package/packages/clipboard/dist/src/clipboard.js +2 -0
  38. package/packages/clipboard/dist/src/clipboard.js.map +1 -1
  39. package/packages/clipboard/dist/src/copy-sync.d.ts.map +1 -1
  40. package/packages/clipboard/dist/src/copy-sync.js +2 -0
  41. package/packages/clipboard/dist/src/copy-sync.js.map +1 -1
  42. package/packages/clipboard/dist/src/gist-relay.d.ts.map +1 -1
  43. package/packages/clipboard/dist/src/gist-relay.js +2 -0
  44. package/packages/clipboard/dist/src/gist-relay.js.map +1 -1
  45. package/packages/clipboard/package.json +1 -1
  46. package/packages/clipboard/src/clipboard.ts +2 -0
  47. package/packages/clipboard/src/copy-sync.ts +2 -0
  48. package/packages/clipboard/src/gist-relay.ts +2 -0
  49. package/packages/clipboard-plugin/package.json +1 -1
  50. package/packages/code-generation/package.json +1 -1
  51. package/packages/code-review/package.json +1 -1
  52. package/packages/codex-adapter/package.json +1 -1
  53. package/packages/codex-watcher/package.json +1 -1
  54. package/packages/config-capture/package.json +1 -1
  55. package/packages/connection-runtime/package.json +1 -1
  56. package/packages/context-compiler/package.json +1 -1
  57. package/packages/context-discovery/package.json +1 -1
  58. package/packages/context-manager/package.json +1 -1
  59. package/packages/contracts/dist/delegation-store.js +1 -4
  60. package/packages/contracts/dist/workspace-env.js +28 -1
  61. package/packages/contracts/package.json +1 -1
  62. package/packages/contracts/src/delegation-store.ts +9 -4
  63. package/packages/contracts/src/workspace-env.ts +29 -4
  64. package/packages/cookie-sanitizer/package.json +1 -1
  65. package/packages/cost-optimizer/package.json +1 -1
  66. package/packages/dependency-analyzer/package.json +1 -1
  67. package/packages/django-plugin/package.json +1 -1
  68. package/packages/doc-generator/package.json +1 -1
  69. package/packages/durable-review/package.json +1 -1
  70. package/packages/e2e-tester/package.json +1 -1
  71. package/packages/evaluation-engine/package.json +1 -1
  72. package/packages/evaluation-runner/package.json +1 -1
  73. package/packages/event-bus/package.json +1 -1
  74. package/packages/event-sink/package.json +1 -1
  75. package/packages/event-store/dist/src/store.d.ts.map +1 -1
  76. package/packages/event-store/dist/src/store.js +2 -0
  77. package/packages/event-store/dist/src/store.js.map +1 -1
  78. package/packages/event-store/package.json +1 -1
  79. package/packages/event-store/src/store.ts +3 -0
  80. package/packages/experience-replay/package.json +1 -1
  81. package/packages/feedback-collector/package.json +1 -1
  82. package/packages/file-copy-helper/package.json +1 -1
  83. package/packages/framework-detector/package.json +1 -1
  84. package/packages/framework-plugin-sdk/package.json +1 -1
  85. package/packages/frappe-plugin/package.json +1 -1
  86. package/packages/generic-web-plugin/dist/analyzer.d.ts.map +1 -1
  87. package/packages/generic-web-plugin/dist/analyzer.js +2 -0
  88. package/packages/generic-web-plugin/dist/analyzer.js.map +1 -1
  89. package/packages/generic-web-plugin/package.json +1 -1
  90. package/packages/generic-web-plugin/src/analyzer.ts +3 -0
  91. package/packages/health-monitor/package.json +1 -1
  92. package/packages/intent-analyzer/package.json +1 -1
  93. package/packages/jev-judge/package.json +1 -1
  94. package/packages/knowledge-graph/package.json +1 -1
  95. package/packages/knowledge-retrieval/dist/mcp-client.d.ts.map +1 -1
  96. package/packages/knowledge-retrieval/dist/mcp-client.js +0 -1
  97. package/packages/knowledge-retrieval/dist/mcp-client.js.map +1 -1
  98. package/packages/knowledge-retrieval/package.json +1 -4
  99. package/packages/knowledge-retrieval/src/mcp-client.ts +0 -1
  100. package/packages/laravel-plugin/package.json +1 -1
  101. package/packages/learning-engine/package.json +1 -1
  102. package/packages/logging/package.json +1 -1
  103. package/packages/mcp-adapter/dist/server.d.ts.map +1 -1
  104. package/packages/mcp-adapter/dist/server.js +2 -0
  105. package/packages/mcp-adapter/dist/server.js.map +1 -1
  106. package/packages/mcp-adapter/package.json +1 -1
  107. package/packages/mcp-adapter/src/server.ts +3 -0
  108. package/packages/memory-engine/package.json +1 -1
  109. package/packages/milestone-manager/package.json +1 -1
  110. package/packages/model-registry/package.json +1 -1
  111. package/packages/nextjs-plugin/package.json +1 -1
  112. package/packages/notification/package.json +1 -1
  113. package/packages/observability/package.json +1 -1
  114. package/packages/okf-indexer/package.json +1 -1
  115. package/packages/paperclip-integration/dist/index.d.ts.map +1 -1
  116. package/packages/paperclip-integration/dist/index.js +2 -0
  117. package/packages/paperclip-integration/dist/index.js.map +1 -1
  118. package/packages/paperclip-integration/package.json +1 -1
  119. package/packages/paperclip-integration/src/index.ts +2 -0
  120. package/packages/performance-optimizer/package.json +1 -1
  121. package/packages/permission-mode/package.json +1 -1
  122. package/packages/privilege-broker/dist/audit.d.ts.map +1 -1
  123. package/packages/privilege-broker/dist/audit.js +2 -0
  124. package/packages/privilege-broker/dist/audit.js.map +1 -1
  125. package/packages/privilege-broker/package.json +1 -1
  126. package/packages/privilege-broker/src/audit.ts +2 -0
  127. package/packages/project-analyzer/package.json +1 -1
  128. package/packages/project-bootstrap/package.json +1 -1
  129. package/packages/projection-engine/package.json +1 -1
  130. package/packages/prompt-compiler/dist/compiler.d.ts.map +1 -1
  131. package/packages/prompt-compiler/dist/compiler.js +2 -0
  132. package/packages/prompt-compiler/dist/compiler.js.map +1 -1
  133. package/packages/prompt-compiler/package.json +1 -1
  134. package/packages/prompt-compiler/src/compiler.ts +2 -0
  135. package/packages/prompt-versioning/package.json +1 -1
  136. package/packages/provider-adapter-sdk/package.json +1 -1
  137. package/packages/provider-router/package.json +1 -1
  138. package/packages/provider-selector/package.json +1 -1
  139. package/packages/providers/package.json +1 -1
  140. package/packages/qdrant-skills/package.json +1 -1
  141. package/packages/quota-manager/package.json +1 -1
  142. package/packages/rate-limiter/package.json +1 -1
  143. package/packages/react-vite-plugin/package.json +1 -1
  144. package/packages/relay/package.json +1 -1
  145. package/packages/release-manager/package.json +1 -1
  146. package/packages/requirement-compiler/package.json +1 -1
  147. package/packages/runtime/package.json +1 -1
  148. package/packages/scheduler/package.json +1 -1
  149. package/packages/scheduler-adapter/package.json +1 -1
  150. package/packages/session/package.json +1 -1
  151. package/packages/session-api/package.json +1 -1
  152. package/packages/session-export/package.json +1 -1
  153. package/packages/shared-context/package.json +1 -1
  154. package/packages/skill-mcp-client/dist/client.d.ts.map +1 -1
  155. package/packages/skill-mcp-client/dist/client.js +0 -1
  156. package/packages/skill-mcp-client/dist/client.js.map +1 -1
  157. package/packages/skill-mcp-client/package.json +1 -4
  158. package/packages/skill-mcp-client/src/client.ts +0 -1
  159. package/packages/skill-registry/package.json +1 -1
  160. package/packages/skills/dist/parser.d.ts.map +1 -1
  161. package/packages/skills/dist/parser.js +2 -0
  162. package/packages/skills/dist/parser.js.map +1 -1
  163. package/packages/skills/dist/skill-versioning.d.ts.map +1 -1
  164. package/packages/skills/dist/skill-versioning.js +2 -0
  165. package/packages/skills/dist/skill-versioning.js.map +1 -1
  166. package/packages/skills/package.json +1 -1
  167. package/packages/skills/src/parser.ts +3 -0
  168. package/packages/skills/src/skill-versioning.ts +2 -0
  169. package/packages/sprint-planner/package.json +1 -1
  170. package/packages/subscription-engine/package.json +1 -1
  171. package/packages/task-compiler/package.json +1 -1
  172. package/packages/tencentdb-memory/package.json +1 -1
  173. package/packages/tencentdb-sync/package.json +1 -1
  174. package/packages/test-data-generator/package.json +1 -1
  175. package/packages/test-generator/package.json +1 -1
  176. package/packages/todo-bd-sync/package.json +1 -1
  177. package/packages/token-estimation/package.json +1 -1
  178. package/packages/token-optimizer/package.json +1 -1
  179. package/packages/tooling-preference/package.json +1 -1
  180. package/packages/trajectory/package.json +1 -1
  181. package/packages/tui/package.json +1 -1
  182. package/packages/two-phase-review/package.json +1 -1
  183. package/packages/types/package.json +1 -1
  184. package/packages/voice-api/package.json +1 -1
  185. package/packages/webmcp-tester/dist/index.js +0 -2
  186. package/packages/webmcp-tester/package.json +1 -1
  187. package/packages/webmcp-tester/src/index.ts +0 -3
  188. package/packages/wiki/package.json +1 -1
  189. package/packages/workflow-events/package.json +1 -1
  190. package/packages/workspace-scanner/package.json +1 -1
  191. package/packages/worktree/package.json +1 -1
  192. package/packages/write-review/dist/trigger.d.ts.map +1 -1
  193. package/packages/write-review/dist/trigger.js +1 -2
  194. package/packages/write-review/dist/trigger.js.map +1 -1
  195. package/packages/write-review/package.json +1 -1
  196. package/packages/write-review/src/trigger.ts +1 -2
  197. package/scripts/lib/runtime-imports.ts +189 -3
  198. package/scripts/telegram-webhook-server.ts +2 -0
  199. package/scripts/verify-package-contents.ts +148 -1
  200. package/harness/e2e/tools-detector.test.ts +0 -155
  201. package/harness/glm-quota-countdown.test.ts +0 -323
  202. package/harness/langchain/agents.test.ts +0 -39
  203. package/harness/langchain/checkpointer.test.ts +0 -243
  204. package/harness/langchain/daemon.test.ts +0 -459
  205. package/harness/langchain/model-router.test.ts +0 -191
  206. package/harness/langchain/ping-pong-middleware.test.ts +0 -69
  207. package/harness/langchain/scoreboard.test.ts +0 -267
  208. package/harness/langchain/status-line.test.ts +0 -106
  209. package/harness/langchain/surge.test.ts +0 -467
  210. package/harness/langchain/widget.test.ts +0 -251
  211. package/harness/skill-sync.test.ts +0 -57
  212. package/harness/ssh-detach-interceptor.test.ts +0 -263
  213. package/harness/ssh-hang-recovery.test.ts +0 -86
  214. package/packages/a2a-adapter/test/a2a.test.ts +0 -151
  215. package/packages/agent-policy/test/interlock.test.ts +0 -101
  216. package/packages/agent-policy/test/policy-retention.test.ts +0 -380
  217. package/packages/agent-policy/test/policy.test.ts +0 -387
  218. package/packages/auth/test/auth.test.ts +0 -238
  219. package/packages/autonomous-refactor/test/refactor.test.ts +0 -202
  220. package/packages/autonomous-runtime/test/types.test.ts +0 -95
  221. package/packages/cache-strategy/test/cache.test.ts +0 -137
  222. package/packages/capability-registry/test/registry.test.ts +0 -204
  223. package/packages/checkpoint/test/engine.test.ts +0 -265
  224. package/packages/cli-plugin-sdk/test/cli.test.ts +0 -45
  225. package/packages/code-generation/test/generator.test.ts +0 -238
  226. package/packages/code-review/test/engine.test.ts +0 -302
  227. package/packages/codex-adapter/test/provider.test.ts +0 -142
  228. package/packages/codex-watcher/test/watch.test.ts +0 -453
  229. package/packages/context-compiler/test/context-compiler.test.ts +0 -746
  230. package/packages/context-manager/test/context.test.ts +0 -198
  231. package/packages/contracts/test/delegation.test.ts +0 -350
  232. package/packages/contracts/test/workspace-env.test.ts +0 -802
  233. package/packages/cookie-sanitizer/test/infer.test.ts +0 -110
  234. package/packages/cookie-sanitizer/test/normalize.test.ts +0 -113
  235. package/packages/cookie-sanitizer/test/parse.test.ts +0 -204
  236. package/packages/cookie-sanitizer/test/sync.test.ts +0 -237
  237. package/packages/cookie-sanitizer/test/watcher.test.ts +0 -27
  238. package/packages/cost-optimizer/test/cost-optimizer.test.ts +0 -438
  239. package/packages/dependency-analyzer/test/analyzer.test.ts +0 -49
  240. package/packages/django-plugin/test/analyzer.test.ts +0 -195
  241. package/packages/doc-generator/test/docs.test.ts +0 -168
  242. package/packages/durable-review/test/acceptance.test.ts +0 -357
  243. package/packages/durable-review/test/smoke.test.ts +0 -107
  244. package/packages/evaluation-engine/test/engine.test.ts +0 -277
  245. package/packages/evaluation-runner/test/runner.test.ts +0 -168
  246. package/packages/event-bus/test/bus.test.ts +0 -100
  247. package/packages/event-store/test/store.test.ts +0 -115
  248. package/packages/experience-replay/test/replay.test.ts +0 -267
  249. package/packages/feedback-collector/test/collector.test.ts +0 -142
  250. package/packages/framework-detector/test/detector.test.ts +0 -75
  251. package/packages/framework-plugin-sdk/test/manager.test.ts +0 -320
  252. package/packages/frappe-plugin/test/analyzer.test.ts +0 -250
  253. package/packages/generic-web-plugin/test/analyzer.test.ts +0 -134
  254. package/packages/health-monitor/test/health.test.ts +0 -231
  255. package/packages/intent-analyzer/test/analyzer.test.ts +0 -72
  256. package/packages/intent-analyzer/test/ping-pong.test.ts +0 -96
  257. package/packages/jev-judge/tests/auto-continue-context.test.ts +0 -280
  258. package/packages/jev-judge/tests/continuation-controller.test.ts +0 -1396
  259. package/packages/jev-judge/tests/jev-provider-chain.test.ts +0 -536
  260. package/packages/jev-judge/tests/outcome-application.test.ts +0 -132
  261. package/packages/jev-judge/tests/question-risk.test.ts +0 -129
  262. package/packages/jev-judge/tests/typesafe-provider.test.ts +0 -640
  263. package/packages/jev-judge/tests/validation-ledger.test.ts +0 -181
  264. package/packages/jev-judge/tests/validation-plan.test.ts +0 -170
  265. package/packages/jev-judge/tests/validation-tie-breaker.test.ts +0 -88
  266. package/packages/laravel-plugin/test/analyzer.test.ts +0 -220
  267. package/packages/learning-engine/test/engine.test.ts +0 -331
  268. package/packages/logging/test/buffer.test.ts +0 -44
  269. package/packages/logging/test/classification.test.ts +0 -137
  270. package/packages/logging/test/config.test.ts +0 -97
  271. package/packages/logging/test/fanout.test.ts +0 -186
  272. package/packages/logging/test/identity-transport.test.ts +0 -197
  273. package/packages/logging/test/incidents.test.ts +0 -404
  274. package/packages/logging/test/ingest-schema.test.ts +0 -184
  275. package/packages/logging/test/integration.test.ts +0 -140
  276. package/packages/logging/test/log-http.test.ts +0 -246
  277. package/packages/logging/test/redact.test.ts +0 -109
  278. package/packages/logging/test/sampler.test.ts +0 -57
  279. package/packages/logging/test/server.test.ts +0 -444
  280. package/packages/logging/test/shipper.test.ts +0 -152
  281. package/packages/logging/test/spool.test.ts +0 -96
  282. package/packages/mcp-adapter/test/server.test.ts +0 -91
  283. package/packages/memory-engine/test/engine.test.ts +0 -456
  284. package/packages/model-registry/test/registry.test.ts +0 -342
  285. package/packages/nextjs-plugin/test/analyzer.test.ts +0 -149
  286. package/packages/notification/test/approval-payload.test.ts +0 -269
  287. package/packages/notification/test/detail-format.test.ts +0 -106
  288. package/packages/notification/test/notification.test.ts +0 -375
  289. package/packages/notification/test/pending-question-lifecycle.test.ts +0 -195
  290. package/packages/notification/test/pending-question.test.ts +0 -331
  291. package/packages/notification/test/tooling-proposal-payload.test.ts +0 -212
  292. package/packages/observability/test/observability.test.ts +0 -1350
  293. package/packages/performance-optimizer/test/optimizer.test.ts +0 -267
  294. package/packages/project-analyzer/test/index.test.ts +0 -1719
  295. package/packages/prompt-compiler/test/prompt-compiler.test.ts +0 -813
  296. package/packages/prompt-versioning/test/versioning.test.ts +0 -112
  297. package/packages/provider-adapter-sdk/test/sdk.test.ts +0 -220
  298. package/packages/provider-router/test/provider-router.test.ts +0 -405
  299. package/packages/provider-selector/test/selector.test.ts +0 -192
  300. package/packages/providers/test/providers.test.ts +0 -225
  301. package/packages/qdrant-skills/test/collection-info.test.ts +0 -165
  302. package/packages/qdrant-skills/test/embedder-batch.test.ts +0 -184
  303. package/packages/qdrant-skills/test/embedder.test.ts +0 -97
  304. package/packages/quota-manager/test/quota.test.ts +0 -432
  305. package/packages/rate-limiter/test/limiter.test.ts +0 -128
  306. package/packages/react-vite-plugin/test/analyzer.test.ts +0 -139
  307. package/packages/relay/test/client.test.ts +0 -446
  308. package/packages/relay/test/relay-e2e.test.ts +0 -299
  309. package/packages/relay/test/relay-endpoint.test.ts +0 -278
  310. package/packages/release-manager/test/release.test.ts +0 -215
  311. package/packages/requirement-compiler/test/requirement-compiler.test.ts +0 -743
  312. package/packages/runtime/test/runtime-api.test.ts +0 -169
  313. package/packages/runtime/test/runtime.test.ts +0 -297
  314. package/packages/scheduler/test/scheduler.test.ts +0 -125
  315. package/packages/session/test/manager.test.ts +0 -233
  316. package/packages/shared-context/test/shared-context.test.ts +0 -114
  317. package/packages/skill-registry/test/registry.test.ts +0 -508
  318. package/packages/skills/test/init-skills.test.ts +0 -114
  319. package/packages/skills/test/remote-skills.test.ts +0 -360
  320. package/packages/skills/test/remote-source.test.ts +0 -248
  321. package/packages/skills/test/sanitize.test.ts +0 -250
  322. package/packages/skills/test/vector-indexing.test.ts +0 -205
  323. package/packages/skills/test/vector-schema.test.ts +0 -180
  324. package/packages/skills/test/vector-threshold.test.ts +0 -108
  325. package/packages/sprint-planner/test/planner.test.ts +0 -187
  326. package/packages/task-compiler/test/task-compiler.test.ts +0 -628
  327. package/packages/test-data-generator/test/generator.test.ts +0 -265
  328. package/packages/test-generator/test/generator.test.ts +0 -156
  329. package/packages/token-estimation/test/token-estimation.test.ts +0 -207
  330. package/packages/token-optimizer/test/optimizer.test.ts +0 -141
  331. package/packages/tooling-preference/tests/changed-files.test.ts +0 -124
  332. package/packages/tooling-preference/tests/detect.test.ts +0 -188
  333. package/packages/tooling-preference/tests/proposal.test.ts +0 -107
  334. package/packages/tooling-preference/tests/quality-check.test.ts +0 -272
  335. package/packages/tooling-preference/tests/registry.test.ts +0 -51
  336. package/packages/tooling-preference/tests/store.test.ts +0 -99
  337. package/packages/trajectory/test/store.test.ts +0 -250
  338. package/packages/tui/test/harness-footer.test.ts +0 -559
  339. package/packages/tui/test/harness-header.test.ts +0 -590
  340. package/packages/two-phase-review/test/classify.test.ts +0 -102
  341. package/packages/two-phase-review/test/helpers.ts +0 -72
  342. package/packages/two-phase-review/test/rules.test.ts +0 -150
  343. package/packages/two-phase-review/test/runtime.test.ts +0 -362
  344. package/packages/two-phase-review/test/scope.test.ts +0 -186
  345. package/packages/two-phase-review/test/signals.test.ts +0 -183
  346. package/packages/two-phase-review/test/state.test.ts +0 -201
  347. package/packages/wiki/test/basic.test.ts +0 -242
  348. package/packages/workspace-scanner/test/scanner.test.ts +0 -43
  349. package/packages/worktree/test/worktree.test.ts +0 -163
@@ -1,1719 +0,0 @@
1
- /**
2
- * Project Analyzer Tests (comprehensive)
3
- *
4
- * Tests for ProjectAnalyzer, detectLanguages, detectPackageManager,
5
- * command discovery, caching, and filesystem walker.
6
- */
7
-
8
- import { describe, test, expect, afterEach, beforeEach } from "bun:test";
9
- import { mkdtemp, writeFile, mkdir, rm } from "node:fs/promises";
10
- import { join } from "node:path";
11
- import { tmpdir } from "node:os";
12
-
13
- import {
14
- ProjectAnalyzer,
15
- createProjectAnalyzer,
16
- detectLanguages,
17
- } from "../src/index.js";
18
- import {
19
- AnalysisCache,
20
- hashRuleFiles,
21
- hashManifestFiles,
22
- } from "../src/cache.js";
23
- import {
24
- parsePackageJsonScripts,
25
- categorizeCommands,
26
- detectPackageManager,
27
- parsePythonCommands,
28
- parseComposerScripts,
29
- } from "../src/command-discovery.js";
30
- import { FileSystemWalker } from "../src/walker.js";
31
- import { GenericFrameworkDetector, scanSignals } from "../src/signals.js";
32
- import {
33
- parseRuleFile,
34
- discoverRuleFiles,
35
- mergeRules,
36
- } from "../src/rule-discovery.js";
37
-
38
- // --- Temp Fixture Helpers ---------------------------------------------
39
-
40
- const tempDirs: string[] = [];
41
-
42
- async function tmpDir(): Promise<string> {
43
- const d = await mkdtemp(join(tmpdir(), "proj-analyzer-"));
44
- tempDirs.push(d);
45
- return d;
46
- }
47
-
48
- afterEach(async () => {
49
- for (const d of tempDirs) {
50
- try {
51
- await rm(d, { recursive: true, force: true });
52
- } catch {
53
- // ignore
54
- }
55
- }
56
- tempDirs.length = 0;
57
- });
58
-
59
- // --- detectLanguages Tests --------------------------------------------
60
-
61
- describe("detectLanguages", () => {
62
- test("detects TypeScript files", () => {
63
- const files = [
64
- { relativePath: "src/index.ts", size: 1000 },
65
- { relativePath: "src/app.ts", size: 500 },
66
- { relativePath: "src/types.ts", size: 300 },
67
- ];
68
- const langs = detectLanguages(files);
69
- expect(langs.find((l) => l.name === "TypeScript")).toBeDefined();
70
- });
71
-
72
- test("detects Python files", () => {
73
- const files = [
74
- { relativePath: "app/main.py", size: 5000 },
75
- { relativePath: "app/models.py", size: 2000 },
76
- ];
77
- const langs = detectLanguages(files);
78
- expect(langs.find((l) => l.name === "Python")).toBeDefined();
79
- });
80
-
81
- test("detects Go files", () => {
82
- const files = [{ relativePath: "cmd/main.go", size: 3000 }];
83
- const langs = detectLanguages(files);
84
- expect(langs.find((l) => l.name === "Go")).toBeDefined();
85
- });
86
-
87
- test("detects Java and Kotlin files", () => {
88
- const files = [
89
- { relativePath: "src/Main.java", size: 2000 },
90
- { relativePath: "src/Utils.kt", size: 1000 },
91
- ];
92
- const langs = detectLanguages(files);
93
- expect(langs.find((l) => l.name === "Java")).toBeDefined();
94
- expect(langs.find((l) => l.name === "JavaScript")).toBeUndefined();
95
- });
96
-
97
- test("detects Rust files", () => {
98
- const files = [{ relativePath: "src/lib.rs", size: 4000 }];
99
- const langs = detectLanguages(files);
100
- expect(langs.find((l) => l.name === "Rust")).toBeDefined();
101
- });
102
-
103
- test("detects C# files", () => {
104
- const files = [{ relativePath: "Program.cs", size: 1500 }];
105
- const langs = detectLanguages(files);
106
- expect(langs.find((l) => l.name === "C#")).toBeDefined();
107
- });
108
-
109
- test("detects HTML and CSS", () => {
110
- const files = [
111
- { relativePath: "index.html", size: 2000 },
112
- { relativePath: "styles.css", size: 500 },
113
- { relativePath: "theme.scss", size: 800 },
114
- ];
115
- const langs = detectLanguages(files);
116
- expect(langs.find((l) => l.name === "HTML")).toBeDefined();
117
- expect(langs.find((l) => l.name === "CSS")).toBeDefined();
118
- });
119
-
120
- test("detects multiple languages with correct coverage", () => {
121
- const files = [
122
- { relativePath: "main.ts", size: 500 },
123
- { relativePath: "app.py", size: 1000 },
124
- ];
125
- const langs = detectLanguages(files);
126
- const py = langs.find((l) => l.name === "Python");
127
- const ts = langs.find((l) => l.name === "TypeScript");
128
- expect(py).toBeDefined();
129
- expect(ts).toBeDefined();
130
- // Python should have higher coverage (1000 vs 500 bytes)
131
- expect(py!.coverage).toBeGreaterThan(ts!.coverage);
132
- });
133
-
134
- test("sorts languages by coverage descending", () => {
135
- const files = [
136
- { relativePath: "main.js", size: 9000 },
137
- { relativePath: "main.ts", size: 1000 },
138
- ];
139
- const langs = detectLanguages(files);
140
- expect(langs[0].name).toBe("JavaScript");
141
- expect(langs[1].name).toBe("TypeScript");
142
- });
143
-
144
- test("handles empty file list", () => {
145
- expect(detectLanguages([])).toEqual([]);
146
- });
147
-
148
- test("handles files with no recognized extensions", () => {
149
- const files = [
150
- { relativePath: "README", size: 500 },
151
- { relativePath: "Makefile", size: 200 },
152
- ];
153
- const langs = detectLanguages(files);
154
- expect(langs).toEqual([]);
155
- });
156
-
157
- test("handles mixed TSX and JSX files", () => {
158
- const files = [
159
- { relativePath: "Component.tsx", size: 500 },
160
- { relativePath: "App.jsx", size: 300 },
161
- ];
162
- const langs = detectLanguages(files);
163
- const ts = langs.find((l) => l.name === "TypeScript");
164
- expect(ts).toBeDefined();
165
- });
166
-
167
- test("handles Swift files", () => {
168
- const files = [{ relativePath: "iOS/App.swift", size: 2000 }];
169
- const langs = detectLanguages(files);
170
- expect(langs.find((l) => l.name === "Swift")).toBeDefined();
171
- });
172
-
173
- test("handles SQL files", () => {
174
- const files = [{ relativePath: "schema.sql", size: 3000 }];
175
- const langs = detectLanguages(files);
176
- expect(langs.find((l) => l.name === "SQL")).toBeDefined();
177
- });
178
-
179
- test("handles Markdown files", () => {
180
- const files = [
181
- { relativePath: "README.md", size: 1000 },
182
- { relativePath: "CHANGELOG.md", size: 500 },
183
- ];
184
- const langs = detectLanguages(files);
185
- expect(langs.find((l) => l.name === "Markdown")).toBeDefined();
186
- });
187
-
188
- test("coverage sums to 1.0 for single language", () => {
189
- const files = [
190
- { relativePath: "a.py", size: 300 },
191
- { relativePath: "b.py", size: 700 },
192
- ];
193
- const langs = detectLanguages(files);
194
- const py = langs.find((l) => l.name === "Python");
195
- expect(py!.coverage).toBeCloseTo(1.0, 2);
196
- });
197
- });
198
-
199
- // --- detectPackageManager Tests ---------------------------------------
200
-
201
- describe("detectPackageManager", () => {
202
- test("detects npm from package.json", () => {
203
- const result = detectPackageManager(["package.json"]);
204
- expect(result).toBe("npm");
205
- });
206
-
207
- test("detects npm from package-lock.json", () => {
208
- const result = detectPackageManager(["package-lock.json"]);
209
- expect(result).toBe("npm");
210
- });
211
-
212
- test("detects yarn from yarn.lock", () => {
213
- const result = detectPackageManager(["yarn.lock"]);
214
- expect(result).toBe("yarn");
215
- });
216
-
217
- test("detects pnpm from pnpm-lock.yaml", () => {
218
- const result = detectPackageManager(["pnpm-lock.yaml"]);
219
- expect(result).toBe("pnpm");
220
- });
221
-
222
- test("detects bun from bun.lockb", () => {
223
- const result = detectPackageManager(["bun.lockb"]);
224
- expect(result).toBe("bun");
225
- });
226
-
227
- test("detects pip from requirements.txt", () => {
228
- const result = detectPackageManager(["requirements.txt"]);
229
- expect(result).toBe("pip");
230
- });
231
-
232
- test("detects poetry from pyproject.toml", () => {
233
- const result = detectPackageManager(["pyproject.toml"]);
234
- expect(result).toBe("poetry");
235
- });
236
-
237
- test("detects poetry from poetry.lock", () => {
238
- const result = detectPackageManager(["poetry.lock"]);
239
- expect(result).toBe("poetry");
240
- });
241
-
242
- test("returns null for empty list", () => {
243
- expect(detectPackageManager([])).toBeNull();
244
- });
245
-
246
- test("prefers npm over yarn when both present", () => {
247
- // npm gets score 1 (package.json), yarn gets score 1 (yarn.lock)
248
- // npm comes first in tiebreak since it's the first non-zero score
249
- const result = detectPackageManager(["package.json", "yarn.lock"]);
250
- // yarn.lock beats package.json signal for yarn
251
- expect(result === "yarn" || result === "npm").toBe(true);
252
- });
253
-
254
- test("handles nested paths", () => {
255
- const result = detectPackageManager(["apps/frontend/package.json"]);
256
- expect(result).toBe("npm");
257
- });
258
-
259
- test("detects bench from apps.txt", () => {
260
- const result = detectPackageManager(["apps.txt"]);
261
- expect(result).toBe("bench");
262
- });
263
-
264
- test("detects bench from sites directory", () => {
265
- const result = detectPackageManager(["sites/", "apps.txt"]);
266
- expect(result).toBe("bench");
267
- });
268
-
269
- test("score beats previous with multiple signals", () => {
270
- // yarn.lock gives yarn score 1; package-lock.json gives npm score 1; yarn wins on order
271
- // Use only yarn.lock so only yarn has a signal
272
- const result = detectPackageManager(["yarn.lock"]);
273
- expect(result).toBe("yarn");
274
- });
275
- });
276
-
277
- // --- parsePackageJsonScripts Tests ------------------------------------
278
-
279
- describe("parsePackageJsonScripts", () => {
280
- test("extracts all scripts", () => {
281
- const pkg = {
282
- scripts: {
283
- test: "vitest",
284
- build: "tsc && vite build",
285
- dev: "vite",
286
- },
287
- };
288
- const cmds = parsePackageJsonScripts(pkg);
289
- expect(cmds.length).toBe(3);
290
- });
291
-
292
- test("marks test as primary", () => {
293
- const pkg = { scripts: { test: "jest" } };
294
- const cmds = parsePackageJsonScripts(pkg);
295
- expect(cmds.find((c) => c.command === "npm run test")?.primary).toBe(true);
296
- });
297
-
298
- test("marks build as primary", () => {
299
- const pkg = { scripts: { build: "webpack" } };
300
- const cmds = parsePackageJsonScripts(pkg);
301
- expect(cmds.find((c) => c.command === "npm run build")?.primary).toBe(true);
302
- });
303
-
304
- test("marks dev as primary", () => {
305
- const pkg = { scripts: { dev: "vite" } };
306
- const cmds = parsePackageJsonScripts(pkg);
307
- expect(cmds.find((c) => c.command === "npm run dev")?.primary).toBe(true);
308
- });
309
-
310
- test("marks start as primary", () => {
311
- const pkg = { scripts: { start: "node server.js" } };
312
- const cmds = parsePackageJsonScripts(pkg);
313
- expect(cmds.find((c) => c.command === "npm run start")?.primary).toBe(true);
314
- });
315
-
316
- test("marks lint as primary", () => {
317
- const pkg = { scripts: { lint: "eslint ." } };
318
- const cmds = parsePackageJsonScripts(pkg);
319
- expect(cmds.find((c) => c.command === "npm run lint")?.primary).toBe(true);
320
- });
321
-
322
- test("marks non-standard scripts as non-primary", () => {
323
- const pkg = { scripts: { myscript: "echo hello" } };
324
- const cmds = parsePackageJsonScripts(pkg);
325
- expect(cmds.find((c) => c.command === "npm run myscript")?.primary).toBe(
326
- false,
327
- );
328
- });
329
-
330
- test("handles empty scripts object", () => {
331
- const pkg = { scripts: {} };
332
- expect(parsePackageJsonScripts(pkg)).toEqual([]);
333
- });
334
-
335
- test("handles undefined scripts", () => {
336
- const pkg = {};
337
- expect(parsePackageJsonScripts(pkg)).toEqual([]);
338
- });
339
-
340
- test("source is always package.json", () => {
341
- const pkg = { scripts: { test: "jest" } };
342
- const cmds = parsePackageJsonScripts(pkg);
343
- expect(cmds.every((c) => c.source === "package.json")).toBe(true);
344
- });
345
-
346
- test("command format is npm run <name>", () => {
347
- const pkg = { scripts: { foo: "bar" } };
348
- const cmds = parsePackageJsonScripts(pkg);
349
- expect(cmds[0].command).toBe("npm run foo");
350
- });
351
- });
352
-
353
- // --- categorizeCommands Tests -----------------------------------------
354
-
355
- describe("categorizeCommands", () => {
356
- test("categorizes jest test commands", () => {
357
- const cmds = [
358
- { command: "npm run test", source: "package.json", primary: true },
359
- ];
360
- const categorized = categorizeCommands(cmds);
361
- expect(categorized.unitTest).toContain("npm run test");
362
- });
363
-
364
- test("categorizes vitest commands", () => {
365
- const cmds = [
366
- { command: "vitest run", source: "package.json", primary: true },
367
- ];
368
- const categorized = categorizeCommands(cmds);
369
- expect(categorized.unitTest.some((c) => c.includes("vitest"))).toBe(true);
370
- });
371
-
372
- test("categorizes pytest commands", () => {
373
- const cmds = [{ command: "pytest", source: "package.json", primary: true }];
374
- const categorized = categorizeCommands(cmds);
375
- expect(categorized.unitTest).toContain("pytest");
376
- });
377
-
378
- test("categorizes playwright e2e commands", () => {
379
- const cmds = [
380
- { command: "playwright e2e", source: "package.json", primary: true },
381
- ];
382
- const categorized = categorizeCommands(cmds);
383
- expect(categorized.e2eTest).toContain("playwright e2e");
384
- });
385
-
386
- test("categorizes cypress e2e commands", () => {
387
- const cmds = [
388
- { command: "cypress run", source: "package.json", primary: true },
389
- ];
390
- const categorized = categorizeCommands(cmds);
391
- expect(categorized.e2eTest).toContain("cypress run");
392
- });
393
-
394
- test("categorizes eslint lint commands", () => {
395
- const cmds = [
396
- { command: "eslint .", source: "package.json", primary: false },
397
- ];
398
- const categorized = categorizeCommands(cmds);
399
- expect(categorized.lint.some((c) => c.includes("eslint"))).toBe(true);
400
- });
401
-
402
- test("categorizes prettier lint commands", () => {
403
- const cmds = [
404
- { command: "prettier --check .", source: "package.json", primary: false },
405
- ];
406
- const categorized = categorizeCommands(cmds);
407
- expect(categorized.lint.some((c) => c.includes("prettier"))).toBe(true);
408
- });
409
-
410
- test("categorizes build commands", () => {
411
- const cmds = [
412
- { command: "npm run build", source: "package.json", primary: true },
413
- ];
414
- const categorized = categorizeCommands(cmds);
415
- expect(categorized.build).toContain("npm run build");
416
- });
417
-
418
- test("categorizes vite build commands", () => {
419
- const cmds = [
420
- { command: "vite build", source: "package.json", primary: true },
421
- ];
422
- const categorized = categorizeCommands(cmds);
423
- expect(categorized.build.some((c) => c.includes("vite"))).toBe(true);
424
- });
425
-
426
- test("categorizes next build commands", () => {
427
- const cmds = [
428
- { command: "next build", source: "package.json", primary: true },
429
- ];
430
- const categorized = categorizeCommands(cmds);
431
- expect(categorized.build.some((c) => c.includes("next"))).toBe(true);
432
- });
433
-
434
- test("categorizes migrate commands", () => {
435
- const cmds = [
436
- { command: "npm run migrate", source: "package.json", primary: false },
437
- ];
438
- const categorized = categorizeCommands(cmds);
439
- expect(categorized.migrate).toContain("npm run migrate");
440
- });
441
-
442
- test("categorizes bench migrate commands", () => {
443
- const cmds = [
444
- { command: "bench migrate", source: "package.json", primary: false },
445
- ];
446
- const categorized = categorizeCommands(cmds);
447
- expect(categorized.migrate.some((c) => c.includes("bench"))).toBe(true);
448
- });
449
-
450
- test("avoids duplicate categorization", () => {
451
- const cmds = [
452
- { command: "npm run test", source: "package.json", primary: true },
453
- { command: "npm run test", source: "package.json", primary: true },
454
- ];
455
- const categorized = categorizeCommands(cmds);
456
- expect(
457
- categorized.unitTest.filter((c) => c === "npm run test").length,
458
- ).toBe(1);
459
- });
460
-
461
- test("handles empty command list", () => {
462
- const categorized = categorizeCommands([]);
463
- expect(categorized.unitTest).toEqual([]);
464
- expect(categorized.build).toEqual([]);
465
- });
466
-
467
- test("categorizes integration test commands", () => {
468
- const cmds = [
469
- {
470
- command: "npm run integration-tests",
471
- source: "package.json",
472
- primary: false,
473
- },
474
- ];
475
- const categorized = categorizeCommands(cmds);
476
- expect(categorized.integrationTest).toContain("npm run integration-tests");
477
- });
478
-
479
- test("categorizes typecheck commands", () => {
480
- const cmds = [
481
- { command: "tsc --noEmit", source: "package.json", primary: false },
482
- ];
483
- const categorized = categorizeCommands(cmds);
484
- expect(categorized.typecheck.some((c) => c.includes("tsc"))).toBe(true);
485
- });
486
- });
487
-
488
- // --- parsePythonCommands Tests ----------------------------------------
489
-
490
- describe("parsePythonCommands", () => {
491
- test("detects pytest from pyproject.toml", () => {
492
- const content = '[tool.pytest]\ntestpaths = ["tests"]';
493
- const cmds = parsePythonCommands(content);
494
- expect(cmds.find((c) => c.command === "pytest")).toBeDefined();
495
- });
496
-
497
- test("detects pytest from [pytest] section", () => {
498
- const cmds = parsePythonCommands("[pytest]\ntestpaths = tests");
499
- expect(cmds.find((c) => c.command === "pytest")).toBeDefined();
500
- });
501
-
502
- test("detects coverage commands", () => {
503
- const cmds = parsePythonCommands("coverage = run: pytest");
504
- expect(cmds.some((c) => c.command.includes("coverage run"))).toBe(true);
505
- });
506
-
507
- test("handles empty content", () => {
508
- const cmds = parsePythonCommands("");
509
- expect(cmds).toEqual([]);
510
- });
511
- });
512
-
513
- // --- parseComposerScripts Tests ---------------------------------------
514
-
515
- describe("parseComposerScripts", () => {
516
- test("extracts composer scripts", () => {
517
- const composer = { scripts: { test: "phpunit", lint: "phpcs" } };
518
- const cmds = parseComposerScripts(composer);
519
- expect(cmds.length).toBe(2);
520
- });
521
-
522
- test("command format is composer <name>", () => {
523
- const composer = { scripts: { test: "phpunit" } };
524
- const cmds = parseComposerScripts(composer);
525
- expect(cmds[0].command).toBe("composer test");
526
- });
527
-
528
- test("marks test as primary", () => {
529
- const composer = { scripts: { test: "phpunit" } };
530
- const cmds = parseComposerScripts(composer);
531
- expect(cmds.find((c) => c.command === "composer test")?.primary).toBe(true);
532
- });
533
-
534
- test("marks lint as primary", () => {
535
- const composer = { scripts: { lint: "phpcs" } };
536
- const cmds = parseComposerScripts(composer);
537
- expect(cmds.find((c) => c.command === "composer lint")?.primary).toBe(true);
538
- });
539
-
540
- test("handles empty scripts", () => {
541
- const composer = { scripts: {} };
542
- expect(parseComposerScripts(composer)).toEqual([]);
543
- });
544
-
545
- test("handles undefined scripts", () => {
546
- const composer = {};
547
- expect(parseComposerScripts(composer)).toEqual([]);
548
- });
549
- });
550
-
551
- // --- AnalysisCache Tests ----------------------------------------------
552
-
553
- describe("AnalysisCache", () => {
554
- test("generates consistent cache keys", () => {
555
- const cache = new AnalysisCache({ enabled: false });
556
- const key1 = cache.generateKey("/repo", "abc123", "def456");
557
- const key2 = cache.generateKey("/repo", "abc123", "def456");
558
- expect(key1.repositoryRoot).toBe(key2.repositoryRoot);
559
- expect(key1.revision).toBe(key2.revision);
560
- expect(key1.configHash).toBe(key2.configHash);
561
- });
562
-
563
- test("generates different keys for different roots", () => {
564
- const cache = new AnalysisCache({ enabled: false });
565
- const key1 = cache.generateKey("/repo1", "abc123", "def456");
566
- const key2 = cache.generateKey("/repo2", "abc123", "def456");
567
- expect(key1.repositoryRoot).not.toBe(key2.repositoryRoot);
568
- });
569
-
570
- test("generates different keys for different revisions", () => {
571
- const cache = new AnalysisCache({ enabled: false });
572
- const key1 = cache.generateKey("/repo", "abc123", "def456");
573
- const key2 = cache.generateKey("/repo", "xyz789", "def456");
574
- expect(key1.revision).not.toBe(key2.revision);
575
- });
576
-
577
- test("calculates hash consistently", () => {
578
- const cache = new AnalysisCache({ enabled: false });
579
- const h1 = cache.calculateHash("hello");
580
- const h2 = cache.calculateHash("hello");
581
- expect(h1).toBe(h2);
582
- });
583
-
584
- test("calculates different hashes for different content", () => {
585
- const cache = new AnalysisCache({ enabled: false });
586
- const h1 = cache.calculateHash("hello");
587
- const h2 = cache.calculateHash("world");
588
- expect(h1).not.toBe(h2);
589
- });
590
-
591
- test("calculates config hash", async () => {
592
- const cache = new AnalysisCache({ enabled: false });
593
- const hash = await cache.calculateConfigHash([
594
- { path: "a.json", content: '{"test":1}' },
595
- ]);
596
- expect(typeof hash).toBe("string");
597
- expect(hash.length).toBe(64); // sha256 hex
598
- });
599
-
600
- test("get returns null when disabled", async () => {
601
- const cache = new AnalysisCache({ enabled: false });
602
- const key = cache.generateKey("/repo", "abc", "def");
603
- const result = await cache.get(key);
604
- expect(result).toBeNull();
605
- });
606
-
607
- test("set does not throw when disabled", async () => {
608
- const cache = new AnalysisCache({ enabled: false });
609
- const key = cache.generateKey("/repo", "abc", "def");
610
- await expect(
611
- cache.set(
612
- key,
613
- {
614
- repositoryRoot: "/repo",
615
- repositoryName: "repo",
616
- revision: "abc",
617
- frameworks: [],
618
- languages: [],
619
- packageManagers: [],
620
- applications: [],
621
- commands: {
622
- unitTest: [],
623
- integrationTest: [],
624
- e2eTest: [],
625
- lint: [],
626
- typecheck: [],
627
- build: [],
628
- migrate: [],
629
- },
630
- rules: [],
631
- sensitivePaths: [],
632
- generatedPaths: [],
633
- testCapabilities: [],
634
- confidence: 0,
635
- warnings: [],
636
- analyzedAt: new Date().toISOString(),
637
- },
638
- { ruleHash: "r", manifestHash: "m", configHash: "c" },
639
- ),
640
- ).resolves.toBeUndefined();
641
- });
642
-
643
- test("clear does not throw when disabled", async () => {
644
- const cache = new AnalysisCache({ enabled: false });
645
- await expect(cache.clear()).resolves.toBeUndefined();
646
- });
647
-
648
- test("invalidate does not throw when disabled", async () => {
649
- const cache = new AnalysisCache({ enabled: false });
650
- await expect(cache.invalidate("/repo")).resolves.toBeUndefined();
651
- });
652
-
653
- test("getStats returns zeros when disabled", async () => {
654
- const cache = new AnalysisCache({ enabled: false });
655
- const stats = await cache.getStats();
656
- expect(stats.entryCount).toBe(0);
657
- expect(stats.totalSize).toBe(0);
658
- });
659
- });
660
-
661
- // --- hashRuleFiles Tests ----------------------------------------------
662
-
663
- describe("hashRuleFiles", () => {
664
- test("generates consistent hash for same files", async () => {
665
- const files = [{ path: "AGENTS.md", content: "# Rules\n\nTest content" }];
666
- const h1 = await hashRuleFiles(files);
667
- const h2 = await hashRuleFiles(files);
668
- expect(h1).toBe(h2);
669
- });
670
-
671
- test("different content produces different hash", async () => {
672
- const h1 = await hashRuleFiles([{ path: "a.md", content: "v1" }]);
673
- const h2 = await hashRuleFiles([{ path: "a.md", content: "v2" }]);
674
- expect(h1).not.toBe(h2);
675
- });
676
-
677
- test("different paths produce different hash", async () => {
678
- const h1 = await hashRuleFiles([{ path: "a.md", content: "same" }]);
679
- const h2 = await hashRuleFiles([{ path: "b.md", content: "same" }]);
680
- expect(h1).not.toBe(h2);
681
- });
682
-
683
- test("empty list produces valid hash", async () => {
684
- const hash = await hashRuleFiles([]);
685
- expect(typeof hash).toBe("string");
686
- expect(hash.length).toBe(64);
687
- });
688
- });
689
-
690
- // --- hashManifestFiles Tests -------------------------------------------
691
-
692
- describe("hashManifestFiles", () => {
693
- test("generates consistent hash", async () => {
694
- const files = [
695
- { path: "package.json", content: '{"scripts":{"test":"jest"}}' },
696
- ];
697
- const h1 = await hashManifestFiles(files);
698
- const h2 = await hashManifestFiles(files);
699
- expect(h1).toBe(h2);
700
- });
701
-
702
- test("only scripts and deps are hashed", async () => {
703
- const files = [
704
- {
705
- path: "package.json",
706
- content: '{"scripts":{"test":"jest"},"extra":"ignored"}',
707
- },
708
- ];
709
- const hash = await hashManifestFiles(files);
710
- expect(typeof hash).toBe("string");
711
- expect(hash.length).toBe(64);
712
- });
713
- });
714
-
715
- // --- FileSystemWalker Tests --------------------------------------------
716
-
717
- describe("FileSystemWalker", () => {
718
- test("rejects non-absolute root path", () => {
719
- expect(() => new FileSystemWalker("relative/path")).toThrow();
720
- });
721
-
722
- test("scans empty directory", async () => {
723
- const dir = await tmpDir();
724
- const walker = new FileSystemWalker(dir);
725
- const result = await walker.scan();
726
- expect(result.files).toEqual([]);
727
- expect(result.truncated).toBe(false);
728
- expect(result.errors).toEqual([]);
729
- });
730
-
731
- test("scans files in directory", async () => {
732
- const dir = await tmpDir();
733
- await writeFile(join(dir, "index.ts"), "const x = 1;", "utf-8");
734
- await writeFile(join(dir, "main.py"), "print('hello')", "utf-8");
735
-
736
- const walker = new FileSystemWalker(dir);
737
- const result = await walker.scan();
738
-
739
- expect(result.files.length).toBeGreaterThanOrEqual(2);
740
- const paths = result.files.map((f) => f.relativePath);
741
- expect(paths).toContain("index.ts");
742
- expect(paths).toContain("main.py");
743
- });
744
-
745
- test("detects sensitive files by pattern", async () => {
746
- const dir = await tmpDir();
747
- await writeFile(join(dir, ".env"), "SECRET=value", "utf-8");
748
- await writeFile(join(dir, ".env.local"), "SECRET=local", "utf-8");
749
-
750
- const walker = new FileSystemWalker(dir);
751
- const result = await walker.scan();
752
-
753
- const envFiles = result.files.filter((f) => f.sensitive);
754
- expect(envFiles.length).toBeGreaterThan(0);
755
- });
756
-
757
- test("detects generated files by pattern", async () => {
758
- const dir = await tmpDir();
759
- await mkdir(join(dir, "dist"), { recursive: true });
760
- await writeFile(join(dir, "dist/bundle.js"), "module.exports={};", "utf-8");
761
-
762
- const walker = new FileSystemWalker(dir);
763
- const result = await walker.scan();
764
-
765
- const generatedFiles = result.files.filter((f) => f.generated);
766
- expect(generatedFiles.length).toBeGreaterThan(0);
767
- });
768
-
769
- test("respects maxScanFiles limit", async () => {
770
- const dir = await tmpDir();
771
- // Create many files
772
- for (let i = 0; i < 50; i++) {
773
- await writeFile(join(dir, `file${i}.txt`), `content ${i}`, "utf-8");
774
- }
775
-
776
- const walker = new FileSystemWalker(dir, { maxScanFiles: 10 });
777
- const result = await walker.scan();
778
-
779
- expect(result.files.length).toBeLessThanOrEqual(10);
780
- expect(result.truncated).toBe(true);
781
- });
782
-
783
- test("skips hidden directories except .git", async () => {
784
- const dir = await tmpDir();
785
- await mkdir(join(dir, ".hidden"), { recursive: true });
786
- await writeFile(join(dir, ".hidden/file.txt"), "secret", "utf-8");
787
- await mkdir(join(dir, ".git"), { recursive: true });
788
- await writeFile(join(dir, ".git/config"), "[core]", "utf-8");
789
-
790
- const walker = new FileSystemWalker(dir);
791
- const result = await walker.scan();
792
-
793
- const hiddenFiles = result.files.filter((f) =>
794
- f.relativePath.includes(".hidden"),
795
- );
796
- expect(hiddenFiles).toEqual([]);
797
- // .git should be included (not skipped)
798
- });
799
-
800
- test("resolves relative path correctly", async () => {
801
- const dir = await tmpDir();
802
- await mkdir(join(dir, "src"), { recursive: true });
803
- await writeFile(join(dir, "src/index.ts"), "", "utf-8");
804
-
805
- const walker = new FileSystemWalker(dir);
806
- const result = await walker.scan();
807
-
808
- const tsFile = result.files.find((f) => f.relativePath.endsWith(".ts"));
809
- expect(tsFile?.relativePath).toBe("src/index.ts");
810
- });
811
-
812
- test("getStats returns scan statistics", async () => {
813
- const dir = await tmpDir();
814
- await writeFile(join(dir, "a.txt"), "a", "utf-8");
815
- await writeFile(join(dir, "b.txt"), "b", "utf-8");
816
-
817
- const walker = new FileSystemWalker(dir);
818
- await walker.scan();
819
- const stats = walker.getStats();
820
-
821
- expect(stats.filesScanned).toBeGreaterThanOrEqual(2);
822
- expect(stats.directoriesScanned).toBeGreaterThanOrEqual(1);
823
- });
824
-
825
- test("readFileSafe returns null for oversized files", async () => {
826
- const dir = await tmpDir();
827
- await writeFile(join(dir, "big.txt"), "x".repeat(1000), "utf-8");
828
-
829
- const walker = new FileSystemWalker(dir, { maxFileSize: 100 });
830
- const content = await walker.readFileSafe(join(dir, "big.txt"));
831
- expect(content).toBeNull();
832
- });
833
-
834
- test("readFileSafe returns content for small files", async () => {
835
- const dir = await tmpDir();
836
- const content = "hello world";
837
- await writeFile(join(dir, "small.txt"), content, "utf-8");
838
-
839
- const walker = new FileSystemWalker(dir);
840
- const result = await walker.readFileSafe(join(dir, "small.txt"));
841
- expect(result).toBe(content);
842
- });
843
-
844
- test("exists returns true for existing path", async () => {
845
- const dir = await tmpDir();
846
- await writeFile(join(dir, "exists.txt"), "x", "utf-8");
847
-
848
- const walker = new FileSystemWalker(dir);
849
- expect(await walker.exists(join(dir, "exists.txt"))).toBe(true);
850
- });
851
-
852
- test("exists returns false for non-existent path", async () => {
853
- const dir = await tmpDir();
854
- const walker = new FileSystemWalker(dir);
855
- expect(await walker.exists(join(dir, "nonexistent.txt"))).toBe(false);
856
- });
857
-
858
- test("findFiles matches regex patterns", async () => {
859
- const dir = await tmpDir();
860
- await writeFile(join(dir, "test.spec.ts"), "", "utf-8");
861
- await writeFile(join(dir, "test.test.ts"), "", "utf-8");
862
- await writeFile(join(dir, "app.ts"), "", "utf-8");
863
-
864
- const walker = new FileSystemWalker(dir);
865
- const results = await walker.findFiles(/\.test\.ts$/);
866
- expect(results.length).toBe(1);
867
- });
868
- });
869
-
870
- // --- Rule Discovery Tests ---------------------------------------------
871
-
872
- describe("rule-discovery", () => {
873
- test("parseRuleFile extracts sections from markdown", async () => {
874
- const ruleFile = {
875
- path: "/repo/AGENTS.md",
876
- name: "AGENTS.md",
877
- content: "# Section 1\n\nContent here.\n\n## Section 2\n\nMore content.",
878
- userDefined: true,
879
- };
880
- const rule = await parseRuleFile(ruleFile);
881
- expect(rule.sections.length).toBeGreaterThan(0);
882
- expect(rule.priority).toBe("mandatory");
883
- expect(rule.userDefined).toBe(true);
884
- });
885
-
886
- test("AGENTS.md has mandatory priority", async () => {
887
- const rule = await parseRuleFile({
888
- path: "/repo/AGENTS.md",
889
- name: "AGENTS.md",
890
- content: "# Rules",
891
- userDefined: true,
892
- });
893
- expect(rule.priority).toBe("mandatory");
894
- });
895
-
896
- test("CONTRIBUTING.md has advisory priority", async () => {
897
- const rule = await parseRuleFile({
898
- path: "/repo/CONTRIBUTING.md",
899
- name: "CONTRIBUTING.md",
900
- content: "# Contributing",
901
- userDefined: false,
902
- });
903
- expect(rule.priority).toBe("advisory");
904
- });
905
-
906
- test("discoverRuleFiles finds files in temp repo", async () => {
907
- const dir = await tmpDir();
908
- await writeFile(
909
- join(dir, "AGENTS.md"),
910
- "# Agent Rules\n\nUse TypeScript.",
911
- "utf-8",
912
- );
913
-
914
- const fs = {
915
- exists: async (p: string) => {
916
- try {
917
- const { access } = await import("node:fs/promises");
918
- await access(p);
919
- return true;
920
- } catch {
921
- return false;
922
- }
923
- },
924
- readFile: async (p: string) => {
925
- const { readFile } = await import("node:fs/promises");
926
- return readFile(p, "utf-8");
927
- },
928
- readDir: async (p: string) => {
929
- const { readdir } = await import("node:fs/promises");
930
- return readdir(p);
931
- },
932
- isDirectory: async (p: string) => {
933
- const { stat } = await import("node:fs/promises");
934
- const s = await stat(p);
935
- return s.isDirectory();
936
- },
937
- };
938
-
939
- const rules = await discoverRuleFiles(dir, fs);
940
- expect(rules.length).toBeGreaterThan(0);
941
- expect(rules.find((r) => r.name === "AGENTS.md")).toBeDefined();
942
- });
943
-
944
- test("mergeRules sorts by priority", async () => {
945
- const files = [
946
- {
947
- path: "/repo/CONTRIBUTING.md",
948
- name: "CONTRIBUTING.md",
949
- content: "# Contributing",
950
- userDefined: false,
951
- },
952
- {
953
- path: "/repo/AGENTS.md",
954
- name: "AGENTS.md",
955
- content: "# Agents",
956
- userDefined: true,
957
- },
958
- ];
959
- const merged = mergeRules(files);
960
- expect(merged[0].id).toBe("rule-AGENTS");
961
- });
962
- });
963
-
964
- // --- Signal Detection Tests -------------------------------------------
965
-
966
- describe("scanSignals", () => {
967
- test("returns empty array when no signals match", async () => {
968
- const fs = {
969
- exists: async () => false,
970
- glob: async () => [],
971
- readFile: async () => "",
972
- readDir: async () => [],
973
- isDirectory: async () => false,
974
- };
975
- const signals = await scanSignals(fs);
976
- expect(signals).toEqual([]);
977
- });
978
-
979
- test("detects file existence signals", async () => {
980
- // Use a non-glob signal: frappe uses "apps.txt"
981
- const fs = {
982
- exists: async (path: string) => path === "apps.txt",
983
- glob: async () => [],
984
- readFile: async () => "",
985
- readDir: async () => [],
986
- isDirectory: async () => false,
987
- };
988
- const signals = await scanSignals(fs);
989
- const frappeSignal = signals.find((s) => s.type === "frappe_hooks");
990
- expect(frappeSignal).toBeDefined();
991
- expect(frappeSignal?.path).toBe("apps.txt");
992
- });
993
- });
994
-
995
- describe("GenericFrameworkDetector", () => {
996
- test("detects Next.js from next.config.js", async () => {
997
- const dir = await tmpDir();
998
- await writeFile(
999
- join(dir, "next.config.js"),
1000
- "module.exports = {}",
1001
- "utf-8",
1002
- );
1003
- await writeFile(
1004
- join(dir, "package.json"),
1005
- '{"dependencies":{"next":"14"}}',
1006
- "utf-8",
1007
- );
1008
-
1009
- const walker = new FileSystemWalker(dir);
1010
- const roFs = {
1011
- exists: async (path: string) => walker.exists(path),
1012
- readFile: async (path: string) => {
1013
- const c = await walker.readFileSafe(path);
1014
- if (c === null) throw new Error("not found");
1015
- return c;
1016
- },
1017
- readDir: async (path: string) => {
1018
- try {
1019
- const { readdir } = await import("node:fs/promises");
1020
- return readdir(walker.resolvePath(path));
1021
- } catch {
1022
- return [];
1023
- }
1024
- },
1025
- isDirectory: async (path: string) => {
1026
- try {
1027
- const { stat } = await import("node:fs/promises");
1028
- const s = await stat(walker.resolvePath(path));
1029
- return s.isDirectory();
1030
- } catch {
1031
- return false;
1032
- }
1033
- },
1034
- // glob is used for wildcard patterns like "next.config.*"
1035
- glob: async (pattern: string) => {
1036
- if (pattern === "next.config.*") {
1037
- return ["next.config.js"];
1038
- }
1039
- return [];
1040
- },
1041
- };
1042
-
1043
- const detector = new GenericFrameworkDetector();
1044
- const results = await detector.detect(roFs);
1045
-
1046
- expect(results.length).toBeGreaterThan(0);
1047
- const nextjs = results.find((r) => r.category === "nextjs");
1048
- expect(nextjs).toBeDefined();
1049
- expect(nextjs!.confidence).toBeGreaterThan(0);
1050
- });
1051
-
1052
- test("detects Django from manage.py", async () => {
1053
- const dir = await tmpDir();
1054
- await writeFile(join(dir, "manage.py"), "#!/usr/bin/env python", "utf-8");
1055
- await writeFile(join(dir, "settings.py"), "SECRET_KEY = 'x'", "utf-8");
1056
-
1057
- const walker = new FileSystemWalker(dir);
1058
- const roFs = {
1059
- exists: async (path: string) => walker.exists(path),
1060
- readFile: async () => "",
1061
- readDir: async () => [],
1062
- isDirectory: async () => false,
1063
- glob: async () => [],
1064
- };
1065
-
1066
- const detector = new GenericFrameworkDetector();
1067
- const results = await detector.detect(roFs);
1068
-
1069
- const django = results.find((r) => r.category === "django");
1070
- expect(django).toBeDefined();
1071
- expect(django!.confidence).toBeGreaterThan(0);
1072
- });
1073
-
1074
- test("returns empty results for unknown directory", async () => {
1075
- await tmpDir(); // Creates empty temp dir
1076
- const roFs = {
1077
- exists: async () => false,
1078
- readFile: async () => {
1079
- throw new Error("not found");
1080
- },
1081
- readDir: async () => [],
1082
- isDirectory: async () => false,
1083
- glob: async () => [],
1084
- };
1085
-
1086
- const detector = new GenericFrameworkDetector();
1087
- const results = await detector.detect(roFs);
1088
- // Unknown framework is possible if signal matching doesn't find anything
1089
- expect(Array.isArray(results)).toBe(true);
1090
- });
1091
- });
1092
-
1093
- // --- ProjectAnalyzer Integration Tests --------------------------------
1094
-
1095
- describe("ProjectAnalyzer", () => {
1096
- let analyzer: ProjectAnalyzer;
1097
-
1098
- beforeEach(() => {
1099
- analyzer = createProjectAnalyzer({
1100
- cache: new AnalysisCache({ enabled: false }),
1101
- });
1102
- });
1103
-
1104
- test("analyzer is instance of ProjectAnalyzer", () => {
1105
- expect(analyzer).toBeInstanceOf(ProjectAnalyzer);
1106
- });
1107
-
1108
- test("analyzes Next.js workspace", async () => {
1109
- const dir = await tmpDir();
1110
- await writeFile(
1111
- join(dir, "package.json"),
1112
- JSON.stringify({ dependencies: { next: "14.0.0" } }),
1113
- "utf-8",
1114
- );
1115
- await writeFile(
1116
- join(dir, "next.config.js"),
1117
- "module.exports = {}",
1118
- "utf-8",
1119
- );
1120
- await mkdir(join(dir, "app"), { recursive: true });
1121
- await writeFile(
1122
- join(dir, "app/page.tsx"),
1123
- "export default function(){}",
1124
- "utf-8",
1125
- );
1126
-
1127
- const result = await analyzer.analyze({ repositoryRoot: dir });
1128
-
1129
- expect(result.success).toBe(true);
1130
- expect(result.profile).toBeDefined();
1131
- expect(result.profile!.repositoryRoot).toBe(dir);
1132
- expect(result.profile!.repositoryName).toBeDefined();
1133
- expect(result.profile!.analyzedAt).toBeDefined();
1134
- });
1135
-
1136
- test("analyzes React/Vite workspace", async () => {
1137
- const dir = await tmpDir();
1138
- await writeFile(
1139
- join(dir, "package.json"),
1140
- JSON.stringify({
1141
- dependencies: { react: "^18" },
1142
- devDependencies: { vite: "^5" },
1143
- }),
1144
- "utf-8",
1145
- );
1146
- await writeFile(
1147
- join(dir, "vite.config.ts"),
1148
- "import { defineConfig } from 'vite'",
1149
- "utf-8",
1150
- );
1151
- await mkdir(join(dir, "src"), { recursive: true });
1152
- await writeFile(
1153
- join(dir, "src/main.tsx"),
1154
- "import React from 'react'",
1155
- "utf-8",
1156
- );
1157
-
1158
- const result = await analyzer.analyze({ repositoryRoot: dir });
1159
-
1160
- expect(result.success).toBe(true);
1161
- expect(result.profile).toBeDefined();
1162
- });
1163
-
1164
- test("analyzes Django workspace", async () => {
1165
- const dir = await tmpDir();
1166
- await writeFile(join(dir, "manage.py"), "#!/usr/bin/env python", "utf-8");
1167
- await writeFile(join(dir, "settings.py"), "SECRET_KEY = 'dev'", "utf-8");
1168
- await writeFile(join(dir, "requirements.txt"), "django==4.0", "utf-8");
1169
-
1170
- const result = await analyzer.analyze({ repositoryRoot: dir });
1171
-
1172
- expect(result.success).toBe(true);
1173
- expect(result.profile).toBeDefined();
1174
- });
1175
-
1176
- test("analyzes Laravel workspace", async () => {
1177
- const dir = await tmpDir();
1178
- await writeFile(join(dir, "artisan"), "#!/usr/bin/env php", "utf-8");
1179
- await writeFile(
1180
- join(dir, "composer.json"),
1181
- JSON.stringify({ require: { laravel: "^10" } }),
1182
- "utf-8",
1183
- );
1184
- await mkdir(join(dir, "app", "Http", "Controllers"), { recursive: true });
1185
-
1186
- const result = await analyzer.analyze({ repositoryRoot: dir });
1187
-
1188
- expect(result.success).toBe(true);
1189
- expect(result.profile).toBeDefined();
1190
- });
1191
-
1192
- test("detects language from scanned files", async () => {
1193
- const dir = await tmpDir();
1194
- await writeFile(join(dir, "package.json"), "{}", "utf-8");
1195
- await writeFile(join(dir, "main.ts"), "const x: number = 1;", "utf-8");
1196
- await writeFile(join(dir, "utils.py"), "def foo(): pass", "utf-8");
1197
-
1198
- const result = await analyzer.analyze({ repositoryRoot: dir });
1199
-
1200
- expect(result.success).toBe(true);
1201
- expect(result.profile!.languages.length).toBeGreaterThan(0);
1202
- });
1203
-
1204
- test("detects package manager from lock files", async () => {
1205
- const dir = await tmpDir();
1206
- await writeFile(join(dir, "package.json"), "{}", "utf-8");
1207
- await writeFile(join(dir, "bun.lockb"), "", "utf-8");
1208
-
1209
- const result = await analyzer.analyze({ repositoryRoot: dir });
1210
-
1211
- expect(result.success).toBe(true);
1212
- expect(result.profile!.packageManagers.length).toBeGreaterThan(0);
1213
- });
1214
-
1215
- test("discovers commands from package.json", async () => {
1216
- const dir = await tmpDir();
1217
- await writeFile(
1218
- join(dir, "package.json"),
1219
- JSON.stringify({
1220
- scripts: {
1221
- test: "vitest",
1222
- build: "tsc",
1223
- lint: "eslint .",
1224
- },
1225
- }),
1226
- "utf-8",
1227
- );
1228
-
1229
- const result = await analyzer.analyze({ repositoryRoot: dir });
1230
-
1231
- expect(result.success).toBe(true);
1232
- expect(result.profile!.commands.unitTest.length).toBeGreaterThan(0);
1233
- });
1234
-
1235
- test("discovers AGENTS.md rules", async () => {
1236
- const dir = await tmpDir();
1237
- await writeFile(join(dir, "package.json"), "{}", "utf-8");
1238
- await writeFile(
1239
- join(dir, "AGENTS.md"),
1240
- "# Agent Rules\n\n## Code Style\n\nUse TypeScript.",
1241
- "utf-8",
1242
- );
1243
-
1244
- const result = await analyzer.analyze({ repositoryRoot: dir });
1245
-
1246
- expect(result.success).toBe(true);
1247
- expect(result.profile!.rules.length).toBeGreaterThan(0);
1248
- });
1249
-
1250
- test("detects test capabilities", async () => {
1251
- const dir = await tmpDir();
1252
- await writeFile(join(dir, "package.json"), "{}", "utf-8");
1253
- await writeFile(
1254
- join(dir, "vitest.config.ts"),
1255
- "import { defineConfig } from 'vitest/config'",
1256
- "utf-8",
1257
- );
1258
-
1259
- const result = await analyzer.analyze({ repositoryRoot: dir });
1260
-
1261
- expect(result.success).toBe(true);
1262
- expect(result.profile!.testCapabilities.length).toBeGreaterThan(0);
1263
- const vitest = result.profile!.testCapabilities.find(
1264
- (t) => t.runner === "vitest",
1265
- );
1266
- expect(vitest).toBeDefined();
1267
- expect(vitest!.available).toBe(true);
1268
- });
1269
-
1270
- test("detects pytest from pyproject.toml", async () => {
1271
- const dir = await tmpDir();
1272
- await writeFile(
1273
- join(dir, "pyproject.toml"),
1274
- "[tool.pytest]\ntestpaths = ['tests']",
1275
- "utf-8",
1276
- );
1277
-
1278
- const result = await analyzer.analyze({ repositoryRoot: dir });
1279
-
1280
- expect(result.success).toBe(true);
1281
- const pytest = result.profile!.testCapabilities.find(
1282
- (t) => t.runner === "pytest",
1283
- );
1284
- expect(pytest).toBeDefined();
1285
- });
1286
-
1287
- test("returns error for non-existent directory", async () => {
1288
- const result = await analyzer.analyze({
1289
- repositoryRoot: "/nonexistent/path/to/repo",
1290
- });
1291
- // The analyzer catches errors and returns success:false for non-existent paths
1292
- if (!result.success) {
1293
- expect(result.error).toBeDefined();
1294
- expect(result.errorCode).toBeDefined();
1295
- }
1296
- });
1297
-
1298
- test("reports duration", async () => {
1299
- const dir = await tmpDir();
1300
- await writeFile(join(dir, "package.json"), "{}", "utf-8");
1301
-
1302
- const result = await analyzer.analyze({ repositoryRoot: dir });
1303
- expect(result.durationMs).toBeGreaterThanOrEqual(0);
1304
- });
1305
-
1306
- test("analyzer with custom cache works", async () => {
1307
- const cache = new AnalysisCache({ enabled: false });
1308
- const customAnalyzer = createProjectAnalyzer({ cache });
1309
-
1310
- const dir = await tmpDir();
1311
- await writeFile(join(dir, "package.json"), "{}", "utf-8");
1312
-
1313
- const result = await customAnalyzer.analyze({ repositoryRoot: dir });
1314
- expect(result.success).toBe(true);
1315
- });
1316
-
1317
- test("analyzer config respects maxScanFiles", async () => {
1318
- const dir = await tmpDir();
1319
- await writeFile(join(dir, "package.json"), "{}", "utf-8");
1320
-
1321
- // Create many files
1322
- for (let i = 0; i < 30; i++) {
1323
- await writeFile(join(dir, `file${i}.txt`), `content ${i}`, "utf-8");
1324
- }
1325
-
1326
- const limitedAnalyzer = createProjectAnalyzer({
1327
- cache: new AnalysisCache({ enabled: false }),
1328
- });
1329
-
1330
- const result = await limitedAnalyzer.analyze({
1331
- repositoryRoot: dir,
1332
- config: { maxScanFiles: 10 },
1333
- });
1334
-
1335
- expect(result.success).toBe(true);
1336
- expect(result.profile).toBeDefined();
1337
- });
1338
-
1339
- test("analyzer with no monorepo detection", async () => {
1340
- const dir = await tmpDir();
1341
- await writeFile(
1342
- join(dir, "package.json"),
1343
- JSON.stringify({ name: "root" }),
1344
- "utf-8",
1345
- );
1346
- await mkdir(join(dir, "packages", "app"), { recursive: true });
1347
- await writeFile(
1348
- join(dir, "packages", "app", "package.json"),
1349
- JSON.stringify({ name: "app" }),
1350
- "utf-8",
1351
- );
1352
-
1353
- const result = await analyzer.analyze({
1354
- repositoryRoot: dir,
1355
- config: { detectMonorepo: false },
1356
- });
1357
-
1358
- expect(result.success).toBe(true);
1359
- });
1360
-
1361
- test("generates warnings for conflicting frameworks", async () => {
1362
- const dir = await tmpDir();
1363
- await writeFile(join(dir, "package.json"), "{}", "utf-8");
1364
- await writeFile(
1365
- join(dir, "next.config.js"),
1366
- "module.exports = {}",
1367
- "utf-8",
1368
- );
1369
- await writeFile(
1370
- join(dir, "vite.config.ts"),
1371
- "import { defineConfig } from 'vite'",
1372
- "utf-8",
1373
- );
1374
-
1375
- const result = await analyzer.analyze({ repositoryRoot: dir });
1376
-
1377
- expect(result.success).toBe(true);
1378
- // Both Next.js and Vite should be detected, may produce framework warnings
1379
- expect(result.profile).toBeDefined();
1380
- });
1381
- });
1382
-
1383
- // --- Complexity Estimation --------------------------------------------
1384
-
1385
- describe("complexity signals from FileSystemWalker", () => {
1386
- test("file count reflects all scanned files", async () => {
1387
- const dir = await tmpDir();
1388
- await writeFile(join(dir, "package.json"), "{}", "utf-8");
1389
- await writeFile(join(dir, "a.ts"), "", "utf-8");
1390
- await writeFile(join(dir, "b.ts"), "", "utf-8");
1391
- await writeFile(join(dir, "c.ts"), "", "utf-8");
1392
-
1393
- const walker = new FileSystemWalker(dir);
1394
- const result = await walker.scan();
1395
-
1396
- expect(result.files.length).toBeGreaterThanOrEqual(4);
1397
- });
1398
-
1399
- test("total size accumulates file sizes", async () => {
1400
- const dir = await tmpDir();
1401
- await writeFile(join(dir, "package.json"), '{"name":"test"}', "utf-8");
1402
- await writeFile(join(dir, "main.ts"), "const x = 1;".repeat(100), "utf-8");
1403
-
1404
- const walker = new FileSystemWalker(dir);
1405
- const result = await walker.scan();
1406
-
1407
- const tsFile = result.files.find((f) => f.relativePath === "main.ts");
1408
- expect(tsFile!.size).toBeGreaterThan(500);
1409
- });
1410
-
1411
- test("scans nested directories", async () => {
1412
- const dir = await tmpDir();
1413
- await mkdir(join(dir, "src", "components", "ui"), { recursive: true });
1414
- await writeFile(join(dir, "src/index.ts"), "", "utf-8");
1415
- await writeFile(join(dir, "src/components/Button.tsx"), "", "utf-8");
1416
- await writeFile(join(dir, "src/components/ui/Card.tsx"), "", "utf-8");
1417
-
1418
- const walker = new FileSystemWalker(dir);
1419
- const result = await walker.scan();
1420
-
1421
- expect(result.files.length).toBeGreaterThanOrEqual(3);
1422
- expect(result.directoriesScanned).toBeGreaterThan(3);
1423
- });
1424
-
1425
- test("node_modules files are marked as generated", async () => {
1426
- const dir = await tmpDir();
1427
- await mkdir(join(dir, "node_modules"), { recursive: true });
1428
- await mkdir(join(dir, "node_modules/lodash"), { recursive: true });
1429
- await writeFile(
1430
- join(dir, "node_modules/lodash/index.js"),
1431
- "module.exports={};",
1432
- "utf-8",
1433
- );
1434
- await mkdir(join(dir, "src"), { recursive: true });
1435
- await writeFile(join(dir, "src/main.ts"), "", "utf-8");
1436
-
1437
- const walker = new FileSystemWalker(dir);
1438
- const result = await walker.scan();
1439
-
1440
- // node_modules files are scanned but marked as generated
1441
- const generatedFiles = result.files.filter((f) => f.generated);
1442
- expect(generatedFiles.length).toBeGreaterThan(0);
1443
- });
1444
-
1445
- test("depth limit prevents deep scanning", async () => {
1446
- const dir = await tmpDir();
1447
- // Create deeply nested structure
1448
- let current = dir;
1449
- for (let i = 0; i < 30; i++) {
1450
- current = join(current, `level${i}`);
1451
- }
1452
- await mkdir(current, { recursive: true });
1453
- await writeFile(join(current, "deep.ts"), "", "utf-8");
1454
-
1455
- const walker = new FileSystemWalker(dir, { maxDepth: 5 });
1456
- const result = await walker.scan();
1457
-
1458
- const deepFile = result.files.find((f) =>
1459
- f.relativePath.includes("deep.ts"),
1460
- );
1461
- expect(deepFile).toBeUndefined();
1462
- });
1463
- });
1464
-
1465
- // --- Error Cases ------------------------------------------------------
1466
-
1467
- describe("error handling", () => {
1468
- test("analyzer handles corrupted JSON gracefully", async () => {
1469
- const dir = await tmpDir();
1470
- await writeFile(join(dir, "package.json"), "{ invalid json }", "utf-8");
1471
-
1472
- const analyzer = createProjectAnalyzer({
1473
- cache: new AnalysisCache({ enabled: false }),
1474
- });
1475
- const result = await analyzer.analyze({ repositoryRoot: dir });
1476
-
1477
- // Should still succeed with partial results
1478
- expect(result.success).toBe(true);
1479
- });
1480
-
1481
- test("walker handles unreadable directories", async () => {
1482
- const dir = await tmpDir();
1483
- // The walker should not crash on edge cases
1484
- const walker = new FileSystemWalker(dir);
1485
- const result = await walker.scan();
1486
- expect(result.errors).toEqual([]);
1487
- });
1488
-
1489
- test("analyzer handles missing package.json scripts", async () => {
1490
- const dir = await tmpDir();
1491
- await writeFile(
1492
- join(dir, "package.json"),
1493
- JSON.stringify({ name: "no-scripts" }),
1494
- "utf-8",
1495
- );
1496
-
1497
- const analyzer = createProjectAnalyzer({
1498
- cache: new AnalysisCache({ enabled: false }),
1499
- });
1500
- const result = await analyzer.analyze({ repositoryRoot: dir });
1501
-
1502
- expect(result.success).toBe(true);
1503
- expect(result.profile!.commands).toBeDefined();
1504
- });
1505
- });
1506
-
1507
- // --- Additional Cache TTL Tests ----------------------------------------
1508
-
1509
- describe("AnalysisCache TTL behavior", () => {
1510
- test("cache returns null for expired entries", async () => {
1511
- const tmp = await mkdtemp(join(tmpdir(), "cache-ttl-"));
1512
- tempDirs.push(tmp);
1513
-
1514
- const cache = new AnalysisCache({
1515
- enabled: true,
1516
- cacheDir: tmp,
1517
- maxAgeMs: 1, // 1 millisecond TTL
1518
- });
1519
-
1520
- const key = cache.generateKey("/repo", "abc", "def");
1521
- await cache.set(
1522
- key,
1523
- {
1524
- repositoryRoot: "/repo",
1525
- repositoryName: "repo",
1526
- revision: "abc",
1527
- frameworks: [],
1528
- languages: [],
1529
- packageManagers: [],
1530
- applications: [],
1531
- commands: {
1532
- unitTest: [],
1533
- integrationTest: [],
1534
- e2eTest: [],
1535
- lint: [],
1536
- typecheck: [],
1537
- build: [],
1538
- migrate: [],
1539
- },
1540
- rules: [],
1541
- sensitivePaths: [],
1542
- generatedPaths: [],
1543
- testCapabilities: [],
1544
- confidence: 0,
1545
- warnings: [],
1546
- analyzedAt: new Date().toISOString(),
1547
- },
1548
- { ruleHash: "r", manifestHash: "m", configHash: "c" },
1549
- );
1550
-
1551
- // Wait for entry to expire
1552
- await new Promise((r) => setTimeout(r, 10));
1553
-
1554
- const result = await cache.get(key);
1555
- expect(result).toBeNull();
1556
-
1557
- await rm(tmp, { recursive: true, force: true });
1558
- tempDirs.pop();
1559
- });
1560
-
1561
- test("cache stores and retrieves profiles", async () => {
1562
- const tmp = await mkdtemp(join(tmpdir(), "cache-store-"));
1563
- tempDirs.push(tmp);
1564
-
1565
- const cache = new AnalysisCache({
1566
- enabled: true,
1567
- cacheDir: tmp,
1568
- maxAgeMs: 60000,
1569
- });
1570
-
1571
- const profile = {
1572
- repositoryRoot: "/test-repo",
1573
- repositoryName: "test-repo",
1574
- revision: "abc123",
1575
- frameworks: [
1576
- {
1577
- category: "react" as const,
1578
- name: "React",
1579
- confidence: 0.9,
1580
- signals: [],
1581
- primary: true,
1582
- },
1583
- ],
1584
- languages: [{ name: "TypeScript", coverage: 1.0 }],
1585
- packageManagers: [
1586
- { type: "npm" as const, configPath: "package.json", primary: true },
1587
- ],
1588
- applications: [],
1589
- commands: {
1590
- unitTest: ["npm run test"],
1591
- integrationTest: [],
1592
- e2eTest: [],
1593
- lint: [],
1594
- typecheck: [],
1595
- build: [],
1596
- migrate: [],
1597
- },
1598
- rules: [],
1599
- sensitivePaths: [],
1600
- generatedPaths: [],
1601
- testCapabilities: [],
1602
- confidence: 0.9,
1603
- warnings: [],
1604
- analyzedAt: new Date().toISOString(),
1605
- };
1606
-
1607
- const key = cache.generateKey("/test-repo", "abc123", "xyz");
1608
- await cache.set(key, profile, {
1609
- ruleHash: "r",
1610
- manifestHash: "m",
1611
- configHash: "xyz",
1612
- });
1613
-
1614
- const retrieved = await cache.get(key);
1615
- expect(retrieved).not.toBeNull();
1616
- expect(retrieved!.repositoryName).toBe("test-repo");
1617
- expect(retrieved!.languages[0].name).toBe("TypeScript");
1618
-
1619
- await rm(tmp, { recursive: true, force: true });
1620
- tempDirs.pop();
1621
- });
1622
-
1623
- test("invalidate removes all entries for a repository", async () => {
1624
- const tmp = await mkdtemp(join(tmpdir(), "cache-inval-"));
1625
- tempDirs.push(tmp);
1626
-
1627
- const cache = new AnalysisCache({
1628
- enabled: true,
1629
- cacheDir: tmp,
1630
- });
1631
-
1632
- const key1 = cache.generateKey("/repo", "v1", "h1");
1633
- await cache.set(
1634
- key1,
1635
- {
1636
- repositoryRoot: "/repo",
1637
- repositoryName: "repo",
1638
- revision: "v1",
1639
- frameworks: [],
1640
- languages: [],
1641
- packageManagers: [],
1642
- applications: [],
1643
- commands: {
1644
- unitTest: [],
1645
- integrationTest: [],
1646
- e2eTest: [],
1647
- lint: [],
1648
- typecheck: [],
1649
- build: [],
1650
- migrate: [],
1651
- },
1652
- rules: [],
1653
- sensitivePaths: [],
1654
- generatedPaths: [],
1655
- testCapabilities: [],
1656
- confidence: 0,
1657
- warnings: [],
1658
- analyzedAt: new Date().toISOString(),
1659
- },
1660
- { ruleHash: "r", manifestHash: "m", configHash: "h1" },
1661
- );
1662
-
1663
- await cache.invalidate("/repo");
1664
-
1665
- const stats = await cache.getStats();
1666
- expect(stats.entryCount).toBe(0);
1667
-
1668
- await rm(tmp, { recursive: true, force: true });
1669
- tempDirs.pop();
1670
- });
1671
-
1672
- test("getStats counts entries correctly", async () => {
1673
- const tmp = await mkdtemp(join(tmpdir(), "cache-stats-"));
1674
- tempDirs.push(tmp);
1675
-
1676
- const cache = new AnalysisCache({ enabled: true, cacheDir: tmp });
1677
-
1678
- // Store two entries
1679
- for (const rev of ["v1", "v2"]) {
1680
- const key = cache.generateKey("/repo", rev, "h");
1681
- await cache.set(
1682
- key,
1683
- {
1684
- repositoryRoot: "/repo",
1685
- repositoryName: "repo",
1686
- revision: rev,
1687
- frameworks: [],
1688
- languages: [],
1689
- packageManagers: [],
1690
- applications: [],
1691
- commands: {
1692
- unitTest: [],
1693
- integrationTest: [],
1694
- e2eTest: [],
1695
- lint: [],
1696
- typecheck: [],
1697
- build: [],
1698
- migrate: [],
1699
- },
1700
- rules: [],
1701
- sensitivePaths: [],
1702
- generatedPaths: [],
1703
- testCapabilities: [],
1704
- confidence: 0,
1705
- warnings: [],
1706
- analyzedAt: new Date().toISOString(),
1707
- },
1708
- { ruleHash: "r", manifestHash: "m", configHash: "h" },
1709
- );
1710
- }
1711
-
1712
- const stats = await cache.getStats();
1713
- expect(stats.entryCount).toBe(2);
1714
- expect(stats.totalSize).toBeGreaterThan(0);
1715
-
1716
- await rm(tmp, { recursive: true, force: true });
1717
- tempDirs.pop();
1718
- });
1719
- });