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