@sentry/junior-dashboard 0.62.0 → 0.64.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.
Files changed (67) hide show
  1. package/dist/app.d.ts +2 -0
  2. package/dist/app.js +1285 -39
  3. package/dist/assets.d.ts +2 -0
  4. package/dist/client/api.d.ts +1 -1
  5. package/dist/client/components/Button.d.ts +15 -0
  6. package/dist/client/components/Metric.d.ts +22 -0
  7. package/dist/client/components/StatusBadge.d.ts +2 -1
  8. package/dist/client/components/TelemetryMetrics.d.ts +24 -0
  9. package/dist/client/components/ToolFrame.d.ts +3 -1
  10. package/dist/client/components/Transcript.d.ts +2 -0
  11. package/dist/client/components/TranscriptHeader.d.ts +2 -0
  12. package/dist/client/components/TranscriptHeadingRow.d.ts +16 -0
  13. package/dist/client/components/TranscriptThinkingView.d.ts +7 -0
  14. package/dist/client/components/TranscriptToolRun.d.ts +9 -0
  15. package/dist/client/components/transcriptRenderModel.d.ts +25 -9
  16. package/dist/client/format.d.ts +45 -8
  17. package/dist/client/markdownExport.d.ts +3 -0
  18. package/dist/client/toolInvocations.d.ts +7 -0
  19. package/dist/client/types.d.ts +17 -100
  20. package/dist/client.js +85 -55998
  21. package/dist/dashboardLoader.d.ts +1 -0
  22. package/dist/handler.js +1288 -40
  23. package/dist/index.d.ts +8 -0
  24. package/dist/index.js +1807 -0
  25. package/dist/mock-conversations.d.ts +3 -0
  26. package/dist/mock-release-conversation.d.ts +3 -0
  27. package/dist/nitro.d.ts +7 -1
  28. package/dist/tailwind.css +1 -1
  29. package/dist/url.d.ts +13 -0
  30. package/package.json +9 -5
  31. package/src/app.ts +42 -18
  32. package/src/assets.ts +2 -0
  33. package/src/auth.ts +2 -35
  34. package/src/client/App.tsx +33 -74
  35. package/src/client/code.tsx +1 -1
  36. package/src/client/components/Button.tsx +75 -0
  37. package/src/client/components/ConversationRowStats.tsx +3 -2
  38. package/src/client/components/FilterTabs.tsx +10 -11
  39. package/src/client/components/LoadingView.tsx +7 -2
  40. package/src/client/components/Metric.tsx +159 -0
  41. package/src/client/components/StatusBadge.tsx +10 -1
  42. package/src/client/components/TelemetryMetrics.tsx +124 -0
  43. package/src/client/components/ToolFrame.tsx +57 -14
  44. package/src/client/components/Transcript.tsx +6 -2
  45. package/src/client/components/TranscriptHeader.tsx +18 -19
  46. package/src/client/components/TranscriptHeadingRow.tsx +64 -0
  47. package/src/client/components/TranscriptThinkingView.tsx +157 -0
  48. package/src/client/components/TranscriptToolRun.tsx +66 -0
  49. package/src/client/components/TranscriptToolView.tsx +16 -8
  50. package/src/client/components/TranscriptTurn.tsx +368 -132
  51. package/src/client/components/TurnDurationChart.tsx +236 -78
  52. package/src/client/components/transcriptRenderModel.ts +60 -20
  53. package/src/client/format.ts +329 -87
  54. package/src/client/markdownExport.ts +360 -0
  55. package/src/client/pages/CommandCenter.tsx +1 -1
  56. package/src/client/pages/ConversationPage.tsx +142 -45
  57. package/src/client/pages/ConversationsPage.tsx +1 -1
  58. package/src/client/toolInvocations.ts +16 -0
  59. package/src/client/types.ts +34 -90
  60. package/src/config.ts +4 -0
  61. package/src/dashboardLoader.ts +12 -0
  62. package/src/index.ts +78 -0
  63. package/src/mock-conversations.ts +726 -0
  64. package/src/mock-release-conversation.ts +605 -0
  65. package/src/nitro.ts +7 -1
  66. package/src/tailwind.css +11 -0
  67. package/src/url.ts +68 -0
