@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,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
- });