@stigmer/server 3.25.0 → 3.27.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 (254) hide show
  1. package/dist/authorization/evaluator.d.ts +4 -1
  2. package/dist/authorization/evaluator.d.ts.map +1 -1
  3. package/dist/authorization/evaluator.js +8 -0
  4. package/dist/authorization/evaluator.js.map +1 -1
  5. package/dist/authorization/model/agent.d.ts.map +1 -1
  6. package/dist/authorization/model/agent.js +4 -2
  7. package/dist/authorization/model/agent.js.map +1 -1
  8. package/dist/authorization/model/agent_channel.d.ts.map +1 -1
  9. package/dist/authorization/model/agent_channel.js +5 -4
  10. package/dist/authorization/model/agent_channel.js.map +1 -1
  11. package/dist/authorization/model/agent_instance.d.ts.map +1 -1
  12. package/dist/authorization/model/agent_instance.js +3 -2
  13. package/dist/authorization/model/agent_instance.js.map +1 -1
  14. package/dist/authorization/model/mcp_server.d.ts.map +1 -1
  15. package/dist/authorization/model/mcp_server.js +3 -2
  16. package/dist/authorization/model/mcp_server.js.map +1 -1
  17. package/dist/authorization/model/organization.d.ts.map +1 -1
  18. package/dist/authorization/model/organization.js +3 -0
  19. package/dist/authorization/model/organization.js.map +1 -1
  20. package/dist/authorization/model/plugin.d.ts.map +1 -1
  21. package/dist/authorization/model/plugin.js +4 -2
  22. package/dist/authorization/model/plugin.js.map +1 -1
  23. package/dist/authorization/model/rewrite.d.ts +25 -7
  24. package/dist/authorization/model/rewrite.d.ts.map +1 -1
  25. package/dist/authorization/model/rewrite.js +40 -1
  26. package/dist/authorization/model/rewrite.js.map +1 -1
  27. package/dist/authorization/model/schedule.d.ts.map +1 -1
  28. package/dist/authorization/model/schedule.js +8 -3
  29. package/dist/authorization/model/schedule.js.map +1 -1
  30. package/dist/authorization/model/skill.d.ts.map +1 -1
  31. package/dist/authorization/model/skill.js +3 -2
  32. package/dist/authorization/model/skill.js.map +1 -1
  33. package/dist/authorization/model/workflow.d.ts.map +1 -1
  34. package/dist/authorization/model/workflow.js +3 -2
  35. package/dist/authorization/model/workflow.js.map +1 -1
  36. package/dist/authorization/model/workflow_execution.d.ts.map +1 -1
  37. package/dist/authorization/model/workflow_execution.js +4 -3
  38. package/dist/authorization/model/workflow_execution.js.map +1 -1
  39. package/dist/authorization/model/workflow_instance.d.ts.map +1 -1
  40. package/dist/authorization/model/workflow_instance.js +5 -1
  41. package/dist/authorization/model/workflow_instance.js.map +1 -1
  42. package/dist/boot/compose.d.ts.map +1 -1
  43. package/dist/boot/compose.js +13 -8
  44. package/dist/boot/compose.js.map +1 -1
  45. package/dist/boot/list-indexes.d.ts +3 -0
  46. package/dist/boot/list-indexes.d.ts.map +1 -0
  47. package/dist/boot/list-indexes.js +24 -0
  48. package/dist/boot/list-indexes.js.map +1 -0
  49. package/dist/domain/agentexecution/controller.d.ts.map +1 -1
  50. package/dist/domain/agentexecution/controller.js +6 -8
  51. package/dist/domain/agentexecution/controller.js.map +1 -1
  52. package/dist/domain/agentexecution/list-index.d.ts +2 -0
  53. package/dist/domain/agentexecution/list-index.d.ts.map +1 -0
  54. package/dist/domain/agentexecution/list-index.js +23 -0
  55. package/dist/domain/agentexecution/list-index.js.map +1 -0
  56. package/dist/domain/agentexecution/steps.d.ts +17 -33
  57. package/dist/domain/agentexecution/steps.d.ts.map +1 -1
  58. package/dist/domain/agentexecution/steps.js +94 -95
  59. package/dist/domain/agentexecution/steps.js.map +1 -1
  60. package/dist/domain/agentexecution/usage.d.ts.map +1 -1
  61. package/dist/domain/agentexecution/usage.js +37 -14
  62. package/dist/domain/agentexecution/usage.js.map +1 -1
  63. package/dist/domain/artifact/controller.d.ts.map +1 -1
  64. package/dist/domain/artifact/controller.js +27 -15
  65. package/dist/domain/artifact/controller.js.map +1 -1
  66. package/dist/domain/artifact/list-index.d.ts +2 -0
  67. package/dist/domain/artifact/list-index.d.ts.map +1 -0
  68. package/dist/domain/artifact/list-index.js +21 -0
  69. package/dist/domain/artifact/list-index.js.map +1 -0
  70. package/dist/domain/iampolicy/access-lists.d.ts +5 -3
  71. package/dist/domain/iampolicy/access-lists.d.ts.map +1 -1
  72. package/dist/domain/iampolicy/access-lists.js +72 -19
  73. package/dist/domain/iampolicy/access-lists.js.map +1 -1
  74. package/dist/domain/iampolicy/constants.d.ts +32 -6
  75. package/dist/domain/iampolicy/constants.d.ts.map +1 -1
  76. package/dist/domain/iampolicy/constants.js +41 -6
  77. package/dist/domain/iampolicy/constants.js.map +1 -1
  78. package/dist/domain/iampolicy/controller.d.ts +6 -0
  79. package/dist/domain/iampolicy/controller.d.ts.map +1 -1
  80. package/dist/domain/iampolicy/controller.js +16 -7
  81. package/dist/domain/iampolicy/controller.js.map +1 -1
  82. package/dist/domain/iampolicy/resource-store.d.ts.map +1 -1
  83. package/dist/domain/iampolicy/resource-store.js +2 -6
  84. package/dist/domain/iampolicy/resource-store.js.map +1 -1
  85. package/dist/domain/iampolicy/steps.d.ts.map +1 -1
  86. package/dist/domain/iampolicy/steps.js +49 -7
  87. package/dist/domain/iampolicy/steps.js.map +1 -1
  88. package/dist/domain/session/list-index.d.ts +4 -0
  89. package/dist/domain/session/list-index.d.ts.map +1 -0
  90. package/dist/domain/session/list-index.js +26 -0
  91. package/dist/domain/session/list-index.js.map +1 -0
  92. package/dist/domain/session/steps.d.ts +7 -8
  93. package/dist/domain/session/steps.d.ts.map +1 -1
  94. package/dist/domain/session/steps.js +76 -78
  95. package/dist/domain/session/steps.js.map +1 -1
  96. package/dist/domain/workflow/registry/data/task-kind-registry.json +1 -0
  97. package/dist/domain/workflowexecution/controller.d.ts.map +1 -1
  98. package/dist/domain/workflowexecution/controller.js +53 -55
  99. package/dist/domain/workflowexecution/controller.js.map +1 -1
  100. package/dist/domain/workflowexecution/execution-filter.d.ts +4 -0
  101. package/dist/domain/workflowexecution/execution-filter.d.ts.map +1 -1
  102. package/dist/domain/workflowexecution/execution-filter.js +11 -5
  103. package/dist/domain/workflowexecution/execution-filter.js.map +1 -1
  104. package/dist/domain/workflowexecution/get-execution-summary.d.ts.map +1 -1
  105. package/dist/domain/workflowexecution/get-execution-summary.js +23 -17
  106. package/dist/domain/workflowexecution/get-execution-summary.js.map +1 -1
  107. package/dist/domain/workflowexecution/list-index.d.ts +2 -0
  108. package/dist/domain/workflowexecution/list-index.d.ts.map +1 -0
  109. package/dist/domain/workflowexecution/list-index.js +21 -0
  110. package/dist/domain/workflowexecution/list-index.js.map +1 -0
  111. package/dist/domain/workflowexecution/list-pending-approvals.d.ts.map +1 -1
  112. package/dist/domain/workflowexecution/list-pending-approvals.js +69 -32
  113. package/dist/domain/workflowexecution/list-pending-approvals.js.map +1 -1
  114. package/dist/domain/workflowexecution/queries.d.ts +30 -4
  115. package/dist/domain/workflowexecution/queries.d.ts.map +1 -1
  116. package/dist/domain/workflowexecution/queries.js +76 -22
  117. package/dist/domain/workflowexecution/queries.js.map +1 -1
  118. package/dist/extensions/drivers.d.ts +12 -0
  119. package/dist/extensions/drivers.d.ts.map +1 -1
  120. package/dist/extensions/gate-slots.d.ts +10 -1
  121. package/dist/extensions/gate-slots.d.ts.map +1 -1
  122. package/dist/extensions/gate-slots.js +1 -0
  123. package/dist/extensions/gate-slots.js.map +1 -1
  124. package/dist/extensions/list-read-scope.d.ts +9 -12
  125. package/dist/extensions/list-read-scope.d.ts.map +1 -1
  126. package/dist/extensions/list-read-scope.js.map +1 -1
  127. package/dist/extensions/principal-display.d.ts +34 -0
  128. package/dist/extensions/principal-display.d.ts.map +1 -0
  129. package/dist/extensions/principal-display.js +2 -0
  130. package/dist/extensions/principal-display.js.map +1 -0
  131. package/dist/extensions/registry.d.ts +6 -0
  132. package/dist/extensions/registry.d.ts.map +1 -1
  133. package/dist/extensions/registry.js +10 -0
  134. package/dist/extensions/registry.js.map +1 -1
  135. package/dist/index.d.ts +4 -2
  136. package/dist/index.d.ts.map +1 -1
  137. package/dist/index.js +6 -1
  138. package/dist/index.js.map +1 -1
  139. package/dist/pipeline/apiresource-meta.d.ts +10 -0
  140. package/dist/pipeline/apiresource-meta.d.ts.map +1 -1
  141. package/dist/pipeline/apiresource-meta.js +16 -0
  142. package/dist/pipeline/apiresource-meta.js.map +1 -1
  143. package/dist/pipeline/steps/list-page.d.ts +63 -0
  144. package/dist/pipeline/steps/list-page.d.ts.map +1 -0
  145. package/dist/pipeline/steps/list-page.js +187 -0
  146. package/dist/pipeline/steps/list-page.js.map +1 -0
  147. package/dist/query/activity/handler.d.ts +6 -7
  148. package/dist/query/activity/handler.d.ts.map +1 -1
  149. package/dist/query/activity/handler.js +42 -59
  150. package/dist/query/activity/handler.js.map +1 -1
  151. package/dist/store/interface.d.ts +29 -0
  152. package/dist/store/interface.d.ts.map +1 -1
  153. package/dist/store/interface.js.map +1 -1
  154. package/dist/store/list-index.d.ts +123 -0
  155. package/dist/store/list-index.d.ts.map +1 -0
  156. package/dist/store/list-index.js +277 -0
  157. package/dist/store/list-index.js.map +1 -0
  158. package/dist/store/logger.d.ts +3 -0
  159. package/dist/store/logger.d.ts.map +1 -1
  160. package/dist/store/logger.js +5 -1
  161. package/dist/store/logger.js.map +1 -1
  162. package/dist/store/postgres/migrations.d.ts +10 -6
  163. package/dist/store/postgres/migrations.d.ts.map +1 -1
  164. package/dist/store/postgres/migrations.js +64 -6
  165. package/dist/store/postgres/migrations.js.map +1 -1
  166. package/dist/store/postgres/store.d.ts +26 -5
  167. package/dist/store/postgres/store.d.ts.map +1 -1
  168. package/dist/store/postgres/store.js +377 -22
  169. package/dist/store/postgres/store.js.map +1 -1
  170. package/dist/store/sqlite/migrations.d.ts +11 -5
  171. package/dist/store/sqlite/migrations.d.ts.map +1 -1
  172. package/dist/store/sqlite/migrations.js +50 -4
  173. package/dist/store/sqlite/migrations.js.map +1 -1
  174. package/dist/store/sqlite/store.d.ts +18 -3
  175. package/dist/store/sqlite/store.d.ts.map +1 -1
  176. package/dist/store/sqlite/store.js +321 -15
  177. package/dist/store/sqlite/store.js.map +1 -1
  178. package/package.json +6 -6
  179. package/src/authorization/__tests__/drivers.ts +6 -1
  180. package/src/authorization/__tests__/evaluator.test.ts +127 -2
  181. package/src/authorization/evaluator.ts +12 -1
  182. package/src/authorization/model/__tests__/registry.test.ts +64 -2
  183. package/src/authorization/model/agent.ts +4 -1
  184. package/src/authorization/model/agent_channel.ts +11 -3
  185. package/src/authorization/model/agent_instance.ts +3 -1
  186. package/src/authorization/model/mcp_server.ts +3 -1
  187. package/src/authorization/model/organization.ts +3 -0
  188. package/src/authorization/model/plugin.ts +4 -1
  189. package/src/authorization/model/rewrite.ts +65 -8
  190. package/src/authorization/model/schedule.ts +11 -2
  191. package/src/authorization/model/skill.ts +3 -1
  192. package/src/authorization/model/workflow.ts +3 -1
  193. package/src/authorization/model/workflow_execution.ts +4 -2
  194. package/src/authorization/model/workflow_instance.ts +5 -0
  195. package/src/boot/__tests__/list-indexes.test.ts +65 -0
  196. package/src/boot/compose.ts +25 -13
  197. package/src/boot/list-indexes.ts +25 -0
  198. package/src/domain/agentexecution/__tests__/agentexecution.test.ts +85 -3
  199. package/src/domain/agentexecution/controller.ts +8 -10
  200. package/src/domain/agentexecution/list-index.ts +24 -0
  201. package/src/domain/agentexecution/steps.ts +118 -115
  202. package/src/domain/agentexecution/usage.ts +38 -17
  203. package/src/domain/artifact/controller.ts +28 -18
  204. package/src/domain/artifact/list-index.ts +22 -0
  205. package/src/domain/iampolicy/__tests__/access-lists.test.ts +120 -6
  206. package/src/domain/iampolicy/__tests__/constants.test.ts +26 -4
  207. package/src/domain/iampolicy/__tests__/controller.test.ts +55 -1
  208. package/src/domain/iampolicy/__tests__/grant-path.test.ts +3 -3
  209. package/src/domain/iampolicy/__tests__/steps.test.ts +123 -1
  210. package/src/domain/iampolicy/access-lists.ts +98 -23
  211. package/src/domain/iampolicy/constants.ts +50 -6
  212. package/src/domain/iampolicy/controller.ts +30 -7
  213. package/src/domain/iampolicy/resource-store.ts +3 -6
  214. package/src/domain/iampolicy/steps.ts +89 -7
  215. package/src/domain/session/list-index.ts +28 -0
  216. package/src/domain/session/steps.ts +108 -123
  217. package/src/domain/workflow/registry/data/task-kind-registry.json +1 -0
  218. package/src/domain/workflowexecution/__tests__/list-read-scope-summaries.test.ts +155 -99
  219. package/src/domain/workflowexecution/__tests__/workflowexecution.test.ts +193 -31
  220. package/src/domain/workflowexecution/controller.ts +54 -82
  221. package/src/domain/workflowexecution/execution-filter.ts +20 -7
  222. package/src/domain/workflowexecution/get-execution-summary.ts +31 -24
  223. package/src/domain/workflowexecution/list-index.ts +22 -0
  224. package/src/domain/workflowexecution/list-pending-approvals.ts +82 -25
  225. package/src/domain/workflowexecution/queries.ts +134 -23
  226. package/src/extensions/__tests__/iam-policy-points.test.ts +30 -0
  227. package/src/extensions/__tests__/registry.test.ts +3 -0
  228. package/src/extensions/drivers.ts +12 -0
  229. package/src/extensions/gate-slots.ts +10 -0
  230. package/src/extensions/list-read-scope.ts +9 -12
  231. package/src/extensions/principal-display.ts +37 -0
  232. package/src/extensions/registry.ts +19 -0
  233. package/src/index.ts +19 -1
  234. package/src/pipeline/apiresource-meta.ts +21 -0
  235. package/src/pipeline/steps/__tests__/list-page.test.ts +186 -0
  236. package/src/pipeline/steps/list-page.ts +284 -0
  237. package/src/query/activity/handler.ts +57 -78
  238. package/src/store/README.md +51 -32
  239. package/src/store/__tests__/list-index.measure.test.ts +293 -0
  240. package/src/store/__tests__/list-index.test.ts +328 -0
  241. package/src/store/__tests__/store-contract.ts +346 -3
  242. package/src/store/interface.ts +39 -0
  243. package/src/store/list-index.ts +465 -0
  244. package/src/store/logger.ts +8 -1
  245. package/src/store/postgres/__tests__/list-index-repair.test.ts +133 -0
  246. package/src/store/postgres/__tests__/migrations.test.ts +27 -19
  247. package/src/store/postgres/__tests__/store-contract.test.ts +50 -3
  248. package/src/store/postgres/migrations.ts +69 -7
  249. package/src/store/postgres/store.ts +485 -29
  250. package/src/store/sqlite/__tests__/migrations.test.ts +51 -7
  251. package/src/store/sqlite/__tests__/store-contract.test.ts +70 -16
  252. package/src/store/sqlite/__tests__/support.ts +12 -3
  253. package/src/store/sqlite/migrations.ts +57 -6
  254. package/src/store/sqlite/store.ts +462 -21
