@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,140 +0,0 @@
1
- import { jest } from "@jest/globals";
2
-
3
- import { NexvoraApiError, NexvoraClient } from "../NexvoraClient.js";
4
- import { nexvora_agentstack_ask } from "../tools/nexvora_agentstack_ask.js";
5
-
6
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
- function makeClient(): any {
8
- const client = new NexvoraClient({
9
- baseUrl: "https://api.nxvora.online",
10
- accessToken: "token",
11
- });
12
- (client as any).get = jest.fn();
13
- (client as any).post = jest.fn();
14
- (client as any).sendAudit = jest.fn().mockResolvedValue(undefined);
15
- return client;
16
- }
17
-
18
- const WALLET_RESPONSE = { coinBalance: 1000 };
19
-
20
- const QUESTION_RESPONSE = {
21
- id: "bbbbbbbb-0000-0000-0000-000000000001",
22
- title: "How to implement RAG?",
23
- bountyCoins: 100,
24
- status: "OPEN",
25
- };
26
-
27
- describe("nexvora_agentstack_ask tool", () => {
28
- beforeEach(() => {
29
- jest.clearAllMocks();
30
- });
31
-
32
- it("posts a question without bounty (skips wallet check)", async () => {
33
- const client = makeClient();
34
- client.post.mockResolvedValueOnce(QUESTION_RESPONSE);
35
-
36
- const result = await nexvora_agentstack_ask.handler(
37
- { title: "How to implement RAG?", body: "Explain in detail.", domainTags: [], bountyCoins: 0 },
38
- client,
39
- );
40
-
41
- expect(client.get).not.toHaveBeenCalled();
42
- expect(client.post).toHaveBeenCalledWith("/agentstack/questions", {
43
- title: "How to implement RAG?",
44
- body: "Explain in detail.",
45
- bountyCoins: 0,
46
- });
47
- expect(result).toContain("## Question Posted");
48
- expect(result).toContain("bbbbbbbb-0000-0000-0000-000000000001");
49
- });
50
-
51
- it("checks wallet before posting when bountyCoins > 0", async () => {
52
- const client = makeClient();
53
- client.get.mockResolvedValueOnce(WALLET_RESPONSE);
54
- client.post.mockResolvedValueOnce(QUESTION_RESPONSE);
55
-
56
- await nexvora_agentstack_ask.handler(
57
- { title: "My question", body: "Body text", domainTags: [], bountyCoins: 100 },
58
- client,
59
- );
60
-
61
- expect(client.get).toHaveBeenCalledWith("/wallet");
62
- expect(client.post).toHaveBeenCalled();
63
- });
64
-
65
- it("returns insufficient balance message when bounty exceeds balance", async () => {
66
- const client = makeClient();
67
- client.get.mockResolvedValueOnce({ coinBalance: 50 });
68
-
69
- const result = await nexvora_agentstack_ask.handler(
70
- { title: "Expensive question", body: "Body text", domainTags: [], bountyCoins: 500 },
71
- client,
72
- );
73
-
74
- expect(client.post).not.toHaveBeenCalled();
75
- expect(result).toContain("Insufficient balance");
76
- expect(result).toContain("50");
77
- expect(result).toContain("500");
78
- expect(result).toContain("nexvora_purchase");
79
- });
80
-
81
- it("returns formatted success with question title and ID", async () => {
82
- const client = makeClient();
83
- client.post.mockResolvedValueOnce(QUESTION_RESPONSE);
84
-
85
- const result = await nexvora_agentstack_ask.handler(
86
- { title: "How to implement RAG?", body: "Details", domainTags: [], bountyCoins: 0 },
87
- client,
88
- );
89
-
90
- expect(result).toContain("How to implement RAG?");
91
- expect(result).toContain("bbbbbbbb-0000-0000-0000-000000000001");
92
- expect(result).toContain("OPEN");
93
- });
94
-
95
- it("posts with bounty=100 to /agentstack/questions", async () => {
96
- const client = makeClient();
97
- client.get.mockResolvedValueOnce(WALLET_RESPONSE);
98
- client.post.mockResolvedValueOnce({ ...QUESTION_RESPONSE, bountyCoins: 100 });
99
-
100
- await nexvora_agentstack_ask.handler(
101
- { title: "My Q", body: "My body", domainTags: [], bountyCoins: 100 },
102
- client,
103
- );
104
-
105
- expect(client.post).toHaveBeenCalledWith("/agentstack/questions", {
106
- title: "My Q",
107
- body: "My body",
108
- bountyCoins: 100,
109
- });
110
- });
111
-
112
- it("returns auth error message on 401", async () => {
113
- const client = makeClient();
114
- client.post.mockRejectedValueOnce(
115
- new NexvoraApiError(401, "Unauthorized", "/agentstack/questions"),
116
- );
117
-
118
- const result = await nexvora_agentstack_ask.handler(
119
- { title: "Q", body: "B", domainTags: [], bountyCoins: 0 },
120
- client,
121
- );
122
-
123
- expect(result).toContain("nexvora login");
124
- expect(result).toContain("Not authenticated");
125
- });
126
-
127
- it("re-throws non-auth errors", async () => {
128
- const client = makeClient();
129
- client.post.mockRejectedValueOnce(
130
- new NexvoraApiError(500, "Server Error", "/agentstack/questions"),
131
- );
132
-
133
- await expect(
134
- nexvora_agentstack_ask.handler(
135
- { title: "Q", body: "B", domainTags: [], bountyCoins: 0 },
136
- client,
137
- ),
138
- ).rejects.toBeInstanceOf(NexvoraApiError);
139
- });
140
- });
@@ -1,188 +0,0 @@
1
- import { jest } from "@jest/globals";
2
-
3
- import { NexvoraApiError, NexvoraClient } from "../NexvoraClient.js";
4
- import { nexvora_agentstack_search } from "../tools/nexvora_agentstack_search.js";
5
-
6
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
- function makeClient(): any {
8
- const client = new NexvoraClient({
9
- baseUrl: "https://api.nxvora.online",
10
- accessToken: "token",
11
- });
12
- (client as any).get = jest.fn();
13
- (client as any).sendAudit = jest.fn().mockResolvedValue(undefined);
14
- return client;
15
- }
16
-
17
- const NOW = new Date("2026-05-07T12:00:00Z").getTime();
18
-
19
- const QUESTION_PAGE = {
20
- content: [
21
- {
22
- id: "aaaaaaaa-0000-0000-0000-000000000001",
23
- title: "How to implement RAG?",
24
- bountyCoins: 500,
25
- status: "OPEN",
26
- createdAt: new Date(NOW - 2 * 86_400_000).toISOString(),
27
- answers: [],
28
- },
29
- {
30
- id: "aaaaaaaa-0000-0000-0000-000000000002",
31
- title: "Best vector store for Java?",
32
- bountyCoins: 0,
33
- status: "OPEN",
34
- createdAt: new Date(NOW - 86_400_000).toISOString(),
35
- answers: [{ id: "ans-1" }],
36
- },
37
- ],
38
- page: 0,
39
- size: 10,
40
- totalElements: 2,
41
- totalPages: 1,
42
- };
43
-
44
- describe("nexvora_agentstack_search tool", () => {
45
- beforeEach(() => {
46
- jest.clearAllMocks();
47
- jest.useFakeTimers({ now: NOW });
48
- });
49
-
50
- afterEach(() => {
51
- jest.useRealTimers();
52
- });
53
-
54
- it("returns formatted markdown list of questions", async () => {
55
- const client = makeClient();
56
- client.get.mockResolvedValueOnce(QUESTION_PAGE);
57
-
58
- const defaults = nexvora_agentstack_search.inputSchema.parse({});
59
- const result = await nexvora_agentstack_search.handler(defaults, client);
60
-
61
- expect(result).toContain("## AgentStack Questions (OPEN)");
62
- expect(result).toContain("How to implement RAG?");
63
- expect(result).toContain("Best vector store for Java?");
64
- expect(result).toContain("500 coins");
65
- expect(result).toContain("no bounty");
66
- });
67
-
68
- it("uses default status=OPEN, page=0, size=10 when no args", async () => {
69
- const client = makeClient();
70
- client.get.mockResolvedValueOnce(QUESTION_PAGE);
71
-
72
- const defaults = nexvora_agentstack_search.inputSchema.parse({});
73
- await nexvora_agentstack_search.handler(defaults, client);
74
-
75
- expect(client.get).toHaveBeenCalledWith(
76
- expect.stringContaining("status=OPEN"),
77
- );
78
- expect(client.get).toHaveBeenCalledWith(expect.stringContaining("page=0"));
79
- expect(client.get).toHaveBeenCalledWith(expect.stringContaining("size=10"));
80
- });
81
-
82
- it("passes custom status, page, size to the backend", async () => {
83
- const client = makeClient();
84
- client.get.mockResolvedValueOnce({ ...QUESTION_PAGE, page: 1, totalPages: 3 });
85
-
86
- await nexvora_agentstack_search.handler(
87
- { status: "AWARDED", page: 1, size: 5, domainTag: undefined },
88
- client,
89
- );
90
-
91
- expect(client.get).toHaveBeenCalledWith(
92
- expect.stringContaining("status=AWARDED"),
93
- );
94
- expect(client.get).toHaveBeenCalledWith(expect.stringContaining("page=1"));
95
- expect(client.get).toHaveBeenCalledWith(expect.stringContaining("size=5"));
96
- });
97
-
98
- it("appends domainTag to URL when provided", async () => {
99
- const client = makeClient();
100
- client.get.mockResolvedValueOnce(QUESTION_PAGE);
101
-
102
- await nexvora_agentstack_search.handler(
103
- { status: "OPEN", domainTag: "machine learning", page: 0, size: 10 },
104
- client,
105
- );
106
-
107
- expect(client.get).toHaveBeenCalledWith(
108
- expect.stringContaining("domainTag=machine%20learning"),
109
- );
110
- });
111
-
112
- it("shows empty state when no questions found", async () => {
113
- const client = makeClient();
114
- client.get.mockResolvedValueOnce({
115
- content: [],
116
- page: 0,
117
- size: 10,
118
- totalElements: 0,
119
- totalPages: 0,
120
- });
121
-
122
- const result = await nexvora_agentstack_search.handler(
123
- { status: "OPEN", page: 0, size: 10, domainTag: undefined },
124
- client,
125
- );
126
-
127
- expect(result).toContain("No questions found");
128
- });
129
-
130
- it("shows pagination summary when results are returned", async () => {
131
- const client = makeClient();
132
- client.get.mockResolvedValueOnce({
133
- ...QUESTION_PAGE,
134
- totalElements: 42,
135
- totalPages: 5,
136
- });
137
-
138
- const result = await nexvora_agentstack_search.handler(
139
- { status: "OPEN", page: 0, size: 10, domainTag: undefined },
140
- client,
141
- );
142
-
143
- expect(result).toContain("42");
144
- expect(result).toContain("Page 1 of 5");
145
- });
146
-
147
- it("shows answer count in question summary", async () => {
148
- const client = makeClient();
149
- client.get.mockResolvedValueOnce(QUESTION_PAGE);
150
-
151
- const result = await nexvora_agentstack_search.handler(
152
- { status: "OPEN", page: 0, size: 10, domainTag: undefined },
153
- client,
154
- );
155
-
156
- expect(result).toContain("0 answers");
157
- expect(result).toContain("1 answer");
158
- });
159
-
160
- it("returns auth error message on 401", async () => {
161
- const client = makeClient();
162
- client.get.mockRejectedValueOnce(
163
- new NexvoraApiError(401, "Unauthorized", "/agentstack/questions"),
164
- );
165
-
166
- const result = await nexvora_agentstack_search.handler(
167
- { status: "OPEN", page: 0, size: 10, domainTag: undefined },
168
- client,
169
- );
170
-
171
- expect(result).toContain("nexvora login");
172
- expect(result).toContain("Not authenticated");
173
- });
174
-
175
- it("re-throws non-auth errors", async () => {
176
- const client = makeClient();
177
- client.get.mockRejectedValueOnce(
178
- new NexvoraApiError(500, "Server Error", "/agentstack/questions"),
179
- );
180
-
181
- await expect(
182
- nexvora_agentstack_search.handler(
183
- { status: "OPEN", page: 0, size: 10, domainTag: undefined },
184
- client,
185
- ),
186
- ).rejects.toBeInstanceOf(NexvoraApiError);
187
- });
188
- });
@@ -1,277 +0,0 @@
1
- import { jest } from "@jest/globals";
2
-
3
- import { NexvoraApiError, NexvoraClient } from "../NexvoraClient.js";
4
- import { nexvora_consulting_book } from "../tools/nexvora_consulting_book.js";
5
-
6
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
- function makeClient(): any {
8
- const client = new NexvoraClient({
9
- baseUrl: "https://api.nxvora.online",
10
- accessToken: "token",
11
- });
12
- (client as any).get = jest.fn();
13
- (client as any).post = jest.fn();
14
- (client as any).sendAudit = jest.fn().mockResolvedValue(undefined);
15
- return client;
16
- }
17
-
18
- const LISTING_ID = "aaaaaaaa-0000-0000-0000-000000000001";
19
- // A future date well within 30 days
20
- const FUTURE_DATE = new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString();
21
- const FAR_FUTURE_DATE = new Date(Date.now() + 31 * 24 * 60 * 60 * 1000).toISOString();
22
-
23
- const LISTING = {
24
- id: LISTING_ID,
25
- agentName: "ExpertBot",
26
- hourlyRateCoins: 500,
27
- };
28
-
29
- const WALLET_OK = { coinBalance: 1000 };
30
- const WALLET_LOW = { coinBalance: 100 };
31
-
32
- const BOOKING_RESPONSE = {
33
- id: "cccccccc-0000-0000-0000-000000000001",
34
- listingId: LISTING_ID,
35
- scheduledAt: FUTURE_DATE,
36
- durationMinutes: 60,
37
- totalCostCoins: 500,
38
- status: "CONFIRMED",
39
- };
40
-
41
- describe("nexvora_consulting_book tool", () => {
42
- beforeEach(() => {
43
- jest.clearAllMocks();
44
- });
45
-
46
- describe("validation", () => {
47
- it("rejects a past scheduledAt", async () => {
48
- const client = makeClient();
49
- const past = new Date(Date.now() - 1000).toISOString();
50
-
51
- const result = await nexvora_consulting_book.handler(
52
- { listingId: LISTING_ID, scheduledAt: past, durationMinutes: 60, confirm: false },
53
- client,
54
- );
55
-
56
- expect(result).toContain("must be in the future");
57
- expect(client.get).not.toHaveBeenCalled();
58
- });
59
-
60
- it("rejects a scheduledAt more than 30 days out", async () => {
61
- const client = makeClient();
62
-
63
- const result = await nexvora_consulting_book.handler(
64
- {
65
- listingId: LISTING_ID,
66
- scheduledAt: FAR_FUTURE_DATE,
67
- durationMinutes: 60,
68
- confirm: false,
69
- },
70
- client,
71
- );
72
-
73
- expect(result).toContain("within 30 days");
74
- expect(client.get).not.toHaveBeenCalled();
75
- });
76
-
77
- it("accepts only 30 or 60 as durationMinutes", () => {
78
- expect(() =>
79
- nexvora_consulting_book.inputSchema.parse({
80
- listingId: LISTING_ID,
81
- scheduledAt: FUTURE_DATE,
82
- durationMinutes: 45,
83
- }),
84
- ).toThrow();
85
- });
86
-
87
- it("defaults confirm to false", () => {
88
- const parsed = nexvora_consulting_book.inputSchema.parse({
89
- listingId: LISTING_ID,
90
- scheduledAt: FUTURE_DATE,
91
- durationMinutes: 30,
92
- });
93
- expect(parsed.confirm).toBe(false);
94
- });
95
- });
96
-
97
- describe("preview mode (confirm=false)", () => {
98
- it("returns cost preview without booking", async () => {
99
- const client = makeClient();
100
- client.get
101
- .mockResolvedValueOnce(LISTING)
102
- .mockResolvedValueOnce(WALLET_OK);
103
-
104
- const result = await nexvora_consulting_book.handler(
105
- { listingId: LISTING_ID, scheduledAt: FUTURE_DATE, durationMinutes: 60, confirm: false },
106
- client,
107
- );
108
-
109
- expect(result).toContain("## Booking Preview");
110
- expect(result).toContain("ExpertBot");
111
- expect(result).toContain("500");
112
- expect(result).toContain("confirm: true");
113
- expect(client.post).not.toHaveBeenCalled();
114
- });
115
-
116
- it("computes cost correctly for 30-minute session", async () => {
117
- const client = makeClient();
118
- client.get
119
- .mockResolvedValueOnce(LISTING) // 500 coins/hr
120
- .mockResolvedValueOnce(WALLET_OK);
121
-
122
- const result = await nexvora_consulting_book.handler(
123
- { listingId: LISTING_ID, scheduledAt: FUTURE_DATE, durationMinutes: 30, confirm: false },
124
- client,
125
- );
126
-
127
- // 500 coins/hr × 0.5 hr = 250 coins
128
- expect(result).toContain("250");
129
- expect(client.post).not.toHaveBeenCalled();
130
- });
131
-
132
- it("shows insufficient balance warning in preview when wallet is too low", async () => {
133
- const client = makeClient();
134
- client.get
135
- .mockResolvedValueOnce(LISTING)
136
- .mockResolvedValueOnce(WALLET_LOW);
137
-
138
- const result = await nexvora_consulting_book.handler(
139
- { listingId: LISTING_ID, scheduledAt: FUTURE_DATE, durationMinutes: 60, confirm: false },
140
- client,
141
- );
142
-
143
- expect(result).toContain("Insufficient balance");
144
- expect(client.post).not.toHaveBeenCalled();
145
- });
146
-
147
- it("fetches listing and wallet in parallel", async () => {
148
- const client = makeClient();
149
- client.get
150
- .mockResolvedValueOnce(LISTING)
151
- .mockResolvedValueOnce(WALLET_OK);
152
-
153
- await nexvora_consulting_book.handler(
154
- { listingId: LISTING_ID, scheduledAt: FUTURE_DATE, durationMinutes: 60, confirm: false },
155
- client,
156
- );
157
-
158
- expect(client.get).toHaveBeenCalledWith(`/consulting/${LISTING_ID}`);
159
- expect(client.get).toHaveBeenCalledWith("/wallet");
160
- expect(client.get).toHaveBeenCalledTimes(2);
161
- });
162
- });
163
-
164
- describe("confirm mode (confirm=true)", () => {
165
- it("submits booking and returns confirmation", async () => {
166
- const client = makeClient();
167
- client.get
168
- .mockResolvedValueOnce(LISTING)
169
- .mockResolvedValueOnce(WALLET_OK);
170
- client.post.mockResolvedValueOnce(BOOKING_RESPONSE);
171
-
172
- const result = await nexvora_consulting_book.handler(
173
- { listingId: LISTING_ID, scheduledAt: FUTURE_DATE, durationMinutes: 60, confirm: true },
174
- client,
175
- );
176
-
177
- expect(result).toContain("## Booking Confirmed");
178
- expect(result).toContain(BOOKING_RESPONSE.id);
179
- expect(result).toContain("CONFIRMED");
180
- expect(result).toContain("500");
181
- });
182
-
183
- it("sends only scheduledAt and durationMinutes in POST body", async () => {
184
- const client = makeClient();
185
- client.get
186
- .mockResolvedValueOnce(LISTING)
187
- .mockResolvedValueOnce(WALLET_OK);
188
- client.post.mockResolvedValueOnce(BOOKING_RESPONSE);
189
-
190
- await nexvora_consulting_book.handler(
191
- { listingId: LISTING_ID, scheduledAt: FUTURE_DATE, durationMinutes: 60, confirm: true },
192
- client,
193
- );
194
-
195
- const [url, body] = client.post.mock.calls[0] as [string, unknown];
196
- expect(url).toBe(`/consulting/${LISTING_ID}/bookings`);
197
- expect(body).toEqual({ scheduledAt: FUTURE_DATE, durationMinutes: 60 });
198
- });
199
-
200
- it("blocks booking when wallet balance is insufficient", async () => {
201
- const client = makeClient();
202
- client.get
203
- .mockResolvedValueOnce(LISTING)
204
- .mockResolvedValueOnce(WALLET_LOW);
205
-
206
- const result = await nexvora_consulting_book.handler(
207
- { listingId: LISTING_ID, scheduledAt: FUTURE_DATE, durationMinutes: 60, confirm: true },
208
- client,
209
- );
210
-
211
- expect(result).toContain("Insufficient balance");
212
- expect(client.post).not.toHaveBeenCalled();
213
- });
214
- });
215
-
216
- describe("error handling", () => {
217
- it("returns 404 message when listing not found", async () => {
218
- const client = makeClient();
219
- client.get.mockRejectedValueOnce(
220
- new NexvoraApiError(404, "Not Found", `/consulting/${LISTING_ID}`),
221
- );
222
-
223
- const result = await nexvora_consulting_book.handler(
224
- { listingId: LISTING_ID, scheduledAt: FUTURE_DATE, durationMinutes: 60, confirm: false },
225
- client,
226
- );
227
-
228
- expect(result).toContain("not found");
229
- expect(result).toContain(LISTING_ID);
230
- });
231
-
232
- it("returns 409 message when time slot is taken", async () => {
233
- const client = makeClient();
234
- client.get
235
- .mockResolvedValueOnce(LISTING)
236
- .mockResolvedValueOnce(WALLET_OK);
237
- client.post.mockRejectedValueOnce(
238
- new NexvoraApiError(409, "Conflict", `/consulting/${LISTING_ID}/bookings`),
239
- );
240
-
241
- const result = await nexvora_consulting_book.handler(
242
- { listingId: LISTING_ID, scheduledAt: FUTURE_DATE, durationMinutes: 60, confirm: true },
243
- client,
244
- );
245
-
246
- expect(result).toContain("no longer available");
247
- });
248
-
249
- it("returns auth error message on 401", async () => {
250
- const client = makeClient();
251
- client.get.mockRejectedValueOnce(
252
- new NexvoraApiError(401, "Unauthorized", `/consulting/${LISTING_ID}`),
253
- );
254
-
255
- const result = await nexvora_consulting_book.handler(
256
- { listingId: LISTING_ID, scheduledAt: FUTURE_DATE, durationMinutes: 60, confirm: false },
257
- client,
258
- );
259
-
260
- expect(result).toContain("nexvora login");
261
- });
262
-
263
- it("re-throws non-handled errors", async () => {
264
- const client = makeClient();
265
- client.get.mockRejectedValueOnce(
266
- new NexvoraApiError(500, "Server Error", `/consulting/${LISTING_ID}`),
267
- );
268
-
269
- await expect(
270
- nexvora_consulting_book.handler(
271
- { listingId: LISTING_ID, scheduledAt: FUTURE_DATE, durationMinutes: 60, confirm: false },
272
- client,
273
- ),
274
- ).rejects.toBeInstanceOf(NexvoraApiError);
275
- });
276
- });
277
- });