Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/tests/streams/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
Streams test suite, organized WPT-style: one subdirectory per functional area
(`identity/`, `encoding/`, `compression/`, `digest/`, `strategies/`,
`readable/`, `readable-byte/`, `writable/`, `transform/`, `piping/`,
`inspect/`, `r2-patterns/`, `iocontext/`). Every
`inspect/`, `r2-patterns/`, `iocontext/`, `cache/`, `htmlrewriter/`,
`formdata/`). Every
test here runs against **both** streams implementations — the legacy C++ one
(`src/workerd/api/streams/`) and the TypeScript one
(`src/per_isolate/webstreams/`) — to prove parity. A test that only makes
Expand Down
35 changes: 35 additions & 0 deletions src/tests/streams/cache/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Cache API × streams

The Cache API consuming and producing stream bodies under both stream
implementations. **The tests are the normative artifact.** The general
Cache API surface (headers, vary, purge, instrumentation) is owned by
`src/workerd/api/tests/cache-*`; this suite owns the STREAMS
interaction only.

## Infrastructure

Both cells wire `cacheApiOutbound` to a loopback `cache-backend` worker
(cache-backend.js). A cache.put() arrives there as a PUT whose body is
the SERIALIZED HTTP RESPONSE (status line + headers + CRLFCRLF + body);
the backend splits at the header boundary, verifies the continuous
prime-modulus byte pattern over the body, extracts the serialized
head's Content-Length, and records everything. The test worker reads
the record back through its MOCK service binding (/last-put).

## Coverage (parity — no divergences observed)

| Test | Shape |
| --- | --- |
| `putJsValueStreamBody` | value stream of Uint8Array chunks, byte-exact at the backend |
| `putJsByteStreamBody` | 64 KiB chunked byte stream |
| `putFixedLengthStreamBody` | FixedLengthStream body; the declared length arrives as a concrete Content-Length in the serialized head |
| `putIdentityStreamBody` | identity body fed by a concurrent writer |
| `putLargeStreamBody` | 1 MiB chunked, byte-exact |
| `putDisturbedBodyRejects` / `putLockedBodyRejects` | TypeError before any backend traffic |
| `putErroringBodyRejects` | source error rejects the put |
| `concurrentClonePuts` | the migrated cache-put-stream-test.js regression: clone + concurrent puts over a live TransformStream body, 1 MiB |
| `matchBodyIsReadableStream` | a HIT body streams out and drains via a reader |

Consumed source (deleted): cache-put-stream-test.js. The backend
worker takes no compatibilityDate — wd_test injects `--compat-date`,
and a worker-level date conflicts with it.
19 changes: 19 additions & 0 deletions src/tests/streams/cache/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
load("//:build/wd_test.bzl", "wd_test")

# The cache streams suite runs the same test modules under two configs:
# cache-cpp against the C++ implementation, cache-ts against the
# TypeScript implementation. Both wire a loopback cache backend that
# records puts for integrity verification.

cache_suite_srcs = glob(["*.js"]) + ["cache-modules.capnp"]

wd_test(
src = "cache-cpp.wd-test",
data = cache_suite_srcs,
)
Comment on lines +10 to +13

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The moved regression previously ran with only nodejs_compat. This C++ cell forces the standard TransformStream constructor, so the test no longer covers workers before transformstream_enable_standard_constructor became enabled. Add an unflagged legacy config/module that runs concurrentClonePuts, then register it here with its all-compat-flags variant disabled.

Suggested change
wd_test(
src = "cache-cpp.wd-test",
data = cache_suite_srcs,
)
wd_test(
src = "cache-cpp.wd-test",
data = cache_suite_srcs,
)
# Retains the cache clone-put regression for workers using the legacy
# TransformStream constructor.
wd_test(
src = "cache-cpp-legacy.wd-test",
data = cache_suite_srcs,
generate_all_compat_flags_variant = False,
)


