@kb-labs/gateway-app 0.2.0 → 0.3.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.
Files changed (49) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.js +2454 -0
  3. package/dist/index.js.map +1 -0
  4. package/package.json +7 -4
  5. package/.kb/database/kb.sqlite-shm +0 -0
  6. package/.kb/database/kb.sqlite-wal +0 -0
  7. package/src/__tests__/auth-routes.test.ts +0 -279
  8. package/src/__tests__/execute-routes.test.ts +0 -408
  9. package/src/__tests__/execution-registry.test.ts +0 -218
  10. package/src/__tests__/health.test.ts +0 -215
  11. package/src/__tests__/live-gateway.e2e.test.ts +0 -648
  12. package/src/__tests__/llm-gateway.test.ts +0 -361
  13. package/src/__tests__/observability-collector.test.ts +0 -59
  14. package/src/__tests__/platform-api.test.ts +0 -317
  15. package/src/__tests__/registry.test.ts +0 -546
  16. package/src/__tests__/retry-executor.test.ts +0 -244
  17. package/src/__tests__/server.integration.test.ts +0 -417
  18. package/src/__tests__/subscription-registry.test.ts +0 -308
  19. package/src/__tests__/telemetry-ingest.test.ts +0 -309
  20. package/src/__tests__/tokens.test.ts +0 -83
  21. package/src/__tests__/ws-client-connect.e2e.test.ts +0 -381
  22. package/src/__tests__/ws-handshake.e2e.test.ts +0 -288
  23. package/src/auth/middleware.ts +0 -50
  24. package/src/auth/routes.ts +0 -57
  25. package/src/auth/tokens.ts +0 -41
  26. package/src/bootstrap.ts +0 -98
  27. package/src/clients/subscription-registry.ts +0 -137
  28. package/src/clients/ws-handler.ts +0 -196
  29. package/src/config.ts +0 -20
  30. package/src/docs/routes.ts +0 -70
  31. package/src/execute/errors.ts +0 -21
  32. package/src/execute/execution-registry.ts +0 -84
  33. package/src/execute/retry-executor.ts +0 -159
  34. package/src/execute/routes.ts +0 -239
  35. package/src/hosts/dispatcher.ts +0 -2
  36. package/src/hosts/registry.ts +0 -305
  37. package/src/hosts/ws-handler.ts +0 -445
  38. package/src/index.ts +0 -7
  39. package/src/llm/routes.ts +0 -343
  40. package/src/manifest.ts +0 -21
  41. package/src/observability/collector.ts +0 -346
  42. package/src/platform/routes.ts +0 -195
  43. package/src/server.ts +0 -447
  44. package/src/telemetry/routes.ts +0 -89
  45. package/src/ws/gateway-ws.ts +0 -73
  46. package/tsconfig.build.json +0 -15
  47. package/tsconfig.json +0 -10
  48. package/tsup.config.ts +0 -8
  49. package/vitest.config.ts +0 -23
