pi-harness-runtime 1.1.439 → 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 (359) hide show
  1. package/harness/e2e/auto-test-loop.ts +8 -2
  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/index.ts +2 -1
  7. package/harness/key-loader.ts +2 -0
  8. package/harness/langchain/checkpointer.ts +2 -0
  9. package/harness/langchain/cron-cli.ts +2 -0
  10. package/harness/langchain/graph.ts +2 -0
  11. package/harness/master-planner.ts +1 -1
  12. package/harness/notification-center.ts +2 -0
  13. package/harness/output-limit-handler.ts +1 -1
  14. package/harness/project-detector/detector.ts +2 -4
  15. package/harness/secret-provider.ts +2 -0
  16. package/harness/task-graph.ts +460 -113
  17. package/index.ts +16 -7
  18. package/package.json +7 -1
  19. package/packages/a2a-adapter/package.json +1 -1
  20. package/packages/agent-policy/dist/extension.d.ts.map +1 -1
  21. package/packages/agent-policy/dist/extension.js +2 -0
  22. package/packages/agent-policy/dist/extension.js.map +1 -1
  23. package/packages/agent-policy/package.json +1 -1
  24. package/packages/agent-policy/src/extension.ts +2 -0
  25. package/packages/architecture-generator/package.json +1 -1
  26. package/packages/auth/dist/minimax-browser-auth.d.ts.map +1 -1
  27. package/packages/auth/dist/minimax-browser-auth.js +23 -1
  28. package/packages/auth/dist/minimax-browser-auth.js.map +1 -1
  29. package/packages/auth/package.json +1 -1
  30. package/packages/auth/src/minimax-browser-auth.ts +30 -5
  31. package/packages/autonomous-refactor/package.json +1 -1
  32. package/packages/autonomous-runtime/dist/inbox.d.ts.map +1 -1
  33. package/packages/autonomous-runtime/dist/inbox.js +2 -0
  34. package/packages/autonomous-runtime/dist/inbox.js.map +1 -1
  35. package/packages/autonomous-runtime/package.json +1 -1
  36. package/packages/autonomous-runtime/src/inbox.ts +2 -0
  37. package/packages/cache-strategy/package.json +1 -1
  38. package/packages/capability-registry/package.json +1 -1
  39. package/packages/checkpoint/package.json +1 -1
  40. package/packages/cli-plugin-sdk/package.json +1 -1
  41. package/packages/clipboard/dist/src/clipboard.d.ts.map +1 -1
  42. package/packages/clipboard/dist/src/clipboard.js +2 -0
  43. package/packages/clipboard/dist/src/clipboard.js.map +1 -1
  44. package/packages/clipboard/dist/src/copy-sync.d.ts.map +1 -1
  45. package/packages/clipboard/dist/src/copy-sync.js +2 -0
  46. package/packages/clipboard/dist/src/copy-sync.js.map +1 -1
  47. package/packages/clipboard/dist/src/gist-relay.d.ts.map +1 -1
  48. package/packages/clipboard/dist/src/gist-relay.js +2 -0
  49. package/packages/clipboard/dist/src/gist-relay.js.map +1 -1
  50. package/packages/clipboard/package.json +1 -1
  51. package/packages/clipboard/src/clipboard.ts +2 -0
  52. package/packages/clipboard/src/copy-sync.ts +2 -0
  53. package/packages/clipboard/src/gist-relay.ts +2 -0
  54. package/packages/clipboard-plugin/dist/types.d.ts +9 -2
  55. package/packages/clipboard-plugin/dist/types.d.ts.map +1 -1
  56. package/packages/clipboard-plugin/package.json +1 -1
  57. package/packages/clipboard-plugin/src/types.ts +9 -2
  58. package/packages/code-generation/package.json +1 -1
  59. package/packages/code-review/package.json +1 -1
  60. package/packages/codex-adapter/package.json +1 -1
  61. package/packages/codex-watcher/package.json +1 -1
  62. package/packages/config-capture/package.json +1 -1
  63. package/packages/connection-runtime/package.json +1 -1
  64. package/packages/context-compiler/package.json +1 -1
  65. package/packages/context-discovery/package.json +1 -1
  66. package/packages/context-manager/package.json +1 -1
  67. package/packages/contracts/dist/delegation-store.js +1 -4
  68. package/packages/contracts/dist/workspace-env.js +28 -1
  69. package/packages/contracts/package.json +1 -1
  70. package/packages/contracts/src/delegation-store.ts +9 -4
  71. package/packages/contracts/src/workspace-env.ts +29 -4
  72. package/packages/cookie-sanitizer/package.json +1 -1
  73. package/packages/cost-optimizer/package.json +1 -1
  74. package/packages/dependency-analyzer/package.json +1 -1
  75. package/packages/django-plugin/package.json +1 -1
  76. package/packages/doc-generator/package.json +1 -1
  77. package/packages/durable-review/package.json +1 -1
  78. package/packages/e2e-tester/package.json +1 -1
  79. package/packages/evaluation-engine/package.json +1 -1
  80. package/packages/evaluation-runner/package.json +1 -1
  81. package/packages/event-bus/package.json +1 -1
  82. package/packages/event-sink/package.json +1 -1
  83. package/packages/event-store/dist/src/store.d.ts.map +1 -1
  84. package/packages/event-store/dist/src/store.js +2 -0
  85. package/packages/event-store/dist/src/store.js.map +1 -1
  86. package/packages/event-store/package.json +1 -1
  87. package/packages/event-store/src/store.ts +3 -0
  88. package/packages/experience-replay/package.json +1 -1
  89. package/packages/feedback-collector/package.json +1 -1
  90. package/packages/file-copy-helper/package.json +1 -1
  91. package/packages/framework-detector/package.json +1 -1
  92. package/packages/framework-plugin-sdk/package.json +1 -1
  93. package/packages/frappe-plugin/package.json +1 -1
  94. package/packages/generic-web-plugin/dist/analyzer.d.ts.map +1 -1
  95. package/packages/generic-web-plugin/dist/analyzer.js +2 -0
  96. package/packages/generic-web-plugin/dist/analyzer.js.map +1 -1
  97. package/packages/generic-web-plugin/package.json +1 -1
  98. package/packages/generic-web-plugin/src/analyzer.ts +3 -0
  99. package/packages/health-monitor/package.json +1 -1
  100. package/packages/intent-analyzer/package.json +1 -1
  101. package/packages/jev-judge/package.json +1 -1
  102. package/packages/knowledge-graph/package.json +1 -1
  103. package/packages/knowledge-retrieval/dist/mcp-client.d.ts.map +1 -1
  104. package/packages/knowledge-retrieval/dist/mcp-client.js +0 -1
  105. package/packages/knowledge-retrieval/dist/mcp-client.js.map +1 -1
  106. package/packages/knowledge-retrieval/package.json +1 -4
  107. package/packages/knowledge-retrieval/src/mcp-client.ts +0 -1
  108. package/packages/laravel-plugin/package.json +1 -1
  109. package/packages/learning-engine/package.json +1 -1
  110. package/packages/logging/package.json +1 -1
  111. package/packages/mcp-adapter/dist/server.d.ts.map +1 -1
  112. package/packages/mcp-adapter/dist/server.js +2 -0
  113. package/packages/mcp-adapter/dist/server.js.map +1 -1
  114. package/packages/mcp-adapter/package.json +1 -1
  115. package/packages/mcp-adapter/src/server.ts +3 -0
  116. package/packages/memory-engine/package.json +1 -1
  117. package/packages/milestone-manager/package.json +1 -1
  118. package/packages/model-registry/package.json +1 -1
  119. package/packages/nextjs-plugin/package.json +1 -1
  120. package/packages/notification/package.json +1 -1
  121. package/packages/observability/package.json +1 -1
  122. package/packages/okf-indexer/package.json +1 -1
  123. package/packages/paperclip-integration/dist/index.d.ts.map +1 -1
  124. package/packages/paperclip-integration/dist/index.js +2 -0
  125. package/packages/paperclip-integration/dist/index.js.map +1 -1
  126. package/packages/paperclip-integration/package.json +1 -1
  127. package/packages/paperclip-integration/src/index.ts +2 -0
  128. package/packages/performance-optimizer/package.json +1 -1
  129. package/packages/permission-mode/package.json +1 -1
  130. package/packages/privilege-broker/dist/audit.d.ts.map +1 -1
  131. package/packages/privilege-broker/dist/audit.js +2 -0
  132. package/packages/privilege-broker/dist/audit.js.map +1 -1
  133. package/packages/privilege-broker/package.json +1 -1
  134. package/packages/privilege-broker/src/audit.ts +2 -0
  135. package/packages/project-analyzer/package.json +1 -1
  136. package/packages/project-bootstrap/package.json +1 -1
  137. package/packages/projection-engine/package.json +1 -1
  138. package/packages/prompt-compiler/dist/compiler.d.ts.map +1 -1
  139. package/packages/prompt-compiler/dist/compiler.js +2 -0
  140. package/packages/prompt-compiler/dist/compiler.js.map +1 -1
  141. package/packages/prompt-compiler/package.json +1 -1
  142. package/packages/prompt-compiler/src/compiler.ts +2 -0
  143. package/packages/prompt-versioning/package.json +1 -1
  144. package/packages/provider-adapter-sdk/package.json +1 -1
  145. package/packages/provider-router/package.json +1 -1
  146. package/packages/provider-selector/package.json +1 -1
  147. package/packages/providers/package.json +1 -1
  148. package/packages/qdrant-skills/package.json +1 -1
  149. package/packages/quota-manager/package.json +1 -1
  150. package/packages/rate-limiter/package.json +1 -1
  151. package/packages/react-vite-plugin/package.json +1 -1
  152. package/packages/relay/package.json +1 -1
  153. package/packages/release-manager/package.json +1 -1
  154. package/packages/requirement-compiler/package.json +1 -1
  155. package/packages/runtime/package.json +1 -1
  156. package/packages/scheduler/package.json +1 -1
  157. package/packages/scheduler-adapter/package.json +1 -1
  158. package/packages/session/package.json +1 -1
  159. package/packages/session-api/package.json +1 -1
  160. package/packages/session-export/package.json +1 -1
  161. package/packages/shared-context/package.json +1 -1
  162. package/packages/skill-mcp-client/dist/client.d.ts.map +1 -1
  163. package/packages/skill-mcp-client/dist/client.js +0 -1
  164. package/packages/skill-mcp-client/dist/client.js.map +1 -1
  165. package/packages/skill-mcp-client/package.json +1 -4
  166. package/packages/skill-mcp-client/src/client.ts +0 -1
  167. package/packages/skill-registry/package.json +1 -1
  168. package/packages/skills/dist/parser.d.ts.map +1 -1
  169. package/packages/skills/dist/parser.js +2 -0
  170. package/packages/skills/dist/parser.js.map +1 -1
  171. package/packages/skills/dist/skill-versioning.d.ts.map +1 -1
  172. package/packages/skills/dist/skill-versioning.js +2 -0
  173. package/packages/skills/dist/skill-versioning.js.map +1 -1
  174. package/packages/skills/package.json +1 -1
  175. package/packages/skills/src/parser.ts +3 -0
  176. package/packages/skills/src/skill-versioning.ts +2 -0
  177. package/packages/sprint-planner/package.json +1 -1
  178. package/packages/subscription-engine/package.json +1 -1
  179. package/packages/task-compiler/package.json +1 -1
  180. package/packages/tencentdb-memory/package.json +1 -1
  181. package/packages/tencentdb-sync/package.json +1 -1
  182. package/packages/test-data-generator/package.json +1 -1
  183. package/packages/test-generator/package.json +1 -1
  184. package/packages/todo-bd-sync/package.json +1 -1
  185. package/packages/token-estimation/package.json +1 -1
  186. package/packages/token-optimizer/package.json +1 -1
  187. package/packages/tooling-preference/package.json +1 -1
  188. package/packages/trajectory/package.json +1 -1
  189. package/packages/tui/package.json +1 -1
  190. package/packages/two-phase-review/package.json +1 -1
  191. package/packages/types/package.json +1 -1
  192. package/packages/voice-api/package.json +1 -1
  193. package/packages/webmcp-tester/dist/index.js +0 -2
  194. package/packages/webmcp-tester/package.json +1 -1
  195. package/packages/webmcp-tester/src/index.ts +0 -3
  196. package/packages/wiki/package.json +1 -1
  197. package/packages/workflow-events/package.json +1 -1
  198. package/packages/workspace-scanner/package.json +1 -1
  199. package/packages/worktree/package.json +1 -1
  200. package/packages/write-review/dist/trigger.d.ts.map +1 -1
  201. package/packages/write-review/dist/trigger.js +1 -2
  202. package/packages/write-review/dist/trigger.js.map +1 -1
  203. package/packages/write-review/package.json +1 -1
  204. package/packages/write-review/src/trigger.ts +1 -2
  205. package/scripts/lib/runtime-imports.ts +189 -3
  206. package/scripts/release-all.ts +5 -3
  207. package/scripts/telegram-webhook-server.ts +2 -0
  208. package/scripts/typecheck-packages.mjs +7 -4
  209. package/scripts/verify-package-contents.ts +161 -2
  210. package/harness/e2e/tools-detector.test.ts +0 -156
  211. package/harness/glm-quota-countdown.test.ts +0 -323
  212. package/harness/langchain/agents.test.ts +0 -39
  213. package/harness/langchain/checkpointer.test.ts +0 -243
  214. package/harness/langchain/daemon.test.ts +0 -459
  215. package/harness/langchain/model-router.test.ts +0 -191
  216. package/harness/langchain/ping-pong-middleware.test.ts +0 -69
  217. package/harness/langchain/scoreboard.test.ts +0 -245
  218. package/harness/langchain/status-line.test.ts +0 -106
  219. package/harness/langchain/surge.test.ts +0 -467
  220. package/harness/langchain/widget.test.ts +0 -251
  221. package/harness/skill-sync.test.ts +0 -57
  222. package/harness/ssh-detach-interceptor.test.ts +0 -263
  223. package/harness/ssh-hang-recovery.test.ts +0 -86
  224. package/packages/a2a-adapter/test/a2a.test.ts +0 -151
  225. package/packages/agent-policy/test/interlock.test.ts +0 -101
  226. package/packages/agent-policy/test/policy-retention.test.ts +0 -380
  227. package/packages/agent-policy/test/policy.test.ts +0 -387
  228. package/packages/auth/test/auth.test.ts +0 -238
  229. package/packages/autonomous-refactor/test/refactor.test.ts +0 -202
  230. package/packages/autonomous-runtime/test/types.test.ts +0 -95
  231. package/packages/cache-strategy/test/cache.test.ts +0 -137
  232. package/packages/capability-registry/test/registry.test.ts +0 -204
  233. package/packages/checkpoint/test/engine.test.ts +0 -265
  234. package/packages/cli-plugin-sdk/test/cli.test.ts +0 -45
  235. package/packages/code-generation/test/generator.test.ts +0 -238
  236. package/packages/code-review/test/engine.test.ts +0 -302
  237. package/packages/codex-adapter/test/provider.test.ts +0 -142
  238. package/packages/codex-watcher/test/watch.test.ts +0 -453
  239. package/packages/context-compiler/test/context-compiler.test.ts +0 -746
  240. package/packages/context-manager/test/context.test.ts +0 -198
  241. package/packages/contracts/test/delegation.test.ts +0 -350
  242. package/packages/contracts/test/workspace-env.test.ts +0 -802
  243. package/packages/cookie-sanitizer/test/infer.test.ts +0 -110
  244. package/packages/cookie-sanitizer/test/normalize.test.ts +0 -113
  245. package/packages/cookie-sanitizer/test/parse.test.ts +0 -204
  246. package/packages/cookie-sanitizer/test/sync.test.ts +0 -237
  247. package/packages/cookie-sanitizer/test/watcher.test.ts +0 -27
  248. package/packages/cost-optimizer/test/cost-optimizer.test.ts +0 -438
  249. package/packages/dependency-analyzer/test/analyzer.test.ts +0 -49
  250. package/packages/django-plugin/test/analyzer.test.ts +0 -195
  251. package/packages/doc-generator/test/docs.test.ts +0 -168
  252. package/packages/durable-review/test/acceptance.test.ts +0 -357
  253. package/packages/durable-review/test/smoke.test.ts +0 -107
  254. package/packages/evaluation-engine/test/engine.test.ts +0 -277
  255. package/packages/evaluation-runner/test/runner.test.ts +0 -168
  256. package/packages/event-bus/test/bus.test.ts +0 -100
  257. package/packages/event-store/test/store.test.ts +0 -115
  258. package/packages/experience-replay/test/replay.test.ts +0 -267
  259. package/packages/feedback-collector/test/collector.test.ts +0 -142
  260. package/packages/framework-detector/test/detector.test.ts +0 -75
  261. package/packages/framework-plugin-sdk/test/manager.test.ts +0 -320
  262. package/packages/frappe-plugin/test/analyzer.test.ts +0 -250
  263. package/packages/generic-web-plugin/test/analyzer.test.ts +0 -134
  264. package/packages/health-monitor/test/health.test.ts +0 -231
  265. package/packages/intent-analyzer/test/analyzer.test.ts +0 -72
  266. package/packages/intent-analyzer/test/ping-pong.test.ts +0 -96
  267. package/packages/jev-judge/tests/auto-continue-context.test.ts +0 -280
  268. package/packages/jev-judge/tests/continuation-controller.test.ts +0 -1396
  269. package/packages/jev-judge/tests/jev-provider-chain.test.ts +0 -536
  270. package/packages/jev-judge/tests/outcome-application.test.ts +0 -132
  271. package/packages/jev-judge/tests/question-risk.test.ts +0 -129
  272. package/packages/jev-judge/tests/typesafe-provider.test.ts +0 -640
  273. package/packages/jev-judge/tests/validation-ledger.test.ts +0 -181
  274. package/packages/jev-judge/tests/validation-plan.test.ts +0 -170
  275. package/packages/jev-judge/tests/validation-tie-breaker.test.ts +0 -88
  276. package/packages/laravel-plugin/test/analyzer.test.ts +0 -220
  277. package/packages/learning-engine/test/engine.test.ts +0 -331
  278. package/packages/logging/test/buffer.test.ts +0 -44
  279. package/packages/logging/test/classification.test.ts +0 -137
  280. package/packages/logging/test/config.test.ts +0 -97
  281. package/packages/logging/test/fanout.test.ts +0 -186
  282. package/packages/logging/test/identity-transport.test.ts +0 -197
  283. package/packages/logging/test/incidents.test.ts +0 -404
  284. package/packages/logging/test/ingest-schema.test.ts +0 -184
  285. package/packages/logging/test/integration.test.ts +0 -140
  286. package/packages/logging/test/log-http.test.ts +0 -246
  287. package/packages/logging/test/redact.test.ts +0 -109
  288. package/packages/logging/test/sampler.test.ts +0 -57
  289. package/packages/logging/test/server.test.ts +0 -444
  290. package/packages/logging/test/shipper.test.ts +0 -152
  291. package/packages/logging/test/spool.test.ts +0 -96
  292. package/packages/mcp-adapter/test/server.test.ts +0 -91
  293. package/packages/memory-engine/test/engine.test.ts +0 -456
  294. package/packages/model-registry/test/registry.test.ts +0 -342
  295. package/packages/nextjs-plugin/test/analyzer.test.ts +0 -149
  296. package/packages/notification/test/approval-payload.test.ts +0 -269
  297. package/packages/notification/test/detail-format.test.ts +0 -106
  298. package/packages/notification/test/notification.test.ts +0 -375
  299. package/packages/notification/test/pending-question-lifecycle.test.ts +0 -195
  300. package/packages/notification/test/pending-question.test.ts +0 -331
  301. package/packages/notification/test/tooling-proposal-payload.test.ts +0 -212
  302. package/packages/observability/test/observability.test.ts +0 -1350
  303. package/packages/performance-optimizer/test/optimizer.test.ts +0 -267
  304. package/packages/project-analyzer/test/index.test.ts +0 -1719
  305. package/packages/prompt-compiler/test/prompt-compiler.test.ts +0 -813
  306. package/packages/prompt-versioning/test/versioning.test.ts +0 -112
  307. package/packages/provider-adapter-sdk/test/sdk.test.ts +0 -220
  308. package/packages/provider-router/test/provider-router.test.ts +0 -405
  309. package/packages/provider-selector/test/selector.test.ts +0 -192
  310. package/packages/providers/test/providers.test.ts +0 -225
  311. package/packages/qdrant-skills/test/collection-info.test.ts +0 -165
  312. package/packages/qdrant-skills/test/embedder-batch.test.ts +0 -184
  313. package/packages/qdrant-skills/test/embedder.test.ts +0 -97
  314. package/packages/quota-manager/test/quota.test.ts +0 -432
  315. package/packages/rate-limiter/test/limiter.test.ts +0 -128
  316. package/packages/react-vite-plugin/test/analyzer.test.ts +0 -139
  317. package/packages/relay/test/client.test.ts +0 -446
  318. package/packages/relay/test/relay-e2e.test.ts +0 -299
  319. package/packages/relay/test/relay-endpoint.test.ts +0 -278
  320. package/packages/release-manager/test/release.test.ts +0 -215
  321. package/packages/requirement-compiler/test/requirement-compiler.test.ts +0 -743
  322. package/packages/runtime/test/runtime-api.test.ts +0 -169
  323. package/packages/runtime/test/runtime.test.ts +0 -297
  324. package/packages/scheduler/test/scheduler.test.ts +0 -125
  325. package/packages/session/test/manager.test.ts +0 -233
  326. package/packages/shared-context/test/shared-context.test.ts +0 -114
  327. package/packages/skill-registry/test/registry.test.ts +0 -508
  328. package/packages/skills/test/init-skills.test.ts +0 -114
  329. package/packages/skills/test/remote-skills.test.ts +0 -360
  330. package/packages/skills/test/remote-source.test.ts +0 -248
  331. package/packages/skills/test/sanitize.test.ts +0 -250
  332. package/packages/skills/test/vector-indexing.test.ts +0 -205
  333. package/packages/skills/test/vector-schema.test.ts +0 -180
  334. package/packages/skills/test/vector-threshold.test.ts +0 -108
  335. package/packages/sprint-planner/test/planner.test.ts +0 -187
  336. package/packages/task-compiler/test/task-compiler.test.ts +0 -628
  337. package/packages/test-data-generator/test/generator.test.ts +0 -265
  338. package/packages/test-generator/test/generator.test.ts +0 -156
  339. package/packages/token-estimation/test/token-estimation.test.ts +0 -207
  340. package/packages/token-optimizer/test/optimizer.test.ts +0 -141
  341. package/packages/tooling-preference/tests/changed-files.test.ts +0 -124
  342. package/packages/tooling-preference/tests/detect.test.ts +0 -188
  343. package/packages/tooling-preference/tests/proposal.test.ts +0 -107
  344. package/packages/tooling-preference/tests/quality-check.test.ts +0 -272
  345. package/packages/tooling-preference/tests/registry.test.ts +0 -51
  346. package/packages/tooling-preference/tests/store.test.ts +0 -99
  347. package/packages/trajectory/test/store.test.ts +0 -250
  348. package/packages/tui/test/harness-footer.test.ts +0 -559
  349. package/packages/tui/test/harness-header.test.ts +0 -590
  350. package/packages/two-phase-review/test/classify.test.ts +0 -102
  351. package/packages/two-phase-review/test/helpers.ts +0 -72
  352. package/packages/two-phase-review/test/rules.test.ts +0 -150
  353. package/packages/two-phase-review/test/runtime.test.ts +0 -362
  354. package/packages/two-phase-review/test/scope.test.ts +0 -186
  355. package/packages/two-phase-review/test/signals.test.ts +0 -183
  356. package/packages/two-phase-review/test/state.test.ts +0 -201
  357. package/packages/wiki/test/basic.test.ts +0 -242
  358. package/packages/workspace-scanner/test/scanner.test.ts +0 -43
  359. 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
- });