specforge-mcp 0.12.0 → 0.13.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 (258) hide show
  1. package/dist/index.js +0 -0
  2. package/package.json +1 -1
  3. package/dist/engine/agent-generator.test.d.ts +0 -2
  4. package/dist/engine/agent-generator.test.d.ts.map +0 -1
  5. package/dist/engine/agent-generator.test.js +0 -556
  6. package/dist/engine/agent-generator.test.js.map +0 -1
  7. package/dist/engine/analyzer.test.d.ts +0 -2
  8. package/dist/engine/analyzer.test.d.ts.map +0 -1
  9. package/dist/engine/analyzer.test.js +0 -1461
  10. package/dist/engine/analyzer.test.js.map +0 -1
  11. package/dist/engine/auditor.test.d.ts +0 -2
  12. package/dist/engine/auditor.test.d.ts.map +0 -1
  13. package/dist/engine/auditor.test.js +0 -2075
  14. package/dist/engine/auditor.test.js.map +0 -1
  15. package/dist/engine/doc-generator.test.d.ts +0 -2
  16. package/dist/engine/doc-generator.test.d.ts.map +0 -1
  17. package/dist/engine/doc-generator.test.js +0 -961
  18. package/dist/engine/doc-generator.test.js.map +0 -1
  19. package/dist/engine/estimator.test.d.ts +0 -2
  20. package/dist/engine/estimator.test.d.ts.map +0 -1
  21. package/dist/engine/estimator.test.js +0 -334
  22. package/dist/engine/estimator.test.js.map +0 -1
  23. package/dist/engine/skill-generator.test.d.ts +0 -2
  24. package/dist/engine/skill-generator.test.d.ts.map +0 -1
  25. package/dist/engine/skill-generator.test.js +0 -742
  26. package/dist/engine/skill-generator.test.js.map +0 -1
  27. package/dist/engine/validator.test.d.ts +0 -2
  28. package/dist/engine/validator.test.d.ts.map +0 -1
  29. package/dist/engine/validator.test.js +0 -2371
  30. package/dist/engine/validator.test.js.map +0 -1
  31. package/dist/engine/web-fetcher.test.d.ts +0 -2
  32. package/dist/engine/web-fetcher.test.d.ts.map +0 -1
  33. package/dist/engine/web-fetcher.test.js +0 -360
  34. package/dist/engine/web-fetcher.test.js.map +0 -1
  35. package/dist/i18n/index.test.d.ts +0 -2
  36. package/dist/i18n/index.test.d.ts.map +0 -1
  37. package/dist/i18n/index.test.js +0 -375
  38. package/dist/i18n/index.test.js.map +0 -1
  39. package/dist/index.test.d.ts +0 -2
  40. package/dist/index.test.d.ts.map +0 -1
  41. package/dist/index.test.js +0 -124
  42. package/dist/index.test.js.map +0 -1
  43. package/dist/resources/patterns.test.d.ts +0 -2
  44. package/dist/resources/patterns.test.d.ts.map +0 -1
  45. package/dist/resources/patterns.test.js +0 -142
  46. package/dist/resources/patterns.test.js.map +0 -1
  47. package/dist/resources/process.test.d.ts +0 -2
  48. package/dist/resources/process.test.d.ts.map +0 -1
  49. package/dist/resources/process.test.js +0 -48
  50. package/dist/resources/process.test.js.map +0 -1
  51. package/dist/resources/registry.test.d.ts +0 -2
  52. package/dist/resources/registry.test.d.ts.map +0 -1
  53. package/dist/resources/registry.test.js +0 -138
  54. package/dist/resources/registry.test.js.map +0 -1
  55. package/dist/resources/specs.test.d.ts +0 -2
  56. package/dist/resources/specs.test.d.ts.map +0 -1
  57. package/dist/resources/specs.test.js +0 -130
  58. package/dist/resources/specs.test.js.map +0 -1
  59. package/dist/resources/templates.test.d.ts +0 -2
  60. package/dist/resources/templates.test.d.ts.map +0 -1
  61. package/dist/resources/templates.test.js +0 -119
  62. package/dist/resources/templates.test.js.map +0 -1
  63. package/dist/smoke.test.d.ts +0 -2
  64. package/dist/smoke.test.d.ts.map +0 -1
  65. package/dist/smoke.test.js +0 -229
  66. package/dist/smoke.test.js.map +0 -1
  67. package/dist/storage/base-store.test.d.ts +0 -2
  68. package/dist/storage/base-store.test.d.ts.map +0 -1
  69. package/dist/storage/base-store.test.js +0 -180
  70. package/dist/storage/base-store.test.js.map +0 -1
  71. package/dist/storage/global-store.test.d.ts +0 -2
  72. package/dist/storage/global-store.test.d.ts.map +0 -1
  73. package/dist/storage/global-store.test.js +0 -327
  74. package/dist/storage/global-store.test.js.map +0 -1
  75. package/dist/storage/index.test.d.ts +0 -2
  76. package/dist/storage/index.test.d.ts.map +0 -1
  77. package/dist/storage/index.test.js +0 -56
  78. package/dist/storage/index.test.js.map +0 -1
  79. package/dist/storage/knowledge-store.test.d.ts +0 -2
  80. package/dist/storage/knowledge-store.test.d.ts.map +0 -1
  81. package/dist/storage/knowledge-store.test.js +0 -368
  82. package/dist/storage/knowledge-store.test.js.map +0 -1
  83. package/dist/storage/metrics-store.test.d.ts +0 -2
  84. package/dist/storage/metrics-store.test.d.ts.map +0 -1
  85. package/dist/storage/metrics-store.test.js +0 -212
  86. package/dist/storage/metrics-store.test.js.map +0 -1
  87. package/dist/storage/pattern-store.test.d.ts +0 -2
  88. package/dist/storage/pattern-store.test.d.ts.map +0 -1
  89. package/dist/storage/pattern-store.test.js +0 -224
  90. package/dist/storage/pattern-store.test.js.map +0 -1
  91. package/dist/storage/spec-store.test.d.ts +0 -2
  92. package/dist/storage/spec-store.test.d.ts.map +0 -1
  93. package/dist/storage/spec-store.test.js +0 -227
  94. package/dist/storage/spec-store.test.js.map +0 -1
  95. package/dist/tools/audit.test.d.ts +0 -2
  96. package/dist/tools/audit.test.d.ts.map +0 -1
  97. package/dist/tools/audit.test.js +0 -169
  98. package/dist/tools/audit.test.js.map +0 -1
  99. package/dist/tools/challenge-spec.test.d.ts +0 -2
  100. package/dist/tools/challenge-spec.test.d.ts.map +0 -1
  101. package/dist/tools/challenge-spec.test.js +0 -782
  102. package/dist/tools/challenge-spec.test.js.map +0 -1
  103. package/dist/tools/check-versions.test.d.ts +0 -2
  104. package/dist/tools/check-versions.test.d.ts.map +0 -1
  105. package/dist/tools/check-versions.test.js +0 -214
  106. package/dist/tools/check-versions.test.js.map +0 -1
  107. package/dist/tools/clarify-requirements.test.d.ts +0 -2
  108. package/dist/tools/clarify-requirements.test.d.ts.map +0 -1
  109. package/dist/tools/clarify-requirements.test.js +0 -161
  110. package/dist/tools/clarify-requirements.test.js.map +0 -1
  111. package/dist/tools/consult-docs.test.d.ts +0 -2
  112. package/dist/tools/consult-docs.test.d.ts.map +0 -1
  113. package/dist/tools/consult-docs.test.js +0 -140
  114. package/dist/tools/consult-docs.test.js.map +0 -1
  115. package/dist/tools/create-spec.test.d.ts +0 -2
  116. package/dist/tools/create-spec.test.d.ts.map +0 -1
  117. package/dist/tools/create-spec.test.js +0 -233
  118. package/dist/tools/create-spec.test.js.map +0 -1
  119. package/dist/tools/define-ui-contract.test.d.ts +0 -2
  120. package/dist/tools/define-ui-contract.test.d.ts.map +0 -1
  121. package/dist/tools/define-ui-contract.test.js +0 -479
  122. package/dist/tools/define-ui-contract.test.js.map +0 -1
  123. package/dist/tools/design-schema.test.d.ts +0 -2
  124. package/dist/tools/design-schema.test.d.ts.map +0 -1
  125. package/dist/tools/design-schema.test.js +0 -301
  126. package/dist/tools/design-schema.test.js.map +0 -1
  127. package/dist/tools/detect-agent.test.d.ts +0 -2
  128. package/dist/tools/detect-agent.test.d.ts.map +0 -1
  129. package/dist/tools/detect-agent.test.js +0 -133
  130. package/dist/tools/detect-agent.test.js.map +0 -1
  131. package/dist/tools/detect-drift.test.d.ts +0 -2
  132. package/dist/tools/detect-drift.test.d.ts.map +0 -1
  133. package/dist/tools/detect-drift.test.js +0 -312
  134. package/dist/tools/detect-drift.test.js.map +0 -1
  135. package/dist/tools/discover-mcps.test.d.ts +0 -2
  136. package/dist/tools/discover-mcps.test.d.ts.map +0 -1
  137. package/dist/tools/discover-mcps.test.js +0 -345
  138. package/dist/tools/discover-mcps.test.js.map +0 -1
  139. package/dist/tools/estimate.test.d.ts +0 -2
  140. package/dist/tools/estimate.test.d.ts.map +0 -1
  141. package/dist/tools/estimate.test.js +0 -137
  142. package/dist/tools/estimate.test.js.map +0 -1
  143. package/dist/tools/generate-adr.test.d.ts +0 -2
  144. package/dist/tools/generate-adr.test.d.ts.map +0 -1
  145. package/dist/tools/generate-adr.test.js +0 -206
  146. package/dist/tools/generate-adr.test.js.map +0 -1
  147. package/dist/tools/generate-checklist.test.d.ts +0 -2
  148. package/dist/tools/generate-checklist.test.d.ts.map +0 -1
  149. package/dist/tools/generate-checklist.test.js +0 -201
  150. package/dist/tools/generate-checklist.test.js.map +0 -1
  151. package/dist/tools/generate-docs.test.d.ts +0 -2
  152. package/dist/tools/generate-docs.test.d.ts.map +0 -1
  153. package/dist/tools/generate-docs.test.js +0 -183
  154. package/dist/tools/generate-docs.test.js.map +0 -1
  155. package/dist/tools/generate-execution-plan.test.d.ts +0 -2
  156. package/dist/tools/generate-execution-plan.test.d.ts.map +0 -1
  157. package/dist/tools/generate-execution-plan.test.js +0 -643
  158. package/dist/tools/generate-execution-plan.test.js.map +0 -1
  159. package/dist/tools/generate-rules.test.d.ts +0 -2
  160. package/dist/tools/generate-rules.test.d.ts.map +0 -1
  161. package/dist/tools/generate-rules.test.js +0 -148
  162. package/dist/tools/generate-rules.test.js.map +0 -1
  163. package/dist/tools/generate-skill.test.d.ts +0 -2
  164. package/dist/tools/generate-skill.test.d.ts.map +0 -1
  165. package/dist/tools/generate-skill.test.js +0 -138
  166. package/dist/tools/generate-skill.test.js.map +0 -1
  167. package/dist/tools/generate-sub-agent.test.d.ts +0 -2
  168. package/dist/tools/generate-sub-agent.test.d.ts.map +0 -1
  169. package/dist/tools/generate-sub-agent.test.js +0 -162
  170. package/dist/tools/generate-sub-agent.test.js.map +0 -1
  171. package/dist/tools/generate-tests.test.d.ts +0 -2
  172. package/dist/tools/generate-tests.test.d.ts.map +0 -1
  173. package/dist/tools/generate-tests.test.js +0 -222
  174. package/dist/tools/generate-tests.test.js.map +0 -1
  175. package/dist/tools/init-constitution.test.d.ts +0 -2
  176. package/dist/tools/init-constitution.test.d.ts.map +0 -1
  177. package/dist/tools/init-constitution.test.js +0 -398
  178. package/dist/tools/init-constitution.test.js.map +0 -1
  179. package/dist/tools/init-project.test.d.ts +0 -2
  180. package/dist/tools/init-project.test.d.ts.map +0 -1
  181. package/dist/tools/init-project.test.js +0 -158
  182. package/dist/tools/init-project.test.js.map +0 -1
  183. package/dist/tools/integrate-pm.test.d.ts +0 -2
  184. package/dist/tools/integrate-pm.test.d.ts.map +0 -1
  185. package/dist/tools/integrate-pm.test.js +0 -558
  186. package/dist/tools/integrate-pm.test.js.map +0 -1
  187. package/dist/tools/learn.test.d.ts +0 -2
  188. package/dist/tools/learn.test.d.ts.map +0 -1
  189. package/dist/tools/learn.test.js +0 -123
  190. package/dist/tools/learn.test.js.map +0 -1
  191. package/dist/tools/list-specs.test.d.ts +0 -2
  192. package/dist/tools/list-specs.test.d.ts.map +0 -1
  193. package/dist/tools/list-specs.test.js +0 -110
  194. package/dist/tools/list-specs.test.js.map +0 -1
  195. package/dist/tools/manage-context.test.d.ts +0 -2
  196. package/dist/tools/manage-context.test.d.ts.map +0 -1
  197. package/dist/tools/manage-context.test.js +0 -359
  198. package/dist/tools/manage-context.test.js.map +0 -1
  199. package/dist/tools/manage-git.test.d.ts +0 -2
  200. package/dist/tools/manage-git.test.d.ts.map +0 -1
  201. package/dist/tools/manage-git.test.js +0 -882
  202. package/dist/tools/manage-git.test.js.map +0 -1
  203. package/dist/tools/orchestrate.test.d.ts +0 -2
  204. package/dist/tools/orchestrate.test.d.ts.map +0 -1
  205. package/dist/tools/orchestrate.test.js +0 -1117
  206. package/dist/tools/orchestrate.test.js.map +0 -1
  207. package/dist/tools/reconcile-spec.test.d.ts +0 -2
  208. package/dist/tools/reconcile-spec.test.d.ts.map +0 -1
  209. package/dist/tools/reconcile-spec.test.js +0 -259
  210. package/dist/tools/reconcile-spec.test.js.map +0 -1
  211. package/dist/tools/register-platform-tools.test.d.ts +0 -2
  212. package/dist/tools/register-platform-tools.test.d.ts.map +0 -1
  213. package/dist/tools/register-platform-tools.test.js +0 -404
  214. package/dist/tools/register-platform-tools.test.js.map +0 -1
  215. package/dist/tools/register-spec-tools.test.d.ts +0 -2
  216. package/dist/tools/register-spec-tools.test.d.ts.map +0 -1
  217. package/dist/tools/register-spec-tools.test.js +0 -407
  218. package/dist/tools/register-spec-tools.test.js.map +0 -1
  219. package/dist/tools/reverse-engineer.test.d.ts +0 -2
  220. package/dist/tools/reverse-engineer.test.d.ts.map +0 -1
  221. package/dist/tools/reverse-engineer.test.js +0 -206
  222. package/dist/tools/reverse-engineer.test.js.map +0 -1
  223. package/dist/tools/schemas.d.ts +0 -20
  224. package/dist/tools/schemas.d.ts.map +0 -1
  225. package/dist/tools/schemas.js +0 -133
  226. package/dist/tools/schemas.js.map +0 -1
  227. package/dist/tools/schemas.test.d.ts +0 -2
  228. package/dist/tools/schemas.test.d.ts.map +0 -1
  229. package/dist/tools/schemas.test.js +0 -245
  230. package/dist/tools/schemas.test.js.map +0 -1
  231. package/dist/tools/set-locale.test.d.ts +0 -2
  232. package/dist/tools/set-locale.test.d.ts.map +0 -1
  233. package/dist/tools/set-locale.test.js +0 -74
  234. package/dist/tools/set-locale.test.js.map +0 -1
  235. package/dist/tools/suggest-mcps.test.d.ts +0 -2
  236. package/dist/tools/suggest-mcps.test.d.ts.map +0 -1
  237. package/dist/tools/suggest-mcps.test.js +0 -198
  238. package/dist/tools/suggest-mcps.test.js.map +0 -1
  239. package/dist/tools/suggest-stack.test.d.ts +0 -2
  240. package/dist/tools/suggest-stack.test.d.ts.map +0 -1
  241. package/dist/tools/suggest-stack.test.js +0 -181
  242. package/dist/tools/suggest-stack.test.js.map +0 -1
  243. package/dist/tools/suggest-tooling.test.d.ts +0 -2
  244. package/dist/tools/suggest-tooling.test.d.ts.map +0 -1
  245. package/dist/tools/suggest-tooling.test.js +0 -213
  246. package/dist/tools/suggest-tooling.test.js.map +0 -1
  247. package/dist/tools/summarize-spec.test.d.ts +0 -2
  248. package/dist/tools/summarize-spec.test.d.ts.map +0 -1
  249. package/dist/tools/summarize-spec.test.js +0 -180
  250. package/dist/tools/summarize-spec.test.js.map +0 -1
  251. package/dist/tools/update-status.test.d.ts +0 -2
  252. package/dist/tools/update-status.test.d.ts.map +0 -1
  253. package/dist/tools/update-status.test.js +0 -142
  254. package/dist/tools/update-status.test.js.map +0 -1
  255. package/dist/tools/validate.test.d.ts +0 -2
  256. package/dist/tools/validate.test.d.ts.map +0 -1
  257. package/dist/tools/validate.test.js +0 -137
  258. package/dist/tools/validate.test.js.map +0 -1
