@opengeni/core 0.28.0 → 1.0.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 (224) hide show
  1. package/README.md +1 -1
  2. package/dist/access/external-actor-authority.d.ts +47 -0
  3. package/dist/access/index.d.ts +182 -3
  4. package/dist/access/service-initiator.d.ts +3 -0
  5. package/dist/application/api-integration-servers.d.ts +20 -0
  6. package/dist/application/archived-session-imports.d.ts +6 -0
  7. package/dist/application/composer-submit.d.ts +24 -0
  8. package/dist/application/connect-authority.d.ts +13 -0
  9. package/dist/application/connect-operation.d.ts +31 -0
  10. package/dist/application/external-continuation.d.ts +15 -0
  11. package/dist/application/external-identity-lifecycle.d.ts +20 -0
  12. package/dist/application/external-link-work-admission.d.ts +12 -0
  13. package/dist/application/external-workspace-members.d.ts +34 -0
  14. package/dist/application/modal-native-worker-host-transport.d.ts +297 -0
  15. package/dist/application/new-session-drafts.d.ts +39 -2
  16. package/dist/application/sandbox-recovery.d.ts +58 -0
  17. package/dist/application/session-commands.d.ts +26 -13
  18. package/dist/application/session-mcp-credential-rotation.d.ts +29 -0
  19. package/dist/application/session-tenancy.d.ts +19 -0
  20. package/dist/application/user-resource-grants.d.ts +20 -0
  21. package/dist/billing/limits.d.ts +11 -0
  22. package/dist/canonical-human-identities.d.ts +7 -2
  23. package/dist/canonical-human-identities.js +17 -4
  24. package/dist/canonical-human-identities.js.map +1 -1
  25. package/dist/{chunk-6WKPWE5S.js → chunk-2MKMRGSY.js} +108 -24
  26. package/dist/chunk-2MKMRGSY.js.map +1 -0
  27. package/dist/{chunk-NYPMQ7NO.js → chunk-GVO3N37G.js} +205 -37
  28. package/dist/chunk-GVO3N37G.js.map +1 -0
  29. package/dist/{chunk-JJU6I5XD.js → chunk-LCQXNR62.js} +36 -10
  30. package/dist/chunk-LCQXNR62.js.map +1 -0
  31. package/dist/chunk-MM6N7CTO.js +387 -0
  32. package/dist/chunk-MM6N7CTO.js.map +1 -0
  33. package/dist/chunk-QO5GVFFO.js +235 -0
  34. package/dist/chunk-QO5GVFFO.js.map +1 -0
  35. package/dist/codex-model-availability.d.ts +14 -0
  36. package/dist/default-session-model.d.ts +139 -0
  37. package/dist/dependencies.d.ts +87 -4
  38. package/dist/developer-setup-credential-ceiling.d.ts +10 -0
  39. package/dist/domain/agent-config-resolution.d.ts +49 -0
  40. package/dist/domain/automations.d.ts +58 -0
  41. package/dist/domain/capabilities.d.ts +22 -4
  42. package/dist/domain/company-profile-agent-admin.d.ts +25 -0
  43. package/dist/domain/connection-account-selection-error.d.ts +8 -0
  44. package/dist/domain/connector-tool-permissions.d.ts +36 -0
  45. package/dist/domain/direct-model-provider.d.ts +3 -0
  46. package/dist/domain/editable-artifacts/agent-application.d.ts +10 -1
  47. package/dist/domain/editable-artifacts/durable-export.d.ts +22 -1
  48. package/dist/domain/editable-artifacts/ports.d.ts +10 -0
  49. package/dist/domain/editable-artifacts/service.d.ts +4 -3
  50. package/dist/domain/environments.d.ts +5 -9
  51. package/dist/domain/external-creation-attribution.d.ts +6 -0
  52. package/dist/domain/file-owner.d.ts +8 -0
  53. package/dist/domain/github-action-policies.d.ts +36 -0
  54. package/dist/domain/github-repository-bindings.d.ts +117 -0
  55. package/dist/domain/github-skill-source.d.ts +5 -0
  56. package/dist/domain/governed-learning-slack-publication.d.ts +45 -0
  57. package/dist/domain/insights-timing.d.ts +16 -0
  58. package/dist/domain/insights-usage.d.ts +27 -0
  59. package/dist/domain/insights.d.ts +28 -1
  60. package/dist/domain/knowledge-files.d.ts +20 -0
  61. package/dist/domain/knowledge-messages.d.ts +39 -0
  62. package/dist/domain/knowledge-messages.test.d.ts +1 -0
  63. package/dist/domain/knowledge-preparation.d.ts +10 -0
  64. package/dist/domain/knowledge-preparation.test.d.ts +1 -0
  65. package/dist/domain/knowledge-search.d.ts +15 -0
  66. package/dist/domain/knowledge-search.test.d.ts +1 -0
  67. package/dist/domain/knowledge.d.ts +9 -0
  68. package/dist/domain/mcp-account-bindings.d.ts +19 -0
  69. package/dist/domain/mcp-account-routes.d.ts +21 -0
  70. package/dist/domain/native-mcp-connection-admission.d.ts +3 -0
  71. package/dist/domain/organization-integration-catalog.d.ts +15 -0
  72. package/dist/domain/organization-membership-lifecycle.d.ts +1 -0
  73. package/dist/domain/personal-connection-delegations.d.ts +87 -4
  74. package/dist/domain/pr-review.d.ts +249 -0
  75. package/dist/domain/resources.d.ts +24 -3
  76. package/dist/domain/scheduled-task-access.d.ts +247 -0
  77. package/dist/domain/scheduled-tasks.d.ts +103 -6
  78. package/dist/domain/session-connection-accounts.d.ts +16 -0
  79. package/dist/domain/session-file-access.d.ts +4 -0
  80. package/dist/domain/session-native-connection-replacement.d.ts +14 -0
  81. package/dist/domain/session-start-timing.d.ts +7 -0
  82. package/dist/domain/session-tool-policy.d.ts +93 -4
  83. package/dist/domain/sessions.d.ts +275 -28
  84. package/dist/domain/skill-imports.d.ts +1 -0
  85. package/dist/domain/skill-search.d.ts +30 -0
  86. package/dist/domain/skills.d.ts +117 -0
  87. package/dist/domain/slack-bot.d.ts +46 -1
  88. package/dist/domain/workspace-members.d.ts +8 -1
  89. package/dist/editable-artifact-live/application.d.ts +12 -3
  90. package/dist/editable-artifact-live/ports.d.ts +0 -1
  91. package/dist/editable-artifact-live/server.d.ts +5 -2
  92. package/dist/editable-artifact-live/ticket.d.ts +2 -2
  93. package/dist/editable-artifact-live/types.d.ts +14 -2
  94. package/dist/editable-artifact-live.js +2 -2
  95. package/dist/editable-artifacts.js +10 -4
  96. package/dist/index.d.ts +49 -3
  97. package/dist/index.js +18826 -6847
  98. package/dist/index.js.map +1 -1
  99. package/dist/managed-auth-session-sets.d.ts +131 -0
  100. package/dist/managed-auth-session-sets.js +51 -0
  101. package/dist/managed-auth-session-sets.js.map +1 -0
  102. package/dist/managed-session.d.ts +98 -11
  103. package/dist/model-catalog.d.ts +117 -0
  104. package/dist/product-usage-metrics.d.ts +30 -0
  105. package/dist/rigs/index.d.ts +22 -5
  106. package/dist/rigs/provider-images.d.ts +1 -1
  107. package/dist/sandbox/fleet.d.ts +59 -10
  108. package/dist/sandbox/routing.d.ts +22 -2
  109. package/dist/sandbox/runtime-settings.d.ts +56 -0
  110. package/dist/session-authorization.d.ts +38 -1
  111. package/dist/site-session-origin.d.ts +9 -0
  112. package/dist/transcription.d.ts +12 -1
  113. package/dist/turn-surface.d.ts +18 -0
  114. package/dist/user-presence.d.ts +32 -0
  115. package/package.json +18 -11
  116. package/src/access/external-actor-authority.ts +94 -0
  117. package/src/access/index.ts +1386 -44
  118. package/src/access/service-initiator.ts +50 -0
  119. package/src/application/api-integration-servers.ts +215 -0
  120. package/src/application/archived-session-imports.ts +161 -0
  121. package/src/application/composer-submit.ts +77 -0
  122. package/src/application/connect-authority.ts +77 -0
  123. package/src/application/connect-operation.ts +80 -0
  124. package/src/application/external-continuation.ts +129 -0
  125. package/src/application/external-identity-lifecycle.ts +48 -0
  126. package/src/application/external-link-work-admission.ts +90 -0
  127. package/src/application/external-workspace-members.ts +232 -0
  128. package/src/application/modal-native-worker-host-transport.ts +263 -0
  129. package/src/application/new-session-drafts.ts +222 -29
  130. package/src/application/sandbox-recovery.ts +78 -0
  131. package/src/application/session-commands.ts +430 -73
  132. package/src/application/session-mcp-credential-rotation.ts +217 -0
  133. package/src/application/session-tenancy.ts +445 -0
  134. package/src/application/user-resource-grants.ts +154 -0
  135. package/src/billing/limits.ts +103 -24
  136. package/src/canonical-human-identities.ts +22 -3
  137. package/src/codex-model-availability.ts +136 -0
  138. package/src/default-session-model.ts +656 -0
  139. package/src/dependencies.ts +97 -5
  140. package/src/developer-setup-credential-ceiling.ts +60 -0
  141. package/src/domain/agent-config-resolution.ts +148 -0
  142. package/src/domain/automations.ts +270 -0
  143. package/src/domain/capabilities.ts +244 -151
  144. package/src/domain/company-profile-agent-admin.ts +56 -0
  145. package/src/domain/connection-account-selection-error.ts +19 -0
  146. package/src/domain/connector-tool-permissions.ts +371 -0
  147. package/src/domain/direct-model-provider.ts +62 -0
  148. package/src/domain/editable-artifacts/agent-application.ts +61 -15
  149. package/src/domain/editable-artifacts/durable-export.ts +100 -5
  150. package/src/domain/editable-artifacts/in-memory.ts +6 -0
  151. package/src/domain/editable-artifacts/ports.ts +11 -0
  152. package/src/domain/editable-artifacts/service.ts +32 -1
  153. package/src/domain/editable-artifacts/snapshot-compaction.ts +9 -5
  154. package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +10 -8
  155. package/src/domain/editable-artifacts/snapshot-verifier.ts +10 -3
  156. package/src/domain/environments.ts +25 -45
  157. package/src/domain/external-creation-attribution.ts +22 -0
  158. package/src/domain/file-owner.ts +67 -0
  159. package/src/domain/github-action-policies.ts +169 -0
  160. package/src/domain/github-repository-bindings.ts +295 -0
  161. package/src/domain/github-skill-source.ts +233 -0
  162. package/src/domain/governed-learning-slack-publication.ts +157 -0
  163. package/src/domain/insights-timing.ts +95 -0
  164. package/src/domain/insights-usage.ts +67 -0
  165. package/src/domain/insights.ts +270 -169
  166. package/src/domain/knowledge-files.ts +46 -0
  167. package/src/domain/knowledge-messages.test.ts +91 -0
  168. package/src/domain/knowledge-messages.ts +106 -0
  169. package/src/domain/knowledge-preparation.test.ts +276 -0
  170. package/src/domain/knowledge-preparation.ts +147 -0
  171. package/src/domain/knowledge-search.test.ts +561 -0
  172. package/src/domain/knowledge-search.ts +256 -0
  173. package/src/domain/knowledge.ts +133 -0
  174. package/src/domain/mcp-account-bindings.ts +171 -0
  175. package/src/domain/mcp-account-routes.ts +121 -0
  176. package/src/domain/native-mcp-connection-admission.ts +14 -0
  177. package/src/domain/organization-integration-catalog.ts +88 -0
  178. package/src/domain/organization-membership-lifecycle.ts +10 -0
  179. package/src/domain/personal-connection-delegations.ts +758 -115
  180. package/src/domain/pr-review.ts +687 -0
  181. package/src/domain/resources.ts +182 -25
  182. package/src/domain/scheduled-task-access.ts +1052 -0
  183. package/src/domain/scheduled-tasks.ts +1952 -215
  184. package/src/domain/session-connection-accounts.ts +67 -0
  185. package/src/domain/session-file-access.ts +47 -0
  186. package/src/domain/session-native-connection-replacement.ts +64 -0
  187. package/src/domain/session-start-timing.ts +55 -0
  188. package/src/domain/session-tool-policy.ts +443 -19
  189. package/src/domain/sessions.ts +3417 -619
  190. package/src/domain/skill-imports.ts +137 -17
  191. package/src/domain/skill-search.ts +221 -0
  192. package/src/domain/skills.ts +104 -0
  193. package/src/domain/slack-bot.ts +128 -3
  194. package/src/domain/workspace-members.ts +34 -2
  195. package/src/editable-artifact-live/application.ts +29 -4
  196. package/src/editable-artifact-live/ports.ts +0 -1
  197. package/src/editable-artifact-live/server.ts +167 -13
  198. package/src/editable-artifact-live/ticket.ts +87 -6
  199. package/src/editable-artifact-live/types.ts +16 -2
  200. package/src/editable-artifact-live/wire.ts +0 -10
  201. package/src/index.ts +69 -4
  202. package/src/managed-auth-session-sets.ts +371 -0
  203. package/src/managed-session.ts +520 -2
  204. package/src/model-catalog.ts +873 -0
  205. package/src/product-usage-metrics.ts +101 -0
  206. package/src/rigs/index.ts +179 -63
  207. package/src/rigs/provider-images.ts +2 -3
  208. package/src/sandbox/fleet.ts +471 -59
  209. package/src/sandbox/routing.ts +160 -33
  210. package/src/sandbox/runtime-settings.ts +281 -0
  211. package/src/session-authorization.ts +262 -78
  212. package/src/site-session-origin.ts +22 -0
  213. package/src/transcription.ts +13 -1
  214. package/src/turn-surface.ts +45 -0
  215. package/src/user-presence.ts +134 -0
  216. package/dist/chunk-6WKPWE5S.js.map +0 -1
  217. package/dist/chunk-IBOEYG6N.js +0 -34
  218. package/dist/chunk-IBOEYG6N.js.map +0 -1
  219. package/dist/chunk-JJU6I5XD.js.map +0 -1
  220. package/dist/chunk-NYPMQ7NO.js.map +0 -1
  221. package/dist/domain/durable-learning-slack-publication.d.ts +0 -72
  222. package/dist/domain/packs.d.ts +0 -55
  223. package/src/domain/durable-learning-slack-publication.ts +0 -202
  224. package/src/domain/packs.ts +0 -556
