feat(enabled): scripts/routes enabled column + declarative data path
Phase-1 `enabled` three-state lifecycle (§4.3), data-model half. Triggers already carried `enabled`; this adds it to scripts and routes and threads it through the declarative project tool. Runtime honoring (disabled route 404 / script non-invocable / dispatcher fire-time re-check) is the next commit — this change only stores and reconciles the flag. - Migration 0045: `enabled BOOLEAN NOT NULL DEFAULT TRUE` on scripts + routes. Default true ⇒ no behavior change on migrate. - `Script`/`Route` (shared) gain `enabled` (serde default true via the new `picloud_shared::default_true`); repos' SELECT/INSERT/UPDATE SQL, row structs, `NewScript`/`NewRoute`/`ScriptPatch` all carry it. - Apply diff treats `enabled` as a declarative field (omitted ⇒ active): `script_update_reason` + `diff_routes` detect a toggle as an Update, and the create/update/insert paths persist it. The bound-plan `state_token` re-includes script/route `enabled` (removed in the earlier review fix precisely because the diff didn't key on it yet — now it does). - CLI manifest model + `build_bundle` + `pull` round-trip `enabled` (serialized only when false; omitted ⇒ active). `pic init` scaffold and the interactive script/route create paths default active. Tested: manager-core lib 365 (incl. enabled diff + token sensitivity) + cli bins 31 (incl. manifest skip-serialize) green; clippy -D warnings clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -122,6 +122,7 @@ fn scaffold_manifest(slug: &str, name: &str) -> Manifest {
|
||||
timeout_seconds: None,
|
||||
memory_limit_mb: None,
|
||||
sandbox: None,
|
||||
enabled: true,
|
||||
}],
|
||||
routes: vec![ManifestRoute {
|
||||
script: "hello".into(),
|
||||
@@ -132,6 +133,7 @@ fn scaffold_manifest(slug: &str, name: &str) -> Manifest {
|
||||
path_kind: PathKind::Exact,
|
||||
path: "/hello".into(),
|
||||
dispatch_mode: DispatchMode::Sync,
|
||||
enabled: true,
|
||||
}],
|
||||
triggers: crate::manifest::ManifestTriggers::default(),
|
||||
secrets: crate::manifest::ManifestSecrets::default(),
|
||||
|
||||
@@ -61,6 +61,7 @@ pub fn build_bundle(manifest: &Manifest, base_dir: &Path) -> Result<Value> {
|
||||
if let Some(sb) = &s.sandbox {
|
||||
obj.insert("sandbox".into(), serde_json::to_value(sb)?);
|
||||
}
|
||||
obj.insert("enabled".into(), json!(s.enabled));
|
||||
scripts.push(Value::Object(obj));
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ pub async fn run(app_ident: &str, dir: &Path, mode: OutputMode) -> Result<()> {
|
||||
path_kind: r.path_kind,
|
||||
path: r.path,
|
||||
dispatch_mode: r.dispatch_mode,
|
||||
enabled: r.enabled,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -90,6 +91,7 @@ pub async fn run(app_ident: &str, dir: &Path, mode: OutputMode) -> Result<()> {
|
||||
} else {
|
||||
Some(s.sandbox)
|
||||
},
|
||||
enabled: s.enabled,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -84,6 +84,13 @@ pub struct ManifestScript {
|
||||
/// Per-script sandbox overrides; omitted entirely when no knob is set.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub sandbox: Option<ScriptSandbox>,
|
||||
/// Three-state lifecycle (§4.3): `false` deploys the script inert (not
|
||||
/// invocable). Omitted ⇒ active; only serialized when disabled.
|
||||
#[serde(
|
||||
default = "picloud_shared::default_true",
|
||||
skip_serializing_if = "is_true"
|
||||
)]
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
@@ -102,6 +109,13 @@ pub struct ManifestRoute {
|
||||
pub path: String,
|
||||
#[serde(default, skip_serializing_if = "is_sync")]
|
||||
pub dispatch_mode: DispatchMode,
|
||||
/// Three-state lifecycle (§4.3): `false` deploys the route inert (404).
|
||||
/// Omitted ⇒ active; only serialized when disabled.
|
||||
#[serde(
|
||||
default = "picloud_shared::default_true",
|
||||
skip_serializing_if = "is_true"
|
||||
)]
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
/// Triggers grouped by kind (arrays-of-tables: `[[triggers.cron]]`, …).
|
||||
@@ -244,6 +258,11 @@ fn is_sync(mode: &DispatchMode) -> bool {
|
||||
*mode == DispatchMode::Sync
|
||||
}
|
||||
|
||||
/// Skip-serialize helper: `enabled` defaults true, so only emit it when false.
|
||||
fn is_true(b: &bool) -> bool {
|
||||
*b
|
||||
}
|
||||
|
||||
fn default_timezone() -> String {
|
||||
"UTC".to_string()
|
||||
}
|
||||
@@ -268,6 +287,7 @@ mod tests {
|
||||
timeout_seconds: Some(10),
|
||||
memory_limit_mb: Some(256),
|
||||
sandbox: None,
|
||||
enabled: true,
|
||||
},
|
||||
ManifestScript {
|
||||
name: "lib".into(),
|
||||
@@ -280,6 +300,7 @@ mod tests {
|
||||
max_operations: Some(5_000_000),
|
||||
..ScriptSandbox::empty()
|
||||
}),
|
||||
enabled: false,
|
||||
},
|
||||
],
|
||||
routes: vec![ManifestRoute {
|
||||
@@ -291,6 +312,7 @@ mod tests {
|
||||
path_kind: PathKind::Exact,
|
||||
path: "/posts".into(),
|
||||
dispatch_mode: DispatchMode::Sync,
|
||||
enabled: true,
|
||||
}],
|
||||
triggers: ManifestTriggers {
|
||||
cron: vec![CronTriggerSpec {
|
||||
@@ -337,6 +359,16 @@ mod tests {
|
||||
);
|
||||
// Module kind IS non-default → emitted.
|
||||
assert!(text.contains("kind = \"module\""), "got:\n{text}");
|
||||
// `enabled` defaults true → omitted for the active script/route, but
|
||||
// the disabled `lib` script emits `enabled = false`.
|
||||
assert!(
|
||||
text.contains("enabled = false"),
|
||||
"a disabled entity must emit enabled:\n{text}"
|
||||
);
|
||||
assert!(
|
||||
!text.contains("enabled = true"),
|
||||
"active entities must omit the default:\n{text}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user