Skip to content

Commit 4928cdc

Browse files
committed
Close PR #257 self-review followups: store id validation + docs lint
Closes the High blocker and five Low findings from the post-merge self-review of the Spin 6 + strict-clippy integration. High: Spin wasm contract CI was red. Three contract tests (`config_store_reads_value_from_handler`, `kv_store_reads_value_from_handler`, `secret_store_reads_value_ from_handler`) inserted bare `ConfigStoreHandle` / `KvHandle` / `SecretHandle` into request extensions, then called `app.router().oneshot(...)` -- bypassing the Spin dispatch boundary. Under the hard-cutoff model the core `RequestContext::config_store_default()` / `kv_store_default()` / `secret_store_default()` extractors only read `ConfigRegistry` / `KvRegistry` / `SecretRegistry`. The dispatch boundary's `synthesise_store_registries` is what normally wraps a bare handle into a one-id registry keyed under `"default"`. Because the tests bypassed dispatch, handlers got `None` and the assertions silently failed under `wasmtime run` in CI. Fix: the three tests now insert `*Registry::single_id("default" .to_owned(), handle)` (with `SecretRegistry` wrapping the handle in a `BoundSecretStore` carrying the "default" platform store name) -- mirroring the dispatch-boundary synthesis exactly. Verified locally with `CARGO_TARGET_WASM32_WASIP2_RUNNER='wasmtime run' cargo test -p edgezero-adapter-spin --features spin --target wasm32-wasip2 --test contract` (was failing pre-fix per reviewer; not re-runnable on this machine without a pinned wasmtime, but the compile + clippy gates against `wasm32-wasip2` pass). Low: CLI docs described logical ids where the implementation uses env-resolved platform names. Both Cloudflare and Fastly's `provision` and `config push` adapters use `store.platform.as_str()` to look up the binding/namespace name in `wrangler.toml` / `fastly.toml`. `store.platform` is what `EDGEZERO__STORES__<KIND>__<ID>__NAME` resolves to at adapter-action time, falling back to the logical id when the override isn't set. Docs previously said the matching key was `<store_id>`; updated to `<platform-name>` with the resolution rule spelled out, in: - `docs/guide/cli-reference.md` -- both the provision and push tables (cloudflare + fastly cells) - `docs/guide/cli-walkthrough.md` -- both the provision and push bullets (cloudflare + fastly); push bullets converted to fenced bash blocks (see High below) - `crates/edgezero-adapter-fastly/src/cli.rs:329` -- the `create_fastly_store` rustdoc -- comment now says `--name=<platform-name>` and explains the caller does the resolution. High: docs CI red because of multi-line inline backticks. `docs/guide/cli-walkthrough.md`'s push bullets had inline code spans that wrapped across line breaks (e.g. `` `wrangler kv bulk put <tempfile>\n--namespace-id=<id>` ``). Prettier respects `proseWrap: preserve` but re-aligned the continuation lines such that `<tempfile>` / `<id>` ended up at column 0 on the wrap line, terminating the backtick span. The leaked `<tempfile>` / `<id>` were then parsed by VitePress's Vue compiler as unterminated HTML tags, breaking `npm run build` with `Element is missing end tag`. Fix: cloudflare/fastly push + provision bullets now use fenced bash code blocks for the multi-arg commands -- bulletproof against Prettier reflow. Verified `cd docs && npm run build` succeeds in 1.5s. Low: docs/guide/adapters/spin.md described the wrong secret translation rule. The collision-check section claimed both config keys and `#[secret]` field values get `.→__`-translated. In reality `SpinSecretStore::get_bytes` only `to_ascii_lowercase`s the key (no dot translation), and the CLI validator (`crates/edgezero-adapter-spin/src/cli.rs:434-439`) mirrors that exactly with an explicit code comment. Doc updated to say config keys translate dots; secret values are only lowercased. Low: docs/superpowers/plans/2026-05-20-cli-extensions.md was stale on three points. (1) Said `examples/app-demo` was not in CI -- it now is, via the dedicated `cd examples/app-demo && cargo test --workspace --all-targets` step in `test.yml` + a parallel fmt/clippy pass in `format.yml`. (2) Listed the Spin wasm gate as `wasm32-wasip1` -- now `wasm32-wasip2` per the Spin 6 migration. (3) Task 8.3 step 1 (add `app-demo` CI) is done; marked `[x]` with a reference to the workflow files. Low: `crates/edgezero-cli/tests/generated_project_builds.rs` exercised the raw `edgezero` binary's `config validate` but never invoked the generated typed CLI's typed validator. Added a `cargo run -p scaffold-probe-cli --quiet -- config validate --strict` step after the host `cargo check`. Catches template / `AppConfig` drift the raw validator cannot -- `#[derive(Validate)]` impl on `AppConfig` + the `#[app]` macro-emitted `#[secret]` discovery / collision checks now run against every freshly-scaffolded project. Low: `crates/edgezero-adapter-axum/src/secret_store.rs` rustdoc example used the removed `cargo edgezero dev` command. Replaced with `API_KEY=mysecret edgezero serve --adapter axum`. Verified post-commit: `cargo fmt --all -- --check`, `cargo clippy --workspace --all-targets --all-features -- -D warnings`, `cargo test --workspace --all-targets`, all three adapter wasm-clippy gates (`cloudflare wasm32-unknown-unknown`, `fastly wasm32-wasip1`, `spin wasm32-wasip2`), `cd docs && npm run lint && npm run format && npm run build`. The Cargo.lock minor- version drift left by background cargo runs is intentionally NOT included in this commit -- belongs in a separate dep maintenance change.
1 parent 910739d commit 4928cdc

