@gwdn/nexus-mcp 0.8.7 → 0.8.9

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 (53) hide show
  1. package/dist/server.js +1 -1
  2. package/dist/tools/create-task.d.ts +1 -1
  3. package/dist/tools/create-task.js +3 -3
  4. package/dist/tools/directives.d.ts +2 -2
  5. package/dist/tools/letters.d.ts +1 -1
  6. package/dist/tools/update-task-status.d.ts +1 -1
  7. package/package.json +8 -2
  8. package/.github/workflows/ci.yml +0 -38
  9. package/.github/workflows/release.yml +0 -57
  10. package/src/__tests__/auth.test.ts +0 -162
  11. package/src/__tests__/decision-comments.test.ts +0 -173
  12. package/src/__tests__/e2e-integration.test.ts +0 -443
  13. package/src/__tests__/helpers.ts +0 -58
  14. package/src/__tests__/layer1-knowledge.test.ts +0 -302
  15. package/src/__tests__/layer2-coordination.test.ts +0 -918
  16. package/src/__tests__/layer3-governance.test.ts +0 -205
  17. package/src/__tests__/machine-id.test.ts +0 -136
  18. package/src/__tests__/nexus-api.test.ts +0 -127
  19. package/src/__tests__/project-list-and-skill-assign.test.ts +0 -282
  20. package/src/__tests__/reviews.test.ts +0 -420
  21. package/src/__tests__/server.test.ts +0 -251
  22. package/src/__tests__/setup.ts +0 -15
  23. package/src/__tests__/task-list-and-doc-list.test.ts +0 -226
  24. package/src/auth.ts +0 -81
  25. package/src/machine-id.ts +0 -60
  26. package/src/nexus-api.ts +0 -149
  27. package/src/server.ts +0 -629
  28. package/src/tools/add-task-note.ts +0 -50
  29. package/src/tools/append-session-entry.ts +0 -113
  30. package/src/tools/classify-document.ts +0 -55
  31. package/src/tools/create-task.ts +0 -66
  32. package/src/tools/decision-comments.ts +0 -103
  33. package/src/tools/delete-document.ts +0 -43
  34. package/src/tools/directives.ts +0 -303
  35. package/src/tools/get-document.ts +0 -80
  36. package/src/tools/get-project-memory.ts +0 -65
  37. package/src/tools/get-related-entities.ts +0 -73
  38. package/src/tools/governance.ts +0 -165
  39. package/src/tools/ingest-document.ts +0 -65
  40. package/src/tools/letter-inbox.ts +0 -157
  41. package/src/tools/letters.ts +0 -144
  42. package/src/tools/list-documents.ts +0 -58
  43. package/src/tools/list-tasks.ts +0 -57
  44. package/src/tools/project-list.ts +0 -52
  45. package/src/tools/reviews.ts +0 -281
  46. package/src/tools/search-knowledge.ts +0 -68
  47. package/src/tools/sessions.ts +0 -157
  48. package/src/tools/skill-assign.ts +0 -142
  49. package/src/tools/skills.ts +0 -255
  50. package/src/tools/update-document.ts +0 -66
  51. package/src/tools/update-task-status.ts +0 -64
  52. package/tsconfig.json +0 -21
  53. package/vitest.config.ts +0 -8
