@oneuptime/common 11.5.12 → 11.6.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 (193) hide show
  1. package/Models/DatabaseModels/AIInsight.ts +30 -0
  2. package/Models/DatabaseModels/EnterpriseLicense.ts +21 -0
  3. package/Models/DatabaseModels/EnterpriseLicenseInstance.ts +25 -0
  4. package/Models/DatabaseModels/GlobalConfig.ts +76 -0
  5. package/Models/DatabaseModels/Project.ts +30 -0
  6. package/Models/DatabaseModels/TelemetryException.ts +128 -0
  7. package/Server/API/AIAgentDataAPI.ts +129 -5
  8. package/Server/API/AIInvestigationAPI.ts +87 -0
  9. package/Server/API/EnterpriseLicenseAPI.ts +57 -0
  10. package/Server/API/GlobalConfigAPI.ts +62 -1
  11. package/Server/EnvironmentConfig.ts +22 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1784640000000-AddExceptionTriageAndPrivacyColumns.ts +56 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.ts +38 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.ts +25 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  16. package/Server/Services/AIRunService.ts +8 -0
  17. package/Server/Services/AnalyticsDatabaseService.ts +48 -45
  18. package/Server/Services/DatabaseService.ts +66 -1
  19. package/Server/Services/TelemetryExceptionService.ts +320 -24
  20. package/Server/Services/WorkflowService.ts +100 -39
  21. package/Server/Types/Workflow/Components/Schedule.ts +57 -32
  22. package/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.ts +15 -4
  23. package/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.ts +15 -4
  24. package/Server/Utils/AI/SRE/Insights/InsightScanner.ts +222 -24
  25. package/Server/Utils/AI/SRE/Insights/InsightTriageRunner.ts +225 -9
  26. package/Server/Utils/AI/SRE/SubjectCodeFixRun.ts +39 -0
  27. package/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.ts +114 -0
  28. package/Server/Utils/AnalyticsDatabase/InsertDedupContext.ts +59 -0
  29. package/Server/Utils/Express.ts +13 -0
  30. package/Server/Utils/Telemetry/ExceptionSanitizer.ts +222 -0
  31. package/Server/Utils/Telemetry/TelemetryFanInWriter.ts +799 -0
  32. package/Server/Utils/Telemetry/TelemetryWriterClient.ts +263 -0
  33. package/Server/Utils/Telemetry/TelemetryWriterServer.ts +240 -0
  34. package/Server/Utils/Telemetry/TelemetryWriterShedMetrics.ts +93 -0
  35. package/Tests/Server/Services/DatabaseServiceSanitizeUpdateData.test.ts +232 -0
  36. package/Tests/Server/Services/TelemetryExceptionCodeFixRun.test.ts +205 -1
  37. package/Tests/Server/Services/UpdateOneByIdKeepsRowId.test.ts +107 -0
  38. package/Tests/Server/Services/WorkflowService.test.ts +308 -0
  39. package/Tests/Server/Utils/AI/Insights/InsightScanner.test.ts +150 -141
  40. package/Tests/Server/Utils/AI/Insights/InsightTriageRunner.test.ts +277 -1
  41. package/Tests/Server/Utils/Attribution.test.ts +203 -0
  42. package/Tests/Server/Utils/ExceptionSanitizer.test.ts +70 -0
  43. package/Tests/Server/Utils/InsightTriageClassification.test.ts +57 -0
  44. package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +183 -0
  45. package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +340 -0
  46. package/Tests/Server/Utils/Monitor/Criteria/ExceptionMonitorCriteria.test.ts +87 -0
  47. package/Tests/Server/Utils/Monitor/Criteria/LogMonitorCriteria.test.ts +165 -0
  48. package/Tests/Server/Utils/Monitor/Criteria/TraceMonitorCriteria.test.ts +86 -0
  49. package/Tests/Server/Utils/Telemetry/TelemetryFanInWriter.test.ts +1565 -0
  50. package/Tests/Server/Utils/Telemetry/TelemetryWriterClient.test.ts +313 -0
  51. package/Tests/Server/Utils/Telemetry/TelemetryWriterServer.test.ts +270 -0
  52. package/Tests/Server/Utils/Telemetry/TelemetryWriterShedMetrics.test.ts +127 -0
  53. package/Tests/Server/Utils/TelemetryImprovementTaskTrigger.test.ts +141 -0
  54. package/Tests/Types/AI/CodeFixTaskType.test.ts +3 -0
  55. package/Tests/Types/Monitor/MonitorCriteriaMetricVariables.test.ts +228 -0
  56. package/Tests/Types/Monitor/MonitorStepDefaultTelemetryConfig.test.ts +169 -0
  57. package/Tests/Types/Monitor/MonitorStepExceptionMonitor.test.ts +289 -0
  58. package/Tests/Types/Monitor/MonitorStepLogMonitor.test.ts +231 -0
  59. package/Tests/Types/Monitor/MonitorStepMetricViewConfigUtil.test.ts +245 -0
  60. package/Tests/Types/Monitor/MonitorStepNetworkDeviceMonitor.test.ts +155 -0
  61. package/Tests/Types/Monitor/MonitorStepTelemetrySerialization.test.ts +141 -0
  62. package/Tests/Types/Monitor/MonitorStepTraceMonitor.test.ts +137 -0
  63. package/Tests/Types/Workspace/NotificationRules/NotificationRuleCondition.test.ts +311 -0
  64. package/Tests/UI/Components/ModelTableSelectFromColumns.test.ts +259 -0
  65. package/Tests/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.test.ts +461 -0
  66. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +2852 -0
  67. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +758 -0
  68. package/Tests/UI/Utils/NotificationMethodUtil.test.ts +564 -0
  69. package/Tests/Utils/Array.test.ts +156 -0
  70. package/Tests/Utils/CronTab.test.ts +199 -0
  71. package/Tests/Utils/ModelImportExport.test.ts +101 -0
  72. package/Tests/Utils/Number.test.ts +179 -0
  73. package/Tests/Utils/VersionUtil.test.ts +348 -0
  74. package/Tests/jest.setup.ts +1 -0
  75. package/Types/AI/CodeFixTaskContext.ts +6 -0
  76. package/Types/AI/CodeFixTaskType.ts +38 -1
  77. package/Types/AI/ExceptionAIClassification.ts +27 -0
  78. package/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.ts +6 -0
  79. package/Types/Log/LogScrubPatternType.ts +7 -0
  80. package/Types/Monitor/MonitorStep.ts +24 -4
  81. package/Types/Monitor/MonitorStepMetricViewConfigUtil.ts +105 -0
  82. package/Types/Trace/TraceScrubPatternType.ts +7 -0
  83. package/UI/Components/EditionLabel/EditionLabel.tsx +445 -12
  84. package/UI/Components/ModelTable/BaseModelTable.tsx +15 -46
  85. package/UI/Components/ModelTable/SelectFromColumns.ts +129 -0
  86. package/UI/Components/Workflow/ArgumentsForm.tsx +68 -30
  87. package/UI/Components/Workflow/CronScheduleField.tsx +411 -0
  88. package/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.ts +176 -0
  89. package/UI/Utils/NotificationMethodUtil.ts +310 -0
  90. package/Utils/CronTab.ts +823 -0
  91. package/Utils/ModelImportExport.ts +21 -6
  92. package/Utils/VersionUtil.ts +260 -0
  93. package/build/dist/Models/DatabaseModels/AIInsight.js +31 -0
  94. package/build/dist/Models/DatabaseModels/AIInsight.js.map +1 -1
  95. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +22 -0
  96. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
  97. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js +26 -0
  98. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js.map +1 -1
  99. package/build/dist/Models/DatabaseModels/GlobalConfig.js +81 -0
  100. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  101. package/build/dist/Models/DatabaseModels/Project.js +31 -0
  102. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  103. package/build/dist/Models/DatabaseModels/TelemetryException.js +134 -0
  104. package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
  105. package/build/dist/Server/API/AIAgentDataAPI.js +97 -8
  106. package/build/dist/Server/API/AIAgentDataAPI.js.map +1 -1
  107. package/build/dist/Server/API/AIInvestigationAPI.js +55 -0
  108. package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
  109. package/build/dist/Server/API/EnterpriseLicenseAPI.js +47 -0
  110. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
  111. package/build/dist/Server/API/GlobalConfigAPI.js +52 -1
  112. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  113. package/build/dist/Server/EnvironmentConfig.js +17 -0
  114. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  115. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784640000000-AddExceptionTriageAndPrivacyColumns.js +33 -0
  116. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784640000000-AddExceptionTriageAndPrivacyColumns.js.map +1 -0
  117. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.js +18 -0
  118. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.js.map +1 -0
  119. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.js +14 -0
  120. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.js.map +1 -0
  121. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  122. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  123. package/build/dist/Server/Services/AIRunService.js +9 -2
  124. package/build/dist/Server/Services/AIRunService.js.map +1 -1
  125. package/build/dist/Server/Services/AnalyticsDatabaseService.js +44 -16
  126. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  127. package/build/dist/Server/Services/DatabaseService.js +53 -1
  128. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  129. package/build/dist/Server/Services/TelemetryExceptionService.js +263 -22
  130. package/build/dist/Server/Services/TelemetryExceptionService.js.map +1 -1
  131. package/build/dist/Server/Services/WorkflowService.js +80 -30
  132. package/build/dist/Server/Services/WorkflowService.js.map +1 -1
  133. package/build/dist/Server/Types/Workflow/Components/Schedule.js +44 -19
  134. package/build/dist/Server/Types/Workflow/Components/Schedule.js.map +1 -1
  135. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.js +12 -4
  136. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.js.map +1 -1
  137. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.js +12 -4
  138. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.js.map +1 -1
  139. package/build/dist/Server/Utils/AI/SRE/Insights/InsightScanner.js +193 -22
  140. package/build/dist/Server/Utils/AI/SRE/Insights/InsightScanner.js.map +1 -1
  141. package/build/dist/Server/Utils/AI/SRE/Insights/InsightTriageRunner.js +178 -9
  142. package/build/dist/Server/Utils/AI/SRE/Insights/InsightTriageRunner.js.map +1 -1
  143. package/build/dist/Server/Utils/AI/SRE/SubjectCodeFixRun.js +37 -0
  144. package/build/dist/Server/Utils/AI/SRE/SubjectCodeFixRun.js.map +1 -1
  145. package/build/dist/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.js +98 -0
  146. package/build/dist/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.js.map +1 -0
  147. package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js +24 -0
  148. package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js.map +1 -0
  149. package/build/dist/Server/Utils/Express.js +12 -0
  150. package/build/dist/Server/Utils/Express.js.map +1 -1
  151. package/build/dist/Server/Utils/Telemetry/ExceptionSanitizer.js +149 -0
  152. package/build/dist/Server/Utils/Telemetry/ExceptionSanitizer.js.map +1 -0
  153. package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js +496 -0
  154. package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js.map +1 -0
  155. package/build/dist/Server/Utils/Telemetry/TelemetryWriterClient.js +165 -0
  156. package/build/dist/Server/Utils/Telemetry/TelemetryWriterClient.js.map +1 -0
  157. package/build/dist/Server/Utils/Telemetry/TelemetryWriterServer.js +136 -0
  158. package/build/dist/Server/Utils/Telemetry/TelemetryWriterServer.js.map +1 -0
  159. package/build/dist/Server/Utils/Telemetry/TelemetryWriterShedMetrics.js +76 -0
  160. package/build/dist/Server/Utils/Telemetry/TelemetryWriterShedMetrics.js.map +1 -0
  161. package/build/dist/Types/AI/CodeFixTaskType.js +38 -1
  162. package/build/dist/Types/AI/CodeFixTaskType.js.map +1 -1
  163. package/build/dist/Types/AI/ExceptionAIClassification.js +28 -0
  164. package/build/dist/Types/AI/ExceptionAIClassification.js.map +1 -0
  165. package/build/dist/Types/Log/LogScrubPatternType.js +7 -0
  166. package/build/dist/Types/Log/LogScrubPatternType.js.map +1 -1
  167. package/build/dist/Types/Monitor/MonitorStep.js +20 -4
  168. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  169. package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js +73 -0
  170. package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js.map +1 -0
  171. package/build/dist/Types/Trace/TraceScrubPatternType.js +7 -0
  172. package/build/dist/Types/Trace/TraceScrubPatternType.js.map +1 -1
  173. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +261 -13
  174. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  175. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +12 -37
  176. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  177. package/build/dist/UI/Components/ModelTable/SelectFromColumns.js +82 -0
  178. package/build/dist/UI/Components/ModelTable/SelectFromColumns.js.map +1 -0
  179. package/build/dist/UI/Components/Workflow/ArgumentsForm.js +30 -6
  180. package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
  181. package/build/dist/UI/Components/Workflow/CronScheduleField.js +209 -0
  182. package/build/dist/UI/Components/Workflow/CronScheduleField.js.map +1 -0
  183. package/build/dist/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.js +129 -0
  184. package/build/dist/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.js.map +1 -0
  185. package/build/dist/UI/Utils/NotificationMethodUtil.js +189 -0
  186. package/build/dist/UI/Utils/NotificationMethodUtil.js.map +1 -0
  187. package/build/dist/Utils/CronTab.js +609 -0
  188. package/build/dist/Utils/CronTab.js.map +1 -0
  189. package/build/dist/Utils/ModelImportExport.js +20 -6
  190. package/build/dist/Utils/ModelImportExport.js.map +1 -1
  191. package/build/dist/Utils/VersionUtil.js +193 -0
  192. package/build/dist/Utils/VersionUtil.js.map +1 -0
  193. package/package.json +1 -1
