@langadventurellc/task-trellis-mcp 1.1.0 → 1.2.1

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 (262) hide show
  1. package/README.md +37 -323
  2. package/dist/__tests__/e2e/autoPrune.e2e.test.d.ts +2 -0
  3. package/dist/__tests__/e2e/autoPrune.e2e.test.d.ts.map +1 -0
  4. package/dist/__tests__/e2e/autoPrune.e2e.test.js +533 -0
  5. package/dist/__tests__/e2e/autoPrune.e2e.test.js.map +1 -0
  6. package/dist/__tests__/e2e/configuration/activation.e2e.test.js +6 -6
  7. package/dist/__tests__/e2e/configuration/activation.e2e.test.js.map +1 -1
  8. package/dist/__tests__/e2e/configuration/commandLineArgs.e2e.test.js +55 -6
  9. package/dist/__tests__/e2e/configuration/commandLineArgs.e2e.test.js.map +1 -1
  10. package/dist/__tests__/e2e/configuration/directorySetup.e2e.test.js +19 -19
  11. package/dist/__tests__/e2e/configuration/directorySetup.e2e.test.js.map +1 -1
  12. package/dist/__tests__/e2e/configuration/invalidConfig.e2e.test.js +9 -9
  13. package/dist/__tests__/e2e/configuration/invalidConfig.e2e.test.js.map +1 -1
  14. package/dist/__tests__/e2e/configuration/preActivation.e2e.test.js +15 -15
  15. package/dist/__tests__/e2e/configuration/preActivation.e2e.test.js.map +1 -1
  16. package/dist/__tests__/e2e/crud/createObject.e2e.test.js +34 -34
  17. package/dist/__tests__/e2e/crud/createObject.e2e.test.js.map +1 -1
  18. package/dist/__tests__/e2e/crud/deleteObject.e2e.test.js +19 -19
  19. package/dist/__tests__/e2e/crud/deleteObject.e2e.test.js.map +1 -1
  20. package/dist/__tests__/e2e/crud/fileValidation.e2e.test.js +32 -32
  21. package/dist/__tests__/e2e/crud/fileValidation.e2e.test.js.map +1 -1
  22. package/dist/__tests__/e2e/crud/getObject.e2e.test.js +143 -22
  23. package/dist/__tests__/e2e/crud/getObject.e2e.test.js.map +1 -1
  24. package/dist/__tests__/e2e/crud/listObjects.e2e.test.js +561 -42
  25. package/dist/__tests__/e2e/crud/listObjects.e2e.test.js.map +1 -1
  26. package/dist/__tests__/e2e/crud/updateObject.e2e.test.js +497 -25
  27. package/dist/__tests__/e2e/crud/updateObject.e2e.test.js.map +1 -1
  28. package/dist/__tests__/e2e/hierarchicalPrerequisites.e2e.test.d.ts +2 -0
  29. package/dist/__tests__/e2e/hierarchicalPrerequisites.e2e.test.d.ts.map +1 -0
  30. package/dist/__tests__/e2e/hierarchicalPrerequisites.e2e.test.js +319 -0
  31. package/dist/__tests__/e2e/hierarchicalPrerequisites.e2e.test.js.map +1 -0
  32. package/dist/__tests__/e2e/infrastructure/client.e2e.test.js +4 -4
  33. package/dist/__tests__/e2e/infrastructure/client.e2e.test.js.map +1 -1
  34. package/dist/__tests__/e2e/infrastructure/server.e2e.test.js +6 -6
  35. package/dist/__tests__/e2e/infrastructure/server.e2e.test.js.map +1 -1
  36. package/dist/__tests__/e2e/utils/extractObjectIds.d.ts +1 -1
  37. package/dist/__tests__/e2e/utils/extractObjectIds.js +1 -1
  38. package/dist/__tests__/e2e/utils/mcpTestClient.d.ts.map +1 -1
  39. package/dist/__tests__/e2e/utils/mcpTestClient.js +3 -2
  40. package/dist/__tests__/e2e/utils/mcpTestClient.js.map +1 -1
  41. package/dist/__tests__/e2e/utils/parseListObjectsResponse.d.ts +1 -1
  42. package/dist/__tests__/e2e/utils/parseListObjectsResponse.js +1 -1
  43. package/dist/__tests__/e2e/utils/parseUpdateObjectResponse.d.ts +1 -1
  44. package/dist/__tests__/e2e/utils/parseUpdateObjectResponse.js +1 -1
  45. package/dist/__tests__/e2e/workflow/appendLog.e2e.test.js +4 -4
  46. package/dist/__tests__/e2e/workflow/appendLog.e2e.test.js.map +1 -1
  47. package/dist/__tests__/e2e/workflow/appendModifiedFiles.e2e.test.js +15 -15
  48. package/dist/__tests__/e2e/workflow/appendModifiedFiles.e2e.test.js.map +1 -1
  49. package/dist/__tests__/e2e/workflow/claimTask.e2e.test.js +44 -0
  50. package/dist/__tests__/e2e/workflow/claimTask.e2e.test.js.map +1 -1
  51. package/dist/__tests__/e2e/workflow/completeTask.e2e.test.js +4 -4
  52. package/dist/__tests__/e2e/workflow/completeTask.e2e.test.js.map +1 -1
  53. package/dist/__tests__/e2e/workflow/prerequisites.e2e.test.js +43 -43
  54. package/dist/__tests__/e2e/workflow/prerequisites.e2e.test.js.map +1 -1
  55. package/dist/__tests__/e2e/workflow/taskLifecycle.e2e.test.js +19 -19
  56. package/dist/__tests__/e2e/workflow/taskLifecycle.e2e.test.js.map +1 -1
  57. package/dist/__tests__/serverStartup.test.d.ts +2 -0
  58. package/dist/__tests__/serverStartup.test.d.ts.map +1 -0
  59. package/dist/__tests__/serverStartup.test.js +171 -0
  60. package/dist/__tests__/serverStartup.test.js.map +1 -0
  61. package/dist/configuration/ServerConfig.d.ts +2 -1
  62. package/dist/configuration/ServerConfig.d.ts.map +1 -1
  63. package/dist/repositories/Repository.d.ts +2 -1
  64. package/dist/repositories/Repository.d.ts.map +1 -1
  65. package/dist/repositories/local/LocalRepository.d.ts +2 -1
  66. package/dist/repositories/local/LocalRepository.d.ts.map +1 -1
  67. package/dist/repositories/local/LocalRepository.js +4 -0
  68. package/dist/repositories/local/LocalRepository.js.map +1 -1
  69. package/dist/repositories/local/__tests__/getChildrenOf.test.d.ts +2 -0
  70. package/dist/repositories/local/__tests__/getChildrenOf.test.d.ts.map +1 -0
  71. package/dist/repositories/local/__tests__/getChildrenOf.test.js +306 -0
  72. package/dist/repositories/local/__tests__/getChildrenOf.test.js.map +1 -0
  73. package/dist/repositories/local/__tests__/getObjects.test.js +309 -0
  74. package/dist/repositories/local/__tests__/getObjects.test.js.map +1 -1
  75. package/dist/repositories/local/deleteObjectById.d.ts.map +1 -1
  76. package/dist/repositories/local/deleteObjectById.js +2 -0
  77. package/dist/repositories/local/deleteObjectById.js.map +1 -1
  78. package/dist/repositories/local/getChildrenOf.d.ts +11 -0
  79. package/dist/repositories/local/getChildrenOf.d.ts.map +1 -0
  80. package/dist/repositories/local/getChildrenOf.js +73 -0
  81. package/dist/repositories/local/getChildrenOf.js.map +1 -0
  82. package/dist/repositories/local/getObjects.d.ts +1 -1
  83. package/dist/repositories/local/getObjects.d.ts.map +1 -1
  84. package/dist/repositories/local/getObjects.js +25 -6
  85. package/dist/repositories/local/getObjects.js.map +1 -1
  86. package/dist/server.js +39 -15
  87. package/dist/server.js.map +1 -1
  88. package/dist/services/TaskTrellisService.d.ts +4 -13
  89. package/dist/services/TaskTrellisService.d.ts.map +1 -1
  90. package/dist/services/local/LocalTaskTrellisService.d.ts +3 -9
  91. package/dist/services/local/LocalTaskTrellisService.d.ts.map +1 -1
  92. package/dist/services/local/LocalTaskTrellisService.js +4 -8
  93. package/dist/services/local/LocalTaskTrellisService.js.map +1 -1
  94. package/dist/services/local/__tests__/appendModifiedFiles.test.js +1 -0
  95. package/dist/services/local/__tests__/appendModifiedFiles.test.js.map +1 -1
  96. package/dist/services/local/__tests__/appendObjectLog.test.js +1 -0
  97. package/dist/services/local/__tests__/appendObjectLog.test.js.map +1 -1
  98. package/dist/services/local/__tests__/claimTask.test.js +127 -131
  99. package/dist/services/local/__tests__/claimTask.test.js.map +1 -1
  100. package/dist/services/local/__tests__/completeTask.test.js +30 -28
  101. package/dist/services/local/__tests__/completeTask.test.js.map +1 -1
  102. package/dist/services/local/__tests__/createObject.test.js +1 -0
  103. package/dist/services/local/__tests__/createObject.test.js.map +1 -1
  104. package/dist/services/local/__tests__/listObjects.test.js +135 -10
  105. package/dist/services/local/__tests__/listObjects.test.js.map +1 -1
  106. package/dist/services/local/__tests__/pruneClosed.test.js +446 -186
  107. package/dist/services/local/__tests__/pruneClosed.test.js.map +1 -1
  108. package/dist/services/local/__tests__/updateObject.test.js +234 -27
  109. package/dist/services/local/__tests__/updateObject.test.js.map +1 -1
  110. package/dist/services/local/claimTask.d.ts.map +1 -1
  111. package/dist/services/local/claimTask.js +25 -34
  112. package/dist/services/local/claimTask.js.map +1 -1
  113. package/dist/services/local/completeTask.d.ts +1 -1
  114. package/dist/services/local/completeTask.d.ts.map +1 -1
  115. package/dist/services/local/completeTask.js +4 -40
  116. package/dist/services/local/completeTask.js.map +1 -1
  117. package/dist/services/local/listObjects.d.ts +1 -1
  118. package/dist/services/local/listObjects.d.ts.map +1 -1
  119. package/dist/services/local/listObjects.js +10 -1
  120. package/dist/services/local/listObjects.js.map +1 -1
  121. package/dist/services/local/pruneClosed.d.ts.map +1 -1
  122. package/dist/services/local/pruneClosed.js +63 -6
  123. package/dist/services/local/pruneClosed.js.map +1 -1
  124. package/dist/services/local/updateObject.d.ts +2 -1
  125. package/dist/services/local/updateObject.d.ts.map +1 -1
  126. package/dist/services/local/updateObject.js +28 -1
  127. package/dist/services/local/updateObject.js.map +1 -1
  128. package/dist/tools/__tests__/appendModifiedFilesTool.test.js +1 -0
  129. package/dist/tools/__tests__/appendModifiedFilesTool.test.js.map +1 -1
  130. package/dist/tools/__tests__/appendObjectLogTool.test.js +1 -0
  131. package/dist/tools/__tests__/appendObjectLogTool.test.js.map +1 -1
  132. package/dist/tools/__tests__/claimTaskTool.test.js +1 -1
  133. package/dist/tools/__tests__/claimTaskTool.test.js.map +1 -1
  134. package/dist/tools/__tests__/completeTaskTool.test.js +23 -16
  135. package/dist/tools/__tests__/completeTaskTool.test.js.map +1 -1
  136. package/dist/tools/__tests__/createObjectTool.test.js +1 -0
  137. package/dist/tools/__tests__/createObjectTool.test.js.map +1 -1
  138. package/dist/tools/__tests__/deleteObjectTool.test.js +1 -0
  139. package/dist/tools/__tests__/deleteObjectTool.test.js.map +1 -1
  140. package/dist/tools/__tests__/getObjectTool.test.js +1 -0
  141. package/dist/tools/__tests__/getObjectTool.test.js.map +1 -1
  142. package/dist/tools/__tests__/listObjectsTool.test.js +160 -1
  143. package/dist/tools/__tests__/listObjectsTool.test.js.map +1 -1
  144. package/dist/tools/__tests__/updateObjectTool.test.js +39 -10
  145. package/dist/tools/__tests__/updateObjectTool.test.js.map +1 -1
  146. package/dist/tools/appendModifiedFilesTool.d.ts +2 -2
  147. package/dist/tools/appendModifiedFilesTool.js +4 -4
  148. package/dist/tools/appendModifiedFilesTool.js.map +1 -1
  149. package/dist/tools/appendObjectLogTool.d.ts +3 -3
  150. package/dist/tools/appendObjectLogTool.js +4 -4
  151. package/dist/tools/appendObjectLogTool.js.map +1 -1
  152. package/dist/tools/completeTaskTool.d.ts +1 -1
  153. package/dist/tools/completeTaskTool.d.ts.map +1 -1
  154. package/dist/tools/completeTaskTool.js +1 -1
  155. package/dist/tools/completeTaskTool.js.map +1 -1
  156. package/dist/tools/createObjectTool.d.ts +8 -8
  157. package/dist/tools/createObjectTool.js +13 -13
  158. package/dist/tools/createObjectTool.js.map +1 -1
  159. package/dist/tools/deleteObjectTool.d.ts +3 -3
  160. package/dist/tools/deleteObjectTool.js +12 -12
  161. package/dist/tools/deleteObjectTool.js.map +1 -1
  162. package/dist/tools/getObjectTool.d.ts +3 -3
  163. package/dist/tools/getObjectTool.d.ts.map +1 -1
  164. package/dist/tools/getObjectTool.js +12 -7
  165. package/dist/tools/getObjectTool.js.map +1 -1
  166. package/dist/tools/index.d.ts +7 -9
  167. package/dist/tools/index.d.ts.map +1 -1
  168. package/dist/tools/index.js +22 -28
  169. package/dist/tools/index.js.map +1 -1
  170. package/dist/tools/listObjectsTool.d.ts +20 -11
  171. package/dist/tools/listObjectsTool.d.ts.map +1 -1
  172. package/dist/tools/listObjectsTool.js +112 -40
  173. package/dist/tools/listObjectsTool.js.map +1 -1
  174. package/dist/tools/updateObjectTool.d.ts +12 -7
  175. package/dist/tools/updateObjectTool.d.ts.map +1 -1
  176. package/dist/tools/updateObjectTool.js +20 -14
  177. package/dist/tools/updateObjectTool.js.map +1 -1
  178. package/dist/utils/__tests__/checkHierarchicalPrerequisitesComplete.test.d.ts +2 -0
  179. package/dist/utils/__tests__/checkHierarchicalPrerequisitesComplete.test.d.ts.map +1 -0
  180. package/dist/utils/__tests__/checkHierarchicalPrerequisitesComplete.test.js +206 -0
  181. package/dist/utils/__tests__/checkHierarchicalPrerequisitesComplete.test.js.map +1 -0
  182. package/dist/utils/__tests__/checkPrerequisitesComplete.test.js +5 -0
  183. package/dist/utils/__tests__/checkPrerequisitesComplete.test.js.map +1 -1
  184. package/dist/utils/__tests__/filterUnavailableObjects.test.js +51 -25
  185. package/dist/utils/__tests__/filterUnavailableObjects.test.js.map +1 -1
  186. package/dist/utils/__tests__/isRequiredForOtherObjects.test.js +5 -0
  187. package/dist/utils/__tests__/isRequiredForOtherObjects.test.js.map +1 -1
  188. package/dist/utils/__tests__/updateParentHierarchy.test.d.ts +2 -0
  189. package/dist/utils/__tests__/updateParentHierarchy.test.d.ts.map +1 -0
  190. package/dist/utils/__tests__/updateParentHierarchy.test.js +137 -0
  191. package/dist/utils/__tests__/updateParentHierarchy.test.js.map +1 -0
  192. package/dist/utils/autoCompleteParentHierarchy.d.ts +11 -0
  193. package/dist/utils/autoCompleteParentHierarchy.d.ts.map +1 -0
  194. package/dist/utils/autoCompleteParentHierarchy.js +49 -0
  195. package/dist/utils/autoCompleteParentHierarchy.js.map +1 -0
  196. package/dist/utils/checkHierarchicalPrerequisitesComplete.d.ts +14 -0
  197. package/dist/utils/checkHierarchicalPrerequisitesComplete.d.ts.map +1 -0
  198. package/dist/utils/checkHierarchicalPrerequisitesComplete.js +47 -0
  199. package/dist/utils/checkHierarchicalPrerequisitesComplete.js.map +1 -0
  200. package/dist/utils/filterUnavailableObjects.d.ts +6 -4
  201. package/dist/utils/filterUnavailableObjects.d.ts.map +1 -1
  202. package/dist/utils/filterUnavailableObjects.js +16 -22
  203. package/dist/utils/filterUnavailableObjects.js.map +1 -1
  204. package/dist/utils/index.d.ts +3 -2
  205. package/dist/utils/index.d.ts.map +1 -1
  206. package/dist/utils/index.js +7 -3
  207. package/dist/utils/index.js.map +1 -1
  208. package/dist/utils/updateParentHierarchy.d.ts +11 -0
  209. package/dist/utils/updateParentHierarchy.d.ts.map +1 -0
  210. package/dist/utils/updateParentHierarchy.js +40 -0
  211. package/dist/utils/updateParentHierarchy.js.map +1 -0
  212. package/dist/validation/__tests__/validateObjectCreation.test.js +1 -0
  213. package/dist/validation/__tests__/validateObjectCreation.test.js.map +1 -1
  214. package/dist/validation/__tests__/validateParentExists.test.js +1 -0
  215. package/dist/validation/__tests__/validateParentExists.test.js.map +1 -1
  216. package/dist/validation/__tests__/validateStatusTransition.test.js +1 -0
  217. package/dist/validation/__tests__/validateStatusTransition.test.js.map +1 -1
  218. package/package.json +1 -1
  219. package/dist/__tests__/e2e/crud/replaceObjectBodyRegex.e2e.test.d.ts +0 -2
  220. package/dist/__tests__/e2e/crud/replaceObjectBodyRegex.e2e.test.d.ts.map +0 -1
  221. package/dist/__tests__/e2e/crud/replaceObjectBodyRegex.e2e.test.js +0 -693
  222. package/dist/__tests__/e2e/crud/replaceObjectBodyRegex.e2e.test.js.map +0 -1
  223. package/dist/__tests__/e2e/workflow/pruneClosed.e2e.test.d.ts +0 -2
  224. package/dist/__tests__/e2e/workflow/pruneClosed.e2e.test.d.ts.map +0 -1
  225. package/dist/__tests__/e2e/workflow/pruneClosed.e2e.test.js +0 -352
  226. package/dist/__tests__/e2e/workflow/pruneClosed.e2e.test.js.map +0 -1
  227. package/dist/services/local/__tests__/replaceObjectBodyRegex.test.d.ts +0 -2
  228. package/dist/services/local/__tests__/replaceObjectBodyRegex.test.d.ts.map +0 -1
  229. package/dist/services/local/__tests__/replaceObjectBodyRegex.test.js +0 -283
  230. package/dist/services/local/__tests__/replaceObjectBodyRegex.test.js.map +0 -1
  231. package/dist/services/local/replaceObjectBodyRegex.d.ts +0 -8
  232. package/dist/services/local/replaceObjectBodyRegex.d.ts.map +0 -1
  233. package/dist/services/local/replaceObjectBodyRegex.js +0 -85
  234. package/dist/services/local/replaceObjectBodyRegex.js.map +0 -1
  235. package/dist/tools/__tests__/pruneClosedTool.test.d.ts +0 -2
  236. package/dist/tools/__tests__/pruneClosedTool.test.d.ts.map +0 -1
  237. package/dist/tools/__tests__/pruneClosedTool.test.js +0 -112
  238. package/dist/tools/__tests__/pruneClosedTool.test.js.map +0 -1
  239. package/dist/tools/__tests__/replaceObjectBodyRegexTool.test.d.ts +0 -2
  240. package/dist/tools/__tests__/replaceObjectBodyRegexTool.test.d.ts.map +0 -1
  241. package/dist/tools/__tests__/replaceObjectBodyRegexTool.test.js +0 -89
  242. package/dist/tools/__tests__/replaceObjectBodyRegexTool.test.js.map +0 -1
  243. package/dist/tools/pruneClosedTool.d.ts +0 -27
  244. package/dist/tools/pruneClosedTool.d.ts.map +0 -1
  245. package/dist/tools/pruneClosedTool.js +0 -57
  246. package/dist/tools/pruneClosedTool.js.map +0 -1
  247. package/dist/tools/replaceObjectBodyRegexTool.d.ts +0 -36
  248. package/dist/tools/replaceObjectBodyRegexTool.d.ts.map +0 -1
  249. package/dist/tools/replaceObjectBodyRegexTool.js +0 -67
  250. package/dist/tools/replaceObjectBodyRegexTool.js.map +0 -1
  251. package/dist/utils/ReplaceStringOptions.d.ts +0 -12
  252. package/dist/utils/ReplaceStringOptions.d.ts.map +0 -1
  253. package/dist/utils/ReplaceStringOptions.js +0 -3
  254. package/dist/utils/ReplaceStringOptions.js.map +0 -1
  255. package/dist/utils/__tests__/replaceStringWithRegex.test.d.ts +0 -2
  256. package/dist/utils/__tests__/replaceStringWithRegex.test.d.ts.map +0 -1
  257. package/dist/utils/__tests__/replaceStringWithRegex.test.js +0 -281
  258. package/dist/utils/__tests__/replaceStringWithRegex.test.js.map +0 -1
  259. package/dist/utils/replaceStringWithRegex.d.ts +0 -45
  260. package/dist/utils/replaceStringWithRegex.d.ts.map +0 -1
  261. package/dist/utils/replaceStringWithRegex.js +0 -91
  262. package/dist/utils/replaceStringWithRegex.js.map +0 -1