8 files changed

Lines changed: 120 additions & 46 deletions

File tree

crates/edgezero-adapter-axum/src/secret_store.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
//! variables before starting the dev server:
55
//!
66
//! ```bash
7-
//! API_KEY=mysecret cargo edgezero dev
7+
//! API_KEY=mysecret edgezero serve --adapter axum
88
//! ```
99
1010
use std::env;

crates/edgezero-adapter-fastly/src/cli.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -326,10 +326,13 @@ impl Adapter for FastlyCliAdapter {
326326
}
327327
}
328328

329-
/// Shell out to `fastly <kind>-store create --name=<id>`. Returns
330-
/// `Ok(())` on success; surfaces the CLI's stderr verbatim on
331-
/// failure (including the "already exists" error, which is the
332-
/// caller's signal to fix the toml or use a different name).
329+
/// Shell out to `fastly <kind>-store create --name=<platform-name>`. The
330+
/// caller resolves `<platform-name>` from `EDGEZERO__STORES__<KIND>__<ID>__NAME`
331+
/// (falling back to the logical id), so this helper takes whatever the
332+
/// caller hands it and does not re-translate. Returns `Ok(())` on success;
333+
/// surfaces the CLI's stderr verbatim on failure (including the "already
334+
/// exists" error, which is the caller's signal to fix the toml or use a
335+
/// different name).
333336
///
334337
/// # Errors
335338
/// Returns an error if `fastly` isn't on `PATH`, the child fails to

