oh-my-opencode-dashboard 0.0.5 → 0.1.1

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.
@@ -155,4 +155,111 @@ describe('toDashboardPayload', () => {
155
155
  // #then: should handle non-array steps gracefully
156
156
  expect(payload.planProgress.steps).toEqual([])
157
157
  })
158
- })
158
+
159
+ it('should parse mainSession.sessionId from camel or snake keys', () => {
160
+ // #given: server JSON with main session id in camel and snake case
161
+ const camelJson = {
162
+ mainSession: {
163
+ agent: "sisyphus",
164
+ currentTool: "dashboard_start",
165
+ currentModel: "anthropic/claude-opus-4-5",
166
+ lastUpdatedLabel: "just now",
167
+ session: "test-session",
168
+ sessionId: "ses_main",
169
+ statusPill: "busy",
170
+ },
171
+ }
172
+
173
+ const snakeJson = {
174
+ main_session: {
175
+ agent: "sisyphus",
176
+ current_tool: "dashboard_start",
177
+ current_model: "anthropic/claude-opus-4-5",
178
+ last_updated: "just now",
179
+ session: "test-session",
180
+ session_id: "ses_snake",
181
+ status: "busy",
182
+ },
183
+ }
184
+
185
+ // #when: converting to dashboard payload
186
+ const camelPayload = toDashboardPayload(camelJson)
187
+ const snakePayload = toDashboardPayload(snakeJson)
188
+
189
+ // #then: sessionId should be preserved
190
+ expect(camelPayload.mainSession.sessionId).toBe("ses_main")
191
+ expect(snakePayload.mainSession.sessionId).toBe("ses_snake")
192
+ })
193
+
194
+ it('should preserve mainSessionTasks from server JSON', () => {
195
+ // #given: server JSON with mainSessionTasks
196
+ const serverJson = {
197
+ mainSession: {
198
+ agent: "sisyphus",
199
+ currentTool: "dashboard_start",
200
+ currentModel: "anthropic/claude-opus-4-5",
201
+ lastUpdatedLabel: "just now",
202
+ session: "test-session",
203
+ sessionId: "ses_main",
204
+ statusPill: "busy",
205
+ },
206
+ planProgress: {
207
+ name: "test-plan",
208
+ completed: 0,
209
+ total: 0,
210
+ path: "/tmp/test-plan.md",
211
+ statusPill: "not started",
212
+ steps: [],
213
+ },
214
+ mainSessionTasks: [
215
+ {
216
+ id: "main-session",
217
+ description: "Main session",
218
+ subline: "ses_main",
219
+ agent: "sisyphus",
220
+ lastModel: "anthropic/claude-opus-4-5",
221
+ sessionId: "ses_main",
222
+ status: "running",
223
+ toolCalls: 3,
224
+ lastTool: "delegate_task",
225
+ timeline: "2026-01-01T00:00:00Z: 2m",
226
+ },
227
+ ],
228
+ backgroundTasks: [],
229
+ timeSeries: {
230
+ windowMs: 300000,
231
+ buckets: 150,
232
+ bucketMs: 2000,
233
+ anchorMs: 1640995200000,
234
+ serverNowMs: 1640995500000,
235
+ series: [
236
+ {
237
+ id: "overall-main",
238
+ label: "Overall",
239
+ tone: "muted",
240
+ values: new Array(150).fill(0),
241
+ },
242
+ ],
243
+ },
244
+ }
245
+
246
+ // #when
247
+ const payload = toDashboardPayload(serverJson)
248
+
249
+ // #then
250
+ expect(payload.mainSessionTasks).toEqual([
251
+ {
252
+ id: "main-session",
253
+ description: "Main session",
254
+ subline: "ses_main",
255
+ agent: "sisyphus",
256
+ lastModel: "anthropic/claude-opus-4-5",
257
+ sessionId: "ses_main",
258
+ status: "running",
259
+ toolCalls: 3,
260
+ lastTool: "delegate_task",
261
+ timeline: "2026-01-01T00:00:00Z: 2m",
262
+ },
263
+ ])
264
+ })
265
+ })
@@ -15,6 +15,10 @@ function mkStorageRoot(): string {
15
15
  return root
16
16
  }
17
17
 