@@ -0,0 +1,726 @@
1
+ import type {
2
+ DashboardConversationReport,
3
+ DashboardSessionFeed,
4
+ DashboardSessionReport,
5
+ DashboardTranscriptMessage,
6
+ DashboardTurnReport,
7
+ JuniorReporting,
8
+ } from "@sentry/junior/reporting";
9
+
10
+ import { longReleaseConversation } from "./mock-release-conversation";
11
+
12
+ const INCIDENT_CONVERSATION_ID = "slack:CQA123:1770000000.000100";
13
+ const ACTIVE_CONVERSATION_ID = "slack:CQA123:1770003600.000200";
14
+ const PRIVATE_CONVERSATION_ID = "slack:DQA123:1770007200.000300";
15
+ const HUNG_CONVERSATION_ID = "slack:CQA999:1770010800.000400";
16
+ const FAILED_CONVERSATION_ID = "slack:CQA777:1770014400.000500";
17
+ const SCHEDULER_CONVERSATION_ID = "scheduler:daily-ops-digest";
18
+
19
+ function iso(nowMs: number, offsetMs = 0): string {
20
+ return new Date(nowMs + offsetMs).toISOString();
21
+ }
22
+
23
+ function sentryConversationUrl(conversationId: string): string {
24
+ return `https://sentry.example.com/organizations/acme/explore/conversations/${encodeURIComponent(conversationId)}/`;
25
+ }
26
+
27
+ function sentryTraceUrl(traceId: string): string {
28
+ return `https://sentry.example.com/performance/trace/${traceId}/`;
29
+ }
30
+
31
+ function sessionFromTurn(turn: DashboardTurnReport): DashboardSessionReport {
32
+ const {
33
+ transcript,
34
+ transcriptAvailable,
35
+ transcriptMessageCount,
36
+ transcriptMetadata,
37
+ transcriptRedacted,
38
+ transcriptRedactionReason,
39
+ ...session
40
+ } = turn;
41
+ return session;
42
+ }
43
+
44
+ function publicIncidentConversation(
45
+ nowMs: number,
46
+ ): DashboardConversationReport {
47
+ const traceId = "5f2c7f7df83e4a37a03c9d4a14f4c991";
48
+ const startedAt = iso(nowMs, -58 * 60_000);
49
+ const secondStartedAt = iso(nowMs, -44 * 60_000);
50
+
51
+ return {
52
+ conversationId: INCIDENT_CONVERSATION_ID,
53
+ generatedAt: iso(nowMs),
54
+ turns: [
55
+ {
56
+ conversationId: INCIDENT_CONVERSATION_ID,
57
+ conversationTitle: "Checkout latency triage",
58
+ id: "mock-incident-turn-1",
59
+ status: "completed",
60
+ startedAt,
61
+ lastProgressAt: iso(nowMs, -56 * 60_000),
62
+ lastSeenAt: iso(nowMs, -55 * 60_000),
63
+ completedAt: iso(nowMs, -55 * 60_000),
64
+ cumulativeDurationMs: 181_000,
65
+ cumulativeUsage: {
66
+ cachedInputTokens: 2200,
67
+ inputTokens: 6900,
68
+ outputTokens: 1400,
69
+ totalTokens: 9700,
70
+ },
71
+ surface: "slack",
72
+ title: "Turn mock-incident-turn-1",
73
+ requesterIdentity: {
74
+ fullName: "Avery Stone",
75
+ slackUserId: "UQA111",
76
+ slackUserName: "avery",
77
+ },
78
+ channel: "CQA123",
79
+ channelName: "proj-checkout",
80
+ sentryConversationUrl: sentryConversationUrl(INCIDENT_CONVERSATION_ID),
81
+ sentryTraceUrl: sentryTraceUrl(traceId),
82
+ traceId,
83
+ transcriptAvailable: true,
84
+ transcriptMessageCount: 4,
85
+ transcript: [
86
+ {
87
+ role: "user",
88
+ timestamp: Date.parse(startedAt),
89
+ parts: [
90
+ {
91
+ type: "text",
92
+ text: "Can you check why checkout p95 jumped after the last deploy? Keep it short but include the likely next owner.",
93
+ },
94
+ ],
95
+ },
96
+ {
97
+ role: "assistant",
98
+ timestamp: Date.parse(startedAt) + 19_000,
99
+ parts: [
100
+ {
101
+ type: "thinking",
102
+ output:
103
+ "Correlate deploy timing, Sentry issue volume, and endpoint latency before assigning ownership.",
104
+ },
105
+ {
106
+ id: "toolu_mock_trace_search",
107
+ name: "sentry.search_traces",
108
+ input: {
109
+ project: "checkout-api",
110
+ query: "transaction:/api/checkout p95:>2s",
111
+ window: "30m",
112
+ },
113
+ type: "tool_call",
114
+ },
115
+ ],
116
+ },
117
+ {
118
+ role: "toolResult",
119
+ timestamp: Date.parse(startedAt) + 51_000,
120
+ parts: [
121
+ {
122
+ id: "toolu_mock_trace_search",
123
+ name: "sentry.search_traces",
124
+ output: {
125
+ examples: [
126
+ {
127
+ durationMs: 2840,
128
+ operation: "POST /api/checkout",
129
+ traceId,
130
+ },
131
+ ],
132
+ p95Ms: 2310,
133
+ suspectedSpan: "stripe.payment_intents.create",
134
+ },
135
+ type: "tool_result",
136
+ },
137
+ ],
138
+ },
139
+ {
140
+ role: "assistant",
141
+ timestamp: Date.parse(startedAt) + 163_000,
142
+ parts: [
143
+ {
144
+ type: "text",
145
+ text: [
146
+ "Checkout p95 is tracking the Stripe payment intent span, not app CPU. The jump starts within five minutes of the `payments-v42` deploy.",
147
+ "",
148
+ "Suggested owner: payments platform. I would ask them to compare Stripe idempotency-key behavior between `v41` and `v42` before rolling back.",
149
+ ].join("\n"),
150
+ },
151
+ ],
152
+ },
153
+ ],
154
+ },
155
+ {
156
+ conversationId: INCIDENT_CONVERSATION_ID,
157
+ conversationTitle: "Checkout latency triage",
158
+ id: "mock-incident-turn-2",
159
+ status: "completed",
160
+ startedAt: secondStartedAt,
161
+ lastProgressAt: iso(nowMs, -42 * 60_000),
162
+ lastSeenAt: iso(nowMs, -41 * 60_000),
163
+ completedAt: iso(nowMs, -41 * 60_000),
164
+ cumulativeDurationMs: 206_000,
165
+ cumulativeUsage: {
166
+ cachedInputTokens: 3100,
167
+ inputTokens: 5200,
168
+ outputTokens: 950,
169
+ totalTokens: 9250,
170
+ },
171
+ surface: "slack",
172
+ title: "Turn mock-incident-turn-2",
173
+ requesterIdentity: {
174
+ fullName: "Morgan Lee",
175
+ slackUserId: "UQA222",
176
+ slackUserName: "morgan",
177
+ },
178
+ channel: "CQA123",
179
+ channelName: "proj-checkout",
180
+ sentryConversationUrl: sentryConversationUrl(INCIDENT_CONVERSATION_ID),
181
+ sentryTraceUrl: sentryTraceUrl(traceId),
182
+ traceId,
183
+ transcriptAvailable: true,
184
+ transcriptMessageCount: 3,
185
+ transcript: [
186
+ {
187
+ role: "user",
188
+ timestamp: Date.parse(secondStartedAt),
189
+ parts: [
190
+ {
191
+ type: "text",
192
+ text: "Can you draft the rollback note with the exact evidence?",
193
+ },
194
+ ],
195
+ },
196
+ {
197
+ role: "assistant",
198
+ timestamp: Date.parse(secondStartedAt) + 12_000,
199
+ parts: [
200
+ {
201
+ id: "toolu_mock_issue_lookup",
202
+ name: "sentry.get_issue",
203
+ input: {
204
+ issue: "CHECKOUT-9B2",
205
+ project: "checkout-api",
206
+ },
207
+ type: "tool_call",
208
+ },
209
+ {
210
+ id: "toolu_mock_release_lookup",
211
+ name: "vercel.get_deployment",
212
+ input: {
213
+ deployment: "payments-v42",
214
+ team: "acme",
215
+ },
216
+ type: "tool_call",
217
+ },
218
+ ],
219
+ },
220
+ {
221
+ role: "assistant",
222
+ timestamp: Date.parse(secondStartedAt) + 188_000,
223
+ parts: [
224
+ {
225
+ type: "text",
226
+ text: [
227
+ "Rollback note:",
228
+ "",
229
+ "```md",
230
+ "Rolling back payments-v42. Evidence: checkout p95 rose from 740ms to 2.3s, traces isolate the regression to stripe.payment_intents.create, and CHECKOUT-9B2 began after the deployment window.",
231
+ "```",
232
+ "",
233
+ "Keep the rollback scoped to payments; frontend traffic and auth spans look stable.",
234
+ ].join("\n"),
235
+ },
236
+ ],
237
+ },
238
+ ],
239
+ },
240
+ ],
241
+ };
242
+ }
243
+
244
+ function activeConversation(nowMs: number): DashboardConversationReport {
245
+ const startedAt = iso(nowMs, -6 * 60_000);
246
+
247
+ return {
248
+ conversationId: ACTIVE_CONVERSATION_ID,
249
+ generatedAt: iso(nowMs),
250
+ turns: [
251
+ {
252
+ conversationId: ACTIVE_CONVERSATION_ID,
253
+ conversationTitle: "Deploy rollout watch",
254
+ id: "mock-active-turn-1",
255
+ status: "active",
256
+ startedAt,
257
+ lastProgressAt: iso(nowMs, -18_000),
258
+ lastSeenAt: iso(nowMs, -12_000),
259
+ cumulativeDurationMs: 348_000,
260
+ cumulativeUsage: {
261
+ inputTokens: 7800,
262
+ outputTokens: 620,
263
+ totalTokens: 8420,
264
+ },
265
+ surface: "slack",
266
+ title: "Turn mock-active-turn-1",
267
+ requesterIdentity: {
268
+ fullName: "Sam Rivera",
269
+ slackUserId: "UQA333",
270
+ slackUserName: "sam",
271
+ },
272
+ channel: "CQA123",
273
+ channelName: "proj-checkout",
274
+ sentryConversationUrl: sentryConversationUrl(ACTIVE_CONVERSATION_ID),
275
+ transcriptAvailable: true,
276
+ transcriptMessageCount: 2,
277
+ transcript: [
278
+ {
279
+ role: "user",
280
+ timestamp: Date.parse(startedAt),
281
+ parts: [
282
+ {
283
+ type: "text",
284
+ text: "Watch the rollout for the next few minutes and call out anything that looks unsafe.",
285
+ },
286
+ ],
287
+ },
288
+ {
289
+ role: "assistant",
290
+ timestamp: Date.parse(startedAt) + 41_000,
291
+ parts: [
292
+ {
293
+ type: "thinking",
294
+ output:
295
+ "Keep the user updated only if the rollout crosses the agreed error-budget threshold.",
296
+ },
297
+ {
298
+ id: "toolu_mock_datacat_rollout",
299
+ name: "datacat.search_logs",
300
+ input: {
301
+ query: "service:checkout-api env:prod rollout:v42",
302
+ window: "15m",
303
+ },
304
+ type: "tool_call",
305
+ },
306
+ ],
307
+ },
308
+ ],
309
+ },
310
+ ],
311
+ };
312
+ }
313
+
314
+ function privateConversation(nowMs: number): DashboardConversationReport {
315
+ const startedAt = iso(nowMs, -24 * 60_000);
316
+
317
+ return {
318
+ conversationId: PRIVATE_CONVERSATION_ID,
319
+ generatedAt: iso(nowMs),
320
+ turns: [
321
+ {
322
+ conversationId: PRIVATE_CONVERSATION_ID,
323
+ conversationTitle: "Direct Message",
324
+ id: "mock-private-turn-1",
325
+ status: "completed",
326
+ startedAt,
327
+ lastProgressAt: iso(nowMs, -23 * 60_000),
328
+ lastSeenAt: iso(nowMs, -22 * 60_000),
329
+ completedAt: iso(nowMs, -22 * 60_000),
330
+ cumulativeDurationMs: 94_000,
331
+ cumulativeUsage: {
332
+ inputTokens: 3100,
333
+ outputTokens: 440,
334
+ totalTokens: 3540,
335
+ },
336
+ surface: "slack",
337
+ title: "Turn mock-private-turn-1",
338
+ requesterIdentity: {
339
+ slackUserId: "UQA444",
340
+ slackUserName: "private-user",
341
+ },
342
+ channel: "DQA123",
343
+ channelName: "Direct Message",
344
+ transcriptAvailable: false,
345
+ transcriptMessageCount: 4,
346
+ transcriptMetadata: redactedPrivateTranscript(Date.parse(startedAt)),
347
+ transcriptRedacted: true,
348
+ transcriptRedactionReason: "non_public_conversation",
349
+ transcript: [],
350
+ },
351
+ ],
352
+ };
353
+ }
354
+
355
+ function redactedPrivateTranscript(
356
+ startedAtMs: number,
357
+ ): DashboardTranscriptMessage[] {
358
+ return [
359
+ {
360
+ role: "user",
361
+ timestamp: startedAtMs,
362
+ parts: [
363
+ {
364
+ bytes: 174,
365
+ chars: 172,
366
+ redacted: true,
367
+ type: "text",
368
+ },
369
+ ],
370
+ },
371
+ {
372
+ role: "assistant",
373
+ timestamp: startedAtMs + 18_000,
374
+ parts: [
375
+ {
376
+ outputKeys: ["strategy", "risk"],
377
+ outputSizeBytes: 188,
378
+ outputSizeChars: 188,
379
+ outputType: "object",
380
+ redacted: true,
381
+ type: "thinking",
382
+ },
383
+ ],
384
+ },
385
+ {
386
+ role: "assistant",
387
+ timestamp: startedAtMs + 29_000,
388
+ parts: [
389
+ {
390
+ id: "toolu_mock_private_thread",
391
+ inputKeys: ["channel", "ts"],
392
+ inputSizeBytes: 58,
393
+ inputSizeChars: 58,
394
+ inputType: "object",
395
+ name: "slack.fetch_thread",
396
+ redacted: true,
397
+ type: "tool_call",
398
+ },
399
+ ],
400
+ },
401
+ {
402
+ role: "toolResult",
403
+ timestamp: startedAtMs + 47_000,
404
+ parts: [
405
+ {
406
+ id: "toolu_mock_private_thread",
407
+ name: "slack.fetch_thread",
408
+ outputKeys: ["messages"],
409
+ outputSizeBytes: 962,
410
+ outputSizeChars: 950,
411
+ outputType: "object",
412
+ redacted: true,
413
+ type: "tool_result",
414
+ },
415
+ ],
416
+ },
417
+ ];
418
+ }
419
+
420
+ function hungConversation(nowMs: number): DashboardConversationReport {
421
+ const startedAt = iso(nowMs, -18 * 60_000);
422
+
423
+ return {
424
+ conversationId: HUNG_CONVERSATION_ID,
425
+ generatedAt: iso(nowMs),
426
+ turns: [
427
+ {
428
+ conversationId: HUNG_CONVERSATION_ID,
429
+ conversationTitle: "Sandbox QA run",
430
+ id: "mock-hung-turn-1",
431
+ status: "hung",
432
+ startedAt,
433
+ lastProgressAt: iso(nowMs, -11 * 60_000),
434
+ lastSeenAt: iso(nowMs, -10 * 60_000),
435
+ cumulativeDurationMs: 480_000,
436
+ cumulativeUsage: {
437
+ inputTokens: 11_200,
438
+ outputTokens: 800,
439
+ totalTokens: 12_000,
440
+ },
441
+ surface: "slack",
442
+ title: "Turn mock-hung-turn-1",
443
+ requesterIdentity: {
444
+ fullName: "Dana Chen",
445
+ slackUserId: "UQA555",
446
+ slackUserName: "dana",
447
+ },
448
+ channel: "CQA999",
449
+ channelName: "quality",
450
+ sentryConversationUrl: sentryConversationUrl(HUNG_CONVERSATION_ID),
451
+ transcriptAvailable: true,
452
+ transcriptMessageCount: 3,
453
+ transcript: [
454
+ {
455
+ role: "user",
456
+ timestamp: Date.parse(startedAt),
457
+ parts: [
458
+ {
459
+ type: "text",
460
+ text: "Run the checkout smoke test in the sandbox and tell me if the redirect loop still reproduces.",
461
+ },
462
+ ],
463
+ },
464
+ {
465
+ role: "assistant",
466
+ timestamp: Date.parse(startedAt) + 35_000,
467
+ parts: [
468
+ {
469
+ id: "toolu_mock_sandbox_run",
470
+ name: "sandbox.run_command",
471
+ input: {
472
+ args: ["pnpm", "test", "checkout-smoke"],
473
+ cwd: "/repo",
474
+ timeoutMs: 600000,
475
+ },
476
+ type: "tool_call",
477
+ },
478
+ ],
479
+ },
480
+ {
481
+ role: "assistant",
482
+ timestamp: Date.parse(startedAt) + 2 * 60_000,
483
+ parts: [
484
+ {
485
+ type: "text",
486
+ text: "The sandbox command started. I am waiting on the browser trace before calling the result.",
487
+ },
488
+ ],
489
+ },
490
+ ],
491
+ },
492
+ ],
493
+ };
494
+ }
495
+
496
+ function failedConversation(nowMs: number): DashboardConversationReport {
497
+ const traceId = "29bbf789f14b469cb4f6ed830a47224d";
498
+ const startedAt = iso(nowMs, -36 * 60_000);
499
+
500
+ return {
501
+ conversationId: FAILED_CONVERSATION_ID,
502
+ generatedAt: iso(nowMs),
503
+ turns: [
504
+ {
505
+ conversationId: FAILED_CONVERSATION_ID,
506
+ conversationTitle: "OAuth callback investigation",
507
+ id: "mock-failed-turn-1",
508
+ status: "failed",
509
+ startedAt,
510
+ lastProgressAt: iso(nowMs, -35 * 60_000),
511
+ lastSeenAt: iso(nowMs, -35 * 60_000),
512
+ cumulativeDurationMs: 72_000,
513
+ cumulativeUsage: {
514
+ inputTokens: 4500,
515
+ outputTokens: 390,
516
+ totalTokens: 4890,
517
+ },
518
+ surface: "slack",
519
+ title: "Turn mock-failed-turn-1",
520
+ requesterIdentity: {
521
+ fullName: "Riley Patel",
522
+ slackUserId: "UQA666",
523
+ slackUserName: "riley",
524
+ },
525
+ channel: "CQA777",
526
+ channelName: "platform-auth",
527
+ sentryConversationUrl: sentryConversationUrl(FAILED_CONVERSATION_ID),
528
+ sentryTraceUrl: sentryTraceUrl(traceId),
529
+ traceId,
530
+ transcriptAvailable: true,
531
+ transcriptMessageCount: 3,
532
+ transcript: [
533
+ {
534
+ role: "user",
535
+ timestamp: Date.parse(startedAt),
536
+ parts: [
537
+ {
538
+ type: "text",
539
+ text: "Why are new Notion OAuth callbacks failing in staging?",
540
+ },
541
+ ],
542
+ },
543
+ {
544
+ role: "assistant",
545
+ timestamp: Date.parse(startedAt) + 15_000,
546
+ parts: [
547
+ {
548
+ id: "toolu_mock_oauth_logs",
549
+ name: "sentry.search_errors",
550
+ input: {
551
+ environment: "staging",
552
+ query: "OAuth callback Notion status:500",
553
+ },
554
+ type: "tool_call",
555
+ },
556
+ ],
557
+ },
558
+ {
559
+ role: "toolResult",
560
+ timestamp: Date.parse(startedAt) + 53_000,
561
+ parts: [
562
+ {
563
+ id: "toolu_mock_oauth_logs",
564
+ name: "sentry.search_errors",
565
+ output: {
566
+ error:
567
+ "Provider token exchange failed: invalid_client for staging callback origin",
568
+ traceId,
569
+ },
570
+ type: "tool_result",
571
+ },
572
+ ],
573
+ },
574
+ ],
575
+ },
576
+ ],
577
+ };
578
+ }
579
+
580
+ function schedulerConversation(nowMs: number): DashboardConversationReport {
581
+ const startedAt = iso(nowMs, -2 * 60 * 60_000);
582
+
583
+ return {
584
+ conversationId: SCHEDULER_CONVERSATION_ID,
585
+ generatedAt: iso(nowMs),
586
+ turns: [
587
+ {
588
+ conversationId: SCHEDULER_CONVERSATION_ID,
589
+ conversationTitle: "Daily operations digest",
590
+ id: "mock-scheduler-turn-1",
591
+ status: "completed",
592
+ startedAt,
593
+ lastProgressAt: iso(nowMs, -119 * 60_000),
594
+ lastSeenAt: iso(nowMs, -118 * 60_000),
595
+ completedAt: iso(nowMs, -118 * 60_000),
596
+ cumulativeDurationMs: 115_000,
597
+ cumulativeUsage: {
598
+ inputTokens: 6200,
599
+ outputTokens: 760,
600
+ totalTokens: 6960,
601
+ },
602
+ surface: "scheduler",
603
+ title: "Turn mock-scheduler-turn-1",
604
+ transcriptAvailable: true,
605
+ transcriptMessageCount: 2,
606
+ transcript: [
607
+ {
608
+ role: "user",
609
+ timestamp: Date.parse(startedAt),
610
+ parts: [
611
+ {
612
+ type: "text",
613
+ text: "Scheduled task: summarize overnight production risk for the checkout team.",
614
+ },
615
+ ],
616
+ },
617
+ {
618
+ role: "assistant",
619
+ timestamp: Date.parse(startedAt) + 109_000,
620
+ parts: [
621
+ {
622
+ type: "text",
623
+ text: "Overnight risk stayed low. One staging OAuth regression is still open; checkout production latency returned to baseline after the payments rollback.",
624
+ },
625
+ ],
626
+ },
627
+ ],
628
+ },
629
+ ],
630
+ };
631
+ }
632
+
633
+ function mockConversations(nowMs: number): DashboardConversationReport[] {
634
+ return [
635
+ activeConversation(nowMs),
636
+ longReleaseConversation(nowMs),
637
+ publicIncidentConversation(nowMs),
638
+ privateConversation(nowMs),
639
+ failedConversation(nowMs),
640
+ hungConversation(nowMs),
641
+ schedulerConversation(nowMs),
642
+ ];
643
+ }
644
+
645
+ function mockConversationMap(
646
+ nowMs: number,
647
+ ): Map<string, DashboardConversationReport> {
648
+ return new Map(
649
+ mockConversations(nowMs).map((conversation) => [
650
+ conversation.conversationId,
651
+ conversation,
652
+ ]),
653
+ );
654
+ }
655
+
656
+ function mockSessionFeed(nowMs: number): DashboardSessionFeed {
657
+ return {
658
+ source: "turn_session_records",
659
+ generatedAt: iso(nowMs),
660
+ sessions: mockConversations(nowMs).flatMap((conversation) =>
661
+ conversation.turns.map(sessionFromTurn),
662
+ ),
663
+ };
664
+ }
665
+
666
+ function mergeSessionFeeds(
667
+ mockFeed: DashboardSessionFeed,
668
+ realFeed: DashboardSessionFeed,
669
+ ): DashboardSessionFeed {
670
+ const mockSessionKeys = new Set(
671
+ mockFeed.sessions.map(
672
+ (session) => `${session.conversationId}:${session.id}`,
673
+ ),
674
+ );
675
+
676
+ return {
677
+ source: realFeed.source,
678
+ generatedAt: realFeed.generatedAt,
679
+ sessions: [
680
+ ...mockFeed.sessions,
681
+ ...realFeed.sessions.filter(
682
+ (session) =>
683
+ !mockSessionKeys.has(`${session.conversationId}:${session.id}`),
684
+ ),
685
+ ],
686
+ };
687
+ }
688
+
689
+ function isLocalPersistenceUnavailable(error: unknown): boolean {
690
+ return (
691
+ error instanceof Error &&
692
+ error.message.includes(
693
+ "REDIS_URL is required for durable Slack thread state",
694
+ )
695
+ );
696
+ }
697
+
698
+ /** Layer visual-QA conversation fixtures over a real read-only reporting source. */
699
+ export function createMockConversationReporting(
700
+ reporting: JuniorReporting,
701
+ ): JuniorReporting {
702
+ return {
703
+ getHealth: reporting.getHealth,
704
+ getRuntimeInfo: reporting.getRuntimeInfo,
705
+ getPlugins: reporting.getPlugins,
706
+ getSkills: reporting.getSkills,
707
+ async getSessions() {
708
+ const mockFeed = mockSessionFeed(Date.now());
709
+ try {
710
+ return mergeSessionFeeds(mockFeed, await reporting.getSessions());
711
+ } catch (error) {
712
+ if (!isLocalPersistenceUnavailable(error)) {
713
+ throw error;
714
+ }
715
+ return mockFeed;
716
+ }
717
+ },
718
+ async getConversation(conversationId: string) {
719
+ const conversation = mockConversationMap(Date.now()).get(conversationId);
720
+ if (conversation) {
721
+ return conversation;
722
+ }
723
+ return reporting.getConversation(conversationId);
724
+ },
725
+ };
726
+ }