diff --git a/crates/console/prometeu-drivers/src/audio.rs b/crates/console/prometeu-drivers/src/audio.rs index f02ad497..e6c490f9 100644 --- a/crates/console/prometeu-drivers/src/audio.rs +++ b/crates/console/prometeu-drivers/src/audio.rs @@ -277,3 +277,54 @@ impl Audio { self.commands.clear(); } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::memory_banks::{MemoryBanks, SoundBankPoolAccess, SoundBankPoolInstaller}; + use prometeu_hal::sound_bank::SoundBank; + + fn sample() -> Arc { + Arc::new(Sample::new(44_100, vec![0, 1, 0, -1])) + } + + #[test] + fn play_returns_voice_invalid_for_out_of_range_voice() { + let banks = Arc::new(MemoryBanks::new()); + let sound_banks = Arc::clone(&banks) as Arc; + let mut audio = Audio::new(sound_banks); + + let status = audio.play(0, 0, MAX_CHANNELS, 255, 128, 1.0, 0, LoopMode::Off); + + assert_eq!(status, AudioOpStatus::VoiceInvalid); + assert!(audio.commands.is_empty()); + } + + #[test] + fn play_returns_sample_not_found_when_sample_is_missing() { + let banks = Arc::new(MemoryBanks::new()); + let installer = Arc::clone(&banks) as Arc; + installer.install_sound_bank(0, Arc::new(SoundBank::new(vec![sample()]))); + let sound_banks = Arc::clone(&banks) as Arc; + let mut audio = Audio::new(sound_banks); + + let status = audio.play(0, 1, 0, 255, 128, 1.0, 0, LoopMode::Off); + + assert_eq!(status, AudioOpStatus::SampleNotFound); + assert!(audio.commands.is_empty()); + } + + #[test] + fn play_sample_returns_voice_invalid_without_side_effects() { + let banks = Arc::new(MemoryBanks::new()); + let sound_banks = Arc::clone(&banks) as Arc; + let mut audio = Audio::new(sound_banks); + + let status = + audio.play_sample(sample(), MAX_CHANNELS, 255, 128, 1.0, 0, LoopMode::Off); + + assert_eq!(status, AudioOpStatus::VoiceInvalid); + assert!(!audio.voices.iter().any(|voice| voice.active)); + assert!(audio.commands.is_empty()); + } +} diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs b/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs index edefbca8..84b4376b 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs @@ -195,10 +195,13 @@ impl NativeInterface for VirtualMachineRuntime { let pan_raw = expect_int(args, 3)?; let pitch = expect_number(args, 4, "pitch")?; + if voice_id_raw < 0 || voice_id_raw >= 16 { + ret.push_int(AudioOpStatus::VoiceInvalid as i64); + return Ok(()); + } + if sample_id_raw < 0 || sample_id_raw > u16::MAX as i64 - || voice_id_raw < 0 - || voice_id_raw >= 16 || !(0..=255).contains(&volume_raw) || !(0..=255).contains(&pan_raw) || !pitch.is_finite() @@ -222,13 +225,7 @@ impl NativeInterface for VirtualMachineRuntime { Ok(()) } Syscall::AudioPlay => { - let asset_name = match args - .first() - .ok_or_else(|| VmFault::Panic("Missing asset_name".into()))? - { - Value::String(s) => s.clone(), - _ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string asset_name".into())), - }; + let asset_name = expect_string(args, 0, "asset_name")?; let sample_id_raw = expect_int(args, 1)?; let voice_id_raw = expect_int(args, 2)?; let volume_raw = expect_int(args, 3)?; @@ -239,10 +236,13 @@ impl NativeInterface for VirtualMachineRuntime { _ => prometeu_hal::LoopMode::On, }; + if voice_id_raw < 0 || voice_id_raw >= 16 { + ret.push_int(AudioOpStatus::VoiceInvalid as i64); + return Ok(()); + } + if sample_id_raw < 0 || sample_id_raw > u16::MAX as i64 - || voice_id_raw < 0 - || voice_id_raw >= 16 || !(0..=255).contains(&volume_raw) || !(0..=255).contains(&pan_raw) || !pitch.is_finite() 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 3d7587da..befcb10d 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs @@ -447,6 +447,102 @@ fn tick_audio_play_sample_operational_error_returns_status_not_crash() { assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AudioOpStatus::ArgRangeInvalid as i64)]); } +#[test] +fn tick_audio_play_voice_invalid_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 16\nPUSH_I32 255\nPUSH_I32 128\nPUSH_I32 1\nPUSH_I32 0\nHOSTCALL 0\nHALT", + ) + .expect("assemble"); + let program = serialized_single_function_module_with_consts( + code, + vec![ConstantPoolEntry::String("missing_sound_bank".into())], + vec![SyscallDecl { + module: "audio".into(), + name: "play".into(), + version: 1, + arg_slots: 7, + 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(), "invalid voice must not crash"); + assert!(vm.is_halted()); + assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AudioOpStatus::VoiceInvalid as i64)]); +} + +#[test] +fn tick_audio_play_missing_asset_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 255\nPUSH_I32 128\nPUSH_I32 1\nPUSH_I32 0\nHOSTCALL 0\nHALT", + ) + .expect("assemble"); + let program = serialized_single_function_module_with_consts( + code, + vec![ConstantPoolEntry::String("missing_sound_bank".into())], + vec![SyscallDecl { + module: "audio".into(), + name: "play".into(), + version: 1, + arg_slots: 7, + 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(), "missing audio asset must not crash"); + assert!(vm.is_halted()); + assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AudioOpStatus::AssetNotFound as i64)]); +} + +#[test] +fn tick_audio_play_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 255\nPUSH_I32 128\nPUSH_I32 1\nPUSH_I32 0\nHOSTCALL 0\nHALT", + ) + .expect("assemble"); + let program = serialized_single_function_module( + code, + vec![SyscallDecl { + module: "audio".into(), + name: "play".into(), + version: 1, + arg_slots: 7, + 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) + .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 { .. }))); +} + #[test] fn tick_asset_commit_operational_error_returns_status_not_crash() { let mut runtime = VirtualMachineRuntime::new(None); diff --git a/docs/runtime/pull-requests/PR003-audio-status-first-surface-and-fault-matrix.md b/docs/runtime/pull-requests/PR003-audio-status-first-surface-and-fault-matrix.md deleted file mode 100644 index 6bcc43f9..00000000 --- a/docs/runtime/pull-requests/PR003-audio-status-first-surface-and-fault-matrix.md +++ /dev/null @@ -1,43 +0,0 @@ -# PR003 - Audio Status-First Surface and Fault Matrix - -## Briefing - -A decision `009` fixou que `audio` deve seguir status-first. - -No MVP atual, `audio.play` e `audio.play_sample` devem retornar `status:int`. - -## Alvo - -Aplicar a decisao `009` em spec e runtime para o dominio `audio`. - -Arquivos principais: - -- `docs/runtime/specs/05-audio-peripheral.md` -- `crates/console/prometeu-hal/src/syscalls/domains/audio.rs` -- `crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs` -- `crates/console/prometeu-drivers/src/audio.rs` - -## Escopo Funcional - -- mudar surface de retorno de `audio.play` e `audio.play_sample` para `status`; -- remover fallback implicito e no-op silencioso para erros operacionais; -- validar faixas normativas (`volume`, `pan`, `pitch`) com status explicito; -- limpar caminhos que poderiam escalar para `Panic` por input de app. - -## Fora de Escopo - -- novos comandos de audio (stop/set pan/set pitch etc) fora do contrato atual; -- mudanca de arquitetura do mixer host. - -## Critérios de Aceite - -- `ret_slots` de `audio.play` e `audio.play_sample` atualizados no registry; -- casos operacionais retornam status inteiro canonico; -- `voice_id` invalido e asset/sample ausente nao ficam silenciosos; -- sem `Panic` operacional no dominio. - -## Tests - -- `cargo test -p prometeu-system` -- `cargo test -p prometeu-drivers` -- cenarios com `voice_id` invalido, asset ausente e range invalido. diff --git a/docs/runtime/specs/05-audio-peripheral.md b/docs/runtime/specs/05-audio-peripheral.md index e475acef..485734ca 100644 --- a/docs/runtime/specs/05-audio-peripheral.md +++ b/docs/runtime/specs/05-audio-peripheral.md @@ -192,6 +192,13 @@ In the current MVP: - `audio.play` returns `status:int`; - `audio.play_sample` returns `status:int`. +Return-shape matrix in v1 syscall surface: + +| Syscall | Return | Policy basis | +| --------------------- | ------------ | -------------------------------------- | +| `audio.play` | `status:int` | operational rejection is observable | +| `audio.play_sample` | `status:int` | operational rejection is observable | + ### 11.2 Minimum status table for `play`/`play_sample` - `0` = `OK` @@ -205,4 +212,5 @@ Operational rules: - no fallback to default bank when an asset cannot be resolved; - no silent no-op for invalid `voice_id`; +- invalid `voice_id` must return `VOICE_INVALID`, not `ARG_RANGE_INVALID`; - invalid numeric ranges (e.g. `volume`, `pan`, `pitch`) must return explicit status.