feat(cli): pic init scaffolds a new declarative project
Offline project scaffolding — the on-ramp to pull/plan/apply. Writes a `picloud.toml` describing a minimal *working* app (a `hello` endpoint bound to GET /hello) plus commented examples for the other blocks, `scripts/hello.rhai`, and a `.gitignore` that pre-ignores the project tool's `.picloud/` link-state directory (the home for the bound-plan state token, landing next). Refuses to overwrite an existing manifest without `--force`; derives the app slug from the directory name when not given (validated against the canonical slug rule). The active manifest is rendered through the same `to_toml` model the rest of the tool uses, so a freshly-init'd project is guaranteed valid and immediately `pic plan`-able. Offline — no server contact, so the journey tests need no DATABASE_URL. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
77
crates/picloud-cli/tests/init.rs
Normal file
77
crates/picloud-cli/tests/init.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
//! `pic init` journey — offline scaffolding, no server/DB needed (so these
|
||||
//! tests are NOT gated on `DATABASE_URL` and never touch `common::fixture`).
|
||||
|
||||
use std::fs;
|
||||
|
||||
use assert_cmd::Command;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn pic() -> Command {
|
||||
Command::cargo_bin("pic").expect("pic binary")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_scaffolds_a_deployable_project() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
pic()
|
||||
.current_dir(dir.path())
|
||||
.args(["init", "demo-app"])
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let toml = fs::read_to_string(dir.path().join("picloud.toml")).unwrap();
|
||||
assert!(toml.contains("slug = \"demo-app\""), "got:\n{toml}");
|
||||
assert!(
|
||||
toml.contains("name = \"Demo App\""),
|
||||
"name defaults to title-cased slug:\n{toml}"
|
||||
);
|
||||
assert!(
|
||||
dir.path().join("scripts/hello.rhai").exists(),
|
||||
"scaffold writes the example script"
|
||||
);
|
||||
let gitignore = fs::read_to_string(dir.path().join(".gitignore")).unwrap();
|
||||
assert!(
|
||||
gitignore.lines().any(|l| l.trim() == ".picloud/"),
|
||||
"init must gitignore .picloud/:\n{gitignore}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_refuses_to_overwrite_without_force() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
pic()
|
||||
.current_dir(dir.path())
|
||||
.args(["init", "demo-app"])
|
||||
.assert()
|
||||
.success();
|
||||
// A second run must refuse rather than clobber edits.
|
||||
pic()
|
||||
.current_dir(dir.path())
|
||||
.args(["init", "demo-app"])
|
||||
.assert()
|
||||
.failure();
|
||||
// `--force` overrides.
|
||||
pic()
|
||||
.current_dir(dir.path())
|
||||
.args(["init", "demo-app", "--force"])
|
||||
.assert()
|
||||
.success();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_appends_to_an_existing_gitignore_once() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
fs::write(dir.path().join(".gitignore"), "target/\n").unwrap();
|
||||
pic()
|
||||
.current_dir(dir.path())
|
||||
.args(["init", "demo-app"])
|
||||
.assert()
|
||||
.success();
|
||||
let gitignore = fs::read_to_string(dir.path().join(".gitignore")).unwrap();
|
||||
assert!(gitignore.contains("target/"), "must preserve existing rules");
|
||||
assert_eq!(
|
||||
gitignore.matches(".picloud/").count(),
|
||||
1,
|
||||
"must add the ignore exactly once:\n{gitignore}"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user