@gwdn/nexus-mcp 0.8.6 → 0.8.8

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 (52) hide show
  1. package/README.md +3 -4
  2. package/dist/nexus-api.d.ts.map +1 -1
  3. package/dist/nexus-api.js +10 -1
  4. package/dist/nexus-api.js.map +1 -1
  5. package/dist/server.js +1 -1
  6. package/package.json +9 -2
  7. package/.github/workflows/ci.yml +0 -38
  8. package/.github/workflows/release.yml +0 -57
  9. package/src/__tests__/auth.test.ts +0 -162
  10. package/src/__tests__/decision-comments.test.ts +0 -173
  11. package/src/__tests__/e2e-integration.test.ts +0 -443
  12. package/src/__tests__/helpers.ts +0 -58
  13. package/src/__tests__/layer1-knowledge.test.ts +0 -302
  14. package/src/__tests__/layer2-coordination.test.ts +0 -918
  15. package/src/__tests__/layer3-governance.test.ts +0 -205
  16. package/src/__tests__/machine-id.test.ts +0 -136
  17. package/src/__tests__/nexus-api.test.ts +0 -127
  18. package/src/__tests__/project-list-and-skill-assign.test.ts +0 -282
  19. package/src/__tests__/reviews.test.ts +0 -420
  20. package/src/__tests__/server.test.ts +0 -251
  21. package/src/__tests__/setup.ts +0 -15
  22. package/src/__tests__/task-list-and-doc-list.test.ts +0 -226
  23. package/src/auth.ts +0 -81
  24. package/src/machine-id.ts +0 -60
  25. package/src/nexus-api.ts +0 -141
  26. package/src/server.ts +0 -629
  27. package/src/tools/add-task-note.ts +0 -50
  28. package/src/tools/append-session-entry.ts +0 -113
  29. package/src/tools/classify-document.ts +0 -55
  30. package/src/tools/create-task.ts +0 -66
  31. package/src/tools/decision-comments.ts +0 -103
  32. package/src/tools/delete-document.ts +0 -43
  33. package/src/tools/directives.ts +0 -303
  34. package/src/tools/get-document.ts +0 -80
  35. package/src/tools/get-project-memory.ts +0 -65
  36. package/src/tools/get-related-entities.ts +0 -73
  37. package/src/tools/governance.ts +0 -165
  38. package/src/tools/ingest-document.ts +0 -65
  39. package/src/tools/letter-inbox.ts +0 -157
  40. package/src/tools/letters.ts +0 -144
  41. package/src/tools/list-documents.ts +0 -58
  42. package/src/tools/list-tasks.ts +0 -57
  43. package/src/tools/project-list.ts +0 -52
  44. package/src/tools/reviews.ts +0 -281
  45. package/src/tools/search-knowledge.ts +0 -68
  46. package/src/tools/sessions.ts +0 -157
  47. package/src/tools/skill-assign.ts +0 -142
  48. package/src/tools/skills.ts +0 -255
  49. package/src/tools/update-document.ts +0 -66
  50. package/src/tools/update-task-status.ts +0 -64
  51. package/tsconfig.json +0 -20
  52. 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
- })