pi-durable-python 1.3.0__py3-none-any.whl

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 (90) hide show
  1. pi_durable/__init__.py +320 -0
  2. pi_durable/context.py +328 -0
  3. pi_durable/delta.py +269 -0
  4. pi_durable/documents.py +366 -0
  5. pi_durable/entries.py +66 -0
  6. pi_durable/env/__init__.py +408 -0
  7. pi_durable/env/decode.py +48 -0
  8. pi_durable/env/line_scan.py +190 -0
  9. pi_durable/env/posix.py +1547 -0
  10. pi_durable/errors.py +89 -0
  11. pi_durable/harness/__init__.py +1 -0
  12. pi_durable/harness/agent.py +344 -0
  13. pi_durable/harness/compaction.py +673 -0
  14. pi_durable/harness/context.py +515 -0
  15. pi_durable/harness/define.py +111 -0
  16. pi_durable/harness/events.py +766 -0
  17. pi_durable/harness/generation.py +1079 -0
  18. pi_durable/harness/harness.py +570 -0
  19. pi_durable/harness/inbox.py +255 -0
  20. pi_durable/harness/live.py +374 -0
  21. pi_durable/harness/output.py +467 -0
  22. pi_durable/harness/prompt.py +288 -0
  23. pi_durable/harness/provider.py +78 -0
  24. pi_durable/harness/registry.py +136 -0
  25. pi_durable/harness/scheduler.py +1977 -0
  26. pi_durable/harness/submissions.py +357 -0
  27. pi_durable/harness/task_graph.py +334 -0
  28. pi_durable/harness/tool.py +1365 -0
  29. pi_durable/harness/types.py +772 -0
  30. pi_durable/harness/usage.py +88 -0
  31. pi_durable/harness/util.py +204 -0
  32. pi_durable/harness/validation.py +460 -0
  33. pi_durable/harness/view.py +324 -0
  34. pi_durable/ids.py +63 -0
  35. pi_durable/json.py +186 -0
  36. pi_durable/providers/__init__.py +362 -0
  37. pi_durable/providers/anthropic.py +603 -0
  38. pi_durable/providers/base.py +97 -0
  39. pi_durable/providers/json_parse.py +117 -0
  40. pi_durable/providers/openai.py +574 -0
  41. pi_durable/providers/request.py +35 -0
  42. pi_durable/providers/retry.py +127 -0
  43. pi_durable/providers/stream.py +183 -0
  44. pi_durable/providers/transcript.py +224 -0
  45. pi_durable/providers/usage.py +58 -0
  46. pi_durable/py.typed +0 -0
  47. pi_durable/search.py +115 -0
  48. pi_durable/session/__init__.py +5 -0
  49. pi_durable/session/drafts.py +762 -0
  50. pi_durable/session/forks.py +109 -0
  51. pi_durable/session/observation.py +374 -0
  52. pi_durable/session/session.py +1193 -0
  53. pi_durable/session/transaction.py +1088 -0
  54. pi_durable/storage/__init__.py +5 -0
  55. pi_durable/storage/jsonl/__init__.py +15 -0
  56. pi_durable/storage/jsonl/storage.py +958 -0
  57. pi_durable/storage/memory.py +889 -0
  58. pi_durable/storage/scan.py +57 -0
  59. pi_durable/storage/sqlite/__init__.py +26 -0
  60. pi_durable/storage/sqlite/database.py +62 -0
  61. pi_durable/storage/sqlite/migrations.py +191 -0
  62. pi_durable/storage/sqlite/posix.py +314 -0
  63. pi_durable/storage/sqlite/schema.py +97 -0
  64. pi_durable/storage/sqlite/storage.py +1130 -0
  65. pi_durable/tasks.py +89 -0
  66. pi_durable/testing/__init__.py +41 -0
  67. pi_durable/testing/assertions.py +44 -0
  68. pi_durable/testing/env_conformance.py +828 -0
  69. pi_durable/testing/runner.py +64 -0
  70. pi_durable/testing/storage_conformance.py +2008 -0
  71. pi_durable/testing/types.py +99 -0
  72. pi_durable/tool_search/__init__.py +159 -0
  73. pi_durable/tools/__init__.py +81 -0
  74. pi_durable/tools/_values.py +43 -0
  75. pi_durable/tools/bash.py +250 -0
  76. pi_durable/tools/edit.py +214 -0
  77. pi_durable/tools/edit_diff.py +452 -0
  78. pi_durable/tools/env.py +19 -0
  79. pi_durable/tools/file_mutation_queue.py +68 -0
  80. pi_durable/tools/image.py +248 -0
  81. pi_durable/tools/image_processor.py +77 -0
  82. pi_durable/tools/path_utils.py +111 -0
  83. pi_durable/tools/read.py +506 -0
  84. pi_durable/tools/write.py +71 -0
  85. pi_durable/truncate.py +185 -0
  86. pi_durable/types.py +1297 -0
  87. pi_durable_python-1.3.0.dist-info/METADATA +276 -0
  88. pi_durable_python-1.3.0.dist-info/RECORD +90 -0
  89. pi_durable_python-1.3.0.dist-info/WHEEL +4 -0
  90. pi_durable_python-1.3.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,2008 @@
