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