@@ -2,212 +2,472 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const models_1 = require("../../../models");
4
4
  const pruneClosed_1 = require("../pruneClosed");
5
- jest.mock("../../../models/isClosed", () => ({
6
- isClosed: jest.fn(),
7
- }));
8
- const { isClosed } = require("../../../models/isClosed");
9
5
  describe("pruneClosed", () => {
10
6
  let mockRepository;
7
+ const mockDate = new Date("2025-01-15T12:00:00Z");
11
8
  beforeEach(() => {
12
9
  mockRepository = {
13
10
  getObjectById: jest.fn(),
14
11
  getObjects: jest.fn(),
15
12
  saveObject: jest.fn(),
16
13
  deleteObject: jest.fn(),
14
+ getChildrenOf: jest.fn(),
17
15
  };
18
- isClosed.mockClear();
16
+ jest.clearAllMocks();
17
+ jest.useFakeTimers();
18
+ jest.setSystemTime(mockDate);
19
19
  });
20
- const createMockObject = (id, status, updatedTime) => ({
21
- id,
22
- type: models_1.TrellisObjectType.TASK,
23
- title: `Test ${id}`,
24
- status,
25
- priority: models_1.TrellisObjectPriority.MEDIUM,
26
- parent: undefined,
27
- prerequisites: [],
28
- affectedFiles: new Map(),
29
- log: [],
30
- schema: "1.0",
31
- childrenIds: [],
32
- created: new Date().toISOString(),
33
- updated: updatedTime,
34
- body: "",
20
+ afterEach(() => {
21
+ jest.useRealTimers();
35
22
  });
36
- it("should prune old closed objects", async () => {
37
- const oldClosedObject = createMockObject("T-old-closed", models_1.TrellisObjectStatus.DONE, new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString());
38
- mockRepository.getObjects.mockResolvedValue([oldClosedObject]);
39
- isClosed.mockReturnValue(true);
40
- mockRepository.deleteObject.mockResolvedValue(undefined);
41
- const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 60); // 1 hour
42
- expect(mockRepository.getObjects).toHaveBeenCalledWith(true, undefined);
43
- expect(isClosed).toHaveBeenCalledWith(oldClosedObject, 0, [
44
- oldClosedObject,
45
- ]);
46
- expect(mockRepository.deleteObject).toHaveBeenCalledWith("T-old-closed", true);
47
- expect(result.content[0].text).toContain("Pruned 1 closed objects older than 60 minutes");
48
- expect(result.content[0].text).toContain("T-old-closed");
49
- });
50
- it("should not prune recent closed objects", async () => {
51
- const recentClosedObject = createMockObject("T-recent-closed", models_1.TrellisObjectStatus.DONE, new Date(Date.now() - 30 * 60 * 1000).toISOString());
52
- mockRepository.getObjects.mockResolvedValue([recentClosedObject]);
53
- isClosed.mockReturnValue(true);
54
- const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 60); // 1 hour
55
- expect(mockRepository.getObjects).toHaveBeenCalledWith(true, undefined);
56
- expect(isClosed).toHaveBeenCalledWith(recentClosedObject, 0, [
57
- recentClosedObject,
58
- ]);
59
- expect(mockRepository.deleteObject).not.toHaveBeenCalled();
60
- expect(result.content[0].text).toContain("Pruned 0 closed objects older than 60 minutes");
61
- });
62
- it("should not prune open objects", async () => {
63
- const openObject = createMockObject("T-open", models_1.TrellisObjectStatus.IN_PROGRESS, new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString());
64
- mockRepository.getObjects.mockResolvedValue([openObject]);
65
- isClosed.mockReturnValue(false);
66
- const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 60); // 1 hour
67
- expect(mockRepository.getObjects).toHaveBeenCalledWith(true, undefined);
68
- expect(isClosed).toHaveBeenCalledWith(openObject, 0, [openObject]);
69
- expect(mockRepository.deleteObject).not.toHaveBeenCalled();
70
- expect(result.content[0].text).toContain("Pruned 0 closed objects older than 60 minutes");
71
- });
72
- it("should handle scope parameter", async () => {
73
- const oldClosedObject = createMockObject("T-scoped-closed", models_1.TrellisObjectStatus.DONE, new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString());
74
- mockRepository.getObjects.mockResolvedValue([oldClosedObject]);
75
- isClosed.mockReturnValue(true);
76
- mockRepository.deleteObject.mockResolvedValue(undefined);
77
- const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 60, "F-test-feature");
78
- expect(mockRepository.getObjects).toHaveBeenCalledWith(true, "F-test-feature");
79
- expect(result.content[0].text).toContain("in scope F-test-feature");
80
- });
81
- it("should handle mixed object types and statuses", async () => {
82
- const oldDoneTask = createMockObject("T-old-done", models_1.TrellisObjectStatus.DONE, new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString());
83
- const oldWontDoTask = createMockObject("T-old-wont-do", models_1.TrellisObjectStatus.WONT_DO, new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString());
84
- const recentDoneTask = createMockObject("T-recent-done", models_1.TrellisObjectStatus.DONE, new Date(Date.now() - 30 * 60 * 1000).toISOString());
85
- const openTask = createMockObject("T-open", models_1.TrellisObjectStatus.IN_PROGRESS, new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString());
86
- mockRepository.getObjects.mockResolvedValue([
87
- oldDoneTask,
88
- oldWontDoTask,
89
- recentDoneTask,
90
- openTask,
91
- ]);
92
- isClosed.mockImplementation((obj) => obj.status === models_1.TrellisObjectStatus.DONE ||
93
- obj.status === models_1.TrellisObjectStatus.WONT_DO);
94
- mockRepository.deleteObject.mockResolvedValue(undefined);
95
- const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 60);
96
- expect(mockRepository.deleteObject).toHaveBeenCalledTimes(2);
97
- expect(mockRepository.deleteObject).toHaveBeenCalledWith("T-old-done", true);
98
- expect(mockRepository.deleteObject).toHaveBeenCalledWith("T-old-wont-do", true);
99
- expect(result.content[0].text).toContain("Pruned 2 closed objects");
100
- expect(result.content[0].text).toContain("T-old-done");
101
- expect(result.content[0].text).toContain("T-old-wont-do");
102
- });
103
- it("should handle deletion failures gracefully", async () => {
104
- const oldClosedObject1 = createMockObject("T-old-closed-1", models_1.TrellisObjectStatus.DONE, new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString());
105
- const oldClosedObject2 = createMockObject("T-old-closed-2", models_1.TrellisObjectStatus.DONE, new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString());
106
- mockRepository.getObjects.mockResolvedValue([
107
- oldClosedObject1,
108
- oldClosedObject2,
109
- ]);
110
- isClosed.mockReturnValue(true);
111
- // First deletion fails, second succeeds
112
- mockRepository.deleteObject
113
- .mockRejectedValueOnce(new Error("Permission denied"))
114
- .mockResolvedValueOnce(undefined);
115
- // Mock console.error to check error logging
116
- const consoleSpy = jest.spyOn(console, "error").mockImplementation();
117
- const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 60);
118
- expect(mockRepository.deleteObject).toHaveBeenCalledTimes(2);
119
- expect(consoleSpy).toHaveBeenCalledWith("Failed to delete object T-old-closed-1:", expect.any(Error));
120
- expect(result.content[0].text).toContain("Pruned 1 closed objects");
121
- expect(result.content[0].text).toContain("T-old-closed-2");
122
- expect(result.content[0].text).not.toContain("T-old-closed-1");
123
- consoleSpy.mockRestore();
124
- });
125
- it("should handle repository getObjects error", async () => {
126
- const errorMessage = "Repository connection failed";
127
- mockRepository.getObjects.mockRejectedValue(new Error(errorMessage));
128
- const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 60);
129
- expect(result.content[0].text).toContain("Error pruning closed objects");
130
- expect(result.content[0].text).toContain(errorMessage);
131
- });
132
- it("should handle empty object list", async () => {
133
- mockRepository.getObjects.mockResolvedValue([]);
134
- const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 60);
135
- expect(mockRepository.deleteObject).not.toHaveBeenCalled();
136
- expect(result.content[0].text).toContain("Pruned 0 closed objects");
23
+ const createMockObject = (id, status, daysAgo) => {
24
+ const updatedTime = new Date(mockDate.getTime() - daysAgo * 24 * 60 * 60 * 1000);
25
+ return {
26
+ id,
27
+ type: models_1.TrellisObjectType.TASK,
28
+ title: `Test ${id}`,
29
+ status,
30
+ priority: models_1.TrellisObjectPriority.MEDIUM,
31
+ prerequisites: [],
32
+ affectedFiles: new Map(),
33
+ log: [],
34
+ schema: "v1.0",
35
+ childrenIds: [],
36
+ body: "",
37
+ created: updatedTime.toISOString(),
38
+ updated: updatedTime.toISOString(),
39
+ };
40
+ };
41
+ describe("day-based age calculation", () => {
42
+ it("should delete objects older than 1 day", async () => {
43
+ const objects = [
44
+ createMockObject("T-old", models_1.TrellisObjectStatus.DONE, 2), // 2 days old
45
+ createMockObject("T-recent", models_1.TrellisObjectStatus.DONE, 0), // Today
46
+ ];
47
+ mockRepository.getObjects.mockResolvedValue(objects);
48
+ mockRepository.getChildrenOf.mockResolvedValue([]);
49
+ mockRepository.deleteObject.mockResolvedValue();
50
+ const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 1);
51
+ expect(mockRepository.deleteObject).toHaveBeenCalledTimes(1);
52
+ expect(mockRepository.deleteObject).toHaveBeenCalledWith("T-old", true);
53
+ expect(result.content[0].text).toContain("Pruned 1 closed objects older than 1 days");
54
+ expect(result.content[0].text).toContain("Deleted objects: T-old");
55
+ });
56
+ it("should delete objects older than 7 days", async () => {
57
+ const objects = [
58
+ createMockObject("T-very-old", models_1.TrellisObjectStatus.DONE, 10), // 10 days old
59
+ createMockObject("T-old", models_1.TrellisObjectStatus.DONE, 5), // 5 days old
60
+ createMockObject("T-recent", models_1.TrellisObjectStatus.DONE, 1), // 1 day old
61
+ ];
62
+ mockRepository.getObjects.mockResolvedValue(objects);
63
+ mockRepository.getChildrenOf.mockResolvedValue([]);
64
+ mockRepository.deleteObject.mockResolvedValue();
65
+ const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 7);
66
+ expect(mockRepository.deleteObject).toHaveBeenCalledTimes(1);
67
+ expect(mockRepository.deleteObject).toHaveBeenCalledWith("T-very-old", true);
68
+ expect(result.content[0].text).toContain("Pruned 1 closed objects older than 7 days");
69
+ });
70
+ it("should delete objects older than 30 days", async () => {
71
+ const objects = [
72
+ createMockObject("T-ancient", models_1.TrellisObjectStatus.DONE, 45), // 45 days old
73
+ createMockObject("T-old", models_1.TrellisObjectStatus.DONE, 20), // 20 days old
74
+ ];
75
+ mockRepository.getObjects.mockResolvedValue(objects);
76
+ mockRepository.getChildrenOf.mockResolvedValue([]);
77
+ mockRepository.deleteObject.mockResolvedValue();
78
+ const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 30);
79
+ expect(mockRepository.deleteObject).toHaveBeenCalledTimes(1);
80
+ expect(mockRepository.deleteObject).toHaveBeenCalledWith("T-ancient", true);
81
+ expect(result.content[0].text).toContain("Pruned 1 closed objects older than 30 days");
82
+ });
137
83
  });
