comisai 1.0.11 → 1.0.13

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 (255) hide show
  1. package/node_modules/@comis/agent/dist/model/operation-model-defaults.js +1 -0
  2. package/node_modules/@comis/agent/dist/session/comis-session-manager.js +9 -9
  3. package/node_modules/@comis/agent/package.json +1 -1
  4. package/node_modules/@comis/channels/package.json +1 -1
  5. package/node_modules/@comis/cli/package.json +1 -1
  6. package/node_modules/@comis/core/dist/event-bus/events-infra.d.ts +2 -0
  7. package/node_modules/@comis/core/package.json +1 -1
  8. package/node_modules/@comis/daemon/dist/wiring/setup-channels.js +12 -1
  9. package/node_modules/@comis/daemon/dist/wiring/setup-gateway.js +1 -0
  10. package/node_modules/@comis/daemon/dist/wiring/setup-schedulers.js +1 -0
  11. package/node_modules/@comis/daemon/package.json +1 -1
  12. package/node_modules/@comis/gateway/package.json +1 -1
  13. package/node_modules/@comis/infra/package.json +1 -1
  14. package/node_modules/@comis/memory/package.json +1 -1
  15. package/node_modules/@comis/scheduler/dist/cron/cron-types.d.ts +6 -0
  16. package/node_modules/@comis/scheduler/dist/cron/cron-types.js +2 -0
  17. package/node_modules/@comis/scheduler/package.json +1 -1
  18. package/node_modules/@comis/shared/package.json +1 -1
  19. package/node_modules/@comis/skills/dist/builtin/platform/gateway-tool.d.ts +1 -1
  20. package/node_modules/@comis/skills/package.json +1 -1
  21. package/package.json +18 -18
  22. package/node_modules/@comis/core/dist/approval/approval-gate.test.d.ts +0 -1
  23. package/node_modules/@comis/core/dist/approval/approval-gate.test.js +0 -1003
  24. package/node_modules/@comis/core/dist/bootstrap.test.d.ts +0 -1
  25. package/node_modules/@comis/core/dist/bootstrap.test.js +0 -150
  26. package/node_modules/@comis/core/dist/config/backup.test.d.ts +0 -1
  27. package/node_modules/@comis/core/dist/config/backup.test.js +0 -160
  28. package/node_modules/@comis/core/dist/config/env-substitution.test.d.ts +0 -1
  29. package/node_modules/@comis/core/dist/config/env-substitution.test.js +0 -259
  30. package/node_modules/@comis/core/dist/config/field-metadata.test.d.ts +0 -1
  31. package/node_modules/@comis/core/dist/config/field-metadata.test.js +0 -72
  32. package/node_modules/@comis/core/dist/config/git-manager.test.d.ts +0 -1
  33. package/node_modules/@comis/core/dist/config/git-manager.test.js +0 -1042
  34. package/node_modules/@comis/core/dist/config/immutable-keys.test.d.ts +0 -1
  35. package/node_modules/@comis/core/dist/config/immutable-keys.test.js +0 -283
  36. package/node_modules/@comis/core/dist/config/include-resolver.test.d.ts +0 -1
  37. package/node_modules/@comis/core/dist/config/include-resolver.test.js +0 -292
  38. package/node_modules/@comis/core/dist/config/layered.test.d.ts +0 -1
  39. package/node_modules/@comis/core/dist/config/layered.test.js +0 -211
  40. package/node_modules/@comis/core/dist/config/loader.test.d.ts +0 -1
  41. package/node_modules/@comis/core/dist/config/loader.test.js +0 -300
  42. package/node_modules/@comis/core/dist/config/migrate.test.d.ts +0 -1
  43. package/node_modules/@comis/core/dist/config/migrate.test.js +0 -244
  44. package/node_modules/@comis/core/dist/config/partial-validator.test.d.ts +0 -1
  45. package/node_modules/@comis/core/dist/config/partial-validator.test.js +0 -98
  46. package/node_modules/@comis/core/dist/config/schema-agent-model.test.d.ts +0 -1
  47. package/node_modules/@comis/core/dist/config/schema-agent-model.test.js +0 -279
  48. package/node_modules/@comis/core/dist/config/schema-agent-session.test.d.ts +0 -1
  49. package/node_modules/@comis/core/dist/config/schema-agent-session.test.js +0 -242
  50. package/node_modules/@comis/core/dist/config/schema-agent.test.d.ts +0 -1
  51. package/node_modules/@comis/core/dist/config/schema-agent.test.js +0 -645
  52. package/node_modules/@comis/core/dist/config/schema-channel.test.d.ts +0 -1
  53. package/node_modules/@comis/core/dist/config/schema-channel.test.js +0 -341
  54. package/node_modules/@comis/core/dist/config/schema-coalescer.test.d.ts +0 -1
  55. package/node_modules/@comis/core/dist/config/schema-coalescer.test.js +0 -51
  56. package/node_modules/@comis/core/dist/config/schema-context-engine.test.d.ts +0 -1
  57. package/node_modules/@comis/core/dist/config/schema-context-engine.test.js +0 -797
  58. package/node_modules/@comis/core/dist/config/schema-context-guard.test.d.ts +0 -1
  59. package/node_modules/@comis/core/dist/config/schema-context-guard.test.js +0 -111
  60. package/node_modules/@comis/core/dist/config/schema-daemon.test.d.ts +0 -1
  61. package/node_modules/@comis/core/dist/config/schema-daemon.test.js +0 -107
  62. package/node_modules/@comis/core/dist/config/schema-delivery-timing.test.d.ts +0 -1
  63. package/node_modules/@comis/core/dist/config/schema-delivery-timing.test.js +0 -51
  64. package/node_modules/@comis/core/dist/config/schema-documentation.test.d.ts +0 -1
  65. package/node_modules/@comis/core/dist/config/schema-documentation.test.js +0 -63
  66. package/node_modules/@comis/core/dist/config/schema-gateway.test.d.ts +0 -1
  67. package/node_modules/@comis/core/dist/config/schema-gateway.test.js +0 -219
  68. package/node_modules/@comis/core/dist/config/schema-gemini-cache.test.d.ts +0 -1
  69. package/node_modules/@comis/core/dist/config/schema-gemini-cache.test.js +0 -41
  70. package/node_modules/@comis/core/dist/config/schema-integrations.test.d.ts +0 -1
  71. package/node_modules/@comis/core/dist/config/schema-integrations.test.js +0 -92
  72. package/node_modules/@comis/core/dist/config/schema-lifecycle-reactions.test.d.ts +0 -1
  73. package/node_modules/@comis/core/dist/config/schema-lifecycle-reactions.test.js +0 -61
  74. package/node_modules/@comis/core/dist/config/schema-misc.test.d.ts +0 -1
  75. package/node_modules/@comis/core/dist/config/schema-misc.test.js +0 -394
  76. package/node_modules/@comis/core/dist/config/schema-observability.test.d.ts +0 -1
  77. package/node_modules/@comis/core/dist/config/schema-observability.test.js +0 -76
  78. package/node_modules/@comis/core/dist/config/schema-providers.test.d.ts +0 -1
  79. package/node_modules/@comis/core/dist/config/schema-providers.test.js +0 -294
  80. package/node_modules/@comis/core/dist/config/schema-queue.test.d.ts +0 -1
  81. package/node_modules/@comis/core/dist/config/schema-queue.test.js +0 -234
  82. package/node_modules/@comis/core/dist/config/schema-response-prefix.test.d.ts +0 -1
  83. package/node_modules/@comis/core/dist/config/schema-response-prefix.test.js +0 -36
  84. package/node_modules/@comis/core/dist/config/schema-security.test.d.ts +0 -1
  85. package/node_modules/@comis/core/dist/config/schema-security.test.js +0 -54
  86. package/node_modules/@comis/core/dist/config/schema-sender-trust-display.test.d.ts +0 -1
  87. package/node_modules/@comis/core/dist/config/schema-sender-trust-display.test.js +0 -60
  88. package/node_modules/@comis/core/dist/config/schema-serializer.test.d.ts +0 -1
  89. package/node_modules/@comis/core/dist/config/schema-serializer.test.js +0 -73
  90. package/node_modules/@comis/core/dist/config/schema-telegram-file-guard.test.d.ts +0 -1
  91. package/node_modules/@comis/core/dist/config/schema-telegram-file-guard.test.js +0 -39
  92. package/node_modules/@comis/core/dist/context/context.test.d.ts +0 -1
  93. package/node_modules/@comis/core/dist/context/context.test.js +0 -276
  94. package/node_modules/@comis/core/dist/domain/agent-response.test.d.ts +0 -1
  95. package/node_modules/@comis/core/dist/domain/agent-response.test.js +0 -159
  96. package/node_modules/@comis/core/dist/domain/credential-mapping.test.d.ts +0 -1
  97. package/node_modules/@comis/core/dist/domain/credential-mapping.test.js +0 -135
  98. package/node_modules/@comis/core/dist/domain/delivery-origin.test.d.ts +0 -1
  99. package/node_modules/@comis/core/dist/domain/delivery-origin.test.js +0 -68
  100. package/node_modules/@comis/core/dist/domain/execution-graph.test.d.ts +0 -1
  101. package/node_modules/@comis/core/dist/domain/execution-graph.test.js +0 -441
  102. package/node_modules/@comis/core/dist/domain/memory-entry.test.d.ts +0 -1
  103. package/node_modules/@comis/core/dist/domain/memory-entry.test.js +0 -193
  104. package/node_modules/@comis/core/dist/domain/normalized-message.test.d.ts +0 -1
  105. package/node_modules/@comis/core/dist/domain/normalized-message.test.js +0 -255
  106. package/node_modules/@comis/core/dist/domain/session-key.test.d.ts +0 -1
  107. package/node_modules/@comis/core/dist/domain/session-key.test.js +0 -291
  108. package/node_modules/@comis/core/dist/domain/subagent-context-config.test.d.ts +0 -1
  109. package/node_modules/@comis/core/dist/domain/subagent-context-config.test.js +0 -131
  110. package/node_modules/@comis/core/dist/domain/subagent-context-types.test.d.ts +0 -1
  111. package/node_modules/@comis/core/dist/domain/subagent-context-types.test.js +0 -182
  112. package/node_modules/@comis/core/dist/event-bus/bus.test.d.ts +0 -1
  113. package/node_modules/@comis/core/dist/event-bus/bus.test.js +0 -152
  114. package/node_modules/@comis/core/dist/event-bus/events-agent.test.d.ts +0 -1
  115. package/node_modules/@comis/core/dist/event-bus/events-agent.test.js +0 -409
  116. package/node_modules/@comis/core/dist/event-bus/events-channel.test.d.ts +0 -1
  117. package/node_modules/@comis/core/dist/event-bus/events-channel.test.js +0 -240
  118. package/node_modules/@comis/core/dist/event-bus/events-infra.test.d.ts +0 -1
  119. package/node_modules/@comis/core/dist/event-bus/events-infra.test.js +0 -416
  120. package/node_modules/@comis/core/dist/event-bus/events-messaging.test.d.ts +0 -1
  121. package/node_modules/@comis/core/dist/event-bus/events-messaging.test.js +0 -433
  122. package/node_modules/@comis/core/dist/event-bus/events.test.d.ts +0 -1
  123. package/node_modules/@comis/core/dist/event-bus/events.test.js +0 -93
  124. package/node_modules/@comis/core/dist/hooks/hook-runner-global.test.d.ts +0 -1
  125. package/node_modules/@comis/core/dist/hooks/hook-runner-global.test.js +0 -29
  126. package/node_modules/@comis/core/dist/hooks/hook-runner.test.d.ts +0 -1
  127. package/node_modules/@comis/core/dist/hooks/hook-runner.test.js +0 -490
  128. package/node_modules/@comis/core/dist/hooks/hook-strategies.test.d.ts +0 -1
  129. package/node_modules/@comis/core/dist/hooks/hook-strategies.test.js +0 -209
  130. package/node_modules/@comis/core/dist/hooks/integration.test.d.ts +0 -1
  131. package/node_modules/@comis/core/dist/hooks/integration.test.js +0 -235
  132. package/node_modules/@comis/core/dist/hooks/plugin-registry.test.d.ts +0 -1
  133. package/node_modules/@comis/core/dist/hooks/plugin-registry.test.js +0 -470
  134. package/node_modules/@comis/core/dist/load-env.test.d.ts +0 -1
  135. package/node_modules/@comis/core/dist/load-env.test.js +0 -21
  136. package/node_modules/@comis/core/dist/security/action-classifier.test.d.ts +0 -1
  137. package/node_modules/@comis/core/dist/security/action-classifier.test.js +0 -298
  138. package/node_modules/@comis/core/dist/security/audit-aggregator.test.d.ts +0 -1
  139. package/node_modules/@comis/core/dist/security/audit-aggregator.test.js +0 -128
  140. package/node_modules/@comis/core/dist/security/audit.test.d.ts +0 -1
  141. package/node_modules/@comis/core/dist/security/audit.test.js +0 -154
  142. package/node_modules/@comis/core/dist/security/canary-token.test.d.ts +0 -1
  143. package/node_modules/@comis/core/dist/security/canary-token.test.js +0 -45
  144. package/node_modules/@comis/core/dist/security/config-redaction.test.d.ts +0 -1
  145. package/node_modules/@comis/core/dist/security/config-redaction.test.js +0 -107
  146. package/node_modules/@comis/core/dist/security/external-content.test.d.ts +0 -1
  147. package/node_modules/@comis/core/dist/security/external-content.test.js +0 -210
  148. package/node_modules/@comis/core/dist/security/injection-patterns.test.d.ts +0 -1
  149. package/node_modules/@comis/core/dist/security/injection-patterns.test.js +0 -213
  150. package/node_modules/@comis/core/dist/security/injection-rate-limiter.test.d.ts +0 -1
  151. package/node_modules/@comis/core/dist/security/injection-rate-limiter.test.js +0 -194
  152. package/node_modules/@comis/core/dist/security/input-guard.test.d.ts +0 -1
  153. package/node_modules/@comis/core/dist/security/input-guard.test.js +0 -215
  154. package/node_modules/@comis/core/dist/security/input-security-guard.test.d.ts +0 -1
  155. package/node_modules/@comis/core/dist/security/input-security-guard.test.js +0 -215
  156. package/node_modules/@comis/core/dist/security/input-validator.test.d.ts +0 -1
  157. package/node_modules/@comis/core/dist/security/input-validator.test.js +0 -133
  158. package/node_modules/@comis/core/dist/security/log-sanitizer.test.d.ts +0 -1
  159. package/node_modules/@comis/core/dist/security/log-sanitizer.test.js +0 -260
  160. package/node_modules/@comis/core/dist/security/memory-write-validator.test.d.ts +0 -1
  161. package/node_modules/@comis/core/dist/security/memory-write-validator.test.js +0 -62
  162. package/node_modules/@comis/core/dist/security/output-guard.test.d.ts +0 -1
  163. package/node_modules/@comis/core/dist/security/output-guard.test.js +0 -397
  164. package/node_modules/@comis/core/dist/security/safe-path.test.d.ts +0 -1
  165. package/node_modules/@comis/core/dist/security/safe-path.test.js +0 -95
  166. package/node_modules/@comis/core/dist/security/scoped-secret-manager.test.d.ts +0 -1
  167. package/node_modules/@comis/core/dist/security/scoped-secret-manager.test.js +0 -231
  168. package/node_modules/@comis/core/dist/security/secret-access.test.d.ts +0 -1
  169. package/node_modules/@comis/core/dist/security/secret-access.test.js +0 -41
  170. package/node_modules/@comis/core/dist/security/secret-crypto.test.d.ts +0 -1
  171. package/node_modules/@comis/core/dist/security/secret-crypto.test.js +0 -113
  172. package/node_modules/@comis/core/dist/security/secret-manager.test.d.ts +0 -1
  173. package/node_modules/@comis/core/dist/security/secret-manager.test.js +0 -394
  174. package/node_modules/@comis/core/dist/security/secret-ref-resolver.test.d.ts +0 -1
  175. package/node_modules/@comis/core/dist/security/secret-ref-resolver.test.js +0 -268
  176. package/node_modules/@comis/core/dist/security/secrets-audit.test.d.ts +0 -10
  177. package/node_modules/@comis/core/dist/security/secrets-audit.test.js +0 -295
  178. package/node_modules/@comis/core/dist/security/ssrf-guard.test.d.ts +0 -1
  179. package/node_modules/@comis/core/dist/security/ssrf-guard.test.js +0 -127
  180. package/node_modules/@comis/core/dist/security/token-generator.test.d.ts +0 -1
  181. package/node_modules/@comis/core/dist/security/token-generator.test.js +0 -35
  182. package/node_modules/@comis/core/dist/tool-metadata.test.d.ts +0 -1
  183. package/node_modules/@comis/core/dist/tool-metadata.test.js +0 -112
  184. package/node_modules/@comis/memory/dist/compaction.test.d.ts +0 -1
  185. package/node_modules/@comis/memory/dist/compaction.test.js +0 -298
  186. package/node_modules/@comis/memory/dist/context-schema.test.d.ts +0 -1
  187. package/node_modules/@comis/memory/dist/context-schema.test.js +0 -246
  188. package/node_modules/@comis/memory/dist/context-store.test.d.ts +0 -1
  189. package/node_modules/@comis/memory/dist/context-store.test.js +0 -708
  190. package/node_modules/@comis/memory/dist/credential-mapping-schema.test.d.ts +0 -7
  191. package/node_modules/@comis/memory/dist/credential-mapping-schema.test.js +0 -73
  192. package/node_modules/@comis/memory/dist/credential-mapping-store.test.d.ts +0 -8
  193. package/node_modules/@comis/memory/dist/credential-mapping-store.test.js +0 -340
  194. package/node_modules/@comis/memory/dist/delivery-mirror-adapter.test.d.ts +0 -1
  195. package/node_modules/@comis/memory/dist/delivery-mirror-adapter.test.js +0 -165
  196. package/node_modules/@comis/memory/dist/delivery-queue-adapter.test.d.ts +0 -1
  197. package/node_modules/@comis/memory/dist/delivery-queue-adapter.test.js +0 -263
  198. package/node_modules/@comis/memory/dist/embedding-batch-indexer.test.d.ts +0 -1
  199. package/node_modules/@comis/memory/dist/embedding-batch-indexer.test.js +0 -202
  200. package/node_modules/@comis/memory/dist/embedding-cache-lru.test.d.ts +0 -1
  201. package/node_modules/@comis/memory/dist/embedding-cache-lru.test.js +0 -241
  202. package/node_modules/@comis/memory/dist/embedding-cache-sqlite.test.d.ts +0 -8
  203. package/node_modules/@comis/memory/dist/embedding-cache-sqlite.test.js +0 -678
  204. package/node_modules/@comis/memory/dist/embedding-cache.test.d.ts +0 -1
  205. package/node_modules/@comis/memory/dist/embedding-cache.test.js +0 -213
  206. package/node_modules/@comis/memory/dist/embedding-fingerprint.test.d.ts +0 -1
  207. package/node_modules/@comis/memory/dist/embedding-fingerprint.test.js +0 -87
  208. package/node_modules/@comis/memory/dist/embedding-hash.test.d.ts +0 -1
  209. package/node_modules/@comis/memory/dist/embedding-hash.test.js +0 -31
  210. package/node_modules/@comis/memory/dist/embedding-integration.test.d.ts +0 -8
  211. package/node_modules/@comis/memory/dist/embedding-integration.test.js +0 -232
  212. package/node_modules/@comis/memory/dist/embedding-provider-factory.test.d.ts +0 -1
  213. package/node_modules/@comis/memory/dist/embedding-provider-factory.test.js +0 -176
  214. package/node_modules/@comis/memory/dist/embedding-provider-local.test.d.ts +0 -8
  215. package/node_modules/@comis/memory/dist/embedding-provider-local.test.js +0 -76
  216. package/node_modules/@comis/memory/dist/embedding-provider-openai.test.d.ts +0 -8
  217. package/node_modules/@comis/memory/dist/embedding-provider-openai.test.js +0 -100
  218. package/node_modules/@comis/memory/dist/embedding-queue.test.d.ts +0 -1
  219. package/node_modules/@comis/memory/dist/embedding-queue.test.js +0 -236
  220. package/node_modules/@comis/memory/dist/hybrid-search.test.d.ts +0 -1
  221. package/node_modules/@comis/memory/dist/hybrid-search.test.js +0 -476
  222. package/node_modules/@comis/memory/dist/identity-link-store.test.d.ts +0 -1
  223. package/node_modules/@comis/memory/dist/identity-link-store.test.js +0 -88
  224. package/node_modules/@comis/memory/dist/memory-api.test.d.ts +0 -1
  225. package/node_modules/@comis/memory/dist/memory-api.test.js +0 -495
  226. package/node_modules/@comis/memory/dist/memory-concurrency.test.d.ts +0 -20
  227. package/node_modules/@comis/memory/dist/memory-concurrency.test.js +0 -222
  228. package/node_modules/@comis/memory/dist/named-graph-store.test.d.ts +0 -1
  229. package/node_modules/@comis/memory/dist/named-graph-store.test.js +0 -227
  230. package/node_modules/@comis/memory/dist/observability-store.test.d.ts +0 -1
  231. package/node_modules/@comis/memory/dist/observability-store.test.js +0 -704
  232. package/node_modules/@comis/memory/dist/row-mapper.test.d.ts +0 -1
  233. package/node_modules/@comis/memory/dist/row-mapper.test.js +0 -409
  234. package/node_modules/@comis/memory/dist/schema.test.d.ts +0 -1
  235. package/node_modules/@comis/memory/dist/schema.test.js +0 -240
  236. package/node_modules/@comis/memory/dist/secret-store-schema.test.d.ts +0 -7
  237. package/node_modules/@comis/memory/dist/secret-store-schema.test.js +0 -90
  238. package/node_modules/@comis/memory/dist/session-store.test.d.ts +0 -1
  239. package/node_modules/@comis/memory/dist/session-store.test.js +0 -262
  240. package/node_modules/@comis/memory/dist/setup-secrets.test.d.ts +0 -1
  241. package/node_modules/@comis/memory/dist/setup-secrets.test.js +0 -140
  242. package/node_modules/@comis/memory/dist/sqlite-memory-adapter.test.d.ts +0 -1
  243. package/node_modules/@comis/memory/dist/sqlite-memory-adapter.test.js +0 -888
  244. package/node_modules/@comis/memory/dist/sqlite-secret-store.test.d.ts +0 -8
  245. package/node_modules/@comis/memory/dist/sqlite-secret-store.test.js +0 -245
  246. package/node_modules/@comis/shared/dist/abort.test.d.ts +0 -1
  247. package/node_modules/@comis/shared/dist/abort.test.js +0 -71
  248. package/node_modules/@comis/shared/dist/result.test.d.ts +0 -1
  249. package/node_modules/@comis/shared/dist/result.test.js +0 -178
  250. package/node_modules/@comis/shared/dist/suppress-error.test.d.ts +0 -1
  251. package/node_modules/@comis/shared/dist/suppress-error.test.js +0 -50
  252. package/node_modules/@comis/shared/dist/timeout.test.d.ts +0 -1
  253. package/node_modules/@comis/shared/dist/timeout.test.js +0 -75
  254. package/node_modules/@comis/shared/dist/ttl-cache.test.d.ts +0 -1
  255. package/node_modules/@comis/shared/dist/ttl-cache.test.js +0 -81
