@mpowr/nexus-mcp 0.5.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.
- package/README.md +59 -0
- package/dist/auth.d.ts +39 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +47 -0
- package/dist/auth.js.map +1 -0
- package/dist/nexus-api.d.ts +29 -0
- package/dist/nexus-api.d.ts.map +1 -0
- package/dist/nexus-api.js +76 -0
- package/dist/nexus-api.js.map +1 -0
- package/dist/server.d.ts +65 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +183 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/add-task-note.d.ts +34 -0
- package/dist/tools/add-task-note.d.ts.map +1 -0
- package/dist/tools/add-task-note.js +39 -0
- package/dist/tools/add-task-note.js.map +1 -0
- package/dist/tools/append-session-entry.d.ts +53 -0
- package/dist/tools/append-session-entry.d.ts.map +1 -0
- package/dist/tools/append-session-entry.js +67 -0
- package/dist/tools/append-session-entry.js.map +1 -0
- package/dist/tools/create-task.d.ts +52 -0
- package/dist/tools/create-task.d.ts.map +1 -0
- package/dist/tools/create-task.js +51 -0
- package/dist/tools/create-task.js.map +1 -0
- package/dist/tools/decision-comments.d.ts +54 -0
- package/dist/tools/decision-comments.d.ts.map +1 -0
- package/dist/tools/decision-comments.js +80 -0
- package/dist/tools/decision-comments.js.map +1 -0
- package/dist/tools/get-document.d.ts +47 -0
- package/dist/tools/get-document.d.ts.map +1 -0
- package/dist/tools/get-document.js +68 -0
- package/dist/tools/get-document.js.map +1 -0
- package/dist/tools/get-project-memory.d.ts +47 -0
- package/dist/tools/get-project-memory.d.ts.map +1 -0
- package/dist/tools/get-project-memory.js +53 -0
- package/dist/tools/get-project-memory.js.map +1 -0
- package/dist/tools/get-related-entities.d.ts +44 -0
- package/dist/tools/get-related-entities.d.ts.map +1 -0
- package/dist/tools/get-related-entities.js +60 -0
- package/dist/tools/get-related-entities.js.map +1 -0
- package/dist/tools/governance.d.ts +90 -0
- package/dist/tools/governance.d.ts.map +1 -0
- package/dist/tools/governance.js +124 -0
- package/dist/tools/governance.js.map +1 -0
- package/dist/tools/ingest-document.d.ts +40 -0
- package/dist/tools/ingest-document.d.ts.map +1 -0
- package/dist/tools/ingest-document.js +48 -0
- package/dist/tools/ingest-document.js.map +1 -0
- package/dist/tools/letter-inbox.d.ts +80 -0
- package/dist/tools/letter-inbox.d.ts.map +1 -0
- package/dist/tools/letter-inbox.js +118 -0
- package/dist/tools/letter-inbox.js.map +1 -0
- package/dist/tools/letters.d.ts +91 -0
- package/dist/tools/letters.d.ts.map +1 -0
- package/dist/tools/letters.js +112 -0
- package/dist/tools/letters.js.map +1 -0
- package/dist/tools/project-list.d.ts +28 -0
- package/dist/tools/project-list.d.ts.map +1 -0
- package/dist/tools/project-list.js +43 -0
- package/dist/tools/project-list.js.map +1 -0
- package/dist/tools/reviews.d.ts +145 -0
- package/dist/tools/reviews.d.ts.map +1 -0
- package/dist/tools/reviews.js +216 -0
- package/dist/tools/reviews.js.map +1 -0
- package/dist/tools/search-knowledge.d.ts +48 -0
- package/dist/tools/search-knowledge.d.ts.map +1 -0
- package/dist/tools/search-knowledge.js +54 -0
- package/dist/tools/search-knowledge.js.map +1 -0
- package/dist/tools/sessions.d.ts +81 -0
- package/dist/tools/sessions.d.ts.map +1 -0
- package/dist/tools/sessions.js +120 -0
- package/dist/tools/sessions.js.map +1 -0
- package/dist/tools/skill-assign.d.ts +77 -0
- package/dist/tools/skill-assign.d.ts.map +1 -0
- package/dist/tools/skill-assign.js +108 -0
- package/dist/tools/skill-assign.js.map +1 -0
- package/dist/tools/skills.d.ts +138 -0
- package/dist/tools/skills.d.ts.map +1 -0
- package/dist/tools/skills.js +192 -0
- package/dist/tools/skills.js.map +1 -0
- package/dist/tools/update-task-status.d.ts +48 -0
- package/dist/tools/update-task-status.d.ts.map +1 -0
- package/dist/tools/update-task-status.js +51 -0
- package/dist/tools/update-task-status.js.map +1 -0
- package/package.json +30 -0
- package/src/__tests__/auth.test.ts +162 -0
- package/src/__tests__/decision-comments.test.ts +173 -0
- package/src/__tests__/helpers.ts +58 -0
- package/src/__tests__/layer1-knowledge.test.ts +302 -0
- package/src/__tests__/layer2-coordination.test.ts +775 -0
- package/src/__tests__/layer3-governance.test.ts +205 -0
- package/src/__tests__/project-list-and-skill-assign.test.ts +282 -0
- package/src/__tests__/reviews.test.ts +420 -0
- package/src/__tests__/server.test.ts +238 -0
- package/src/__tests__/setup.ts +15 -0
- package/src/auth.ts +81 -0
- package/src/nexus-api.ts +110 -0
- package/src/server.ts +499 -0
- package/src/tools/add-task-note.ts +50 -0
- package/src/tools/append-session-entry.ts +83 -0
- package/src/tools/create-task.ts +66 -0
- package/src/tools/decision-comments.ts +102 -0
- package/src/tools/get-document.ts +80 -0
- package/src/tools/get-project-memory.ts +65 -0
- package/src/tools/get-related-entities.ts +73 -0
- package/src/tools/governance.ts +162 -0
- package/src/tools/ingest-document.ts +64 -0
- package/src/tools/letter-inbox.ts +157 -0
- package/src/tools/letters.ts +144 -0
- package/src/tools/project-list.ts +52 -0
- package/src/tools/reviews.ts +277 -0
- package/src/tools/search-knowledge.ts +68 -0
- package/src/tools/sessions.ts +154 -0
- package/src/tools/skill-assign.ts +142 -0
- package/src/tools/skills.ts +252 -0
- package/src/tools/update-task-status.ts +64 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Layer 2 Coordination tools.
|
|
3
|
+
* All tools delegate to the Nexus API via nexusPost().
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
7
|
+
import { mockApiError, mockApiSuccess, parseToolResponse, TEST_IDS } from './helpers'
|
|
8
|
+
|
|
9
|
+
vi.mock('../nexus-api.js', () => ({
|
|
10
|
+
nexusPost: vi.fn(),
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
import { nexusPost } from '../nexus-api.js'
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Session tools
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
describe('Layer 2: Session tools', () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
vi.restoreAllMocks()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
describe('createSession', () => {
|
|
25
|
+
it('should create a session and return session data', async () => {
|
|
26
|
+
vi.mocked(nexusPost).mockResolvedValue(
|
|
27
|
+
mockApiSuccess({
|
|
28
|
+
action: 'session_create',
|
|
29
|
+
id: TEST_IDS.sessionId,
|
|
30
|
+
project_id: TEST_IDS.projectId,
|
|
31
|
+
title: 'Test Session',
|
|
32
|
+
status: 'open',
|
|
33
|
+
created_at: '2026-04-12T00:00:00Z',
|
|
34
|
+
}),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
const { createSession } = await import('../tools/sessions.js')
|
|
38
|
+
const result = await createSession({
|
|
39
|
+
project_id: TEST_IDS.projectId,
|
|
40
|
+
title: 'Test Session',
|
|
41
|
+
user_id: TEST_IDS.userId,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
expect(result.isError).toBeUndefined()
|
|
45
|
+
const parsed = parseToolResponse(result)
|
|
46
|
+
expect(parsed.action).toBe('session_create')
|
|
47
|
+
expect(parsed.id).toBe(TEST_IDS.sessionId)
|
|
48
|
+
expect(parsed.status).toBe('open')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should return error on API failure', async () => {
|
|
52
|
+
vi.mocked(nexusPost).mockResolvedValue(mockApiError('Failed to create session'))
|
|
53
|
+
|
|
54
|
+
const { createSession } = await import('../tools/sessions.js')
|
|
55
|
+
const result = await createSession({
|
|
56
|
+
project_id: TEST_IDS.projectId,
|
|
57
|
+
title: 'Fail',
|
|
58
|
+
user_id: TEST_IDS.userId,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
expect(result.isError).toBe(true)
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('closeSession', () => {
|
|
66
|
+
it('should close a session', async () => {
|
|
67
|
+
vi.mocked(nexusPost).mockResolvedValue(
|
|
68
|
+
mockApiSuccess({
|
|
69
|
+
action: 'session_close',
|
|
70
|
+
session_id: TEST_IDS.sessionId,
|
|
71
|
+
project_id: TEST_IDS.projectId,
|
|
72
|
+
status: 'closed',
|
|
73
|
+
summary: 'Work done',
|
|
74
|
+
next_entry_point: 'Continue with task 47',
|
|
75
|
+
}),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
const { closeSession } = await import('../tools/sessions.js')
|
|
79
|
+
const result = await closeSession({
|
|
80
|
+
session_id: TEST_IDS.sessionId,
|
|
81
|
+
summary: 'Work done',
|
|
82
|
+
next_entry_point: 'Continue with task 47',
|
|
83
|
+
user_id: TEST_IDS.userId,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
expect(result.isError).toBeUndefined()
|
|
87
|
+
const parsed = parseToolResponse(result)
|
|
88
|
+
expect(parsed.action).toBe('session_close')
|
|
89
|
+
expect(parsed.status).toBe('closed')
|
|
90
|
+
expect(parsed.summary).toBe('Work done')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('should return error on API failure', async () => {
|
|
94
|
+
vi.mocked(nexusPost).mockResolvedValue(mockApiError('Session not found'))
|
|
95
|
+
|
|
96
|
+
const { closeSession } = await import('../tools/sessions.js')
|
|
97
|
+
const result = await closeSession({
|
|
98
|
+
session_id: TEST_IDS.sessionId,
|
|
99
|
+
user_id: TEST_IDS.userId,
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
expect(result.isError).toBe(true)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
describe('listOpenSessions', () => {
|
|
107
|
+
it('should list open sessions for a project', async () => {
|
|
108
|
+
vi.mocked(nexusPost).mockResolvedValue(
|
|
109
|
+
mockApiSuccess({
|
|
110
|
+
action: 'session_list',
|
|
111
|
+
project_id: TEST_IDS.projectId,
|
|
112
|
+
count: 2,
|
|
113
|
+
sessions: [
|
|
114
|
+
{ id: 'sess-1', title: 'Session 1', status: 'open' },
|
|
115
|
+
{ id: 'sess-2', title: 'Session 2', status: 'open' },
|
|
116
|
+
],
|
|
117
|
+
}),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
const { listOpenSessions } = await import('../tools/sessions.js')
|
|
121
|
+
const result = await listOpenSessions({ project_id: TEST_IDS.projectId })
|
|
122
|
+
|
|
123
|
+
expect(result.isError).toBeUndefined()
|
|
124
|
+
const parsed = parseToolResponse(result)
|
|
125
|
+
expect(parsed.action).toBe('session_list')
|
|
126
|
+
expect(parsed.count).toBe(2)
|
|
127
|
+
expect(parsed.sessions).toHaveLength(2)
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Session entry tools
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
describe('Layer 2: session_append', () => {
|
|
137
|
+
beforeEach(() => {
|
|
138
|
+
vi.restoreAllMocks()
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('should append an entry to a session', async () => {
|
|
142
|
+
vi.mocked(nexusPost).mockResolvedValue(
|
|
143
|
+
mockApiSuccess({
|
|
144
|
+
action: 'session_append',
|
|
145
|
+
entry_id: TEST_IDS.entryId,
|
|
146
|
+
session_id: TEST_IDS.sessionId,
|
|
147
|
+
entry_type: 'note',
|
|
148
|
+
project_id: TEST_IDS.projectId,
|
|
149
|
+
}),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
const { appendSessionEntry } = await import('../tools/append-session-entry.js')
|
|
153
|
+
const result = await appendSessionEntry({
|
|
154
|
+
session_id: TEST_IDS.sessionId,
|
|
155
|
+
entry_type: 'note',
|
|
156
|
+
summary: 'Test note',
|
|
157
|
+
user_id: TEST_IDS.userId,
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
expect(result.isError).toBeUndefined()
|
|
161
|
+
const parsed = parseToolResponse(result)
|
|
162
|
+
expect(parsed.action).toBe('session_append')
|
|
163
|
+
expect(parsed.entry_id).toBe(TEST_IDS.entryId)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('should return error on write isolation violation', async () => {
|
|
167
|
+
vi.mocked(nexusPost).mockResolvedValue(
|
|
168
|
+
mockApiError('Session write isolation: only the session creator can append entries', 403),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
const { appendSessionEntry } = await import('../tools/append-session-entry.js')
|
|
172
|
+
const result = await appendSessionEntry({
|
|
173
|
+
session_id: TEST_IDS.sessionId,
|
|
174
|
+
entry_type: 'note',
|
|
175
|
+
summary: 'Unauthorized note',
|
|
176
|
+
user_id: TEST_IDS.userId,
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
expect(result.isError).toBe(true)
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// Task tools
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
describe('Layer 2: Task tools', () => {
|
|
188
|
+
beforeEach(() => {
|
|
189
|
+
vi.restoreAllMocks()
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
describe('createTask', () => {
|
|
193
|
+
it('should create a task and return task data', async () => {
|
|
194
|
+
vi.mocked(nexusPost).mockResolvedValue(
|
|
195
|
+
mockApiSuccess({
|
|
196
|
+
action: 'task_create',
|
|
197
|
+
task_id: TEST_IDS.taskId,
|
|
198
|
+
project_id: TEST_IDS.projectId,
|
|
199
|
+
title: 'New Task',
|
|
200
|
+
status: 'open',
|
|
201
|
+
priority: 'normal',
|
|
202
|
+
}),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
const { createTask } = await import('../tools/create-task.js')
|
|
206
|
+
const result = await createTask({
|
|
207
|
+
project_id: TEST_IDS.projectId,
|
|
208
|
+
title: 'New Task',
|
|
209
|
+
user_id: TEST_IDS.userId,
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
expect(result.isError).toBeUndefined()
|
|
213
|
+
const parsed = parseToolResponse(result)
|
|
214
|
+
expect(parsed.action).toBe('task_create')
|
|
215
|
+
expect(parsed.task_id).toBe(TEST_IDS.taskId)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('should return error on API failure', async () => {
|
|
219
|
+
vi.mocked(nexusPost).mockResolvedValue(mockApiError('FK violation'))
|
|
220
|
+
|
|
221
|
+
const { createTask } = await import('../tools/create-task.js')
|
|
222
|
+
const result = await createTask({
|
|
223
|
+
project_id: TEST_IDS.projectId,
|
|
224
|
+
title: 'Fail Task',
|
|
225
|
+
user_id: TEST_IDS.userId,
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
expect(result.isError).toBe(true)
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
describe('updateTaskStatus', () => {
|
|
233
|
+
it('should update task status', async () => {
|
|
234
|
+
vi.mocked(nexusPost).mockResolvedValue(
|
|
235
|
+
mockApiSuccess({
|
|
236
|
+
action: 'task_update',
|
|
237
|
+
task_id: TEST_IDS.taskId,
|
|
238
|
+
previous_status: 'open',
|
|
239
|
+
new_status: 'in_progress',
|
|
240
|
+
}),
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
const { updateTaskStatus } = await import('../tools/update-task-status.js')
|
|
244
|
+
const result = await updateTaskStatus({
|
|
245
|
+
task_id: TEST_IDS.taskId,
|
|
246
|
+
status: 'in_progress',
|
|
247
|
+
user_id: TEST_IDS.userId,
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
expect(result.isError).toBeUndefined()
|
|
251
|
+
const parsed = parseToolResponse(result)
|
|
252
|
+
expect(parsed.action).toBe('task_update')
|
|
253
|
+
expect(parsed.previous_status).toBe('open')
|
|
254
|
+
expect(parsed.new_status).toBe('in_progress')
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('should return error if task not found', async () => {
|
|
258
|
+
vi.mocked(nexusPost).mockResolvedValue(mockApiError('Task not found', 404))
|
|
259
|
+
|
|
260
|
+
const { updateTaskStatus } = await import('../tools/update-task-status.js')
|
|
261
|
+
const result = await updateTaskStatus({
|
|
262
|
+
task_id: TEST_IDS.taskId,
|
|
263
|
+
status: 'done',
|
|
264
|
+
user_id: TEST_IDS.userId,
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
expect(result.isError).toBe(true)
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
describe('addTaskNote', () => {
|
|
272
|
+
it('should add a note to an existing task', async () => {
|
|
273
|
+
vi.mocked(nexusPost).mockResolvedValue(
|
|
274
|
+
mockApiSuccess({
|
|
275
|
+
action: 'task_note',
|
|
276
|
+
note_id: TEST_IDS.noteId,
|
|
277
|
+
task_id: TEST_IDS.taskId,
|
|
278
|
+
}),
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
const { addTaskNote } = await import('../tools/add-task-note.js')
|
|
282
|
+
const result = await addTaskNote({
|
|
283
|
+
task_id: TEST_IDS.taskId,
|
|
284
|
+
note: 'Progress update',
|
|
285
|
+
user_id: TEST_IDS.userId,
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
expect(result.isError).toBeUndefined()
|
|
289
|
+
const parsed = parseToolResponse(result)
|
|
290
|
+
expect(parsed.action).toBe('task_note')
|
|
291
|
+
expect(parsed.note_id).toBe(TEST_IDS.noteId)
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('should return error if task does not exist', async () => {
|
|
295
|
+
vi.mocked(nexusPost).mockResolvedValue(mockApiError('Task not found', 404))
|
|
296
|
+
|
|
297
|
+
const { addTaskNote } = await import('../tools/add-task-note.js')
|
|
298
|
+
const result = await addTaskNote({
|
|
299
|
+
task_id: TEST_IDS.taskId,
|
|
300
|
+
note: 'Ghost note',
|
|
301
|
+
user_id: TEST_IDS.userId,
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
expect(result.isError).toBe(true)
|
|
305
|
+
})
|
|
306
|
+
})
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
// Letter tools
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
describe('Layer 2: Letter tools', () => {
|
|
314
|
+
beforeEach(() => {
|
|
315
|
+
vi.restoreAllMocks()
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
describe('createLetter', () => {
|
|
319
|
+
it('should create a letter', async () => {
|
|
320
|
+
vi.mocked(nexusPost).mockResolvedValue(
|
|
321
|
+
mockApiSuccess({
|
|
322
|
+
action: 'vl_create',
|
|
323
|
+
letter_id: TEST_IDS.letterId,
|
|
324
|
+
project_id: TEST_IDS.projectId,
|
|
325
|
+
from_actor: 'nexus-app-agent',
|
|
326
|
+
to_actor: 'human',
|
|
327
|
+
subject: 'Review needed',
|
|
328
|
+
status: 'new',
|
|
329
|
+
from_resolved: { agent_id: null, user_id: null },
|
|
330
|
+
to_resolved: { agent_id: null, user_id: null },
|
|
331
|
+
}),
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
const { createLetter } = await import('../tools/letters.js')
|
|
335
|
+
const result = await createLetter({
|
|
336
|
+
project_id: TEST_IDS.projectId,
|
|
337
|
+
from_actor: 'nexus-app-agent',
|
|
338
|
+
to_actor: 'human',
|
|
339
|
+
subject: 'Review needed',
|
|
340
|
+
body: 'Please review the ADR',
|
|
341
|
+
user_id: TEST_IDS.userId,
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
expect(result.isError).toBeUndefined()
|
|
345
|
+
const parsed = parseToolResponse(result)
|
|
346
|
+
expect(parsed.action).toBe('vl_create')
|
|
347
|
+
expect(parsed.letter_id).toBe(TEST_IDS.letterId)
|
|
348
|
+
expect(parsed.from_resolved).toBeDefined()
|
|
349
|
+
expect(parsed.to_resolved).toBeDefined()
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
it('should return error on API failure', async () => {
|
|
353
|
+
vi.mocked(nexusPost).mockResolvedValue(mockApiError('Failed to create letter'))
|
|
354
|
+
|
|
355
|
+
const { createLetter } = await import('../tools/letters.js')
|
|
356
|
+
const result = await createLetter({
|
|
357
|
+
project_id: TEST_IDS.projectId,
|
|
358
|
+
from_actor: 'a',
|
|
359
|
+
to_actor: 'b',
|
|
360
|
+
subject: 'Test',
|
|
361
|
+
body: 'Body',
|
|
362
|
+
user_id: TEST_IDS.userId,
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
expect(result.isError).toBe(true)
|
|
366
|
+
})
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
describe('replyLetter', () => {
|
|
370
|
+
it('should reply to a letter', async () => {
|
|
371
|
+
vi.mocked(nexusPost).mockResolvedValue(
|
|
372
|
+
mockApiSuccess({
|
|
373
|
+
action: 'vl_reply',
|
|
374
|
+
letter_id: TEST_IDS.letterId,
|
|
375
|
+
message_type: 'response',
|
|
376
|
+
}),
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
const { replyLetter } = await import('../tools/letters.js')
|
|
380
|
+
const result = await replyLetter({
|
|
381
|
+
letter_id: TEST_IDS.letterId,
|
|
382
|
+
body: 'Reply content',
|
|
383
|
+
user_id: TEST_IDS.userId,
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
expect(result.isError).toBeUndefined()
|
|
387
|
+
const parsed = parseToolResponse(result)
|
|
388
|
+
expect(parsed.action).toBe('vl_reply')
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
it('should include new_status when provided', async () => {
|
|
392
|
+
vi.mocked(nexusPost).mockResolvedValue(
|
|
393
|
+
mockApiSuccess({
|
|
394
|
+
action: 'vl_reply',
|
|
395
|
+
letter_id: TEST_IDS.letterId,
|
|
396
|
+
message_type: 'response',
|
|
397
|
+
new_status: 'acknowledged',
|
|
398
|
+
}),
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
const { replyLetter } = await import('../tools/letters.js')
|
|
402
|
+
const result = await replyLetter({
|
|
403
|
+
letter_id: TEST_IDS.letterId,
|
|
404
|
+
body: 'Acknowledged',
|
|
405
|
+
new_status: 'acknowledged',
|
|
406
|
+
user_id: TEST_IDS.userId,
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
expect(result.isError).toBeUndefined()
|
|
410
|
+
const parsed = parseToolResponse(result)
|
|
411
|
+
expect(parsed.new_status).toBe('acknowledged')
|
|
412
|
+
})
|
|
413
|
+
})
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
// ---------------------------------------------------------------------------
|
|
417
|
+
// Inbox / Outbox / Acknowledge tools
|
|
418
|
+
// ---------------------------------------------------------------------------
|
|
419
|
+
|
|
420
|
+
describe('Layer 2: Inbox and Outbox tools', () => {
|
|
421
|
+
beforeEach(() => {
|
|
422
|
+
vi.restoreAllMocks()
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
describe('listInbox', () => {
|
|
426
|
+
it('should list inbox letters', async () => {
|
|
427
|
+
vi.mocked(nexusPost).mockResolvedValue(
|
|
428
|
+
mockApiSuccess({
|
|
429
|
+
action: 'vl_inbox',
|
|
430
|
+
project_id: TEST_IDS.projectId,
|
|
431
|
+
count: 1,
|
|
432
|
+
letters: [
|
|
433
|
+
{
|
|
434
|
+
id: TEST_IDS.letterId,
|
|
435
|
+
from_actor: 'other-agent',
|
|
436
|
+
to_actor: 'human',
|
|
437
|
+
subject: 'Incoming letter',
|
|
438
|
+
status: 'new',
|
|
439
|
+
},
|
|
440
|
+
],
|
|
441
|
+
}),
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
const { listInbox } = await import('../tools/letter-inbox.js')
|
|
445
|
+
const result = await listInbox({
|
|
446
|
+
project_id: TEST_IDS.projectId,
|
|
447
|
+
user_id: TEST_IDS.userId,
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
expect(result.isError).toBeUndefined()
|
|
451
|
+
const parsed = parseToolResponse(result)
|
|
452
|
+
expect(parsed.action).toBe('vl_inbox')
|
|
453
|
+
expect(parsed.count).toBe(1)
|
|
454
|
+
expect(parsed.letters).toHaveLength(1)
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
it('should return error on API failure', async () => {
|
|
458
|
+
vi.mocked(nexusPost).mockResolvedValue(mockApiError('Failed to list inbox'))
|
|
459
|
+
|
|
460
|
+
const { listInbox } = await import('../tools/letter-inbox.js')
|
|
461
|
+
const result = await listInbox({
|
|
462
|
+
project_id: TEST_IDS.projectId,
|
|
463
|
+
user_id: TEST_IDS.userId,
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
expect(result.isError).toBe(true)
|
|
467
|
+
})
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
describe('listOutbox', () => {
|
|
471
|
+
it('should list outbox letters', async () => {
|
|
472
|
+
vi.mocked(nexusPost).mockResolvedValue(
|
|
473
|
+
mockApiSuccess({
|
|
474
|
+
action: 'vl_outbox',
|
|
475
|
+
project_id: TEST_IDS.projectId,
|
|
476
|
+
count: 1,
|
|
477
|
+
letters: [
|
|
478
|
+
{
|
|
479
|
+
id: TEST_IDS.letterId,
|
|
480
|
+
from_actor: 'nexus-app-agent',
|
|
481
|
+
to_actor: 'human',
|
|
482
|
+
subject: 'Outgoing letter',
|
|
483
|
+
status: 'new',
|
|
484
|
+
},
|
|
485
|
+
],
|
|
486
|
+
}),
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
const { listOutbox } = await import('../tools/letter-inbox.js')
|
|
490
|
+
const result = await listOutbox({
|
|
491
|
+
project_id: TEST_IDS.projectId,
|
|
492
|
+
user_id: TEST_IDS.userId,
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
expect(result.isError).toBeUndefined()
|
|
496
|
+
const parsed = parseToolResponse(result)
|
|
497
|
+
expect(parsed.action).toBe('vl_outbox')
|
|
498
|
+
expect(parsed.count).toBe(1)
|
|
499
|
+
})
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
describe('acknowledgeLetter', () => {
|
|
503
|
+
it('should acknowledge a letter', async () => {
|
|
504
|
+
vi.mocked(nexusPost).mockResolvedValue(
|
|
505
|
+
mockApiSuccess({
|
|
506
|
+
action: 'vl_ack',
|
|
507
|
+
letter_id: TEST_IDS.letterId,
|
|
508
|
+
new_status: 'acknowledged',
|
|
509
|
+
}),
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
const { acknowledgeLetter } = await import('../tools/letter-inbox.js')
|
|
513
|
+
const result = await acknowledgeLetter({
|
|
514
|
+
letter_id: TEST_IDS.letterId,
|
|
515
|
+
user_id: TEST_IDS.userId,
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
expect(result.isError).toBeUndefined()
|
|
519
|
+
const parsed = parseToolResponse(result)
|
|
520
|
+
expect(parsed.action).toBe('vl_ack')
|
|
521
|
+
expect(parsed.new_status).toBe('acknowledged')
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
it('should return error on API failure', async () => {
|
|
525
|
+
vi.mocked(nexusPost).mockResolvedValue(mockApiError('Letter not found', 404))
|
|
526
|
+
|
|
527
|
+
const { acknowledgeLetter } = await import('../tools/letter-inbox.js')
|
|
528
|
+
const result = await acknowledgeLetter({
|
|
529
|
+
letter_id: TEST_IDS.letterId,
|
|
530
|
+
user_id: TEST_IDS.userId,
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
expect(result.isError).toBe(true)
|
|
534
|
+
})
|
|
535
|
+
})
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
// ---------------------------------------------------------------------------
|
|
539
|
+
// Ingest document tool
|
|
540
|
+
// ---------------------------------------------------------------------------
|
|
541
|
+
|
|
542
|
+
describe('Layer 2: doc_ingest', () => {
|
|
543
|
+
beforeEach(() => {
|
|
544
|
+
vi.restoreAllMocks()
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
it('should ingest a document', async () => {
|
|
548
|
+
vi.mocked(nexusPost).mockResolvedValue(
|
|
549
|
+
mockApiSuccess({
|
|
550
|
+
action: 'doc_ingest',
|
|
551
|
+
document_id: TEST_IDS.documentId,
|
|
552
|
+
project_id: TEST_IDS.projectId,
|
|
553
|
+
title: 'Research Findings',
|
|
554
|
+
classification: 'unclassified',
|
|
555
|
+
source: 'agent:nexus-app-agent',
|
|
556
|
+
body_length: 33,
|
|
557
|
+
}),
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
const { ingestDocument } = await import('../tools/ingest-document.js')
|
|
561
|
+
const result = await ingestDocument({
|
|
562
|
+
project_id: TEST_IDS.projectId,
|
|
563
|
+
title: 'Research Findings',
|
|
564
|
+
body: '# Findings\n\nSome research content',
|
|
565
|
+
agent_id: 'nexus-app-agent',
|
|
566
|
+
user_id: TEST_IDS.userId,
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
expect(result.isError).toBeUndefined()
|
|
570
|
+
const parsed = parseToolResponse(result)
|
|
571
|
+
expect(parsed.action).toBe('doc_ingest')
|
|
572
|
+
expect(parsed.document_id).toBe(TEST_IDS.documentId)
|
|
573
|
+
expect(parsed.classification).toBe('unclassified')
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
it('should return error on API failure', async () => {
|
|
577
|
+
vi.mocked(nexusPost).mockResolvedValue(mockApiError('RLS denied'))
|
|
578
|
+
|
|
579
|
+
const { ingestDocument } = await import('../tools/ingest-document.js')
|
|
580
|
+
const result = await ingestDocument({
|
|
581
|
+
project_id: TEST_IDS.projectId,
|
|
582
|
+
title: 'Fail',
|
|
583
|
+
body: 'Content',
|
|
584
|
+
user_id: TEST_IDS.userId,
|
|
585
|
+
})
|
|
586
|
+
|
|
587
|
+
expect(result.isError).toBe(true)
|
|
588
|
+
})
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
// ---------------------------------------------------------------------------
|
|
592
|
+
// Skill tools
|
|
593
|
+
// ---------------------------------------------------------------------------
|
|
594
|
+
|
|
595
|
+
describe('Layer 2: Skill tools', () => {
|
|
596
|
+
beforeEach(() => {
|
|
597
|
+
vi.restoreAllMocks()
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
describe('skList', () => {
|
|
601
|
+
it('should list skills', async () => {
|
|
602
|
+
vi.mocked(nexusPost).mockResolvedValue(
|
|
603
|
+
mockApiSuccess({
|
|
604
|
+
action: 'sk_list',
|
|
605
|
+
count: 2,
|
|
606
|
+
skills: [
|
|
607
|
+
{ id: TEST_IDS.skillId, skill_id: 'nx-init-nexus', name: 'Init Nexus', status: 'active' },
|
|
608
|
+
{ id: '99999999-0000-1111-2222-333333333333', skill_id: 'nx-git-commit', name: 'Git Commit', status: 'active' },
|
|
609
|
+
],
|
|
610
|
+
}),
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
const { skList } = await import('../tools/skills.js')
|
|
614
|
+
const result = await skList({ user_id: TEST_IDS.userId })
|
|
615
|
+
|
|
616
|
+
expect(result.isError).toBeUndefined()
|
|
617
|
+
const parsed = parseToolResponse(result)
|
|
618
|
+
expect(parsed.action).toBe('sk_list')
|
|
619
|
+
expect(parsed.count).toBe(2)
|
|
620
|
+
expect(parsed.skills).toHaveLength(2)
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
it('should return error on API failure', async () => {
|
|
624
|
+
vi.mocked(nexusPost).mockResolvedValue(mockApiError('Could not resolve tenant'))
|
|
625
|
+
|
|
626
|
+
const { skList } = await import('../tools/skills.js')
|
|
627
|
+
const result = await skList({ user_id: TEST_IDS.userId })
|
|
628
|
+
|
|
629
|
+
expect(result.isError).toBe(true)
|
|
630
|
+
})
|
|
631
|
+
})
|
|
632
|
+
|
|
633
|
+
describe('skGet', () => {
|
|
634
|
+
it('should get a skill by identifier', async () => {
|
|
635
|
+
vi.mocked(nexusPost).mockResolvedValue(
|
|
636
|
+
mockApiSuccess({
|
|
637
|
+
action: 'sk_get',
|
|
638
|
+
skill: {
|
|
639
|
+
id: TEST_IDS.skillId,
|
|
640
|
+
skill_id: 'nx-init-nexus',
|
|
641
|
+
name: 'Init Nexus',
|
|
642
|
+
body: '# Init Nexus\n\nInstructions here.',
|
|
643
|
+
status: 'active',
|
|
644
|
+
},
|
|
645
|
+
command: { id: '11111111-0000-1111-2222-333333333333', command_slug: 'nexus-init-nexus', active: true },
|
|
646
|
+
}),
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
const { skGet } = await import('../tools/skills.js')
|
|
650
|
+
const result = await skGet({ skill_id: 'nx-init-nexus', user_id: TEST_IDS.userId })
|
|
651
|
+
|
|
652
|
+
expect(result.isError).toBeUndefined()
|
|
653
|
+
const parsed = parseToolResponse(result)
|
|
654
|
+
expect(parsed.action).toBe('sk_get')
|
|
655
|
+
expect(parsed.skill.skill_id).toBe('nx-init-nexus')
|
|
656
|
+
expect(parsed.command.command_slug).toBe('nexus-init-nexus')
|
|
657
|
+
})
|
|
658
|
+
|
|
659
|
+
it('should return error if skill not found', async () => {
|
|
660
|
+
vi.mocked(nexusPost).mockResolvedValue(mockApiError('Skill not found', 404))
|
|
661
|
+
|
|
662
|
+
const { skGet } = await import('../tools/skills.js')
|
|
663
|
+
const result = await skGet({ skill_id: 'nx-nonexistent', user_id: TEST_IDS.userId })
|
|
664
|
+
|
|
665
|
+
expect(result.isError).toBe(true)
|
|
666
|
+
})
|
|
667
|
+
})
|
|
668
|
+
|
|
669
|
+
describe('skCreate', () => {
|
|
670
|
+
it('should create a skill in draft status', async () => {
|
|
671
|
+
vi.mocked(nexusPost).mockResolvedValue(
|
|
672
|
+
mockApiSuccess({
|
|
673
|
+
action: 'sk_create',
|
|
674
|
+
skill_id: TEST_IDS.skillId,
|
|
675
|
+
skill_identifier: 'nx-test-skill',
|
|
676
|
+
status: 'draft',
|
|
677
|
+
command_slug: 'nexus-test-skill',
|
|
678
|
+
}),
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
const { skCreate } = await import('../tools/skills.js')
|
|
682
|
+
const result = await skCreate({
|
|
683
|
+
skill_id: 'nx-test-skill',
|
|
684
|
+
name: 'Test Skill',
|
|
685
|
+
body: '# Test\n\nSkill body',
|
|
686
|
+
user_id: TEST_IDS.userId,
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
expect(result.isError).toBeUndefined()
|
|
690
|
+
const parsed = parseToolResponse(result)
|
|
691
|
+
expect(parsed.action).toBe('sk_create')
|
|
692
|
+
expect(parsed.skill_identifier).toBe('nx-test-skill')
|
|
693
|
+
expect(parsed.status).toBe('draft')
|
|
694
|
+
expect(parsed.command_slug).toBe('nexus-test-skill')
|
|
695
|
+
})
|
|
696
|
+
})
|
|
697
|
+
|
|
698
|
+
describe('skUpdate', () => {
|
|
699
|
+
it('should update a skill', async () => {
|
|
700
|
+
vi.mocked(nexusPost).mockResolvedValue(
|
|
701
|
+
mockApiSuccess({
|
|
702
|
+
action: 'sk_update',
|
|
703
|
+
skill_id: TEST_IDS.skillId,
|
|
704
|
+
skill_identifier: 'nx-init-nexus',
|
|
705
|
+
updated_fields: ['body', 'version'],
|
|
706
|
+
}),
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
const { skUpdate } = await import('../tools/skills.js')
|
|
710
|
+
const result = await skUpdate({
|
|
711
|
+
skill_id: 'nx-init-nexus',
|
|
712
|
+
body: '# Updated\n\nNew content',
|
|
713
|
+
user_id: TEST_IDS.userId,
|
|
714
|
+
})
|
|
715
|
+
|
|
716
|
+
expect(result.isError).toBeUndefined()
|
|
717
|
+
const parsed = parseToolResponse(result)
|
|
718
|
+
expect(parsed.action).toBe('sk_update')
|
|
719
|
+
expect(parsed.updated_fields).toContain('body')
|
|
720
|
+
})
|
|
721
|
+
|
|
722
|
+
it('should return error if skill not found', async () => {
|
|
723
|
+
vi.mocked(nexusPost).mockResolvedValue(mockApiError('Skill not found', 404))
|
|
724
|
+
|
|
725
|
+
const { skUpdate } = await import('../tools/skills.js')
|
|
726
|
+
const result = await skUpdate({
|
|
727
|
+
skill_id: 'nx-nonexistent',
|
|
728
|
+
name: 'Updated',
|
|
729
|
+
user_id: TEST_IDS.userId,
|
|
730
|
+
})
|
|
731
|
+
|
|
732
|
+
expect(result.isError).toBe(true)
|
|
733
|
+
})
|
|
734
|
+
})
|
|
735
|
+
|
|
736
|
+
describe('skActivate', () => {
|
|
737
|
+
it('should activate a skill', async () => {
|
|
738
|
+
vi.mocked(nexusPost).mockResolvedValue(
|
|
739
|
+
mockApiSuccess({
|
|
740
|
+
action: 'sk_activate',
|
|
741
|
+
skill_id: TEST_IDS.skillId,
|
|
742
|
+
skill_identifier: 'nx-code-review',
|
|
743
|
+
previous_status: 'draft',
|
|
744
|
+
new_status: 'active',
|
|
745
|
+
}),
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
const { skActivate } = await import('../tools/skills.js')
|
|
749
|
+
const result = await skActivate({
|
|
750
|
+
skill_id: 'nx-code-review',
|
|
751
|
+
status: 'active',
|
|
752
|
+
user_id: TEST_IDS.userId,
|
|
753
|
+
})
|
|
754
|
+
|
|
755
|
+
expect(result.isError).toBeUndefined()
|
|
756
|
+
const parsed = parseToolResponse(result)
|
|
757
|
+
expect(parsed.action).toBe('sk_activate')
|
|
758
|
+
expect(parsed.previous_status).toBe('draft')
|
|
759
|
+
expect(parsed.new_status).toBe('active')
|
|
760
|
+
})
|
|
761
|
+
|
|
762
|
+
it('should return error if skill not found', async () => {
|
|
763
|
+
vi.mocked(nexusPost).mockResolvedValue(mockApiError('Skill not found', 404))
|
|
764
|
+
|
|
765
|
+
const { skActivate } = await import('../tools/skills.js')
|
|
766
|
+
const result = await skActivate({
|
|
767
|
+
skill_id: 'nx-nonexistent',
|
|
768
|
+
status: 'active',
|
|
769
|
+
user_id: TEST_IDS.userId,
|
|
770
|
+
})
|
|
771
|
+
|
|
772
|
+
expect(result.isError).toBe(true)
|
|
773
|
+
})
|
|
774
|
+
})
|
|
775
|
+
})
|