diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs b/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs index f3b0fdfc..84a5cea4 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs @@ -2,8 +2,10 @@ use super::*; use prometeu_bytecode::TRAP_TYPE; use prometeu_bytecode::Value; use prometeu_bytecode::assembler::assemble; -use prometeu_bytecode::model::{BytecodeModule, FunctionMeta, SyscallDecl}; +use prometeu_bytecode::model::{BytecodeModule, ConstantPoolEntry, FunctionMeta, SyscallDecl}; use prometeu_drivers::hardware::Hardware; +use prometeu_hal::AudioOpStatus; +use prometeu_hal::asset::AssetOpStatus; use prometeu_hal::InputSignals; use prometeu_hal::cartridge::Cartridge; use prometeu_hal::syscalls::caps; @@ -25,9 +27,17 @@ fn cartridge_with_program(program: Vec, capabilities: u64) -> Cartridge { } fn serialized_single_function_module(code: Vec, syscalls: Vec) -> Vec { + serialized_single_function_module_with_consts(code, vec![], syscalls) +} + +fn serialized_single_function_module_with_consts( + code: Vec, + const_pool: Vec, + syscalls: Vec, +) -> Vec { BytecodeModule { version: 0, - const_pool: vec![], + const_pool, functions: vec![FunctionMeta { code_offset: 0, code_len: code.len() as u32, @@ -275,3 +285,122 @@ fn initialize_vm_failure_clears_previous_identity_and_handles() { assert_eq!(runtime.telemetry_current.cycles_used, 0); assert!(matches!(runtime.last_crash_report, Some(CrashReport::VmInit { .. }))); } + +#[test] +fn tick_gfx_set_sprite_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_CONST 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 1\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT", + ) + .expect("assemble"); + let program = serialized_single_function_module_with_consts( + code, + vec![ConstantPoolEntry::String("missing_tile_bank".into())], + vec![SyscallDecl { + module: "gfx".into(), + name: "set_sprite".into(), + version: 1, + arg_slots: 10, + ret_slots: 1, + }], + ); + let cartridge = cartridge_with_program(program, caps::GFX); + + 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(1)]); +} + +#[test] +fn tick_audio_play_sample_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 -1\nPUSH_I32 0\nPUSH_I32 255\nPUSH_I32 128\nPUSH_I32 1\nHOSTCALL 0\nHALT") + .expect("assemble"); + let program = serialized_single_function_module( + code, + vec![SyscallDecl { + module: "audio".into(), + name: "play_sample".into(), + version: 1, + arg_slots: 5, + ret_slots: 1, + }], + ); + let cartridge = cartridge_with_program(program, caps::AUDIO); + + 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(AudioOpStatus::ArgRangeInvalid as i64)]); +} + +#[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_gfx_set_sprite_type_mismatch_surfaces_trap_not_panic() { + let mut runtime = VirtualMachineRuntime::new(None); + let mut vm = VirtualMachine::default(); + let mut hardware = Hardware::new(); + let signals = InputSignals::default(); + let code = assemble( + "PUSH_BOOL 1\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 1\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT", + ) + .expect("assemble"); + let program = serialized_single_function_module( + code, + vec![SyscallDecl { + module: "gfx".into(), + name: "set_sprite".into(), + version: 1, + arg_slots: 10, + ret_slots: 1, + }], + ); + let cartridge = cartridge_with_program(program, caps::GFX); + + runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime + .tick(&mut vm, &signals, &mut hardware) + .expect("type mismatch must surface as trap"); + match report { + CrashReport::VmTrap { trap } => { + assert_eq!(trap.code, TRAP_TYPE); + assert!(trap.message.contains("Expected string asset_name")); + } + other => panic!("expected VmTrap crash report, got {:?}", other), + } + assert!(matches!(runtime.last_crash_report, Some(CrashReport::VmTrap { .. }))); +} diff --git a/crates/console/prometeu-vm/src/virtual_machine.rs b/crates/console/prometeu-vm/src/virtual_machine.rs index 68094a2a..2433385e 100644 --- a/crates/console/prometeu-vm/src/virtual_machine.rs +++ b/crates/console/prometeu-vm/src/virtual_machine.rs @@ -2560,6 +2560,41 @@ mod tests { } } + #[test] + fn test_status_first_syscall_results_count_mismatch_panic() { + // GfxSetSprite (0x1007) expects 1 result. + let code = assemble( + "PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nSYSCALL 0x1007", + ) + .expect("assemble"); + + struct BadNativeNoReturn; + impl NativeInterface for BadNativeNoReturn { + fn syscall( + &mut self, + _id: u32, + _args: &[Value], + _ret: &mut HostReturn, + _ctx: &mut HostContext, + ) -> Result<(), VmFault> { + // Wrong: status-first syscall must return one status slot. + Ok(()) + } + } + + let mut vm = new_test_vm(code, vec![]); + vm.set_capabilities(prometeu_hal::syscalls::caps::GFX); + let mut native = BadNativeNoReturn; + let mut ctx = HostContext::new(None); + + vm.prepare_call("0"); + let report = vm.run_budget(100, &mut native, &mut ctx).expect("run"); + match report.reason { + LogicalFrameEndingReason::Panic(msg) => assert!(msg.contains("results mismatch")), + other => panic!("Expected Panic, got {:?}", other), + } + } + #[test] fn test_loader_hardening_invalid_magic() { let mut vm = VirtualMachine::default();