From bf5b8b3240913dc9b8a8d28bddf9991a7edb5396 Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Thu, 18 Jun 2026 17:28:27 +0900 Subject: [PATCH 1/5] feat: apply bindings wrapper to AI bindings --- packages/cli/tests/bindings-test/src/test_vectorize.py | 10 ++++++++++ packages/cli/tests/bindings-test/wrangler.jsonc | 3 +++ packages/runtime-sdk/src/workers/_workers.py | 2 ++ 3 files changed, 15 insertions(+) create mode 100644 packages/cli/tests/bindings-test/src/test_vectorize.py diff --git a/packages/cli/tests/bindings-test/src/test_vectorize.py b/packages/cli/tests/bindings-test/src/test_vectorize.py new file mode 100644 index 0000000..38826ad --- /dev/null +++ b/packages/cli/tests/bindings-test/src/test_vectorize.py @@ -0,0 +1,10 @@ +import pytest +from workers._workers import _BindingWrapper + + +@pytest.mark.asyncio +async def test_vectorize_is_wrapped(env): + # Vectorize requires remote database even in local development environment + # so we cannot test it in this unittest + # Here, we just make sure it is wrapped properly with our bindings wrapper + assert isinstance(env.VECTORIZE, _BindingWrapper) diff --git a/packages/cli/tests/bindings-test/wrangler.jsonc b/packages/cli/tests/bindings-test/wrangler.jsonc index 2b486b7..f36efe9 100644 --- a/packages/cli/tests/bindings-test/wrangler.jsonc +++ b/packages/cli/tests/bindings-test/wrangler.jsonc @@ -31,5 +31,8 @@ }, "migrations": [ { "tag": "v1", "new_sqlite_classes": ["TestDurableObject"] } + ], + "vectorize": [ + { "binding": "VECTORIZE", "index_name": "test-index" } ] } diff --git a/packages/runtime-sdk/src/workers/_workers.py b/packages/runtime-sdk/src/workers/_workers.py index 3709bca..a9f193c 100644 --- a/packages/runtime-sdk/src/workers/_workers.py +++ b/packages/runtime-sdk/src/workers/_workers.py @@ -1313,6 +1313,8 @@ class _EnvWrapper: "R2Bucket", "D1Database", "WorkerQueue", + "Ai", + "VectorizeIndexImpl", } def __init__(self, env: Any): From a5cf2c5746a711286c41b51c8b4777ea43cd063d Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Thu, 18 Jun 2026 19:57:48 +0900 Subject: [PATCH 2/5] feat: apply bindings wrapper to Images, RateLimit, and Analytics Engine --- .../src/test_analytics_engine.py | 21 ++++++ .../tests/bindings-test/src/test_images.py | 73 +++++++++++++++++++ .../tests/bindings-test/src/test_ratelimit.py | 13 ++++ .../cli/tests/bindings-test/wrangler.jsonc | 7 ++ packages/runtime-sdk/src/workers/_workers.py | 8 ++ 5 files changed, 122 insertions(+) create mode 100644 packages/cli/tests/bindings-test/src/test_analytics_engine.py create mode 100644 packages/cli/tests/bindings-test/src/test_images.py create mode 100644 packages/cli/tests/bindings-test/src/test_ratelimit.py diff --git a/packages/cli/tests/bindings-test/src/test_analytics_engine.py b/packages/cli/tests/bindings-test/src/test_analytics_engine.py new file mode 100644 index 0000000..c7e4c15 --- /dev/null +++ b/packages/cli/tests/bindings-test/src/test_analytics_engine.py @@ -0,0 +1,21 @@ +import pytest +from workers._workers import _BindingWrapper + + +@pytest.mark.asyncio +async def test_is_wrapped(env): + assert isinstance(env.ANALYTICS, _BindingWrapper) + + +@pytest.mark.asyncio +async def test_write_data_point_blobs_and_doubles(env): + env.ANALYTICS.writeDataPoint({ + "blobs": ["blob1", "blob2"], + "doubles": [1.0, 2.5], + "indexes": ["idx"], + }) + + +@pytest.mark.asyncio +async def test_write_data_point_empty(env): + env.ANALYTICS.writeDataPoint({}) diff --git a/packages/cli/tests/bindings-test/src/test_images.py b/packages/cli/tests/bindings-test/src/test_images.py new file mode 100644 index 0000000..3e847f5 --- /dev/null +++ b/packages/cli/tests/bindings-test/src/test_images.py @@ -0,0 +1,73 @@ +import base64 + +import js +import pytest +from pyodide.ffi import create_proxy, to_js +from workers._workers import _BindingWrapper + +PNG_1X1 = base64.b64decode( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==" +) + + +def _make_stream(data): + def start(controller): + controller.enqueue(to_js(data)) + controller.close() + + return js.ReadableStream.new(to_js({"start": create_proxy(start)})) + + +@pytest.mark.asyncio +async def test_is_wrapped(env): + assert isinstance(env.IMAGES, _BindingWrapper) + + +@pytest.mark.asyncio +async def test_input_returns_pipeline(env): + pipeline = env.IMAGES.input(_make_stream(PNG_1X1)) + assert isinstance(pipeline, _BindingWrapper) + assert callable(pipeline.output) + assert callable(pipeline.transform) + + +@pytest.mark.asyncio +async def test_output_as_png(env): + pipeline = env.IMAGES.input(_make_stream(PNG_1X1)) + output = await pipeline.output({"format": "image/png"}) + resp = output.response() + assert resp.headers.get("content-type") == "image/png" + + +@pytest.mark.asyncio +async def test_transform_and_output(env): + pipeline = env.IMAGES.input(_make_stream(PNG_1X1)) + transformed = pipeline.transform({"width": 1, "height": 1}) + output = await transformed.output({"format": "image/png"}) + resp = output.response() + assert resp.headers.get("content-type") == "image/png" + + +@pytest.mark.asyncio +async def test_info_callable(env): + assert callable(env.IMAGES.info) + + +@pytest.mark.asyncio +async def test_hosted_is_wrapped(env): + assert isinstance(env.IMAGES.hosted, _BindingWrapper) + + +@pytest.mark.asyncio +async def test_hosted_image_callable(env): + assert callable(env.IMAGES.hosted.image) + + +@pytest.mark.asyncio +async def test_hosted_upload_callable(env): + assert callable(env.IMAGES.hosted.upload) + + +@pytest.mark.asyncio +async def test_hosted_list_callable(env): + assert callable(env.IMAGES.hosted.list) diff --git a/packages/cli/tests/bindings-test/src/test_ratelimit.py b/packages/cli/tests/bindings-test/src/test_ratelimit.py new file mode 100644 index 0000000..0023a81 --- /dev/null +++ b/packages/cli/tests/bindings-test/src/test_ratelimit.py @@ -0,0 +1,13 @@ +import pytest +from workers._workers import _BindingWrapper + + +@pytest.mark.asyncio +async def test_is_wrapped(env): + assert isinstance(env.RATE_LIMITER, _BindingWrapper) + + +@pytest.mark.asyncio +async def test_limit_success(env): + result = await env.RATE_LIMITER.limit({"key": "test-key"}) + assert result.success is True diff --git a/packages/cli/tests/bindings-test/wrangler.jsonc b/packages/cli/tests/bindings-test/wrangler.jsonc index 2b486b7..9fe7ffc 100644 --- a/packages/cli/tests/bindings-test/wrangler.jsonc +++ b/packages/cli/tests/bindings-test/wrangler.jsonc @@ -31,5 +31,12 @@ }, "migrations": [ { "tag": "v1", "new_sqlite_classes": ["TestDurableObject"] } + ], + "analytics_engine_datasets": [ + { "binding": "ANALYTICS", "dataset": "test-dataset" } + ], + "images": { "binding": "IMAGES" }, + "ratelimits": [ + { "name": "RATE_LIMITER", "namespace_id": "1001", "simple": { "period": 60, "limit": 100 } } ] } diff --git a/packages/runtime-sdk/src/workers/_workers.py b/packages/runtime-sdk/src/workers/_workers.py index 3709bca..ade6f33 100644 --- a/packages/runtime-sdk/src/workers/_workers.py +++ b/packages/runtime-sdk/src/workers/_workers.py @@ -1313,6 +1313,14 @@ class _EnvWrapper: "R2Bucket", "D1Database", "WorkerQueue", + "SendEmail", + "Ai", + "VectorizeIndexImpl", + "AnalyticsEngineDataset", + "LocalAnalyticsEngineDataset", + "ImagesBindingImpl", + "HostedImagesBindingImpl", + "Ratelimit", } def __init__(self, env: Any): From 9690444d17e1970863b9f492f672111d8fdfc1f1 Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Thu, 18 Jun 2026 19:59:21 +0900 Subject: [PATCH 3/5] chore: tidy up images test --- .../tests/bindings-test/src/test_images.py | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/packages/cli/tests/bindings-test/src/test_images.py b/packages/cli/tests/bindings-test/src/test_images.py index 3e847f5..fc687d7 100644 --- a/packages/cli/tests/bindings-test/src/test_images.py +++ b/packages/cli/tests/bindings-test/src/test_images.py @@ -23,14 +23,6 @@ async def test_is_wrapped(env): assert isinstance(env.IMAGES, _BindingWrapper) -@pytest.mark.asyncio -async def test_input_returns_pipeline(env): - pipeline = env.IMAGES.input(_make_stream(PNG_1X1)) - assert isinstance(pipeline, _BindingWrapper) - assert callable(pipeline.output) - assert callable(pipeline.transform) - - @pytest.mark.asyncio async def test_output_as_png(env): pipeline = env.IMAGES.input(_make_stream(PNG_1X1)) @@ -46,28 +38,3 @@ async def test_transform_and_output(env): output = await transformed.output({"format": "image/png"}) resp = output.response() assert resp.headers.get("content-type") == "image/png" - - -@pytest.mark.asyncio -async def test_info_callable(env): - assert callable(env.IMAGES.info) - - -@pytest.mark.asyncio -async def test_hosted_is_wrapped(env): - assert isinstance(env.IMAGES.hosted, _BindingWrapper) - - -@pytest.mark.asyncio -async def test_hosted_image_callable(env): - assert callable(env.IMAGES.hosted.image) - - -@pytest.mark.asyncio -async def test_hosted_upload_callable(env): - assert callable(env.IMAGES.hosted.upload) - - -@pytest.mark.asyncio -async def test_hosted_list_callable(env): - assert callable(env.IMAGES.hosted.list) From 7d41ed6d6668b4aef3c5dfe0232ca07390f7fd08 Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Thu, 18 Jun 2026 20:00:56 +0900 Subject: [PATCH 4/5] chore: lint --- .../tests/bindings-test/src/test_analytics_engine.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/cli/tests/bindings-test/src/test_analytics_engine.py b/packages/cli/tests/bindings-test/src/test_analytics_engine.py index c7e4c15..371228e 100644 --- a/packages/cli/tests/bindings-test/src/test_analytics_engine.py +++ b/packages/cli/tests/bindings-test/src/test_analytics_engine.py @@ -9,11 +9,13 @@ async def test_is_wrapped(env): @pytest.mark.asyncio async def test_write_data_point_blobs_and_doubles(env): - env.ANALYTICS.writeDataPoint({ - "blobs": ["blob1", "blob2"], - "doubles": [1.0, 2.5], - "indexes": ["idx"], - }) + env.ANALYTICS.writeDataPoint( + { + "blobs": ["blob1", "blob2"], + "doubles": [1.0, 2.5], + "indexes": ["idx"], + } + ) @pytest.mark.asyncio From 487758eb4b887dad5e124b91eafb61e8f4a37b5f Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Tue, 23 Jun 2026 12:04:49 +0900 Subject: [PATCH 5/5] chore: delete packages/cli/tests/BINDING_NOTES.md --- packages/cli/tests/BINDING_NOTES.md | 63 ----------------------------- 1 file changed, 63 deletions(-) delete mode 100644 packages/cli/tests/BINDING_NOTES.md diff --git a/packages/cli/tests/BINDING_NOTES.md b/packages/cli/tests/BINDING_NOTES.md deleted file mode 100644 index f3aa542..0000000 --- a/packages/cli/tests/BINDING_NOTES.md +++ /dev/null @@ -1,63 +0,0 @@ -# Binding Test Notes - -## Bindings with full local tests - -| Binding | Constructor (local) | Tests | -|---------|-------------------|-------| -| KV | `KvNamespace` | Full CRUD, metadata, list, pagination | -| R2 | `R2Bucket` | Full CRUD, metadata, list, ranges, multipart | -| D1 | `D1Database` | Prepare/bind/run, all/first/raw, batch, exec, sessions | -| Queue | `WorkerQueue` | Send various types, sendBatch, consumer receive | -| Durable Objects | `DurableObjectNamespace` | Storage KV, SQL, alarms, transactions, RPC | -| Analytics Engine | `LocalAnalyticsEngineDataset` | writeDataPoint with blobs/doubles/indexes | -| Rate Limiting | `Ratelimit` | limit() with different keys | -| Vectorize | `VectorizeIndexImpl` | Wrapper verification + method presence only (API calls need remote) | - -## Bindings with wrapper-only tests - -| Binding | Constructor (local) | Reason | -|---------|-------------------|--------| -| Images | `ImagesBindingImpl` | `input()` needs a ReadableStream of image bytes; `info()` needs a real image. Local simulator may not support transformations. | -| Images (hosted) | `HostedImagesBindingImpl` | Sub-binding via `env.IMAGES.hosted`. Methods: `image()`, `upload()`, `list()`. `upload()` needs image data, `image()` needs valid image ID, `list()` needs uploaded images. | -| Vectorize | `VectorizeIndexImpl` | "Binding VECTORIZE needs to be run remotely" — wrangler stubs it out locally. | - -## Bindings not testable locally - -| Binding | Reason | Config key | -|---------|--------|------------| -| AI | Always forces remote mode, crashes dev server without account_id | `ai` | -| Stream | Constructor is `Fetcher`, always remote | `stream` | -| Media Transforms | Always remote, no local simulation | `media` | - -## Bindings not yet tested - -| Binding | Config key | Notes | -|---------|-----------|-------| -| Email Send | `send_email` | Constructor is `Fetcher` locally but `SendEmail` in prod. Needs `_FetcherWrapper` + `callRpcMethod` fix locally. Tests exist on a separate branch (PR pending). | -| Email Routing | N/A (handler) | `email()` handler with `message.forward()`/`setReject()`. Tests exist on a separate branch (PR pending). | -| Service Bindings | `services` | Constructor is `Fetcher`. Needs JS service worker running alongside. Tests for `send`/`next`/`normalMethod` RPC shadowing exist on a separate branch (PR pending). | - -## Known issues - -### Images binding — methods need real image data -- `input(stream)` takes a `ReadableStream` of image bytes — requires actual image data to test. -- `info(url_or_blob)` inspects image metadata — requires a real image URL or blob. -- `hosted.upload(stream, options)` uploads an image — requires real image data. -- `hosted.image(id)` gets a handle — requires a valid uploaded image ID. -- `hosted.list(options)` lists images — needs uploaded images to return results. -- All could be tested with a small synthetic PNG, but the local simulator may not support full transformation/hosting operations. - -### Analytics Engine — write-only in local dev -`writeDataPoint()` succeeds locally but data is not queryable from within the Worker. Analytics Engine data is only queryable via the GraphQL API externally. Tests verify the write doesn't error but can't verify the data was recorded. - -### Rate Limiting — always succeeds locally -`limit()` returns `{success: true}` in local dev regardless of how many times it's called. The rate limiting logic is not simulated — it's a passthrough. Tests verify the call succeeds and returns the expected shape but can't verify actual rate limiting behavior. - -### Media Transforms — remote only, no local simulation -The `media` binding does not support local simulation at all. Setting `remote: true` in the binding config enables remote mode but requires account authentication. Cannot be included in the shared test worker without blocking other tests. - -### Stream — remote only, is a Fetcher -The `stream` binding's constructor is `Fetcher`. It always requires remote mode. Same account authentication issue as Media Transforms. - -### AI — remote only, crashes dev server -The `ai` binding always forces remote mode. If no `account_id` is configured, wrangler fails to start the dev server entirely, blocking all other tests. Cannot be included in the shared test worker.