Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b750c87
Specify and plan asynchronous HTML finalization
prk-Jr Oct 12, 2026
35a28e3
Add optional asynchronous HTML finalization
prk-Jr Oct 12, 2026
853f293
Use a platform clock for auction telemetry
prk-Jr Oct 12, 2026
ee6f082
Preserve partial HTML and harden finalization
prk-Jr Oct 12, 2026
5ef1591
Clarify partial HTML and pending auction handling
prk-Jr Oct 12, 2026
3981d02
Merge branch 'main' into feat/1241-async-html-finalization
aram356 Oct 12, 2026
a533df2
Use ordered asynchronous HTML processor completion
prk-Jr Oct 13, 2026
597a528
Record ordered EOF revision publication
prk-Jr Oct 13, 2026
de735bc
Harden HTML EOF buffering and runtime verification
prk-Jr Oct 13, 2026
0ac4b60
Tighten HTML EOF runtime evidence and regression checks
prk-Jr Oct 13, 2026
7418409
Replace HTML EOF Python harness with Rust tests
prk-Jr Oct 13, 2026
f0110be
Merge origin/main and preserve CI verification gates
prk-Jr Oct 13, 2026
463e0cb
Install clippy in the Axum native test job
prk-Jr Oct 14, 2026
142a839
Merge branch 'main' into feat/1241-async-html-finalization
prk-Jr Oct 15, 2026
3e51922
Move finalizer regression tests onto plain stream processor fixtures
prk-Jr Oct 15, 2026
6eda72c
Drop the build-version settings filter duplicated by #1326
prk-Jr Oct 15, 2026
c5667fe
Skip the guide's KV lookup without a cooperative timer
prk-Jr Oct 15, 2026
af831c5
Scope HTML EOF checks to EOF and publisher changes
prk-Jr Oct 15, 2026
912767c
Revert unrelated workflow quote churn and reorder test imports
prk-Jr Oct 15, 2026
7f1c678
Warn that async HTML finish disables template caching
prk-Jr Oct 15, 2026
911e260
Merge branch 'main' into feat/1241-async-html-finalization
prk-Jr Oct 16, 2026
886748a
Address async HTML finalization review feedback
prk-Jr Oct 19, 2026
f5ec39c
Merge branch 'main' into feat/1241-async-html-finalization
prk-Jr Oct 19, 2026
f2dce56
Address async HTML finalization review findings
prk-Jr Oct 24, 2026
4236828
Merge main and reconcile HTML EOF runtime verification
prk-Jr Oct 24, 2026
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
85 changes: 82 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ jobs:
id: viceroy-version
# `.tool-versions` is the single source of truth so this workflow and
# `.github/actions/setup-integration-test-env/action.yml` can't drift.
run: echo "viceroy-version=$(grep '^viceroy ' .tool-versions | awk '{print $2}')" >> $GITHUB_OUTPUT
run: echo "viceroy-version=$(grep '^viceroy ' .tool-versions | awk '{print $2}')" >> "$GITHUB_OUTPUT"
shell: bash

- name: Retrieve Node.js version
id: node-version
run: echo "node-version=$(grep '^nodejs ' .tool-versions | awk '{print $2}')" >> $GITHUB_OUTPUT
run: echo "node-version=$(grep '^nodejs ' .tool-versions | awk '{print $2}')" >> "$GITHUB_OUTPUT"
shell: bash

- name: Set up Rust toolchain
Expand Down Expand Up @@ -65,6 +65,55 @@ jobs:
- name: Run cache purge harness
run: BID_DELAY=3 ./scripts/template-cache-local-test.sh purge

test-fastly-eof:
Comment thread
prk-Jr marked this conversation as resolved.
name: HTML EOF runtime (Fastly)
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v4

- name: Read pinned build tool versions
id: versions
shell: bash
run: |
echo "viceroy=$(awk '$1 == "viceroy" { print $2 }' .tool-versions)" >> "$GITHUB_OUTPUT"
echo "node=$(awk '$1 == "nodejs" { print $2 }' .tool-versions)" >> "$GITHUB_OUTPUT"

