@ronkovic/aad 0.3.8 → 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/README.md +292 -12
- package/package.json +6 -1
- package/src/__tests__/e2e/pipeline-e2e.test.ts +1 -0
- package/src/__tests__/e2e/resume-e2e.test.ts +2 -0
- package/src/__tests__/integration/pipeline.test.ts +1 -0
- package/src/modules/claude-provider/__tests__/claude-sdk-real-env.test.ts +1 -1
- package/src/modules/claude-provider/__tests__/claude-sdk.adapter.test.ts +2 -0
- package/src/modules/claude-provider/__tests__/provider-registry.test.ts +1 -0
- package/src/modules/cli/__tests__/cleanup.test.ts +72 -0
- package/src/modules/cli/__tests__/resume.test.ts +1 -0
- package/src/modules/cli/__tests__/run.test.ts +1 -0
- package/src/modules/cli/commands/__tests__/task-dispatch-handler.test.ts +145 -0
- package/src/modules/cli/commands/cleanup.ts +26 -11
- package/src/modules/cli/commands/resume.ts +3 -2
- package/src/modules/cli/commands/run.ts +57 -7
- package/src/modules/cli/commands/task-dispatch-handler.ts +73 -3
- package/src/modules/dashboard/__tests__/api-graph.test.ts +332 -0
- package/src/modules/dashboard/__tests__/api-timeline.test.ts +461 -0
- package/src/modules/dashboard/routes/sse.ts +3 -2
- package/src/modules/dashboard/server.ts +1 -0
- package/src/modules/dashboard/services/sse-broadcaster.ts +29 -0
- package/src/modules/dashboard/ui/dashboard.html +143 -18
- package/src/modules/git-workspace/__tests__/branch-manager.test.ts +52 -0
- package/src/modules/git-workspace/__tests__/dependency-installer.test.ts +77 -0
- package/src/modules/git-workspace/__tests__/git-exec.test.ts +26 -0
- package/src/modules/git-workspace/__tests__/merge-service.test.ts +19 -0
- package/src/modules/git-workspace/__tests__/pr-manager.test.ts +80 -0
- package/src/modules/git-workspace/__tests__/template-copy.test.ts +189 -0
- package/src/modules/git-workspace/__tests__/worktree-cleanup.test.ts +29 -2
- package/src/modules/git-workspace/__tests__/worktree-manager.test.ts +64 -4
- package/src/modules/git-workspace/branch-manager.ts +24 -3
- package/src/modules/git-workspace/dependency-installer.ts +113 -0
- package/src/modules/git-workspace/git-exec.ts +3 -2
- package/src/modules/git-workspace/index.ts +10 -1
- package/src/modules/git-workspace/merge-service.ts +40 -10
- package/src/modules/git-workspace/pr-manager.ts +278 -0
- package/src/modules/git-workspace/template-copy.ts +302 -0
- package/src/modules/git-workspace/worktree-manager.ts +37 -11
- package/src/modules/planning/__tests__/planning-service.test.ts +1 -0
- package/src/modules/planning/__tests__/planning.service.test.ts +149 -0
- package/src/modules/planning/__tests__/project-detection.test.ts +7 -1
- package/src/modules/planning/planning.service.ts +16 -2
- package/src/modules/planning/project-detection.ts +4 -1
- package/src/modules/process-manager/__tests__/process-manager.test.ts +1 -0
- package/src/modules/task-execution/__tests__/executor.test.ts +86 -0
- package/src/modules/task-execution/__tests__/tester-verify.test.ts +4 -3
- package/src/modules/task-execution/executor.ts +87 -4
- package/src/modules/task-execution/phases/implementer-green.ts +22 -5
- package/src/modules/task-execution/phases/merge.ts +44 -2
- package/src/modules/task-execution/phases/tester-red.ts +22 -5
- package/src/modules/task-execution/phases/tester-verify.ts +22 -6
- package/src/modules/task-queue/dispatcher.ts +50 -1
- package/src/shared/__tests__/prerequisites.test.ts +176 -0
- package/src/shared/config.ts +6 -0
- package/src/shared/prerequisites.ts +190 -0
- package/src/shared/types.ts +13 -0
- package/templates/CLAUDE.md +122 -0
- package/templates/settings.json +117 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/progress.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/completed/task-getall-2.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-1.json +0 -13
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-getall-1.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-status-change.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-1.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-2.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/progress.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/completed/task-getall-2.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-1.json +0 -13
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-getall-1.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-status-change.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-1.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-2.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/progress.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/completed/task-getall-2.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-1.json +0 -13
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-getall-1.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-status-change.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-1.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-2.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/progress.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/completed/task-getall-2.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-1.json +0 -13
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-getall-1.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-status-change.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-1.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-2.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/progress.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/completed/task-getall-2.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-1.json +0 -13
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-getall-1.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-status-change.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-1.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-2.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/progress.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/completed/task-getall-2.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-1.json +0 -13
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-getall-1.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-status-change.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-1.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-2.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/progress.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/completed/task-getall-2.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-1.json +0 -13
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-getall-1.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-status-change.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-1.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-2.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/progress.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/completed/task-getall-2.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-1.json +0 -13
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-getall-1.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-status-change.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-1.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-2.json +0 -5
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import type { StateAggregator } from "../services/state-aggregator";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* テストケース: /api/timeline エンドポイント
|
|
6
|
+
*
|
|
7
|
+
* タスクの実行タイムラインを返す
|
|
8
|
+
* - 各タスクの開始時刻、終了時刻、実行期間を可視化
|
|
9
|
+
* - Ganttチャートやタイムライン表示に使用
|
|
10
|
+
*
|
|
11
|
+
* NOTE: 現在の実装はタスク全体のタイムラインのみ。
|
|
12
|
+
* 将来的にはフェーズ別タイムライン (Red, Green, Verify, Review, Merge) に拡張予定。
|
|
13
|
+
*/
|
|
14
|
+
describe("GET /api/timeline - Task Execution Timeline", () => {
|
|
15
|
+
/**
|
|
16
|
+
* テストケース 1: タスクなし (空のタイムライン)
|
|
17
|
+
*
|
|
18
|
+
* 期待値:
|
|
19
|
+
* - tasks: 空配列
|
|
20
|
+
*/
|
|
21
|
+
test("returns empty timeline when no tasks exist", async () => {
|
|
22
|
+
const mockAggregator: StateAggregator = {
|
|
23
|
+
getTimeline: async () => ({
|
|
24
|
+
tasks: [],
|
|
25
|
+
}),
|
|
26
|
+
} as any;
|
|
27
|
+
|
|
28
|
+
const result = await mockAggregator.getTimeline();
|
|
29
|
+
|
|
30
|
+
expect(result.tasks).toEqual([]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* テストケース 2: 単一タスク (完了済み)
|
|
35
|
+
*
|
|
36
|
+
* タイムライン:
|
|
37
|
+
* [task-1] ████████ (10:00 - 10:05)
|
|
38
|
+
*
|
|
39
|
+
* 期待値:
|
|
40
|
+
* - 開始時刻と終了時刻が設定されている
|
|
41
|
+
* - ステータスが正しく反映される
|
|
42
|
+
*/
|
|
43
|
+
test("returns timeline for single completed task", async () => {
|
|
44
|
+
const startTime = "2025-02-11T10:00:00.000Z";
|
|
45
|
+
const endTime = "2025-02-11T10:05:00.000Z";
|
|
46
|
+
|
|
47
|
+
const mockAggregator: StateAggregator = {
|
|
48
|
+
getTimeline: async () => ({
|
|
49
|
+
tasks: [
|
|
50
|
+
{
|
|
51
|
+
id: "task-1",
|
|
52
|
+
status: "completed",
|
|
53
|
+
startTime,
|
|
54
|
+
endTime,
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
}),
|
|
58
|
+
} as any;
|
|
59
|
+
|
|
60
|
+
const result = await mockAggregator.getTimeline();
|
|
61
|
+
|
|
62
|
+
expect(result.tasks).toHaveLength(1);
|
|
63
|
+
expect(result.tasks[0]).toEqual({
|
|
64
|
+
id: "task-1",
|
|
65
|
+
status: "completed",
|
|
66
|
+
startTime,
|
|
67
|
+
endTime,
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* テストケース 3: 実行中タスク (終了時刻なし)
|
|
73
|
+
*
|
|
74
|
+
* タイムライン:
|
|
75
|
+
* [task-1] ████→ (10:00 - 進行中)
|
|
76
|
+
*
|
|
77
|
+
* 期待値:
|
|
78
|
+
* - startTime は設定されている
|
|
79
|
+
* - endTime は undefined
|
|
80
|
+
* - status は "running"
|
|
81
|
+
*/
|
|
82
|
+
test("returns timeline for running task without endTime", async () => {
|
|
83
|
+
const startTime = "2025-02-11T10:00:00.000Z";
|
|
84
|
+
|
|
85
|
+
const mockAggregator: StateAggregator = {
|
|
86
|
+
getTimeline: async () => ({
|
|
87
|
+
tasks: [
|
|
88
|
+
{
|
|
89
|
+
id: "task-1",
|
|
90
|
+
status: "running",
|
|
91
|
+
startTime,
|
|
92
|
+
endTime: undefined,
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
}),
|
|
96
|
+
} as any;
|
|
97
|
+
|
|
98
|
+
const result = await mockAggregator.getTimeline();
|
|
99
|
+
|
|
100
|
+
expect(result.tasks).toHaveLength(1);
|
|
101
|
+
expect(result.tasks[0]).toEqual({
|
|
102
|
+
id: "task-1",
|
|
103
|
+
status: "running",
|
|
104
|
+
startTime,
|
|
105
|
+
endTime: undefined,
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* テストケース 4: 保留中タスク (開始時刻・終了時刻なし)
|
|
111
|
+
*
|
|
112
|
+
* タイムライン:
|
|
113
|
+
* [task-1] (未開始)
|
|
114
|
+
*
|
|
115
|
+
* 期待値:
|
|
116
|
+
* - startTime, endTime ともに undefined
|
|
117
|
+
* - status は "pending"
|
|
118
|
+
*/
|
|
119
|
+
test("returns timeline for pending task without timestamps", async () => {
|
|
120
|
+
const mockAggregator: StateAggregator = {
|
|
121
|
+
getTimeline: async () => ({
|
|
122
|
+
tasks: [
|
|
123
|
+
{
|
|
124
|
+
id: "task-1",
|
|
125
|
+
status: "pending",
|
|
126
|
+
startTime: undefined,
|
|
127
|
+
endTime: undefined,
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
}),
|
|
131
|
+
} as any;
|
|
132
|
+
|
|
133
|
+
const result = await mockAggregator.getTimeline();
|
|
134
|
+
|
|
135
|
+
expect(result.tasks).toHaveLength(1);
|
|
136
|
+
expect(result.tasks[0]).toEqual({
|
|
137
|
+
id: "task-1",
|
|
138
|
+
status: "pending",
|
|
139
|
+
startTime: undefined,
|
|
140
|
+
endTime: undefined,
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* テストケース 5: 並列タスク (task-1, task-2 同時実行)
|
|
146
|
+
*
|
|
147
|
+
* タイムライン:
|
|
148
|
+
* [task-1] ████████ (10:00 - 10:05)
|
|
149
|
+
* [task-2] ██████ (10:01 - 10:04)
|
|
150
|
+
*
|
|
151
|
+
* 期待値:
|
|
152
|
+
* - 2つのタスクが時間的に重複
|
|
153
|
+
* - task-2 は task-1 の実行期間内に開始・終了
|
|
154
|
+
*/
|
|
155
|
+
test("returns timeline for overlapping parallel tasks", async () => {
|
|
156
|
+
const task1Start = "2025-02-11T10:00:00.000Z";
|
|
157
|
+
const task1End = "2025-02-11T10:05:00.000Z";
|
|
158
|
+
const task2Start = "2025-02-11T10:01:00.000Z";
|
|
159
|
+
const task2End = "2025-02-11T10:04:00.000Z";
|
|
160
|
+
|
|
161
|
+
const mockAggregator: StateAggregator = {
|
|
162
|
+
getTimeline: async () => ({
|
|
163
|
+
tasks: [
|
|
164
|
+
{
|
|
165
|
+
id: "task-1",
|
|
166
|
+
status: "completed",
|
|
167
|
+
startTime: task1Start,
|
|
168
|
+
endTime: task1End,
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: "task-2",
|
|
172
|
+
status: "completed",
|
|
173
|
+
startTime: task2Start,
|
|
174
|
+
endTime: task2End,
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
}),
|
|
178
|
+
} as any;
|
|
179
|
+
|
|
180
|
+
const result = await mockAggregator.getTimeline();
|
|
181
|
+
|
|
182
|
+
expect(result.tasks).toHaveLength(2);
|
|
183
|
+
|
|
184
|
+
// task-1 と task-2 の実行期間が重複していることを確認
|
|
185
|
+
const task1 = result.tasks.find((t) => t.id === "task-1")!;
|
|
186
|
+
const task2 = result.tasks.find((t) => t.id === "task-2")!;
|
|
187
|
+
|
|
188
|
+
expect(task1.startTime! < task2.startTime!).toBe(true);
|
|
189
|
+
expect(task2.startTime! < task1.endTime!).toBe(true);
|
|
190
|
+
expect(task2.endTime! < task1.endTime!).toBe(true);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* テストケース 6: 順次タスク (task-1 完了後に task-2 開始)
|
|
195
|
+
*
|
|
196
|
+
* タイムライン:
|
|
197
|
+
* [task-1] ████████ (10:00 - 10:05)
|
|
198
|
+
* [task-2] ████████ (10:06 - 10:10)
|
|
199
|
+
*
|
|
200
|
+
* 期待値:
|
|
201
|
+
* - task-1 の終了後に task-2 が開始
|
|
202
|
+
* - 実行期間に重複なし
|
|
203
|
+
*/
|
|
204
|
+
test("returns timeline for sequential tasks", async () => {
|
|
205
|
+
const task1Start = "2025-02-11T10:00:00.000Z";
|
|
206
|
+
const task1End = "2025-02-11T10:05:00.000Z";
|
|
207
|
+
const task2Start = "2025-02-11T10:06:00.000Z";
|
|
208
|
+
const task2End = "2025-02-11T10:10:00.000Z";
|
|
209
|
+
|
|
210
|
+
const mockAggregator: StateAggregator = {
|
|
211
|
+
getTimeline: async () => ({
|
|
212
|
+
tasks: [
|
|
213
|
+
{
|
|
214
|
+
id: "task-1",
|
|
215
|
+
status: "completed",
|
|
216
|
+
startTime: task1Start,
|
|
217
|
+
endTime: task1End,
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
id: "task-2",
|
|
221
|
+
status: "completed",
|
|
222
|
+
startTime: task2Start,
|
|
223
|
+
endTime: task2End,
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
}),
|
|
227
|
+
} as any;
|
|
228
|
+
|
|
229
|
+
const result = await mockAggregator.getTimeline();
|
|
230
|
+
|
|
231
|
+
expect(result.tasks).toHaveLength(2);
|
|
232
|
+
|
|
233
|
+
const task1 = result.tasks.find((t) => t.id === "task-1")!;
|
|
234
|
+
const task2 = result.tasks.find((t) => t.id === "task-2")!;
|
|
235
|
+
|
|
236
|
+
// task-1 の終了後に task-2 が開始
|
|
237
|
+
expect(task1.endTime! < task2.startTime!).toBe(true);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* テストケース 7: 全ステータスタイプのタイムライン
|
|
242
|
+
*
|
|
243
|
+
* タイムライン:
|
|
244
|
+
* [task-1] ████████ (completed)
|
|
245
|
+
* [task-2] ████→ (running)
|
|
246
|
+
* [task-3] (pending)
|
|
247
|
+
* [task-4] ██✗ (failed)
|
|
248
|
+
*
|
|
249
|
+
* 期待値:
|
|
250
|
+
* - 各ステータス (pending, running, completed, failed) が正しく反映
|
|
251
|
+
*/
|
|
252
|
+
test("returns timeline with all task status types", async () => {
|
|
253
|
+
const mockAggregator: StateAggregator = {
|
|
254
|
+
getTimeline: async () => ({
|
|
255
|
+
tasks: [
|
|
256
|
+
{
|
|
257
|
+
id: "task-1",
|
|
258
|
+
status: "completed",
|
|
259
|
+
startTime: "2025-02-11T10:00:00.000Z",
|
|
260
|
+
endTime: "2025-02-11T10:05:00.000Z",
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
id: "task-2",
|
|
264
|
+
status: "running",
|
|
265
|
+
startTime: "2025-02-11T10:06:00.000Z",
|
|
266
|
+
endTime: undefined,
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
id: "task-3",
|
|
270
|
+
status: "pending",
|
|
271
|
+
startTime: undefined,
|
|
272
|
+
endTime: undefined,
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
id: "task-4",
|
|
276
|
+
status: "failed",
|
|
277
|
+
startTime: "2025-02-11T10:10:00.000Z",
|
|
278
|
+
endTime: "2025-02-11T10:12:00.000Z",
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
}),
|
|
282
|
+
} as any;
|
|
283
|
+
|
|
284
|
+
const result = await mockAggregator.getTimeline();
|
|
285
|
+
|
|
286
|
+
expect(result.tasks).toHaveLength(4);
|
|
287
|
+
|
|
288
|
+
const statuses = result.tasks.map((t) => t.status).sort();
|
|
289
|
+
expect(statuses).toEqual(["completed", "failed", "pending", "running"]);
|
|
290
|
+
|
|
291
|
+
// completed タスク: 開始・終了時刻あり
|
|
292
|
+
const completedTask = result.tasks.find((t) => t.status === "completed")!;
|
|
293
|
+
expect(completedTask.startTime).toBeDefined();
|
|
294
|
+
expect(completedTask.endTime).toBeDefined();
|
|
295
|
+
|
|
296
|
+
// running タスク: 開始時刻あり、終了時刻なし
|
|
297
|
+
const runningTask = result.tasks.find((t) => t.status === "running")!;
|
|
298
|
+
expect(runningTask.startTime).toBeDefined();
|
|
299
|
+
expect(runningTask.endTime).toBeUndefined();
|
|
300
|
+
|
|
301
|
+
// pending タスク: 開始・終了時刻なし
|
|
302
|
+
const pendingTask = result.tasks.find((t) => t.status === "pending")!;
|
|
303
|
+
expect(pendingTask.startTime).toBeUndefined();
|
|
304
|
+
expect(pendingTask.endTime).toBeUndefined();
|
|
305
|
+
|
|
306
|
+
// failed タスク: 開始・終了時刻あり
|
|
307
|
+
const failedTask = result.tasks.find((t) => t.status === "failed")!;
|
|
308
|
+
expect(failedTask.startTime).toBeDefined();
|
|
309
|
+
expect(failedTask.endTime).toBeDefined();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* テストケース 8: 複数タスクの複雑なタイムライン
|
|
314
|
+
*
|
|
315
|
+
* タイムライン:
|
|
316
|
+
* [task-1] ████████ (10:00 - 10:05)
|
|
317
|
+
* [task-2] ██████ (10:01 - 10:04)
|
|
318
|
+
* [task-3] ████████ (10:06 - 10:10)
|
|
319
|
+
* [task-4] ██████ (10:07 - 10:09)
|
|
320
|
+
* [task-5] ████→ (10:11 - 進行中)
|
|
321
|
+
*
|
|
322
|
+
* 期待値:
|
|
323
|
+
* - 並列・順次が混在したタイムライン
|
|
324
|
+
* - 各タスクのタイムスタンプが正確
|
|
325
|
+
*/
|
|
326
|
+
test("returns complex timeline with mixed parallel and sequential execution", async () => {
|
|
327
|
+
const mockAggregator: StateAggregator = {
|
|
328
|
+
getTimeline: async () => ({
|
|
329
|
+
tasks: [
|
|
330
|
+
{
|
|
331
|
+
id: "task-1",
|
|
332
|
+
status: "completed",
|
|
333
|
+
startTime: "2025-02-11T10:00:00.000Z",
|
|
334
|
+
endTime: "2025-02-11T10:05:00.000Z",
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
id: "task-2",
|
|
338
|
+
status: "completed",
|
|
339
|
+
startTime: "2025-02-11T10:01:00.000Z",
|
|
340
|
+
endTime: "2025-02-11T10:04:00.000Z",
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
id: "task-3",
|
|
344
|
+
status: "completed",
|
|
345
|
+
startTime: "2025-02-11T10:06:00.000Z",
|
|
346
|
+
endTime: "2025-02-11T10:10:00.000Z",
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
id: "task-4",
|
|
350
|
+
status: "completed",
|
|
351
|
+
startTime: "2025-02-11T10:07:00.000Z",
|
|
352
|
+
endTime: "2025-02-11T10:09:00.000Z",
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
id: "task-5",
|
|
356
|
+
status: "running",
|
|
357
|
+
startTime: "2025-02-11T10:11:00.000Z",
|
|
358
|
+
endTime: undefined,
|
|
359
|
+
},
|
|
360
|
+
],
|
|
361
|
+
}),
|
|
362
|
+
} as any;
|
|
363
|
+
|
|
364
|
+
const result = await mockAggregator.getTimeline();
|
|
365
|
+
|
|
366
|
+
expect(result.tasks).toHaveLength(5);
|
|
367
|
+
|
|
368
|
+
// 並列実行グループ1: task-1 と task-2
|
|
369
|
+
const task1 = result.tasks.find((t) => t.id === "task-1")!;
|
|
370
|
+
const task2 = result.tasks.find((t) => t.id === "task-2")!;
|
|
371
|
+
expect(task1.startTime! < task2.startTime!).toBe(true);
|
|
372
|
+
expect(task2.endTime! < task1.endTime!).toBe(true);
|
|
373
|
+
|
|
374
|
+
// 並列実行グループ2: task-3 と task-4
|
|
375
|
+
const task3 = result.tasks.find((t) => t.id === "task-3")!;
|
|
376
|
+
const task4 = result.tasks.find((t) => t.id === "task-4")!;
|
|
377
|
+
expect(task3.startTime! < task4.startTime!).toBe(true);
|
|
378
|
+
expect(task4.endTime! < task3.endTime!).toBe(true);
|
|
379
|
+
|
|
380
|
+
// 順次実行: task-5 は task-3 完了後に開始
|
|
381
|
+
const task5 = result.tasks.find((t) => t.id === "task-5")!;
|
|
382
|
+
expect(task3.endTime! < task5.startTime!).toBe(true);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* テストケース 9: タイムスタンプのISO 8601形式検証
|
|
387
|
+
*
|
|
388
|
+
* 期待値:
|
|
389
|
+
* - startTime, endTime が有効なISO 8601形式の文字列
|
|
390
|
+
*/
|
|
391
|
+
test("validates ISO 8601 timestamp format", async () => {
|
|
392
|
+
const mockAggregator: StateAggregator = {
|
|
393
|
+
getTimeline: async () => ({
|
|
394
|
+
tasks: [
|
|
395
|
+
{
|
|
396
|
+
id: "task-1",
|
|
397
|
+
status: "completed",
|
|
398
|
+
startTime: "2025-02-11T10:00:00.000Z",
|
|
399
|
+
endTime: "2025-02-11T10:05:00.000Z",
|
|
400
|
+
},
|
|
401
|
+
],
|
|
402
|
+
}),
|
|
403
|
+
} as any;
|
|
404
|
+
|
|
405
|
+
const result = await mockAggregator.getTimeline();
|
|
406
|
+
|
|
407
|
+
const task = result.tasks[0];
|
|
408
|
+
if (!task) {
|
|
409
|
+
throw new Error("Expected at least one task in timeline");
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ISO 8601形式の検証 (Date.parse で有効な日時か確認)
|
|
413
|
+
expect(Date.parse(task.startTime!)).not.toBeNaN();
|
|
414
|
+
expect(Date.parse(task.endTime!)).not.toBeNaN();
|
|
415
|
+
|
|
416
|
+
// startTime < endTime の検証
|
|
417
|
+
expect(new Date(task.startTime!).getTime()).toBeLessThan(
|
|
418
|
+
new Date(task.endTime!).getTime()
|
|
419
|
+
);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* テストケース 10: タスクID重複なしの検証
|
|
424
|
+
*
|
|
425
|
+
* 期待値:
|
|
426
|
+
* - タイムライン内の全タスクIDがユニーク
|
|
427
|
+
*/
|
|
428
|
+
test("validates unique task IDs in timeline", async () => {
|
|
429
|
+
const mockAggregator: StateAggregator = {
|
|
430
|
+
getTimeline: async () => ({
|
|
431
|
+
tasks: [
|
|
432
|
+
{
|
|
433
|
+
id: "task-1",
|
|
434
|
+
status: "completed",
|
|
435
|
+
startTime: "2025-02-11T10:00:00.000Z",
|
|
436
|
+
endTime: "2025-02-11T10:05:00.000Z",
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
id: "task-2",
|
|
440
|
+
status: "running",
|
|
441
|
+
startTime: "2025-02-11T10:06:00.000Z",
|
|
442
|
+
endTime: undefined,
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
id: "task-3",
|
|
446
|
+
status: "pending",
|
|
447
|
+
startTime: undefined,
|
|
448
|
+
endTime: undefined,
|
|
449
|
+
},
|
|
450
|
+
],
|
|
451
|
+
}),
|
|
452
|
+
} as any;
|
|
453
|
+
|
|
454
|
+
const result = await mockAggregator.getTimeline();
|
|
455
|
+
|
|
456
|
+
const taskIds = result.tasks.map((t) => t.id);
|
|
457
|
+
const uniqueTaskIds = new Set(taskIds);
|
|
458
|
+
|
|
459
|
+
expect(taskIds.length).toBe(uniqueTaskIds.size);
|
|
460
|
+
});
|
|
461
|
+
});
|
|
@@ -21,10 +21,11 @@ export function createSSERoutes(broadcaster: SSEBroadcaster): Hono {
|
|
|
21
21
|
broadcaster.addClient(client);
|
|
22
22
|
|
|
23
23
|
// Keep connection alive until client disconnects
|
|
24
|
-
//
|
|
24
|
+
// Send heartbeat every 15 seconds to prevent idle timeout
|
|
25
25
|
try {
|
|
26
26
|
while (true) {
|
|
27
|
-
await stream.sleep(
|
|
27
|
+
await stream.sleep(15000); // 15 seconds
|
|
28
|
+
await stream.writeSSE({ event: "heartbeat", data: "" });
|
|
28
29
|
}
|
|
29
30
|
} finally {
|
|
30
31
|
// Cleanup on disconnect (via error or abort)
|
|
@@ -85,6 +85,7 @@ export class DashboardServer {
|
|
|
85
85
|
port: this.port,
|
|
86
86
|
hostname: this.host,
|
|
87
87
|
fetch: this.app.fetch,
|
|
88
|
+
idleTimeout: 255, // SSE long-lived connections (max: 255 seconds)
|
|
88
89
|
}) as BunServer;
|
|
89
90
|
this.logger?.info({ port: this.port, host: this.host }, "Dashboard server started");
|
|
90
91
|
} catch (error) {
|
|
@@ -10,6 +10,7 @@ export class SSEBroadcaster {
|
|
|
10
10
|
private eventBus: EventBus;
|
|
11
11
|
private clients: Set<SSEClient> = new Set();
|
|
12
12
|
private listener: EventListener<AADEvent> | null = null;
|
|
13
|
+
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
13
14
|
private logger?: Logger;
|
|
14
15
|
|
|
15
16
|
constructor(eventBus: EventBus, logger?: Logger) {
|
|
@@ -23,9 +24,37 @@ export class SSEBroadcaster {
|
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
this.eventBus.on("*", this.listener);
|
|
27
|
+
|
|
28
|
+
// Independent heartbeat to keep connections alive
|
|
29
|
+
this.heartbeatTimer = setInterval(() => {
|
|
30
|
+
const failedClients: SSEClient[] = [];
|
|
31
|
+
for (const client of this.clients) {
|
|
32
|
+
try {
|
|
33
|
+
client.send(`:ping\n\n`);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
this.logger?.debug({ error }, "Client failed heartbeat ping");
|
|
36
|
+
failedClients.push(client);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Remove failed clients
|
|
40
|
+
for (const client of failedClients) {
|
|
41
|
+
this.clients.delete(client);
|
|
42
|
+
try {
|
|
43
|
+
client.close();
|
|
44
|
+
} catch (error) {
|
|
45
|
+
this.logger?.debug({ error }, "Failed to close dead client");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}, 15000); // 15 seconds
|
|
26
49
|
}
|
|
27
50
|
|
|
28
51
|
stop(): void {
|
|
52
|
+
// Clear heartbeat timer
|
|
53
|
+
if (this.heartbeatTimer) {
|
|
54
|
+
clearInterval(this.heartbeatTimer);
|
|
55
|
+
this.heartbeatTimer = null;
|
|
56
|
+
}
|
|
57
|
+
|
|
29
58
|
if (this.listener) {
|
|
30
59
|
this.eventBus.off("*", this.listener);
|
|
31
60
|
this.listener = null;
|