@@ -1,1042 +0,0 @@
1
- import { describe, it, expect, vi } from "vitest";
2
- import { ok, err } from "@comis/shared";
3
- import { createConfigGitManager, encodeCommitMessage, GITIGNORE_CONTENT, parseCommitMessage, } from "./git-manager.js";
4
- function createSha(index) {
5
- return `${"a".repeat(7)}${String(index).padStart(33, "0")}`;
6
- }
7
- /**
8
- * Create mock deps with a simulated in-memory git repo.
9
- *
10
- * Handles git init, add, commit, log, diff, status, rev-parse, cat-file,
11
- * and checkout commands with deterministic responses.
12
- */
13
- function createMockDeps(opts) {
14
- const calls = [];
15
- const writtenFiles = opts?.writtenFiles ?? new Map();
16
- const repo = {
17
- initialized: opts?.preInitialized ?? false,
18
- commits: [],
19
- workingTree: new Map(opts?.initialFiles ?? []),
20
- stagedFiles: new Set(),
21
- dirty: false,
22
- };
23
- // If pre-initialized, create an initial commit
24
- if (opts?.preInitialized) {
25
- repo.commits.push({
26
- sha: createSha(0),
27
- message: "Initial config snapshot",
28
- timestamp: "2026-02-25T10:00:00+00:00",
29
- files: new Map(repo.workingTree),
30
- });
31
- }
32
- const execGit = async (args, cwd) => {
33
- calls.push({ args, cwd });
34
- const command = args[0];
35
- // Check for forced failures
36
- const cmdKey = args.join(" ");
37
- if (opts?.failCommands) {
38
- for (const [pattern, errorMsg] of opts.failCommands) {
39
- if (cmdKey.includes(pattern)) {
40
- return err(errorMsg);
41
- }
42
- }
43
- }
44
- switch (command) {
45
- case "status": {
46
- if (!repo.initialized) {
47
- return err("fatal: not a git repository");
48
- }
49
- if (repo.dirty) {
50
- return ok(" M config.yaml\n");
51
- }
52
- return ok("");
53
- }
54
- case "init": {
55
- repo.initialized = true;
56
- return ok("Initialized empty Git repository");
57
- }
58
- case "add": {
59
- if (!repo.initialized) {
60
- return err("fatal: not a git repository");
61
- }
62
- // Track staged files
63
- for (let i = 1; i < args.length; i++) {
64
- const pattern = args[i];
65
- if (pattern === "--ignore-errors")
66
- continue;
67
- if (pattern.includes("*")) {
68
- // Glob — stage all matching files from working tree
69
- const ext = pattern.replace("*", "");
70
- for (const [name] of repo.workingTree) {
71
- if (name.endsWith(ext)) {
72
- repo.stagedFiles.add(name);
73
- }
74
- }
75
- }
76
- else {
77
- repo.stagedFiles.add(pattern);
78
- }
79
- }
80
- return ok("");
81
- }
82
- case "commit": {
83
- if (!repo.initialized) {
84
- return err("fatal: not a git repository");
85
- }
86
- // Extract message from -m flag
87
- const mIndex = args.indexOf("-m");
88
- const message = mIndex >= 0 ? args[mIndex + 1] : "";
89
- // Check for --allow-empty
90
- const allowEmpty = args.includes("--allow-empty");
91
- if (!allowEmpty && repo.stagedFiles.size === 0 && !repo.dirty) {
92
- return err("nothing to commit, working tree clean");
93
- }
94
- const sha = createSha(repo.commits.length);
95
- const files = new Map(repo.workingTree);
96
- repo.commits.push({
97
- sha,
98
- message,
99
- timestamp: `2026-02-25T1${String(repo.commits.length).padStart(1, "0")}:00:00+00:00`,
100
- files,
101
- });
102
- repo.stagedFiles.clear();
103
- repo.dirty = false;
104
- return ok(`[main ${sha.slice(0, 7)}] ${message.split("\n")[0]}`);
105
- }
106
- case "rev-parse": {
107
- if (!repo.initialized) {
108
- return err("fatal: not a git repository");
109
- }
110
- if (args[1] === "HEAD") {
111
- if (repo.commits.length === 0) {
112
- return err("fatal: ambiguous argument 'HEAD'");
113
- }
114
- return ok(repo.commits[repo.commits.length - 1].sha);
115
- }
116
- if (args[1] === "--verify" && args[2] === "HEAD~1") {
117
- if (repo.commits.length < 2) {
118
- return err("fatal: ambiguous argument 'HEAD~1'");
119
- }
120
- return ok(repo.commits[repo.commits.length - 2].sha);
121
- }
122
- return err(`fatal: bad revision '${args[1]}'`);
123
- }
124
- case "log": {
125
- if (!repo.initialized || repo.commits.length === 0) {
126
- return err("fatal: your current branch 'main' does not have any commits yet");
127
- }
128
- // Parse --max-count
129
- const maxCountArg = args.find((a) => a.startsWith("--max-count="));
130
- const maxCount = maxCountArg
131
- ? parseInt(maxCountArg.split("=")[1], 10)
132
- : repo.commits.length;
133
- // Build log output matching the format %H%n%aI%n%B---END---
134
- const entries = [...repo.commits]
135
- .reverse()
136
- .slice(0, maxCount);
137
- const output = entries
138
- .map((c) => `${c.sha}\n${c.timestamp}\n${c.message}\n`)
139
- .join("---END---");
140
- return ok(output + "---END---");
141
- }
142
- case "diff": {
143
- if (!repo.initialized) {
144
- return err("fatal: not a git repository");
145
- }
146
- // Return a mock unified diff
147
- return ok("--- a/config.yaml\n+++ b/config.yaml\n@@ -1,3 +1,3 @@\n setting: old\n-value: before\n+value: after\n other: same\n");
148
- }
149
- case "cat-file": {
150
- if (!repo.initialized) {
151
- return err("fatal: not a git repository");
152
- }
153
- const targetSha = args[2];
154
- const found = repo.commits.find((c) => c.sha === targetSha);
155
- if (found) {
156
- return ok("commit");
157
- }
158
- return err(`fatal: Not a valid object name ${targetSha}`);
159
- }
160
- case "checkout": {
161
- if (!repo.initialized) {
162
- return err("fatal: not a git repository");
163
- }
164
- // Find the target commit and restore files
165
- const sha = args[1];
166
- const commit = repo.commits.find((c) => c.sha === sha);
167
- if (commit) {
168
- // Restore files from that commit
169
- for (const [name, content] of commit.files) {
170
- repo.workingTree.set(name, content);
171
- }
172
- repo.dirty = true;
173
- return ok("");
174
- }
175
- return err(`error: pathspec '${sha}' did not match any file(s) known to git`);
176
- }
177
- default:
178
- return ok("");
179
- }
180
- };
181
- const deps = {
182
- configDir: "/test/config",
183
- execGit,
184
- writeFile: async (relativePath, content) => {
185
- writtenFiles.set(relativePath, content);
186
- return ok(undefined);
187
- },
188
- };
189
- return { deps, calls, repo, writtenFiles };
190
- }
191
- // ---------------------------------------------------------------------------
192
- // Tests: Task 1 — init, commit, checkDirty, auto-reinit, best-effort
193
- // ---------------------------------------------------------------------------
194
- describe("config/git-manager", () => {
195
- describe("init()", () => {
196
- it("creates .git directory by running git init", async () => {
197
- const { deps, calls, repo } = createMockDeps();
198
- const manager = createConfigGitManager(deps);
199
- const result = await manager.init();
200
- expect(result.ok).toBe(true);
201
- expect(repo.initialized).toBe(true);
202
- // Should have called git status (check existing), git init, git add, git commit
203
- const commands = calls.map((c) => c.args[0]);
204
- expect(commands).toContain("status");
205
- expect(commands).toContain("init");
206
- expect(commands).toContain("commit");
207
- });
208
- it("writes .gitignore with YAML whitelist pattern", async () => {
209
- const { deps, writtenFiles } = createMockDeps();
210
- const manager = createConfigGitManager(deps);
211
- await manager.init();
212
- expect(writtenFiles.has(".gitignore")).toBe(true);
213
- const content = writtenFiles.get(".gitignore");
214
- expect(content).toContain("!*.yaml");
215
- expect(content).toContain("!*.yml");
216
- expect(content).toContain("!.gitignore");
217
- expect(content).toBe(GITIGNORE_CONTENT);
218
- });
219
- it("creates initial commit with allow-empty", async () => {
220
- const { deps, calls } = createMockDeps();
221
- const manager = createConfigGitManager(deps);
222
- await manager.init();
223
- const commitCall = calls.find((c) => c.args[0] === "commit" && c.args.includes("--allow-empty"));
224
- expect(commitCall).toBeDefined();
225
- expect(commitCall.args).toContain("Initial config snapshot");
226
- });
227
- it("returns ok() early if repo already exists", async () => {
228
- const { deps, calls } = createMockDeps({ preInitialized: true });
229
- const manager = createConfigGitManager(deps);
230
- const result = await manager.init();
231
- expect(result.ok).toBe(true);
232
- // Should only call status (which succeeds) — no init needed
233
- const commands = calls.map((c) => c.args[0]);
234
- expect(commands).not.toContain("init");
235
- });
236
- it("is idempotent — second call is a no-op", async () => {
237
- const { deps, calls } = createMockDeps();
238
- const manager = createConfigGitManager(deps);
239
- await manager.init();
240
- const callsAfterFirst = calls.length;
241
- const result = await manager.init();
242
- expect(result.ok).toBe(true);
243
- // Second call should not make any new git calls (cached initialized flag)
244
- expect(calls.length).toBe(callsAfterFirst);
245
- });
246
- it("returns err() on git init failure", async () => {
247
- const failCommands = new Map([["init", "fatal: permission denied"]]);
248
- const { deps } = createMockDeps({ failCommands });
249
- const manager = createConfigGitManager(deps);
250
- const result = await manager.init();
251
- expect(result.ok).toBe(false);
252
- if (!result.ok) {
253
- expect(result.error).toContain("git init failed");
254
- }
255
- });
256
- });
257
- describe("commit()", () => {
258
- it("stages YAML files and creates commit with structured metadata", async () => {
259
- const initialFiles = new Map([["config.yaml", "setting: value"]]);
260
- const { deps, calls, repo } = createMockDeps({
261
- preInitialized: true,
262
- initialFiles,
263
- });
264
- // Mark as dirty so commit succeeds
265
- repo.dirty = true;
266
- const manager = createConfigGitManager(deps);
267
- const result = await manager.commit({
268
- section: "agent",
269
- key: "agent.model",
270
- summary: "Changed model to claude-opus",
271
- });
272
- expect(result.ok).toBe(true);
273
- if (result.ok) {
274
- expect(result.value).toBeTruthy();
275
- expect(result.value.length).toBeGreaterThan(0);
276
- }
277
- // Verify commit message contains structured metadata
278
- const commitCall = calls.find((c) => c.args[0] === "commit" && !c.args.includes("--allow-empty"));
279
- expect(commitCall).toBeDefined();
280
- const message = commitCall.args[commitCall.args.indexOf("-m") + 1];
281
- expect(message).toContain("config: Changed model to claude-opus");
282
- expect(message).toContain("[section] agent");
283
- expect(message).toContain("[key] agent.model");
284
- });
285
- it("returns commit SHA on success", async () => {
286
- const { deps, repo } = createMockDeps({ preInitialized: true });
287
- repo.dirty = true;
288
- const manager = createConfigGitManager(deps);
289
- const result = await manager.commit({
290
- section: "gateway",
291
- summary: "Updated rate limit",
292
- });
293
- expect(result.ok).toBe(true);
294
- if (result.ok) {
295
- // SHA should be non-empty
296
- expect(result.value.length).toBeGreaterThan(0);
297
- expect(result.value).not.toBe("");
298
- }
299
- });
300
- it("triggers lazy init when repo is not initialized", async () => {
301
- const { deps, calls, repo } = createMockDeps();
302
- // After init, mark dirty so commit succeeds
303
- const originalExecGit = deps.execGit;
304
- deps.execGit = async (args, cwd) => {
305
- const result = await originalExecGit(args, cwd);
306
- // After init completes, make the repo dirty for the actual commit
307
- if (args[0] === "commit" && args.includes("--allow-empty")) {
308
- repo.dirty = true;
309
- }
310
- return result;
311
- };
312
- const manager = createConfigGitManager(deps);
313
- const result = await manager.commit({
314
- section: "agent",
315
- summary: "First change",
316
- });
317
- expect(result.ok).toBe(true);
318
- // Should have called init first
319
- const commands = calls.map((c) => c.args[0]);
320
- expect(commands).toContain("init");
321
- });
322
- it("returns ok('') when nothing to commit", async () => {
323
- const { deps } = createMockDeps({ preInitialized: true });
324
- const manager = createConfigGitManager(deps);
325
- const result = await manager.commit({
326
- section: "agent",
327
- summary: "No changes",
328
- });
329
- expect(result.ok).toBe(true);
330
- if (result.ok) {
331
- expect(result.value).toBe("");
332
- }
333
- });
334
- it("omits optional metadata fields when undefined", async () => {
335
- const { deps, calls, repo } = createMockDeps({ preInitialized: true });
336
- repo.dirty = true;
337
- const manager = createConfigGitManager(deps);
338
- await manager.commit({
339
- section: "daemon",
340
- summary: "Updated logging level",
341
- });
342
- const commitCall = calls.find((c) => c.args[0] === "commit" && !c.args.includes("--allow-empty"));
343
- expect(commitCall).toBeDefined();
344
- const message = commitCall.args[commitCall.args.indexOf("-m") + 1];
345
- expect(message).toContain("[section] daemon");
346
- expect(message).not.toContain("[key]");
347
- expect(message).not.toContain("[agent]");
348
- expect(message).not.toContain("[user]");
349
- expect(message).not.toContain("[trace]");
350
- });
351
- it("includes all optional metadata when provided", async () => {
352
- const { deps, calls, repo } = createMockDeps({ preInitialized: true });
353
- repo.dirty = true;
354
- const manager = createConfigGitManager(deps);
355
- await manager.commit({
356
- section: "agent",
357
- key: "agent.model",
358
- agent: "assistant-1",
359
- user: "admin",
360
- traceId: "trace-abc-123",
361
- summary: "Full metadata commit",
362
- });
363
- const commitCall = calls.find((c) => c.args[0] === "commit" && !c.args.includes("--allow-empty"));
364
- const message = commitCall.args[commitCall.args.indexOf("-m") + 1];
365
- expect(message).toContain("[section] agent");
366
- expect(message).toContain("[key] agent.model");
367
- expect(message).toContain("[agent] assistant-1");
368
- expect(message).toContain("[user] admin");
369
- expect(message).toContain("[trace] trace-abc-123");
370
- });
371
- });
372
- describe("checkDirty()", () => {
373
- it("returns true when YAML files have uncommitted changes", async () => {
374
- const { deps, repo } = createMockDeps({ preInitialized: true });
375
- repo.dirty = true;
376
- const manager = createConfigGitManager(deps);
377
- const result = await manager.checkDirty();
378
- expect(result.ok).toBe(true);
379
- if (result.ok) {
380
- expect(result.value).toBe(true);
381
- }
382
- });
383
- it("returns false when working tree is clean", async () => {
384
- const { deps } = createMockDeps({ preInitialized: true });
385
- const manager = createConfigGitManager(deps);
386
- const result = await manager.checkDirty();
387
- expect(result.ok).toBe(true);
388
- if (result.ok) {
389
- expect(result.value).toBe(false);
390
- }
391
- });
392
- it("calls init() first to ensure repo exists", async () => {
393
- const { deps, calls } = createMockDeps();
394
- const manager = createConfigGitManager(deps);
395
- await manager.checkDirty();
396
- // Should have init-related calls before status
397
- const commands = calls.map((c) => c.args[0]);
398
- expect(commands).toContain("init");
399
- });
400
- });
401
- describe("auto-reinitialize", () => {
402
- it("re-initializes when git command returns 'not a git repository'", async () => {
403
- let callCount = 0;
404
- const warnCalls = [];
405
- const writtenFiles = new Map();
406
- const mockLogger = {
407
- debug: vi.fn(),
408
- warn: (...args) => {
409
- warnCalls.push(args);
410
- },
411
- };
412
- // Start initialized so init() succeeds, but then fail on first status
413
- // command from checkDirty with "not a git repository"
414
- let removeDirCalled = false;
415
- const execGit = async (args) => {
416
- callCount++;
417
- // First status check for init — succeed (pretend repo exists)
418
- if (args[0] === "status" && callCount === 1) {
419
- return ok("");
420
- }
421
- // Second status check from checkDirty via execWithReinit — fail
422
- if (args[0] === "status" && callCount === 2) {
423
- return err("fatal: not a git repository (or any of the parent directories): .git");
424
- }
425
- // After reinit: status check in initRepo — fail (since we "removed" .git)
426
- if (args[0] === "status" && callCount > 2 && !removeDirCalled) {
427
- return err("fatal: not a git repository");
428
- }
429
- // Handle init after reinit
430
- if (args[0] === "init") {
431
- return ok("Initialized empty Git repository");
432
- }
433
- // Handle add, commit, etc.
434
- if (args[0] === "add")
435
- return ok("");
436
- if (args[0] === "commit")
437
- return ok("[main abc1234] Initial config snapshot");
438
- // After reinit, status works
439
- if (args[0] === "status") {
440
- return ok("");
441
- }
442
- return ok("");
443
- };
444
- const deps = {
445
- configDir: "/test/config",
446
- execGit,
447
- writeFile: async (rel, content) => {
448
- writtenFiles.set(rel, content);
449
- return ok(undefined);
450
- },
451
- removeDir: async () => {
452
- removeDirCalled = true;
453
- return ok(undefined);
454
- },
455
- logger: mockLogger,
456
- };
457
- const manager = createConfigGitManager(deps);
458
- // First call succeeds (init)
459
- const initResult = await manager.init();
460
- expect(initResult.ok).toBe(true);
461
- // checkDirty triggers the "not a git repository" error and auto-reinit
462
- const dirtyResult = await manager.checkDirty();
463
- // Should have logged warning
464
- expect(warnCalls.length).toBeGreaterThan(0);
465
- const warnObj = warnCalls[0];
466
- expect(warnObj[0].hint).toContain("re-initialized");
467
- });
468
- });
469
- describe("best-effort (never throw)", () => {
470
- it("init() returns err() on git failure, never throws", async () => {
471
- const failCommands = new Map([["init", "fatal: permission denied"]]);
472
- const { deps } = createMockDeps({ failCommands });
473
- const manager = createConfigGitManager(deps);
474
- const result = await manager.init();
475
- expect(result.ok).toBe(false);
476
- // No exception thrown — we got here
477
- });
478
- it("commit() returns err() on git failure, never throws", async () => {
479
- const failCommands = new Map([
480
- ["commit -m", "fatal: unable to create commit"],
481
- ]);
482
- const { deps, repo } = createMockDeps({
483
- preInitialized: true,
484
- failCommands,
485
- });
486
- repo.dirty = true;
487
- const manager = createConfigGitManager(deps);
488
- const result = await manager.commit({
489
- section: "test",
490
- summary: "Should fail",
491
- });
492
- expect(result.ok).toBe(false);
493
- if (!result.ok) {
494
- expect(result.error).toContain("unable to create commit");
495
- }
496
- });
497
- it("checkDirty() returns err() on git failure, never throws", async () => {
498
- const failCommands = new Map([
499
- ["status --porcelain", "fatal: bad config"],
500
- ]);
501
- const { deps } = createMockDeps({
502
- preInitialized: true,
503
- failCommands,
504
- });
505
- const manager = createConfigGitManager(deps);
506
- const result = await manager.checkDirty();
507
- expect(result.ok).toBe(false);
508
- });
509
- });
510
- describe("parseCommitMessage()", () => {
511
- it("extracts summary from 'config: ' prefix", () => {
512
- const body = "config: Updated model\n\n[section] agent";
513
- const meta = parseCommitMessage(body);
514
- expect(meta.summary).toBe("Updated model");
515
- });
516
- it("extracts all metadata tags", () => {
517
- const body = [
518
- "config: Full metadata",
519
- "",
520
- "[section] agent",
521
- "[key] agent.model",
522
- "[agent] assistant-1",
523
- "[user] admin",
524
- "[trace] trace-123",
525
- ].join("\n");
526
- const meta = parseCommitMessage(body);
527
- expect(meta.section).toBe("agent");
528
- expect(meta.key).toBe("agent.model");
529
- expect(meta.agent).toBe("assistant-1");
530
- expect(meta.user).toBe("admin");
531
- expect(meta.traceId).toBe("trace-123");
532
- });
533
- it("uses 'unknown' section when no [section] tag found", () => {
534
- const meta = parseCommitMessage("Initial config snapshot");
535
- expect(meta.section).toBe("unknown");
536
- });
537
- it("handles commit messages without 'config: ' prefix", () => {
538
- const meta = parseCommitMessage("Initial config snapshot");
539
- expect(meta.summary).toBe("Initial config snapshot");
540
- });
541
- });
542
- // -----------------------------------------------------------------------
543
- // Tests: Task 2 — history, diff, rollback
544
- // -----------------------------------------------------------------------
545
- describe("history()", () => {
546
- it("returns up to 10 entries by default, newest first", async () => {
547
- const { deps, repo } = createMockDeps({ preInitialized: true });
548
- // Create several commits
549
- for (let i = 1; i <= 5; i++) {
550
- repo.commits.push({
551
- sha: createSha(i),
552
- message: `config: Change ${i}\n\n[section] agent`,
553
- timestamp: `2026-02-25T1${i}:00:00+00:00`,
554
- files: new Map(),
555
- });
556
- }
557
- const manager = createConfigGitManager(deps);
558
- const result = await manager.history();
559
- expect(result.ok).toBe(true);
560
- if (result.ok) {
561
- // Should include the initial commit + 5 added = 6 total
562
- expect(result.value.length).toBe(6);
563
- // Newest first (reversed in our mock)
564
- expect(result.value[0].metadata.summary).toBe("Change 5");
565
- }
566
- });
567
- it("respects limit option", async () => {
568
- const { deps, repo } = createMockDeps({ preInitialized: true });
569
- for (let i = 1; i <= 10; i++) {
570
- repo.commits.push({
571
- sha: createSha(i),
572
- message: `config: Change ${i}\n\n[section] agent`,
573
- timestamp: `2026-02-25T1${i}:00:00+00:00`,
574
- files: new Map(),
575
- });
576
- }
577
- const manager = createConfigGitManager(deps);
578
- const result = await manager.history({ limit: 5 });
579
- expect(result.ok).toBe(true);
580
- if (result.ok) {
581
- expect(result.value.length).toBe(5);
582
- }
583
- });
584
- it("filters by section when specified", async () => {
585
- const { deps, repo } = createMockDeps({ preInitialized: true });
586
- repo.commits.push({
587
- sha: createSha(1),
588
- message: "config: Agent change\n\n[section] agent",
589
- timestamp: "2026-02-25T11:00:00+00:00",
590
- files: new Map(),
591
- });
592
- repo.commits.push({
593
- sha: createSha(2),
594
- message: "config: Gateway change\n\n[section] gateway",
595
- timestamp: "2026-02-25T12:00:00+00:00",
596
- files: new Map(),
597
- });
598
- repo.commits.push({
599
- sha: createSha(3),
600
- message: "config: Another agent change\n\n[section] agent",
601
- timestamp: "2026-02-25T13:00:00+00:00",
602
- files: new Map(),
603
- });
604
- const manager = createConfigGitManager(deps);
605
- const result = await manager.history({ section: "agent" });
606
- expect(result.ok).toBe(true);
607
- if (result.ok) {
608
- // Only agent entries (not gateway, not initial which has section "unknown")
609
- expect(result.value.every((e) => e.metadata.section === "agent")).toBe(true);
610
- expect(result.value.length).toBe(2);
611
- }
612
- });
613
- it("returns ok([]) on empty repo (no commits besides initial)", async () => {
614
- // Create a mock where log returns "does not have any commits"
615
- const failCommands = new Map([
616
- [
617
- "log",
618
- "fatal: your current branch 'main' does not have any commits yet",
619
- ],
620
- ]);
621
- const { deps } = createMockDeps({
622
- preInitialized: true,
623
- failCommands,
624
- });
625
- const manager = createConfigGitManager(deps);
626
- const result = await manager.history();
627
- expect(result.ok).toBe(true);
628
- if (result.ok) {
629
- expect(result.value).toEqual([]);
630
- }
631
- });
632
- it("parses structured metadata from commit messages", async () => {
633
- const { deps, repo } = createMockDeps({ preInitialized: true });
634
- repo.commits.push({
635
- sha: createSha(1),
636
- message: "config: Updated model\n\n[section] agent\n[key] agent.model\n[agent] assistant-1\n[user] admin\n[trace] trace-abc",
637
- timestamp: "2026-02-25T11:00:00+00:00",
638
- files: new Map(),
639
- });
640
- const manager = createConfigGitManager(deps);
641
- const result = await manager.history();
642
- expect(result.ok).toBe(true);
643
- if (result.ok) {
644
- const entry = result.value[0];
645
- expect(entry.metadata.section).toBe("agent");
646
- expect(entry.metadata.key).toBe("agent.model");
647
- expect(entry.metadata.agent).toBe("assistant-1");
648
- expect(entry.metadata.user).toBe("admin");
649
- expect(entry.metadata.traceId).toBe("trace-abc");
650
- expect(entry.metadata.summary).toBe("Updated model");
651
- }
652
- });
653
- });
654
- describe("diff()", () => {
655
- it("returns unified diff of last commit vs previous when no SHA given", async () => {
656
- const { deps, repo } = createMockDeps({ preInitialized: true });
657
- // Add a second commit so HEAD~1 exists
658
- repo.commits.push({
659
- sha: createSha(1),
660
- message: "config: Second commit",
661
- timestamp: "2026-02-25T11:00:00+00:00",
662
- files: new Map(),
663
- });
664
- const manager = createConfigGitManager(deps);
665
- const result = await manager.diff();
666
- expect(result.ok).toBe(true);
667
- if (result.ok) {
668
- // Mock returns a unified diff format
669
- expect(result.value).toContain("---");
670
- expect(result.value).toContain("+++");
671
- expect(result.value).toContain("@@");
672
- }
673
- });
674
- it("returns diff from specified SHA to HEAD", async () => {
675
- const { deps, repo, calls } = createMockDeps({ preInitialized: true });
676
- repo.commits.push({
677
- sha: createSha(1),
678
- message: "config: Second commit",
679
- timestamp: "2026-02-25T11:00:00+00:00",
680
- files: new Map(),
681
- });
682
- const manager = createConfigGitManager(deps);
683
- const targetSha = createSha(0);
684
- const result = await manager.diff(targetSha);
685
- expect(result.ok).toBe(true);
686
- // Verify diff was called with the specified SHA
687
- const diffCall = calls.find((c) => c.args[0] === "diff" && c.args.some((a) => a.includes(targetSha)));
688
- expect(diffCall).toBeDefined();
689
- });
690
- it("returns ok('') on single-commit repo (no previous)", async () => {
691
- const { deps } = createMockDeps({ preInitialized: true });
692
- const manager = createConfigGitManager(deps);
693
- const result = await manager.diff();
694
- expect(result.ok).toBe(true);
695
- if (result.ok) {
696
- expect(result.value).toBe("");
697
- }
698
- });
699
- it("uses -U3 for 3 context lines", async () => {
700
- const { deps, repo, calls } = createMockDeps({ preInitialized: true });
701
- repo.commits.push({
702
- sha: createSha(1),
703
- message: "config: Second commit",
704
- timestamp: "2026-02-25T11:00:00+00:00",
705
- files: new Map(),
706
- });
707
- const manager = createConfigGitManager(deps);
708
- await manager.diff();
709
- const diffCall = calls.find((c) => c.args[0] === "diff");
710
- expect(diffCall).toBeDefined();
711
- expect(diffCall.args).toContain("-U3");
712
- });
713
- });
714
- describe("rollback()", () => {
715
- it("restores files from target SHA and creates forward commit", async () => {
716
- const { deps, repo, calls } = createMockDeps({ preInitialized: true });
717
- // Add commits with different file states
718
- const filesV1 = new Map([["config.yaml", "version: 1"]]);
719
- repo.commits.push({
720
- sha: createSha(1),
721
- message: "config: Version 1\n\n[section] agent",
722
- timestamp: "2026-02-25T11:00:00+00:00",
723
- files: filesV1,
724
- });
725
- const filesV2 = new Map([["config.yaml", "version: 2"]]);
726
- repo.commits.push({
727
- sha: createSha(2),
728
- message: "config: Version 2\n\n[section] agent",
729
- timestamp: "2026-02-25T12:00:00+00:00",
730
- files: filesV2,
731
- });
732
- // Also set working tree to v2
733
- repo.workingTree.set("config.yaml", "version: 2");
734
- const manager = createConfigGitManager(deps);
735
- const targetSha = createSha(1);
736
- const result = await manager.rollback(targetSha);
737
- expect(result.ok).toBe(true);
738
- if (result.ok) {
739
- expect(result.value).toBeTruthy();
740
- }
741
- // Verify checkout was called with the target SHA
742
- const checkoutCall = calls.find((c) => c.args[0] === "checkout" && c.args[1] === targetSha);
743
- expect(checkoutCall).toBeDefined();
744
- // Verify a forward commit was created (not a reset)
745
- const commitCalls = calls.filter((c) => c.args[0] === "commit" && !c.args.includes("--allow-empty"));
746
- expect(commitCalls.length).toBeGreaterThan(0);
747
- const rollbackCommit = commitCalls[commitCalls.length - 1];
748
- const message = rollbackCommit.args[rollbackCommit.args.indexOf("-m") + 1];
749
- expect(message).toContain("rollback to");
750
- });
751
- it("returns new commit SHA after rollback", async () => {
752
- const { deps, repo } = createMockDeps({ preInitialized: true });
753
- repo.commits.push({
754
- sha: createSha(1),
755
- message: "config: V1\n\n[section] agent",
756
- timestamp: "2026-02-25T11:00:00+00:00",
757
- files: new Map([["config.yaml", "v1"]]),
758
- });
759
- repo.workingTree.set("config.yaml", "v2");
760
- const manager = createConfigGitManager(deps);
761
- const result = await manager.rollback(createSha(1));
762
- expect(result.ok).toBe(true);
763
- if (result.ok) {
764
- // Should return a SHA (new forward commit)
765
- expect(result.value.length).toBeGreaterThan(0);
766
- }
767
- });
768
- it("returns err() with invalid SHA", async () => {
769
- const { deps } = createMockDeps({ preInitialized: true });
770
- const manager = createConfigGitManager(deps);
771
- const result = await manager.rollback("invalid-sha-does-not-exist");
772
- expect(result.ok).toBe(false);
773
- if (!result.ok) {
774
- expect(result.error).toContain("Invalid SHA");
775
- }
776
- });
777
- it("preserves history after rollback (forward commit, not reset)", async () => {
778
- const { deps, repo } = createMockDeps({ preInitialized: true });
779
- const sha1 = createSha(1);
780
- repo.commits.push({
781
- sha: sha1,
782
- message: "config: V1\n\n[section] agent",
783
- timestamp: "2026-02-25T11:00:00+00:00",
784
- files: new Map([["config.yaml", "v1"]]),
785
- });
786
- const sha2 = createSha(2);
787
- repo.commits.push({
788
- sha: sha2,
789
- message: "config: V2\n\n[section] agent",
790
- timestamp: "2026-02-25T12:00:00+00:00",
791
- files: new Map([["config.yaml", "v2"]]),
792
- });
793
- repo.workingTree.set("config.yaml", "v2");
794
- const manager = createConfigGitManager(deps);
795
- await manager.rollback(sha1);
796
- // History should include all commits: initial + v1 + v2 + rollback
797
- const historyResult = await manager.history({ limit: 10 });
798
- expect(historyResult.ok).toBe(true);
799
- if (historyResult.ok) {
800
- // At least 4 commits (initial + v1 + v2 + rollback)
801
- expect(historyResult.value.length).toBeGreaterThanOrEqual(4);
802
- // The rollback commit should contain "rollback" in its summary
803
- const rollbackEntry = historyResult.value.find((e) => e.metadata.summary.includes("rollback"));
804
- expect(rollbackEntry).toBeDefined();
805
- }
806
- });
807
- it("includes [section] * in rollback commit metadata", async () => {
808
- const { deps, repo, calls } = createMockDeps({ preInitialized: true });
809
- repo.commits.push({
810
- sha: createSha(1),
811
- message: "config: V1\n\n[section] agent",
812
- timestamp: "2026-02-25T11:00:00+00:00",
813
- files: new Map([["config.yaml", "v1"]]),
814
- });
815
- repo.workingTree.set("config.yaml", "v2");
816
- const manager = createConfigGitManager(deps);
817
- await manager.rollback(createSha(1));
818
- const commitCalls = calls.filter((c) => c.args[0] === "commit" &&
819
- !c.args.includes("--allow-empty"));
820
- const rollbackCommit = commitCalls[commitCalls.length - 1];
821
- const message = rollbackCommit.args[rollbackCommit.args.indexOf("-m") + 1];
822
- expect(message).toContain("[section] *");
823
- });
824
- });
825
- // -----------------------------------------------------------------------
826
- // Tests: gc and squash (GITGC-01, GITGC-02)
827
- // -----------------------------------------------------------------------
828
- describe("gc()", () => {
829
- it("runs git garbage collection", async () => {
830
- const { deps, calls } = createMockDeps({ preInitialized: true });
831
- const manager = createConfigGitManager(deps);
832
- const result = await manager.gc();
833
- expect(result.ok).toBe(true);
834
- if (result.ok) {
835
- expect(result.value.prunedObjects).toBe(true);
836
- }
837
- // Verify gc was called with --aggressive --prune=now
838
- const gcCall = calls.find((c) => c.args[0] === "gc" &&
839
- c.args.includes("--aggressive") &&
840
- c.args.includes("--prune=now"));
841
- expect(gcCall).toBeDefined();
842
- });
843
- it("returns err on git failure", async () => {
844
- const failCommands = new Map([["gc", "fatal: gc failed"]]);
845
- const { deps } = createMockDeps({ preInitialized: true, failCommands });
846
- const manager = createConfigGitManager(deps);
847
- const result = await manager.gc();
848
- expect(result.ok).toBe(false);
849
- if (!result.ok) {
850
- expect(result.error).toContain("git gc failed");
851
- }
852
- });
853
- });
854
- // -----------------------------------------------------------------------
855
- // Tests: CORE-08 — encodeCommitMessage sanitization
856
- // -----------------------------------------------------------------------
857
- describe("encodeCommitMessage (CORE-08)", () => {
858
- it("strips newlines from metadata fields", () => {
859
- const message = encodeCommitMessage({
860
- section: "agent",
861
- summary: "line1\nline2\rline3",
862
- });
863
- expect(message).not.toContain("\nline2");
864
- expect(message).toContain("line1 line2 line3");
865
- });
866
- it("strips leading [ from metadata fields", () => {
867
- const message = encodeCommitMessage({
868
- section: "[injected] tag",
869
- summary: "normal summary",
870
- });
871
- expect(message).toContain("[section] injected] tag");
872
- expect(message).not.toContain("[section] [injected]");
873
- });
874
- it("round-trips clean metadata correctly", () => {
875
- const original = { section: "gateway", summary: "Updated host", key: "host", agent: "bot-1" };
876
- const encoded = encodeCommitMessage(original);
877
- const decoded = parseCommitMessage(encoded);
878
- expect(decoded.section).toBe("gateway");
879
- expect(decoded.summary).toBe("Updated host");
880
- expect(decoded.key).toBe("host");
881
- expect(decoded.agent).toBe("bot-1");
882
- });
883
- it("sanitizes all optional fields", () => {
884
- const message = encodeCommitMessage({
885
- section: "test",
886
- summary: "test",
887
- key: "line\none",
888
- agent: "[bad]agent",
889
- user: "user\r\ninjected",
890
- traceId: "[[trace",
891
- });
892
- expect(message).not.toMatch(/\[key\] line\n/);
893
- expect(message).toContain("[key] line one");
894
- expect(message).toContain("[agent] bad]agent");
895
- expect(message).toContain("[user] user injected");
896
- expect(message).toContain("[trace] trace");
897
- });
898
- });
899
- describe("squash()", () => {
900
- it("returns squashedCount: 0 when fewer than 2 old commits", async () => {
901
- // Use a custom execGit that returns only 1 commit for log --reverse
902
- let initDone = false;
903
- const execGit = async (args) => {
904
- if (args[0] === "status" && !initDone) {
905
- initDone = true;
906
- return ok("");
907
- }
908
- if (args[0] === "log" && args.includes("--reverse")) {
909
- return ok("abc1234567890000000000000000000000000000 2026-01-01T00:00:00+00:00\n");
910
- }
911
- return ok("");
912
- };
913
- const deps = {
914
- configDir: "/test/config",
915
- execGit,
916
- writeFile: async () => ok(undefined),
917
- };
918
- const manager = createConfigGitManager(deps);
919
- const result = await manager.squash("2099-01-01T00:00:00Z");
920
- expect(result.ok).toBe(true);
921
- if (result.ok) {
922
- expect(result.value.squashedCount).toBe(0);
923
- expect(result.value.newRootSha).toBe("");
924
- }
925
- });
926
- it("squashes old commits and rebases newer ones", async () => {
927
- let initDone = false;
928
- const calls = [];
929
- const execGit = async (args) => {
930
- calls.push({ args });
931
- if (args[0] === "status" && !initDone) {
932
- initDone = true;
933
- return ok("");
934
- }
935
- // Return 5 commits: 3 old (2025), 2 new (2026)
936
- if (args[0] === "log" && args.includes("--reverse")) {
937
- return ok([
938
- "aaa0000000000000000000000000000000000001 2025-01-01T00:00:00+00:00",
939
- "aaa0000000000000000000000000000000000002 2025-06-01T00:00:00+00:00",
940
- "aaa0000000000000000000000000000000000003 2025-12-01T00:00:00+00:00",
941
- "bbb0000000000000000000000000000000000004 2026-02-01T00:00:00+00:00",
942
- "bbb0000000000000000000000000000000000005 2026-02-15T00:00:00+00:00",
943
- ].join("\n"));
944
- }
945
- // rev-parse for tree
946
- if (args[0] === "rev-parse" && args[1]?.includes("^{tree}")) {
947
- return ok("treeSha123456");
948
- }
949
- // commit-tree
950
- if (args[0] === "commit-tree") {
951
- return ok("newRootSha789");
952
- }
953
- // symbolic-ref
954
- if (args[0] === "symbolic-ref") {
955
- return ok("main");
956
- }
957
- // rebase
958
- if (args[0] === "rebase") {
959
- return ok("Successfully rebased");
960
- }
961
- return ok("");
962
- };
963
- const deps = {
964
- configDir: "/test/config",
965
- execGit,
966
- writeFile: async () => ok(undefined),
967
- };
968
- const manager = createConfigGitManager(deps);
969
- // Squash everything older than 2026-01-01
970
- const result = await manager.squash("2026-01-01T00:00:00Z");
971
- expect(result.ok).toBe(true);
972
- if (result.ok) {
973
- expect(result.value.squashedCount).toBe(3);
974
- expect(result.value.newRootSha).toBe("newRootSha789");
975
- }
976
- // Verify rebase was called with correct args
977
- const rebaseCall = calls.find((c) => c.args[0] === "rebase");
978
- expect(rebaseCall).toBeDefined();
979
- expect(rebaseCall.args).toContain("--onto");
980
- expect(rebaseCall.args).toContain("newRootSha789");
981
- });
982
- it("returns err on invalid date", async () => {
983
- const { deps } = createMockDeps({ preInitialized: true });
984
- const manager = createConfigGitManager(deps);
985
- const result = await manager.squash("not-a-date");
986
- expect(result.ok).toBe(false);
987
- if (!result.ok) {
988
- expect(result.error).toContain("Invalid date");
989
- }
990
- });
991
- it("aborts rebase on failure and returns err", async () => {
992
- let initDone = false;
993
- const calls = [];
994
- const execGit = async (args) => {
995
- calls.push({ args });
996
- if (args[0] === "status" && !initDone) {
997
- initDone = true;
998
- return ok("");
999
- }
1000
- if (args[0] === "log" && args.includes("--reverse")) {
1001
- return ok([
1002
- "aaa0000000000000000000000000000000000001 2025-01-01T00:00:00+00:00",
1003
- "aaa0000000000000000000000000000000000002 2025-06-01T00:00:00+00:00",
1004
- "bbb0000000000000000000000000000000000003 2026-02-01T00:00:00+00:00",
1005
- ].join("\n"));
1006
- }
1007
- if (args[0] === "rev-parse" && args[1]?.includes("^{tree}")) {
1008
- return ok("treeSha123456");
1009
- }
1010
- if (args[0] === "commit-tree") {
1011
- return ok("newRootSha789");
1012
- }
1013
- if (args[0] === "symbolic-ref") {
1014
- return ok("main");
1015
- }
1016
- // Rebase fails
1017
- if (args[0] === "rebase" && !args.includes("--abort")) {
1018
- return err("CONFLICT: merge conflict in config.yaml");
1019
- }
1020
- // Rebase --abort succeeds
1021
- if (args[0] === "rebase" && args.includes("--abort")) {
1022
- return ok("Rebase aborted");
1023
- }
1024
- return ok("");
1025
- };
1026
- const deps = {
1027
- configDir: "/test/config",
1028
- execGit,
1029
- writeFile: async () => ok(undefined),
1030
- };
1031
- const manager = createConfigGitManager(deps);
1032
- const result = await manager.squash("2026-01-01T00:00:00Z");
1033
- expect(result.ok).toBe(false);
1034
- if (!result.ok) {
1035
- expect(result.error).toContain("Squash rebase failed");
1036
- }
1037
- // Verify rebase --abort was called
1038
- const abortCall = calls.find((c) => c.args[0] === "rebase" && c.args.includes("--abort"));
1039
- expect(abortCall).toBeDefined();
1040
- });
1041
- });
1042
- });