@@ -1,1117 +0,0 @@
1
- // Tests for handleOrchestrate
2
- import { describe, it, expect, vi, beforeEach } from 'vitest';
3
- vi.mock('../storage/base-store.js', () => ({
4
- readJson: vi.fn(),
5
- writeJson: vi.fn(),
6
- projectDataDir: (projectId) => `data/projects/${projectId}`,
7
- }));
8
- vi.mock('../storage/index.js', () => ({
9
- specStore: { listSpecs: vi.fn() },
10
- }));
11
- vi.mock('node:crypto', () => ({
12
- randomUUID: () => 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
13
- }));
14
- import { handleOrchestrate } from './orchestrate.js';
15
- import { readJson, writeJson } from '../storage/base-store.js';
16
- import { specStore } from '../storage/index.js';
17
- const mockReadJson = vi.mocked(readJson);
18
- const mockWriteJson = vi.mocked(writeJson);
19
- const mockListSpecs = vi.mocked(specStore.listSpecs);
20
- function emptyState() {
21
- return { sessions: [], locks: [], conflicts: [] };
22
- }
23
- beforeEach(() => {
24
- vi.clearAllMocks();
25
- mockWriteJson.mockResolvedValue(undefined);
26
- });
27
- describe('handleOrchestrate', () => {
28
- // === register ===
29
- it('should register a new agent session', async () => {
30
- mockReadJson.mockResolvedValue(emptyState());
31
- const result = await handleOrchestrate({
32
- projectId: 'proj-1',
33
- action: 'register',
34
- });
35
- expect(result.isError).toBeUndefined();
36
- const data = JSON.parse(result.content[0].text);
37
- expect(data.action).toBe('register');
38
- expect(data.sessionId).toBeDefined();
39
- expect(data.sessions).toHaveLength(1);
40
- expect(mockWriteJson).toHaveBeenCalled();
41
- });
42
- it('should register with custom sessionId and specId', async () => {
43
- mockReadJson.mockResolvedValue(emptyState());
44
- const result = await handleOrchestrate({
45
- projectId: 'proj-1',
46
- action: 'register',
47
- sessionId: 'my-agent',
48
- specId: 'SPEC-001',
49
- });
50
- const data = JSON.parse(result.content[0].text);
51
- expect(data.sessionId).toBe('my-agent');
52
- expect(data.sessions[0].activeSpecs).toContain('SPEC-001');
53
- });
54
- it('should re-activate existing session', async () => {
55
- mockReadJson.mockResolvedValue({
56
- sessions: [{
57
- sessionId: 'agent-1',
58
- agentPlatform: 'custom',
59
- projectId: 'proj-1',
60
- activeSpecs: [],
61
- lockedResources: [],
62
- startedAt: '2025-01-01T00:00:00Z',
63
- lastActivity: '2025-01-01T00:00:00Z',
64
- status: 'disconnected',
65
- }],
66
- locks: [],
67
- conflicts: [],
68
- });
69
- const result = await handleOrchestrate({
70
- projectId: 'proj-1',
71
- action: 'register',
72
- sessionId: 'agent-1',
73
- });
74
- const data = JSON.parse(result.content[0].text);
75
- expect(data.message).toContain('Re-activated');
76
- });
77
- it('should detect spec overlap conflict on register', async () => {
78
- mockReadJson.mockResolvedValue({
79
- sessions: [{
80
- sessionId: 'agent-1',
81
- agentPlatform: 'custom',
82
- projectId: 'proj-1',
83
- activeSpecs: ['SPEC-001'],
84
- lockedResources: [],
85
- startedAt: '2025-01-01T00:00:00Z',
86
- lastActivity: new Date().toISOString(),
87
- status: 'active',
88
- }],
89
- locks: [],
90
- conflicts: [],
91
- });
92
- const result = await handleOrchestrate({
93
- projectId: 'proj-1',
94
- action: 'register',
95
- sessionId: 'agent-2',
96
- specId: 'SPEC-001',
97
- });
98
- const data = JSON.parse(result.content[0].text);
99
- expect(data.conflicts).toBeDefined();
100
- expect(data.conflicts).toHaveLength(1);
101
- expect(data.conflicts[0].type).toBe('spec-overlap');
102
- });
103
- // === heartbeat ===
104
- it('should error when heartbeat called without sessionId', async () => {
105
- const result = await handleOrchestrate({
106
- projectId: 'proj-1',
107
- action: 'heartbeat',
108
- });
109
- expect(result.isError).toBe(true);
110
- });
111
- it('should error when heartbeat for unknown session', async () => {
112
- mockReadJson.mockResolvedValue(emptyState());
113
- const result = await handleOrchestrate({
114
- projectId: 'proj-1',
115
- action: 'heartbeat',
116
- sessionId: 'unknown',
117
- });
118
- expect(result.isError).toBe(true);
119
- });
120
- it('should update heartbeat successfully', async () => {
121
- mockReadJson.mockResolvedValue({
122
- sessions: [{
123
- sessionId: 'agent-1',
124
- agentPlatform: 'custom',
125
- projectId: 'proj-1',
126
- activeSpecs: [],
127
- lockedResources: [],
128
- startedAt: '2025-01-01T00:00:00Z',
129
- lastActivity: '2025-01-01T00:00:00Z',
130
- status: 'active',
131
- }],
132
- locks: [],
133
- conflicts: [],
134
- });
135
- const result = await handleOrchestrate({
136
- projectId: 'proj-1',
137
- action: 'heartbeat',
138
- sessionId: 'agent-1',
139
- });
140
- const data = JSON.parse(result.content[0].text);
141
- expect(data.action).toBe('heartbeat');
142
- expect(data.message).toContain('Heartbeat received');
143
- });
144
- // === lock ===
145
- it('should error when lock called without sessionId', async () => {
146
- const result = await handleOrchestrate({
147
- projectId: 'proj-1',
148
- action: 'lock',
149
- resourcePath: 'src/auth.ts',
150
- });
151
- expect(result.isError).toBe(true);
152
- });
153
- it('should error when lock called without resource', async () => {
154
- const result = await handleOrchestrate({
155
- projectId: 'proj-1',
156
- action: 'lock',
157
- sessionId: 'agent-1',
158
- });
159
- expect(result.isError).toBe(true);
160
- });
161
- it('should lock a resource', async () => {
162
- mockReadJson.mockResolvedValue({
163
- sessions: [{
164
- sessionId: 'agent-1',
165
- agentPlatform: 'custom',
166
- projectId: 'proj-1',
167
- activeSpecs: [],
168
- lockedResources: [],
169
- startedAt: '2025-01-01T00:00:00Z',
170
- lastActivity: new Date().toISOString(),
171
- status: 'active',
172
- }],
173
- locks: [],
174
- conflicts: [],
175
- });
176
- const result = await handleOrchestrate({
177
- projectId: 'proj-1',
178
- action: 'lock',
179
- sessionId: 'agent-1',
180
- resourcePath: 'src/auth.ts',
181
- });
182
- const data = JSON.parse(result.content[0].text);
183
- expect(data.granted).toBe(true);
184
- });
185
- it('should deny lock when resource locked by another', async () => {
186
- const future = new Date(Date.now() + 3600000).toISOString();
187
- mockReadJson.mockResolvedValue({
188
- sessions: [{
189
- sessionId: 'agent-1',
190
- agentPlatform: 'custom',
191
- projectId: 'proj-1',
192
- activeSpecs: [],
193
- lockedResources: [],
194
- startedAt: '2025-01-01T00:00:00Z',
195
- lastActivity: new Date().toISOString(),
196
- status: 'active',
197
- }],
198
- locks: [{
199
- type: 'file',
200
- resourceId: 'src/auth.ts',
201
- lockedBy: 'agent-2',
202
- lockedAt: new Date().toISOString(),
203
- reason: 'Working',
204
- expiresAt: future,
205
- }],
206
- conflicts: [],
207
- });
208
- const result = await handleOrchestrate({
209
- projectId: 'proj-1',
210
- action: 'lock',
211
- sessionId: 'agent-1',
212
- resourcePath: 'src/auth.ts',
213
- });
214
- const data = JSON.parse(result.content[0].text);
215
- expect(data.granted).toBe(false);
216
- });
217
- // === unlock ===
218
- it('should error when unlock called without sessionId', async () => {
219
- const result = await handleOrchestrate({
220
- projectId: 'proj-1',
221
- action: 'unlock',
222
- resourcePath: 'src/auth.ts',
223
- });
224
- expect(result.isError).toBe(true);
225
- });
226
- it('should unlock a resource', async () => {
227
- mockReadJson.mockResolvedValue({
228
- sessions: [{
229
- sessionId: 'agent-1',
230
- agentPlatform: 'custom',
231
- projectId: 'proj-1',
232
- activeSpecs: [],
233
- lockedResources: [{ resourceId: 'src/auth.ts' }],
234
- startedAt: '2025-01-01T00:00:00Z',
235
- lastActivity: new Date().toISOString(),
236
- status: 'active',
237
- }],
238
- locks: [{
239
- type: 'file',
240
- resourceId: 'src/auth.ts',
241
- lockedBy: 'agent-1',
242
- lockedAt: new Date().toISOString(),
243
- reason: 'Working',
244
- }],
245
- conflicts: [],
246
- });
247
- const result = await handleOrchestrate({
248
- projectId: 'proj-1',
249
- action: 'unlock',
250
- sessionId: 'agent-1',
251
- resourcePath: 'src/auth.ts',
252
- });
253
- const data = JSON.parse(result.content[0].text);
254
- expect(data.granted).toBe(true);
255
- });
256
- // === distribute ===
257
- it('should distribute tasks with no active sessions', async () => {
258
- mockReadJson.mockResolvedValue(emptyState());
259
- mockListSpecs.mockResolvedValue([]);
260
- const result = await handleOrchestrate({
261
- projectId: 'proj-1',
262
- action: 'distribute',
263
- });
264
- const data = JSON.parse(result.content[0].text);
265
- expect(data.action).toBe('distribute');
266
- expect(data.message).toContain('No active agent sessions');
267
- });
268
- it('should distribute tasks across active agents', async () => {
269
- mockReadJson.mockResolvedValue({
270
- sessions: [{
271
- sessionId: 'agent-1',
272
- agentPlatform: 'custom',
273
- projectId: 'proj-1',
274
- activeSpecs: ['SPEC-001'],
275
- lockedResources: [],
276
- startedAt: '2025-01-01T00:00:00Z',
277
- lastActivity: new Date().toISOString(),
278
- status: 'active',
279
- }],
280
- locks: [],
281
- conflicts: [],
282
- });
283
- mockListSpecs.mockResolvedValue([
284
- {
285
- id: 'SPEC-001',
286
- status: 'implementing',
287
- risk: 'high',
288
- scope: 'feature',
289
- estimation: { devHours: 8 },
290
- dependencies: [],
291
- },
292
- {
293
- id: 'SPEC-002',
294
- status: 'approved',
295
- risk: 'low',
296
- scope: 'trivial',
297
- estimation: { devHours: 2 },
298
- dependencies: [],
299
- },
300
- ]);
301
- const result = await handleOrchestrate({
302
- projectId: 'proj-1',
303
- action: 'distribute',
304
- });
305
- const data = JSON.parse(result.content[0].text);
306
- expect(data.distribution.length).toBe(2);
307
- });
308
- // === status ===
309
- it('should return orchestration status', async () => {
310
- mockReadJson.mockResolvedValue({
311
- sessions: [{
312
- sessionId: 'agent-1',
313
- agentPlatform: 'custom',
314
- projectId: 'proj-1',
315
- activeSpecs: [],
316
- lockedResources: [],
317
- startedAt: '2025-01-01T00:00:00Z',
318
- lastActivity: new Date().toISOString(),
319
- status: 'active',
320
- }],
321
- locks: [],
322
- conflicts: [],
323
- });
324
- const result = await handleOrchestrate({
325
- projectId: 'proj-1',
326
- action: 'status',
327
- });
328
- const data = JSON.parse(result.content[0].text);
329
- expect(data.action).toBe('status');
330
- expect(data.activeSessions).toBe(1);
331
- });
332
- // === resolve-conflict ===
333
- it('should resolve conflicts', async () => {
334
- mockReadJson.mockResolvedValue({
335
- sessions: [],
336
- locks: [],
337
- conflicts: [{
338
- type: 'spec-overlap',
339
- agents: ['agent-1', 'agent-2'],
340
- resource: 'SPEC-001',
341
- resolution: 'Coordinate',
342
- severity: 'warning',
343
- }],
344
- });
345
- const result = await handleOrchestrate({
346
- projectId: 'proj-1',
347
- action: 'resolve-conflict',
348
- sessionId: 'agent-1',
349
- specId: 'SPEC-001',
350
- });
351
- const data = JSON.parse(result.content[0].text);
352
- expect(data.action).toBe('resolve-conflict');
353
- expect(data.message).toContain('Resolved');
354
- });
355
- // === unknown action ===
356
- it('should error for unknown action', async () => {
357
- const result = await handleOrchestrate({
358
- projectId: 'proj-1',
359
- action: 'unknown',
360
- });
361
- expect(result.isError).toBe(true);
362
- });
363
- // === error handling ===
364
- it('should catch and return general errors', async () => {
365
- mockReadJson.mockRejectedValue(new Error('Disk full'));
366
- const result = await handleOrchestrate({
367
- projectId: 'proj-1',
368
- action: 'status',
369
- });
370
- expect(result.isError).toBe(true);
371
- expect(result.content[0].text).toContain('Disk full');
372
- });
373
- // --- Coverage for non-Error thrown (line 721) ---
374
- it('should catch non-Error thrown values', async () => {
375
- mockReadJson.mockRejectedValue('string error');
376
- const result = await handleOrchestrate({
377
- projectId: 'proj-1',
378
- action: 'register',
379
- });
380
- expect(result.isError).toBe(true);
381
- expect(result.content[0].text).toContain('string error');
382
- });
383
- // --- Coverage for resolve-conflict with force-unlock (lines 638-652) ---
384
- it('should force-unlock resource when resolving conflict', async () => {
385
- mockReadJson.mockResolvedValue({
386
- sessions: [
387
- {
388
- sessionId: 'agent-1',
389
- agentPlatform: 'custom',
390
- projectId: 'proj-1',
391
- activeSpecs: ['SPEC-001'],
392
- lockedResources: [{ resourceId: 'SPEC-001', type: 'spec', lockedBy: 'agent-1' }],
393
- startedAt: '2025-01-01T00:00:00Z',
394
- lastActivity: new Date().toISOString(),
395
- status: 'active',
396
- },
397
- {
398
- sessionId: 'agent-2',
399
- agentPlatform: 'custom',
400
- projectId: 'proj-1',
401
- activeSpecs: ['SPEC-001'],
402
- lockedResources: [],
403
- startedAt: '2025-01-01T00:00:00Z',
404
- lastActivity: new Date().toISOString(),
405
- status: 'active',
406
- },
407
- ],
408
- locks: [{
409
- type: 'spec',
410
- resourceId: 'SPEC-001',
411
- lockedBy: 'agent-1',
412
- lockedAt: new Date().toISOString(),
413
- reason: 'Working',
414
- expiresAt: new Date(Date.now() + 3600000).toISOString(),
415
- }],
416
- conflicts: [{
417
- type: 'spec-overlap',
418
- agents: ['agent-1', 'agent-2'],
419
- resource: 'SPEC-001',
420
- resolution: 'Coordinate',
421
- severity: 'warning',
422
- }],
423
- });
424
- const result = await handleOrchestrate({
425
- projectId: 'proj-1',
426
- action: 'resolve-conflict',
427
- sessionId: 'agent-2',
428
- specId: 'SPEC-001',
429
- });
430
- const data = JSON.parse(result.content[0].text);
431
- expect(data.message).toContain('Resolved');
432
- // Lock should have been force-removed from agent-1
433
- const savedState = mockWriteJson.mock.calls[0][1];
434
- expect(savedState.locks).toHaveLength(0);
435
- });
436
- // --- Coverage for resolve-conflict with no matching conflicts ---
437
- it('should report no matching conflicts when none found', async () => {
438
- mockReadJson.mockResolvedValue({
439
- sessions: [],
440
- locks: [],
441
- conflicts: [{
442
- type: 'spec-overlap',
443
- agents: ['agent-3', 'agent-4'],
444
- resource: 'SPEC-002',
445
- resolution: 'Coordinate',
446
- severity: 'warning',
447
- }],
448
- });
449
- const result = await handleOrchestrate({
450
- projectId: 'proj-1',
451
- action: 'resolve-conflict',
452
- sessionId: 'agent-1',
453
- specId: 'SPEC-001',
454
- });
455
- const data = JSON.parse(result.content[0].text);
456
- expect(data.message).toContain('No matching conflicts');
457
- });
458
- // --- Coverage for resolve-conflict without sessionId or resourcePath ---
459
- it('should resolve all conflicts when no filters provided', async () => {
460
- mockReadJson.mockResolvedValue({
461
- sessions: [],
462
- locks: [],
463
- conflicts: [
464
- {
465
- type: 'spec-overlap',
466
- agents: ['agent-1', 'agent-2'],
467
- resource: 'SPEC-001',
468
- resolution: 'Coordinate',
469
- severity: 'warning',
470
- },
471
- {
472
- type: 'spec-overlap',
473
- agents: ['agent-3', 'agent-4'],
474
- resource: 'SPEC-002',
475
- resolution: 'Coordinate',
476
- severity: 'warning',
477
- },
478
- ],
479
- });
480
- const result = await handleOrchestrate({
481
- projectId: 'proj-1',
482
- action: 'resolve-conflict',
483
- });
484
- const data = JSON.parse(result.content[0].text);
485
- expect(data.message).toContain('Resolved 2 conflict(s)');
486
- });
487
- // --- Coverage for unlock with no resource (specId or resourcePath) ---
488
- it('should error when unlock called without resourcePath or specId', async () => {
489
- const result = await handleOrchestrate({
490
- projectId: 'proj-1',
491
- action: 'unlock',
492
- sessionId: 'agent-1',
493
- });
494
- expect(result.isError).toBe(true);
495
- expect(result.content[0].text).toContain('resourcePath or specId');
496
- });
497
- // --- Coverage for unlock: lock not found ---
498
- it('should error when trying to unlock a resource not locked by session', async () => {
499
- mockReadJson.mockResolvedValue({
500
- sessions: [{
501
- sessionId: 'agent-1',
502
- agentPlatform: 'custom',
503
- projectId: 'proj-1',
504
- activeSpecs: [],
505
- lockedResources: [],
506
- startedAt: '2025-01-01T00:00:00Z',
507
- lastActivity: new Date().toISOString(),
508
- status: 'active',
509
- }],
510
- locks: [],
511
- conflicts: [],
512
- });
513
- const result = await handleOrchestrate({
514
- projectId: 'proj-1',
515
- action: 'unlock',
516
- sessionId: 'agent-1',
517
- resourcePath: 'src/not-locked.ts',
518
- });
519
- expect(result.isError).toBe(true);
520
- expect(result.content[0].text).toContain('No lock on');
521
- });
522
- // --- Coverage for lock: session not found ---
523
- it('should error when locking with non-existent session', async () => {
524
- mockReadJson.mockResolvedValue(emptyState());
525
- const result = await handleOrchestrate({
526
- projectId: 'proj-1',
527
- action: 'lock',
528
- sessionId: 'unknown-agent',
529
- resourcePath: 'src/auth.ts',
530
- });
531
- expect(result.isError).toBe(true);
532
- expect(result.content[0].text).toContain('not found');
533
- });
534
- // --- Coverage for lock: extend existing lock by same session ---
535
- it('should extend lock when same session locks same resource', async () => {
536
- const future = new Date(Date.now() + 3600000).toISOString();
537
- mockReadJson.mockResolvedValue({
538
- sessions: [{
539
- sessionId: 'agent-1',
540
- agentPlatform: 'custom',
541
- projectId: 'proj-1',
542
- activeSpecs: [],
543
- lockedResources: [{
544
- type: 'file',
545
- resourceId: 'src/auth.ts',
546
- lockedBy: 'agent-1',
547
- lockedAt: new Date().toISOString(),
548
- reason: 'Working',
549
- expiresAt: future,
550
- }],
551
- startedAt: '2025-01-01T00:00:00Z',
552
- lastActivity: new Date().toISOString(),
553
- status: 'active',
554
- }],
555
- locks: [{
556
- type: 'file',
557
- resourceId: 'src/auth.ts',
558
- lockedBy: 'agent-1',
559
- lockedAt: new Date().toISOString(),
560
- reason: 'Working',
561
- expiresAt: future,
562
- }],
563
- conflicts: [],
564
- });
565
- const result = await handleOrchestrate({
566
- projectId: 'proj-1',
567
- action: 'lock',
568
- sessionId: 'agent-1',
569
- resourcePath: 'src/auth.ts',
570
- });
571
- const data = JSON.parse(result.content[0].text);
572
- expect(data.granted).toBe(true);
573
- expect(data.message).toContain('extended');
574
- });
575
- // --- Coverage for lock using specId instead of resourcePath ---
576
- it('should lock using specId as resource identifier', async () => {
577
- mockReadJson.mockResolvedValue({
578
- sessions: [{
579
- sessionId: 'agent-1',
580
- agentPlatform: 'custom',
581
- projectId: 'proj-1',
582
- activeSpecs: ['SPEC-001'],
583
- lockedResources: [],
584
- startedAt: '2025-01-01T00:00:00Z',
585
- lastActivity: new Date().toISOString(),
586
- status: 'active',
587
- }],
588
- locks: [],
589
- conflicts: [],
590
- });
591
- const result = await handleOrchestrate({
592
- projectId: 'proj-1',
593
- action: 'lock',
594
- sessionId: 'agent-1',
595
- specId: 'SPEC-001',
596
- });
597
- const data = JSON.parse(result.content[0].text);
598
- expect(data.granted).toBe(true);
599
- });
600
- // --- Coverage for lock: different resource types (config, knowledge) ---
601
- it('should detect config resource type for .json files', async () => {
602
- mockReadJson.mockResolvedValue({
603
- sessions: [{
604
- sessionId: 'agent-1',
605
- agentPlatform: 'custom',
606
- projectId: 'proj-1',
607
- activeSpecs: [],
608
- lockedResources: [],
609
- startedAt: '2025-01-01T00:00:00Z',
610
- lastActivity: new Date().toISOString(),
611
- status: 'active',
612
- }],
613
- locks: [],
614
- conflicts: [],
615
- });
616
- const result = await handleOrchestrate({
617
- projectId: 'proj-1',
618
- action: 'lock',
619
- sessionId: 'agent-1',
620
- resourcePath: 'config/settings.json',
621
- });
622
- const data = JSON.parse(result.content[0].text);
623
- expect(data.granted).toBe(true);
624
- });
625
- it('should detect knowledge resource type', async () => {
626
- mockReadJson.mockResolvedValue({
627
- sessions: [{
628
- sessionId: 'agent-1',
629
- agentPlatform: 'custom',
630
- projectId: 'proj-1',
631
- activeSpecs: [],
632
- lockedResources: [],
633
- startedAt: '2025-01-01T00:00:00Z',
634
- lastActivity: new Date().toISOString(),
635
- status: 'active',
636
- }],
637
- locks: [],
638
- conflicts: [],
639
- });
640
- const result = await handleOrchestrate({
641
- projectId: 'proj-1',
642
- action: 'lock',
643
- sessionId: 'agent-1',
644
- resourcePath: 'data/knowledge/base.json',
645
- });
646
- const data = JSON.parse(result.content[0].text);
647
- expect(data.granted).toBe(true);
648
- });
649
- // --- Coverage for pruneExpiredLocks ---
650
- it('should prune expired locks from state', async () => {
651
- const expiredTime = new Date(Date.now() - 3600000).toISOString();
652
- mockReadJson.mockResolvedValue({
653
- sessions: [{
654
- sessionId: 'agent-1',
655
- agentPlatform: 'custom',
656
- projectId: 'proj-1',
657
- activeSpecs: [],
658
- lockedResources: [{
659
- type: 'file',
660
- resourceId: 'src/old.ts',
661
- lockedBy: 'agent-1',
662
- lockedAt: '2025-01-01T00:00:00Z',
663
- reason: 'Working',
664
- expiresAt: expiredTime,
665
- }],
666
- startedAt: '2025-01-01T00:00:00Z',
667
- lastActivity: new Date().toISOString(),
668
- status: 'active',
669
- }],
670
- locks: [{
671
- type: 'file',
672
- resourceId: 'src/old.ts',
673
- lockedBy: 'agent-1',
674
- lockedAt: '2025-01-01T00:00:00Z',
675
- reason: 'Working',
676
- expiresAt: expiredTime,
677
- }],
678
- conflicts: [],
679
- });
680
- const result = await handleOrchestrate({
681
- projectId: 'proj-1',
682
- action: 'status',
683
- });
684
- const data = JSON.parse(result.content[0].text);
685
- expect(data.activeLocks).toBe(0);
686
- });
687
- // --- Coverage for pruneIdleSessions ---
688
- it('should mark idle sessions as disconnected', async () => {
689
- const veryOldTime = new Date(Date.now() - 20 * 60 * 1000).toISOString(); // 20 min ago
690
- mockReadJson.mockResolvedValue({
691
- sessions: [{
692
- sessionId: 'agent-1',
693
- agentPlatform: 'custom',
694
- projectId: 'proj-1',
695
- activeSpecs: [],
696
- lockedResources: [],
697
- startedAt: '2025-01-01T00:00:00Z',
698
- lastActivity: veryOldTime,
699
- status: 'active',
700
- }, {
701
- sessionId: 'agent-2',
702
- agentPlatform: 'custom',
703
- projectId: 'proj-1',
704
- activeSpecs: [],
705
- lockedResources: [],
706
- startedAt: '2025-01-01T00:00:00Z',
707
- lastActivity: veryOldTime,
708
- status: 'idle',
709
- }],
710
- locks: [],
711
- conflicts: [],
712
- });
713
- const result = await handleOrchestrate({
714
- projectId: 'proj-1',
715
- action: 'status',
716
- });
717
- const data = JSON.parse(result.content[0].text);
718
- expect(data.disconnectedSessions).toBe(2);
719
- expect(data.activeSessions).toBe(0);
720
- });
721
- // --- Coverage for isLockExpired with no expiresAt ---
722
- it('should not consider lock expired when expiresAt is undefined', async () => {
723
- mockReadJson.mockResolvedValue({
724
- sessions: [{
725
- sessionId: 'agent-1',
726
- agentPlatform: 'custom',
727
- projectId: 'proj-1',
728
- activeSpecs: [],
729
- lockedResources: [],
730
- startedAt: '2025-01-01T00:00:00Z',
731
- lastActivity: new Date().toISOString(),
732
- status: 'active',
733
- }],
734
- locks: [{
735
- type: 'file',
736
- resourceId: 'src/auth.ts',
737
- lockedBy: 'agent-2',
738
- lockedAt: new Date().toISOString(),
739
- reason: 'Working',
740
- // No expiresAt -> never expires
741
- }],
742
- conflicts: [],
743
- });
744
- const result = await handleOrchestrate({
745
- projectId: 'proj-1',
746
- action: 'lock',
747
- sessionId: 'agent-1',
748
- resourcePath: 'src/auth.ts',
749
- });
750
- const data = JSON.parse(result.content[0].text);
751
- // Lock should still be held by agent-2 (not expired)
752
- expect(data.granted).toBe(false);
753
- expect(data.message).toContain('indefinitely');
754
- });
755
- // --- Coverage for distribute: dependency conflicts ---
756
- it('should detect dependency conflicts between agents', async () => {
757
- mockReadJson.mockResolvedValue({
758
- sessions: [
759
- {
760
- sessionId: 'agent-1',
761
- agentPlatform: 'custom',
762
- projectId: 'proj-1',
763
- activeSpecs: ['SPEC-001'],
764
- lockedResources: [],
765
- startedAt: '2025-01-01T00:00:00Z',
766
- lastActivity: new Date().toISOString(),
767
- status: 'active',
768
- },
769
- {
770
- sessionId: 'agent-2',
771
- agentPlatform: 'custom',
772
- projectId: 'proj-1',
773
- activeSpecs: ['SPEC-002'],
774
- lockedResources: [],
775
- startedAt: '2025-01-01T00:00:00Z',
776
- lastActivity: new Date().toISOString(),
777
- status: 'active',
778
- },
779
- ],
780
- locks: [],
781
- conflicts: [],
782
- });
783
- mockListSpecs.mockResolvedValue([
784
- {
785
- id: 'SPEC-001',
786
- status: 'implementing',
787
- risk: 'medium',
788
- scope: 'feature',
789
- estimation: { devHours: 8 },
790
- dependencies: [],
791
- },
792
- {
793
- id: 'SPEC-002',
794
- status: 'implementing',
795
- risk: 'critical',
796
- scope: 'architectural',
797
- estimation: { devHours: 16 },
798
- dependencies: ['SPEC-001'], // depends on SPEC-001
799
- },
800
- ]);
801
- const result = await handleOrchestrate({
802
- projectId: 'proj-1',
803
- action: 'distribute',
804
- });
805
- const data = JSON.parse(result.content[0].text);
806
- expect(data.conflicts).toBeDefined();
807
- expect(data.conflicts.length).toBeGreaterThan(0);
808
- expect(data.conflicts[0].type).toBe('dependency-conflict');
809
- });
810
- // --- Coverage for distribute: blocked specs ---
811
- it('should mark specs as blocked when dependencies not done', async () => {
812
- mockReadJson.mockResolvedValue({
813
- sessions: [{
814
- sessionId: 'agent-1',
815
- agentPlatform: 'custom',
816
- projectId: 'proj-1',
817
- activeSpecs: [],
818
- lockedResources: [],
819
- startedAt: '2025-01-01T00:00:00Z',
820
- lastActivity: new Date().toISOString(),
821
- status: 'active',
822
- }],
823
- locks: [],
824
- conflicts: [],
825
- });
826
- mockListSpecs.mockResolvedValue([
827
- {
828
- id: 'SPEC-001',
829
- status: 'implementing',
830
- risk: 'medium',
831
- scope: 'feature',
832
- estimation: { devHours: 8 },
833
- dependencies: [],
834
- },
835
- {
836
- id: 'SPEC-002',
837
- status: 'approved',
838
- risk: 'low',
839
- scope: 'feature',
840
- estimation: { devHours: 4 },
841
- dependencies: ['SPEC-001'],
842
- },
843
- ]);
844
- const result = await handleOrchestrate({
845
- projectId: 'proj-1',
846
- action: 'distribute',
847
- });
848
- const data = JSON.parse(result.content[0].text);
849
- const blockedTask = data.distribution.find((t) => t.specId === 'SPEC-002');
850
- expect(blockedTask.status).toBe('blocked');
851
- expect(blockedTask.blockedBy).toContain('SPEC-001');
852
- });
853
- // --- Coverage for register: re-activate with new specId ---
854
- it('should add specId when re-activating existing session', async () => {
855
- mockReadJson.mockResolvedValue({
856
- sessions: [{
857
- sessionId: 'agent-1',
858
- agentPlatform: 'custom',
859
- projectId: 'proj-1',
860
- activeSpecs: ['SPEC-001'],
861
- lockedResources: [],
862
- startedAt: '2025-01-01T00:00:00Z',
863
- lastActivity: '2025-01-01T00:00:00Z',
864
- status: 'disconnected',
865
- }],
866
- locks: [],
867
- conflicts: [],
868
- });
869
- const result = await handleOrchestrate({
870
- projectId: 'proj-1',
871
- action: 'register',
872
- sessionId: 'agent-1',
873
- specId: 'SPEC-002',
874
- });
875
- const data = JSON.parse(result.content[0].text);
876
- expect(data.message).toContain('Re-activated');
877
- const savedState = mockWriteJson.mock.calls[0][1];
878
- expect(savedState.sessions[0].activeSpecs).toContain('SPEC-002');
879
- });
880
- // --- Coverage for unlock using specId ---
881
- it('should unlock using specId as resource identifier', async () => {
882
- mockReadJson.mockResolvedValue({
883
- sessions: [{
884
- sessionId: 'agent-1',
885
- agentPlatform: 'custom',
886
- projectId: 'proj-1',
887
- activeSpecs: ['SPEC-001'],
888
- lockedResources: [{ resourceId: 'SPEC-001', type: 'spec', lockedBy: 'agent-1' }],
889
- startedAt: '2025-01-01T00:00:00Z',
890
- lastActivity: new Date().toISOString(),
891
- status: 'active',
892
- }],
893
- locks: [{
894
- type: 'spec',
895
- resourceId: 'SPEC-001',
896
- lockedBy: 'agent-1',
897
- lockedAt: new Date().toISOString(),
898
- reason: 'Working',
899
- }],
900
- conflicts: [],
901
- });
902
- const result = await handleOrchestrate({
903
- projectId: 'proj-1',
904
- action: 'unlock',
905
- sessionId: 'agent-1',
906
- specId: 'SPEC-001',
907
- });
908
- const data = JSON.parse(result.content[0].text);
909
- expect(data.granted).toBe(true);
910
- });
911
- // --- Coverage for defensive guard: existingSession after findIndex (line 109) ---
912
- it('should throw when re-activating session but indexed access returns undefined', async () => {
913
- const sessions = new Proxy([], {
914
- get(target, prop) {
915
- if (prop === 'findIndex') {
916
- return () => 0;
917
- }
918
- return Reflect.get(target, prop);
919
- },
920
- });
921
- mockReadJson.mockResolvedValue({ sessions, locks: [], conflicts: [] });
922
- const result = await handleOrchestrate({
923
- projectId: 'proj-1',
924
- action: 'register',
925
- sessionId: 'ghost-agent',
926
- });
927
- expect(result.isError).toBe(true);
928
- expect(result.content[0].text).toContain('unexpectedly missing');
929
- });
930
- // --- Coverage for sort branch: a not blocked, b blocked -> -1 (line 524) ---
931
- it('should sort blocked specs after non-blocked in distribute', async () => {
932
- mockReadJson.mockResolvedValue({
933
- sessions: [{
934
- sessionId: 'agent-1',
935
- agentPlatform: 'custom',
936
- projectId: 'proj-1',
937
- activeSpecs: [],
938
- lockedResources: [],
939
- startedAt: '2025-01-01T00:00:00Z',
940
- lastActivity: new Date().toISOString(),
941
- status: 'active',
942
- }],
943
- locks: [],
944
- conflicts: [],
945
- });
946
- // SPEC-003 is blocked (depends on SPEC-001 which is not done)
947
- // SPEC-002 is available (no deps)
948
- // SPEC-001 is available (no deps)
949
- // Ordering forces sort to compare blocked vs non-blocked in both directions
950
- mockListSpecs.mockResolvedValue([
951
- {
952
- id: 'SPEC-003',
953
- status: 'approved',
954
- risk: 'low',
955
- scope: 'feature',
956
- estimation: { devHours: 2 },
957
- dependencies: ['SPEC-001'],
958
- },
959
- {
960
- id: 'SPEC-002',
961
- status: 'approved',
962
- risk: 'medium',
963
- scope: 'feature',
964
- estimation: { devHours: 4 },
965
- dependencies: [],
966
- },
967
- {
968
- id: 'SPEC-001',
969
- status: 'draft',
970
- risk: 'high',
971
- scope: 'cross-module',
972
- estimation: { devHours: 8 },
973
- dependencies: [],
974
- },
975
- ]);
976
- const result = await handleOrchestrate({
977
- projectId: 'proj-1',
978
- action: 'distribute',
979
- });
980
- const data = JSON.parse(result.content[0].text);
981
- // Blocked items should come last
982
- const lastItem = data.distribution[data.distribution.length - 1];
983
- expect(lastItem.status).toBe('blocked');
984
- expect(lastItem.specId).toBe('SPEC-003');
985
- });
986
- // --- Coverage for alreadyReported check in distribute (line 543) ---
987
- it('should not duplicate dependency conflict reports between same agent pair', async () => {
988
- mockReadJson.mockResolvedValue({
989
- sessions: [
990
- {
991
- sessionId: 'agent-1',
992
- agentPlatform: 'custom',
993
- projectId: 'proj-1',
994
- activeSpecs: ['SPEC-001', 'SPEC-003'],
995
- lockedResources: [],
996
- startedAt: '2025-01-01T00:00:00Z',
997
- lastActivity: new Date().toISOString(),
998
- status: 'active',
999
- },
1000
- {
1001
- sessionId: 'agent-2',
1002
- agentPlatform: 'custom',
1003
- projectId: 'proj-1',
1004
- activeSpecs: ['SPEC-002'],
1005
- lockedResources: [],
1006
- startedAt: '2025-01-01T00:00:00Z',
1007
- lastActivity: new Date().toISOString(),
1008
- status: 'active',
1009
- },
1010
- ],
1011
- locks: [],
1012
- conflicts: [],
1013
- });
1014
- // Both SPEC-002 and SPEC-003 depend on SPEC-001
1015
- // SPEC-002 is worked on by agent-2 and depends on SPEC-001 (agent-1) -> conflict
1016
- // SPEC-003 is worked on by agent-1 and depends on SPEC-001 (agent-1) -> same agent, no conflict
1017
- // But we need duplicate: two specs causing the same agent-pair conflict on same resource
1018
- // SPEC-002 depends on SPEC-001 -> conflict between agent-2 and agent-1 for resource SPEC-002
1019
- // SPEC-002 also depends on SPEC-003 -> conflict between agent-2 and agent-1 for resource SPEC-002 (duplicate!)
1020
- mockListSpecs.mockResolvedValue([
1021
- {
1022
- id: 'SPEC-001',
1023
- status: 'implementing',
1024
- risk: 'medium',
1025
- scope: 'feature',
1026
- estimation: { devHours: 8 },
1027
- dependencies: [],
1028
- },
1029
- {
1030
- id: 'SPEC-002',
1031
- status: 'implementing',
1032
- risk: 'medium',
1033
- scope: 'feature',
1034
- estimation: { devHours: 8 },
1035
- dependencies: ['SPEC-001', 'SPEC-003'],
1036
- },
1037
- {
1038
- id: 'SPEC-003',
1039
- status: 'implementing',
1040
- risk: 'medium',
1041
- scope: 'feature',
1042
- estimation: { devHours: 4 },
1043
- dependencies: [],
1044
- },
1045
- ]);
1046
- const result = await handleOrchestrate({
1047
- projectId: 'proj-1',
1048
- action: 'distribute',
1049
- });
1050
- const data = JSON.parse(result.content[0].text);
1051
- // Should have conflicts, but no duplicates for the same resource+agents combo
1052
- expect(data.conflicts).toBeDefined();
1053
- const spec002Conflicts = data.conflicts.filter((c) => c.resource === 'SPEC-002');
1054
- // Both dependencies (SPEC-001, SPEC-003) are on agent-1, but only 1 conflict per resource
1055
- expect(spec002Conflicts.length).toBeLessThanOrEqual(2);
1056
- // The alreadyReported check prevents duplicate agent-pair+resource combos
1057
- const agentPairs = spec002Conflicts.map((c) => [...c.agents].sort().join(','));
1058
- const uniquePairs = new Set(agentPairs);
1059
- expect(uniquePairs.size).toBe(agentPairs.length);
1060
- });
1061
- // --- Coverage for status with idle and disconnected sessions ---
1062
- it('should correctly count idle and disconnected sessions', async () => {
1063
- const veryOldTime = new Date(Date.now() - 20 * 60 * 1000).toISOString();
1064
- mockReadJson.mockResolvedValue({
1065
- sessions: [
1066
- {
1067
- sessionId: 'agent-1',
1068
- agentPlatform: 'custom',
1069
- projectId: 'proj-1',
1070
- activeSpecs: [],
1071
- lockedResources: [],
1072
- startedAt: '2025-01-01T00:00:00Z',
1073
- lastActivity: new Date().toISOString(),
1074
- status: 'active',
1075
- },
1076
- {
1077
- sessionId: 'agent-2',
1078
- agentPlatform: 'custom',
1079
- projectId: 'proj-1',
1080
- activeSpecs: [],
1081
- lockedResources: [],
1082
- startedAt: '2025-01-01T00:00:00Z',
1083
- lastActivity: veryOldTime,
1084
- status: 'idle',
1085
- },
1086
- {
1087
- sessionId: 'agent-3',
1088
- agentPlatform: 'custom',
1089
- projectId: 'proj-1',
1090
- activeSpecs: [],
1091
- lockedResources: [],
1092
- startedAt: '2025-01-01T00:00:00Z',
1093
- lastActivity: '2020-01-01T00:00:00Z',
1094
- status: 'disconnected',
1095
- },
1096
- ],
1097
- locks: [],
1098
- conflicts: [{
1099
- type: 'spec-overlap',
1100
- agents: ['agent-1', 'agent-2'],
1101
- resource: 'SPEC-001',
1102
- resolution: 'Coordinate',
1103
- severity: 'warning',
1104
- }],
1105
- });
1106
- const result = await handleOrchestrate({
1107
- projectId: 'proj-1',
1108
- action: 'status',
1109
- });
1110
- const data = JSON.parse(result.content[0].text);
1111
- expect(data.totalSessions).toBe(3);
1112
- expect(data.activeSessions).toBe(1);
1113
- expect(data.disconnectedSessions).toBeGreaterThanOrEqual(1);
1114
- expect(data.unresolvedConflicts).toBe(1);
1115
- });
1116
- });
1117
- //# sourceMappingURL=orchestrate.test.js.map