loki-mode 5.7.2 → 5.7.3
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/VERSION +1 -1
- package/api/README.md +297 -0
- package/api/client.ts +377 -0
- package/api/middleware/auth.ts +129 -0
- package/api/middleware/cors.ts +145 -0
- package/api/middleware/error.ts +226 -0
- package/api/mod.ts +58 -0
- package/api/openapi.yaml +614 -0
- package/api/routes/events.ts +165 -0
- package/api/routes/health.ts +169 -0
- package/api/routes/sessions.ts +262 -0
- package/api/routes/tasks.ts +182 -0
- package/api/server.js +637 -0
- package/api/server.ts +328 -0
- package/api/server_test.ts +265 -0
- package/api/services/cli-bridge.ts +503 -0
- package/api/services/event-bus.ts +189 -0
- package/api/services/state-watcher.ts +517 -0
- package/api/test.js +494 -0
- package/api/types/api.ts +122 -0
- package/api/types/events.ts +132 -0
- package/autonomy/loki +28 -2
- package/package.json +3 -2
package/api/server.ts
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loki Mode API Server
|
|
3
|
+
*
|
|
4
|
+
* HTTP/SSE server for loki-mode remote control and monitoring.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* deno run --allow-net --allow-read --allow-write --allow-env --allow-run api/server.ts
|
|
8
|
+
*
|
|
9
|
+
* Or via CLI:
|
|
10
|
+
* loki serve [--port 8420] [--host localhost]
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { stateWatcher } from "./services/state-watcher.ts";
|
|
14
|
+
import { corsMiddleware } from "./middleware/cors.ts";
|
|
15
|
+
import { authMiddleware } from "./middleware/auth.ts";
|
|
16
|
+
import { errorMiddleware } from "./middleware/error.ts";
|
|
17
|
+
import { LokiApiError, ErrorCodes } from "./middleware/error.ts";
|
|
18
|
+
|
|
19
|
+
// Import routes
|
|
20
|
+
import {
|
|
21
|
+
startSession,
|
|
22
|
+
listSessions,
|
|
23
|
+
getSession,
|
|
24
|
+
stopSession,
|
|
25
|
+
pauseSession,
|
|
26
|
+
resumeSession,
|
|
27
|
+
injectInput,
|
|
28
|
+
deleteSession,
|
|
29
|
+
} from "./routes/sessions.ts";
|
|
30
|
+
import {
|
|
31
|
+
listTasks,
|
|
32
|
+
getTask,
|
|
33
|
+
listAllTasks,
|
|
34
|
+
getActiveTasks,
|
|
35
|
+
getQueuedTasks,
|
|
36
|
+
} from "./routes/tasks.ts";
|
|
37
|
+
import {
|
|
38
|
+
streamEvents,
|
|
39
|
+
getEventHistory,
|
|
40
|
+
getEventStats,
|
|
41
|
+
} from "./routes/events.ts";
|
|
42
|
+
import {
|
|
43
|
+
healthCheck,
|
|
44
|
+
readinessCheck,
|
|
45
|
+
livenessCheck,
|
|
46
|
+
detailedStatus,
|
|
47
|
+
} from "./routes/health.ts";
|
|
48
|
+
|
|
49
|
+
// Server configuration
|
|
50
|
+
interface ServerConfig {
|
|
51
|
+
port: number;
|
|
52
|
+
host: string;
|
|
53
|
+
cors: boolean;
|
|
54
|
+
auth: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const defaultConfig: ServerConfig = {
|
|
58
|
+
port: parseInt(Deno.env.get("LOKI_API_PORT") || "8420", 10),
|
|
59
|
+
host: Deno.env.get("LOKI_API_HOST") || "localhost",
|
|
60
|
+
cors: true,
|
|
61
|
+
auth: true,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Route handler type
|
|
66
|
+
*/
|
|
67
|
+
type RouteHandler = (
|
|
68
|
+
req: Request,
|
|
69
|
+
...params: string[]
|
|
70
|
+
) => Promise<Response> | Response;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Route definition
|
|
74
|
+
*/
|
|
75
|
+
interface Route {
|
|
76
|
+
method: string;
|
|
77
|
+
pattern: RegExp;
|
|
78
|
+
handler: RouteHandler;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Define all routes
|
|
83
|
+
*/
|
|
84
|
+
const routes: Route[] = [
|
|
85
|
+
// Health endpoints (no auth required)
|
|
86
|
+
{ method: "GET", pattern: /^\/health$/, handler: healthCheck },
|
|
87
|
+
{ method: "GET", pattern: /^\/health\/ready$/, handler: readinessCheck },
|
|
88
|
+
{ method: "GET", pattern: /^\/health\/live$/, handler: livenessCheck },
|
|
89
|
+
|
|
90
|
+
// Status endpoint
|
|
91
|
+
{ method: "GET", pattern: /^\/api\/status$/, handler: detailedStatus },
|
|
92
|
+
|
|
93
|
+
// Session endpoints
|
|
94
|
+
{ method: "POST", pattern: /^\/api\/sessions$/, handler: startSession },
|
|
95
|
+
{ method: "GET", pattern: /^\/api\/sessions$/, handler: listSessions },
|
|
96
|
+
{
|
|
97
|
+
method: "GET",
|
|
98
|
+
pattern: /^\/api\/sessions\/([^/]+)$/,
|
|
99
|
+
handler: getSession,
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
method: "POST",
|
|
103
|
+
pattern: /^\/api\/sessions\/([^/]+)\/stop$/,
|
|
104
|
+
handler: stopSession,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
method: "POST",
|
|
108
|
+
pattern: /^\/api\/sessions\/([^/]+)\/pause$/,
|
|
109
|
+
handler: pauseSession,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
method: "POST",
|
|
113
|
+
pattern: /^\/api\/sessions\/([^/]+)\/resume$/,
|
|
114
|
+
handler: resumeSession,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
method: "POST",
|
|
118
|
+
pattern: /^\/api\/sessions\/([^/]+)\/input$/,
|
|
119
|
+
handler: injectInput,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
method: "DELETE",
|
|
123
|
+
pattern: /^\/api\/sessions\/([^/]+)$/,
|
|
124
|
+
handler: deleteSession,
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
// Task endpoints
|
|
128
|
+
{ method: "GET", pattern: /^\/api\/tasks$/, handler: listAllTasks },
|
|
129
|
+
{ method: "GET", pattern: /^\/api\/tasks\/active$/, handler: getActiveTasks },
|
|
130
|
+
{ method: "GET", pattern: /^\/api\/tasks\/queue$/, handler: getQueuedTasks },
|
|
131
|
+
{
|
|
132
|
+
method: "GET",
|
|
133
|
+
pattern: /^\/api\/sessions\/([^/]+)\/tasks$/,
|
|
134
|
+
handler: listTasks,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
method: "GET",
|
|
138
|
+
pattern: /^\/api\/sessions\/([^/]+)\/tasks\/([^/]+)$/,
|
|
139
|
+
handler: getTask,
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
// Event endpoints
|
|
143
|
+
{ method: "GET", pattern: /^\/api\/events$/, handler: streamEvents },
|
|
144
|
+
{
|
|
145
|
+
method: "GET",
|
|
146
|
+
pattern: /^\/api\/events\/history$/,
|
|
147
|
+
handler: getEventHistory,
|
|
148
|
+
},
|
|
149
|
+
{ method: "GET", pattern: /^\/api\/events\/stats$/, handler: getEventStats },
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Route a request to the appropriate handler
|
|
154
|
+
*/
|
|
155
|
+
async function routeRequest(req: Request): Promise<Response> {
|
|
156
|
+
const url = new URL(req.url);
|
|
157
|
+
const path = url.pathname;
|
|
158
|
+
const method = req.method;
|
|
159
|
+
|
|
160
|
+
// Find matching route
|
|
161
|
+
for (const route of routes) {
|
|
162
|
+
if (route.method !== method && method !== "OPTIONS") {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const match = path.match(route.pattern);
|
|
167
|
+
if (match) {
|
|
168
|
+
// Extract path parameters
|
|
169
|
+
const params = match.slice(1);
|
|
170
|
+
return route.handler(req, ...params);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// No route matched
|
|
175
|
+
throw new LokiApiError(
|
|
176
|
+
`No route found for ${method} ${path}`,
|
|
177
|
+
ErrorCodes.NOT_FOUND
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Create the main request handler with middleware
|
|
183
|
+
*/
|
|
184
|
+
function createHandler(config: ServerConfig): Deno.ServeHandler {
|
|
185
|
+
let handler: (req: Request) => Promise<Response> = routeRequest;
|
|
186
|
+
|
|
187
|
+
// Apply middleware in reverse order (innermost first)
|
|
188
|
+
handler = errorMiddleware(handler);
|
|
189
|
+
|
|
190
|
+
if (config.auth) {
|
|
191
|
+
// Skip auth for health endpoints
|
|
192
|
+
const authHandler = handler;
|
|
193
|
+
handler = async (req: Request) => {
|
|
194
|
+
const url = new URL(req.url);
|
|
195
|
+
if (url.pathname.startsWith("/health")) {
|
|
196
|
+
return authHandler(req);
|
|
197
|
+
}
|
|
198
|
+
return authMiddleware(authHandler)(req);
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (config.cors) {
|
|
203
|
+
handler = corsMiddleware(handler);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return handler;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Print startup banner
|
|
211
|
+
*/
|
|
212
|
+
function printBanner(config: ServerConfig): void {
|
|
213
|
+
console.log(`
|
|
214
|
+
╔═══════════════════════════════════════════════════════════════╗
|
|
215
|
+
║ LOKI MODE API SERVER ║
|
|
216
|
+
╠═══════════════════════════════════════════════════════════════╣
|
|
217
|
+
║ Version: ${Deno.env.get("LOKI_VERSION") || "dev".padEnd(50)}║
|
|
218
|
+
║ Host: ${config.host.padEnd(50)}║
|
|
219
|
+
║ Port: ${String(config.port).padEnd(50)}║
|
|
220
|
+
║ CORS: ${(config.cors ? "enabled" : "disabled").padEnd(50)}║
|
|
221
|
+
║ Auth: ${(config.auth ? "enabled (localhost bypass)" : "disabled").padEnd(50)}║
|
|
222
|
+
╠═══════════════════════════════════════════════════════════════╣
|
|
223
|
+
║ Endpoints: ║
|
|
224
|
+
║ GET /health - Health check ║
|
|
225
|
+
║ GET /api/status - Detailed status ║
|
|
226
|
+
║ GET /api/events - SSE event stream ║
|
|
227
|
+
║ POST /api/sessions - Start new session ║
|
|
228
|
+
║ GET /api/sessions - List sessions ║
|
|
229
|
+
║ GET /api/sessions/:id - Get session details ║
|
|
230
|
+
║ POST /api/sessions/:id/stop - Stop session ║
|
|
231
|
+
║ POST /api/sessions/:id/input - Inject input ║
|
|
232
|
+
║ GET /api/sessions/:id/tasks - List tasks ║
|
|
233
|
+
╚═══════════════════════════════════════════════════════════════╝
|
|
234
|
+
`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Parse command line arguments
|
|
239
|
+
*/
|
|
240
|
+
function parseArgs(): ServerConfig {
|
|
241
|
+
const config = { ...defaultConfig };
|
|
242
|
+
const args = Deno.args;
|
|
243
|
+
|
|
244
|
+
for (let i = 0; i < args.length; i++) {
|
|
245
|
+
switch (args[i]) {
|
|
246
|
+
case "--port":
|
|
247
|
+
case "-p":
|
|
248
|
+
config.port = parseInt(args[++i], 10);
|
|
249
|
+
break;
|
|
250
|
+
case "--host":
|
|
251
|
+
case "-h":
|
|
252
|
+
config.host = args[++i];
|
|
253
|
+
break;
|
|
254
|
+
case "--no-cors":
|
|
255
|
+
config.cors = false;
|
|
256
|
+
break;
|
|
257
|
+
case "--no-auth":
|
|
258
|
+
config.auth = false;
|
|
259
|
+
break;
|
|
260
|
+
case "--help":
|
|
261
|
+
console.log(`
|
|
262
|
+
Loki Mode API Server
|
|
263
|
+
|
|
264
|
+
Usage:
|
|
265
|
+
deno run --allow-all api/server.ts [options]
|
|
266
|
+
|
|
267
|
+
Options:
|
|
268
|
+
--port, -p <port> Port to listen on (default: 8420)
|
|
269
|
+
--host, -h <host> Host to bind to (default: localhost)
|
|
270
|
+
--no-cors Disable CORS
|
|
271
|
+
--no-auth Disable authentication
|
|
272
|
+
--help Show this help message
|
|
273
|
+
|
|
274
|
+
Environment Variables:
|
|
275
|
+
LOKI_API_PORT Port (overridden by --port)
|
|
276
|
+
LOKI_API_HOST Host (overridden by --host)
|
|
277
|
+
LOKI_API_TOKEN API token for remote access
|
|
278
|
+
LOKI_DIR Loki installation directory
|
|
279
|
+
LOKI_VERSION Version string
|
|
280
|
+
LOKI_DEBUG Enable debug output
|
|
281
|
+
`);
|
|
282
|
+
Deno.exit(0);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return config;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Start the server
|
|
291
|
+
*/
|
|
292
|
+
async function main(): Promise<void> {
|
|
293
|
+
const config = parseArgs();
|
|
294
|
+
|
|
295
|
+
// Start state watcher
|
|
296
|
+
await stateWatcher.start();
|
|
297
|
+
|
|
298
|
+
// Print banner
|
|
299
|
+
printBanner(config);
|
|
300
|
+
|
|
301
|
+
// Create handler
|
|
302
|
+
const handler = createHandler(config);
|
|
303
|
+
|
|
304
|
+
// Start server
|
|
305
|
+
console.log(`Server listening on http://${config.host}:${config.port}`);
|
|
306
|
+
|
|
307
|
+
await Deno.serve(
|
|
308
|
+
{
|
|
309
|
+
port: config.port,
|
|
310
|
+
hostname: config.host,
|
|
311
|
+
onListen: ({ hostname, port }) => {
|
|
312
|
+
console.log(`Ready to accept connections on ${hostname}:${port}`);
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
handler
|
|
316
|
+
).finished;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Run if this is the main module
|
|
320
|
+
if (import.meta.main) {
|
|
321
|
+
main().catch((err) => {
|
|
322
|
+
console.error("Server error:", err);
|
|
323
|
+
Deno.exit(1);
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Export for testing
|
|
328
|
+
export { createHandler, routeRequest, parseArgs };
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Server Tests
|
|
3
|
+
*
|
|
4
|
+
* Run with: deno test --allow-all api/server_test.ts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
assertEquals,
|
|
9
|
+
assertExists,
|
|
10
|
+
assertStringIncludes,
|
|
11
|
+
} from "https://deno.land/std@0.220.0/assert/mod.ts";
|
|
12
|
+
|
|
13
|
+
import { createHandler, routeRequest } from "./server.ts";
|
|
14
|
+
import { eventBus } from "./services/event-bus.ts";
|
|
15
|
+
|
|
16
|
+
// Test utilities
|
|
17
|
+
function createRequest(
|
|
18
|
+
method: string,
|
|
19
|
+
path: string,
|
|
20
|
+
body?: unknown,
|
|
21
|
+
headers: Record<string, string> = {}
|
|
22
|
+
): Request {
|
|
23
|
+
const url = `http://localhost:8420${path}`;
|
|
24
|
+
const init: RequestInit = {
|
|
25
|
+
method,
|
|
26
|
+
headers: {
|
|
27
|
+
"Content-Type": "application/json",
|
|
28
|
+
...headers,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
if (body) {
|
|
32
|
+
init.body = JSON.stringify(body);
|
|
33
|
+
}
|
|
34
|
+
return new Request(url, init);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ============================================================
|
|
38
|
+
// Health Endpoint Tests
|
|
39
|
+
// ============================================================
|
|
40
|
+
|
|
41
|
+
Deno.test("GET /health returns health status", async () => {
|
|
42
|
+
const handler = createHandler({ port: 8420, host: "localhost", cors: true, auth: false });
|
|
43
|
+
const req = createRequest("GET", "/health");
|
|
44
|
+
const res = await handler(req);
|
|
45
|
+
|
|
46
|
+
assertEquals(res.status, 200);
|
|
47
|
+
const data = await res.json();
|
|
48
|
+
assertExists(data.status);
|
|
49
|
+
assertExists(data.version);
|
|
50
|
+
assertExists(data.uptime);
|
|
51
|
+
assertExists(data.providers);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
Deno.test("GET /health/live returns alive status", async () => {
|
|
55
|
+
const handler = createHandler({ port: 8420, host: "localhost", cors: true, auth: false });
|
|
56
|
+
const req = createRequest("GET", "/health/live");
|
|
57
|
+
const res = await handler(req);
|
|
58
|
+
|
|
59
|
+
assertEquals(res.status, 200);
|
|
60
|
+
const data = await res.json();
|
|
61
|
+
assertEquals(data.alive, true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ============================================================
|
|
65
|
+
// Session Endpoint Tests
|
|
66
|
+
// ============================================================
|
|
67
|
+
|
|
68
|
+
Deno.test("GET /api/sessions returns empty list initially", async () => {
|
|
69
|
+
const handler = createHandler({ port: 8420, host: "localhost", cors: true, auth: false });
|
|
70
|
+
const req = createRequest("GET", "/api/sessions");
|
|
71
|
+
const res = await handler(req);
|
|
72
|
+
|
|
73
|
+
assertEquals(res.status, 200);
|
|
74
|
+
const data = await res.json();
|
|
75
|
+
assertExists(data.sessions);
|
|
76
|
+
assertEquals(Array.isArray(data.sessions), true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
Deno.test("POST /api/sessions validates provider", async () => {
|
|
80
|
+
const handler = createHandler({ port: 8420, host: "localhost", cors: true, auth: false });
|
|
81
|
+
const req = createRequest("POST", "/api/sessions", { provider: "invalid" });
|
|
82
|
+
const res = await handler(req);
|
|
83
|
+
|
|
84
|
+
assertEquals(res.status, 422);
|
|
85
|
+
const data = await res.json();
|
|
86
|
+
assertStringIncludes(data.error, "Invalid provider");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
Deno.test("GET /api/sessions/:id returns 404 for unknown session", async () => {
|
|
90
|
+
const handler = createHandler({ port: 8420, host: "localhost", cors: true, auth: false });
|
|
91
|
+
const req = createRequest("GET", "/api/sessions/unknown-id");
|
|
92
|
+
const res = await handler(req);
|
|
93
|
+
|
|
94
|
+
assertEquals(res.status, 404);
|
|
95
|
+
const data = await res.json();
|
|
96
|
+
assertEquals(data.code, "SESSION_NOT_FOUND");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ============================================================
|
|
100
|
+
// Task Endpoint Tests
|
|
101
|
+
// ============================================================
|
|
102
|
+
|
|
103
|
+
Deno.test("GET /api/tasks returns empty list initially", async () => {
|
|
104
|
+
const handler = createHandler({ port: 8420, host: "localhost", cors: true, auth: false });
|
|
105
|
+
const req = createRequest("GET", "/api/tasks");
|
|
106
|
+
const res = await handler(req);
|
|
107
|
+
|
|
108
|
+
assertEquals(res.status, 200);
|
|
109
|
+
const data = await res.json();
|
|
110
|
+
assertExists(data.tasks);
|
|
111
|
+
assertEquals(Array.isArray(data.tasks), true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
Deno.test("GET /api/tasks/active returns active tasks", async () => {
|
|
115
|
+
const handler = createHandler({ port: 8420, host: "localhost", cors: true, auth: false });
|
|
116
|
+
const req = createRequest("GET", "/api/tasks/active");
|
|
117
|
+
const res = await handler(req);
|
|
118
|
+
|
|
119
|
+
assertEquals(res.status, 200);
|
|
120
|
+
const data = await res.json();
|
|
121
|
+
assertExists(data.tasks);
|
|
122
|
+
assertExists(data.count);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ============================================================
|
|
126
|
+
// Event Endpoint Tests
|
|
127
|
+
// ============================================================
|
|
128
|
+
|
|
129
|
+
Deno.test("GET /api/events/stats returns event statistics", async () => {
|
|
130
|
+
const handler = createHandler({ port: 8420, host: "localhost", cors: true, auth: false });
|
|
131
|
+
const req = createRequest("GET", "/api/events/stats");
|
|
132
|
+
const res = await handler(req);
|
|
133
|
+
|
|
134
|
+
assertEquals(res.status, 200);
|
|
135
|
+
const data = await res.json();
|
|
136
|
+
assertExists(data.subscribers);
|
|
137
|
+
assertExists(data.historySize);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
Deno.test("GET /api/events/history returns event history", async () => {
|
|
141
|
+
const handler = createHandler({ port: 8420, host: "localhost", cors: true, auth: false });
|
|
142
|
+
const req = createRequest("GET", "/api/events/history");
|
|
143
|
+
const res = await handler(req);
|
|
144
|
+
|
|
145
|
+
assertEquals(res.status, 200);
|
|
146
|
+
const data = await res.json();
|
|
147
|
+
assertExists(data.events);
|
|
148
|
+
assertEquals(Array.isArray(data.events), true);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// ============================================================
|
|
152
|
+
// Event Bus Tests
|
|
153
|
+
// ============================================================
|
|
154
|
+
|
|
155
|
+
Deno.test("EventBus publishes and receives events", () => {
|
|
156
|
+
const received: unknown[] = [];
|
|
157
|
+
|
|
158
|
+
const subId = eventBus.subscribe({}, (event) => {
|
|
159
|
+
received.push(event);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
eventBus.publish("log:info", "test-session", { message: "test" });
|
|
163
|
+
|
|
164
|
+
assertEquals(received.length, 1);
|
|
165
|
+
assertEquals((received[0] as { type: string }).type, "log:info");
|
|
166
|
+
|
|
167
|
+
eventBus.unsubscribe(subId);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
Deno.test("EventBus filters by session ID", () => {
|
|
171
|
+
const received: unknown[] = [];
|
|
172
|
+
|
|
173
|
+
const subId = eventBus.subscribe({ sessionId: "session-1" }, (event) => {
|
|
174
|
+
received.push(event);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
eventBus.publish("log:info", "session-1", { message: "included" });
|
|
178
|
+
eventBus.publish("log:info", "session-2", { message: "excluded" });
|
|
179
|
+
|
|
180
|
+
assertEquals(received.length, 1);
|
|
181
|
+
assertEquals((received[0] as { sessionId: string }).sessionId, "session-1");
|
|
182
|
+
|
|
183
|
+
eventBus.unsubscribe(subId);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
Deno.test("EventBus filters by event type", () => {
|
|
187
|
+
const received: unknown[] = [];
|
|
188
|
+
|
|
189
|
+
const subId = eventBus.subscribe({ types: ["log:error"] }, (event) => {
|
|
190
|
+
received.push(event);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
eventBus.publish("log:info", "test", { message: "info" });
|
|
194
|
+
eventBus.publish("log:error", "test", { message: "error" });
|
|
195
|
+
eventBus.publish("log:warn", "test", { message: "warn" });
|
|
196
|
+
|
|
197
|
+
assertEquals(received.length, 1);
|
|
198
|
+
assertEquals((received[0] as { type: string }).type, "log:error");
|
|
199
|
+
|
|
200
|
+
eventBus.unsubscribe(subId);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// ============================================================
|
|
204
|
+
// CORS Tests
|
|
205
|
+
// ============================================================
|
|
206
|
+
|
|
207
|
+
Deno.test("OPTIONS request returns CORS headers", async () => {
|
|
208
|
+
const handler = createHandler({ port: 8420, host: "localhost", cors: true, auth: false });
|
|
209
|
+
const req = new Request("http://localhost:8420/api/sessions", {
|
|
210
|
+
method: "OPTIONS",
|
|
211
|
+
headers: {
|
|
212
|
+
Origin: "http://localhost:3000",
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
const res = await handler(req);
|
|
216
|
+
|
|
217
|
+
assertEquals(res.status, 204);
|
|
218
|
+
assertExists(res.headers.get("Access-Control-Allow-Origin"));
|
|
219
|
+
assertExists(res.headers.get("Access-Control-Allow-Methods"));
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// ============================================================
|
|
223
|
+
// Error Handling Tests
|
|
224
|
+
// ============================================================
|
|
225
|
+
|
|
226
|
+
Deno.test("Unknown route returns 404", async () => {
|
|
227
|
+
const handler = createHandler({ port: 8420, host: "localhost", cors: true, auth: false });
|
|
228
|
+
const req = createRequest("GET", "/api/unknown");
|
|
229
|
+
const res = await handler(req);
|
|
230
|
+
|
|
231
|
+
assertEquals(res.status, 404);
|
|
232
|
+
const data = await res.json();
|
|
233
|
+
assertEquals(data.code, "NOT_FOUND");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
Deno.test("Invalid JSON returns 400", async () => {
|
|
237
|
+
const handler = createHandler({ port: 8420, host: "localhost", cors: true, auth: false });
|
|
238
|
+
const req = new Request("http://localhost:8420/api/sessions", {
|
|
239
|
+
method: "POST",
|
|
240
|
+
headers: { "Content-Type": "application/json" },
|
|
241
|
+
body: "not valid json",
|
|
242
|
+
});
|
|
243
|
+
const res = await handler(req);
|
|
244
|
+
|
|
245
|
+
assertEquals(res.status, 400);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// ============================================================
|
|
249
|
+
// Status Endpoint Tests
|
|
250
|
+
// ============================================================
|
|
251
|
+
|
|
252
|
+
Deno.test("GET /api/status returns detailed status", async () => {
|
|
253
|
+
const handler = createHandler({ port: 8420, host: "localhost", cors: true, auth: false });
|
|
254
|
+
const req = createRequest("GET", "/api/status");
|
|
255
|
+
const res = await handler(req);
|
|
256
|
+
|
|
257
|
+
assertEquals(res.status, 200);
|
|
258
|
+
const data = await res.json();
|
|
259
|
+
assertExists(data.version);
|
|
260
|
+
assertExists(data.uptime);
|
|
261
|
+
assertExists(data.providers);
|
|
262
|
+
assertExists(data.sessions);
|
|
263
|
+
assertExists(data.events);
|
|
264
|
+
assertExists(data.system);
|
|
265
|
+
});
|