@kb-labs/gateway-app 0.2.0 → 0.4.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/index.d.ts +2 -0
- package/dist/index.js +2454 -0
- package/dist/index.js.map +1 -0
- package/package.json +14 -11
- package/.kb/database/kb.sqlite-shm +0 -0
- package/.kb/database/kb.sqlite-wal +0 -0
- package/src/__tests__/auth-routes.test.ts +0 -279
- package/src/__tests__/execute-routes.test.ts +0 -408
- package/src/__tests__/execution-registry.test.ts +0 -218
- package/src/__tests__/health.test.ts +0 -215
- package/src/__tests__/live-gateway.e2e.test.ts +0 -648
- package/src/__tests__/llm-gateway.test.ts +0 -361
- package/src/__tests__/observability-collector.test.ts +0 -59
- package/src/__tests__/platform-api.test.ts +0 -317
- package/src/__tests__/registry.test.ts +0 -546
- package/src/__tests__/retry-executor.test.ts +0 -244
- package/src/__tests__/server.integration.test.ts +0 -417
- package/src/__tests__/subscription-registry.test.ts +0 -308
- package/src/__tests__/telemetry-ingest.test.ts +0 -309
- package/src/__tests__/tokens.test.ts +0 -83
- package/src/__tests__/ws-client-connect.e2e.test.ts +0 -381
- package/src/__tests__/ws-handshake.e2e.test.ts +0 -288
- package/src/auth/middleware.ts +0 -50
- package/src/auth/routes.ts +0 -57
- package/src/auth/tokens.ts +0 -41
- package/src/bootstrap.ts +0 -98
- package/src/clients/subscription-registry.ts +0 -137
- package/src/clients/ws-handler.ts +0 -196
- package/src/config.ts +0 -20
- package/src/docs/routes.ts +0 -70
- package/src/execute/errors.ts +0 -21
- package/src/execute/execution-registry.ts +0 -84
- package/src/execute/retry-executor.ts +0 -159
- package/src/execute/routes.ts +0 -239
- package/src/hosts/dispatcher.ts +0 -2
- package/src/hosts/registry.ts +0 -305
- package/src/hosts/ws-handler.ts +0 -445
- package/src/index.ts +0 -7
- package/src/llm/routes.ts +0 -343
- package/src/manifest.ts +0 -21
- package/src/observability/collector.ts +0 -346
- package/src/platform/routes.ts +0 -195
- package/src/server.ts +0 -447
- package/src/telemetry/routes.ts +0 -89
- package/src/ws/gateway-ws.ts +0 -73
- package/tsconfig.build.json +0 -15
- package/tsconfig.json +0 -10
- package/tsup.config.ts +0 -8
- 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
|
-
});
|