@@ -1,205 +0,0 @@
1
- /**
2
- * Tests for Layer 3 Governance 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
- describe('Layer 3: ADR Governance tools', () => {
16
- beforeEach(() => {
17
- vi.restoreAllMocks()
18
- })
19
-
20
- describe('createAdrDraft', () => {
21
- it('should create an ADR draft with auto-incremented number', async () => {
22
- vi.mocked(nexusPost).mockResolvedValue(
23
- mockApiSuccess({
24
- action: 'adr_create',
25
- adr_id: TEST_IDS.adrId,
26
- adr_number: '0006',
27
- project_id: TEST_IDS.projectId,
28
- title: 'New ADR',
29
- status: 'draft',
30
- }),
31
- )
32
-
33
- const { createAdrDraft } = await import('../tools/governance.js')
34
- const result = await createAdrDraft({
35
- project_id: TEST_IDS.projectId,
36
- title: 'New ADR',
37
- context: 'ADR context',
38
- decision: 'ADR decision content',
39
- user_id: TEST_IDS.userId,
40
- })
41
-
42
- expect(result.isError).toBeUndefined()
43
- const parsed = parseToolResponse(result)
44
- expect(parsed.action).toBe('adr_create')
45
- expect(parsed.adr_number).toBe('0006')
46
- expect(parsed.status).toBe('draft')
47
- })
48
-
49
- it('should return error on API failure', async () => {
50
- vi.mocked(nexusPost).mockResolvedValue(mockApiError('Failed to create ADR draft'))
51
-
52
- const { createAdrDraft } = await import('../tools/governance.js')
53
- const result = await createAdrDraft({
54
- project_id: TEST_IDS.projectId,
55
- title: 'Fail',
56
- context: 'Context',
57
- decision: 'Decision',
58
- user_id: TEST_IDS.userId,
59
- })
60
-
61
- expect(result.isError).toBe(true)
62
- })
63
- })
64
-
65
- describe('submitAdrReview', () => {
66
- it('should transition a draft ADR to under_review', async () => {
67
- vi.mocked(nexusPost).mockResolvedValue(
68
- mockApiSuccess({
69
- action: 'adr_submit',
70
- adr_id: TEST_IDS.adrId,
71
- title: 'ADR to review',
72
- new_state: 'under_review',
73
- }),
74
- )
75
-
76
- const { submitAdrReview } = await import('../tools/governance.js')
77
- const result = await submitAdrReview({
78
- adr_id: TEST_IDS.adrId,
79
- user_id: TEST_IDS.userId,
80
- })
81
-
82
- expect(result.isError).toBeUndefined()
83
- const parsed = parseToolResponse(result)
84
- expect(parsed.action).toBe('adr_submit')
85
- expect(parsed.new_state).toBe('under_review')
86
- })
87
-
88
- it('should return error for non-draft ADR', async () => {
89
- vi.mocked(nexusPost).mockResolvedValue(
90
- mockApiError('ADR must be in draft state to submit for review', 409),
91
- )
92
-
93
- const { submitAdrReview } = await import('../tools/governance.js')
94
- const result = await submitAdrReview({
95
- adr_id: TEST_IDS.adrId,
96
- user_id: TEST_IDS.userId,
97
- })
98
-
99
- expect(result.isError).toBe(true)
100
- })
101
-
102
- it('should return error if ADR not found', async () => {
103
- vi.mocked(nexusPost).mockResolvedValue(mockApiError('ADR not found', 404))
104
-
105
- const { submitAdrReview } = await import('../tools/governance.js')
106
- const result = await submitAdrReview({
107
- adr_id: TEST_IDS.adrId,
108
- user_id: TEST_IDS.userId,
109
- })
110
-
111
- expect(result.isError).toBe(true)
112
- })
113
- })
114
-
115
- describe('recordAdrDecision', () => {
116
- it('should accept an ADR under review', async () => {
117
- vi.mocked(nexusPost).mockResolvedValue(
118
- mockApiSuccess({
119
- action: 'adr_decide',
120
- adr_id: TEST_IDS.adrId,
121
- title: 'Test ADR',
122
- decision: 'accepted',
123
- new_state: 'accepted',
124
- }),
125
- )
126
-
127
- const { recordAdrDecision } = await import('../tools/governance.js')
128
- const result = await recordAdrDecision({
129
- adr_id: TEST_IDS.adrId,
130
- decision: 'accepted',
131
- user_id: TEST_IDS.userId,
132
- })
133
-
134
- expect(result.isError).toBeUndefined()
135
- const parsed = parseToolResponse(result)
136
- expect(parsed.action).toBe('adr_decide')
137
- expect(parsed.decision).toBe('accepted')
138
- expect(parsed.new_state).toBe('accepted')
139
- })
140
-
141
- it('should reject an ADR under review', async () => {
142
- vi.mocked(nexusPost).mockResolvedValue(
143
- mockApiSuccess({
144
- action: 'adr_decide',
145
- adr_id: TEST_IDS.adrId,
146
- title: 'Test ADR',
147
- decision: 'rejected',
148
- new_state: 'rejected',
149
- }),
150
- )
151
-
152
- const { recordAdrDecision } = await import('../tools/governance.js')
153
- const result = await recordAdrDecision({
154
- adr_id: TEST_IDS.adrId,
155
- decision: 'rejected',
156
- rationale: 'Does not align with platform direction',
157
- user_id: TEST_IDS.userId,
158
- })
159
-
160
- expect(result.isError).toBeUndefined()
161
- const parsed = parseToolResponse(result)
162
- expect(parsed.decision).toBe('rejected')
163
- })
164
-
165
- it('should include superseded_adr in response when accepting with supersedes', async () => {
166
- const supersededId = '11111111-1111-1111-1111-111111111111'
167
- vi.mocked(nexusPost).mockResolvedValue(
168
- mockApiSuccess({
169
- action: 'adr_decide',
170
- adr_id: TEST_IDS.adrId,
171
- title: 'Superseding ADR',
172
- decision: 'accepted',
173
- new_state: 'accepted',
174
- superseded_adr: supersededId,
175
- }),
176
- )
177
-
178
- const { recordAdrDecision } = await import('../tools/governance.js')
179
- const result = await recordAdrDecision({
180
- adr_id: TEST_IDS.adrId,
181
- decision: 'accepted',
182
- user_id: TEST_IDS.userId,
183
- })
184
-
185
- expect(result.isError).toBeUndefined()
186
- const parsed = parseToolResponse(result)
187
- expect(parsed.superseded_adr).toBe(supersededId)
188
- })
189
-
190
- it('should return error for non-review ADR', async () => {
191
- vi.mocked(nexusPost).mockResolvedValue(
192
- mockApiError('ADR must be under review to record a decision', 409),
193
- )
194
-
195
- const { recordAdrDecision } = await import('../tools/governance.js')
196
- const result = await recordAdrDecision({
197
- adr_id: TEST_IDS.adrId,
198
- decision: 'accepted',
199
- user_id: TEST_IDS.userId,
200
- })
201
-
202
- expect(result.isError).toBe(true)
203
- })
204
- })
205
- })
@@ -1,136 +0,0 @@
1
- /**
2
- * Tests for src/machine-id.ts
3
- *
4
- * Covers:
5
- * - Reading existing machine.toml
6
- * - Generating new machine ID when file doesn't exist
7
- * - Caching behavior
8
- * - Graceful failure on I/O errors
9
- */
10
-
11
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
12
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
13
- import { join } from 'node:path'
14
- import { homedir } from 'node:os'
15
-
16
- // Mock fs and os modules
17
- vi.mock('node:fs', () => ({
18
- existsSync: vi.fn(),
19
- mkdirSync: vi.fn(),
20
- readFileSync: vi.fn(),
21
- writeFileSync: vi.fn(),
22
- }))
23
-
24
- vi.mock('node:os', () => ({
25
- homedir: vi.fn(() => '/mock-home'),
26
- }))
27
-
28
- const EXPECTED_TOML_PATH = join('/mock-home', '.config', 'nexus', 'machine.toml')
29
-
30
- describe('machine-id', () => {
31
- beforeEach(() => {
32
- vi.resetModules()
33
- vi.restoreAllMocks()
34
- vi.mocked(homedir).mockReturnValue('/mock-home')
35
- })
36
-
37
- afterEach(() => {
38
- vi.restoreAllMocks()
39
- })
40
-
41
- it('should read machine_id from existing machine.toml', async () => {
42
- vi.mocked(existsSync).mockReturnValue(true)
43
- vi.mocked(readFileSync).mockReturnValue(
44
- '# Nexus persistent machine identifier\nmachine_id = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"\n',
45
- )
46
-
47
- const { getMachineId, resetMachineId } = await import('../machine-id.js')
48
- resetMachineId()
49
- const id = getMachineId()
50
-
51
- expect(id).toBe('aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee')
52
- expect(existsSync).toHaveBeenCalledWith(EXPECTED_TOML_PATH)
53
- expect(readFileSync).toHaveBeenCalledWith(EXPECTED_TOML_PATH, 'utf-8')
54
- })
55
-
56
- it('should generate and persist machine_id when file does not exist', async () => {
57
- vi.mocked(existsSync).mockReturnValue(false)
58
-
59
- const { getMachineId, resetMachineId } = await import('../machine-id.js')
60
- resetMachineId()
61
- const id = getMachineId()
62
-
63
- expect(id).toBeDefined()
64
- expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)
65
- expect(mkdirSync).toHaveBeenCalledWith(
66
- join('/mock-home', '.config', 'nexus'),
67
- { recursive: true },
68
- )
69
- expect(writeFileSync).toHaveBeenCalledWith(
70
- EXPECTED_TOML_PATH,
71
- expect.stringContaining(`machine_id = "${id}"`),
72
- 'utf-8',
73
- )
74
- })
75
-
76
- it('should cache the machine_id after first read', async () => {
77
- vi.mocked(existsSync).mockReturnValue(true)
78
- vi.mocked(readFileSync).mockReturnValue('machine_id = "cached-id-1234"\n')
79
-
80
- const { getMachineId, resetMachineId } = await import('../machine-id.js')
81
- resetMachineId()
82
-
83
- const callsBefore = vi.mocked(readFileSync).mock.calls.length
84
- const id1 = getMachineId()
85
- const id2 = getMachineId()
86
-
87
- expect(id1).toBe('cached-id-1234')
88
- expect(id1).toBe(id2)
89
- // readFileSync should only be called once more after reset (cached on second call)
90
- expect(vi.mocked(readFileSync).mock.calls.length - callsBefore).toBe(1)
91
- })
92
-
93
- it('should return null on I/O error', async () => {
94
- vi.mocked(existsSync).mockImplementation(() => {
95
- throw new Error('Permission denied')
96
- })
97
-
98
- const { getMachineId, resetMachineId } = await import('../machine-id.js')
99
- resetMachineId()
100
- const id = getMachineId()
101
-
102
- expect(id).toBeNull()
103
- })
104
-
105
- it('should generate new ID if toml exists but has no machine_id line', async () => {
106
- vi.mocked(existsSync).mockReturnValue(true)
107
- vi.mocked(readFileSync).mockReturnValue('# empty config\n')
108
-
109
- const { getMachineId, resetMachineId } = await import('../machine-id.js')
110
- resetMachineId()
111
- const id = getMachineId()
112
-
113
- expect(id).toBeDefined()
114
- expect(id).toMatch(/^[0-9a-f]{8}-/)
115
- expect(writeFileSync).toHaveBeenCalled()
116
- })
117
-
118
- it('should resetMachineId clear the cache', async () => {
119
- vi.mocked(existsSync).mockReturnValue(true)
120
- vi.mocked(readFileSync)
121
- .mockReturnValueOnce('machine_id = "first-id"\n')
122
- .mockReturnValueOnce('machine_id = "second-id"\n')
123
-
124
- const { getMachineId, resetMachineId } = await import('../machine-id.js')
125
- resetMachineId()
126
-
127
- const id1 = getMachineId()
128
- expect(id1).toBe('first-id')
129
-
130
- resetMachineId()
131
- const id2 = getMachineId()
132
- expect(id2).toBe('second-id')
133
-
134
- expect(id1).not.toBe(id2)
135
- })
136
- })
@@ -1,127 +0,0 @@
1
- /**
2
- * Tests for src/nexus-api.ts
3
- *
4
- * Covers:
5
- * - X-Nexus-Machine-Id header is sent automatically when machine_id is available
6
- * - Custom headers parameter is forwarded
7
- * - nexusPost convenience works correctly
8
- */
9
-
10
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
11
-
12
- // Mock machine-id module before importing nexus-api
13
- vi.mock('../machine-id.js', () => ({
14
- getMachineId: vi.fn(() => 'test-machine-uuid-1234'),
15
- resetMachineId: vi.fn(),
16
- }))
17
-
18
- describe('nexus-api: header injection', () => {
19
- let nexusApi: typeof import('../nexus-api.js').nexusApi
20
- let nexusPost: typeof import('../nexus-api.js').nexusPost
21
- let fetchSpy: ReturnType<typeof vi.fn>
22
-
23
- beforeEach(async () => {
24
- vi.resetModules()
25
-
26
- process.env.NEXUS_API_URL = 'https://test.nexus.example.com'
27
- process.env.NEXUS_PRIVATE_TOKEN = 'nxs_pat_test-token-for-header-tests-abc'
28
-
29
- fetchSpy = vi.fn().mockResolvedValue({
30
- ok: true,
31
- status: 200,
32
- json: async () => ({ action: 'test' }),
33
- })
34
- vi.stubGlobal('fetch', fetchSpy)
35
-
36
- const mod = await import('../nexus-api.js')
37
- nexusApi = mod.nexusApi
38
- nexusPost = mod.nexusPost
39
- mod.resetApiConfig()
40
- })
41
-
42
- afterEach(() => {
43
- vi.unstubAllGlobals()
44
- })
45
-
46
- it('should include X-Nexus-Machine-Id header in requests', async () => {
47
- await nexusApi('/api/mcp/sessions', { method: 'POST', body: { action: 'test' } })
48
-
49
- expect(fetchSpy).toHaveBeenCalledTimes(1)
50
- const [, opts] = fetchSpy.mock.calls[0]
51
- expect(opts.headers['X-Nexus-Machine-Id']).toBe('test-machine-uuid-1234')
52
- })
53
-
54
- it('should include Authorization and Content-Type headers', async () => {
55
- await nexusApi('/api/mcp/sessions')
56
-
57
- const [, opts] = fetchSpy.mock.calls[0]
58
- expect(opts.headers['Authorization']).toBe(
59
- 'Bearer nxs_pat_test-token-for-header-tests-abc',
60
- )
61
- expect(opts.headers['Content-Type']).toBe('application/json')
62
- })
63
-
64
- it('should merge custom headers with defaults', async () => {
65
- await nexusApi('/api/mcp/sessions', {
66
- method: 'POST',
67
- body: { action: 'test' },
68
- headers: { 'X-Nexus-Toolstack': 'opencode v0.3.0' },
69
- })
70
-
71
- const [, opts] = fetchSpy.mock.calls[0]
72
- expect(opts.headers['X-Nexus-Machine-Id']).toBe('test-machine-uuid-1234')
73
- expect(opts.headers['X-Nexus-Toolstack']).toBe('opencode v0.3.0')
74
- })
75
-
76
- it('should allow custom headers to override machine-id', async () => {
77
- await nexusApi('/api/mcp/sessions', {
78
- headers: { 'X-Nexus-Machine-Id': 'override-id' },
79
- })
80
-
81
- const [, opts] = fetchSpy.mock.calls[0]
82
- expect(opts.headers['X-Nexus-Machine-Id']).toBe('override-id')
83
- })
84
-
85
- it('nexusPost should also include machine-id header', async () => {
86
- await nexusPost('/api/mcp/sessions', { action: 'session_create' })
87
-
88
- const [, opts] = fetchSpy.mock.calls[0]
89
- expect(opts.headers['X-Nexus-Machine-Id']).toBe('test-machine-uuid-1234')
90
- expect(opts.method).toBe('POST')
91
- })
92
- })
93
-
94
- describe('nexus-api: no machine-id available', () => {
95
- beforeEach(async () => {
96
- vi.resetModules()
97
- // Re-mock with null return
98
- vi.doMock('../machine-id.js', () => ({
99
- getMachineId: vi.fn(() => null),
100
- resetMachineId: vi.fn(),
101
- }))
102
-
103
- process.env.NEXUS_API_URL = 'https://test.nexus.example.com'
104
- process.env.NEXUS_PRIVATE_TOKEN = 'nxs_pat_test-token-no-machine-id-abcde'
105
- })
106
-
107
- afterEach(() => {
108
- vi.unstubAllGlobals()
109
- })
110
-
111
- it('should not include X-Nexus-Machine-Id header when machine_id is null', async () => {
112
- const fetchSpy = vi.fn().mockResolvedValue({
113
- ok: true,
114
- status: 200,
115
- json: async () => ({ action: 'test' }),
116
- })
117
- vi.stubGlobal('fetch', fetchSpy)
118
-
119
- const mod = await import('../nexus-api.js')
120
- mod.resetApiConfig()
121
-
122
- await mod.nexusApi('/api/mcp/sessions')
123
-
124
- const [, opts] = fetchSpy.mock.calls[0]
125
- expect(opts.headers).not.toHaveProperty('X-Nexus-Machine-Id')
126
- })
127
- })