@nexvora/mcp-server 0.3.2 → 0.4.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.
Files changed (69) 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.js +17 -11
  6. package/dist/cli.js.map +1 -1
  7. package/dist/createServer.d.ts +7 -0
  8. package/dist/createServer.d.ts.map +1 -1
  9. package/dist/createServer.js +3 -3
  10. package/dist/createServer.js.map +1 -1
  11. package/dist/tools/nexvora_submit_task.d.ts +7 -4
  12. package/dist/tools/nexvora_submit_task.d.ts.map +1 -1
  13. package/dist/tools/nexvora_submit_task.js +74 -4
  14. package/dist/tools/nexvora_submit_task.js.map +1 -1
  15. package/package.json +5 -1
  16. package/CHANGELOG.md +0 -208
  17. package/docs/setup/chatgpt-desktop.md +0 -120
  18. package/docs/setup/claude-code.md +0 -152
  19. package/docs/setup/cursor.md +0 -129
  20. package/src/NexvoraClient.ts +0 -328
  21. package/src/RateLimiter.ts +0 -74
  22. package/src/__tests__/NexvoraClient.test.ts +0 -424
  23. package/src/__tests__/RateLimiter.test.ts +0 -151
  24. package/src/__tests__/auth/oauth.test.ts +0 -246
  25. package/src/__tests__/cache.test.ts +0 -64
  26. package/src/__tests__/config.test.ts +0 -98
  27. package/src/__tests__/defineTool.test.ts +0 -223
  28. package/src/__tests__/fixtures/config.json +0 -7
  29. package/src/__tests__/integration/agentstack.integration.test.ts +0 -259
  30. package/src/__tests__/integration/auth_refresh.integration.test.ts +0 -227
  31. package/src/__tests__/integration/consulting.integration.test.ts +0 -213
  32. package/src/__tests__/integration/feed.integration.test.ts +0 -200
  33. package/src/__tests__/integration/helpers.ts +0 -118
  34. package/src/__tests__/integration/knowledge.integration.test.ts +0 -194
  35. package/src/__tests__/integration/rate_limiting.integration.test.ts +0 -207
  36. package/src/__tests__/integration/submit_task.integration.test.ts +0 -120
  37. package/src/__tests__/integration/wallet_observatory.integration.test.ts +0 -240
  38. package/src/__tests__/nexvora_agentstack_answer.test.ts +0 -120
  39. package/src/__tests__/nexvora_agentstack_ask.test.ts +0 -140
  40. package/src/__tests__/nexvora_agentstack_search.test.ts +0 -188
  41. package/src/__tests__/nexvora_consulting_book.test.ts +0 -277
  42. package/src/__tests__/nexvora_consulting_search.test.ts +0 -153
  43. package/src/__tests__/nexvora_feed_post.test.ts +0 -147
  44. package/src/__tests__/nexvora_feed_react.test.ts +0 -98
  45. package/src/__tests__/nexvora_knowledge_search.test.ts +0 -148
  46. package/src/__tests__/nexvora_knowledge_subscribe.test.ts +0 -173
  47. package/src/__tests__/nexvora_observatory.test.ts +0 -125
  48. package/src/__tests__/nexvora_wallet_balance.test.ts +0 -165
  49. package/src/auth/oauth.ts +0 -247
  50. package/src/cache.ts +0 -34
  51. package/src/cli.ts +0 -171
  52. package/src/config.ts +0 -70
  53. package/src/createServer.ts +0 -90
  54. package/src/defineTool.ts +0 -120
  55. package/src/index.ts +0 -36
  56. package/src/server/sse.ts +0 -149
  57. package/src/tools/nexvora_agentstack_answer.ts +0 -62
  58. package/src/tools/nexvora_agentstack_ask.ts +0 -70
  59. package/src/tools/nexvora_agentstack_search.ts +0 -82
  60. package/src/tools/nexvora_consulting_book.ts +0 -130
  61. package/src/tools/nexvora_consulting_search.ts +0 -85
  62. package/src/tools/nexvora_feed_post.ts +0 -69
  63. package/src/tools/nexvora_feed_react.ts +0 -48
  64. package/src/tools/nexvora_knowledge_search.ts +0 -81
  65. package/src/tools/nexvora_knowledge_subscribe.ts +0 -90
  66. package/src/tools/nexvora_observatory.ts +0 -87
  67. package/src/tools/nexvora_submit_task.ts +0 -42
  68. package/src/tools/nexvora_wallet_balance.ts +0 -112
  69. 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
- });