diff --git a/crates/launcher-common/src/standard.rs b/crates/launcher-common/src/standard.rs index 56cfa3f..53d813d 100644 --- a/crates/launcher-common/src/standard.rs +++ b/crates/launcher-common/src/standard.rs @@ -100,6 +100,18 @@ mod tests { #[test] fn baked_standard_parses() { let s = LauncherStandard::baked().expect("baked standard must parse"); - assert_eq!(s.spec_version, "0.1.0"); + + // Derive the expected version from the baked source rather than + // hardcoding it. This assertion previously read "0.1.0" and had gone + // stale against a standard that declares 0.2.0 - the test failed for + // a reason that had nothing to do with parsing, which is what it is + // named for. Deriving it means the check cannot rot again. + let declared = BAKED_STANDARD + .lines() + .find_map(|l| l.trim().strip_prefix("version = ")) + .map(|v| v.trim().trim_matches('"')) + .expect("baked standard must declare a version"); + assert_eq!(s.spec_version, declared); + assert!(!s.spec_version.is_empty(), "spec version must not be empty"); } } diff --git a/crates/launcher-common/src/template.rs b/crates/launcher-common/src/template.rs index 626f20b..492a27b 100644 --- a/crates/launcher-common/src/template.rs +++ b/crates/launcher-common/src/template.rs @@ -146,3 +146,79 @@ pub fn render( tera.render("launcher.sh", &ctx) .context("rendering launcher template") } + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::{LauncherConfig, Project, Repo, Runtime, RuntimeKind}; + use crate::standard::LauncherStandard; + + fn sample_config() -> LauncherConfig { + LauncherConfig { + project: Project { + name: "foo".into(), + display: "Foo".into(), + description: Some("Foo desc".into()), + categories: vec!["Development".into()], + version: None, + license: None, + generic_name: Some("Foo Thing".into()), + }, + repo: Repo { + path: "/tmp/foo".into(), + }, + runtime: Runtime { + kind: RuntimeKind::Process, + port: None, + url: None, + startup_command_search: vec![], + command: vec!["foo".into()], + pid_file: None, + log_file: None, + wait_for_url_timeout_seconds: 15, + }, + icon: None, + integration: None, + soft_attach: None, + exceptions: None, + } + } + + /// The template source must OPEN with the shebang. + /// + /// It previously opened with a Tera comment block, so every launcher the + /// generator emitted began with a blank line: shellcheck SC2148, and a + /// script the kernel will not dispatch by shebang. That single template + /// defect produced ~20 identical one-line fix PRs across the estate, each + /// of which the next `launch-scaffolder realign` would have overwritten. + #[test] + fn template_source_opens_with_shebang() { + assert_eq!( + LAUNCHER_TEMPLATE.lines().next(), + Some("#!/usr/bin/env bash"), + "the shebang must be the literal first line of launcher.sh.tera" + ); + } + + /// And the RENDERED output must too — the property that actually matters. + /// Asserting only on the template source would miss Tera emitting leading + /// whitespace of its own, which is precisely how the original defect + /// escaped notice. + #[test] + fn rendered_launcher_starts_with_shebang_on_line_one() { + let cfg = sample_config(); + let std_ = LauncherStandard::baked().expect("baked standard should parse"); + let out = render(&cfg, &std_, None).expect("template should render"); + + assert!( + out.starts_with("#!/usr/bin/env bash\n"), + "rendered launcher must begin with the shebang; got: {:?}", + &out[..out.len().min(60)] + ); + assert_eq!( + out.lines().next(), + Some("#!/usr/bin/env bash"), + "shebang must be on line 1 of the rendered launcher" + ); + } +} diff --git a/templates/launcher.sh.tera b/templates/launcher.sh.tera index 710634c..51e629f 100644 --- a/templates/launcher.sh.tera +++ b/templates/launcher.sh.tera @@ -1,3 +1,13 @@ +#!/usr/bin/env bash +{# NOTE: the shebang MUST be the literal first line of this template. + It previously sat below this comment block, and every launcher the + generator emitted began with a blank line -- shellcheck SC2148, and a + script whose kernel shebang is not on line 1. That produced ~20 + identical one-line fix PRs across the estate, each of which would have + been overwritten by the next `launch-scaffolder realign`. Do not move + the shebang below a Tera tag again, whatever whitespace-control markers + the tag carries. +-#} {#- SPDX-License-Identifier: MPL-2.0 Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) @@ -13,7 +23,6 @@ All three shapes emit the same --integ / --disinteg / --help cross-platform surface so desktop integration stays uniform across the estate. -#} -#!/usr/bin/env bash # SPDX-License-Identifier: MPL-2.0 # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) #