@gwdn/nexus-mcp 0.8.7 → 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.
- package/dist/server.js +0 -0
- package/package.json +8 -2
- package/.github/workflows/ci.yml +0 -38
- package/.github/workflows/release.yml +0 -57
- package/src/__tests__/auth.test.ts +0 -162
- package/src/__tests__/decision-comments.test.ts +0 -173
- package/src/__tests__/e2e-integration.test.ts +0 -443
- package/src/__tests__/helpers.ts +0 -58
- package/src/__tests__/layer1-knowledge.test.ts +0 -302
- package/src/__tests__/layer2-coordination.test.ts +0 -918
- package/src/__tests__/layer3-governance.test.ts +0 -205
- package/src/__tests__/machine-id.test.ts +0 -136
- package/src/__tests__/nexus-api.test.ts +0 -127
- package/src/__tests__/project-list-and-skill-assign.test.ts +0 -282
- package/src/__tests__/reviews.test.ts +0 -420
- package/src/__tests__/server.test.ts +0 -251
- package/src/__tests__/setup.ts +0 -15
- package/src/__tests__/task-list-and-doc-list.test.ts +0 -226
- package/src/auth.ts +0 -81
- package/src/machine-id.ts +0 -60
- package/src/nexus-api.ts +0 -149
- package/src/server.ts +0 -629
- package/src/tools/add-task-note.ts +0 -50
- package/src/tools/append-session-entry.ts +0 -113
- package/src/tools/classify-document.ts +0 -55
- package/src/tools/create-task.ts +0 -66
- package/src/tools/decision-comments.ts +0 -103
- package/src/tools/delete-document.ts +0 -43
- package/src/tools/directives.ts +0 -303
- package/src/tools/get-document.ts +0 -80
- package/src/tools/get-project-memory.ts +0 -65
- package/src/tools/get-related-entities.ts +0 -73
- package/src/tools/governance.ts +0 -165
- package/src/tools/ingest-document.ts +0 -65
- package/src/tools/letter-inbox.ts +0 -157
- package/src/tools/letters.ts +0 -144
- package/src/tools/list-documents.ts +0 -58
- package/src/tools/list-tasks.ts +0 -57
- package/src/tools/project-list.ts +0 -52
- package/src/tools/reviews.ts +0 -281
- package/src/tools/search-knowledge.ts +0 -68
- package/src/tools/sessions.ts +0 -157
- package/src/tools/skill-assign.ts +0 -142
- package/src/tools/skills.ts +0 -255
- package/src/tools/update-document.ts +0 -66
- package/src/tools/update-task-status.ts +0 -64
- package/tsconfig.json +0 -21
- 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
|
-
})
|