@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.
- package/README.md +15 -13
- package/dist/NexvoraClient.d.ts.map +1 -1
- package/dist/NexvoraClient.js +21 -3
- package/dist/NexvoraClient.js.map +1 -1
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +26 -20
- package/dist/cli.js.map +1 -1
- package/dist/createServer.d.ts +7 -0
- package/dist/createServer.d.ts.map +1 -1
- package/dist/createServer.js +3 -3
- package/dist/createServer.js.map +1 -1
- package/package.json +6 -2
- package/CHANGELOG.md +0 -208
- package/docs/setup/chatgpt-desktop.md +0 -120
- package/docs/setup/claude-code.md +0 -152
- package/docs/setup/cursor.md +0 -129
- package/src/NexvoraClient.ts +0 -328
- package/src/RateLimiter.ts +0 -74
- package/src/__tests__/NexvoraClient.test.ts +0 -424
- package/src/__tests__/RateLimiter.test.ts +0 -151
- package/src/__tests__/auth/oauth.test.ts +0 -246
- package/src/__tests__/cache.test.ts +0 -64
- package/src/__tests__/config.test.ts +0 -98
- package/src/__tests__/defineTool.test.ts +0 -223
- package/src/__tests__/fixtures/config.json +0 -7
- package/src/__tests__/integration/agentstack.integration.test.ts +0 -259
- package/src/__tests__/integration/auth_refresh.integration.test.ts +0 -227
- package/src/__tests__/integration/consulting.integration.test.ts +0 -213
- package/src/__tests__/integration/feed.integration.test.ts +0 -200
- package/src/__tests__/integration/helpers.ts +0 -118
- package/src/__tests__/integration/knowledge.integration.test.ts +0 -194
- package/src/__tests__/integration/rate_limiting.integration.test.ts +0 -207
- package/src/__tests__/integration/submit_task.integration.test.ts +0 -120
- package/src/__tests__/integration/wallet_observatory.integration.test.ts +0 -240
- package/src/__tests__/nexvora_agentstack_answer.test.ts +0 -120
- package/src/__tests__/nexvora_agentstack_ask.test.ts +0 -140
- package/src/__tests__/nexvora_agentstack_search.test.ts +0 -188
- package/src/__tests__/nexvora_consulting_book.test.ts +0 -277
- package/src/__tests__/nexvora_consulting_search.test.ts +0 -153
- package/src/__tests__/nexvora_feed_post.test.ts +0 -147
- package/src/__tests__/nexvora_feed_react.test.ts +0 -98
- package/src/__tests__/nexvora_knowledge_search.test.ts +0 -148
- package/src/__tests__/nexvora_knowledge_subscribe.test.ts +0 -173
- package/src/__tests__/nexvora_observatory.test.ts +0 -125
- package/src/__tests__/nexvora_wallet_balance.test.ts +0 -165
- package/src/auth/oauth.ts +0 -247
- package/src/cache.ts +0 -34
- package/src/cli.ts +0 -171
- package/src/config.ts +0 -70
- package/src/createServer.ts +0 -90
- package/src/defineTool.ts +0 -120
- package/src/index.ts +0 -36
- package/src/server/sse.ts +0 -149
- package/src/tools/nexvora_agentstack_answer.ts +0 -62
- package/src/tools/nexvora_agentstack_ask.ts +0 -70
- package/src/tools/nexvora_agentstack_search.ts +0 -82
- package/src/tools/nexvora_consulting_book.ts +0 -130
- package/src/tools/nexvora_consulting_search.ts +0 -85
- package/src/tools/nexvora_feed_post.ts +0 -69
- package/src/tools/nexvora_feed_react.ts +0 -48
- package/src/tools/nexvora_knowledge_search.ts +0 -81
- package/src/tools/nexvora_knowledge_subscribe.ts +0 -90
- package/src/tools/nexvora_observatory.ts +0 -87
- package/src/tools/nexvora_submit_task.ts +0 -42
- package/src/tools/nexvora_wallet_balance.ts +0 -112
- 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
|
-
});
|