implements PLN-0039

This commit is contained in:
bQUARKz 2026-04-20 18:08:10 +01:00
parent 77351a8813
commit eddd99b754
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
5 changed files with 453 additions and 447 deletions

View File

@ -171,6 +171,12 @@ fn runtime_test_scene(glyph_bank_id: u8, palette_id: u8, tile_size: TileSize) ->
SceneBank { layers: std::array::from_fn(|_| layer.clone()) }
}
#[path = "tests_asset_bank.rs"]
mod asset_bank;
#[path = "tests_fs_memcard.rs"]
mod fs_memcard;
#[test]
fn initialize_vm_applies_cartridge_capabilities_before_loader_resolution() {
let mut runtime = VirtualMachineRuntime::new(None);
@ -869,352 +875,6 @@ fn tick_audio_play_type_mismatch_surfaces_trap_not_panic() {
assert!(matches!(runtime.last_crash_report, Some(CrashReport::VmTrap { .. })));
}
#[test]
fn tick_asset_commit_operational_error_returns_status_not_crash() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
let code = assemble("PUSH_I32 999\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "asset".into(),
name: "commit".into(),
version: 1,
arg_slots: 1,
ret_slots: 1,
}],
);
let cartridge = cartridge_with_program(program, caps::ASSET);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
assert!(report.is_none(), "operational error must not crash");
assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AssetOpStatus::UnknownHandle as i64)]);
}
#[test]
fn tick_asset_load_missing_asset_returns_status_and_zero_handle() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
let code = assemble("PUSH_I32 999\nPUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "asset".into(),
name: "load".into(),
version: 1,
arg_slots: 2,
ret_slots: 2,
}],
);
let cartridge = cartridge_with_program(program, caps::ASSET);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
assert!(report.is_none(), "missing asset must not crash");
assert!(vm.is_halted());
assert_eq!(
vm.operand_stack_top(2),
vec![Value::Int64(0), Value::Int64(AssetLoadError::AssetNotFound as i64)]
);
}
#[test]
fn tick_asset_load_invalid_slot_returns_status_and_zero_handle() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
let asset_data = test_glyph_asset_data();
hardware.assets.initialize_for_cartridge(
vec![test_glyph_asset_entry("tile_asset", asset_data.len())],
vec![],
AssetsPayloadSource::from_bytes(asset_data),
);
let code = assemble("PUSH_I32 7\nPUSH_I32 16\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "asset".into(),
name: "load".into(),
version: 1,
arg_slots: 2,
ret_slots: 2,
}],
);
let cartridge = cartridge_with_program(program, caps::ASSET);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
assert!(report.is_none(), "invalid slot must not crash");
assert!(vm.is_halted());
assert_eq!(
vm.operand_stack_top(2),
vec![Value::Int64(0), Value::Int64(AssetLoadError::SlotIndexInvalid as i64)]
);
}
#[test]
fn tick_asset_status_unknown_handle_returns_status_not_crash() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
let code = assemble("PUSH_I32 999\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "asset".into(),
name: "status".into(),
version: 1,
arg_slots: 1,
ret_slots: 1,
}],
);
let cartridge = cartridge_with_program(program, caps::ASSET);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
assert!(report.is_none(), "unknown asset handle must not crash");
assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(LoadStatus::UnknownHandle as i64)]);
}
#[test]
fn tick_bank_info_returns_slot_summary_not_json() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
let asset_data = test_glyph_asset_data();
hardware.assets.initialize_for_cartridge(
vec![test_glyph_asset_entry("tile_asset", asset_data.len())],
vec![prometeu_hal::asset::PreloadEntry { asset_id: 7, slot: 0 }],
AssetsPayloadSource::from_bytes(asset_data),
);
let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "bank".into(),
name: "info".into(),
version: 1,
arg_slots: 1,
ret_slots: 2,
}],
);
let cartridge = cartridge_with_program(program, caps::BANK);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
assert!(report.is_none(), "bank summary must not crash");
assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(2), vec![Value::Int64(16), Value::Int64(1)]);
}
#[test]
fn initialize_vm_rejects_removed_bank_slot_info_syscall_identity() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let code = assemble("PUSH_I32 0\nPUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "bank".into(),
name: "slot_info".into(),
version: 1,
arg_slots: 2,
ret_slots: 1,
}],
);
let cartridge = cartridge_with_program(program, caps::BANK);
let res = runtime.initialize_vm(&mut vm, &cartridge);
assert!(matches!(res, Err(CrashReport::VmInit { error: VmInitError::LoaderPatchFailed(_) })));
}
#[test]
fn tick_asset_commit_invalid_transition_returns_status_not_crash() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
let code = assemble("PUSH_I32 1\nHOSTCALL 0\nPOP_N 1\nPUSH_I32 1\nHOSTCALL 1\nHALT")
.expect("assemble");
let program = serialized_single_function_module(
code,
vec![
SyscallDecl {
module: "asset".into(),
name: "cancel".into(),
version: 1,
arg_slots: 1,
ret_slots: 1,
},
SyscallDecl {
module: "asset".into(),
name: "commit".into(),
version: 1,
arg_slots: 1,
ret_slots: 1,
},
],
);
let cartridge = cartridge_with_program(program, caps::ASSET);
let asset_data = test_glyph_asset_data();
hardware.assets.initialize_for_cartridge(
vec![test_glyph_asset_entry("tile_asset", asset_data.len())],
vec![],
AssetsPayloadSource::from_bytes(asset_data),
);
let handle = hardware.assets.load(7, 0).expect("asset handle must be allocated");
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
assert!(report.is_none(), "invalid transition must not crash");
assert!(vm.is_halted());
assert_eq!(handle, 1);
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AssetOpStatus::InvalidState as i64)]);
}
#[test]
fn tick_asset_cancel_unknown_handle_returns_status_not_crash() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
let code = assemble("PUSH_I32 999\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "asset".into(),
name: "cancel".into(),
version: 1,
arg_slots: 1,
ret_slots: 1,
}],
);
let cartridge = cartridge_with_program(program, caps::ASSET);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
assert!(report.is_none(), "unknown handle cancel must not crash");
assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AssetOpStatus::UnknownHandle as i64)]);
}
#[test]
fn tick_asset_cancel_invalid_transition_returns_status_not_crash() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
let code = assemble("PUSH_I32 1\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "asset".into(),
name: "cancel".into(),
version: 1,
arg_slots: 1,
ret_slots: 1,
}],
);
let cartridge = cartridge_with_program(program, caps::ASSET);
let asset_data = test_glyph_asset_data();
hardware.assets.initialize_for_cartridge(
vec![test_glyph_asset_entry("tile_asset", asset_data.len())],
vec![],
AssetsPayloadSource::from_bytes(asset_data),
);
let handle = hardware.assets.load(7, 0).expect("asset handle must be allocated");
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
loop {
match hardware.assets.status(handle) {
LoadStatus::READY => break,
LoadStatus::PENDING | LoadStatus::LOADING => {
std::thread::sleep(std::time::Duration::from_millis(1));
}
other => panic!("unexpected asset status before commit: {:?}", other),
}
}
assert_eq!(hardware.assets.commit(handle), AssetOpStatus::Ok);
hardware.assets.apply_commits();
assert_eq!(hardware.assets.status(handle), LoadStatus::COMMITTED);
let report = runtime.tick(&mut vm, &signals, &mut hardware);
assert!(report.is_none(), "cancel after commit must not crash");
assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AssetOpStatus::InvalidState as i64)]);
}
#[test]
fn tick_status_first_surface_smoke_across_composer_audio_and_asset() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
let code = assemble(
"PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\n\
PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 255\nPUSH_I32 128\nPUSH_I32 1\nPUSH_I32 0\nHOSTCALL 1\n\
PUSH_I32 999\nPUSH_I32 0\nHOSTCALL 2\n\
HALT"
)
.expect("assemble");
let program = serialized_single_function_module(
code,
vec![
SyscallDecl {
module: "composer".into(),
name: "emit_sprite".into(),
version: 1,
arg_slots: 9,
ret_slots: 1,
},
SyscallDecl {
module: "audio".into(),
name: "play".into(),
version: 1,
arg_slots: 7,
ret_slots: 1,
},
SyscallDecl {
module: "asset".into(),
name: "load".into(),
version: 1,
arg_slots: 2,
ret_slots: 2,
},
],
);
let cartridge = cartridge_with_program(program, caps::GFX | caps::AUDIO | caps::ASSET);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
assert!(report.is_none(), "mixed status-first surface must not crash");
assert!(vm.is_halted());
assert_eq!(
vm.operand_stack_top(4),
vec![
Value::Int64(0),
Value::Int64(AssetLoadError::AssetNotFound as i64),
Value::Int64(AudioOpStatus::BankInvalid as i64),
Value::Int64(ComposerOpStatus::BankInvalid as i64),
]
);
}
#[test]
fn tick_composer_emit_sprite_type_mismatch_surfaces_trap_not_panic() {
let mut runtime = VirtualMachineRuntime::new(None);
@ -1249,100 +909,3 @@ fn tick_composer_emit_sprite_type_mismatch_surfaces_trap_not_panic() {
}
assert!(matches!(runtime.last_crash_report, Some(CrashReport::VmTrap { .. })));
}
#[test]
fn tick_memcard_slot_roundtrip_for_game_profile() {
let mut runtime = VirtualMachineRuntime::new(None);
runtime.mount_fs(Box::new(MemFsBackend::default()));
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
let code = assemble(
"PUSH_I32 0\nPUSH_I32 0\nPUSH_CONST 0\nHOSTCALL 0\nPOP_N 2\nPUSH_I32 0\nHOSTCALL 1\nPOP_N 1\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 10\nHOSTCALL 2\nHALT",
)
.expect("assemble");
let program = serialized_single_function_module_with_consts(
code,
vec![ConstantPoolEntry::String("6869".into())], // "hi" in hex
vec![
SyscallDecl {
module: "mem".into(),
name: "slot_write".into(),
version: 1,
arg_slots: 3,
ret_slots: 2,
},
SyscallDecl {
module: "mem".into(),
name: "slot_commit".into(),
version: 1,
arg_slots: 1,
ret_slots: 1,
},
SyscallDecl {
module: "mem".into(),
name: "slot_read".into(),
version: 1,
arg_slots: 3,
ret_slots: 3,
},
],
);
let cartridge = Cartridge {
app_id: 42,
title: "Memcard Game".into(),
app_version: "1.0.0".into(),
app_mode: AppMode::Game,
capabilities: caps::FS,
program,
assets: AssetsPayloadSource::empty(),
asset_table: vec![],
preload: vec![],
};
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
assert!(report.is_none(), "memcard roundtrip must not crash");
assert!(vm.is_halted());
assert_eq!(
vm.operand_stack_top(3),
vec![Value::Int64(2), Value::String("6869".into()), Value::Int64(0)]
);
}
#[test]
fn tick_memcard_access_is_denied_for_non_game_profile() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
let code = assemble("HOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "mem".into(),
name: "slot_count".into(),
version: 1,
arg_slots: 0,
ret_slots: 2,
}],
);
let cartridge = Cartridge {
app_id: 101,
title: "System App".into(),
app_version: "1.0.0".into(),
app_mode: AppMode::System,
capabilities: caps::FS,
program,
assets: AssetsPayloadSource::empty(),
asset_table: vec![],
preload: vec![],
};
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
assert!(report.is_none(), "non-game memcard call must return status");
assert!(vm.is_halted());
// top-first: count then status
assert_eq!(vm.operand_stack_top(2), vec![Value::Int64(0), Value::Int64(4)]);
}

