memory-journal-mcp 4.4.2 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (291) hide show
  1. package/.github/workflows/codeql.yml +1 -6
  2. package/.github/workflows/docker-publish.yml +15 -49
  3. package/.github/workflows/lint-and-test.yml +1 -1
  4. package/.github/workflows/secrets-scanning.yml +4 -3
  5. package/.github/workflows/security-update.yml +3 -3
  6. package/CHANGELOG.md +213 -0
  7. package/CONTRIBUTING.md +132 -97
  8. package/DOCKER_README.md +184 -235
  9. package/Dockerfile +27 -24
  10. package/README.md +218 -190
  11. package/SECURITY.md +27 -35
  12. package/dist/cli.js +16 -1
  13. package/dist/cli.js.map +1 -1
  14. package/dist/constants/ServerInstructions.d.ts +5 -1
  15. package/dist/constants/ServerInstructions.d.ts.map +1 -1
  16. package/dist/constants/ServerInstructions.js +133 -73
  17. package/dist/constants/ServerInstructions.js.map +1 -1
  18. package/dist/constants/icons.d.ts +2 -2
  19. package/dist/constants/icons.d.ts.map +1 -1
  20. package/dist/constants/icons.js +7 -6
  21. package/dist/constants/icons.js.map +1 -1
  22. package/dist/database/SqliteAdapter.d.ts +37 -24
  23. package/dist/database/SqliteAdapter.d.ts.map +1 -1
  24. package/dist/database/SqliteAdapter.js +319 -157
  25. package/dist/database/SqliteAdapter.js.map +1 -1
  26. package/dist/database/schema.d.ts +45 -0
  27. package/dist/database/schema.d.ts.map +1 -0
  28. package/dist/database/schema.js +92 -0
  29. package/dist/database/schema.js.map +1 -0
  30. package/dist/filtering/ToolFilter.d.ts +1 -1
  31. package/dist/filtering/ToolFilter.d.ts.map +1 -1
  32. package/dist/filtering/ToolFilter.js +13 -2
  33. package/dist/filtering/ToolFilter.js.map +1 -1
  34. package/dist/github/GitHubIntegration.d.ts.map +1 -1
  35. package/dist/github/GitHubIntegration.js +1 -3
  36. package/dist/github/GitHubIntegration.js.map +1 -1
  37. package/dist/handlers/prompts/github.d.ts +12 -0
  38. package/dist/handlers/prompts/github.d.ts.map +1 -0
  39. package/dist/handlers/prompts/github.js +178 -0
  40. package/dist/handlers/prompts/github.js.map +1 -0
  41. package/dist/handlers/prompts/index.d.ts +23 -2
  42. package/dist/handlers/prompts/index.d.ts.map +1 -1
  43. package/dist/handlers/prompts/index.js +7 -432
  44. package/dist/handlers/prompts/index.js.map +1 -1
  45. package/dist/handlers/prompts/workflow.d.ts +12 -0
  46. package/dist/handlers/prompts/workflow.d.ts.map +1 -0
  47. package/dist/handlers/prompts/workflow.js +277 -0
  48. package/dist/handlers/prompts/workflow.js.map +1 -0
  49. package/dist/handlers/resources/core.d.ts +11 -0
  50. package/dist/handlers/resources/core.d.ts.map +1 -0
  51. package/dist/handlers/resources/core.js +433 -0
  52. package/dist/handlers/resources/core.js.map +1 -0
  53. package/dist/handlers/resources/github.d.ts +11 -0
  54. package/dist/handlers/resources/github.d.ts.map +1 -0
  55. package/dist/handlers/resources/github.js +314 -0
  56. package/dist/handlers/resources/github.js.map +1 -0
  57. package/dist/handlers/resources/graph.d.ts +11 -0
  58. package/dist/handlers/resources/graph.d.ts.map +1 -0
  59. package/dist/handlers/resources/graph.js +204 -0
  60. package/dist/handlers/resources/graph.js.map +1 -0
  61. package/dist/handlers/resources/index.d.ts +5 -20
  62. package/dist/handlers/resources/index.d.ts.map +1 -1
  63. package/dist/handlers/resources/index.js +16 -1278
  64. package/dist/handlers/resources/index.js.map +1 -1
  65. package/dist/handlers/resources/shared.d.ts +60 -0
  66. package/dist/handlers/resources/shared.d.ts.map +1 -0
  67. package/dist/handlers/resources/shared.js +49 -0
  68. package/dist/handlers/resources/shared.js.map +1 -0
  69. package/dist/handlers/resources/team.d.ts +13 -0
  70. package/dist/handlers/resources/team.d.ts.map +1 -0
  71. package/dist/handlers/resources/team.js +119 -0
  72. package/dist/handlers/resources/team.js.map +1 -0
  73. package/dist/handlers/resources/templates.d.ts +13 -0
  74. package/dist/handlers/resources/templates.d.ts.map +1 -0
  75. package/dist/handlers/resources/templates.js +310 -0
  76. package/dist/handlers/resources/templates.js.map +1 -0
  77. package/dist/handlers/tools/admin.d.ts +8 -0
  78. package/dist/handlers/tools/admin.d.ts.map +1 -0
  79. package/dist/handlers/tools/admin.js +270 -0
  80. package/dist/handlers/tools/admin.js.map +1 -0
  81. package/dist/handlers/tools/analytics.d.ts +8 -0
  82. package/dist/handlers/tools/analytics.d.ts.map +1 -0
  83. package/dist/handlers/tools/analytics.js +256 -0
  84. package/dist/handlers/tools/analytics.js.map +1 -0
  85. package/dist/handlers/tools/backup.d.ts +8 -0
  86. package/dist/handlers/tools/backup.d.ts.map +1 -0
  87. package/dist/handlers/tools/backup.js +224 -0
  88. package/dist/handlers/tools/backup.js.map +1 -0
  89. package/dist/handlers/tools/core.d.ts +9 -0
  90. package/dist/handlers/tools/core.d.ts.map +1 -0
  91. package/dist/handlers/tools/core.js +326 -0
  92. package/dist/handlers/tools/core.js.map +1 -0
  93. package/dist/handlers/tools/export.d.ts +8 -0
  94. package/dist/handlers/tools/export.d.ts.map +1 -0
  95. package/dist/handlers/tools/export.js +89 -0
  96. package/dist/handlers/tools/export.js.map +1 -0
  97. package/dist/handlers/tools/github/helpers.d.ts +34 -0
  98. package/dist/handlers/tools/github/helpers.d.ts.map +1 -0
  99. package/dist/handlers/tools/github/helpers.js +52 -0
  100. package/dist/handlers/tools/github/helpers.js.map +1 -0
  101. package/dist/handlers/tools/github/insights-tools.d.ts +8 -0
  102. package/dist/handlers/tools/github/insights-tools.d.ts.map +1 -0
  103. package/dist/handlers/tools/github/insights-tools.js +104 -0
  104. package/dist/handlers/tools/github/insights-tools.js.map +1 -0
  105. package/dist/handlers/tools/github/issue-tools.d.ts +8 -0
  106. package/dist/handlers/tools/github/issue-tools.d.ts.map +1 -0
  107. package/dist/handlers/tools/github/issue-tools.js +359 -0
  108. package/dist/handlers/tools/github/issue-tools.js.map +1 -0
  109. package/dist/handlers/tools/github/kanban-tools.d.ts +8 -0
  110. package/dist/handlers/tools/github/kanban-tools.d.ts.map +1 -0
  111. package/dist/handlers/tools/github/kanban-tools.js +108 -0
  112. package/dist/handlers/tools/github/kanban-tools.js.map +1 -0
  113. package/dist/handlers/tools/github/milestone-tools.d.ts +9 -0
  114. package/dist/handlers/tools/github/milestone-tools.d.ts.map +1 -0
  115. package/dist/handlers/tools/github/milestone-tools.js +302 -0
  116. package/dist/handlers/tools/github/milestone-tools.js.map +1 -0
  117. package/dist/handlers/tools/github/mutation-tools.d.ts +12 -0
  118. package/dist/handlers/tools/github/mutation-tools.d.ts.map +1 -0
  119. package/dist/handlers/tools/github/mutation-tools.js +15 -0
  120. package/dist/handlers/tools/github/mutation-tools.js.map +1 -0
  121. package/dist/handlers/tools/github/read-tools.d.ts +8 -0
  122. package/dist/handlers/tools/github/read-tools.d.ts.map +1 -0
  123. package/dist/handlers/tools/github/read-tools.js +260 -0
  124. package/dist/handlers/tools/github/read-tools.js.map +1 -0
  125. package/dist/handlers/tools/github/schemas.d.ts +467 -0
  126. package/dist/handlers/tools/github/schemas.d.ts.map +1 -0
  127. package/dist/handlers/tools/github/schemas.js +335 -0
  128. package/dist/handlers/tools/github/schemas.js.map +1 -0
  129. package/dist/handlers/tools/github.d.ts +14 -0
  130. package/dist/handlers/tools/github.d.ts.map +1 -0
  131. package/dist/handlers/tools/github.js +28 -0
  132. package/dist/handlers/tools/github.js.map +1 -0
  133. package/dist/handlers/tools/index.d.ts +15 -20
  134. package/dist/handlers/tools/index.d.ts.map +1 -1
  135. package/dist/handlers/tools/index.js +117 -2909
  136. package/dist/handlers/tools/index.js.map +1 -1
  137. package/dist/handlers/tools/relationships.d.ts +8 -0
  138. package/dist/handlers/tools/relationships.d.ts.map +1 -0
  139. package/dist/handlers/tools/relationships.js +308 -0
  140. package/dist/handlers/tools/relationships.js.map +1 -0
  141. package/dist/handlers/tools/schemas.d.ts +108 -0
  142. package/dist/handlers/tools/schemas.d.ts.map +1 -0
  143. package/dist/handlers/tools/schemas.js +122 -0
  144. package/dist/handlers/tools/schemas.js.map +1 -0
  145. package/dist/handlers/tools/search.d.ts +8 -0
  146. package/dist/handlers/tools/search.d.ts.map +1 -0
  147. package/dist/handlers/tools/search.js +282 -0
  148. package/dist/handlers/tools/search.js.map +1 -0
  149. package/dist/handlers/tools/team.d.ts +11 -0
  150. package/dist/handlers/tools/team.d.ts.map +1 -0
  151. package/dist/handlers/tools/team.js +239 -0
  152. package/dist/handlers/tools/team.js.map +1 -0
  153. package/dist/server/McpServer.d.ts +4 -0
  154. package/dist/server/McpServer.d.ts.map +1 -1
  155. package/dist/server/McpServer.js +48 -297
  156. package/dist/server/McpServer.js.map +1 -1
  157. package/dist/server/Scheduler.d.ts +91 -0
  158. package/dist/server/Scheduler.d.ts.map +1 -0
  159. package/dist/server/Scheduler.js +201 -0
  160. package/dist/server/Scheduler.js.map +1 -0
  161. package/dist/transports/http.d.ts +66 -0
  162. package/dist/transports/http.d.ts.map +1 -0
  163. package/dist/transports/http.js +519 -0
  164. package/dist/transports/http.js.map +1 -0
  165. package/dist/types/entities.d.ts +101 -0
  166. package/dist/types/entities.d.ts.map +1 -0
  167. package/dist/types/entities.js +5 -0
  168. package/dist/types/entities.js.map +1 -0
  169. package/dist/types/filtering.d.ts +34 -0
  170. package/dist/types/filtering.d.ts.map +1 -0
  171. package/dist/types/filtering.js +5 -0
  172. package/dist/types/filtering.js.map +1 -0
  173. package/dist/types/github.d.ts +166 -0
  174. package/dist/types/github.d.ts.map +1 -0
  175. package/dist/types/github.js +5 -0
  176. package/dist/types/github.js.map +1 -0
  177. package/dist/types/index.d.ts +35 -292
  178. package/dist/types/index.d.ts.map +1 -1
  179. package/dist/types/index.js +2 -2
  180. package/dist/types/index.js.map +1 -1
  181. package/dist/utils/error-helpers.d.ts +37 -0
  182. package/dist/utils/error-helpers.d.ts.map +1 -0
  183. package/dist/utils/error-helpers.js +47 -0
  184. package/dist/utils/error-helpers.js.map +1 -0
  185. package/dist/utils/logger.d.ts.map +1 -1
  186. package/dist/utils/logger.js +6 -3
  187. package/dist/utils/logger.js.map +1 -1
  188. package/dist/utils/security-utils.d.ts +0 -21
  189. package/dist/utils/security-utils.d.ts.map +1 -1
  190. package/dist/utils/security-utils.js +0 -47
  191. package/dist/utils/security-utils.js.map +1 -1
  192. package/dist/vector/VectorSearchManager.d.ts.map +1 -1
  193. package/dist/vector/VectorSearchManager.js +9 -32
  194. package/dist/vector/VectorSearchManager.js.map +1 -1
  195. package/docker-compose.yml +11 -2
  196. package/hooks/README.md +107 -0
  197. package/hooks/cursor/hooks.json +10 -0
  198. package/hooks/cursor/memory-journal.mdc +22 -0
  199. package/hooks/cursor/session-end.sh +19 -0
  200. package/hooks/kilo-code/session-end-mode.json +11 -0
  201. package/hooks/kiro/session-end.md +13 -0
  202. package/mcp-config-example.json +1 -0
  203. package/package.json +11 -9
  204. package/playwright.config.ts +29 -0
  205. package/releases/v4.5.0.md +116 -0
  206. package/releases/v5.0.0.md +105 -0
  207. package/scripts/generate-server-instructions.ts +176 -0
  208. package/scripts/server-instructions-function-body.ts +77 -0
  209. package/server.json +3 -3
  210. package/src/cli.ts +45 -1
  211. package/src/constants/ServerInstructions.ts +133 -73
  212. package/src/constants/icons.ts +8 -7
  213. package/src/constants/server-instructions.md +268 -0
  214. package/src/database/SqliteAdapter.ts +358 -192
  215. package/src/database/schema.ts +125 -0
  216. package/src/filtering/ToolFilter.ts +13 -2
  217. package/src/github/GitHubIntegration.ts +1 -3
  218. package/src/handlers/prompts/github.ts +209 -0
  219. package/src/handlers/prompts/index.ts +10 -499
  220. package/src/handlers/prompts/workflow.ts +314 -0
  221. package/src/handlers/resources/core.ts +528 -0
  222. package/src/handlers/resources/github.ts +358 -0
  223. package/src/handlers/resources/graph.ts +254 -0
  224. package/src/handlers/resources/index.ts +23 -1570
  225. package/src/handlers/resources/shared.ts +103 -0
  226. package/src/handlers/resources/team.ts +133 -0
  227. package/src/handlers/resources/templates.ts +374 -0
  228. package/src/handlers/tools/admin.ts +285 -0
  229. package/src/handlers/tools/analytics.ts +301 -0
  230. package/src/handlers/tools/backup.ts +242 -0
  231. package/src/handlers/tools/core.ts +350 -0
  232. package/src/handlers/tools/export.ts +115 -0
  233. package/src/handlers/tools/github/helpers.ts +86 -0
  234. package/src/handlers/tools/github/insights-tools.ts +119 -0
  235. package/src/handlers/tools/github/issue-tools.ts +439 -0
  236. package/src/handlers/tools/github/kanban-tools.ts +134 -0
  237. package/src/handlers/tools/github/milestone-tools.ts +392 -0
  238. package/src/handlers/tools/github/mutation-tools.ts +17 -0
  239. package/src/handlers/tools/github/read-tools.ts +328 -0
  240. package/src/handlers/tools/github/schemas.ts +369 -0
  241. package/src/handlers/tools/github.ts +36 -0
  242. package/src/handlers/tools/index.ts +144 -3325
  243. package/src/handlers/tools/relationships.ts +358 -0
  244. package/src/handlers/tools/schemas.ts +132 -0
  245. package/src/handlers/tools/search.ts +343 -0
  246. package/src/handlers/tools/team.ts +273 -0
  247. package/src/server/McpServer.ts +63 -358
  248. package/src/server/Scheduler.ts +278 -0
  249. package/src/transports/http.ts +635 -0
  250. package/src/types/entities.ts +145 -0
  251. package/src/types/filtering.ts +54 -0
  252. package/src/types/github.ts +180 -0
  253. package/src/types/index.ts +67 -375
  254. package/src/utils/error-helpers.ts +52 -0
  255. package/src/utils/logger.ts +6 -3
  256. package/src/utils/security-utils.ts +0 -52
  257. package/src/vector/VectorSearchManager.ts +9 -33
  258. package/tests/constants/icons.test.ts +1 -2
  259. package/tests/constants/server-instructions.test.ts +30 -4
  260. package/tests/database/sqlite-adapter.test.ts +91 -7
  261. package/tests/e2e/auth.spec.ts +154 -0
  262. package/tests/e2e/health.spec.ts +63 -0
  263. package/tests/e2e/protocols.spec.ts +134 -0
  264. package/tests/e2e/resources.spec.ts +103 -0
  265. package/tests/e2e/scheduler.spec.ts +79 -0
  266. package/tests/e2e/security.spec.ts +91 -0
  267. package/tests/e2e/sessions.spec.ts +95 -0
  268. package/tests/e2e/stateless.spec.ts +121 -0
  269. package/tests/e2e/tools.spec.ts +111 -0
  270. package/tests/filtering/tool-filter.test.ts +46 -0
  271. package/tests/handlers/error-path-coverage.test.ts +324 -0
  272. package/tests/handlers/github-resource-handlers.test.ts +453 -0
  273. package/tests/handlers/github-tool-handlers.test.ts +899 -0
  274. package/tests/handlers/prompt-handler-coverage.test.ts +106 -0
  275. package/tests/handlers/prompt-handlers.test.ts +40 -0
  276. package/tests/handlers/resource-handler-coverage.test.ts +181 -0
  277. package/tests/handlers/resource-handlers.test.ts +33 -9
  278. package/tests/handlers/search-tool-handlers.test.ts +272 -0
  279. package/tests/handlers/targeted-gap-closure.test.ts +387 -0
  280. package/tests/handlers/team-resource-handlers.test.ts +156 -0
  281. package/tests/handlers/team-tool-handlers.test.ts +301 -0
  282. package/tests/handlers/tool-handler-coverage.test.ts +469 -0
  283. package/tests/handlers/tool-handlers.test.ts +2 -2
  284. package/tests/security/sql-injection.test.ts +3 -54
  285. package/tests/server/mcp-server.test.ts +503 -8
  286. package/tests/server/scheduler.test.ts +400 -0
  287. package/tests/transports/http-transport.test.ts +620 -0
  288. package/tests/vector/vector-search-manager.test.ts +60 -0
  289. package/vitest.config.ts +4 -1
  290. package/.memory-journal-team.db +0 -0
  291. package/.vscode/settings.json +0 -84