@@ -0,0 +1,461 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import realBreadcrumbTrails, {
3
+ BreadcrumbTrailFixture,
4
+ } from "./fixtures/RealBreadcrumbTrails";
5
+ import rawRealRoutePatterns from "./fixtures/RealRoutePatterns";
6
+ import {
7
+ BreadcrumbTarget,
8
+ BreadcrumbTargetKind,
9
+ normalizeRoutePath,
10
+ resolveBreadcrumbTarget,
11
+ resolveBreadcrumbTrail,
12
+ } from "../../../../UI/Utils/Breadcrumb/BreadcrumbTrailResolver";
13
+
14
+ /*
15
+ * Callers pass only real, navigable route patterns — mount points that only
16
+ * nest child routers ("/foo/*") are not pages. The Dashboard adapter filters
17
+ * them out before calling the resolver; the tests mirror that contract.
18
+ */
19
+ const REAL_ROUTES: Array<string> = rawRealRoutePatterns.filter(
20
+ (pattern: string): boolean => {
21
+ return !pattern.endsWith("/*");
22
+ },
23
+ );
24
+
25
+ const REAL_ROUTE_SET: Set<string> = new Set(
26
+ REAL_ROUTES.map(normalizeRoutePath),
27
+ );
28
+
29
+ const PROJECT: string = "/dashboard/:projectId";
30
+
31
+ /*
32
+ * The concrete route pattern a resolved target points at, so we can assert it is
33
+ * a real, navigable route.
34
+ */
35
+ function resolvedPattern(
36
+ target: BreadcrumbTarget,
37
+ pagePattern: string,
38
+ ): string {
39
+ const segments: Array<string> = normalizeRoutePath(pagePattern).split("/");
40
+ switch (target.kind) {
41
+ case BreadcrumbTargetKind.Home:
42
+ return segments.slice(0, target.depth ?? 3).join("/");
43
+ case BreadcrumbTargetKind.Ancestor:
44
+ return segments.slice(0, target.depth ?? 3).join("/");
45
+ case BreadcrumbTargetKind.SectionLanding:
46
+ return target.routePattern ?? "";
47
+ case BreadcrumbTargetKind.CurrentPage:
48
+ default:
49
+ return normalizeRoutePath(pagePattern);
50
+ }
51
+ }
52
+
53
+ // Home / Ancestor / SectionLanding render as links; CurrentPage renders as text.
54
+ function isLink(target: BreadcrumbTarget): boolean {
55
+ return target.kind !== BreadcrumbTargetKind.CurrentPage;
56
+ }
57
+
58
+ function resolveOne(options: {
59
+ index: number;
60
+ crumbCount: number;
61
+ pagePattern: string;
62
+ realRoutePatterns?: Array<string>;
63
+ }): BreadcrumbTarget {
64
+ return resolveBreadcrumbTarget({
65
+ index: options.index,
66
+ crumbCount: options.crumbCount,
67
+ pagePattern: options.pagePattern,
68
+ realRoutePatterns: options.realRoutePatterns ?? REAL_ROUTES,
69
+ });
70
+ }
71
+
72
+ describe("BreadcrumbTrailResolver", () => {
73
+ describe("normalizeRoutePath", () => {
74
+ test("strips a single trailing slash", () => {
75
+ expect(normalizeRoutePath("/dashboard/:projectId/settings/")).toBe(
76
+ "/dashboard/:projectId/settings",
77
+ );
78
+ });
79
+
80
+ test("strips repeated trailing slashes", () => {
81
+ expect(normalizeRoutePath("/a/b///")).toBe("/a/b");
82
+ });
83
+
84
+ test("leaves a path without a trailing slash unchanged", () => {
85
+ expect(normalizeRoutePath("/a/b")).toBe("/a/b");
86
+ });
87
+
88
+ test("keeps the root path intact", () => {
89
+ expect(normalizeRoutePath("/")).toBe("/");
90
+ });
91
+
92
+ test("handles an empty string", () => {
93
+ expect(normalizeRoutePath("")).toBe("");
94
+ });
95
+ });
96
+
97
+ describe("crumb position rules", () => {
98
+ test("the first crumb always links to the project home", () => {
99
+ const target: BreadcrumbTarget = resolveOne({
100
+ index: 0,
101
+ crumbCount: 3,
102
+ pagePattern: `${PROJECT}/user-settings/notification-methods`,
103
+ });
104
+ expect(target.kind).toBe(BreadcrumbTargetKind.Home);
105
+ expect(target.depth).toBe(3);
106
+ expect(resolvedPattern(target, `${PROJECT}/anything`)).toBe(PROJECT);
107
+ });
108
+
109
+ test("the last crumb is always the current page (plain text)", () => {
110
+ const target: BreadcrumbTarget = resolveOne({
111
+ index: 2,
112
+ crumbCount: 3,
113
+ pagePattern: `${PROJECT}/user-settings/notification-methods`,
114
+ });
115
+ expect(target.kind).toBe(BreadcrumbTargetKind.CurrentPage);
116
+ expect(isLink(target)).toBe(false);
117
+ });
118
+
119
+ test("a two-crumb trail is home then current page", () => {
120
+ const targets: Array<BreadcrumbTarget> = resolveBreadcrumbTrail({
121
+ pagePattern: `${PROJECT}/monitors`,
122
+ crumbCount: 2,
123
+ realRoutePatterns: REAL_ROUTES,
124
+ });
125
+ expect(
126
+ targets.map((t: BreadcrumbTarget) => {
127
+ return t.kind;
128
+ }),
129
+ ).toEqual([BreadcrumbTargetKind.Home, BreadcrumbTargetKind.CurrentPage]);
130
+ });
131
+ });
132
+
133
+ describe("intermediate crumb pointing at a real section list page", () => {
134
+ test('"Traces" links to /traces on a /traces/settings/pipelines page', () => {
135
+ const target: BreadcrumbTarget = resolveOne({
136
+ index: 1,
137
+ crumbCount: 4,
138
+ pagePattern: `${PROJECT}/traces/settings/pipelines`,
139
+ });
140
+ expect(target.kind).toBe(BreadcrumbTargetKind.Ancestor);
141
+ expect(
142
+ resolvedPattern(target, `${PROJECT}/traces/settings/pipelines`),
143
+ ).toBe(`${PROJECT}/traces`);
144
+ });
145
+ });
146
+
147
+ describe("the reported bug: User Settings middle crumb", () => {
148
+ const page: string = `${PROJECT}/user-settings/notification-methods`;
149
+
150
+ test('"User Settings" never links to the bare /user-settings prefix', () => {
151
+ const target: BreadcrumbTarget = resolveOne({
152
+ index: 1,
153
+ crumbCount: 3,
154
+ pagePattern: page,
155
+ });
156
+ // The bare prefix has no page — that is exactly what blanked the dashboard.
157
+ expect(resolvedPattern(target, page)).not.toBe(
158
+ `${PROJECT}/user-settings`,
159
+ );
160
+ });
161
+
162
+ test('"User Settings" is plain text on the User Settings landing page', () => {
163
+ // notification-methods IS the section landing, so its own crumb is text.
164
+ const target: BreadcrumbTarget = resolveOne({
165
+ index: 1,
166
+ crumbCount: 3,
167
+ pagePattern: page,
168
+ });
169
+ expect(target.kind).toBe(BreadcrumbTargetKind.CurrentPage);
170
+ });
171
+
172
+ test('"User Settings" links to the landing page from a sibling settings page', () => {
173
+ const target: BreadcrumbTarget = resolveOne({
174
+ index: 1,
175
+ crumbCount: 3,
176
+ pagePattern: `${PROJECT}/user-settings/notification-settings`,
177
+ });
178
+ expect(target.kind).toBe(BreadcrumbTargetKind.SectionLanding);
179
+ expect(target.routePattern).toBe(
180
+ `${PROJECT}/user-settings/notification-methods`,
181
+ );
182
+ expect(REAL_ROUTE_SET.has(target.routePattern ?? "")).toBe(true);
183
+ });
184
+ });
185
+
186
+ describe("bare section prefix with an off-path landing", () => {
187
+ test('"On-Call Duty" links to /policies while viewing /schedules', () => {
188
+ const target: BreadcrumbTarget = resolveOne({
189
+ index: 1,
190
+ crumbCount: 3,
191
+ pagePattern: `${PROJECT}/on-call-duty/schedules`,
192
+ });
193
+ expect(target.kind).toBe(BreadcrumbTargetKind.SectionLanding);
194
+ expect(target.routePattern).toBe(`${PROJECT}/on-call-duty/policies`);
195
+ });
196
+
197
+ test('"On-Call Duty" is plain text on the /policies landing page itself', () => {
198
+ const target: BreadcrumbTarget = resolveOne({
199
+ index: 1,
200
+ crumbCount: 3,
201
+ pagePattern: `${PROJECT}/on-call-duty/policies`,
202
+ });
203
+ expect(target.kind).toBe(BreadcrumbTargetKind.CurrentPage);
204
+ });
205
+
206
+ test('"Settings" links to a real settings tab on a /monitors/settings/probes page', () => {
207
+ const target: BreadcrumbTarget = resolveOne({
208
+ index: 2,
209
+ crumbCount: 4,
210
+ pagePattern: `${PROJECT}/monitors/settings/probes`,
211
+ });
212
+ // /monitors/settings has no page of its own; land on the first real tab.
213
+ expect(target.kind).toBe(BreadcrumbTargetKind.SectionLanding);
214
+ expect(target.routePattern).toBe(`${PROJECT}/monitors/settings/status`);
215
+ expect(REAL_ROUTE_SET.has(target.routePattern ?? "")).toBe(true);
216
+ });
217
+ });
218
+
219
+ describe("nearest real ancestor on the current path", () => {
220
+ test('"Insights" links to /ai/insights on an /ai/insights/settings page', () => {
221
+ const target: BreadcrumbTarget = resolveOne({
222
+ index: 1,
223
+ crumbCount: 3,
224
+ pagePattern: `${PROJECT}/ai/insights/settings`,
225
+ });
226
+ expect(target.kind).toBe(BreadcrumbTargetKind.Ancestor);
227
+ expect(resolvedPattern(target, `${PROJECT}/ai/insights/settings`)).toBe(
228
+ `${PROJECT}/ai/insights`,
229
+ );
230
+ });
231
+
232
+ test('"AI Tasks" links to /ai/agents on an /ai/agents/:id page', () => {
233
+ const target: BreadcrumbTarget = resolveOne({
234
+ index: 1,
235
+ crumbCount: 3,
236
+ pagePattern: `${PROJECT}/ai/agents/:id`,
237
+ });
238
+ expect(target.kind).toBe(BreadcrumbTargetKind.Ancestor);
239
+ expect(resolvedPattern(target, `${PROJECT}/ai/agents/:id`)).toBe(
240
+ `${PROJECT}/ai/agents`,
241
+ );
242
+ });
243
+ });
244
+
245
+ describe("leaf detail pages (the truncated-id bug)", () => {
246
+ test('"Trace Details" is plain text, not a link to /traces/view', () => {
247
+ const target: BreadcrumbTarget = resolveOne({
248
+ index: 2,
249
+ crumbCount: 3,
250
+ pagePattern: `${PROJECT}/traces/view/:id`,
251
+ });
252
+ // The naive truncation would have produced the non-existent /traces/view.
253
+ expect(target.kind).toBe(BreadcrumbTargetKind.CurrentPage);
254
+ });
255
+
256
+ test('"Traces" still links to /traces on a /traces/view/:id page', () => {
257
+ const target: BreadcrumbTarget = resolveOne({
258
+ index: 1,
259
+ crumbCount: 3,
260
+ pagePattern: `${PROJECT}/traces/view/:id`,
261
+ });
262
+ expect(target.kind).toBe(BreadcrumbTargetKind.Ancestor);
263
+ expect(resolvedPattern(target, `${PROJECT}/traces/view/:id`)).toBe(
264
+ `${PROJECT}/traces`,
265
+ );
266
+ });
267
+ });
268
+
269
+ describe("defensive fallbacks on synthetic route tables", () => {
270
+ test("a prefix with no page at all resolves to plain text", () => {
271
+ const target: BreadcrumbTarget = resolveOne({
272
+ index: 1,
273
+ crumbCount: 3,
274
+ pagePattern: "/dashboard/:projectId/ghost/leaf",
275
+ realRoutePatterns: ["/dashboard/:projectId/ghost/leaf"],
276
+ });
277
+ // Only the page itself exists; the "ghost" prefix has no landing.
278
+ expect(target.kind).toBe(BreadcrumbTargetKind.CurrentPage);
279
+ });
280
+
281
+ test("trailing-slash-only page routes are matched after normalization", () => {
282
+ const target: BreadcrumbTarget = resolveOne({
283
+ index: 1,
284
+ crumbCount: 3,
285
+ pagePattern: "/dashboard/:projectId/section/leaf",
286
+ realRoutePatterns: [
287
+ "/dashboard/:projectId/section/", // real, only differs by trailing slash
288
+ "/dashboard/:projectId/section/leaf",
289
+ ],
290
+ });
291
+ expect(target.kind).toBe(BreadcrumbTargetKind.Ancestor);
292
+ expect(
293
+ resolvedPattern(target, "/dashboard/:projectId/section/leaf"),
294
+ ).toBe("/dashboard/:projectId/section");
295
+ });
296
+
297
+ test("more titles than URL segments resolves to plain text, never a broken link", () => {
298
+ const target: BreadcrumbTarget = resolveOne({
299
+ index: 2,
300
+ crumbCount: 5,
301
+ pagePattern: "/dashboard/:projectId/short",
302
+ realRoutePatterns: ["/dashboard/:projectId/short"],
303
+ });
304
+ expect(target.kind).toBe(BreadcrumbTargetKind.CurrentPage);
305
+ });
306
+
307
+ test("a packed trail crumb sharing the page's terminal segment stays plain text", () => {
308
+ /*
309
+ * Profiles Overview: "Project > Profiler > Overview" on /profiles, where
310
+ * "Profiler" and "Overview" both map onto the single /profiles segment.
311
+ * "Profiler" must not become a link to a sibling like /profiles/insights.
312
+ */
313
+ const target: BreadcrumbTarget = resolveOne({
314
+ index: 1,
315
+ crumbCount: 3,
316
+ pagePattern: `${PROJECT}/profiles`,
317
+ realRoutePatterns: [
318
+ `${PROJECT}/profiles`,
319
+ `${PROJECT}/profiles/insights`,
320
+ ],
321
+ });
322
+ expect(target.kind).toBe(BreadcrumbTargetKind.CurrentPage);
323
+ });
324
+
325
+ test("a bare section prefix picks the first declared child as its landing", () => {
326
+ const target: BreadcrumbTarget = resolveOne({
327
+ index: 1,
328
+ crumbCount: 3,
329
+ pagePattern: "/dashboard/:projectId/sect/two",
330
+ realRoutePatterns: [
331
+ "/dashboard/:projectId/sect/one", // first declared -> landing
332
+ "/dashboard/:projectId/sect/two",
333
+ ],
334
+ });
335
+ expect(target.kind).toBe(BreadcrumbTargetKind.SectionLanding);
336
+ expect(target.routePattern).toBe("/dashboard/:projectId/sect/one");
337
+ });
338
+ });
339
+
340
+ /*
341
+ * The core guarantee. For every real breadcrumb trail in the app, no crumb may
342
+ * ever resolve to a route that does not exist — that is precisely the class of
343
+ * bug that blanked the dashboard.
344
+ */
345
+ describe("invariant: no crumb links to a non-existent route (all real trails)", () => {
346
+ test("the fixture covers a broad, representative set of the app's trails", () => {
347
+ expect(realBreadcrumbTrails.length).toBeGreaterThan(400);
348
+ const sections: Set<string> = new Set(
349
+ realBreadcrumbTrails.map((trail: BreadcrumbTrailFixture): string => {
350
+ return normalizeRoutePath(trail.pagePattern).split("/")[3] ?? "";
351
+ }),
352
+ );
353
+ /*
354
+ * Every top-level product section should be represented, including the
355
+ * originally-reported user-settings section.
356
+ */
357
+ expect(sections.has("user-settings")).toBe(true);
358
+ expect(sections.has("on-call-duty")).toBe(true);
359
+ expect(sections.size).toBeGreaterThan(25);
360
+ });
361
+
362
+ test.each(
363
+ realBreadcrumbTrails.map((trail: BreadcrumbTrailFixture) => {
364
+ return [`${trail.getter} :: ${trail.pagePattern}`, trail] as [
365
+ string,
366
+ BreadcrumbTrailFixture,
367
+ ];
368
+ }),
369
+ )("%s", (_label: string, trail: BreadcrumbTrailFixture) => {
370
+ const targets: Array<BreadcrumbTarget> = resolveBreadcrumbTrail({
371
+ pagePattern: trail.pagePattern,
372
+ crumbCount: trail.titles.length,
373
+ realRoutePatterns: REAL_ROUTES,
374
+ });
375
+
376
+ expect(targets).toHaveLength(trail.titles.length);
377
+
378
+ // First crumb -> home; last crumb -> current page.
379
+ expect(targets[0]?.kind).toBe(BreadcrumbTargetKind.Home);
380
+ expect(targets[targets.length - 1]?.kind).toBe(
381
+ BreadcrumbTargetKind.CurrentPage,
382
+ );
383
+
384
+ const normalizedPage: string = normalizeRoutePath(trail.pagePattern);
385
+
386
+ targets.forEach((target: BreadcrumbTarget) => {
387
+ if (!isLink(target)) {
388
+ return;
389
+ }
390
+ const pattern: string = normalizeRoutePath(
391
+ resolvedPattern(target, trail.pagePattern),
392
+ );
393
+
394
+ // A link must always resolve to a real, navigable route.
395
+ expect(REAL_ROUTE_SET.has(pattern)).toBe(true);
396
+
397
+ /*
398
+ * An intermediate link must not point at the current page (that would be
399
+ * rendered as plain text instead).
400
+ */
401
+ if (target.kind === BreadcrumbTargetKind.SectionLanding) {
402
+ expect(pattern).not.toBe(normalizedPage);
403
+ }
404
+ });
405
+ });
406
+
407
+ test("no real trail produces a link to a bare, page-less section prefix", () => {
408
+ const deadPrefixes: Array<string> = [
409
+ `${PROJECT}/user-settings`,
410
+ `${PROJECT}/on-call-duty`,
411
+ `${PROJECT}/ai`,
412
+ `${PROJECT}/llm`,
413
+ `${PROJECT}/monitors/settings`,
414
+ `${PROJECT}/incidents/settings`,
415
+ `${PROJECT}/alerts/settings`,
416
+ `${PROJECT}/traces/settings`,
417
+ `${PROJECT}/traces/view`,
418
+ `${PROJECT}/profiles/view`,
419
+ ];
420
+
421
+ // Sanity: these prefixes really are not routes.
422
+ deadPrefixes.forEach((prefix: string) => {
423
+ expect(REAL_ROUTE_SET.has(prefix)).toBe(false);
424
+ });
425
+
426
+ /*
427
+ * Guard against a vacuous pass: every dead prefix must belong to a section
428
+ * that the fixture actually exercises (has at least one trail underneath
429
+ * it). Without this, a section silently dropping out of the fixture would
430
+ * make its guarantee unreachable while the test still passed green.
431
+ */
432
+ deadPrefixes.forEach((prefix: string) => {
433
+ const covered: boolean = realBreadcrumbTrails.some(
434
+ (trail: BreadcrumbTrailFixture): boolean => {
435
+ return normalizeRoutePath(trail.pagePattern).startsWith(
436
+ `${prefix}/`,
437
+ );
438
+ },
439
+ );
440
+ expect(covered).toBe(true);
441
+ });
442
+
443
+ realBreadcrumbTrails.forEach((trail: BreadcrumbTrailFixture) => {
444
+ const targets: Array<BreadcrumbTarget> = resolveBreadcrumbTrail({
445
+ pagePattern: trail.pagePattern,
446
+ crumbCount: trail.titles.length,
447
+ realRoutePatterns: REAL_ROUTES,
448
+ });
449
+ targets.forEach((target: BreadcrumbTarget) => {
450
+ if (!isLink(target)) {
451
+ return;
452
+ }
453
+ const pattern: string = normalizeRoutePath(
454
+ resolvedPattern(target, trail.pagePattern),
455
+ );
456
+ expect(deadPrefixes).not.toContain(pattern);
457
+ });
458
+ });
459
+ });
460
+ });
461
+ });