pub(crate) const CEREMONY_CONNECT_SCRIPT: &str = r#"
import sys
# Cache invalidation: MicroPython's sys.modules persists across raw-REPL
# sessions on V5 (REPL state is sticky until device reboot). If an earlier
# import populated sys.modules with a partial/namespace-package version of
# jumperless_mcp (e.g. during install verification when only some files
# existed), subsequent imports would see the cached version missing
# _ceremony_connect. Pop the package + submodules to force a fresh load
# from disk after every install.
sys.modules.pop('jumperless_mcp', None)
sys.modules.pop('jumperless_mcp.effects', None)
sys.modules.pop('jumperless_mcp.font', None)
try:
import jumperless_mcp
except ImportError:
import time
time.sleep_ms(100)
import jumperless_mcp
jumperless_mcp._ceremony_connect()
"#;
pub(crate) const CEREMONY_DISCONNECT_SCRIPT: &str = r#"
import sys
# Cache invalidation: same rationale as CONNECT_SCRIPT — force fresh load
# rather than trust sys.modules.
sys.modules.pop('jumperless_mcp', None)
sys.modules.pop('jumperless_mcp.effects', None)
sys.modules.pop('jumperless_mcp.font', None)
try:
import jumperless_mcp
except ImportError:
import time
time.sleep_ms(100)
import jumperless_mcp
jumperless_mcp._ceremony_disconnect()
"#;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn connect_script_is_non_empty() {
assert!(!CEREMONY_CONNECT_SCRIPT.is_empty());
}
#[test]
fn connect_script_uses_import_not_exec_jfs_read() {
assert!(
CEREMONY_CONNECT_SCRIPT.contains("import jumperless_mcp"),
"CEREMONY_CONNECT_SCRIPT must use 'import jumperless_mcp' (Phase B); got:\n{}",
CEREMONY_CONNECT_SCRIPT
);
assert!(
!CEREMONY_CONNECT_SCRIPT.contains("exec("),
"CEREMONY_CONNECT_SCRIPT must NOT use exec() — import replaces it; got:\n{}",
CEREMONY_CONNECT_SCRIPT
);
assert!(
!CEREMONY_CONNECT_SCRIPT.contains("jfs.open"),
"CEREMONY_CONNECT_SCRIPT must NOT use jfs.open — import replaces it; got:\n{}",
CEREMONY_CONNECT_SCRIPT
);
assert!(
!CEREMONY_CONNECT_SCRIPT.contains("jfs.read"),
"CEREMONY_CONNECT_SCRIPT must NOT use jfs.read — import replaces it; got:\n{}",
CEREMONY_CONNECT_SCRIPT
);
assert!(
!CEREMONY_CONNECT_SCRIPT.contains("fs_read("),
"CEREMONY_CONNECT_SCRIPT must NOT use fs_read() fallback — import replaces it; got:\n{}",
CEREMONY_CONNECT_SCRIPT
);
}
#[test]
fn connect_script_calls_ceremony_connect() {
assert!(
CEREMONY_CONNECT_SCRIPT.contains("jumperless_mcp._ceremony_connect()"),
"CEREMONY_CONNECT_SCRIPT must call jumperless_mcp._ceremony_connect(); got:\n{}",
CEREMONY_CONNECT_SCRIPT
);
}
#[test]
fn connect_script_has_import_retry_on_import_error() {
assert!(
CEREMONY_CONNECT_SCRIPT.contains("except ImportError:"),
"CEREMONY_CONNECT_SCRIPT must have ImportError retry (VFS settle time); got:\n{}",
CEREMONY_CONNECT_SCRIPT
);
assert!(
CEREMONY_CONNECT_SCRIPT.contains("sleep_ms(100)"),
"CEREMONY_CONNECT_SCRIPT retry must sleep 100ms before retry; got:\n{}",
CEREMONY_CONNECT_SCRIPT
);
}
#[test]
fn connect_script_does_not_attempt_rail_color_flip() {
assert!(
!CEREMONY_CONNECT_SCRIPT.contains("set_net_color"),
"CEREMONY_CONNECT_SCRIPT must NOT call set_net_color — rails are \
firmware-locked (LEDs.cpp:1985-1988 assignNetColors overwrites every tick). Got:\n{}",
CEREMONY_CONNECT_SCRIPT
);
}
#[test]
fn connect_script_does_not_inline_font_dict() {
assert!(
!CEREMONY_CONNECT_SCRIPT.contains("FONT = {"),
"CEREMONY_CONNECT_SCRIPT must NOT inline FONT dict — it's in the resident library; got:\n{}",
CEREMONY_CONNECT_SCRIPT
);
}
#[test]
fn connect_script_does_not_define_marquee_function() {
assert!(
!CEREMONY_CONNECT_SCRIPT.contains("def marquee("),
"CEREMONY_CONNECT_SCRIPT must NOT define marquee() inline — it's in the resident library; got:\n{}",
CEREMONY_CONNECT_SCRIPT
);
}
#[test]
fn disconnect_script_is_non_empty() {
assert!(!CEREMONY_DISCONNECT_SCRIPT.is_empty());
}
#[test]
fn disconnect_script_uses_import_not_exec_jfs_read() {
assert!(
CEREMONY_DISCONNECT_SCRIPT.contains("import jumperless_mcp"),
"CEREMONY_DISCONNECT_SCRIPT must use 'import jumperless_mcp' (Phase B); got:\n{}",
CEREMONY_DISCONNECT_SCRIPT
);
assert!(
!CEREMONY_DISCONNECT_SCRIPT.contains("exec("),
"CEREMONY_DISCONNECT_SCRIPT must NOT use exec() — import replaces it; got:\n{}",
CEREMONY_DISCONNECT_SCRIPT
);
assert!(
!CEREMONY_DISCONNECT_SCRIPT.contains("jfs.open"),
"CEREMONY_DISCONNECT_SCRIPT must NOT use jfs.open — import replaces it; got:\n{}",
CEREMONY_DISCONNECT_SCRIPT
);
assert!(
!CEREMONY_DISCONNECT_SCRIPT.contains("jfs.read"),
"CEREMONY_DISCONNECT_SCRIPT must NOT use jfs.read — import replaces it; got:\n{}",
CEREMONY_DISCONNECT_SCRIPT
);
assert!(
!CEREMONY_DISCONNECT_SCRIPT.contains("fs_read("),
"CEREMONY_DISCONNECT_SCRIPT must NOT use fs_read() fallback — import replaces it; got:\n{}",
CEREMONY_DISCONNECT_SCRIPT
);
}
#[test]
fn disconnect_script_calls_ceremony_disconnect() {
assert!(
CEREMONY_DISCONNECT_SCRIPT.contains("jumperless_mcp._ceremony_disconnect()"),
"CEREMONY_DISCONNECT_SCRIPT must call jumperless_mcp._ceremony_disconnect(); got:\n{}",
CEREMONY_DISCONNECT_SCRIPT
);
}
#[test]
fn disconnect_script_has_import_retry_on_import_error() {
assert!(
CEREMONY_DISCONNECT_SCRIPT.contains("except ImportError:"),
"CEREMONY_DISCONNECT_SCRIPT must have ImportError retry; got:\n{}",
CEREMONY_DISCONNECT_SCRIPT
);
assert!(
CEREMONY_DISCONNECT_SCRIPT.contains("sleep_ms(100)"),
"CEREMONY_DISCONNECT_SCRIPT retry must sleep 100ms before retry; got:\n{}",
CEREMONY_DISCONNECT_SCRIPT
);
}
#[test]
fn disconnect_script_does_not_call_bitmap_file_loader() {
assert!(
!CEREMONY_DISCONNECT_SCRIPT.contains("oled_show_bitmap_file"),
"CEREMONY_DISCONNECT_SCRIPT must NOT call oled_show_bitmap_file. Got:\n{}",
CEREMONY_DISCONNECT_SCRIPT
);
}
#[test]
fn disconnect_script_does_not_attempt_rail_restore() {
assert!(
!CEREMONY_DISCONNECT_SCRIPT.contains("set_net_color"),
"CEREMONY_DISCONNECT_SCRIPT must NOT call set_net_color — rails were \
never flipped on connect (firmware-locked). Got:\n{}",
CEREMONY_DISCONNECT_SCRIPT
);
}
}