@@ -0,0 +1,561 @@
1
+ import { afterAll, expect, mock, test } from "bun:test";
2
+ import type { Settings } from "@opengeni/config";
3
+ import type { KnowledgeContext } from "@opengeni/db";
4
+ const serviceContext: KnowledgeContext = {
5
+ accountId: "account",
6
+ workspaceId: "workspace",
7
+ actor: {
8
+ kind: "service",
9
+ principalKind: "service",
10
+ subjectId: "service:knowledge",
11
+ writeScopes: ["workspace"],
12
+ settingsScopes: [],
13
+ review: false,
14
+ },
15
+ };
16
+ const list = mock(
17
+ async (_db: unknown, _context: unknown, _request: unknown, _embedding?: unknown) => {
18
+ if (failNextRetrieval) {
19
+ failNextRetrieval = false;
20
+ throw new Error("retrieval unavailable");
21
+ }
22
+ return { entries: [], nextCursor: null };
23
+ },
24
+ );
25
+ let balance = 0;
26
+ let usedChunks = 0;
27
+ let queryRateBytes = 0;
28
+ let queryRateMicros = 0;
29
+ let failNextRetrieval = false;
30
+ let failSettlement = false;
31
+ let allowanceRefusal: Record<string, unknown> | null = null;
32
+ const allowanceChecks = mock(async (_db: unknown, _input: unknown) => allowanceRefusal);
33
+ const debits = mock(async (_db: unknown, input: { amountMicros: number }) => {
34
+ if (failSettlement) throw new Error("settlement unavailable");
35
+ balance -= input.amountMicros;
36
+ return { balance: { balanceMicros: balance }, debitedMicros: input.amountMicros };
37
+ });
38
+ const usage = mock(
39
+ async (_db: unknown, _input: { quantity: number; eventType: string }) => undefined,
40
+ );
41
+ mock.module("@opengeni/db", () => ({
42
+ listKnowledgeEntries: list,
43
+ getBillingBalance: async () => ({ balanceMicros: balance }),
44
+ applyCreditDebitAfterUse: debits,
45
+ checkWorkspaceAllowance: allowanceChecks,
46
+ creditDebitAttributionForTurn: async (_db: unknown, input: { turnId: string }) => ({
47
+ kind: "turn",
48
+ turnId: input.turnId,
49
+ initiatingHumanSubjectId: "human:root",
50
+ }),
51
+ recordUsageEvent: usage,
52
+ withKnowledgeQueryAccountLock: async (
53
+ _db: unknown,
54
+ _account: unknown,
55
+ _workspace: unknown,
56
+ fn: (db: unknown) => Promise<unknown>,
57
+ ) => fn(_db),
58
+ isCodexBilledTurn: async () => false,
59
+ sumUsageQuantity: async (_db: unknown, input: { eventType: string }) =>
60
+ input.eventType === "document.query_embedding_bytes"
61
+ ? queryRateBytes
62
+ : input.eventType === "document.query_embedding_cost"
63
+ ? queryRateMicros
64
+ : usedChunks,
65
+ countScheduledTasksForWorkspace: async () => 0,
66
+ countWorkspacesForAccount: async () => 0,
67
+ countActiveApiKeysForWorkspace: async () => 0,
68
+ countActiveOrganizationApiKeysForAccount: async () => 0,
69
+ }));
70
+ const { searchKnowledgeEntries } = await import("./knowledge-search");
71
+ const { checkLimit } = await import("../billing/limits");
72
+ afterAll(() => mock.restore());
73
+ const unavailable = () => {
74
+ throw new Error("embedding unavailable");
75
+ };
76
+ test("hybrid fallback keeps lexical recall and passes the exact scope and cursor", async () => {
77
+ const result = await searchKnowledgeEntries(
78
+ {} as never,
79
+ {} as never,
80
+ {
81
+ query: "Acme renewal renew contract expiration renewal date",
82
+ scope: "workspace",
83
+ cursor: "page-2",
84
+ },
85
+ unavailable,
86
+ );
87
+ expect(result.searchMode).toBe("keyword");
88
+ expect(result.fallbackReason).toBe("provider_unavailable");
89
+ expect(list.mock.calls.at(-1)?.[2]).toMatchObject({
90
+ query: '"Acme" OR "renewal" OR "renew" OR "contract" OR "expiration" OR "date"',
91
+ mode: "keyword",
92
+ scope: "workspace",
93
+ cursor: "page-2",
94
+ });
95
+ });
96
+ test("keyword and explicit search syntax remain unchanged", async () => {
97
+ for (const query of ['"Acme renewal"', "Acme -expired", "Acme OR renewal"]) {
98
+ await searchKnowledgeEntries({} as never, {} as never, { query }, unavailable);
99
+ expect(list.mock.calls.at(-1)?.[2]).toMatchObject({ query });
100
+ }
101
+ await searchKnowledgeEntries(
102
+ {} as never,
103
+ {} as never,
104
+ { query: "Acme renewal", mode: "keyword" },
105
+ unavailable,
106
+ );
107
+ expect(list.mock.calls.at(-1)?.[2]).toMatchObject({ query: "Acme renewal", mode: "keyword" });
108
+ });
109
+
110
+ test("paid vector queries check funding, debit post-use, and preserve hybrid keyword fallback", async () => {
111
+ const settings = {
112
+ documentEmbeddingProvider: "openai",
113
+ documentEmbeddingBillingMode: "credits",
114
+ documentEmbeddingRateMicrosPerMillionBytes: 1_000_000,
115
+ } as Settings;
116
+ const context = serviceContext;
117
+ let embedded = 0;
118
+ const provider = () =>
119
+ ({
120
+ model: "test",
121
+ dimensions: 3,
122
+ embedQuery: async () => {
123
+ embedded++;
124
+ return [1, 0, 0];
125
+ },
126
+ }) as never;
127
+ balance = 0;
128
+ const unfunded = await searchKnowledgeEntries(
129
+ {} as never,
130
+ context,
131
+ { query: "paid" },
132
+ provider,
133
+ settings,
134
+ );
135
+ expect(unfunded.searchMode).toBe("keyword");
136
+ expect(unfunded.fallbackReason).toBe("awaiting_funding");
137
+ await expect(
138
+ searchKnowledgeEntries(
139
+ {} as never,
140
+ context,
141
+ { query: "paid", mode: "vector" },
142
+ provider,
143
+ settings,
144
+ ),
145
+ ).rejects.toMatchObject({ code: "knowledge_vector_funding_required" });
146
+ expect(embedded).toBe(0);
147
+ balance = 1;
148
+ const paid = await searchKnowledgeEntries(
149
+ {} as never,
150
+ context,
151
+ { query: "paid", mode: "vector" },
152
+ provider,
153
+ settings,
154
+ );
155
+ expect(paid.searchMode).toBe("vector");
156
+ expect(embedded).toBe(1);
157
+ expect(debits.mock.calls.at(-1)?.[1]).toMatchObject({
158
+ amountMicros: 4,
159
+ sourceType: "knowledge_query",
160
+ });
161
+ expect(balance).toBe(-3);
162
+ const priorUsage = usage.mock.calls.length;
163
+ await searchKnowledgeEntries(
164
+ {} as never,
165
+ context,
166
+ { query: "paid", mode: "keyword" },
167
+ provider,
168
+ settings,
169
+ );
170
+ expect(embedded).toBe(1);
171
+ expect(usage.mock.calls.length).toBe(priorUsage);
172
+ });
173
+
174
+ test("shadow query usage meters bytes without checking or consuming credits", async () => {
175
+ const settings = {
176
+ documentEmbeddingProvider: "openai",
177
+ documentEmbeddingBillingMode: "shadow",
178
+ documentEmbeddingRateMicrosPerMillionBytes: 1_000_000,
179
+ } as Settings;
180
+ balance = 0;
181
+ const before = debits.mock.calls.length;
182
+ const found = await searchKnowledgeEntries(
183
+ {} as never,
184
+ serviceContext,
185
+ { query: "é", mode: "vector" },
186
+ () => ({ model: "test", dimensions: 3, embedQuery: async () => [1, 0, 0] }) as never,
187
+ settings,
188
+ );
189
+ expect(found.searchMode).toBe("vector");
190
+ expect(usage.mock.calls.at(-1)?.[1]).toMatchObject({
191
+ quantity: 2,
192
+ eventType: "document.query_embedding_bytes",
193
+ });
194
+ expect(debits.mock.calls.length).toBe(before);
195
+ });
196
+
197
+ test("paid queries attribute exact turns or verified humans, never service/API-key subjects", async () => {
198
+ const settings = {
199
+ documentEmbeddingProvider: "openai",
200
+ documentEmbeddingBillingMode: "credits",
201
+ documentEmbeddingRateMicrosPerMillionBytes: 1_000_000,
202
+ } as Settings;
203
+ const actors: KnowledgeContext["actor"][] = [
204
+ {
205
+ kind: "agent",
206
+ sessionId: crypto.randomUUID(),
207
+ turnId: crypto.randomUUID(),
208
+ attemptId: crypto.randomUUID(),
209
+ executionGeneration: 1,
210
+ },
211
+ {
212
+ kind: "human",
213
+ principalKind: "human_session",
214
+ subjectId: "human:frozen",
215
+ writeScopes: ["workspace"],
216
+ settingsScopes: ["workspace"],
217
+ review: true,
218
+ },
219
+ {
220
+ ...serviceContext.actor,
221
+ principalKind: "api_key",
222
+ subjectId: "api-key:not-human",
223
+ } as KnowledgeContext["actor"],
224
+ serviceContext.actor,
225
+ ];
226
+ for (const actor of actors) {
227
+ balance = 100;
228
+ await searchKnowledgeEntries(
229
+ {} as never,
230
+ { ...serviceContext, actor },
231
+ { query: "paid", mode: "vector" },
232
+ () => ({ model: "test", dimensions: 3, embedQuery: async () => [1, 0, 0] }) as never,
233
+ settings,
234
+ );
235
+ const debit = debits.mock.calls.at(-1)?.[1] as unknown as {
236
+ metadata: Record<string, unknown>;
237
+ };
238
+ expect(debit.metadata).toEqual({
239
+ model: "test",
240
+ bytes: 4,
241
+ ...(actor.kind === "agent"
242
+ ? { turnId: actor.turnId }
243
+ : actor.kind === "human"
244
+ ? { initiatingHumanSubjectId: actor.subjectId }
245
+ : {}),
246
+ });
247
+ const receipt = usage.mock.calls.at(-1)?.[1] as unknown as {
248
+ eventType: string;
249
+ sourceResourceId: string;
250
+ initiatorContext: { creditDebitAttribution: Record<string, unknown> };
251
+ };
252
+ expect(receipt.eventType).toBe("document.query_embedding_cost");
253
+ expect(receipt.initiatorContext.creditDebitAttribution).toEqual(
254
+ actor.kind === "agent"
255
+ ? { kind: "turn", turnId: actor.turnId, initiatingHumanSubjectId: "human:root" }
256
+ : actor.kind === "human"
257
+ ? { kind: "human", initiatingHumanSubjectId: actor.subjectId }
258
+ : { kind: "service" },
259
+ );
260
+ }
261
+ });
262
+
263
+ test("paid query checks the frozen root human allowance before provider use", async () => {
264
+ const settings = {
265
+ documentEmbeddingProvider: "openai",
266
+ documentEmbeddingBillingMode: "credits",
267
+ documentEmbeddingRateMicrosPerMillionBytes: 1_000_000,
268
+ } as Settings;
269
+ let calls = 0;
270
+ const context: KnowledgeContext = {
271
+ ...serviceContext,
272
+ actor: {
273
+ kind: "agent",
274
+ sessionId: crypto.randomUUID(),
275
+ turnId: crypto.randomUUID(),
276
+ attemptId: crypto.randomUUID(),
277
+ executionGeneration: 1,
278
+ },
279
+ };
280
+ balance = 100;
281
+ const before = debits.mock.calls.length;
282
+ allowanceRefusal = {
283
+ code: "allowance_exhausted",
284
+ scope: "member",
285
+ subjectId: "human:root",
286
+ resetsAt: null,
287
+ message: "Member allowance exhausted",
288
+ };
289
+ const provider = () =>
290
+ ({
291
+ model: "test",
292
+ dimensions: 3,
293
+ embedQuery: async () => {
294
+ calls++;
295
+ return [1, 0, 0];
296
+ },
297
+ }) as never;
298
+ try {
299
+ const hybrid = await searchKnowledgeEntries(
300
+ {} as never,
301
+ context,
302
+ { query: "paid" },
303
+ provider,
304
+ settings,
305
+ );
306
+ expect(hybrid).toMatchObject({ searchMode: "keyword", fallbackReason: "quota" });
307
+ await expect(
308
+ searchKnowledgeEntries(
309
+ {} as never,
310
+ context,
311
+ { query: "paid", mode: "vector" },
312
+ provider,
313
+ settings,
314
+ ),
315
+ ).rejects.toMatchObject({ code: "allowance_exhausted", subjectId: "human:root" });
316
+ expect(allowanceChecks.mock.calls.at(-1)?.[1]).toMatchObject({ subjectId: "human:root" });
317
+ expect(calls).toBe(0);
318
+ expect(debits.mock.calls.length).toBe(before);
319
+ } finally {
320
+ allowanceRefusal = null;
321
+ }
322
+ });
323
+
324
+ test("paid query does not debit when vector retrieval fails after embedding", async () => {
325
+ const settings = {
326
+ documentEmbeddingProvider: "openai",
327
+ documentEmbeddingBillingMode: "credits",
328
+ documentEmbeddingRateMicrosPerMillionBytes: 1_000_000,
329
+ } as Settings;
330
+ balance = 1;
331
+ const debitsBefore = debits.mock.calls.length;
332
+ failNextRetrieval = true;
333
+ await expect(
334
+ searchKnowledgeEntries(
335
+ {} as never,
336
+ serviceContext,
337
+ { query: "paid", mode: "vector" },
338
+ () => ({ model: "test", dimensions: 3, embedQuery: async () => [1, 0, 0] }) as never,
339
+ settings,
340
+ ),
341
+ ).rejects.toThrow("retrieval unavailable");
342
+ expect(debits.mock.calls.length).toBe(debitsBefore);
343
+ });
344
+
345
+ test("query debit preserves initiating human when its caller context changes during provider work", async () => {
346
+ const context: KnowledgeContext = {
347
+ ...serviceContext,
348
+ actor: {
349
+ kind: "human",
350
+ principalKind: "human_session",
351
+ subjectId: "human:original",
352
+ writeScopes: ["workspace"],
353
+ settingsScopes: ["workspace"],
354
+ review: true,
355
+ },
356
+ };
357
+ balance = 100;
358
+ await searchKnowledgeEntries(
359
+ {} as never,
360
+ context,
361
+ { query: "paid", mode: "vector" },
362
+ () =>
363
+ ({
364
+ model: "test",
365
+ dimensions: 3,
366
+ embedQuery: async () => {
367
+ context.actor = serviceContext.actor;
368
+ return [1, 0, 0];
369
+ },
370
+ }) as never,
371
+ {
372
+ documentEmbeddingProvider: "openai",
373
+ documentEmbeddingBillingMode: "credits",
374
+ documentEmbeddingRateMicrosPerMillionBytes: 1_000_000,
375
+ } as Settings,
376
+ );
377
+ expect(debits.mock.calls.at(-1)?.[1]).toMatchObject({
378
+ metadata: { initiatingHumanSubjectId: "human:original" },
379
+ });
380
+ expect(usage.mock.calls.at(-1)?.[1]).toMatchObject({
381
+ initiatorContext: {
382
+ creditDebitAttribution: { kind: "human", initiatingHumanSubjectId: "human:original" },
383
+ },
384
+ });
385
+ });
386
+
387
+ test("paid semantic pagination fails closed before provider use", async () => {
388
+ let embedded = 0;
389
+ const before = debits.mock.calls.length;
390
+ await expect(
391
+ searchKnowledgeEntries(
392
+ {} as never,
393
+ serviceContext,
394
+ { query: "paid", mode: "vector", cursor: "next-page" },
395
+ () =>
396
+ ({
397
+ model: "test",
398
+ dimensions: 3,
399
+ embedQuery: async () => {
400
+ embedded++;
401
+ return [1, 0, 0];
402
+ },
403
+ }) as never,
404
+ {
405
+ documentEmbeddingProvider: "openai",
406
+ documentEmbeddingBillingMode: "credits",
407
+ documentEmbeddingRateMicrosPerMillionBytes: 1_000_000,
408
+ } as Settings,
409
+ ),
410
+ ).rejects.toThrow("pagination is unavailable");
411
+ expect(embedded).toBe(0);
412
+ expect(debits.mock.calls.length).toBe(before);
413
+ });
414
+
415
+ test("paid query rate ceiling falls back for hybrid and rejects vector without using the provider", async () => {
416
+ queryRateBytes = 64 * 1024;
417
+ balance = 1;
418
+ let embedded = 0;
419
+ const provider = () =>
420
+ ({
421
+ model: "test",
422
+ dimensions: 3,
423
+ embedQuery: async () => {
424
+ embedded++;
425
+ return [1, 0, 0];
426
+ },
427
+ }) as never;
428
+ const settings = {
429
+ documentEmbeddingProvider: "openai",
430
+ documentEmbeddingBillingMode: "credits",
431
+ documentEmbeddingRateMicrosPerMillionBytes: 1_000_000,
432
+ } as Settings;
433
+ try {
434
+ expect(
435
+ (
436
+ await searchKnowledgeEntries(
437
+ {} as never,
438
+ serviceContext,
439
+ { query: "paid" },
440
+ provider,
441
+ settings,
442
+ )
443
+ ).searchMode,
444
+ ).toBe("keyword");
445
+ await expect(
446
+ searchKnowledgeEntries(
447
+ {} as never,
448
+ serviceContext,
449
+ { query: "paid", mode: "vector" },
450
+ provider,
451
+ settings,
452
+ ),
453
+ ).rejects.toThrow("rate limit reached");
454
+ expect(embedded).toBe(0);
455
+ } finally {
456
+ queryRateBytes = 0;
457
+ }
458
+ });
459
+
460
+ test("paid micro-cost ceiling blocks repeated tiny queries before embedding", async () => {
461
+ balance = 1;
462
+ queryRateMicros = 10_000;
463
+ let embedded = 0;
464
+ try {
465
+ const result = await searchKnowledgeEntries(
466
+ {} as never,
467
+ serviceContext,
468
+ { query: "tiny" },
469
+ () =>
470
+ ({
471
+ model: "test",
472
+ dimensions: 3,
473
+ embedQuery: async () => {
474
+ embedded++;
475
+ return [1, 0, 0];
476
+ },
477
+ }) as never,
478
+ {
479
+ documentEmbeddingProvider: "openai",
480
+ documentEmbeddingBillingMode: "credits",
481
+ documentEmbeddingRateMicrosPerMillionBytes: 1_000_000,
482
+ } as Settings,
483
+ );
484
+ expect(result.searchMode).toBe("keyword");
485
+ expect(embedded).toBe(0);
486
+ } finally {
487
+ queryRateMicros = 0;
488
+ }
489
+ });
490
+
491
+ test("paid settlement failure propagates without a fallback", async () => {
492
+ balance = 1;
493
+ failSettlement = true;
494
+ const before = list.mock.calls.length;
495
+ try {
496
+ await expect(
497
+ searchKnowledgeEntries(
498
+ {} as never,
499
+ serviceContext,
500
+ { query: "paid" },
501
+ () => ({ model: "test", dimensions: 3, embedQuery: async () => [1, 0, 0] }) as never,
502
+ {
503
+ documentEmbeddingProvider: "openai",
504
+ documentEmbeddingBillingMode: "credits",
505
+ documentEmbeddingRateMicrosPerMillionBytes: 1_000_000,
506
+ } as Settings,
507
+ ),
508
+ ).rejects.toThrow("settlement unavailable");
509
+ expect(list.mock.calls.length).toBe(before + 1);
510
+ } finally {
511
+ failSettlement = false;
512
+ }
513
+ });
514
+
515
+ test("deterministic embeddings remain unpriced even with credits mode selected", async () => {
516
+ balance = 0;
517
+ const before = debits.mock.calls.length;
518
+ const found = await searchKnowledgeEntries(
519
+ {} as never,
520
+ serviceContext,
521
+ { query: "local", mode: "vector" },
522
+ () => ({ model: "local", dimensions: 3, embedQuery: async () => [1, 0, 0] }) as never,
523
+ {
524
+ documentEmbeddingProvider: "deterministic",
525
+ documentEmbeddingBillingMode: "credits",
526
+ documentEmbeddingRateMicrosPerMillionBytes: 1_000_000,
527
+ } as Settings,
528
+ );
529
+ expect(found.searchMode).toBe("vector");
530
+ expect(debits.mock.calls.length).toBe(before);
531
+ });
532
+
533
+ test("unpriced document source admission ignores zero credits but preserves the monthly chunk quota", async () => {
534
+ balance = 0;
535
+ usedChunks = 1;
536
+ const deps = {
537
+ db: {} as never,
538
+ settings: {
539
+ billingMode: "stripe",
540
+ usageLimitsMode: "static",
541
+ staticUsageLimitsJson: JSON.stringify({ maxDocumentIndexedChunksPerWorkspace: 2 }),
542
+ } as Settings,
543
+ };
544
+ expect(
545
+ await checkLimit(deps, {
546
+ accountId: "account",
547
+ workspaceId: "workspace",
548
+ action: "document:index",
549
+ quantity: 0,
550
+ }),
551
+ ).toMatchObject({ allowed: true });
552
+ usedChunks = 2;
553
+ expect(
554
+ await checkLimit(deps, {
555
+ accountId: "account",
556
+ workspaceId: "workspace",
557
+ action: "document:index",
558
+ quantity: 1,
559
+ }),
560
+ ).toMatchObject({ allowed: false, code: "max_document_indexed_chunks_per_workspace" });
561
+ });