use assert_cmd::Command;
use std::fs;
use std::io::Read;
use std::path::Path;
use std::time::{Duration, Instant};
const MANIFEST: &str = r#"
version = 1
canonical = "json-schema-2020-12"
authoring = "zod"
[[contracts]]
name = "user"
source = "contracts/user.zod.ts"
emit = ["zod"]
[[mappings]]
glob = "app/api/**/*.ts"
contracts = ["user"]
require = "boundary-validation"
[gates]
protected_paths = ["pushkin.toml"]
"#;
const USER_SCHEMA: &str = r#"{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"name": { "type": "string", "minLength": 1 },
"email": { "type": "string", "format": "email" }
},
"required": ["name", "email"],
"additionalProperties": false
}"#;
fn repo() -> std::io::Result<tempfile::TempDir> {
let dir = tempfile::tempdir()?;
fs::write(dir.path().join("pushkin.toml"), MANIFEST)?;
fs::create_dir_all(dir.path().join("schemas"))?;
fs::write(dir.path().join("schemas/user.schema.json"), USER_SCHEMA)?;
Ok(dir)
}
fn serve_once(dir: &Path) -> Result<String, String> {
let binary = assert_cmd::cargo::cargo_bin("pushkin");
let mut child = std::process::Command::new(binary)
.current_dir(dir)
.args(["daemon", "serve"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.spawn()
.map_err(|e| format!("serve spawns: {e}"))?;
let socket = dir.join(".pushkin/daemon.sock");
let deadline = Instant::now() + Duration::from_secs(10);
while Instant::now() < deadline && !socket.exists() {
std::thread::sleep(Duration::from_millis(20));
}
if !socket.exists() {
return Err("daemon never came up".to_owned());
}
let stop = Command::cargo_bin("pushkin")
.map_err(|e| format!("binary resolves: {e}"))?
.current_dir(dir)
.args(["daemon", "stop"])
.output()
.map_err(|e| format!("stop runs: {e}"))?;
if stop.status.code() != Some(0) {
return Err(format!("stop exited {:?}", stop.status.code()));
}
let status = child.wait().map_err(|e| format!("serve joins: {e}"))?;
if !status.success() {
return Err(format!("serve exited {status:?}"));
}
let mut stderr = String::new();
child
.stderr
.take()
.ok_or("stderr piped")?
.read_to_string(&mut stderr)
.map_err(|e| format!("stderr reads: {e}"))?;
Ok(stderr)
}
#[test]
fn serve_startup_regenerates_stale_epoch_artifacts() {
let dir = repo().unwrap();
fs::create_dir_all(dir.path().join("generated")).unwrap();
fs::write(
dir.path().join("generated/user.zod.gen.ts"),
"// GENERATED by pushkin compile from contract 'user' — do not hand-edit.\n\
// pushkin-epoch: 0\nexport const stale = true;\n",
)
.unwrap();
let stderr = serve_once(dir.path()).unwrap();
assert!(
stderr.contains("stale epoch"),
"stale artifact must be named at startup: {stderr}"
);
assert!(
stderr.contains("user.zod.gen.ts"),
"the named artifact is the stale one: {stderr}"
);
let regenerated = fs::read_to_string(dir.path().join("generated/user.zod.gen.ts")).unwrap();
assert!(
regenerated.contains("pushkin-epoch: 1"),
"artifact must carry the current epoch after the startup queue: {regenerated}"
);
assert!(
!regenerated.contains("export const stale"),
"stale content must be regenerated, not just re-stamped"
);
}
#[test]
fn serve_startup_silent_on_current_epochs() {
let dir = repo().unwrap();
let compile = Command::cargo_bin("pushkin")
.unwrap()
.current_dir(dir.path())
.arg("compile")
.output()
.unwrap();
assert_eq!(compile.status.code(), Some(0));
let before = fs::read_to_string(dir.path().join("generated/user.zod.gen.ts")).unwrap();
let stderr = serve_once(dir.path()).unwrap();
assert!(
!stderr.contains("stale epoch"),
"an up-to-date tree stays silent: {stderr}"
);
let after = fs::read_to_string(dir.path().join("generated/user.zod.gen.ts")).unwrap();
assert_eq!(before, after, "an up-to-date artifact is not rewritten");
}
#[test]
fn serve_startup_tolerates_missing_generated_dir() {
let dir = repo().unwrap();
let stderr = serve_once(dir.path()).unwrap();
assert!(
!stderr.contains("stale epoch"),
"no generated/ dir means nothing to probe: {stderr}"
);
}