comisai 1.0.11 → 1.0.12

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 (247) hide show
  1. package/node_modules/@comis/agent/dist/executor/pi-executor.d.ts +1 -4
  2. package/node_modules/@comis/agent/package.json +1 -1
  3. package/node_modules/@comis/channels/package.json +1 -1
  4. package/node_modules/@comis/cli/package.json +1 -1
  5. package/node_modules/@comis/core/package.json +1 -1
  6. package/node_modules/@comis/daemon/package.json +1 -1
  7. package/node_modules/@comis/gateway/package.json +1 -1
  8. package/node_modules/@comis/infra/package.json +1 -1
  9. package/node_modules/@comis/memory/package.json +1 -1
  10. package/node_modules/@comis/scheduler/package.json +1 -1
  11. package/node_modules/@comis/shared/package.json +1 -1
  12. package/node_modules/@comis/skills/package.json +1 -1
  13. package/package.json +18 -18
  14. package/node_modules/@comis/core/dist/approval/approval-gate.test.d.ts +0 -1
  15. package/node_modules/@comis/core/dist/approval/approval-gate.test.js +0 -1003
  16. package/node_modules/@comis/core/dist/bootstrap.test.d.ts +0 -1
  17. package/node_modules/@comis/core/dist/bootstrap.test.js +0 -150
  18. package/node_modules/@comis/core/dist/config/backup.test.d.ts +0 -1
  19. package/node_modules/@comis/core/dist/config/backup.test.js +0 -160
  20. package/node_modules/@comis/core/dist/config/env-substitution.test.d.ts +0 -1
  21. package/node_modules/@comis/core/dist/config/env-substitution.test.js +0 -259
  22. package/node_modules/@comis/core/dist/config/field-metadata.test.d.ts +0 -1
  23. package/node_modules/@comis/core/dist/config/field-metadata.test.js +0 -72
  24. package/node_modules/@comis/core/dist/config/git-manager.test.d.ts +0 -1
  25. package/node_modules/@comis/core/dist/config/git-manager.test.js +0 -1042
  26. package/node_modules/@comis/core/dist/config/immutable-keys.test.d.ts +0 -1
  27. package/node_modules/@comis/core/dist/config/immutable-keys.test.js +0 -283
  28. package/node_modules/@comis/core/dist/config/include-resolver.test.d.ts +0 -1
  29. package/node_modules/@comis/core/dist/config/include-resolver.test.js +0 -292
  30. package/node_modules/@comis/core/dist/config/layered.test.d.ts +0 -1
  31. package/node_modules/@comis/core/dist/config/layered.test.js +0 -211
  32. package/node_modules/@comis/core/dist/config/loader.test.d.ts +0 -1
  33. package/node_modules/@comis/core/dist/config/loader.test.js +0 -300
  34. package/node_modules/@comis/core/dist/config/migrate.test.d.ts +0 -1
  35. package/node_modules/@comis/core/dist/config/migrate.test.js +0 -244
  36. package/node_modules/@comis/core/dist/config/partial-validator.test.d.ts +0 -1
  37. package/node_modules/@comis/core/dist/config/partial-validator.test.js +0 -98
  38. package/node_modules/@comis/core/dist/config/schema-agent-model.test.d.ts +0 -1
  39. package/node_modules/@comis/core/dist/config/schema-agent-model.test.js +0 -279
  40. package/node_modules/@comis/core/dist/config/schema-agent-session.test.d.ts +0 -1
  41. package/node_modules/@comis/core/dist/config/schema-agent-session.test.js +0 -242
  42. package/node_modules/@comis/core/dist/config/schema-agent.test.d.ts +0 -1
  43. package/node_modules/@comis/core/dist/config/schema-agent.test.js +0 -645
  44. package/node_modules/@comis/core/dist/config/schema-channel.test.d.ts +0 -1
  45. package/node_modules/@comis/core/dist/config/schema-channel.test.js +0 -341
  46. package/node_modules/@comis/core/dist/config/schema-coalescer.test.d.ts +0 -1
  47. package/node_modules/@comis/core/dist/config/schema-coalescer.test.js +0 -51
  48. package/node_modules/@comis/core/dist/config/schema-context-engine.test.d.ts +0 -1
  49. package/node_modules/@comis/core/dist/config/schema-context-engine.test.js +0 -797
  50. package/node_modules/@comis/core/dist/config/schema-context-guard.test.d.ts +0 -1
  51. package/node_modules/@comis/core/dist/config/schema-context-guard.test.js +0 -111
  52. package/node_modules/@comis/core/dist/config/schema-daemon.test.d.ts +0 -1
  53. package/node_modules/@comis/core/dist/config/schema-daemon.test.js +0 -107
  54. package/node_modules/@comis/core/dist/config/schema-delivery-timing.test.d.ts +0 -1
  55. package/node_modules/@comis/core/dist/config/schema-delivery-timing.test.js +0 -51
  56. package/node_modules/@comis/core/dist/config/schema-documentation.test.d.ts +0 -1
  57. package/node_modules/@comis/core/dist/config/schema-documentation.test.js +0 -63
  58. package/node_modules/@comis/core/dist/config/schema-gateway.test.d.ts +0 -1
  59. package/node_modules/@comis/core/dist/config/schema-gateway.test.js +0 -219
  60. package/node_modules/@comis/core/dist/config/schema-gemini-cache.test.d.ts +0 -1
  61. package/node_modules/@comis/core/dist/config/schema-gemini-cache.test.js +0 -41
  62. package/node_modules/@comis/core/dist/config/schema-integrations.test.d.ts +0 -1
  63. package/node_modules/@comis/core/dist/config/schema-integrations.test.js +0 -92
  64. package/node_modules/@comis/core/dist/config/schema-lifecycle-reactions.test.d.ts +0 -1
  65. package/node_modules/@comis/core/dist/config/schema-lifecycle-reactions.test.js +0 -61
  66. package/node_modules/@comis/core/dist/config/schema-misc.test.d.ts +0 -1
  67. package/node_modules/@comis/core/dist/config/schema-misc.test.js +0 -394
  68. package/node_modules/@comis/core/dist/config/schema-observability.test.d.ts +0 -1
  69. package/node_modules/@comis/core/dist/config/schema-observability.test.js +0 -76
  70. package/node_modules/@comis/core/dist/config/schema-providers.test.d.ts +0 -1
  71. package/node_modules/@comis/core/dist/config/schema-providers.test.js +0 -294
  72. package/node_modules/@comis/core/dist/config/schema-queue.test.d.ts +0 -1
  73. package/node_modules/@comis/core/dist/config/schema-queue.test.js +0 -234
  74. package/node_modules/@comis/core/dist/config/schema-response-prefix.test.d.ts +0 -1
  75. package/node_modules/@comis/core/dist/config/schema-response-prefix.test.js +0 -36
  76. package/node_modules/@comis/core/dist/config/schema-security.test.d.ts +0 -1
  77. package/node_modules/@comis/core/dist/config/schema-security.test.js +0 -54
  78. package/node_modules/@comis/core/dist/config/schema-sender-trust-display.test.d.ts +0 -1
  79. package/node_modules/@comis/core/dist/config/schema-sender-trust-display.test.js +0 -60
  80. package/node_modules/@comis/core/dist/config/schema-serializer.test.d.ts +0 -1
  81. package/node_modules/@comis/core/dist/config/schema-serializer.test.js +0 -73
  82. package/node_modules/@comis/core/dist/config/schema-telegram-file-guard.test.d.ts +0 -1
  83. package/node_modules/@comis/core/dist/config/schema-telegram-file-guard.test.js +0 -39
  84. package/node_modules/@comis/core/dist/context/context.test.d.ts +0 -1
  85. package/node_modules/@comis/core/dist/context/context.test.js +0 -276
  86. package/node_modules/@comis/core/dist/domain/agent-response.test.d.ts +0 -1
  87. package/node_modules/@comis/core/dist/domain/agent-response.test.js +0 -159
  88. package/node_modules/@comis/core/dist/domain/credential-mapping.test.d.ts +0 -1
  89. package/node_modules/@comis/core/dist/domain/credential-mapping.test.js +0 -135
  90. package/node_modules/@comis/core/dist/domain/delivery-origin.test.d.ts +0 -1
  91. package/node_modules/@comis/core/dist/domain/delivery-origin.test.js +0 -68
  92. package/node_modules/@comis/core/dist/domain/execution-graph.test.d.ts +0 -1
  93. package/node_modules/@comis/core/dist/domain/execution-graph.test.js +0 -441
  94. package/node_modules/@comis/core/dist/domain/memory-entry.test.d.ts +0 -1
  95. package/node_modules/@comis/core/dist/domain/memory-entry.test.js +0 -193
  96. package/node_modules/@comis/core/dist/domain/normalized-message.test.d.ts +0 -1
  97. package/node_modules/@comis/core/dist/domain/normalized-message.test.js +0 -255
  98. package/node_modules/@comis/core/dist/domain/session-key.test.d.ts +0 -1
  99. package/node_modules/@comis/core/dist/domain/session-key.test.js +0 -291
  100. package/node_modules/@comis/core/dist/domain/subagent-context-config.test.d.ts +0 -1
  101. package/node_modules/@comis/core/dist/domain/subagent-context-config.test.js +0 -131
  102. package/node_modules/@comis/core/dist/domain/subagent-context-types.test.d.ts +0 -1
  103. package/node_modules/@comis/core/dist/domain/subagent-context-types.test.js +0 -182
  104. package/node_modules/@comis/core/dist/event-bus/bus.test.d.ts +0 -1
  105. package/node_modules/@comis/core/dist/event-bus/bus.test.js +0 -152
  106. package/node_modules/@comis/core/dist/event-bus/events-agent.test.d.ts +0 -1
  107. package/node_modules/@comis/core/dist/event-bus/events-agent.test.js +0 -409
  108. package/node_modules/@comis/core/dist/event-bus/events-channel.test.d.ts +0 -1
  109. package/node_modules/@comis/core/dist/event-bus/events-channel.test.js +0 -240
  110. package/node_modules/@comis/core/dist/event-bus/events-infra.test.d.ts +0 -1
  111. package/node_modules/@comis/core/dist/event-bus/events-infra.test.js +0 -416
  112. package/node_modules/@comis/core/dist/event-bus/events-messaging.test.d.ts +0 -1
  113. package/node_modules/@comis/core/dist/event-bus/events-messaging.test.js +0 -433
  114. package/node_modules/@comis/core/dist/event-bus/events.test.d.ts +0 -1
  115. package/node_modules/@comis/core/dist/event-bus/events.test.js +0 -93
  116. package/node_modules/@comis/core/dist/hooks/hook-runner-global.test.d.ts +0 -1
  117. package/node_modules/@comis/core/dist/hooks/hook-runner-global.test.js +0 -29
  118. package/node_modules/@comis/core/dist/hooks/hook-runner.test.d.ts +0 -1
  119. package/node_modules/@comis/core/dist/hooks/hook-runner.test.js +0 -490
  120. package/node_modules/@comis/core/dist/hooks/hook-strategies.test.d.ts +0 -1
  121. package/node_modules/@comis/core/dist/hooks/hook-strategies.test.js +0 -209
  122. package/node_modules/@comis/core/dist/hooks/integration.test.d.ts +0 -1
  123. package/node_modules/@comis/core/dist/hooks/integration.test.js +0 -235
  124. package/node_modules/@comis/core/dist/hooks/plugin-registry.test.d.ts +0 -1
  125. package/node_modules/@comis/core/dist/hooks/plugin-registry.test.js +0 -470
  126. package/node_modules/@comis/core/dist/load-env.test.d.ts +0 -1
  127. package/node_modules/@comis/core/dist/load-env.test.js +0 -21
  128. package/node_modules/@comis/core/dist/security/action-classifier.test.d.ts +0 -1
  129. package/node_modules/@comis/core/dist/security/action-classifier.test.js +0 -298
  130. package/node_modules/@comis/core/dist/security/audit-aggregator.test.d.ts +0 -1
  131. package/node_modules/@comis/core/dist/security/audit-aggregator.test.js +0 -128
  132. package/node_modules/@comis/core/dist/security/audit.test.d.ts +0 -1
  133. package/node_modules/@comis/core/dist/security/audit.test.js +0 -154
  134. package/node_modules/@comis/core/dist/security/canary-token.test.d.ts +0 -1
  135. package/node_modules/@comis/core/dist/security/canary-token.test.js +0 -45
  136. package/node_modules/@comis/core/dist/security/config-redaction.test.d.ts +0 -1
  137. package/node_modules/@comis/core/dist/security/config-redaction.test.js +0 -107
  138. package/node_modules/@comis/core/dist/security/external-content.test.d.ts +0 -1
  139. package/node_modules/@comis/core/dist/security/external-content.test.js +0 -210
  140. package/node_modules/@comis/core/dist/security/injection-patterns.test.d.ts +0 -1
  141. package/node_modules/@comis/core/dist/security/injection-patterns.test.js +0 -213
  142. package/node_modules/@comis/core/dist/security/injection-rate-limiter.test.d.ts +0 -1
  143. package/node_modules/@comis/core/dist/security/injection-rate-limiter.test.js +0 -194
  144. package/node_modules/@comis/core/dist/security/input-guard.test.d.ts +0 -1
  145. package/node_modules/@comis/core/dist/security/input-guard.test.js +0 -215
  146. package/node_modules/@comis/core/dist/security/input-security-guard.test.d.ts +0 -1
  147. package/node_modules/@comis/core/dist/security/input-security-guard.test.js +0 -215
  148. package/node_modules/@comis/core/dist/security/input-validator.test.d.ts +0 -1
  149. package/node_modules/@comis/core/dist/security/input-validator.test.js +0 -133
  150. package/node_modules/@comis/core/dist/security/log-sanitizer.test.d.ts +0 -1
  151. package/node_modules/@comis/core/dist/security/log-sanitizer.test.js +0 -260
  152. package/node_modules/@comis/core/dist/security/memory-write-validator.test.d.ts +0 -1
  153. package/node_modules/@comis/core/dist/security/memory-write-validator.test.js +0 -62
  154. package/node_modules/@comis/core/dist/security/output-guard.test.d.ts +0 -1
  155. package/node_modules/@comis/core/dist/security/output-guard.test.js +0 -397
  156. package/node_modules/@comis/core/dist/security/safe-path.test.d.ts +0 -1
  157. package/node_modules/@comis/core/dist/security/safe-path.test.js +0 -95
  158. package/node_modules/@comis/core/dist/security/scoped-secret-manager.test.d.ts +0 -1
  159. package/node_modules/@comis/core/dist/security/scoped-secret-manager.test.js +0 -231
  160. package/node_modules/@comis/core/dist/security/secret-access.test.d.ts +0 -1
  161. package/node_modules/@comis/core/dist/security/secret-access.test.js +0 -41
  162. package/node_modules/@comis/core/dist/security/secret-crypto.test.d.ts +0 -1
  163. package/node_modules/@comis/core/dist/security/secret-crypto.test.js +0 -113
  164. package/node_modules/@comis/core/dist/security/secret-manager.test.d.ts +0 -1
  165. package/node_modules/@comis/core/dist/security/secret-manager.test.js +0 -394
  166. package/node_modules/@comis/core/dist/security/secret-ref-resolver.test.d.ts +0 -1
  167. package/node_modules/@comis/core/dist/security/secret-ref-resolver.test.js +0 -268
  168. package/node_modules/@comis/core/dist/security/secrets-audit.test.d.ts +0 -10
  169. package/node_modules/@comis/core/dist/security/secrets-audit.test.js +0 -295
  170. package/node_modules/@comis/core/dist/security/ssrf-guard.test.d.ts +0 -1
  171. package/node_modules/@comis/core/dist/security/ssrf-guard.test.js +0 -127
  172. package/node_modules/@comis/core/dist/security/token-generator.test.d.ts +0 -1
  173. package/node_modules/@comis/core/dist/security/token-generator.test.js +0 -35
  174. package/node_modules/@comis/core/dist/tool-metadata.test.d.ts +0 -1
  175. package/node_modules/@comis/core/dist/tool-metadata.test.js +0 -112
  176. package/node_modules/@comis/memory/dist/compaction.test.d.ts +0 -1
  177. package/node_modules/@comis/memory/dist/compaction.test.js +0 -298
  178. package/node_modules/@comis/memory/dist/context-schema.test.d.ts +0 -1
  179. package/node_modules/@comis/memory/dist/context-schema.test.js +0 -246
  180. package/node_modules/@comis/memory/dist/context-store.test.d.ts +0 -1
  181. package/node_modules/@comis/memory/dist/context-store.test.js +0 -708
  182. package/node_modules/@comis/memory/dist/credential-mapping-schema.test.d.ts +0 -7
  183. package/node_modules/@comis/memory/dist/credential-mapping-schema.test.js +0 -73
  184. package/node_modules/@comis/memory/dist/credential-mapping-store.test.d.ts +0 -8
  185. package/node_modules/@comis/memory/dist/credential-mapping-store.test.js +0 -340
  186. package/node_modules/@comis/memory/dist/delivery-mirror-adapter.test.d.ts +0 -1
  187. package/node_modules/@comis/memory/dist/delivery-mirror-adapter.test.js +0 -165
  188. package/node_modules/@comis/memory/dist/delivery-queue-adapter.test.d.ts +0 -1
  189. package/node_modules/@comis/memory/dist/delivery-queue-adapter.test.js +0 -263
  190. package/node_modules/@comis/memory/dist/embedding-batch-indexer.test.d.ts +0 -1
  191. package/node_modules/@comis/memory/dist/embedding-batch-indexer.test.js +0 -202
  192. package/node_modules/@comis/memory/dist/embedding-cache-lru.test.d.ts +0 -1
  193. package/node_modules/@comis/memory/dist/embedding-cache-lru.test.js +0 -241
  194. package/node_modules/@comis/memory/dist/embedding-cache-sqlite.test.d.ts +0 -8
  195. package/node_modules/@comis/memory/dist/embedding-cache-sqlite.test.js +0 -678
  196. package/node_modules/@comis/memory/dist/embedding-cache.test.d.ts +0 -1
  197. package/node_modules/@comis/memory/dist/embedding-cache.test.js +0 -213
  198. package/node_modules/@comis/memory/dist/embedding-fingerprint.test.d.ts +0 -1
  199. package/node_modules/@comis/memory/dist/embedding-fingerprint.test.js +0 -87
  200. package/node_modules/@comis/memory/dist/embedding-hash.test.d.ts +0 -1
  201. package/node_modules/@comis/memory/dist/embedding-hash.test.js +0 -31
  202. package/node_modules/@comis/memory/dist/embedding-integration.test.d.ts +0 -8
  203. package/node_modules/@comis/memory/dist/embedding-integration.test.js +0 -232
  204. package/node_modules/@comis/memory/dist/embedding-provider-factory.test.d.ts +0 -1
  205. package/node_modules/@comis/memory/dist/embedding-provider-factory.test.js +0 -176
  206. package/node_modules/@comis/memory/dist/embedding-provider-local.test.d.ts +0 -8
  207. package/node_modules/@comis/memory/dist/embedding-provider-local.test.js +0 -76
  208. package/node_modules/@comis/memory/dist/embedding-provider-openai.test.d.ts +0 -8
  209. package/node_modules/@comis/memory/dist/embedding-provider-openai.test.js +0 -100
  210. package/node_modules/@comis/memory/dist/embedding-queue.test.d.ts +0 -1
  211. package/node_modules/@comis/memory/dist/embedding-queue.test.js +0 -236
  212. package/node_modules/@comis/memory/dist/hybrid-search.test.d.ts +0 -1
  213. package/node_modules/@comis/memory/dist/hybrid-search.test.js +0 -476
  214. package/node_modules/@comis/memory/dist/identity-link-store.test.d.ts +0 -1
  215. package/node_modules/@comis/memory/dist/identity-link-store.test.js +0 -88
  216. package/node_modules/@comis/memory/dist/memory-api.test.d.ts +0 -1
  217. package/node_modules/@comis/memory/dist/memory-api.test.js +0 -495
  218. package/node_modules/@comis/memory/dist/memory-concurrency.test.d.ts +0 -20
  219. package/node_modules/@comis/memory/dist/memory-concurrency.test.js +0 -222
  220. package/node_modules/@comis/memory/dist/named-graph-store.test.d.ts +0 -1
  221. package/node_modules/@comis/memory/dist/named-graph-store.test.js +0 -227
  222. package/node_modules/@comis/memory/dist/observability-store.test.d.ts +0 -1
  223. package/node_modules/@comis/memory/dist/observability-store.test.js +0 -704
  224. package/node_modules/@comis/memory/dist/row-mapper.test.d.ts +0 -1
  225. package/node_modules/@comis/memory/dist/row-mapper.test.js +0 -409
  226. package/node_modules/@comis/memory/dist/schema.test.d.ts +0 -1
  227. package/node_modules/@comis/memory/dist/schema.test.js +0 -240
  228. package/node_modules/@comis/memory/dist/secret-store-schema.test.d.ts +0 -7
  229. package/node_modules/@comis/memory/dist/secret-store-schema.test.js +0 -90
  230. package/node_modules/@comis/memory/dist/session-store.test.d.ts +0 -1
  231. package/node_modules/@comis/memory/dist/session-store.test.js +0 -262
  232. package/node_modules/@comis/memory/dist/setup-secrets.test.d.ts +0 -1
  233. package/node_modules/@comis/memory/dist/setup-secrets.test.js +0 -140
  234. package/node_modules/@comis/memory/dist/sqlite-memory-adapter.test.d.ts +0 -1
  235. package/node_modules/@comis/memory/dist/sqlite-memory-adapter.test.js +0 -888
  236. package/node_modules/@comis/memory/dist/sqlite-secret-store.test.d.ts +0 -8
  237. package/node_modules/@comis/memory/dist/sqlite-secret-store.test.js +0 -245
  238. package/node_modules/@comis/shared/dist/abort.test.d.ts +0 -1
  239. package/node_modules/@comis/shared/dist/abort.test.js +0 -71
  240. package/node_modules/@comis/shared/dist/result.test.d.ts +0 -1
  241. package/node_modules/@comis/shared/dist/result.test.js +0 -178
  242. package/node_modules/@comis/shared/dist/suppress-error.test.d.ts +0 -1
  243. package/node_modules/@comis/shared/dist/suppress-error.test.js +0 -50
  244. package/node_modules/@comis/shared/dist/timeout.test.d.ts +0 -1
  245. package/node_modules/@comis/shared/dist/timeout.test.js +0 -75
  246. package/node_modules/@comis/shared/dist/ttl-cache.test.d.ts +0 -1
  247. 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
- });