138
- it("should handle zero age parameter", async () => {
139
- const oldClosedObject = createMockObject("T-old-closed", models_1.TrellisObjectStatus.DONE, new Date(Date.now() - 1000).toISOString());
140
- mockRepository.getObjects.mockResolvedValue([oldClosedObject]);
141
- isClosed.mockReturnValue(true);
142
- mockRepository.deleteObject.mockResolvedValue(undefined);
143
- const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 0);
144
- expect(mockRepository.deleteObject).toHaveBeenCalledWith("T-old-closed", true);
145
- expect(result.content[0].text).toContain("Pruned 1 closed objects older than 0 minutes");
84
+ describe("objects with no children", () => {
85
+ it("should delete closed objects with no children (existing behavior)", async () => {
86
+ const objects = [
87
+ createMockObject("T-done", models_1.TrellisObjectStatus.DONE, 5),
88
+ createMockObject("T-wont-do", models_1.TrellisObjectStatus.WONT_DO, 3),
89
+ ];
90
+ mockRepository.getObjects.mockResolvedValue(objects);
91
+ mockRepository.getChildrenOf.mockResolvedValue([]);
92
+ mockRepository.deleteObject.mockResolvedValue();
93
+ const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 1);
94
+ expect(mockRepository.deleteObject).toHaveBeenCalledTimes(2);
95
+ expect(mockRepository.deleteObject).toHaveBeenCalledWith("T-done", true);
96
+ expect(mockRepository.deleteObject).toHaveBeenCalledWith("T-wont-do", true);
97
+ expect(result.content[0].text).toContain("Pruned 2 closed objects older than 1 days");
98
+ expect(result.content[0].text).toContain("Deleted objects: T-done, T-wont-do");
99
+ });
100
+ it("should not delete open objects", async () => {
101
+ const objects = [
102
+ createMockObject("T-open", models_1.TrellisObjectStatus.OPEN, 5),
103
+ createMockObject("T-in-progress", models_1.TrellisObjectStatus.IN_PROGRESS, 5),
104
+ createMockObject("T-done", models_1.TrellisObjectStatus.DONE, 5),
105
+ ];
106
+ mockRepository.getObjects.mockResolvedValue(objects);
107
+ mockRepository.getChildrenOf.mockResolvedValue([]);
108
+ mockRepository.deleteObject.mockResolvedValue();
109
+ const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 1);
110
+ expect(mockRepository.deleteObject).toHaveBeenCalledTimes(1);
111
+ expect(mockRepository.deleteObject).toHaveBeenCalledWith("T-done", true);
112
+ expect(result.content[0].text).toContain("Pruned 1 closed objects older than 1 days");
113
+ });
146
114
  });
