Skip to content

test: close host-testable coverage gaps (handler, nested-AppConfig checker, AdapterPushContext, run_native_cli, compression)#285

Open
aram356 wants to merge 297 commits into
mainfrom
spec/test-coverage-gap
Open

test: close host-testable coverage gaps (handler, nested-AppConfig checker, AdapterPushContext, run_native_cli, compression)#285
aram356 wants to merge 297 commits into
mainfrom
spec/test-coverage-gap

Conversation

@aram356

@aram356 aram356 commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Closes host-testable coverage gaps identified in a test-coverage assessment spec, adding 12 characterization tests across 5 files.
  • Tests pin existing behavior (coverage closure) — no source logic changed. Tier 1 (handler bridge, nested-AppConfig CI checker) and Tier 2 (push context, native CLI dispatch, compression error paths) gaps are now covered.
  • Includes the assessment + closure spec doc (revised through a deep multi-agent review, round 7).

Changes

Crate / File Change
crates/edgezero-core/src/handler.rs 2 tests: DynHandler::call + IntoHandler bridge, including error propagation (Tier 1)
crates/edgezero-cli/src/bin/check_no_nested_app_config.rs 4 tests: syn-based recursive helpers struct_derives_app_config / type_contains_app_config_struct for the nested-AppConfig CI checker (Tier 1)
crates/edgezero-adapter/src/registry.rs 2 tests: AdapterPushContext builder (Tier 2)
crates/edgezero-adapter/src/cli_support.rs 2 tests: run_native_cli not-found + non-zero-exit error paths (Tier 2)
crates/edgezero-core/src/compression.rs 2 tests: gzip + brotli decode error paths (Tier 2)
docs/superpowers/specs/2026-06-16-test-coverage-gap.md Test-coverage assessment + closure spec

Closes

Test plan

  • cargo test --workspace --all-targets
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo fmt --all -- --check
  • cargo check --workspace --all-targets --features "fastly cloudflare spin"
  • WASM builds: wasm32-wasip2 (Spin) via cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spin
  • Other: prettier on the spec doc

Checklist

  • Changes follow CLAUDE.md conventions
  • No Tokio deps added to core or adapter crates
  • Route params use {id} syntax (not :id)
  • Types imported from edgezero_core (not http crate)
  • New code has tests
  • No secrets or credentials committed

aram356 added 30 commits April 29, 2026 17:05
Investigated removing the allow: 40 sites in edgezero-core alone (every
public error type and handle: EdgeError, KvError, SecretError,
ConfigStoreError, ConfigStoreHandle, plus the entire Manifest* family).
The renames would force consumers in 4 adapter crates + cli + demo to
either write `kv::Error`/`secret::Error`/etc. at every callsite or set
up `use ... as KvError` aliases — a net loss in readability for a
deliberately-prefixed cross-crate API.

Replaced the terse comment with a longer one documenting the audit and
why the allow is load-bearing rather than a leftover.
Attempted the rename and surfaced three blockers:

  1. `proxy::Request`/`proxy::Response` would collide with
     `http::Request`/`http::Response` already imported at every
     consumer; the only non-colliding alternatives (`OutboundRequest`,
     `Outbound`) are strictly more verbose than `ProxyRequest`.
  2. `manifest.rs` has 17 `Manifest*` types used directly by adapters,
     cli, demos, scaffold templates, and the `#[app]` macro output.
     Stripping the prefix would force every site to write
     `use edgezero_core::manifest::Spec as Manifest` etc.
  3. The macro emits code that references these names by their current
     spelling; renaming requires regenerating every app and updating
     CLAUDE.md examples.

The lint's intent (the std-style `module::Type` idiom) is sound but
fights this crate's flat re-export surface, and several names cannot
be deprefixed without losing meaning. Allow stays with the audit
documented inline.
Two sites in middleware.rs computed `start.elapsed().as_secs_f64() *
1000.0` to get milliseconds with sub-ms precision for the
request-logging line. Sub-ms precision in a log line is unnecessary —
switch to `Duration::as_millis()` (returns `u128`) and drop the
`{:.2}` format spec. No precision loss that any reader would notice;
removes the only float-arithmetic site in the workspace.
Audit: only `Body { Once, Stream }` triggers the lint workspace-wide.
Marking it `#[non_exhaustive]` would force `_ => unreachable!()` at
each of the 37 external match sites in the four adapter crates, and
a third Body variant would silently `panic!` at runtime instead of
producing a compile error at every consumer. Body is intentionally
closed; the lint is genuinely incompatible with the design.
Add `#[inline]` to every public function and trait method across the
workspace. Touches 44 files: edgezero-core (~242 sites) and the four
adapter crates. Placement is right above the `pub fn` after any doc
comments and `#[must_use]`. No `#[inline(always)]` — leaving the call
to rustc/LLVM, which is the actual inlining decision-maker.