- uses: ./.github/actions/setup-rust
with:
target: wasm32-wasip1
cache-key: cargo-fastly-eof

- uses: actions/setup-node@v4
with:
node-version: ${{ steps.versions.outputs.node }}

- name: Cache Viceroy binary
id: cache-viceroy
uses: actions/cache@v4
with:
path: ~/.cargo/bin/viceroy
key: viceroy-${{ runner.os }}-v${{ steps.versions.outputs.viceroy }}

- name: Install Viceroy
if: steps.cache-viceroy.outputs.cache-hit != 'true'
run: cargo install viceroy --version "${{ steps.versions.outputs.viceroy }}" --locked --force

- name: Verify actual Fastly HTML EOF completion and empty-bid assembly
timeout-minutes: 15
env:
CARGO_TARGET_DIR: ${{ github.workspace }}/target
TMPDIR: ${{ runner.temp }}
run: ./scripts/async-html-finalization-local-test.sh fastly --release --auction

- name: Upload EOF runtime diagnostics on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: fastly-html-eof-diagnostics
path: ${{ runner.temp }}/trusted-server-finalize-fastly-*/
if-no-files-found: ignore

test-axum:
name: cargo test (axum native)
runs-on: ubuntu-latest
Expand All @@ -77,6 +126,7 @@ jobs:
# wasm32-wasip1 is required by the "Verify Fastly WASM release
# build" step below; the axum build and tests are native.
target: wasm32-wasip1
components: clippy
cache-key: cargo-axum

- name: Read pinned Node.js version for CLI fixtures
Expand All @@ -94,6 +144,27 @@ jobs:
- name: Run Axum adapter tests
run: cargo test-axum

- name: Verify finalization harness process cleanup
run: cargo test --manifest-path crates/trusted-server-integration-tests/Cargo.toml --target x86_64-unknown-linux-gnu --features html-eof-runtime --test html_eof_runtime

- name: Lint native finalization harness
run: cargo clippy --manifest-path crates/trusted-server-integration-tests/Cargo.toml --target x86_64-unknown-linux-gnu --features html-eof-runtime --test html_eof_runtime -- -D warnings

- name: Verify actual Axum HTML finalization and empty-bid assembly
timeout-minutes: 10
env:
CARGO_TARGET_DIR: ${{ github.workspace }}/target
TMPDIR: ${{ runner.temp }}
run: ./scripts/async-html-finalization-local-test.sh axum --auction

- name: Upload finalization runtime diagnostics on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: axum-html-finalization-diagnostics
path: ${{ runner.temp }}/trusted-server-finalize-axum-*/
if-no-files-found: ignore

- name: Test template build digest inputs (native)
run: cargo test -p trusted-server-core --test template_build_digest

Expand Down Expand Up @@ -123,6 +194,7 @@ jobs:
uses: ./.github/actions/setup-rust
with:
target: wasm32-unknown-unknown
components: clippy
cache-key: cargo-cloudflare

- name: Check Cloudflare adapter (native host)
Expand All @@ -131,6 +203,9 @@ jobs:
- name: Check Cloudflare adapter (wasm32-unknown-unknown)
run: cargo check -p trusted-server-adapter-cloudflare --target wasm32-unknown-unknown --features cloudflare

- name: Lint Cloudflare EOF fixture (WASM)
run: cargo clippy -p trusted-server-adapter-cloudflare --target wasm32-unknown-unknown --features cloudflare,test-utils --lib -- -D warnings

- name: Run Cloudflare adapter tests (native host)
run: cargo test-cloudflare

Expand All @@ -144,6 +219,7 @@ jobs:
uses: ./.github/actions/setup-rust
with:
target: wasm32-wasip1
components: clippy
cache-key: cargo-spin

- name: Check Spin adapter (native host)
Expand All @@ -152,6 +228,9 @@ jobs:
- name: Check Spin adapter (wasm32-wasip1)
run: cargo check-spin

