1use std::collections::{BTreeMap, VecDeque};
21use std::fmt;
22use std::sync::{Arc, Mutex};
23use std::time::Duration;
24
25use async_trait::async_trait;
26use harn_clock::{Clock, PausedClock, RealClock};
27use time::OffsetDateTime;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34pub enum HarnessKind {
35 Root,
36 Stdio,
37 Term,
38 Clock,
39 Fs,
40 Env,
41 Random,
42 Net,
43 Process,
44 Crypto,
45 System,
46 Llm,
47 Tenant,
51 Auth,
56 Obs,
62}
63
64impl HarnessKind {
65 pub const fn type_name(self) -> &'static str {
69 match self {
70 HarnessKind::Root => "Harness",
71 HarnessKind::Stdio => "HarnessStdio",
72 HarnessKind::Term => "HarnessTerm",
73 HarnessKind::Clock => "HarnessClock",
74 HarnessKind::Fs => "HarnessFs",
75 HarnessKind::Env => "HarnessEnv",
76 HarnessKind::Random => "HarnessRandom",
77 HarnessKind::Net => "HarnessNet",
78 HarnessKind::Process => "HarnessProcess",
79 HarnessKind::Crypto => "HarnessCrypto",
80 HarnessKind::System => "HarnessSystem",
81 HarnessKind::Llm => "HarnessLlm",
82 HarnessKind::Tenant => "HarnessTenant",
83 HarnessKind::Auth => "HarnessAuth",
84 HarnessKind::Obs => "HarnessObs",
85 }
86 }
87
88 pub const fn field_name(self) -> Option<&'static str> {
91 match self {
92 HarnessKind::Root => None,
93 HarnessKind::Stdio => Some("stdio"),
94 HarnessKind::Term => Some("term"),
95 HarnessKind::Clock => Some("clock"),
96 HarnessKind::Fs => Some("fs"),
97 HarnessKind::Env => Some("env"),
98 HarnessKind::Random => Some("random"),
99 HarnessKind::Net => Some("net"),
100 HarnessKind::Process => Some("process"),
101 HarnessKind::Crypto => Some("crypto"),
102 HarnessKind::System => Some("system"),
103 HarnessKind::Llm => Some("llm"),
104 HarnessKind::Tenant => Some("tenant"),
105 HarnessKind::Auth => Some("auth"),
106 HarnessKind::Obs => Some("obs"),
107 }
108 }
109
110 pub fn from_field_name(name: &str) -> Option<Self> {
112 match name {
113 "stdio" => Some(HarnessKind::Stdio),
114 "term" => Some(HarnessKind::Term),
115 "clock" => Some(HarnessKind::Clock),
116 "fs" => Some(HarnessKind::Fs),
117 "env" => Some(HarnessKind::Env),
118 "random" => Some(HarnessKind::Random),
119 "net" => Some(HarnessKind::Net),
120 "process" => Some(HarnessKind::Process),
121 "crypto" => Some(HarnessKind::Crypto),
122 "system" => Some(HarnessKind::System),
123 "llm" => Some(HarnessKind::Llm),
124 "tenant" => Some(HarnessKind::Tenant),
125 "auth" => Some(HarnessKind::Auth),
126 "obs" => Some(HarnessKind::Obs),
127 _ => None,
128 }
129 }
130
131 pub const SUB_HANDLES: &'static [HarnessKind] = &[
133 HarnessKind::Stdio,
134 HarnessKind::Term,
135 HarnessKind::Clock,
136 HarnessKind::Fs,
137 HarnessKind::Env,
138 HarnessKind::Random,
139 HarnessKind::Net,
140 HarnessKind::Process,
141 HarnessKind::Crypto,
142 HarnessKind::System,
143 HarnessKind::Llm,
144 HarnessKind::Tenant,
145 HarnessKind::Auth,
146 HarnessKind::Obs,
147 ];
148
149 pub const ALL: &'static [HarnessKind] = &[
151 HarnessKind::Root,
152 HarnessKind::Stdio,
153 HarnessKind::Term,
154 HarnessKind::Clock,
155 HarnessKind::Fs,
156 HarnessKind::Env,
157 HarnessKind::Random,
158 HarnessKind::Net,
159 HarnessKind::Process,
160 HarnessKind::Crypto,
161 HarnessKind::System,
162 HarnessKind::Llm,
163 HarnessKind::Tenant,
164 HarnessKind::Auth,
165 HarnessKind::Obs,
166 ];
167}
168
169#[derive(Debug)]
175pub struct HarnessInner {
176 clock: Arc<dyn Clock>,
177 mode: HarnessMode,
178 net_policy: Option<crate::harness_net::NetPolicy>,
183 quarantined: Mutex<bool>,
190}
191
192impl HarnessInner {
193 pub fn clock(&self) -> &Arc<dyn Clock> {
194 &self.clock
195 }
196
197 pub(crate) fn mode(&self) -> &HarnessMode {
198 &self.mode
199 }
200
201 pub fn net_policy(&self) -> Option<&crate::harness_net::NetPolicy> {
202 self.net_policy.as_ref()
203 }
204
205 pub(crate) fn mark_quarantined(&self) {
206 if let Ok(mut guard) = self.quarantined.lock() {
207 *guard = true;
208 }
209 }
210
211 pub fn is_quarantined(&self) -> bool {
212 self.quarantined.lock().map(|guard| *guard).unwrap_or(false)
213 }
214}
215
216#[derive(Debug)]
217pub(crate) enum HarnessMode {
218 Real,
219 Null(NullHarnessState),
220 Mock(Arc<MockHarnessState>),
221}
222
223#[derive(Debug, Default)]
224pub(crate) struct NullHarnessState {
225 deny_events: Mutex<Vec<DenyEvent>>,
226}
227
228impl NullHarnessState {
229 pub(crate) fn record_deny(
230 &self,
231 sub_handle: HarnessKind,
232 method: &str,
233 args: &[crate::VmValue],
234 ) {
235 self.deny_events
236 .lock()
237 .expect("deny events poisoned")
238 .push(DenyEvent::new(
239 sub_handle,
240 method,
241 args.iter().map(crate::VmValue::display).collect(),
242 ));
243 }
244
245 pub(crate) fn deny_events(&self) -> Vec<DenyEvent> {
246 self.deny_events
247 .lock()
248 .expect("deny events poisoned")
249 .clone()
250 }
251}
252
253#[derive(Debug, Clone, PartialEq, Eq)]
254pub struct DenyEvent {
255 pub sub_handle: HarnessKind,
256 pub method: String,
257 pub args: Vec<String>,
258}
259
260impl DenyEvent {
261 fn new(sub_handle: HarnessKind, method: &str, args: Vec<String>) -> Self {
262 Self {
263 sub_handle,
264 method: method.to_string(),
265 args,
266 }
267 }
268}
269
270#[derive(Debug)]
271pub(crate) struct MockHarnessState {
272 calls: Mutex<Vec<HarnessCall>>,
273 clock: Arc<PausedClock>,
274 env: BTreeMap<String, String>,
275 fs_reads: BTreeMap<String, Vec<u8>>,
276 net_gets: BTreeMap<String, String>,
277 random_u64: Mutex<VecDeque<u64>>,
278 stdin_lines: Mutex<VecDeque<String>>,
279 stdio: Mutex<String>,
280 stderr: Mutex<String>,
281}
282
283impl MockHarnessState {
284 pub(crate) fn record_call(
285 &self,
286 sub_handle: HarnessKind,
287 method: &str,
288 args: &[crate::VmValue],
289 ) {
290 self.calls
291 .lock()
292 .expect("calls poisoned")
293 .push(HarnessCall::new(
294 sub_handle,
295 method,
296 args.iter().map(crate::VmValue::display).collect(),
297 ));
298 }
299
300 pub(crate) fn calls(&self) -> Vec<HarnessCall> {
301 self.calls.lock().expect("calls poisoned").clone()
302 }
303
304 pub(crate) fn env_get(&self, key: &str) -> Option<&str> {
305 self.env.get(key).map(String::as_str)
306 }
307
308 pub(crate) fn fs_read(&self, path: &str) -> Option<&[u8]> {
309 self.fs_reads.get(path).map(Vec::as_slice)
310 }
311
312 pub(crate) fn net_get(&self, url: &str) -> Option<&str> {
313 self.net_gets.get(url).map(String::as_str)
314 }
315
316 pub(crate) fn next_random_u64(&self) -> Option<u64> {
317 let mut values = self.random_u64.lock().expect("random values poisoned");
318 values.pop_front()
319 }
320
321 pub(crate) fn advance_clock(&self, duration: std::time::Duration) {
322 self.clock.advance(duration);
323 }
324
325 pub(crate) fn push_stdio(&self, text: &str) {
326 self.stdio
327 .lock()
328 .expect("stdio buffer poisoned")
329 .push_str(text);
330 }
331
332 pub(crate) fn stdio(&self) -> String {
333 self.stdio.lock().expect("stdio buffer poisoned").clone()
334 }
335
336 pub(crate) fn push_stderr(&self, text: &str) {
337 self.stderr
338 .lock()
339 .expect("stderr buffer poisoned")
340 .push_str(text);
341 }
342
343 pub(crate) fn stderr(&self) -> String {
344 self.stderr.lock().expect("stderr buffer poisoned").clone()
345 }
346
347 pub(crate) fn pop_stdin_line(&self) -> Option<String> {
348 self.stdin_lines
349 .lock()
350 .expect("stdin queue poisoned")
351 .pop_front()
352 }
353}
354
355#[derive(Debug, Clone, PartialEq, Eq)]
356pub struct HarnessCall {
357 pub sub_handle: HarnessKind,
358 pub method: String,
359 pub args: Vec<String>,
360}
361
362impl HarnessCall {
363 fn new(sub_handle: HarnessKind, method: &str, args: Vec<String>) -> Self {
364 Self {
365 sub_handle,
366 method: method.to_string(),
367 args,
368 }
369 }
370}
371
372#[derive(Debug)]
373pub struct MockHarnessBuilder {
374 clock: Arc<PausedClock>,
375 env: BTreeMap<String, String>,
376 fs_reads: BTreeMap<String, Vec<u8>>,
377 net_gets: BTreeMap<String, String>,
378 random_u64: Vec<u64>,
379 stdin_lines: Vec<String>,
380}
381
382impl MockHarnessBuilder {
383 fn new() -> Self {
384 Self {
385 clock: paused_clock_at_unix_ms(0),
386 env: BTreeMap::new(),
387 fs_reads: BTreeMap::new(),
388 net_gets: BTreeMap::new(),
389 random_u64: Vec::new(),
390 stdin_lines: Vec::new(),
391 }
392 }
393
394 pub fn clock_at_unix_ms(mut self, unix_ms: i64) -> Self {
395 self.clock = paused_clock_at_unix_ms(unix_ms);
396 self
397 }
398
399 pub fn clock_at(mut self, origin: OffsetDateTime) -> Self {
400 self.clock = PausedClock::new(origin);
401 self
402 }
403
404 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
405 self.env.insert(key.into(), value.into());
406 self
407 }
408
409 pub fn fs_read(mut self, path: impl Into<String>, data: impl Into<Vec<u8>>) -> Self {
410 self.fs_reads.insert(path.into(), data.into());
411 self
412 }
413
414 pub fn net_get(mut self, url: impl Into<String>, body: impl Into<String>) -> Self {
415 self.net_gets.insert(url.into(), body.into());
416 self
417 }
418
419 pub fn random_u64(mut self, value: u64) -> Self {
420 self.random_u64.push(value);
421 self
422 }
423
424 pub fn stdin_line(mut self, line: impl Into<String>) -> Self {
430 self.stdin_lines.push(line.into());
431 self
432 }
433
434 pub fn build(self) -> Harness {
435 let clock = self.clock;
436 Harness::with_mode(
437 clock.clone() as Arc<dyn Clock>,
438 HarnessMode::Mock(Arc::new(MockHarnessState {
439 calls: Mutex::new(Vec::new()),
440 clock,
441 env: self.env,
442 fs_reads: self.fs_reads,
443 net_gets: self.net_gets,
444 random_u64: Mutex::new(self.random_u64.into()),
445 stdin_lines: Mutex::new(self.stdin_lines.into()),
446 stdio: Mutex::new(String::new()),
447 stderr: Mutex::new(String::new()),
448 })),
449 )
450 }
451}
452
453#[derive(Debug, Clone)]
457pub struct Harness {
458 inner: Arc<HarnessInner>,
459}
460
461impl Harness {
462 pub fn real() -> Self {
473 Self::with_mode(
474 Arc::new(MockAwareClock::new(RealClock::new())),
475 HarnessMode::Real,
476 )
477 }
478
479 pub fn null() -> Self {
482 Self::with_mode(
483 paused_clock_at_unix_ms(0) as Arc<dyn Clock>,
484 HarnessMode::Null(NullHarnessState::default()),
485 )
486 }
487
488 pub fn mock() -> MockHarnessBuilder {
490 MockHarnessBuilder::new()
491 }
492
493 pub fn with_clock(clock: Arc<dyn Clock>) -> Self {
498 Self::with_mode(clock, HarnessMode::Real)
499 }
500
501 pub fn from_inner(inner: Arc<HarnessInner>) -> Self {
506 Self { inner }
507 }
508
509 fn with_mode(clock: Arc<dyn Clock>, mode: HarnessMode) -> Self {
510 let inner = Arc::new(HarnessInner {
511 clock,
512 mode,
513 net_policy: None,
514 quarantined: Mutex::new(false),
515 });
516 Self { inner }
517 }
518
519 pub fn with_net_policy(&self, policy: crate::harness_net::NetPolicy) -> Self {
533 let clock = Arc::clone(&self.inner.clock);
534 let mode = match &self.inner.mode {
535 HarnessMode::Real => HarnessMode::Real,
536 HarnessMode::Null(_) => HarnessMode::Null(NullHarnessState::default()),
537 HarnessMode::Mock(state) => HarnessMode::Mock(Arc::clone(state)),
538 };
539 #[allow(clippy::arc_with_non_send_sync)]
541 let inner = Arc::new(HarnessInner {
542 clock,
543 mode,
544 net_policy: Some(policy),
545 quarantined: Mutex::new(self.is_quarantined()),
546 });
547 Self { inner }
548 }
549
550 pub fn is_quarantined(&self) -> bool {
553 self.inner.is_quarantined()
554 }
555
556 pub fn deny_events(&self) -> Vec<DenyEvent> {
557 match self.inner.mode() {
558 HarnessMode::Null(state) => state.deny_events(),
559 HarnessMode::Real | HarnessMode::Mock(_) => Vec::new(),
560 }
561 }
562
563 pub fn calls(&self) -> Vec<HarnessCall> {
564 match self.inner.mode() {
565 HarnessMode::Mock(state) => state.calls(),
566 HarnessMode::Real | HarnessMode::Null(_) => Vec::new(),
567 }
568 }
569
570 pub fn captured_stdio(&self) -> String {
571 match self.inner.mode() {
572 HarnessMode::Mock(state) => state.stdio(),
573 HarnessMode::Real | HarnessMode::Null(_) => String::new(),
574 }
575 }
576
577 pub fn captured_stderr(&self) -> String {
578 match self.inner.mode() {
579 HarnessMode::Mock(state) => state.stderr(),
580 HarnessMode::Real | HarnessMode::Null(_) => String::new(),
581 }
582 }
583
584 pub fn test() -> (Self, Arc<PausedClock>) {
596 Self::with_paused_clock(OffsetDateTime::UNIX_EPOCH)
597 }
598
599 pub fn with_paused_clock(origin: OffsetDateTime) -> (Self, Arc<PausedClock>) {
603 let paused = PausedClock::new(origin);
604 let as_dyn: Arc<dyn Clock> = paused.clone();
605 (Self::with_clock(as_dyn), paused)
606 }
607
608 pub fn stdio(&self) -> HarnessStdio {
610 HarnessStdio {
611 inner: Arc::clone(&self.inner),
612 }
613 }
614
615 pub fn term(&self) -> HarnessTerm {
617 HarnessTerm {
618 inner: Arc::clone(&self.inner),
619 }
620 }
621
622 pub fn clock(&self) -> HarnessClock {
624 HarnessClock {
625 inner: Arc::clone(&self.inner),
626 }
627 }
628
629 pub fn fs(&self) -> HarnessFs {
631 HarnessFs {
632 inner: Arc::clone(&self.inner),
633 }
634 }
635
636 pub fn env(&self) -> HarnessEnv {
638 HarnessEnv {
639 inner: Arc::clone(&self.inner),
640 }
641 }
642
643 pub fn random(&self) -> HarnessRandom {
645 HarnessRandom {
646 inner: Arc::clone(&self.inner),
647 }
648 }
649
650 pub fn net(&self) -> HarnessNet {
652 HarnessNet {
653 inner: Arc::clone(&self.inner),
654 }
655 }
656
657 pub fn process(&self) -> HarnessProcess {
659 HarnessProcess {
660 inner: Arc::clone(&self.inner),
661 }
662 }
663
664 pub fn crypto(&self) -> HarnessCrypto {
666 HarnessCrypto {
667 inner: Arc::clone(&self.inner),
668 }
669 }
670
671 pub fn system(&self) -> HarnessSystem {
673 HarnessSystem {
674 inner: Arc::clone(&self.inner),
675 }
676 }
677
678 pub fn llm(&self) -> HarnessLlm {
680 HarnessLlm {
681 inner: Arc::clone(&self.inner),
682 }
683 }
684
685 pub fn tenant(&self) -> HarnessTenant {
687 HarnessTenant {
688 inner: Arc::clone(&self.inner),
689 }
690 }
691
692 pub fn auth(&self) -> HarnessAuth {
694 HarnessAuth {
695 inner: Arc::clone(&self.inner),
696 }
697 }
698
699 pub fn obs(&self) -> HarnessObs {
701 HarnessObs {
702 inner: Arc::clone(&self.inner),
703 }
704 }
705
706 pub fn into_vm_value(self) -> crate::value::VmValue {
708 crate::value::VmValue::harness(VmHarness {
709 inner: self.inner,
710 kind: HarnessKind::Root,
711 })
712 }
713}
714
715fn paused_clock_at_unix_ms(unix_ms: i64) -> Arc<PausedClock> {
716 let nanos = (unix_ms as i128).saturating_mul(1_000_000);
717 let origin =
718 OffsetDateTime::from_unix_timestamp_nanos(nanos).unwrap_or(OffsetDateTime::UNIX_EPOCH);
719 PausedClock::new(origin)
720}
721
722pub(crate) fn vm_string(value: impl Into<String>) -> crate::VmValue {
723 crate::VmValue::String(std::sync::Arc::from(value.into()))
724}
725
726impl Default for Harness {
727 fn default() -> Self {
728 Self::real()
729 }
730}
731
732#[derive(Debug, Clone)]
735pub struct HarnessStdio {
736 inner: Arc<HarnessInner>,
737}
738
739#[derive(Debug, Clone)]
741pub struct HarnessTerm {
742 inner: Arc<HarnessInner>,
743}
744
745#[derive(Debug, Clone)]
747pub struct HarnessClock {
748 inner: Arc<HarnessInner>,
749}
750
751impl HarnessClock {
752 pub fn clock(&self) -> &Arc<dyn Clock> {
753 self.inner.clock()
754 }
755}
756
757#[derive(Debug, Clone)]
760pub struct HarnessFs {
761 inner: Arc<HarnessInner>,
762}
763
764#[derive(Debug, Clone)]
766pub struct HarnessEnv {
767 inner: Arc<HarnessInner>,
768}
769
770#[derive(Debug, Clone)]
772pub struct HarnessRandom {
773 inner: Arc<HarnessInner>,
774}
775
776#[derive(Debug, Clone)]
778pub struct HarnessNet {
779 inner: Arc<HarnessInner>,
780}
781
782#[derive(Debug, Clone)]
784pub struct HarnessProcess {
785 inner: Arc<HarnessInner>,
786}
787
788#[derive(Debug, Clone)]
790pub struct HarnessCrypto {
791 inner: Arc<HarnessInner>,
792}
793
794#[derive(Debug, Clone)]
800pub struct HarnessSystem {
801 inner: Arc<HarnessInner>,
802}
803
804#[derive(Debug, Clone)]
806pub struct HarnessLlm {
807 inner: Arc<HarnessInner>,
808}
809
810#[derive(Debug, Clone)]
816pub struct HarnessTenant {
817 inner: Arc<HarnessInner>,
818}
819
820#[derive(Debug, Clone)]
828pub struct HarnessAuth {
829 inner: Arc<HarnessInner>,
830}
831
832#[derive(Debug, Clone)]
842pub struct HarnessObs {
843 inner: Arc<HarnessInner>,
844}
845
846macro_rules! sub_handle_inner {
847 ($($ty:ty),* $(,)?) => {
848 $(
849 impl $ty {
850 #[allow(dead_code)]
851 pub(crate) fn inner(&self) -> &Arc<HarnessInner> {
852 &self.inner
853 }
854 }
855 )*
856 };
857}
858sub_handle_inner!(
859 HarnessStdio,
860 HarnessTerm,
861 HarnessFs,
862 HarnessEnv,
863 HarnessRandom,
864 HarnessNet,
865 HarnessProcess,
866 HarnessCrypto,
867 HarnessSystem,
868 HarnessLlm,
869 HarnessTenant,
870 HarnessAuth,
871 HarnessObs,
872);
873
874impl HarnessClock {
875 #[allow(dead_code)]
876 pub(crate) fn inner(&self) -> &Arc<HarnessInner> {
877 &self.inner
878 }
879}
880
881#[derive(Clone)]
886pub struct VmHarness {
887 inner: Arc<HarnessInner>,
888 kind: HarnessKind,
889}
890
891impl VmHarness {
892 pub fn kind(&self) -> HarnessKind {
893 self.kind
894 }
895
896 pub fn type_name(&self) -> &'static str {
897 self.kind.type_name()
898 }
899
900 pub fn inner(&self) -> &Arc<HarnessInner> {
901 &self.inner
902 }
903
904 pub fn sub_handle(&self, field: &str) -> Option<VmHarness> {
908 if self.kind != HarnessKind::Root {
909 return None;
910 }
911 let kind = HarnessKind::from_field_name(field)?;
912 self.sub_handle_kind(kind)
913 }
914
915 pub(crate) fn sub_handle_kind(&self, kind: HarnessKind) -> Option<VmHarness> {
916 if self.kind != HarnessKind::Root || kind == HarnessKind::Root {
917 return None;
918 }
919 Some(VmHarness {
920 inner: Arc::clone(&self.inner),
921 kind,
922 })
923 }
924}
925
926impl fmt::Debug for VmHarness {
927 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
928 f.debug_struct("VmHarness")
929 .field("kind", &self.kind)
930 .finish_non_exhaustive()
931 }
932}
933
934#[derive(Debug)]
940pub struct MockAwareClock<C: Clock + 'static> {
941 inner: C,
942}
943
944impl<C: Clock + 'static> MockAwareClock<C> {
945 pub fn new(inner: C) -> Self {
946 Self { inner }
947 }
948}
949
950#[async_trait]
951impl<C: Clock + 'static> Clock for MockAwareClock<C> {
952 fn now_utc(&self) -> OffsetDateTime {
953 if let Some(mock) = crate::clock_mock::active_mock_clock() {
954 return mock.now_utc();
955 }
956 self.inner.now_utc()
957 }
958
959 fn monotonic_ms(&self) -> i64 {
960 if let Some(mock) = crate::clock_mock::active_mock_clock() {
961 return mock.monotonic_ms();
962 }
963 self.inner.monotonic_ms()
964 }
965
966 async fn sleep(&self, duration: Duration) {
967 if duration.is_zero() {
968 return;
969 }
970 if let Some(mock) = crate::clock_mock::active_mock_clock() {
971 mock.advance_std_sync(duration);
977 return;
978 }
979 self.inner.sleep(duration).await;
980 }
981
982 async fn sleep_until_utc(&self, deadline: OffsetDateTime) {
983 if let Some(mock) = crate::clock_mock::active_mock_clock() {
984 let now = mock.now_utc();
985 if deadline > now {
986 if let Ok(delta) = Duration::try_from(deadline - now) {
987 mock.advance_std_sync(delta);
988 }
989 }
990 return;
991 }
992 self.inner.sleep_until_utc(deadline).await;
993 }
994}
995
996#[cfg(test)]
997mod tests {
998 use super::*;
999
1000 #[test]
1001 fn real_constructs_without_panic() {
1002 let _harness = Harness::real();
1003 }
1004
1005 #[test]
1006 fn sub_handles_share_inner_state() {
1007 let harness = Harness::real();
1008 let stdio_inner = Arc::as_ptr(harness.stdio().inner());
1009 let clock_inner = Arc::as_ptr(harness.clock().inner());
1010 assert_eq!(stdio_inner, clock_inner, "sub-handles share Arc<Inner>");
1011 }
1012
1013 #[test]
1014 fn kinds_round_trip_through_field_names() {
1015 for kind in HarnessKind::SUB_HANDLES {
1016 let field = kind.field_name().unwrap();
1017 assert_eq!(HarnessKind::from_field_name(field), Some(*kind));
1018 }
1019 assert!(HarnessKind::from_field_name("nope").is_none());
1020 assert!(HarnessKind::Root.field_name().is_none());
1021 }
1022
1023 #[test]
1024 fn vm_harness_property_access_returns_sub_handle() {
1025 let root = match Harness::real().into_vm_value() {
1026 crate::value::VmValue::Harness(h) => h,
1027 other => panic!("expected Harness variant, got {}", other.type_name()),
1028 };
1029 let stdio = root.sub_handle("stdio").expect("stdio sub-handle");
1030 assert_eq!(stdio.kind(), HarnessKind::Stdio);
1031 assert!(stdio.sub_handle("clock").is_none(), "nested access denied");
1032 assert!(root.sub_handle("not_a_field").is_none());
1033 }
1034
1035 #[test]
1036 fn test_constructor_clock_advances_under_paused_clock_advance() {
1037 let (harness, paused) = Harness::test();
1038 let clock = harness.clock();
1039 let start_wall = clock.clock().now_utc();
1040 assert_eq!(start_wall, OffsetDateTime::UNIX_EPOCH);
1041 assert_eq!(clock.clock().monotonic_ms(), 0);
1042
1043 paused.advance(Duration::from_millis(1_500));
1044 assert_eq!(clock.clock().monotonic_ms(), 1_500);
1045 let after_wall = clock.clock().now_utc();
1046 assert_eq!(after_wall - start_wall, time::Duration::milliseconds(1_500));
1047 }
1048
1049 #[test]
1050 fn with_paused_clock_pins_origin() {
1051 let origin = OffsetDateTime::from_unix_timestamp(1_700_000_000).unwrap();
1052 let (harness, paused) = Harness::with_paused_clock(origin);
1053 assert_eq!(harness.clock().clock().now_utc(), origin);
1054 paused.advance(Duration::from_mins(1));
1055 assert_eq!(
1056 harness.clock().clock().now_utc() - origin,
1057 time::Duration::seconds(60)
1058 );
1059 }
1060
1061 #[test]
1062 fn null_harness_records_deny_events_for_every_sub_handle() {
1063 let harness = Harness::null();
1064 for source in [
1065 r#"fn main(harness: Harness) { harness.stdio.println("blocked") }"#,
1066 r"fn main(harness: Harness) { harness.term.width() }",
1067 r"fn main(harness: Harness) { harness.clock.now_ms() }",
1068 r#"fn main(harness: Harness) { harness.fs.read_text("/x") }"#,
1069 r#"fn main(harness: Harness) { harness.env.get("KEY") }"#,
1070 r"fn main(harness: Harness) { harness.random.gen_u64() }",
1071 r#"fn main(harness: Harness) { harness.net.get("https://example.test") }"#,
1072 r#"fn main(harness: Harness) { harness.process.spawn_captured({cmd: "printf", args: ["x"]}) }"#,
1073 r#"fn main(harness: Harness) { harness.crypto.sha256("") }"#,
1074 r"fn main(harness: Harness) { harness.system.cpu() }",
1075 r"fn main(harness: Harness) { harness.llm.catalog() }",
1076 r"fn main(harness: Harness) { harness.tenant.id() }",
1077 r"fn main(harness: Harness) { harness.auth.subject() }",
1078 r#"fn main(harness: Harness) { harness.obs.log("blocked", "info", {}) }"#,
1079 ] {
1080 let error = run_harness_source(source, harness.clone()).expect_err("call denied");
1081 assert!(
1082 error.contains("NullHarness denied"),
1083 "unexpected deny error: {error}"
1084 );
1085 }
1086
1087 let events = harness.deny_events();
1088 let observed: Vec<_> = events
1089 .iter()
1090 .map(|event| (event.sub_handle, event.method.as_str()))
1091 .collect();
1092 assert_eq!(
1093 observed,
1094 vec![
1095 (HarnessKind::Stdio, "println"),
1096 (HarnessKind::Term, "width"),
1097 (HarnessKind::Clock, "now_ms"),
1098 (HarnessKind::Fs, "read_text"),
1099 (HarnessKind::Env, "get"),
1100 (HarnessKind::Random, "gen_u64"),
1101 (HarnessKind::Net, "get"),
1102 (HarnessKind::Process, "spawn_captured"),
1103 (HarnessKind::Crypto, "sha256"),
1104 (HarnessKind::System, "cpu"),
1105 (HarnessKind::Llm, "catalog"),
1106 (HarnessKind::Tenant, "id"),
1107 (HarnessKind::Auth, "subject"),
1108 (HarnessKind::Obs, "log"),
1109 ]
1110 );
1111 assert_eq!(events[0].args, vec!["blocked"]);
1112 assert_eq!(events[3].args, vec!["/x"]);
1113 }
1114
1115 #[test]
1116 fn auth_sub_handle_reads_bound_principal() {
1117 use crate::harness_auth::{enter_auth_principal, AuthPrincipal};
1118 let _principal = enter_auth_principal(AuthPrincipal {
1119 subject: "k_123".to_string(),
1120 scheme: "apikey".to_string(),
1121 scopes: ["admin:dlq:write", "read:events"]
1122 .iter()
1123 .map(|scope| scope.to_string())
1124 .collect(),
1125 kind: Some("operator".to_string()),
1126 });
1127 let source = r#"
1128fn main(harness: Harness) {
1129 __io_println(harness.auth.is_authenticated())
1130 __io_println(harness.auth.subject())
1131 __io_println(harness.auth.scheme())
1132 __io_println(harness.auth.kind())
1133 __io_println(harness.auth.has_scope("admin:dlq:write"))
1134 __io_println(harness.auth.has_scope("missing:scope"))
1135 __io_println(len(harness.auth.scopes()))
1136}
1137"#;
1138 let output = run_harness_source(source, Harness::real()).expect("dispatch succeeds");
1139 assert_eq!(output, "true\nk_123\napikey\noperator\ntrue\nfalse\n2\n");
1140 }
1141
1142 #[test]
1143 fn auth_sub_handle_without_principal_reports_anonymous() {
1144 let source = r#"
1148fn main(harness: Harness) {
1149 if harness.auth.is_authenticated() { __io_println("auth") } else { __io_println("anon") }
1150 __io_println(harness.auth.has_scope("x"))
1151 __io_println(len(harness.auth.scopes()))
1152}
1153"#;
1154 let output = run_harness_source(source, Harness::real()).expect("dispatch succeeds");
1155 assert_eq!(output, "anon\nfalse\n0\n");
1156
1157 let error = run_harness_source(
1158 r"fn main(harness: Harness) { harness.auth.subject() }",
1159 Harness::real(),
1160 )
1161 .expect_err("subject() requires a bound principal");
1162 assert!(
1163 error.contains("no principal bound"),
1164 "unexpected error: {error}"
1165 );
1166 }
1167
1168 #[test]
1169 fn mock_harness_replays_canned_responses_and_records_calls() {
1170 let harness = Harness::mock()
1171 .clock_at_unix_ms(1_700_000_000_000)
1172 .env("KEY", "value")
1173 .fs_read("/x", b"data".to_vec())
1174 .random_u64(42)
1175 .net_get("https://example.test", "body")
1176 .build();
1177
1178 let output = run_harness_source(
1179 r#"
1180fn main(harness: Harness) {
1181 harness.stdio.print("partial ")
1182 harness.stdio.println("line")
1183 __io_println(harness.term.width())
1184 __io_println(harness.term.height())
1185 __io_println(harness.clock.now_ms())
1186 harness.clock.sleep_ms(250)
1187 __io_println(harness.clock.now_ms())
1188 __io_println(harness.clock.monotonic_ms())
1189 __io_println(harness.env.get("KEY"))
1190 __io_println(harness.fs.read_text("/x"))
1191 __io_println(harness.fs.exists("/missing"))
1192 __io_println(harness.random.gen_u64())
1193 __io_println(harness.net.get("https://example.test"))
1194 __io_println(harness.crypto.sha256(""))
1195 __io_println(len(harness.llm.catalog()) > 0)
1196}
1197"#,
1198 harness.clone(),
1199 )
1200 .expect("mock harness run succeeds");
1201
1202 assert_eq!(harness.captured_stdio(), "partial line\n");
1203 assert_eq!(
1204 output,
1205 "80\n24\n1700000000000\n1700000000250\n250\nvalue\ndata\nfalse\n42\nbody\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\ntrue\n"
1206 );
1207 let observed: Vec<_> = harness
1208 .calls()
1209 .into_iter()
1210 .map(|call| (call.sub_handle, call.method))
1211 .collect();
1212 assert_eq!(
1213 observed,
1214 vec![
1215 (HarnessKind::Stdio, "print".to_string()),
1216 (HarnessKind::Stdio, "println".to_string()),
1217 (HarnessKind::Term, "width".to_string()),
1218 (HarnessKind::Term, "height".to_string()),
1219 (HarnessKind::Clock, "now_ms".to_string()),
1220 (HarnessKind::Clock, "sleep_ms".to_string()),
1221 (HarnessKind::Clock, "now_ms".to_string()),
1222 (HarnessKind::Clock, "monotonic_ms".to_string()),
1223 (HarnessKind::Env, "get".to_string()),
1224 (HarnessKind::Fs, "read_text".to_string()),
1225 (HarnessKind::Fs, "exists".to_string()),
1226 (HarnessKind::Random, "gen_u64".to_string()),
1227 (HarnessKind::Net, "get".to_string()),
1228 (HarnessKind::Crypto, "sha256".to_string()),
1229 (HarnessKind::Llm, "catalog".to_string()),
1230 ]
1231 );
1232 }
1233
1234 #[test]
1235 fn mock_harness_records_repeated_cached_harness_method_calls() {
1236 let harness = Harness::mock().env("KEY", "value").build();
1237
1238 run_harness_source(
1239 r#"
1240fn main(harness: Harness) {
1241 var i = 0
1242 while i < 3 {
1243 let _ = harness.clock.elapsed()
1244 let value = harness.env.get_or("KEY", "")
1245 harness.stdio.println(value)
1246 i = i + 1
1247 }
1248}
1249"#,
1250 harness.clone(),
1251 )
1252 .expect("mock harness run succeeds");
1253
1254 assert_eq!(harness.captured_stdio(), "value\nvalue\nvalue\n");
1255 let observed: Vec<_> = harness
1256 .calls()
1257 .into_iter()
1258 .map(|call| (call.sub_handle, call.method))
1259 .collect();
1260 assert_eq!(
1261 observed,
1262 vec![
1263 (HarnessKind::Clock, "elapsed".to_string()),
1264 (HarnessKind::Env, "get_or".to_string()),
1265 (HarnessKind::Stdio, "println".to_string()),
1266 (HarnessKind::Clock, "elapsed".to_string()),
1267 (HarnessKind::Env, "get_or".to_string()),
1268 (HarnessKind::Stdio, "println".to_string()),
1269 (HarnessKind::Clock, "elapsed".to_string()),
1270 (HarnessKind::Env, "get_or".to_string()),
1271 (HarnessKind::Stdio, "println".to_string()),
1272 ]
1273 );
1274 }
1275
1276 #[test]
1277 fn mock_harness_replays_random_values_fifo() {
1278 let harness = Harness::mock()
1279 .random_u64(7)
1280 .random_u64(11)
1281 .random_u64(u64::MAX)
1282 .build();
1283
1284 let output = run_harness_source(
1285 r"
1286fn main(harness: Harness) {
1287 __io_println(harness.random.gen_u64())
1288 __io_println(harness.random.gen_u64())
1289 __io_println(harness.random.gen_u64())
1290}
1291",
1292 harness,
1293 )
1294 .expect("mock random succeeds");
1295
1296 assert_eq!(output, "7\n11\n9223372036854775807\n");
1297 }
1298
1299 #[test]
1300 fn mock_harness_reports_missing_canned_responses() {
1301 let cases = [
1302 (
1303 r#"fn main(harness: Harness) { harness.fs.read_text("/missing") }"#,
1304 "MockHarness has no fs_read response for /missing",
1305 ),
1306 (
1307 r"fn main(harness: Harness) { harness.random.gen_u64() }",
1308 "MockHarness has no random_u64 response",
1309 ),
1310 (
1311 r#"fn main(harness: Harness) { harness.net.get("https://missing.test") }"#,
1312 "MockHarness has no net_get response for https://missing.test",
1313 ),
1314 (
1315 r#"fn main(harness: Harness) { harness.process.spawn_captured({cmd: "printf", args: ["x"]}) }"#,
1316 "MockHarness has no process spawn response",
1317 ),
1318 ];
1319
1320 for (source, expected) in cases {
1321 let error = run_harness_source(source, Harness::mock().build())
1322 .expect_err("missing mock response fails");
1323 assert!(
1324 error.contains(expected),
1325 "expected `{expected}` in `{error}`"
1326 );
1327 }
1328 }
1329
1330 #[test]
1331 fn mock_harness_records_failed_calls() {
1332 let harness = Harness::mock().build();
1333 let error = run_harness_source(
1334 r#"fn main(harness: Harness) { harness.net.get("https://missing.test") }"#,
1335 harness.clone(),
1336 )
1337 .expect_err("missing mock response fails");
1338
1339 assert!(error.contains("MockHarness has no net_get response"));
1340 assert_eq!(
1341 harness.calls(),
1342 vec![HarnessCall {
1343 sub_handle: HarnessKind::Net,
1344 method: "get".to_string(),
1345 args: vec!["https://missing.test".to_string()],
1346 }]
1347 );
1348 }
1349
1350 #[test]
1351 fn mock_harness_captures_stderr_separately_from_stdout() {
1352 let harness = Harness::mock().build();
1353 run_harness_source(
1354 r#"
1355fn main(harness: Harness) {
1356 harness.stdio.println("stdout line")
1357 harness.stdio.eprint("err ")
1358 harness.stdio.eprintln("trail")
1359}
1360"#,
1361 harness.clone(),
1362 )
1363 .expect("stderr capture run succeeds");
1364 assert_eq!(harness.captured_stdio(), "stdout line\n");
1365 assert_eq!(harness.captured_stderr(), "err trail\n");
1366 }
1367
1368 #[test]
1369 fn mock_harness_replays_stdin_lines_for_read_and_prompt() {
1370 let harness = Harness::mock()
1371 .stdin_line("first")
1372 .stdin_line("second")
1373 .build();
1374 let output = run_harness_source(
1375 r#"
1376fn main(harness: Harness) {
1377 harness.stdio.println(harness.stdio.read_line())
1378 harness.stdio.println(harness.stdio.prompt("answer: "))
1379 let eof = harness.stdio.read_line({trim: false})
1380 harness.stdio.println(eof.status)
1381}
1382"#,
1383 harness.clone(),
1384 )
1385 .expect("stdin replay succeeds");
1386 assert_eq!(output, "");
1388 assert_eq!(harness.captured_stdio(), "first\nanswer: second\neof\n");
1389 }
1390
1391 #[test]
1392 fn mock_harness_replays_password_input_without_stdout_echo() {
1393 let harness = Harness::mock().stdin_line("secret").build();
1394 let output = run_harness_source(
1395 r#"
1396fn main(harness: Harness) {
1397 __io_println(harness.term.read_password("password: "))
1398}
1399"#,
1400 harness.clone(),
1401 )
1402 .expect("stdin replay succeeds");
1403
1404 assert_eq!(output, "secret\n");
1405 assert_eq!(harness.captured_stdio(), "");
1406 assert_eq!(harness.captured_stderr(), "password: ");
1407 assert_eq!(
1408 harness.calls(),
1409 vec![HarnessCall {
1410 sub_handle: HarnessKind::Term,
1411 method: "read_password".to_string(),
1412 args: vec!["password: ".to_string()],
1413 }]
1414 );
1415 }
1416
1417 #[test]
1418 fn mock_harness_rejects_wrong_argument_types() {
1419 let error = run_harness_source(
1420 r"fn main(harness: Harness) { harness.fs.read_text(1) }",
1421 Harness::mock().build(),
1422 )
1423 .expect_err("wrong argument type fails");
1424
1425 let runtime_rejection =
1435 error.contains("HarnessFs.read_text expects string argument 1, got int");
1436 let static_rejection = error.contains("argument 1 `path`: expected string, found int");
1437 assert!(
1438 runtime_rejection || static_rejection,
1439 "expected a string/int type rejection for read_text, got: {error}"
1440 );
1441 }
1442
1443 #[test]
1444 fn real_harness_fs_write_outside_workspace_roots_surfaces_cap_201() {
1445 use crate::orchestration::{
1446 clear_execution_policy_stacks, push_execution_policy, CapabilityPolicy, SandboxProfile,
1447 };
1448 clear_execution_policy_stacks();
1449 let temp = tempfile::tempdir().unwrap();
1450 let policy = CapabilityPolicy {
1451 sandbox_profile: SandboxProfile::Worktree,
1452 workspace_roots: vec![temp.path().to_string_lossy().into_owned()],
1453 ..CapabilityPolicy::default()
1454 };
1455 push_execution_policy(policy);
1456 let outside = std::env::temp_dir().join("harn_e4_4_cap_201_outside.txt");
1457 let source = format!(
1458 r#"fn main(harness: Harness) {{ harness.fs.write_text("{}", "x") }}"#,
1459 outside.to_string_lossy().replace('\\', "/"),
1460 );
1461 let error = run_harness_source(&source, Harness::real())
1462 .expect_err("write outside workspace_roots must reject");
1463 clear_execution_policy_stacks();
1464 assert!(
1465 error.contains("HARN-CAP-201"),
1466 "expected HARN-CAP-201 prefix, got: {error}"
1467 );
1468 assert!(
1469 error.contains("sandbox violation"),
1470 "deny should keep the underlying sandbox-rejection message, got: {error}"
1471 );
1472 }
1473
1474 fn run_harness_source(source: &str, harness: Harness) -> Result<String, String> {
1475 let rt = tokio::runtime::Builder::new_current_thread()
1476 .enable_all()
1477 .build()
1478 .unwrap();
1479 rt.block_on(async move {
1480 let local = tokio::task::LocalSet::new();
1481 local
1482 .run_until(async move {
1483 let chunk = crate::compile_source(source)?;
1484 let mut vm = crate::Vm::new();
1485 crate::stdlib::register_vm_stdlib(&mut vm);
1486 vm.set_harness(harness);
1487 vm.execute(&chunk)
1488 .await
1489 .map_err(|error| error.to_string())?;
1490 Ok(vm.output().to_string())
1491 })
1492 .await
1493 })
1494 }
1495}