147
- it("should handle large age parameter", async () => {
148
- const veryOldObject = createMockObject("T-ancient", models_1.TrellisObjectStatus.DONE, new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString());
149
- mockRepository.getObjects.mockResolvedValue([veryOldObject]);
150
- isClosed.mockReturnValue(true);
151
- mockRepository.deleteObject.mockResolvedValue(undefined);
152
- const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 999999);
153
- expect(mockRepository.deleteObject).not.toHaveBeenCalled();
154
- expect(result.content[0].text).toContain("Pruned 0 closed objects older than 999999 minutes");
115
+ describe("hierarchical child validation", () => {
116
+ it("should skip closed parent with open children", async () => {
117
+ const parentObject = createMockObject("F-parent", models_1.TrellisObjectStatus.DONE, 5);
118
+ const openChild = createMockObject("T-open-child", models_1.TrellisObjectStatus.OPEN, 1);
119
+ mockRepository.getObjects.mockResolvedValue([parentObject]);
120
+ mockRepository.getChildrenOf.mockResolvedValue([openChild]);
121
+ mockRepository.deleteObject.mockResolvedValue();
122
+ const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 1);
123
+ expect(mockRepository.getChildrenOf).toHaveBeenCalledWith("F-parent", true);
124
+ expect(mockRepository.deleteObject).not.toHaveBeenCalled();
125
+ expect(result.content[0].text).toContain("Pruned 0 closed objects older than 1 days");
126
+ expect(result.content[0].text).toContain("Skipped 1 objects with open children: F-parent");
127
+ });
128
+ it("should delete closed parent with only closed children", async () => {
129
+ const parentObject = createMockObject("F-parent", models_1.TrellisObjectStatus.DONE, 5);
130
+ const closedChild1 = createMockObject("T-done-child", models_1.TrellisObjectStatus.DONE, 3);
131
+ const closedChild2 = createMockObject("T-wont-do-child", models_1.TrellisObjectStatus.WONT_DO, 2);
132
+ mockRepository.getObjects.mockResolvedValue([parentObject]);
133
+ mockRepository.getChildrenOf.mockResolvedValue([
134
+ closedChild1,
135
+ closedChild2,
136
+ ]);
137
+ mockRepository.deleteObject.mockResolvedValue();
138
+ const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 1);
139
+ expect(mockRepository.getChildrenOf).toHaveBeenCalledWith("F-parent", true);
140
+ expect(mockRepository.deleteObject).toHaveBeenCalledTimes(1);
141
+ expect(mockRepository.deleteObject).toHaveBeenCalledWith("F-parent", true);
142
+ expect(result.content[0].text).toContain("Pruned 1 closed objects older than 1 days");
143
+ expect(result.content[0].text).toContain("Deleted objects: F-parent");
144
+ });
145
+ it("should handle mixed scenarios with some parents having open children and others not", async () => {
146
+ const parentWithOpenChild = createMockObject("F-parent-1", models_1.TrellisObjectStatus.DONE, 5);
147
+ const parentWithClosedChildren = createMockObject("F-parent-2", models_1.TrellisObjectStatus.DONE, 5);
148
+ const parentWithNoChildren = createMockObject("T-orphan", models_1.TrellisObjectStatus.DONE, 5);
149
+ const openChild = createMockObject("T-open", models_1.TrellisObjectStatus.OPEN, 1);
150
+ const closedChild = createMockObject("T-closed", models_1.TrellisObjectStatus.DONE, 1);
151
+ mockRepository.getObjects.mockResolvedValue([
152
+ parentWithOpenChild,
153
+ parentWithClosedChildren,
154
+ parentWithNoChildren,
155
+ ]);
156
+ mockRepository.getChildrenOf.mockImplementation((parentId) => {
157
+ if (parentId === "F-parent-1")
158
+ return Promise.resolve([openChild]);
159
+ if (parentId === "F-parent-2")
160
+ return Promise.resolve([closedChild]);
161
+ return Promise.resolve([]); // No children for T-orphan
162
+ });
163
+ mockRepository.deleteObject.mockResolvedValue();
164
+ const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 1);
165
+ // With recursive checking, we call getChildrenOf for each object and their descendants
166
+ expect(mockRepository.getChildrenOf).toHaveBeenCalledTimes(4);
167
+ expect(mockRepository.deleteObject).toHaveBeenCalledTimes(2);
168
+ expect(mockRepository.deleteObject).toHaveBeenCalledWith("F-parent-2", true);
169
+ expect(mockRepository.deleteObject).toHaveBeenCalledWith("T-orphan", true);
170
+ expect(result.content[0].text).toContain("Pruned 2 closed objects older than 1 days");
171
+ expect(result.content[0].text).toContain("Deleted objects: F-parent-2, T-orphan");
172
+ expect(result.content[0].text).toContain("Skipped 1 objects with open children: F-parent-1");
173
+ });
155
174
  });
