From eddd99b75472a725ea0c781274f436b1c23f7b65 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 20 Apr 2026 18:08:10 +0100 Subject: [PATCH] implements PLN-0039 --- .../src/virtual_machine_runtime/tests.rs | 449 +----------------- .../tests_asset_bank.rs | 347 ++++++++++++++ .../tests_fs_memcard.rs | 97 ++++ discussion/index.ndjson | 2 +- ...untime-domain-suite-split-and-baselines.md | 5 +- 5 files changed, 453 insertions(+), 447 deletions(-) create mode 100644 crates/console/prometeu-system/src/virtual_machine_runtime/tests_asset_bank.rs create mode 100644 crates/console/prometeu-system/src/virtual_machine_runtime/tests_fs_memcard.rs 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 21378beb..dbe3eee5 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs @@ -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)]); -} diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime/tests_asset_bank.rs b/crates/console/prometeu-system/src/virtual_machine_runtime/tests_asset_bank.rs new file mode 100644 index 00000000..39b99e4e --- /dev/null +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/tests_asset_bank.rs @@ -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), + ] + ); +} diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime/tests_fs_memcard.rs b/crates/console/prometeu-system/src/virtual_machine_runtime/tests_fs_memcard.rs new file mode 100644 index 00000000..a9878795 --- /dev/null +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/tests_fs_memcard.rs @@ -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)]); +} diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 61be358f..0c546111 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -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":[]} diff --git a/discussion/workflow/plans/PLN-0039-incremental-runtime-domain-suite-split-and-baselines.md b/discussion/workflow/plans/PLN-0039-incremental-runtime-domain-suite-split-and-baselines.md index 16e24dd4..55121b87 100644 --- a/discussion/workflow/plans/PLN-0039-incremental-runtime-domain-suite-split-and-baselines.md +++ b/discussion/workflow/plans/PLN-0039-incremental-runtime-domain-suite-split-and-baselines.md @@ -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. -