Note: the original workspace-allow rationale ("rustc/LLVM make better
choices than us") is still half true — the lint just wants the *hint*
present, even though rustc inlines monomorphised generics aggressively
without it. Adding the hint is cheap and the lint is satisfied.
Defends against the CodeQL `rust/cleartext-logging` rule, which heuristically
flagged `log_store_bindings` because it pipes
`manifest_data.secret_store_name(adapter)` into `log::info!`. The method
returns the binding identifier from `edgezero.toml` (e.g. `"MY_SECRETS"`),
not the secret value — but the function name pattern triggers the
analyzer's "credential getter" heuristic. Renaming to
`secret_store_binding` makes the intent unambiguous and the alert no
longer fires. Also reorders the impl method block so
`secret_store_binding` lands before `secret_store_enabled` per
`arbitrary_source_item_ordering`.
GitHub deprecated Node 20 as the JavaScript actions runtime on
2025-09-19; v4 of these three actions still ships Node 20 and triggers
the deprecation warning on every CI run. v5 majors ship the Node 24
binary and the warning goes away. All three v5 majors are stable;
the bump is mechanical and covers test.yml, format.yml,
deploy-docs.yml, and codeql.yml (11 sites total).
Previous commit only went to v5 for the three Node-deprecation actions.
Audit of all actions used across the four workflows shows five more
behind by one or two majors:

  actions/checkout                 v5 → v6
  actions/setup-node               v5 → v6
  actions/configure-pages          v4 → v6
  actions/deploy-pages             v4 → v5
  actions/upload-pages-artifact    v3 → v5

All other pins are already current:
  actions/cache                                       v5  (latest)
  actions-rust-lang/setup-rust-toolchain              v1  (latest)
  github/codeql-action/{init,analyze}                 v4  (latest)
CodeQL's `rust/cleartext-logging` rule (alert #7) taints any value
returned by a function whose name contains "secret" — it can't tell
configuration metadata (the binding identifier from edgezero.toml)
from secret material. The previous rename
`secret_store_name → secret_store_binding` did NOT defeat the
heuristic because "secret" is still in the function name.

Real fix: stop logging the binding name. Operators can read their
own `edgezero.toml` to verify which store binding was configured.
The presence message ("secrets enabled for axum") is still emitted,
which is the only thing the log line was actually load-bearing for.

Updated the affected unit test assertion to match the new wording.
Same heuristic as alert #7 — CodeQL taints any value returned by a
function whose name contains "secret" and tracks it through to HTTP
sinks. The test helper `start_test_server_with_secret_handle` was
flagged because its return value's `base_url` flowed into
`reqwest::Client::get(url)`.

Rename the helper to `start_test_server_with_store_handle` and the
return struct to `TestServerWithStore`. Functionally identical — the
test just bootstraps a dev server with an optional handle. The
remaining `with_secret_handle` builder method on `AxumDevServer` is
unaffected because it returns `Self`, not a sink-bound value.
Three real coverage gaps from earlier commits were untested:

  1. `KvStore::put_bytes_with_ttl` overflow error path
     (axum/PersistentKvStore). Asserts `Duration::MAX` triggers
     `SystemTime::checked_add` overflow and surfaces as
     `KvError::Internal("ttl overflows system time")`.
  2. `Manifest::try_load_from_str` Err path. Two cases: invalid TOML
     bytes and a manifest that fails `validator` (empty config-store
     name). Both should return `io::ErrorKind::InvalidData`.
  3. `GeneratorError::Format` smoke test. The variant cannot fire in
     practice (write-to-String is infallible), but it is part of the
     public error surface and the `From<fmt::Error>` wiring must keep
     working — assert construction + Display.

Existing coverage for the other behaviour-affecting changes was
already adequate: `KvStore::exists` is exercised by the
`contract_exists` macro across every impl plus 3 dedicated unit
tests, and `Hooks` default-method overrides are exercised by the
`TestHooks`/`DefaultHooks` tests already in app.rs.
ctor 1.0 requires explicit `#[ctor(unsafe)]` to acknowledge that
pre-main static-initialisation runs without the usual Rust safety
guarantees. The annotation is an attribute argument, not an
`unsafe { }` block, so the workspace `unsafe_code = "deny"` lint is
still satisfied. Updated the four adapter cli.rs files
(axum/cloudflare/fastly/spin).

spin-sdk 6.0 is NOT bumped: it raises the MSRV to rustc 1.93 but the
workspace ships rustc 1.91.1 (.tool-versions). Pin stays at 5.2 with
an explanatory comment until we bump the toolchain.
Bumps `.tool-versions`:
  rust       1.91.1   →  1.95.0
  viceroy    0.16.4   →  0.17.0

Both viceroy 0.17 and spin-sdk 6.0 raised their MSRV to rustc 1.93/1.95
respectively. We can now take viceroy 0.17 freely; spin-sdk 6.0 has
breaking API changes (Method variants → http::Method constants,
`IncomingRequest` removed, Builder::build() → .body()) and is left at
5.2 with a TODO until a focused migration PR.

New 1.95 clippy lints fixed in-place:
  - `result_map_unwrap_or_default`: `.map(p).unwrap_or(false)` → `.is_ok_and(p)` (2 sites)
  - `manual_map`: `.map(x).unwrap_or(default)` → `.map_or(default, x)` (1 site)
  - `duration_suboptimal_units`: `Duration::from_secs(60)` → `from_mins(1)` in
    non-const contexts. Two const items keep `from_secs(60 * 60 * 24 * 365)`
    with a localized `#[expect(clippy::duration_suboptimal_units, reason =
    "from_days/from_mins not stable in const context")]` because
    `Duration::from_{mins,days}` const variants are still nightly-only.
  - `to_string_in_format_args` / `inefficient_to_string`: replaced two
    `ToString::to_string` / `str::to_string` with `str::to_owned`
  - `missing_inline_in_public_items`: added `#[inline]` to two proc-macro
    entrypoints in edgezero-macros, three EnvOverride methods + the
    `env_guard` helper in axum/test_utils, and `From<Action>` for
    AdapterAction in cli/adapter.rs
  - `doc_paragraph_terminators`: added trailing punctuation to clap doc
    comments on every variant/field of `Command`/`NewArgs` (cli/args.rs)
    and the `KV_TABLE` doc in axum/key_value_store.rs

Docs:
  - CLAUDE.md "Rust": 1.91.1 → 1.95.0
  - CLAUDE.md "Fastly CLI": v13.0.0 → 15.1.0
  - Fix typo `fasltly` → `fastly` in .tool-versions; remove dup line
  - examples/app-demo/.../rust-toolchain.toml: 1.91.1 → 1.95.0
  - test.yml: drop the now-stale "1.91 MSRV constraint" comment on the
    viceroy install step
Both warnings sat behind `#[cfg]` gates that the `--all-features`
build profile hid:

1. `fastly::init_logger` (no-features stub) needed `#[inline]` —
   `missing_inline_in_public_items` only fires when the stub branch
   is selected, i.e. when the `fastly` feature is off.
2. `cli::dev_server::EchoParams` (no-`dev-example` build) was
   defined after `default_router`/`build_dev_router`; the canonical
   item ordering wants structs before fns at module level. Moved
   `EchoParams` to the top of the module so the order is correct
   in either feature profile.

Surfaces only via `cargo clippy --workspace --all-targets` (no
`--all-features`); the existing CI runs `--all-features` so we did
not catch this until now.
The `https://wasmtime.dev/install.sh` script broke as of 2026-05-19:
its version-detection interpolation failed and it tried to download
literal version `{`, causing the spin-wasm-tests CI job to fail
("Could not download Wasmtime version '{'").

Replace the install path with a direct GitHub-release tarball
download, pinned to the version recorded in `.tool-versions` (same
single-source-of-truth pattern already used for rust + viceroy).
Adds `wasmtime 44.0.1` to `.tool-versions` and a `Resolve Wasmtime
version` step in the workflow that greps it out.
1. `pub_with_shorthand` comment direction was reversed in the
   workspace `Cargo.toml`. Confirmed by removing the allow: 6 sites
   fire `usage of \`pub\` without \`in\`` (i.e. clippy flags
   `pub(crate)` and wants `pub(in crate)`). Restore the allow with
   wording that matches the actual lint direction and reflects
   the audited 6-site count.

2. Workspace `.cargo/config.toml` was hard-coding the
   `wasm32-wasip1` runner to Viceroy, which silently broke
   `cargo test -p edgezero-adapter-spin --target wasm32-wasip1`
   from the workspace root (used viceroy host ABI instead of
   wasmtime).

   Fix: remove the workspace-level runner entirely and add a
   per-package config for spin (`crates/edgezero-adapter-spin/
   .cargo/config.toml`) that selects `wasmtime run`. Fastly
   already had its own per-package config. CI continues to
   override via `CARGO_TARGET_WASM32_WASIP1_RUNNER` env var, so
   workspace-root invocations work in CI without the global
   default.

3. Add a module-level doc comment at the top of
   `crates/edgezero-adapter-spin/tests/contract.rs` explaining
   that the tests cover internal router/dispatch logic, NOT the
   Spin host ABI (no `spin_sdk`/WIT imports). A breaking change
   in the Spin runtime's WIT would not be caught here.
`parse_handler_path` previously panicked on a syntactically-invalid
handler path in `edgezero.toml`, which rustc surfaced as a confusing
"proc-macro panicked" message. Refactor to return `Result<ExprPath,
String>`; `build_middleware_tokens` and `build_route_tokens` propagate
the error; `expand_app` returns `compile_error!()` with the message,
matching the existing error path for manifest read/parse/validation
failures.

Two new tests: parse_handler_path_accepts_absolute_crate_path (happy
path) and parse_handler_path_rejects_invalid_syntax_with_message
(asserts the error message names the failure and echoes the offending
input).

Addresses the PR review comment on `crates/edgezero-macros/src/app.rs`.
PR reviewer claimed the lint warns *against* longhand and recommends
shorthand (i.e. our `pub(crate)` use should never fire it). Verified
empirically — removing the allow on clippy 1.95 produces 6 errors:

  error: usage of `pub` without `in`
    |  pub(crate) fn decompress_body(...)
    |  ^^^^^^^^^^ help: add it: `pub(in crate)`
    = help: ...index.html#pub_with_shorthand

So `pub_with_shorthand` flags `pub(crate)` and suggests `pub(in crate)`;
the reviewer's reading is 180° off. Quote the diagnostic in the comment
itself so future maintainers don't fall into the same trap.
Sub-project #1 of 7 in the CLI extensions roadmap. Turns edgezero-cli into
lib + bin, exposes per-command Args structs and run_* functions for
downstream projects to compose their own CLIs via clap subcommand
flattening, and adds app-demo-cli as the canonical consumer.

Force-added because docs/superpowers/ is gitignored project-wide for plans;
this spec is shared design intent and meant to be reviewed in the repo.
Replaces the sub-project-#1-only spec with a single design document that
covers the full effort: extensible edgezero-cli library, generator updates
for <name>-cli and <name>.toml scaffolding, per-service typed app-config
schema with validator integration, four new commands (auth, provision,
config validate, config push), shell-out mocking via a private
CommandRunner trait, and the app-demo overhaul that exercises everything
end-to-end.

Implementation still ships in 7 incremental PRs but the design decisions
live in one place so reviewers see the whole picture.

Force-added because docs/superpowers/ is gitignored project-wide.
High-severity fixes:
- Add --manifest to ProvisionArgs and ConfigPushArgs (matches validate)
- Update Wrangler invocations to 3.60+ syntax (space-form, --namespace-id)
- Persist provisioned IDs in edgezero.toml [stores.*.adapters.<x>].id;
  cross-write to per-adapter manifests where deploys need them
- Mermaid diagram in §3 replacing ASCII art

Medium-severity fixes:
- config push runs strict validation as pre-flight (no separate flag)
- Move --adapter to each AuthSub variant so UX is `auth login --adapter X`
- Constrain typed config push to serde_json::to_value(C) -> Object;
  document flatten / rename / skip / Option::None handling
- Unify raw + typed serialization rules; raw drops Validate + secret skip
- Replace CommandRunner positional args with CommandSpec struct
  (program, args, cwd, stdin, env)
- "Backwards-compatible" language replacing "unchanged" for default bin
- Move walkthrough doc to docs/guide/ with explicit sidebar update

Low + open questions:
- Document consumer-facing Cargo feature names and adapter opt-outs
- Generator migration note: sub-project 1 outputs don't auto-migrate
- Deprecate [stores.config.defaults] in favor of <name>.toml [config]
- Mark Spin provision / config push as "not yet supported" with pointer
  to the in-flight Spin stores PR; clear error message until then

Secret annotation:
- New §6.6 documenting #[derive(AppConfig)] from edgezero-macros
- #[secret] field attribute marks runtime-secret-store-backed fields
- Toml value for those fields is the secret-store binding name
- config validate (typed) cross-checks the binding appears in [stores.secrets]
- config push (typed) skips SECRET_FIELDS entirely

The implementation still ships in 7 incremental PRs.
…cope

Manifest schema rewrite (new sub-projects #2 and #3):
- [stores.<kind>].ids = [...] + default declare the logical stores the
  app uses (kv, secrets, config all multi-store)
- [adapters.<X>.stores.<kind>.<id>].name = "..." maps each logical id to
  the platform-specific name on adapter X, with optional adapter-specific
  tuning fields stored as free-form extras
- Provisioned platform resource IDs (Cloudflare namespace ID, Fastly
  store ID) live in each platform's native manifest (wrangler.toml,
  fastly.toml), not in edgezero.toml. provision writes them there;
  config push reads them back.
- RequestContext store accessors become id-keyed:
  ctx.kv_store("id") / ctx.kv_store_default() (and similarly for
  config_store / secret_store). Each adapter builds a StoreRegistry<H>
  at request setup from [adapters.<self>.stores.*].
- Manifest validator enforces: ids non-empty; default in ids;
  every adapter has a name mapping for every id.

Naming:
- Field on the per-adapter block is `name` (matches the user's example),
  not `binding`. The Cloudflare wrangler.toml term `binding` is now
  called out as wrangler's terminology, not ours.

Secret references (§6.7):
- The string a #[secret] field holds is an app-defined reference; the
  spec documents both valid runtime patterns (logical store id or key
  within the default secret store). Validate just confirms the string
  is non-empty and that the app has a secret store available.

config validate (§11) explicitly covers app-config validation:
- TOML syntax, [config] table presence, type matching against C,
  serde-rejected unknown fields, validator business rules, non-empty
  secret references, and the manifest-side cross-checks.

Sub-project count: 7 → 9 (added schema rewrite + RequestContext API
rewrite as #2 and #3; existing app-config/validate/auth/provision/push/
polish become #4-#9).

This is a breaking change to the on-disk manifest schema; the in-tree
example/app-demo is migrated as part of the work, and a migration guide
ships with sub-project #2.
…cret forms

HIGH severity fixes:
- Cloudflare config store rewritten from [vars] to KV (§6.9) so
  `config push` actually reaches the runtime without redeploying.
  Lands in sub-project #3 alongside the rest of the runtime work.
- Sub-project #2 is now purely additive on the schema: no runtime
  changes, no removal of [stores.config.defaults]. The runtime bridge
  and the defaults removal move out of #2 (into #3 and #9 respectively).
- Spin completeness: validator skips adapters without an
  [adapters.<X>.stores] section. App-demo's Spin adapter omits stores
  until the in-flight Spin stores PR lands.
- Extractor design (§6.8): existing Kv / Secrets extractors keep
  working as default-store accessors; new KvNamed<const ID> /
  SecretsNamed<const ID> extractors give type-safe named access. No
  handler-facing break.
- Hooks, ConfigStoreMetadata, and app! macro added to sub-project #3
  scope; they all become id-keyed. Multi-store rewrite is now complete.

MEDIUM severity fixes:
- Validate bound is DeserializeOwned + Validate + AppConfigMeta (no
  Serialize). The serde_json::to_value object check is push-only;
  push adds Serialize.
- Secret semantics: two explicit forms via attribute. #[secret] = key
  inside the default secret store. #[secret(store_ref)] = logical store
  id in [stores.secrets].ids. Validate cross-checks the latter.
- AppConfigMeta::SECRET_FIELDS is now &'static [SecretField] carrying
  SecretKind so the CLI can apply the right validation per field.
- #[secret] constrained to non-flattened, non-renamed scalar fields;
  combinations with #[serde(flatten)] / rename / skip produce compile
  errors. Macro tests cover the constraints.
- Unknown-field rejection is no longer a validate guarantee; the
  generator template emits #[serde(deny_unknown_fields)] on the
  generated config struct so new projects opt in by default.
- Every public *Args derives Default + #[non_exhaustive]; external
  construction documented as Default + field mutation.

LOW severity fixes:
- Macro example fixed: #[proc_macro_derive(AppConfig, attributes(
  secret))] in edgezero-macros/src/lib.rs directly. No bogus _impl
  re-export.
