@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,207 +0,0 @@
1
- import { jest } from "@jest/globals";
2
- import { http, HttpResponse } from "msw";
3
- import { setupServer } from "msw/node";
4
-
5
- import { defineTool } from "../../defineTool.js";
6
- import { RateLimiterRegistry } from "../../RateLimiter.js";
7
- import { nexvora_feed_post } from "../../tools/nexvora_feed_post.js";
8
- import { nexvora_submit_task } from "../../tools/nexvora_submit_task.js";
9
- import {
10
- BASE_URL,
11
- makeClient,
12
- silentAuditHandler,
13
- } from "./helpers.js";
14
-
15
- const FAKE_NOW = 1_748_340_000_000;
16
-
17
- const TASK_RESPONSE = {
18
- id: "task-rl-001",
19
- status: "COMPLETED",
20
- result: "Done.",
21
- coinsCharged: 1,
22
- };
23
-
24
- const POST_RESPONSE = {
25
- id: "post-rl-001",
26
- agentId: "00000000-0000-0000-0000-000000000001",
27
- content: "test",
28
- postType: "OPINION",
29
- createdAt: "2024-01-01T00:00:00Z",
30
- };
31
-
32
- const server = setupServer(silentAuditHandler);
33
-
34
- beforeAll(() => server.listen({ onUnhandledRequest: "warn" }));
35
- beforeEach(() => jest.useFakeTimers({ now: FAKE_NOW }));
36
- afterEach(() => {
37
- server.resetHandlers();
38
- jest.useRealTimers();
39
- });
40
- afterAll(() => server.close());
41
-
42
- // ── Capacity enforcement ──────────────────────────────────────────────────────
43
-
44
- describe("capacity enforcement", () => {
45
- it("allows exactly N calls and rate-limits all beyond capacity", async () => {
46
- // Capacity of 3 per minute → only 3 calls allowed before exhaustion
47
- const rateLimiter = new RateLimiterRegistry({ nexvora_submit_task: 3 });
48
- const client = makeClient();
49
- const wrappedHandler = defineTool(nexvora_submit_task, client, rateLimiter);
50
-
51
- server.use(
52
- http.post(`${BASE_URL}/tasks`, () => HttpResponse.json(TASK_RESPONSE)),
53
- );
54
-
55
- const TOTAL = 5;
56
- const results = await Promise.all(
57
- Array.from({ length: TOTAL }, () =>
58
- wrappedHandler({ prompt: "test", model: "PLATFORM_CHOICE" }),
59
- ),
60
- );
61
-
62
- const successes = results.filter((r) => !r.isError);
63
- const rateLimited = results.filter(
64
- (r) => r.isError && r.content[0].text.includes("Rate limit exceeded"),
65
- );
66
-
67
- expect(successes).toHaveLength(3);
68
- expect(rateLimited).toHaveLength(2);
69
- });
70
-
71
- it("all calls succeed when capacity is not exceeded", async () => {
72
- const rateLimiter = new RateLimiterRegistry({ nexvora_submit_task: 10 });
73
- const client = makeClient();
74
- const wrappedHandler = defineTool(nexvora_submit_task, client, rateLimiter);
75
-
76
- server.use(
77
- http.post(`${BASE_URL}/tasks`, () => HttpResponse.json(TASK_RESPONSE)),
78
- );
79
-
80
- const results = await Promise.all(
81
- Array.from({ length: 5 }, () =>
82
- wrappedHandler({ prompt: "test", model: "PLATFORM_CHOICE" }),
83
- ),
84
- );
85
-
86
- expect(results.every((r) => !r.isError)).toBe(true);
87
- });
88
- });
89
-
90
- // ── Rate-limited response format ──────────────────────────────────────────────
91
-
92
- describe("rate-limited response format", () => {
93
- it("includes the tool name and retry-after seconds in the error message", async () => {
94
- const rateLimiter = new RateLimiterRegistry({ nexvora_submit_task: 1 });
95
- const client = makeClient();
96
- const wrappedHandler = defineTool(nexvora_submit_task, client, rateLimiter);
97
-
98
- server.use(
99
- http.post(`${BASE_URL}/tasks`, () => HttpResponse.json(TASK_RESPONSE)),
100
- );
101
-
102
- // First call consumes the only token
103
- await wrappedHandler({ prompt: "first", model: "PLATFORM_CHOICE" });
104
-
105
- // Second call must be rate-limited
106
- const result = await wrappedHandler({ prompt: "second", model: "PLATFORM_CHOICE" });
107
-
108
- expect(result.isError).toBe(true);
109
- expect(result.content[0].text).toContain("nexvora_submit_task");
110
- expect(result.content[0].text).toContain("Retry after");
111
- // Retry-after should be formatted as "X.Y seconds"
112
- expect(result.content[0].text).toMatch(/Retry after \d+\.\d+ seconds/);
113
- });
114
- });
115
-
116
- // ── Tool isolation ────────────────────────────────────────────────────────────
117
-
118
- describe("tool isolation", () => {
119
- it("exhausting one tool's bucket does not affect a different tool's bucket", async () => {
120
- // Give nexvora_submit_task only 1 token and nexvora_feed_post 10 tokens
121
- const rateLimiter = new RateLimiterRegistry({
122
- nexvora_submit_task: 1,
123
- nexvora_feed_post: 10,
124
- });
125
- const client = makeClient();
126
-
127
- const wrappedTask = defineTool(nexvora_submit_task, client, rateLimiter);
128
- const wrappedPost = defineTool(nexvora_feed_post, client, rateLimiter);
129
-
130
- server.use(
131
- http.post(`${BASE_URL}/tasks`, () => HttpResponse.json(TASK_RESPONSE)),
132
- http.post(`${BASE_URL}/feed/posts`, () =>
133
- HttpResponse.json(POST_RESPONSE, { status: 201 }),
134
- ),
135
- );
136
-
137
- // Exhaust nexvora_submit_task
138
- await wrappedTask({ prompt: "first", model: "PLATFORM_CHOICE" });
139
- const taskResult = await wrappedTask({ prompt: "second", model: "PLATFORM_CHOICE" });
140
- expect(taskResult.isError).toBe(true);
141
- expect(taskResult.content[0].text).toContain("Rate limit exceeded");
142
-
143
- // nexvora_feed_post bucket is independent — should still succeed
144
- const postResult = await wrappedPost({
145
- agentId: "00000000-0000-0000-0000-000000000001",
146
- content: "test post",
147
- postType: "OPINION",
148
- });
149
- expect(postResult.isError).toBeUndefined();
150
- expect(postResult.content[0].text).toContain("Post Published");
151
- });
152
- });
153
-
154
- // ── Refill restores capacity ──────────────────────────────────────────────────
155
-
156
- describe("bucket refill", () => {
157
- it("allows new calls after the bucket has refilled", async () => {
158
- // 1 token per minute = 1 token per 60 seconds refill rate
159
- const rateLimiter = new RateLimiterRegistry({ nexvora_submit_task: 1 });
160
- const client = makeClient();
161
- const wrappedHandler = defineTool(nexvora_submit_task, client, rateLimiter);
162
-
163
- server.use(
164
- http.post(`${BASE_URL}/tasks`, () => HttpResponse.json(TASK_RESPONSE)),
165
- );
166
-
167
- // Consume the only token
168
- const first = await wrappedHandler({ prompt: "first", model: "PLATFORM_CHOICE" });
169
- expect(first.isError).toBeUndefined();
170
-
171
- // Immediately rate-limited
172
- const blocked = await wrappedHandler({ prompt: "blocked", model: "PLATFORM_CHOICE" });
173
- expect(blocked.isError).toBe(true);
174
-
175
- // Advance fake time by 61 seconds to allow full refill of 1 token
176
- jest.advanceTimersByTime(61_000);
177
-
178
- // Now the call should succeed again
179
- const refilled = await wrappedHandler({ prompt: "after refill", model: "PLATFORM_CHOICE" });
180
- expect(refilled.isError).toBeUndefined();
181
- expect(refilled.content[0].text).toContain("task-rl-001");
182
- });
183
- });
184
-
185
- // ── Tools not in registry are never rate-limited ──────────────────────────────
186
-
187
- describe("unknown tool passthrough", () => {
188
- it("never rate-limits a tool that is not registered in the registry", async () => {
189
- // Registry only knows about nexvora_feed_post — submit_task is not registered
190
- const rateLimiter = new RateLimiterRegistry({ nexvora_feed_post: 1 });
191
- const client = makeClient();
192
- const wrappedHandler = defineTool(nexvora_submit_task, client, rateLimiter);
193
-
194
- server.use(
195
- http.post(`${BASE_URL}/tasks`, () => HttpResponse.json(TASK_RESPONSE)),
196
- );
197
-
198
- // Calling more times than the feed_post bucket capacity — should all succeed
199
- const results = await Promise.all(
200
- Array.from({ length: 5 }, () =>
201
- wrappedHandler({ prompt: "test", model: "PLATFORM_CHOICE" }),
202
- ),
203
- );
204
-
205
- expect(results.every((r) => !r.isError)).toBe(true);
206
- });
207
- });
@@ -1,120 +0,0 @@
1
- import { http, HttpResponse } from "msw";
2
- import { setupServer } from "msw/node";
3
-
4
- import { nexvora_submit_task } from "../../tools/nexvora_submit_task.js";
5
- import {
6
- BASE_URL,
7
- REFRESH_RESPONSE,
8
- auditCaptureHandler,
9
- makeClient,
10
- makeClientWithStore,
11
- silentAuditHandler,
12
- type AuditEntry,
13
- } from "./helpers.js";
14
-
15
- const TASK_RESPONSE = {
16
- id: "task-abc-123",
17
- status: "COMPLETED",
18
- result: "The answer is 42.",
19
- coinsCharged: 5,
20
- };
21
-
22
- const server = setupServer(silentAuditHandler);
23
-
24
- beforeAll(() => server.listen({ onUnhandledRequest: "warn" }));
25
- afterEach(() => server.resetHandlers());
26
- afterAll(() => server.close());
27
-
28
- describe("nexvora_submit_task integration", () => {
29
- it("happy path: returns task result parsed from backend response", async () => {
30
- server.use(
31
- http.post(`${BASE_URL}/tasks`, () => HttpResponse.json(TASK_RESPONSE)),
32
- );
33
-
34
- const result = await nexvora_submit_task.handler(
35
- { prompt: "What is the meaning of life?", model: "PLATFORM_CHOICE" },
36
- makeClient(),
37
- );
38
-
39
- expect(result).toMatchObject({
40
- id: "task-abc-123",
41
- status: "COMPLETED",
42
- result: "The answer is 42.",
43
- });
44
- });
45
-
46
- it("error path: propagates NexvoraApiError on 402 (payment required)", async () => {
47
- server.use(
48
- http.post(`${BASE_URL}/tasks`, () =>
49
- new Response("Payment Required", { status: 402 }),
50
- ),
51
- );
52
-
53
- await expect(
54
- nexvora_submit_task.handler({ prompt: "test", model: "PLATFORM_CHOICE" }, makeClient()),
55
- ).rejects.toMatchObject({ statusCode: 402 });
56
- });
57
-
58
- it("sends correct request body including model and agentId", async () => {
59
- let captured: Record<string, unknown> = {};
60
- server.use(
61
- http.post(`${BASE_URL}/tasks`, async ({ request }) => {
62
- captured = (await request.json()) as Record<string, unknown>;
63
- return HttpResponse.json(TASK_RESPONSE);
64
- }),
65
- );
66
-
67
- await nexvora_submit_task.handler(
68
- {
69
- prompt: "hello",
70
- model: "CLAUDE_SONNET",
71
- agentId: "00000000-0000-0000-0000-000000000001",
72
- },
73
- makeClient(),
74
- );
75
-
76
- expect(captured).toMatchObject({
77
- prompt: "hello",
78
- model: "CLAUDE_SONNET",
79
- agentId: "00000000-0000-0000-0000-000000000001",
80
- });
81
- });
82
-
83
- it("audit: audit call carries correct toolName", async () => {
84
- const auditLog: AuditEntry[] = [];
85
- server.use(
86
- http.post(`${BASE_URL}/tasks`, () => HttpResponse.json(TASK_RESPONSE)),
87
- auditCaptureHandler(auditLog),
88
- );
89
-
90
- const client = makeClient();
91
- await nexvora_submit_task.handler({ prompt: "test", model: "PLATFORM_CHOICE" }, client);
92
- await client.sendAudit({ toolName: "nexvora_submit_task", outcome: "success" });
93
-
94
- expect(auditLog[0]).toMatchObject({ toolName: "nexvora_submit_task", outcome: "success" });
95
- });
96
-
97
- it("auth refresh: retries after 401 and returns result", async () => {
98
- const { client, cleanup } = makeClientWithStore();
99
-
100
- let callCount = 0;
101
- server.use(
102
- http.post(`${BASE_URL}/tasks`, () => {
103
- if (++callCount === 1) return new Response("Unauthorized", { status: 401 });
104
- return HttpResponse.json(TASK_RESPONSE);
105
- }),
106
- http.post(`${BASE_URL}/auth/refresh`, () => HttpResponse.json(REFRESH_RESPONSE)),
107
- );
108
-
109
- try {
110
- const result = await nexvora_submit_task.handler(
111
- { prompt: "test", model: "PLATFORM_CHOICE" },
112
- client,
113
- );
114
- expect(result).toMatchObject({ id: "task-abc-123" });
115
- expect(callCount).toBe(2);
116
- } finally {
117
- cleanup();
118
- }
119
- });
120
- });
@@ -1,240 +0,0 @@
1
- import { jest } from "@jest/globals";
2
- import { http, HttpResponse } from "msw";
3
- import { setupServer } from "msw/node";
4
-
5
- import { nexvora_observatory } from "../../tools/nexvora_observatory.js";
6
- import { nexvora_wallet_balance } from "../../tools/nexvora_wallet_balance.js";
7
- import {
8
- ACCESS_TOKEN,
9
- BASE_URL,
10
- LEDGER_RESPONSE,
11
- OBSERVATORY_RESPONSE,
12
- REFRESH_RESPONSE,
13
- WALLET_RESPONSE,
14
- auditCaptureHandler,
15
- makeClient,
16
- makeClientWithStore,
17
- silentAuditHandler,
18
- type AuditEntry,
19
- } from "./helpers.js";
20
-
21
- const server = setupServer(silentAuditHandler);
22
-
23
- beforeAll(() => server.listen({ onUnhandledRequest: "warn" }));
24
- afterEach(() => {
25
- server.resetHandlers();
26
- jest.useRealTimers();
27
- });
28
- afterAll(() => server.close());
29
-
30
- // Each test uses a unique fake-time epoch incremented by 90s to expire the 60s TtlCache
31
- let epoch = 1_748_340_000_000;
32
- function nextEpoch() {
33
- const e = epoch;
34
- epoch += 90_000;
35
- return e;
36
- }
37
-
38
- // ── nexvora_wallet_balance ────────────────────────────────────────────────────
39
-
40
- describe("nexvora_wallet_balance integration", () => {
41
- it("happy path: returns formatted balance and transactions", async () => {
42
- const now = nextEpoch();
43
- jest.useFakeTimers({ now });
44
-
45
- server.use(
46
- http.get(`${BASE_URL}/wallet`, () => HttpResponse.json(WALLET_RESPONSE)),
47
- http.get(`${BASE_URL}/wallet/ledger`, () => HttpResponse.json(LEDGER_RESPONSE)),
48
- );
49
-
50
- const result = await nexvora_wallet_balance.handler({}, makeClient());
51
-
52
- expect(result).toContain("NexVora Wallet");
53
- expect(result).toContain("250 coins");
54
- expect(result).toContain("Task reward");
55
- });
56
-
57
- it("error path: returns friendly message on 401", async () => {
58
- const now = nextEpoch();
59
- jest.useFakeTimers({ now });
60
-
61
- server.use(
62
- http.get(`${BASE_URL}/wallet`, () => new Response("Unauthorized", { status: 401 })),
63
- http.get(`${BASE_URL}/wallet/ledger`, () => new Response("Unauthorized", { status: 401 })),
64
- );
65
-
66
- const result = await nexvora_wallet_balance.handler({}, makeClient());
67
-
68
- expect(result).toContain("Not authenticated");
69
- expect(result).toContain("nexvora login");
70
- });
71
-
72
- it("audit: records success outcome after successful call", async () => {
73
- const now = nextEpoch();
74
- jest.useFakeTimers({ now });
75
-
76
- const auditLog: AuditEntry[] = [];
77
- server.use(
78
- http.get(`${BASE_URL}/wallet`, () => HttpResponse.json(WALLET_RESPONSE)),
79
- http.get(`${BASE_URL}/wallet/ledger`, () => HttpResponse.json(LEDGER_RESPONSE)),
80
- auditCaptureHandler(auditLog),
81
- );
82
-
83
- const client = makeClient();
84
- // Use sendAudit directly to verify audit plumbing works
85
- await client.sendAudit({ toolName: "nexvora_wallet_balance", outcome: "success" });
86
-
87
- expect(auditLog).toHaveLength(1);
88
- expect(auditLog[0]).toMatchObject({ toolName: "nexvora_wallet_balance", outcome: "success" });
89
- });
90
-
91
- it("auth refresh: retries transparently after 401 when configStore is set", async () => {
92
- const now = nextEpoch();
93
- jest.useFakeTimers({ now });
94
-
95
- const { client, cleanup } = makeClientWithStore({
96
- expiresAt: Math.floor(now / 1000) + 3600,
97
- });
98
-
99
- let walletCallCount = 0;
100
- server.use(
101
- http.get(`${BASE_URL}/wallet`, () => {
102
- if (++walletCallCount === 1) return new Response("Unauthorized", { status: 401 });
103
- return HttpResponse.json(WALLET_RESPONSE);
104
- }),
105
- http.get(`${BASE_URL}/wallet/ledger`, () => HttpResponse.json(LEDGER_RESPONSE)),
106
- http.post(`${BASE_URL}/auth/refresh`, () => HttpResponse.json(REFRESH_RESPONSE)),
107
- );
108
-
109
- try {
110
- const result = await nexvora_wallet_balance.handler({}, client);
111
- expect(result).toContain("250 coins");
112
- expect(walletCallCount).toBe(2);
113
- } finally {
114
- cleanup();
115
- }
116
- });
117
- });
118
-
119
- // ── nexvora_observatory ───────────────────────────────────────────────────────
120
-
121
- describe("nexvora_observatory integration", () => {
122
- it("happy path: returns formatted platform snapshot table", async () => {
123
- const now = nextEpoch();
124
- jest.useFakeTimers({ now });
125
-
126
- server.use(
127
- http.get(`${BASE_URL}/observatory`, () => HttpResponse.json(OBSERVATORY_RESPONSE)),
128
- );
129
-
130
- const result = await nexvora_observatory.handler({}, makeClient());
131
-
132
- expect(result).toContain("NexVora Platform Health");
133
- expect(result).toContain("42"); // onlineAgentsCount
134
- expect(result).toContain("1,200"); // tasksCompletedToday formatted
135
- expect(result).toContain("BestAgent");
136
- expect(result).toContain("1,500 ms"); // avgTaskLatencyMs
137
- });
138
-
139
- it("error path: returns friendly message on 401", async () => {
140
- const now = nextEpoch();
141
- jest.useFakeTimers({ now });
142
-
143
- server.use(
144
- http.get(`${BASE_URL}/observatory`, () => new Response("Unauthorized", { status: 401 })),
145
- );
146
-
147
- const result = await nexvora_observatory.handler({}, makeClient());
148
-
149
- expect(result).toContain("Not authenticated");
150
- });
151
-
152
- it("auth refresh: succeeds after 401 when configStore present", async () => {
153
- const now = nextEpoch();
154
- jest.useFakeTimers({ now });
155
-
156
- const { client, cleanup } = makeClientWithStore({
157
- expiresAt: Math.floor(now / 1000) + 3600,
158
- });
159
-
160
- let obsCallCount = 0;
161
- server.use(
162
- http.get(`${BASE_URL}/observatory`, () => {
163
- if (++obsCallCount === 1) return new Response("Unauthorized", { status: 401 });
164
- return HttpResponse.json(OBSERVATORY_RESPONSE);
165
- }),
166
- http.post(`${BASE_URL}/auth/refresh`, () => HttpResponse.json(REFRESH_RESPONSE)),
167
- );
168
-
169
- try {
170
- const result = await nexvora_observatory.handler({}, client);
171
- expect(result).toContain("42");
172
- expect(obsCallCount).toBe(2);
173
- } finally {
174
- cleanup();
175
- }
176
- });
177
- });
178
-
179
- // ── Cache behaviour ───────────────────────────────────────────────────────────
180
-
181
- describe("TtlCache integration", () => {
182
- it("observatory result is served from cache within 60s TTL", async () => {
183
- const now = nextEpoch();
184
- jest.useFakeTimers({ now });
185
-
186
- let fetchCount = 0;
187
- server.use(
188
- http.get(`${BASE_URL}/observatory`, () => {
189
- fetchCount++;
190
- return HttpResponse.json(OBSERVATORY_RESPONSE);
191
- }),
192
- );
193
-
194
- const client = makeClient();
195
- await nexvora_observatory.handler({}, client); // populates cache
196
- await nexvora_observatory.handler({}, client); // should hit cache
197
-
198
- expect(fetchCount).toBe(1); // only ONE HTTP call
199
- });
200
-
201
- it("observatory refetches after cache TTL expires", async () => {
202
- const now = nextEpoch();
203
- jest.useFakeTimers({ now });
204
-
205
- let fetchCount = 0;
206
- server.use(
207
- http.get(`${BASE_URL}/observatory`, () => {
208
- fetchCount++;
209
- return HttpResponse.json(OBSERVATORY_RESPONSE);
210
- }),
211
- );
212
-
213
- const client = makeClient();
214
- await nexvora_observatory.handler({}, client); // first fetch, fills cache
215
-
216
- jest.advanceTimersByTime(65_000); // advance past 60s TTL
217
-
218
- await nexvora_observatory.handler({}, client); // should refetch
219
-
220
- expect(fetchCount).toBe(2);
221
- });
222
-
223
- it("wallet ACCESS_TOKEN added in Authorization header", async () => {
224
- const now = nextEpoch();
225
- jest.useFakeTimers({ now });
226
-
227
- let receivedAuth = "";
228
- server.use(
229
- http.get(`${BASE_URL}/wallet`, ({ request }) => {
230
- receivedAuth = request.headers.get("Authorization") ?? "";
231
- return HttpResponse.json(WALLET_RESPONSE);
232
- }),
233
- http.get(`${BASE_URL}/wallet/ledger`, () => HttpResponse.json(LEDGER_RESPONSE)),
234
- );
235
-
236
- await nexvora_wallet_balance.handler({}, makeClient());
237
-
238
- expect(receivedAuth).toBe(`Bearer ${ACCESS_TOKEN}`);
239
- });
240
- });
@@ -1,120 +0,0 @@
1
- import { jest } from "@jest/globals";
2
-
3
- import { NexvoraApiError, NexvoraClient } from "../NexvoraClient.js";
4
- import { nexvora_agentstack_answer } from "../tools/nexvora_agentstack_answer.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).post = jest.fn();
13
- (client as any).sendAudit = jest.fn().mockResolvedValue(undefined);
14
- return client;
15
- }
16
-
17
- const QUESTION_ID = "cccccccc-0000-0000-0000-000000000001";
18
- const AGENT_ID = "dddddddd-0000-0000-0000-000000000001";
19
-
20
- const ANSWER_RESPONSE = {
21
- id: "eeeeeeee-0000-0000-0000-000000000001",
22
- questionId: QUESTION_ID,
23
- body: "Use pgvector with HNSW indexing for best results.",
24
- authorId: "ffffffff-0000-0000-0000-000000000001",
25
- upvoteCount: 0,
26
- createdAt: "2026-05-07T10:00:00Z",
27
- };
28
-
29
- describe("nexvora_agentstack_answer tool", () => {
30
- beforeEach(() => {
31
- jest.clearAllMocks();
32
- });
33
-
34
- it("posts answer body to /agentstack/questions/{id}/answer", async () => {
35
- const client = makeClient();
36
- client.post.mockResolvedValueOnce(ANSWER_RESPONSE);
37
-
38
- await nexvora_agentstack_answer.handler(
39
- { questionId: QUESTION_ID, body: "My answer text.", agentId: AGENT_ID },
40
- client,
41
- );
42
-
43
- expect(client.post).toHaveBeenCalledWith(
44
- `/agentstack/questions/${QUESTION_ID}/answer`,
45
- { body: "My answer text." },
46
- );
47
- });
48
-
49
- it("returns formatted success with answer ID and question ID", async () => {
50
- const client = makeClient();
51
- client.post.mockResolvedValueOnce(ANSWER_RESPONSE);
52
-
53
- const result = await nexvora_agentstack_answer.handler(
54
- { questionId: QUESTION_ID, body: "Use pgvector.", agentId: AGENT_ID },
55
- client,
56
- );
57
-
58
- expect(result).toContain("## Answer Submitted");
59
- expect(result).toContain("eeeeeeee-0000-0000-0000-000000000001");
60
- expect(result).toContain(QUESTION_ID);
61
- });
62
-
63
- it("returns graceful message on 403 (does not own agent)", async () => {
64
- const client = makeClient();
65
- client.post.mockRejectedValueOnce(
66
- new NexvoraApiError(403, "Forbidden", `/agentstack/questions/${QUESTION_ID}/answer`),
67
- );
68
-
69
- const result = await nexvora_agentstack_answer.handler(
70
- { questionId: QUESTION_ID, body: "My answer.", agentId: AGENT_ID },
71
- client,
72
- );
73
-
74
- expect(result).toContain("permission");
75
- expect(result).toContain("agent");
76
- });
77
-
78
- it("returns auth error message on 401", async () => {
79
- const client = makeClient();
80
- client.post.mockRejectedValueOnce(
81
- new NexvoraApiError(401, "Unauthorized", `/agentstack/questions/${QUESTION_ID}/answer`),
82
- );
83
-
84
- const result = await nexvora_agentstack_answer.handler(
85
- { questionId: QUESTION_ID, body: "My answer.", agentId: AGENT_ID },
86
- client,
87
- );
88
-
89
- expect(result).toContain("nexvora login");
90
- expect(result).toContain("Not authenticated");
91
- });
92
-
93
- it("does not include agentId in the POST body sent to backend", async () => {
94
- const client = makeClient();
95
- client.post.mockResolvedValueOnce(ANSWER_RESPONSE);
96
-
97
- await nexvora_agentstack_answer.handler(
98
- { questionId: QUESTION_ID, body: "My answer.", agentId: AGENT_ID },
99
- client,
100
- );
101
-
102
- const [, body] = client.post.mock.calls[0] as [string, Record<string, unknown>];
103
- expect(body).not.toHaveProperty("agentId");
104
- expect(body).toEqual({ body: "My answer." });
105
- });
106
-
107
- it("re-throws non-auth, non-403 errors", async () => {
108
- const client = makeClient();
109
- client.post.mockRejectedValueOnce(
110
- new NexvoraApiError(500, "Server Error", `/agentstack/questions/${QUESTION_ID}/answer`),
111
- );
112
-
113
- await expect(
114
- nexvora_agentstack_answer.handler(
115
- { questionId: QUESTION_ID, body: "My answer.", agentId: AGENT_ID },
116
- client,
117
- ),
118
- ).rejects.toBeInstanceOf(NexvoraApiError);
119
- });
120
- });