156
- it("should handle non-Error exceptions", async () => {
157
- const errorValue = "String error";
158
- mockRepository.getObjects.mockRejectedValue(errorValue);
159
- const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 60);
160
- expect(result.content[0].text).toContain("Error pruning closed objects");
161
- expect(result.content[0].text).toContain(errorValue);
175
+ describe("multi-level hierarchies", () => {
176
+ it("should handle grandparent → parent → child hierarchy correctly", async () => {
177
+ const grandparent = createMockObject("E-grandparent", models_1.TrellisObjectStatus.DONE, 5);
178
+ const parent = createMockObject("F-parent", models_1.TrellisObjectStatus.DONE, 4);
179
+ const openChild = createMockObject("T-open-child", models_1.TrellisObjectStatus.OPEN, 1);
180
+ mockRepository.getObjects.mockResolvedValue([grandparent, parent]);
181
+ mockRepository.getChildrenOf.mockImplementation((parentId) => {
182
+ if (parentId === "E-grandparent")
183
+ return Promise.resolve([parent]); // Parent is closed
184
+ if (parentId === "F-parent")
185
+ return Promise.resolve([openChild]); // Child is open
186
+ return Promise.resolve([]);
187
+ });
188
+ mockRepository.deleteObject.mockResolvedValue();
189
+ const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 1);
190
+ // Both grandparent and parent should be skipped because they have open descendants
191
+ expect(mockRepository.deleteObject).toHaveBeenCalledTimes(0);
192
+ expect(result.content[0].text).toContain("Pruned 0 closed objects older than 1 days");
193
+ expect(result.content[0].text).toContain("Skipped 2 objects with open children: E-grandparent, F-parent");
194
+ });
162
195
  });