1
+ """Runner-independent storage conformance cases, translated from v1.3.0 ``testing``.
2
+
3
+ The suite covers atomic commits, detached values, cursor pagination and order, fork ancestry,
4
+ task and submission indexes, document history and copies, global ID ownership, and close
5
+ behavior. It exercises only the public :class:`~pi_durable.types.Storage` contract.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from collections.abc import Awaitable, Callable, Sequence
12
+ from typing import Any, TypeAlias, cast
13
+
14
+ from ..context import BACKGROUND_CONTEXT
15
+ from ..ids import (
16
+ ROOT_CONVERSATION_ID,
17
+ ConversationId,
18
+ EntryId,
19
+ id_from_number,
20
+ )
21
+ from ..types import Cursor, Page, ScanOrder, Storage, StorageWrite
22
+ from .types import (
23
+ StorageConformanceAssertions,
24
+ StorageConformanceCase,
25
+ StorageConformanceOptions,
26
+ )
27
+
28
+ context = BACKGROUND_CONTEXT
29
+
30
+ _MAX_SAFE_INTEGER = 9_007_199_254_740_991
31
+
32
+
33
+ async def _create_root(storage: Storage) -> ConversationId:
34
+ await storage.commit([{"type": "conversation", "value": {"id": ROOT_CONVERSATION_ID}}], context)
35
+ return ROOT_CONVERSATION_ID
36
+
37
+
38
+ def _pending_task(id: int, conversation_id: ConversationId, phase: str = "ready") -> dict[str, Any]:
39
+ return {
40
+ "id": id,
41
+ "conversationId": conversation_id,
42
+ "kind": "test.task",
43
+ "version": 1,
44
+ "input": {"value": id},
45
+ "state": {"status": "pending", "checkpoint": {"phase": phase}},
46
+ "background": False,
47
+ "abortRequested": False,
48
+ }
49
+
50
+
51
+ def _entry(
52
+ id: int,
53
+ conversation_id: ConversationId,
54
+ kind: str = "message",
55
+ **extra: Any,
56
+ ) -> dict[str, Any]:
57
+ return {"id": id, "conversationId": conversation_id, "kind": kind, **extra}
58
+
59
+
60
+ class _Rejects:
61
+ def __init__(self, actual: object, assertions: StorageConformanceAssertions) -> None:
62
+ self._actual = actual
63
+ self._assertions = assertions
64
+
65
+ async def to_throw(self, message_includes: str) -> None:
66
+ await self._assertions.rejects(cast(Awaitable[object], self._actual), message_includes)
67
+
68
+
69
+ class _Resolves:
70
+ def __init__(self, actual: object, assertions: StorageConformanceAssertions) -> None:
71
+ self._actual = actual
72
+ self._assertions = assertions
73
+
74
+ async def to_be(self, expected: object) -> None:
75
+ self._assertions.strict_equal(await cast(Awaitable[Any], self._actual), expected)
76
+
77
+
78
+ class _AssertionResult:
79
+ def __init__(self, actual: object, assertions: StorageConformanceAssertions) -> None:
80
+ self._actual = actual
81
+ self._assertions = assertions
82
+
83
+ def to_be(self, expected: object) -> None:
84
+ self._assertions.strict_equal(self._actual, expected)
85
+
86
+ def to_be_defined(self) -> None:
87
+ self._assertions.ok(self._actual is not None, "Expected value to be defined")
88
+
89
+ def to_be_greater_than(self, expected: float) -> None:
90
+ self._assertions.greater_than(cast(float, self._actual), expected)
91
+
92
+ def to_be_undefined(self) -> None:
93
+ self._assertions.strict_equal(self._actual, None)
94
+
95
+ def to_equal(self, expected: object) -> None:
96
+ self._assertions.deep_equal(self._actual, expected)
97
+
98
+ def to_have_length(self, expected: int) -> None:
99
+ self._assertions.strict_equal(len(cast(Sequence[Any], self._actual)), expected)
100
+
101
+ def to_match_object(self, expected: object) -> None:
102
+ self._assertions.partial_deep_equal(self._actual, expected)
103
+
104
+ @property
105
+ def rejects(self) -> _Rejects:
106
+ return _Rejects(self._actual, self._assertions)
107
+
108
+ @property
109
+ def resolves(self) -> _Resolves:
110
+ return _Resolves(self._actual, self._assertions)
111
+
112
+
113
+ def _assertion_facade(
114
+ assertions: StorageConformanceAssertions,
115
+ ) -> Callable[[object], _AssertionResult]:
116
+ return lambda actual: _AssertionResult(actual, assertions)
117
+
118
+
119
+ def _create_case(
120
+ options: StorageConformanceOptions,
121
+ name: str,
122
+ test: Callable[[Storage], Awaitable[None]],
123
+ ) -> StorageConformanceCase:
124
+ async def run() -> None:
125
+ await options["with_storage"](test)
126
+
127
+ return StorageConformanceCase(name=name, run=run)
128
+
129
+
130
+ def create_storage_conformance(options: StorageConformanceOptions) -> list[StorageConformanceCase]:
131
+ """Create runner-independent cases; the provider calls and awaits its callback once per case."""
132
+
133
+ expect = _assertion_facade(options["assertions"])
134
+
135
+ async def reserves_id_one(storage: Storage) -> None:
136
+ expect(await storage.mint_id()).to_be(2)
137
+ await expect(_create_root(storage)).resolves.to_be(ROOT_CONVERSATION_ID)
138
+ expect(await storage.conversation(ROOT_CONVERSATION_ID, context)).to_equal(
139
+ {"id": ROOT_CONVERSATION_ID}
140
+ )
141
+ await expect(
142
+ storage.commit(
143
+ [{"type": "conversation", "value": {"id": ROOT_CONVERSATION_ID}}], context
144
+ )
145
+ ).rejects.to_throw(f"ID {ROOT_CONVERSATION_ID} already belongs to conversation")
146
+
147
+ async def commits_mixed_table_writes(storage: Storage) -> None:
148
+ root_id = await _create_root(storage)
149
+ entry_id = await storage.mint_id()
150
+ task_id = await storage.mint_id()
151
+ submission_id = await storage.mint_id()
152
+ task = _pending_task(task_id, root_id)
153
+ submission = {
154
+ "id": submission_id,
155
+ "conversationId": root_id,
156
+ "requestId": "request-1",
157
+ "type": "input",
158
+ "status": "placed",
159
+ "entry": entry_id,
160
+ }
161
+ initial_seq = await storage.commit(
162
+ [
163
+ {
164
+ "type": "entry",
165
+ "value": _entry(entry_id, root_id, "user", data={"text": "hello"}),
166
+ },
167
+ {"type": "task", "value": task},
168
+ {"type": "submission", "value": submission},
169
+ ],
170
+ context,
171
+ )
172
+
173
+ expect(await storage.entry(entry_id, context)).to_equal(
174
+ {
175
+ "entry": _entry(entry_id, root_id, "user", data={"text": "hello"}),
176
+ "commitSeq": initial_seq,
177
+ }
178
+ )
179
+ expect(await storage.task(task_id, context)).to_equal(task)
180
+ expect(await storage.submission(submission_id, context)).to_equal(submission)
181
+
182
+ transient_entry_id = await storage.mint_id()
183
+ running_task = {**task, "state": {"status": "running", "checkpoint": {"phase": "effect"}}}
184
+ done_submission = {**submission, "status": "done", "answer": transient_entry_id}
185
+ await expect(
186
+ storage.commit(
187
+ [
188
+ {"type": "task", "value": running_task},
189
+ {"type": "submission", "value": done_submission},
190
+ {"type": "entry", "value": _entry(transient_entry_id, root_id, "assistant")},
191
+ {"type": "conversation", "value": {"id": root_id}},
192
+ ],
193
+ context,
194
+ )
195
+ ).rejects.to_throw(f"ID {root_id} already belongs to conversation")
196
+
197
+ expect(await storage.task(task_id, context)).to_equal(task)
198
+ expect(await storage.submission(submission_id, context)).to_equal(submission)
199
+ expect(await storage.entry(transient_entry_id, context)).to_be_undefined()
200
+ after_rollback_seq = await storage.commit(
201
+ [
202
+ {
203
+ "type": "entry",
204
+ "value": _entry(await storage.mint_id(), root_id, "after-rollback"),
205
+ }
206
+ ],
207
+ context,
208
+ )
209
+ expect(after_rollback_seq).to_be_greater_than(initial_seq)
210
+
211
+ async def detaches_retained_writes(storage: Storage) -> None:
212
+ root_id = await _create_root(storage)
213
+ entry_id = await storage.mint_id()
214
+ task_id = await storage.mint_id()
215
+ submission_id = await storage.mint_id()
216
+ entry_data: dict[str, Any] = {"nested": [1, 2]}
217
+ checkpoint: dict[str, Any] = {"phase": "ready", "nested": {"count": 1}}
218
+ detail: dict[str, Any] = {"codes": ["initial"]}
219
+ stored_entry = _entry(entry_id, root_id, "note", data=entry_data)
220
+ stored_task = {
221
+ **_pending_task(task_id, root_id),
222
+ "state": {"status": "pending", "checkpoint": checkpoint},
223
+ }
224
+ stored_submission = {
225
+ "id": submission_id,
226
+ "conversationId": root_id,
227
+ "type": "input",
228
+ "status": "unanswered",
229
+ "reason": "failed",
230
+ "detail": detail,
231
+ }
232
+ await storage.commit(
233
+ [
234
+ {"type": "entry", "value": stored_entry},
235
+ {"type": "task", "value": stored_task},
236
+ {"type": "submission", "value": stored_submission},
237
+ ],
238
+ context,
239
+ )
240
+
241
+ entry_data["nested"].append(3)
242
+ checkpoint["nested"]["count"] = 2
243
+ detail["codes"].append("mutated")
244
+ read_entry = await storage.entry(entry_id, context)
245
+ assert read_entry is not None
246
+ expect(read_entry["entry"]["data"]).to_equal({"nested": [1, 2]})
247
+ read_task = await storage.task(task_id, context)
248
+ assert read_task is not None
249
+ expect(read_task["state"]).to_equal(
250
+ {"status": "pending", "checkpoint": {"phase": "ready", "nested": {"count": 1}}}
251
+ )
252
+ read_submission = await storage.submission(submission_id, context)
253
+ assert read_submission is not None
254
+ expect(read_submission["detail"]).to_equal({"codes": ["initial"]})
255
+
256
+ read_entry["entry"]["data"]["nested"].append(9)
257
+ if read_task["state"]["status"] != "terminal":
258
+ read_task["state"]["checkpoint"]["nested"]["count"] = 9
259
+ read_submission["detail"]["codes"].append("read mutation")
260
+
261
+ again_entry = await storage.entry(entry_id, context)
262
+ assert again_entry is not None
263
+ expect(again_entry["entry"]["data"]).to_equal({"nested": [1, 2]})
264
+ again_task = await storage.task(task_id, context)
265
+ assert again_task is not None
266
+ expect(again_task["state"]).to_equal(
267
+ {"status": "pending", "checkpoint": {"phase": "ready", "nested": {"count": 1}}}
268
+ )
269
+ again_submission = await storage.submission(submission_id, context)
270
+ assert again_submission is not None
271
+ expect(again_submission["detail"]).to_equal({"codes": ["initial"]})
272
+
273
+ async def detaches_prototype_like_keys(storage: Storage) -> None:
274
+ # Python has no prototype chain, so the analogue is that these names stay ordinary,
275
+ # lossless JSON keys and stored values are detached from the caller's objects.
276
+ root_id = await _create_root(storage)
277
+ entry_id = await storage.mint_id()
278
+ data: dict[str, Any] = json.loads(
279
+ '{"__proto__":{"polluted":false},"constructor":{"label":"stored"},"toString":"value"}'
280
+ )
281
+ await storage.commit(
282
+ [{"type": "entry", "value": _entry(entry_id, root_id, "note", data=data)}], context
283
+ )
284
+
285
+ data["__proto__"]["polluted"] = True
286
+ data["constructor"]["label"] = "mutated"
287
+ first_read = await storage.entry(entry_id, context)
288
+ assert first_read is not None
289
+ first_data = first_read["entry"]["data"]
290
+ expect(sorted(first_data)).to_equal(["__proto__", "constructor", "toString"])
291
+ expect(first_data["__proto__"]).to_equal({"polluted": False})
292
+ expect(first_data["constructor"]).to_equal({"label": "stored"})
293
+ expect(first_data["toString"]).to_be("value")
294
+
295
+ first_data["__proto__"]["polluted"] = True
296
+ second_read = await storage.entry(entry_id, context)
297
+ assert second_read is not None
298
+ second_data = second_read["entry"]["data"]
299
+ expect(second_data["__proto__"]).to_equal({"polluted": False})
300
+ expect(second_data["constructor"]).to_equal({"label": "stored"})
301
+ expect(second_data["toString"]).to_be("value")
302
+
303
+ async def indexes_entries_out_of_order(storage: Storage) -> None:
304
+ root_id = await _create_root(storage)
305
+ await storage.commit(
306
+ [
307
+ {"type": "entry", "value": _entry(30, root_id)},
308
+ {"type": "entry", "value": _entry(10, root_id)},
309
+ {
310
+ "type": "entry",
311
+ "value": _entry(20, root_id, "marker", head=EntryId(10)),
312
+ },
313
+ ],
314
+ context,
315
+ )
316
+
317
+ page = await storage.scan_entries({"conversationId": root_id}, 10, None, context)
318
+ expect([item["id"] for item in page["items"]]).to_equal([30, 20, 10])
319
+ marker = await storage.find_latest_head_marker(root_id, None, context)
320
+ assert marker is not None
321
+ expect(marker["id"]).to_be(20)
322
+
323
+ async def continues_entry_cursor(storage: Storage) -> None:
324
+ root_id = await _create_root(storage)
325
+ oldest_id = await storage.mint_id()
326
+ middle_id = await storage.mint_id()
327
+ newest_id = await storage.mint_id()
328
+ await storage.commit(
329
+ [
330
+ {"type": "entry", "value": _entry(oldest_id, root_id)},
331
+ {"type": "entry", "value": _entry(middle_id, root_id)},
332
+ {"type": "entry", "value": _entry(newest_id, root_id)},
333
+ ],
334
+ context,
335
+ )
336
+
337
+ first = await storage.scan_entries({"conversationId": root_id}, 2, None, context)
338
+ expect([item["id"] for item in first["items"]]).to_equal([newest_id, middle_id])
339
+ appended_id = await storage.mint_id()
340
+ await storage.commit([{"type": "entry", "value": _entry(appended_id, root_id)}], context)
341
+ second = await storage.scan_entries({"conversationId": root_id}, 2, first.get("next"), context)
342
+ expect([item["id"] for item in second["items"]]).to_equal([oldest_id])
343
+ expect(second.get("next")).to_be_undefined()
344
+
345
+ async def paginates_conversations(storage: Storage) -> None:
346
+ root_id = await _create_root(storage)
347
+ second_id = await storage.mint_id()
348
+ third_id = await storage.mint_id()
349
+ await storage.commit(
350
+ [
351
+ {"type": "conversation", "value": {"id": third_id}},
352
+ {"type": "conversation", "value": {"id": second_id}},
353
+ ],
354
+ context,
355
+ )
356
+
357
+ first = await storage.scan_conversations({}, 2, None, context)
358
+ expect([item["id"] for item in first["items"]]).to_equal([root_id, second_id])
359
+ expect(first.get("next")).to_be_defined()
360
+ round_tripped: Cursor = json.loads(json.dumps(first["next"]))
361
+ second = await storage.scan_conversations({}, 2, round_tripped, context)
362
+ expect([item["id"] for item in second["items"]]).to_equal([third_id])
363
+ expect(second.get("next")).to_be_undefined()
364
+
365
+ async def filters_conversations_by_owner(storage: Storage) -> None:
366
+ root_id = await _create_root(storage)
367
+ other_owner_id = await storage.mint_id()
368
+ first_task_id = await storage.mint_id()
369
+ second_task_id = await storage.mint_id()
370
+ first_id = await storage.mint_id()
371
+ second_id = await storage.mint_id()
372
+ third_id = await storage.mint_id()
373
+ await storage.commit(
374
+ [
375
+ {"type": "conversation", "value": {"id": other_owner_id}},
376
+ {
377
+ "type": "conversation",
378
+ "value": {
379
+ "id": first_id,
380
+ "owner": {"conversationId": root_id, "taskId": first_task_id},
381
+ },
382
+ },
383
+ {
384
+ "type": "conversation",
385
+ "value": {
386
+ "id": second_id,
387
+ "owner": {"conversationId": root_id, "taskId": second_task_id},
388
+ },
389
+ },
390
+ {
391
+ "type": "conversation",
392
+ "value": {
393
+ "id": third_id,
394
+ "owner": {"conversationId": other_owner_id, "taskId": first_task_id},
395
+ },
396
+ },
397
+ ],
398
+ context,
399
+ )
400
+
401
+ first = await storage.scan_conversations(
402
+ {"ownerConversationId": root_id}, 1, None, context
403
+ )
404
+ expect([item["id"] for item in first["items"]]).to_equal([first_id])
405
+ expect(first.get("next")).to_be_defined()
406
+ second = await storage.scan_conversations(
407
+ {"ownerConversationId": root_id}, 1, first.get("next"), context
408
+ )
409
+ expect([item["id"] for item in second["items"]]).to_equal([second_id])
410
+ expect(second.get("next")).to_be_undefined()
411
+ expect(
412
+ [
413
+ item["id"]
414
+ for item in (
415
+ await storage.scan_conversations({"ownerTaskId": first_task_id}, 10, None, context)
416
+ )["items"]
417
+ ]
418
+ ).to_equal([first_id, third_id])
419
+ expect(
420
+ [
421
+ item["id"]
422
+ for item in (
423
+ await storage.scan_conversations(
424
+ {"ownerConversationId": root_id, "ownerTaskId": first_task_id},
425
+ 10,
426
+ None,
427
+ context,
428
+ )
429
+ )["items"]
430
+ ]
431
+ ).to_equal([first_id])
432
+
433
+ async def scans_deep_fork_history(storage: Storage) -> None:
434
+ root_id = await _create_root(storage)
435
+ root_first = await storage.mint_id()
436
+ root_fork_point = await storage.mint_id()
437
+ root_excluded_same_commit = await storage.mint_id()
438
+ root_entries_seq = await storage.commit(
439
+ [
440
+ {"type": "entry", "value": _entry(root_first, root_id)},
441
+ {
442
+ "type": "entry",
443
+ "value": _entry(root_fork_point, root_id, "marker", head=root_first),
444
+ },
445
+ {"type": "entry", "value": _entry(root_excluded_same_commit, root_id)},
446
+ ],
447
+ context,
448
+ )
449
+ child_id = await storage.mint_id()
450
+ await storage.commit(
451
+ [
452
+ {
453
+ "type": "conversation",
454
+ "value": {
455
+ "id": child_id,
456
+ "parent": {"conversationId": root_id, "at": root_fork_point},
457
+ },
458
+ },
459
+ ],
460
+ context,
461
+ )
462
+ child_fork_point = await storage.mint_id()
463
+ child_excluded = await storage.mint_id()
464
+ await storage.commit(
465
+ [
466
+ {"type": "entry", "value": _entry(child_fork_point, child_id, "note")},
467
+ {"type": "entry", "value": _entry(child_excluded, child_id)},
468
+ ],
469
+ context,
470
+ )
471
+ root_excluded_later = await storage.mint_id()
472
+ await storage.commit(
473
+ [{"type": "entry", "value": _entry(root_excluded_later, root_id)}], context
474
+ )
475
+ grandchild_id = await storage.mint_id()
476
+ await storage.commit(
477
+ [
478
+ {
479
+ "type": "conversation",
480
+ "value": {
481
+ "id": grandchild_id,
482
+ "parent": {"conversationId": child_id, "at": child_fork_point},
483
+ },
484
+ },
485
+ ],
486
+ context,
487
+ )
488
+ grandchild_head = await storage.mint_id()
489
+ grandchild_tail = await storage.mint_id()
490
+ grandchild_entries_seq = await storage.commit(
491
+ [
492
+ {
493
+ "type": "entry",
494
+ "value": _entry(grandchild_head, grandchild_id, "marker", head=grandchild_head),
495
+ },
496
+ {"type": "entry", "value": _entry(grandchild_tail, grandchild_id)},
497
+ ],
498
+ context,
499
+ )
500
+ child_excluded_later = await storage.mint_id()
501
+ await storage.commit(
502
+ [{"type": "entry", "value": _entry(child_excluded_later, child_id)}], context
503
+ )
504
+
505
+ first = await storage.scan_entries({"conversationId": grandchild_id}, 2, None, context)
506
+ expect([item["id"] for item in first["items"]]).to_equal(
507
+ [grandchild_tail, grandchild_head]
508
+ )
509
+ second = await storage.scan_entries(
510
+ {"conversationId": grandchild_id}, 2, first.get("next"), context
511
+ )
512
+ expect([item["id"] for item in second["items"]]).to_equal(
513
+ [child_fork_point, root_fork_point]
514
+ )
515
+ third = await storage.scan_entries(
516
+ {"conversationId": grandchild_id}, 2, second.get("next"), context
517
+ )
518
+ expect([item["id"] for item in third["items"]]).to_equal([root_first])
519
+ expect(third.get("next")).to_be_undefined()
520
+
521
+ # Oldest first: the root's segment, then each fork's, each capped at the next fork point.
522
+ ascending: dict[str, Any] = {"conversationId": grandchild_id, "order": "ascending"}
523
+ first_up = await storage.scan_entries(ascending, 2, None, context)
524
+ expect([item["id"] for item in first_up["items"]]).to_equal([root_first, root_fork_point])
525
+ second_up = await storage.scan_entries(ascending, 2, first_up.get("next"), context)
526
+ expect([item["id"] for item in second_up["items"]]).to_equal(
527
+ [child_fork_point, grandchild_head]
528
+ )
529
+ third_up = await storage.scan_entries(
530
+ {"conversationId": grandchild_id}, 2, second_up.get("next"), context
531
+ )
532
+ expect([item["id"] for item in third_up["items"]]).to_equal([grandchild_tail])
533
+ expect(third_up.get("next")).to_be_undefined()
534
+ expect(
535
+ [
536
+ item["id"]
537
+ for item in (
538
+ await storage.scan_entries(
539
+ {
540
+ **ascending,
541
+ "minEntryId": root_fork_point,
542
+ "maxEntryId": child_fork_point,
543
+ },
544
+ 10,
545
+ None,
546
+ context,
547
+ )
548
+ )["items"]
549
+ ]
550
+ ).to_equal([root_fork_point, child_fork_point])
551
+ await expect(
552
+ storage.scan_entries(
553
+ {"conversationId": grandchild_id, "order": "descending"},
554
+ 2,
555
+ first_up.get("next"),
556
+ context,
557
+ )
558
+ ).rejects.to_throw("cursor")
559
+
560
+ current_marker = await storage.find_latest_head_marker(grandchild_id, None, context)
561
+ assert current_marker is not None
562
+ expect(current_marker["id"]).to_be(grandchild_head)
563
+ expect(current_marker["head"]).to_be(grandchild_head)
564
+ historical_marker = await storage.find_latest_head_marker(
565
+ grandchild_id, child_fork_point, context
566
+ )
567
+ assert historical_marker is not None
568
+ expect(historical_marker["id"]).to_be(root_fork_point)
569
+ expect(historical_marker["head"]).to_be(root_first)
570
+ expect(
571
+ await storage.find_latest_head_marker(grandchild_id, root_first, context)
572
+ ).to_be_undefined()
573
+
574
+ active_first = await storage.scan_entries(
575
+ {"conversationId": grandchild_id, "minEntryId": current_marker["head"]},
576
+ 1,
577
+ None,
578
+ context,
579
+ )
580
+ expect([item["id"] for item in active_first["items"]]).to_equal([grandchild_tail])
581
+ expect(active_first.get("next")).to_be_defined()
582
+ active_second = await storage.scan_entries(
583
+ {"conversationId": grandchild_id, "minEntryId": current_marker["head"]},
584
+ 1,
585
+ active_first.get("next"),
586
+ context,
587
+ )
588
+ expect([item["id"] for item in active_second["items"]]).to_equal([grandchild_head])
589
+ expect(active_second.get("next")).to_be_undefined()
590
+
591
+ expect(
592
+ [
593
+ item["id"]
594
+ for item in (
595
+ await storage.scan_entries(
596
+ {
597
+ "conversationId": grandchild_id,
598
+ "minEntryId": historical_marker["head"],
599
+ "maxEntryId": child_fork_point,
600
+ },
601
+ 10,
602
+ None,
603
+ context,
604
+ )
605
+ )["items"]
606
+ ]
607
+ ).to_equal([child_fork_point, root_fork_point, root_first])
608
+
609
+ expect(await storage.entry(root_first, context)).to_equal(
610
+ {"entry": _entry(root_first, root_id), "commitSeq": root_entries_seq}
611
+ )
612
+ root_fork_lookup = await storage.entry(root_fork_point, context)
613
+ assert root_fork_lookup is not None
614
+ expect(root_fork_lookup["commitSeq"]).to_be(root_entries_seq)
615
+ grandchild_head_lookup = await storage.entry(grandchild_head, context)
616
+ assert grandchild_head_lookup is not None
617
+ expect(grandchild_head_lookup["commitSeq"]).to_be(grandchild_entries_seq)
618
+ grandchild_tail_lookup = await storage.entry(grandchild_tail, context)
619
+ assert grandchild_tail_lookup is not None
620
+ expect(grandchild_tail_lookup["commitSeq"]).to_be(grandchild_entries_seq)
621
+ expect(
622
+ await storage.entry(id_from_number(999_999), context)
623
+ ).to_be_undefined()
624
+
625
+ expect(await storage.entry(grandchild_id, root_first, context)).to_equal(
626
+ {"entry": _entry(root_first, root_id), "commitSeq": root_entries_seq}
627
+ )
628
+ child_fork_lookup = await storage.entry(grandchild_id, child_fork_point, context)
629
+ assert child_fork_lookup is not None
630
+ expect(child_fork_lookup["entry"]["conversationId"]).to_be(child_id)
631
+ tail_lookup = await storage.entry(grandchild_id, grandchild_tail, context)
632
+ assert tail_lookup is not None
633
+ expect(tail_lookup["commitSeq"]).to_be(grandchild_entries_seq)
634
+ expect(
635
+ await storage.entry(grandchild_id, root_excluded_same_commit, context)
636
+ ).to_be_undefined()
637
+ expect(
638
+ await storage.entry(grandchild_id, root_excluded_later, context)
639
+ ).to_be_undefined()
640
+ expect(await storage.entry(grandchild_id, child_excluded, context)).to_be_undefined()
641
+ expect(
642
+ await storage.entry(grandchild_id, child_excluded_later, context)
643
+ ).to_be_undefined()
644
+ expect(await storage.entry(root_id, grandchild_head, context)).to_be_undefined()
645
+ expect(await storage.entry(grandchild_id, id_from_number(999_999), context)).to_be_undefined()
646
+ await expect(
647
+ storage.entry(id_from_number(999_999), root_first, context)
648
+ ).rejects.to_throw("Unknown conversation")
649
+ await expect(
650
+ storage.scan_entries(
651
+ {"conversationId": id_from_number(999_999)}, 10, None, context
652
+ )
653
+ ).rejects.to_throw("Unknown conversation")
654
+
655
+ async def scans_tables_in_either_order(storage: Storage) -> None:
656
+ root_id = await _create_root(storage)
657
+ conversation_ids: list[int] = [root_id]
658
+ task_ids: list[int] = []
659
+ submission_ids: list[int] = []
660
+ for _ in range(3):
661
+ conversation_id = await storage.mint_id()
662
+ task_id = await storage.mint_id()
663
+ submission_id = await storage.mint_id()
664
+ await storage.commit(
665
+ [
666
+ {"type": "conversation", "value": {"id": conversation_id}},
667
+ {"type": "task", "value": _pending_task(task_id, root_id)},
668
+ {
669
+ "type": "submission",
670
+ "value": {
671
+ "id": submission_id,
672
+ "conversationId": root_id,
673
+ "type": "input",
674
+ "status": "queued",
675
+ },
676
+ },
677
+ ],
678
+ context,
679
+ )
680
+ conversation_ids.append(conversation_id)
681
+ task_ids.append(task_id)
682
+ submission_ids.append(submission_id)
683
+
684
+ Scan: TypeAlias = Callable[[ScanOrder | None, Cursor | None], Awaitable[Page[Any, Cursor]]]
685
+ scans: list[tuple[Scan, list[int]]] = [
686
+ (
687
+ lambda order, cursor: storage.scan_conversations(
688
+ {"order": order} if order is not None else {}, 2, cursor, context
689
+ ),
690
+ conversation_ids,
691
+ ),
692
+ (
693
+ lambda order, cursor: storage.scan_tasks(
694
+ {"order": order} if order is not None else {}, 2, cursor, context
695
+ ),
696
+ task_ids,
697
+ ),
698
+ (
699
+ lambda order, cursor: storage.scan_submissions(
700
+ {"order": order} if order is not None else {}, 2, cursor, context
701
+ ),
702
+ submission_ids,
703
+ ),
704
+ ]
705
+
706
+ async def all_ids(scan: Scan, order: ScanOrder | None) -> list[int]:
707
+ found: list[int] = []
708
+ page = await scan(order, None)
709
+ found.extend(item["id"] for item in page["items"])
710
+ while page.get("next") is not None:
711
+ # A cursor carries its order; the query may omit it.
712
+ page = await scan(None, json.loads(json.dumps(page["next"])))
713
+ found.extend(item["id"] for item in page["items"])
714
+ return found
715
+
716
+ for scan, ids in scans:
717
+ reversed_ids = list(reversed(ids))
718
+ expect(await all_ids(scan, None)).to_equal(ids)
719
+ expect(await all_ids(scan, "ascending")).to_equal(ids)
720
+ expect(await all_ids(scan, "descending")).to_equal(reversed_ids)
721
+ descending = await scan("descending", None)
722
+ expect(
723
+ [item["id"] for item in (await scan("descending", descending.get("next")))["items"]]
724
+ ).to_equal(reversed_ids[2:4])
725
+ await expect(scan("ascending", descending.get("next"))).rejects.to_throw("cursor")
726
+
727
+ async def replaces_task_records(storage: Storage) -> None:
728
+ root_id = await _create_root(storage)
729
+ first_id = await storage.mint_id()
730
+ second_id = await storage.mint_id()
731
+ third_id = await storage.mint_id()
732
+ first = {**_pending_task(first_id, root_id), "memos": {"winner": "first"}}
733
+ second = {**_pending_task(second_id, root_id), "background": True}
734
+ third = {**_pending_task(third_id, root_id), "abortRequested": True}
735
+ await storage.commit(
736
+ [
737
+ {"type": "task", "value": first},
738
+ {"type": "task", "value": second},
739
+ {"type": "task", "value": third},
740
+ ],
741
+ context,
742
+ )
743
+
744
+ running = {
745
+ **first,
746
+ "state": {"status": "running", "checkpoint": {"phase": "effect", "attempt": 1}},
747
+ "abortRequested": True,
748
+ }
749
+ await storage.commit([{"type": "task", "value": running}], context)
750
+ expect(await storage.task(first_id, context)).to_equal(running)
751
+ terminal = {
752
+ "id": first_id,
753
+ "conversationId": root_id,
754
+ "kind": first["kind"],
755
+ "version": first["version"],
756
+ "input": first["input"],
757
+ "state": {"status": "terminal", "outcome": {"status": "completed", "result": {"entryId": 99}}},
758
+ "background": False,
759
+ "abortRequested": True,
760
+ }
761
+ await storage.commit([{"type": "task", "value": terminal}], context)
762
+ expect(await storage.task(first_id, context)).to_equal(terminal)
763
+
764
+ pending_page = await storage.scan_tasks({"status": "pending"}, 1, None, context)
765
+ expect([item["id"] for item in pending_page["items"]]).to_equal([second_id])
766
+ expect(pending_page.get("next")).to_be_defined()
767
+ second_page = await storage.scan_tasks(
768
+ {"status": "pending"}, 1, pending_page.get("next"), context
769
+ )
770
+ expect([item["id"] for item in second_page["items"]]).to_equal([third_id])
771
+ expect(
772
+ [
773
+ item["id"]
774
+ for item in (
775
+ await storage.scan_tasks(
776
+ {"status": "terminal", "abortRequested": True}, 10, None, context
777
+ )
778
+ )["items"]
779
+ ]
780
+ ).to_equal([first_id])
781
+ expect(
782
+ [
783
+ item["id"]
784
+ for item in (
785
+ await storage.scan_tasks({"background": True}, 10, None, context)
786
+ )["items"]
787
+ ]
788
+ ).to_equal([second_id])
789
+
790
+ async def stores_owners_and_scans_statuses(storage: Storage) -> None:
791
+ root_id = await _create_root(storage)
792
+ owner_id = await storage.mint_id()
793
+ waiting_id = await storage.mint_id()
794
+ completing_id = await storage.mint_id()
795
+ owner = _pending_task(owner_id, root_id)
796
+ waiting = {
797
+ **_pending_task(waiting_id, root_id),
798
+ "owner": owner_id,
799
+ "state": {
800
+ "status": "waiting",
801
+ "checkpoint": {"phase": "next"},
802
+ "on": [owner_id],
803
+ "policy": "allSettled",
804
+ },
805
+ "memos": {"kept": True},
806
+ }
807
+ base = {key: value for key, value in _pending_task(completing_id, root_id).items() if key != "state"}
808
+ completing = {
809
+ **base,
810
+ "owner": owner_id,
811
+ "state": {"status": "completing", "outcome": {"status": "failed", "error": {"message": "held"}}},
812
+ }
813
+ await storage.commit(
814
+ [
815
+ {"type": "task", "value": owner},
816
+ {"type": "task", "value": waiting},
817
+ {"type": "task", "value": completing},
818
+ ],
819
+ context,
820
+ )
821
+ expect(await storage.task(waiting_id, context)).to_equal(waiting)
822
+ expect(await storage.task(completing_id, context)).to_equal(completing)
823
+
824
+ async def scan(status: str) -> list[Any]:
825
+ page = await storage.scan_tasks({"status": status}, 10, None, context)
826
+ return list(page["items"])
827
+
828
+ expect(await scan("waiting")).to_equal([waiting])
829
+ expect(await scan("completing")).to_equal([completing])
830
+ expect([item["id"] for item in await scan("pending")]).to_equal([owner_id])
831
+ terminal = {
832
+ **completing,
833
+ "state": {"status": "terminal", "outcome": completing["state"]["outcome"]},
834
+ }
835
+ await storage.commit([{"type": "task", "value": terminal}], context)
836
+ expect(await scan("completing")).to_equal([])
837
+ expect(await scan("terminal")).to_equal([terminal])
838
+
839
+ async def indexes_request_ids(storage: Storage) -> None:
840
+ root_id = await _create_root(storage)
841
+ second_conversation_id = await storage.mint_id()
842
+ await storage.commit(
843
+ [{"type": "conversation", "value": {"id": second_conversation_id}}], context
844
+ )
845
+ first_id = await storage.mint_id()
846
+ second_id = await storage.mint_id()
847
+ other_conversation_id = await storage.mint_id()
848
+ first = {
849
+ "id": first_id,
850
+ "conversationId": root_id,
851
+ "requestId": "same",
852
+ "type": "input",
853
+ "status": "queued",
854
+ }
855
+ second = {
856
+ "id": second_id,
857
+ "conversationId": root_id,
858
+ "requestId": "other",
859
+ "type": "input",
860
+ "status": "queued",
861
+ }
862
+ other_conversation = {
863
+ "id": other_conversation_id,
864
+ "conversationId": second_conversation_id,
865
+ "requestId": "same",
866
+ "type": "input",
867
+ "status": "queued",
868
+ }
869
+ await storage.commit(
870
+ [
871
+ {"type": "submission", "value": first},
872
+ {"type": "submission", "value": second},
873
+ {"type": "submission", "value": other_conversation},
874
+ ],
875
+ context,
876
+ )
877
+ expect(await storage.submission_by_request(root_id, "same", context)).to_equal(first)
878
+ expect(
879
+ await storage.submission_by_request(second_conversation_id, "same", context)
880
+ ).to_equal(other_conversation)
881
+
882
+ placed_second = {**second, "status": "placed", "entry": await storage.mint_id()}
883
+ await storage.commit([{"type": "submission", "value": placed_second}], context)
884
+ expect(await storage.submission(second_id, context)).to_equal(placed_second)
885
+ expect(await storage.submission_by_request(root_id, "other", context)).to_equal(
886
+ placed_second
887
+ )
888
+
889
+ async def ids(query: dict[str, Any]) -> list[int]:
890
+ found: list[int] = []
891
+ cursor: Cursor | None = None
892
+ while True:
893
+ page = await storage.scan_submissions(query, 1, cursor, context)
894
+ found.extend(item["id"] for item in page["items"])
895
+ cursor = page.get("next")
896
+ if cursor is None:
897
+ return found
898
+
899
+ expect(await ids({})).to_equal([first_id, second_id, other_conversation_id])
900
+ expect(await ids({"conversationId": root_id})).to_equal([first_id, second_id])
901
+ # A status change moves the record between status scans.
902
+ expect(await ids({"status": "queued"})).to_equal([first_id, other_conversation_id])
903
+ expect(await ids({"status": "placed"})).to_equal([second_id])
904
+ expect(
905
+ await ids({"conversationId": second_conversation_id, "status": "queued"})
906
+ ).to_equal([other_conversation_id])
907
+ expect(
908
+ await ids({"conversationId": second_conversation_id, "status": "placed"})
909
+ ).to_equal([])
910
+ expect(
911
+ (await storage.scan_submissions({"status": "placed"}, 10, None, context))["items"]
912
+ ).to_equal([placed_second])
913
+
914
+ async def stores_passive_write_submissions(storage: Storage) -> None:
915
+ root_id = await _create_root(storage)
916
+ done_id = await storage.mint_id()
917
+ failed_id = await storage.mint_id()
918
+ queued_done = {
919
+ "id": done_id,
920
+ "conversationId": root_id,
921
+ "requestId": "passive-done",
922
+ "type": "write",
923
+ "status": "queued",
924
+ }
925
+ queued_failed = {
926
+ "id": failed_id,
927
+ "conversationId": root_id,
928
+ "requestId": "passive-failed",
929
+ "type": "write",
930
+ "status": "queued",
931
+ }
932
+ await storage.commit(
933
+ [
934
+ {"type": "submission", "value": queued_done},
935
+ {"type": "submission", "value": queued_failed},
936
+ ],
937
+ context,
938
+ )
939
+
940
+ done = {**queued_done, "status": "done", "entry": await storage.mint_id()}
941
+ unanswered = {
942
+ **queued_failed,
943
+ "status": "unanswered",
944
+ "reason": "closed",
945
+ "detail": {"retryable": False},
946
+ }
947
+ await storage.commit(
948
+ [
949
+ {"type": "submission", "value": done},
950
+ {"type": "submission", "value": unanswered},
951
+ ],
952
+ context,
953
+ )
954
+ expect(await storage.submission(done_id, context)).to_equal(done)
955
+ expect(await storage.submission_by_request(root_id, "passive-done", context)).to_equal(
956
+ done
957
+ )
958
+ expect(await storage.submission(failed_id, context)).to_equal(unanswered)
959
+ expect(
960
+ await storage.submission_by_request(root_id, "passive-failed", context)
961
+ ).to_equal(unanswered)
962
+
963
+ async def reconstructs_rewindable_documents(storage: Storage) -> None:
964
+ root_id = await _create_root(storage)
965
+ first_id = await storage.mint_id()
966
+ first_record = {
967
+ "id": first_id,
968
+ "kind": "conversation.notes",
969
+ "scope": {"kind": "conversation", "conversationId": root_id},
970
+ "history": "rewindable",
971
+ "fork": "asOf",
972
+ }
973
+ initial: dict[str, Any] = {"items": ["a"], "nested": {"count": 1}}
974
+ created_at = await storage.commit(
975
+ [
976
+ {
977
+ "type": "document.create",
978
+ "record": first_record,
979
+ "content": {"kind": "base", "version": 1, "value": initial},
980
+ }
981
+ ],
982
+ context,
983
+ )
984
+ appended = ["b"]
985
+ ops = [
986
+ ["p", ["items"], 1, 0, appended],
987
+ ["s", ["nested", "count"], 2],
988
+ ]
989
+ changed_at = await storage.commit(
990
+ [
991
+ {
992
+ "type": "document.change",
993
+ "id": first_id,
994
+ "content": {"kind": "delta", "version": 1, "ops": ops},
995
+ }
996
+ ],
997
+ context,
998
+ )
999
+
1000
+ initial["items"].append("caller mutation")
1001
+ appended.append("caller mutation")
1002
+ expect(await storage.document(first_id, created_at, context)).to_match_object(
1003
+ {"version": 1, "value": {"items": ["a"], "nested": {"count": 1}}, "deltasSinceBase": 0}
1004
+ )
1005
+ changed = await storage.document(first_id, changed_at, context)
1006
+ assert changed is not None
1007
+ expect(changed["value"]).to_equal({"items": ["a", "b"], "nested": {"count": 2}})
1008
+ expect(changed["deltasSinceBase"]).to_be(1)
1009
+ changed["value"]["items"].append("read mutation")
1010
+ current = await storage.document(first_id, "current", context)
1011
+ assert current is not None
1012
+ expect(current["value"]).to_equal({"items": ["a", "b"], "nested": {"count": 2}})
1013
+
1014
+ checkpoint_at = await storage.commit(
1015
+ [
1016
+ {
1017
+ "type": "document.change",
1018
+ "id": first_id,
1019
+ "content": {
1020
+ "kind": "base",
1021
+ "version": 2,
1022
+ "value": {"items": ["checkpoint"], "nested": {"count": 3}},
1023
+ },
1024
+ }
1025
+ ],
1026
+ context,
1027
+ )
1028
+ replaced_at = await storage.commit(
1029
+ [
1030
+ {
1031
+ "type": "document.change",
1032
+ "id": first_id,
1033
+ "content": {
1034
+ "kind": "delta",
1035
+ "version": 2,
1036
+ "ops": [["r", {"items": ["replacement"], "nested": {"count": 4}}]],
1037
+ },
1038
+ }
1039
+ ],
1040
+ context,
1041
+ )
1042
+ expect(await storage.document(first_id, changed_at, context)).to_match_object(
1043
+ {"version": 1, "value": {"items": ["a", "b"], "nested": {"count": 2}}}
1044
+ )
1045
+ expect(await storage.document(first_id, checkpoint_at, context)).to_match_object(
1046
+ {"version": 2, "value": {"items": ["checkpoint"], "nested": {"count": 3}}, "deltasSinceBase": 0}
1047
+ )
1048
+ expect(await storage.document(first_id, replaced_at, context)).to_match_object(
1049
+ {"value": {"items": ["replacement"], "nested": {"count": 4}}, "deltasSinceBase": 1}
1050
+ )
1051
+ current_after = await storage.document(first_id, "current", context)
1052
+ assert current_after is not None
1053
+ expect(current_after["deltasSinceBase"]).to_be(1)
1054
+
1055
+ second_id = await storage.mint_id()
1056
+ retired_at = await storage.commit(
1057
+ [
1058
+ {
1059
+ "type": "document.create",
1060
+ "record": {**first_record, "id": second_id},
1061
+ "content": {"kind": "base", "version": 1, "value": {"items": ["new"]}},
1062
+ },
1063
+ {"type": "document.retire", "id": first_id},
1064
+ {
1065
+ "type": "document.change",
1066
+ "id": first_id,
1067
+ "content": {"kind": "delta", "version": 2, "ops": [["s", ["retiring"], True]]},
1068
+ },
1069
+ ],
1070
+ context,
1071
+ )
1072
+ address = {"kind": first_record["kind"], "scope": first_record["scope"]}
1073
+ historical = await storage.find_document(address, changed_at, context)
1074
+ assert historical is not None
1075
+ expect(historical["id"]).to_be(first_id)
1076
+ expect(await storage.find_document(address, retired_at, context)).to_match_object(
1077
+ {"id": second_id, "createdAt": retired_at}
1078
+ )
1079
+ expect(
1080
+ [
1081
+ item["id"]
1082
+ for item in (
1083
+ await storage.scan_documents(
1084
+ {"scope": first_record["scope"], "at": changed_at}, 10, None, context
1085
+ )
1086
+ )["items"]
1087
+ ]
1088
+ ).to_equal([first_id])
1089
+ expect(
1090
+ [
1091
+ item["id"]
1092
+ for item in (
1093
+ await storage.scan_documents(
1094
+ {"scope": first_record["scope"], "at": retired_at}, 10, None, context
1095
+ )
1096
+ )["items"]
1097
+ ]
1098
+ ).to_equal([second_id])
1099
+ expect(await storage.document(first_id, retired_at, context)).to_be_undefined()
1100
+ second_current = await storage.document(second_id, "current", context)
1101
+ assert second_current is not None
1102
+ expect(second_current["value"]).to_equal({"items": ["new"]})
1103
+
1104
+ async def streams_long_document_tails(storage: Storage) -> None:
1105
+ root_id = await _create_root(storage)
1106
+ id = await storage.mint_id()
1107
+ record = {
1108
+ "id": id,
1109
+ "kind": "conversation.long-tail",
1110
+ "scope": {"kind": "conversation", "conversationId": root_id},
1111
+ "history": "rewindable",
1112
+ "fork": "asOf",
1113
+ }
1114
+ initial = {
1115
+ "revision": 0,
1116
+ "rows": [{"value": value, "stable": f"row-{value}"} for value in range(512)],
1117
+ }
1118
+ created_at = await storage.commit(
1119
+ [
1120
+ {
1121
+ "type": "document.create",
1122
+ "record": record,
1123
+ "content": {"kind": "base", "version": 1, "value": initial},
1124
+ }
1125
+ ],
1126
+ context,
1127
+ )
1128
+ before_replacement = json.loads(json.dumps(initial))
1129
+ before_replacement_at = created_at
1130
+ for revision in range(1, 25):
1131
+ index = (revision * 17) % len(before_replacement["rows"])
1132
+ before_replacement["rows"][index]["value"] = -revision
1133
+ before_replacement["revision"] = revision
1134
+ before_replacement_at = await storage.commit(
1135
+ [
1136
+ {
1137
+ "type": "document.change",
1138
+ "id": id,
1139
+ "content": {
1140
+ "kind": "delta",
1141
+ "version": 1,
1142
+ "ops": [
1143
+ ["s", ["rows", index, "value"], -revision],
1144
+ ["s", ["revision"], revision],
1145
+ ],
1146
+ },
1147
+ }
1148
+ ],
1149
+ context,
1150
+ )
1151
+
1152
+ replacement = {
1153
+ "revision": 100,
1154
+ "rows": [
1155
+ {"value": 10_000 + value, "stable": f"new-{value}"} for value in range(512)
1156
+ ],
1157
+ }
1158
+ replacement_snapshot = json.loads(json.dumps(replacement))
1159
+ replacement_at = await storage.commit(
1160
+ [
1161
+ {
1162
+ "type": "document.change",
1163
+ "id": id,
1164
+ "content": {"kind": "delta", "version": 1, "ops": [["r", replacement]]},
1165
+ }
1166
+ ],
1167
+ context,
1168
+ )
1169
+ replacement["rows"][0]["value"] = -999
1170
+
1171
+ current = json.loads(json.dumps(replacement_snapshot))
1172
+ for revision in range(101, 125):
1173
+ index = (revision * 19) % len(current["rows"])
1174
+ current["rows"][index]["value"] = -revision
1175
+ current["revision"] = revision
1176
+ await storage.commit(
1177
+ [
1178
+ {
1179
+ "type": "document.change",
1180
+ "id": id,
1181
+ "content": {
1182
+ "kind": "delta",
1183
+ "version": 1,
1184
+ "ops": [
1185
+ ["s", ["rows", index, "value"], -revision],
1186
+ ["s", ["revision"], revision],
1187
+ ],
1188
+ },
1189
+ }
1190
+ ],
1191
+ context,
1192
+ )
1193
+
1194
+ created = await storage.document(id, created_at, context)
1195
+ assert created is not None
1196
+ expect(created["value"]).to_equal(initial)
1197
+ before = await storage.document(id, before_replacement_at, context)
1198
+ assert before is not None
1199
+ expect(before["value"]).to_equal(before_replacement)
1200
+ replaced = await storage.document(id, replacement_at, context)
1201
+ assert replaced is not None
1202
+ expect(replaced["value"]).to_equal(replacement_snapshot)
1203
+ read = await storage.document(id, "current", context)
1204
+ assert read is not None
1205
+ expect(read["value"]).to_equal(current)
1206
+ read["value"]["rows"][0]["value"] = -1_000
1207
+ current_again = await storage.document(id, "current", context)
1208
+ assert current_again is not None
1209
+ expect(current_again["value"]).to_equal(current)
1210
+
1211
+ async def copies_document_bases(storage: Storage) -> None:
1212
+ root_id = await _create_root(storage)
1213
+ child_id = await storage.mint_id()
1214
+ second_child_id = await storage.mint_id()
1215
+ await storage.commit(
1216
+ [
1217
+ {"type": "conversation", "value": {"id": child_id}},
1218
+ {"type": "conversation", "value": {"id": second_child_id}},
1219
+ ],
1220
+ context,
1221
+ )
1222
+ source_id = await storage.mint_id()
1223
+ source_record = {
1224
+ "id": source_id,
1225
+ "kind": "copy.source",
1226
+ "scope": {"kind": "conversation", "conversationId": root_id},
1227
+ "history": "rewindable",
1228
+ "fork": "asOf",
1229
+ }
1230
+ created_at = await storage.commit(
1231
+ [
1232
+ {
1233
+ "type": "document.create",
1234
+ "record": source_record,
1235
+ "content": {
1236
+ "kind": "base",
1237
+ "version": 2,
1238
+ "value": {"count": 1, "rows": [{"value": "base"}]},
1239
+ },
1240
+ }
1241
+ ],
1242
+ context,
1243
+ )
1244
+ await storage.commit(
1245
+ [
1246
+ {
1247
+ "type": "document.change",
1248
+ "id": source_id,
1249
+ "content": {
1250
+ "kind": "delta",
1251
+ "version": 2,
1252
+ "ops": [
1253
+ ["s", ["count"], 2],
1254
+ ["p", ["rows"], 1, 0, [{"value": "current"}]],
1255
+ ],
1256
+ },
1257
+ }
1258
+ ],
1259
+ context,
1260
+ )
1261
+ historical_copy_id = await storage.mint_id()
1262
+ current_copy_id = await storage.mint_id()
1263
+ retired_copy_id = await storage.mint_id()
1264
+
1265
+ def child_record(id: int, conversation_id: int) -> dict[str, Any]:
1266
+ return {
1267
+ "id": id,
1268
+ "kind": source_record["kind"],
1269
+ "scope": {"kind": "conversation", "conversationId": conversation_id},
1270
+ "history": "rewindable",
1271
+ "fork": "asOf",
1272
+ }
1273
+
1274
+ await storage.commit(
1275
+ [
1276
+ {
1277
+ "type": "document.copy",
1278
+ "record": child_record(historical_copy_id, child_id),
1279
+ "source": {"id": source_id, "at": created_at},
1280
+ },
1281
+ {
1282
+ "type": "document.copy",
1283
+ "record": child_record(current_copy_id, second_child_id),
1284
+ "source": {"id": source_id, "at": "current"},
1285
+ },
1286
+ {
1287
+ "type": "document.copy",
1288
+ "record": child_record(retired_copy_id, root_id),
1289
+ "source": {"id": source_id, "at": "current"},
1290
+ },
1291
+ {"type": "document.retire", "id": retired_copy_id},
1292
+ ],
1293
+ context,
1294
+ )
1295
+ expect(await storage.document(historical_copy_id, "current", context)).to_match_object(
1296
+ {"version": 2, "value": {"count": 1, "rows": [{"value": "base"}]}}
1297
+ )
1298
+ expect(await storage.document(current_copy_id, "current", context)).to_match_object(
1299
+ {"version": 2, "value": {"count": 2, "rows": [{"value": "base"}, {"value": "current"}]}}
1300
+ )
1301
+ expect(await storage.document(retired_copy_id, "current", context)).to_be_undefined()
1302
+
1303
+ await storage.commit(
1304
+ [
1305
+ {
1306
+ "type": "document.change",
1307
+ "id": source_id,
1308
+ "content": {"kind": "base", "version": 2, "value": {"count": 99, "rows": []}},
1309
+ },
1310
+ {"type": "document.retire", "id": source_id},
1311
+ ],
1312
+ context,
1313
+ )
1314
+ surviving_copy = await storage.document(current_copy_id, "current", context)
1315
+ assert surviving_copy is not None
1316
+ expect(surviving_copy["value"]).to_equal(
1317
+ {"count": 2, "rows": [{"value": "base"}, {"value": "current"}]}
1318
+ )
1319
+
1320
+ latest_source_id = await storage.mint_id()
1321
+ latest_copy_id = await storage.mint_id()
1322
+ latest_source = {
1323
+ "id": latest_source_id,
1324
+ "kind": "copy.latest",
1325
+ "scope": {"kind": "conversation", "conversationId": root_id},
1326
+ "history": "latest",
1327
+ "fork": "current",
1328
+ }
1329
+ await storage.commit(
1330
+ [
1331
+ {
1332
+ "type": "document.create",
1333
+ "record": latest_source,
1334
+ "content": {"kind": "base", "version": 4, "value": {"retained": "copy"}},
1335
+ }
1336
+ ],
1337
+ context,
1338
+ )
1339
+ await storage.commit(
1340
+ [
1341
+ {
1342
+ "type": "document.copy",
1343
+ "record": {
1344
+ **latest_source,
1345
+ "id": latest_copy_id,
1346
+ "scope": {"kind": "conversation", "conversationId": child_id},
1347
+ },
1348
+ "source": {"id": latest_source_id, "at": "current"},
1349
+ }
1350
+ ],
1351
+ context,
1352
+ )
1353
+ await storage.commit(
1354
+ [
1355
+ {
1356
+ "type": "document.change",
1357
+ "id": latest_source_id,
1358
+ "content": {"kind": "base", "version": 4, "value": {"retained": "source-only"}},
1359
+ },
1360
+ {"type": "document.retire", "id": latest_source_id},
1361
+ ],
1362
+ context,
1363
+ )
1364
+ expect(await storage.document(latest_copy_id, "current", context)).to_match_object(
1365
+ {"version": 4, "value": {"retained": "copy"}}
1366
+ )
1367
+
1368
+ conflict_id = await storage.mint_id()
1369
+ conflict_error: object | None = None
1370
+ try:
1371
+ await storage.commit(
1372
+ [
1373
+ {
1374
+ "type": "document.copy",
1375
+ "record": child_record(conflict_id, child_id),
1376
+ "source": {"id": current_copy_id, "at": "current"},
1377
+ },
1378
+ {"type": "document.retire", "id": current_copy_id},
1379
+ ],
1380
+ context,
1381
+ )
1382
+ except Exception as error:
1383
+ conflict_error = error
1384
+ # Rejected without effect: the copy and the retirement in the same batch are both absent.
1385
+ expect(isinstance(conflict_error, Exception)).to_be(True)
1386
+ expect(await storage.document(conflict_id, "current", context)).to_be_undefined()
1387
+ current_copy = await storage.document(current_copy_id, "current", context)
1388
+ assert current_copy is not None
1389
+ expect(current_copy["value"]).to_equal(
1390
+ {"count": 2, "rows": [{"value": "base"}, {"value": "current"}]}
1391
+ )
1392
+
1393
+ mismatch_id = await storage.mint_id()
1394
+ mismatch_error: object | None = None
1395
+ try:
1396
+ await storage.commit(
1397
+ [
1398
+ {
1399
+ "type": "document.copy",
1400
+ "record": {**child_record(mismatch_id, child_id), "kind": "copy.mismatch"},
1401
+ "source": {"id": current_copy_id, "at": "current"},
1402
+ }
1403
+ ],
1404
+ context,
1405
+ )
1406
+ except Exception as error:
1407
+ mismatch_error = error
1408
+ expect(isinstance(mismatch_error, Exception)).to_be(True)
1409
+ expect(await storage.document(mismatch_id, "current", context)).to_be_undefined()
1410
+
1411
+ async def uses_bases_for_version_transitions(storage: Storage) -> None:
1412
+ await _create_root(storage)
1413
+ id = await storage.mint_id()
1414
+ record = {"id": id, "kind": "session.settings", "scope": {"kind": "session"}}
1415
+ await storage.commit(
1416
+ [
1417
+ {
1418
+ "type": "document.create",
1419
+ "record": record,
1420
+ "content": {"kind": "base", "version": 1, "value": {"count": 1}},
1421
+ }
1422
+ ],
1423
+ context,
1424
+ )
1425
+ await storage.commit(
1426
+ [
1427
+ {
1428
+ "type": "document.change",
1429
+ "id": id,
1430
+ "content": {"kind": "delta", "version": 1, "ops": [["s", ["count"], 2]]},
1431
+ }
1432
+ ],
1433
+ context,
1434
+ )
1435
+ migrated_at = await storage.commit(
1436
+ [
1437
+ {
1438
+ "type": "document.change",
1439
+ "id": id,
1440
+ "content": {"kind": "base", "version": 2, "value": {"count": 3}},
1441
+ }
1442
+ ],
1443
+ context,
1444
+ )
1445
+ expect(await storage.document(id, "current", context)).to_match_object(
1446
+ {"version": 2, "value": {"count": 3}}
1447
+ )
1448
+ await expect(storage.document(id, migrated_at, context)).rejects.to_throw(
1449
+ "does not retain historical content"
1450
+ )
1451
+
1452
+ await expect(
1453
+ storage.commit(
1454
+ [
1455
+ {
1456
+ "type": "document.change",
1457
+ "id": id,
1458
+ "content": {"kind": "delta", "version": 1, "ops": [["s", ["count"], 4]]},
1459
+ }
1460
+ ],
1461
+ context,
1462
+ )
1463
+ ).rejects.to_throw("version transition requires a base")
1464
+ unchanged = await storage.document(id, "current", context)
1465
+ assert unchanged is not None
1466
+ expect(unchanged["value"]).to_equal({"count": 3})
1467
+ await storage.commit([{"type": "document.retire", "id": id}], context)
1468
+ expect(await storage.document(id, "current", context)).to_be_undefined()
1469
+
1470
+ async def indexes_logical_addresses(storage: Storage) -> None:
1471
+ root_id = await _create_root(storage)
1472
+ first_id = await storage.mint_id()
1473
+ second_id = await storage.mint_id()
1474
+ conversation_id = await storage.mint_id()
1475
+ task_id = await storage.mint_id()
1476
+ task_singleton_id = await storage.mint_id()
1477
+ task_family_id = await storage.mint_id()
1478
+ task_other_kind_id = await storage.mint_id()
1479
+ created_at = await storage.commit(
1480
+ [
1481
+ {"type": "task", "value": _pending_task(task_id, root_id)},
1482
+ {
1483
+ "type": "document.create",
1484
+ "record": {
1485
+ "id": first_id,
1486
+ "kind": "cache",
1487
+ "scope": {"kind": "session"},
1488
+ "key": "__proto__",
1489
+ },
1490
+ "content": {"kind": "base", "version": 1, "value": {"owner": "first"}},
1491
+ },
1492
+ {
1493
+ "type": "document.create",
1494
+ "record": {
1495
+ "id": second_id,
1496
+ "kind": "cache",
1497
+ "scope": {"kind": "session"},
1498
+ "key": "constructor",
1499
+ },
1500
+ "content": {"kind": "base", "version": 1, "value": {"owner": "second"}},
1501
+ },
1502
+ {
1503
+ "type": "document.create",
1504
+ "record": {
1505
+ "id": conversation_id,
1506
+ "kind": "cache",
1507
+ "scope": {"kind": "conversation", "conversationId": root_id},
1508
+ "history": "latest",
1509
+ "fork": "current",
1510
+ "key": "__proto__",
1511
+ },
1512
+ "content": {"kind": "base", "version": 1, "value": {"owner": "conversation"}},
1513
+ },
1514
+ {
1515
+ "type": "document.create",
1516
+ "record": {
1517
+ "id": task_singleton_id,
1518
+ "kind": "task.cache",
1519
+ "scope": {"kind": "task", "taskId": task_id},
1520
+ },
1521
+ "content": {"kind": "base", "version": 1, "value": {"owner": "singleton"}},
1522
+ },
1523
+ {
1524
+ "type": "document.create",
1525
+ "record": {
1526
+ "id": task_family_id,
1527
+ "kind": "task.cache",
1528
+ "scope": {"kind": "task", "taskId": task_id},
1529
+ "key": "member",
1530
+ },
1531
+ "content": {"kind": "base", "version": 1, "value": {"owner": "family"}},
1532
+ },
1533
+ {
1534
+ "type": "document.create",
1535
+ "record": {
1536
+ "id": task_other_kind_id,
1537
+ "kind": "task.other",
1538
+ "scope": {"kind": "task", "taskId": task_id},
1539
+ },
1540
+ "content": {"kind": "base", "version": 1, "value": {"owner": "other"}},
1541
+ },
1542
+ ],
1543
+ context,
1544
+ )
1545
+
1546
+ first_address = await storage.find_document(
1547
+ {"kind": "cache", "scope": {"kind": "session"}, "key": "__proto__"}, "current", context
1548
+ )
1549
+ assert first_address is not None
1550
+ expect(first_address["id"]).to_be(first_id)
1551
+ first_scan = await storage.scan_documents(
1552
+ {"scope": {"kind": "session"}, "at": "current"}, 1, None, context
1553
+ )
1554
+ expect(first_scan["items"]).to_have_length(1)
1555
+ second_scan = await storage.scan_documents(
1556
+ {"scope": {"kind": "session"}, "at": "current"}, 1, first_scan.get("next"), context
1557
+ )
1558
+ expect([item["id"] for item in [*first_scan["items"], *second_scan["items"]]]).to_equal(
1559
+ [first_id, second_id]
1560
+ )
1561
+ expect(
1562
+ [
1563
+ item["id"]
1564
+ for item in (
1565
+ await storage.scan_documents(
1566
+ {"scope": {"kind": "conversation", "conversationId": root_id}, "at": "current"},
1567
+ 10,
1568
+ None,
1569
+ context,
1570
+ )
1571
+ )["items"]
1572
+ ]
1573
+ ).to_equal([conversation_id])
1574
+ singleton = await storage.find_document(
1575
+ {"kind": "task.cache", "scope": {"kind": "task", "taskId": task_id}}, "current", context
1576
+ )
1577
+ assert singleton is not None
1578
+ expect(singleton["id"]).to_be(task_singleton_id)
1579
+ family = await storage.find_document(
1580
+ {"kind": "task.cache", "scope": {"kind": "task", "taskId": task_id}, "key": "member"},
1581
+ "current",
1582
+ context,
1583
+ )
1584
+ assert family is not None
1585
+ expect(family["id"]).to_be(task_family_id)
1586
+ expect(
1587
+ [
1588
+ item["id"]
1589
+ for item in (
1590
+ await storage.scan_documents(
1591
+ {"scope": {"kind": "task", "taskId": task_id}, "at": "current", "kind": "task.cache"},
1592
+ 10,
1593
+ None,
1594
+ context,
1595
+ )
1596
+ )["items"]
1597
+ ]
1598
+ ).to_equal([task_singleton_id, task_family_id])
1599
+ await expect(storage.document(task_singleton_id, created_at, context)).rejects.to_throw(
1600
+ "does not retain historical content"
1601
+ )
1602
+
1603
+ async def keeps_lifecycle_failures_atomic(storage: Storage) -> None:
1604
+ root_id = await _create_root(storage)
1605
+ first_id = await storage.mint_id()
1606
+ second_id = await storage.mint_id()
1607
+ record = {"id": first_id, "kind": "singleton", "scope": {"kind": "session"}}
1608
+ await storage.commit(
1609
+ [
1610
+ {
1611
+ "type": "document.create",
1612
+ "record": record,
1613
+ "content": {"kind": "base", "version": 1, "value": {"value": 1}},
1614
+ }
1615
+ ],
1616
+ context,
1617
+ )
1618
+ await expect(
1619
+ storage.commit(
1620
+ [
1621
+ {
1622
+ "type": "document.create",
1623
+ "record": {**record, "id": second_id},
1624
+ "content": {"kind": "base", "version": 1, "value": {"value": 2}},
1625
+ },
1626
+ {
1627
+ "type": "document.change",
1628
+ "id": first_id,
1629
+ "content": {"kind": "delta", "version": 1, "ops": []},
1630
+ },
1631
+ ],
1632
+ context,
1633
+ )
1634
+ ).rejects.to_throw("already has a current incarnation")
1635
+ first_current = await storage.document(first_id, "current", context)
1636
+ assert first_current is not None
1637
+ expect(first_current["value"]).to_equal({"value": 1})
1638
+ expect(await storage.document(second_id, "current", context)).to_be_undefined()
1639
+
1640
+ empty_id = await storage.mint_id()
1641
+ empty_at = await storage.commit(
1642
+ [
1643
+ {
1644
+ "type": "document.create",
1645
+ "record": {
1646
+ "id": empty_id,
1647
+ "kind": record["kind"],
1648
+ "key": "empty",
1649
+ "scope": {"kind": "conversation", "conversationId": root_id},
1650
+ "history": "rewindable",
1651
+ "fork": "initial",
1652
+ },
1653
+ "content": {"kind": "base", "version": 1, "value": {}},
1654
+ },
1655
+ {"type": "document.retire", "id": empty_id},
1656
+ ],
1657
+ context,
1658
+ )
1659
+ expect(await storage.document(empty_id, "current", context)).to_be_undefined()
1660
+ expect(await storage.document(empty_id, empty_at, context)).to_be_undefined()
1661
+ expect(
1662
+ await storage.find_document(
1663
+ {
1664
+ "kind": record["kind"],
1665
+ "scope": {"kind": "conversation", "conversationId": root_id},
1666
+ "key": "empty",
1667
+ },
1668
+ empty_at,
1669
+ context,
1670
+ )
1671
+ ).to_be_undefined()
1672
+
1673
+ async def rolls_back_document_failures(storage: Storage) -> None:
1674
+ root_id = await _create_root(storage)
1675
+ task_id = await storage.mint_id()
1676
+ submission_id = await storage.mint_id()
1677
+ document_id = await storage.mint_id()
1678
+ task = _pending_task(task_id, root_id)
1679
+ submission = {
1680
+ "id": submission_id,
1681
+ "conversationId": root_id,
1682
+ "requestId": "atomic",
1683
+ "type": "input",
1684
+ "status": "queued",
1685
+ }
1686
+ record = {"id": document_id, "kind": "atomic", "scope": {"kind": "session"}}
1687
+ baseline_seq = await storage.commit(
1688
+ [
1689
+ {"type": "task", "value": task},
1690
+ {"type": "submission", "value": submission},
1691
+ {
1692
+ "type": "document.create",
1693
+ "record": record,
1694
+ "content": {"kind": "base", "version": 1, "value": {"count": 1}},
1695
+ },
1696
+ ],
1697
+ context,
1698
+ )
1699
+
1700
+ entry_id = await storage.mint_id()
1701
+ conflicting_document_id = await storage.mint_id()
1702
+ await expect(
1703
+ storage.commit(
1704
+ [
1705
+ {
1706
+ "type": "task",
1707
+ "value": {**task, "state": {"status": "running", "checkpoint": {"phase": "effect"}}},
1708
+ },
1709
+ {
1710
+ "type": "submission",
1711
+ "value": {**submission, "status": "unanswered", "reason": "failed"},
1712
+ },
1713
+ {"type": "entry", "value": _entry(entry_id, root_id, "transient")},
1714
+ {
1715
+ "type": "document.create",
1716
+ "record": {**record, "id": conflicting_document_id},
1717
+ "content": {"kind": "base", "version": 1, "value": {"count": 2}},
1718
+ },
1719
+ ],
1720
+ context,
1721
+ )
1722
+ ).rejects.to_throw("already has a current incarnation")
1723
+
1724
+ expect(await storage.task(task_id, context)).to_equal(task)
1725
+ expect((await storage.scan_tasks({"status": "pending"}, 10, None, context))["items"]).to_equal(
1726
+ [task]
1727
+ )
1728
+ expect(await storage.submission_by_request(root_id, "atomic", context)).to_equal(
1729
+ submission
1730
+ )
1731
+ expect(await storage.entry(entry_id, context)).to_be_undefined()
1732
+ expect(
1733
+ await storage.document(conflicting_document_id, "current", context)
1734
+ ).to_be_undefined()
1735
+ address = await storage.find_document(
1736
+ {"kind": record["kind"], "scope": record["scope"]}, "current", context
1737
+ )
1738
+ assert address is not None
1739
+ expect(address["id"]).to_be(document_id)
1740
+ after_rollback_seq = await storage.commit(
1741
+ [
1742
+ {
1743
+ "type": "document.change",
1744
+ "id": document_id,
1745
+ "content": {"kind": "delta", "version": 1, "ops": [["s", ["count"], 3]]},
1746
+ }
1747
+ ],
1748
+ context,
1749
+ )
1750
+ expect(after_rollback_seq).to_be_greater_than(baseline_seq)
1751
+
1752
+ async def keeps_string_identities_lossless(storage: Storage) -> None:
1753
+ root_id = await _create_root(storage)
1754
+ first = "\ud800"
1755
+ second = "\ud801"
1756
+ first_task_id = await storage.mint_id()
1757
+ second_task_id = await storage.mint_id()
1758
+ first_submission_id = await storage.mint_id()
1759
+ second_submission_id = await storage.mint_id()
1760
+ first_kind_document_id = await storage.mint_id()
1761
+ second_kind_document_id = await storage.mint_id()
1762
+ first_key_document_id = await storage.mint_id()
1763
+ second_key_document_id = await storage.mint_id()
1764
+ await storage.commit(
1765
+ [
1766
+ {"type": "task", "value": {**_pending_task(first_task_id, root_id), "kind": first}},
1767
+ {"type": "task", "value": {**_pending_task(second_task_id, root_id), "kind": second}},
1768
+ {
1769
+ "type": "submission",
1770
+ "value": {
1771
+ "id": first_submission_id,
1772
+ "conversationId": root_id,
1773
+ "requestId": first,
1774
+ "type": "input",
1775
+ "status": "queued",
1776
+ },
1777
+ },
1778
+ {
1779
+ "type": "submission",
1780
+ "value": {
1781
+ "id": second_submission_id,
1782
+ "conversationId": root_id,
1783
+ "requestId": second,
1784
+ "type": "input",
1785
+ "status": "queued",
1786
+ },
1787
+ },
1788
+ {
1789
+ "type": "document.create",
1790
+ "record": {"id": first_kind_document_id, "kind": first, "scope": {"kind": "session"}},
1791
+ "content": {"kind": "base", "version": 1, "value": {"identity": "first kind"}},
1792
+ },
1793
+ {
1794
+ "type": "document.create",
1795
+ "record": {"id": second_kind_document_id, "kind": second, "scope": {"kind": "session"}},
1796
+ "content": {"kind": "base", "version": 1, "value": {"identity": "second kind"}},
1797
+ },
1798
+ {
1799
+ "type": "document.create",
1800
+ "record": {
1801
+ "id": first_key_document_id,
1802
+ "kind": "family",
1803
+ "key": first,
1804
+ "scope": {"kind": "session"},
1805
+ },
1806
+ "content": {"kind": "base", "version": 1, "value": {"identity": "first key"}},
1807
+ },
1808
+ {
1809
+ "type": "document.create",
1810
+ "record": {
1811
+ "id": second_key_document_id,
1812
+ "kind": "family",
1813
+ "key": second,
1814
+ "scope": {"kind": "session"},
1815
+ },
1816
+ "content": {"kind": "base", "version": 1, "value": {"identity": "second key"}},
1817
+ },
1818
+ ],
1819
+ context,
1820
+ )
1821
+
1822
+ expect(
1823
+ [item["id"] for item in (await storage.scan_tasks({"kind": first}, 10, None, context))["items"]]
1824
+ ).to_equal([first_task_id])
1825
+ expect(
1826
+ [item["id"] for item in (await storage.scan_tasks({"kind": second}, 10, None, context))["items"]]
1827
+ ).to_equal([second_task_id])
1828
+ first_task = await storage.task(first_task_id, context)
1829
+ assert first_task is not None
1830
+ expect(first_task["kind"]).to_be(first)
1831
+ second_task = await storage.task(second_task_id, context)
1832
+ assert second_task is not None
1833
+ expect(second_task["kind"]).to_be(second)
1834
+ first_request = await storage.submission_by_request(root_id, first, context)
1835
+ assert first_request is not None
1836
+ expect(first_request["requestId"]).to_be(first)
1837
+ expect(first_request["id"]).to_be(first_submission_id)
1838
+ second_request = await storage.submission_by_request(root_id, second, context)
1839
+ assert second_request is not None
1840
+ expect(second_request["id"]).to_be(second_submission_id)
1841
+ first_kind = await storage.find_document(
1842
+ {"kind": first, "scope": {"kind": "session"}}, "current", context
1843
+ )
1844
+ assert first_kind is not None
1845
+ expect(first_kind["id"]).to_be(first_kind_document_id)
1846
+ second_kind = await storage.find_document(
1847
+ {"kind": second, "scope": {"kind": "session"}}, "current", context
1848
+ )
1849
+ assert second_kind is not None
1850
+ expect(second_kind["id"]).to_be(second_kind_document_id)
1851
+ first_key = await storage.find_document(
1852
+ {"kind": "family", "key": first, "scope": {"kind": "session"}}, "current", context
1853
+ )
1854
+ assert first_key is not None
1855
+ expect(first_key["id"]).to_be(first_key_document_id)
1856
+ second_key = await storage.find_document(
1857
+ {"kind": "family", "key": second, "scope": {"kind": "session"}}, "current", context
1858
+ )
1859
+ assert second_key is not None
1860
+ expect(second_key["id"]).to_be(second_key_document_id)
1861
+ expect(
1862
+ [
1863
+ item["id"]
1864
+ for item in (
1865
+ await storage.scan_documents(
1866
+ {"scope": {"kind": "session"}, "at": "current", "kind": first}, 10, None, context
1867
+ )
1868
+ )["items"]
1869
+ ]
1870
+ ).to_equal([first_kind_document_id])
1871
+
1872
+ async def keeps_one_global_id_namespace(storage: Storage) -> None:
1873
+ root_id = await _create_root(storage)
1874
+ explicit_entry_id = 100
1875
+ await storage.commit(
1876
+ [{"type": "entry", "value": _entry(explicit_entry_id, root_id)}], context
1877
+ )
1878
+ expect(await storage.mint_id()).to_be(101)
1879
+ await expect(
1880
+ storage.commit(
1881
+ [
1882
+ {
1883
+ "type": "task",
1884
+ "value": _pending_task(explicit_entry_id, root_id),
1885
+ }
1886
+ ],
1887
+ context,
1888
+ )
1889
+ ).rejects.to_throw(f"ID {explicit_entry_id} already belongs to entry")
1890
+
1891
+ await storage.commit(
1892
+ [{"type": "entry", "value": _entry(_MAX_SAFE_INTEGER, root_id, "last-id")}], context
1893
+ )
1894
+ await expect(storage.mint_id()).rejects.to_throw("ID space is exhausted")
1895
+ await expect(storage.mint_id()).rejects.to_throw("ID space is exhausted")
1896
+
1897
+ async def rejects_every_operation_after_close(storage: Storage) -> None:
1898
+ await _create_root(storage)
1899
+ await storage.close(context)
1900
+ await expect(storage.conversation(ROOT_CONVERSATION_ID, context)).rejects.to_throw("closed")
1901
+ await expect(storage.commit(cast(Sequence[StorageWrite], []), context)).rejects.to_throw(
1902
+ "closed"
1903
+ )
1904
+ await expect(storage.mint_id()).rejects.to_throw("closed")
1905
+
1906
+ return [
1907
+ _create_case(options, "reserves ID 1 for the immutable root conversation", reserves_id_one),
1908
+ _create_case(
1909
+ options,
1910
+ "commits mixed table writes atomically and rolls all of them back on failure",
1911
+ commits_mixed_table_writes,
1912
+ ),
1913
+ _create_case(
1914
+ options, "detaches retained writes and every returned record", detaches_retained_writes
1915
+ ),
1916
+ _create_case(
1917
+ options,
1918
+ "detaches prototype-like JSON keys without changing object prototypes",
1919
+ detaches_prototype_like_keys,
1920
+ ),
1921
+ _create_case(options, "indexes entries committed out of ID order", indexes_entries_out_of_order),
1922
+ _create_case(
1923
+ options,
1924
+ "continues an entry cursor below its last item after a newer commit",
1925
+ continues_entry_cursor,
1926
+ ),
1927
+ _create_case(
1928
+ options,
1929
+ "paginates conversations by opaque cursor in ascending ID order",
1930
+ paginates_conversations,
1931
+ ),
1932
+ _create_case(
1933
+ options, "filters and pages conversations by durable owner edges", filters_conversations_by_owner
1934
+ ),
1935
+ _create_case(
1936
+ options,
1937
+ "scans deep fork history newest-first through every ancestor cap",
1938
+ scans_deep_fork_history,
1939
+ ),
1940
+ _create_case(
1941
+ options,
1942
+ "scans tables in either ID order and continues a cursor in its order",
1943
+ scans_tables_in_either_order,
1944
+ ),
1945
+ _create_case(
1946
+ options, "replaces complete task records and pages filtered task scans", replaces_task_records
1947
+ ),
1948
+ _create_case(
1949
+ options,
1950
+ "stores owners and scans waiting and completing tasks by status",
1951
+ stores_owners_and_scans_statuses,
1952
+ ),
1953
+ _create_case(
1954
+ options,
1955
+ "indexes request IDs per conversation and replaces complete submission records",
1956
+ indexes_request_ids,
1957
+ ),
1958
+ _create_case(
1959
+ options,
1960
+ "stores passive write submissions without input-only lifecycle states",
1961
+ stores_passive_write_submissions,
1962
+ ),
1963
+ _create_case(
1964
+ options,
1965
+ "reconstructs rewindable documents and preserves half-open incarnations",
1966
+ reconstructs_rewindable_documents,
1967
+ ),
1968
+ _create_case(
1969
+ options, "streams long document tails across root replacement deltas", streams_long_document_tails
1970
+ ),
1971
+ _create_case(
1972
+ options,
1973
+ "copies stored document bases independently and rejects ambiguous sources",
1974
+ copies_document_bases,
1975
+ ),
1976
+ _create_case(
1977
+ options,
1978
+ "uses bases for version transitions and rejects historical reads of current-only documents",
1979
+ uses_bases_for_version_transitions,
1980
+ ),
1981
+ _create_case(
1982
+ options,
1983
+ "indexes logical addresses and exact-scope scans independently",
1984
+ indexes_logical_addresses,
1985
+ ),
1986
+ _create_case(
1987
+ options,
1988
+ "keeps document lifecycle failures atomic and gives create-plus-retire an empty lifetime",
1989
+ keeps_lifecycle_failures_atomic,
1990
+ ),
1991
+ _create_case(
1992
+ options,
1993
+ "rolls back record tables and secondary indexes when a document command fails",
1994
+ rolls_back_document_failures,
1995
+ ),
1996
+ _create_case(
1997
+ options, "keeps indexed string identities lossless", keeps_string_identities_lossless
1998
+ ),
1999
+ _create_case(
2000
+ options,
2001
+ "keeps one global record ID namespace and rejects exhausted ID minting",
2002
+ keeps_one_global_id_namespace,
2003
+ ),
2004
+ _create_case(options, "rejects every operation after close", rejects_every_operation_after_close),
2005
+ ]
2006
+
2007
+
2008
+ __all__ = ["create_storage_conformance"]