@@ -0,0 +1,400 @@
1
+ /**
2
+ * Tests for Scheduler module
3
+ */
4
+
5
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
6
+ import { Scheduler, type SchedulerOptions } from '../../src/server/Scheduler.js'
7
+
8
+ // Mock logger
9
+ vi.mock('../../src/utils/logger.js', () => ({
10
+ logger: {
11
+ info: vi.fn(),
12
+ warning: vi.fn(),
13
+ error: vi.fn(),
14
+ debug: vi.fn(),
15
+ },
16
+ }))
17
+
18
+ /**
19
+ * Creates a mock SqliteAdapter with the methods used by Scheduler.
20
+ */
21
+ function createMockDb() {
22
+ return {
23
+ exportToFile: vi.fn().mockReturnValue({
24
+ filename: 'backup_2026-03-01.db',
25
+ path: '/data/backups/backup_2026-03-01.db',
26
+ sizeBytes: 4096,
27
+ }),
28
+ deleteOldBackups: vi.fn().mockReturnValue({
29
+ deleted: ['old_backup.db'],
30
+ kept: 5,
31
+ }),
32
+ getRawDb: vi.fn().mockReturnValue({
33
+ run: vi.fn(),
34
+ }),
35
+ flushSave: vi.fn(),
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Creates a mock VectorSearchManager with rebuildIndex.
41
+ */
42
+ function createMockVectorManager() {
43
+ return {
44
+ rebuildIndex: vi.fn().mockResolvedValue(42),
45
+ }
46
+ }
47
+
48
+ function defaultOptions(overrides: Partial<SchedulerOptions> = {}): SchedulerOptions {
49
+ return {
50
+ backupIntervalMinutes: 0,
51
+ keepBackups: 5,
52
+ vacuumIntervalMinutes: 0,
53
+ rebuildIndexIntervalMinutes: 0,
54
+ ...overrides,
55
+ }
56
+ }
57
+
58
+ describe('Scheduler', () => {
59
+ beforeEach(() => {
60
+ vi.useFakeTimers()
61
+ })
62
+
63
+ afterEach(() => {
64
+ vi.useRealTimers()
65
+ vi.restoreAllMocks()
66
+ })
67
+
68
+ // ========================================================================
69
+ // Construction & start/stop
70
+ // ========================================================================
71
+
72
+ describe('start and stop', () => {
73
+ it('should start with no jobs when all intervals are 0', () => {
74
+ const db = createMockDb()
75
+ const scheduler = new Scheduler(defaultOptions(), db as never)
76
+ scheduler.start()
77
+
78
+ const status = scheduler.getStatus()
79
+ expect(status.active).toBe(true)
80
+ expect(status.jobs).toHaveLength(0)
81
+
82
+ scheduler.stop()
83
+ })
84
+
85
+ it('should create timers for enabled jobs', () => {
86
+ const db = createMockDb()
87
+ const vectorManager = createMockVectorManager()
88
+ const scheduler = new Scheduler(
89
+ defaultOptions({
90
+ backupIntervalMinutes: 60,
91
+ vacuumIntervalMinutes: 120,
92
+ rebuildIndexIntervalMinutes: 180,
93
+ }),
94
+ db as never,
95
+ vectorManager as never
96
+ )
97
+ scheduler.start()
98
+
99
+ const status = scheduler.getStatus()
100
+ expect(status.active).toBe(true)
101
+ expect(status.jobs).toHaveLength(3)
102
+ expect(status.jobs.map((j) => j.name)).toEqual(['backup', 'vacuum', 'rebuild-index'])
103
+
104
+ scheduler.stop()
105
+ })
106
+
107
+ it('should ignore duplicate start calls', () => {
108
+ const db = createMockDb()
109
+ const scheduler = new Scheduler(
110
+ defaultOptions({ backupIntervalMinutes: 60 }),
111
+ db as never
112
+ )
113
+ scheduler.start()
114
+ scheduler.start() // duplicate — should be ignored
115
+
116
+ const status = scheduler.getStatus()
117
+ expect(status.jobs).toHaveLength(1) // still just 1
118
+
119
+ scheduler.stop()
120
+ })
121
+
122
+ it('should clear all timers on stop', () => {
123
+ const db = createMockDb()
124
+ const scheduler = new Scheduler(
125
+ defaultOptions({ backupIntervalMinutes: 30, vacuumIntervalMinutes: 60 }),
126
+ db as never
127
+ )
128
+ scheduler.start()
129
+ expect(scheduler.getStatus().jobs).toHaveLength(2)
130
+
131
+ scheduler.stop()
132
+ expect(scheduler.getStatus().active).toBe(false)
133
+ expect(scheduler.getStatus().jobs).toHaveLength(0)
134
+ })
135
+
136
+ it('should be safe to stop multiple times', () => {
137
+ const db = createMockDb()
138
+ const scheduler = new Scheduler(
139
+ defaultOptions({ backupIntervalMinutes: 30 }),
140
+ db as never
141
+ )
142
+ scheduler.start()
143
+ scheduler.stop()
144
+ scheduler.stop() // should not throw
145
+
146
+ expect(scheduler.getStatus().active).toBe(false)
147
+ })
148
+
149
+ it('should skip rebuild-index when vectorManager is not provided', () => {
150
+ const db = createMockDb()
151
+ const scheduler = new Scheduler(
152
+ defaultOptions({ rebuildIndexIntervalMinutes: 60 }),
153
+ db as never
154
+ // no vectorManager
155
+ )
156
+ scheduler.start()
157
+
158
+ const status = scheduler.getStatus()
159
+ expect(status.jobs).toHaveLength(0) // skipped
160
+
161
+ scheduler.stop()
162
+ })
163
+ })
164
+
165
+ // ========================================================================
166
+ // Job execution
167
+ // ========================================================================
168
+
169
+ describe('backup job', () => {
170
+ it('should call exportToFile and deleteOldBackups on interval', async () => {
171
+ const db = createMockDb()
172
+ const scheduler = new Scheduler(
173
+ defaultOptions({ backupIntervalMinutes: 1, keepBackups: 3 }),
174
+ db as never
175
+ )
176
+ scheduler.start()
177
+
178
+ // Advance by 1 minute
179
+ await vi.advanceTimersByTimeAsync(60_000)
180
+
181
+ expect(db.exportToFile).toHaveBeenCalledOnce()
182
+ expect(db.deleteOldBackups).toHaveBeenCalledWith(3)
183
+
184
+ // Advance another minute
185
+ await vi.advanceTimersByTimeAsync(60_000)
186
+
187
+ expect(db.exportToFile).toHaveBeenCalledTimes(2)
188
+ expect(db.deleteOldBackups).toHaveBeenCalledTimes(2)
189
+
190
+ scheduler.stop()
191
+ })
192
+
193
+ it('should track job status after successful run', async () => {
194
+ const db = createMockDb()
195
+ const scheduler = new Scheduler(
196
+ defaultOptions({ backupIntervalMinutes: 1 }),
197
+ db as never
198
+ )
199
+ scheduler.start()
200
+
201
+ await vi.advanceTimersByTimeAsync(60_000)
202
+
203
+ const status = scheduler.getStatus()
204
+ const backupJob = status.jobs.find((j) => j.name === 'backup')
205
+ expect(backupJob).toBeDefined()
206
+ expect(backupJob!.lastResult).toBe('success')
207
+ expect(backupJob!.lastError).toBeNull()
208
+ expect(backupJob!.runCount).toBe(1)
209
+ expect(backupJob!.lastRun).toBeTruthy()
210
+
211
+ scheduler.stop()
212
+ })
213
+
214
+ it('should track error status when backup fails', async () => {
215
+ const db = createMockDb()
216
+ db.exportToFile.mockImplementation(() => {
217
+ throw new Error('Disk full')
218
+ })
219
+
220
+ const scheduler = new Scheduler(
221
+ defaultOptions({ backupIntervalMinutes: 1 }),
222
+ db as never
223
+ )
224
+ scheduler.start()
225
+
226
+ await vi.advanceTimersByTimeAsync(60_000)
227
+
228
+ const status = scheduler.getStatus()
229
+ const backupJob = status.jobs.find((j) => j.name === 'backup')
230
+ expect(backupJob!.lastResult).toBe('error')
231
+ expect(backupJob!.lastError).toBe('Disk full')
232
+ expect(backupJob!.runCount).toBe(1)
233
+
234
+ scheduler.stop()
235
+ })
236
+
237
+ it('should continue running after a failure', async () => {
238
+ const db = createMockDb()
239
+ db.exportToFile
240
+ .mockImplementationOnce(() => {
241
+ throw new Error('Disk full')
242
+ })
243
+ .mockReturnValue({
244
+ filename: 'backup.db',
245
+ path: '/data/backups/backup.db',
246
+ sizeBytes: 4096,
247
+ })
248
+
249
+ const scheduler = new Scheduler(
250
+ defaultOptions({ backupIntervalMinutes: 1 }),
251
+ db as never
252
+ )
253
+ scheduler.start()
254
+
255
+ // First run — fails
256
+ await vi.advanceTimersByTimeAsync(60_000)
257
+ expect(scheduler.getStatus().jobs[0].lastResult).toBe('error')
258
+
259
+ // Second run — succeeds
260
+ await vi.advanceTimersByTimeAsync(60_000)
261
+ expect(scheduler.getStatus().jobs[0].lastResult).toBe('success')
262
+ expect(scheduler.getStatus().jobs[0].runCount).toBe(2)
263
+
264
+ scheduler.stop()
265
+ })
266
+ })
267
+
268
+ describe('vacuum job', () => {
269
+ it('should call PRAGMA optimize and flushSave on interval', async () => {
270
+ const db = createMockDb()
271
+ const rawDb = db.getRawDb()
272
+
273
+ const scheduler = new Scheduler(
274
+ defaultOptions({ vacuumIntervalMinutes: 1 }),
275
+ db as never
276
+ )
277
+ scheduler.start()
278
+
279
+ await vi.advanceTimersByTimeAsync(60_000)
280
+
281
+ expect(rawDb.run).toHaveBeenCalledWith('PRAGMA optimize')
282
+ expect(db.flushSave).toHaveBeenCalledOnce()
283
+
284
+ scheduler.stop()
285
+ })
286
+ })
287
+
288
+ describe('rebuild-index job', () => {
289
+ it('should call vectorManager.rebuildIndex on interval', async () => {
290
+ const db = createMockDb()
291
+ const vectorManager = createMockVectorManager()
292
+
293
+ const scheduler = new Scheduler(
294
+ defaultOptions({ rebuildIndexIntervalMinutes: 1 }),
295
+ db as never,
296
+ vectorManager as never
297
+ )
298
+ scheduler.start()
299
+
300
+ await vi.advanceTimersByTimeAsync(60_000)
301
+
302
+ expect(vectorManager.rebuildIndex).toHaveBeenCalledWith(db)
303
+
304
+ const status = scheduler.getStatus()
305
+ const job = status.jobs.find((j) => j.name === 'rebuild-index')
306
+ expect(job!.lastResult).toBe('success')
307
+
308
+ scheduler.stop()
309
+ })
310
+ })
311
+
312
+ // ========================================================================
313
+ // getStatus
314
+ // ========================================================================
315
+
316
+ describe('getStatus', () => {
317
+ it('should return inactive status before start', () => {
318
+ const db = createMockDb()
319
+ const scheduler = new Scheduler(
320
+ defaultOptions({ backupIntervalMinutes: 60 }),
321
+ db as never
322
+ )
323
+
324
+ const status = scheduler.getStatus()
325
+ expect(status.active).toBe(false)
326
+ expect(status.jobs).toHaveLength(0)
327
+ })
328
+
329
+ it('should include nextRun for jobs that have not run yet', () => {
330
+ const db = createMockDb()
331
+ const scheduler = new Scheduler(
332
+ defaultOptions({ backupIntervalMinutes: 60 }),
333
+ db as never
334
+ )
335
+ scheduler.start()
336
+
337
+ const status = scheduler.getStatus()
338
+ const job = status.jobs[0]
339
+ expect(job.lastRun).toBeNull()
340
+ expect(job.nextRun).toBeTruthy()
341
+
342
+ scheduler.stop()
343
+ })
344
+
345
+ it('should correctly report interval for each job', () => {
346
+ const db = createMockDb()
347
+ const vectorManager = createMockVectorManager()
348
+ const scheduler = new Scheduler(
349
+ defaultOptions({
350
+ backupIntervalMinutes: 10,
351
+ vacuumIntervalMinutes: 20,
352
+ rebuildIndexIntervalMinutes: 30,
353
+ }),
354
+ db as never,
355
+ vectorManager as never
356
+ )
357
+ scheduler.start()
358
+
359
+ const status = scheduler.getStatus()
360
+ expect(status.jobs[0].intervalMinutes).toBe(10)
361
+ expect(status.jobs[1].intervalMinutes).toBe(20)
362
+ expect(status.jobs[2].intervalMinutes).toBe(30)
363
+
364
+ scheduler.stop()
365
+ })
366
+ })
367
+
368
+ // ========================================================================
369
+ // Error isolation
370
+ // ========================================================================
371
+
372
+ describe('error isolation', () => {
373
+ it('should not stop other jobs when one fails', async () => {
374
+ const db = createMockDb()
375
+ db.exportToFile.mockImplementation(() => {
376
+ throw new Error('Backup failed')
377
+ })
378
+
379
+ const scheduler = new Scheduler(
380
+ defaultOptions({
381
+ backupIntervalMinutes: 1,
382
+ vacuumIntervalMinutes: 1,
383
+ }),
384
+ db as never
385
+ )
386
+ scheduler.start()
387
+
388
+ await vi.advanceTimersByTimeAsync(60_000)
389
+
390
+ // Backup failed but vacuum should still have run
391
+ const status = scheduler.getStatus()
392
+ const backupJob = status.jobs.find((j) => j.name === 'backup')
393
+ const vacuumJob = status.jobs.find((j) => j.name === 'vacuum')
394
+ expect(backupJob!.lastResult).toBe('error')
395
+ expect(vacuumJob!.lastResult).toBe('success')
396
+
397
+ scheduler.stop()
398
+ })
399
+ })
400
+ })