use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::sync::atomic::{AtomicUsize, Ordering};
use slave_framework::testing::TestDatabase;
struct TempDir(PathBuf);
impl TempDir {
fn new() -> Self {
static COUNTER: AtomicUsize = AtomicUsize::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let dir = std::env::temp_dir().join(format!("slave-test-{}-{n}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).expect("create temp dir");
Self(dir)
}
fn new_app(&self) -> PathBuf {
let out = slave(
&self.0,
&["new", "app", "--framework-path", &framework_path()],
);
assert!(out.status.success(), "{}", stderr(&out));
self.0.join("app")
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
fn framework_path() -> String {
format!("{}/framework", env!("CARGO_MANIFEST_DIR"))
}
fn slave(dir: &Path, args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_slave"))
.args(args)
.current_dir(dir)
.output()
.expect("run slave")
}
fn stderr(output: &Output) -> String {
String::from_utf8_lossy(&output.stderr).into_owned()
}
fn read(path: impl AsRef<Path>) -> String {
fs::read_to_string(path.as_ref())
.unwrap_or_else(|err| panic!("read {}: {err}", path.as_ref().display()))
}
const SKELETON: &[&str] = &[
"Cargo.toml",
"AGENTS.md",
"CLAUDE.md",
".env.example",
".env",
"src/main.rs",
"src/app.rs",
"src/base/mod.rs",
"src/base/auth.rs",
"src/base/mcp.rs",
"src/base/config.rs",
"src/base/error.rs",
"src/base/state.rs",
"src/modules/mod.rs",
"src/modules/user/mod.rs",
"src/modules/user/tools.rs",
"src/modules/user/models/user.rs",
"src/modules/user/services/user_service.rs",
"src/modules/user/dto/user.rs",
"src/modules/role/mod.rs",
"src/modules/role/tools.rs",
"src/modules/role/models/role.rs",
"src/modules/role/services/role_service.rs",
"src/modules/role/dto/role.rs",
"src/modules/auth/mod.rs",
"src/modules/auth/tools.rs",
"src/modules/auth/services/auth_service.rs",
"src/modules/auth/dto/auth.rs",
"src/lib.rs",
"src/bin/migrate.rs",
"config/apps.toml",
"apps/README.md",
"apps/admin/Cargo.toml",
"apps/admin/src/pages.rs",
"apps/admin/templates/login.html",
"apps/admin/templates/team.html",
"src/modules/team/services/access.rs",
"src/modules/app/tools.rs",
"runtime/Cargo.toml",
"runtime/src/main.rs",
"runtime/src/apps.rs",
"tests/mcp.rs",
];
#[test]
fn new_creates_core_application() {
let tmp = TempDir::new();
let app = tmp.new_app();
for file in SKELETON {
assert!(app.join(file).is_file(), "missing {file}");
}
let manifest = read(app.join("Cargo.toml"));
assert!(manifest.contains("name = \"app\""));
assert!(manifest.contains("[package.metadata.slave]"));
assert!(manifest.contains("slave-framework = { path = "));
assert!(manifest.contains("default-run = \"app\""));
assert!(
manifest.contains("members = [\"apps/*\", \"runtime\"]"),
"{manifest}"
);
assert!(
manifest.contains("slave-gateway = { path = ")
&& manifest.contains("default-features = false }"),
"{manifest}"
);
assert!(!app.join("gateway").exists());
let runtime = read(app.join("runtime/Cargo.toml"));
assert!(runtime.contains("app = { path = \"..\" }"), "{runtime}");
assert!(
runtime.contains("slave-gateway = { workspace = true, features = [\"embed\"] }"),
"{runtime}"
);
assert!(read(app.join("runtime/src/apps.rs")).contains("// slave:apps"));
assert!(read(app.join("config/apps.toml")).contains("app = \"mcp\""));
assert!(read(app.join("CLAUDE.md")).contains("@AGENTS.md"));
let agents = read(app.join("AGENTS.md"));
assert!(agents.starts_with("# AGENTS.md — app"), "{agents}");
assert!(agents.contains("No REST business endpoints"));
let modules = read(app.join("src/modules/mod.rs"));
for module in ["auth", "role", "user"] {
assert!(modules.contains(&format!("pub mod {module};")), "{modules}");
assert!(
modules.contains(&format!("{module}::tools(),")),
"{modules}"
);
}
let env = read(app.join(".env"));
assert!(
env.contains("SURREAL_URL=ws://127.0.0.1:8000") && env.contains("SURREAL_NS=app"),
"{env}"
);
assert!(!env.contains("TURSO_"), "{env}");
assert!(read(app.join(".gitignore")).contains(".env.*"));
}
#[test]
fn new_application_has_no_rest_business_api() {
let tmp = TempDir::new();
let app = tmp.new_app();
for dir in ["auth", "role", "user"] {
let domain = app.join("src/modules").join(dir);
assert!(!domain.join("routes.rs").exists(), "{dir}/routes.rs");
assert!(!domain.join("controllers").exists(), "{dir}/controllers");
}
let router = read(app.join("src/app.rs"));
assert_eq!(router.matches(".route(").count(), 1, "{router}");
assert!(
router.contains(".route(\"/health\", get(health))"),
"{router}"
);
assert!(router.contains(".nest_service(mcp::PATH,"), "{router}");
assert!(!router.contains(".merge("), "{router}");
assert!(read(app.join("src/base/mcp.rs")).contains("pub const PATH: &str = \"/mcp\";"));
}
#[test]
fn new_defaults_to_crates_io_framework() {
let tmp = TempDir::new();
assert!(slave(&tmp.0, &["new", "app"]).status.success());
let manifest = read(tmp.0.join("app/Cargo.toml"));
assert!(
manifest.contains("slave-framework = { version = \"0.12\", default-features = false }"),
"{manifest}"
);
assert!(
manifest.contains("slave-gateway = { version = \"0.12\", default-features = false }"),
"{manifest}"
);
assert!(
!slave(&tmp.0, &["new", "routed", "--gateway"])
.status
.success()
);
assert!(!tmp.0.join("routed").exists());
}
#[test]
fn runtime_mounts_apps_and_runs_separate_ones() {
let tmp = TempDir::new();
let app = tmp.new_app();
let mounted = |name: &str| {
read(app.join("runtime/src/apps.rs"))
.contains(&format!("apps.app(\"{name}\", {name}::embedded)?;"))
&& read(app.join("runtime/Cargo.toml"))
.contains(&format!("{name} = {{ path = \"../apps/{name}\" }}"))
};
let out = slave(&app, &["make:app", "tool"]);
assert!(out.status.success(), "{}", stderr(&out));
assert!(read(app.join("config/apps.toml")).contains("app = \"tool\"\nport = 4001"));
assert!(mounted("tool"));
let list = String::from_utf8_lossy(&slave(&app, &["route:list"]).stdout).into_owned();
assert!(list.contains("(direct)"), "{list}");
assert!(list.contains("runtime: http://"), "{list}");
let out = slave(
&app,
&[
"make:app",
"dashboard",
"--domain",
"dash.localhost",
"--separate",
],
);
assert!(out.status.success(), "{}", stderr(&out));
assert!(
read(app.join("config/apps.toml"))
.contains("app = \"dashboard\"\nport = 4002\nembed = false"),
"{}",
read(app.join("config/apps.toml"))
);
assert!(!mounted("dashboard"));
let out = slave(&app, &["build", "--list"]);
assert!(out.status.success(), "{}", stderr(&out));
let list = String::from_utf8_lossy(&out.stdout).into_owned();
for expected in ["backend", "runtime/**", "dashboard", "apps/dashboard/**"] {
assert!(list.contains(expected), "missing `{expected}` in:\n{list}");
}
assert!(!list.contains("apps/tool/**"), "{list}");
let out = slave(&app, &["build", "nope"]);
assert!(!out.status.success());
assert!(
stderr(&out).contains("unknown component `nope`"),
"{}",
stderr(&out)
);
let out = slave(&app, &["app:embed", "dashboard"]);
assert!(out.status.success(), "{}", stderr(&out));
assert!(mounted("dashboard"));
assert!(
!read(app.join("config/apps.toml"))
.lines()
.any(|line| line.starts_with("embed")),
"embed removed"
);
let out = slave(&app, &["app:separate", "tool"]);
assert!(out.status.success(), "{}", stderr(&out));
assert!(!mounted("tool"));
assert!(read(app.join("config/apps.toml")).contains("app = \"tool\"\nembed = false"));
assert!(!slave(&app, &["app:separate", "nope"]).status.success());
assert!(!slave(&app, &["install:gateway"]).status.success());
assert!(!app.join("gateway").exists());
}
#[test]
fn serve_requires_a_build() {
let tmp = TempDir::new();
let app = tmp.new_app();
let out = slave(&app, &["serve", "--port", "39123"]);
assert!(!out.status.success());
assert!(
stderr(&out).contains("run `slave build runtime` first"),
"{}",
stderr(&out)
);
}
#[test]
fn new_refuses_existing_directory_and_bad_input() {
let tmp = TempDir::new();
fs::create_dir(tmp.0.join("taken")).unwrap();
assert!(!slave(&tmp.0, &["new", "taken"]).status.success());
assert!(!slave(&tmp.0, &["new", "../escape"]).status.success());
assert!(!slave(&tmp.0, &["new", "has space"]).status.success());
let out = slave(&tmp.0, &["new", "x", "--framework-path", "/nonexistent"]);
assert!(!out.status.success());
assert!(!tmp.0.join("x").exists());
}
#[test]
fn make_model_creates_domain_with_default_module() {
let tmp = TempDir::new();
let app = tmp.new_app();
let out = slave(&app, &["make:model", "BlogPost"]);
assert!(out.status.success(), "{}", stderr(&out));
let domain = app.join("src/modules/blog_post");
assert!(read(domain.join("models/blog_post.rs")).contains("pub struct BlogPost"));
assert!(read(domain.join("models/mod.rs")).contains("pub mod blog_post;"));
assert!(read(domain.join("mod.rs")).contains("mod models;"));
assert!(read(app.join("src/modules/mod.rs")).contains("pub mod blog_post;"));
}
#[test]
fn make_model_supports_several_models_per_module() {
let tmp = TempDir::new();
let app = tmp.new_app();
let out = slave(&app, &["make:model", "Profile", "--module", "user"]);
assert!(out.status.success(), "{}", stderr(&out));
let models = read(app.join("src/modules/user/models/mod.rs"));
assert_eq!(models, "pub mod profile;\npub mod user;\n");
assert!(app.join("src/modules/user/models/profile.rs").is_file());
assert_eq!(
read(app.join("src/modules/user/mod.rs"))
.matches("mod models;")
.count(),
1
);
}
#[test]
fn make_tools_creates_and_registers_tools() {
let tmp = TempDir::new();
let app = tmp.new_app();
assert!(slave(&app, &["make:model", "Post"]).status.success());
let out = slave(&app.join("src"), &["make:tools", "PostTools"]);
assert!(out.status.success(), "{}", stderr(&out));
let domain = read(app.join("src/modules/post/mod.rs"));
for line in ["mod models;", "mod tools;", "pub use tools::tools;"] {
assert!(domain.contains(line), "missing `{line}` in:\n{domain}");
}
let tools = read(app.join("src/modules/post/tools.rs"));
for expected in [
"#[tool_router(router = post_tools,",
"McpServer::post_tools()",
"async fn post_list(",
"async fn post_get(",
"self.authenticate(&parts)",
] {
assert!(
tools.contains(expected),
"missing `{expected}` in:\n{tools}"
);
}
assert!(!app.join("src/modules/post/routes.rs").exists());
assert!(!app.join("src/modules/post/controllers").exists());
let modules = read(app.join("src/modules/mod.rs"));
assert_eq!(modules.matches("pub mod post;").count(), 1, "{modules}");
assert_eq!(modules.matches("post::tools(),").count(), 1, "{modules}");
let out = slave(&app, &["make:tools", "PostTools"]);
assert!(!out.status.success());
assert_eq!(read(app.join("src/modules/mod.rs")), modules);
}
#[test]
fn make_accepts_explicit_module() {
let tmp = TempDir::new();
let app = tmp.new_app();
let args = ["make:tools", "AdminTools", "--module", "admin_panel"];
let out = slave(&app, &args);
assert!(out.status.success(), "{}", stderr(&out));
let tools = read(app.join("src/modules/admin_panel/tools.rs"));
assert!(tools.contains("McpServer::admin_panel_tools()"), "{tools}");
assert!(tools.contains("async fn admin_list("), "{tools}");
let modules = read(app.join("src/modules/mod.rs"));
assert!(modules.contains("admin_panel::tools(),"), "{modules}");
}
#[test]
fn make_controller_is_gone() {
let tmp = TempDir::new();
let app = tmp.new_app();
let out = slave(&app, &["make:controller", "PostController"]);
assert!(!out.status.success());
assert!(!app.join("src/modules/post").exists());
}
fn routed_hosts(app: &Path) -> Vec<String> {
read(app.join("config/apps.toml"))
.lines()
.filter_map(|line| line.trim().strip_prefix("host = "))
.map(|host| host.trim_matches('"').to_owned())
.collect()
}
#[test]
fn make_app_creates_an_axum_app_and_routes_its_domain() {
let tmp = TempDir::new();
let app = tmp.new_app();
let out = slave(
&app,
&["make:app", "storefront", "--domain", "{toko}.localhost"],
);
assert!(out.status.success(), "{}", stderr(&out));
let out = slave(
&app.join("src"),
&["make:app", "dashboard", "--domain", "dash.localhost"],
);
assert!(out.status.success(), "{}", stderr(&out));
for file in [
"Cargo.toml",
"src/lib.rs",
"src/main.rs",
"src/mcp.rs",
"src/pages.rs",
"src/views.rs",
"templates/layout.html",
"templates/home.html",
"templates/login.html",
"src/sign_in.rs",
] {
assert!(
app.join("apps/dashboard").join(file).is_file(),
"missing {file}"
);
}
let manifest = read(app.join("apps/dashboard/Cargo.toml"));
assert!(manifest.contains("name = \"dashboard\""), "{manifest}");
assert!(
manifest.contains("slave-framework.workspace = true"),
"{manifest}"
);
assert!(read(app.join("apps/dashboard/src/lib.rs")).contains("DEFAULT_PORT: u16 = 4002;"));
assert!(read(app.join("apps/storefront/src/lib.rs")).contains("DEFAULT_PORT: u16 = 4001;"));
assert!(read(app.join("apps/dashboard/src/lib.rs")).contains("TITLE: &str = \"Dashboard\";"));
assert!(read(app.join("apps/dashboard/src/main.rs")).contains("dashboard::serve()"));
assert!(manifest.contains("minijinja = "), "{manifest}");
assert!(read(app.join("apps/dashboard/src/views.rs")).contains("minijinja::Environment"));
assert_eq!(
routed_hosts(&app),
[
"mcp.localhost",
"admin.localhost",
"{toko}.localhost",
"dash.localhost"
]
);
let out = slave(&app, &["route:match", "dash.localhost"]);
assert!(String::from_utf8_lossy(&out.stdout).contains("→ dashboard"));
let out = slave(&app, &["route:list"]);
assert!(out.status.success(), "{}", stderr(&out));
let list = String::from_utf8_lossy(&out.stdout);
for expected in [
"dash.localhost",
"dashboard",
"SERVED BY",
"runtime",
"STATUS",
"4 entries",
] {
assert!(list.contains(expected), "missing `{expected}` in:\n{list}");
}
let out = slave(&app, &["route:match", "TokoX.localhost:8000"]);
assert!(out.status.success(), "{}", stderr(&out));
let matched = String::from_utf8_lossy(&out.stdout);
assert!(matched.contains("storefront (127.0.0.1:4001)"), "{matched}");
assert!(matched.contains("x-slave-param-toko: tokox"), "{matched}");
let out = slave(&app, &["route:match", "a.b.localhost"]);
assert!(!out.status.success());
assert!(stderr(&out).contains("no route matches"));
}
#[test]
fn make_app_rejects_unknown_stacks() {
let tmp = TempDir::new();
let app = tmp.new_app();
for args in [
&["make:app", "x", "--domain", "x.localhost", "--stack", "vue"][..],
&["make:app", "x", "--stack", "axum"],
&["make:app", "x", "--stack", "dioxus"],
] {
assert!(!slave(&app, args).status.success(), "{args:?}");
}
assert!(!app.join("apps/x").exists());
}
#[test]
fn make_app_uses_the_chosen_template_engine() {
let tmp = TempDir::new();
let app = tmp.new_app();
for (engine, dependency, view) in [
("tera", "tera = ", "templates/home.html"),
("handlebars", "handlebars = ", "templates/home.hbs"),
("liquid", "liquid = ", "templates/home.liquid"),
] {
let out = slave(&app, &["make:app", engine, "--stack", engine]);
assert!(out.status.success(), "{}", stderr(&out));
let dir = app.join("apps").join(engine);
let manifest = read(dir.join("Cargo.toml"));
assert!(manifest.contains(dependency), "{manifest}");
assert!(!manifest.contains("minijinja"), "{manifest}");
assert!(dir.join(view).is_file(), "{engine}: missing {view}");
assert!(dir.join("src/views.rs").is_file());
}
}
#[test]
fn make_app_routes_paths_on_one_domain() {
let tmp = TempDir::new();
let app = tmp.new_app();
for args in [
&["make:app", "site", "--domain", "localhost"][..],
&["make:app", "dashboard", "--path", "/admin"],
&["make:app", "console", "--path", "/console"],
&[
"make:app",
"panel",
"--domain",
"localhost",
"--path",
"/panel",
],
] {
let out = slave(&app, args);
assert!(out.status.success(), "{args:?}: {}", stderr(&out));
}
let config = read(app.join("config/apps.toml"));
assert!(
config.contains("path = \"/admin\"\napp = \"dashboard\""),
"{config}"
);
assert!(
config.contains("host = \"localhost\"\npath = \"/panel\""),
"{config}"
);
let matches = |target: &str| {
let out = slave(&app, &["route:match", target]);
assert!(out.status.success(), "{target}: {}", stderr(&out));
String::from_utf8_lossy(&out.stdout).into_owned()
};
assert!(matches("localhost:8000/admin/users").contains("→ dashboard"));
assert!(matches("localhost/administrator").contains("→ site"));
assert!(matches("localhost/panel").contains("→ panel"));
assert!(matches("example.com/console/x").contains("→ console"));
assert!(matches("localhost").contains("→ site"));
for args in [
&["make:app", "other", "--path", "/admin"][..],
&["make:app", "other", "--path", "admin"],
&["make:app", "other", "--path", "/admin/"],
] {
assert!(!slave(&app, args).status.success(), "{args:?}");
}
}
#[test]
fn make_app_rejects_conflicts_and_bad_input() {
let tmp = TempDir::new();
let app = tmp.new_app();
let ok = slave(
&app,
&["make:app", "dashboard", "--domain", "dash.localhost"],
);
assert!(ok.status.success(), "{}", stderr(&ok));
let config = read(app.join("config/apps.toml"));
for args in [
&["make:app", "dashboard", "--domain", "other.localhost"][..],
&["make:app", "panel", "--domain", "admin.localhost"],
&[
"make:app",
"panel",
"--domain",
"x.localhost",
"--port",
"4000",
],
&["make:app", "mcp", "--domain", "x.localhost"],
&["make:app", "app", "--domain", "x.localhost"],
&["make:app", "runtime", "--domain", "x.localhost"],
&["make:app", "Panel", "--domain", "x.localhost"],
&["make:app", "panel", "--domain", "{a}{b}.localhost"],
&["make:app", "panel", "--domain", "bad host"],
] {
let out = slave(&app, args);
assert!(!out.status.success(), "{args:?} should fail");
}
assert_eq!(read(app.join("config/apps.toml")), config, "unchanged");
assert!(!app.join("apps/panel").exists());
}
#[test]
fn make_refuses_to_overwrite_without_force() {
let tmp = TempDir::new();
let app = tmp.new_app();
let model = app.join("src/modules/user/models/user.rs");
let original = read(&model);
let modules_before = read(app.join("src/modules/mod.rs"));
let out = slave(&app, &["make:model", "User"]);
assert!(!out.status.success());
assert!(stderr(&out).contains("already exists"));
assert_eq!(read(&model), original);
assert_eq!(read(app.join("src/modules/mod.rs")), modules_before);
let out = slave(&app, &["make:model", "User", "--force"]);
assert!(out.status.success(), "{}", stderr(&out));
assert_ne!(read(&model), original);
let models = read(app.join("src/modules/user/models/mod.rs"));
assert_eq!(models.matches("pub mod user;").count(), 1);
}
#[test]
fn make_requires_a_project() {
let tmp = TempDir::new();
let out = slave(&tmp.0, &["make:model", "User"]);
assert!(!out.status.success());
assert!(stderr(&out).contains("not inside a slave application"));
}
#[test]
fn make_rejects_invalid_names() {
let tmp = TempDir::new();
let app = tmp.new_app();
for args in [
&["make:model", "user"][..],
&["make:model", "User-Model"],
&["make:model", "../User"],
&["make:model", "Type"],
&["make:tools", "Tools"],
&["make:model", "Post", "--module", "Post"],
&["make:model", "Post", "--module", "../post"],
] {
assert!(!slave(&app, args).status.success(), "{args:?}");
}
}
fn file_names(dir: &Path) -> Vec<String> {
let mut files: Vec<String> = fs::read_dir(dir)
.unwrap()
.map(|entry| entry.unwrap().file_name().to_string_lossy().into_owned())
.collect();
files.sort();
files
}
#[test]
fn new_includes_the_initial_migration() {
let tmp = TempDir::new();
let app = tmp.new_app();
let migrations = file_names(&app.join("database/migrations"));
assert_eq!(migrations.len(), 1, "{migrations:?}");
let migration = &migrations[0];
assert!(
migration.as_bytes()[8] == b'_' && migration.ends_with("_create_users_and_teams.surql"),
"{migration}"
);
let surql = read(app.join("database/migrations").join(migration));
for table in [
"user",
"role",
"user_role",
"team",
"membership",
"invitation",
"app",
] {
assert!(
surql.contains(&format!("DEFINE TABLE {table} SCHEMAFULL;")),
"{surql}"
);
}
assert!(
surql.contains("CREATE role:1 SET name = 'super_admin';"),
"{surql}"
);
assert!(!app.join("Toasty.toml").exists());
}
#[test]
fn make_migration_writes_an_empty_surql_file() {
let tmp = TempDir::new();
let app = tmp.new_app();
let out = slave(&app, &["make:migration", "add_bio_to_users"]);
assert!(out.status.success(), "{}", stderr(&out));
let migrations = file_names(&app.join("database/migrations"));
assert_eq!(migrations.len(), 2, "{migrations:?}");
assert!(
migrations[1].ends_with("_add_bio_to_users.surql"),
"{migrations:?}"
);
let surql = read(app.join("database/migrations").join(&migrations[1]));
assert!(
surql.starts_with("-- Migration `add_bio_to_users`"),
"{surql}"
);
}
#[test]
fn make_model_adds_a_table_migration() {
let tmp = TempDir::new();
let app = tmp.new_app();
let out = slave(&app, &["make:model", "BlogPost"]);
assert!(out.status.success(), "{}", stderr(&out));
let model = read(app.join("src/modules/blog_post/models/blog_post.rs"));
assert!(
model.contains("#[derive(Debug, Clone, SurrealValue)]"),
"{model}"
);
let migrations = file_names(&app.join("database/migrations"));
let created = migrations
.iter()
.find(|m| m.ends_with("_create_blog_post.surql"))
.expect("table migration");
let surql = read(app.join("database/migrations").join(created));
assert!(
surql.contains("DEFINE SEQUENCE blog_post_id START 1;"),
"{surql}"
);
assert!(
surql.contains("DEFINE TABLE blog_post SCHEMAFULL;"),
"{surql}"
);
}
#[test]
fn make_migration_rejects_bad_names() {
let tmp = TempDir::new();
let app = tmp.new_app();
for name in ["CreatePosts", "create-posts", "1_init", ""] {
let out = slave(&app, &["make:migration", name]);
assert!(!out.status.success(), "{name}");
}
assert_eq!(file_names(&app.join("database/migrations")).len(), 1);
}
#[test]
fn migrate_commands_require_a_project() {
let tmp = TempDir::new();
for command in [
"migrate",
"migrate:status",
"migrate:fresh",
"make:migration",
] {
let mut args = vec![command];
if command == "make:migration" {
args.push("create_posts");
}
let out = slave(&tmp.0, &args);
assert!(!out.status.success(), "{command}");
assert!(
stderr(&out).contains("not inside a slave application"),
"{command}"
);
}
}
#[test]
#[ignore]
fn generated_application_builds_migrates_and_passes_its_tests() {
let tmp = TempDir::new();
let app = tmp.new_app();
let target = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target/generated-app");
let database = TestDatabase::start();
let run = |program: &str, args: &[&str]| {
let out = Command::new(program)
.args(args)
.envs(database.envs())
.env("CARGO_TARGET_DIR", &target)
.current_dir(&app)
.output()
.expect("run command");
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
assert!(
out.status.success(),
"{program} {args:?}\n{stdout}\n{}",
stderr(&out)
);
stdout
};
let slave_bin = env!("CARGO_BIN_EXE_slave");
let built = run(slave_bin, &["build"]);
assert!(
built.contains("Built app (MCP backend), runtime in"),
"{built}"
);
for args in [
&["make:model", "Post"][..],
&["make:tools", "PostTools"],
&["make:app", "storefront", "--domain", "{toko}.localhost"],
&["make:app", "help", "--path", "/help", "--separate"],
] {
let out = slave(&app, args);
assert!(out.status.success(), "{args:?}: {}", stderr(&out));
}
let built = run(slave_bin, &["build", "runtime", "help"]);
assert!(built.contains("Built runtime, help in"), "{built}");
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into());
run(&cargo, &["fmt", "--all", "--check"]);
run(&cargo, &["test", "--workspace", "--quiet"]);
let migrations = file_names(&app.join("database/migrations"));
assert_eq!(migrations.len(), 2, "{migrations:?}");
assert!(
migrations[1].ends_with("_create_post.surql"),
"{migrations:?}"
);
let migrated = run(slave_bin, &["migrate"]);
assert_eq!(migrated.matches("Applied ").count(), 2, "{migrated}");
let again = run(slave_bin, &["migrate"]);
assert!(again.contains("Nothing to migrate."), "{again}");
let status = run(slave_bin, &["migrate:status"]);
assert_eq!(status.matches("applied ").count(), 2, "{status}");
let fresh = run(slave_bin, &["migrate:fresh", "--force"]);
assert!(fresh.contains("Dropped every table."), "{fresh}");
assert_eq!(fresh.matches("Applied ").count(), 2, "{fresh}");
}