From c2698e6a225a911f7c3ea4b919ca8304bf0b18a3 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 15 May 2026 11:47:26 +0100 Subject: [PATCH] implements PLN-0061 shell running lifecycle integration --- .../src/firmware/firmware.rs | 99 ++++++++++++++++++- .../firmware/firmware_step_shell_running.rs | 7 +- .../src/programs/prometeu_hub/prometeu_hub.rs | 6 +- discussion/index.ndjson | 2 +- ...lan-shell-running-lifecycle-integration.md | 14 +-- 5 files changed, 112 insertions(+), 16 deletions(-) diff --git a/crates/console/prometeu-firmware/src/firmware/firmware.rs b/crates/console/prometeu-firmware/src/firmware/firmware.rs index 3bb60111..b60811ed 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware.rs @@ -175,10 +175,13 @@ mod tests { use prometeu_bytecode::model::{BytecodeModule, FunctionMeta, SyscallDecl}; use prometeu_drivers::hardware::Hardware; use prometeu_hal::cartridge::{AppMode, AssetsPayloadSource}; + use prometeu_hal::color::Color; + use prometeu_hal::primitives::Rect; use prometeu_hal::syscalls::caps; use prometeu_system::CrashReport; use prometeu_system::process::ProcessState; - use prometeu_system::task::TaskState; + use prometeu_system::task::{TaskId, TaskState}; + use prometeu_system::windows::WindowOwner; fn halting_program() -> Vec { let code = assemble("HALT").expect("assemble"); @@ -262,6 +265,34 @@ mod tests { } } + fn load_shell_running_firmware() -> (Firmware, Hardware, InputSignals, TaskId) { + let mut firmware = Firmware::new(None); + let mut hardware = Hardware::new(); + let signals = InputSignals::default(); + + firmware.load_cartridge(valid_cartridge(AppMode::Shell)); + firmware.tick(&signals, &mut hardware); + + let task_id = match &firmware.state { + FirmwareState::ShellRunning(step) => step.task_id, + other => panic!("expected ShellRunning state, got {:?}", other), + }; + + (firmware, hardware, signals, task_id) + } + + fn focus_window(firmware: &mut Firmware, owner: WindowOwner) { + let mut windows = firmware.os.windows(); + windows.remove_all_windows(); + let id = windows.add_window( + "Test Window".to_string(), + owner, + Rect { x: 0, y: 0, w: 10, h: 10 }, + Color::WHITE, + ); + windows.set_focus(id); + } + #[test] fn load_cartridge_transitions_to_app_crashes_when_vm_init_fails() { let mut firmware = Firmware::new(None); @@ -362,14 +393,79 @@ mod tests { firmware.load_cartridge(valid_cartridge(AppMode::Shell)); firmware.tick(&signals, &mut hardware); + let task_id = match &firmware.state { + FirmwareState::ShellRunning(step) => step.task_id, + other => panic!("expected ShellRunning state, got {:?}", other), + }; let tick_index_before_system_update = firmware.os.vm().tick_index(); firmware.tick(&signals, &mut hardware); assert!(matches!(firmware.state, FirmwareState::ShellRunning(_))); + assert!(firmware.os.windows().focused_window_belongs_to_task(task_id)); assert_eq!(firmware.os.vm().tick_index(), tick_index_before_system_update + 1); } + #[test] + fn shell_running_closes_task_and_returns_hub_home_without_focused_window() { + let (mut firmware, mut hardware, signals, task_id) = load_shell_running_firmware(); + + firmware.os.windows().remove_all_windows(); + firmware.tick(&signals, &mut hardware); + + assert!(matches!(firmware.state, FirmwareState::HubHome(_))); + assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Closed)); + assert_eq!( + firmware.os.lifecycle().process_state_for_task(task_id), + Ok(ProcessState::Stopped) + ); + } + + #[test] + fn shell_running_closes_task_and_returns_hub_home_for_focused_hub_window() { + let (mut firmware, mut hardware, signals, task_id) = load_shell_running_firmware(); + + focus_window(&mut firmware, WindowOwner::Hub); + firmware.tick(&signals, &mut hardware); + + assert!(matches!(firmware.state, FirmwareState::HubHome(_))); + assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Closed)); + assert_eq!( + firmware.os.lifecycle().process_state_for_task(task_id), + Ok(ProcessState::Stopped) + ); + } + + #[test] + fn shell_running_closes_task_and_returns_hub_home_for_focused_overlay_window() { + let (mut firmware, mut hardware, signals, task_id) = load_shell_running_firmware(); + + focus_window(&mut firmware, WindowOwner::Overlay); + firmware.tick(&signals, &mut hardware); + + assert!(matches!(firmware.state, FirmwareState::HubHome(_))); + assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Closed)); + assert_eq!( + firmware.os.lifecycle().process_state_for_task(task_id), + Ok(ProcessState::Stopped) + ); + } + + #[test] + fn shell_running_closes_task_and_returns_hub_home_for_other_task_window() { + let (mut firmware, mut hardware, signals, task_id) = load_shell_running_firmware(); + + focus_window(&mut firmware, WindowOwner::Task(TaskId(task_id.0 + 1))); + firmware.tick(&signals, &mut hardware); + + assert!(matches!(firmware.state, FirmwareState::HubHome(_))); + assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Closed)); + assert_eq!( + firmware.os.lifecycle().process_state_for_task(task_id), + Ok(ProcessState::Stopped) + ); + } + #[test] fn load_cartridge_routes_game_apps_to_game_running() { let mut firmware = Firmware::new(None); @@ -380,5 +476,6 @@ mod tests { firmware.tick(&signals, &mut hardware); assert!(matches!(firmware.state, FirmwareState::GameRunning(_))); + assert_eq!(firmware.os.windows().window_count(), 0); } } diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_shell_running.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_shell_running.rs index 94339e96..9a150073 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_shell_running.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_shell_running.rs @@ -66,10 +66,13 @@ impl ShellRunningStep { return Some(FirmwareState::AppCrashes(AppCrashesStep { report })); } - if !outcome.focused_window_active { + if !ctx.os.windows().focused_window_belongs_to_task(self.task_id) { ctx.os.info( LogSource::Hub, - format!("Closing shell task {:?}: no focused window active", self.task_id), + format!( + "Closing shell task {:?}: focused window does not belong to task", + self.task_id, + ), ); if let Err(error) = ctx.os.lifecycle().close_task(self.task_id) { ctx.os.error( diff --git a/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs b/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs index 8c219df7..2828113f 100644 --- a/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs +++ b/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs @@ -11,7 +11,6 @@ pub struct PrometeuHub; pub struct SystemProfileUpdate { pub crash: Option, - pub focused_window_active: bool, } impl Default for PrometeuHub { @@ -103,9 +102,6 @@ impl PrometeuHub { self.render(os, hw); hw.gfx_mut().present(); - SystemProfileUpdate { - crash, - focused_window_active: os.windows().focused_window().is_some(), - } + SystemProfileUpdate { crash } } } diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 8a419f18..93123ad9 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,5 +1,5 @@ {"type":"meta","next_id":{"DSC":36,"AGD":36,"DEC":28,"PLN":62,"LSN":44,"CLSN":1}} -{"type":"discussion","id":"DSC-0035","status":"in_progress","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[{"id":"AGD-0035","file":"AGD-0035-agenda-task-owned-shell-windows.md","status":"accepted","created_at":"2026-05-15","updated_at":"2026-05-15"}],"decisions":[{"id":"DEC-0027","file":"DEC-0027-decision-task-owned-shell-windows.md","status":"accepted","created_at":"2026-05-15","updated_at":"2026-05-15","ref_agenda":"AGD-0035"}],"plans":[{"id":"PLN-0059","file":"PLN-0059-plan-task-window-contract-specs.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0027"]},{"id":"PLN-0060","file":"PLN-0060-plan-focused-window-ownership-api.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0027"]},{"id":"PLN-0061","file":"PLN-0061-plan-shell-running-lifecycle-integration.md","status":"open","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0027"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0035","status":"in_progress","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[{"id":"AGD-0035","file":"AGD-0035-agenda-task-owned-shell-windows.md","status":"accepted","created_at":"2026-05-15","updated_at":"2026-05-15"}],"decisions":[{"id":"DEC-0027","file":"DEC-0027-decision-task-owned-shell-windows.md","status":"accepted","created_at":"2026-05-15","updated_at":"2026-05-15","ref_agenda":"AGD-0035"}],"plans":[{"id":"PLN-0059","file":"PLN-0059-plan-task-window-contract-specs.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0027"]},{"id":"PLN-0060","file":"PLN-0060-plan-focused-window-ownership-api.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0027"]},{"id":"PLN-0061","file":"PLN-0061-plan-shell-running-lifecycle-integration.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0027"]}],"lessons":[]} {"type":"discussion","id":"DSC-0034","status":"done","ticket":"system-os-domain-facades","title":"Agenda - SystemOS Domain Facades","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","services","api-surface","lifecycle","fs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0043","file":"discussion/lessons/DSC-0034-system-os-domain-facades/LSN-0043-systemos-domain-facades.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} {"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":"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":"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":"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":"discussion/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":"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":"DEC-0003-jenkins-gitea-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0003","file":"PLN-0003-jenkins-gitea-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0021","file":"discussion/lessons/DSC-0020-jenkins-gitea-integration/LSN-0021-jenkins-gitea-integration.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]} diff --git a/discussion/workflow/plans/PLN-0061-plan-shell-running-lifecycle-integration.md b/discussion/workflow/plans/PLN-0061-plan-shell-running-lifecycle-integration.md index 91abc07a..413468cf 100644 --- a/discussion/workflow/plans/PLN-0061-plan-shell-running-lifecycle-integration.md +++ b/discussion/workflow/plans/PLN-0061-plan-shell-running-lifecycle-integration.md @@ -2,9 +2,9 @@ id: PLN-0061 ticket: task-owned-shell-windows title: Plan - Shell Running Lifecycle Integration -status: open +status: done created: 2026-05-15 -completed: +completed: 2026-05-15 ref_decisions: [DEC-0027] tags: [runtime, os, task, window-manager, shell, lifecycle, firmware, tests] --- @@ -100,15 +100,15 @@ Included: ## Criterios de Aceite -- [ ] `ShellRunningStep` no longer uses `focused_window_active` as its +- [x] `ShellRunningStep` no longer uses `focused_window_active` as its continuation contract. -- [ ] `ShellRunningStep` uses +- [x] `ShellRunningStep` uses `os.windows().focused_window_belongs_to_task(self.task_id)`. -- [ ] A false predicate closes the task through lifecycle before returning +- [x] A false predicate closes the task through lifecycle before returning `HubHome`. -- [ ] `Hub`, `Overlay`, absent focus, and another task's focused window all +- [x] `Hub`, `Overlay`, absent focus, and another task's focused window all fail the predicate for the current shell task. -- [ ] Game-running behavior is unchanged and remains outside `WindowManager`. +- [x] Game-running behavior is unchanged and remains outside `WindowManager`. ## Tests / Validacao