wd_test(
src = "cache-ts.wd-test",
args = ["--experimental"],
data = cache_suite_srcs,
)
77 changes: 77 additions & 0 deletions src/tests/streams/cache/cache-backend.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright (c) 2026 Cloudflare, Inc.
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
// https://opensource.org/licenses/Apache-2.0

// Recording cache backend: a cache.put() arrives as a PUT whose body
// is the SERIALIZED HTTP RESPONSE (status line + headers + CRLFCRLF +
// body). The backend splits at the header boundary, verifies the
// continuous byte pattern over the BODY, extracts the serialized
// head's Content-Length, and records it all so the test worker can
// verify integrity via its MOCK service binding (/last-put). GETs
// serve the cache.match protocol (only-if-cached → HIT for
// /cached-resource, else 504 MISS).

const PATTERN_MODULUS = 251;
let lastPut = null;

function findHeaderBoundary(bytes) {
for (let i = 0; i + 3 < bytes.byteLength; i++) {
if (
bytes[i] === 13 &&
bytes[i + 1] === 10 &&
bytes[i + 2] === 13 &&
bytes[i + 3] === 10
) {
return i + 4;
}
}
return -1;
}

export default {
async fetch(request) {
const url = new URL(request.url);

if (request.method === 'PUT') {
const bytes = new Uint8Array(await request.arrayBuffer());
const bodyStart = findHeaderBoundary(bytes);
const head = new TextDecoder().decode(bytes.subarray(0, bodyStart));
const declaredLength = /content-length:\s*(\d+)/i.exec(head)?.[1] ?? null;
let patternOk = bodyStart >= 0;
for (let i = bodyStart; i < bytes.byteLength; i++) {
if (bytes[i] !== (i - bodyStart) % PATTERN_MODULUS) {
patternOk = false;
break;
}
}
lastPut = {
url: url.href,
byteLength: bytes.byteLength - bodyStart,
declaredLength,
patternOk,
};
return new Response(null, { status: 204 });
}

if (request.method === 'GET') {
if (url.pathname === '/last-put') {
return Response.json(lastPut);
}
const cacheControl = request.headers.get('cache-control');
if (cacheControl?.includes('only-if-cached')) {
if (url.pathname.includes('cached-resource')) {
return new Response('Cached content', {
status: 200,
headers: { 'CF-Cache-Status': 'HIT' },
});
}
return new Response(null, {
status: 504,
headers: { 'CF-Cache-Status': 'MISS' },
});
}
}

return new Response('Not Found', { status: 404 });
},
};
33 changes: 33 additions & 0 deletions src/tests/streams/cache/cache-cpp.wd-test
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Workerd = import "/workerd/workerd.capnp";
using CacheModules = import "cache-modules.capnp";

# Cache API × streams against the C++ implementation. The cache backend
# is a loopback worker that records each put (byte count, pattern
# integrity, declared Content-Length); the MOCK binding lets tests read
# the record back.

const unitTests :Workerd.Config = (
services = [
( name = "streams-cache-cpp",
worker = (
modules = CacheModules.modules,
cacheApiOutbound = "cache-backend",
compatibilityFlags = [
"nodejs_compat",
"streams_enable_constructors",
"transformstream_enable_standard_constructor",
],
bindings = [
( name = "MOCK", service = "cache-backend" ),
],
)
),
( name = "cache-backend",
worker = (
modules = [
(name = "backend", esModule = embed "cache-backend.js"),
],
)
),
],
);
9 changes: 9 additions & 0 deletions src/tests/streams/cache/cache-modules.capnp
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Workerd = import "/workerd/workerd.capnp";

# The single source of truth for the cache suite's test modules.

const modules :List(Workerd.Worker.Module) = [
(name = "main", esModule = embed "main.js"),
(name = "which-impl", esModule = embed "which-impl.js"),
(name = "cache-streams", esModule = embed "cache-streams.js"),
];
Loading
Loading