163
- it("should calculate cutoff time correctly for different age values", async () => {
164
- const currentTime = Date.now();
165
- const testCases = [
166
- { age: 30, expectedCutoff: currentTime - 30 * 60 * 1000 },
167
- { age: 120, expectedCutoff: currentTime - 120 * 60 * 1000 },
168
- { age: 1440, expectedCutoff: currentTime - 1440 * 60 * 1000 }, // 24 hours
169
- ];
170
- for (const { age, expectedCutoff } of testCases) {
171
- const objectBeforeCutoff = createMockObject(`T-before-${age}`, models_1.TrellisObjectStatus.DONE, new Date(expectedCutoff - 1000).toISOString());
172
- const objectAfterCutoff = createMockObject(`T-after-${age}`, models_1.TrellisObjectStatus.DONE, new Date(expectedCutoff + 1000).toISOString());
196
+ describe("deep hierarchy recursive descendant checking", () => {
197
+ it("should protect ancestors when deep descendant is open (4+ levels)", async () => {
198
+ const project = createMockObject("P-project", models_1.TrellisObjectStatus.DONE, 10);
199
+ const epic = createMockObject("E-epic", models_1.TrellisObjectStatus.DONE, 8);
200
+ const feature = createMockObject("F-feature", models_1.TrellisObjectStatus.DONE, 6);
201
+ const task = createMockObject("T-task", models_1.TrellisObjectStatus.DONE, 4);
202
+ const subtask = createMockObject("ST-subtask", models_1.TrellisObjectStatus.OPEN, 2);
173
203
  mockRepository.getObjects.mockResolvedValue([
174
- objectBeforeCutoff,
175
- objectAfterCutoff,
204
+ project,
205
+ epic,
206
+ feature,
207
+ task,
176
208
  ]);
177
- isClosed.mockReturnValue(true);
178
- mockRepository.deleteObject.mockResolvedValue(undefined);
179
- const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, age);
209
+ mockRepository.getChildrenOf.mockImplementation((parentId) => {
210
+ if (parentId === "P-project")
211
+ return Promise.resolve([epic]);
212
+ if (parentId === "E-epic")
213
+ return Promise.resolve([feature]);
214
+ if (parentId === "F-feature")
215
+ return Promise.resolve([task]);
216
+ if (parentId === "T-task")
217
+ return Promise.resolve([subtask]);
218
+ return Promise.resolve([]);
219
+ });
220
+ mockRepository.deleteObject.mockResolvedValue();
221
+ const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 1);
222
+ // All ancestors should be protected due to the open subtask at the deepest level
223
+ expect(mockRepository.deleteObject).toHaveBeenCalledTimes(0);
224
+ expect(result.content[0].text).toContain("Pruned 0 closed objects older than 1 days");
225
+ expect(result.content[0].text).toContain("Skipped 4 objects with open children: P-project, E-epic, F-feature, T-task");
226
+ });
227
+ it("should allow deletion when all deep descendants are closed", async () => {
228
+ const project = createMockObject("P-project", models_1.TrellisObjectStatus.DONE, 10);
229
+ const epic = createMockObject("E-epic", models_1.TrellisObjectStatus.DONE, 8);
230
+ const feature = createMockObject("F-feature", models_1.TrellisObjectStatus.DONE, 6);
231
+ const task1 = createMockObject("T-task1", models_1.TrellisObjectStatus.DONE, 4);
232
+ const task2 = createMockObject("T-task2", models_1.TrellisObjectStatus.WONT_DO, 3);
233
+ const subtask1 = createMockObject("ST-subtask1", models_1.TrellisObjectStatus.DONE, 2);
234
+ const subtask2 = createMockObject("ST-subtask2", models_1.TrellisObjectStatus.WONT_DO, 1);
235
+ mockRepository.getObjects.mockResolvedValue([
236
+ project,
237
+ epic,
238
+ feature,
239
+ task1,
240
+ task2,
241
+ ]);
242
+ mockRepository.getChildrenOf.mockImplementation((parentId) => {
243
+ if (parentId === "P-project")
244
+ return Promise.resolve([epic]);
245
+ if (parentId === "E-epic")
246
+ return Promise.resolve([feature]);
247
+ if (parentId === "F-feature")
248
+ return Promise.resolve([task1, task2]);
249
+ if (parentId === "T-task1")
250
+ return Promise.resolve([subtask1]);
251
+ if (parentId === "T-task2")
252
+ return Promise.resolve([subtask2]);
253
+ return Promise.resolve([]);
254
+ });
255
+ mockRepository.deleteObject.mockResolvedValue();
256
+ const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 1);
257
+ // All objects should be deleted since all descendants are closed
258
+ expect(mockRepository.deleteObject).toHaveBeenCalledTimes(5);
259
+ expect(mockRepository.deleteObject).toHaveBeenCalledWith("P-project", true);
260
+ expect(mockRepository.deleteObject).toHaveBeenCalledWith("E-epic", true);
261
+ expect(mockRepository.deleteObject).toHaveBeenCalledWith("F-feature", true);
262
+ expect(mockRepository.deleteObject).toHaveBeenCalledWith("T-task1", true);
263
+ expect(mockRepository.deleteObject).toHaveBeenCalledWith("T-task2", true);
264
+ expect(result.content[0].text).toContain("Pruned 5 closed objects older than 1 days");
265
+ });
266
+ it("should handle mixed branches - some with open descendants, some without", async () => {
267
+ const epic = createMockObject("E-epic", models_1.TrellisObjectStatus.DONE, 8);
268
+ const feature1 = createMockObject("F-feature1", models_1.TrellisObjectStatus.DONE, 6);
269
+ const feature2 = createMockObject("F-feature2", models_1.TrellisObjectStatus.DONE, 6);
270
+ const task1 = createMockObject("T-task1", models_1.TrellisObjectStatus.OPEN, 4); // Open task
271
+ const task2 = createMockObject("T-task2", models_1.TrellisObjectStatus.DONE, 4); // Closed task
272
+ const subtask = createMockObject("ST-subtask", models_1.TrellisObjectStatus.DONE, 2);
273
+ mockRepository.getObjects.mockResolvedValue([
274
+ epic,
275
+ feature1,
276
+ feature2,
277
+ task2,
278
+ ]);
279
+ mockRepository.getChildrenOf.mockImplementation((parentId) => {
280
+ if (parentId === "E-epic")
281
+ return Promise.resolve([feature1, feature2]);
282
+ if (parentId === "F-feature1")
283
+ return Promise.resolve([task1]); // Branch with open task
284
+ if (parentId === "F-feature2")
285
+ return Promise.resolve([task2]); // Branch with closed task
286
+ if (parentId === "T-task2")
287
+ return Promise.resolve([subtask]);
288
+ return Promise.resolve([]);
289
+ });
290
+ mockRepository.deleteObject.mockResolvedValue();
291
+ const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 1);
292
+ // Epic and feature1 should be protected due to open task1
293
+ // Feature2 and task2 should be deleted (all descendants closed)
294
+ expect(mockRepository.deleteObject).toHaveBeenCalledTimes(2);
295
+ expect(mockRepository.deleteObject).toHaveBeenCalledWith("F-feature2", true);
296
+ expect(mockRepository.deleteObject).toHaveBeenCalledWith("T-task2", true);
297
+ expect(result.content[0].text).toContain("Pruned 2 closed objects older than 1 days");
298
+ expect(result.content[0].text).toContain("Skipped 2 objects with open children: E-epic, F-feature1");
299
+ });
300
+ it("should handle circular reference protection gracefully", async () => {
301
+ const obj1 = createMockObject("T-obj1", models_1.TrellisObjectStatus.DONE, 5);
302
+ const obj2 = createMockObject("T-obj2", models_1.TrellisObjectStatus.DONE, 5);
303
+ mockRepository.getObjects.mockResolvedValue([obj1, obj2]);
304
+ // Create circular reference: obj1 -> obj2 -> obj1
305
+ mockRepository.getChildrenOf.mockImplementation((parentId) => {
306
+ if (parentId === "T-obj1")
307
+ return Promise.resolve([obj2]);
308
+ if (parentId === "T-obj2")
309
+ return Promise.resolve([obj1]);
310
+ return Promise.resolve([]);
311
+ });
312
+ mockRepository.deleteObject.mockResolvedValue();
313
+ const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 1);
314
+ // Should complete without infinite loop and delete both objects
315
+ // (since they're all closed, the circular reference shouldn't prevent deletion)
316
+ expect(mockRepository.deleteObject).toHaveBeenCalledTimes(2);
317
+ expect(result.content[0].text).toContain("Pruned 2 closed objects older than 1 days");
318
+ });
319
+ it("should handle empty hierarchy levels gracefully", async () => {
320
+ const parent = createMockObject("F-parent", models_1.TrellisObjectStatus.DONE, 5);
321
+ const middleChild = createMockObject("T-middle", models_1.TrellisObjectStatus.DONE, 4);
322
+ const leafChild = createMockObject("ST-leaf", models_1.TrellisObjectStatus.OPEN, 2);
323
+ mockRepository.getObjects.mockResolvedValue([parent, middleChild]);
324
+ mockRepository.getChildrenOf.mockImplementation((parentId) => {
325
+ if (parentId === "F-parent")
326
+ return Promise.resolve([middleChild]);
327
+ if (parentId === "T-middle")
328
+ return Promise.resolve([leafChild]);
329
+ return Promise.resolve([]);
330
+ });
331
+ mockRepository.deleteObject.mockResolvedValue();
332
+ const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 1);
333
+ // Both parent and middle should be protected due to open leaf
334
+ expect(mockRepository.deleteObject).toHaveBeenCalledTimes(0);
335
+ expect(result.content[0].text).toContain("Skipped 2 objects with open children: F-parent, T-middle");
336
+ });
337
+ it("should handle child query errors gracefully in recursive checking", async () => {
338
+ const parent = createMockObject("F-parent", models_1.TrellisObjectStatus.DONE, 5);
339
+ const failingChild = createMockObject("T-failing", models_1.TrellisObjectStatus.DONE, 4);
340
+ mockRepository.getObjects.mockResolvedValue([parent]);
341
+ mockRepository.getChildrenOf.mockImplementation((parentId) => {
342
+ if (parentId === "F-parent")
343
+ return Promise.resolve([failingChild]);
344
+ if (parentId === "T-failing")
345
+ return Promise.reject(new Error("Query failed"));
346
+ return Promise.resolve([]);
347
+ });
348
+ mockRepository.deleteObject.mockResolvedValue();
349
+ const consoleSpy = jest.spyOn(console, "error").mockImplementation();
350
+ const _result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 1);
351
+ // Should handle error gracefully and proceed with deletion
352
+ // (when we can't check descendants, we err on the side of deletion)
353
+ expect(consoleSpy).toHaveBeenCalledWith("Error checking descendants for T-failing:", expect.any(Error));
180
354
  expect(mockRepository.deleteObject).toHaveBeenCalledTimes(1);
181
- expect(mockRepository.deleteObject).toHaveBeenCalledWith(`T-before-${age}`, true);
182
- expect(result.content[0].text).toContain("Pruned 1 closed objects");
183
- // Reset mocks for next iteration
184
- mockRepository.deleteObject.mockClear();
185
- }
355
+ expect(mockRepository.deleteObject).toHaveBeenCalledWith("F-parent", true);
356
+ consoleSpy.mockRestore();
357
+ });
358
+ });
359
+ describe("error handling", () => {
360
+ it("should handle errors when child queries fail gracefully", async () => {
361
+ const objects = [
362
+ createMockObject("F-parent-1", models_1.TrellisObjectStatus.DONE, 5),
363
+ createMockObject("F-parent-2", models_1.TrellisObjectStatus.DONE, 5),
364
+ ];
365
+ mockRepository.getObjects.mockResolvedValue(objects);
366
+ mockRepository.getChildrenOf.mockImplementation((parentId) => {
367
+ if (parentId === "F-parent-1") {
368
+ return Promise.reject(new Error("Failed to query children"));
369
+ }
370
+ return Promise.resolve([]); // No children for parent-2
371
+ });
372
+ mockRepository.deleteObject.mockResolvedValue();
373
+ const consoleSpy = jest.spyOn(console, "error").mockImplementation();
374
+ const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 1);
375
+ // Should continue processing other objects even if one child query fails
376
+ // With recursive checking, parent-1 gets deleted too (error in descendant check is handled gracefully)
377
+ expect(mockRepository.deleteObject).toHaveBeenCalledTimes(2);
378
+ expect(mockRepository.deleteObject).toHaveBeenCalledWith("F-parent-1", true);
379
+ expect(mockRepository.deleteObject).toHaveBeenCalledWith("F-parent-2", true);
380
+ expect(consoleSpy).toHaveBeenCalledWith("Error checking descendants for F-parent-1:", expect.any(Error));
381
+ expect(result.content[0].text).toContain("Pruned 2 closed objects older than 1 days");
382
+ consoleSpy.mockRestore();
383
+ });
384
+ it("should handle repository deletion errors gracefully", async () => {
385
+ const objects = [
386
+ createMockObject("T-fail", models_1.TrellisObjectStatus.DONE, 5),
387
+ createMockObject("T-success", models_1.TrellisObjectStatus.DONE, 5),
388
+ ];
389
+ mockRepository.getObjects.mockResolvedValue(objects);
390
+ mockRepository.getChildrenOf.mockResolvedValue([]);
391
+ mockRepository.deleteObject.mockImplementation((id) => {
392
+ if (id === "T-fail") {
393
+ return Promise.reject(new Error("Deletion failed"));
394
+ }
395
+ return Promise.resolve();
396
+ });
397
+ const consoleSpy = jest.spyOn(console, "error").mockImplementation();
398
+ const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 1);
399
+ expect(mockRepository.deleteObject).toHaveBeenCalledTimes(2);
400
+ expect(consoleSpy).toHaveBeenCalledWith("Failed to delete object T-fail:", expect.any(Error));
401
+ expect(result.content[0].text).toContain("Pruned 1 closed objects older than 1 days");
402
+ expect(result.content[0].text).toContain("Deleted objects: T-success");
403
+ consoleSpy.mockRestore();
404
+ });
405
+ it("should handle repository getObjects error gracefully", async () => {
406
+ mockRepository.getObjects.mockRejectedValue(new Error("Failed to fetch objects"));
407
+ const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 1);
408
+ expect(result.content[0].text).toContain("Error pruning closed objects: Failed to fetch objects");
409
+ expect(mockRepository.deleteObject).not.toHaveBeenCalled();
410
+ });
186
411
  });
