use assert_cmd::Command;
use serde_json::Value;
use std::fs;
use std::path::{Path, PathBuf};
fn scaffold_profile(root: &Path, profile: &str, name: &str) -> PathBuf {
let project = root.join(name);
Command::cargo_bin("node-app")
.unwrap()
.args(["new", name, "--profile", profile, "--out"])
.arg(&project)
.args(["--yes"])
.assert()
.success();
project
}
fn read_manifest(project: &Path) -> Value {
serde_json::from_slice(&fs::read(project.join("manifest.json")).unwrap()).unwrap()
}
fn stage_minimum_build_outputs(project: &Path) {
let manifest = read_manifest(project);
let name = manifest["name"].as_str().unwrap();
match manifest["app_type"].as_str().unwrap() {
"bun" => {
let dist = project.join("dist");
fs::create_dir_all(&dist).unwrap();
fs::write(dist.join("index.js"), "console.log('ok');\n").unwrap();
}
"native" => {
let entrypoint = manifest["entrypoint"].as_str().unwrap();
let built = project.join("target/node-app/amd64");
fs::create_dir_all(&built).unwrap();
fs::write(built.join(entrypoint), b"\x7fELF-fake").unwrap();
let workflows = project.join(".github/workflows");
fs::create_dir_all(&workflows).unwrap();
fs::write(
workflows.join("release.yml"),
"name: release\njobs:\n sign:\n runs-on: ubuntu-latest\n steps:\n - run: gpg --detach-sign manifest.json # manifest sidecar\n - run: echo SIDECAR\n",
)
.unwrap();
}
"standalone" => {
let built = project.join("target/node-app/amd64");
fs::create_dir_all(&built).unwrap();
fs::write(built.join(format!("node-app-{name}")), b"\x7fELF-fake").unwrap();
}
other => panic!("unexpected app_type {other}"),
}
if manifest["has_ui"].as_bool().unwrap_or(false) {
let ui_root = project.join(manifest["ui_path"].as_str().unwrap_or("ui/dist"));
fs::create_dir_all(&ui_root).unwrap();
fs::write(
ui_root.join("index.html"),
"<!doctype html><title>ok</title>\n",
)
.unwrap();
if let Some(entry) = manifest
.get("ui")
.and_then(|ui| ui.get("entry"))
.and_then(|value| value.as_str())
{
let entry_path = project.join(entry);
if let Some(parent) = entry_path.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(entry_path, "console.log('ui ok');\n").unwrap();
}
if let Some(icon) = manifest
.get("ui")
.and_then(|ui| ui.get("icon"))
.and_then(|value| value.as_str())
{
let icon_path = project.join(icon);
if let Some(parent) = icon_path.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(
icon_path,
"<svg xmlns=\"http://www.w3.org/2000/svg\"></svg>\n",
)
.unwrap();
}
}
}
fn stage_package(project: &Path, arch: &str, out: &Path) {
Command::cargo_bin("node-app")
.unwrap()
.current_dir(project)
.env("NODE_APP_PACKAGE_STAGE_ONLY", "1")
.args(["package", "--arch", arch, "--out"])
.arg(out)
.assert()
.success();
}
fn read_trigger_file(staging_root: &Path) -> Option<String> {
fs::read_to_string(staging_root.join("DEBIAN/triggers")).ok()
}
#[test]
fn package_triggers_platform_loaded_profiles_emit_activate_noawait_only() {
let temp = tempfile::tempdir().unwrap();
for (profile, arch) in [
("bun", "all"),
("bun-stage", "all"),
("bun-fullstack", "all"),
("native", "amd64"),
("native-fullstack", "amd64"),
] {
let name = format!("trigger-{}", profile.replace('-', "-x-"));
let project = scaffold_profile(temp.path(), profile, &name);
stage_minimum_build_outputs(&project);
let out = project.join("target/test-package-triggers");
stage_package(&project, arch, &out);
let trigger_text = read_trigger_file(&out.join("staging"))
.unwrap_or_else(|| format!("missing DEBIAN/triggers for profile {profile}"));
assert_eq!(
trigger_text, "activate-noawait /usr/share/node/triggers/apps\n",
"unexpected trigger contract for profile {profile}"
);
assert!(
!trigger_text.contains("interest-noawait"),
"profile {profile} must not declare interest-noawait:\n{trigger_text}"
);
assert!(
!trigger_text.contains("dpkg-trigger"),
"profile {profile} must not shell out to dpkg-trigger:\n{trigger_text}"
);
assert!(
!out.join("staging/DEBIAN/triggered").exists(),
"profile {profile} must not stage a DEBIAN/triggered callback"
);
}
}
#[test]
fn package_triggers_standalone_profiles_remain_trigger_free() {
let temp = tempfile::tempdir().unwrap();
for profile in ["standalone-bun", "standalone-native"] {
let name = format!("trigger-{}", profile.replace('-', "-x-"));
let project = scaffold_profile(temp.path(), profile, &name);
stage_minimum_build_outputs(&project);
let out = project.join("target/test-package-triggers");
stage_package(&project, "amd64", &out);
assert!(
read_trigger_file(&out.join("staging")).is_none(),
"standalone profile {profile} must not stage DEBIAN/triggers"
);
}
}