test/docs(cli): §3 M3 per-env approval journey; Tier 1 complete
`tests/env_approval.rs`: a `[project.environments]` gating `production` → `pic apply --env production` refused (names --approve); `--yes` alone does not bypass; `--approve production` applies; an un-gated `staging` applies freely. Design doc §3 records M3 shipped and the §6/§3 group-tree Tier 1 (M1 create · M2 divergence/reparent · M3 per-env approval) COMPLETE. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@ mod config;
|
||||
mod dead_letters;
|
||||
mod email_queue;
|
||||
mod enabled;
|
||||
mod env_approval;
|
||||
mod env_overlay;
|
||||
mod extension_points;
|
||||
mod group_create;
|
||||
@@ -44,13 +45,13 @@ mod roles;
|
||||
mod routes;
|
||||
mod scripts;
|
||||
mod sealed;
|
||||
mod structural_divergence;
|
||||
mod secrets;
|
||||
mod shared_queues;
|
||||
mod shared_topics;
|
||||
mod shared_triggers;
|
||||
mod staleness;
|
||||
mod stateful_templates;
|
||||
mod structural_divergence;
|
||||
mod suppress;
|
||||
mod tree;
|
||||
mod triggers;
|
||||
|
||||
88
crates/picloud-cli/tests/env_approval.rs
Normal file
88
crates/picloud-cli/tests/env_approval.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
//! §3 M3 — per-env approval gating (`[project.environments]`).
|
||||
//!
|
||||
//! A `[project.environments]` block marks some envs confirm-required. Applying
|
||||
//! to such an env with `pic apply --env <e>` is refused unless it is explicitly
|
||||
//! `--approve <e>`d — and a blanket `--yes` does NOT cover it (§4.2, "CI must
|
||||
//! opt in per environment"). An unlisted or `confirm = false` env applies
|
||||
//! freely. The gate is client-side, so a refused apply never reaches the server.
|
||||
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use tempfile::TempDir;
|
||||
|
||||
use crate::common;
|
||||
use crate::common::cleanup::GroupGuard;
|
||||
|
||||
/// A repo whose `[project]` gates `production` (confirm-required) but not
|
||||
/// `staging`, managing one pre-existing `[group]`. Minimal per-env overlay files
|
||||
/// exist so `--env` can load them.
|
||||
fn gated_repo(dir: &Path, project: &str, group: &str) {
|
||||
fs::write(
|
||||
dir.join("picloud.toml"),
|
||||
format!(
|
||||
"[project]\nslug = \"{project}\"\n\n\
|
||||
[project.environments]\nproduction = {{ confirm = true }}\n\
|
||||
staging = {{ confirm = false }}\n\n\
|
||||
[group]\nslug = \"{group}\"\nname = \"Env Gated\"\n"
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(dir.join("picloud.production.toml"), "# production overlay\n").unwrap();
|
||||
fs::write(dir.join("picloud.staging.toml"), "# staging overlay\n").unwrap();
|
||||
}
|
||||
|
||||
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
||||
#[test]
|
||||
fn confirm_required_env_needs_explicit_approve() {
|
||||
let Some(fx) = common::fixture_or_skip() else {
|
||||
return;
|
||||
};
|
||||
let env = common::admin_env(fx);
|
||||
let group = common::unique_slug("ea-grp");
|
||||
let project = common::unique_slug("ea-proj");
|
||||
let _g = GroupGuard::new(&env.url, &env.token, &group);
|
||||
common::pic_as(&env)
|
||||
.args(["groups", "create", &group])
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let dir = TempDir::new().unwrap();
|
||||
gated_repo(dir.path(), &project, &group);
|
||||
let manifest = dir.path().join("picloud.toml");
|
||||
let apply = |args: &[&str]| -> std::process::Output {
|
||||
let mut c = common::pic_as(&env);
|
||||
c.args(["apply", "--file"]).arg(&manifest).args(args);
|
||||
c.output().expect("apply")
|
||||
};
|
||||
|
||||
// production is confirm-required → a bare `--env production` is refused,
|
||||
// and the message names `--approve`. (Client-side: never hits the server.)
|
||||
let out = apply(&["--env", "production"]);
|
||||
assert!(!out.status.success(), "production must require approval");
|
||||
let err = String::from_utf8_lossy(&out.stderr).to_lowercase();
|
||||
assert!(
|
||||
err.contains("approve") && err.contains("production"),
|
||||
"the refusal must point at --approve production:\n{err}"
|
||||
);
|
||||
|
||||
// A blanket `--yes` does NOT cover a gated env.
|
||||
assert!(
|
||||
!apply(&["--env", "production", "--yes"]).status.success(),
|
||||
"--yes must not bypass a confirm-required env"
|
||||
);
|
||||
|
||||
// `--approve production` lets it through.
|
||||
assert!(
|
||||
apply(&["--env", "production", "--approve", "production"])
|
||||
.status
|
||||
.success(),
|
||||
"an explicit --approve production must apply"
|
||||
);
|
||||
|
||||
// staging is `confirm = false` → applies freely, no approval needed.
|
||||
assert!(
|
||||
apply(&["--env", "staging"]).status.success(),
|
||||
"an un-gated env must apply without --approve"
|
||||
);
|
||||
}
|
||||
@@ -824,8 +824,19 @@ reshapes the tree): `Refuse` → `ApplyError::StructuralDivergence` (422, naming
|
||||
divergence (`divergence_preview` → a `structure` row: `diverged`, server + manifest parents). CLI:
|
||||
`--force-local-structure` / `--adopt-server-structure` (mutually exclusive) → the `structure_mode` wire field; the
|
||||
422 surfaces verbatim. A concurrent `pic groups reparent` between plan and apply still trips `StateMoved` (the tree
|
||||
token folds `structure_version`). Pinned by `tests/structural_divergence.rs`. **Still deferred:** per-env approval
|
||||
gating (`[project.environments]`).
|
||||
token folds `structure_version`). Pinned by `tests/structural_divergence.rs`.
|
||||
|
||||
**§3 M3 shipped — per-env approval gating (`[project.environments]`).** A `[project.environments]` block on the
|
||||
root `[project]` maps an env name to `{ confirm = bool }` (`ManifestProject.environments`, `#[serde(skip_serializing)]`
|
||||
— purely CLI-side). `pic apply --env <e>` is REFUSED (client-side, before any request) when `<e>` is confirm-required
|
||||
unless it is explicitly `--approve <e>`d; a blanket `--yes` does NOT cover a gated env (§4.2 "CI must opt in per
|
||||
environment"); an unlisted / `confirm = false` env applies freely. `require_env_approval` gates both the single
|
||||
(`run`) and tree (`run_tree`) apply paths; `--approve <env>` is a repeatable flag. Value overlays
|
||||
(`picloud.<env>.toml`) already merge client-side, so this milestone is the confirm-policy gate only. Pinned by
|
||||
`tests/env_approval.rs`. **Deferred (documented, not built):** server-side audited approval-override capability
|
||||
(§4.2 → `authz::can`); env-as-app mapping + declarative server-side `@E` config scoping.
|
||||
|
||||
**§6/§3 group-tree Tier 1 (M1 create · M2 divergence/reparent · M3 per-env approval) is COMPLETE.**
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user