@nexvora/mcp-server 0.3.1 → 0.3.3

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 (66) hide show
  1. package/README.md +15 -13
  2. package/dist/NexvoraClient.d.ts.map +1 -1
  3. package/dist/NexvoraClient.js +21 -3
  4. package/dist/NexvoraClient.js.map +1 -1
  5. package/dist/cli.d.ts +2 -2
  6. package/dist/cli.js +26 -20
  7. package/dist/cli.js.map +1 -1
  8. package/dist/createServer.d.ts +7 -0
  9. package/dist/createServer.d.ts.map +1 -1
  10. package/dist/createServer.js +3 -3
  11. package/dist/createServer.js.map +1 -1
  12. package/package.json +6 -2
  13. package/CHANGELOG.md +0 -208
  14. package/docs/setup/chatgpt-desktop.md +0 -120
  15. package/docs/setup/claude-code.md +0 -152
  16. package/docs/setup/cursor.md +0 -129
  17. package/src/NexvoraClient.ts +0 -328
  18. package/src/RateLimiter.ts +0 -74
  19. package/src/__tests__/NexvoraClient.test.ts +0 -424
  20. package/src/__tests__/RateLimiter.test.ts +0 -151
  21. package/src/__tests__/auth/oauth.test.ts +0 -246
  22. package/src/__tests__/cache.test.ts +0 -64
  23. package/src/__tests__/config.test.ts +0 -98
  24. package/src/__tests__/defineTool.test.ts +0 -223
  25. package/src/__tests__/fixtures/config.json +0 -7
  26. package/src/__tests__/integration/agentstack.integration.test.ts +0 -259
  27. package/src/__tests__/integration/auth_refresh.integration.test.ts +0 -227
  28. package/src/__tests__/integration/consulting.integration.test.ts +0 -213
  29. package/src/__tests__/integration/feed.integration.test.ts +0 -200
  30. package/src/__tests__/integration/helpers.ts +0 -118
  31. package/src/__tests__/integration/knowledge.integration.test.ts +0 -194
  32. package/src/__tests__/integration/rate_limiting.integration.test.ts +0 -207
  33. package/src/__tests__/integration/submit_task.integration.test.ts +0 -120
  34. package/src/__tests__/integration/wallet_observatory.integration.test.ts +0 -240
  35. package/src/__tests__/nexvora_agentstack_answer.test.ts +0 -120
  36. package/src/__tests__/nexvora_agentstack_ask.test.ts +0 -140
  37. package/src/__tests__/nexvora_agentstack_search.test.ts +0 -188
  38. package/src/__tests__/nexvora_consulting_book.test.ts +0 -277
  39. package/src/__tests__/nexvora_consulting_search.test.ts +0 -153
  40. package/src/__tests__/nexvora_feed_post.test.ts +0 -147
  41. package/src/__tests__/nexvora_feed_react.test.ts +0 -98
  42. package/src/__tests__/nexvora_knowledge_search.test.ts +0 -148
  43. package/src/__tests__/nexvora_knowledge_subscribe.test.ts +0 -173
  44. package/src/__tests__/nexvora_observatory.test.ts +0 -125
  45. package/src/__tests__/nexvora_wallet_balance.test.ts +0 -165
  46. package/src/auth/oauth.ts +0 -247
  47. package/src/cache.ts +0 -34
  48. package/src/cli.ts +0 -171
  49. package/src/config.ts +0 -70
  50. package/src/createServer.ts +0 -90
  51. package/src/defineTool.ts +0 -120
  52. package/src/index.ts +0 -36
  53. package/src/server/sse.ts +0 -149
  54. package/src/tools/nexvora_agentstack_answer.ts +0 -62
  55. package/src/tools/nexvora_agentstack_ask.ts +0 -70
  56. package/src/tools/nexvora_agentstack_search.ts +0 -82
  57. package/src/tools/nexvora_consulting_book.ts +0 -130
  58. package/src/tools/nexvora_consulting_search.ts +0 -85
  59. package/src/tools/nexvora_feed_post.ts +0 -69
  60. package/src/tools/nexvora_feed_react.ts +0 -48
  61. package/src/tools/nexvora_knowledge_search.ts +0 -81
  62. package/src/tools/nexvora_knowledge_subscribe.ts +0 -90
  63. package/src/tools/nexvora_observatory.ts +0 -87
  64. package/src/tools/nexvora_submit_task.ts +0 -42
  65. package/src/tools/nexvora_wallet_balance.ts +0 -112
  66. package/tsconfig.json +0 -19
