|
6 | 6 | from durable_workflow import serializer |
7 | 7 | from durable_workflow.external_storage import ( |
8 | 8 | EXTERNAL_PAYLOAD_REFERENCE_SCHEMA, |
| 9 | + ExternalPayloadCache, |
9 | 10 | ExternalPayloadIntegrityError, |
10 | 11 | ExternalPayloadReference, |
11 | 12 | LocalFilesystemExternalStorage, |
@@ -103,6 +104,74 @@ def test_fetch_external_payload_rejects_mutated_bytes(tmp_path: Path) -> None: |
103 | 104 | fetch_external_payload(storage, reference) |
104 | 105 |
|
105 | 106 |
|
| 107 | +def test_fetch_external_payload_cache_reuses_verified_bytes(tmp_path: Path) -> None: |
| 108 | + storage = LocalFilesystemExternalStorage(tmp_path) |
| 109 | + cache = ExternalPayloadCache(max_entries=2, max_bytes=1024) |
| 110 | + reference = store_external_payload(storage, b'{"stable":true}', codec="json") |
| 111 | + path = Path(reference.uri.removeprefix("file://")) |
| 112 | + |
| 113 | + assert fetch_external_payload(storage, reference, cache=cache) == b'{"stable":true}' |
| 114 | + path.write_bytes(b'{"stable":false}') |
| 115 | + |
| 116 | + assert fetch_external_payload(storage, reference, cache=cache) == b'{"stable":true}' |
| 117 | + assert len(cache) == 1 |
| 118 | + |
| 119 | + |
| 120 | +def test_fetch_external_payload_does_not_cache_failed_integrity_check(tmp_path: Path) -> None: |
| 121 | + storage = LocalFilesystemExternalStorage(tmp_path) |
| 122 | + cache = ExternalPayloadCache(max_entries=2, max_bytes=1024) |
| 123 | + reference = store_external_payload(storage, b'{"safe":true}', codec="json") |
| 124 | + path = Path(reference.uri.removeprefix("file://")) |
| 125 | + path.write_bytes(b'{"safe":false}') |
| 126 | + |
| 127 | + with pytest.raises(ExternalPayloadIntegrityError, match="size|hash"): |
| 128 | + fetch_external_payload(storage, reference, cache=cache) |
| 129 | + |
| 130 | + assert len(cache) == 0 |
| 131 | + |
| 132 | + |
| 133 | +def test_decode_envelopes_can_share_external_payload_cache(tmp_path: Path) -> None: |
| 134 | + storage = LocalFilesystemExternalStorage(tmp_path) |
| 135 | + cache = ExternalPayloadCache(max_entries=2, max_bytes=1024) |
| 136 | + env = serializer.external_storage_envelope( |
| 137 | + {"message": "x" * 64}, |
| 138 | + external_storage=storage, |
| 139 | + threshold_bytes=10, |
| 140 | + codec="json", |
| 141 | + ) |
| 142 | + path = Path(env["external_storage"]["uri"].removeprefix("file://")) |
| 143 | + |
| 144 | + assert serializer.decode_envelope(env, external_storage=storage, external_storage_cache=cache) == { |
| 145 | + "message": "x" * 64 |
| 146 | + } |
| 147 | + path.write_bytes(b'{"message":"mutated"}') |
| 148 | + |
| 149 | + assert serializer.decode_envelopes( |
| 150 | + [env], |
| 151 | + external_storage=storage, |
| 152 | + external_storage_cache=cache, |
| 153 | + ) == [{"message": "x" * 64}] |
| 154 | + |
| 155 | + |
| 156 | +def test_external_payload_cache_is_bounded_by_entries_and_bytes(tmp_path: Path) -> None: |
| 157 | + storage = LocalFilesystemExternalStorage(tmp_path) |
| 158 | + cache = ExternalPayloadCache(max_entries=1, max_bytes=20) |
| 159 | + first = store_external_payload(storage, b"first", codec="json") |
| 160 | + second = store_external_payload(storage, b"second", codec="json") |
| 161 | + |
| 162 | + fetch_external_payload(storage, first, cache=cache) |
| 163 | + fetch_external_payload(storage, second, cache=cache) |
| 164 | + |
| 165 | + assert cache.get(first) is None |
| 166 | + assert cache.get(second) == b"second" |
| 167 | + |
| 168 | + too_large = store_external_payload(storage, b"x" * 21, codec="json") |
| 169 | + fetch_external_payload(storage, too_large, cache=cache) |
| 170 | + |
| 171 | + assert cache.get(too_large) is None |
| 172 | + assert cache.current_bytes <= cache.max_bytes |
| 173 | + |
| 174 | + |
106 | 175 | def test_local_storage_rejects_file_uri_outside_root(tmp_path: Path) -> None: |
107 | 176 | storage = LocalFilesystemExternalStorage(tmp_path / "root") |
108 | 177 | outside = tmp_path / "outside" |
|
0 commit comments