18
+ function mkProjectRoot(): string {
19
+ return fs.mkdtempSync(path.join(os.tmpdir(), "omo-dashboard-project-"))
20
+ }
21
+
18
22
  function writeMessageMeta(opts: {
19
23
  storageRoot: string
20
24
  sessionId: string
@@ -75,9 +79,18 @@ function hasSensitiveKeys(value: unknown): boolean {
75
79
 
76
80
  const createStore = (): DashboardStore => ({
77
81
  getSnapshot: (): DashboardPayload => ({
78
- mainSession: { agent: "x", currentModel: null, currentTool: "-", lastUpdatedLabel: "never", session: "s", statusPill: "idle" },
82
+ mainSession: {
83
+ agent: "x",
84
+ currentModel: null,
85
+ currentTool: "-",
86
+ lastUpdatedLabel: "never",
87
+ session: "s",
88
+ sessionId: null,
89
+ statusPill: "idle",
90
+ },
79
91
  planProgress: { name: "p", completed: 0, total: 0, path: "", statusPill: "not started", steps: [] as PlanStep[] },
80
92
  backgroundTasks: [],
93
+ mainSessionTasks: [],
81
94
  timeSeries: {
82
95
  windowMs: 0,
83
96
  bucketMs: 0,
@@ -93,8 +106,9 @@ const createStore = (): DashboardStore => ({
93
106
  describe('API Routes', () => {
94
107
  it('should return health check', async () => {
95
108
  const storageRoot = mkStorageRoot()
109
+ const projectRoot = mkProjectRoot()
96
110
  const store = createStore()
97
- const api = createApi({ store, storageRoot })
111
+ const api = createApi({ store, storageRoot, projectRoot })
98
112
 
99
113
  const res = await api.request("/health")
100
114
  expect(res.status).toBe(200)
@@ -103,8 +117,9 @@ describe('API Routes', () => {
103
117
 
104
118
  it('should return dashboard data without sensitive keys', async () => {
105
119
  const storageRoot = mkStorageRoot()
120
+ const projectRoot = mkProjectRoot()
106
121
  const store = createStore()
107
- const api = createApi({ store, storageRoot })
122
+ const api = createApi({ store, storageRoot, projectRoot })
108
123
 
109
124
  const res = await api.request("/dashboard")
110
125
  expect(res.status).toBe(200)
@@ -122,8 +137,9 @@ describe('API Routes', () => {
122
137
 
123
138
  it('should reject invalid session IDs', async () => {
124
139
  const storageRoot = mkStorageRoot()
140
+ const projectRoot = mkProjectRoot()
125
141
  const store = createStore()
126
- const api = createApi({ store, storageRoot })
142
+ const api = createApi({ store, storageRoot, projectRoot })
127
143
 
128
144
  const res = await api.request("/tool-calls/not_valid!")
129
145
  expect(res.status).toBe(400)
@@ -132,8 +148,9 @@ describe('API Routes', () => {
132
148
 
133
149
  it('should return 404 for missing sessions', async () => {
134
150
  const storageRoot = mkStorageRoot()
151
+ const projectRoot = mkProjectRoot()
135
152
  const store = createStore()
136
- const api = createApi({ store, storageRoot })
153
+ const api = createApi({ store, storageRoot, projectRoot })
137
154
 
138
155
  const res = await api.request("/tool-calls/ses_missing")
139
156
  expect(res.status).toBe(404)
@@ -142,9 +159,10 @@ describe('API Routes', () => {
142
159
 
143
160
  it('should return empty tool calls for existing sessions', async () => {
144
161
  const storageRoot = mkStorageRoot()
162
+ const projectRoot = mkProjectRoot()
145
163
  writeMessageMeta({ storageRoot, sessionId: "ses_empty", messageId: "msg_1", created: 1000 })
146
164
  const store = createStore()
147
- const api = createApi({ store, storageRoot })
165
+ const api = createApi({ store, storageRoot, projectRoot })
148
166
 
149
167
  const res = await api.request("/tool-calls/ses_empty")
150
168
  expect(res.status).toBe(200)
@@ -160,6 +178,7 @@ describe('API Routes', () => {
160
178
 
161
179
  it('should redact tool call payload fields', async () => {
162
180
  const storageRoot = mkStorageRoot()
181
+ const projectRoot = mkProjectRoot()
163
182
  writeMessageMeta({ storageRoot, sessionId: "ses_redact", messageId: "msg_1", created: 1000 })
164
183
  writeToolPart({
165
184
  storageRoot,
@@ -175,7 +194,7 @@ describe('API Routes', () => {
175
194
  },
176
195
  })
177
196
  const store = createStore()
178
- const api = createApi({ store, storageRoot })
197
+ const api = createApi({ store, storageRoot, projectRoot })
179
198
 
180
199
  const res = await api.request("/tool-calls/ses_redact")
181
200
  expect(res.status).toBe(200)
@@ -185,4 +204,6 @@ describe('API Routes', () => {
185
204
  expect(data.toolCalls.length).toBe(1)
186
205
  expect(hasSensitiveKeys(data)).toBe(false)
187
206
  })
207
+
208
+ // /sessions was intentionally removed along with the manual session picker.
188
209
  })
package/src/server/api.ts CHANGED
@@ -6,7 +6,7 @@ import { deriveToolCalls, MAX_TOOL_CALL_MESSAGES, MAX_TOOL_CALLS } from "../inge
6
6
 
7
7
  const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/
8
8
 
9
- export function createApi(opts: { store: DashboardStore; storageRoot: string }): Hono {
9
+ export function createApi(opts: { store: DashboardStore; storageRoot: string; projectRoot: string }): Hono {
10
10
  const api = new Hono()
11
11
 
12
12
  api.get("/health", (c) => {
@@ -76,9 +76,49 @@ describe("buildDashboardPayload", () => {
76
76
  expect(payload.mainSession.currentTool).toBe("delegate_task")
77
77
  expect(payload.mainSession.agent).toBe("sisyphus")
78
78
  expect(payload.mainSession.currentModel).toBeNull()
79
+ expect(payload.mainSession.sessionId).toBe(sessionId)
79
80
 
80
81
  expect(payload.raw).not.toHaveProperty("prompt")
81
82
  expect(payload.raw).not.toHaveProperty("input")
83
+
84
+ expect(payload).toHaveProperty("mainSessionTasks")
85
+ expect((payload as any).mainSessionTasks).toEqual([
86
+ {
87
+ id: "main-session",
88
+ description: "Main session",
89
+ subline: sessionId,
90
+ agent: "sisyphus",
91
+ lastModel: null,
92
+ status: "running",
93
+ toolCalls: 1,
94
+ lastTool: "delegate_task",
95
+ timeline: "1970-01-01T00:00:01Z: 1s",
96
+ sessionId,
97
+ },
98
+ ])
99
+
100
+ expect(payload.raw).toHaveProperty("mainSessionTasks.0.lastTool", "delegate_task")
101
+ } finally {
102
+ fs.rmSync(storageRoot, { recursive: true, force: true })
103
+ fs.rmSync(projectRoot, { recursive: true, force: true })
104
+ }
105
+ })
106
+
107
+ it("includes mainSessionTasks in raw payload when no sessions exist", () => {
108
+ const storageRoot = mkStorageRoot()
109
+ const storage = getStorageRoots(storageRoot)
110
+ const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), "omo-project-"))
111
+
112
+ try {
113
+ const payload = buildDashboardPayload({
114
+ projectRoot,
115
+ storage,
116
+ nowMs: 2000,
117
+ })
118
+
119
+ expect(payload).toHaveProperty("mainSessionTasks")
120
+ expect((payload as any).mainSessionTasks).toEqual([])
121
+ expect(payload.raw).toHaveProperty("mainSessionTasks")
82
122
  } finally {
83
123
  fs.rmSync(storageRoot, { recursive: true, force: true })
84
124
  fs.rmSync(projectRoot, { recursive: true, force: true })
@@ -149,6 +189,7 @@ describe("buildDashboardPayload", () => {
149
189
  expect(payload).toHaveProperty("timeSeries")
150
190
  expect(payload.raw).toHaveProperty("timeSeries")
151
191
  expect(payload.mainSession.currentModel).toBeNull()
192
+ expect(payload.mainSession.sessionId).toBeNull()
152
193
 
153
194
  const sensitiveKeys = ["prompt", "input", "output", "error", "state"]
154
195
 
@@ -4,6 +4,7 @@ import { readBoulderState, readPlanProgress, readPlanSteps, type PlanStep } from
4
4
  import { deriveBackgroundTasks } from "../ingest/background-tasks"
5
5
  import { deriveTimeSeriesActivity, type TimeSeriesPayload } from "../ingest/timeseries"
6
6
  import { getMainSessionView, getStorageRoots, pickActiveSessionId, readMainSessionMetas, type MainSessionView, type OpenCodeStorageRoots, type SessionMetadata } from "../ingest/session"
7
+ import { deriveToolCalls } from "../ingest/tool-calls"
7
8
 
8
9
  export type DashboardPayload = {
9
10
  mainSession: {
@@ -12,6 +13,7 @@ export type DashboardPayload = {
12
13
  currentTool: string
13
14
  lastUpdatedLabel: string
14
15
  session: string
16
+ sessionId: string | null
15
17
  statusPill: string
16
18
  }
17
19
  planProgress: {
@@ -33,6 +35,18 @@ export type DashboardPayload = {
33
35
  timeline: string
34
36
  sessionId: string | null
35
37
  }>
38
+ mainSessionTasks: Array<{
39
+ id: string
40
+ description: string
41
+ subline?: string
42
+ agent: string
43
+ lastModel: string | null
44
+ status: string
45
+ toolCalls: number
46
+ lastTool: string
47
+ timeline: string
48
+ sessionId: string | null
49
+ }>
36
50
  timeSeries: TimeSeriesPayload
37
51
  raw: unknown
38
52
  }
@@ -92,6 +106,33 @@ function mainStatusPill(status: string): string {
92
106
  return "unknown"
93
107
  }
94
108
 
109
+ function formatIsoNoMs(ts: number): string {
110
+ const iso = new Date(ts).toISOString()
111
+ return iso.replace(/\.\d{3}Z$/, "Z")
112
+ }
113
+
114
+ function formatElapsed(ms: number): string {
115
+ const totalSeconds = Math.max(0, Math.floor(ms / 1000))
116
+ const seconds = totalSeconds % 60
117
+ const totalMinutes = Math.floor(totalSeconds / 60)
118
+ const minutes = totalMinutes % 60
119
+ const totalHours = Math.floor(totalMinutes / 60)
120
+ const hours = totalHours % 24
121
+ const days = Math.floor(totalHours / 24)
122
+
123
+ if (days > 0) return hours > 0 ? `${days}d${hours}h` : `${days}d`
124
+ if (totalHours > 0) return minutes > 0 ? `${totalHours}h${minutes}m` : `${totalHours}h`
125
+ if (totalMinutes > 0) return seconds > 0 ? `${totalMinutes}m${seconds}s` : `${totalMinutes}m`
126
+ return `${seconds}s`
127
+ }
128
+
129
+ function formatTimeline(startAt: number | null, endAtMs: number): string {
130
+ if (typeof startAt !== "number") return ""
131
+ const start = formatIsoNoMs(startAt)
132
+ const elapsed = formatElapsed(endAtMs - startAt)
133
+ return `${start}: ${elapsed}`
134
+ }
135
+
95
136
  export function buildDashboardPayload(opts: {
96
137
  projectRoot: string
97
138
  storage: OpenCodeStorageRoots
@@ -136,6 +177,41 @@ export function buildDashboardPayload(opts: {
136
177
  const mainCurrentModel = "currentModel" in main
137
178
  ? (main as MainSessionView).currentModel
138
179
  : null
180
+
181
+ const mainSessionTasks = (() => {
182
+ if (!sessionId) return []
183
+
184
+ const mainStatus = main.status
185
+ const status = mainStatus === "running_tool" || mainStatus === "thinking" || mainStatus === "busy"
186
+ ? "running"
187
+ : mainStatus === "idle"
188
+ ? "idle"
189
+ : "unknown"
190
+
191
+ const { toolCalls } = deriveToolCalls({
192
+ storage: opts.storage,
193
+ sessionId,
194
+ })
195
+
196
+ const startAt = sessionMeta?.time?.created ?? null
197
+ const endAtMs = status === "running" ? nowMs : (main.lastUpdated ?? nowMs)
198
+
199
+ return [
200
+ {
201
+ id: "main-session",
202
+ description: "Main session",
203
+ subline: sessionId,
204
+ agent: main.agent,
205
+ lastModel: mainCurrentModel,
206
+ status,
207
+ toolCalls: toolCalls.length,
208
+ lastTool: toolCalls[0]?.tool ?? "-",
209
+ timeline: formatTimeline(startAt, endAtMs),
210
+ sessionId,
211
+ },
212
+ ]
213
+ })()
214
+
139
215
  const payload: DashboardPayload = {
140
216
  mainSession: {
141
217
  agent: main.agent,
@@ -143,6 +219,7 @@ export function buildDashboardPayload(opts: {
143
219
  currentTool: main.currentTool ?? "-",
144
220
  lastUpdatedLabel: formatIso(main.lastUpdated),
145
221
  session: main.sessionLabel,
222
+ sessionId: sessionId ?? null,
146
223
  statusPill: mainStatusPill(main.status),
147
224
  },
148
225
  planProgress: {
@@ -164,6 +241,7 @@ export function buildDashboardPayload(opts: {
164
241
  timeline: typeof t.timeline === "string" ? t.timeline : "",
165
242
  sessionId: t.sessionId ?? null,
166
243
  })),
244
+ mainSessionTasks,
167
245
  timeSeries,
168
246
  raw: null,
169
247
  }
@@ -172,6 +250,7 @@ export function buildDashboardPayload(opts: {
172
250
  mainSession: payload.mainSession,
173
251
  planProgress: payload.planProgress,
174
252
  backgroundTasks: payload.backgroundTasks,
253
+ mainSessionTasks: payload.mainSessionTasks,
175
254
  timeSeries: payload.timeSeries,
176
255
  }
177
256
  return payload
package/src/server/dev.ts CHANGED
@@ -35,7 +35,7 @@ const store = createDashboardStore({
35
35
  pollIntervalMs: 2000,
36
36
  })
37
37
 
38
- app.route("/api", createApi({ store, storageRoot }))
38
+ app.route("/api", createApi({ store, storageRoot, projectRoot: resolvedProjectPath }))
39
39
 
40
40
  Bun.serve({
41
41
  fetch: app.fetch,
@@ -30,7 +30,7 @@ const store = createDashboardStore({
30
30
  pollIntervalMs: 2000,
31
31
  })
32
32
 
33
- app.route('/api', createApi({ store, storageRoot }))
33
+ app.route('/api', createApi({ store, storageRoot, projectRoot: project }))
34
34
 
35
35
  const distRoot = join(import.meta.dir, '../../dist')
36
36
 
package/src/styles.css CHANGED
@@ -161,6 +161,64 @@ body {
161
161
  transform: translateY(0px);
162
162
  }
163
163
 
164
+ .fieldRow {
165
+ display: flex;
166
+ align-items: center;
167
+ flex-wrap: wrap;
168
+ gap: 10px;
169
+ }
170
+
171
+ .field {
172
+ border: 1px solid rgba(31, 36, 38, 0.14);
173
+ background: rgba(255, 255, 255, 0.64);
174
+ padding: 9px 12px;
175
+ border-radius: 999px;
176
+ color: var(--ink);
177
+ font-size: 12px;
178
+ line-height: 1;
179
+ box-shadow: 0 6px 16px rgba(29, 32, 33, 0.06);
180
+ transition: transform 120ms ease, background 120ms ease, border-color 120ms ease;
181
+ min-width: 0;
182
+ }
183
+
184
+ .field:hover {
185
+ transform: translateY(-1px);
186
+ background: rgba(255, 255, 255, 0.78);
187
+ border-color: rgba(31, 36, 38, 0.18);
188
+ }
189
+
190
+ .field:active {
191
+ transform: translateY(0px);
192
+ }
193
+
194
+ .field:focus-visible {
195
+ outline: 2px solid rgba(15, 90, 81, 0.30);
196
+ outline-offset: 2px;
197
+ }
198
+
199
+ .field:disabled {
200
+ opacity: 0.55;
201
+ cursor: not-allowed;
202
+ }
203
+
204
+ select.field {
205
+ appearance: none;
206
+ padding-right: 34px;
207
+ background-image:
208
+ linear-gradient(45deg, transparent 50%, rgba(31, 36, 38, 0.50) 50%),
209
+ linear-gradient(135deg, rgba(31, 36, 38, 0.50) 50%, transparent 50%);
210
+ background-position:
211
+ calc(100% - 18px) 50%,
212
+ calc(100% - 13px) 50%;
213
+ background-size: 5px 5px;
214
+ background-repeat: no-repeat;
215
+ }
216
+
217
+ input.field {
218
+ appearance: none;
219
+ flex: 1 1 220px;
220
+ }
221
+
164
222
  .pill {
165
223
  display: inline-flex;
166
224
  align-items: center;
@@ -0,0 +1,111 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import * as React from "react";
3
+ import { renderToStaticMarkup } from "react-dom/server";
4
+
5
+ import {
6
+ TimeSeriesActivitySection,
7
+ computeMainAgentsScaleMax,
8
+ computeOtherMainAgentsCount,
9
+ } from "./App";
10
+
11
+ type TimeSeriesProps = React.ComponentProps<typeof TimeSeriesActivitySection>;
12
+
13
+ function mkTimeSeries(override?: Partial<TimeSeriesProps["timeSeries"]>): TimeSeriesProps["timeSeries"] {
14
+ const buckets = 3;
15
+ const mkSeries = (id: TimeSeriesProps["timeSeries"]["series"][number]["id"], values: number[]) => ({
16
+ id,
17
+ label: id,
18
+ tone: "muted" as const,
19
+ values,
20
+ });
21
+
22
+ return {
23
+ windowMs: 300_000,
24
+ buckets,
25
+ bucketMs: 2_000,
26
+ anchorMs: 0,
27
+ serverNowMs: 0,
28
+ series: [
29
+ mkSeries("overall-main", [0, 0, 0]),
30
+ mkSeries("agent:sisyphus", [0, 0, 0]),
31
+ mkSeries("agent:prometheus", [0, 0, 0]),
32
+ mkSeries("agent:atlas", [0, 0, 0]),
33
+ mkSeries("background-total", [0, 0, 0]),
34
+ ],
35
+ ...override,
36
+ };
37
+ }
38
+
39
+ describe("TimeSeriesActivitySection (SSR)", () => {
40
+ it("should not render top axis and should keep bottom axis", () => {
41
+ // #given
42
+ const timeSeries = mkTimeSeries();
43
+
44
+ // #when
45
+ const html = renderToStaticMarkup(<TimeSeriesActivitySection timeSeries={timeSeries} />);
46
+
47
+ // #then
48
+ expect(html).not.toContain("timeSeriesAxisTop");
49
+ expect(html).toContain("timeSeriesAxisBottom");
50
+ });
51
+
52
+ it("should render sand bars for other main agents when derived otherMain is non-zero", () => {
53
+ // #given
54
+ const timeSeries = mkTimeSeries({
55
+ series: [
56
+ { id: "overall-main", label: "Overall", tone: "muted", values: [10, 0, 0] },
57
+ { id: "agent:sisyphus", label: "Sisyphus", tone: "teal", values: [0, 0, 0] },
58
+ { id: "agent:prometheus", label: "Prometheus", tone: "red", values: [0, 0, 0] },
59
+ { id: "agent:atlas", label: "Atlas", tone: "green", values: [0, 0, 0] },
60
+ { id: "background-total", label: "Background", tone: "muted", values: [0, 0, 0] },
61
+ ],
62
+ });
63
+
64
+ // #when
65
+ const html = renderToStaticMarkup(<TimeSeriesActivitySection timeSeries={timeSeries} />);
66
+
67
+ // #then
68
+ expect(html).toContain("timeSeriesBar--sand");
69
+ });
70
+ });
71
+
72
+ describe("time-series helpers", () => {
73
+ it("computeOtherMainAgentsCount should clamp to >= 0 and ignore invalid numbers", () => {
74
+ // #given
75
+ const value = computeOtherMainAgentsCount({
76
+ overall: 10,
77
+ background: 3,
78
+ sisyphus: 2,
79
+ prometheus: 1,
80
+ atlas: 0,
81
+ });
82
+
83
+ // #then
84
+ expect(value).toBe(4);
85
+
86
+ expect(
87
+ computeOtherMainAgentsCount({
88
+ overall: NaN,
89
+ background: Infinity,
90
+ sisyphus: -1,
91
+ prometheus: 0,
92
+ atlas: 0,
93
+ })
94
+ ).toBe(0);
95
+ });
96
+
97
+ it("computeMainAgentsScaleMax should include otherMain in the max", () => {
98
+ // #given
99
+ const scaleMax = computeMainAgentsScaleMax({
100
+ buckets: 1,
101
+ overallValues: [10],
102
+ backgroundValues: [0],
103
+ sisyphusValues: [0],
104
+ prometheusValues: [0],
105
+ atlasValues: [0],
106
+ });
107
+
108
+ // #then
109
+ expect(scaleMax).toBe(10);
110
+ });
111
+ });