- name: Lint Spin EOF fixture (WASM)
run: cargo clippy -p trusted-server-adapter-spin --target wasm32-wasip1 --features spin,test-utils --lib -- -D warnings

- name: Build Spin adapter release WASM
# Mirror the Fastly release-build overrides so the artifact embeds usable
# settings instead of the trusted-server.toml placeholders, which startup
Expand Down Expand Up @@ -268,7 +347,7 @@ jobs:
- name: Retrieve Node.js version
id: node-version
working-directory: .
run: echo "node-version=$(grep '^nodejs ' .tool-versions | awk '{print $2}')" >> $GITHUB_OUTPUT
run: echo "node-version=$(grep '^nodejs ' .tool-versions | awk '{print $2}')" >> "$GITHUB_OUTPUT"
shell: bash

- name: Use Node.js
Expand Down
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,13 @@ Every PR must pass:
10. Wasm clock lint: `scripts/lint-wasm-clock.sh` (crates built into the Cloudflare Worker must use `web_time`, not the `std::time::{Instant,SystemTime}` `now`/`elapsed` methods, which panic there)
11. Documentation proposal scripts: `shellcheck scripts/docs-proposal/*.sh && node --test scripts/docs-proposal/hunks.test.mjs && scripts/docs-proposal/test.sh`

CI also runs these HTML EOF checks. Run them locally only when changing EOF
finalization, publisher or HTML-processor code:

- Native Rust HTML EOF harness configuration, timing and cleanup: `cargo test --manifest-path crates/trusted-server-integration-tests/Cargo.toml --features html-eof-runtime --test html_eof_runtime` and `cargo clippy --manifest-path crates/trusted-server-integration-tests/Cargo.toml --features html-eof-runtime --test html_eof_runtime -- -D warnings`
- Actual Axum EOF/auction controls: `./scripts/async-html-finalization-local-test.sh axum --auction`
- Actual Fastly EOF/auction controls with repository-pinned Viceroy (the fixture requires 0.19+): `./scripts/async-html-finalization-local-test.sh fastly --release --auction`. The auction control checks empty-bid assembly with a deliberately unreachable bidder. See `scripts/README.md` for retained diagnostics and other runtimes.

---