View File

@ -0,0 +1,347 @@
use super::*;
#[test]
fn tick_asset_commit_operational_error_returns_status_not_crash() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
let code = assemble("PUSH_I32 999\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "asset".into(),
name: "commit".into(),
version: 1,
arg_slots: 1,
ret_slots: 1,
}],
);
let cartridge = cartridge_with_program(program, caps::ASSET);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
assert!(report.is_none(), "operational error must not crash");
assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AssetOpStatus::UnknownHandle as i64)]);
}
#[test]
fn tick_asset_load_missing_asset_returns_status_and_zero_handle() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
let code = assemble("PUSH_I32 999\nPUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "asset".into(),
name: "load".into(),
version: 1,
arg_slots: 2,
ret_slots: 2,
}],
);
let cartridge = cartridge_with_program(program, caps::ASSET);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
assert!(report.is_none(), "missing asset must not crash");
assert!(vm.is_halted());
assert_eq!(
vm.operand_stack_top(2),
vec![Value::Int64(0), Value::Int64(AssetLoadError::AssetNotFound as i64)]
);
}
#[test]
fn tick_asset_load_invalid_slot_returns_status_and_zero_handle() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
let asset_data = test_glyph_asset_data();
hardware.assets.initialize_for_cartridge(
vec![test_glyph_asset_entry("tile_asset", asset_data.len())],
vec![],
AssetsPayloadSource::from_bytes(asset_data),
);
let code = assemble("PUSH_I32 7\nPUSH_I32 16\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "asset".into(),
name: "load".into(),
version: 1,
arg_slots: 2,
ret_slots: 2,
}],
);
let cartridge = cartridge_with_program(program, caps::ASSET);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
assert!(report.is_none(), "invalid slot must not crash");
assert!(vm.is_halted());
assert_eq!(
vm.operand_stack_top(2),
vec![Value::Int64(0), Value::Int64(AssetLoadError::SlotIndexInvalid as i64)]
);
}
#[test]
fn tick_asset_status_unknown_handle_returns_status_not_crash() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
let code = assemble("PUSH_I32 999\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "asset".into(),
name: "status".into(),
version: 1,
arg_slots: 1,
ret_slots: 1,
}],
);
let cartridge = cartridge_with_program(program, caps::ASSET);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
assert!(report.is_none(), "unknown asset handle must not crash");
assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(LoadStatus::UnknownHandle as i64)]);
}
#[test]
fn tick_bank_info_returns_slot_summary_not_json() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
let asset_data = test_glyph_asset_data();
hardware.assets.initialize_for_cartridge(
vec![test_glyph_asset_entry("tile_asset", asset_data.len())],
vec![prometeu_hal::asset::PreloadEntry { asset_id: 7, slot: 0 }],
AssetsPayloadSource::from_bytes(asset_data),
);
let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "bank".into(),
name: "info".into(),
version: 1,
arg_slots: 1,
ret_slots: 2,
}],
);
let cartridge = cartridge_with_program(program, caps::BANK);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
assert!(report.is_none(), "bank summary must not crash");
assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(2), vec![Value::Int64(16), Value::Int64(1)]);
}
#[test]
fn initialize_vm_rejects_removed_bank_slot_info_syscall_identity() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let code = assemble("PUSH_I32 0\nPUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "bank".into(),
name: "slot_info".into(),
version: 1,
arg_slots: 2,
ret_slots: 1,
}],
);
let cartridge = cartridge_with_program(program, caps::BANK);
let res = runtime.initialize_vm(&mut vm, &cartridge);
assert!(matches!(res, Err(CrashReport::VmInit { error: VmInitError::LoaderPatchFailed(_) })));
}
#[test]
fn tick_asset_commit_invalid_transition_returns_status_not_crash() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
let code = assemble("PUSH_I32 1\nHOSTCALL 0\nPOP_N 1\nPUSH_I32 1\nHOSTCALL 1\nHALT")
.expect("assemble");
let program = serialized_single_function_module(
code,
vec![
SyscallDecl {
module: "asset".into(),
name: "cancel".into(),
version: 1,
arg_slots: 1,
ret_slots: 1,
},
SyscallDecl {
module: "asset".into(),
name: "commit".into(),
version: 1,
arg_slots: 1,
ret_slots: 1,
},
],
);
let cartridge = cartridge_with_program(program, caps::ASSET);
let asset_data = test_glyph_asset_data();
hardware.assets.initialize_for_cartridge(
vec![test_glyph_asset_entry("tile_asset", asset_data.len())],
vec![],
AssetsPayloadSource::from_bytes(asset_data),
);
let handle = hardware.assets.load(7, 0).expect("asset handle must be allocated");
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
assert!(report.is_none(), "invalid transition must not crash");
assert!(vm.is_halted());
assert_eq!(handle, 1);
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AssetOpStatus::InvalidState as i64)]);
}
#[test]
fn tick_asset_cancel_unknown_handle_returns_status_not_crash() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
let code = assemble("PUSH_I32 999\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "asset".into(),
name: "cancel".into(),
version: 1,
arg_slots: 1,
ret_slots: 1,
}],
);
let cartridge = cartridge_with_program(program, caps::ASSET);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
assert!(report.is_none(), "unknown handle cancel must not crash");
assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AssetOpStatus::UnknownHandle as i64)]);
}
#[test]
fn tick_asset_cancel_invalid_transition_returns_status_not_crash() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
let code = assemble("PUSH_I32 1\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "asset".into(),
name: "cancel".into(),
version: 1,
arg_slots: 1,
ret_slots: 1,
}],
);
let cartridge = cartridge_with_program(program, caps::ASSET);
let asset_data = test_glyph_asset_data();
hardware.assets.initialize_for_cartridge(
vec![test_glyph_asset_entry("tile_asset", asset_data.len())],
vec![],
AssetsPayloadSource::from_bytes(asset_data),
);
let handle = hardware.assets.load(7, 0).expect("asset handle must be allocated");
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
loop {
match hardware.assets.status(handle) {
LoadStatus::READY => break,
LoadStatus::PENDING | LoadStatus::LOADING => {
std::thread::sleep(std::time::Duration::from_millis(1));
}
other => panic!("unexpected asset status before commit: {:?}", other),
}
}
assert_eq!(hardware.assets.commit(handle), AssetOpStatus::Ok);
hardware.assets.apply_commits();
assert_eq!(hardware.assets.status(handle), LoadStatus::COMMITTED);
let report = runtime.tick(&mut vm, &signals, &mut hardware);
assert!(report.is_none(), "cancel after commit must not crash");
assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AssetOpStatus::InvalidState as i64)]);
}
#[test]
fn tick_status_first_surface_smoke_across_composer_audio_and_asset() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
let code = assemble(
"PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\n\
PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 255\nPUSH_I32 128\nPUSH_I32 1\nPUSH_I32 0\nHOSTCALL 1\n\
PUSH_I32 999\nPUSH_I32 0\nHOSTCALL 2\n\
HALT"
)
.expect("assemble");
let program = serialized_single_function_module(
code,
vec![
SyscallDecl {
module: "composer".into(),
name: "emit_sprite".into(),
version: 1,
arg_slots: 9,
ret_slots: 1,
},
SyscallDecl {
module: "audio".into(),
name: "play".into(),
version: 1,
arg_slots: 7,
ret_slots: 1,
},
SyscallDecl {
module: "asset".into(),
name: "load".into(),
version: 1,
arg_slots: 2,
ret_slots: 2,
},
],
);
let cartridge = cartridge_with_program(program, caps::GFX | caps::AUDIO | caps::ASSET);
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
assert!(report.is_none(), "mixed status-first surface must not crash");
assert!(vm.is_halted());
assert_eq!(
vm.operand_stack_top(4),
vec![
Value::Int64(0),
Value::Int64(AssetLoadError::AssetNotFound as i64),
Value::Int64(AudioOpStatus::BankInvalid as i64),
Value::Int64(ComposerOpStatus::BankInvalid as i64),
]
);
}