- Cloudflare-invalid JS-identifier `name` values are errors (would
  break worker deploy), not warnings.

Sub-project ordering and risk:
- #2 risk dropped to L (purely additive).
- #3 grows to absorb Cloudflare KV swap + Hooks/macro/extractor.
- #9 now also drops [stores.config.defaults] and wires axum dev-server
  to seed from <name>.toml.
HIGH severity fixes:
- ConfigStore::get becomes async (#[async_trait(?Send)]). Cloudflare
  config moves [vars] -> KV with real async reads. Cascade (trait, 3
  adapter impls, Hooks, handlers, extractors) contained to #3.
- Drop const-generic &'static str extractors (don't compile on stable
  1.95). Kv / Secrets extractors refactored to yield a registry handle
  with default() / named(id) accessors.
- Introduce BoundKvStore / BoundConfigStore / BoundSecretStore so
  runtime accessors return a handle bound to the resolved platform
  name; callers just .get(key).await.
- Sub-project #2 models logical store declarations as
  Option<LogicalStoreConfig> so old-shape manifests (None) are
  distinguishable from new-but-incomplete ones (Some with empty ids).
  Keeps #2 genuinely additive.

MEDIUM severity fixes:
- Fastly native-manifest writeback: spec commits to a read/write-path-
  agreement contract; exact fastly.toml sections pinned in #7's plan.
