use std::io::{Read, Write as IoWrite};
use std::path::{Path, PathBuf};
use std::process::Command;
mod common;
const GOLDEN: &str = include_str!("../../../conformance/designs/todo-api.design.json");
const REFERENCE: &str = include_str!("../../../conformance/designs/reference-slice.design.json");
fn repo_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.ancestors()
.nth(2)
.unwrap()
.to_path_buf()
}
fn scaffold_golden(tmp: &Path) -> PathBuf {
let design = tmp.join("design.json");
std::fs::write(&design, GOLDEN).unwrap();
let app = tmp.join("todo-api");
let dep = format!(
"jerrycan = {{ path = \"{}\", default-features = false }}",
repo_root().join("crates/jerrycan").display()
);
let st = Command::new(env!("CARGO_BIN_EXE_jerrycan"))
.env("JERRYCAN_FRAMEWORK_DEP", &dep)
.arg("new")
.arg(&app)
.arg("--design")
.arg(&design)
.status()
.unwrap();
assert!(st.success());
app
}
fn scaffold_golden_db(tmp: &Path) -> PathBuf {
let mut design: serde_json::Value = serde_json::from_str(GOLDEN).unwrap();
design["dependencies"] = serde_json::json!(["db", "validate"]);
let design_path = tmp.join("design.json");
std::fs::write(&design_path, serde_json::to_string_pretty(&design).unwrap()).unwrap();
let app = tmp.join("todo-api");
let dep = format!(
"jerrycan = {{ path = \"{}\", default-features = false }}",
repo_root().join("crates/jerrycan").display()
);
let st = Command::new(env!("CARGO_BIN_EXE_jerrycan"))
.env("JERRYCAN_FRAMEWORK_DEP", &dep)
.arg("new")
.arg(&app)
.arg("--design")
.arg(&design_path)
.status()
.unwrap();
assert!(st.success());
app
}
#[test]
#[ignore = "heavy: db-mode golden app must build and pass the full gate"]
fn db_mode_scaffold_passes_jerrycan_check() {
let tmp = tempfile::tempdir().unwrap();
let app = scaffold_golden_db(tmp.path());
let out = Command::new(env!("CARGO_BIN_EXE_jerrycan"))
.current_dir(&app)
.env("CARGO_TARGET_DIR", common::shared_app_target())
.args(["--json", "check"])
.output()
.unwrap();
let payload: serde_json::Value = serde_json::from_slice(&out.stdout).expect("one JSON doc");
assert_eq!(
payload["ok"], true,
"diagnostics: {}",
payload["diagnostics"]
);
assert!(out.status.success());
}
#[test]
#[ignore = "heavy: full cargo build of a generated workspace"]
fn scaffolded_app_builds_with_zero_warnings() {
let tmp = tempfile::tempdir().unwrap();
let app = scaffold_golden(tmp.path());
let out = Command::new("cargo")
.current_dir(&app)
.env("CARGO_TARGET_DIR", common::shared_app_target())
.env("RUSTFLAGS", "-D warnings")
.args(["build", "--workspace"])
.output()
.unwrap();
assert!(
out.status.success(),
"generated app must build warning-free:\n{}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
#[ignore = "heavy: full verification pipeline incl. cargo-audit/cargo-deny"]
fn fresh_scaffold_passes_jerrycan_check() {
let tmp = tempfile::tempdir().unwrap();
let app = scaffold_golden(tmp.path());
let out = Command::new(env!("CARGO_BIN_EXE_jerrycan"))
.current_dir(&app)
.env("CARGO_TARGET_DIR", common::shared_app_target())
.args(["--json", "check"])
.output()
.unwrap();
let payload: serde_json::Value =
serde_json::from_slice(&out.stdout).expect("check --json emits one JSON document");
assert_eq!(
payload["ok"], true,
"diagnostics: {}",
payload["diagnostics"]
);
assert!(out.status.success());
}
#[cfg(feature = "auth")]
fn scaffold_golden_auth(tmp: &Path) -> PathBuf {
let mut design: serde_json::Value = serde_json::from_str(GOLDEN).unwrap();
design["dependencies"] = serde_json::json!(["auth", "observe"]);
design["auth"] = serde_json::json!({ "model": "session", "roles": ["admin"] });
for ep in design["modules"][0]["endpoints"].as_array_mut().unwrap() {
if ep["operation_id"] == "create_todo" {
ep["auth_required"] = serde_json::json!(true);
}
if ep["operation_id"] == "delete_todo" {
ep["required_roles"] = serde_json::json!(["admin"]);
}
}
for ep in design["modules"][0]["subroutes"][0]["endpoints"]
.as_array_mut()
.unwrap()
{
if ep["operation_id"] == "create_comment" {
ep["auth_required"] = serde_json::json!(true);
}
}
for ep in design["modules"][1]["endpoints"].as_array_mut().unwrap() {
if ep["operation_id"] == "create_user" {
ep["auth_required"] = serde_json::json!(true);
}
}
let design_path = tmp.join("design.json");
std::fs::write(&design_path, serde_json::to_string_pretty(&design).unwrap()).unwrap();
let app = tmp.join("todo-api");
let dep = format!(
"jerrycan = {{ path = \"{}\", default-features = false }}",
repo_root().join("crates/jerrycan").display()
);
let st = Command::new(env!("CARGO_BIN_EXE_jerrycan"))
.env("JERRYCAN_FRAMEWORK_DEP", &dep)
.arg("new")
.arg(&app)
.arg("--design")
.arg(&design_path)
.status()
.unwrap();
assert!(st.success());
app
}
#[cfg(feature = "auth")]
#[test]
#[ignore = "heavy: auth+observe golden app builds, checks, and serves guarded routes"]
fn auth_observe_app_builds_checks_and_guards() {
let tmp = tempfile::tempdir().unwrap();
let app = scaffold_golden_auth(tmp.path());
for (fixture, target) in [
(
"auth/todos_handlers.rs",
"crates/routes/todos/src/handlers.rs",
),
(
"auth/comments_handlers.rs",
"crates/routes/todos/src/subroutes/comments/handlers.rs",
),
(
"auth/users_handlers.rs",
"crates/routes/users/src/handlers.rs",
),
] {
std::fs::copy(
repo_root().join("conformance/fixtures").join(fixture),
app.join(target),
)
.unwrap();
}
let out = Command::new(env!("CARGO_BIN_EXE_jerrycan"))
.current_dir(&app)
.env("CARGO_TARGET_DIR", common::shared_app_target())
.args(["--json", "check"])
.output()
.unwrap();
let payload: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
assert_eq!(
payload["ok"], true,
"diagnostics: {}",
payload["diagnostics"]
);
let port = {
let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
l.local_addr().unwrap().port()
};
let addr = format!("127.0.0.1:{port}");
let mut server = Command::new("cargo")
.current_dir(&app)
.env("CARGO_TARGET_DIR", common::shared_app_target())
.env("JERRYCAN_ADDR", &addr)
.env("JERRYCAN_SECRET", "a-very-long-development-secret-string!!")
.args(["run", "-p", "app"])
.spawn()
.unwrap();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(180);
while std::time::Instant::now() < deadline {
if std::net::TcpStream::connect(&addr).is_ok() {
break;
}
std::thread::sleep(std::time::Duration::from_millis(400));
}
let http = |req: String| -> String {
let mut s = std::net::TcpStream::connect(&addr).unwrap();
s.write_all(req.as_bytes()).unwrap();
let mut buf = Vec::new();
s.read_to_end(&mut buf).unwrap();
String::from_utf8_lossy(&buf).into_owned()
};
assert!(
http("GET /todos/ HTTP/1.1\r\nHost: l\r\nConnection: close\r\n\r\n".into())
.starts_with("HTTP/1.1 200")
);
let body = r#"{"title":"x","done":false}"#;
let create = |cookie: &str| {
format!(
"POST /todos/ HTTP/1.1\r\nHost: l\r\nContent-Type: application/json\r\nContent-Length: {}\r\n{}Connection: close\r\n\r\n{body}",
body.len(),
cookie
)
};
assert!(
http(create("")).starts_with("HTTP/1.1 401"),
"no cookie → 401"
);
let cookie = {
let auth = jerrycan::auth::Auth::with_secret("a-very-long-development-secret-string!!");
let token = auth
.sessions()
.encode(&serde_json::json!({ "id": "1", "role": "admin" }))
.unwrap();
format!("Cookie: jerrycan_session={token}\r\n")
};
assert!(
http(create(&cookie)).starts_with("HTTP/1.1 201"),
"admin cookie → 201"
);
assert_eq!(
http("GET /healthz HTTP/1.1\r\nHost: l\r\nConnection: close\r\n\r\n".into())
.lines()
.next()
.unwrap(),
"HTTP/1.1 200 OK"
);
assert!(
http("GET /metrics HTTP/1.1\r\nHost: l\r\nConnection: close\r\n\r\n".into())
.contains("jerrycan_requests_total")
);
let _ = server.kill();
let _ = server.wait();
}
#[test]
#[ignore = "heavy: MCP loop + cargo build + live HTTP round-trips"]
fn agent_generates_working_crud_service_via_mcp_only() {
let tmp = tempfile::tempdir().unwrap();
let dep = format!(
"jerrycan = {{ path = \"{}\", default-features = false }}",
repo_root().join("crates/jerrycan").display()
);
let shared_target = common::shared_app_target();
let mut c = common::McpClient::start_in_with_env(
tmp.path(),
&[
("JERRYCAN_FRAMEWORK_DEP", &dep),
("CARGO_TARGET_DIR", shared_target.to_str().unwrap()),
],
);
let draft: serde_json::Value = serde_json::from_str(GOLDEN).unwrap();
let (err, payload) = c.call_tool(
"jerrycan_design",
serde_json::json!({"requirements": "multi-module todo backend", "draft": draft}),
);
assert!(!err, "{payload}");
assert_eq!(payload["status"], "complete");
let design_path = payload["design_path"].as_str().unwrap().to_string();
let app = tmp.path().join("todo-api");
let (err, payload) = c.call_tool(
"jerrycan_scaffold",
serde_json::json!({"design_path": design_path, "directory": app.to_str().unwrap()}),
);
assert!(!err, "{payload}");
for (fixture, target) in [
("todos_handlers.rs", "crates/routes/todos/src/handlers.rs"),
(
"comments_handlers.rs",
"crates/routes/todos/src/subroutes/comments/handlers.rs",
),
("users_handlers.rs", "crates/routes/users/src/handlers.rs"),
] {
std::fs::copy(
repo_root().join("conformance/fixtures").join(fixture),
app.join(target),
)
.unwrap();
}
let (err, payload) = c.call_tool(
"jerrycan_check",
serde_json::json!({"directory": app.to_str().unwrap()}),
);
assert!(!err, "{payload}");
assert_eq!(
payload["ok"], true,
"diagnostics: {}",
payload["diagnostics"]
);
c.shutdown();
let port = {
let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
l.local_addr().unwrap().port()
};
let addr = format!("127.0.0.1:{port}");
let mut server = Command::new("cargo")
.current_dir(&app)
.env("CARGO_TARGET_DIR", common::shared_app_target())
.env("JERRYCAN_ADDR", &addr)
.args(["run", "-p", "app"])
.spawn()
.unwrap();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(120);
let mut connected = None;
while std::time::Instant::now() < deadline {
if let Ok(s) = std::net::TcpStream::connect(&addr) {
connected = Some(s);
break;
}
std::thread::sleep(std::time::Duration::from_millis(300));
}
let http = |req: String| -> String {
let mut s = std::net::TcpStream::connect(&addr).unwrap();
s.write_all(req.as_bytes()).unwrap();
let mut buf = Vec::new();
s.read_to_end(&mut buf).unwrap();
String::from_utf8_lossy(&buf).into_owned()
};
drop(connected.expect("generated app started serving within 120s"));
let res = http("GET /todos/ HTTP/1.1\r\nHost: l\r\nConnection: close\r\n\r\n".into());
assert!(res.starts_with("HTTP/1.1 200"), "{res}");
assert!(res.ends_with("[]"), "empty store first: {res}");
let body = r#"{"title":"ship phase 1"}"#;
let res = http(format!(
"POST /todos/ HTTP/1.1\r\nHost: l\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
body.len()
));
assert!(res.starts_with("HTTP/1.1 201"), "{res}");
let res = http("GET /todos/1 HTTP/1.1\r\nHost: l\r\nConnection: close\r\n\r\n".into());
assert!(
res.starts_with("HTTP/1.1 200") && res.contains("ship phase 1"),
"{res}"
);
let res = http("GET /users/ HTTP/1.1\r\nHost: l\r\nConnection: close\r\n\r\n".into());
assert!(res.starts_with("HTTP/1.1 200"), "multi-module proof: {res}");
let res = http("DELETE /todos/1 HTTP/1.1\r\nHost: l\r\nConnection: close\r\n\r\n".into());
assert!(res.starts_with("HTTP/1.1 204"), "{res}");
let res = http("GET /todos/1 HTTP/1.1\r\nHost: l\r\nConnection: close\r\n\r\n".into());
assert!(res.starts_with("HTTP/1.1 404"), "{res}");
let _ = server.kill();
let _ = server.wait();
}
#[test]
#[ignore = "heavy: scaffold + gen-tests + red run + implement + green run"]
fn tdd_loop_goes_red_then_green_on_sqlite() {
let tmp = tempfile::tempdir().unwrap();
let app = scaffold_golden_db(tmp.path());
let mut expected_failing = 0usize;
for module in ["todos", "users"] {
let out = Command::new(env!("CARGO_BIN_EXE_jerrycan"))
.current_dir(&app)
.env("CARGO_TARGET_DIR", common::shared_app_target())
.args(["--json", "gen-tests", "--module", module])
.output()
.unwrap();
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let payload: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
expected_failing += payload["expected_failing"].as_u64().unwrap() as usize;
}
assert_eq!(expected_failing, 10, "todos 8 + users 2");
let out = Command::new("cargo")
.current_dir(&app)
.env("CARGO_TARGET_DIR", common::shared_app_target())
.args(["test", "--workspace", "--no-fail-fast"])
.output()
.unwrap();
assert!(
!out.status.success(),
"stub handlers must fail the acceptance suite"
);
let test_output = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
let failed: usize = test_output
.lines()
.filter_map(|l| {
l.strip_prefix("test result: FAILED. ")?
.split("; ")
.nth(1)?
.strip_suffix(" failed")
.map(|n| n.parse::<usize>().unwrap_or(0))
})
.sum();
assert_eq!(
failed, expected_failing,
"every generated test red:\n{test_output}"
);
for (fixture, target) in [
(
"db/todos_handlers.rs",
"crates/routes/todos/src/handlers.rs",
),
(
"db/comments_handlers.rs",
"crates/routes/todos/src/subroutes/comments/handlers.rs",
),
(
"db/users_handlers.rs",
"crates/routes/users/src/handlers.rs",
),
] {
let dest = app.join(target);
std::fs::copy(
repo_root().join("conformance/fixtures").join(fixture),
&dest,
)
.unwrap();
let future = std::time::SystemTime::now() + std::time::Duration::from_secs(10);
std::fs::File::options()
.write(true)
.open(&dest)
.unwrap()
.set_modified(future)
.unwrap();
}
let st = Command::new("cargo")
.current_dir(&app)
.env("CARGO_TARGET_DIR", common::shared_app_target())
.args(["test", "--workspace"])
.status()
.unwrap();
assert!(
st.success(),
"implemented handlers must satisfy the design contract"
);
let out = Command::new(env!("CARGO_BIN_EXE_jerrycan"))
.current_dir(&app)
.env("CARGO_TARGET_DIR", common::shared_app_target())
.args(["--json", "check"])
.output()
.unwrap();
let payload: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
assert_eq!(
payload["ok"], true,
"diagnostics: {}",
payload["diagnostics"]
);
}
#[test]
#[ignore = "heavy: full TDD loop against live Postgres (JERRYCAN_TEST_PG_URL)"]
fn agent_builds_postgres_backed_api_test_first() {
let Ok(pg_url) = std::env::var("JERRYCAN_TEST_PG_URL") else {
eprintln!("SKIP: JERRYCAN_TEST_PG_URL not set (CI provides a postgres service)");
return;
};
let tmp = tempfile::tempdir().unwrap();
let app = scaffold_golden_db(tmp.path());
for module in ["todos", "users"] {
let st = Command::new(env!("CARGO_BIN_EXE_jerrycan"))
.current_dir(&app)
.env("CARGO_TARGET_DIR", common::shared_app_target())
.args(["gen-tests", "--module", module])
.status()
.unwrap();
assert!(st.success());
}
for (fixture, target) in [
(
"db/todos_handlers.rs",
"crates/routes/todos/src/handlers.rs",
),
(
"db/comments_handlers.rs",
"crates/routes/todos/src/subroutes/comments/handlers.rs",
),
(
"db/users_handlers.rs",
"crates/routes/users/src/handlers.rs",
),
] {
std::fs::copy(
repo_root().join("conformance/fixtures").join(fixture),
app.join(target),
)
.unwrap();
}
let st = Command::new(env!("CARGO_BIN_EXE_jerrycan"))
.current_dir(&app)
.env("CARGO_TARGET_DIR", common::shared_app_target())
.args(["db", "migrate", "--url", &pg_url])
.status()
.unwrap();
assert!(st.success(), "migrations must apply to live Postgres");
let port = {
let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
l.local_addr().unwrap().port()
};
let addr = format!("127.0.0.1:{port}");
let mut server = Command::new("cargo")
.current_dir(&app)
.env("CARGO_TARGET_DIR", common::shared_app_target())
.env("JERRYCAN_ADDR", &addr)
.env("JERRYCAN_DATABASE_URL", &pg_url)
.args(["run", "-p", "app"])
.spawn()
.unwrap();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(180);
while std::time::Instant::now() < deadline {
if std::net::TcpStream::connect(&addr).is_ok() {
break;
}
std::thread::sleep(std::time::Duration::from_millis(400));
}
let http = |req: String| -> String {
let mut s = std::net::TcpStream::connect(&addr).unwrap();
s.write_all(req.as_bytes()).unwrap();
let mut buf = Vec::new();
s.read_to_end(&mut buf).unwrap();
String::from_utf8_lossy(&buf).into_owned()
};
let body = r#"{"title":"pg ship","done":false}"#;
let res = http(format!(
"POST /todos/ HTTP/1.1\r\nHost: l\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
body.len()
));
assert!(res.starts_with("HTTP/1.1 201"), "{res}");
let res = http("GET /todos/1 HTTP/1.1\r\nHost: l\r\nConnection: close\r\n\r\n".into());
assert!(
res.starts_with("HTTP/1.1 200") && res.contains("pg ship"),
"{res}"
);
let res = http("GET /openapi.json HTTP/1.1\r\nHost: l\r\nConnection: close\r\n\r\n".into());
assert!(
res.starts_with("HTTP/1.1 200") && res.contains("3.1.0"),
"validate extension live: {res}"
);
let res = http("DELETE /todos/1 HTTP/1.1\r\nHost: l\r\nConnection: close\r\n\r\n".into());
assert!(res.starts_with("HTTP/1.1 204"), "{res}");
let st = Command::new("cargo")
.current_dir(&app)
.env("CARGO_TARGET_DIR", common::shared_app_target())
.env("JERRYCAN_DATABASE_URL", &pg_url)
.args(["test", "--workspace"])
.status()
.unwrap();
let _ = server.kill();
let _ = server.wait();
assert!(
st.success(),
"acceptance suite must be green against Postgres"
);
}
#[test]
#[ignore = "heavy: package the golden app and prove binary/docker/k8s deploy paths"]
fn golden_app_deploys_everywhere() {
if !cfg!(target_os = "linux") {
eprintln!(
"SKIP golden_app_deploys_everywhere: needs a Linux host for the musl \
binary + container legs (host is {}); CI covers it.",
std::env::consts::OS
);
return;
}
let tmp = tempfile::tempdir().unwrap();
let app = scaffold_golden(tmp.path());
for (fixture, target) in [
("todos_handlers.rs", "crates/routes/todos/src/handlers.rs"),
(
"comments_handlers.rs",
"crates/routes/todos/src/subroutes/comments/handlers.rs",
),
("users_handlers.rs", "crates/routes/users/src/handlers.rs"),
] {
std::fs::copy(
repo_root().join("conformance/fixtures").join(fixture),
app.join(target),
)
.unwrap();
}
let out = Command::new(env!("CARGO_BIN_EXE_jerrycan"))
.current_dir(&app)
.env("CARGO_TARGET_DIR", common::shared_app_target())
.args([
"--json",
"package",
"--binary",
"--docker",
"--k8s",
"--systemd",
])
.output()
.unwrap();
assert!(
out.status.success(),
"package failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let payload: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
let artifacts = payload["artifacts"].as_array().unwrap();
for expected in [
"deploy/Dockerfile",
"deploy/k8s.yaml",
"deploy/todo-api.service",
"deploy/todo-api",
"deploy/sbom.json",
] {
assert!(
artifacts.iter().any(|a| a == expected) || app.join(expected).exists(),
"missing {expected}"
);
}
let sbom: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(app.join("deploy/sbom.json")).unwrap())
.unwrap();
assert_eq!(sbom["bomFormat"], "CycloneDX");
assert!(
sbom["components"]
.as_array()
.unwrap()
.iter()
.any(|c| c["name"] == "tokio")
);
let port = pick_port();
let addr = format!("127.0.0.1:{port}");
let mut bin = Command::new(app.join("deploy/todo-api"))
.env("JERRYCAN_ADDR", &addr)
.spawn()
.expect("packaged binary runs");
await_listen(&addr, 60);
assert!(
http_get(&addr, "/todos/").starts_with("HTTP/1.1 200"),
"bare binary serves"
);
let _ = bin.kill();
let _ = bin.wait();
if !tool_present("docker") {
eprintln!("SKIP docker leg: docker not present");
} else if std::env::consts::OS != "linux" {
eprintln!(
"SKIP docker leg: host is {} — the host-built binary is not a Linux \
executable and cannot run in a Linux container (CI proves this leg on Linux)",
std::env::consts::OS
);
} else {
let base = if musl_built(&app) {
"gcr.io/distroless/static:nonroot"
} else {
"debian:stable-slim"
};
let test_dockerfile = format!(
"FROM {base}\nCOPY deploy/todo-api /usr/local/bin/todo-api\n\
EXPOSE 8000\nENV JERRYCAN_ADDR=0.0.0.0:8000\n\
ENTRYPOINT [\"/usr/local/bin/todo-api\"]\n"
);
std::fs::write(app.join("Dockerfile.thin"), &test_dockerfile).unwrap();
let tag = "jerrycan-conformance:test";
let build = Command::new("docker")
.current_dir(&app)
.env("CARGO_TARGET_DIR", common::shared_app_target())
.args(["build", "-f", "Dockerfile.thin", "-t", tag, "."])
.status()
.unwrap();
assert!(build.success(), "thin-image docker build");
let port = pick_port();
let run = Command::new("docker")
.args([
"run",
"-d",
"--rm",
"-p",
&format!("{port}:8000"),
"--name",
"jerrycan-conformance",
tag,
])
.output()
.unwrap();
assert!(
run.status.success(),
"docker run: {}",
String::from_utf8_lossy(&run.stderr)
);
let addr = format!("127.0.0.1:{port}");
await_listen(&addr, 60);
let body = http_get(&addr, "/todos/");
let _ = Command::new("docker")
.args(["stop", "jerrycan-conformance"])
.status();
let _ = Command::new("docker").args(["rmi", "-f", tag]).status();
assert!(
body.starts_with("HTTP/1.1 200"),
"containerized app serves: {body}"
);
}
if kubectl_present() && cluster_reachable() {
let out = Command::new("kubectl")
.current_dir(&app)
.env("CARGO_TARGET_DIR", common::shared_app_target())
.args(["apply", "--dry-run=client", "-f", "deploy/k8s.yaml"])
.output()
.unwrap();
assert!(
out.status.success(),
"kubectl dry-run: {}",
String::from_utf8_lossy(&out.stderr)
);
} else {
let y = std::fs::read_to_string(app.join("deploy/k8s.yaml")).unwrap();
let docs: Vec<&str> = y.split("\n---\n").collect();
assert_eq!(docs.len(), 3, "Deployment + Service + NetworkPolicy");
for d in docs {
assert!(
d.contains("apiVersion:") && d.contains("kind:"),
"valid manifest doc"
);
}
eprintln!(
"SKIP kubectl dry-run: no reachable cluster — used structural manifest validation"
);
}
}
#[test]
#[ignore = "heavy: reference-slice (SeaORM) scaffolds, builds, reds-on-stubs; records cold-build baseline"]
fn reference_slice_scaffold_passes_check() {
let tmp = tempfile::tempdir().unwrap();
let design_path = tmp.path().join("design.json");
std::fs::write(&design_path, REFERENCE).unwrap();
let app = tmp.path().join("reference-slice");
let dep = format!(
"jerrycan = {{ path = \"{}\", default-features = false }}",
repo_root().join("crates/jerrycan").display()
);
let st = Command::new(env!("CARGO_BIN_EXE_jerrycan"))
.env("JERRYCAN_FRAMEWORK_DEP", &dep)
.arg("new")
.arg(&app)
.arg("--design")
.arg(&design_path)
.status()
.unwrap();
assert!(st.success(), "reference-slice must scaffold");
assert!(
app.join("schema.json").exists(),
"db-mode scaffold must emit schema.json"
);
let mut expected_failing = 0usize;
for module in [
"users",
"workspaces",
"leads",
"api-keys",
"billing",
"integrations",
] {
let out = Command::new(env!("CARGO_BIN_EXE_jerrycan"))
.current_dir(&app)
.env("CARGO_TARGET_DIR", common::shared_app_target())
.args(["--json", "gen-tests", "--module", module])
.output()
.unwrap();
assert!(
out.status.success(),
"gen-tests {module} failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let payload: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
expected_failing += payload["expected_failing"].as_u64().unwrap() as usize;
}
assert!(
expected_failing > 0,
"the design must generate failing acceptance tests"
);
let t0 = std::time::Instant::now();
let build = Command::new("cargo")
.current_dir(&app)
.env("CARGO_TARGET_DIR", common::shared_app_target())
.args(["build", "--workspace"])
.output()
.unwrap();
let cold_build = t0.elapsed();
assert!(
build.status.success(),
"reference-slice (SeaORM) generated workspace must build:\n{}",
String::from_utf8_lossy(&build.stderr)
);
eprintln!("reference-slice cold build: {cold_build:?}");
let t1 = std::time::Instant::now();
let leads_build = Command::new("cargo")
.current_dir(&app)
.env("CARGO_TARGET_DIR", common::shared_app_target())
.args(["test", "-p", "route-leads", "--no-run"])
.output()
.unwrap();
let leads_test_build = t1.elapsed();
assert!(
leads_build.status.success(),
"route-leads test binary must compile:\n{}",
String::from_utf8_lossy(&leads_build.stderr)
);
eprintln!("reference-slice route-leads incremental test-build: {leads_test_build:?}");
let out = Command::new("cargo")
.current_dir(&app)
.env("CARGO_TARGET_DIR", common::shared_app_target())
.args(["test", "--workspace", "--no-fail-fast"])
.output()
.unwrap();
assert!(
!out.status.success(),
"pre-implementation stubs must fail the acceptance suite"
);
let test_output = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
let observed: Vec<u16> = test_output
.lines()
.filter_map(|l| l.trim().strip_prefix("left: "))
.filter_map(|n| n.trim().parse::<u16>().ok())
.collect();
assert!(
!observed.is_empty(),
"expected failed-assertion `left:` lines in:\n{test_output}"
);
let non_stub: Vec<u16> = observed.iter().copied().filter(|s| *s != 500).collect();
assert!(
non_stub.is_empty(),
"acceptance failures must be ONLY JC0500 stubs (500); found non-stub \
observed statuses {non_stub:?} — a guard/validation false-failure is a \
generator bug:\n{test_output}"
);
assert!(
!test_output.contains("JC0422"),
"no acceptance failure may carry JC0422 (validation false-reject):\n{test_output}"
);
assert!(
test_output.contains("_without_auth_is_401 ... ok"),
"guard tests must pass (guard precedes the stub):\n{test_output}"
);
assert!(
!test_output.contains("_without_auth_is_401 ... FAILED"),
"a guard test must never fail — the guard precedes the stub:\n{test_output}"
);
let design: jerrycan::platform::design::Design = serde_json::from_str(REFERENCE).unwrap();
let lints = jerrycan::platform::lints::run(&app, &design);
assert!(
lints.is_empty(),
"jerrycan lints must be clean on a fresh scaffold: {lints:?}"
);
let schema_drift = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(jerrycan::platform::schema::verify_fresh(&app, &design))
.expect("schema derivation must succeed");
assert!(
schema_drift.is_empty(),
"scaffolded schema.json must match a fresh derivation: {schema_drift:?}"
);
}
fn pick_port() -> u16 {
std::net::TcpListener::bind("127.0.0.1:0")
.unwrap()
.local_addr()
.unwrap()
.port()
}
fn tool_present(tool: &str) -> bool {
Command::new(tool)
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn kubectl_present() -> bool {
Command::new("kubectl")
.args(["version", "--client"])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn cluster_reachable() -> bool {
Command::new("kubectl")
.args(["cluster-info"])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn await_listen(addr: &str, secs: u64) {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(secs);
while std::time::Instant::now() < deadline {
if std::net::TcpStream::connect(addr).is_ok() {
return;
}
std::thread::sleep(std::time::Duration::from_millis(400));
}
panic!("nothing listening on {addr} after {secs}s");
}
fn http_get(addr: &str, path: &str) -> String {
let mut s = std::net::TcpStream::connect(addr).unwrap();
s.write_all(format!("GET {path} HTTP/1.1\r\nHost: l\r\nConnection: close\r\n\r\n").as_bytes())
.unwrap();
let mut buf = Vec::new();
let _ = s.read_to_end(&mut buf);
String::from_utf8_lossy(&buf).into_owned()
}
fn musl_built(app: &Path) -> bool {
app.join("target/x86_64-unknown-linux-musl/release/app")
.exists()
}