View File

@ -0,0 +1,97 @@
use super::*;
#[test]
fn tick_memcard_slot_roundtrip_for_game_profile() {
let mut runtime = VirtualMachineRuntime::new(None);
runtime.mount_fs(Box::new(MemFsBackend::default()));
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
let code = assemble(
"PUSH_I32 0\nPUSH_I32 0\nPUSH_CONST 0\nHOSTCALL 0\nPOP_N 2\nPUSH_I32 0\nHOSTCALL 1\nPOP_N 1\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 10\nHOSTCALL 2\nHALT",
)
.expect("assemble");
let program = serialized_single_function_module_with_consts(
code,
vec![ConstantPoolEntry::String("6869".into())],
vec![
SyscallDecl {
module: "mem".into(),
name: "slot_write".into(),
version: 1,
arg_slots: 3,
ret_slots: 2,
},
SyscallDecl {
module: "mem".into(),
name: "slot_commit".into(),
version: 1,
arg_slots: 1,
ret_slots: 1,
},
SyscallDecl {
module: "mem".into(),
name: "slot_read".into(),
version: 1,
arg_slots: 3,
ret_slots: 3,
},
],
);
let cartridge = Cartridge {
app_id: 42,
title: "Memcard Game".into(),
app_version: "1.0.0".into(),
app_mode: AppMode::Game,
capabilities: caps::FS,
program,
assets: AssetsPayloadSource::empty(),
asset_table: vec![],
preload: vec![],
};
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
assert!(report.is_none(), "memcard roundtrip must not crash");
assert!(vm.is_halted());
assert_eq!(
vm.operand_stack_top(3),
vec![Value::Int64(2), Value::String("6869".into()), Value::Int64(0)]
);
}
#[test]
fn tick_memcard_access_is_denied_for_non_game_profile() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
let code = assemble("HOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "mem".into(),
name: "slot_count".into(),
version: 1,
arg_slots: 0,
ret_slots: 2,
}],
);
let cartridge = Cartridge {
app_id: 101,
title: "System App".into(),
app_version: "1.0.0".into(),
app_mode: AppMode::System,
capabilities: caps::FS,
program,
assets: AssetsPayloadSource::empty(),
asset_table: vec![],
preload: vec![],
};
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(&mut vm, &signals, &mut hardware);
assert!(report.is_none(), "non-game memcard call must return status");
assert!(vm.is_halted());
assert_eq!(vm.operand_stack_top(2), vec![Value::Int64(0), Value::Int64(4)]);
}

