From bc2538b5b54000509dab7e07c36d6ca50b4fd6c9 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Mon, 13 Jul 2026 19:50:02 +0200 Subject: [PATCH] fix(cli): reject unknown fields in manifest entry specs (fail-loud on typos) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ManifestScript`, `ManifestRoute`, and every trigger spec (`KvTriggerSpec`…`QueueTriggerSpec`) lacked `#[serde(deny_unknown_fields)]`, while their parent structs all had it. So a typo inside a `[[triggers.kv]]` / `[[routes]]` / `[[scripts]]` entry (e.g. `collection` for `collection_glob`, `methid` for `method`) was silently dropped instead of erroring — the manifest applied with the mistyped directive missing. Add the attribute to the 9 entry structs (all fields are exhaustive, so `pull`-emitted TOML still round-trips — verified against the full journey suite, 151/151). Pinned by a new `unknown_key_in_an_entry_spec_is_rejected` unit test. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/picloud-cli/src/manifest.rs | 34 ++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/crates/picloud-cli/src/manifest.rs b/crates/picloud-cli/src/manifest.rs index 43ae035..aab8669 100644 --- a/crates/picloud-cli/src/manifest.rs +++ b/crates/picloud-cli/src/manifest.rs @@ -372,6 +372,7 @@ impl CollectionDecl { } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct ManifestScript { pub name: String, /// Path to the `.rhai` source, relative to the manifest's directory. @@ -397,6 +398,7 @@ pub struct ManifestScript { } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct ManifestRoute { /// Name of the script this route binds to. pub script: String, @@ -459,6 +461,7 @@ impl ManifestTriggers { } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct KvTriggerSpec { pub script: String, pub collection_glob: String, @@ -479,6 +482,7 @@ pub struct KvTriggerSpec { } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct DocsTriggerSpec { pub script: String, pub collection_glob: String, @@ -497,6 +501,7 @@ pub struct DocsTriggerSpec { } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct FilesTriggerSpec { pub script: String, pub collection_glob: String, @@ -515,6 +520,7 @@ pub struct FilesTriggerSpec { } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct CronTriggerSpec { pub script: String, /// 6-field cron expression (with seconds). @@ -528,6 +534,7 @@ pub struct CronTriggerSpec { } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct PubsubTriggerSpec { pub script: String, pub topic_pattern: String, @@ -546,6 +553,7 @@ pub struct PubsubTriggerSpec { } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct EmailTriggerSpec { pub script: String, /// Name of the secret (set via `pic secret set`) holding the inbound @@ -558,6 +566,7 @@ pub struct EmailTriggerSpec { } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct QueueTriggerSpec { pub script: String, pub queue_name: String, @@ -1080,6 +1089,31 @@ mod tests { assert_eq!(m.project.unwrap().parent_group.as_deref(), Some("acme")); } + #[test] + fn unknown_key_in_an_entry_spec_is_rejected() { + // Entry specs (scripts/routes/triggers) are `deny_unknown_fields`, so a + // typo inside a `[[…]]` entry is a hard error, not silently dropped. + // A misspelled trigger field (`collection` for `collection_glob`): + Manifest::parse( + "[app]\nslug = \"a\"\nname = \"A\"\n\n\ + [[triggers.kv]]\nscript = \"h\"\ncollection = \"c\"\n", + ) + .expect_err("a misspelled [[triggers.kv]] field must be rejected"); + // A misspelled route field: + Manifest::parse( + "[app]\nslug = \"a\"\nname = \"A\"\n\n\ + [[routes]]\nscript = \"h\"\nhost_kind = \"any\"\npath_kind = \"exact\"\n\ + path = \"/x\"\nmethid = \"GET\"\n", + ) + .expect_err("a misspelled [[routes]] field must be rejected"); + // The correctly-spelled form still parses. + Manifest::parse( + "[app]\nslug = \"a\"\nname = \"A\"\n\n\ + [[triggers.kv]]\nscript = \"h\"\ncollection_glob = \"c\"\n", + ) + .expect("a correct [[triggers.kv]] entry parses"); + } + #[test] fn overlay_vars_override_base_per_key() { let mut base = sample();