@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,88 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { Context7Backend } from "../../src/registry/context7-backend.js";
|
|
3
|
+
|
|
4
|
+
function createMockFetch(handlers: Record<string, (url: string, init?: RequestInit) => Response>) {
|
|
5
|
+
return async (url: string, init?: RequestInit) => {
|
|
6
|
+
for (const [pattern, handler] of Object.entries(handlers)) {
|
|
7
|
+
if (url.includes(pattern)) return handler(url, init);
|
|
8
|
+
}
|
|
9
|
+
return new Response("not found", { status: 404 });
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("Context7Backend", () => {
|
|
14
|
+
it("list / should return usage instructions", async () => {
|
|
15
|
+
const backend = new Context7Backend({ fetchFn: (() => {}) as any });
|
|
16
|
+
const entries = await backend.list("/");
|
|
17
|
+
expect(entries.length).toBeGreaterThan(0);
|
|
18
|
+
expect(entries.some((e) => e.includes("grep"))).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("search / should search libraries", async () => {
|
|
22
|
+
const backend = new Context7Backend({
|
|
23
|
+
fetchFn: createMockFetch({
|
|
24
|
+
"/libs/search": () =>
|
|
25
|
+
new Response(JSON.stringify([
|
|
26
|
+
{ id: "/facebook/react", name: "React", description: "UI library", totalSnippets: 100 },
|
|
27
|
+
{ id: "/vuejs/core", name: "Vue", description: "Progressive framework", totalSnippets: 80 },
|
|
28
|
+
]), { status: 200 }),
|
|
29
|
+
}) as any,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const results = await backend.search("/", "react");
|
|
33
|
+
expect(results).toHaveLength(2);
|
|
34
|
+
expect((results[0] as any).id).toBe("/facebook/react");
|
|
35
|
+
expect((results[0] as any).name).toBe("React");
|
|
36
|
+
expect((results[0] as any).snippets).toBe(100);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("read /{owner}/{repo} should query overview docs", async () => {
|
|
40
|
+
const backend = new Context7Backend({
|
|
41
|
+
fetchFn: createMockFetch({
|
|
42
|
+
"/context": () => new Response("React is a JavaScript library for building user interfaces.", { status: 200 }),
|
|
43
|
+
}) as any,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const result = (await backend.read("/facebook/react")) as any;
|
|
47
|
+
expect(result.libraryId).toBe("/facebook/react");
|
|
48
|
+
expect(result.docs).toContain("React");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("search /{owner}/{repo} should query specific docs", async () => {
|
|
52
|
+
const backend = new Context7Backend({
|
|
53
|
+
fetchFn: createMockFetch({
|
|
54
|
+
"/context": (url) => {
|
|
55
|
+
expect(url).toContain("query=useState");
|
|
56
|
+
return new Response("useState is a Hook that lets you add state...", { status: 200 });
|
|
57
|
+
},
|
|
58
|
+
}) as any,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const results = await backend.search("/facebook/react", "useState");
|
|
62
|
+
expect(results).toHaveLength(1);
|
|
63
|
+
expect((results[0] as any).libraryId).toBe("/facebook/react");
|
|
64
|
+
expect((results[0] as any).docs).toContain("useState");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("read / should return usage hint", async () => {
|
|
68
|
+
const backend = new Context7Backend({ fetchFn: (() => {}) as any });
|
|
69
|
+
const result = (await backend.read("/")) as any;
|
|
70
|
+
expect(result.usage).toBeTruthy();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("write should throw read-only error", async () => {
|
|
74
|
+
const backend = new Context7Backend({ fetchFn: (() => {}) as any });
|
|
75
|
+
await expect(backend.write("/test", {})).rejects.toThrow("read-only");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("remove should throw read-only error", async () => {
|
|
79
|
+
const backend = new Context7Backend({ fetchFn: (() => {}) as any });
|
|
80
|
+
await expect(backend.remove("/test")).rejects.toThrow("read-only");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("search with single-segment path should return empty", async () => {
|
|
84
|
+
const backend = new Context7Backend({ fetchFn: (() => {}) as any });
|
|
85
|
+
const results = await backend.search("/react", "hooks");
|
|
86
|
+
expect(results).toEqual([]);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E integration test: Context7 API (real network).
|
|
3
|
+
*
|
|
4
|
+
* Requires CONTEXT7_API_KEY environment variable.
|
|
5
|
+
* Skipped automatically when no API key is available.
|
|
6
|
+
*
|
|
7
|
+
* Run with: CONTEXT7_API_KEY=ctx7sk_xxx npx vitest run packages/gateway/test/registry/context7-e2e.test.ts
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect } from "vitest";
|
|
10
|
+
import { Context7Client } from "../../src/registry/context7.js";
|
|
11
|
+
import { Context7Backend } from "../../src/registry/context7-backend.js";
|
|
12
|
+
import { MemoryRegistryStore } from "../../src/registry/memory-store.js";
|
|
13
|
+
import { createRegistryResolver } from "../../src/registry/resolver.js";
|
|
14
|
+
import { AgentFs } from "@nkmc/agent-fs";
|
|
15
|
+
|
|
16
|
+
const API_KEY = process.env.CONTEXT7_API_KEY;
|
|
17
|
+
|
|
18
|
+
describe.skipIf(!API_KEY)("Context7 Client E2E (real network)", () => {
|
|
19
|
+
const client = new Context7Client({ apiKey: API_KEY });
|
|
20
|
+
|
|
21
|
+
it("should search for React library", async () => {
|
|
22
|
+
const results = await client.searchLibraries("react");
|
|
23
|
+
expect(results.length).toBeGreaterThan(0);
|
|
24
|
+
|
|
25
|
+
const react = results.find((r) => r.id.includes("react"));
|
|
26
|
+
expect(react).toBeDefined();
|
|
27
|
+
expect(react!.name).toBeTruthy();
|
|
28
|
+
}, 15_000);
|
|
29
|
+
|
|
30
|
+
it("should query React hooks documentation", async () => {
|
|
31
|
+
const results = await client.searchLibraries("react");
|
|
32
|
+
const react = results.find((r) => r.id.includes("facebook/react") || r.id.includes("react"));
|
|
33
|
+
expect(react).toBeDefined();
|
|
34
|
+
|
|
35
|
+
const docs = await client.queryDocs(react!.id, "useState hook");
|
|
36
|
+
expect(docs.length).toBeGreaterThan(0);
|
|
37
|
+
expect(docs.toLowerCase()).toMatch(/react|hook|state|usestate/);
|
|
38
|
+
}, 30_000);
|
|
39
|
+
|
|
40
|
+
it("should search for Next.js library", async () => {
|
|
41
|
+
const results = await client.searchLibraries("nextjs");
|
|
42
|
+
expect(results.length).toBeGreaterThan(0);
|
|
43
|
+
}, 15_000);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe.skipIf(!API_KEY)("Context7 via AgentFs E2E (real network)", () => {
|
|
47
|
+
const fs = new AgentFs({
|
|
48
|
+
mounts: [
|
|
49
|
+
{ path: "/context7", backend: new Context7Backend({ apiKey: API_KEY }) },
|
|
50
|
+
],
|
|
51
|
+
onMiss: async () => false,
|
|
52
|
+
listDomains: async () => [],
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("ls / should show context7", async () => {
|
|
56
|
+
const result = await fs.execute("ls /");
|
|
57
|
+
expect(result.ok).toBe(true);
|
|
58
|
+
expect(result.data).toContain("context7/");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("ls /context7/ should show usage hints", async () => {
|
|
62
|
+
const result = await fs.execute("ls /context7/");
|
|
63
|
+
expect(result.ok).toBe(true);
|
|
64
|
+
const entries = result.data as string[];
|
|
65
|
+
expect(entries.length).toBeGreaterThan(0);
|
|
66
|
+
expect(entries.some((e) => e.includes("grep"))).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('grep "react" /context7/ should search libraries', async () => {
|
|
70
|
+
const result = await fs.execute('grep "react" /context7/');
|
|
71
|
+
expect(result.ok).toBe(true);
|
|
72
|
+
const data = result.data as any[];
|
|
73
|
+
expect(data.length).toBeGreaterThan(0);
|
|
74
|
+
expect(data.some((r: any) => r.id?.includes("react"))).toBe(true);
|
|
75
|
+
}, 15_000);
|
|
76
|
+
|
|
77
|
+
it("cat /context7/facebook/react should return overview docs", async () => {
|
|
78
|
+
const result = await fs.execute("cat /context7/facebook/react");
|
|
79
|
+
expect(result.ok).toBe(true);
|
|
80
|
+
const data = result.data as any;
|
|
81
|
+
expect(data.libraryId).toBe("/facebook/react");
|
|
82
|
+
expect(data.docs.length).toBeGreaterThan(0);
|
|
83
|
+
}, 30_000);
|
|
84
|
+
|
|
85
|
+
it('grep "useState" /context7/facebook/react should query specific docs', async () => {
|
|
86
|
+
const result = await fs.execute('grep "useState" /context7/facebook/react');
|
|
87
|
+
expect(result.ok).toBe(true);
|
|
88
|
+
const data = result.data as any[];
|
|
89
|
+
expect(data.length).toBeGreaterThan(0);
|
|
90
|
+
expect((data[0] as any).docs).toBeTruthy();
|
|
91
|
+
}, 30_000);
|
|
92
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { Context7Client } from "../../src/registry/context7.js";
|
|
3
|
+
|
|
4
|
+
describe("Context7Client", () => {
|
|
5
|
+
it("should search libraries successfully", async () => {
|
|
6
|
+
const mockFetch = async (url: string) => {
|
|
7
|
+
expect(url).toContain("/libs/search");
|
|
8
|
+
expect(url).toContain("libraryName=react");
|
|
9
|
+
return new Response(JSON.stringify([{ id: "/facebook/react", name: "React" }]), { status: 200 });
|
|
10
|
+
};
|
|
11
|
+
const client = new Context7Client({ fetchFn: mockFetch as any });
|
|
12
|
+
const results = await client.searchLibraries("react");
|
|
13
|
+
expect(results).toHaveLength(1);
|
|
14
|
+
expect(results[0].id).toBe("/facebook/react");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("should query docs successfully", async () => {
|
|
18
|
+
const mockFetch = async (url: string) => {
|
|
19
|
+
expect(url).toContain("/context");
|
|
20
|
+
expect(url).toContain("libraryId=");
|
|
21
|
+
expect(url).toContain("query=hooks");
|
|
22
|
+
return new Response("React hooks documentation...", { status: 200 });
|
|
23
|
+
};
|
|
24
|
+
const client = new Context7Client({ fetchFn: mockFetch as any });
|
|
25
|
+
const result = await client.queryDocs("/facebook/react", "hooks");
|
|
26
|
+
expect(result).toContain("React hooks");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should throw on failed query", async () => {
|
|
30
|
+
const mockFetch = async () => new Response("error", { status: 500 });
|
|
31
|
+
const client = new Context7Client({ fetchFn: mockFetch as any });
|
|
32
|
+
await expect(client.queryDocs("/bad/lib", "test")).rejects.toThrow("Context7 query failed");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should throw on failed search", async () => {
|
|
36
|
+
const mockFetch = async () => new Response("error", { status: 403 });
|
|
37
|
+
const client = new Context7Client({ fetchFn: mockFetch as any });
|
|
38
|
+
await expect(client.searchLibraries("test")).rejects.toThrow("Context7 search failed");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should use custom baseUrl", async () => {
|
|
42
|
+
let requestedUrl = "";
|
|
43
|
+
const mockFetch = async (url: string) => {
|
|
44
|
+
requestedUrl = url;
|
|
45
|
+
return new Response("ok", { status: 200 });
|
|
46
|
+
};
|
|
47
|
+
const client = new Context7Client({ baseUrl: "https://custom.api.com", fetchFn: mockFetch as any });
|
|
48
|
+
await client.queryDocs("/lib/id", "test");
|
|
49
|
+
expect(requestedUrl).toContain("https://custom.api.com");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should send auth header when apiKey is set", async () => {
|
|
53
|
+
let capturedHeaders: Record<string, string> = {};
|
|
54
|
+
const mockFetch = async (_url: string, init?: RequestInit) => {
|
|
55
|
+
capturedHeaders = init?.headers as Record<string, string>;
|
|
56
|
+
return new Response("ok", { status: 200 });
|
|
57
|
+
};
|
|
58
|
+
const client = new Context7Client({ apiKey: "ctx7sk_test123", fetchFn: mockFetch as any });
|
|
59
|
+
await client.queryDocs("/lib/id", "test");
|
|
60
|
+
expect(capturedHeaders["Authorization"]).toBe("Bearer ctx7sk_test123");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should not send auth header when apiKey is not set", async () => {
|
|
64
|
+
let capturedHeaders: Record<string, string> = {};
|
|
65
|
+
const mockFetch = async (_url: string, init?: RequestInit) => {
|
|
66
|
+
capturedHeaders = init?.headers as Record<string, string>;
|
|
67
|
+
return new Response("ok", { status: 200 });
|
|
68
|
+
};
|
|
69
|
+
const client = new Context7Client({ fetchFn: mockFetch as any });
|
|
70
|
+
await client.queryDocs("/lib/id", "test");
|
|
71
|
+
expect(capturedHeaders["Authorization"]).toBeUndefined();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { D1RegistryStore } from "../../src/registry/d1-store.js";
|
|
3
|
+
import { SqliteD1 } from "../../src/testing/sqlite-d1.js";
|
|
4
|
+
import type { ServiceRecord } from "../../src/registry/types.js";
|
|
5
|
+
|
|
6
|
+
function makeRecord(
|
|
7
|
+
domain: string,
|
|
8
|
+
overrides?: Partial<ServiceRecord>,
|
|
9
|
+
): ServiceRecord {
|
|
10
|
+
return {
|
|
11
|
+
domain,
|
|
12
|
+
name: overrides?.name ?? domain,
|
|
13
|
+
description: overrides?.description ?? `Service ${domain}`,
|
|
14
|
+
version: overrides?.version ?? "1.0",
|
|
15
|
+
roles: ["agent"],
|
|
16
|
+
skillMd: "---\nname: test\n---\n# Test",
|
|
17
|
+
endpoints: [
|
|
18
|
+
{ method: "GET", path: "/api/test", description: "test endpoint" },
|
|
19
|
+
],
|
|
20
|
+
isFirstParty: overrides?.isFirstParty ?? false,
|
|
21
|
+
createdAt: overrides?.createdAt ?? Date.now(),
|
|
22
|
+
updatedAt: overrides?.updatedAt ?? Date.now(),
|
|
23
|
+
status: overrides?.status ?? "active",
|
|
24
|
+
isDefault: overrides?.isDefault ?? true,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("D1RegistryStore", () => {
|
|
29
|
+
let db: SqliteD1;
|
|
30
|
+
let store: D1RegistryStore;
|
|
31
|
+
|
|
32
|
+
beforeEach(async () => {
|
|
33
|
+
db = new SqliteD1();
|
|
34
|
+
store = new D1RegistryStore(db);
|
|
35
|
+
await store.initSchema();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
db.close();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should put and get a service", async () => {
|
|
43
|
+
const record = makeRecord("acme-store.com");
|
|
44
|
+
await store.put("acme-store.com", record);
|
|
45
|
+
const result = await store.get("acme-store.com");
|
|
46
|
+
expect(result).toEqual(record);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should return null for unknown domain", async () => {
|
|
50
|
+
const result = await store.get("unknown.com");
|
|
51
|
+
expect(result).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should delete a service", async () => {
|
|
55
|
+
await store.put("acme.com", makeRecord("acme.com"));
|
|
56
|
+
await store.delete("acme.com");
|
|
57
|
+
expect(await store.get("acme.com")).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should list all services as summaries", async () => {
|
|
61
|
+
await store.put("acme.com", makeRecord("acme.com"));
|
|
62
|
+
await store.put("memory", makeRecord("memory", { isFirstParty: true }));
|
|
63
|
+
const list = await store.list();
|
|
64
|
+
expect(list).toHaveLength(2);
|
|
65
|
+
expect(list[0]).toEqual({
|
|
66
|
+
domain: "acme.com",
|
|
67
|
+
name: "acme.com",
|
|
68
|
+
description: "Service acme.com",
|
|
69
|
+
isFirstParty: false,
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should search by description", async () => {
|
|
74
|
+
await store.put(
|
|
75
|
+
"weather.com",
|
|
76
|
+
makeRecord("weather.com", { description: "Weather forecasts" }),
|
|
77
|
+
);
|
|
78
|
+
await store.put(
|
|
79
|
+
"acme.com",
|
|
80
|
+
makeRecord("acme.com", { description: "E-commerce store" }),
|
|
81
|
+
);
|
|
82
|
+
const results = await store.search("weather");
|
|
83
|
+
expect(results).toHaveLength(1);
|
|
84
|
+
expect(results[0].domain).toBe("weather.com");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should search by endpoint description", async () => {
|
|
88
|
+
const record = makeRecord("stripe.com", { description: "Payments" });
|
|
89
|
+
record.endpoints = [
|
|
90
|
+
{ method: "POST", path: "/charges", description: "Create a charge" },
|
|
91
|
+
];
|
|
92
|
+
await store.put("stripe.com", record);
|
|
93
|
+
const results = await store.search("charge");
|
|
94
|
+
expect(results).toHaveLength(1);
|
|
95
|
+
expect(results[0].domain).toBe("stripe.com");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should return empty for no search match", async () => {
|
|
99
|
+
await store.put("acme.com", makeRecord("acme.com"));
|
|
100
|
+
const results = await store.search("zzzzz");
|
|
101
|
+
expect(results).toHaveLength(0);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should overwrite on duplicate put", async () => {
|
|
105
|
+
await store.put("acme.com", makeRecord("acme.com", { description: "v1" }));
|
|
106
|
+
await store.put("acme.com", makeRecord("acme.com", { description: "v2" }));
|
|
107
|
+
const result = await store.get("acme.com");
|
|
108
|
+
expect(result?.description).toBe("v2");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should call initSchema multiple times (idempotent)", async () => {
|
|
112
|
+
await store.initSchema();
|
|
113
|
+
await store.initSchema();
|
|
114
|
+
await store.put("test.com", makeRecord("test.com"));
|
|
115
|
+
expect(await store.get("test.com")).not.toBeNull();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("should search by name", async () => {
|
|
119
|
+
await store.put(
|
|
120
|
+
"acme.com",
|
|
121
|
+
makeRecord("acme.com", { name: "Acme Store" }),
|
|
122
|
+
);
|
|
123
|
+
const results = await store.search("Acme");
|
|
124
|
+
expect(results).toHaveLength(1);
|
|
125
|
+
expect(results[0].domain).toBe("acme.com");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("get() should return only isDefault=true records", async () => {
|
|
129
|
+
await store.put("acme.com", makeRecord("acme.com", { version: "1.0", isDefault: false }));
|
|
130
|
+
await store.put("acme.com", makeRecord("acme.com", { version: "2.0", isDefault: true }));
|
|
131
|
+
const result = await store.get("acme.com");
|
|
132
|
+
expect(result?.version).toBe("2.0");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("list() should only return default versions", async () => {
|
|
136
|
+
await store.put("acme.com", makeRecord("acme.com", { version: "1.0", isDefault: false }));
|
|
137
|
+
await store.put("acme.com", makeRecord("acme.com", { version: "2.0", isDefault: true }));
|
|
138
|
+
const list = await store.list();
|
|
139
|
+
expect(list).toHaveLength(1);
|
|
140
|
+
expect(list[0].domain).toBe("acme.com");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("getVersion() should return specific version", async () => {
|
|
144
|
+
await store.put("acme.com", makeRecord("acme.com", { version: "1.0", isDefault: false }));
|
|
145
|
+
await store.put("acme.com", makeRecord("acme.com", { version: "2.0", isDefault: true }));
|
|
146
|
+
const v1 = await store.getVersion("acme.com", "1.0");
|
|
147
|
+
expect(v1?.version).toBe("1.0");
|
|
148
|
+
expect(v1?.isDefault).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("listVersions() should return all versions for a domain", async () => {
|
|
152
|
+
await store.put("acme.com", makeRecord("acme.com", { version: "1.0", createdAt: 1000 }));
|
|
153
|
+
await store.put("acme.com", makeRecord("acme.com", { version: "2.0", createdAt: 2000 }));
|
|
154
|
+
const versions = await store.listVersions("acme.com");
|
|
155
|
+
expect(versions).toHaveLength(2);
|
|
156
|
+
expect(versions[0].version).toBe("2.0"); // most recent first
|
|
157
|
+
expect(versions[1].version).toBe("1.0");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("delete() should remove all versions", async () => {
|
|
161
|
+
await store.put("acme.com", makeRecord("acme.com", { version: "1.0" }));
|
|
162
|
+
await store.put("acme.com", makeRecord("acme.com", { version: "2.0" }));
|
|
163
|
+
await store.delete("acme.com");
|
|
164
|
+
expect(await store.get("acme.com")).toBeNull();
|
|
165
|
+
expect(await store.getVersion("acme.com", "1.0")).toBeNull();
|
|
166
|
+
expect(await store.listVersions("acme.com")).toEqual([]);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("should store and retrieve source config", async () => {
|
|
170
|
+
const record = makeRecord("acme.com");
|
|
171
|
+
record.source = { type: "openapi", url: "https://acme.com/spec.json", refreshInterval: 3600 };
|
|
172
|
+
await store.put("acme.com", record);
|
|
173
|
+
const result = await store.get("acme.com");
|
|
174
|
+
expect(result?.source).toEqual(record.source);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("should store and retrieve sunsetDate", async () => {
|
|
178
|
+
const record = makeRecord("acme.com");
|
|
179
|
+
record.sunsetDate = Date.now() + 86400000;
|
|
180
|
+
await store.put("acme.com", record);
|
|
181
|
+
const result = await store.get("acme.com");
|
|
182
|
+
expect(result?.sunsetDate).toBe(record.sunsetDate);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { AgentFs } from "@nkmc/agent-fs";
|
|
3
|
+
import { MemoryBackend } from "@nkmc/agent-fs/testing";
|
|
4
|
+
import { MemoryRegistryStore } from "../../src/registry/memory-store.js";
|
|
5
|
+
import { parseSkillMd } from "../../src/registry/skill-parser.js";
|
|
6
|
+
import { createRegistryResolver } from "../../src/registry/resolver.js";
|
|
7
|
+
|
|
8
|
+
const WEATHER_SKILL = `---
|
|
9
|
+
name: "Weather API"
|
|
10
|
+
gateway: nkmc
|
|
11
|
+
version: "1.0"
|
|
12
|
+
roles: [agent]
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Weather API
|
|
16
|
+
|
|
17
|
+
Real-time weather forecasts and historical data.
|
|
18
|
+
|
|
19
|
+
## API
|
|
20
|
+
|
|
21
|
+
### Get forecast
|
|
22
|
+
|
|
23
|
+
\`GET /api/forecast\` — 0.01 USDC / 次,agent
|
|
24
|
+
|
|
25
|
+
### Get history
|
|
26
|
+
|
|
27
|
+
\`GET /api/history\` — 0.02 USDC / 次,agent
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
const STORE_SKILL = `---
|
|
31
|
+
name: "Acme Store"
|
|
32
|
+
gateway: nkmc
|
|
33
|
+
version: "2.0"
|
|
34
|
+
roles: [agent, premium]
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
# Acme Store
|
|
38
|
+
|
|
39
|
+
E-commerce platform for widgets and gadgets.
|
|
40
|
+
|
|
41
|
+
## Schema
|
|
42
|
+
|
|
43
|
+
### products (读: public / 写: agent)
|
|
44
|
+
|
|
45
|
+
All available products.
|
|
46
|
+
|
|
47
|
+
| field | type | description |
|
|
48
|
+
|-------|------|-------------|
|
|
49
|
+
| id | string | Product ID |
|
|
50
|
+
| name | string | Product name |
|
|
51
|
+
| price | number | Price in USD |
|
|
52
|
+
|
|
53
|
+
## API
|
|
54
|
+
|
|
55
|
+
### Create order
|
|
56
|
+
|
|
57
|
+
\`POST /api/orders\` — 0.05 USDC / 次,agent
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
describe("Registry Integration", () => {
|
|
61
|
+
let fs: AgentFs;
|
|
62
|
+
let store: MemoryRegistryStore;
|
|
63
|
+
|
|
64
|
+
beforeEach(async () => {
|
|
65
|
+
store = new MemoryRegistryStore();
|
|
66
|
+
|
|
67
|
+
await store.put(
|
|
68
|
+
"weather-api.com",
|
|
69
|
+
parseSkillMd("weather-api.com", WEATHER_SKILL),
|
|
70
|
+
);
|
|
71
|
+
await store.put(
|
|
72
|
+
"acme-store.com",
|
|
73
|
+
parseSkillMd("acme-store.com", STORE_SKILL),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const { onMiss, listDomains, searchDomains } =
|
|
77
|
+
createRegistryResolver(store);
|
|
78
|
+
const memoryBackend = new MemoryBackend();
|
|
79
|
+
memoryBackend.seed("notes", [{ id: "1", text: "hello" }]);
|
|
80
|
+
|
|
81
|
+
fs = new AgentFs({
|
|
82
|
+
mounts: [{ path: "/memory", backend: memoryBackend }],
|
|
83
|
+
onMiss,
|
|
84
|
+
listDomains,
|
|
85
|
+
searchDomains,
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("ls / shows static mounts and registry services", async () => {
|
|
90
|
+
const result = await fs.execute("ls /");
|
|
91
|
+
expect(result.ok).toBe(true);
|
|
92
|
+
const entries = result.data as string[];
|
|
93
|
+
expect(entries).toContain("memory/");
|
|
94
|
+
expect(entries).toContain("weather-api.com/");
|
|
95
|
+
expect(entries).toContain("acme-store.com/");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("grep on root searches across all registered services", async () => {
|
|
99
|
+
const result = await fs.execute('grep "weather" /');
|
|
100
|
+
expect(result.ok).toBe(true);
|
|
101
|
+
const data = result.data as Array<{ domain: string }>;
|
|
102
|
+
expect(data.some((d) => d.domain === "weather-api.com")).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("grep on root returns empty for no match", async () => {
|
|
106
|
+
const result = await fs.execute('grep "zzzznotfound" /');
|
|
107
|
+
expect(result.ok).toBe(true);
|
|
108
|
+
expect(result.data).toEqual([]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("static mount still works normally", async () => {
|
|
112
|
+
const result = await fs.execute("cat /memory/notes/1.json");
|
|
113
|
+
expect(result.ok).toBe(true);
|
|
114
|
+
expect((result.data as { text: string }).text).toBe("hello");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("new service registration is immediately discoverable", async () => {
|
|
118
|
+
const newSkill = `---\nname: "New Service"\ngateway: nkmc\nversion: "1.0"\nroles: [agent]\n---\n\n# New Service\n\nBrand new.\n`;
|
|
119
|
+
await store.put(
|
|
120
|
+
"new-service.com",
|
|
121
|
+
parseSkillMd("new-service.com", newSkill),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const result = await fs.execute("ls /");
|
|
125
|
+
expect(result.ok).toBe(true);
|
|
126
|
+
const entries = result.data as string[];
|
|
127
|
+
expect(entries).toContain("new-service.com/");
|
|
128
|
+
});
|
|
129
|
+
});
|