@oneuptime/common 12.0.0 → 12.0.2

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 (237) hide show
  1. package/Models/DatabaseModels/Host.ts +12 -4
  2. package/Models/DatabaseModels/KubernetesContainer.ts +5 -4
  3. package/Models/DatabaseModels/KubernetesResource.ts +8 -7
  4. package/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.ts +12 -2
  5. package/Models/DatabaseModels/PodmanResource.ts +6 -6
  6. package/Models/DatabaseModels/ServiceLevelObjective.ts +91 -0
  7. package/Models/DatabaseModels/StatusPage.ts +95 -0
  8. package/Server/API/TeamMemberAPI.ts +85 -0
  9. package/Server/Infrastructure/Postgres/LocalMigrationGenerationDataSource.ts +12 -2
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.ts +84 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.ts +40 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.ts +131 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.ts +25 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  15. package/Server/Middleware/ProjectAuthorization.ts +66 -4
  16. package/Server/Services/BillingService.ts +600 -51
  17. package/Server/Services/CephClusterService.ts +47 -5
  18. package/Server/Services/CephResourceService.ts +43 -4
  19. package/Server/Services/CloudResourceService.ts +47 -5
  20. package/Server/Services/DatabaseService.ts +87 -6
  21. package/Server/Services/DockerHostService.ts +47 -5
  22. package/Server/Services/DockerResourceService.ts +62 -8
  23. package/Server/Services/DockerSwarmClusterService.ts +48 -5
  24. package/Server/Services/DockerSwarmResourceService.ts +53 -4
  25. package/Server/Services/HostService.ts +48 -5
  26. package/Server/Services/IoTDeviceService.ts +1 -17
  27. package/Server/Services/IoTFleetService.ts +47 -5
  28. package/Server/Services/KubernetesClusterService.ts +48 -5
  29. package/Server/Services/KubernetesContainerService.ts +50 -9
  30. package/Server/Services/KubernetesResourceService.ts +53 -7
  31. package/Server/Services/LabelService.ts +106 -1
  32. package/Server/Services/LlmProviderService.ts +69 -9
  33. package/Server/Services/MonitorService.ts +57 -0
  34. package/Server/Services/PodmanHostService.ts +47 -5
  35. package/Server/Services/PodmanResourceService.ts +62 -8
  36. package/Server/Services/ProjectService.ts +165 -35
  37. package/Server/Services/ProxmoxClusterService.ts +47 -5
  38. package/Server/Services/ProxmoxResourceService.ts +43 -4
  39. package/Server/Services/RumApplicationService.ts +48 -5
  40. package/Server/Services/RunnerService.ts +1 -7
  41. package/Server/Services/ServerlessFunctionService.ts +48 -5
  42. package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +426 -0
  43. package/Server/Services/ServiceLevelObjectiveService.ts +39 -0
  44. package/Server/Services/StatusPageService.ts +95 -36
  45. package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +18 -1
  46. package/Server/Services/StatusPageSubscriberService.ts +3 -0
  47. package/Server/Types/Database/JSONColumnQuery.ts +621 -0
  48. package/Server/Types/Database/Permissions/PublicPermission.ts +9 -2
  49. package/Server/Types/Database/QueryHelper.ts +17 -32
  50. package/Server/Utils/Database/TruncateColumnValue.ts +84 -0
  51. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +12 -0
  52. package/Tests/App/Dashboard/RunnerInstallInstructions.test.tsx +119 -0
  53. package/Tests/App/Dashboard/RunnerStatus.test.tsx +274 -0
  54. package/Tests/App/Dashboard/UsersTableGroupsByPerson.test.tsx +448 -0
  55. package/Tests/Server/API/BaseAPIApiKeyAuth.test.ts +704 -0
  56. package/Tests/Server/API/TeamMemberRemoveUserFromProjectAPI.test.ts +303 -0
  57. package/Tests/Server/Middleware/MasterAdminAuthorization.test.ts +350 -0
  58. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyHeader.test.ts +362 -0
  59. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +687 -0
  60. package/Tests/Server/Services/BillingService.test.ts +78 -3
  61. package/Tests/Server/Services/BillingServiceChangePlanCancel.test.ts +645 -0
  62. package/Tests/Server/Services/BillingServiceTrialPlanChange.test.ts +1005 -0
  63. package/Tests/Server/Services/HookFreeWriteLengthClamp.test.ts +503 -0
  64. package/Tests/Server/Services/HostIpAddressesColumnWidth.test.ts +198 -0
  65. package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +510 -0
  66. package/Tests/Server/Services/InventoryResourceNameColumnWidth.test.ts +500 -0
  67. package/Tests/Server/Services/InventoryUpsertSchemaParity.test.ts +586 -0
  68. package/Tests/Server/Services/InventoryUpsertTruncationGuard.test.ts +1242 -0
  69. package/Tests/Server/Services/LlmProviderUsableByProject.test.ts +293 -0
  70. package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +650 -0
  71. package/Tests/Server/Services/ProjectServiceExtendTrial.test.ts +1 -0
  72. package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +387 -0
  73. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +770 -0
  74. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +130 -0
  75. package/Tests/Server/Services/SloMonitorLabelRuleHooks.test.ts +352 -0
  76. package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +29 -13
  77. package/Tests/Server/TestingUtils/Services/BillingServiceHelper.ts +69 -8
  78. package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +593 -0
  79. package/Tests/Server/Types/Database/Permissions/PublicPermission.test.ts +495 -0
  80. package/Tests/Server/Types/Database/QueryUtil.test.ts +113 -0
  81. package/Tests/Server/Utils/AI/ToolArgsExtractors.test.ts +141 -0
  82. package/Tests/Server/Utils/AI/Toolbox/WidgetBuilder.test.ts +205 -0
  83. package/Tests/Server/Utils/Database/TruncateColumnValue.test.ts +179 -0
  84. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +897 -0
  85. package/Tests/Types/Events/Recurring.test.ts +157 -0
  86. package/Tests/Types/Runner/RunnerLiveStatus.test.ts +320 -0
  87. package/Tests/UI/Components/AiInvestigationSettingsCard.test.tsx +285 -0
  88. package/Tests/UI/Components/Detail/EntityFields.test.tsx +166 -0
  89. package/Tests/UI/Components/ModelTable/ModelTableGroupsProjectUsers.test.tsx +375 -0
  90. package/Tests/UI/Components/ModelTable/useCustomFieldColumns.test.tsx +196 -0
  91. package/Tests/UI/Utils/ProjectUsersModelAPI.test.ts +739 -0
  92. package/Tests/UI/Utils/TeamMembersByUser.test.ts +633 -0
  93. package/Tests/Utils/StatusPage/ReportPeriod.test.ts +218 -0
  94. package/Tests/Utils/Telemetry/HostIpAddresses.test.ts +291 -0
  95. package/Types/CustomField/CustomFieldDefinition.ts +24 -0
  96. package/Types/Date.ts +112 -0
  97. package/Types/Events/Recurring.ts +99 -1
  98. package/Types/Runbook/RunbookStep.ts +15 -0
  99. package/Types/Runner/RunnerLiveStatus.ts +114 -0
  100. package/Types/StatusPage/StatusPageReport.ts +11 -0
  101. package/Types/StatusPage/StatusPageReportPeriodType.ts +21 -0
  102. package/UI/Components/Detail/Detail.tsx +155 -0
  103. package/UI/Components/ModelTable/CustomFieldColumns.tsx +2 -6
  104. package/UI/Components/ModelTable/useCustomFieldColumns.ts +21 -2
  105. package/UI/Utils/ModelAPI/ProjectUsersModelAPI.ts +305 -0
  106. package/UI/Utils/TeamMembersByUser.ts +302 -0
  107. package/Utils/StatusPage/ReportPeriod.ts +236 -0
  108. package/Utils/Telemetry/HostIpAddresses.ts +98 -0
  109. package/build/dist/Models/DatabaseModels/Host.js +12 -4
  110. package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
  111. package/build/dist/Models/DatabaseModels/KubernetesContainer.js +4 -4
  112. package/build/dist/Models/DatabaseModels/KubernetesContainer.js.map +1 -1
  113. package/build/dist/Models/DatabaseModels/KubernetesResource.js +7 -7
  114. package/build/dist/Models/DatabaseModels/KubernetesResource.js.map +1 -1
  115. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js +16 -4
  116. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js.map +1 -1
  117. package/build/dist/Models/DatabaseModels/PodmanResource.js +6 -6
  118. package/build/dist/Models/DatabaseModels/PodmanResource.js.map +1 -1
  119. package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js +87 -0
  120. package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js.map +1 -1
  121. package/build/dist/Models/DatabaseModels/StatusPage.js +98 -0
  122. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  123. package/build/dist/Server/API/TeamMemberAPI.js +57 -2
  124. package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
  125. package/build/dist/Server/Infrastructure/Postgres/LocalMigrationGenerationDataSource.js +11 -1
  126. package/build/dist/Server/Infrastructure/Postgres/LocalMigrationGenerationDataSource.js.map +1 -1
  127. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js +45 -0
  128. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js.map +1 -0
  129. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js +31 -0
  130. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js.map +1 -0
  131. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js +90 -0
  132. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js.map +1 -0
  133. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js +14 -0
  134. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js.map +1 -0
  135. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  136. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  137. package/build/dist/Server/Middleware/ProjectAuthorization.js +59 -4
  138. package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
  139. package/build/dist/Server/Services/BillingService.js +466 -36
  140. package/build/dist/Server/Services/BillingService.js.map +1 -1
  141. package/build/dist/Server/Services/CephClusterService.js +42 -5
  142. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  143. package/build/dist/Server/Services/CephResourceService.js +28 -3
  144. package/build/dist/Server/Services/CephResourceService.js.map +1 -1
  145. package/build/dist/Server/Services/CloudResourceService.js +42 -5
  146. package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
  147. package/build/dist/Server/Services/DatabaseService.js +69 -2
  148. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  149. package/build/dist/Server/Services/DockerHostService.js +42 -5
  150. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  151. package/build/dist/Server/Services/DockerResourceService.js +34 -4
  152. package/build/dist/Server/Services/DockerResourceService.js.map +1 -1
  153. package/build/dist/Server/Services/DockerSwarmClusterService.js +43 -5
  154. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  155. package/build/dist/Server/Services/DockerSwarmResourceService.js +28 -3
  156. package/build/dist/Server/Services/DockerSwarmResourceService.js.map +1 -1
  157. package/build/dist/Server/Services/HostService.js +43 -5
  158. package/build/dist/Server/Services/HostService.js.map +1 -1
  159. package/build/dist/Server/Services/IoTDeviceService.js +1 -8
  160. package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
  161. package/build/dist/Server/Services/IoTFleetService.js +42 -5
  162. package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
  163. package/build/dist/Server/Services/KubernetesClusterService.js +43 -5
  164. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  165. package/build/dist/Server/Services/KubernetesContainerService.js +29 -3
  166. package/build/dist/Server/Services/KubernetesContainerService.js.map +1 -1
  167. package/build/dist/Server/Services/KubernetesResourceService.js +32 -4
  168. package/build/dist/Server/Services/KubernetesResourceService.js.map +1 -1
  169. package/build/dist/Server/Services/LabelService.js +92 -0
  170. package/build/dist/Server/Services/LabelService.js.map +1 -1
  171. package/build/dist/Server/Services/LlmProviderService.js +61 -8
  172. package/build/dist/Server/Services/LlmProviderService.js.map +1 -1
  173. package/build/dist/Server/Services/MonitorService.js +47 -0
  174. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  175. package/build/dist/Server/Services/PodmanHostService.js +42 -5
  176. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  177. package/build/dist/Server/Services/PodmanResourceService.js +34 -4
  178. package/build/dist/Server/Services/PodmanResourceService.js.map +1 -1
  179. package/build/dist/Server/Services/ProjectService.js +135 -21
  180. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  181. package/build/dist/Server/Services/ProxmoxClusterService.js +42 -5
  182. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  183. package/build/dist/Server/Services/ProxmoxResourceService.js +28 -3
  184. package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
  185. package/build/dist/Server/Services/RumApplicationService.js +43 -5
  186. package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
  187. package/build/dist/Server/Services/RunnerService.js +1 -6
  188. package/build/dist/Server/Services/RunnerService.js.map +1 -1
  189. package/build/dist/Server/Services/ServerlessFunctionService.js +43 -5
  190. package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
  191. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +347 -0
  192. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -0
  193. package/build/dist/Server/Services/ServiceLevelObjectiveService.js +33 -1
  194. package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
  195. package/build/dist/Server/Services/StatusPageService.js +69 -46
  196. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  197. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +17 -1
  198. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
  199. package/build/dist/Server/Services/StatusPageSubscriberService.js +3 -0
  200. package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
  201. package/build/dist/Server/Types/Database/JSONColumnQuery.js +402 -0
  202. package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -0
  203. package/build/dist/Server/Types/Database/Permissions/PublicPermission.js +9 -2
  204. package/build/dist/Server/Types/Database/Permissions/PublicPermission.js.map +1 -1
  205. package/build/dist/Server/Types/Database/QueryHelper.js +17 -27
  206. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  207. package/build/dist/Server/Utils/Database/TruncateColumnValue.js +32 -0
  208. package/build/dist/Server/Utils/Database/TruncateColumnValue.js.map +1 -0
  209. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +11 -0
  210. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  211. package/build/dist/Types/CustomField/CustomFieldDefinition.js +2 -0
  212. package/build/dist/Types/CustomField/CustomFieldDefinition.js.map +1 -0
  213. package/build/dist/Types/Date.js +68 -0
  214. package/build/dist/Types/Date.js.map +1 -1
  215. package/build/dist/Types/Events/Recurring.js +63 -0
  216. package/build/dist/Types/Events/Recurring.js.map +1 -1
  217. package/build/dist/Types/Runbook/RunbookStep.js.map +1 -1
  218. package/build/dist/Types/Runner/RunnerLiveStatus.js +69 -0
  219. package/build/dist/Types/Runner/RunnerLiveStatus.js.map +1 -0
  220. package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js +22 -0
  221. package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js.map +1 -0
  222. package/build/dist/UI/Components/Detail/Detail.js +97 -0
  223. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  224. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
  225. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +18 -2
  226. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -1
  227. package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js +213 -0
  228. package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js.map +1 -0
  229. package/build/dist/UI/Utils/TeamMembersByUser.js +195 -0
  230. package/build/dist/UI/Utils/TeamMembersByUser.js.map +1 -0
  231. package/build/dist/Utils/StatusPage/ReportPeriod.js +131 -0
  232. package/build/dist/Utils/StatusPage/ReportPeriod.js.map +1 -0
  233. package/build/dist/Utils/Telemetry/HostIpAddresses.js +82 -0
  234. package/build/dist/Utils/Telemetry/HostIpAddresses.js.map +1 -0
  235. package/jest.config.json +2 -0
  236. package/package.json +1 -1
  237. package/tsconfig.json +12 -1