- Adapter store completeness uses an explicit
  STORES_SUPPORTED_ADAPTERS allowlist (axum, cloudflare, fastly). A
  supported adapter omitting [adapters.<X>.stores] is an error; only
  non-allowlisted adapters (spin) skip.
- All "default store" prose uses the resolved default id (explicit
  default, else single ids[0]).
- AuthArgs no longer derives Default (avoids a placeholder subcommand
  leaking into a real auth path). §6.11 documents which *Args get
  Default.
- config push gains explicit "validate passes, push serialization
  fails" test scenarios (non-object typed config, compound shapes,
  skip_serializing_if, Option::None, flatten).

LOW severity:
- Ship-gate wording: existing commands stay backwards-compatible
  rather than "edgezero --help unchanged" (false once auth/provision/
  config land).

New requirement - environment-variable override resolution (§6.10):
- load_app_config overlays env vars on the toml [config] table.
- Env var format: <APP_NAME>__<SECTION>__..__<KEY>; __ separates every
  nesting level; APP_NAME is [app].name uppercased, hyphens to
  underscores.
- Type coercion against the target TOML type; --no-env escape hatch on
  validate and push.

app-demo (§15) now explicitly exercises every new capability: multi-
store, async config, named-kv extractor, nested config section, env
override, both secret forms, validate/push, auth/provision via mock.
…n, Fastly contract

HIGH severity fixes:
- Manifest old-vs-new discrimination corrected. Existing manifests
  already have [stores.kv/secrets/config] tables, so table-presence
  can't discriminate. Sub-project #2 now uses compatibility structs
  carrying legacy fields (name, legacy adapters) plus new logical
  fields (ids, default) side by side; the discriminator is
  ids.is_some(). The current app-demo edgezero.toml parses unchanged.
- Hooks cannot return bound handles. Hooks / ConfigStoreMetadata are
  static compile-time app metadata; bound handles need per-request
  adapter state. Split: Hooks/app! emit store metadata registries;
  only RequestContext returns Bound*Store handles. Adapters consume
  the metadata at request setup to build the runtime registries.
- Env overlay type coercion: with C: DeserializeOwned there is no
  pre-deserialization type reflection. Env vars now override existing
  keys only, coerced to the existing TOML value's type. Matches the
  current AxumConfigStore::from_env behavior. To make a key
  env-overridable it must appear in <name>.toml.
- Axum config push and runtime read agreed: the axum config store is
  backed by .edgezero/local-config-<id>.json; config push --adapter
  axum writes that file; edgezero dev regenerates it at startup. No
  more disagreement between push target and dev-server source.

MEDIUM severity fixes:
- Fastly writeback contract made concrete from Fastly's docs:
  [setup.<kind>_stores.<name>] + [local_server.<kind>_stores.<name>]
  keyed by resource link name (== our `name`). provision creates the
  store and ensures both fastly.toml sections exist; config push
  resolves the store id on demand via `fastly config-store list
  --json` (Fastly has no stable persisted id slot). Read/write paths
  all key off [adapters.fastly.stores.<kind>.<id>].name.
- Env key matching is deterministic and ambiguity-rejecting: keys
  transform to an env segment form (uppercase); two siblings mapping
  to the same segment is an AppConfigError. No case-insensitive fuzzy
  fallback.
- Cloudflare KV eventual consistency: §6.9 no longer claims values are
  live "on the next request"; CI does not assert immediate global
  Cloudflare visibility.

