mod support;
use std::error::Error;
use std::fs;
use std::path::Path;
use std::process::{Command, Output};
use frame_cli::NewError;
use support::{TestDirectory, generate, generated_files, options};
type TestResult = Result<(), Box<dyn Error>>;
#[test]
fn generated_project_passes_all_gates_and_real_browser_e2e_untouched() -> TestResult {
require_gleam()?;
require_npm()?;
require_chrome()?;
let directory = TestDirectory::new("all-gates")?;
let project = generate(&options(directory.path(), "gate_app"))?;
assert_page_pipeline(&project)?;
let before = generated_files(&project)?;
assert_success(
run(&project, "cargo", &["fmt", "--all"])?,
"cargo fmt --all",
)?;
assert_success(
run(
&project,
"cargo",
&[
"clippy",
"--workspace",
"--all-targets",
"--",
"-D",
"warnings",
],
)?,
"cargo clippy --workspace --all-targets -- -D warnings",
)?;
let tree_output = run(&project, "cargo", &["tree", "--duplicates"])?;
if !tree_output.status.success() {
return Err(format!(
"cargo tree --duplicates failed with {}\nstdout:\n{}\nstderr:\n{}",
tree_output.status,
String::from_utf8_lossy(&tree_output.stdout),
String::from_utf8_lossy(&tree_output.stderr)
)
.into());
}
let tree_stdout = String::from_utf8_lossy(&tree_output.stdout).into_owned();
let offenders = duplicate_offenders(&tree_stdout);
assert!(
offenders.is_empty(),
"cargo tree --duplicates reported duplicate Frame-stack crates (B6 regression): \
{offenders:#?}\nfull output:\n{tree_stdout}"
);
let frame_test = run(&project, env!("CARGO_BIN_EXE_frame"), &["test"])?;
let frame_test_stdout = String::from_utf8_lossy(&frame_test.stdout).into_owned();
assert_success(frame_test, "frame test (full default verdict)")?;
assert!(
frame_test_stdout.contains("frame test: PASS"),
"frame test exited 0 without printing its one PASS verdict:\n{frame_test_stdout}"
);
for scope in ["component", "host", "browser"] {
assert!(
frame_test_stdout.contains(scope),
"frame test's default verdict must name the {scope} scope:\n{frame_test_stdout}"
);
}
let mut doc = Command::new("cargo");
doc.args(["doc", "--workspace", "--no-deps"])
.env("RUSTDOCFLAGS", "-D warnings")
.current_dir(&project);
assert_success(
doc.output()?,
"RUSTDOCFLAGS=-D warnings cargo doc --workspace --no-deps",
)?;
assert_eq!(
before,
generated_files(&project)?,
"gates changed generated source"
);
Ok(())
}
#[test]
fn generated_manifests_pin_the_actual_dependency_set_and_files_stay_bounded() -> TestResult {
let directory = TestDirectory::new("manifest-pins")?;
let project = generate(&options(directory.path(), "pinned_app"))?;
let root_manifest = fs::read_to_string(project.join("Cargo.toml"))?;
assert!(root_manifest.contains("frame-core = { version = \"=0.2.0\" }"));
assert!(root_manifest.contains("frame-state = { version = \"=0.2.0\" }"));
assert!(root_manifest.contains("frame-host = { version = \"=0.3.0\" }"));
assert!(
!root_manifest.contains("beamr"),
"the unified scaffold carries zero beamr manifest entries (B6): the runtime and its \
feature selection are frame-core's own published dependency"
);
assert!(
!root_manifest.contains("path ="),
"registry-pinned scaffold must carry no path dependencies"
);
assert!(!root_manifest.contains("haematite ="));
for (path, bytes) in generated_files(&project)? {
assert!(
!bytes
.windows(b"{{FRAME_ROOT}}".len())
.any(|window| window == b"{{FRAME_ROOT}}"),
"{} carried {{{{FRAME_ROOT}}}} residue",
path.display()
);
}
let host_source = fs::read_to_string(project.join("host/src/lib.rs"))?;
assert!(host_source.contains("Process observation is event-driven"));
assert!(!host_source.contains("observation_interval"));
assert!(!host_source.contains("OBSERVATION_INTERVAL"));
assert!(
!host_source.contains("beamr"),
"the generated host must carry no beamr token (B6)"
);
let host_manifest = fs::read_to_string(project.join("host/Cargo.toml"))?;
let dependency_block = host_manifest
.split_once("[dependencies]")
.and_then(|(_, tail)| tail.split_once("[lints]"))
.map(|(dependencies, _)| dependencies)
.ok_or("generated host manifest lacked its dependency block")?;
assert_eq!(
dependency_block
.lines()
.filter(|line| !line.trim().is_empty())
.collect::<Vec<_>>(),
[
"frame-core = { workspace = true }",
"frame-state = { workspace = true }",
"frame-host = { workspace = true }",
"serde_json = { workspace = true }",
"tracing = { workspace = true }",
"tracing-subscriber = { workspace = true }",
]
);
let page_manifest = fs::read_to_string(project.join("page/package.json"))?;
assert!(page_manifest.contains("\"@ablative/liminal\""));
assert!(
!page_manifest.contains("file:") && !page_manifest.contains("link:"),
"the page manifest must carry no file:/link: dependencies (registry packages only)"
);
assert!(
!page_manifest.contains("\"dev\""),
"the page manifest must declare no dev script: there is no dev server"
);
assert!(
page_manifest.contains("\"build\": \"tsc\""),
"the page build must be tsc alone — no bundler"
);
assert!(
page_manifest.contains("\"typescript\": \"5.8.3\""),
"typescript must be pinned exactly: the scaffold ships pre-compiled page modules whose \
byte-identity with a real build is only stable against one pinned compiler"
);
assert!(
!page_manifest.to_lowercase().contains("vite"),
"the page manifest must carry no vite dependency or script"
);
assert!(
!project.join("page/vite.config.ts").exists(),
"no bundler config may be generated"
);
assert_servable_root_shape(&project)?;
for (path, bytes) in generated_files(&project)? {
if path.starts_with("page/dist/vendor") {
continue;
}
assert!(
bytes.split(|byte| *byte == b'\n').count() < 500,
"{} exceeded the 500-line cap",
path.display()
);
}
Ok(())
}
fn assert_page_pipeline(project: &Path) -> TestResult {
const COMPILED_MODULES: [&str; 4] = ["config.js", "app-status.js", "connection.js", "main.js"];
let shipped_compiled: Vec<(String, Vec<u8>)> = COMPILED_MODULES
.iter()
.map(|module| {
fs::read(project.join("page/dist").join(module))
.map(|bytes| ((*module).to_owned(), bytes))
})
.collect::<Result<_, _>>()?;
assert_success(
run(&project.join("page"), "npm", &["install"])?,
"npm --prefix page install",
)?;
assert!(
!project.join("page/node_modules/vite").exists()
&& !project.join("page/node_modules/.bin/vite").exists(),
"vite must not be installed: the page builds with tsc alone"
);
assert_success(
run(&project.join("page"), "npm", &["run", "build"])?,
"npm --prefix page run build",
)?;
let emitted_main = fs::read_to_string(project.join("page/dist/main.js"))?;
for specifier in ["./app-status.js", "./config.js", "./connection.js"] {
assert!(
emitted_main.contains(&format!("from \"{specifier}\"")),
"emitted main.js lost its explicit-extension relative import of {specifier}"
);
}
let emitted_connection = fs::read_to_string(project.join("page/dist/connection.js"))?;
assert!(
emitted_connection.contains("from \"@ablative/liminal\""),
"emitted connection.js lost the bare @ablative/liminal specifier the import map serves"
);
for (module, shipped_bytes) in &shipped_compiled {
let rebuilt = fs::read(project.join("page/dist").join(module))?;
assert_eq!(
&rebuilt, shipped_bytes,
"page/dist/{module}: the scaffold's shipped pre-compiled module must be \
byte-identical to the pinned tsc's own output over the generated sources — \
regenerate the page-dist-* templates with the pinned typescript"
);
}
let lockfile = fs::read_to_string(project.join("page/package-lock.json"))?;
assert!(
!lockfile.contains("file:") && !lockfile.contains("\"link\""),
"the resolved page lockfile must carry no file:/link: entries (registry packages only)"
);
Ok(())
}
fn assert_servable_root_shape(project: &Path) -> TestResult {
let index_html = fs::read_to_string(project.join("page/dist/index.html"))?;
assert!(
index_html.contains("<script type=\"importmap\">")
&& index_html.contains("\"@ablative/liminal\": \"./vendor/liminal.js\""),
"dist/index.html must serve the import map wiring @ablative/liminal to ./vendor/liminal.js"
);
assert!(
index_html.contains("<script type=\"module\" src=\"./main.js\"></script>")
&& index_html.contains("<link rel=\"stylesheet\" href=\"./styles.css\" />"),
"dist/index.html must load ./main.js as a module and link ./styles.css"
);
for module in ["main.js", "config.js", "app-status.js", "connection.js"] {
assert!(
project.join("page/dist").join(module).is_file(),
"page/dist/{module} must ship with the scaffold: frame run needs no npm"
);
}
let vendored_sdk = fs::read_to_string(project.join("page/dist/vendor/liminal.js"))?;
assert_eq!(
vendored_sdk.len(),
155_614,
"the vendored SDK artifact must be the exact published build (155,614 bytes)"
);
assert!(
vendored_sdk.contains("LiminalFeedSource"),
"the vendored SDK must carry the feed source the page imports"
);
assert!(
!vendored_sdk.contains("import("),
"the vendored SDK must be self-contained: no dynamic import may remain"
);
Ok(())
}
#[test]
fn generation_is_byte_deterministic_and_frame_core_name_cannot_collide() -> TestResult {
let left = TestDirectory::new("deterministic-left")?;
let right = TestDirectory::new("deterministic-right")?;
let first = generate(&options(left.path(), "same_app"))?;
let second = generate(&options(right.path(), "same_app"))?;
assert_eq!(tree_hash(&first)?, tree_hash(&second)?);
let collision = generate(&options(left.path(), "frame-core"));
assert!(matches!(collision, Err(NewError::InvalidBothNames { .. })));
Ok(())
}
#[test]
fn existing_target_is_typed_and_untouched() -> TestResult {
let directory = TestDirectory::new("existing")?;
let target = directory.path().join("kept_app");
fs::create_dir(&target)?;
fs::write(target.join("marker"), b"untouched")?;
let result = generate(&options(directory.path(), "kept_app"));
assert!(matches!(result, Err(NewError::TargetExists { .. })));
assert_eq!(fs::read(target.join("marker"))?, b"untouched");
Ok(())
}
#[test]
fn missing_gleam_is_actionable_and_never_skipped() -> TestResult {
let directory = TestDirectory::new("missing-gleam")?;
let project = generate(&options(directory.path(), "tool_app"))?;
let builder = directory.path().join("generated-build-script");
assert_success(
Command::new("rustc")
.args(["--edition", "2024"])
.arg(project.join("host/build.rs"))
.arg("-o")
.arg(&builder)
.output()?,
"compile generated build script",
)?;
let output = Command::new(builder)
.current_dir(project.join("host"))
.env("PATH", directory.path())
.env("OUT_DIR", directory.path())
.output()?;
assert!(!output.status.success());
let message = String::from_utf8_lossy(&output.stderr);
assert!(message.contains("Gleam toolchain is required"));
assert!(message.contains("https://gleam.run/getting-started/installing/"));
Ok(())
}
fn duplicate_offenders(tree_output: &str) -> Vec<String> {
tree_output
.lines()
.filter(|line| line.starts_with(|c: char| c.is_ascii_alphanumeric()))
.filter(|line| {
let name = line.split_whitespace().next().unwrap_or("");
name == "beamr" || name == "haematite" || name == "frame" || name.starts_with("frame-")
})
.map(str::to_owned)
.collect()
}
fn require_gleam() -> Result<(), Box<dyn Error>> {
let output = Command::new("gleam").arg("--version").output().map_err(|error| {
std::io::Error::new(
error.kind(),
format!("Gleam is mandatory for scaffold acceptance; install it from https://gleam.run/getting-started/installing/: {error}"),
)
})?;
assert_success(output, "mandatory gleam --version")
}
fn require_npm() -> Result<(), Box<dyn Error>> {
let output = Command::new("npm").arg("--version").output().map_err(|error| {
std::io::Error::new(
error.kind(),
format!("npm is mandatory for scaffold acceptance; install Node.js 20+ from https://nodejs.org/: {error}"),
)
})?;
assert_success(output, "mandatory npm --version")
}
fn require_chrome() -> Result<(), Box<dyn Error>> {
const DEFAULT_CHROME: &str = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
let path = std::env::var("CHROME_BIN").unwrap_or_else(|_| DEFAULT_CHROME.to_owned());
require_chrome_at(&path)
}
fn require_chrome_at(path: &str) -> Result<(), Box<dyn Error>> {
if Path::new(path).is_file() {
return Ok(());
}
Err(format!(
"installed Chrome is mandatory for the scaffold's real-browser e2e gate; no executable \
found at `{path}`. playwright-core does not bundle or download a browser: set \
CHROME_BIN to an installed Chrome/Chromium executable, or install Google Chrome at the \
default macOS path."
)
.into())
}
fn run(directory: &Path, program: &str, args: &[&str]) -> Result<Output, std::io::Error> {
Command::new(program)
.args(args)
.current_dir(directory)
.output()
}
fn assert_success(output: Output, command: &str) -> Result<(), Box<dyn Error>> {
let Output {
status,
stdout,
stderr,
} = output;
if status.success() {
return Ok(());
}
Err(format!(
"{command} failed with {status}\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&stdout),
String::from_utf8_lossy(&stderr)
)
.into())
}
fn tree_hash(root: &Path) -> Result<blake3::Hash, std::io::Error> {
let mut hasher = blake3::Hasher::new();
for (path, bytes) in generated_files(root)? {
let path = path.to_string_lossy();
hasher.update(&(path.len() as u64).to_le_bytes());
hasher.update(path.as_bytes());
hasher.update(&(bytes.len() as u64).to_le_bytes());
hasher.update(&bytes);
}
Ok(hasher.finalize())
}
#[cfg(test)]
mod duplicate_offenders_tests {
use super::duplicate_offenders;
const SEEDED_DUPLICATE_EXCERPT: &str = "\
beamr v0.13.0
└── app-host v0.1.0 (/tmp/example_app/host)
beamr v0.15.4
├── frame-core v0.2.0
│ └── app-host v0.1.0 (/tmp/example_app/host) (*)
└── frame-host v0.2.0 (*)
";
const CLEAN_EXCERPT: &str = "\
bitflags v1.3.2
└── region v3.0.2
└── cranelift-jit v0.131.3
└── beamr v0.15.4 (*)
syn v2.0.119
└── clap_derive v4.5.41 (proc-macro)
└── clap v4.5.41 (*)
syn v3.0.2
└── async-trait v0.1.91 (proc-macro)
└── liminal-rs v0.3.1 (*)
";
#[test]
fn flags_a_seeded_duplicate_beamr() {
let offenders = duplicate_offenders(SEEDED_DUPLICATE_EXCERPT);
assert_eq!(offenders, vec!["beamr v0.13.0", "beamr v0.15.4"]);
}
#[test]
fn does_not_flag_ordinary_transitive_duplicates() {
assert!(duplicate_offenders(CLEAN_EXCERPT).is_empty());
}
}
#[cfg(test)]
mod require_chrome_tests {
use super::require_chrome_at;
#[test]
fn refuses_loudly_when_chrome_binary_is_missing() {
let message = require_chrome_at("/nonexistent/definitely-not-chrome")
.err()
.map(|error| error.to_string())
.unwrap_or_default();
assert!(message.contains("installed Chrome is mandatory"));
assert!(message.contains("playwright-core does not bundle or download a browser"));
}
}