@kb-labs/gateway-app 0.2.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/.kb/database/kb.sqlite-shm +0 -0
- package/.kb/database/kb.sqlite-wal +0 -0
- package/package.json +49 -0
- package/src/__tests__/auth-routes.test.ts +279 -0
- package/src/__tests__/execute-routes.test.ts +408 -0
- package/src/__tests__/execution-registry.test.ts +218 -0
- package/src/__tests__/health.test.ts +215 -0
- package/src/__tests__/live-gateway.e2e.test.ts +648 -0
- package/src/__tests__/llm-gateway.test.ts +361 -0
- package/src/__tests__/observability-collector.test.ts +59 -0
- package/src/__tests__/platform-api.test.ts +317 -0
- package/src/__tests__/registry.test.ts +546 -0
- package/src/__tests__/retry-executor.test.ts +244 -0
- package/src/__tests__/server.integration.test.ts +417 -0
- package/src/__tests__/subscription-registry.test.ts +308 -0
- package/src/__tests__/telemetry-ingest.test.ts +309 -0
- package/src/__tests__/tokens.test.ts +83 -0
- package/src/__tests__/ws-client-connect.e2e.test.ts +381 -0
- package/src/__tests__/ws-handshake.e2e.test.ts +288 -0
- package/src/auth/middleware.ts +50 -0
- package/src/auth/routes.ts +57 -0
- package/src/auth/tokens.ts +41 -0
- package/src/bootstrap.ts +98 -0
- package/src/clients/subscription-registry.ts +137 -0
- package/src/clients/ws-handler.ts +196 -0
- package/src/config.ts +20 -0
- package/src/docs/routes.ts +70 -0
- package/src/execute/errors.ts +21 -0
- package/src/execute/execution-registry.ts +84 -0
- package/src/execute/retry-executor.ts +159 -0
- package/src/execute/routes.ts +239 -0
- package/src/hosts/dispatcher.ts +2 -0
- package/src/hosts/registry.ts +305 -0
- package/src/hosts/ws-handler.ts +445 -0
- package/src/index.ts +7 -0
- package/src/llm/routes.ts +343 -0
- package/src/manifest.ts +21 -0
- package/src/observability/collector.ts +346 -0
- package/src/platform/routes.ts +195 -0
- package/src/server.ts +447 -0
- package/src/telemetry/routes.ts +89 -0
- package/src/ws/gateway-ws.ts +73 -0
- package/tsconfig.build.json +15 -0
- package/tsconfig.json +10 -0
- package/tsup.config.ts +8 -0
- package/vitest.config.ts +23 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for Gateway /health endpoint.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* GET /health
|
|
6
|
+
* - healthy when all adapters available
|
|
7
|
+
* - degraded when non-critical adapter missing
|
|
8
|
+
* - unhealthy when LLM unavailable
|
|
9
|
+
* - includes uptime and timestamp
|
|
10
|
+
* - adapter latency reported
|
|
11
|
+
* - upstream health probing
|
|
12
|
+
*/
|
|
13
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
14
|
+
import type { ICache, ILogger } from '@kb-labs/core-platform';
|
|
15
|
+
import type { JwtConfig } from '@kb-labs/gateway-auth';
|
|
16
|
+
import type { GatewayConfig } from '@kb-labs/gateway-contracts';
|
|
17
|
+
|
|
18
|
+
// ── Mocks ─────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
let mockAdapters: Record<string, unknown> = {};
|
|
21
|
+
|
|
22
|
+
vi.mock('@kb-labs/core-runtime', () => ({
|
|
23
|
+
platform: new Proxy(
|
|
24
|
+
{},
|
|
25
|
+
{
|
|
26
|
+
get(_target, prop) {
|
|
27
|
+
return mockAdapters[prop as string];
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
// Mock fetch for upstream probing
|
|
34
|
+
const mockFetch = vi.fn();
|
|
35
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
36
|
+
|
|
37
|
+
function makeCache(): ICache {
|
|
38
|
+
const store = new Map<string, unknown>();
|
|
39
|
+
return {
|
|
40
|
+
async get<T>(k: string) { return (store.get(k) as T) ?? null; },
|
|
41
|
+
async set(k: string, v: unknown) { store.set(k, v); },
|
|
42
|
+
async delete(k: string) { store.delete(k); },
|
|
43
|
+
async clear() { store.clear(); },
|
|
44
|
+
} as unknown as ICache;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const noopLogger: ILogger = {
|
|
48
|
+
info: vi.fn(),
|
|
49
|
+
warn: vi.fn(),
|
|
50
|
+
error: vi.fn(),
|
|
51
|
+
debug: vi.fn(),
|
|
52
|
+
child: vi.fn(() => noopLogger),
|
|
53
|
+
} as unknown as ILogger;
|
|
54
|
+
|
|
55
|
+
const stubJwtConfig: JwtConfig = { secret: 'test-secret' };
|
|
56
|
+
|
|
57
|
+
// ── App builder ───────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
async function buildHealthApp(
|
|
60
|
+
config: Partial<GatewayConfig> = {},
|
|
61
|
+
) {
|
|
62
|
+
// Dynamically import createServer — it uses the mocked platform
|
|
63
|
+
const { createServer } = await import('../server.js');
|
|
64
|
+
|
|
65
|
+
const fullConfig: GatewayConfig = {
|
|
66
|
+
port: 0,
|
|
67
|
+
upstreams: {},
|
|
68
|
+
staticTokens: {},
|
|
69
|
+
...config,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const cache = makeCache();
|
|
73
|
+
return createServer(fullConfig, cache, noopLogger, stubJwtConfig);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── Tests ─────────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
describe('Gateway /health endpoint', () => {
|
|
79
|
+
let app: Awaited<ReturnType<typeof buildHealthApp>>;
|
|
80
|
+
|
|
81
|
+
afterEach(async () => {
|
|
82
|
+
if (app) {await app.close();}
|
|
83
|
+
vi.clearAllMocks();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('returns healthy when all adapters available', async () => {
|
|
87
|
+
mockAdapters = {
|
|
88
|
+
llm: { complete: vi.fn() },
|
|
89
|
+
cache: { get: vi.fn() },
|
|
90
|
+
analytics: { track: vi.fn() },
|
|
91
|
+
vectorStore: { search: vi.fn() },
|
|
92
|
+
embeddings: { embed: vi.fn() },
|
|
93
|
+
};
|
|
94
|
+
app = await buildHealthApp();
|
|
95
|
+
|
|
96
|
+
const res = await app.inject({ method: 'GET', url: '/health' });
|
|
97
|
+
expect(res.statusCode).toBe(200);
|
|
98
|
+
|
|
99
|
+
const body = res.json();
|
|
100
|
+
expect(body.status).toBe('healthy');
|
|
101
|
+
expect(body.version).toBe('1.0');
|
|
102
|
+
expect(body.adapters.llm.available).toBe(true);
|
|
103
|
+
expect(body.adapters.cache.available).toBe(true);
|
|
104
|
+
expect(body.adapters.analytics.available).toBe(true);
|
|
105
|
+
expect(body.adapters.vectorStore.available).toBe(true);
|
|
106
|
+
expect(body.adapters.embeddings.available).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('returns degraded when non-critical adapter missing', async () => {
|
|
110
|
+
mockAdapters = {
|
|
111
|
+
llm: { complete: vi.fn() },
|
|
112
|
+
cache: { get: vi.fn() },
|
|
113
|
+
analytics: undefined, // missing
|
|
114
|
+
vectorStore: undefined, // missing
|
|
115
|
+
embeddings: undefined, // missing
|
|
116
|
+
};
|
|
117
|
+
app = await buildHealthApp();
|
|
118
|
+
|
|
119
|
+
const res = await app.inject({ method: 'GET', url: '/health' });
|
|
120
|
+
const body = res.json();
|
|
121
|
+
expect(body.status).toBe('degraded');
|
|
122
|
+
expect(body.adapters.llm.available).toBe(true);
|
|
123
|
+
expect(body.adapters.analytics.available).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('returns unhealthy when LLM unavailable', async () => {
|
|
127
|
+
mockAdapters = {
|
|
128
|
+
llm: undefined, // critical missing
|
|
129
|
+
cache: { get: vi.fn() },
|
|
130
|
+
analytics: { track: vi.fn() },
|
|
131
|
+
vectorStore: undefined,
|
|
132
|
+
embeddings: undefined,
|
|
133
|
+
};
|
|
134
|
+
app = await buildHealthApp();
|
|
135
|
+
|
|
136
|
+
const res = await app.inject({ method: 'GET', url: '/health' });
|
|
137
|
+
const body = res.json();
|
|
138
|
+
expect(body.status).toBe('unhealthy');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('includes uptime and timestamp', async () => {
|
|
142
|
+
mockAdapters = { llm: { complete: vi.fn() } };
|
|
143
|
+
app = await buildHealthApp();
|
|
144
|
+
|
|
145
|
+
const res = await app.inject({ method: 'GET', url: '/health' });
|
|
146
|
+
const body = res.json();
|
|
147
|
+
expect(typeof body.uptime).toBe('number');
|
|
148
|
+
expect(body.uptime).toBeGreaterThanOrEqual(0);
|
|
149
|
+
expect(typeof body.timestamp).toBe('string');
|
|
150
|
+
expect(new Date(body.timestamp).getTime()).toBeGreaterThan(0);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('reports adapter latency', async () => {
|
|
154
|
+
mockAdapters = {
|
|
155
|
+
llm: { complete: vi.fn() },
|
|
156
|
+
cache: { get: vi.fn() },
|
|
157
|
+
};
|
|
158
|
+
app = await buildHealthApp();
|
|
159
|
+
|
|
160
|
+
const res = await app.inject({ method: 'GET', url: '/health' });
|
|
161
|
+
const body = res.json();
|
|
162
|
+
expect(typeof body.adapters.llm.latencyMs).toBe('number');
|
|
163
|
+
expect(body.adapters.llm.latencyMs).toBeGreaterThanOrEqual(0);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('probes upstream health', async () => {
|
|
167
|
+
mockAdapters = { llm: { complete: vi.fn() } };
|
|
168
|
+
mockFetch.mockResolvedValueOnce({ ok: true });
|
|
169
|
+
|
|
170
|
+
app = await buildHealthApp({
|
|
171
|
+
upstreams: {
|
|
172
|
+
'rest-api': {
|
|
173
|
+
url: 'http://localhost:5050',
|
|
174
|
+
prefix: '/api/v1',
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const res = await app.inject({ method: 'GET', url: '/health' });
|
|
180
|
+
const body = res.json();
|
|
181
|
+
expect(body.upstreams['rest-api']).toBeDefined();
|
|
182
|
+
expect(body.upstreams['rest-api'].status).toBe('up');
|
|
183
|
+
expect(typeof body.upstreams['rest-api'].latencyMs).toBe('number');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('logs structured diagnostics when upstream health probe fails', async () => {
|
|
187
|
+
mockAdapters = { llm: { complete: vi.fn() } };
|
|
188
|
+
mockFetch.mockRejectedValueOnce(new Error('connect ETIMEDOUT'));
|
|
189
|
+
|
|
190
|
+
app = await buildHealthApp({
|
|
191
|
+
upstreams: {
|
|
192
|
+
workflow: {
|
|
193
|
+
url: 'http://localhost:7778',
|
|
194
|
+
prefix: '/api/v1/workflow',
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const res = await app.inject({ method: 'GET', url: '/health' });
|
|
200
|
+
expect(res.statusCode).toBe(200);
|
|
201
|
+
expect(noopLogger.warn).toHaveBeenCalledWith(
|
|
202
|
+
'Gateway upstream health probe failed',
|
|
203
|
+
expect.objectContaining({
|
|
204
|
+
diagnosticEvent: 'gateway.upstream.health',
|
|
205
|
+
reasonCode: 'upstream_unavailable',
|
|
206
|
+
serviceId: 'gateway',
|
|
207
|
+
route: '/api/v1/workflow/health',
|
|
208
|
+
evidence: expect.objectContaining({
|
|
209
|
+
upstreamId: 'workflow',
|
|
210
|
+
upstreamUrl: 'http://localhost:7778',
|
|
211
|
+
}),
|
|
212
|
+
}),
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
});
|