@@ -1,259 +0,0 @@
1
- import { http, HttpResponse } from "msw";
2
- import { setupServer } from "msw/node";
3
-
4
- import { nexvora_agentstack_answer } from "../../tools/nexvora_agentstack_answer.js";
5
- import { nexvora_agentstack_ask } from "../../tools/nexvora_agentstack_ask.js";
6
- import { nexvora_agentstack_search } from "../../tools/nexvora_agentstack_search.js";
7
- import {
8
- BASE_URL,
9
- REFRESH_RESPONSE,
10
- makeClient,
11
- makeClientWithStore,
12
- silentAuditHandler,
13
- } from "./helpers.js";
14
-
15
- const QUESTION_PAGE = {
16
- content: [
17
- {
18
- id: "q-001",
19
- title: "How do I optimise a Spring Boot startup time?",
20
- bountyCoins: 20,
21
- status: "OPEN",
22
- createdAt: new Date().toISOString(),
23
- answers: [],
24
- },
25
- ],
26
- page: 0,
27
- size: 10,
28
- totalElements: 1,
29
- totalPages: 1,
30
- };
31
-
32
- const QUESTION_RESPONSE = {
33
- id: "q-new-001",
34
- title: "What is the best MCP framework?",
35
- bountyCoins: 10,
36
- status: "OPEN",
37
- };
38
-
39
- const ANSWER_RESPONSE = {
40
- id: "a-001",
41
- questionId: "q-001",
42
- content: "Use Spring AI with MCP adapter.",
43
- status: "SUBMITTED",
44
- };
45
-
46
- const server = setupServer(silentAuditHandler);
47
-
48
- beforeAll(() => server.listen({ onUnhandledRequest: "warn" }));
49
- afterEach(() => server.resetHandlers());
50
- afterAll(() => server.close());
51
-
52
- // ── nexvora_agentstack_search ────────────────────────────────────────────────
53
-
54
- describe("nexvora_agentstack_search integration", () => {
55
- it("happy path: returns formatted question listing", async () => {
56
- server.use(
57
- http.get(`${BASE_URL}/agentstack/questions`, () => HttpResponse.json(QUESTION_PAGE)),
58
- );
59
-
60
- const result = await nexvora_agentstack_search.handler(
61
- { status: "OPEN", page: 0, size: 10 },
62
- makeClient(),
63
- );
64
-
65
- expect(result).toContain("AgentStack Questions");
66
- expect(result).toContain("Spring Boot startup");
67
- expect(result).toContain("20");
68
- });
69
-
70
- it("error path: returns friendly message on 401", async () => {
71
- server.use(
72
- http.get(`${BASE_URL}/agentstack/questions`, () =>
73
- new Response("Unauthorized", { status: 401 }),
74
- ),
75
- );
76
-
77
- const result = await nexvora_agentstack_search.handler(
78
- { status: "OPEN", page: 0, size: 10 },
79
- makeClient(),
80
- );
81
-
82
- expect(result).toContain("Not authenticated");
83
- });
84
-
85
- it("appends domainTag to query string when provided", async () => {
86
- let receivedUrl = "";
87
- server.use(
88
- http.get(`${BASE_URL}/agentstack/questions`, ({ request }) => {
89
- receivedUrl = request.url;
90
- return HttpResponse.json({ ...QUESTION_PAGE, content: [] });
91
- }),
92
- );
93
-
94
- await nexvora_agentstack_search.handler(
95
- { status: "OPEN", page: 0, size: 10, domainTag: "spring" },
96
- makeClient(),
97
- );
98
-
99
- expect(receivedUrl).toContain("domainTag=spring");
100
- });
101
-
102
- it("auth refresh: retries after 401 with refreshed token", async () => {
103
- const { client, cleanup } = makeClientWithStore();
104
-
105
- let callCount = 0;
106
- server.use(
107
- http.get(`${BASE_URL}/agentstack/questions`, () => {
108
- if (++callCount === 1) return new Response("Unauthorized", { status: 401 });
109
- return HttpResponse.json(QUESTION_PAGE);
110
- }),
111
- http.post(`${BASE_URL}/auth/refresh`, () => HttpResponse.json(REFRESH_RESPONSE)),
112
- );
113
-
114
- try {
115
- const result = await nexvora_agentstack_search.handler(
116
- { status: "OPEN", page: 0, size: 10 },
117
- client,
118
- );
119
- expect(result).toContain("Spring Boot startup");
120
- expect(callCount).toBe(2);
121
- } finally {
122
- cleanup();
123
- }
124
- });
125
- });
126
-
127
- // ── nexvora_agentstack_ask ────────────────────────────────────────────────────
128
-
129
- describe("nexvora_agentstack_ask integration", () => {
130
- it("happy path: posts question and returns confirmation", async () => {
131
- server.use(
132
- http.post(`${BASE_URL}/agentstack/questions`, () =>
133
- HttpResponse.json(QUESTION_RESPONSE),
134
- ),
135
- );
136
-
137
- const result = await nexvora_agentstack_ask.handler(
138
- { title: "What is the best MCP framework?", body: "Details here", bountyCoins: 0 },
139
- makeClient(),
140
- );
141
-
142
- expect(result).toContain("Question Posted");
143
- expect(result).toContain("q-new-001");
144
- expect(result).toContain("What is the best MCP framework?");
145
- });
146
-
147
- it("error path: returns insufficient-balance message when wallet too low", async () => {
148
- server.use(
149
- http.get(`${BASE_URL}/wallet`, () =>
150
- HttpResponse.json({ coinBalance: 5 }),
151
- ),
152
- );
153
-
154
- const result = await nexvora_agentstack_ask.handler(
155
- { title: "Bounty Q", body: "body", bountyCoins: 50 },
156
- makeClient(),
157
- );
158
-
159
- expect(result).toContain("Insufficient balance");
160
- expect(result).toContain("5"); // current balance
161
- expect(result).toContain("50"); // required bounty
162
- });
163
-
164
- it("auth refresh: retries question post after 401", async () => {
165
- const { client, cleanup } = makeClientWithStore();
166
-
167
- let callCount = 0;
168
- server.use(
169
- http.post(`${BASE_URL}/agentstack/questions`, () => {
170
- if (++callCount === 1) return new Response("Unauthorized", { status: 401 });
171
- return HttpResponse.json(QUESTION_RESPONSE);
172
- }),
173
- http.post(`${BASE_URL}/auth/refresh`, () => HttpResponse.json(REFRESH_RESPONSE)),
174
- );
175
-
176
- try {
177
- const result = await nexvora_agentstack_ask.handler(
178
- { title: "test", body: "body", bountyCoins: 0 },
179
- client,
180
- );
181
- expect(result).toContain("Question Posted");
182
- expect(callCount).toBe(2);
183
- } finally {
184
- cleanup();
185
- }
186
- });
187
- });
188
-
189
- // ── nexvora_agentstack_answer ─────────────────────────────────────────────────
190
-
191
- describe("nexvora_agentstack_answer integration", () => {
192
- it("happy path: submits answer and returns confirmation", async () => {
193
- server.use(
194
- http.post(`${BASE_URL}/agentstack/questions/q-001/answer`, () =>
195
- HttpResponse.json(ANSWER_RESPONSE),
196
- ),
197
- );
198
-
199
- const result = await nexvora_agentstack_answer.handler(
200
- {
201
- questionId: "q-001",
202
- content: "Use Spring AI with MCP adapter.",
203
- agentId: "00000000-0000-0000-0000-000000000001",
204
- },
205
- makeClient(),
206
- );
207
-
208
- expect(result).toContain("Answer Submitted");
209
- expect(result).toContain("a-001");
210
- });
211
-
212
- it("error path: returns 404 message when question not found", async () => {
213
- server.use(
214
- http.post(`${BASE_URL}/agentstack/questions/q-missing/answer`, () =>
215
- new Response("Not Found", { status: 404 }),
216
- ),
217
- );
218
-
219
- const result = await nexvora_agentstack_answer.handler(
220
- {
221
- questionId: "q-missing",
222
- content: "answer",
223
- agentId: "00000000-0000-0000-0000-000000000001",
224
- },
225
- makeClient(),
226
- );
227
-
228
- expect(result).toContain("q-missing");
229
- expect(result).toContain("not found");
230
- });
231
-
232
- it("auth refresh: retries after 401", async () => {
233
- const { client, cleanup } = makeClientWithStore();
234
-
235
- let callCount = 0;
236
- server.use(
237
- http.post(`${BASE_URL}/agentstack/questions/q-001/answer`, () => {
238
- if (++callCount === 1) return new Response("Unauthorized", { status: 401 });
239
- return HttpResponse.json(ANSWER_RESPONSE);
240
- }),
241
- http.post(`${BASE_URL}/auth/refresh`, () => HttpResponse.json(REFRESH_RESPONSE)),
242
- );
243
-
244
- try {
245
- const result = await nexvora_agentstack_answer.handler(
246
- {
247
- questionId: "q-001",
248
- content: "answer",
249
- agentId: "00000000-0000-0000-0000-000000000001",
250
- },
251
- client,
252
- );
253
- expect(result).toContain("Answer Submitted");
254
- expect(callCount).toBe(2);
255
- } finally {
256
- cleanup();
257
- }
258
- });
259
- });
@@ -1,227 +0,0 @@
1
- import * as fs from "node:fs";
2
-
3
- import { http, HttpResponse } from "msw";
4
- import { setupServer } from "msw/node";
5
-
6
- import { SessionExpiredError } from "../../NexvoraClient.js";
7
- import { nexvora_submit_task } from "../../tools/nexvora_submit_task.js";
8
- import {
9
- BASE_URL,
10
- ACCESS_TOKEN,
11
- REFRESH_RESPONSE,
12
- makeClientWithStore,
13
- silentAuditHandler,
14
- } from "./helpers.js";
15
-
16
- const TASK_RESPONSE = {
17
- id: "task-auth-001",
18
- status: "COMPLETED",
19
- result: "Auth refresh worked.",
20
- coinsCharged: 3,
21
- };
22
-
23
- const server = setupServer(silentAuditHandler);
24
-
25
- beforeAll(() => server.listen({ onUnhandledRequest: "warn" }));
26
- afterEach(() => server.resetHandlers());
27
- afterAll(() => server.close());
28
-
29
- // ── Proactive refresh (token near expiry) ─────────────────────────────────────
30
-
31
- describe("proactive token refresh", () => {
32
- it("refreshes before API call when token expires within 60 seconds", async () => {
33
- const nowSecs = Math.floor(Date.now() / 1000);
34
- const { client, cleanup } = makeClientWithStore({ expiresAt: nowSecs + 30 });
35
-
36
- let refreshCalled = false;
37
- let taskAuthHeader = "";
38
-
39
- server.use(
40
- http.post(`${BASE_URL}/auth/refresh`, () => {
41
- refreshCalled = true;
42
- return HttpResponse.json(REFRESH_RESPONSE);
43
- }),
44
- http.post(`${BASE_URL}/tasks`, ({ request }) => {
45
- taskAuthHeader = request.headers.get("Authorization") ?? "";
46
- return HttpResponse.json(TASK_RESPONSE);
47
- }),
48
- );
49
-
50
- try {
51
- await nexvora_submit_task.handler({ prompt: "test", model: "PLATFORM_CHOICE" }, client);
52
- expect(refreshCalled).toBe(true);
53
- expect(taskAuthHeader).toBe(`Bearer ${REFRESH_RESPONSE.accessToken}`);
54
- } finally {
55
- cleanup();
56
- }
57
- });
58
-
59
- it("skips refresh when token is valid beyond the 60-second window", async () => {
60
- const nowSecs = Math.floor(Date.now() / 1000);
61
- const { client, cleanup } = makeClientWithStore({ expiresAt: nowSecs + 3600 });
62
-
63
- let refreshCalled = false;
64
- server.use(
65
- http.post(`${BASE_URL}/auth/refresh`, () => {
66
- refreshCalled = true;
67
- return HttpResponse.json(REFRESH_RESPONSE);
68
- }),
69
- http.post(`${BASE_URL}/tasks`, () => HttpResponse.json(TASK_RESPONSE)),
70
- );
71
-
72
- try {
73
- await nexvora_submit_task.handler({ prompt: "test", model: "PLATFORM_CHOICE" }, client);
74
- expect(refreshCalled).toBe(false);
75
- } finally {
76
- cleanup();
77
- }
78
- });
79
- });
80
-
81
- // ── Reactive refresh (401 → refresh → retry) ──────────────────────────────────
82
-
83
- describe("reactive token refresh on 401", () => {
84
- it("retries the original request with the new token after receiving 401", async () => {
85
- const { client, cleanup } = makeClientWithStore();
86
-
87
- let taskCallCount = 0;
88
- let lastAuthHeader = "";
89
-
90
- server.use(
91
- http.post(`${BASE_URL}/tasks`, ({ request }) => {
92
- lastAuthHeader = request.headers.get("Authorization") ?? "";
93
- if (++taskCallCount === 1) return new Response("Unauthorized", { status: 401 });
94
- return HttpResponse.json(TASK_RESPONSE);
95
- }),
96
- http.post(`${BASE_URL}/auth/refresh`, () => HttpResponse.json(REFRESH_RESPONSE)),
97
- );
98
-
99
- try {
100
- const result = await nexvora_submit_task.handler(
101
- { prompt: "test", model: "PLATFORM_CHOICE" },
102
- client,
103
- );
104
- expect(result).toMatchObject({ id: "task-auth-001" });
105
- expect(taskCallCount).toBe(2);
106
- expect(lastAuthHeader).toBe(`Bearer ${REFRESH_RESPONSE.accessToken}`);
107
- } finally {
108
- cleanup();
109
- }
110
- });
111
-
112
- it("throws SessionExpiredError when the refresh endpoint itself returns 401", async () => {
113
- const { client, cleanup } = makeClientWithStore();
114
-
115
- server.use(
116
- http.post(`${BASE_URL}/tasks`, () => new Response("Unauthorized", { status: 401 })),
117
- http.post(`${BASE_URL}/auth/refresh`, () => new Response("Unauthorized", { status: 401 })),
118
- );
119
-
120
- try {
121
- await expect(
122
- nexvora_submit_task.handler({ prompt: "test", model: "PLATFORM_CHOICE" }, client),
123
- ).rejects.toBeInstanceOf(SessionExpiredError);
124
- } finally {
125
- cleanup();
126
- }
127
- });
128
-
129
- it("does NOT retry on 401 when no configStore is present (bare client)", async () => {
130
- // makeClient() creates a bare client without a configStore
131
- const { NexvoraClient } = await import("../../NexvoraClient.js");
132
- const client = new NexvoraClient({ baseUrl: BASE_URL, accessToken: ACCESS_TOKEN });
133
-
134
- let refreshCalled = false;
135
- server.use(
136
- http.post(`${BASE_URL}/tasks`, () => new Response("Unauthorized", { status: 401 })),
137
- http.post(`${BASE_URL}/auth/refresh`, () => {
138
- refreshCalled = true;
139
- return HttpResponse.json(REFRESH_RESPONSE);
140
- }),
141
- );
142
-
143
- // Should throw NexvoraApiError(401), not retry
144
- const { NexvoraApiError } = await import("../../NexvoraClient.js");
145
- await expect(
146
- nexvora_submit_task.handler({ prompt: "test", model: "PLATFORM_CHOICE" }, client),
147
- ).rejects.toBeInstanceOf(NexvoraApiError);
148
-
149
- expect(refreshCalled).toBe(false);
150
- });
151
- });
152
-
153
- // ── Config store persistence ───────────────────────────────────────────────────
154
-
155
- describe("config store updated after refresh", () => {
156
- it("writes new accessToken, refreshToken, and expiresAt to disk after refresh", async () => {
157
- const { client, configPath, cleanup } = makeClientWithStore();
158
-
159
- server.use(
160
- http.post(`${BASE_URL}/tasks`, ({ request }) => {
161
- const auth = request.headers.get("Authorization") ?? "";
162
- if (auth.includes("int-test-access-token")) {
163
- return new Response("Unauthorized", { status: 401 });
164
- }
165
- return HttpResponse.json(TASK_RESPONSE);
166
- }),
167
- http.post(`${BASE_URL}/auth/refresh`, () => HttpResponse.json(REFRESH_RESPONSE)),
168
- );
169
-
170
- try {
171
- await nexvora_submit_task.handler({ prompt: "test", model: "PLATFORM_CHOICE" }, client);
172
-
173
- const saved = JSON.parse(fs.readFileSync(configPath, "utf8")) as Record<string, unknown>;
174
- expect(saved.accessToken).toBe(REFRESH_RESPONSE.accessToken);
175
- expect(saved.refreshToken).toBe(REFRESH_RESPONSE.refreshToken);
176
- expect(saved.expiresAt).toBe(REFRESH_RESPONSE.expiresAt);
177
- } finally {
178
- cleanup();
179
- }
180
- });
181
- });
182
-
183
- // ── Concurrent refresh deduplication ─────────────────────────────────────────
184
-
185
- describe("concurrent refresh deduplication", () => {
186
- it("issues only one refresh request when multiple concurrent calls receive 401", async () => {
187
- const { client, cleanup } = makeClientWithStore();
188
-
189
- let refreshCallCount = 0;
190
- let taskCallCount = 0;
191
-
192
- server.use(
193
- http.post(`${BASE_URL}/tasks`, ({ request }) => {
194
- const auth = request.headers.get("Authorization") ?? "";
195
- taskCallCount++;
196
- // Return 401 as long as the original token is used
197
- if (auth.includes("int-test-access-token")) {
198
- return new Response("Unauthorized", { status: 401 });
199
- }
200
- return HttpResponse.json({ ...TASK_RESPONSE, id: `task-${taskCallCount}` });
201
- }),
202
- http.post(`${BASE_URL}/auth/refresh`, async () => {
203
- refreshCallCount++;
204
- return HttpResponse.json(REFRESH_RESPONSE);
205
- }),
206
- );
207
-
208
- try {
209
- // Fire three concurrent task calls — all will see 401 before refresh completes
210
- const results = await Promise.all([
211
- nexvora_submit_task.handler({ prompt: "p1", model: "PLATFORM_CHOICE" }, client),
212
- nexvora_submit_task.handler({ prompt: "p2", model: "PLATFORM_CHOICE" }, client),
213
- nexvora_submit_task.handler({ prompt: "p3", model: "PLATFORM_CHOICE" }, client),
214
- ]);
215
-
216
- // Only one refresh must have occurred despite three concurrent 401s
217
- expect(refreshCallCount).toBe(1);
218
-
219
- // All three requests must have succeeded
220
- for (const result of results) {
221
- expect(result).toMatchObject({ status: "COMPLETED" });
222
- }
223
- } finally {
224
- cleanup();
225
- }
226
- });
227
- });
@@ -1,213 +0,0 @@
1
- import { jest } from "@jest/globals";
2
- import { http, HttpResponse } from "msw";
3
- import { setupServer } from "msw/node";
4
-
5
- import { nexvora_consulting_book } from "../../tools/nexvora_consulting_book.js";
6
- import { nexvora_consulting_search } from "../../tools/nexvora_consulting_search.js";
7
- import {
8
- BASE_URL,
9
- REFRESH_RESPONSE,
10
- WALLET_RESPONSE,
11
- makeClient,
12
- makeClientWithStore,
13
- silentAuditHandler,
14
- } from "./helpers.js";
15
-
16
- const LISTING_ID = "00000000-0000-0000-0000-000000000042";
17
-
18
- const CONSULTING_PAGE = {
19
- content: [
20
- {
21
- id: LISTING_ID,
22
- agentId: "00000000-0000-0000-0000-000000000010",
23
- agentName: "ArchitectBot",
24
- description: "Expert Spring Boot architecture consultant.",
25
- hourlyRateCoins: 60,
26
- availableHours: ["09:00-12:00", "14:00-18:00"],
27
- timezone: "UTC",
28
- domainTags: ["java", "spring"],
29
- },
30
- ],
31
- page: 0,
32
- size: 10,
33
- totalElements: 1,
34
- totalPages: 1,
35
- };
36
-
37
- const LISTING_DETAIL = {
38
- id: LISTING_ID,
39
- agentName: "ArchitectBot",
40
- hourlyRateCoins: 60,
41
- };
42
-
43
- const BOOKING_RESPONSE = {
44
- id: "booking-001",
45
- listingId: LISTING_ID,
46
- scheduledAt: "2026-06-01T10:00:00Z",
47
- durationMinutes: 30,
48
- totalCostCoins: 30,
49
- status: "CONFIRMED",
50
- };
51
-
52
- // Use a fixed fake epoch for consistent Date.now() across tests
53
- const FAKE_NOW = 1_748_340_000_000;
54
- // scheduledAt that is always 1 hour in the future relative to FAKE_NOW
55
- const FUTURE_AT = new Date(FAKE_NOW + 3_600_000).toISOString();
56
-
57
- const server = setupServer(silentAuditHandler);
58
-
59
- beforeAll(() => server.listen({ onUnhandledRequest: "warn" }));
60
- beforeEach(() => jest.useFakeTimers({ now: FAKE_NOW }));
61
- afterEach(() => {
62
- server.resetHandlers();
63
- jest.useRealTimers();
64
- });
65
- afterAll(() => server.close());
66
-
67
- // ── nexvora_consulting_search ─────────────────────────────────────────────────
68
-
69
- describe("nexvora_consulting_search integration", () => {
70
- it("happy path: returns formatted listing cards", async () => {
71
- server.use(
72
- http.get(`${BASE_URL}/consulting`, () => HttpResponse.json(CONSULTING_PAGE)),
73
- );
74
-
75
- const result = await nexvora_consulting_search.handler({}, makeClient());
76
-
77
- expect(result).toContain("Consulting Listings");
78
- expect(result).toContain("ArchitectBot");
79
- expect(result).toContain("60"); // coins per hour
80
- });
81
-
82
- it("error path: returns friendly message on 401", async () => {
83
- server.use(
84
- http.get(`${BASE_URL}/consulting`, () =>
85
- new Response("Unauthorized", { status: 401 }),
86
- ),
87
- );
88
-
89
- const result = await nexvora_consulting_search.handler({}, makeClient());
90
-
91
- expect(result).toContain("Not authenticated");
92
- });
93
-
94
- it("auth refresh: retries after 401 and returns listings", async () => {
95
- const { client, cleanup } = makeClientWithStore();
96
-
97
- let callCount = 0;
98
- server.use(
99
- http.get(`${BASE_URL}/consulting`, () => {
100
- if (++callCount === 1) return new Response("Unauthorized", { status: 401 });
101
- return HttpResponse.json(CONSULTING_PAGE);
102
- }),
103
- http.post(`${BASE_URL}/auth/refresh`, () => HttpResponse.json(REFRESH_RESPONSE)),
104
- );
105
-
106
- try {
107
- const result = await nexvora_consulting_search.handler({}, client);
108
- expect(result).toContain("ArchitectBot");
109
- expect(callCount).toBe(2);
110
- } finally {
111
- cleanup();
112
- }
113
- });
114
- });
115
-
116
- // ── nexvora_consulting_book ───────────────────────────────────────────────────
117
-
118
- describe("nexvora_consulting_book integration", () => {
119
- it("preview mode (confirm=false): returns cost breakdown without booking", async () => {
120
- server.use(
121
- http.get(`${BASE_URL}/consulting/${LISTING_ID}`, () => HttpResponse.json(LISTING_DETAIL)),
122
- http.get(`${BASE_URL}/wallet`, () => HttpResponse.json(WALLET_RESPONSE)),
123
- );
124
-
125
- const result = await nexvora_consulting_book.handler(
126
- { listingId: LISTING_ID, scheduledAt: FUTURE_AT, durationMinutes: 30, confirm: false },
127
- makeClient(),
128
- );
129
-
130
- expect(result).toContain("Booking Preview");
131
- expect(result).toContain("ArchitectBot");
132
- expect(result).toContain("30 coins"); // 60/hr × 0.5hr = 30
133
- expect(result).toContain("confirm: true");
134
- });
135
-
136
- it("confirm mode (confirm=true): creates booking and returns confirmation", async () => {
137
- server.use(
138
- http.get(`${BASE_URL}/consulting/${LISTING_ID}`, () => HttpResponse.json(LISTING_DETAIL)),
139
- http.get(`${BASE_URL}/wallet`, () => HttpResponse.json(WALLET_RESPONSE)),
140
- http.post(`${BASE_URL}/consulting/${LISTING_ID}/bookings`, () =>
141
- HttpResponse.json(BOOKING_RESPONSE, { status: 201 }),
142
- ),
143
- );
144
-
145
- const result = await nexvora_consulting_book.handler(
146
- { listingId: LISTING_ID, scheduledAt: FUTURE_AT, durationMinutes: 30, confirm: true },
147
- makeClient(),
148
- );
149
-
150
- expect(result).toContain("Booking Confirmed");
151
- expect(result).toContain("booking-001");
152
- expect(result).toContain("30 coins");
153
- });
154
-
155
- it("error path: returns slot-conflict message on 409", async () => {
156
- server.use(
157
- http.get(`${BASE_URL}/consulting/${LISTING_ID}`, () => HttpResponse.json(LISTING_DETAIL)),
158
- http.get(`${BASE_URL}/wallet`, () => HttpResponse.json(WALLET_RESPONSE)),
159
- http.post(`${BASE_URL}/consulting/${LISTING_ID}/bookings`, () =>
160
- new Response("Conflict", { status: 409 }),
161
- ),
162
- );
163
-
164
- const result = await nexvora_consulting_book.handler(
165
- { listingId: LISTING_ID, scheduledAt: FUTURE_AT, durationMinutes: 30, confirm: true },
166
- makeClient(),
167
- );
168
-
169
- expect(result).toContain("no longer available");
170
- });
171
-
172
- it("error path: returns not-found message when listing ID is unknown", async () => {
173
- const unknownId = "00000000-ffff-0000-0000-000000000000";
174
- server.use(
175
- http.get(`${BASE_URL}/consulting/${unknownId}`, () =>
176
- new Response("Not Found", { status: 404 }),
177
- ),
178
- http.get(`${BASE_URL}/wallet`, () => HttpResponse.json(WALLET_RESPONSE)),
179
- );
180
-
181
- const result = await nexvora_consulting_book.handler(
182
- { listingId: unknownId, scheduledAt: FUTURE_AT, durationMinutes: 30, confirm: false },
183
- makeClient(),
184
- );
185
-
186
- expect(result).toContain(unknownId);
187
- expect(result).toContain("not found");
188
- });
189
-
190
- it("auth refresh: fetches listing after 401 with refreshed token", async () => {
191
- const { client, cleanup } = makeClientWithStore();
192
-
193
- let listingCalls = 0;
194
- server.use(
195
- http.get(`${BASE_URL}/consulting/${LISTING_ID}`, () => {
196
- if (++listingCalls === 1) return new Response("Unauthorized", { status: 401 });
197
- return HttpResponse.json(LISTING_DETAIL);
198
- }),
199
- http.get(`${BASE_URL}/wallet`, () => HttpResponse.json(WALLET_RESPONSE)),
200
- http.post(`${BASE_URL}/auth/refresh`, () => HttpResponse.json(REFRESH_RESPONSE)),
201
- );
202
-
203
- try {
204
- const result = await nexvora_consulting_book.handler(
205
- { listingId: LISTING_ID, scheduledAt: FUTURE_AT, durationMinutes: 30, confirm: false },
206
- client,
207
- );
208
- expect(result).toContain("Booking Preview");
209
- } finally {
210
- cleanup();
211
- }
212
- });
213
- });