@ronkovic/aad 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.
- package/LICENSE +21 -0
- package/README.md +312 -0
- package/bin/aad.js +2 -0
- package/package.json +78 -0
- package/src/__tests__/e2e/pipeline-e2e.test.ts +279 -0
- package/src/__tests__/e2e/resume-e2e.test.ts +200 -0
- package/src/__tests__/integration/cli-smoke.test.ts +175 -0
- package/src/__tests__/integration/pipeline.test.ts +346 -0
- package/src/bun-imports.d.ts +14 -0
- package/src/main.ts +52 -0
- package/src/modules/claude-provider/__tests__/claude-cli.adapter.test.ts +277 -0
- package/src/modules/claude-provider/__tests__/claude-sdk-real-env.test.ts +127 -0
- package/src/modules/claude-provider/__tests__/claude-sdk.adapter.test.ts +347 -0
- package/src/modules/claude-provider/__tests__/effort-strategy.test.ts +212 -0
- package/src/modules/claude-provider/__tests__/provider-registry.test.ts +251 -0
- package/src/modules/claude-provider/__tests__/retry.test.ts +201 -0
- package/src/modules/claude-provider/claude-cli.adapter.ts +156 -0
- package/src/modules/claude-provider/claude-provider.port.ts +35 -0
- package/src/modules/claude-provider/claude-sdk.adapter.ts +217 -0
- package/src/modules/claude-provider/effort-strategy.ts +94 -0
- package/src/modules/claude-provider/index.ts +32 -0
- package/src/modules/claude-provider/provider-registry.ts +92 -0
- package/src/modules/claude-provider/retry.ts +81 -0
- package/src/modules/cli/__tests__/app.test.ts +160 -0
- package/src/modules/cli/__tests__/cleanup.test.ts +111 -0
- package/src/modules/cli/__tests__/commands.test.ts +186 -0
- package/src/modules/cli/__tests__/output.test.ts +329 -0
- package/src/modules/cli/__tests__/resume.test.ts +324 -0
- package/src/modules/cli/__tests__/run.test.ts +168 -0
- package/src/modules/cli/__tests__/shutdown.test.ts +168 -0
- package/src/modules/cli/__tests__/status.test.ts +144 -0
- package/src/modules/cli/app.ts +241 -0
- package/src/modules/cli/commands/cleanup.ts +120 -0
- package/src/modules/cli/commands/resume.ts +156 -0
- package/src/modules/cli/commands/run.ts +322 -0
- package/src/modules/cli/commands/status.ts +101 -0
- package/src/modules/cli/index.ts +29 -0
- package/src/modules/cli/output.ts +256 -0
- package/src/modules/cli/shutdown.ts +122 -0
- package/src/modules/dashboard/__tests__/api-routes.test.ts +204 -0
- package/src/modules/dashboard/__tests__/file-watcher.test.ts +34 -0
- package/src/modules/dashboard/__tests__/server.test.ts +120 -0
- package/src/modules/dashboard/__tests__/sse-broadcaster.test.ts +163 -0
- package/src/modules/dashboard/__tests__/sse-routes.test.ts +58 -0
- package/src/modules/dashboard/__tests__/state-aggregator.test.ts +330 -0
- package/src/modules/dashboard/index.ts +8 -0
- package/src/modules/dashboard/routes/api.ts +84 -0
- package/src/modules/dashboard/routes/sse.ts +37 -0
- package/src/modules/dashboard/server.ts +111 -0
- package/src/modules/dashboard/services/file-watcher.ts +36 -0
- package/src/modules/dashboard/services/sse-broadcaster.ts +81 -0
- package/src/modules/dashboard/services/state-aggregator.ts +132 -0
- package/src/modules/dashboard/ui/dashboard.html +405 -0
- package/src/modules/git-workspace/__tests__/branch-manager.test.ts +335 -0
- package/src/modules/git-workspace/__tests__/git-exec.test.ts +91 -0
- package/src/modules/git-workspace/__tests__/memory-sync.test.ts +273 -0
- package/src/modules/git-workspace/__tests__/merge-service.test.ts +286 -0
- package/src/modules/git-workspace/__tests__/settings-merge.test.ts +163 -0
- package/src/modules/git-workspace/__tests__/worktree-manager.test.ts +247 -0
- package/src/modules/git-workspace/branch-manager.ts +191 -0
- package/src/modules/git-workspace/git-exec.ts +124 -0
- package/src/modules/git-workspace/index.ts +17 -0
- package/src/modules/git-workspace/memory-sync.ts +89 -0
- package/src/modules/git-workspace/merge-service.ts +156 -0
- package/src/modules/git-workspace/settings-merge.ts +95 -0
- package/src/modules/git-workspace/worktree-manager.ts +199 -0
- package/src/modules/logging/__tests__/log-store.test.ts +242 -0
- package/src/modules/logging/__tests__/logger.test.ts +81 -0
- package/src/modules/logging/__tests__/sse-transport.test.ts +93 -0
- package/src/modules/logging/index.ts +7 -0
- package/src/modules/logging/log-store.ts +80 -0
- package/src/modules/logging/logger.ts +55 -0
- package/src/modules/logging/transports/sse-transport.ts +28 -0
- package/src/modules/multi-repo/__tests__/multi-repo-planner.test.ts +93 -0
- package/src/modules/multi-repo/__tests__/repo-context.test.ts +79 -0
- package/src/modules/multi-repo/index.ts +12 -0
- package/src/modules/multi-repo/multi-repo-planner.ts +112 -0
- package/src/modules/multi-repo/repo-context.ts +71 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/progress.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/completed/task-getall-2.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-1.json +13 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-getall-1.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-status-change.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-1.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-2.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/progress.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/completed/task-getall-2.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-1.json +13 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-getall-1.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-status-change.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-1.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-2.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/progress.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/completed/task-getall-2.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-1.json +13 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-getall-1.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-status-change.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-1.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-2.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/progress.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/completed/task-getall-2.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-1.json +13 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-getall-1.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-status-change.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-1.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-2.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/progress.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/completed/task-getall-2.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-1.json +13 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-getall-1.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-status-change.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-1.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-2.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/progress.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/completed/task-getall-2.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-1.json +13 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-getall-1.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-status-change.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-1.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-2.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/progress.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/completed/task-getall-2.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-1.json +13 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-getall-1.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-status-change.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-1.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-2.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/progress.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/completed/task-getall-2.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-1.json +13 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-getall-1.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-status-change.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-1.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-2.json +5 -0
- package/src/modules/persistence/__tests__/file-lock.test.ts +141 -0
- package/src/modules/persistence/__tests__/index.test.ts +38 -0
- package/src/modules/persistence/__tests__/stores.test.ts +594 -0
- package/src/modules/persistence/file-lock.ts +158 -0
- package/src/modules/persistence/fs-run-store.ts +73 -0
- package/src/modules/persistence/fs-task-store.ts +152 -0
- package/src/modules/persistence/fs-worker-store.ts +116 -0
- package/src/modules/persistence/in-memory-stores.ts +98 -0
- package/src/modules/persistence/index.ts +60 -0
- package/src/modules/persistence/stores.port.ts +60 -0
- package/src/modules/planning/__tests__/file-conflict-validator.test.ts +256 -0
- package/src/modules/planning/__tests__/planning-service.test.ts +366 -0
- package/src/modules/planning/__tests__/project-detection.test.ts +707 -0
- package/src/modules/planning/file-conflict-validator.ts +135 -0
- package/src/modules/planning/index.ts +40 -0
- package/src/modules/planning/planning.service.ts +262 -0
- package/src/modules/planning/project-detection.ts +525 -0
- package/src/modules/plugin/__tests__/plugin-loader.test.ts +83 -0
- package/src/modules/plugin/__tests__/plugin-manager.test.ts +187 -0
- package/src/modules/plugin/index.ts +3 -0
- package/src/modules/plugin/plugin-loader.ts +46 -0
- package/src/modules/plugin/plugin-manager.ts +90 -0
- package/src/modules/plugin/plugin.types.ts +37 -0
- package/src/modules/process-manager/__tests__/process-manager.test.ts +210 -0
- package/src/modules/process-manager/__tests__/worker.test.ts +89 -0
- package/src/modules/process-manager/index.ts +5 -0
- package/src/modules/process-manager/process-manager.ts +193 -0
- package/src/modules/process-manager/worker.ts +106 -0
- package/src/modules/task-execution/__tests__/default-spawner.test.ts +154 -0
- package/src/modules/task-execution/__tests__/executor.test.ts +760 -0
- package/src/modules/task-execution/__tests__/implementer-green.test.ts +286 -0
- package/src/modules/task-execution/__tests__/merge-phase.test.ts +368 -0
- package/src/modules/task-execution/__tests__/reviewer.test.ts +302 -0
- package/src/modules/task-execution/__tests__/tester-red.test.ts +281 -0
- package/src/modules/task-execution/__tests__/tester-verify.test.ts +313 -0
- package/src/modules/task-execution/executor.ts +303 -0
- package/src/modules/task-execution/index.ts +45 -0
- package/src/modules/task-execution/phases/default-spawner.ts +49 -0
- package/src/modules/task-execution/phases/implementer-green.ts +100 -0
- package/src/modules/task-execution/phases/merge.ts +122 -0
- package/src/modules/task-execution/phases/reviewer.ts +160 -0
- package/src/modules/task-execution/phases/tester-red.ts +100 -0
- package/src/modules/task-execution/phases/tester-verify.ts +120 -0
- package/src/modules/task-queue/__tests__/dependency-resolver.test.ts +456 -0
- package/src/modules/task-queue/__tests__/dispatcher.test.ts +824 -0
- package/src/modules/task-queue/__tests__/task-plan.test.ts +122 -0
- package/src/modules/task-queue/__tests__/task.test.ts +130 -0
- package/src/modules/task-queue/dependency-resolver.ts +171 -0
- package/src/modules/task-queue/dispatcher.ts +372 -0
- package/src/modules/task-queue/index.ts +16 -0
- package/src/modules/task-queue/task-plan.ts +40 -0
- package/src/modules/task-queue/task.ts +67 -0
- package/src/shared/__tests__/config.test.ts +204 -0
- package/src/shared/__tests__/errors.test.ts +285 -0
- package/src/shared/__tests__/events.test.ts +496 -0
- package/src/shared/__tests__/types.test.ts +360 -0
- package/src/shared/config.ts +133 -0
- package/src/shared/errors.ts +128 -0
- package/src/shared/events.ts +171 -0
- package/src/shared/types.ts +143 -0
- package/tsconfig.json +30 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shutdown Manager
|
|
3
|
+
* LIFO順でshutdownハンドラを実行し、二重shutdown防止、シグナルハンドリングを提供
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Logger } from "pino";
|
|
7
|
+
|
|
8
|
+
export type ShutdownHandler = () => Promise<void>;
|
|
9
|
+
|
|
10
|
+
export interface ShutdownManager {
|
|
11
|
+
/**
|
|
12
|
+
* shutdownハンドラを登録 (LIFO順で実行される)
|
|
13
|
+
* @param name ハンドラの識別名
|
|
14
|
+
* @param handler 非同期shutdown処理
|
|
15
|
+
*/
|
|
16
|
+
register(name: string, handler: ShutdownHandler): void;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 登録された全ハンドラをLIFO順で実行
|
|
20
|
+
* 二重呼び出しは無視される
|
|
21
|
+
*/
|
|
22
|
+
onShutdown(): Promise<void>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* SIGINT/SIGTERMシグナルハンドラをインストール
|
|
26
|
+
* シグナル受信時にonShutdown()を呼び出す
|
|
27
|
+
*/
|
|
28
|
+
installSignalHandlers(): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface HandlerEntry {
|
|
32
|
+
name: string;
|
|
33
|
+
handler: ShutdownHandler;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class ShutdownManagerImpl implements ShutdownManager {
|
|
37
|
+
private handlers: HandlerEntry[] = [];
|
|
38
|
+
private isShuttingDown = false;
|
|
39
|
+
private hasShutdown = false;
|
|
40
|
+
|
|
41
|
+
constructor(private logger: Logger) {}
|
|
42
|
+
|
|
43
|
+
register(name: string, handler: ShutdownHandler): void {
|
|
44
|
+
if (this.hasShutdown) {
|
|
45
|
+
this.logger.warn({ name }, "Cannot register handler after shutdown");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.handlers.push({ name, handler });
|
|
50
|
+
this.logger.debug({ name, total: this.handlers.length }, "Shutdown handler registered");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async onShutdown(): Promise<void> {
|
|
54
|
+
// 二重shutdown防止
|
|
55
|
+
if (this.isShuttingDown || this.hasShutdown) {
|
|
56
|
+
this.logger.debug("Shutdown already in progress or completed, ignoring");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.isShuttingDown = true;
|
|
61
|
+
this.logger.info({ handlerCount: this.handlers.length }, "Starting shutdown sequence");
|
|
62
|
+
|
|
63
|
+
const errors: Array<{ name: string; error: unknown }> = [];
|
|
64
|
+
|
|
65
|
+
// LIFO順で実行 (最後に登録されたものから実行)
|
|
66
|
+
for (let i = this.handlers.length - 1; i >= 0; i--) {
|
|
67
|
+
const entry = this.handlers[i];
|
|
68
|
+
if (!entry) continue;
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
this.logger.info({ name: entry.name }, `Executing shutdown handler: ${entry.name}`);
|
|
72
|
+
await entry.handler();
|
|
73
|
+
this.logger.debug({ name: entry.name }, `Shutdown handler completed: ${entry.name}`);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
this.logger.error({ name: entry.name, error }, `Shutdown handler failed: ${entry.name}`);
|
|
76
|
+
errors.push({ name: entry.name, error });
|
|
77
|
+
// エラーが発生しても残りのハンドラを実行
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.hasShutdown = true;
|
|
82
|
+
this.isShuttingDown = false;
|
|
83
|
+
|
|
84
|
+
if (errors.length > 0) {
|
|
85
|
+
this.logger.error({ errors, total: errors.length }, "Shutdown completed with errors");
|
|
86
|
+
// 最初のエラーをthrow
|
|
87
|
+
const firstError = errors[0];
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Shutdown handler "${firstError!.name}" failed: ${
|
|
90
|
+
firstError!.error instanceof Error ? firstError!.error.message : String(firstError!.error)
|
|
91
|
+
}`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.logger.info("Shutdown sequence completed successfully");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
installSignalHandlers(): void {
|
|
99
|
+
const handleSignal = (signal: string): void => {
|
|
100
|
+
this.logger.info({ signal }, `Received ${signal} signal`);
|
|
101
|
+
void this.onShutdown().then(() => {
|
|
102
|
+
this.logger.info({ signal }, `Shutdown complete, exiting with code 0`);
|
|
103
|
+
process.exit(0);
|
|
104
|
+
}).catch((error) => {
|
|
105
|
+
this.logger.error({ signal, error }, `Shutdown failed, exiting with code 1`);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
process.on("SIGINT", () => handleSignal("SIGINT"));
|
|
111
|
+
process.on("SIGTERM", () => handleSignal("SIGTERM"));
|
|
112
|
+
|
|
113
|
+
this.logger.debug("Signal handlers installed (SIGINT, SIGTERM)");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* ShutdownManagerインスタンスを作成
|
|
119
|
+
*/
|
|
120
|
+
export function createShutdownManager(logger: Logger): ShutdownManager {
|
|
121
|
+
return new ShutdownManagerImpl(logger);
|
|
122
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { describe, test, expect, mock } from "bun:test";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { createAPIRoutes } from "../routes/api";
|
|
4
|
+
import type { StateAggregator } from "../services/state-aggregator";
|
|
5
|
+
import { createTaskId } from "../../../shared/types";
|
|
6
|
+
|
|
7
|
+
// Mock StateAggregator
|
|
8
|
+
function createMockAggregator(): StateAggregator {
|
|
9
|
+
return {
|
|
10
|
+
getProgress: mock(() => ({
|
|
11
|
+
progress: {
|
|
12
|
+
total: 10,
|
|
13
|
+
pending: 2,
|
|
14
|
+
running: 3,
|
|
15
|
+
completed: 4,
|
|
16
|
+
failed: 1,
|
|
17
|
+
},
|
|
18
|
+
percentComplete: 40,
|
|
19
|
+
})),
|
|
20
|
+
getTasks: mock(() => ({
|
|
21
|
+
tasks: [
|
|
22
|
+
{
|
|
23
|
+
id: createTaskId("task-1"),
|
|
24
|
+
status: "completed" as const,
|
|
25
|
+
dependsOn: [],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: createTaskId("task-2"),
|
|
29
|
+
status: "running" as const,
|
|
30
|
+
dependsOn: [createTaskId("task-1")],
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
total: 2,
|
|
34
|
+
})),
|
|
35
|
+
getTaskLogs: mock((taskId) => [
|
|
36
|
+
{
|
|
37
|
+
level: "info",
|
|
38
|
+
service: "task-execution",
|
|
39
|
+
message: `Log for ${taskId}`,
|
|
40
|
+
timestamp: Date.now(),
|
|
41
|
+
taskId: taskId as string,
|
|
42
|
+
},
|
|
43
|
+
]),
|
|
44
|
+
getWorkers: mock(() => ({
|
|
45
|
+
stats: {
|
|
46
|
+
total: 4,
|
|
47
|
+
idle: 1,
|
|
48
|
+
busy: 3,
|
|
49
|
+
},
|
|
50
|
+
workers: [
|
|
51
|
+
{ id: "worker-1" as any, status: "busy" as const },
|
|
52
|
+
{ id: "worker-2" as any, status: "idle" as const },
|
|
53
|
+
],
|
|
54
|
+
})),
|
|
55
|
+
getGraph: mock(() => ({
|
|
56
|
+
nodes: [
|
|
57
|
+
{ id: "task-1", status: "completed" },
|
|
58
|
+
{ id: "task-2", status: "running" },
|
|
59
|
+
],
|
|
60
|
+
edges: [{ from: "task-1", to: "task-2" }],
|
|
61
|
+
})),
|
|
62
|
+
getTimeline: mock(() => ({
|
|
63
|
+
tasks: [
|
|
64
|
+
{
|
|
65
|
+
id: "task-1",
|
|
66
|
+
status: "completed",
|
|
67
|
+
startTime: Date.now() - 10000,
|
|
68
|
+
endTime: Date.now() - 5000,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
})),
|
|
72
|
+
queryLogs: mock(() => [
|
|
73
|
+
{
|
|
74
|
+
level: "error",
|
|
75
|
+
service: "planning",
|
|
76
|
+
message: "Test error",
|
|
77
|
+
timestamp: Date.now(),
|
|
78
|
+
},
|
|
79
|
+
]),
|
|
80
|
+
} as any;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
describe("API Routes", () => {
|
|
84
|
+
test("GET /api/progress returns progress data", async () => {
|
|
85
|
+
const aggregator = createMockAggregator();
|
|
86
|
+
const app = new Hono();
|
|
87
|
+
app.route("/", createAPIRoutes(aggregator));
|
|
88
|
+
|
|
89
|
+
const res = await app.request("/api/progress");
|
|
90
|
+
expect(res.status).toBe(200);
|
|
91
|
+
|
|
92
|
+
const data = (await res.json()) as any;
|
|
93
|
+
expect(data.progress.total).toBe(10);
|
|
94
|
+
expect(data.percentComplete).toBe(40);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("GET /api/tasks returns tasks array", async () => {
|
|
98
|
+
const aggregator = createMockAggregator();
|
|
99
|
+
const app = new Hono();
|
|
100
|
+
app.route("/", createAPIRoutes(aggregator));
|
|
101
|
+
|
|
102
|
+
const res = await app.request("/api/tasks");
|
|
103
|
+
expect(res.status).toBe(200);
|
|
104
|
+
|
|
105
|
+
const data = (await res.json()) as any;
|
|
106
|
+
expect(data.tasks).toHaveLength(2);
|
|
107
|
+
expect(data.total).toBe(2);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("GET /api/tasks/:id/logs returns task-specific logs", async () => {
|
|
111
|
+
const aggregator = createMockAggregator();
|
|
112
|
+
const app = new Hono();
|
|
113
|
+
app.route("/", createAPIRoutes(aggregator));
|
|
114
|
+
|
|
115
|
+
const res = await app.request("/api/tasks/task-1/logs");
|
|
116
|
+
expect(res.status).toBe(200);
|
|
117
|
+
|
|
118
|
+
const data = (await res.json()) as any;
|
|
119
|
+
expect(Array.isArray(data)).toBe(true);
|
|
120
|
+
expect(data[0].message).toContain("task-1");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("GET /api/workers returns worker stats and list", async () => {
|
|
124
|
+
const aggregator = createMockAggregator();
|
|
125
|
+
const app = new Hono();
|
|
126
|
+
app.route("/", createAPIRoutes(aggregator));
|
|
127
|
+
|
|
128
|
+
const res = await app.request("/api/workers");
|
|
129
|
+
expect(res.status).toBe(200);
|
|
130
|
+
|
|
131
|
+
const data = (await res.json()) as any;
|
|
132
|
+
expect(data.stats.total).toBe(4);
|
|
133
|
+
expect(data.workers).toHaveLength(2);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("GET /api/graph returns dependency graph", async () => {
|
|
137
|
+
const aggregator = createMockAggregator();
|
|
138
|
+
const app = new Hono();
|
|
139
|
+
app.route("/", createAPIRoutes(aggregator));
|
|
140
|
+
|
|
141
|
+
const res = await app.request("/api/graph");
|
|
142
|
+
expect(res.status).toBe(200);
|
|
143
|
+
|
|
144
|
+
const data = (await res.json()) as any;
|
|
145
|
+
expect(data.nodes).toHaveLength(2);
|
|
146
|
+
expect(data.edges).toHaveLength(1);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("GET /api/timeline returns task timeline", async () => {
|
|
150
|
+
const aggregator = createMockAggregator();
|
|
151
|
+
const app = new Hono();
|
|
152
|
+
app.route("/", createAPIRoutes(aggregator));
|
|
153
|
+
|
|
154
|
+
const res = await app.request("/api/timeline");
|
|
155
|
+
expect(res.status).toBe(200);
|
|
156
|
+
|
|
157
|
+
const data = (await res.json()) as any;
|
|
158
|
+
expect(data.tasks).toHaveLength(1);
|
|
159
|
+
expect(data.tasks[0].status).toBe("completed");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("GET /api/logs returns filtered logs", async () => {
|
|
163
|
+
const aggregator = createMockAggregator();
|
|
164
|
+
const app = new Hono();
|
|
165
|
+
app.route("/", createAPIRoutes(aggregator));
|
|
166
|
+
|
|
167
|
+
const res = await app.request("/api/logs");
|
|
168
|
+
expect(res.status).toBe(200);
|
|
169
|
+
|
|
170
|
+
const data = (await res.json()) as any;
|
|
171
|
+
expect(Array.isArray(data)).toBe(true);
|
|
172
|
+
expect(data[0].level).toBe("error");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("GET /api/logs with query params applies filters", async () => {
|
|
176
|
+
const aggregator = createMockAggregator();
|
|
177
|
+
const app = new Hono();
|
|
178
|
+
app.route("/", createAPIRoutes(aggregator));
|
|
179
|
+
|
|
180
|
+
const res = await app.request("/api/logs?level=error&limit=50");
|
|
181
|
+
expect(res.status).toBe(200);
|
|
182
|
+
|
|
183
|
+
// Verify queryLogs was called
|
|
184
|
+
expect(aggregator.queryLogs).toHaveBeenCalled();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("GET /api/logs validates query parameters", async () => {
|
|
188
|
+
const aggregator = createMockAggregator();
|
|
189
|
+
const app = new Hono();
|
|
190
|
+
app.route("/", createAPIRoutes(aggregator));
|
|
191
|
+
|
|
192
|
+
// Valid limit
|
|
193
|
+
const res1 = await app.request("/api/logs?limit=100");
|
|
194
|
+
expect(res1.status).toBe(200);
|
|
195
|
+
|
|
196
|
+
// Invalid limit (negative)
|
|
197
|
+
const res2 = await app.request("/api/logs?limit=-10");
|
|
198
|
+
expect(res2.status).toBe(400);
|
|
199
|
+
|
|
200
|
+
// Invalid limit (non-numeric)
|
|
201
|
+
const res3 = await app.request("/api/logs?limit=abc");
|
|
202
|
+
expect(res3.status).toBe(400);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { FileWatcher } from "../services/file-watcher";
|
|
3
|
+
import { EventBus } from "../../../shared/events";
|
|
4
|
+
|
|
5
|
+
describe("FileWatcher", () => {
|
|
6
|
+
test("constructor does not throw", () => {
|
|
7
|
+
const eventBus = new EventBus();
|
|
8
|
+
const watcher = new FileWatcher(eventBus, "/tmp/test-watch");
|
|
9
|
+
|
|
10
|
+
expect(watcher).toBeDefined();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("start initializes watcher", () => {
|
|
14
|
+
const eventBus = new EventBus();
|
|
15
|
+
const watcher = new FileWatcher(eventBus, "/tmp/test-watch");
|
|
16
|
+
|
|
17
|
+
expect(() => watcher.start()).not.toThrow();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("stop closes watcher", () => {
|
|
21
|
+
const eventBus = new EventBus();
|
|
22
|
+
const watcher = new FileWatcher(eventBus, "/tmp/test-watch");
|
|
23
|
+
|
|
24
|
+
watcher.start();
|
|
25
|
+
expect(() => watcher.stop()).not.toThrow();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("stop without start is safe", () => {
|
|
29
|
+
const eventBus = new EventBus();
|
|
30
|
+
const watcher = new FileWatcher(eventBus, "/tmp/test-watch");
|
|
31
|
+
|
|
32
|
+
expect(() => watcher.stop()).not.toThrow();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { DashboardServer } from "../server";
|
|
3
|
+
import { EventBus } from "../../../shared/events";
|
|
4
|
+
import { LogStore } from "../../logging/log-store";
|
|
5
|
+
|
|
6
|
+
// Mock dependencies
|
|
7
|
+
class MockTaskStore {
|
|
8
|
+
getAll() {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
get() {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class MockProcessManager {
|
|
17
|
+
getStats() {
|
|
18
|
+
return { total: 0, idle: 0, busy: 0 };
|
|
19
|
+
}
|
|
20
|
+
getWorkers() {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("DashboardServer", () => {
|
|
26
|
+
let server: DashboardServer;
|
|
27
|
+
let eventBus: EventBus;
|
|
28
|
+
let logStore: LogStore;
|
|
29
|
+
let taskStore: MockTaskStore;
|
|
30
|
+
let processManager: MockProcessManager;
|
|
31
|
+
const testPort = 13333 + Math.floor(Math.random() * 1000); // Random port to avoid conflicts
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
eventBus = new EventBus();
|
|
35
|
+
logStore = new LogStore();
|
|
36
|
+
taskStore = new MockTaskStore();
|
|
37
|
+
processManager = new MockProcessManager();
|
|
38
|
+
|
|
39
|
+
server = new DashboardServer({
|
|
40
|
+
eventBus,
|
|
41
|
+
logStore,
|
|
42
|
+
taskStore: taskStore as any,
|
|
43
|
+
processManager: processManager as any,
|
|
44
|
+
port: testPort,
|
|
45
|
+
host: "localhost",
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(async () => {
|
|
50
|
+
await server.stop();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("constructor does not throw", () => {
|
|
54
|
+
expect(server).toBeDefined();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("start initializes HTTP server", async () => {
|
|
58
|
+
await server.start();
|
|
59
|
+
|
|
60
|
+
// Verify server is listening by making a request
|
|
61
|
+
const res = await fetch(`http://localhost:${testPort}/api/progress`);
|
|
62
|
+
expect(res.ok).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("GET / returns HTML page", async () => {
|
|
66
|
+
await server.start();
|
|
67
|
+
|
|
68
|
+
const res = await fetch(`http://localhost:${testPort}/`);
|
|
69
|
+
expect(res.status).toBe(200);
|
|
70
|
+
expect(res.headers.get("Content-Type")).toContain("text/html");
|
|
71
|
+
|
|
72
|
+
const html = await res.text();
|
|
73
|
+
expect(html).toContain("<html");
|
|
74
|
+
expect(html).toContain("AAD Dashboard");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("GET /api/progress returns JSON", async () => {
|
|
78
|
+
await server.start();
|
|
79
|
+
|
|
80
|
+
const res = await fetch(`http://localhost:${testPort}/api/progress`);
|
|
81
|
+
expect(res.status).toBe(200);
|
|
82
|
+
expect(res.headers.get("Content-Type")).toContain("application/json");
|
|
83
|
+
|
|
84
|
+
const data = (await res.json()) as any;
|
|
85
|
+
expect(data.progress).toBeDefined();
|
|
86
|
+
expect(data.percentComplete).toBeDefined();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("stop closes HTTP server", async () => {
|
|
90
|
+
await server.start();
|
|
91
|
+
|
|
92
|
+
// Server is listening
|
|
93
|
+
const res1 = await fetch(`http://localhost:${testPort}/api/progress`);
|
|
94
|
+
expect(res1.ok).toBe(true);
|
|
95
|
+
|
|
96
|
+
await server.stop();
|
|
97
|
+
|
|
98
|
+
// Server should no longer respond
|
|
99
|
+
try {
|
|
100
|
+
await fetch(`http://localhost:${testPort}/api/progress`);
|
|
101
|
+
expect.unreachable("Server should be stopped");
|
|
102
|
+
} catch (error) {
|
|
103
|
+
// Expected: connection refused or similar error
|
|
104
|
+
expect(error).toBeDefined();
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("multiple start calls are safe", async () => {
|
|
109
|
+
await server.start();
|
|
110
|
+
await server.start(); // Should not throw
|
|
111
|
+
|
|
112
|
+
const res = await fetch(`http://localhost:${testPort}/api/progress`);
|
|
113
|
+
expect(res.ok).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("stop without start is safe", async () => {
|
|
117
|
+
await server.stop(); // Should not throw
|
|
118
|
+
expect(true).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { describe, test, expect, mock, beforeEach } from "bun:test";
|
|
2
|
+
import { SSEBroadcaster } from "../services/sse-broadcaster";
|
|
3
|
+
import { EventBus, type AADEvent } from "../../../shared/events";
|
|
4
|
+
|
|
5
|
+
describe("SSEBroadcaster", () => {
|
|
6
|
+
let eventBus: EventBus;
|
|
7
|
+
let broadcaster: SSEBroadcaster;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
eventBus = new EventBus();
|
|
11
|
+
broadcaster = new SSEBroadcaster(eventBus);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("start subscribes to EventBus wildcard", () => {
|
|
15
|
+
const onSpy = mock(() => {});
|
|
16
|
+
eventBus.on = onSpy;
|
|
17
|
+
|
|
18
|
+
broadcaster.start();
|
|
19
|
+
|
|
20
|
+
expect(onSpy).toHaveBeenCalledWith("*", expect.any(Function));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("addClient increases client count", () => {
|
|
24
|
+
const mockClient = {
|
|
25
|
+
send: mock(() => {}),
|
|
26
|
+
close: mock(() => {}),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
broadcaster.addClient(mockClient);
|
|
30
|
+
expect(broadcaster.getClientCount()).toBe(1);
|
|
31
|
+
|
|
32
|
+
broadcaster.addClient({
|
|
33
|
+
send: mock(() => {}),
|
|
34
|
+
close: mock(() => {}),
|
|
35
|
+
});
|
|
36
|
+
expect(broadcaster.getClientCount()).toBe(2);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("removeClient decreases client count", () => {
|
|
40
|
+
const mockClient = {
|
|
41
|
+
send: mock(() => {}),
|
|
42
|
+
close: mock(() => {}),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
broadcaster.addClient(mockClient);
|
|
46
|
+
expect(broadcaster.getClientCount()).toBe(1);
|
|
47
|
+
|
|
48
|
+
broadcaster.removeClient(mockClient);
|
|
49
|
+
expect(broadcaster.getClientCount()).toBe(0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("EventBus emit triggers send on all clients", () => {
|
|
53
|
+
const client1 = {
|
|
54
|
+
send: mock(() => {}),
|
|
55
|
+
close: mock(() => {}),
|
|
56
|
+
};
|
|
57
|
+
const client2 = {
|
|
58
|
+
send: mock(() => {}),
|
|
59
|
+
close: mock(() => {}),
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
broadcaster.start();
|
|
63
|
+
broadcaster.addClient(client1);
|
|
64
|
+
broadcaster.addClient(client2);
|
|
65
|
+
|
|
66
|
+
const event: AADEvent = {
|
|
67
|
+
type: "task:dispatched",
|
|
68
|
+
taskId: "task-1" as any,
|
|
69
|
+
workerId: "worker-1" as any,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
eventBus.emit(event);
|
|
73
|
+
|
|
74
|
+
expect(client1.send).toHaveBeenCalledTimes(1);
|
|
75
|
+
expect(client1.send).toHaveBeenCalledWith(expect.stringContaining("task:dispatched"));
|
|
76
|
+
expect(client2.send).toHaveBeenCalledTimes(1);
|
|
77
|
+
expect(client2.send).toHaveBeenCalledWith(expect.stringContaining("task:dispatched"));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("send error removes failing client automatically", () => {
|
|
81
|
+
const failingClient = {
|
|
82
|
+
send: mock(() => {
|
|
83
|
+
throw new Error("Connection closed");
|
|
84
|
+
}),
|
|
85
|
+
close: mock(() => {}),
|
|
86
|
+
};
|
|
87
|
+
const goodClient = {
|
|
88
|
+
send: mock(() => {}),
|
|
89
|
+
close: mock(() => {}),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
broadcaster.start();
|
|
93
|
+
broadcaster.addClient(failingClient);
|
|
94
|
+
broadcaster.addClient(goodClient);
|
|
95
|
+
|
|
96
|
+
expect(broadcaster.getClientCount()).toBe(2);
|
|
97
|
+
|
|
98
|
+
const event: AADEvent = {
|
|
99
|
+
type: "worker:idle",
|
|
100
|
+
workerId: "worker-1" as any,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
eventBus.emit(event);
|
|
104
|
+
|
|
105
|
+
// Failing client should be removed
|
|
106
|
+
expect(broadcaster.getClientCount()).toBe(1);
|
|
107
|
+
expect(goodClient.send).toHaveBeenCalledTimes(1);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("stop closes all clients and unsubscribes", () => {
|
|
111
|
+
const client1 = {
|
|
112
|
+
send: mock(() => {}),
|
|
113
|
+
close: mock(() => {}),
|
|
114
|
+
};
|
|
115
|
+
const client2 = {
|
|
116
|
+
send: mock(() => {}),
|
|
117
|
+
close: mock(() => {}),
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
broadcaster.start();
|
|
121
|
+
broadcaster.addClient(client1);
|
|
122
|
+
broadcaster.addClient(client2);
|
|
123
|
+
|
|
124
|
+
broadcaster.stop();
|
|
125
|
+
|
|
126
|
+
expect(client1.close).toHaveBeenCalledTimes(1);
|
|
127
|
+
expect(client2.close).toHaveBeenCalledTimes(1);
|
|
128
|
+
expect(broadcaster.getClientCount()).toBe(0);
|
|
129
|
+
|
|
130
|
+
// EventBus should not trigger sends after stop
|
|
131
|
+
const event: AADEvent = {
|
|
132
|
+
type: "run:completed",
|
|
133
|
+
runId: "run-1" as any,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
eventBus.emit(event);
|
|
137
|
+
expect(client1.send).toHaveBeenCalledTimes(0);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("handles multiple events to same clients", () => {
|
|
141
|
+
const client = {
|
|
142
|
+
send: mock(() => {}),
|
|
143
|
+
close: mock(() => {}),
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
broadcaster.start();
|
|
147
|
+
broadcaster.addClient(client);
|
|
148
|
+
|
|
149
|
+
eventBus.emit({
|
|
150
|
+
type: "task:dispatched",
|
|
151
|
+
taskId: "task-1" as any,
|
|
152
|
+
workerId: "worker-1" as any,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
eventBus.emit({
|
|
156
|
+
type: "task:completed",
|
|
157
|
+
taskId: "task-1" as any,
|
|
158
|
+
result: { success: true } as any,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
expect(client.send).toHaveBeenCalledTimes(2);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, test, expect, mock } from "bun:test";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { createSSERoutes } from "../routes/sse";
|
|
4
|
+
import type { SSEBroadcaster, SSEClient } from "../services/sse-broadcaster";
|
|
5
|
+
|
|
6
|
+
// Mock SSEBroadcaster
|
|
7
|
+
function createMockBroadcaster(): SSEBroadcaster {
|
|
8
|
+
return {
|
|
9
|
+
start: mock(() => {}),
|
|
10
|
+
stop: mock(() => {}),
|
|
11
|
+
addClient: mock(() => {}),
|
|
12
|
+
removeClient: mock(() => {}),
|
|
13
|
+
getClientCount: mock(() => 0),
|
|
14
|
+
} as any;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("SSE Routes", () => {
|
|
18
|
+
test("GET /events/all returns 200 with SSE content type", async () => {
|
|
19
|
+
const broadcaster = createMockBroadcaster();
|
|
20
|
+
const app = new Hono();
|
|
21
|
+
app.route("/", createSSERoutes(broadcaster));
|
|
22
|
+
|
|
23
|
+
const res = await app.request("/events/all");
|
|
24
|
+
|
|
25
|
+
expect(res.status).toBe(200);
|
|
26
|
+
expect(res.headers.get("Content-Type")).toContain("text/event-stream");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("GET /events/all calls addClient on broadcaster", async () => {
|
|
30
|
+
const broadcaster = createMockBroadcaster();
|
|
31
|
+
const app = new Hono();
|
|
32
|
+
app.route("/", createSSERoutes(broadcaster));
|
|
33
|
+
|
|
34
|
+
await app.request("/events/all");
|
|
35
|
+
|
|
36
|
+
expect(broadcaster.addClient).toHaveBeenCalled();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("SSE route registers client with send and close methods", async () => {
|
|
40
|
+
const broadcaster = createMockBroadcaster();
|
|
41
|
+
let capturedClient: SSEClient | undefined;
|
|
42
|
+
|
|
43
|
+
broadcaster.addClient = mock((client: SSEClient) => {
|
|
44
|
+
capturedClient = client;
|
|
45
|
+
}) as any;
|
|
46
|
+
|
|
47
|
+
const app = new Hono();
|
|
48
|
+
app.route("/", createSSERoutes(broadcaster));
|
|
49
|
+
|
|
50
|
+
await app.request("/events/all");
|
|
51
|
+
|
|
52
|
+
expect(capturedClient).toBeDefined();
|
|
53
|
+
if (capturedClient !== undefined) {
|
|
54
|
+
expect(typeof capturedClient.send).toBe("function");
|
|
55
|
+
expect(typeof capturedClient.close).toBe("function");
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
});
|