187
- it("should handle objects with different timestamps", async () => {
188
- const objects = [
189
- createMockObject("T-1-hour-ago", models_1.TrellisObjectStatus.DONE, new Date(Date.now() - 60 * 60 * 1000).toISOString()),
190
- createMockObject("T-2-hours-ago", models_1.TrellisObjectStatus.DONE, new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString()),
191
- createMockObject("T-30-minutes-ago", models_1.TrellisObjectStatus.DONE, new Date(Date.now() - 30 * 60 * 1000).toISOString()),
192
- ];
193
- mockRepository.getObjects.mockResolvedValue(objects);
194
- isClosed.mockReturnValue(true);
195
- mockRepository.deleteObject.mockResolvedValue(undefined);
196
- const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 90); // 1.5 hours
197
- expect(mockRepository.deleteObject).toHaveBeenCalledTimes(1);
198
- expect(mockRepository.deleteObject).toHaveBeenCalledWith("T-2-hours-ago", true);
199
- expect(result.content[0].text).toContain("Pruned 1 closed objects");
200
- expect(result.content[0].text).toContain("T-2-hours-ago");
412
+ describe("scope filtering", () => {
413
+ it("should include scope in message when provided", async () => {
414
+ const objects = [
415
+ createMockObject("T-scoped", models_1.TrellisObjectStatus.DONE, 5),
416
+ ];
417
+ mockRepository.getObjects.mockResolvedValue(objects);
418
+ mockRepository.getChildrenOf.mockResolvedValue([]);
419
+ mockRepository.deleteObject.mockResolvedValue();
420
+ const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 1, "P-test-project");
421
+ expect(mockRepository.getObjects).toHaveBeenCalledWith(true, "P-test-project");
422
+ expect(result.content[0].text).toContain("Pruned 1 closed objects older than 1 days in scope P-test-project");
423
+ });
424
+ it("should handle empty results with scope", async () => {
425
+ mockRepository.getObjects.mockResolvedValue([]);
426
+ const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 1, "P-empty-project");
427
+ expect(result.content[0].text).toContain("Pruned 0 closed objects older than 1 days in scope P-empty-project");
428
+ });
201
429
  });