View File

@ -4,7 +4,7 @@
{"type":"discussion","id":"DSC-0021","status":"done","ticket":"asset-entry-codec-enum-with-metadata","title":"Asset Entry Codec Enum Contract","created_at":"2026-04-09","updated_at":"2026-04-09","tags":["asset","runtime","codec","metadata"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0024","file":"lessons/DSC-0021-asset-entry-codec-enum-contract/LSN-0024-string-on-the-wire-enum-in-runtime.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]}
{"type":"discussion","id":"DSC-0022","status":"done","ticket":"tile-bank-vs-glyph-bank-domain-naming","title":"Glyph Bank Domain Naming Contract","created_at":"2026-04-09","updated_at":"2026-04-10","tags":["gfx","runtime","naming","domain-model"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0025","file":"lessons/DSC-0022-glyph-bank-domain-naming/LSN-0025-rename-artifact-by-meaning-not-by-token.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
{"type":"discussion","id":"DSC-0001","status":"done","ticket":"legacy-runtime-learn-import","title":"Import legacy runtime learn into discussion lessons","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["migration","tech-debt"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0001","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0001-prometeu-learn-index.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0002","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0002-historical-asset-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0003","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0003-historical-audio-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0004","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0004-historical-cartridge-boot-protocol-and-manifest-authority.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0005","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0005-historical-game-memcard-slots-surface-and-semantics.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0006","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0006-historical-gfx-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0007","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0007-historical-retired-fault-and-input-decisions.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0008","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0008-historical-vm-core-and-assets.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0009","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0009-mental-model-asset-management.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0010","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0010-mental-model-audio.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0011","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0011-mental-model-gfx.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0012","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0012-mental-model-input.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0013","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0013-mental-model-observability-and-debugging.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0014","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0014-mental-model-portability-and-cross-platform.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0015","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0015-mental-model-save-memory-and-memcard.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0016","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0016-mental-model-status-first-and-fault-thinking.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0017","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0017-mental-model-time-and-cycles.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0018","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0018-mental-model-touch.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]}
{"type":"discussion","id":"DSC-0002","status":"open","ticket":"runtime-edge-test-plan","title":"Agenda - Runtime Edge Test Plan","created_at":"2026-03-27","updated_at":"2026-04-20","tags":[],"agendas":[{"id":"AGD-0001","file":"workflow/agendas/AGD-0001-runtime-edge-test-plan.md","status":"done","created_at":"2026-03-27","updated_at":"2026-04-20"}],"decisions":[{"id":"DEC-0020","file":"workflow/decisions/DEC-0020-runtime-edge-coverage-governance-by-domain.md","status":"in_progress","created_at":"2026-04-20","updated_at":"2026-04-20","ref_agenda":"AGD-0001"}],"plans":[{"id":"PLN-0037","file":"workflow/plans/PLN-0037-runtime-edge-coverage-governance-foundation.md","status":"done","created_at":"2026-04-20","updated_at":"2026-04-20","ref_decisions":["DEC-0020"]},{"id":"PLN-0038","file":"workflow/plans/PLN-0038-firmware-and-host-dependent-domain-coverage-expansion.md","status":"done","created_at":"2026-04-20","updated_at":"2026-04-20","ref_decisions":["DEC-0020"]},{"id":"PLN-0039","file":"workflow/plans/PLN-0039-incremental-runtime-domain-suite-split-and-baselines.md","status":"open","created_at":"2026-04-20","updated_at":"2026-04-20","ref_decisions":["DEC-0020"]}],"lessons":[]}
{"type":"discussion","id":"DSC-0002","status":"open","ticket":"runtime-edge-test-plan","title":"Agenda - Runtime Edge Test Plan","created_at":"2026-03-27","updated_at":"2026-04-20","tags":[],"agendas":[{"id":"AGD-0001","file":"workflow/agendas/AGD-0001-runtime-edge-test-plan.md","status":"done","created_at":"2026-03-27","updated_at":"2026-04-20"}],"decisions":[{"id":"DEC-0020","file":"workflow/decisions/DEC-0020-runtime-edge-coverage-governance-by-domain.md","status":"in_progress","created_at":"2026-04-20","updated_at":"2026-04-20","ref_agenda":"AGD-0001"}],"plans":[{"id":"PLN-0037","file":"workflow/plans/PLN-0037-runtime-edge-coverage-governance-foundation.md","status":"done","created_at":"2026-04-20","updated_at":"2026-04-20","ref_decisions":["DEC-0020"]},{"id":"PLN-0038","file":"workflow/plans/PLN-0038-firmware-and-host-dependent-domain-coverage-expansion.md","status":"done","created_at":"2026-04-20","updated_at":"2026-04-20","ref_decisions":["DEC-0020"]},{"id":"PLN-0039","file":"workflow/plans/PLN-0039-incremental-runtime-domain-suite-split-and-baselines.md","status":"done","created_at":"2026-04-20","updated_at":"2026-04-20","ref_decisions":["DEC-0020"]}],"lessons":[]}
{"type":"discussion","id":"DSC-0003","status":"open","ticket":"packed-cartridge-loader-pmc","title":"Agenda - Packed Cartridge Loader PMC","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0002","file":"workflow/agendas/AGD-0002-packed-cartridge-loader-pmc.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0004","status":"open","ticket":"system-run-cart","title":"Agenda - System Run Cart","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0003","file":"workflow/agendas/AGD-0003-system-run-cart.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0005","status":"open","ticket":"system-fault-semantics-and-control-surface","title":"Agenda - System Fault Semantics and Control Surface","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0004","file":"workflow/agendas/AGD-0004-system-fault-semantics-and-control-surface.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}

View File

@ -2,9 +2,9 @@
id: PLN-0039
ticket: runtime-edge-test-plan
title: Plan - Incremental Runtime Domain Suite Split and Baselines
status: open
status: done
created: 2026-04-20
completed:
completed: 2026-04-20
tags: [tests, runtime, fs, asset, organization]
---
@ -130,4 +130,3 @@ Document or encode enough structure that maintainers can tell where to add tests
- Moving tests mechanically without improving domain clarity.
- Creating fragmented helpers that are harder to reuse than the current monolith.
- Treating structural cleanup as more important than preserving behavioral coverage.