1use std::collections::{BTreeMap, VecDeque};
22use std::fmt;
23use std::sync::{Arc, Mutex};
24use std::time::Duration;
25
26use async_trait::async_trait;
27use harn_clock::{Clock, PausedClock, RealClock};
28use time::OffsetDateTime;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub enum HarnessKind {
36 Root,
37 Stdio,
38 Term,
39 Clock,
40 Fs,
41 Env,
42 Random,
43 Net,
44 Process,
45 Crypto,
46 System,
47 Secrets,
48 Llm,
49 Tenant,
53 Auth,
58 Obs,
64}
65
66impl HarnessKind {
67 pub const fn type_name(self) -> &'static str {
71 match self {
72 HarnessKind::Root => "Harness",
73 HarnessKind::Stdio => "HarnessStdio",
74 HarnessKind::Term => "HarnessTerm",
75 HarnessKind::Clock => "HarnessClock",
76 HarnessKind::Fs => "HarnessFs",
77 HarnessKind::Env => "HarnessEnv",
78 HarnessKind::Random => "HarnessRandom",
79 HarnessKind::Net => "HarnessNet",
80 HarnessKind::Process => "HarnessProcess",
81 HarnessKind::Crypto => "HarnessCrypto",
82 HarnessKind::System => "HarnessSystem",
83 HarnessKind::Secrets => "HarnessSecrets",
84 HarnessKind::Llm => "HarnessLlm",
85 HarnessKind::Tenant => "HarnessTenant",
86 HarnessKind::Auth => "HarnessAuth",
87 HarnessKind::Obs => "HarnessObs",
88 }
89 }
90
91 pub const fn field_name(self) -> Option<&'static str> {
94 match self {
95 HarnessKind::Root => None,
96 HarnessKind::Stdio => Some("stdio"),
97 HarnessKind::Term => Some("term"),
98 HarnessKind::Clock => Some("clock"),
99 HarnessKind::Fs => Some("fs"),
100 HarnessKind::Env => Some("env"),
101 HarnessKind::Random => Some("random"),
102 HarnessKind::Net => Some("net"),
103 HarnessKind::Process => Some("process"),
104 HarnessKind::Crypto => Some("crypto"),
105 HarnessKind::System => Some("system"),
106 HarnessKind::Secrets => Some("secrets"),
107 HarnessKind::Llm => Some("llm"),
108 HarnessKind::Tenant => Some("tenant"),
109 HarnessKind::Auth => Some("auth"),
110 HarnessKind::Obs => Some("obs"),
111 }
112 }
113
114 pub fn from_field_name(name: &str) -> Option<Self> {
116 match name {
117 "stdio" => Some(HarnessKind::Stdio),
118 "term" => Some(HarnessKind::Term),
119 "clock" => Some(HarnessKind::Clock),
120 "fs" => Some(HarnessKind::Fs),
121 "env" => Some(HarnessKind::Env),
122 "random" => Some(HarnessKind::Random),
123 "net" => Some(HarnessKind::Net),
124 "process" => Some(HarnessKind::Process),
125 "crypto" => Some(HarnessKind::Crypto),
126 "system" => Some(HarnessKind::System),
127 "secrets" => Some(HarnessKind::Secrets),
128 "llm" => Some(HarnessKind::Llm),
129 "tenant" => Some(HarnessKind::Tenant),
130 "auth" => Some(HarnessKind::Auth),
131 "obs" => Some(HarnessKind::Obs),
132 _ => None,
133 }
134 }
135
136 pub const SUB_HANDLES: &'static [HarnessKind] = &[
138 HarnessKind::Stdio,
139 HarnessKind::Term,
140 HarnessKind::Clock,
141 HarnessKind::Fs,
142 HarnessKind::Env,
143 HarnessKind::Random,
144 HarnessKind::Net,
145 HarnessKind::Process,
146 HarnessKind::Crypto,
147 HarnessKind::System,
148 HarnessKind::Secrets,
149 HarnessKind::Llm,
150 HarnessKind::Tenant,
151 HarnessKind::Auth,
152 HarnessKind::Obs,
153 ];
154
155 pub const ALL: &'static [HarnessKind] = &[
157 HarnessKind::Root,
158 HarnessKind::Stdio,
159 HarnessKind::Term,
160 HarnessKind::Clock,
161 HarnessKind::Fs,
162 HarnessKind::Env,
163 HarnessKind::Random,
164 HarnessKind::Net,
165 HarnessKind::Process,
166 HarnessKind::Crypto,
167 HarnessKind::System,
168 HarnessKind::Secrets,
169 HarnessKind::Llm,
170 HarnessKind::Tenant,
171 HarnessKind::Auth,
172 HarnessKind::Obs,
173 ];
174}
175
176pub struct HarnessInner {
182 clock: Arc<dyn Clock>,
183 mode: HarnessMode,
184 net_policy: Option<crate::harness_net::NetPolicy>,
189 secret_provider: Option<Arc<dyn crate::secrets::SecretProvider>>,
192 quarantined: Mutex<bool>,
199}
200
201impl fmt::Debug for HarnessInner {
202 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203 f.debug_struct("HarnessInner")
204 .field("clock", &"<dyn Clock>")
205 .field("mode", &self.mode)
206 .field("net_policy", &self.net_policy)
207 .field(
208 "secret_provider",
209 &self
210 .secret_provider
211 .as_ref()
212 .map(|provider| provider.namespace().to_string()),
213 )
214 .field("quarantined", &self.is_quarantined())
215 .finish()
216 }
217}
218
219impl HarnessInner {
220 pub fn clock(&self) -> &Arc<dyn Clock> {
221 &self.clock
222 }
223
224 pub(crate) fn mode(&self) -> &HarnessMode {
225 &self.mode
226 }
227
228 pub fn net_policy(&self) -> Option<&crate::harness_net::NetPolicy> {
229 self.net_policy.as_ref()
230 }
231
232 pub fn secret_provider(&self) -> Option<&Arc<dyn crate::secrets::SecretProvider>> {
233 self.secret_provider.as_ref()
234 }
235
236 pub(crate) fn mark_quarantined(&self) {
237 if let Ok(mut guard) = self.quarantined.lock() {
238 *guard = true;
239 }
240 }
241
242 pub fn is_quarantined(&self) -> bool {
243 self.quarantined.lock().map(|guard| *guard).unwrap_or(false)
244 }
245}
246
247#[derive(Debug)]
248pub(crate) enum HarnessMode {
249 Real,
250 Null(NullHarnessState),
251 Mock(Arc<MockHarnessState>),
252}
253
254#[derive(Debug, Default)]
255pub(crate) struct NullHarnessState {
256 deny_events: Mutex<Vec<DenyEvent>>,
257}
258
259impl NullHarnessState {
260 pub(crate) fn record_deny(
261 &self,
262 sub_handle: HarnessKind,
263 method: &str,
264 args: &[crate::VmValue],
265 ) {
266 self.deny_events
267 .lock()
268 .expect("deny events poisoned")
269 .push(DenyEvent::new(
270 sub_handle,
271 method,
272 args.iter().map(crate::VmValue::display).collect(),
273 ));
274 }
275
276 pub(crate) fn deny_events(&self) -> Vec<DenyEvent> {
277 self.deny_events
278 .lock()
279 .expect("deny events poisoned")
280 .clone()
281 }
282}
283
284#[derive(Debug, Clone, PartialEq, Eq)]
285pub struct DenyEvent {
286 pub sub_handle: HarnessKind,
287 pub method: String,
288 pub args: Vec<String>,
289}
290
291impl DenyEvent {
292 fn new(sub_handle: HarnessKind, method: &str, args: Vec<String>) -> Self {
293 Self {
294 sub_handle,
295 method: method.to_string(),
296 args,
297 }
298 }
299}
300
301#[derive(Debug)]
302pub(crate) struct MockHarnessState {
303 calls: Mutex<Vec<HarnessCall>>,
304 clock: Arc<PausedClock>,
305 env: BTreeMap<String, String>,
306 fs_reads: BTreeMap<String, Vec<u8>>,
307 net_gets: BTreeMap<String, String>,
308 random_u64: Mutex<VecDeque<u64>>,
309 stdin_lines: Mutex<VecDeque<String>>,
310 stdio: Mutex<String>,
311 stderr: Mutex<String>,
312}
313
314impl MockHarnessState {
315 pub(crate) fn record_call(
316 &self,
317 sub_handle: HarnessKind,
318 method: &str,
319 args: &[crate::VmValue],
320 ) {
321 self.calls
322 .lock()
323 .expect("calls poisoned")
324 .push(HarnessCall::new(
325 sub_handle,
326 method,
327 args.iter().map(crate::VmValue::display).collect(),
328 ));
329 }
330
331 pub(crate) fn calls(&self) -> Vec<HarnessCall> {
332 self.calls.lock().expect("calls poisoned").clone()
333 }
334
335 pub(crate) fn env_get(&self, key: &str) -> Option<&str> {
336 self.env.get(key).map(String::as_str)
337 }
338
339 pub(crate) fn fs_read(&self, path: &str) -> Option<&[u8]> {
340 self.fs_reads.get(path).map(Vec::as_slice)
341 }
342
343 pub(crate) fn net_get(&self, url: &str) -> Option<&str> {
344 self.net_gets.get(url).map(String::as_str)
345 }
346
347 pub(crate) fn next_random_u64(&self) -> Option<u64> {
348 let mut values = self.random_u64.lock().expect("random values poisoned");
349 values.pop_front()
350 }
351
352 pub(crate) fn advance_clock(&self, duration: std::time::Duration) {
353 self.clock.advance(duration);
354 }
355
356 pub(crate) fn push_stdio(&self, text: &str) {
357 self.stdio
358 .lock()
359 .expect("stdio buffer poisoned")
360 .push_str(text);
361 }
362
363 pub(crate) fn stdio(&self) -> String {
364 self.stdio.lock().expect("stdio buffer poisoned").clone()
365 }
366
367 pub(crate) fn push_stderr(&self, text: &str) {
368 self.stderr
369 .lock()
370 .expect("stderr buffer poisoned")
371 .push_str(text);
372 }
373
374 pub(crate) fn stderr(&self) -> String {
375 self.stderr.lock().expect("stderr buffer poisoned").clone()
376 }
377
378 pub(crate) fn pop_stdin_line(&self) -> Option<String> {
379 self.stdin_lines
380 .lock()
381 .expect("stdin queue poisoned")
382 .pop_front()
383 }
384}
385
386#[derive(Debug, Clone, PartialEq, Eq)]
387pub struct HarnessCall {
388 pub sub_handle: HarnessKind,
389 pub method: String,
390 pub args: Vec<String>,
391}
392
393impl HarnessCall {
394 fn new(sub_handle: HarnessKind, method: &str, args: Vec<String>) -> Self {
395 Self {
396 sub_handle,
397 method: method.to_string(),
398 args,
399 }
400 }
401}
402
403#[derive(Debug)]
404pub struct MockHarnessBuilder {
405 clock: Arc<PausedClock>,
406 env: BTreeMap<String, String>,
407 fs_reads: BTreeMap<String, Vec<u8>>,
408 net_gets: BTreeMap<String, String>,
409 random_u64: Vec<u64>,
410 stdin_lines: Vec<String>,
411}
412
413impl MockHarnessBuilder {
414 fn new() -> Self {
415 Self {
416 clock: paused_clock_at_unix_ms(0),
417 env: BTreeMap::new(),
418 fs_reads: BTreeMap::new(),
419 net_gets: BTreeMap::new(),
420 random_u64: Vec::new(),
421 stdin_lines: Vec::new(),
422 }
423 }
424
425 pub fn clock_at_unix_ms(mut self, unix_ms: i64) -> Self {
426 self.clock = paused_clock_at_unix_ms(unix_ms);
427 self
428 }
429
430 pub fn clock_at(mut self, origin: OffsetDateTime) -> Self {
431 self.clock = PausedClock::new(origin);
432 self
433 }
434
435 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
436 self.env.insert(key.into(), value.into());
437 self
438 }
439
440 pub fn fs_read(mut self, path: impl Into<String>, data: impl Into<Vec<u8>>) -> Self {
441 self.fs_reads.insert(path.into(), data.into());
442 self
443 }
444
445 pub fn net_get(mut self, url: impl Into<String>, body: impl Into<String>) -> Self {
446 self.net_gets.insert(url.into(), body.into());
447 self
448 }
449
450 pub fn random_u64(mut self, value: u64) -> Self {
451 self.random_u64.push(value);
452 self
453 }
454
455 pub fn stdin_line(mut self, line: impl Into<String>) -> Self {
461 self.stdin_lines.push(line.into());
462 self
463 }
464
465 pub fn build(self) -> Harness {
466 let clock = self.clock;
467 Harness::with_mode(
468 clock.clone() as Arc<dyn Clock>,
469 HarnessMode::Mock(Arc::new(MockHarnessState {
470 calls: Mutex::new(Vec::new()),
471 clock,
472 env: self.env,
473 fs_reads: self.fs_reads,
474 net_gets: self.net_gets,
475 random_u64: Mutex::new(self.random_u64.into()),
476 stdin_lines: Mutex::new(self.stdin_lines.into()),
477 stdio: Mutex::new(String::new()),
478 stderr: Mutex::new(String::new()),
479 })),
480 )
481 }
482}
483
484#[derive(Debug, Clone)]
488pub struct Harness {
489 inner: Arc<HarnessInner>,
490}
491
492impl Harness {
493 pub fn real() -> Self {
504 Self::with_mode(
505 Arc::new(MockAwareClock::new(RealClock::new())),
506 HarnessMode::Real,
507 )
508 }
509
510 pub fn null() -> Self {
513 Self::with_mode(
514 paused_clock_at_unix_ms(0) as Arc<dyn Clock>,
515 HarnessMode::Null(NullHarnessState::default()),
516 )
517 }
518
519 pub fn mock() -> MockHarnessBuilder {
521 MockHarnessBuilder::new()
522 }
523
524 pub fn with_clock(clock: Arc<dyn Clock>) -> Self {
529 Self::with_mode(clock, HarnessMode::Real)
530 }
531
532 pub fn from_inner(inner: Arc<HarnessInner>) -> Self {
537 Self { inner }
538 }
539
540 fn with_mode(clock: Arc<dyn Clock>, mode: HarnessMode) -> Self {
541 let inner = Arc::new(HarnessInner {
542 clock,
543 mode,
544 net_policy: None,
545 secret_provider: None,
546 quarantined: Mutex::new(false),
547 });
548 Self { inner }
549 }
550
551 pub fn with_net_policy(&self, policy: crate::harness_net::NetPolicy) -> Self {
565 let clock = Arc::clone(&self.inner.clock);
566 let mode = self.clone_mode_for_child();
567 #[allow(clippy::arc_with_non_send_sync)]
569 let inner = Arc::new(HarnessInner {
570 clock,
571 mode,
572 net_policy: Some(policy),
573 secret_provider: self.inner.secret_provider.clone(),
574 quarantined: Mutex::new(self.is_quarantined()),
575 });
576 Self { inner }
577 }
578
579 pub fn with_secret_provider(&self, provider: Arc<dyn crate::secrets::SecretProvider>) -> Self {
585 let clock = Arc::clone(&self.inner.clock);
586 let mode = self.clone_mode_for_child();
587 #[allow(clippy::arc_with_non_send_sync)]
588 let inner = Arc::new(HarnessInner {
589 clock,
590 mode,
591 net_policy: self.inner.net_policy.clone(),
592 secret_provider: Some(provider),
593 quarantined: Mutex::new(self.is_quarantined()),
594 });
595 Self { inner }
596 }
597
598 fn clone_mode_for_child(&self) -> HarnessMode {
599 match &self.inner.mode {
600 HarnessMode::Real => HarnessMode::Real,
601 HarnessMode::Null(_) => HarnessMode::Null(NullHarnessState::default()),
602 HarnessMode::Mock(state) => HarnessMode::Mock(Arc::clone(state)),
603 }
604 }
605
606 pub fn is_quarantined(&self) -> bool {
609 self.inner.is_quarantined()
610 }
611
612 pub fn deny_events(&self) -> Vec<DenyEvent> {
613 match self.inner.mode() {
614 HarnessMode::Null(state) => state.deny_events(),
615 HarnessMode::Real | HarnessMode::Mock(_) => Vec::new(),
616 }
617 }
618
619 pub fn calls(&self) -> Vec<HarnessCall> {
620 match self.inner.mode() {
621 HarnessMode::Mock(state) => state.calls(),
622 HarnessMode::Real | HarnessMode::Null(_) => Vec::new(),
623 }
624 }
625
626 pub fn captured_stdio(&self) -> String {
627 match self.inner.mode() {
628 HarnessMode::Mock(state) => state.stdio(),
629 HarnessMode::Real | HarnessMode::Null(_) => String::new(),
630 }
631 }
632
633 pub fn captured_stderr(&self) -> String {
634 match self.inner.mode() {
635 HarnessMode::Mock(state) => state.stderr(),
636 HarnessMode::Real | HarnessMode::Null(_) => String::new(),
637 }
638 }
639
640 pub fn test() -> (Self, Arc<PausedClock>) {
652 Self::with_paused_clock(OffsetDateTime::UNIX_EPOCH)
653 }
654
655 pub fn with_paused_clock(origin: OffsetDateTime) -> (Self, Arc<PausedClock>) {
659 let paused = PausedClock::new(origin);
660 let as_dyn: Arc<dyn Clock> = paused.clone();
661 (Self::with_clock(as_dyn), paused)
662 }
663
664 pub fn stdio(&self) -> HarnessStdio {
666 HarnessStdio {
667 inner: Arc::clone(&self.inner),
668 }
669 }
670
671 pub fn term(&self) -> HarnessTerm {
673 HarnessTerm {
674 inner: Arc::clone(&self.inner),
675 }
676 }
677
678 pub fn clock(&self) -> HarnessClock {
680 HarnessClock {
681 inner: Arc::clone(&self.inner),
682 }
683 }
684
685 pub fn fs(&self) -> HarnessFs {
687 HarnessFs {
688 inner: Arc::clone(&self.inner),
689 }
690 }
691
692 pub fn env(&self) -> HarnessEnv {
694 HarnessEnv {
695 inner: Arc::clone(&self.inner),
696 }
697 }
698
699 pub fn random(&self) -> HarnessRandom {
701 HarnessRandom {
702 inner: Arc::clone(&self.inner),
703 }
704 }
705
706 pub fn net(&self) -> HarnessNet {
708 HarnessNet {
709 inner: Arc::clone(&self.inner),
710 }
711 }
712
713 pub fn process(&self) -> HarnessProcess {
715 HarnessProcess {
716 inner: Arc::clone(&self.inner),
717 }
718 }
719
720 pub fn crypto(&self) -> HarnessCrypto {
722 HarnessCrypto {
723 inner: Arc::clone(&self.inner),
724 }
725 }
726
727 pub fn system(&self) -> HarnessSystem {
729 HarnessSystem {
730 inner: Arc::clone(&self.inner),
731 }
732 }
733
734 pub fn secrets(&self) -> HarnessSecrets {
736 HarnessSecrets {
737 inner: Arc::clone(&self.inner),
738 }
739 }
740
741 pub fn llm(&self) -> HarnessLlm {
743 HarnessLlm {
744 inner: Arc::clone(&self.inner),
745 }
746 }
747
748 pub fn tenant(&self) -> HarnessTenant {
750 HarnessTenant {
751 inner: Arc::clone(&self.inner),
752 }
753 }
754
755 pub fn auth(&self) -> HarnessAuth {
757 HarnessAuth {
758 inner: Arc::clone(&self.inner),
759 }
760 }
761
762 pub fn obs(&self) -> HarnessObs {
764 HarnessObs {
765 inner: Arc::clone(&self.inner),
766 }
767 }
768
769 pub fn into_vm_value(self) -> crate::value::VmValue {
771 crate::value::VmValue::harness(VmHarness {
772 inner: self.inner,
773 kind: HarnessKind::Root,
774 })
775 }
776}
777
778fn paused_clock_at_unix_ms(unix_ms: i64) -> Arc<PausedClock> {
779 let nanos = (unix_ms as i128).saturating_mul(1_000_000);
780 let origin =
781 OffsetDateTime::from_unix_timestamp_nanos(nanos).unwrap_or(OffsetDateTime::UNIX_EPOCH);
782 PausedClock::new(origin)
783}
784
785pub(crate) fn vm_string(value: impl Into<String>) -> crate::VmValue {
786 crate::VmValue::String(arcstr::ArcStr::from(value.into()))
787}
788
789impl Default for Harness {
790 fn default() -> Self {
791 Self::real()
792 }
793}
794
795#[derive(Debug, Clone)]
798pub struct HarnessStdio {
799 inner: Arc<HarnessInner>,
800}
801
802#[derive(Debug, Clone)]
804pub struct HarnessTerm {
805 inner: Arc<HarnessInner>,
806}
807
808#[derive(Debug, Clone)]
810pub struct HarnessClock {
811 inner: Arc<HarnessInner>,
812}
813
814impl HarnessClock {
815 pub fn clock(&self) -> &Arc<dyn Clock> {
816 self.inner.clock()
817 }
818}
819
820#[derive(Debug, Clone)]
823pub struct HarnessFs {
824 inner: Arc<HarnessInner>,
825}
826
827#[derive(Debug, Clone)]
829pub struct HarnessEnv {
830 inner: Arc<HarnessInner>,
831}
832
833#[derive(Debug, Clone)]
835pub struct HarnessRandom {
836 inner: Arc<HarnessInner>,
837}
838
839#[derive(Debug, Clone)]
841pub struct HarnessNet {
842 inner: Arc<HarnessInner>,
843}
844
845#[derive(Debug, Clone)]
847pub struct HarnessProcess {
848 inner: Arc<HarnessInner>,
849}
850
851#[derive(Debug, Clone)]
853pub struct HarnessCrypto {
854 inner: Arc<HarnessInner>,
855}
856
857#[derive(Debug, Clone)]
863pub struct HarnessSystem {
864 inner: Arc<HarnessInner>,
865}
866
867#[derive(Debug, Clone)]
869pub struct HarnessSecrets {
870 inner: Arc<HarnessInner>,
871}
872
873#[derive(Debug, Clone)]
875pub struct HarnessLlm {
876 inner: Arc<HarnessInner>,
877}
878
879#[derive(Debug, Clone)]
885pub struct HarnessTenant {
886 inner: Arc<HarnessInner>,
887}
888
889#[derive(Debug, Clone)]
897pub struct HarnessAuth {
898 inner: Arc<HarnessInner>,
899}
900
901#[derive(Debug, Clone)]
911pub struct HarnessObs {
912 inner: Arc<HarnessInner>,
913}
914
915macro_rules! sub_handle_inner {
916 ($($ty:ty),* $(,)?) => {
917 $(
918 impl $ty {
919 #[allow(dead_code)]
920 pub(crate) fn inner(&self) -> &Arc<HarnessInner> {
921 &self.inner
922 }
923 }
924 )*
925 };
926}
927sub_handle_inner!(
928 HarnessStdio,
929 HarnessTerm,
930 HarnessFs,
931 HarnessEnv,
932 HarnessRandom,
933 HarnessNet,
934 HarnessProcess,
935 HarnessCrypto,
936 HarnessSystem,
937 HarnessSecrets,
938 HarnessLlm,
939 HarnessTenant,
940 HarnessAuth,
941 HarnessObs,
942);
943
944impl HarnessClock {
945 #[allow(dead_code)]
946 pub(crate) fn inner(&self) -> &Arc<HarnessInner> {
947 &self.inner
948 }
949}
950
951#[derive(Clone)]
956pub struct VmHarness {
957 inner: Arc<HarnessInner>,
958 kind: HarnessKind,
959}
960
961impl VmHarness {
962 pub fn kind(&self) -> HarnessKind {
963 self.kind
964 }
965
966 pub fn type_name(&self) -> &'static str {
967 self.kind.type_name()
968 }
969
970 pub fn inner(&self) -> &Arc<HarnessInner> {
971 &self.inner
972 }
973
974 pub fn sub_handle(&self, field: &str) -> Option<VmHarness> {
978 if self.kind != HarnessKind::Root {
979 return None;
980 }
981 let kind = HarnessKind::from_field_name(field)?;
982 self.sub_handle_kind(kind)
983 }
984
985 pub(crate) fn sub_handle_kind(&self, kind: HarnessKind) -> Option<VmHarness> {
986 if self.kind != HarnessKind::Root || kind == HarnessKind::Root {
987 return None;
988 }
989 Some(VmHarness {
990 inner: Arc::clone(&self.inner),
991 kind,
992 })
993 }
994}
995
996impl fmt::Debug for VmHarness {
997 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
998 f.debug_struct("VmHarness")
999 .field("kind", &self.kind)
1000 .finish_non_exhaustive()
1001 }
1002}
1003
1004#[derive(Debug)]
1010pub struct MockAwareClock<C: Clock + 'static> {
1011 inner: C,
1012}
1013
1014impl<C: Clock + 'static> MockAwareClock<C> {
1015 pub fn new(inner: C) -> Self {
1016 Self { inner }
1017 }
1018}
1019
1020#[async_trait]
1021impl<C: Clock + 'static> Clock for MockAwareClock<C> {
1022 fn now_utc(&self) -> OffsetDateTime {
1023 if let Some(mock) = crate::clock_mock::active_mock_clock() {
1024 return mock.now_utc();
1025 }
1026 self.inner.now_utc()
1027 }
1028
1029 fn monotonic_ms(&self) -> i64 {
1030 if let Some(mock) = crate::clock_mock::active_mock_clock() {
1031 return mock.monotonic_ms();
1032 }
1033 self.inner.monotonic_ms()
1034 }
1035
1036 async fn sleep(&self, duration: Duration) {
1037 if duration.is_zero() {
1038 return;
1039 }
1040 if let Some(mock) = crate::clock_mock::active_mock_clock() {
1041 mock.advance_std_sync(duration);
1047 return;
1048 }
1049 self.inner.sleep(duration).await;
1050 }
1051
1052 async fn sleep_until_utc(&self, deadline: OffsetDateTime) {
1053 if let Some(mock) = crate::clock_mock::active_mock_clock() {
1054 let now = mock.now_utc();
1055 if deadline > now {
1056 if let Ok(delta) = Duration::try_from(deadline - now) {
1057 mock.advance_std_sync(delta);
1058 }
1059 }
1060 return;
1061 }
1062 self.inner.sleep_until_utc(deadline).await;
1063 }
1064}
1065
1066#[cfg(test)]
1067mod tests {
1068 use super::*;
1069 use crate::secrets::SecretProvider;
1070 use async_trait::async_trait;
1071
1072 #[derive(Clone, Debug, PartialEq, Eq)]
1073 struct SecretCall {
1074 operation: &'static str,
1075 id: crate::secrets::SecretId,
1076 scope: crate::secrets::SecretScope,
1077 request_id: Option<String>,
1078 actor_subject: Option<String>,
1079 actor_kind: Option<String>,
1080 duration_ms: Option<u64>,
1081 grace_ms: Option<u64>,
1082 ttl_ms: Option<u64>,
1083 }
1084
1085 #[derive(Clone, Default)]
1086 struct RecordingSecretProvider {
1087 inner: Arc<RecordingSecretProviderInner>,
1088 }
1089
1090 #[derive(Default)]
1091 struct RecordingSecretProviderInner {
1092 versions: Mutex<BTreeMap<crate::secrets::SecretId, Vec<Vec<u8>>>>,
1093 calls: Mutex<Vec<SecretCall>>,
1094 }
1095
1096 impl RecordingSecretProvider {
1097 fn calls(&self) -> Vec<SecretCall> {
1098 self.inner
1099 .calls
1100 .lock()
1101 .expect("calls lock poisoned")
1102 .clone()
1103 }
1104
1105 fn record(
1106 &self,
1107 operation: &'static str,
1108 id: &crate::secrets::SecretId,
1109 scope: &crate::secrets::SecretScope,
1110 audit: &crate::secrets::SecretAuditContext,
1111 duration_ms: Option<u64>,
1112 grace_ms: Option<u64>,
1113 ttl_ms: Option<u64>,
1114 ) {
1115 self.inner
1116 .calls
1117 .lock()
1118 .expect("calls lock poisoned")
1119 .push(SecretCall {
1120 operation,
1121 id: id.clone(),
1122 scope: scope.clone(),
1123 request_id: audit.request_id.clone(),
1124 actor_subject: audit.actor_subject.clone(),
1125 actor_kind: audit.actor_kind.clone(),
1126 duration_ms,
1127 grace_ms,
1128 ttl_ms,
1129 });
1130 }
1131
1132 fn read_latest(
1133 &self,
1134 id: &crate::secrets::SecretId,
1135 ) -> Result<(u64, Vec<u8>), crate::secrets::SecretError> {
1136 let versions = self.inner.versions.lock().expect("versions lock poisoned");
1137 let values = versions
1138 .get(id)
1139 .filter(|values| !values.is_empty())
1140 .ok_or_else(|| crate::secrets::SecretError::NotFound {
1141 provider: self.namespace().to_string(),
1142 id: id.clone(),
1143 })?;
1144 Ok((
1145 values.len() as u64,
1146 values.last().expect("non-empty").clone(),
1147 ))
1148 }
1149
1150 fn write_version(
1151 &self,
1152 id: &crate::secrets::SecretId,
1153 value: &crate::secrets::SecretBytes,
1154 ) -> u64 {
1155 let mut versions = self.inner.versions.lock().expect("versions lock poisoned");
1156 let values = versions.entry(id.clone()).or_default();
1157 values.push(value.with_exposed(|bytes| bytes.to_vec()));
1158 values.len() as u64
1159 }
1160 }
1161
1162 fn duration_ms(duration: Duration) -> u64 {
1163 duration.as_millis().min(u128::from(u64::MAX)) as u64
1164 }
1165
1166 #[async_trait]
1167 impl crate::secrets::SecretProvider for RecordingSecretProvider {
1168 async fn get(
1169 &self,
1170 id: &crate::secrets::SecretId,
1171 ) -> Result<crate::secrets::SecretBytes, crate::secrets::SecretError> {
1172 self.read_latest(id)
1173 .map(|(_, value)| crate::secrets::SecretBytes::from(value))
1174 }
1175
1176 async fn put(
1177 &self,
1178 id: &crate::secrets::SecretId,
1179 value: crate::secrets::SecretBytes,
1180 ) -> Result<(), crate::secrets::SecretError> {
1181 self.write_version(id, &value);
1182 Ok(())
1183 }
1184
1185 async fn rotate(
1186 &self,
1187 id: &crate::secrets::SecretId,
1188 ) -> Result<crate::secrets::RotationHandle, crate::secrets::SecretError> {
1189 let (from_version, value) = self.read_latest(id)?;
1190 let to_version =
1191 self.write_version(id, &crate::secrets::SecretBytes::from(value.as_slice()));
1192 Ok(crate::secrets::RotationHandle {
1193 provider: self.namespace().to_string(),
1194 id: id
1195 .clone()
1196 .with_version(crate::secrets::SecretVersion::Exact(to_version)),
1197 from_version: Some(from_version),
1198 to_version: Some(to_version),
1199 })
1200 }
1201
1202 async fn list(
1203 &self,
1204 _prefix: &crate::secrets::SecretId,
1205 ) -> Result<Vec<crate::secrets::SecretMeta>, crate::secrets::SecretError> {
1206 Ok(Vec::new())
1207 }
1208
1209 async fn read_scoped(
1210 &self,
1211 request: crate::secrets::SecretReadRequest,
1212 ) -> Result<crate::secrets::SecretBytes, crate::secrets::SecretError> {
1213 self.record(
1214 "read",
1215 &request.id,
1216 &request.scope,
1217 &request.audit,
1218 None,
1219 None,
1220 None,
1221 );
1222 self.read_latest(&request.id)
1223 .map(|(_, value)| crate::secrets::SecretBytes::from(value))
1224 }
1225
1226 async fn write_scoped(
1227 &self,
1228 request: crate::secrets::SecretWriteRequest,
1229 ) -> Result<crate::secrets::SecretWriteReceipt, crate::secrets::SecretError> {
1230 let ttl_ms = request.options.ttl.map(duration_ms);
1231 self.record(
1232 "write",
1233 &request.id,
1234 &request.scope,
1235 &request.audit,
1236 None,
1237 None,
1238 ttl_ms,
1239 );
1240 let version = self.write_version(&request.id, &request.value);
1241 Ok(crate::secrets::SecretWriteReceipt {
1242 provider: self.namespace().to_string(),
1243 id: request
1244 .id
1245 .with_version(crate::secrets::SecretVersion::Exact(version)),
1246 scope: request.scope,
1247 version: Some(version),
1248 expires_at_unix_ms: ttl_ms.map(|ttl| 1_700_000_000_000_i64 + ttl as i64),
1249 })
1250 }
1251
1252 async fn rotate_scoped(
1253 &self,
1254 request: crate::secrets::SecretRotateRequest,
1255 ) -> Result<crate::secrets::SecretRotationReceipt, crate::secrets::SecretError> {
1256 let grace_ms = request.options.grace.map(duration_ms);
1257 let ttl_ms = request.options.ttl.map(duration_ms);
1258 self.record(
1259 "rotate",
1260 &request.id,
1261 &request.scope,
1262 &request.audit,
1263 None,
1264 grace_ms,
1265 ttl_ms,
1266 );
1267 let from_version = self
1268 .inner
1269 .versions
1270 .lock()
1271 .expect("versions lock poisoned")
1272 .get(&request.id)
1273 .map(|values| values.len() as u64);
1274 let to_version = self.write_version(&request.id, &request.value);
1275 Ok(crate::secrets::SecretRotationReceipt {
1276 provider: self.namespace().to_string(),
1277 id: request
1278 .id
1279 .with_version(crate::secrets::SecretVersion::Exact(to_version)),
1280 scope: request.scope,
1281 from_version,
1282 to_version: Some(to_version),
1283 grace_until_unix_ms: grace_ms.map(|grace| 1_700_000_000_000_i64 + grace as i64),
1284 expires_at_unix_ms: ttl_ms.map(|ttl| 1_700_000_000_000_i64 + ttl as i64),
1285 })
1286 }
1287
1288 async fn lease_scoped(
1289 &self,
1290 request: crate::secrets::SecretLeaseRequest,
1291 ) -> Result<crate::secrets::SecretLeaseGrant, crate::secrets::SecretError> {
1292 let duration = duration_ms(request.duration);
1293 self.record(
1294 "lease",
1295 &request.id,
1296 &request.scope,
1297 &request.audit,
1298 Some(duration),
1299 None,
1300 None,
1301 );
1302 let (version, value) = self.read_latest(&request.id)?;
1303 Ok(crate::secrets::SecretLeaseGrant {
1304 provider: self.namespace().to_string(),
1305 id: request
1306 .id
1307 .with_version(crate::secrets::SecretVersion::Exact(version)),
1308 scope: request.scope,
1309 lease_id: format!("lease-{version}"),
1310 value: crate::secrets::SecretBytes::from(value),
1311 expires_at_unix_ms: 1_700_000_000_000_i64 + duration as i64,
1312 })
1313 }
1314
1315 fn namespace(&self) -> &'static str {
1316 "recording"
1317 }
1318
1319 fn supports_versions(&self) -> bool {
1320 true
1321 }
1322 }
1323
1324 #[test]
1325 fn real_constructs_without_panic() {
1326 let _harness = Harness::real();
1327 }
1328
1329 #[test]
1330 fn sub_handles_share_inner_state() {
1331 let harness = Harness::real();
1332 let stdio_inner = Arc::as_ptr(harness.stdio().inner());
1333 let clock_inner = Arc::as_ptr(harness.clock().inner());
1334 assert_eq!(stdio_inner, clock_inner, "sub-handles share Arc<Inner>");
1335 }
1336
1337 #[test]
1338 fn kinds_round_trip_through_field_names() {
1339 for kind in HarnessKind::SUB_HANDLES {
1340 let field = kind.field_name().unwrap();
1341 assert_eq!(HarnessKind::from_field_name(field), Some(*kind));
1342 }
1343 assert!(HarnessKind::from_field_name("nope").is_none());
1344 assert!(HarnessKind::Root.field_name().is_none());
1345 }
1346
1347 #[test]
1348 fn vm_harness_property_access_returns_sub_handle() {
1349 let root = match Harness::real().into_vm_value() {
1350 crate::value::VmValue::Harness(h) => h,
1351 other => panic!("expected Harness variant, got {}", other.type_name()),
1352 };
1353 let stdio = root.sub_handle("stdio").expect("stdio sub-handle");
1354 assert_eq!(stdio.kind(), HarnessKind::Stdio);
1355 assert!(stdio.sub_handle("clock").is_none(), "nested access denied");
1356 assert!(root.sub_handle("not_a_field").is_none());
1357 }
1358
1359 #[test]
1360 fn test_constructor_clock_advances_under_paused_clock_advance() {
1361 let (harness, paused) = Harness::test();
1362 let clock = harness.clock();
1363 let start_wall = clock.clock().now_utc();
1364 assert_eq!(start_wall, OffsetDateTime::UNIX_EPOCH);
1365 assert_eq!(clock.clock().monotonic_ms(), 0);
1366
1367 paused.advance(Duration::from_millis(1_500));
1368 assert_eq!(clock.clock().monotonic_ms(), 1_500);
1369 let after_wall = clock.clock().now_utc();
1370 assert_eq!(after_wall - start_wall, time::Duration::milliseconds(1_500));
1371 }
1372
1373 #[test]
1374 fn with_paused_clock_pins_origin() {
1375 let origin = OffsetDateTime::from_unix_timestamp(1_700_000_000).unwrap();
1376 let (harness, paused) = Harness::with_paused_clock(origin);
1377 assert_eq!(harness.clock().clock().now_utc(), origin);
1378 paused.advance(Duration::from_mins(1));
1379 assert_eq!(
1380 harness.clock().clock().now_utc() - origin,
1381 time::Duration::seconds(60)
1382 );
1383 }
1384
1385 #[test]
1386 fn null_harness_records_deny_events_for_every_sub_handle() {
1387 let harness = Harness::null();
1388 for source in [
1389 r#"fn main(harness: Harness) { harness.stdio.println("blocked") }"#,
1390 r"fn main(harness: Harness) { harness.term.width() }",
1391 r"fn main(harness: Harness) { harness.clock.now_ms() }",
1392 r#"fn main(harness: Harness) { harness.fs.read_text("/x") }"#,
1393 r#"fn main(harness: Harness) { harness.env.get("KEY") }"#,
1394 r"fn main(harness: Harness) { harness.random.gen_u64() }",
1395 r#"fn main(harness: Harness) { harness.net.get("https://example.test") }"#,
1396 r#"fn main(harness: Harness) { harness.process.spawn_captured({cmd: "printf", args: ["x"]}) }"#,
1397 r#"fn main(harness: Harness) { harness.crypto.sha256("") }"#,
1398 r"fn main(harness: Harness) { harness.system.cpu() }",
1399 r#"fn main(harness: Harness) { harness.secrets.read("blocked") }"#,
1400 r"fn main(harness: Harness) { harness.llm.catalog() }",
1401 r"fn main(harness: Harness) { harness.tenant.id() }",
1402 r"fn main(harness: Harness) { harness.auth.subject() }",
1403 r#"fn main(harness: Harness) { harness.obs.log("blocked", "info", {}) }"#,
1404 ] {
1405 let error = run_harness_source(source, harness.clone()).expect_err("call denied");
1406 assert!(
1407 error.contains("NullHarness denied"),
1408 "unexpected deny error: {error}"
1409 );
1410 }
1411
1412 let events = harness.deny_events();
1413 let observed: Vec<_> = events
1414 .iter()
1415 .map(|event| (event.sub_handle, event.method.as_str()))
1416 .collect();
1417 assert_eq!(
1418 observed,
1419 vec![
1420 (HarnessKind::Stdio, "println"),
1421 (HarnessKind::Term, "width"),
1422 (HarnessKind::Clock, "now_ms"),
1423 (HarnessKind::Fs, "read_text"),
1424 (HarnessKind::Env, "get"),
1425 (HarnessKind::Random, "gen_u64"),
1426 (HarnessKind::Net, "get"),
1427 (HarnessKind::Process, "spawn_captured"),
1428 (HarnessKind::Crypto, "sha256"),
1429 (HarnessKind::System, "cpu"),
1430 (HarnessKind::Secrets, "read"),
1431 (HarnessKind::Llm, "catalog"),
1432 (HarnessKind::Tenant, "id"),
1433 (HarnessKind::Auth, "subject"),
1434 (HarnessKind::Obs, "log"),
1435 ]
1436 );
1437 assert_eq!(events[0].args, vec!["blocked"]);
1438 assert_eq!(events[3].args, vec!["/x"]);
1439 }
1440
1441 #[test]
1442 fn auth_sub_handle_reads_bound_principal() {
1443 use crate::harness_auth::{enter_auth_principal, AuthPrincipal};
1444 let _principal = enter_auth_principal(AuthPrincipal {
1445 subject: "k_123".to_string(),
1446 scheme: "apikey".to_string(),
1447 scopes: ["admin:dlq:write", "read:events"]
1448 .iter()
1449 .map(|scope| scope.to_string())
1450 .collect(),
1451 kind: Some("operator".to_string()),
1452 });
1453 let source = r#"
1454fn main(harness: Harness) {
1455 __io_println(harness.auth.is_authenticated())
1456 __io_println(harness.auth.subject())
1457 __io_println(harness.auth.scheme())
1458 __io_println(harness.auth.kind())
1459 __io_println(harness.auth.has_scope("admin:dlq:write"))
1460 __io_println(harness.auth.has_scope("missing:scope"))
1461 __io_println(len(harness.auth.scopes()))
1462}
1463"#;
1464 let output = run_harness_source(source, Harness::real()).expect("dispatch succeeds");
1465 assert_eq!(output, "true\nk_123\napikey\noperator\ntrue\nfalse\n2\n");
1466 }
1467
1468 #[test]
1469 fn auth_sub_handle_without_principal_reports_anonymous() {
1470 let source = r#"
1474fn main(harness: Harness) {
1475 if harness.auth.is_authenticated() { __io_println("auth") } else { __io_println("anon") }
1476 __io_println(harness.auth.has_scope("x"))
1477 __io_println(len(harness.auth.scopes()))
1478}
1479"#;
1480 let output = run_harness_source(source, Harness::real()).expect("dispatch succeeds");
1481 assert_eq!(output, "anon\nfalse\n0\n");
1482
1483 let error = run_harness_source(
1484 r"fn main(harness: Harness) { harness.auth.subject() }",
1485 Harness::real(),
1486 )
1487 .expect_err("subject() requires a bound principal");
1488 assert!(
1489 error.contains("no principal bound"),
1490 "unexpected error: {error}"
1491 );
1492 }
1493
1494 #[test]
1495 fn secrets_sub_handle_uses_provider_scope_and_audit_context() {
1496 use crate::harness_auth::{enter_auth_principal, AuthPrincipal};
1497 use crate::harness_tenant::enter_tenant;
1498 use crate::observability::request_id::enter_request_id;
1499
1500 let provider = RecordingSecretProvider::default();
1501 let harness = Harness::real().with_secret_provider(Arc::new(provider.clone()));
1502 let _tenant = enter_tenant(crate::TenantId::new("tenant-a"));
1503 let _request = enter_request_id("req-499");
1504 let _principal = enter_auth_principal(AuthPrincipal {
1505 subject: "api-key-1".to_string(),
1506 scheme: "apikey".to_string(),
1507 scopes: ["secrets:read", "secrets:write"]
1508 .iter()
1509 .map(|scope| scope.to_string())
1510 .collect(),
1511 kind: Some("tenant_api_key".to_string()),
1512 });
1513
1514 let source = r#"
1515fn main(harness: Harness) {
1516 let scope = {kind: "workspace", id: "workspace-a"}
1517 let written = harness.secrets.write("github.token", "v1", scope, 5000)
1518 __io_println(written.provider)
1519 __io_println(written.scope.kind)
1520 __io_println(written.scope.id)
1521 __io_println(written.id.namespace)
1522 __io_println(written.version)
1523 __io_println(harness.secrets.read("github.token", scope))
1524 let rotated = harness.secrets.rotate("github.token", { -> "v2" }, scope, {grace_ms: 250, ttl_ms: 7500})
1525 __io_println(rotated.from_version)
1526 __io_println(rotated.to_version)
1527 let grant = harness.secrets.lease("github.token", 1000, scope)
1528 __io_println(grant.value)
1529 __io_println(grant.scope.id)
1530}
1531"#;
1532 let output = run_harness_source(source, harness).expect("dispatch succeeds");
1533 assert_eq!(
1534 output,
1535 "recording\nworkspace\nworkspace-a\nharn.workspace.workspace-a\n1\nv1\n1\n2\nv2\nworkspace-a\n"
1536 );
1537
1538 let calls = provider.calls();
1539 assert_eq!(
1540 calls.iter().map(|call| call.operation).collect::<Vec<_>>(),
1541 vec!["write", "read", "rotate", "lease"]
1542 );
1543 for call in &calls {
1544 assert_eq!(
1545 call.scope,
1546 crate::secrets::SecretScope::workspace("workspace-a")
1547 );
1548 assert_eq!(call.request_id.as_deref(), Some("req-499"));
1549 assert_eq!(call.actor_subject.as_deref(), Some("api-key-1"));
1550 assert_eq!(call.actor_kind.as_deref(), Some("tenant_api_key"));
1551 }
1552 assert_eq!(calls[0].ttl_ms, Some(5_000));
1553 assert_eq!(calls[2].grace_ms, Some(250));
1554 assert_eq!(calls[2].ttl_ms, Some(7_500));
1555 assert_eq!(calls[3].duration_ms, Some(1_000));
1556 }
1557
1558 #[test]
1559 fn mock_harness_replays_canned_responses_and_records_calls() {
1560 let harness = Harness::mock()
1561 .clock_at_unix_ms(1_700_000_000_000)
1562 .env("KEY", "value")
1563 .fs_read("/x", b"data".to_vec())
1564 .random_u64(42)
1565 .net_get("https://example.test", "body")
1566 .build();
1567
1568 let output = run_harness_source(
1569 r#"
1570fn main(harness: Harness) {
1571 harness.stdio.print("partial ")
1572 harness.stdio.println("line")
1573 __io_println(harness.term.width())
1574 __io_println(harness.term.height())
1575 __io_println(harness.clock.now_ms())
1576 harness.clock.sleep_ms(250)
1577 __io_println(harness.clock.now_ms())
1578 __io_println(harness.clock.monotonic_ms())
1579 __io_println(harness.env.get("KEY"))
1580 __io_println(harness.fs.read_text("/x"))
1581 __io_println(harness.fs.exists("/missing"))
1582 __io_println(harness.random.gen_u64())
1583 __io_println(harness.net.get("https://example.test"))
1584 __io_println(harness.crypto.sha256(""))
1585 __io_println(len(harness.llm.catalog()) > 0)
1586}
1587"#,
1588 harness.clone(),
1589 )
1590 .expect("mock harness run succeeds");
1591
1592 assert_eq!(harness.captured_stdio(), "partial line\n");
1593 assert_eq!(
1594 output,
1595 "80\n24\n1700000000000\n1700000000250\n250\nvalue\ndata\nfalse\n42\nbody\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\ntrue\n"
1596 );
1597 let observed: Vec<_> = harness
1598 .calls()
1599 .into_iter()
1600 .map(|call| (call.sub_handle, call.method))
1601 .collect();
1602 assert_eq!(
1603 observed,
1604 vec![
1605 (HarnessKind::Stdio, "print".to_string()),
1606 (HarnessKind::Stdio, "println".to_string()),
1607 (HarnessKind::Term, "width".to_string()),
1608 (HarnessKind::Term, "height".to_string()),
1609 (HarnessKind::Clock, "now_ms".to_string()),
1610 (HarnessKind::Clock, "sleep_ms".to_string()),
1611 (HarnessKind::Clock, "now_ms".to_string()),
1612 (HarnessKind::Clock, "monotonic_ms".to_string()),
1613 (HarnessKind::Env, "get".to_string()),
1614 (HarnessKind::Fs, "read_text".to_string()),
1615 (HarnessKind::Fs, "exists".to_string()),
1616 (HarnessKind::Random, "gen_u64".to_string()),
1617 (HarnessKind::Net, "get".to_string()),
1618 (HarnessKind::Crypto, "sha256".to_string()),
1619 (HarnessKind::Llm, "catalog".to_string()),
1620 ]
1621 );
1622 }
1623
1624 #[test]
1625 fn mock_harness_records_repeated_cached_harness_method_calls() {
1626 let harness = Harness::mock().env("KEY", "value").build();
1627
1628 run_harness_source(
1629 r#"
1630fn main(harness: Harness) {
1631 var i = 0
1632 while i < 3 {
1633 let _ = harness.clock.elapsed()
1634 let value = harness.env.get_or("KEY", "")
1635 harness.stdio.println(value)
1636 i = i + 1
1637 }
1638}
1639"#,
1640 harness.clone(),
1641 )
1642 .expect("mock harness run succeeds");
1643
1644 assert_eq!(harness.captured_stdio(), "value\nvalue\nvalue\n");
1645 let observed: Vec<_> = harness
1646 .calls()
1647 .into_iter()
1648 .map(|call| (call.sub_handle, call.method))
1649 .collect();
1650 assert_eq!(
1651 observed,
1652 vec![
1653 (HarnessKind::Clock, "elapsed".to_string()),
1654 (HarnessKind::Env, "get_or".to_string()),
1655 (HarnessKind::Stdio, "println".to_string()),
1656 (HarnessKind::Clock, "elapsed".to_string()),
1657 (HarnessKind::Env, "get_or".to_string()),
1658 (HarnessKind::Stdio, "println".to_string()),
1659 (HarnessKind::Clock, "elapsed".to_string()),
1660 (HarnessKind::Env, "get_or".to_string()),
1661 (HarnessKind::Stdio, "println".to_string()),
1662 ]
1663 );
1664 }
1665
1666 #[test]
1667 fn mock_harness_replays_random_values_fifo() {
1668 let harness = Harness::mock()
1669 .random_u64(7)
1670 .random_u64(11)
1671 .random_u64(u64::MAX)
1672 .build();
1673
1674 let output = run_harness_source(
1675 r"
1676fn main(harness: Harness) {
1677 __io_println(harness.random.gen_u64())
1678 __io_println(harness.random.gen_u64())
1679 __io_println(harness.random.gen_u64())
1680}
1681",
1682 harness,
1683 )
1684 .expect("mock random succeeds");
1685
1686 assert_eq!(output, "7\n11\n9223372036854775807\n");
1687 }
1688
1689 #[test]
1690 fn mock_harness_reports_missing_canned_responses() {
1691 let cases = [
1692 (
1693 r#"fn main(harness: Harness) { harness.fs.read_text("/missing") }"#,
1694 "MockHarness has no fs_read response for /missing",
1695 ),
1696 (
1697 r"fn main(harness: Harness) { harness.random.gen_u64() }",
1698 "MockHarness has no random_u64 response",
1699 ),
1700 (
1701 r#"fn main(harness: Harness) { harness.net.get("https://missing.test") }"#,
1702 "MockHarness has no net_get response for https://missing.test",
1703 ),
1704 (
1705 r#"fn main(harness: Harness) { harness.process.spawn_captured({cmd: "printf", args: ["x"]}) }"#,
1706 "MockHarness has no process spawn response",
1707 ),
1708 ];
1709
1710 for (source, expected) in cases {
1711 let error = run_harness_source(source, Harness::mock().build())
1712 .expect_err("missing mock response fails");
1713 assert!(
1714 error.contains(expected),
1715 "expected `{expected}` in `{error}`"
1716 );
1717 }
1718 }
1719
1720 #[test]
1721 fn mock_harness_records_failed_calls() {
1722 let harness = Harness::mock().build();
1723 let error = run_harness_source(
1724 r#"fn main(harness: Harness) { harness.net.get("https://missing.test") }"#,
1725 harness.clone(),
1726 )
1727 .expect_err("missing mock response fails");
1728
1729 assert!(error.contains("MockHarness has no net_get response"));
1730 assert_eq!(
1731 harness.calls(),
1732 vec![HarnessCall {
1733 sub_handle: HarnessKind::Net,
1734 method: "get".to_string(),
1735 args: vec!["https://missing.test".to_string()],
1736 }]
1737 );
1738 }
1739
1740 #[test]
1741 fn mock_harness_captures_stderr_separately_from_stdout() {
1742 let harness = Harness::mock().build();
1743 run_harness_source(
1744 r#"
1745fn main(harness: Harness) {
1746 harness.stdio.println("stdout line")
1747 harness.stdio.eprint("err ")
1748 harness.stdio.eprintln("trail")
1749}
1750"#,
1751 harness.clone(),
1752 )
1753 .expect("stderr capture run succeeds");
1754 assert_eq!(harness.captured_stdio(), "stdout line\n");
1755 assert_eq!(harness.captured_stderr(), "err trail\n");
1756 }
1757
1758 #[test]
1759 fn mock_harness_replays_stdin_lines_for_read_and_prompt() {
1760 let harness = Harness::mock()
1761 .stdin_line("first")
1762 .stdin_line("second")
1763 .build();
1764 let output = run_harness_source(
1765 r#"
1766fn main(harness: Harness) {
1767 harness.stdio.println(harness.stdio.read_line())
1768 harness.stdio.println(harness.stdio.prompt("answer: "))
1769 let eof = harness.stdio.read_line({trim: false})
1770 harness.stdio.println(eof.status)
1771}
1772"#,
1773 harness.clone(),
1774 )
1775 .expect("stdin replay succeeds");
1776 assert_eq!(output, "");
1778 assert_eq!(harness.captured_stdio(), "first\nanswer: second\neof\n");
1779 }
1780
1781 #[test]
1782 fn mock_harness_replays_password_input_without_stdout_echo() {
1783 let harness = Harness::mock().stdin_line("secret").build();
1784 let output = run_harness_source(
1785 r#"
1786fn main(harness: Harness) {
1787 __io_println(harness.term.read_password("password: "))
1788}
1789"#,
1790 harness.clone(),
1791 )
1792 .expect("stdin replay succeeds");
1793
1794 assert_eq!(output, "secret\n");
1795 assert_eq!(harness.captured_stdio(), "");
1796 assert_eq!(harness.captured_stderr(), "password: ");
1797 assert_eq!(
1798 harness.calls(),
1799 vec![HarnessCall {
1800 sub_handle: HarnessKind::Term,
1801 method: "read_password".to_string(),
1802 args: vec!["password: ".to_string()],
1803 }]
1804 );
1805 }
1806
1807 #[test]
1808 fn mock_harness_rejects_wrong_argument_types() {
1809 let error = run_harness_source(
1810 r"fn main(harness: Harness) { harness.fs.read_text(1) }",
1811 Harness::mock().build(),
1812 )
1813 .expect_err("wrong argument type fails");
1814
1815 let runtime_rejection =
1825 error.contains("HarnessFs.read_text expects string argument 1, got int");
1826 let static_rejection = error.contains("argument 1 `path`: expected string, found int");
1827 assert!(
1828 runtime_rejection || static_rejection,
1829 "expected a string/int type rejection for read_text, got: {error}"
1830 );
1831 }
1832
1833 #[test]
1834 fn real_harness_fs_write_outside_workspace_roots_surfaces_cap_201() {
1835 use crate::orchestration::{
1836 clear_execution_policy_stacks, push_execution_policy, CapabilityPolicy, SandboxProfile,
1837 };
1838 clear_execution_policy_stacks();
1839 let temp = tempfile::tempdir().unwrap();
1840 let policy = CapabilityPolicy {
1841 sandbox_profile: SandboxProfile::Worktree,
1842 workspace_roots: vec![temp.path().to_string_lossy().into_owned()],
1843 ..CapabilityPolicy::default()
1844 };
1845 push_execution_policy(policy);
1846 let outside = std::env::temp_dir().join("harn_e4_4_cap_201_outside.txt");
1847 let source = format!(
1848 r#"fn main(harness: Harness) {{ harness.fs.write_text("{}", "x") }}"#,
1849 outside.to_string_lossy().replace('\\', "/"),
1850 );
1851 let error = run_harness_source(&source, Harness::real())
1852 .expect_err("write outside workspace_roots must reject");
1853 clear_execution_policy_stacks();
1854 assert!(
1855 error.contains("HARN-CAP-201"),
1856 "expected HARN-CAP-201 prefix, got: {error}"
1857 );
1858 assert!(
1859 error.contains("sandbox violation"),
1860 "deny should keep the underlying sandbox-rejection message, got: {error}"
1861 );
1862 }
1863
1864 fn run_harness_source(source: &str, harness: Harness) -> Result<String, String> {
1865 let rt = tokio::runtime::Builder::new_current_thread()
1866 .enable_all()
1867 .build()
1868 .unwrap();
1869 rt.block_on(async move {
1870 let local = tokio::task::LocalSet::new();
1871 local
1872 .run_until(async move {
1873 let chunk = crate::compile_source(source)?;
1874 let mut vm = crate::Vm::new();
1875 crate::stdlib::register_vm_stdlib(&mut vm);
1876 vm.set_harness(harness);
1877 vm.execute(&chunk)
1878 .await
1879 .map_err(|error| error.to_string())?;
1880 Ok(vm.output().to_string())
1881 })
1882 .await
1883 })
1884 }
1885}