@@ -0,0 +1,328 @@
1
+ /**
2
+ * Pins the list index's one derivation (../list-index.ts), the pieces
3
+ * both drivers share: a declaration refuses a key its schema cannot
4
+ * answer at load; a row's facts are its organization, its creation
5
+ * instant as fixed-width text and every key it names; the instant sorts
6
+ * as text exactly as `compareCreatedAtDesc` orders timestamps (absent
7
+ * last); the query predicate and the merge the drivers finish with; and
8
+ * the registry's refusals. The drivers' SQL is pinned per driver by the
9
+ * list-index arms of store-contract.ts.
10
+ */
11
+ import { create } from "@bufbuild/protobuf";
12
+ import { describe, expect, it } from "vitest";
13
+
14
+ import { AgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
15
+ import { ArtifactSchema } from "@stigmer/protos/ai/stigmer/agentic/artifact/v1/api_pb";
16
+ import { SessionSchema } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
17
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
18
+
19
+ import { compareCreatedAtDesc } from "../../pipeline/steps/helpers.js";
20
+ import {
21
+ ListIndexRegistry,
22
+ compareListIndexOrder,
23
+ declareListIndex,
24
+ field,
25
+ label,
26
+ listIndexFactsOf,
27
+ listIndexInstant,
28
+ listIndexInstantOfMillis,
29
+ matchesListIndexQuery,
30
+ mergeListIndexRows,
31
+ sameListKeyRows,
32
+ } from "../list-index.js";
33
+ import type { ListIndexFacts, ListIndexRow } from "../list-index.js";
34
+
35
+ const sessions = declareListIndex({
36
+ kind: ApiResourceKind.session,
37
+ schema: SessionSchema,
38
+ revision: 1,
39
+ keys: {
40
+ agent_instance: field("spec.agent_instance_id"),
41
+ channel: label("stigmer.ai/channel-id"),
42
+ },
43
+ });
44
+
45
+ function row(createdAt: string, id: string): ListIndexRow {
46
+ return { id, data: new Uint8Array(), cursor: { createdAt, id } };
47
+ }
48
+
49
+ function facts(overrides: Partial<ListIndexFacts> = {}): ListIndexFacts {
50
+ return {
51
+ org: "acme",
52
+ createdAt: "2026-09-23T10:00:00.000000000Z",
53
+ revision: 1,
54
+ keys: [],
55
+ ...overrides,
56
+ };
57
+ }
58
+
59
+ describe("declareListIndex", () => {
60
+ it("refuses a path with no such field", () => {
61
+ expect(() =>
62
+ declareListIndex({
63
+ kind: ApiResourceKind.session,
64
+ schema: SessionSchema,
65
+ revision: 1,
66
+ keys: { wrong: field("spec.agent_instance") },
67
+ }),
68
+ ).toThrow(
69
+ "list index for session, key 'wrong': 'spec.agent_instance' has no field 'agent_instance'",
70
+ );
71
+ });
72
+
73
+ it("refuses a path that ends on a message, not a string", () => {
74
+ expect(() =>
75
+ declareListIndex({
76
+ kind: ApiResourceKind.session,
77
+ schema: SessionSchema,
78
+ revision: 1,
79
+ keys: { spec: field("spec") },
80
+ }),
81
+ ).toThrow("'spec' is not a string field");
82
+ });
83
+
84
+ it("refuses a path that walks through a scalar", () => {
85
+ expect(() =>
86
+ declareListIndex({
87
+ kind: ApiResourceKind.session,
88
+ schema: SessionSchema,
89
+ revision: 1,
90
+ keys: { deep: field("spec.agent_instance_id.nested") },
91
+ }),
92
+ ).toThrow(
93
+ "'agent_instance_id' in 'spec.agent_instance_id.nested' is not a message",
94
+ );
95
+ });
96
+
97
+ it("refuses a revision that is not a positive integer", () => {
98
+ expect(() =>
99
+ declareListIndex({
100
+ kind: ApiResourceKind.session,
101
+ schema: SessionSchema,
102
+ revision: 0,
103
+ keys: {},
104
+ }),
105
+ ).toThrow("revision must be a positive integer");
106
+ });
107
+ });
108
+
109
+ describe("listIndexFactsOf", () => {
110
+ it("reads the organization, the creation instant and every named key", () => {
111
+ const session = create(SessionSchema, {
112
+ metadata: {
113
+ id: "ses_1",
114
+ org: "acme",
115
+ labels: { "stigmer.ai/channel-id": "ach_1" },
116
+ },
117
+ spec: { agentInstanceId: "ain_1" },
118
+ status: {
119
+ audit: {
120
+ specAudit: { createdAt: { seconds: 1_790_000_000n, nanos: 5 } },
121
+ },
122
+ },
123
+ });
124
+ expect(listIndexFactsOf(sessions, session)).toEqual({
125
+ org: "acme",
126
+ createdAt: "2026-09-21T14:13:20.000000005Z",
127
+ revision: 1,
128
+ keys: [
129
+ { key: "agent_instance", value: "ain_1" },
130
+ { key: "channel", value: "ach_1" },
131
+ ],
132
+ });
133
+ });
134
+
135
+ it("leaves out a key the row does not name, and reads absences as empty", () => {
136
+ expect(listIndexFactsOf(sessions, create(SessionSchema, {}))).toEqual({
137
+ org: "",
138
+ createdAt: "",
139
+ revision: 1,
140
+ keys: [],
141
+ });
142
+ });
143
+
144
+ it("follows a nested field path", () => {
145
+ const artifacts = declareListIndex({
146
+ kind: ApiResourceKind.artifact,
147
+ schema: ArtifactSchema,
148
+ revision: 1,
149
+ keys: { agent_execution: field("spec.source.agent_execution_id") },
150
+ });
151
+ const artifact = create(ArtifactSchema, {
152
+ spec: { source: { agentExecutionId: "aex_1" } },
153
+ });
154
+ expect(listIndexFactsOf(artifacts, artifact).keys).toEqual([
155
+ { key: "agent_execution", value: "aex_1" },
156
+ ]);
157
+ });
158
+
159
+ it("refuses a declaration that was not made by declareListIndex", () => {
160
+ expect(() =>
161
+ listIndexFactsOf(
162
+ {
163
+ kind: ApiResourceKind.agent_execution,
164
+ schema: AgentExecutionSchema,
165
+ revision: 1,
166
+ keys: {},
167
+ },
168
+ create(AgentExecutionSchema, {}),
169
+ ),
170
+ ).toThrow("was not made by declareListIndex");
171
+ });
172
+ });
173
+
174
+ describe("listIndexInstant", () => {
175
+ it("is fixed-width UTC text to the nanosecond", () => {
176
+ expect(listIndexInstant({ seconds: 0n, nanos: 0 })).toBe(
177
+ "1970-01-01T00:00:00.000000000Z",
178
+ );
179
+ expect(listIndexInstantOfMillis(1_500)).toBe(
180
+ "1970-01-01T00:00:01.500000000Z",
181
+ );
182
+ });
183
+
184
+ it("is empty for an absent or unrepresentable stamp", () => {
185
+ expect(listIndexInstant(undefined)).toBe("");
186
+ expect(listIndexInstant({ seconds: 300_000_000_000n, nanos: 0 })).toBe("");
187
+ });
188
+
189
+ it("sorts as text exactly as compareCreatedAtDesc orders the timestamps", () => {
190
+ const stamps = [
191
+ { seconds: 10n, nanos: 2 },
192
+ undefined,
193
+ { seconds: 10n, nanos: 10 },
194
+ { seconds: 9n, nanos: 999_999_999 },
195
+ { seconds: 1_790_000_000n, nanos: 0 },
196
+ ];
197
+ const byTimestamp = [...stamps].sort((a, b) =>
198
+ compareCreatedAtDesc(a as never, b as never),
199
+ );
200
+ const byText = [...stamps].sort((a, b) =>
201
+ compareListIndexOrder(
202
+ { createdAt: listIndexInstant(a), id: "" },
203
+ { createdAt: listIndexInstant(b), id: "" },
204
+ ),
205
+ );
206
+ expect(byText).toEqual(byTimestamp);
207
+ });
208
+ });
209
+
210
+ describe("matchesListIndexQuery", () => {
211
+ const withChannel = facts({ keys: [{ key: "channel", value: "ach_1" }] });
212
+
213
+ it("matches an organization only exactly, and every organization when absent or empty", () => {
214
+ expect(matchesListIndexQuery("x", withChannel, { org: "acme" })).toBe(true);
215
+ expect(matchesListIndexQuery("x", withChannel, { org: "other" })).toBe(
216
+ false,
217
+ );
218
+ expect(matchesListIndexQuery("x", withChannel, { org: "" })).toBe(true);
219
+ });
220
+
221
+ it("matches any one of the listed keys", () => {
222
+ expect(
223
+ matchesListIndexQuery("x", withChannel, {
224
+ anyKey: [
225
+ { name: "agent_instance", value: "ain_9" },
226
+ { name: "channel", value: "ach_1" },
227
+ ],
228
+ }),
229
+ ).toBe(true);
230
+ expect(
231
+ matchesListIndexQuery("x", withChannel, {
232
+ anyKey: [{ name: "channel", value: "ach_2" }],
233
+ }),
234
+ ).toBe(false);
235
+ });
236
+
237
+ it("keeps rows created at or after the bound, and rows with no stamp", () => {
238
+ const bound = "2026-09-23T10:00:00.000000000Z";
239
+ expect(
240
+ matchesListIndexQuery("x", facts(), { createdAtOrAfter: bound }),
241
+ ).toBe(true);
242
+ expect(
243
+ matchesListIndexQuery(
244
+ "x",
245
+ facts({ createdAt: "2026-09-22T00:00:00.000000000Z" }),
246
+ {
247
+ createdAtOrAfter: bound,
248
+ },
249
+ ),
250
+ ).toBe(false);
251
+ expect(
252
+ matchesListIndexQuery("x", facts({ createdAt: "" }), {
253
+ createdAtOrAfter: bound,
254
+ }),
255
+ ).toBe(true);
256
+ });
257
+
258
+ it("keeps only rows strictly after the cursor in newest-first order", () => {
259
+ const at = "2026-09-23T10:00:00.000000000Z";
260
+ const after = { createdAt: at, id: "m" };
261
+ expect(
262
+ matchesListIndexQuery("a", facts({ createdAt: at }), { after }),
263
+ ).toBe(true);
264
+ expect(
265
+ matchesListIndexQuery("m", facts({ createdAt: at }), { after }),
266
+ ).toBe(false);
267
+ expect(
268
+ matchesListIndexQuery("z", facts({ createdAt: at }), { after }),
269
+ ).toBe(false);
270
+ expect(
271
+ matchesListIndexQuery(
272
+ "z",
273
+ facts({ createdAt: "2026-09-22T00:00:00.000000000Z" }),
274
+ { after },
275
+ ),
276
+ ).toBe(true);
277
+ });
278
+ });
279
+
280
+ describe("mergeListIndexRows", () => {
281
+ it("merges in order, keeps one row per id and cuts to the limit", () => {
282
+ const merged = mergeListIndexRows(
283
+ [row("2026-09-23", "b"), row("2026-09-21", "d")],
284
+ [row("2026-09-22", "c"), row("2026-09-23", "b"), row("", "e")],
285
+ 3,
286
+ );
287
+ expect(merged.map((r) => r.id)).toEqual(["b", "c", "d"]);
288
+ expect(
289
+ mergeListIndexRows(
290
+ [row("", "e")],
291
+ [row("2026-09-23", "a")],
292
+ undefined,
293
+ ).map((r) => r.id),
294
+ ).toEqual(["a", "e"]);
295
+ });
296
+ });
297
+
298
+ describe("sameListKeyRows", () => {
299
+ it("is equal only when the instant and every key agree", () => {
300
+ const a = facts({ keys: [{ key: "channel", value: "ach_1" }] });
301
+ expect(
302
+ sameListKeyRows(a, facts({ keys: [{ key: "channel", value: "ach_1" }] })),
303
+ ).toBe(true);
304
+ expect(
305
+ sameListKeyRows(a, facts({ keys: [{ key: "channel", value: "ach_2" }] })),
306
+ ).toBe(false);
307
+ expect(sameListKeyRows(a, facts({ createdAt: "", keys: a.keys }))).toBe(
308
+ false,
309
+ );
310
+ });
311
+ });
312
+
313
+ describe("ListIndexRegistry", () => {
314
+ it("refuses a kind declared twice", () => {
315
+ expect(() => new ListIndexRegistry([sessions, sessions])).toThrow(
316
+ "list index for session is declared twice",
317
+ );
318
+ });
319
+
320
+ it("refuses a read through a declaration it was not opened with", () => {
321
+ const other = declareListIndex({ ...sessions, keys: {} });
322
+ const registry = new ListIndexRegistry([sessions]);
323
+ expect(() => registry.require(sessions)).not.toThrow();
324
+ expect(() => registry.require(other)).toThrow(
325
+ "list index for session is not registered with this store",
326
+ );
327
+ });
328
+ });