@nkmc/gateway 0.1.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.
- package/dist/chunk-56RA53VS.js +37 -0
- package/dist/chunk-CZJ75YTV.js +969 -0
- package/dist/chunk-QGM4M3NI.js +37 -0
- package/dist/http.cjs +1772 -0
- package/dist/http.d.cts +49 -0
- package/dist/http.d.ts +49 -0
- package/dist/http.js +748 -0
- package/dist/index.cjs +2436 -0
- package/dist/index.d.cts +436 -0
- package/dist/index.d.ts +436 -0
- package/dist/index.js +1434 -0
- package/dist/proxy-ClPcDgsO.d.cts +283 -0
- package/dist/proxy-qpda1ANS.d.ts +283 -0
- package/dist/proxy.cjs +148 -0
- package/dist/proxy.d.cts +6 -0
- package/dist/proxy.d.ts +6 -0
- package/dist/proxy.js +90 -0
- package/dist/testing.cjs +865 -0
- package/dist/testing.d.cts +12 -0
- package/dist/testing.d.ts +12 -0
- package/dist/testing.js +831 -0
- package/dist/tunnels-BviBEaih.d.cts +12 -0
- package/dist/tunnels-DFHNgmN7.d.ts +12 -0
- package/dist/types-C6JC9oTm.d.cts +21 -0
- package/dist/types-C6JC9oTm.d.ts +21 -0
- package/package.json +47 -0
- package/src/__tests__/sqlite-integration.test.ts +384 -0
- package/src/credential/d1-vault.ts +134 -0
- package/src/credential/memory-vault.ts +50 -0
- package/src/credential/types.ts +16 -0
- package/src/d1/__tests__/sqlite-adapter.test.ts +75 -0
- package/src/d1/sqlite-adapter.ts +59 -0
- package/src/d1/types.ts +22 -0
- package/src/federation/__tests__/d1-peer-store.test.ts +218 -0
- package/src/federation/__tests__/peer-client.test.ts +205 -0
- package/src/federation/__tests__/peer-store.test.ts +114 -0
- package/src/federation/d1-peer-store.ts +164 -0
- package/src/federation/peer-backend.ts +60 -0
- package/src/federation/peer-client.ts +122 -0
- package/src/federation/peer-store.ts +45 -0
- package/src/federation/types.ts +39 -0
- package/src/http/app.ts +152 -0
- package/src/http/lib/dns.ts +30 -0
- package/src/http/middleware/admin-auth.ts +18 -0
- package/src/http/middleware/agent-auth.ts +27 -0
- package/src/http/middleware/publish-auth.ts +39 -0
- package/src/http/routes/__tests__/federation.test.ts +364 -0
- package/src/http/routes/__tests__/peers.test.ts +290 -0
- package/src/http/routes/__tests__/proxy.test.ts +159 -0
- package/src/http/routes/auth.ts +39 -0
- package/src/http/routes/byok.ts +62 -0
- package/src/http/routes/credentials.ts +40 -0
- package/src/http/routes/domains.ts +174 -0
- package/src/http/routes/federation.ts +170 -0
- package/src/http/routes/fs.ts +89 -0
- package/src/http/routes/peers.ts +103 -0
- package/src/http/routes/proxy.ts +57 -0
- package/src/http/routes/registry.ts +222 -0
- package/src/http/routes/tunnels.ts +124 -0
- package/src/http.ts +9 -0
- package/src/index.ts +63 -0
- package/src/metering/d1-store.ts +123 -0
- package/src/metering/memory-store.ts +29 -0
- package/src/metering/pricing-guard.ts +68 -0
- package/src/metering/types.ts +25 -0
- package/src/onboard/apis-guru.ts +64 -0
- package/src/onboard/index.ts +4 -0
- package/src/onboard/manifest.ts +362 -0
- package/src/onboard/pipeline.ts +214 -0
- package/src/onboard/types.ts +72 -0
- package/src/proxy/__tests__/tool-registry.test.ts +93 -0
- package/src/proxy/tool-registry.ts +122 -0
- package/src/proxy.ts +12 -0
- package/src/registry/context7-backend.ts +93 -0
- package/src/registry/context7.ts +54 -0
- package/src/registry/d1-store.ts +242 -0
- package/src/registry/memory-store.ts +101 -0
- package/src/registry/openapi-compiler.ts +284 -0
- package/src/registry/resolver.ts +196 -0
- package/src/registry/rpc-compiler.ts +142 -0
- package/src/registry/skill-parser.ts +119 -0
- package/src/registry/skill-to-config.ts +239 -0
- package/src/registry/source-refresher.ts +83 -0
- package/src/registry/types.ts +129 -0
- package/src/registry/virtual-files.ts +76 -0
- package/src/testing/sqlite-d1.ts +64 -0
- package/src/testing.ts +2 -0
- package/src/tunnel/__tests__/cloudflare-provider.test.ts +255 -0
- package/src/tunnel/__tests__/tunnel.test.ts +542 -0
- package/src/tunnel/cloudflare-provider.ts +121 -0
- package/src/tunnel/memory-store.ts +30 -0
- package/src/tunnel/types.ts +28 -0
- package/test/credential/d1-vault.test.ts +127 -0
- package/test/credential/injection.test.ts +67 -0
- package/test/credential/memory-vault.test.ts +63 -0
- package/test/http/app.test.ts +300 -0
- package/test/http/byok-e2e.test.ts +240 -0
- package/test/http/byok.test.ts +115 -0
- package/test/http/credentials.test.ts +57 -0
- package/test/http/e2e.test.ts +260 -0
- package/test/integration/authenticated-apis.test.ts +185 -0
- package/test/integration/free-apis-e2e.test.ts +222 -0
- package/test/metering/d1-store.test.ts +82 -0
- package/test/metering/memory-store.test.ts +76 -0
- package/test/metering/pricing-guard.test.ts +108 -0
- package/test/onboard/apis-guru.test.ts +57 -0
- package/test/onboard/e2e.test.ts +70 -0
- package/test/onboard/pipeline.test.ts +318 -0
- package/test/onboard/real-apis.test.ts +483 -0
- package/test/registry/compilation-correctness.test.ts +132 -0
- package/test/registry/context7-backend.test.ts +88 -0
- package/test/registry/context7-e2e.test.ts +92 -0
- package/test/registry/context7.test.ts +73 -0
- package/test/registry/d1-store.test.ts +184 -0
- package/test/registry/integration.test.ts +129 -0
- package/test/registry/lazy-mount.test.ts +138 -0
- package/test/registry/memory-store.test.ts +171 -0
- package/test/registry/openapi-compiler.test.ts +267 -0
- package/test/registry/openapi-e2e.test.ts +154 -0
- package/test/registry/passthrough-e2e.test.ts +109 -0
- package/test/registry/resolver-peer.test.ts +299 -0
- package/test/registry/resolver.test.ts +228 -0
- package/test/registry/rpc-compiler.test.ts +112 -0
- package/test/registry/skill-parser.test.ts +151 -0
- package/test/registry/skill-to-config.test.ts +151 -0
- package/test/registry/skill-to-rpc-config.test.ts +142 -0
- package/test/registry/source-refresher.test.ts +90 -0
- package/test/registry/virtual-files.test.ts +96 -0
- package/tsconfig.json +4 -0
- package/tsup.config.ts +8 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { HttpBackend, type HttpAuth } from "@nkmc/agent-fs";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Smoke tests for authenticated APIs using real HTTP calls.
|
|
6
|
+
* Each test block is skipped when the required environment variable is absent.
|
|
7
|
+
* These tests validate: request format + authentication + response parsing.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
function makeBackend(baseUrl: string, auth: HttpAuth, bodyEncoding?: "json" | "form") {
|
|
11
|
+
return new HttpBackend({ baseUrl, auth, bodyEncoding });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ── GitHub ─────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
describe.skipIf(!process.env.GITHUB_TOKEN)("GitHub API", { timeout: 30_000 }, () => {
|
|
17
|
+
it("GET /user should return login and id", async () => {
|
|
18
|
+
const backend = makeBackend("https://api.github.com", {
|
|
19
|
+
type: "bearer",
|
|
20
|
+
token: process.env.GITHUB_TOKEN!,
|
|
21
|
+
});
|
|
22
|
+
const result = await backend.read("/_api/user") as Record<string, unknown>;
|
|
23
|
+
// Direct passthrough read — need to set up endpoint or use passthrough
|
|
24
|
+
// Use passthrough mode since no resources/endpoints configured
|
|
25
|
+
expect(result).toBeDefined();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Helper: create a passthrough backend (no resources/endpoints → passthrough mode)
|
|
30
|
+
function makePassthroughBackend(baseUrl: string, auth: HttpAuth, bodyEncoding?: "json" | "form") {
|
|
31
|
+
return new HttpBackend({ baseUrl, auth, bodyEncoding });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── HuggingFace ────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
describe.skipIf(!process.env.HF_TOKEN)("HuggingFace API", { timeout: 30_000 }, () => {
|
|
37
|
+
it("GET /api/models?limit=1 should return models", async () => {
|
|
38
|
+
const backend = makePassthroughBackend("https://huggingface.co", {
|
|
39
|
+
type: "bearer",
|
|
40
|
+
token: process.env.HF_TOKEN!,
|
|
41
|
+
});
|
|
42
|
+
const result = await backend.read("/api/models?limit=1");
|
|
43
|
+
expect(result).toBeDefined();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// ── GitLab ─────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
describe.skipIf(!process.env.GITLAB_TOKEN)("GitLab API", { timeout: 30_000 }, () => {
|
|
50
|
+
it("GET /api/v4/projects?per_page=1 should return projects", async () => {
|
|
51
|
+
const backend = makePassthroughBackend("https://gitlab.com", {
|
|
52
|
+
type: "bearer",
|
|
53
|
+
token: process.env.GITLAB_TOKEN!,
|
|
54
|
+
});
|
|
55
|
+
const result = await backend.read("/api/v4/projects?per_page=1");
|
|
56
|
+
expect(result).toBeDefined();
|
|
57
|
+
expect(Array.isArray(result)).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// ── Vercel ─────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
describe.skipIf(!process.env.VERCEL_TOKEN)("Vercel API", { timeout: 30_000 }, () => {
|
|
64
|
+
it("GET /v9/projects?limit=1 should return projects", async () => {
|
|
65
|
+
const backend = makePassthroughBackend("https://api.vercel.com", {
|
|
66
|
+
type: "bearer",
|
|
67
|
+
token: process.env.VERCEL_TOKEN!,
|
|
68
|
+
});
|
|
69
|
+
const result = await backend.read("/v9/projects?limit=1") as Record<string, unknown>;
|
|
70
|
+
expect(result).toBeDefined();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// ── Sentry ─────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
describe.skipIf(!process.env.SENTRY_AUTH_TOKEN)("Sentry API", { timeout: 30_000 }, () => {
|
|
77
|
+
it("GET /api/0/organizations/ should return organizations", async () => {
|
|
78
|
+
const backend = makePassthroughBackend("https://sentry.io", {
|
|
79
|
+
type: "bearer",
|
|
80
|
+
token: process.env.SENTRY_AUTH_TOKEN!,
|
|
81
|
+
});
|
|
82
|
+
const result = await backend.read("/api/0/organizations/");
|
|
83
|
+
expect(result).toBeDefined();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// ── PagerDuty ──────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
describe.skipIf(!process.env.PAGERDUTY_TOKEN)("PagerDuty API", { timeout: 30_000 }, () => {
|
|
90
|
+
it("GET /services?limit=1 should return services", async () => {
|
|
91
|
+
const backend = makePassthroughBackend("https://api.pagerduty.com", {
|
|
92
|
+
type: "bearer",
|
|
93
|
+
token: process.env.PAGERDUTY_TOKEN!,
|
|
94
|
+
});
|
|
95
|
+
const result = await backend.read("/services?limit=1") as Record<string, unknown>;
|
|
96
|
+
expect(result).toBeDefined();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ── Mistral ────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
describe.skipIf(!process.env.MISTRAL_API_KEY)("Mistral API", { timeout: 30_000 }, () => {
|
|
103
|
+
it("GET /v1/models should return models", async () => {
|
|
104
|
+
const backend = makePassthroughBackend("https://api.mistral.ai", {
|
|
105
|
+
type: "bearer",
|
|
106
|
+
token: process.env.MISTRAL_API_KEY!,
|
|
107
|
+
});
|
|
108
|
+
const result = await backend.read("/v1/models") as Record<string, unknown>;
|
|
109
|
+
expect(result).toBeDefined();
|
|
110
|
+
expect(result.data).toBeDefined();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// ── Cloudflare ─────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
describe.skipIf(!process.env.CLOUDFLARE_API_TOKEN)("Cloudflare API", { timeout: 30_000 }, () => {
|
|
117
|
+
it("GET /client/v4/zones should return result array", async () => {
|
|
118
|
+
const backend = makePassthroughBackend("https://api.cloudflare.com", {
|
|
119
|
+
type: "bearer",
|
|
120
|
+
token: process.env.CLOUDFLARE_API_TOKEN!,
|
|
121
|
+
});
|
|
122
|
+
const result = await backend.read("/client/v4/zones") as Record<string, unknown>;
|
|
123
|
+
expect(result).toBeDefined();
|
|
124
|
+
expect(result.result).toBeDefined();
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// ── DigitalOcean ───────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
describe.skipIf(!process.env.DIGITALOCEAN_TOKEN)("DigitalOcean API", { timeout: 30_000 }, () => {
|
|
131
|
+
it("GET /v2/account should return account info", async () => {
|
|
132
|
+
const backend = makePassthroughBackend("https://api.digitalocean.com", {
|
|
133
|
+
type: "bearer",
|
|
134
|
+
token: process.env.DIGITALOCEAN_TOKEN!,
|
|
135
|
+
});
|
|
136
|
+
const result = await backend.read("/v2/account") as Record<string, unknown>;
|
|
137
|
+
expect(result).toBeDefined();
|
|
138
|
+
expect(result.account).toBeDefined();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// ── Stripe ─────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
describe.skipIf(!process.env.STRIPE_SECRET_KEY)("Stripe API", { timeout: 30_000 }, () => {
|
|
145
|
+
it("GET /v1/customers?limit=1 should return data array", async () => {
|
|
146
|
+
const backend = makePassthroughBackend("https://api.stripe.com", {
|
|
147
|
+
type: "bearer",
|
|
148
|
+
token: process.env.STRIPE_SECRET_KEY!,
|
|
149
|
+
}, "form");
|
|
150
|
+
const result = await backend.read("/v1/customers?limit=1") as Record<string, unknown>;
|
|
151
|
+
expect(result).toBeDefined();
|
|
152
|
+
expect(result.data).toBeDefined();
|
|
153
|
+
expect(Array.isArray(result.data)).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ── Slack ──────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
describe.skipIf(!process.env.SLACK_BOT_TOKEN)("Slack API", { timeout: 30_000 }, () => {
|
|
160
|
+
it("GET /api/auth.test should return ok", async () => {
|
|
161
|
+
const backend = makePassthroughBackend("https://slack.com", {
|
|
162
|
+
type: "bearer",
|
|
163
|
+
token: process.env.SLACK_BOT_TOKEN!,
|
|
164
|
+
});
|
|
165
|
+
const result = await backend.read("/api/auth.test") as Record<string, unknown>;
|
|
166
|
+
expect(result).toBeDefined();
|
|
167
|
+
expect(result.ok).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ── Discord ────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
describe.skipIf(!process.env.DISCORD_BOT_TOKEN)("Discord API", { timeout: 30_000 }, () => {
|
|
174
|
+
it("GET /api/v10/users/@me should return id and username", async () => {
|
|
175
|
+
const backend = makePassthroughBackend("https://discord.com", {
|
|
176
|
+
type: "bearer",
|
|
177
|
+
token: process.env.DISCORD_BOT_TOKEN!,
|
|
178
|
+
prefix: "Bot",
|
|
179
|
+
});
|
|
180
|
+
const result = await backend.read("/api/v10/users/@me") as Record<string, unknown>;
|
|
181
|
+
expect(result).toBeDefined();
|
|
182
|
+
expect(result.id).toBeDefined();
|
|
183
|
+
expect(result.username).toBeDefined();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { createServer, type Server, type IncomingMessage, type ServerResponse } from "node:http";
|
|
3
|
+
import { OnboardPipeline } from "../../src/onboard/pipeline.js";
|
|
4
|
+
import { AgentFs, HttpBackend } from "@nkmc/agent-fs";
|
|
5
|
+
import { createRegistryResolver } from "../../src/registry/resolver.js";
|
|
6
|
+
import { MemoryRegistryStore } from "../../src/registry/memory-store.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Full-chain integration tests: CLI command → AgentFs → registry → HttpBackend → HTTP server.
|
|
10
|
+
*
|
|
11
|
+
* Uses a local mock HTTP server for reliable, deterministic assertions.
|
|
12
|
+
* Also includes real-network tests against free public APIs.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// ── Local mock server: full-chain CLI → API test ─────────────────────
|
|
16
|
+
|
|
17
|
+
describe("CLI → API full-chain (local mock server)", () => {
|
|
18
|
+
let mockServer: Server;
|
|
19
|
+
let mockPort: number;
|
|
20
|
+
let fs: AgentFs;
|
|
21
|
+
|
|
22
|
+
// In-memory data store for the mock server
|
|
23
|
+
const pets: Record<string, any> = {
|
|
24
|
+
"1": { id: 1, name: "Buddy", status: "available" },
|
|
25
|
+
"2": { id: 2, name: "Milo", status: "sold" },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
beforeAll(async () => {
|
|
29
|
+
// 1. Start a mock HTTP server that mimics a REST API with basePath /api/v2
|
|
30
|
+
await new Promise<void>((resolve) => {
|
|
31
|
+
mockServer = createServer((req: IncomingMessage, res: ServerResponse) => {
|
|
32
|
+
const url = new URL(req.url ?? "/", `http://localhost`);
|
|
33
|
+
const path = url.pathname;
|
|
34
|
+
res.setHeader("Content-Type", "application/json");
|
|
35
|
+
|
|
36
|
+
const chunks: Buffer[] = [];
|
|
37
|
+
req.on("data", (c: Buffer) => chunks.push(c));
|
|
38
|
+
req.on("end", () => {
|
|
39
|
+
const body = chunks.length ? JSON.parse(Buffer.concat(chunks).toString()) : undefined;
|
|
40
|
+
|
|
41
|
+
// GET /api/v2/pet → list
|
|
42
|
+
if (path === "/api/v2/pet" && req.method === "GET") {
|
|
43
|
+
res.writeHead(200);
|
|
44
|
+
res.end(JSON.stringify(Object.values(pets)));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// POST /api/v2/pet → create
|
|
49
|
+
if (path === "/api/v2/pet" && req.method === "POST") {
|
|
50
|
+
const id = String(body?.id ?? Date.now());
|
|
51
|
+
pets[id] = { ...body, id: Number(id) };
|
|
52
|
+
res.writeHead(201);
|
|
53
|
+
res.end(JSON.stringify(pets[id]));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// GET/DELETE /api/v2/pet/{id}
|
|
58
|
+
const match = path.match(/^\/api\/v2\/pet\/(\w+)$/);
|
|
59
|
+
if (match && req.method === "GET") {
|
|
60
|
+
const pet = pets[match[1]];
|
|
61
|
+
if (pet) { res.writeHead(200); res.end(JSON.stringify(pet)); }
|
|
62
|
+
else { res.writeHead(404); res.end(JSON.stringify({ error: "Not found" })); }
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (match && req.method === "DELETE") {
|
|
66
|
+
const existed = !!pets[match[1]];
|
|
67
|
+
delete pets[match[1]];
|
|
68
|
+
res.writeHead(existed ? 200 : 404);
|
|
69
|
+
res.end(JSON.stringify({ deleted: existed }));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
res.writeHead(404);
|
|
74
|
+
res.end(JSON.stringify({ error: "Unknown route" }));
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
mockServer.listen(0, () => {
|
|
78
|
+
mockPort = (mockServer.address() as any).port;
|
|
79
|
+
resolve();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// 2. Create HttpBackend directly with correct http:// baseUrl + basePath
|
|
84
|
+
// This tests the full chain: cat/ls/write/rm CLI commands → AgentFs → HttpBackend → real HTTP
|
|
85
|
+
const backend = new HttpBackend({
|
|
86
|
+
baseUrl: `http://localhost:${mockPort}/api/v2`,
|
|
87
|
+
resources: [{ name: "pet", apiPath: "/pet" }],
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// 3. Mount directly into AgentFs
|
|
91
|
+
fs = new AgentFs({ mounts: [{ path: "/mock-api", backend }] });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
afterAll(() => {
|
|
95
|
+
mockServer?.close();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("ls: should list pet resource", async () => {
|
|
99
|
+
const result = await fs.execute("ls /mock-api/");
|
|
100
|
+
expect(result.ok).toBe(true);
|
|
101
|
+
const entries = result.data as string[];
|
|
102
|
+
expect(entries).toContain("pet/");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("ls resource: should list pet IDs from mock server", async () => {
|
|
106
|
+
const result = await fs.execute("ls /mock-api/pet/");
|
|
107
|
+
expect(result.ok).toBe(true);
|
|
108
|
+
const entries = result.data as string[];
|
|
109
|
+
expect(entries).toContain("1.json");
|
|
110
|
+
expect(entries).toContain("2.json");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("cat: should read pet by ID with correct data", async () => {
|
|
114
|
+
const result = await fs.execute("cat /mock-api/pet/1.json");
|
|
115
|
+
expect(result.ok).toBe(true);
|
|
116
|
+
const pet = result.data as Record<string, unknown>;
|
|
117
|
+
expect(pet.id).toBe(1);
|
|
118
|
+
expect(pet.name).toBe("Buddy");
|
|
119
|
+
expect(pet.status).toBe("available");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("write → cat round-trip: created pet is readable via cat", async () => {
|
|
123
|
+
// write (POST): parseCommand → AgentFs → HttpBackend → mock server POST /api/v2/pet
|
|
124
|
+
const writeResult = await fs.execute(
|
|
125
|
+
`write /mock-api/pet/ ${JSON.stringify({ id: 42, name: "Nakamichi", status: "available" })}`,
|
|
126
|
+
);
|
|
127
|
+
expect(writeResult.ok).toBe(true);
|
|
128
|
+
expect((writeResult.data as any).id).toBe("42");
|
|
129
|
+
|
|
130
|
+
// cat (GET): parseCommand → AgentFs → HttpBackend → mock server GET /api/v2/pet/42
|
|
131
|
+
const catResult = await fs.execute("cat /mock-api/pet/42.json");
|
|
132
|
+
expect(catResult.ok).toBe(true);
|
|
133
|
+
const pet = catResult.data as Record<string, unknown>;
|
|
134
|
+
expect(pet.id).toBe(42);
|
|
135
|
+
expect(pet.name).toBe("Nakamichi");
|
|
136
|
+
expect(pet.status).toBe("available");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("rm → cat: deleted pet is no longer readable", async () => {
|
|
140
|
+
// rm (DELETE): parseCommand → AgentFs → HttpBackend → mock server DELETE /api/v2/pet/42
|
|
141
|
+
const rmResult = await fs.execute("rm /mock-api/pet/42.json");
|
|
142
|
+
expect(rmResult.ok).toBe(true);
|
|
143
|
+
|
|
144
|
+
// cat after delete should fail with NotFoundError
|
|
145
|
+
const catResult = await fs.execute("cat /mock-api/pet/42.json");
|
|
146
|
+
expect(catResult.ok).toBe(false);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// ── Petstore E2E (real network, demo API) ────────────────────────────
|
|
151
|
+
|
|
152
|
+
describe("Petstore E2E (real HTTP)", { timeout: 60_000 }, () => {
|
|
153
|
+
let fs: AgentFs;
|
|
154
|
+
const store = new MemoryRegistryStore();
|
|
155
|
+
|
|
156
|
+
beforeAll(async () => {
|
|
157
|
+
const pipeline = new OnboardPipeline({ store, smokeTest: false });
|
|
158
|
+
const result = await pipeline.onboardOne({
|
|
159
|
+
domain: "petstore3.swagger.io",
|
|
160
|
+
specUrl: "https://petstore3.swagger.io/api/v3/openapi.json",
|
|
161
|
+
});
|
|
162
|
+
expect(result.status).toBe("ok");
|
|
163
|
+
expect(result.endpoints).toBeGreaterThan(0);
|
|
164
|
+
|
|
165
|
+
const { onMiss, listDomains } = createRegistryResolver({ store, wrapVirtualFiles: false });
|
|
166
|
+
fs = new AgentFs({ mounts: [], onMiss, listDomains });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("basePath should be /api/v3", async () => {
|
|
170
|
+
const record = await store.get("petstore3.swagger.io");
|
|
171
|
+
expect(record?.source?.basePath).toBe("/api/v3");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("ls / should contain pet resource", async () => {
|
|
175
|
+
const result = await fs.execute("ls /petstore3.swagger.io/");
|
|
176
|
+
expect(result.ok).toBe(true);
|
|
177
|
+
const entries = result.data as string[];
|
|
178
|
+
expect(entries.length).toBeGreaterThan(0);
|
|
179
|
+
expect(entries.some((e) => e.includes("pet"))).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("should have _api/ listing", async () => {
|
|
183
|
+
const result = await fs.execute("ls /petstore3.swagger.io/_api/");
|
|
184
|
+
expect(result.ok).toBe(true);
|
|
185
|
+
const entries = result.data as string[];
|
|
186
|
+
expect(entries.length).toBeGreaterThan(0);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ── NWS Weather E2E (real network) ──────────────────────────────────
|
|
191
|
+
|
|
192
|
+
describe("NWS Weather E2E (real HTTP)", { timeout: 60_000 }, () => {
|
|
193
|
+
let fs: AgentFs;
|
|
194
|
+
const store = new MemoryRegistryStore();
|
|
195
|
+
|
|
196
|
+
beforeAll(async () => {
|
|
197
|
+
const pipeline = new OnboardPipeline({ store, smokeTest: false });
|
|
198
|
+
const result = await pipeline.onboardOne({
|
|
199
|
+
domain: "api.weather.gov",
|
|
200
|
+
specUrl: "https://api.weather.gov/openapi.json",
|
|
201
|
+
});
|
|
202
|
+
expect(result.status).toBe("ok");
|
|
203
|
+
expect(result.endpoints).toBeGreaterThan(0);
|
|
204
|
+
|
|
205
|
+
const { onMiss, listDomains } = createRegistryResolver({ store, wrapVirtualFiles: false });
|
|
206
|
+
fs = new AgentFs({ mounts: [], onMiss, listDomains });
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("ls / should contain resources", async () => {
|
|
210
|
+
const result = await fs.execute("ls /api.weather.gov/");
|
|
211
|
+
expect(result.ok).toBe(true);
|
|
212
|
+
const entries = result.data as string[];
|
|
213
|
+
expect(entries.length).toBeGreaterThan(0);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("should have _api/ listing", async () => {
|
|
217
|
+
const result = await fs.execute("ls /api.weather.gov/_api/");
|
|
218
|
+
expect(result.ok).toBe(true);
|
|
219
|
+
const entries = result.data as string[];
|
|
220
|
+
expect(entries.length).toBeGreaterThan(0);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { D1MeterStore } from "../../src/metering/d1-store.js";
|
|
3
|
+
import { SqliteD1 } from "../../src/testing/sqlite-d1.js";
|
|
4
|
+
import type { MeterRecord } from "../../src/metering/types.js";
|
|
5
|
+
|
|
6
|
+
function makeEntry(overrides?: Partial<MeterRecord>): MeterRecord {
|
|
7
|
+
return {
|
|
8
|
+
id: overrides?.id ?? `m_${Math.random().toString(36).slice(2)}`,
|
|
9
|
+
timestamp: overrides?.timestamp ?? Date.now(),
|
|
10
|
+
domain: overrides?.domain ?? "api.example.com",
|
|
11
|
+
version: overrides?.version ?? "1.0",
|
|
12
|
+
endpoint: overrides?.endpoint ?? "GET /api/data",
|
|
13
|
+
agentId: overrides?.agentId ?? "agent-1",
|
|
14
|
+
developerId: overrides?.developerId,
|
|
15
|
+
cost: overrides?.cost ?? 0.05,
|
|
16
|
+
currency: overrides?.currency ?? "USDC",
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("D1MeterStore", () => {
|
|
21
|
+
let db: SqliteD1;
|
|
22
|
+
let store: D1MeterStore;
|
|
23
|
+
|
|
24
|
+
beforeEach(async () => {
|
|
25
|
+
db = new SqliteD1();
|
|
26
|
+
store = new D1MeterStore(db);
|
|
27
|
+
await store.initSchema();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
db.close();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should record and query entries", async () => {
|
|
35
|
+
await store.record(makeEntry({ id: "m1" }));
|
|
36
|
+
await store.record(makeEntry({ id: "m2" }));
|
|
37
|
+
const results = await store.query({});
|
|
38
|
+
expect(results).toHaveLength(2);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should filter by domain", async () => {
|
|
42
|
+
await store.record(makeEntry({ id: "m1", domain: "api.example.com" }));
|
|
43
|
+
await store.record(makeEntry({ id: "m2", domain: "other.com" }));
|
|
44
|
+
const results = await store.query({ domain: "api.example.com" });
|
|
45
|
+
expect(results).toHaveLength(1);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should filter by agentId", async () => {
|
|
49
|
+
await store.record(makeEntry({ id: "m1", agentId: "agent-1" }));
|
|
50
|
+
await store.record(makeEntry({ id: "m2", agentId: "agent-2" }));
|
|
51
|
+
const results = await store.query({ agentId: "agent-1" });
|
|
52
|
+
expect(results).toHaveLength(1);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should filter by time range", async () => {
|
|
56
|
+
await store.record(makeEntry({ id: "m1", timestamp: 1000 }));
|
|
57
|
+
await store.record(makeEntry({ id: "m2", timestamp: 2000 }));
|
|
58
|
+
await store.record(makeEntry({ id: "m3", timestamp: 3000 }));
|
|
59
|
+
const results = await store.query({ from: 1500, to: 2500 });
|
|
60
|
+
expect(results).toHaveLength(1);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should sum costs", async () => {
|
|
64
|
+
await store.record(makeEntry({ id: "m1", cost: 0.05 }));
|
|
65
|
+
await store.record(makeEntry({ id: "m2", cost: 0.10 }));
|
|
66
|
+
const { total, currency } = await store.sum({});
|
|
67
|
+
expect(total).toBeCloseTo(0.15);
|
|
68
|
+
expect(currency).toBe("USDC");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should return zero for no matches", async () => {
|
|
72
|
+
const { total } = await store.sum({ domain: "nonexistent.com" });
|
|
73
|
+
expect(total).toBe(0);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("should call initSchema multiple times (idempotent)", async () => {
|
|
77
|
+
await store.initSchema();
|
|
78
|
+
await store.record(makeEntry({ id: "m1" }));
|
|
79
|
+
const results = await store.query({});
|
|
80
|
+
expect(results).toHaveLength(1);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { MemoryMeterStore } from "../../src/metering/memory-store.js";
|
|
3
|
+
import type { MeterRecord } from "../../src/metering/types.js";
|
|
4
|
+
|
|
5
|
+
function makeEntry(overrides?: Partial<MeterRecord>): MeterRecord {
|
|
6
|
+
return {
|
|
7
|
+
id: overrides?.id ?? `m_${Date.now()}`,
|
|
8
|
+
timestamp: overrides?.timestamp ?? Date.now(),
|
|
9
|
+
domain: overrides?.domain ?? "api.example.com",
|
|
10
|
+
version: overrides?.version ?? "1.0",
|
|
11
|
+
endpoint: overrides?.endpoint ?? "GET /api/data",
|
|
12
|
+
agentId: overrides?.agentId ?? "agent-1",
|
|
13
|
+
developerId: overrides?.developerId,
|
|
14
|
+
cost: overrides?.cost ?? 0.05,
|
|
15
|
+
currency: overrides?.currency ?? "USDC",
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("MemoryMeterStore", () => {
|
|
20
|
+
let store: MemoryMeterStore;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
store = new MemoryMeterStore();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should record and query entries", async () => {
|
|
27
|
+
await store.record(makeEntry({ id: "m1" }));
|
|
28
|
+
await store.record(makeEntry({ id: "m2" }));
|
|
29
|
+
const results = await store.query({});
|
|
30
|
+
expect(results).toHaveLength(2);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should filter by domain", async () => {
|
|
34
|
+
await store.record(makeEntry({ id: "m1", domain: "api.example.com" }));
|
|
35
|
+
await store.record(makeEntry({ id: "m2", domain: "other.com" }));
|
|
36
|
+
const results = await store.query({ domain: "api.example.com" });
|
|
37
|
+
expect(results).toHaveLength(1);
|
|
38
|
+
expect(results[0].domain).toBe("api.example.com");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should filter by agentId", async () => {
|
|
42
|
+
await store.record(makeEntry({ id: "m1", agentId: "agent-1" }));
|
|
43
|
+
await store.record(makeEntry({ id: "m2", agentId: "agent-2" }));
|
|
44
|
+
const results = await store.query({ agentId: "agent-1" });
|
|
45
|
+
expect(results).toHaveLength(1);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should filter by time range", async () => {
|
|
49
|
+
await store.record(makeEntry({ id: "m1", timestamp: 1000 }));
|
|
50
|
+
await store.record(makeEntry({ id: "m2", timestamp: 2000 }));
|
|
51
|
+
await store.record(makeEntry({ id: "m3", timestamp: 3000 }));
|
|
52
|
+
const results = await store.query({ from: 1500, to: 2500 });
|
|
53
|
+
expect(results).toHaveLength(1);
|
|
54
|
+
expect(results[0].id).toBe("m2");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should sum costs", async () => {
|
|
58
|
+
await store.record(makeEntry({ id: "m1", cost: 0.05 }));
|
|
59
|
+
await store.record(makeEntry({ id: "m2", cost: 0.10 }));
|
|
60
|
+
const { total, currency } = await store.sum({});
|
|
61
|
+
expect(total).toBeCloseTo(0.15);
|
|
62
|
+
expect(currency).toBe("USDC");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should sum with filter", async () => {
|
|
66
|
+
await store.record(makeEntry({ id: "m1", domain: "a.com", cost: 0.05 }));
|
|
67
|
+
await store.record(makeEntry({ id: "m2", domain: "b.com", cost: 0.10 }));
|
|
68
|
+
const { total } = await store.sum({ domain: "a.com" });
|
|
69
|
+
expect(total).toBeCloseTo(0.05);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should return zero sum for no matches", async () => {
|
|
73
|
+
const { total } = await store.sum({ domain: "nonexistent.com" });
|
|
74
|
+
expect(total).toBe(0);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { lookupPricing, checkAccess, meter } from "../../src/metering/pricing-guard.js";
|
|
3
|
+
import { MemoryMeterStore } from "../../src/metering/memory-store.js";
|
|
4
|
+
import type { ServiceRecord } from "../../src/registry/types.js";
|
|
5
|
+
|
|
6
|
+
function makeRecord(overrides?: Partial<ServiceRecord>): ServiceRecord {
|
|
7
|
+
return {
|
|
8
|
+
domain: "api.example.com",
|
|
9
|
+
name: "Example API",
|
|
10
|
+
description: "Test",
|
|
11
|
+
version: "1.0",
|
|
12
|
+
roles: ["agent"],
|
|
13
|
+
skillMd: "",
|
|
14
|
+
endpoints: [
|
|
15
|
+
{ method: "GET", path: "/api/data", description: "Get data" },
|
|
16
|
+
{ method: "POST", path: "/api/orders", description: "Create order", pricing: { cost: 0.05, currency: "USDC", per: "call" } },
|
|
17
|
+
{ method: "GET", path: "/api/users/:id", description: "Get user", pricing: { cost: 0.01, currency: "USDC", per: "call" } },
|
|
18
|
+
],
|
|
19
|
+
isFirstParty: false,
|
|
20
|
+
createdAt: Date.now(),
|
|
21
|
+
updatedAt: Date.now(),
|
|
22
|
+
status: "active",
|
|
23
|
+
isDefault: true,
|
|
24
|
+
...overrides,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("lookupPricing", () => {
|
|
29
|
+
it("should return pricing for paid endpoint", () => {
|
|
30
|
+
const record = makeRecord();
|
|
31
|
+
const pricing = lookupPricing(record, "POST", "/api/orders");
|
|
32
|
+
expect(pricing).toEqual({ cost: 0.05, currency: "USDC", per: "call" });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should return null for free endpoint", () => {
|
|
36
|
+
const record = makeRecord();
|
|
37
|
+
const pricing = lookupPricing(record, "GET", "/api/data");
|
|
38
|
+
expect(pricing).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should match path with :param", () => {
|
|
42
|
+
const record = makeRecord();
|
|
43
|
+
const pricing = lookupPricing(record, "GET", "/api/users/123");
|
|
44
|
+
expect(pricing).toEqual({ cost: 0.01, currency: "USDC", per: "call" });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should return null for unknown endpoint", () => {
|
|
48
|
+
const record = makeRecord();
|
|
49
|
+
const pricing = lookupPricing(record, "DELETE", "/api/unknown");
|
|
50
|
+
expect(pricing).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("checkAccess", () => {
|
|
55
|
+
it("should allow active services", () => {
|
|
56
|
+
const record = makeRecord({ status: "active" });
|
|
57
|
+
expect(checkAccess(record).allowed).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should deny sunset services", () => {
|
|
61
|
+
const record = makeRecord({ status: "sunset" });
|
|
62
|
+
const result = checkAccess(record);
|
|
63
|
+
expect(result.allowed).toBe(false);
|
|
64
|
+
expect(result.reason).toContain("sunset");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("should deny when sunset date has passed", () => {
|
|
68
|
+
const record = makeRecord({ sunsetDate: Date.now() - 1000 });
|
|
69
|
+
const result = checkAccess(record);
|
|
70
|
+
expect(result.allowed).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should allow when sunset date is in the future", () => {
|
|
74
|
+
const record = makeRecord({ sunsetDate: Date.now() + 86400000 });
|
|
75
|
+
expect(checkAccess(record).allowed).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("meter", () => {
|
|
80
|
+
it("should record a meter entry", async () => {
|
|
81
|
+
const store = new MemoryMeterStore();
|
|
82
|
+
const entry = await meter(store, {
|
|
83
|
+
domain: "api.example.com",
|
|
84
|
+
version: "1.0",
|
|
85
|
+
endpoint: "POST /api/orders",
|
|
86
|
+
agentId: "agent-1",
|
|
87
|
+
pricing: { cost: 0.05, currency: "USDC", per: "call" },
|
|
88
|
+
});
|
|
89
|
+
expect(entry.id).toBeTruthy();
|
|
90
|
+
expect(entry.cost).toBe(0.05);
|
|
91
|
+
|
|
92
|
+
const records = await store.query({ domain: "api.example.com" });
|
|
93
|
+
expect(records).toHaveLength(1);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should include developerId when provided", async () => {
|
|
97
|
+
const store = new MemoryMeterStore();
|
|
98
|
+
const entry = await meter(store, {
|
|
99
|
+
domain: "api.example.com",
|
|
100
|
+
version: "1.0",
|
|
101
|
+
endpoint: "POST /api/orders",
|
|
102
|
+
agentId: "agent-1",
|
|
103
|
+
developerId: "dev-1",
|
|
104
|
+
pricing: { cost: 0.05, currency: "USDC", per: "call" },
|
|
105
|
+
});
|
|
106
|
+
expect(entry.developerId).toBe("dev-1");
|
|
107
|
+
});
|
|
108
|
+
});
|