@@ -0,0 +1,293 @@
1
+ import LlmProviderService from "../../../Server/Services/LlmProviderService";
2
+ import LlmProvider from "../../../Models/DatabaseModels/LlmProvider";
3
+ import ObjectID from "../../../Types/ObjectID";
4
+ import Select from "../../../Server/Types/Database/Select";
5
+ import { describe, expect, test, afterEach } from "@jest/globals";
6
+
7
+ /*
8
+ * isProviderUsableByProject answers "may this project run against this
9
+ * provider id?" for callers that hold a caller-supplied id — today the
10
+ * runbook AI step's optional provider pin, whose id comes out of an
11
+ * unvalidated JSON column.
12
+ *
13
+ * The invariant: a project may use a global provider or one it owns, and
14
+ * nothing else. A provider carries an apiKey, so a yes here for another
15
+ * project's row would hand project A the use of project B's key.
16
+ */
17
+
18
+ const projectId: ObjectID = ObjectID.generate();
19
+ const otherProjectId: ObjectID = ObjectID.generate();
20
+ const providerId: ObjectID = ObjectID.generate();
21
+
22
+ function fakeProvider(overrides: Partial<LlmProvider> = {}): LlmProvider {
23
+ return {
24
+ id: providerId,
25
+ ...overrides,
26
+ } as unknown as LlmProvider;
27
+ }
28
+
29
+ describe("LlmProviderService.isProviderUsableByProject", () => {
30
+ afterEach(() => {
31
+ jest.restoreAllMocks();
32
+ });
33
+
34
+ test("accepts a provider the project owns", async () => {
35
+ jest
36
+ .spyOn(LlmProviderService, "findOneBy")
37
+ .mockResolvedValue(fakeProvider({ projectId, isGlobalLlm: false }));
38
+
39
+ await expect(
40
+ LlmProviderService.isProviderUsableByProject({
41
+ projectId,
42
+ llmProviderId: providerId,
43
+ }),
44
+ ).resolves.toBe(true);
45
+ });
46
+
47
+ test("accepts a global provider, which is shared with every project", async () => {
48
+ jest
49
+ .spyOn(LlmProviderService, "findOneBy")
50
+ .mockResolvedValue(fakeProvider({ isGlobalLlm: true }));
51
+
52
+ await expect(
53
+ LlmProviderService.isProviderUsableByProject({
54
+ projectId,
55
+ llmProviderId: providerId,
56
+ }),
57
+ ).resolves.toBe(true);
58
+ });
59
+
60
+ test("rejects a provider owned by a different project", async () => {
61
+ jest.spyOn(LlmProviderService, "findOneBy").mockResolvedValue(
62
+ fakeProvider({
63
+ projectId: otherProjectId,
64
+ isGlobalLlm: false,
65
+ }),
66
+ );
67
+
68
+ await expect(
69
+ LlmProviderService.isProviderUsableByProject({
70
+ projectId,
71
+ llmProviderId: providerId,
72
+ }),
73
+ ).resolves.toBe(false);
74
+ });
75
+
76
+ test("rejects a provider that no longer exists", async () => {
77
+ jest.spyOn(LlmProviderService, "findOneBy").mockResolvedValue(null);
78
+
79
+ await expect(
80
+ LlmProviderService.isProviderUsableByProject({
81
+ projectId,
82
+ llmProviderId: providerId,
83
+ }),
84
+ ).resolves.toBe(false);
85
+ });
86
+
87
+ test("rejects a provider with no owner and no global flag", async () => {
88
+ /*
89
+ * A row that is neither global nor owned matches nobody. Treating an
90
+ * absent projectId as a match would make an orphaned row usable by every
91
+ * project at once.
92
+ */
93
+ jest
94
+ .spyOn(LlmProviderService, "findOneBy")
95
+ .mockResolvedValue(fakeProvider({ isGlobalLlm: false }));
96
+
97
+ await expect(
98
+ LlmProviderService.isProviderUsableByProject({
99
+ projectId,
100
+ llmProviderId: providerId,
101
+ }),
102
+ ).resolves.toBe(false);
103
+ });
104
+
105
+ test("rejects a non-uuid id without ever touching the database", async () => {
106
+ /*
107
+ * _id is a uuid column: querying it with garbage is a Postgres cast
108
+ * error, not an empty result. The id arrives from an unvalidated JSON
109
+ * config, so the shape check has to happen before the query — otherwise
110
+ * a typo in a runbook surfaces as a driver error during an incident.
111
+ */
112
+ const findSpy: jest.SpiedFunction<typeof LlmProviderService.findOneBy> =
113
+ jest.spyOn(LlmProviderService, "findOneBy");
114
+
115
+ await expect(
116
+ LlmProviderService.isProviderUsableByProject({
117
+ projectId,
118
+ llmProviderId: new ObjectID("not-a-uuid"),
119
+ }),
120
+ ).resolves.toBe(false);
121
+
122
+ expect(findSpy).not.toHaveBeenCalled();
123
+ });
124
+
125
+ test("rejects an empty id without touching the database", async () => {
126
+ const findSpy: jest.SpiedFunction<typeof LlmProviderService.findOneBy> =
127
+ jest.spyOn(LlmProviderService, "findOneBy");
128
+
129
+ await expect(
130
+ LlmProviderService.isProviderUsableByProject({
131
+ projectId,
132
+ llmProviderId: new ObjectID(""),
133
+ }),
134
+ ).resolves.toBe(false);
135
+
136
+ expect(findSpy).not.toHaveBeenCalled();
137
+ });
138
+
139
+ test("never selects the apiKey — a yes/no must not load secrets", async () => {
140
+ const findSpy: jest.SpiedFunction<typeof LlmProviderService.findOneBy> =
141
+ jest
142
+ .spyOn(LlmProviderService, "findOneBy")
143
+ .mockResolvedValue(fakeProvider({ projectId }));
144
+
145
+ await LlmProviderService.isProviderUsableByProject({
146
+ projectId,
147
+ llmProviderId: providerId,
148
+ });
149
+
150
+ const select: Select<LlmProvider> = findSpy.mock.calls[0]![0]!
151
+ .select as Select<LlmProvider>;
152
+ expect(select["apiKey"]).toBeUndefined();
153
+ expect(select["projectId"]).toBe(true);
154
+ expect(select["isGlobalLlm"]).toBe(true);
155
+ });
156
+
157
+ test("looks the provider up by id alone, so a global row is reachable", async () => {
158
+ /*
159
+ * Scoping the query by projectId would silently exclude global
160
+ * providers (their projectId is NULL) — the tenant decision belongs in
161
+ * the check on the loaded row, not in the query.
162
+ */
163
+ const findSpy: jest.SpiedFunction<typeof LlmProviderService.findOneBy> =
164
+ jest
165
+ .spyOn(LlmProviderService, "findOneBy")
166
+ .mockResolvedValue(fakeProvider({ isGlobalLlm: true }));
167
+
168
+ await LlmProviderService.isProviderUsableByProject({
169
+ projectId,
170
+ llmProviderId: providerId,
171
+ });
172
+
173
+ const query: Record<string, unknown> = findSpy.mock.calls[0]![0]!
174
+ .query as Record<string, unknown>;
175
+ expect(query["_id"]).toBe(providerId.toString());
176
+ expect(query["projectId"]).toBeUndefined();
177
+ });
178
+ });
179
+
180
+ /*
181
+ * getProviderForChat shares the same usability predicate. Its contract is the
182
+ * opposite of the runbook step's on failure — a human is watching a chat, so
183
+ * an unusable id falls back to the default instead of erroring — and these
184
+ * tests pin that difference down so a future refactor of the shared predicate
185
+ * cannot quietly swap one behaviour for the other.
186
+ */
187
+ describe("LlmProviderService.getProviderForChat", () => {
188
+ afterEach(() => {
189
+ jest.restoreAllMocks();
190
+ });
191
+
192
+ test("uses an explicitly chosen provider the project owns", async () => {
193
+ const chosen: LlmProvider = fakeProvider({ projectId });
194
+ const findSpy: jest.SpiedFunction<typeof LlmProviderService.findOneBy> =
195
+ jest.spyOn(LlmProviderService, "findOneBy").mockResolvedValue(chosen);
196
+
197
+ const result: LlmProvider | null =
198
+ await LlmProviderService.getProviderForChat({
199
+ projectId,
200
+ llmProviderId: providerId,
201
+ });
202
+
203
+ expect(result).toBe(chosen);
204
+ expect(findSpy).toHaveBeenCalledTimes(1);
205
+ });
206
+
207
+ test("uses an explicitly chosen global provider", async () => {
208
+ const chosen: LlmProvider = fakeProvider({ isGlobalLlm: true });
209
+ jest.spyOn(LlmProviderService, "findOneBy").mockResolvedValue(chosen);
210
+
211
+ await expect(
212
+ LlmProviderService.getProviderForChat({
213
+ projectId,
214
+ llmProviderId: providerId,
215
+ }),
216
+ ).resolves.toBe(chosen);
217
+ });
218
+
219
+ test("falls back to the project default when the chosen provider belongs to another project", async () => {
220
+ const foreign: LlmProvider = fakeProvider({ projectId: otherProjectId });
221
+ const projectDefault: LlmProvider = fakeProvider({ projectId });
222
+ jest
223
+ .spyOn(LlmProviderService, "findOneBy")
224
+ .mockResolvedValueOnce(foreign)
225
+ .mockResolvedValueOnce(projectDefault);
226
+
227
+ const result: LlmProvider | null =
228
+ await LlmProviderService.getProviderForChat({
229
+ projectId,
230
+ llmProviderId: providerId,
231
+ });
232
+
233
+ expect(result).toBe(projectDefault);
234
+ });
235
+
236
+ test("falls back when the chosen provider no longer exists", async () => {
237
+ const projectDefault: LlmProvider = fakeProvider({ projectId });
238
+ jest
239
+ .spyOn(LlmProviderService, "findOneBy")
240
+ .mockResolvedValueOnce(null)
241
+ .mockResolvedValueOnce(projectDefault);
242
+
243
+ await expect(
244
+ LlmProviderService.getProviderForChat({
245
+ projectId,
246
+ llmProviderId: providerId,
247
+ }),
248
+ ).resolves.toBe(projectDefault);
249
+ });
250
+
251
+ test("a non-uuid choice falls back instead of hitting the database with it", async () => {
252
+ /*
253
+ * Without the shape check this query is a Postgres cast error rather
254
+ * than a miss, so a stale or hand-edited id would break a whole
255
+ * conversation instead of quietly resolving to the default.
256
+ */
257
+ const projectDefault: LlmProvider = fakeProvider({ projectId });
258
+ const findSpy: jest.SpiedFunction<typeof LlmProviderService.findOneBy> =
259
+ jest
260
+ .spyOn(LlmProviderService, "findOneBy")
261
+ .mockResolvedValue(projectDefault);
262
+
263
+ const result: LlmProvider | null =
264
+ await LlmProviderService.getProviderForChat({
265
+ projectId,
266
+ llmProviderId: new ObjectID("not-a-uuid"),
267
+ });
268
+
269
+ expect(result).toBe(projectDefault);
270
+ // Only the default-resolution query ran — never one keyed on the garbage id.
271
+ for (const call of findSpy.mock.calls) {
272
+ expect(
273
+ (call[0]!.query as Record<string, unknown>)["_id"],
274
+ ).toBeUndefined();
275
+ }
276
+ });
277
+
278
+ test("with no choice at all it goes straight to default resolution", async () => {
279
+ const projectDefault: LlmProvider = fakeProvider({ projectId });
280
+ const findSpy: jest.SpiedFunction<typeof LlmProviderService.findOneBy> =
281
+ jest
282
+ .spyOn(LlmProviderService, "findOneBy")
283
+ .mockResolvedValue(projectDefault);
284
+
285
+ await expect(
286
+ LlmProviderService.getProviderForChat({ projectId }),
287
+ ).resolves.toBe(projectDefault);
288
+
289
+ const query: Record<string, unknown> = findSpy.mock.calls[0]![0]!
290
+ .query as Record<string, unknown>;
291
+ expect(query["isDefault"]).toBe(true);
292
+ });
293
+ });