use super::*;
use crate::config::SeamConfig;
fn parse_config(toml_str: &str) -> SeamConfig {
toml::from_str(toml_str).unwrap()
}
fn parse_fullstack(extra_build: &str, extra: &str) -> SeamConfig {
parse_config(&format!(
r#"
[project]
name = "test"
[frontend]
entry = "src/client/main.tsx"
[build]
routes = "./src/routes.ts"
out_dir = ".seam/output"
backend_build_command = "bun build"
router_file = "src/server/router.ts"
{extra_build}
{extra}"#
))
}
#[test]
fn builtin_bundler_with_entry() {
let config = parse_fullstack("", "");
let build = BuildConfig::from_seam_config(&config).unwrap();
assert_eq!(build.entry, "src/client/main.tsx");
assert_eq!(build.bundler_manifest(), ".seam/dist/.vite/manifest.json");
assert!(build.is_fullstack);
}
#[test]
fn bundler_command_rejected() {
let config = parse_config(
r#"
[project]
name = "test"
[frontend]
entry = "src/main.tsx"
[build]
routes = "./src/routes.ts"
bundler_command = "npx vite build"
"#,
);
let result = BuildConfig::from_seam_config(&config);
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("bundlerCommand has been removed"));
}
#[test]
fn build_command_from_frontend_rejected() {
let config = parse_config(
r#"
[project]
name = "test"
[frontend]
build_command = "bun run build"
entry = "src/main.tsx"
[build]
routes = "./src/routes.ts"
"#,
);
let result = BuildConfig::from_seam_config(&config);
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("bundlerCommand has been removed"));
}
#[test]
fn no_entry_errors() {
let config = parse_config(
r#"
[project]
name = "test"
[build]
routes = "./src/routes.ts"
"#,
);
let result = BuildConfig::from_seam_config(&config);
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("frontend.entry"));
}
#[test]
fn build_config_obfuscate_defaults() {
let config = parse_fullstack("", "");
let bc = BuildConfig::from_seam_config(&config).unwrap();
assert!(bc.obfuscate, "build defaults to obfuscate=true");
assert!(!bc.sourcemap, "build defaults to sourcemap=false");
assert!(bc.rpc_salt.is_none());
}
#[test]
fn dev_config_obfuscate_defaults() {
let config = parse_fullstack("", "");
let bc = BuildConfig::from_seam_config_dev(&config).unwrap();
assert!(!bc.obfuscate, "dev defaults to obfuscate=false");
assert!(bc.sourcemap, "dev defaults to sourcemap=true");
}
#[test]
fn explicit_obfuscate_overrides_defaults() {
let config = parse_fullstack(
"obfuscate = false\nsourcemap = true",
"\n[dev]\nobfuscate = true\nsourcemap = false",
);
let bc = BuildConfig::from_seam_config(&config).unwrap();
assert!(!bc.obfuscate);
assert!(bc.sourcemap);
let bc_dev = BuildConfig::from_seam_config_dev(&config).unwrap();
assert!(bc_dev.obfuscate);
assert!(!bc_dev.sourcemap);
}
#[test]
fn build_config_type_hint_defaults() {
let config = parse_fullstack("", "");
let bc = BuildConfig::from_seam_config(&config).unwrap();
assert!(bc.type_hint, "build defaults to type_hint=true");
let bc_dev = BuildConfig::from_seam_config_dev(&config).unwrap();
assert!(bc_dev.type_hint, "dev defaults to type_hint=true");
}
#[test]
fn explicit_type_hint_overrides() {
let config = parse_fullstack("type_hint = false", "\n[dev]\ntype_hint = false");
let bc = BuildConfig::from_seam_config(&config).unwrap();
assert!(!bc.type_hint);
let bc_dev = BuildConfig::from_seam_config_dev(&config).unwrap();
assert!(!bc_dev.type_hint);
}
#[test]
fn build_config_hash_length_defaults() {
let config = parse_fullstack("", "");
let bc = BuildConfig::from_seam_config(&config).unwrap();
assert_eq!(bc.hash_length, 12, "build defaults to hash_length=12");
let bc_dev = BuildConfig::from_seam_config_dev(&config).unwrap();
assert_eq!(bc_dev.hash_length, 12, "dev inherits hash_length from build");
}
#[test]
fn explicit_hash_length_overrides() {
let config = parse_fullstack("hash_length = 20", "\n[dev]\nhash_length = 8");
let bc = BuildConfig::from_seam_config(&config).unwrap();
assert_eq!(bc.hash_length, 20);
let bc_dev = BuildConfig::from_seam_config_dev(&config).unwrap();
assert_eq!(bc_dev.hash_length, 8);
}
#[test]
fn hash_length_validation() {
let config = parse_fullstack("hash_length = 3", "");
let result = BuildConfig::from_seam_config(&config);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("hash_length"));
let config = parse_fullstack("hash_length = 65", "");
let result = BuildConfig::from_seam_config(&config);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("hash_length"));
}
#[test]
fn pages_dir_without_routes() {
let config = parse_config(
r#"
[project]
name = "test"
[frontend]
entry = "src/client/main.tsx"
[build]
pages_dir = "src/pages"
out_dir = ".seam/output"
backend_build_command = "bun build"
router_file = "src/server/router.ts"
"#,
);
let bc = BuildConfig::from_seam_config(&config).unwrap();
assert_eq!(bc.routes, ".seam/generated/routes.ts");
assert_eq!(bc.pages_dir.as_deref(), Some("src/pages"));
}
#[test]
fn pages_dir_with_routes_errors() {
let config = parse_config(
r#"
[project]
name = "test"
[frontend]
entry = "src/client/main.tsx"
[build]
routes = "./src/routes.ts"
pages_dir = "src/pages"
out_dir = ".seam/output"
"#,
);
let result = BuildConfig::from_seam_config(&config);
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("mutually exclusive"));
}
#[test]
fn no_routes_no_pages_dir_errors() {
let config = parse_config(
r#"
[project]
name = "test"
[frontend]
entry = "src/client/main.tsx"
[build]
out_dir = ".seam/output"
"#,
);
let result = BuildConfig::from_seam_config(&config);
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("build.routes") || msg.contains("build.pages_dir"));
}
#[test]
fn parse_transport_section() {
let config = parse_config(
r#"
[project]
name = "test"
[frontend]
entry = "src/client/main.tsx"
[build]
routes = "./src/routes.ts"
[transport]
subscription = { prefer = "ws" }
stream = { prefer = "sse", fallback = ["http"] }
"#,
);
let t = config.transport.as_ref().unwrap();
assert!(t.query.is_none());
let sub = t.subscription.as_ref().unwrap();
assert_eq!(sub.prefer, crate::config::TransportPreference::Ws);
let stream = t.stream.as_ref().unwrap();
assert_eq!(stream.prefer, crate::config::TransportPreference::Sse);
assert_eq!(stream.fallback.as_ref().unwrap().len(), 1);
}
#[test]
fn absent_transport_is_none() {
let config = parse_fullstack("", "");
assert!(config.transport.is_none());
}
#[test]
fn partial_transport_override() {
let config = parse_config(
r#"
[project]
name = "test"
[frontend]
entry = "src/client/main.tsx"
[build]
routes = "./src/routes.ts"
[transport]
channel = { prefer = "ws", fallback = ["http"] }
"#,
);
let t = config.transport.as_ref().unwrap();
assert!(t.query.is_none());
assert!(t.command.is_none());
assert!(t.stream.is_none());
assert!(t.subscription.is_none());
assert!(t.upload.is_none());
assert!(t.channel.is_some());
}
#[test]
fn dist_dir_is_constant() {
let config = parse_fullstack("", "");
let bc = BuildConfig::from_seam_config(&config).unwrap();
assert_eq!(bc.dist_dir(), ".seam/dist");
}
#[test]
fn config_hash_deterministic() {
let config = parse_fullstack("", "");
let bc = BuildConfig::from_seam_config(&config).unwrap();
let h1 = bc.config_hash();
let h2 = bc.config_hash();
assert_eq!(h1, h2);
assert_eq!(h1.len(), 16, "hash should be 16 hex chars");
}
#[test]
fn config_hash_changes_with_entry() {
let c1 = parse_fullstack("", "");
let c2 = parse_config(
r#"
[project]
name = "test"
[frontend]
entry = "src/client/other.tsx"
[build]
routes = "./src/routes.ts"
out_dir = ".seam/output"
backend_build_command = "bun build"
router_file = "src/server/router.ts"
"#,
);
let h1 = BuildConfig::from_seam_config(&c1).unwrap().config_hash();
let h2 = BuildConfig::from_seam_config(&c2).unwrap().config_hash();
assert_ne!(h1, h2);
}
#[test]
fn config_hash_changes_with_obfuscate() {
let c1 = parse_fullstack("obfuscate = true", "");
let c2 = parse_fullstack("obfuscate = false", "");
let h1 = BuildConfig::from_seam_config(&c1).unwrap().config_hash();
let h2 = BuildConfig::from_seam_config(&c2).unwrap().config_hash();
assert_ne!(h1, h2);
}
#[test]
fn config_hash_ignores_rpc_salt() {
let config = parse_fullstack("", "");
let mut bc1 = BuildConfig::from_seam_config(&config).unwrap();
let mut bc2 = BuildConfig::from_seam_config(&config).unwrap();
bc1.rpc_salt = Some("salt-a".to_string());
bc2.rpc_salt = Some("salt-b".to_string());
assert_eq!(bc1.config_hash(), bc2.config_hash());
}
#[test]
fn config_hash_ignores_config_path() {
let config = parse_fullstack("", "");
let mut bc1 = BuildConfig::from_seam_config(&config).unwrap();
let mut bc2 = BuildConfig::from_seam_config(&config).unwrap();
bc1.config_path = Some("/a/seam.config.ts".to_string());
bc2.config_path = Some("/b/seam.config.ts".to_string());
assert_eq!(bc1.config_hash(), bc2.config_hash());
}