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.
- pi_durable/__init__.py +320 -0
- pi_durable/context.py +328 -0
- pi_durable/delta.py +269 -0
- pi_durable/documents.py +366 -0
- pi_durable/entries.py +66 -0
- pi_durable/env/__init__.py +408 -0
- pi_durable/env/decode.py +48 -0
- pi_durable/env/line_scan.py +190 -0
- pi_durable/env/posix.py +1547 -0
- pi_durable/errors.py +89 -0
- pi_durable/harness/__init__.py +1 -0
- pi_durable/harness/agent.py +344 -0
- pi_durable/harness/compaction.py +673 -0
- pi_durable/harness/context.py +515 -0
- pi_durable/harness/define.py +111 -0
- pi_durable/harness/events.py +766 -0
- pi_durable/harness/generation.py +1079 -0
- pi_durable/harness/harness.py +570 -0
- pi_durable/harness/inbox.py +255 -0
- pi_durable/harness/live.py +374 -0
- pi_durable/harness/output.py +467 -0
- pi_durable/harness/prompt.py +288 -0
- pi_durable/harness/provider.py +78 -0
- pi_durable/harness/registry.py +136 -0
- pi_durable/harness/scheduler.py +1977 -0
- pi_durable/harness/submissions.py +357 -0
- pi_durable/harness/task_graph.py +334 -0
- pi_durable/harness/tool.py +1365 -0
- pi_durable/harness/types.py +772 -0
- pi_durable/harness/usage.py +88 -0
- pi_durable/harness/util.py +204 -0
- pi_durable/harness/validation.py +460 -0
- pi_durable/harness/view.py +324 -0
- pi_durable/ids.py +63 -0
- pi_durable/json.py +186 -0
- pi_durable/providers/__init__.py +362 -0
- pi_durable/providers/anthropic.py +603 -0
- pi_durable/providers/base.py +97 -0
- pi_durable/providers/json_parse.py +117 -0
- pi_durable/providers/openai.py +574 -0
- pi_durable/providers/request.py +35 -0
- pi_durable/providers/retry.py +127 -0
- pi_durable/providers/stream.py +183 -0
- pi_durable/providers/transcript.py +224 -0
- pi_durable/providers/usage.py +58 -0
- pi_durable/py.typed +0 -0
- pi_durable/search.py +115 -0
- pi_durable/session/__init__.py +5 -0
- pi_durable/session/drafts.py +762 -0
- pi_durable/session/forks.py +109 -0
- pi_durable/session/observation.py +374 -0
- pi_durable/session/session.py +1193 -0
- pi_durable/session/transaction.py +1088 -0
- pi_durable/storage/__init__.py +5 -0
- pi_durable/storage/jsonl/__init__.py +15 -0
- pi_durable/storage/jsonl/storage.py +958 -0
- pi_durable/storage/memory.py +889 -0
- pi_durable/storage/scan.py +57 -0
- pi_durable/storage/sqlite/__init__.py +26 -0
- pi_durable/storage/sqlite/database.py +62 -0
- pi_durable/storage/sqlite/migrations.py +191 -0
- pi_durable/storage/sqlite/posix.py +314 -0
- pi_durable/storage/sqlite/schema.py +97 -0
- pi_durable/storage/sqlite/storage.py +1130 -0
- pi_durable/tasks.py +89 -0
- pi_durable/testing/__init__.py +41 -0
- pi_durable/testing/assertions.py +44 -0
- pi_durable/testing/env_conformance.py +828 -0
- pi_durable/testing/runner.py +64 -0
- pi_durable/testing/storage_conformance.py +2008 -0
- pi_durable/testing/types.py +99 -0
- pi_durable/tool_search/__init__.py +159 -0
- pi_durable/tools/__init__.py +81 -0
- pi_durable/tools/_values.py +43 -0
- pi_durable/tools/bash.py +250 -0
- pi_durable/tools/edit.py +214 -0
- pi_durable/tools/edit_diff.py +452 -0
- pi_durable/tools/env.py +19 -0
- pi_durable/tools/file_mutation_queue.py +68 -0
- pi_durable/tools/image.py +248 -0
- pi_durable/tools/image_processor.py +77 -0
- pi_durable/tools/path_utils.py +111 -0
- pi_durable/tools/read.py +506 -0
- pi_durable/tools/write.py +71 -0
- pi_durable/truncate.py +185 -0
- pi_durable/types.py +1297 -0
- pi_durable_python-1.3.0.dist-info/METADATA +276 -0
- pi_durable_python-1.3.0.dist-info/RECORD +90 -0
- pi_durable_python-1.3.0.dist-info/WHEEL +4 -0
- 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"]
|