202
- it("should handle bulk deletion scenarios", async () => {
203
- const objectCount = 50;
204
- const objects = Array.from({ length: objectCount }, (_, i) => createMockObject(`T-bulk-${i}`, models_1.TrellisObjectStatus.DONE, new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString()));
205
- mockRepository.getObjects.mockResolvedValue(objects);
206
- isClosed.mockReturnValue(true);
207
- mockRepository.deleteObject.mockResolvedValue(undefined);
208
- const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 60);
209
- expect(mockRepository.deleteObject).toHaveBeenCalledTimes(objectCount);
210
- expect(result.content[0].text).toContain(`Pruned ${objectCount} closed objects`);
430
+ describe("edge cases", () => {
431
+ it("should handle objects with no updated timestamp gracefully", async () => {
432
+ const objectWithoutTimestamp = {
433
+ ...createMockObject("T-no-timestamp", models_1.TrellisObjectStatus.DONE, 0),
434
+ updated: "", // Invalid timestamp
435
+ };
436
+ mockRepository.getObjects.mockResolvedValue([objectWithoutTimestamp]);
437
+ mockRepository.getChildrenOf.mockResolvedValue([]);
438
+ mockRepository.deleteObject.mockResolvedValue();
439
+ const _result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 1);
440
+ // Invalid date comparisons result in false, so object won't be considered old enough
441
+ expect(mockRepository.deleteObject).not.toHaveBeenCalled();
442
+ });
443
+ it("should return disabled message for zero age threshold", async () => {
444
+ const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 0);
445
+ // Should return disabled message without calling any repository methods
446
+ expect(mockRepository.getObjects).not.toHaveBeenCalled();
447
+ expect(mockRepository.getChildrenOf).not.toHaveBeenCalled();
448
+ expect(mockRepository.deleteObject).not.toHaveBeenCalled();
449
+ expect(result.content[0].text).toBe("Auto-prune disabled (threshold: 0 days)");
450
+ });
451
+ it("should return disabled message for negative age threshold", async () => {
452
+ const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, -1);
453
+ // Should return disabled message without calling any repository methods
454
+ expect(mockRepository.getObjects).not.toHaveBeenCalled();
455
+ expect(mockRepository.getChildrenOf).not.toHaveBeenCalled();
456
+ expect(mockRepository.deleteObject).not.toHaveBeenCalled();
457
+ expect(result.content[0].text).toBe("Auto-prune disabled (threshold: -1 days)");
458
+ });
459
+ it("should include scope in disabled message when provided", async () => {
460
+ const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 0, "P-test-project");
461
+ expect(mockRepository.getObjects).not.toHaveBeenCalled();
462
+ expect(result.content[0].text).toBe("Auto-prune disabled (threshold: 0 days) in scope P-test-project");
463
+ });
464
+ it("should handle empty object list", async () => {
465
+ mockRepository.getObjects.mockResolvedValue([]);
466
+ const result = await (0, pruneClosed_1.pruneClosed)(mockRepository, 1);
467
+ expect(mockRepository.getChildrenOf).not.toHaveBeenCalled();
468
+ expect(mockRepository.deleteObject).not.toHaveBeenCalled();
469
+ expect(result.content[0].text).toContain("Pruned 0 closed objects older than 1 days");
470
+ });
211
471
  });
212
472
  });
213
473
  //# sourceMappingURL=pruneClosed.test.js.map