@@ -1,218 +0,0 @@
1
- /**
2
- * Unit tests for ExecutionRegistry (CC2 — Cancellation).
3
- */
4
- import { describe, it, expect, beforeEach } from 'vitest';
5
- import { ExecutionRegistry } from '../execute/execution-registry.js';
6
-
7
- function makeEntry(overrides: Partial<{
8
- executionId: string;
9
- requestId: string;
10
- namespaceId: string;
11
- hostId: string;
12
- pluginId: string;
13
- handlerRef: string;
14
- }> = {}) {
15
- return {
16
- executionId: 'exec-1',
17
- requestId: 'req-1',
18
- namespaceId: 'ns-1',
19
- hostId: 'host-1',
20
- pluginId: 'plugin-a',
21
- handlerRef: 'handlers/run.ts',
22
- ...overrides,
23
- };
24
- }
25
-
26
- describe('ExecutionRegistry', () => {
27
- let registry: ExecutionRegistry;
28
-
29
- beforeEach(() => {
30
- registry = new ExecutionRegistry();
31
- });
32
-
33
- // ── register ──────────────────────────────────────────────────────────────
34
-
35
- describe('register()', () => {
36
- it('returns an AbortSignal', () => {
37
- const signal = registry.register(makeEntry());
38
- expect(signal).toBeInstanceOf(AbortSignal);
39
- });
40
-
41
- it('signal is not aborted initially', () => {
42
- const signal = registry.register(makeEntry());
43
- expect(signal.aborted).toBe(false);
44
- });
45
-
46
- it('stores execution metadata', () => {
47
- registry.register(makeEntry({ executionId: 'exec-x' }));
48
- const entry = registry.get('exec-x');
49
- expect(entry?.namespaceId).toBe('ns-1');
50
- expect(entry?.hostId).toBe('host-1');
51
- expect(entry?.pluginId).toBe('plugin-a');
52
- expect(entry?.startedAt).toBeTypeOf('number');
53
- });
54
-
55
- it('increments size', () => {
56
- registry.register(makeEntry({ executionId: 'a' }));
57
- registry.register(makeEntry({ executionId: 'b' }));
58
- expect(registry.size).toBe(2);
59
- });
60
- });
61
-
62
- // ── cancel ────────────────────────────────────────────────────────────────
63
-
64
- describe('cancel()', () => {
65
- it('returns true and aborts signal', () => {
66
- const signal = registry.register(makeEntry());
67
- const result = registry.cancel('exec-1', 'user');
68
- expect(result).toBe(true);
69
- expect(signal.aborted).toBe(true);
70
- });
71
-
72
- it('stores cancellation reason on the entry', () => {
73
- registry.register(makeEntry());
74
- registry.cancel('exec-1', 'timeout');
75
- expect(registry.get('exec-1')?.cancelledReason).toBe('timeout');
76
- });
77
-
78
- it('returns false for non-existent executionId', () => {
79
- expect(registry.cancel('does-not-exist', 'user')).toBe(false);
80
- });
81
-
82
- it('returns false when already cancelled (idempotent)', () => {
83
- registry.register(makeEntry());
84
- expect(registry.cancel('exec-1', 'user')).toBe(true);
85
- expect(registry.cancel('exec-1', 'timeout')).toBe(false);
86
- });
87
-
88
- it('abort signal reason matches provided reason', () => {
89
- const signal = registry.register(makeEntry());
90
- registry.cancel('exec-1', 'disconnect');
91
- expect(signal.reason).toBe('disconnect');
92
- });
93
- });
94
-
95
- // ── remove ────────────────────────────────────────────────────────────────
96
-
97
- describe('remove()', () => {
98
- it('removes execution from registry', () => {
99
- registry.register(makeEntry());
100
- registry.remove('exec-1');
101
- expect(registry.get('exec-1')).toBeUndefined();
102
- });
103
-
104
- it('decrements size', () => {
105
- registry.register(makeEntry({ executionId: 'a' }));
106
- registry.register(makeEntry({ executionId: 'b' }));
107
- registry.remove('a');
108
- expect(registry.size).toBe(1);
109
- });
110
-
111
- it('is no-op for non-existent id', () => {
112
- expect(() => registry.remove('ghost')).not.toThrow();
113
- });
114
- });
115
-
116
- // ── get ───────────────────────────────────────────────────────────────────
117
-
118
- describe('get()', () => {
119
- it('returns undefined for unknown id', () => {
120
- expect(registry.get('unknown')).toBeUndefined();
121
- });
122
-
123
- it('returns the active execution', () => {
124
- registry.register(makeEntry({ executionId: 'exec-abc' }));
125
- const entry = registry.get('exec-abc');
126
- expect(entry?.executionId).toBe('exec-abc');
127
- });
128
- });
129
-
130
- // ── cancelByHost ──────────────────────────────────────────────────────────
131
-
132
- describe('cancelByHost()', () => {
133
- it('cancels all executions for given hostId', () => {
134
- const sig1 = registry.register(makeEntry({ executionId: 'e1', hostId: 'host-A' }));
135
- const sig2 = registry.register(makeEntry({ executionId: 'e2', hostId: 'host-A' }));
136
- const sig3 = registry.register(makeEntry({ executionId: 'e3', hostId: 'host-B' }));
137
-
138
- const cancelled = registry.cancelByHost('host-A', 'disconnect');
139
-
140
- expect(cancelled).toContain('e1');
141
- expect(cancelled).toContain('e2');
142
- expect(cancelled).not.toContain('e3');
143
- expect(sig1.aborted).toBe(true);
144
- expect(sig2.aborted).toBe(true);
145
- expect(sig3.aborted).toBe(false);
146
- });
147
-
148
- it('returns empty array when no executions for host', () => {
149
- registry.register(makeEntry({ executionId: 'e1', hostId: 'host-X' }));
150
- const result = registry.cancelByHost('host-Z', 'disconnect');
151
- expect(result).toEqual([]);
152
- });
153
-
154
- it('skips already-aborted executions', () => {
155
- registry.register(makeEntry({ executionId: 'e1', hostId: 'host-A' }));
156
- registry.cancel('e1', 'user'); // pre-cancel
157
- const cancelled = registry.cancelByHost('host-A', 'disconnect');
158
- expect(cancelled).not.toContain('e1');
159
- });
160
-
161
- it('returns executionIds of cancelled executions', () => {
162
- registry.register(makeEntry({ executionId: 'alpha', hostId: 'host-1' }));
163
- const result = registry.cancelByHost('host-1', 'disconnect');
164
- expect(result).toEqual(['alpha']);
165
- });
166
- });
167
-
168
- // ── size ──────────────────────────────────────────────────────────────────
169
-
170
- describe('size', () => {
171
- it('starts at 0', () => {
172
- expect(registry.size).toBe(0);
173
- });
174
-
175
- it('tracks additions and removals correctly', () => {
176
- registry.register(makeEntry({ executionId: 'a' }));
177
- registry.register(makeEntry({ executionId: 'b' }));
178
- registry.register(makeEntry({ executionId: 'c' }));
179
- registry.remove('b');
180
- expect(registry.size).toBe(2);
181
- });
182
- });
183
- });
184
-
185
- // ── CancelledError ────────────────────────────────────────────────────────────
186
-
187
- import { CancelledError } from '../execute/errors.js';
188
-
189
- describe('CancelledError', () => {
190
- it('stores reason', () => {
191
- const err = new CancelledError('timeout');
192
- expect(err.reason).toBe('timeout');
193
- });
194
-
195
- it('has descriptive message', () => {
196
- const err = new CancelledError('user');
197
- expect(err.message).toContain('Execution cancelled');
198
- expect(err.message).toContain('user');
199
- });
200
-
201
- it('has correct name', () => {
202
- expect(new CancelledError('disconnect').name).toBe('CancelledError');
203
- });
204
-
205
- it('is instanceof Error', () => {
206
- expect(new CancelledError('user')).toBeInstanceOf(Error);
207
- });
208
-
209
- it('is instanceof CancelledError', () => {
210
- expect(new CancelledError('user')).toBeInstanceOf(CancelledError);
211
- });
212
-
213
- it('supports all cancellation reasons', () => {
214
- for (const reason of ['user', 'timeout', 'disconnect'] as const) {
215
- expect(() => new CancelledError(reason)).not.toThrow();
216
- }
217
- });
218
- });
@@ -1,215 +0,0 @@
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
- });