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 Obs,
57}
58
59impl HarnessKind {
60 pub const fn type_name(self) -> &'static str {
64 match self {
65 HarnessKind::Root => "Harness",
66 HarnessKind::Stdio => "HarnessStdio",
67 HarnessKind::Term => "HarnessTerm",
68 HarnessKind::Clock => "HarnessClock",
69 HarnessKind::Fs => "HarnessFs",
70 HarnessKind::Env => "HarnessEnv",
71 HarnessKind::Random => "HarnessRandom",
72 HarnessKind::Net => "HarnessNet",
73 HarnessKind::Process => "HarnessProcess",
74 HarnessKind::Crypto => "HarnessCrypto",
75 HarnessKind::System => "HarnessSystem",
76 HarnessKind::Llm => "HarnessLlm",
77 HarnessKind::Tenant => "HarnessTenant",
78 HarnessKind::Obs => "HarnessObs",
79 }
80 }
81
82 pub const fn field_name(self) -> Option<&'static str> {
85 match self {
86 HarnessKind::Root => None,
87 HarnessKind::Stdio => Some("stdio"),
88 HarnessKind::Term => Some("term"),
89 HarnessKind::Clock => Some("clock"),
90 HarnessKind::Fs => Some("fs"),
91 HarnessKind::Env => Some("env"),
92 HarnessKind::Random => Some("random"),
93 HarnessKind::Net => Some("net"),
94 HarnessKind::Process => Some("process"),
95 HarnessKind::Crypto => Some("crypto"),
96 HarnessKind::System => Some("system"),
97 HarnessKind::Llm => Some("llm"),
98 HarnessKind::Tenant => Some("tenant"),
99 HarnessKind::Obs => Some("obs"),
100 }
101 }
102
103 pub fn from_field_name(name: &str) -> Option<Self> {
105 match name {
106 "stdio" => Some(HarnessKind::Stdio),
107 "term" => Some(HarnessKind::Term),
108 "clock" => Some(HarnessKind::Clock),
109 "fs" => Some(HarnessKind::Fs),
110 "env" => Some(HarnessKind::Env),
111 "random" => Some(HarnessKind::Random),
112 "net" => Some(HarnessKind::Net),
113 "process" => Some(HarnessKind::Process),
114 "crypto" => Some(HarnessKind::Crypto),
115 "system" => Some(HarnessKind::System),
116 "llm" => Some(HarnessKind::Llm),
117 "tenant" => Some(HarnessKind::Tenant),
118 "obs" => Some(HarnessKind::Obs),
119 _ => None,
120 }
121 }
122
123 pub const SUB_HANDLES: &'static [HarnessKind] = &[
125 HarnessKind::Stdio,
126 HarnessKind::Term,
127 HarnessKind::Clock,
128 HarnessKind::Fs,
129 HarnessKind::Env,
130 HarnessKind::Random,
131 HarnessKind::Net,
132 HarnessKind::Process,
133 HarnessKind::Crypto,
134 HarnessKind::System,
135 HarnessKind::Llm,
136 HarnessKind::Tenant,
137 HarnessKind::Obs,
138 ];
139
140 pub const ALL: &'static [HarnessKind] = &[
142 HarnessKind::Root,
143 HarnessKind::Stdio,
144 HarnessKind::Term,
145 HarnessKind::Clock,
146 HarnessKind::Fs,
147 HarnessKind::Env,
148 HarnessKind::Random,
149 HarnessKind::Net,
150 HarnessKind::Process,
151 HarnessKind::Crypto,
152 HarnessKind::System,
153 HarnessKind::Llm,
154 HarnessKind::Tenant,
155 HarnessKind::Obs,
156 ];
157}
158
159#[derive(Debug)]
165pub struct HarnessInner {
166 clock: Arc<dyn Clock>,
167 mode: HarnessMode,
168 net_policy: Option<crate::harness_net::NetPolicy>,
173 quarantined: Mutex<bool>,
180}
181
182impl HarnessInner {
183 pub fn clock(&self) -> &Arc<dyn Clock> {
184 &self.clock
185 }
186
187 pub(crate) fn mode(&self) -> &HarnessMode {
188 &self.mode
189 }
190
191 pub fn net_policy(&self) -> Option<&crate::harness_net::NetPolicy> {
192 self.net_policy.as_ref()
193 }
194
195 pub(crate) fn mark_quarantined(&self) {
196 if let Ok(mut guard) = self.quarantined.lock() {
197 *guard = true;
198 }
199 }
200
201 pub fn is_quarantined(&self) -> bool {
202 self.quarantined.lock().map(|guard| *guard).unwrap_or(false)
203 }
204}
205
206#[derive(Debug)]
207pub(crate) enum HarnessMode {
208 Real,
209 Null(NullHarnessState),
210 Mock(Arc<MockHarnessState>),
211}
212
213#[derive(Debug, Default)]
214pub(crate) struct NullHarnessState {
215 deny_events: Mutex<Vec<DenyEvent>>,
216}
217
218impl NullHarnessState {
219 pub(crate) fn record_deny(
220 &self,
221 sub_handle: HarnessKind,
222 method: &str,
223 args: &[crate::VmValue],
224 ) {
225 self.deny_events
226 .lock()
227 .expect("deny events poisoned")
228 .push(DenyEvent::new(
229 sub_handle,
230 method,
231 args.iter().map(crate::VmValue::display).collect(),
232 ));
233 }
234
235 pub(crate) fn deny_events(&self) -> Vec<DenyEvent> {
236 self.deny_events
237 .lock()
238 .expect("deny events poisoned")
239 .clone()
240 }
241}
242
243#[derive(Debug, Clone, PartialEq, Eq)]
244pub struct DenyEvent {
245 pub sub_handle: HarnessKind,
246 pub method: String,
247 pub args: Vec<String>,
248}
249
250impl DenyEvent {
251 fn new(sub_handle: HarnessKind, method: &str, args: Vec<String>) -> Self {
252 Self {
253 sub_handle,
254 method: method.to_string(),
255 args,
256 }
257 }
258}
259
260#[derive(Debug)]
261pub(crate) struct MockHarnessState {
262 calls: Mutex<Vec<HarnessCall>>,
263 clock: Arc<PausedClock>,
264 env: BTreeMap<String, String>,
265 fs_reads: BTreeMap<String, Vec<u8>>,
266 net_gets: BTreeMap<String, String>,
267 random_u64: Mutex<VecDeque<u64>>,
268 stdin_lines: Mutex<VecDeque<String>>,
269 stdio: Mutex<String>,
270 stderr: Mutex<String>,
271}
272
273impl MockHarnessState {
274 pub(crate) fn record_call(
275 &self,
276 sub_handle: HarnessKind,
277 method: &str,
278 args: &[crate::VmValue],
279 ) {
280 self.calls
281 .lock()
282 .expect("calls poisoned")
283 .push(HarnessCall::new(
284 sub_handle,
285 method,
286 args.iter().map(crate::VmValue::display).collect(),
287 ));
288 }
289
290 pub(crate) fn calls(&self) -> Vec<HarnessCall> {
291 self.calls.lock().expect("calls poisoned").clone()
292 }
293
294 pub(crate) fn env_get(&self, key: &str) -> Option<&str> {
295 self.env.get(key).map(String::as_str)
296 }
297
298 pub(crate) fn fs_read(&self, path: &str) -> Option<&[u8]> {
299 self.fs_reads.get(path).map(Vec::as_slice)
300 }
301
302 pub(crate) fn net_get(&self, url: &str) -> Option<&str> {
303 self.net_gets.get(url).map(String::as_str)
304 }
305
306 pub(crate) fn next_random_u64(&self) -> Option<u64> {
307 let mut values = self.random_u64.lock().expect("random values poisoned");
308 values.pop_front()
309 }
310
311 pub(crate) fn advance_clock(&self, duration: std::time::Duration) {
312 self.clock.advance(duration);
313 }
314
315 pub(crate) fn push_stdio(&self, text: &str) {
316 self.stdio
317 .lock()
318 .expect("stdio buffer poisoned")
319 .push_str(text);
320 }
321
322 pub(crate) fn stdio(&self) -> String {
323 self.stdio.lock().expect("stdio buffer poisoned").clone()
324 }
325
326 pub(crate) fn push_stderr(&self, text: &str) {
327 self.stderr
328 .lock()
329 .expect("stderr buffer poisoned")
330 .push_str(text);
331 }
332
333 pub(crate) fn stderr(&self) -> String {
334 self.stderr.lock().expect("stderr buffer poisoned").clone()
335 }
336
337 pub(crate) fn pop_stdin_line(&self) -> Option<String> {
338 self.stdin_lines
339 .lock()
340 .expect("stdin queue poisoned")
341 .pop_front()
342 }
343}
344
345#[derive(Debug, Clone, PartialEq, Eq)]
346pub struct HarnessCall {
347 pub sub_handle: HarnessKind,
348 pub method: String,
349 pub args: Vec<String>,
350}
351
352impl HarnessCall {
353 fn new(sub_handle: HarnessKind, method: &str, args: Vec<String>) -> Self {
354 Self {
355 sub_handle,
356 method: method.to_string(),
357 args,
358 }
359 }
360}
361
362#[derive(Debug)]
363pub struct MockHarnessBuilder {
364 clock: Arc<PausedClock>,
365 env: BTreeMap<String, String>,
366 fs_reads: BTreeMap<String, Vec<u8>>,
367 net_gets: BTreeMap<String, String>,
368 random_u64: Vec<u64>,
369 stdin_lines: Vec<String>,
370}
371
372impl MockHarnessBuilder {
373 fn new() -> Self {
374 Self {
375 clock: paused_clock_at_unix_ms(0),
376 env: BTreeMap::new(),
377 fs_reads: BTreeMap::new(),
378 net_gets: BTreeMap::new(),
379 random_u64: Vec::new(),
380 stdin_lines: Vec::new(),
381 }
382 }
383
384 pub fn clock_at_unix_ms(mut self, unix_ms: i64) -> Self {
385 self.clock = paused_clock_at_unix_ms(unix_ms);
386 self
387 }
388
389 pub fn clock_at(mut self, origin: OffsetDateTime) -> Self {
390 self.clock = PausedClock::new(origin);
391 self
392 }
393
394 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
395 self.env.insert(key.into(), value.into());
396 self
397 }
398
399 pub fn fs_read(mut self, path: impl Into<String>, data: impl Into<Vec<u8>>) -> Self {
400 self.fs_reads.insert(path.into(), data.into());
401 self
402 }
403
404 pub fn net_get(mut self, url: impl Into<String>, body: impl Into<String>) -> Self {
405 self.net_gets.insert(url.into(), body.into());
406 self
407 }
408
409 pub fn random_u64(mut self, value: u64) -> Self {
410 self.random_u64.push(value);
411 self
412 }
413
414 pub fn stdin_line(mut self, line: impl Into<String>) -> Self {
420 self.stdin_lines.push(line.into());
421 self
422 }
423
424 pub fn build(self) -> Harness {
425 let clock = self.clock;
426 Harness::with_mode(
427 clock.clone() as Arc<dyn Clock>,
428 HarnessMode::Mock(Arc::new(MockHarnessState {
429 calls: Mutex::new(Vec::new()),
430 clock,
431 env: self.env,
432 fs_reads: self.fs_reads,
433 net_gets: self.net_gets,
434 random_u64: Mutex::new(self.random_u64.into()),
435 stdin_lines: Mutex::new(self.stdin_lines.into()),
436 stdio: Mutex::new(String::new()),
437 stderr: Mutex::new(String::new()),
438 })),
439 )
440 }
441}
442
443#[derive(Debug, Clone)]
447pub struct Harness {
448 inner: Arc<HarnessInner>,
449}
450
451impl Harness {
452 pub fn real() -> Self {
463 Self::with_mode(
464 Arc::new(MockAwareClock::new(RealClock::new())),
465 HarnessMode::Real,
466 )
467 }
468
469 pub fn null() -> Self {
472 Self::with_mode(
473 paused_clock_at_unix_ms(0) as Arc<dyn Clock>,
474 HarnessMode::Null(NullHarnessState::default()),
475 )
476 }
477
478 pub fn mock() -> MockHarnessBuilder {
480 MockHarnessBuilder::new()
481 }
482
483 pub fn with_clock(clock: Arc<dyn Clock>) -> Self {
488 Self::with_mode(clock, HarnessMode::Real)
489 }
490
491 pub fn from_inner(inner: Arc<HarnessInner>) -> Self {
496 Self { inner }
497 }
498
499 fn with_mode(clock: Arc<dyn Clock>, mode: HarnessMode) -> Self {
500 let inner = Arc::new(HarnessInner {
501 clock,
502 mode,
503 net_policy: None,
504 quarantined: Mutex::new(false),
505 });
506 Self { inner }
507 }
508
509 pub fn with_net_policy(&self, policy: crate::harness_net::NetPolicy) -> Self {
523 let clock = Arc::clone(&self.inner.clock);
524 let mode = match &self.inner.mode {
525 HarnessMode::Real => HarnessMode::Real,
526 HarnessMode::Null(_) => HarnessMode::Null(NullHarnessState::default()),
527 HarnessMode::Mock(state) => HarnessMode::Mock(Arc::clone(state)),
528 };
529 #[allow(clippy::arc_with_non_send_sync)]
531 let inner = Arc::new(HarnessInner {
532 clock,
533 mode,
534 net_policy: Some(policy),
535 quarantined: Mutex::new(self.is_quarantined()),
536 });
537 Self { inner }
538 }
539
540 pub fn is_quarantined(&self) -> bool {
543 self.inner.is_quarantined()
544 }
545
546 pub fn deny_events(&self) -> Vec<DenyEvent> {
547 match self.inner.mode() {
548 HarnessMode::Null(state) => state.deny_events(),
549 HarnessMode::Real | HarnessMode::Mock(_) => Vec::new(),
550 }
551 }
552
553 pub fn calls(&self) -> Vec<HarnessCall> {
554 match self.inner.mode() {
555 HarnessMode::Mock(state) => state.calls(),
556 HarnessMode::Real | HarnessMode::Null(_) => Vec::new(),
557 }
558 }
559
560 pub fn captured_stdio(&self) -> String {
561 match self.inner.mode() {
562 HarnessMode::Mock(state) => state.stdio(),
563 HarnessMode::Real | HarnessMode::Null(_) => String::new(),
564 }
565 }
566
567 pub fn captured_stderr(&self) -> String {
568 match self.inner.mode() {
569 HarnessMode::Mock(state) => state.stderr(),
570 HarnessMode::Real | HarnessMode::Null(_) => String::new(),
571 }
572 }
573
574 pub fn test() -> (Self, Arc<PausedClock>) {
586 Self::with_paused_clock(OffsetDateTime::UNIX_EPOCH)
587 }
588
589 pub fn with_paused_clock(origin: OffsetDateTime) -> (Self, Arc<PausedClock>) {
593 let paused = PausedClock::new(origin);
594 let as_dyn: Arc<dyn Clock> = paused.clone();
595 (Self::with_clock(as_dyn), paused)
596 }
597
598 pub fn stdio(&self) -> HarnessStdio {
600 HarnessStdio {
601 inner: Arc::clone(&self.inner),
602 }
603 }
604
605 pub fn term(&self) -> HarnessTerm {
607 HarnessTerm {
608 inner: Arc::clone(&self.inner),
609 }
610 }
611
612 pub fn clock(&self) -> HarnessClock {
614 HarnessClock {
615 inner: Arc::clone(&self.inner),
616 }
617 }
618
619 pub fn fs(&self) -> HarnessFs {
621 HarnessFs {
622 inner: Arc::clone(&self.inner),
623 }
624 }
625
626 pub fn env(&self) -> HarnessEnv {
628 HarnessEnv {
629 inner: Arc::clone(&self.inner),
630 }
631 }
632
633 pub fn random(&self) -> HarnessRandom {
635 HarnessRandom {
636 inner: Arc::clone(&self.inner),
637 }
638 }
639
640 pub fn net(&self) -> HarnessNet {
642 HarnessNet {
643 inner: Arc::clone(&self.inner),
644 }
645 }
646
647 pub fn process(&self) -> HarnessProcess {
649 HarnessProcess {
650 inner: Arc::clone(&self.inner),
651 }
652 }
653
654 pub fn crypto(&self) -> HarnessCrypto {
656 HarnessCrypto {
657 inner: Arc::clone(&self.inner),
658 }
659 }
660
661 pub fn system(&self) -> HarnessSystem {
663 HarnessSystem {
664 inner: Arc::clone(&self.inner),
665 }
666 }
667
668 pub fn llm(&self) -> HarnessLlm {
670 HarnessLlm {
671 inner: Arc::clone(&self.inner),
672 }
673 }
674
675 pub fn tenant(&self) -> HarnessTenant {
677 HarnessTenant {
678 inner: Arc::clone(&self.inner),
679 }
680 }
681
682 pub fn obs(&self) -> HarnessObs {
684 HarnessObs {
685 inner: Arc::clone(&self.inner),
686 }
687 }
688
689 pub fn into_vm_value(self) -> crate::value::VmValue {
691 crate::value::VmValue::harness(VmHarness {
692 inner: self.inner,
693 kind: HarnessKind::Root,
694 })
695 }
696}
697
698fn paused_clock_at_unix_ms(unix_ms: i64) -> Arc<PausedClock> {
699 let nanos = (unix_ms as i128).saturating_mul(1_000_000);
700 let origin =
701 OffsetDateTime::from_unix_timestamp_nanos(nanos).unwrap_or(OffsetDateTime::UNIX_EPOCH);
702 PausedClock::new(origin)
703}
704
705pub(crate) fn vm_string(value: impl Into<String>) -> crate::VmValue {
706 crate::VmValue::String(std::sync::Arc::from(value.into()))
707}
708
709impl Default for Harness {
710 fn default() -> Self {
711 Self::real()
712 }
713}
714
715#[derive(Debug, Clone)]
718pub struct HarnessStdio {
719 inner: Arc<HarnessInner>,
720}
721
722#[derive(Debug, Clone)]
724pub struct HarnessTerm {
725 inner: Arc<HarnessInner>,
726}
727
728#[derive(Debug, Clone)]
730pub struct HarnessClock {
731 inner: Arc<HarnessInner>,
732}
733
734impl HarnessClock {
735 pub fn clock(&self) -> &Arc<dyn Clock> {
736 self.inner.clock()
737 }
738}
739
740#[derive(Debug, Clone)]
743pub struct HarnessFs {
744 inner: Arc<HarnessInner>,
745}
746
747#[derive(Debug, Clone)]
749pub struct HarnessEnv {
750 inner: Arc<HarnessInner>,
751}
752
753#[derive(Debug, Clone)]
755pub struct HarnessRandom {
756 inner: Arc<HarnessInner>,
757}
758
759#[derive(Debug, Clone)]
761pub struct HarnessNet {
762 inner: Arc<HarnessInner>,
763}
764
765#[derive(Debug, Clone)]
767pub struct HarnessProcess {
768 inner: Arc<HarnessInner>,
769}
770
771#[derive(Debug, Clone)]
773pub struct HarnessCrypto {
774 inner: Arc<HarnessInner>,
775}
776
777#[derive(Debug, Clone)]
783pub struct HarnessSystem {
784 inner: Arc<HarnessInner>,
785}
786
787#[derive(Debug, Clone)]
789pub struct HarnessLlm {
790 inner: Arc<HarnessInner>,
791}
792
793#[derive(Debug, Clone)]
799pub struct HarnessTenant {
800 inner: Arc<HarnessInner>,
801}
802
803#[derive(Debug, Clone)]
813pub struct HarnessObs {
814 inner: Arc<HarnessInner>,
815}
816
817macro_rules! sub_handle_inner {
818 ($($ty:ty),* $(,)?) => {
819 $(
820 impl $ty {
821 #[allow(dead_code)]
822 pub(crate) fn inner(&self) -> &Arc<HarnessInner> {
823 &self.inner
824 }
825 }
826 )*
827 };
828}
829sub_handle_inner!(
830 HarnessStdio,
831 HarnessTerm,
832 HarnessFs,
833 HarnessEnv,
834 HarnessRandom,
835 HarnessNet,
836 HarnessProcess,
837 HarnessCrypto,
838 HarnessSystem,
839 HarnessLlm,
840 HarnessTenant,
841 HarnessObs,
842);
843
844impl HarnessClock {
845 #[allow(dead_code)]
846 pub(crate) fn inner(&self) -> &Arc<HarnessInner> {
847 &self.inner
848 }
849}
850
851#[derive(Clone)]
856pub struct VmHarness {
857 inner: Arc<HarnessInner>,
858 kind: HarnessKind,
859}
860
861impl VmHarness {
862 pub fn kind(&self) -> HarnessKind {
863 self.kind
864 }
865
866 pub fn type_name(&self) -> &'static str {
867 self.kind.type_name()
868 }
869
870 pub fn inner(&self) -> &Arc<HarnessInner> {
871 &self.inner
872 }
873
874 pub fn sub_handle(&self, field: &str) -> Option<VmHarness> {
878 if self.kind != HarnessKind::Root {
879 return None;
880 }
881 let kind = HarnessKind::from_field_name(field)?;
882 self.sub_handle_kind(kind)
883 }
884
885 pub(crate) fn sub_handle_kind(&self, kind: HarnessKind) -> Option<VmHarness> {
886 if self.kind != HarnessKind::Root || kind == HarnessKind::Root {
887 return None;
888 }
889 Some(VmHarness {
890 inner: Arc::clone(&self.inner),
891 kind,
892 })
893 }
894}
895
896impl fmt::Debug for VmHarness {
897 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
898 f.debug_struct("VmHarness")
899 .field("kind", &self.kind)
900 .finish_non_exhaustive()
901 }
902}
903
904#[derive(Debug)]
910pub struct MockAwareClock<C: Clock + 'static> {
911 inner: C,
912}
913
914impl<C: Clock + 'static> MockAwareClock<C> {
915 pub fn new(inner: C) -> Self {
916 Self { inner }
917 }
918}
919
920#[async_trait]
921impl<C: Clock + 'static> Clock for MockAwareClock<C> {
922 fn now_utc(&self) -> OffsetDateTime {
923 if let Some(mock) = crate::clock_mock::active_mock_clock() {
924 return mock.now_utc();
925 }
926 self.inner.now_utc()
927 }
928
929 fn monotonic_ms(&self) -> i64 {
930 if let Some(mock) = crate::clock_mock::active_mock_clock() {
931 return mock.monotonic_ms();
932 }
933 self.inner.monotonic_ms()
934 }
935
936 async fn sleep(&self, duration: Duration) {
937 if duration.is_zero() {
938 return;
939 }
940 if let Some(mock) = crate::clock_mock::active_mock_clock() {
941 mock.advance_std_sync(duration);
947 return;
948 }
949 self.inner.sleep(duration).await;
950 }
951
952 async fn sleep_until_utc(&self, deadline: OffsetDateTime) {
953 if let Some(mock) = crate::clock_mock::active_mock_clock() {
954 let now = mock.now_utc();
955 if deadline > now {
956 if let Ok(delta) = Duration::try_from(deadline - now) {
957 mock.advance_std_sync(delta);
958 }
959 }
960 return;
961 }
962 self.inner.sleep_until_utc(deadline).await;
963 }
964}
965
966#[cfg(test)]
967mod tests {
968 use super::*;
969
970 #[test]
971 fn real_constructs_without_panic() {
972 let _harness = Harness::real();
973 }
974
975 #[test]
976 fn sub_handles_share_inner_state() {
977 let harness = Harness::real();
978 let stdio_inner = Arc::as_ptr(harness.stdio().inner());
979 let clock_inner = Arc::as_ptr(harness.clock().inner());
980 assert_eq!(stdio_inner, clock_inner, "sub-handles share Arc<Inner>");
981 }
982
983 #[test]
984 fn kinds_round_trip_through_field_names() {
985 for kind in HarnessKind::SUB_HANDLES {
986 let field = kind.field_name().unwrap();
987 assert_eq!(HarnessKind::from_field_name(field), Some(*kind));
988 }
989 assert!(HarnessKind::from_field_name("nope").is_none());
990 assert!(HarnessKind::Root.field_name().is_none());
991 }
992
993 #[test]
994 fn vm_harness_property_access_returns_sub_handle() {
995 let root = match Harness::real().into_vm_value() {
996 crate::value::VmValue::Harness(h) => h,
997 other => panic!("expected Harness variant, got {}", other.type_name()),
998 };
999 let stdio = root.sub_handle("stdio").expect("stdio sub-handle");
1000 assert_eq!(stdio.kind(), HarnessKind::Stdio);
1001 assert!(stdio.sub_handle("clock").is_none(), "nested access denied");
1002 assert!(root.sub_handle("not_a_field").is_none());
1003 }
1004
1005 #[test]
1006 fn test_constructor_clock_advances_under_paused_clock_advance() {
1007 let (harness, paused) = Harness::test();
1008 let clock = harness.clock();
1009 let start_wall = clock.clock().now_utc();
1010 assert_eq!(start_wall, OffsetDateTime::UNIX_EPOCH);
1011 assert_eq!(clock.clock().monotonic_ms(), 0);
1012
1013 paused.advance(Duration::from_millis(1_500));
1014 assert_eq!(clock.clock().monotonic_ms(), 1_500);
1015 let after_wall = clock.clock().now_utc();
1016 assert_eq!(after_wall - start_wall, time::Duration::milliseconds(1_500));
1017 }
1018
1019 #[test]
1020 fn with_paused_clock_pins_origin() {
1021 let origin = OffsetDateTime::from_unix_timestamp(1_700_000_000).unwrap();
1022 let (harness, paused) = Harness::with_paused_clock(origin);
1023 assert_eq!(harness.clock().clock().now_utc(), origin);
1024 paused.advance(Duration::from_mins(1));
1025 assert_eq!(
1026 harness.clock().clock().now_utc() - origin,
1027 time::Duration::seconds(60)
1028 );
1029 }
1030
1031 #[test]
1032 fn null_harness_records_deny_events_for_every_sub_handle() {
1033 let harness = Harness::null();
1034 for source in [
1035 r#"fn main(harness: Harness) { harness.stdio.println("blocked") }"#,
1036 r"fn main(harness: Harness) { harness.term.width() }",
1037 r"fn main(harness: Harness) { harness.clock.now_ms() }",
1038 r#"fn main(harness: Harness) { harness.fs.read_text("/x") }"#,
1039 r#"fn main(harness: Harness) { harness.env.get("KEY") }"#,
1040 r"fn main(harness: Harness) { harness.random.gen_u64() }",
1041 r#"fn main(harness: Harness) { harness.net.get("https://example.test") }"#,
1042 r#"fn main(harness: Harness) { harness.process.spawn_captured({cmd: "printf", args: ["x"]}) }"#,
1043 r#"fn main(harness: Harness) { harness.crypto.sha256("") }"#,
1044 r"fn main(harness: Harness) { harness.system.cpu() }",
1045 r"fn main(harness: Harness) { harness.llm.catalog() }",
1046 r"fn main(harness: Harness) { harness.tenant.id() }",
1047 r#"fn main(harness: Harness) { harness.obs.log("blocked", "info", {}) }"#,
1048 ] {
1049 let error = run_harness_source(source, harness.clone()).expect_err("call denied");
1050 assert!(
1051 error.contains("NullHarness denied"),
1052 "unexpected deny error: {error}"
1053 );
1054 }
1055
1056 let events = harness.deny_events();
1057 let observed: Vec<_> = events
1058 .iter()
1059 .map(|event| (event.sub_handle, event.method.as_str()))
1060 .collect();
1061 assert_eq!(
1062 observed,
1063 vec![
1064 (HarnessKind::Stdio, "println"),
1065 (HarnessKind::Term, "width"),
1066 (HarnessKind::Clock, "now_ms"),
1067 (HarnessKind::Fs, "read_text"),
1068 (HarnessKind::Env, "get"),
1069 (HarnessKind::Random, "gen_u64"),
1070 (HarnessKind::Net, "get"),
1071 (HarnessKind::Process, "spawn_captured"),
1072 (HarnessKind::Crypto, "sha256"),
1073 (HarnessKind::System, "cpu"),
1074 (HarnessKind::Llm, "catalog"),
1075 (HarnessKind::Tenant, "id"),
1076 (HarnessKind::Obs, "log"),
1077 ]
1078 );
1079 assert_eq!(events[0].args, vec!["blocked"]);
1080 assert_eq!(events[3].args, vec!["/x"]);
1081 }
1082
1083 #[test]
1084 fn mock_harness_replays_canned_responses_and_records_calls() {
1085 let harness = Harness::mock()
1086 .clock_at_unix_ms(1_700_000_000_000)
1087 .env("KEY", "value")
1088 .fs_read("/x", b"data".to_vec())
1089 .random_u64(42)
1090 .net_get("https://example.test", "body")
1091 .build();
1092
1093 let output = run_harness_source(
1094 r#"
1095fn main(harness: Harness) {
1096 harness.stdio.print("partial ")
1097 harness.stdio.println("line")
1098 __io_println(harness.term.width())
1099 __io_println(harness.term.height())
1100 __io_println(harness.clock.now_ms())
1101 harness.clock.sleep_ms(250)
1102 __io_println(harness.clock.now_ms())
1103 __io_println(harness.clock.monotonic_ms())
1104 __io_println(harness.env.get("KEY"))
1105 __io_println(harness.fs.read_text("/x"))
1106 __io_println(harness.fs.exists("/missing"))
1107 __io_println(harness.random.gen_u64())
1108 __io_println(harness.net.get("https://example.test"))
1109 __io_println(harness.crypto.sha256(""))
1110 __io_println(len(harness.llm.catalog()) > 0)
1111}
1112"#,
1113 harness.clone(),
1114 )
1115 .expect("mock harness run succeeds");
1116
1117 assert_eq!(harness.captured_stdio(), "partial line\n");
1118 assert_eq!(
1119 output,
1120 "80\n24\n1700000000000\n1700000000250\n250\nvalue\ndata\nfalse\n42\nbody\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\ntrue\n"
1121 );
1122 let observed: Vec<_> = harness
1123 .calls()
1124 .into_iter()
1125 .map(|call| (call.sub_handle, call.method))
1126 .collect();
1127 assert_eq!(
1128 observed,
1129 vec![
1130 (HarnessKind::Stdio, "print".to_string()),
1131 (HarnessKind::Stdio, "println".to_string()),
1132 (HarnessKind::Term, "width".to_string()),
1133 (HarnessKind::Term, "height".to_string()),
1134 (HarnessKind::Clock, "now_ms".to_string()),
1135 (HarnessKind::Clock, "sleep_ms".to_string()),
1136 (HarnessKind::Clock, "now_ms".to_string()),
1137 (HarnessKind::Clock, "monotonic_ms".to_string()),
1138 (HarnessKind::Env, "get".to_string()),
1139 (HarnessKind::Fs, "read_text".to_string()),
1140 (HarnessKind::Fs, "exists".to_string()),
1141 (HarnessKind::Random, "gen_u64".to_string()),
1142 (HarnessKind::Net, "get".to_string()),
1143 (HarnessKind::Crypto, "sha256".to_string()),
1144 (HarnessKind::Llm, "catalog".to_string()),
1145 ]
1146 );
1147 }
1148
1149 #[test]
1150 fn mock_harness_records_repeated_cached_harness_method_calls() {
1151 let harness = Harness::mock().env("KEY", "value").build();
1152
1153 run_harness_source(
1154 r#"
1155fn main(harness: Harness) {
1156 var i = 0
1157 while i < 3 {
1158 let _ = harness.clock.elapsed()
1159 let value = harness.env.get_or("KEY", "")
1160 harness.stdio.println(value)
1161 i = i + 1
1162 }
1163}
1164"#,
1165 harness.clone(),
1166 )
1167 .expect("mock harness run succeeds");
1168
1169 assert_eq!(harness.captured_stdio(), "value\nvalue\nvalue\n");
1170 let observed: Vec<_> = harness
1171 .calls()
1172 .into_iter()
1173 .map(|call| (call.sub_handle, call.method))
1174 .collect();
1175 assert_eq!(
1176 observed,
1177 vec![
1178 (HarnessKind::Clock, "elapsed".to_string()),
1179 (HarnessKind::Env, "get_or".to_string()),
1180 (HarnessKind::Stdio, "println".to_string()),
1181 (HarnessKind::Clock, "elapsed".to_string()),
1182 (HarnessKind::Env, "get_or".to_string()),
1183 (HarnessKind::Stdio, "println".to_string()),
1184 (HarnessKind::Clock, "elapsed".to_string()),
1185 (HarnessKind::Env, "get_or".to_string()),
1186 (HarnessKind::Stdio, "println".to_string()),
1187 ]
1188 );
1189 }
1190
1191 #[test]
1192 fn mock_harness_replays_random_values_fifo() {
1193 let harness = Harness::mock()
1194 .random_u64(7)
1195 .random_u64(11)
1196 .random_u64(u64::MAX)
1197 .build();
1198
1199 let output = run_harness_source(
1200 r"
1201fn main(harness: Harness) {
1202 __io_println(harness.random.gen_u64())
1203 __io_println(harness.random.gen_u64())
1204 __io_println(harness.random.gen_u64())
1205}
1206",
1207 harness,
1208 )
1209 .expect("mock random succeeds");
1210
1211 assert_eq!(output, "7\n11\n9223372036854775807\n");
1212 }
1213
1214 #[test]
1215 fn mock_harness_reports_missing_canned_responses() {
1216 let cases = [
1217 (
1218 r#"fn main(harness: Harness) { harness.fs.read_text("/missing") }"#,
1219 "MockHarness has no fs_read response for /missing",
1220 ),
1221 (
1222 r"fn main(harness: Harness) { harness.random.gen_u64() }",
1223 "MockHarness has no random_u64 response",
1224 ),
1225 (
1226 r#"fn main(harness: Harness) { harness.net.get("https://missing.test") }"#,
1227 "MockHarness has no net_get response for https://missing.test",
1228 ),
1229 (
1230 r#"fn main(harness: Harness) { harness.process.spawn_captured({cmd: "printf", args: ["x"]}) }"#,
1231 "MockHarness has no process spawn response",
1232 ),
1233 ];
1234
1235 for (source, expected) in cases {
1236 let error = run_harness_source(source, Harness::mock().build())
1237 .expect_err("missing mock response fails");
1238 assert!(
1239 error.contains(expected),
1240 "expected `{expected}` in `{error}`"
1241 );
1242 }
1243 }
1244
1245 #[test]
1246 fn mock_harness_records_failed_calls() {
1247 let harness = Harness::mock().build();
1248 let error = run_harness_source(
1249 r#"fn main(harness: Harness) { harness.net.get("https://missing.test") }"#,
1250 harness.clone(),
1251 )
1252 .expect_err("missing mock response fails");
1253
1254 assert!(error.contains("MockHarness has no net_get response"));
1255 assert_eq!(
1256 harness.calls(),
1257 vec![HarnessCall {
1258 sub_handle: HarnessKind::Net,
1259 method: "get".to_string(),
1260 args: vec!["https://missing.test".to_string()],
1261 }]
1262 );
1263 }
1264
1265 #[test]
1266 fn mock_harness_captures_stderr_separately_from_stdout() {
1267 let harness = Harness::mock().build();
1268 run_harness_source(
1269 r#"
1270fn main(harness: Harness) {
1271 harness.stdio.println("stdout line")
1272 harness.stdio.eprint("err ")
1273 harness.stdio.eprintln("trail")
1274}
1275"#,
1276 harness.clone(),
1277 )
1278 .expect("stderr capture run succeeds");
1279 assert_eq!(harness.captured_stdio(), "stdout line\n");
1280 assert_eq!(harness.captured_stderr(), "err trail\n");
1281 }
1282
1283 #[test]
1284 fn mock_harness_replays_stdin_lines_for_read_and_prompt() {
1285 let harness = Harness::mock()
1286 .stdin_line("first")
1287 .stdin_line("second")
1288 .build();
1289 let output = run_harness_source(
1290 r#"
1291fn main(harness: Harness) {
1292 harness.stdio.println(harness.stdio.read_line())
1293 harness.stdio.println(harness.stdio.prompt("answer: "))
1294 let eof = harness.stdio.read_line({trim: false})
1295 harness.stdio.println(eof.status)
1296}
1297"#,
1298 harness.clone(),
1299 )
1300 .expect("stdin replay succeeds");
1301 assert_eq!(output, "");
1303 assert_eq!(harness.captured_stdio(), "first\nanswer: second\neof\n");
1304 }
1305
1306 #[test]
1307 fn mock_harness_replays_password_input_without_stdout_echo() {
1308 let harness = Harness::mock().stdin_line("secret").build();
1309 let output = run_harness_source(
1310 r#"
1311fn main(harness: Harness) {
1312 __io_println(harness.term.read_password("password: "))
1313}
1314"#,
1315 harness.clone(),
1316 )
1317 .expect("stdin replay succeeds");
1318
1319 assert_eq!(output, "secret\n");
1320 assert_eq!(harness.captured_stdio(), "");
1321 assert_eq!(harness.captured_stderr(), "password: ");
1322 assert_eq!(
1323 harness.calls(),
1324 vec![HarnessCall {
1325 sub_handle: HarnessKind::Term,
1326 method: "read_password".to_string(),
1327 args: vec!["password: ".to_string()],
1328 }]
1329 );
1330 }
1331
1332 #[test]
1333 fn mock_harness_rejects_wrong_argument_types() {
1334 let error = run_harness_source(
1335 r"fn main(harness: Harness) { harness.fs.read_text(1) }",
1336 Harness::mock().build(),
1337 )
1338 .expect_err("wrong argument type fails");
1339
1340 let runtime_rejection =
1350 error.contains("HarnessFs.read_text expects string argument 1, got int");
1351 let static_rejection = error.contains("argument 1 `path`: expected string, found int");
1352 assert!(
1353 runtime_rejection || static_rejection,
1354 "expected a string/int type rejection for read_text, got: {error}"
1355 );
1356 }
1357
1358 #[test]
1359 fn real_harness_fs_write_outside_workspace_roots_surfaces_cap_201() {
1360 use crate::orchestration::{
1361 clear_execution_policy_stacks, push_execution_policy, CapabilityPolicy, SandboxProfile,
1362 };
1363 clear_execution_policy_stacks();
1364 let temp = tempfile::tempdir().unwrap();
1365 let policy = CapabilityPolicy {
1366 sandbox_profile: SandboxProfile::Worktree,
1367 workspace_roots: vec![temp.path().to_string_lossy().into_owned()],
1368 ..CapabilityPolicy::default()
1369 };
1370 push_execution_policy(policy);
1371 let outside = std::env::temp_dir().join("harn_e4_4_cap_201_outside.txt");
1372 let source = format!(
1373 r#"fn main(harness: Harness) {{ harness.fs.write_text("{}", "x") }}"#,
1374 outside.to_string_lossy().replace('\\', "/"),
1375 );
1376 let error = run_harness_source(&source, Harness::real())
1377 .expect_err("write outside workspace_roots must reject");
1378 clear_execution_policy_stacks();
1379 assert!(
1380 error.contains("HARN-CAP-201"),
1381 "expected HARN-CAP-201 prefix, got: {error}"
1382 );
1383 assert!(
1384 error.contains("sandbox violation"),
1385 "deny should keep the underlying sandbox-rejection message, got: {error}"
1386 );
1387 }
1388
1389 fn run_harness_source(source: &str, harness: Harness) -> Result<String, String> {
1390 let rt = tokio::runtime::Builder::new_current_thread()
1391 .enable_all()
1392 .build()
1393 .unwrap();
1394 rt.block_on(async move {
1395 let local = tokio::task::LocalSet::new();
1396 local
1397 .run_until(async move {
1398 let chunk = crate::compile_source(source)?;
1399 let mut vm = crate::Vm::new();
1400 crate::stdlib::register_vm_stdlib(&mut vm);
1401 vm.set_harness(harness);
1402 vm.execute(&chunk)
1403 .await
1404 .map_err(|error| error.to_string())?;
1405 Ok(vm.output().to_string())
1406 })
1407 .await
1408 })
1409 }
1410}