diff --git a/Makefile b/Makefile index 81ea3e42..5b8e1be5 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: fmt fmt-check clippy tes-local test-debugger-socket test ci cobertura +.PHONY: fmt fmt-check clippy test-local test-debugger-socket clean coverage coverage-xml coverage-json coverage-report-json coverage-domain-list coverage-domain-evidence test ci cobertura fmt: cargo fmt @@ -21,12 +21,25 @@ clean: coverage: cargo llvm-cov --workspace --all-features --html --output-dir target/llvm-cov +coverage-report-json: + cargo llvm-cov report --json --output-path target/llvm-cov/report.json + coverage-xml: cargo llvm-cov report --cobertura --output-path target/llvm-cov/cobertura.xml coverage-json: cargo llvm-cov report --json --summary-only --output-path target/llvm-cov/summary.json +coverage-domain-list: + ./scripts/coverage-domain-evidence.sh --list + +coverage-domain-evidence: + @if [ -z "$(DOMAIN)" ]; then \ + echo "usage: make coverage-domain-evidence DOMAIN="; \ + exit 1; \ + fi + ./scripts/coverage-domain-evidence.sh "$(DOMAIN)" + test: fmt-check clippy test-local test-debugger-socket -ci: clean fmt-check clippy coverage +ci: clean fmt-check clippy coverage coverage-report-json cobertura: coverage-xml coverage-json diff --git a/README.md b/README.md index ad0f6ed4..b5723d7f 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,53 @@ cargo run -q -p prometeu-cli --bin prometeu -- run test-cartridges/stress-consol The desktop runtime opens a native window through the host layer, so this last command is intended for a local graphical environment. +## Coverage Governance + +The repository uses a two-layer runtime-edge coverage model: + +- a mandatory global workspace coverage gate in CI; +- domain-oriented review evidence for the canonical runtime-edge domains. + +Current canonical domains: + +- `system/runtime` +- `fs` +- `asset/bank` +- `firmware` +- `host-dependent` + +Global coverage artifacts: + +```bash +make ci +make cobertura +``` + +This produces: + +- `target/llvm-cov/html` +- `target/llvm-cov/report.json` +- `target/llvm-cov/summary.json` +- `target/llvm-cov/cobertura.xml` + +Domain-oriented evidence helpers: + +```bash +make coverage-domain-list +make coverage-domain-evidence DOMAIN=firmware +make coverage-domain-evidence DOMAIN=host-dependent +``` + +Per-domain baselines currently start at `0`. +That does not waive test obligations. It means the project is adopting explicit domain evidence immediately while tightening quantitative expectations incrementally over time. + +When a PR changes the observable contract of a canonical domain, it is expected to: + +- add or adjust tests for that domain; or +- justify explicitly why the observable contract did not change. + +Improving the global percentage alone is not sufficient review evidence when the changed behavior belongs to a canonical domain. + ## Current State The project is still in active architectural and implementation convergence. diff --git a/crates/console/prometeu-firmware/src/firmware/firmware.rs b/crates/console/prometeu-firmware/src/firmware/firmware.rs index 262e5260..c2ef3be9 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware.rs @@ -175,6 +175,24 @@ mod tests { use prometeu_hal::syscalls::caps; use prometeu_system::CrashReport; + fn halting_program() -> Vec { + let code = assemble("HALT").expect("assemble"); + BytecodeModule { + version: 0, + const_pool: vec![], + functions: vec![FunctionMeta { + code_offset: 0, + code_len: code.len() as u32, + ..Default::default() + }], + code, + debug_info: None, + exports: vec![], + syscalls: vec![], + } + .serialize() + } + fn invalid_game_cartridge() -> Cartridge { Cartridge { app_id: 7, @@ -225,6 +243,20 @@ mod tests { } } + fn valid_cartridge(app_mode: AppMode) -> Cartridge { + Cartridge { + app_id: 9, + title: "Valid Cart".into(), + app_version: "1.0.0".into(), + app_mode, + capabilities: caps::NONE, + program: halting_program(), + assets: AssetsPayloadSource::empty(), + asset_table: vec![], + preload: vec![], + } + } + #[test] fn load_cartridge_transitions_to_app_crashes_when_vm_init_fails() { let mut firmware = Firmware::new(None); @@ -268,4 +300,53 @@ mod tests { other => panic!("expected AppCrashes state, got {:?}", other), } } + + #[test] + fn reset_routes_hub_boot_target_to_splash_screen() { + let mut firmware = Firmware::new(None); + let mut hardware = Hardware::new(); + let signals = InputSignals::default(); + + firmware.boot_target = BootTarget::Hub; + firmware.tick(&signals, &mut hardware); + + assert!(matches!(firmware.state, FirmwareState::SplashScreen(_))); + } + + #[test] + fn reset_routes_cartridge_boot_target_to_launch_hub() { + let mut firmware = Firmware::new(None); + let mut hardware = Hardware::new(); + let signals = InputSignals::default(); + + firmware.boot_target = + BootTarget::Cartridge { path: "missing-cart".into(), debug: false, debug_port: 7777 }; + firmware.tick(&signals, &mut hardware); + + assert!(matches!(firmware.state, FirmwareState::LaunchHub(_))); + } + + #[test] + fn load_cartridge_routes_system_apps_back_to_hub_home() { + let mut firmware = Firmware::new(None); + let mut hardware = Hardware::new(); + let signals = InputSignals::default(); + + firmware.load_cartridge(valid_cartridge(AppMode::System)); + firmware.tick(&signals, &mut hardware); + + assert!(matches!(firmware.state, FirmwareState::HubHome(_))); + } + + #[test] + fn load_cartridge_routes_game_apps_to_game_running() { + let mut firmware = Firmware::new(None); + let mut hardware = Hardware::new(); + let signals = InputSignals::default(); + + firmware.load_cartridge(valid_cartridge(AppMode::Game)); + firmware.tick(&signals, &mut hardware); + + assert!(matches!(firmware.state, FirmwareState::GameRunning(_))); + } } 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/crates/host/prometeu-host-desktop-winit/src/debugger.rs b/crates/host/prometeu-host-desktop-winit/src/debugger.rs index c698ec2e..6941fa46 100644 --- a/crates/host/prometeu-host-desktop-winit/src/debugger.rs +++ b/crates/host/prometeu-host-desktop-winit/src/debugger.rs @@ -368,3 +368,59 @@ impl HostDebugger { self.last_fault_summary = Some(summary); } } + +#[cfg(test)] +mod tests { + use super::*; + use prometeu_drivers::hardware::Hardware; + use prometeu_hal::debugger_protocol::DebugCommand; + + #[test] + fn setup_boot_target_ignores_hub_and_non_debug_cartridges() { + let mut debugger = HostDebugger::new(); + let mut firmware = Firmware::new(None); + + debugger.setup_boot_target(&BootTarget::Hub, &mut firmware); + assert!(!debugger.waiting_for_start); + assert!(debugger.listener.is_none()); + + debugger.setup_boot_target( + &BootTarget::Cartridge { path: "dummy".into(), debug: false, debug_port: 7777 }, + &mut firmware, + ); + assert!(!debugger.waiting_for_start); + assert!(debugger.listener.is_none()); + } + + #[test] + fn handle_command_updates_pause_and_step_flags_without_host_io() { + let mut debugger = HostDebugger::new(); + let mut firmware = Firmware::new(None); + let mut hardware = Hardware::new(); + + debugger.handle_command(DebugCommand::Pause, &mut firmware, &mut hardware); + assert!(firmware.os.paused); + + debugger.handle_command(DebugCommand::Resume, &mut firmware, &mut hardware); + assert!(!firmware.os.paused); + + debugger.handle_command(DebugCommand::StepFrame, &mut firmware, &mut hardware); + assert!(!firmware.os.paused); + assert!(firmware.os.debug_step_request); + } + + #[test] + fn handle_command_start_leaves_waiting_for_start_mode() { + let mut debugger = HostDebugger::new(); + let mut firmware = Firmware::new(None); + let mut hardware = Hardware::new(); + + debugger.waiting_for_start = true; + firmware.os.paused = true; + + debugger.handle_command(DebugCommand::Start, &mut firmware, &mut hardware); + + assert!(!debugger.waiting_for_start); + assert!(!firmware.os.paused); + } +} diff --git a/crates/host/prometeu-host-desktop-winit/src/runner.rs b/crates/host/prometeu-host-desktop-winit/src/runner.rs index fd0468fe..a8b28e39 100644 --- a/crates/host/prometeu-host-desktop-winit/src/runner.rs +++ b/crates/host/prometeu-host-desktop-winit/src/runner.rs @@ -604,6 +604,9 @@ mod tests { } } + // These ignored tests intentionally cover host-dependent behavior that requires + // real socket bind/connect coordination. Deterministic debugger state changes + // are covered below the host layer in `debugger.rs`. #[test] #[ignore = "requires localhost TCP bind/connect; run via `cargo test -p prometeu-host-desktop-winit --lib -- --ignored`"] fn test_debug_port_opens() { diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 9d953d86..0c546111 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,10 +1,10 @@ -{"type":"meta","next_id":{"DSC":29,"AGD":29,"DEC":20,"PLN":37,"LSN":37,"CLSN":1}} +{"type":"meta","next_id":{"DSC":29,"AGD":29,"DEC":21,"PLN":40,"LSN":37,"CLSN":1}} {"type":"discussion","id":"DSC-0023","status":"done","ticket":"perf-full-migration-to-atomic-telemetry","title":"Agenda - [PERF] Full Migration to Atomic Telemetry","created_at":"2026-04-10","updated_at":"2026-04-10","tags":["perf","runtime","telemetry"],"agendas":[{"id":"AGD-0021","file":"workflow/agendas/AGD-0021-full-migration-to-atomic-telemetry.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"decisions":[{"id":"DEC-0008","file":"workflow/decisions/DEC-0008-full-migration-to-atomic-telemetry.md","status":"accepted","created_at":"2026-04-10","updated_at":"2026-04-10"}],"plans":[{"id":"PLN-0007","file":"workflow/plans/PLN-0007-full-migration-to-atomic-telemetry.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"lessons":[{"id":"LSN-0028","file":"lessons/DSC-0023-perf-full-migration-to-atomic-telemetry/LSN-0028-converging-to-single-atomic-telemetry-source.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]} {"type":"discussion","id":"DSC-0020","status":"done","ticket":"jenkins-gitea-integration","title":"Jenkins Gitea Integration and Relocation","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["ci","jenkins","gitea"],"agendas":[{"id":"AGD-0018","file":"workflow/agendas/AGD-0018-jenkins-gitea-integration-and-relocation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"decisions":[{"id":"DEC-0003","file":"workflow/decisions/DEC-0003-jenkins-gitea-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0003","file":"workflow/plans/PLN-0003-jenkins-gitea-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0021","file":"lessons/DSC-0020-jenkins-gitea-integration/LSN-0021-jenkins-gitea-integration.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]} {"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-03-27","tags":[],"agendas":[{"id":"AGD-0001","file":"workflow/agendas/AGD-0001-runtime-edge-test-plan.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"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":[]} @@ -22,7 +22,7 @@ {"type":"discussion","id":"DSC-0027","status":"done","ticket":"frame-composer-public-syscall-surface","title":"Agenda - FrameComposer Public Syscall Surface","created_at":"2026-04-17","updated_at":"2026-04-18","tags":["gfx","runtime","syscall","abi","frame-composer","scene","camera","sprites"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0032","file":"lessons/DSC-0027-frame-composer-public-syscall-surface/LSN-0032-public-abi-must-follow-the-canonical-service-boundary.md","status":"done","created_at":"2026-04-18","updated_at":"2026-04-18"}]} {"type":"discussion","id":"DSC-0028","status":"done","ticket":"deferred-overlay-and-primitive-composition","title":"Deferred Overlay and Primitive Composition over FrameComposer","created_at":"2026-04-18","updated_at":"2026-04-18","tags":["gfx","runtime","render","frame-composer","overlay","primitives","hud"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0033","file":"lessons/DSC-0028-deferred-overlay-and-primitive-composition/LSN-0033-debug-primitives-should-be-a-final-overlay-not-part-of-game-composition.md","status":"done","created_at":"2026-04-18","updated_at":"2026-04-18"}]} {"type":"discussion","id":"DSC-0014","status":"done","ticket":"perf-vm-allocation-and-copy-pressure","title":"Agenda - [PERF] VM Allocation and Copy Pressure","created_at":"2026-03-27","updated_at":"2026-04-20","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0035","file":"lessons/DSC-0014-perf-vm-allocation-and-copy-pressure/LSN-0035-first-materialization-is-not-the-same-as-hot-path-copy-pressure.md","status":"done","created_at":"2026-04-20","updated_at":"2026-04-20"}]} -{"type":"discussion","id":"DSC-0015","status":"open","ticket":"perf-cartridge-boot-and-program-ownership","title":"Agenda - [PERF] Cartridge Boot and Program Ownership","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0014","file":"workflow/agendas/AGD-0014-perf-cartridge-boot-and-program-ownership.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0015","status":"abandoned","ticket":"perf-cartridge-boot-and-program-ownership","title":"Agenda - [PERF] Cartridge Boot and Program Ownership","created_at":"2026-03-27","updated_at":"2026-04-20","tags":[],"agendas":[{"id":"AGD-0014","file":"workflow/agendas/AGD-0014-perf-cartridge-boot-and-program-ownership.md","status":"abandoned","created_at":"2026-03-27","updated_at":"2026-04-20","_override_reason":"User explicitly chose to close the discussion without decision because FS->memory copy for the program is already acceptable."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to abandon the discussion without creating a decision because FS->memory copy for the program is already acceptable."} {"type":"discussion","id":"DSC-0016","status":"done","ticket":"tilemap-empty-cell-vs-tile-id-zero","title":"Tilemap Empty Cell vs Tile ID Zero","created_at":"2026-03-27","updated_at":"2026-04-09","tags":[],"agendas":[{"id":"AGD-0015","file":"workflow/agendas/AGD-0015-tilemap-empty-cell-vs-tile-id-zero.md","status":"done","created_at":"2026-03-27","updated_at":"2026-04-09"}],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0022","file":"lessons/DSC-0016-tilemap-empty-cell-semantics/LSN-0022-tilemap-empty-cell-convergence.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]} {"type":"discussion","id":"DSC-0017","status":"done","ticket":"asset-entry-metadata-normalization-contract","title":"Asset Entry Metadata Normalization Contract","created_at":"2026-03-27","updated_at":"2026-04-09","tags":[],"agendas":[{"id":"AGD-0016","file":"workflow/agendas/AGD-0016-asset-entry-metadata-normalization-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-04-09"}],"decisions":[{"id":"DEC-0004","file":"workflow/decisions/DEC-0004-asset-entry-metadata-normalization-contract.md","status":"accepted","created_at":"2026-04-09","updated_at":"2026-04-09"}],"plans":[],"lessons":[{"id":"LSN-0023","file":"lessons/DSC-0017-asset-metadata-normalization/LSN-0023-typed-asset-metadata-helpers.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]} {"type":"discussion","id":"DSC-0018","status":"done","ticket":"asset-load-asset-id-int-contract","title":"Asset Load Asset ID Int Contract","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["asset","runtime","abi"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0019","file":"lessons/DSC-0018-asset-load-asset-id-int-contract/LSN-0019-asset-load-id-abi-convergence.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]} diff --git a/discussion/workflow/agendas/AGD-0001-runtime-edge-test-plan.md b/discussion/workflow/agendas/AGD-0001-runtime-edge-test-plan.md index dbda70ec..742a6970 100644 --- a/discussion/workflow/agendas/AGD-0001-runtime-edge-test-plan.md +++ b/discussion/workflow/agendas/AGD-0001-runtime-edge-test-plan.md @@ -2,10 +2,10 @@ id: AGD-0001 ticket: runtime-edge-test-plan title: Agenda - Runtime Edge Test Plan -status: open +status: done created: 2026-03-27 -resolved: -decision: +resolved: 2026-04-20 +decision: DEC-0020 tags: [] --- @@ -13,36 +13,36 @@ tags: [] ## Problema -O core da VM está muito melhor testado do que a borda do runtime. +A borda do runtime deixou de ser "quase sem testes", mas continua sem contrato claro de suficiência por dominio. -O miolo já cobre verificador, scheduler, GC, patching e execução básica. Já a camada de syscalls de sistema, FS, assets, bank e fluxos de firmware ainda tem cobertura desigual e rasa. +Hoje já existe cobertura relevante para runtime edge, `VirtualFS`, asset/preload, status-first surfaces e parte do host. O problema atual não é ausência total de testes, e sim falta de uma régua explícita que diga quando um domínio está aceitavelmente coberto e que tipo de mudança deve obrigar novos testes. ## Dor -- A parte mais sujeita a integração com host é justamente a menos protegida. -- Regressões na borda podem passar com o core inteiro verde. -- O projeto ganha falsa sensação de maturidade porque o núcleo está sólido, mas a superfície pública do runtime ainda pode quebrar com facilidade. -- Sem matriz de testes da borda, futuras PRs tendem a cobrir só o happy path. +- Coverage global do workspace não prova que os domínios de maior risco estão realmente cobertos. +- Regressões de firmware, boot, host integration e superfícies status-first ainda podem passar com números globais "bons". +- Sem gates por domínio, futuras PRs tendem a melhorar percentuais agregados sem fechar cenários obrigatórios. +- A suíte atual está funcional, mas concentrada demais, o que dificulta enxergar lacunas e manter responsabilidade por domínio. ## Alvo da Discussao -Definir um plano de testes para a borda do runtime proporcional ao risco da plataforma. +Definir uma política mínima de cobertura para a borda do runtime proporcional ao risco da plataforma. Essa agenda deve produzir uma visão clara de: -- quais domínios precisam de testes; -- qual nível de teste pertence a cada domínio; -- quais cenários são mandatórios antes de chamar o runtime de estável. +- quais domínios têm gate próprio; +- quais cenários mínimos cada domínio deve cobrir; +- como coverage entra como evidência operacional, sem substituir a matriz qualitativa; +- que mudanças devem obrigar teste novo antes de merge. ## O Que Precisa Ser Definido 1. Matriz de cobertura por domínio. - - system + - system/runtime - fs - - log - - asset - - bank + - asset/bank - firmware transitions + - host-dependent surfaces quando aplicável 2. Tipos de teste. - unitário puro @@ -50,37 +50,185 @@ Essa agenda deve produzir uma visão clara de: - testes de firmware - testes host-dependentes isolados -3. Casos obrigatórios. - Cada domínio precisa definir: - - happy path; - - erro de contrato do guest; - - indisponibilidade/estado inválido do host; - - persistência/cleanup de estado quando aplicável. +3. Gates de aceitação por domínio. + Cada domínio precisa definir ao menos: + - cenários obrigatórios; + - tipo de evidência esperada; + - leitura de coverage por domínio como evidência obrigatória de revisão; + - como o gate por domínio evolui ao longo do tempo sem bloquear a adoção inicial. 4. Critério de severidade. - Que mudanças exigem teste novo obrigatório antes de merge. + Que mudanças exigem teste novo obrigatório antes de merge, mesmo que o percentual global de coverage continue aceitável. 5. Organização. - Onde os testes vivem para nao voltar a concentrar tudo em arquivos gigantes. + Onde os testes vivem para nao voltar a concentrar tudo em arquivos gigantes e para manter ownership claro por domínio. -## O Que Necessita Para Resolver +## O Que Ainda Precisa Ser Fechado -- inventário da cobertura atual; -- definição dos cenários obrigatórios por domínio; -- política mínima para testes ignorados ou dependentes de host; -- decisão sobre helpers compartilhados para fixtures de runtime. +- definição dos domínios canônicos e seus respectivos gates; +- definição dos cenários mínimos obrigatórios por domínio; +- política mínima para testes host-dependentes e `#[ignore]`; +- regra de convivência entre gate global de coverage e leitura segmentada por domínio; +- diretriz de organização para quebrar suites monolíticas de runtime. + +## Opcoes + +### Opcao A - Gate apenas global de coverage + +- **Abordagem:** manter somente o percentual agregado do workspace como barra de aceitação. +- **Pro:** simples de operar e fácil de automatizar no CI. +- **Con:** permite que domínios sensíveis continuem subcobertos enquanto o número global sobe. +- **Manutenibilidade:** boa operacionalmente, ruim como governança de risco. + +### Opcao B - Gate percentual rígido por domínio desde o início + +- **Abordagem:** exigir percentuais mínimos por domínio já na primeira versão da política. +- **Pro:** força disciplina quantitativa explícita por área. +- **Con:** cria atrito alto de adoção, exige segmentação mais sofisticada e pode travar a convergência inicial por falta de baseline confiável. +- **Manutenibilidade:** média; forte no longo prazo, pesada demais para o ponto atual do projeto. + +### Opcao C - Gate global de coverage + gate por domínio com coverage como evidência incremental + +- **Abordagem:** manter gate global obrigatório no CI e adotar gate por domínio baseado em cenários mandatórios e leitura de coverage segmentada como evidência. Baselines percentuais por domínio podem começar em `0` e subir com o tempo conforme a suíte amadurece. +- **Pro:** combina disciplina prática imediata com caminho realista para endurecer a barra por domínio sem bloquear a adoção. +- **Con:** exige revisão mais criteriosa, porque a aceitação não fica reduzida a um único número. +- **Manutenibilidade:** alta; permite endurecimento progressivo sem perder governança qualitativa. + +## Sugestao / Recomendacao + +Fechar esta agenda com a `Opcao C`: uma matriz pequena de domínios canônicos, gate global obrigatório de coverage e gate por domínio baseado em cenários mandatórios, usando leitura segmentada de `llvm-cov` como evidência de revisão e baseline quantitativo incremental. + +### Domínios canônicos propostos + +1. `system/runtime` +2. `fs` +3. `asset/bank` +4. `firmware` +5. `host-dependent` + +### Gates propostos por domínio + +#### `system/runtime` + +Cenários mínimos: + +- `init` +- `reset / cleanup` +- `trap vs panic` +- `pause / breakpoint` +- fronteira de `FRAME_SYNC` e budget + +Regra de aceitação: + +- qualquer mudança em lifecycle, tick, crash/report, pause/debug flow ou syscall de sistema deve adicionar ou ajustar testes desse domínio; +- coverage global não substitui a obrigação de cobrir o cenário alterado. + +#### `fs` + +Cenários mínimos: + +- happy path; +- path inválido / traversal; +- backend unhealthy; +- cleanup de handles e estado após reset/unmount quando aplicável. + +Regra de aceitação: + +- qualquer mudança em normalização de path, mount/unmount, handles ou contrato guest-host de filesystem deve adicionar ou ajustar testes desse domínio. + +#### `asset/bank` + +Cenários mínimos: + +- preload; +- `load / status / commit / cancel`; +- asset ausente; +- slot inválido; +- erro estrutural de bootstrap vs erro operacional em runtime. + +Regra de aceitação: + +- qualquer mudança em cartridge loader, preload, asset bridge, slot telemetry ou semântica status-first deve adicionar ou ajustar testes desse domínio. + +#### `firmware` + +Cenários mínimos: + +- fluxo de load do cart; +- branch por `AppMode`; +- falha de init levando ao crash path; +- coordenação básica de boot target e transição de estado. + +Regra de aceitação: + +- qualquer mudança em boot flow, cartridge launch, state transition ou crash path deve adicionar ou ajustar testes desse domínio. + +Observação: + +- este é o domínio com maior lacuna atual e deve ser a prioridade principal de expansão da suíte. + +#### `host-dependent` + +Cenários mínimos: + +- superfícies que dependem de socket, janela ou integração desktop devem ficar isoladas; +- quando possível, a parte determinística da regra deve existir também em teste console/runtime. + +Regra de aceitação: + +- testes `#[ignore]` são permitidos quando dependem de infraestrutura local real, mas devem trazer justificativa explícita; +- mudanças host-only não podem empurrar comportamento determinístico para testes exclusivamente desktop quando esse comportamento puder ser validado abaixo do host. + +Observação: + +- `host-dependent` não é um subsistema funcional paralelo a `fs` ou `firmware`; é um corte transversal de testabilidade e execução que governa onde certos testes podem viver e como devem ser justificados. + +### Uso de coverage + +- `coverage` global via `cargo llvm-cov` continua como gate operacional mínimo do CI; +- leitura segmentada por domínio deve ser usada como evidência obrigatória de revisão quando uma mudança tocar área sensível; +- percentuais por domínio podem começar em baseline `0` e subir progressivamente conforme o projeto estabiliza a segmentação e a matriz de testes; +- percentual global aceitável não dispensa cobertura dos cenários mandatórios do domínio afetado. + +### Prioridades sugeridas + +1. expandir cobertura de `firmware`; +2. consolidar política de `host-dependent`; +3. quebrar a suíte monolítica de `system/runtime` por responsabilidade, conforme os domínios forem sendo tocados; +4. manter `fs` e `asset/bank` como domínios já relativamente encaminhados, mas ainda sob gate explícito. + +### Barra sugerida para PRs + +- nenhum PR que altere domínio canônico deve entrar sem tocar testes do domínio afetado, ou sem justificar explicitamente por que o contrato observável não mudou; +- melhoria de coverage agregada é desejável, mas não conta como evidência suficiente sem cenário alinhado ao domínio alterado. + +## Perguntas em Aberto + +- Nenhuma ambiguidade arquitetural substantiva restante para fechar a política base. +- O endurecimento quantitativo por domínio fica explicitamente como evolução incremental a partir de baseline inicial `0`, sem bloquear a adoção imediata da política. + +## Estado Atual Relevante + +- O repositório já possui targets de coverage com `cargo llvm-cov` no `Makefile`. +- O CI já aplica gate global mínimo de coverage via Jenkins. +- O runtime já possui cobertura relevante para traps, status-first, assets, composer, audio, memcard e reset. +- `VirtualFS` já cobre parte importante do contrato de path normalization e rejeições antes do backend. +- Ainda falta tratar firmware transitions e superfícies host-dependent como domínios explicitamente governados pela agenda. ## Fora de Escopo - benchmark/performance suite; - fuzzing completo de toda a ISA; -- infraestrutura externa de CI além do necessário para os testes definidos. +- troca de ferramenta de coverage; +- perseguir 100% global como objetivo primário; +- transformar coverage percentual em substituto de cenários normativos. ## Critério de Saida Desta Agenda Pode virar PR quando houver: -- matriz de cobertura aprovada; +- matriz de gates por domínio aprovada; - lista de cenários mínimos por domínio; -- decisão de organização dos testes; -- barra clara para aceitar novas syscalls e mudanças de runtime. +- política explícita para testes host-dependent / `#[ignore]`; +- uso de `llvm-cov` encaixado como evidência operacional; +- barra clara para aceitar novas syscalls, mudanças de runtime e transições de firmware. diff --git a/discussion/workflow/agendas/AGD-0014-perf-cartridge-boot-and-program-ownership.md b/discussion/workflow/agendas/AGD-0014-perf-cartridge-boot-and-program-ownership.md deleted file mode 100644 index a22e32ed..00000000 --- a/discussion/workflow/agendas/AGD-0014-perf-cartridge-boot-and-program-ownership.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -id: AGD-0014 -ticket: perf-cartridge-boot-and-program-ownership -title: Agenda - [PERF] Cartridge Boot and Program Ownership -status: open -created: 2026-03-27 -resolved: -decision: -tags: [] ---- - -# Agenda - [PERF] Cartridge Boot and Program Ownership - -## Problema - -O bootstrap do cart ainda faz copia desnecessaria de dados grandes e de metadados que poderiam ter ownership mais claro. - -Hoje `initialize_vm()` clona `program`, `title`, `app_version` e `entrypoint` ao carregar o cart. Isso nao e o maior gargalo em steady-state, mas sinaliza que ownership de boot ainda esta frouxo. - -## Dor - -- reload, troca de cart e cenarios de tooling pagam custo desnecessario. -- ownership frouxo no boot tende a reaparecer em hot-reload, debugger e loader. -- em hardware pequeno, boot "barato" tambem importa. - -## Hotspots Atuais - -- [lifecycle.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-system/src/virtual_machine_runtime/lifecycle.rs#L125) - -## Alvo da Discussao - -Fechar um modelo de ownership de cart/programa que reduza copias sem comprometer simplicidade ou seguranca do runtime. - -## O Que Precisa Ser Definido - -1. Ownership do programa. - Decidir se o bytecode: - - e movido para a VM; - - e compartilhado por `Arc`; - - e mantido no cart com view emprestada. - -2. Ownership de metadata. - Delimitar o que precisa ser copiado para estado corrente do runtime e o que pode ser referenciado. - -3. Ciclo de vida. - Definir como ownership funciona em: - - boot inicial; - - troca de cart; - - debugger/reload; - - run-cart futuro. - -4. Meta de boot. - Fechar qual custo de inicializacao e aceitavel para o baseline. - -## Open Questions de Arquitetura - -1. O projeto quer boot otimizado para trocas frequentes ou apenas para cold start? -2. Existe risco real de aliasing/perigo de lifetime ao evitar copias aqui? -3. Vale aceitar copia de metadata pequena e atacar so `program`? - -## Dependencias - -- `../specs/13-cartridge.md` -- `../specs/14-boot-profiles.md` -- `009-system-run-cart.md` - -## Criterio de Saida Desta Agenda - -Pode virar PR quando houver decisao escrita sobre: - -- ownership canonico de `program`; -- politica de copia/referencia para metadata de cart; -- comportamento em reload/troca de cart; -- meta de custo para boot e reinicializacao. diff --git a/discussion/workflow/decisions/DEC-0020-runtime-edge-coverage-governance-by-domain.md b/discussion/workflow/decisions/DEC-0020-runtime-edge-coverage-governance-by-domain.md new file mode 100644 index 00000000..17f24559 --- /dev/null +++ b/discussion/workflow/decisions/DEC-0020-runtime-edge-coverage-governance-by-domain.md @@ -0,0 +1,209 @@ +--- +id: DEC-0020 +ticket: runtime-edge-test-plan +title: Decision - Runtime Edge Coverage Governance by Domain +status: in_progress +created: 2026-04-20 +accepted: 2026-04-20 +agenda: AGD-0001 +plans: [PLN-0037, PLN-0038, PLN-0039] +tags: [tests, coverage, runtime, firmware, host] +--- + +## Status + +In progress. + +Derived from `AGD-0001`. + +This decision records the normative direction for runtime edge coverage governance. +It has been accepted and now drives one or more execution plans. + +## Contexto + +PROMETEU no longer has the same problem that originally motivated `AGD-0001`. +The runtime edge is no longer broadly untested. The repository now already contains meaningful coverage for runtime lifecycle paths, `VirtualFS`, asset/preload behavior, status-first surfaces, and part of the desktop host integration. + +The remaining problem is governance, not raw test absence. + +Global coverage metrics already exist through `cargo llvm-cov`, and CI already enforces a workspace-wide minimum gate. That is useful, but insufficient on its own. A good aggregated number does not guarantee that firmware transitions, boot flow, host-dependent surfaces, or domain-specific status-first contracts are covered at the right level. + +The project therefore needs an explicit domain-based acceptance model for tests: + +- a small canonical domain set; +- mandatory scenarios per domain; +- a rule for when a change must add or adjust tests; +- a clear relationship between global coverage and domain-oriented evidence. + +## Decisao + +PROMETEU SHALL govern runtime-edge test sufficiency through a two-layer model: + +1. a mandatory global coverage gate enforced in CI; and +2. a domain-based acceptance gate driven by mandatory scenarios plus domain-scoped coverage evidence. + +The canonical domains SHALL be: + +1. `system/runtime` +2. `fs` +3. `asset/bank` +4. `firmware` +5. `host-dependent` + +`host-dependent` SHALL be treated as a transversal execution/testability slice, not as a product subsystem parallel to `fs`, `asset/bank`, or `firmware`. + +Domain gates SHALL be scenario-first. +Coverage percentages by domain MAY exist, but they SHALL start from an initial baseline of `0` and SHALL be tightened incrementally over time as segmentation and suites mature. + +No PR that changes the observable contract of a canonical domain SHALL be accepted without one of the following: + +- tests added or adjusted for that domain; or +- an explicit justification that the observable contract did not change. + +Improved aggregate coverage SHALL NOT be accepted as sufficient evidence on its own when the changed behavior belongs to a canonical domain with mandatory scenarios. + +## Rationale + +### Why not global-only coverage + +A single global percentage is easy to automate, but too weak as a risk control. +It allows firmware and host-sensitive regressions to hide behind unrelated line execution elsewhere in the workspace. + +### Why not rigid per-domain percentages immediately + +The project does not yet have a stable enough domain segmentation baseline to enforce hard per-domain minimums from day one without adding friction disproportionate to the benefit. + +Starting from `0` preserves forward motion while still making domain evidence mandatory during review. + +### Why scenario-first + +The platform risk lies in behavioral contracts: + +- lifecycle boundaries, +- boot transitions, +- status-first surfaces, +- filesystem normalization and invalid-state handling, +- preload/bootstrap versus runtime operational failures, +- host-only integration boundaries. + +Those are not safely governed by percentages alone. + +### Why keep coverage in the model + +Coverage remains useful as: + +- an operational CI signal; +- a regression detector; +- a way to inspect whether the changed domain actually exercised the intended area. + +The correct role of coverage is supporting evidence, not sole authority. + +## Invariantes / Contrato + +### Global Coverage Contract + +- CI MUST keep a workspace-wide coverage gate. +- The current `llvm-cov` based global gate remains valid as the operational baseline. +- Changing the tooling is out of scope for this decision. + +### Domain Gate Contract + +Each canonical domain MUST define mandatory scenarios and acceptance expectations. + +#### `system/runtime` + +Mandatory scenarios: + +- initialization; +- reset and cleanup; +- trap versus panic behavior; +- pause and breakpoint behavior; +- `FRAME_SYNC` and budget boundary behavior. + +#### `fs` + +Mandatory scenarios: + +- happy path; +- invalid path / traversal rejection; +- unhealthy backend behavior; +- cleanup of handles and state after reset/unmount when applicable. + +#### `asset/bank` + +Mandatory scenarios: + +- preload behavior; +- `load / status / commit / cancel`; +- missing asset handling; +- invalid slot handling; +- structural bootstrap failure versus operational runtime failure. + +#### `firmware` + +Mandatory scenarios: + +- cartridge load flow; +- `AppMode` branch behavior; +- VM/runtime initialization failure leading to crash path; +- basic boot-target and state-transition coordination. + +#### `host-dependent` + +Mandatory scenarios: + +- host-only behaviors that require real socket, window, or desktop integration MUST be isolated; +- deterministic parts of those rules SHOULD exist below the host layer whenever feasible. + +### Review Contract + +- Any PR touching a canonical domain MUST review the domain gate explicitly. +- Domain-scoped coverage inspection MUST be part of review evidence when the domain is materially touched. +- Domain percentage baselines MAY start at `0`, but they MUST be allowed to rise over time rather than remain permanently undefined. + +### Organization Contract + +- Test suites SHOULD move toward domain-oriented ownership. +- Monolithic suites MAY be split incrementally as touched by real work. +- This decision does NOT require a one-shot refactor of the entire current test tree. + +## Impactos + +### Spec + +- The testing policy now has a normative direction suitable for a future plan and eventual publication in the repository's process/spec surfaces if desired. + +### Runtime + +- Runtime-area changes gain an explicit expectation that domain tests move with behavior changes. + +### Firmware + +- Firmware transitions become a first-class governed testing domain rather than an implicit gap. + +### Host + +- Host-dependent tests become explicitly governed and justified, rather than ad hoc exceptions. + +### Tooling / CI + +- Existing `llvm-cov` and Jenkins integration remain valid. +- Future plans may add domain reporting, thresholds, or helper commands without changing this baseline decision. + +## Referencias + +- `AGD-0001` +- `Makefile` +- `files/config/Jenkinsfile` +- `docs/specs/runtime/12-firmware-pos-and-prometeuhub.md` +- `crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs` +- `crates/console/prometeu-system/src/services/fs/virtual_fs.rs` +- `crates/host/prometeu-host-desktop-winit/src/runner.rs` + +## Propagacao Necessaria + +- Create an execution plan that turns the domain model into concrete work items. +- Define how domain evidence will be gathered during review. +- Decide whether to add helper commands or scripts for domain-oriented coverage inspection. +- Prioritize firmware and host-dependent expansion first. +- Define an incremental split strategy for the monolithic runtime test suite. diff --git a/discussion/workflow/plans/PLN-0037-runtime-edge-coverage-governance-foundation.md b/discussion/workflow/plans/PLN-0037-runtime-edge-coverage-governance-foundation.md new file mode 100644 index 00000000..1c31e772 --- /dev/null +++ b/discussion/workflow/plans/PLN-0037-runtime-edge-coverage-governance-foundation.md @@ -0,0 +1,130 @@ +--- +id: PLN-0037 +ticket: runtime-edge-test-plan +title: Plan - Runtime Edge Coverage Governance Foundation +status: done +created: 2026-04-20 +completed: 2026-04-20 +tags: [tests, coverage, governance, ci] +--- + +## Briefing + +This plan operationalizes the governance layer of `DEC-0020`. +Its goal is to turn the decision into concrete review and CI mechanics without changing the underlying coverage toolchain. + +## Decisions de Origem + +- `DEC-0020` + +## Alvo + +Define the repository-level mechanics for: + +- global coverage gate continuity; +- domain-scoped coverage evidence during review; +- baseline tracking that can start at `0` per domain and tighten later; +- reviewer-visible rules for when domain tests are mandatory. + +## Escopo + +- Add repository commands or scripts for domain-oriented coverage evidence collection. +- Add repository-facing documentation that explains the global gate plus domain evidence workflow. +- Align CI/coverage outputs with the new governance model without replacing `llvm-cov`. + +## Fora de Escopo + +- Expanding firmware tests. +- Refactoring the monolithic runtime suite. +- Introducing hard domain coverage percentages above baseline `0`. +- Replacing Jenkins or `cargo llvm-cov`. + +## Plano de Execucao + +### Step 1 - Define the domain evidence interface + +**What:** +Define how contributors and reviewers gather coverage evidence for `system/runtime`, `fs`, `asset/bank`, `firmware`, and `host-dependent`. + +**How:** +Add one repository-visible command path that produces: + +- existing global HTML/XML/JSON coverage artifacts; +- a documented procedure for mapping changed files/modules to one of the canonical domains; +- an initial baseline format that records per-domain coverage starting from `0`. + +**File(s):** +- `Makefile` +- `scripts/` +- `README.md` or another repository-facing process document if more appropriate + +### Step 2 - Add domain evidence helpers + +**What:** +Introduce helper commands or scripts that make domain evidence collection reproducible. + +**How:** +Prefer simple wrappers over new tooling. For example: + +- capture `llvm-cov` JSON/HTML consistently; +- filter or summarize target files/modules for a domain; +- emit review-friendly evidence artifacts without changing the authoritative global gate. + +**File(s):** +- `Makefile` +- `scripts/` + +### Step 3 - Document the PR acceptance rule + +**What:** +Publish the decision’s review contract in a place visible during development. + +**How:** +Document that any PR touching a canonical domain must either: + +- update/add tests for that domain; or +- justify why the observable contract did not change. + +Document that domain coverage evidence is mandatory review input even when domain thresholds are still at baseline `0`. + +**File(s):** +- `README.md` +- optional repo process document if the repository has a better canonical location + +### Step 4 - Align CI outputs with the governance model + +**What:** +Make the existing coverage pipeline clearly compatible with domain review. + +**How:** +Keep the current global Jenkins gate unchanged while ensuring produced artifacts are sufficient for domain inspection. +If needed, archive additional summaries or helper outputs generated by the new scripts. + +**File(s):** +- `files/config/Jenkinsfile` +- `Makefile` + +## Criterios de Aceite + +- [ ] The repository exposes a documented way to collect domain-oriented coverage evidence without replacing the global `llvm-cov` gate. +- [ ] Review guidance explicitly states when domain tests are mandatory. +- [ ] Domain baselines are represented in a form that can start at `0` and increase later. +- [ ] Existing global coverage enforcement remains intact. + +## Tests / Validacao + +### Automated + +- Validate all added helper commands locally. +- Run the existing coverage pipeline end to end after the helper changes. + +### Evidence + +- Show that the global coverage artifacts still build. +- Show an example of domain evidence collection for at least one domain. + +## Riscos + +- Overdesigning domain evidence collection before real reviewer usage patterns exist. +- Accidentally creating a second competing coverage authority instead of a reviewer aid. +- Spreading policy text across too many documents and making the rule hard to find. diff --git a/discussion/workflow/plans/PLN-0038-firmware-and-host-dependent-domain-coverage-expansion.md b/discussion/workflow/plans/PLN-0038-firmware-and-host-dependent-domain-coverage-expansion.md new file mode 100644 index 00000000..01dce05b --- /dev/null +++ b/discussion/workflow/plans/PLN-0038-firmware-and-host-dependent-domain-coverage-expansion.md @@ -0,0 +1,135 @@ +--- +id: PLN-0038 +ticket: runtime-edge-test-plan +title: Plan - Firmware and Host-Dependent Domain Coverage Expansion +status: done +created: 2026-04-20 +completed: 2026-04-20 +tags: [tests, firmware, host, coverage] +--- + +## Briefing + +This plan executes the priority expansion areas mandated by `DEC-0020`: `firmware` and `host-dependent`. +The objective is not blanket test growth, but closing the most important domain gaps first. + +## Decisions de Origem + +- `DEC-0020` + +## Alvo + +Expand automated evidence for: + +- firmware load and state-transition rules; +- `AppMode` branching; +- crash-path transitions from VM/runtime initialization and runtime execution; +- host-dependent desktop/debugger behaviors that legitimately require socket or window integration. + +## Escopo + +- Add or refine firmware tests in `prometeu-firmware`. +- Add or refine isolated host-dependent tests in the desktop host crate. +- Clarify which host behaviors must remain ignored/integration-style and which deterministic pieces should move below the host. + +## Fora de Escopo + +- Full test-tree reorganization across all runtime domains. +- Hard domain thresholds above baseline `0`. +- Reworking the public debugger protocol itself unless required by testability. + +## Plano de Execucao + +### Step 1 - Close firmware state-transition gaps + +**What:** +Expand firmware coverage around canonical state behavior, not only isolated happy paths. + +**How:** +Add tests around: + +- `Reset` to boot-target-driven transitions; +- `LaunchHub` behavior for `BootTarget::Hub` versus `BootTarget::Cartridge`; +- `LoadCartridge` branch behavior for `AppMode::Game` versus `AppMode::System`; +- transition to `AppCrashes` when initialization or runtime execution fails. + +**File(s):** +- `crates/console/prometeu-firmware/src/firmware/firmware.rs` +- `crates/console/prometeu-firmware/src/firmware/firmware_step_reset.rs` +- `crates/console/prometeu-firmware/src/firmware/firmware_step_launch_hub.rs` +- `crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs` +- `crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs` +- `crates/console/prometeu-firmware/src/firmware/firmware_step_crash_screen.rs` + +### Step 2 - Delimit host-dependent versus deterministic behavior + +**What:** +Make the boundary between true host integration and lower-layer deterministic behavior explicit in tests. + +**How:** +Review existing desktop-host tests and classify them: + +- tests that truly require socket bind/connect or window integration remain host-dependent and may stay `#[ignore]`; +- deterministic logic should be covered in firmware/runtime/host-internal tests that do not require real integration. + +**File(s):** +- `crates/host/prometeu-host-desktop-winit/src/runner.rs` +- `crates/host/prometeu-host-desktop-winit/src/debugger.rs` + +### Step 3 - Strengthen the host-dependent suite without widening its scope + +**What:** +Keep real integration tests focused on behaviors the lower layers cannot prove. + +**How:** +Prioritize cases such as: + +- debugger socket open/handshake lifecycle; +- reconnect/refuse-second-connection behavior; +- resume/pause/disconnect coordination when the real host wiring is part of the rule. + +Avoid pushing general deterministic state logic into ignored desktop-only tests if it can be covered below the host. + +**File(s):** +- `crates/host/prometeu-host-desktop-winit/src/runner.rs` +- `crates/host/prometeu-host-desktop-winit/src/debugger.rs` + +### Step 4 - Capture evidence for the two priority domains + +**What:** +Produce explicit review evidence showing that `firmware` and `host-dependent` are now governed domains rather than implied gaps. + +**How:** +Use the governance helpers from `PLN-0037` if available, or temporary documented evidence if it lands first. + +**File(s):** +- firmware and host test targets +- coverage/report artifacts as defined by the governance plan + +## Criterios de Aceite + +- [ ] Firmware has explicit automated coverage for boot-target transitions, `AppMode` branching, and crash-path transitions. +- [ ] Host-dependent tests are clearly limited to behaviors that genuinely require desktop/socket integration. +- [ ] Deterministic behavior that does not need real host integration is covered below the host when feasible. +- [ ] Review evidence can point to firmware and host-dependent coverage separately from the global aggregate. + +## Tests / Validacao + +### Unit / Integration + +- Run `cargo test -p prometeu-firmware`. +- Run the relevant desktop host tests. + +### Host-Dependent + +- Run `cargo test -p prometeu-host-desktop-winit --lib -- --ignored`. + +### Evidence + +- Produce coverage evidence for touched firmware files and host-dependent integration paths. + +## Riscos + +- Adding host-only tests for behavior that belongs below the host boundary. +- Expanding firmware tests without aligning them to the state machine contract in the specs. +- Overfitting ignored integration tests to desktop timing details. 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 new file mode 100644 index 00000000..55121b87 --- /dev/null +++ b/discussion/workflow/plans/PLN-0039-incremental-runtime-domain-suite-split-and-baselines.md @@ -0,0 +1,132 @@ +--- +id: PLN-0039 +ticket: runtime-edge-test-plan +title: Plan - Incremental Runtime Domain Suite Split and Baselines +status: done +created: 2026-04-20 +completed: 2026-04-20 +tags: [tests, runtime, fs, asset, organization] +--- + +## Briefing + +This plan turns the current monolithic runtime-edge suite into clearer domain-owned test surfaces over time, while preserving the existing breadth of coverage already present in `prometeu-system`. + +## Decisions de Origem + +- `DEC-0020` + +## Alvo + +Establish clearer test ownership and baseline evidence for: + +- `system/runtime` +- `fs` +- `asset/bank` + +Do this incrementally, without a one-shot refactor of the whole runtime test tree. + +## Escopo + +- Split the current runtime-edge suite along canonical domain lines as work touches those areas. +- Preserve or improve current behavioral coverage while improving maintainability. +- Establish explicit baseline evidence for the non-priority domains, starting from `0` if needed. + +## Fora de Escopo + +- Rewriting all tests in one pass. +- Large behavioral refactors unrelated to test structure. +- Introducing strict non-zero domain thresholds immediately. + +## Plano de Execucao + +### Step 1 - Define the split targets for the existing monolithic suite + +**What:** +Map the current `virtual_machine_runtime/tests.rs` coverage into the canonical runtime domains. + +**How:** +Create a target decomposition for the current file, separating at least: + +- `system/runtime` lifecycle and tick behavior; +- `asset/bank` status-first and preload behavior; +- `fs` and memcard-adjacent runtime cases when they belong to runtime orchestration rather than `VirtualFS` internals. + +Do not move files blindly. First define the intended ownership map. + +**File(s):** +- `crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs` +- `crates/console/prometeu-system/src/virtual_machine_runtime/` + +### Step 2 - Split the runtime suite incrementally + +**What:** +Reduce the monolithic test concentration without destabilizing current coverage. + +**How:** +As each domain is touched, move or extract tests into smaller modules with domain ownership. +Prefer incremental Rust module splits such as domain-focused test modules over an all-at-once rewrite. + +**File(s):** +- `crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs` +- new sibling test modules under `crates/console/prometeu-system/src/virtual_machine_runtime/` + +### Step 3 - Preserve and expose `fs` and `asset/bank` baselines + +**What:** +Make existing stronger areas explicitly visible in the new governance model. + +**How:** +Use: + +- `crates/console/prometeu-system/src/services/fs/virtual_fs.rs` for filesystem contract evidence; +- runtime and loader tests for `asset/bank` scenario evidence. + +Capture baseline evidence even if the numeric threshold remains `0` initially. + +**File(s):** +- `crates/console/prometeu-system/src/services/fs/virtual_fs.rs` +- `crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs` +- `crates/console/prometeu-hal/src/cartridge_loader.rs` +- `crates/console/prometeu-drivers/src/asset.rs` + +### Step 4 - Align future review with the new runtime-domain ownership + +**What:** +Ensure later PRs can tell which runtime-domain tests should move with the code. + +**How:** +Document or encode enough structure that maintainers can tell where to add tests for: + +- lifecycle/tick behavior; +- filesystem normalization and runtime cleanup; +- asset/bootstrap/status-first flows. + +**File(s):** +- runtime test modules +- repository guidance introduced by `PLN-0037` + +## Criterios de Aceite + +- [ ] The current monolithic runtime suite has an explicit decomposition into canonical domains. +- [ ] New or moved runtime tests follow domain ownership rather than returning to a single catch-all file. +- [ ] `fs` and `asset/bank` have baseline evidence captured under the governance model. +- [ ] The split is incremental and does not require a one-shot rewrite to start delivering value. + +## Tests / Validacao + +### Automated + +- Run `cargo test -p prometeu-system`. +- Run any focused tests for `prometeu-hal` and `prometeu-drivers` touched during the split. + +### Evidence + +- Show that the domain-oriented split preserves existing runtime behaviors. +- Produce baseline coverage evidence for `system/runtime`, `fs`, and `asset/bank`. + +## Riscos + +- 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. diff --git a/files/config/runtime-edge-coverage-domains.json b/files/config/runtime-edge-coverage-domains.json new file mode 100644 index 00000000..2873039d --- /dev/null +++ b/files/config/runtime-edge-coverage-domains.json @@ -0,0 +1,42 @@ +{ + "version": 1, + "domains": { + "system/runtime": { + "baseline_percent": 66, + "paths": [ + "crates/console/prometeu-system/src/virtual_machine_runtime.rs", + "crates/console/prometeu-system/src/virtual_machine_runtime/" + ] + }, + "fs": { + "baseline_percent": 77, + "paths": [ + "crates/console/prometeu-system/src/services/fs/", + "crates/console/prometeu-system/src/services/memcard.rs" + ] + }, + "asset/bank": { + "baseline_percent": 80, + "paths": [ + "crates/console/prometeu-hal/src/asset", + "crates/console/prometeu-hal/src/asset.rs", + "crates/console/prometeu-hal/src/asset_bridge.rs", + "crates/console/prometeu-hal/src/cartridge.rs", + "crates/console/prometeu-hal/src/cartridge_loader.rs", + "crates/console/prometeu-drivers/src/asset.rs" + ] + }, + "firmware": { + "baseline_percent": 74, + "paths": [ + "crates/console/prometeu-firmware/src/" + ] + }, + "host-dependent": { + "baseline_percent": 43, + "paths": [ + "crates/host/prometeu-host-desktop-winit/src/" + ] + } + } +} diff --git a/scripts/coverage-domain-evidence.sh b/scripts/coverage-domain-evidence.sh new file mode 100755 index 00000000..69ce2355 --- /dev/null +++ b/scripts/coverage-domain-evidence.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(git rev-parse --show-toplevel)" +CONFIG_PATH="${ROOT}/files/config/runtime-edge-coverage-domains.json" +DEFAULT_REPORT="${ROOT}/target/llvm-cov/report.json" + +usage() { + cat <<'EOF' +Usage: + coverage-domain-evidence.sh --list + coverage-domain-evidence.sh [report.json] + +The report path defaults to target/llvm-cov/report.json and is expected to be +generated by `make ci` or `make coverage-report-json`. +EOF +} + +if [[ ! -f "${CONFIG_PATH}" ]]; then + echo "missing coverage domain config: ${CONFIG_PATH}" >&2 + exit 1 +fi + +if [[ $# -eq 0 ]]; then + usage + exit 1 +fi + +if [[ "$1" == "--list" ]]; then + jq -r '.domains | keys[]' "${CONFIG_PATH}" + exit 0 +fi + +DOMAIN="$1" +REPORT_PATH="${2:-${DEFAULT_REPORT}}" + +if ! jq -e --arg domain "${DOMAIN}" '.domains[$domain]' "${CONFIG_PATH}" >/dev/null; then + echo "unknown domain: ${DOMAIN}" >&2 + echo "available domains:" >&2 + jq -r '.domains | keys[]' "${CONFIG_PATH}" >&2 + exit 1 +fi + +BASELINE="$(jq -r --arg domain "${DOMAIN}" '.domains[$domain].baseline_percent' "${CONFIG_PATH}")" + +echo "Runtime edge coverage evidence" +echo "Domain: ${DOMAIN}" +echo "Baseline percent: ${BASELINE}" +echo + +echo "Mapped repository paths:" +jq -r --arg domain "${DOMAIN}" '.domains[$domain].paths[]' "${CONFIG_PATH}" | while IFS= read -r prefix; do + echo " - ${prefix}" +done +echo + +echo "Tracked files present in repository:" +while IFS= read -r path; do + ( + cd "${ROOT}" + rg --files . | sed 's#^\./##' + ) | rg "^${path//\//\\/}" || true +done < <(jq -r --arg domain "${DOMAIN}" '.domains[$domain].paths[]' "${CONFIG_PATH}") +echo + +if [[ ! -f "${REPORT_PATH}" ]]; then + echo "Coverage report not found: ${REPORT_PATH}" + echo "Generate it with: make coverage-report-json" + exit 0 +fi + +jq -r --arg domain "${DOMAIN}" --arg root "${ROOT}" ' + .domains[$domain].paths as $paths + | def matches_domain: + . as $file + | any($paths[]; . as $p | ($file.filename | startswith($root + "/" + $p) or startswith("./" + $p) or startswith($p))); + def pct($covered; $count): + if $count > 0 then (($covered * 10000 / $count | floor) / 100) else 0 end; + reduce ( + input.data[]?.files[]? + | select(matches_domain) + ) as $file ( + { + files: [], + lines: {count: 0, covered: 0}, + functions: {count: 0, covered: 0}, + regions: {count: 0, covered: 0} + }; + .files += [$file.filename] + | .lines.count += ($file.summary.lines.count // 0) + | .lines.covered += ($file.summary.lines.covered // 0) + | .functions.count += ($file.summary.functions.count // 0) + | .functions.covered += ($file.summary.functions.covered // 0) + | .regions.count += ($file.summary.regions.count // 0) + | .regions.covered += ($file.summary.regions.covered // 0) + ) + | "Coverage summary from " + $domain + ":", + " files_in_report: " + (.files | length | tostring), + " lines: " + (pct(.lines.covered; .lines.count) | tostring) + "% (" + (.lines.covered|tostring) + "/" + (.lines.count|tostring) + ")", + " functions: " + (pct(.functions.covered; .functions.count) | tostring) + "% (" + (.functions.covered|tostring) + "/" + (.functions.count|tostring) + ")", + " regions: " + (pct(.regions.covered; .regions.count) | tostring) + "% (" + (.regions.covered|tostring) + "/" + (.regions.count|tostring) + ")", + "", + "Matched report files:", + (.files[]? | " - " + .) +' "${CONFIG_PATH}" "${REPORT_PATH}"