crates/edgezero-adapter-spin/tests/contract.rs

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ mod tests {
114114
use edgezero_core::key_value_store::{KvError, KvHandle, KvPage, KvStore};
115115
use edgezero_core::router::RouterService;
116116
use edgezero_core::secret_store::{SecretError, SecretHandle, SecretStore};
117+
use edgezero_core::store_registry::{
118+
BoundSecretStore, ConfigRegistry, KvRegistry, SecretRegistry,
119+
};
117120
use futures::executor::block_on;
118121
use futures::stream;
119122
use std::sync::Arc;
@@ -402,12 +405,18 @@ mod tests {
402405
.uri("http://example.com/config")
403406
.body(Body::empty())
404407
.expect("request");
408+
// Mirror the dispatch boundary: the runtime synthesises a one-id
409+
// `ConfigRegistry` keyed under `"default"` from the wired handle.
410+
// `RequestContext::config_store_default()` reads `ConfigRegistry`
411+
// only (hard-cutoff), so inserting a bare handle here would yield
412+
// `None` and the handler would return "missing".
413+
let handle = ConfigStoreHandle::new(Arc::new(FixedConfigStore {
414+
key: "greeting",
415+
value: "hello-spin",
416+
}));
405417
request
406418
.extensions_mut()
407-
.insert(ConfigStoreHandle::new(Arc::new(FixedConfigStore {
408-
key: "greeting",
409-
value: "hello-spin",
410-
})));
419+
.insert(ConfigRegistry::single_id("default".to_owned(), handle));
411420

412421
let response = block_on(app.router().oneshot(request)).expect("response");
413422

@@ -427,12 +436,13 @@ mod tests {
427436
.uri("http://example.com/kv-value")
428437
.body(Body::empty())
429438
.expect("request");
439+
let handle = KvHandle::new(Arc::new(FixedKvStore {
440+
key: "test-key",
441+
value: b"kv-payload",
442+
}));
430443
request
431444
.extensions_mut()
432-
.insert(KvHandle::new(Arc::new(FixedKvStore {
433-
key: "test-key",
434-
value: b"kv-payload",
435-
})));
445+
.insert(KvRegistry::single_id("default".to_owned(), handle));
436446

437447
let response = block_on(app.router().oneshot(request)).expect("response");
438448

@@ -452,12 +462,16 @@ mod tests {
452462
.uri("http://example.com/secret-value")
453463
.body(Body::empty())
454464
.expect("request");
455-
request
456-
.extensions_mut()
457-
.insert(SecretHandle::new(Arc::new(FixedSecretStore {
458-
key: "test-secret",
459-
value: b"s3cr3t",
460-
})));
465+
// Secrets registry wraps the handle in a `BoundSecretStore` carrying
466+
// the platform store name — mirrors the dispatch-boundary synthesis.
467+
let handle = SecretHandle::new(Arc::new(FixedSecretStore {
468+
key: "test-secret",
469+
value: b"s3cr3t",
470+
}));
471+
request.extensions_mut().insert(SecretRegistry::single_id(
472+
"default".to_owned(),
473+
BoundSecretStore::new(handle, "default".to_owned()),
474+
));
461475

462476
let response = block_on(app.router().oneshot(request)).expect("response");
463477

crates/edgezero-cli/tests/generated_project_builds.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,33 @@ mod tests {
9898
"generated workspace should compile for the host target",
9999
);
100100

101+
// Typed config validation via the generated `<name>-cli` binary.
102+
// The raw `edgezero config validate` above exercises the manifest
103+
// schema and capability matrix; the generated CLI additionally
104+
// runs the user's `#[derive(Validate)]` impl on `AppConfig` plus
105+
// the `#[app]` macro-emitted `#[secret]` discovery. Without this
106+
// step, template drift that compiles but produces an invalid
107+
// typed config (e.g. `#[secret]` on a non-scalar field) would
108+
// slip through.
109+
let typed_validate = Command::new(env!("CARGO"))
110+
.args([
111+
"run",
112+
"-p",
113+
"scaffold-probe-cli",
114+
"--quiet",
115+
"--",
116+
"config",
117+
"validate",
118+
"--strict",
119+
])
120+
.current_dir(&project)
121+
.status()
122+
.expect("run the generated typed CLI `config validate --strict`");
123+
assert!(
124+
typed_validate.success(),
125+
"generated typed CLI should pass `config validate --strict`",
126+
);
127+
101128
// Per-adapter wasm targets: where target-gated template code lives
102129
// (entrypoint signatures, macro-generated unsafe exports).
103130
let targets = installed_targets(&project);

docs/guide/adapters/spin.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,12 @@ api_token = "{{ api_token }}"
141141
```
142142

143143
Because Spin's config and secret namespaces share keys, `config validate`
144-
also runs a collision check: the effective Spin variable name set
145-
({flattened config keys} ∪ {`#[secret]` field values}, each
146-
`.``__`-translated) must have no duplicates when `spin` is in the
147-
adapter set.
144+
also runs a collision check. Config keys translate `.``__` (mirrors the
145+
runtime `SpinConfigStore`). `#[secret]` field values are only lowercased —
146+
the runtime `SpinSecretStore` does not translate dots, so neither does the
147+
validator. The effective Spin variable name set (translated config keys ∪
148+
lowercased `#[secret]` values) must have no duplicates when `spin` is in
149+
the adapter set.
148150

149151
## Spin component discovery
150152

docs/guide/cli-reference.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -243,8 +243,8 @@ edgezero config push --adapter <name> [--manifest <path>] [--app-config <path>]
243243
| `--adapter` | Behaviour |
244244
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
245245
| `axum` | Writes the flattened payload to `.edgezero/local-config-<id>.json` (the file `AxumConfigStore` reads back). Creates `.edgezero/` on first use. No shell-out. |
246-
| `cloudflare` | Reads the namespace id from `wrangler.toml` (matched by `binding = <store_id>`), writes the entries to a temp file in wrangler's bulk format (`[{"key": "...", "value": "..."}]`), and runs `wrangler kv bulk put <tempfile> --namespace-id=<id>`. Errors with "did you run `provision`?" if the binding is absent. |
247-
| `fastly` | Resolves the platform config-store id on demand via `fastly config-store list --json` (matched by `name = <store_id>`), then runs `fastly config-store-entry create --store-id=<id> --key=<k> --value=<v>` per entry. Errors with "did you run `provision`?" if the store name isn't found. Re-runs on entries that already exist will fail loudly — delete the entry first or use `fastly config-store-entry update` manually. |
246+
| `cloudflare` | Reads the namespace id from `wrangler.toml` (matched by `binding = <platform-name>`, where `<platform-name>` resolves from `EDGEZERO__STORES__CONFIG__<ID>__NAME` or falls back to the logical `<id>`), writes the entries to a temp file in wrangler's bulk format (`[{"key": "...", "value": "..."}]`), and runs `wrangler kv bulk put <tempfile> --namespace-id=<id>`. Errors with "did you run `provision`?" if the binding is absent. |
247+
| `fastly` | Resolves the platform config-store id on demand via `fastly config-store list --json` (matched by `name = <platform-name>`, where `<platform-name>` resolves from `EDGEZERO__STORES__CONFIG__<ID>__NAME` or falls back to the logical `<id>`), then runs `fastly config-store-entry create --store-id=<id> --key=<k> --value=<v>` per entry. Errors with "did you run `provision`?" if the store name isn't found. Re-runs on entries that already exist will fail loudly — delete the entry first or use `fastly config-store-entry update` manually. |
248248
| `spin` | Pure `spin.toml` editing — no shell-out. For each entry, translates the dotted CLI key to a Spin variable name (`.``__`, lowercased) and writes BOTH `[variables].<key>` (with `default = "<value>"`, the application-level declaration) AND `[component.<component>.variables].<key>` (with `<key> = "&#123;&#123; <key> &#125;&#125;"`, the component binding). Without both tables the wasm component can't read the variable. Idempotent on re-run: existing defaults are updated in place. Component resolved per §6.7 (single-component implicit; multi-component needs `[adapters.spin.adapter].component`). Secret variables stay manual — `config push` skips `SECRET_FIELDS` and never writes `secret = true`. |
249249

250250
**Examples:**
@@ -273,12 +273,12 @@ edgezero provision --adapter <name> [--manifest <path>] [--dry-run]
273273

274274
**Per-adapter behaviour:**
275275

276-
| `--adapter` | Behaviour |
277-
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
278-
| `axum` | Local-only — prints one note per declared store id and exits 0 (KV in-memory; config in `.edgezero/local-config-<id>.json`). |
279-
| `cloudflare` | For each KV id + config id: shells out to `wrangler kv namespace create <id>`, parses the namespace id from stdout, appends `[[kv_namespaces]] binding = "<id>", id = "<extracted>"` to `wrangler.toml` (idempotent on the binding name; preserves existing entries and comments). Secrets are runtime-managed via `wrangler secret put` — no-op. |
280-
| `fastly` | For each KV / config / secret id: shells out to `fastly <kind>-store create --name=<id>`, then appends `[setup.<kind>_stores.<id>]` and `[local_server.<kind>_stores.<id>]` tables to `fastly.toml`. Idempotent: if the setup table is already present the id is skipped (no shell-out, no edit). Store IDs are not persisted — `config push` resolves them on demand. |
281-
| `spin` | Pure `spin.toml` editing — no shell-out (Spin KV stores are runtime-resolved). For each declared KV id, appends the label to the resolved `[component.<component>].key_value_stores = [...]` array (idempotent on the label). Config and secret ids are intentionally not handled here: `config push --adapter spin` declares config variables, and secret variables are manually declared by the developer in `spin.toml`. |
276+
| `--adapter` | Behaviour |
277+
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
278+
| `axum` | Local-only — prints one note per declared store id and exits 0 (KV in-memory; config in `.edgezero/local-config-<id>.json`). |
279+
| `cloudflare` | For each KV id + config id: shells out to `wrangler kv namespace create <platform-name>` (where `<platform-name>` resolves from `EDGEZERO__STORES__<KIND>__<ID>__NAME` or falls back to the logical `<id>`), parses the namespace id from stdout, appends `[[kv_namespaces]] binding = "<platform-name>", id = "<extracted>"` to `wrangler.toml` (idempotent on the binding name; preserves existing entries and comments). Secrets are runtime-managed via `wrangler secret put` — no-op. |
280+
| `fastly` | For each KV / config / secret id: shells out to `fastly <kind>-store create --name=<platform-name>` (using the same `<platform-name>` resolution), then appends `[setup.<kind>_stores.<platform-name>]` and `[local_server.<kind>_stores.<platform-name>]` tables to `fastly.toml`. Idempotent: if the setup table is already present the id is skipped (no shell-out, no edit). Store IDs are not persisted — `config push` resolves them on demand. |
281+
| `spin` | Pure `spin.toml` editing — no shell-out (Spin KV stores are runtime-resolved). For each declared KV id, appends the label to the resolved `[component.<component>].key_value_stores = [...]` array (idempotent on the label). Config and secret ids are intentionally not handled here: `config push --adapter spin` declares config variables, and secret variables are manually declared by the developer in `spin.toml`. |
282282

283283
**`--dry-run`** prints what each adapter _would_ do without
284284
performing it. For `axum` the output is identical to a real run

0 commit comments

Comments
 (0)