## Standard Workflow
Expand Down
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/trusted-server-adapter-axum/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ async fn dispatch_fallback(
// slots; `handle_publisher_request` matches them against the request path.
let mut ec_context = build_ec_context(state, services, &req);
let auction = AuctionDispatch {
integration_registry: &state.registry,
orchestrator: &state.orchestrator,
slots: state.settings.creative_opportunity_slots(),
registry: None,
Expand Down Expand Up @@ -526,6 +527,7 @@ fn named_route_handler(
} else {
let mut ec_context = build_ec_context(&state, &services, &req);
let auction = AuctionDispatch {
integration_registry: &state.registry,
orchestrator: &state.orchestrator,
slots: state.settings.creative_opportunity_slots(),
registry: None,
Expand Down
35 changes: 35 additions & 0 deletions crates/trusted-server-adapter-axum/src/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,11 @@ pub fn build_runtime_services(ctx: &edgezero_core::context::RequestContext) -> R
static BACKEND: std::sync::OnceLock<Arc<dyn PlatformBackend>> = std::sync::OnceLock::new();
static GEO: std::sync::OnceLock<Arc<dyn PlatformGeo>> = std::sync::OnceLock::new();

static TIMER: std::sync::OnceLock<Arc<dyn trusted_server_core::platform::PlatformTimer>> =
std::sync::OnceLock::new();

RuntimeServices::builder()
.timer(Arc::clone(TIMER.get_or_init(|| Arc::new(AxumTimer))))
.config_store(Arc::clone(CONFIG_STORE.get_or_init(|| {
Arc::new(AxumPlatformConfigStore) as Arc<dyn PlatformConfigStore>
})))
Expand Down Expand Up @@ -581,6 +585,16 @@ pub fn build_runtime_services(ctx: &edgezero_core::context::RequestContext) -> R
.build()
}

struct AxumTimer;

impl trusted_server_core::platform::PlatformTimer for AxumTimer {
fn delay(&self, duration: std::time::Duration) -> futures::future::LocalBoxFuture<'static, ()> {
Box::pin(async move {
tokio::time::sleep(duration).await;
})
}
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
Expand All @@ -592,6 +606,27 @@ mod tests {
use std::time::Duration;
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};

#[test]
fn html_finalization_timer_is_shared_without_an_executor() {
let request = edgezero_core::http::request_builder()
.body(edgezero_core::body::Body::empty())
.expect("should build a request");
let context = edgezero_core::context::RequestContext::new(request, Default::default());
let services = build_runtime_services(&context);
let next_services = build_runtime_services(&context);
assert!(
std::ptr::eq(
services
.timer()
.expect("should install a cooperative timer"),
next_services
.timer()
.expect("should reuse the cooperative timer"),
),
"should share the stateless timer without requiring an executor"
);
}
Comment thread
prk-Jr marked this conversation as resolved.

#[test]
fn auction_http_capabilities_are_explicit() {
let client = AxumPlatformHttpClient::new();
Expand Down
2 changes: 2 additions & 0 deletions crates/trusted-server-adapter-cloudflare/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ crate-type = ["cdylib", "rlib"]

[features]
default = []
# Opt-in local runtime fixture and its diagnostic console sink.
test-utils = ["trusted-server-core/test-utils"]
# Keep for explicit `cargo check --features cloudflare --target wasm32-unknown-unknown`
cloudflare = ["edgezero-adapter-cloudflare/cloudflare", "dep:worker"]

Expand Down
2 changes: 2 additions & 0 deletions crates/trusted-server-adapter-cloudflare/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,7 @@ fn build_router(state: &Arc<AppState>) -> RouterService {
} else {
let mut ec_context = build_ec_context(&state.settings, &services, &req);
let auction = AuctionDispatch {
integration_registry: &state.registry,
orchestrator: &state.orchestrator,
slots: state.settings.creative_opportunity_slots(),
registry: None,
Expand Down Expand Up @@ -724,6 +725,7 @@ fn build_router(state: &Arc<AppState>) -> RouterService {
let page_bids = make_handler(Arc::clone(&state), |s, services, req| async move {
let mut ec_context = build_ec_context(&s.settings, &services, &req);
let auction = AuctionDispatch {
integration_registry: &s.registry,
orchestrator: &s.orchestrator,
slots: s.settings.creative_opportunity_slots(),
registry: None,
Expand Down
31 changes: 31 additions & 0 deletions crates/trusted-server-adapter-cloudflare/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ use worker::{Context, Env, Request, Response, Result, event};
/// Returns a Workers runtime error when the fallback error response cannot be
/// constructed.
pub async fn main(req: Request, env: Env, ctx: Context) -> Result<Response> {
#[cfg(feature = "test-utils")]
install_fixture_logger();
if let Ok(config) = env.var("TRUSTED_SERVER_CONFIG") {
app::set_cloudflare_config_json(config.to_string());
}
Expand All @@ -37,3 +39,32 @@ pub async fn main(req: Request, env: Env, ctx: Context) -> Result<Response> {
}
}
}

#[cfg(all(feature = "test-utils", target_arch = "wasm32"))]
struct FixtureLogger;

#[cfg(all(feature = "test-utils", target_arch = "wasm32"))]
impl log::Log for FixtureLogger {
fn enabled(&self, metadata: &log::Metadata<'_>) -> bool {
metadata.level() <= log::Level::Warn
}

fn log(&self, record: &log::Record<'_>) {
if self.enabled(record.metadata()) {
worker::console_warn!("{}", record.args());
}
}

fn flush(&self) {}
}

#[cfg(all(feature = "test-utils", target_arch = "wasm32"))]
fn install_fixture_logger() {
static LOGGER: FixtureLogger = FixtureLogger;
if log::set_logger(&LOGGER).is_ok() {
std::panic::set_hook(Box::new(|information| {
worker::console_error!("{}", information);
}));
log::set_max_level(log::LevelFilter::Warn);
}
}
Loading
Loading