ast10x0: timer test scenario 1 - ordered expiry, tie-break, one-shot
diff --git a/target/ast10x0/tests/timer/main.rs b/target/ast10x0/tests/timer/main.rs
index 0c48ac5..6cbb3bb 100644
--- a/target/ast10x0/tests/timer/main.rs
+++ b/target/ast10x0/tests/timer/main.rs
@@ -1,17 +1,105 @@
 // Licensed under the Apache-2.0 license
 // SPDX-License-Identifier: Apache-2.0
 
-//! Timer QEMU test: TimerManager <-> object_wait seam. Scaffold stub.
+//! Timer QEMU test: proves the TimerManager <-> object_wait seam with the
+//! production types (userspace Instant, orchestrator-sm ComponentId).
+//!
+//! The wait loop is the intended orchestrator runtime shape: block in
+//! object_wait on {interrupt object, next timer deadline}; DeadlineExceeded
+//! drains TimerManager::poll, Ok is the event path.
 
 #![no_main]
 #![no_std]
 
+use app_test_timer::{handle, signals};
+use openprot_orchestrator_sm::ComponentId;
+use openprot_orchestrator_timer::{Expired, TimerManager};
+use pw_status::{Error, Result};
+use userspace::time::{Clock, Duration, Instant, SystemClock};
 use userspace::{entry, syscall};
 
+const C0: ComponentId = ComponentId::new(0);
+const C1: ComponentId = ComponentId::new(1);
+
+/// Chain capacity 4 — matches the crate's own unit-test sizing.
+type Tm = TimerManager<Instant, ComponentId, 4>;
+
+/// Scenario 1: arm Boot(C0)@+50ms, Boot(C1)@+100ms, Commit@+100ms. Expect
+/// expiries in exactly that order (Boot(C1) before Commit exercises the
+/// boot-before-commit tie-break at the shared deadline), each at or after its
+/// armed offset (lower bound only — no upper bound under QEMU), one-shot.
+fn scenario_ordered_expiry() -> Result<()> {
+    pw_log::info!("scenario 1: ordered expiry + tie-break + one-shot");
+    let mut tm = Tm::new();
+    let t0 = SystemClock::now();
+    tm.arm_boot(C0, t0 + Duration::from_millis(50))
+        .map_err(|_| Error::ResourceExhausted)?;
+    tm.arm_boot(C1, t0 + Duration::from_millis(100))
+        .map_err(|_| Error::ResourceExhausted)?;
+    tm.arm_commit(t0 + Duration::from_millis(100));
+
+    let expected: [(Expired<ComponentId>, Duration); 3] = [
+        (Expired::Boot(C0), Duration::from_millis(50)),
+        (Expired::Boot(C1), Duration::from_millis(100)),
+        (Expired::Commit, Duration::from_millis(100)),
+    ];
+
+    let mut idx = 0;
+    while idx < expected.len() {
+        let deadline = tm.next_deadline().ok_or(Error::Internal)?;
+        match syscall::object_wait(handle::TIMER_IRQ, signals::TEST_IRQ, deadline) {
+            Err(Error::DeadlineExceeded) => {
+                let now = SystemClock::now();
+                while let Some(fired) = tm.poll(now) {
+                    let (want, offset) = expected[idx];
+                    if fired != want {
+                        pw_log::error!("scenario 1: wrong expiry at index {}", idx as u32);
+                        return Err(Error::Internal);
+                    }
+                    if now < t0 + offset {
+                        pw_log::error!("scenario 1: expiry {} fired early", idx as u32);
+                        return Err(Error::Internal);
+                    }
+                    idx += 1;
+                }
+            }
+            Ok(_) => {
+                pw_log::error!("scenario 1: unexpected event wakeup");
+                return Err(Error::Internal);
+            }
+            Err(e) => return Err(e),
+        }
+    }
+
+    if tm.poll(SystemClock::now()).is_some() {
+        pw_log::error!("scenario 1: watchdog fired twice (not one-shot)");
+        return Err(Error::Internal);
+    }
+    if tm.next_deadline().is_some() {
+        pw_log::error!("scenario 1: deadline outstanding after full drain");
+        return Err(Error::Internal);
+    }
+    pw_log::info!("scenario 1: PASS");
+    Ok(())
+}
+
+fn run_test() -> Result<()> {
+    scenario_ordered_expiry()?;
+    Ok(())
+}
+
 #[entry]
 fn entry() {
-    pw_log::info!("timer test: scaffold up");
-    let _ = syscall::debug_shutdown(Ok(()));
+    match run_test() {
+        Ok(()) => {
+            pw_log::info!("timer test: all scenarios PASSED");
+            let _ = syscall::debug_shutdown(Ok(()));
+        }
+        Err(e) => {
+            pw_log::error!("timer test FAILED: {}", e as u32);
+            let _ = syscall::debug_shutdown(Err(e));
+        }
+    }
     loop {}
 }