opedd-mcp 0.4.1 → 0.5.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.
@@ -1,283 +0,0 @@
1
- // Env-gated tool tests — covers the 7 tools that register conditionally
2
- // based on env-var presence:
3
- //
4
- // article_53_attestation, get_buyer_account, get_audit_events,
5
- // get_compliance_dossier (require OPEDD_BUYER_JWT)
6
- // list_feed, stream_feed_ndjson (require OPEDD_ACCESS_KEY)
7
- // list_publisher_content (requires OPEDD_PUB_BEARER OR legacy OPEDD_API_KEY)
8
- //
9
- // Separate file from dispatcher.test.ts because the TOOLS array push +
10
- // CallTool switch cases evaluate env-var presence at module-import time;
11
- // mixing env-set and env-unset tests in the same file requires vitest
12
- // resetModules() round-trips that compound state-leak risk. One file =
13
- // one env-vector for cleaner test isolation.
14
-
15
- import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
16
-
17
- const TEST_BUYER_TOKEN = "opedd_buyer_test_env_gated_xxx";
18
- const TEST_BUYER_JWT = "eyJhbGc.env-gated-test.sig";
19
- const TEST_ACCESS_KEY = "ent_env_gated_test";
20
- const TEST_API_KEY = "op_env_gated_test_pub";
21
- const TEST_PUB_BEARER = "opedd_pub_test_env_gated_test_pub_canonical_xxxx"; // v0.4.0 canonical Bearer
22
- const TEST_BUYER_EMAIL = "env-gated@opedd-test.com";
23
-
24
- // Stub ALL env vars before any import — guarantees every conditional
25
- // TOOLS push registers + every env-gated switch case is callable.
26
- beforeAll(() => {
27
- vi.stubEnv("OPEDD_BUYER_TOKEN", TEST_BUYER_TOKEN);
28
- vi.stubEnv("OPEDD_BUYER_JWT", TEST_BUYER_JWT);
29
- vi.stubEnv("OPEDD_ACCESS_KEY", TEST_ACCESS_KEY);
30
- vi.stubEnv("OPEDD_API_KEY", TEST_API_KEY);
31
- vi.stubEnv("OPEDD_PUB_BEARER", TEST_PUB_BEARER);
32
- vi.stubEnv("OPEDD_BUYER_EMAIL", TEST_BUYER_EMAIL);
33
- vi.stubEnv("OPEDD_API_URL", "https://api.opedd.com");
34
- });
35
-
36
- afterEach(() => {
37
- vi.restoreAllMocks();
38
- });
39
-
40
- async function loadDispatcher() {
41
- vi.resetModules();
42
- return await import("../src/index.ts");
43
- }
44
-
45
- function mockFetchOk(body: unknown, status = 200) {
46
- const fn = vi.fn(async () =>
47
- new Response(JSON.stringify(body), {
48
- status,
49
- headers: { "content-type": "application/json" },
50
- }),
51
- );
52
- globalThis.fetch = fn as unknown as typeof fetch;
53
- return fn;
54
- }
55
-
56
- function mockFetchNdjson(lines: unknown[]): ReturnType<typeof vi.fn> {
57
- const body = lines.map((l) => JSON.stringify(l)).join("\n") + "\n";
58
- const fn = vi.fn(async () =>
59
- new Response(body, {
60
- status: 200,
61
- headers: { "content-type": "application/x-ndjson" },
62
- }),
63
- );
64
- globalThis.fetch = fn as unknown as typeof fetch;
65
- return fn;
66
- }
67
-
68
- // ───────────────────────────── all env-gated tools register ─────────────────────────────
69
-
70
- describe("TOOLS array with all env vars set", () => {
71
- it("exposes the 7 env-gated tools alongside the 8 always-available", async () => {
72
- const { TOOLS } = await loadDispatcher();
73
- const names = TOOLS.map((t) => t.name);
74
- // BUYER_TOKEN-gated
75
- expect(names).toContain("get_content");
76
- // BUYER_JWT-gated
77
- expect(names).toContain("get_buyer_account");
78
- expect(names).toContain("article_53_attestation");
79
- expect(names).toContain("get_audit_events");
80
- expect(names).toContain("get_compliance_dossier");
81
- // ACCESS_KEY-gated
82
- expect(names).toContain("list_feed");
83
- expect(names).toContain("stream_feed_ndjson");
84
- // API_KEY-gated
85
- expect(names).toContain("list_publisher_content");
86
- expect(names.length).toBe(16);
87
- });
88
- });
89
-
90
- // ───────────────────────────── BUYER_TOKEN-gated tool ─────────────────────────────
91
-
92
- describe("dispatchTool: get_content (BUYER_TOKEN gated)", () => {
93
- it("sends GET /content-delivery with bearer auth", async () => {
94
- const f = mockFetchOk({ success: true, data: { id: "art-1", content: "..." } });
95
- const { dispatchTool } = await loadDispatcher();
96
- await dispatchTool("get_content", { article_id: "art-1" });
97
- const call = f.mock.calls[0];
98
- expect(String(call[0])).toContain("/content-delivery?article_id=art-1");
99
- const headers = (call[1] as RequestInit).headers as Record<string, string>;
100
- expect(headers["Authorization"]).toBe(`Bearer ${TEST_BUYER_TOKEN}`);
101
- });
102
- });
103
-
104
- // ───────────────────────────── BUYER_JWT-gated tools ─────────────────────────────
105
-
106
- describe("dispatchTool: get_buyer_account (BUYER_JWT gated, chip 13)", () => {
107
- it("sends GET /buyer-account with JWT auth", async () => {
108
- const f = mockFetchOk({ success: true, data: { buyer: { contact_email: "x" } } });
109
- const { dispatchTool } = await loadDispatcher();
110
- await dispatchTool("get_buyer_account", {});
111
- const call = f.mock.calls[0];
112
- expect(String(call[0])).toContain("/buyer-account");
113
- const headers = (call[1] as RequestInit).headers as Record<string, string>;
114
- expect(headers["Authorization"]).toBe(`Bearer ${TEST_BUYER_JWT}`);
115
- });
116
- });
117
-
118
- describe("dispatchTool: article_53_attestation (BUYER_JWT gated, chip 2)", () => {
119
- it("happy path — license_id only", async () => {
120
- const f = mockFetchOk({
121
- success: true,
122
- data: { jwt: "eyJ.test.sig", claims: { eu_ai_act_article: 53, aud: "eu-ai-act-article-53" } },
123
- });
124
- const { dispatchTool } = await loadDispatcher();
125
- await dispatchTool("article_53_attestation", {
126
- license_id: "11111111-2222-3333-4444-555555555555",
127
- });
128
- const call = f.mock.calls[0];
129
- expect(String(call[0])).toContain(
130
- "/eu-ai-act/article-53-attestation?license_id=11111111-2222-3333-4444-555555555555",
131
- );
132
- });
133
-
134
- it("with all optional params (content_id + window_start + window_end)", async () => {
135
- const f = mockFetchOk({ success: true, data: {} });
136
- const { dispatchTool } = await loadDispatcher();
137
- await dispatchTool("article_53_attestation", {
138
- license_id: "11111111-2222-3333-4444-555555555555",
139
- content_id: "22222222-3333-4444-5555-666666666666",
140
- window_start: "2026-02-22T00:00:00Z",
141
- window_end: "2026-05-22T00:00:00Z",
142
- });
143
- const call = String(f.mock.calls[0][0]);
144
- expect(call).toContain("content_id=22222222");
145
- expect(call).toContain("window_start=2026-02-22T00%3A00%3A00Z");
146
- expect(call).toContain("window_end=2026-05-22T00%3A00%3A00Z");
147
- });
148
-
149
- it("rejects missing license_id", async () => {
150
- mockFetchOk({});
151
- const { dispatchTool } = await loadDispatcher();
152
- const result = await dispatchTool("article_53_attestation", {});
153
- expect(result.isError).toBe(true);
154
- expect(result.content[0].text).toContain("license_id is required");
155
- });
156
- });
157
-
158
- describe("dispatchTool: get_audit_events (BUYER_JWT gated)", () => {
159
- it("happy path with all filters", async () => {
160
- const f = mockFetchOk({ success: true, events: [] });
161
- const { dispatchTool } = await loadDispatcher();
162
- await dispatchTool("get_audit_events", {
163
- from: "2026-05-01",
164
- to: "2026-05-15",
165
- event_type: "content_access",
166
- limit: 50,
167
- });
168
- const call = String(f.mock.calls[0][0]);
169
- expect(call).toContain("from=2026-05-01");
170
- expect(call).toContain("event_type=content_access");
171
- expect(call).toContain("limit=50");
172
- });
173
-
174
- it("limit capped at 200", async () => {
175
- const f = mockFetchOk({ success: true, events: [] });
176
- const { dispatchTool } = await loadDispatcher();
177
- await dispatchTool("get_audit_events", { limit: 999 });
178
- const call = String(f.mock.calls[0][0]);
179
- expect(call).toContain("limit=200");
180
- });
181
- });
182
-
183
- describe("dispatchTool: get_compliance_dossier (BUYER_JWT gated)", () => {
184
- it("happy path — from + to required", async () => {
185
- const f = mockFetchOk({
186
- success: true,
187
- dossier_metadata: { summary: { total_retrievals: 100 } },
188
- });
189
- const { dispatchTool } = await loadDispatcher();
190
- await dispatchTool("get_compliance_dossier", {
191
- from: "2026-04-01",
192
- to: "2026-04-30",
193
- });
194
- const call = String(f.mock.calls[0][0]);
195
- expect(call).toContain("from=2026-04-01");
196
- expect(call).toContain("to=2026-04-30");
197
- expect(call).toContain("format=json");
198
- });
199
-
200
- it("rejects missing from", async () => {
201
- mockFetchOk({});
202
- const { dispatchTool } = await loadDispatcher();
203
- const result = await dispatchTool("get_compliance_dossier", { to: "2026-04-30" });
204
- expect(result.isError).toBe(true);
205
- expect(result.content[0].text).toContain("from");
206
- });
207
- });
208
-
209
- // ───────────────────────────── ACCESS_KEY-gated tools ─────────────────────────────
210
-
211
- describe("dispatchTool: list_feed (ACCESS_KEY gated)", () => {
212
- it("happy path — sends access_key + format=json", async () => {
213
- const f = mockFetchOk({ success: true, data: { articles: [] } });
214
- const { dispatchTool } = await loadDispatcher();
215
- await dispatchTool("list_feed", { limit: 50 });
216
- const call = String(f.mock.calls[0][0]);
217
- expect(call).toContain("access_key=" + TEST_ACCESS_KEY);
218
- expect(call).toContain("format=json");
219
- expect(call).toContain("limit=50");
220
- });
221
-
222
- it("limit capped at 200", async () => {
223
- const f = mockFetchOk({ success: true, data: {} });
224
- const { dispatchTool } = await loadDispatcher();
225
- await dispatchTool("list_feed", { limit: 5000 });
226
- const call = String(f.mock.calls[0][0]);
227
- expect(call).toContain("limit=200");
228
- });
229
- });
230
-
231
- describe("dispatchTool: stream_feed_ndjson (ACCESS_KEY gated)", () => {
232
- it("parses NDJSON lines + extracts _meta", async () => {
233
- const f = mockFetchNdjson([
234
- { id: "art-1", title: "A" },
235
- { id: "art-2", title: "B" },
236
- { _meta: { count: 2, truncated: false } },
237
- ]);
238
- const { dispatchTool } = await loadDispatcher();
239
- const result = await dispatchTool("stream_feed_ndjson", {});
240
- const call = String(f.mock.calls[0][0]);
241
- expect(call).toContain("format=ndjson");
242
- const payload = JSON.parse(result.content[0].text) as {
243
- articles: Array<Record<string, unknown>>;
244
- meta: Record<string, unknown>;
245
- };
246
- expect(payload.articles.length).toBe(2);
247
- expect(payload.articles[0].id).toBe("art-1");
248
- expect(payload.meta.count).toBe(2);
249
- });
250
-
251
- it("limit capped at 1000", async () => {
252
- const f = mockFetchNdjson([{ _meta: {} }]);
253
- const { dispatchTool } = await loadDispatcher();
254
- await dispatchTool("stream_feed_ndjson", { limit: 99999 });
255
- const call = String(f.mock.calls[0][0]);
256
- expect(call).toContain("limit=1000");
257
- });
258
- });
259
-
260
- // ───────────────────────────── API_KEY-gated tool ─────────────────────────────
261
-
262
- describe("dispatchTool: list_publisher_content (PUB_BEARER preferred; API_KEY fallback)", () => {
263
- it("happy path — sends /api?action=articles + Authorization: Bearer canonical (v0.4.0)", async () => {
264
- const f = mockFetchOk({ success: true, data: { articles: [] } });
265
- const { dispatchTool } = await loadDispatcher();
266
- await dispatchTool("list_publisher_content", { limit: 20 });
267
- const call = f.mock.calls[0];
268
- expect(String(call[0])).toContain("/api?action=articles");
269
- const headers = (call[1] as RequestInit).headers as Record<string, string>;
270
- // v0.4.0: Bearer preferred when OPEDD_PUB_BEARER is set; X-API-Key
271
- // legacy fallback only when PUB_BEARER unset.
272
- expect(headers["Authorization"]).toBe("Bearer " + TEST_PUB_BEARER);
273
- expect(headers["X-API-Key"]).toBeUndefined();
274
- });
275
-
276
- it("limit capped at 100", async () => {
277
- const f = mockFetchOk({ success: true, data: {} });
278
- const { dispatchTool } = await loadDispatcher();
279
- await dispatchTool("list_publisher_content", { limit: 999 });
280
- const call = String(f.mock.calls[0][0]);
281
- expect(call).toContain("limit=100");
282
- });
283
- });