LOW severity:
- BoundSecretStore keeps the existing bytes::Bytes API (get ->
  Option<Bytes>, require_str), not Vec<u8>.
…e-PR delivery

Hard cutoff (per user directive — projects fully migrated, no compat):
- Removed all old-vs-new manifest discrimination: no compat structs,
  no ids.is_some() check, no legacy-field parsing. The store schema is
  rewritten outright. Legacy fields (name, legacy adapters overrides,
  [stores.config.defaults]) are hard load errors pointing at the
  migration guide.

Spin as a first-class store-capable adapter (PR #253 baseline):
- Removed the "Spin deferred" non-goal. Spin participates fully.
- New §6.7 Spin store semantics: KV is label-backed multi-store with a
  max_list_keys cap; config and secrets are both spin_sdk::variables —
  a single flat namespace, lowercase [a-z0-9_] keys, no dots.
- Replaced the flat STORES_SUPPORTED_ADAPTERS allowlist with an
  adapter x kind capability matrix (Multi vs Single). Validation: if
  any target adapter is Single for a kind, [stores.<kind>].ids must
  have exactly one id (you cannot have two config stores if you also
  target Spin).
- §6.4 config key model: nested config flattens to dotted keys;
  canonical handler form is dotted; Spin config store translates
  . -> __ internally; config push writes platform-native key form.
- Spin wired into commit 2 (runtime registry, async ConfigStore now
  cascades across all FOUR adapters), commit 6 (provision: spin.toml
  writeback for key_value_stores / [variables] /
  [component.<name>.variables]), commit 7 (config push: Spin variables
  in spin.toml).
- provision now has explicit axum (no-op, prints local-store note) and
  spin (manifest writeback, no CommandRunner) contracts; config push
  is split per adapter — no universal native-resource-ID assumption.

Other review fixes:
- Default resolution made strict: `default` required when ids.len() > 1.
- Docs config path corrected to docs/.vitepress/config.mts (not .ts).

Delivery: one PR with eight commits (one per sub-project), not eight
PRs. CI gates the PR head; each commit should still build for
bisectability. Sub-project count stays at 8 (manifest+runtime stay
merged as the atomic commit 2).
aram356 added 29 commits June 21, 2026 20:14
Phase C of the blob app-config rewrite per
docs/superpowers/specs/2026-06-16-blob-app-config.md.

This is the atomic cutover commit per spec §10.1 — splitting any
piece below into a separate commit leaves an unbisectable
intermediate state (new runtime + old writer would fail because
no blob exists in the store, and the §10.2.1 grep gate would
fail in any commit that moves the extractor without migrating
app-demo handlers in the same commit).

- §3.3.3 AppConfig<C> extractor: envelope parse, SHA verify,
  secret walk per Model A (#[secret] / #[secret(store_ref)] /
  #[secret(store_ref = "field")]), serde_path_to_error
  deserialise, validate_excluding_secrets. Missing/corrupt
  blob → EdgeError::ConfigOutOfDate per Q3 (d).
- §8.2 config push rewrite: single envelope per [stores.config]
  key per adapter. `build_config_envelope` builds ONCE in CLI;
  adapter writers stay opaque-bytes. Inline diff via
  `similar::TextDiff` over `render_for_diff` (recursive-key-
  sort + serde_json::to_string_pretty). Consent gate per §8.2
  default and §8.3 Spin Cloud four-branch UX. Round-36/37/38
  re-fetch + concurrent-push detection. `run_config_push_typed`
  shrunk from 311 to 74 lines via 7 extracted helpers
  (handle_consent / push_info / read_remote / recheck_before_
  write / render_first_read_diff / resolve_push_paths /
  write_envelope).
- §10.2 app-demo migration: handlers switch to AppConfig<C>;
  hand-managed config_store_default()?.get(...) +
  secret_store.require_str(&cfg.<field>) paths removed per
  Model A's framework-resolved-secret rule.
- §10.2.2 scaffold templates migrate (Push + Validate only;
  Diff arm lands in Phase D): core/src/config.rs.hbs,
  core/src/handlers.rs.hbs, app/name.toml.hbs,
  root/edgezero.toml.hbs, root/README.md.hbs.
- §10.2.1 / §13.1 CI gates land together:
  - scripts/check_no_legacy_typed_reads.sh: greps for legacy
    typed reads + nested AppConfig<AppConfig<...>> shape.
  - scripts/check_no_placeholder_pins.sh: refuses unresolved
    placeholder hex in canonical_form_pins.rs.
  - crates/edgezero-cli/src/bin/check_no_nested_app_config.rs:
    syn-based AST audit (behind `nested-app-config-check`
    feature) catching nested AppConfig extractors.

.gitignore: narrow the blanket `bin/` ignore so legitimate
Cargo `src/bin/` directories aren't blocked — required to
land the new CI-gate binary.
Phase D of the blob app-config rewrite per spec §8.1.

- ConfigDiffArgs with --format (unified/structured/json),
  --local, --exit-code, --store, --key, --runtime-config,
  --no-env. clap parser tests in args.rs cover every flag.
- run_config_diff_typed wires through Phase C's read trait +
  envelope decode. --local flips read_config_entry to
  read_config_entry_local. --exit-code semantics per Q10:
  errors always non-zero regardless of the flag; the flag
  toggles 0 vs 1 only on the diff-present success branch.
- Three format renderers per spec §8.1.1-§8.1.3:
  - unified: re-uses the C4 print_unified_diff_inline helper
    (similar::TextDiff over render_for_diff with recursive
    key-sort + serde_json::to_string_pretty). No duplication.
  - structured: human-readable per-path block list via a
    local walk_leaves helper inside diff.rs.
  - json: machine-readable envelope { remote_sha, local_sha,
    changes: [...] } for jq consumers.
- DiffExit { code: i32 } typed exit-code outcome (round-31
  H-1) propagated through the scaffold template's
  main.rs.hbs so generated CLIs exit with the right code.
- 13 colocated tests in diff.rs cover skip-on-equal,
  missing-key, missing-store with provisioning hint,
  Unsupported, format dispatch, and --exit-code semantics
  for each outcome.
Fastly Config Store enforces an 8 000-character per-entry limit.
Entries at or below the limit are written unchanged; entries that
exceed it are split into UTF-8-safe 7 000-byte content-addressed
chunks with a JSON root pointer written last.

- chunked_config.rs: new module with prepare_fastly_config_entries
  (split + pointer build) and resolve_fastly_config_value (reassemble
  + integrity verify). Includes 13 unit tests.
- cli.rs: push_config_entries / push_config_entries_local expand
  logical entries through the helper before writing; read_config_entry
  / read_config_entry_local resolve chunk pointers on read.
  fetch_remote_config_store_entry added as a pull-side helper.
  Adds 12 integration tests covering direct, chunked, error, and
  local roundtrip paths.
- config_store.rs: FastlyConfigStore.get resolves chunk pointers via
  a new synchronous get_sync helper.
- Cargo.toml: serde, serde_json, and sha2 promoted to non-optional
  deps so chunked_config compiles in all feature combinations.
Phase E (post-cutover docs) of the blob app-config rewrite.

- docs/guide/blob-app-config-migration.md: operator-facing
  migration guide. Covers Model A (secret key NAMES at rest +
  framework-resolved secrets), the envelope + canonical-SHA
  contract, per-adapter writer mechanics (including Fastly
  chunked-config storage for oversized envelopes), the
  operator runbook (first push, per-environment KEY override
  via __KEY env var, drift detection in CI, orphan key
  cleanup recipes per adapter, Fastly chunk-pointer hygiene),
  and ConfigOutOfDate troubleshooting. Linked from the
  VitePress sidebar.
- scripts/smoke_test_config_key_override.sh: multi-adapter
  smoke covering spec §12.7 (KEY override), §9.3 (Fastly
  oversized chunk-pointer roundtrip), and §8.3 (Spin Cloud
  Unsupported diff + write-only push). Per-row SKIP_<ADAPTER>
  env vars + SKIP_SPIN_CLOUD_SMOKE for CI gating.
- crates/edgezero-cli/src/templates/root/README.md.hbs:
  scaffold README updated to describe the blob model + the
  new config diff usage with --exit-code for CI gates.
- README.md top-level: link to the migration guide.
- All .hbs templates: drop em-dashes and en-dashes in
  comments / doc-comments / Markdown -- generated projects
  ship ASCII-only punctuation per project convention.
CI's wasm32-wasip1 + --features fastly job (no cli) tripped
dead-code warnings on prepare_fastly_config_entries and the
constants only it uses (FASTLY_CONFIG_ENTRY_LIMIT,
CHUNK_PAYLOAD_TARGET, CHUNK_KEY_INFIX, find_utf8_boundary).
That target only needs the runtime resolver path.

Split the module by usage:

- Writer-side (prepare + find_utf8_boundary + the three
  writer-only constants) is gated `#[cfg(any(feature = "cli",
  test))]` so it compiles for the CLI binary and the in-tree
  unit tests but not for the runtime-only wasm build.
- Resolver-side (resolve_fastly_config_value + sha256_hex +
  POINTER_KIND + the pointer schema) stays unconditional --
  config_store.rs calls it under `#[cfg(feature = "fastly")]`.
Reviewer flagged four issues:

- Bash 4+ `${adapter^^}` and `${!skip_var}` failed on macOS's
  /usr/bin/env bash (3.2) with "bad substitution"; the script
  silently false-passed. Replaced with a portable `upper()` helper
  (tr-based) + `eval "skip_val=\${${skip_var}:-0}"` indirect.

- Axum row hardcoded PORT=8765 while app-demo's edgezero.toml binds
  to 8787, so the wait_for_port loop polled the wrong port and never
  saw the staging blob. Aligned PORT=8787 across all four rows.

- Fastly/Spin boot helpers ran viceroy / spin against
  target/...debug/*.wasm artifacts that a clean checkout doesn't
  have. Added an ensure_runtime_built() step that cargo-builds the
  required wasm target before the boot.

- §12.7 fastly row and §9.3 oversized smoke both rewrote
  examples/app-demo/crates/app-demo-adapter-fastly/fastly.toml in
  the checked-in tree. Added backup_in_tree() + cleanup-time
  restore so the smoke leaves the worktree clean. The other local-
  state directories (.wrangler/, .spin/, .edgezero/) are gitignored
  and need no backup.

Also reviewer-flagged: migration guide's Spin SQLite cleanup
example preserved only `app_config`, which would delete operator-
configured staging/canary blobs. Updated to mention every
`__KEY`-selectable key.
…ault

Two reviewer-flagged regressions in the §12.7 / §9.3 smoke:

- The curls were targeting /config/greeting (the legacy raw
  config-store handler at app-demo edgezero.toml:57), which
  always 404s on the blob model. The Phase E requirement is
  to prove the AppConfig<AppDemoConfig> extractor reads the
  right blob -- that's /config/typed (handlers.rs:185).
  Repointed both the per-adapter __KEY override assertions
  and the Fastly oversized chunk-pointer assertion.

- cleanup() did BOTH stop_server() + restore_backups(); the
  per-row teardown called it between the staging assertion
  and the default-blob reboot, so fastly.toml got restored
  before step 4 could read the default blob. Split into
  stop_server() (kill only) and restore_backups() (restore
  only); step 3 calls stop_server() to keep pushed state
  intact, step 4's end-of-row uses the combined cleanup().

Verified locally on Bash 3.2: all-skip mode prints SKIPPED
rows cleanly with 0 passed, 0 failed (no "bad substitution").
Three reviewer-flagged correctness issues + a comment-style sweep:

- config push --key was wiping sibling keys. Both Axum (cli.rs:236)
  and Fastly local (cli.rs:895) rewrote the whole config map on
  every push; pushing default then staging left only staging.
  Fixed both to upsert per-key (spec 12.7 requires default +
  staging to coexist for the runtime KEY override). Added
  push_config_entries_preserves_sibling_keys for Axum and
  push_config_entries_local_preserves_sibling_keys for Fastly.

- Smoke harness now seeds demo_api_token for the Axum row so the
  AppConfig<AppDemoConfig> extractor's secret walk resolves before
  the assertion fires; the stop_server helper SIGTERMs, waits up to
  5s, then SIGKILLs survivors and polls until the port is free so
  the next boot's wait_for_port doesn't race the previous socket
  close.

- Three new trybuild compile-fail fixtures pin the non-secret
  coverage spec 4.2/12.1 calls for: skip_serializing, skip_serializing_if,
  and flatten are banned on EVERY field, not just #[secret] ones.
  The macro already enforced this universally; the fixtures lock
  it in.

Plus a comment-style sweep across 36 source files:

- Strip plan-process tags (Step N, Task <X>N, Phase N, round-N,
  C4 Step 5 etc) from comments; spec-section refs stay.
- Strip the section sign (U+00A7) from comment + error-message
  strings. Trybuild .stderr fixtures regenerated to match.
- second_oversized_push_converges_runtime_on_new_envelope gets
  #[expect(clippy::too_many_lines)] -- splitting the linear push
  A -> inspect -> push B -> inspect -> read scenario obscures the
  chunk-set comparison.

All five workspace gates clean + all three Phase C CI gates clean +
docs prettier clean.
Two reviewer-flagged smoke-harness issues:

- stop_server waited for the port to free but didn't fail when it
  stayed live. On the prior run the first server kept serving
  staging into the default-blob assertion. stop_server now:
  * pkill -TERM the SERVER_PID + children, wait 5s
  * pkill -KILL stragglers
  * lsof -ti :PORT and kill -9 any process still bound
    (catches grand-children that pkill -P misses)
  * verify port is free within 10s; if not, log FAIL and
    return non-zero.
  Per-row callers now check stop_server's exit and skip to
  cleanup on failure (no silent default-key assertion against
  a stale runtime). boot_runtime also refuses to launch if the
  port is already bound -- prevents wait_for_port from racing
  the previous server's response.

- Only Axum was secret-seeded (env var). Cloudflare/Fastly/Spin
  required manual platform setup, so the all-adapter smoke
  failed in the secret walk before reaching the KEY override.
  New seed_secret_for_adapter helper writes per-adapter local
  secret state:
  * cloudflare: .dev.vars file with demo_api_token=resolved-token
    (gitignored). New per-row backup_in_tree call so cleanup
    restores the worktree.
  * fastly: appends [local_server.secret_stores.default] block
    to fastly.toml (caller's backup_in_tree already covers
    this).
  * spin: SPIN_VARIABLE_DEMO_API_TOKEN=resolved-token at boot.
  Fastly chunk-pointer smoke now seeds the same secret before
  the runtime read.
- Fix Task 1 compile blocker (EdgeError::internal needs Into<anyhow::Error>)
- Add missed Tier-1 gap: check_no_nested_app_config.rs syn helpers (Task 1b)
- Correct Fastly gating (feature-gated, not wasm32-gated)
- Replace fabricated raw_push_* test names with real typed_push_* suite
- Drop stale Body-no-Debug rationale (Body does impl Debug)
Three reviewer-flagged smoke issues on 699bdc9:

- The fastly.toml fixture's [local_server.secret_stores.default]
  is an array-of-tables (each entry exposes one key + the env
  var to read its value from). The prior seed appended a
  normal-table block at the same path; TOML rejects the mix.
  Switched to a [[local_server.secret_stores.default]] append
  with key = "demo_api_token" + env = "DEMO_API_TOKEN_SECRET".
  Viceroy boots with DEMO_API_TOKEN_SECRET=resolved-token. The
  whole edit is covered by the existing fastly.toml
  backup_in_tree.

- The Spin fixture's spin.toml declared `api_token` (the
  AppDemoConfig field name) but the runtime secret walk asks
  for the blob VALUE -- "demo_api_token". An awk patch now
  appends `demo_api_token = { required = true, secret = true }`
  to the [variables] block AND the matching
  demo_api_token = "{{ demo_api_token }}" line to the
  [component.app-demo.variables] map. spin.toml is added to
  the per-row backup_in_tree so cleanup restores it.

- The lsof port-cleanup log line used single quotes around
  '${PORT}', printing the literal text. Replaced with a
  printf %s + "$PORT" so the actual port number renders.

Verified both patched TOMLs round-trip through tomllib.
Also fix Task 3 run command in the spec to pass --features cli (the
cli_support module is cfg-gated on the cli feature).
Three reviewer-flagged issues:

- examples/app-demo/crates/app-demo-cli config_flow.rs imports
  edgezero_core::secret_store::InMemorySecretStore, which is
  gated behind the test-utils feature. CI runs
  `cd examples/app-demo && cargo test --workspace --all-targets`
  and was blocked at compile. Enabled
  features = ["test-utils"] on the edgezero-core dev-dep.

- examples/app-demo/Cargo.lock was stale after the Fastly
  chunked-config commit added serde + sha2 as non-optional
  deps to edgezero-adapter-fastly. Regenerated; --locked
  workspace test now passes.

- smoke_test_config_key_override.sh false-greened when an
  unskipped runtime row failed to boot. boot_runtime
  propagated the error, but the caller's `cleanup; continue`
  loop incremented neither PASS nor FAIL, so a sandbox bind
  failure printed `Results: 0 passed, 0 failed` and exited 0.
  Every unskipped row's boot/seed failure now bumps FAIL with
  an explicit `FAIL  <adapter> row: ...` log line. stop_server
  already accounted via its own port-still-live diagnostic;
  the per-row site just notes that.
- handler.rs: match -> let...else (manual_let_else)
- compression.rs: 0xFFu8 -> 0xFF_u8 (separated literal suffix)
- check_no_nested_app_config.rs: String::from + .as_deref() (str_to_string,
  single-char ident)
- sync the spec's Task snippets to the clippy-clean forms
Reviewer ran SKIP_SPIN_CLOUD_SMOKE=1 ./scripts/smoke_test_config_key_override.sh
on c64558a from a dev machine with prior .wrangler/state in
place. The Axum row passed but Cloudflare local row failed at:

    remote envelope parse failed: expected value at line 1 column 1

The CLI is correct to reject it: the cutover spec hard-fails
when read-back finds a non-BlobEnvelope value at the target
key. The smoke harness was wrong to leave gitignored emulator
state in place; the new push reads the existing value (per
read-back skip-on-equal + diff) and the stale data trips the
parse.

Per-row reset of the local-state directory before the push:
- axum:       rm -rf examples/app-demo/.edgezero
- cloudflare: rm -rf .../.wrangler
- spin:       rm -rf .../.spin
- fastly:     no-op (fastly.toml IS the local store, already
              backup_in_tree'd)

All four directories are gitignored and regenerated by the push.
Worktree stays clean (backup_in_tree of fastly.toml/.dev.vars/
spin.toml restores the tracked-fixture mutations on cleanup).
# Conflicts:
#	docs/superpowers/specs/2026-06-16-test-coverage-gap.md
Wrangler 4.x (verified 4.64.0) returns exit 0 + stdout
"Value not found" for a missing key instead of exit 1 + stderr.
The previous read path treated every exit-0 stdout as a
Present envelope, which made the next CLI step try to parse
"Value not found" as a BlobEnvelope and abort with:

    remote envelope parse failed: expected value at line 1
    column 1

A missing key in the blob model is valid initial state (the
first push hasn't run yet), not corrupt remote state, so it
must map to ReadConfigEntry::MissingKey.

Detect by trimming the success-branch stdout and matching
'value not found' / 'value not found.' case-insensitively
before returning Present. Adds a fake-Wrangler regression
test that pins the exit-0 stdout shape verbatim.

Also: stale Fastly comment said entries arrive pre-flattened
(per-leaf model). Reworded to describe the blob-envelope
shape and the chunked_config expansion path.
- Cloudflare: env_config_from_worker only queried __NAME for
  each store id; the runtime never saw __KEY, so a staging
  push at app_config_staging silently fell back to the
  default blob. Spec 5.4 routes the runtime extractor via
  EDGEZERO__STORES__CONFIG__<ID>__KEY. Added a __KEY query
  for CONFIG ids alongside the existing __NAME (KV / SECRETS
  bindings have no per-id key override and are unchanged).
  Smoke boot for Cloudflare now writes both the secret and
  the per-row __KEY env override into .dev.vars so
  `wrangler dev` surfaces them to env.var(...).
- Spin: the fixture declared `api_token = { required = true,
  secret = true }`, but the blob's secret-key NAME is
  `demo_api_token`. spin up failed before serving because
  the required `api_token` had no provider. Smoke awk patch
  now ALSO downgrades the legacy `api_token` line to
  `{ default = "", secret = true }` while inserting the new
  `demo_api_token` declaration; the obsolete field can
  remain unset without blocking startup.
- Fastly: viceroy 0.17.0 uses `serve` for the long-running
  HTTP path (`run` was renamed). Smoke boot updated.
- Cloudflare cli.rs: dropped "pre-flattened/dotted form"
  wording in two doc comments; the writer now handles one
  logical blob-envelope (key, envelope_json) per push.
- Fastly smoke booted from the wrong wasm path: cargo writes
  examples/app-demo/target/wasm32-wasip1/debug/app-demo-adapter-fastly.wasm
  (workspace target + hyphenated name), but the smoke ran from
  the adapter crate dir and reached for
  target/wasm32-wasip1/debug/app_demo_adapter_fastly.wasm.
  Smoke now serves the absolute workspace-target path with the
  correct hyphenated filename.

- Tracked Spin fixture had the wrong variable name. The
  AppDemoConfig field `api_token` HOLDS the secret-store key
  NAME (`demo_api_token`, per Model A); the runtime secret
  walk asks Spin for `demo_api_token`, not `api_token`. The
  fixture declared a required `api_token` variable that
  blocked `spin up` before the row could even fail meaning-
  fully. Renamed the [variables] entry and the matching
  [component.app-demo.variables] mapping to `demo_api_token`
  with default = "" so `spin up` starts without a configured
  provider. Smoke drops its awk patch and spin.toml backup-
  restore (no longer mutating the fixture).

- spin-sdk ~6 imports wasi:http/types@0.3.0-rc-2026-03-15
  which Spin < 3.7 doesn't provide. Smoke's spin row now
  pre-checks `spin --version` and SKIPs with a clear note
  if CLI is missing or older than 3.7. Documented the
  requirement in .tool-versions (asdf has no widespread
  spin plugin so the version isn't pinned there).
Fastly Compute@Edge has no process env -- `EnvConfig::from_env()`
reads `std::env::vars()`, which returns empty inside the wasm
guest. That meant
EDGEZERO__STORES__CONFIG__APP_CONFIG__KEY=app_config_staging
was silently dropped by the runtime, and the spec 12.7 staging
override fell back to the binding's default id.

Mirror what the Cloudflare adapter does: open a dedicated
`edgezero_runtime_env` Config Store, probe for the known
EDGEZERO__* keys derived from `StoresMetadata`, and feed
those into `EnvConfig::from_vars`. Missing-store is a no-op
(empty env, same as the pre-fix default behaviour).

Smoke now seeds the local viceroy copy of that store with the
per-row `__KEY` value via a `seed_fastly_runtime_env` python
helper that idempotently rewrites the
`[local_server.config_stores.edgezero_runtime_env]` block in
fastly.toml (already covered by the existing backup_in_tree
cleanup).

Operators deploying to remote Fastly create the matching
Config Store with `fastly config-store create --name=edgezero_runtime_env`
and `config-store-entry update --upsert` per env-var key.
Reviewer flagged that the runtime store the previous commit
introduced was never provisioned and the docs still told
operators to set EDGEZERO__* as a shell env var.

- `edgezero provision --adapter fastly` now creates the
  `edgezero_runtime_env` Fastly Config Store alongside the
  declared app stores. Skips if the
  `[setup.config_stores.edgezero_runtime_env]` block is
  already present in fastly.toml. Output line nudges the
  operator with the exact `config-store-entry update --upsert`
  invocation for the staging key.
- The runtime path in env_config_from_runtime_dictionary
  now logs a one-shot warning when the store is missing
  (instead of silently falling back to baked defaults), so
  operators see the gap in Fastly logs and can run provision.
- README.md.hbs scaffold mentions the per-adapter override
  mechanism explicitly: Axum=process env, Cloudflare=worker
  vars, Spin=application variables, Fastly=Config Store
  created by provision.
- Migration guide gets a per-adapter table for where to set
  __KEY, plus a Fastly-specific block walking through the
  config-store-entry update flow + the runtime warning
  behaviour when the store isn't provisioned.

Three provision unit tests updated for the new line / skip
fixture; provision_dry_run_does_not_invoke_fastly expects
4 lines (now includes the runtime-env row);
provision_with_no_declared_stores_says_so and
provision_skips_id_when_setup_block_already_present
pre-populate `[setup.config_stores.edgezero_runtime_env]`
so the new step skips and the tests don't shell out to
real `fastly`.
The declared-store provisioning path emits a remediation note
when fastly.toml carries `service_id` -- the next `compute
deploy` won't re-apply `[setup]`, so a freshly-created store
needs a `fastly resource-link create` to attach to the live
service. The runtime-env branch I added in the previous commit
created/appended the setup block but skipped that same check,
leaving operators with a runtime warning ("`edgezero_runtime_env`
not found") even after running provision against an
already-deployed service.

Factored the post-create note into a `resource_link_note`
helper and called it from both the declared-store loop and
the runtime-env branch. The runtime-env line now carries
BOTH the populate-keys hint AND (when service_id is set) the
resource-link command.

Two regression tests on the helper directly:
- provision_emits_resource_link_note_for_runtime_env_on_existing_service:
  service_id set => note quotes the id, the store-id lookup
  command, the resource-link command with `--name=edgezero_runtime_env`.
- provision_skips_resource_link_note_when_service_undeployed:
  no service_id => returns None so the next `compute deploy`'s
  `[setup]` pass handles it without a stale-prompt false positive.
…_tokens)

- registry.rs: default_validation_and_kind_methods_are_noops exercises the
  Adapter trait no-op defaults via the existing FIRST TestAdapter
- app.rs: build_route_tokens_propagates_invalid_handler_path covers the
  route-builder error path (file-level expand_app diagnostics left
  uncovered — their messages embed absolute paths, flaky for trybuild)
- spec: mark Task 5 / Tier-2 backlog resolved, tick acceptance-gate items
Base automatically changed from feature/extensible-cli to main June 29, 2026 21:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant