Skip to main content

harn_hostlib/
verdict.rs

1//! `harness.verdict.*` issuance validator — the host authority that decides
2//! whether a real host-executed test run earns a positive verdict.
3//!
4//! `issue(result_handle)` resolves the opaque handle of a real `run_test`
5//! execution to the host-owned execution
6//! record, and returns a PLAIN dict
7//! `{outcome, passed, total, artifact_id, artifact_hash, subject?, detail}`
8//! built from the disposition the host FROZE at execution time. The VM harness
9//! method (`crate::vm::methods::harness`) mints the opaque, non-serializable
10//! `VerdictReceipt` from this dict on a `"pass"` outcome — the receipt is never
11//! constructed here, so it never crosses the hostlib response-schema boundary.
12//!
13//! A caller cannot assert a pass, choose the positive-authority command, or —
14//! the class the earlier path-based design
15//! missed — a caller cannot FABRICATE the evidence either: issuance reads no
16//! caller-supplied filesystem bytes. It consumes only a `result_handle` that
17//! resolves in the host-owned execution store, whose disposition the host
18//! computed from bytes IT captured. A file written with `harness.fs.write_text`
19//! has no handle; a hand-authored handle string resolves to nothing; a mutated
20//! artifact cannot change the frozen summary. Provenance, not just type, is
21//! unforgeable.
22
23use harn_vm::value::DictMap;
24use harn_vm::{VmDictExt, VmValue};
25
26use crate::error::HostlibError;
27use crate::registry::{BuiltinRegistry, HostlibCapability};
28use crate::tools::inspect_test_results::{get_run, AuthorizedTestPlanIdentity};
29use crate::tools::payload::{optional_string, require_dict_arg, require_string};
30
31/// The registered builtin name; must match `harness_verdict_ambient("issue")`
32/// in `harn_parser::harness_methods`.
33pub const ISSUE_BUILTIN: &str = "__harness_verdict_issue";
34
35/// Hostlib capability exposing the verdict issuance validator.
36pub struct VerdictCapability;
37
38impl HostlibCapability for VerdictCapability {
39    fn module_name(&self) -> &'static str {
40        "verdict"
41    }
42
43    fn register_builtins(&self, registry: &mut BuiltinRegistry) {
44        // Resolves a host-owned execution handle (never a caller path), but stays
45        // on the deterministic-tools gate: issuing a verdict is part of the same
46        // opt-in tool surface as the `run_test` that produced the handle.
47        registry.register_fn("verdict", ISSUE_BUILTIN, "issue", verdict_issue_builtin);
48    }
49}
50
51fn verdict_issue_builtin(args: &[VmValue]) -> Result<VmValue, HostlibError> {
52    let map = require_dict_arg(ISSUE_BUILTIN, args)?;
53    let result_handle = require_string(ISSUE_BUILTIN, &map, "result_handle")?;
54    let subject = optional_string(ISSUE_BUILTIN, &map, "subject")?;
55
56    // GATE 1 — handle resolves. The store's sole writer is a real `run_test`
57    // spawn (`store_run`), so a caller-authored file, a fabricated handle
58    // string, or a mock result never resolves here.
59    let Some(run) = get_run(&result_handle) else {
60        return Ok(outcome_dict(
61            "unavailable",
62            0,
63            0,
64            &result_handle,
65            "",
66            None,
67            None,
68            subject.as_deref(),
69            "no host execution recorded under this result handle; a positive verdict requires a real run_test execution",
70        ));
71    };
72    let owner = run.execution_scope.as_deref();
73
74    // GATE 2 — EXECUTION SCOPE (fail-closed). The run that PRODUCED this evidence
75    // must still be the active owner. No active scope, or an active scope that
76    // differs from the one recorded at execution time, cannot mint — this is
77    // what stops an old green handle from blessing a later, different run
78    // (the cross-run replay class). There is no sentinel fallback.
79    let active = harn_vm::current_execution_scope();
80    let scope_ok = matches!((&active, &run.execution_scope), (Some(a), Some(o)) if a == o);
81    if !scope_ok {
82        return Ok(outcome_dict(
83            "unavailable",
84            0,
85            0,
86            &result_handle,
87            &run.content_hash,
88            owner,
89            run.artifacts.authorized_test_plan.as_ref(),
90            subject.as_deref(),
91            "result handle belongs to a different or ended execution; a positive verdict must be issued within the run that produced it",
92        ));
93    }
94
95    // GATE 3 — the host must have OBSERVED the process succeed. A nonzero exit
96    // cannot mint a pass even when the captured output looks passing (e.g. a
97    // build that failed after echoing stale green test text).
98    if run.artifacts.exit_code != 0 {
99        return Ok(outcome_dict(
100            "fail",
101            0,
102            0,
103            &result_handle,
104            &run.content_hash,
105            owner,
106            run.artifacts.authorized_test_plan.as_ref(),
107            subject.as_deref(),
108            "host execution exited nonzero",
109        ));
110    }
111
112    // The disposition is the host's OWN, frozen at execution time from bytes it
113    // captured — never re-derived here from mutable input. `None` means the real
114    // execution produced nothing a parser recognized: honestly unavailable.
115    let Some(summary) = run.summary else {
116        return Ok(outcome_dict(
117            "unavailable",
118            0,
119            0,
120            &result_handle,
121            &run.content_hash,
122            owner,
123            run.artifacts.authorized_test_plan.as_ref(),
124            subject.as_deref(),
125            "host execution produced no recognized test results",
126        ));
127    };
128    let total = summary.passed + summary.failed + summary.skipped;
129    if summary.failed > 0 {
130        return Ok(outcome_dict(
131            "fail",
132            summary.passed,
133            total,
134            &result_handle,
135            &run.content_hash,
136            owner,
137            run.artifacts.authorized_test_plan.as_ref(),
138            subject.as_deref(),
139            "host execution reported failing tests",
140        ));
141    }
142    if summary.passed == 0 {
143        return Ok(outcome_dict(
144            "unavailable",
145            0,
146            total,
147            &result_handle,
148            &run.content_hash,
149            owner,
150            run.artifacts.authorized_test_plan.as_ref(),
151            subject.as_deref(),
152            "host execution reported zero passing tests",
153        ));
154    }
155
156    // GATE 4 — positive evidence must come from the plan selected by the host's
157    // workspace discovery boundary. Explicit caller argv is still useful for
158    // diagnostics and may produce a red verdict, but passing-looking output
159    // from an arbitrary command is not test authority and cannot mint PASS.
160    let Some(plan) = run.artifacts.authorized_test_plan.as_ref() else {
161        return Ok(outcome_dict(
162            "unavailable",
163            0,
164            total,
165            &result_handle,
166            &run.content_hash,
167            owner,
168            None,
169            subject.as_deref(),
170            "execution was not produced by a host-discovered test plan bound to the active workspace",
171        ));
172    };
173    Ok(outcome_dict(
174        "pass",
175        summary.passed,
176        total,
177        &result_handle,
178        &run.content_hash,
179        owner,
180        Some(plan),
181        subject.as_deref(),
182        "",
183    ))
184}
185
186#[allow(clippy::too_many_arguments)]
187fn outcome_dict(
188    outcome: &str,
189    passed: u32,
190    total: u32,
191    artifact_id: &str,
192    artifact_hash: &str,
193    execution_scope: Option<&str>,
194    plan: Option<&AuthorizedTestPlanIdentity>,
195    subject: Option<&str>,
196    detail: &str,
197) -> VmValue {
198    let mut map = DictMap::new();
199    map.put_str("outcome", outcome);
200    map.put_int("passed", i64::from(passed));
201    map.put_int("total", i64::from(total));
202    map.put_str("artifact_id", artifact_id);
203    map.put_str("artifact_hash", artifact_hash);
204    // The producing-execution owner the receipt binds to (VM-side mint reads it).
205    map.put_opt_str("execution_scope", execution_scope);
206    map.put_opt_str("plan_id", plan.map(|identity| identity.plan_id.as_str()));
207    map.put_opt_str(
208        "workspace_hash",
209        plan.map(|identity| identity.workspace_hash.as_str()),
210    );
211    map.put_opt_str(
212        "command_hash",
213        plan.map(|identity| identity.command_hash.as_str()),
214    );
215    map.put_opt_str("subject", subject);
216    map.put_str("detail", detail);
217    VmValue::dict_map(map)
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223    use crate::process::{install_spawner, MockProcessConfig, MockSpawner, SpawnerGuard};
224    use crate::tools::inspect_test_results::{store_run, RawArtifacts, TestSummaryData};
225    use harn_lexer::Lexer;
226    use harn_parser::Parser;
227    use harn_vm::orchestration::RunExecutionRecord;
228    use harn_vm::{enter_execution_scope, mint_execution_scope, register_vm_stdlib, Compiler, Vm};
229    use std::path::Path;
230    use std::sync::Arc;
231    use tempfile::TempDir;
232
233    const PASSING_CARGO_OUTPUT: &str = "running 1 test\n\
234test tests::green ... ok\n\n\
235test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out\n";
236
237    /// Build the `issue` arg — a single dict `{result_handle}` — the way the VM
238    /// dispatcher passes it.
239    fn issue_arg(result_handle: &str) -> Vec<VmValue> {
240        let mut map = DictMap::new();
241        map.put_str("result_handle", result_handle);
242        vec![VmValue::dict_map(map)]
243    }
244
245    fn outcome_of(v: &VmValue) -> String {
246        v.as_dict()
247            .and_then(|d| d.get("outcome"))
248            .map(|o| o.as_str_cow().into_owned())
249            .unwrap_or_default()
250    }
251
252    fn artifacts(stdout: &str, exit_code: i32) -> RawArtifacts {
253        RawArtifacts {
254            stdout: stdout.to_string(),
255            stderr: String::new(),
256            exit_code,
257            junit_path: None,
258            ecosystem: None,
259            argv: Vec::new(),
260            authorized_test_plan: None,
261        }
262    }
263
264    fn cargo_fixture(parent: &Path, name: &str) -> TempDir {
265        let dir = tempfile::Builder::new()
266            .prefix(name)
267            .tempdir_in(parent)
268            .expect("fixture dir");
269        std::fs::write(
270            dir.path().join("Cargo.toml"),
271            format!(
272                "[package]\nname = \"{name}\"\nversion = \"0.0.0\"\nedition = \"2021\"\n\n[lib]\npath = \"lib.rs\"\n"
273            ),
274        )
275        .expect("fixture manifest");
276        std::fs::write(
277            dir.path().join("lib.rs"),
278            "#[cfg(test)]\nmod tests {\n    #[test]\n    fn green() {}\n}\n",
279        )
280        .expect("fixture source");
281        dir
282    }
283
284    fn install_passing_cargo_spawner(count: usize) -> (Arc<MockSpawner>, SpawnerGuard) {
285        let spawner = Arc::new(MockSpawner::new());
286        for offset in 0..count {
287            let mut config = MockProcessConfig::with_stdout(0, PASSING_CARGO_OUTPUT);
288            config.pid += u32::try_from(offset).expect("fixture spawn count fits in u32");
289            let _controller = spawner.enqueue(config);
290        }
291        let guard = install_spawner(spawner.clone());
292        (spawner, guard)
293    }
294
295    fn assert_cargo_plan(spawner: &MockSpawner, count: usize, filter: Option<&str>) {
296        let expected_args = match filter {
297            Some(filter) => vec!["test".to_string(), filter.to_string()],
298            None => vec!["test".to_string()],
299        };
300        let captured = spawner.captured();
301        assert_eq!(captured.len(), count, "every expected plan must execute");
302        for spec in captured {
303            assert_eq!(spec.program, "cargo");
304            assert_eq!(spec.args, expected_args);
305        }
306    }
307
308    fn run_test_in(cwd: &Path, argv: Option<Vec<&str>>, filter: Option<&str>) -> VmValue {
309        let mut req = DictMap::new();
310        req.put_str("cwd", cwd.to_string_lossy());
311        if let Some(argv) = argv {
312            req.put(
313                "argv",
314                VmValue::List(Arc::new(argv.into_iter().map(VmValue::string).collect())),
315            );
316        }
317        req.put_opt_str("filter", filter);
318        crate::tools::run_test::handle(&[VmValue::dict_map(req)]).expect("run_test ok")
319    }
320
321    fn result_handle(result: &VmValue) -> String {
322        result
323            .as_dict()
324            .and_then(|dict| dict.get("result_handle"))
325            .map(|handle| handle.as_str_cow().into_owned())
326            .expect("run_test returns a result_handle")
327    }
328
329    fn compile(source: &str) -> harn_vm::Chunk {
330        let mut lexer = Lexer::new(source);
331        let tokens = lexer.tokenize().expect("tokenize");
332        let mut parser = Parser::new(tokens);
333        let program = parser.parse().expect("parse");
334        Compiler::new().compile(&program).expect("compile")
335    }
336
337    fn harn_string(value: &Path) -> String {
338        value
339            .to_string_lossy()
340            .replace('\\', "\\\\")
341            .replace('"', "\\\"")
342    }
343
344    fn overlap_vm(barrier: Arc<tokio::sync::Barrier>) -> Vm {
345        let mut vm = Vm::new();
346        register_vm_stdlib(&mut vm);
347        let _ = crate::install_default(&mut vm);
348        vm.register_async_builtin("__test_overlap", move |_ctx, _args| {
349            let barrier = barrier.clone();
350            async move {
351                barrier.wait().await;
352                Ok(VmValue::Nil)
353            }
354        });
355        vm
356    }
357
358    /// Record a run under `scope`, dropping the scope guard afterward — the owner
359    /// is frozen into the `StoredRun` at record time. Used to stage the
360    /// cross-scope / no-active-scope NEGATIVES (the POSITIVE below deliberately
361    /// drives the real execution path instead).
362    fn record_in_scope(
363        scope: &Arc<str>,
364        arts: RawArtifacts,
365        summary: Option<TestSummaryData>,
366    ) -> String {
367        let _g = enter_execution_scope(scope.clone());
368        store_run(arts, summary)
369    }
370
371    /// EXECUTED NEGATIVE: arbitrary caller argv can print perfect passing test
372    /// prose, but it has no host-discovered plan identity and cannot mint PASS.
373    #[cfg(unix)]
374    #[test]
375    fn arbitrary_passing_command_cannot_issue_pass() {
376        let _scope = enter_execution_scope(mint_execution_scope());
377        let cwd = std::env::current_dir().expect("cwd");
378        let res = run_test_in(
379            &cwd,
380            Some(vec![
381                "sh",
382                "-c",
383                "printf 'running 1 test\\ntest a ... ok\\n\\ntest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out\\n'",
384            ]),
385            None,
386        );
387        let handle = result_handle(&res);
388        let out = verdict_issue_builtin(&issue_arg(&handle)).expect("issue ok");
389        assert_eq!(outcome_of(&out), "unavailable");
390    }
391
392    /// EXECUTED POSITIVE: omitting argv delegates plan selection to the host's
393    /// workspace discovery boundary. The exact discovered Cargo plan executes,
394    /// records measured green results, and can issue inside its owning run.
395    #[test]
396    fn host_discovered_test_plan_issues_pass() {
397        let workspace = TempDir::new().expect("workspace");
398        let fixture = cargo_fixture(workspace.path(), "verdict_positive");
399        let (spawner, _spawner_guard) = install_passing_cargo_spawner(1);
400        harn_vm::stdlib::process::set_thread_execution_context(Some(RunExecutionRecord {
401            cwd: Some(fixture.path().to_string_lossy().into_owned()),
402            project_root: Some(fixture.path().to_string_lossy().into_owned()),
403            ..RunExecutionRecord::default()
404        }));
405        let _scope = enter_execution_scope(mint_execution_scope());
406        let res = run_test_in(fixture.path(), None, None);
407        let out = verdict_issue_builtin(&issue_arg(&result_handle(&res))).expect("issue ok");
408        harn_vm::stdlib::process::set_thread_execution_context(None);
409        assert_cargo_plan(&spawner, 1, None);
410        assert_eq!(outcome_of(&out), "pass");
411    }
412
413    /// EXECUTED NEGATIVE: a caller-selected filter narrows the discovered plan
414    /// to a partial diagnostic run, so even a green result is non-authoritative.
415    #[test]
416    fn filtered_discovered_test_plan_cannot_issue_pass() {
417        let workspace = TempDir::new().expect("workspace");
418        let fixture = cargo_fixture(workspace.path(), "verdict_filtered");
419        let (spawner, _spawner_guard) = install_passing_cargo_spawner(1);
420        harn_vm::stdlib::process::set_thread_execution_context(Some(RunExecutionRecord {
421            cwd: Some(fixture.path().to_string_lossy().into_owned()),
422            project_root: Some(fixture.path().to_string_lossy().into_owned()),
423            ..RunExecutionRecord::default()
424        }));
425        let _scope = enter_execution_scope(mint_execution_scope());
426        let res = run_test_in(fixture.path(), None, Some("green"));
427        let out = verdict_issue_builtin(&issue_arg(&result_handle(&res))).expect("issue ok");
428        harn_vm::stdlib::process::set_thread_execution_context(None);
429        assert_cargo_plan(&spawner, 1, Some("green"));
430        assert_eq!(outcome_of(&out), "unavailable");
431    }
432
433    /// Deterministic top-level overlap oracle. Both VMs execute and record their
434    /// own host-discovered plan, then rendezvous while both top-level futures are
435    /// pending on one thread. Per-poll ambient isolation must restore each VM's
436    /// owner before issuance, so both receipts mint under their producing run.
437    #[tokio::test(flavor = "current_thread")]
438    async fn overlapping_top_level_vms_issue_only_their_own_receipts() {
439        tokio::task::LocalSet::new()
440            .run_until(async {
441                let workspace = TempDir::new().expect("workspace");
442                let fixture = cargo_fixture(workspace.path(), "verdict_overlap");
443                let (spawner, _spawner_guard) = install_passing_cargo_spawner(2);
444                harn_vm::stdlib::process::set_thread_execution_context(Some(RunExecutionRecord {
445                    cwd: Some(fixture.path().to_string_lossy().into_owned()),
446                    project_root: Some(fixture.path().to_string_lossy().into_owned()),
447                    ..RunExecutionRecord::default()
448                }));
449
450                let source = |cwd: &Path| {
451                    format!(
452                        r#"
453import {{ verdict_disposition, verdict_from_run }} from "std/agent/verdict"
454
455pipeline default(harness: Harness, _task) {{
456  const run = harness.tools.run_test({{cwd: "{}", timeout_ms: 120000}})
457  __test_overlap()
458  return verdict_disposition(verdict_from_run(harness.verdict, run))
459}}
460"#,
461                        harn_string(cwd)
462                    )
463                };
464                let chunk_a = compile(&source(fixture.path()));
465                let chunk_b = compile(&source(fixture.path()));
466                let barrier = Arc::new(tokio::sync::Barrier::new(2));
467                let mut vm_a = overlap_vm(barrier.clone());
468                let mut vm_b = overlap_vm(barrier);
469
470                let (result_a, result_b) =
471                    tokio::join!(vm_a.execute(&chunk_a), vm_b.execute(&chunk_b));
472                harn_vm::stdlib::process::set_thread_execution_context(None);
473                assert_cargo_plan(&spawner, 2, None);
474                assert_eq!(result_a.expect("vm A").display(), "pass");
475                assert_eq!(result_b.expect("vm B").display(), "pass");
476            })
477            .await;
478    }
479
480    /// NEGATIVE: a handle the host never recorded — the shape of a caller-authored
481    /// "evidence" file or a fabricated handle string — can NEVER reach a pass.
482    #[test]
483    fn unrecorded_handle_is_never_a_pass() {
484        let _scope = enter_execution_scope(mint_execution_scope());
485        let out = verdict_issue_builtin(&issue_arg("htr-deadbeef-999999")).expect("issue ok");
486        assert_eq!(outcome_of(&out), "unavailable");
487    }
488
489    /// NEGATIVE (cross-run replay): a green handle recorded in run A cannot be
490    /// issued while run B is active — the owner does not match the active scope.
491    #[test]
492    fn cross_scope_handle_is_rejected() {
493        let scope_a: Arc<str> = mint_execution_scope();
494        let handle = record_in_scope(
495            &scope_a,
496            artifacts("test result: ok. 2 passed; 0 failed", 0),
497            Some(TestSummaryData {
498                passed: 2,
499                failed: 0,
500                skipped: 0,
501            }),
502        );
503        let _scope_b = enter_execution_scope(mint_execution_scope());
504        let out = verdict_issue_builtin(&issue_arg(&handle)).expect("issue ok");
505        assert_eq!(outcome_of(&out), "unavailable");
506    }
507
508    /// NEGATIVE (fail-closed): a green handle cannot be issued with NO active
509    /// scope — issuance outside any owning run is refused.
510    #[test]
511    fn no_active_scope_is_rejected() {
512        let scope_a: Arc<str> = mint_execution_scope();
513        let handle = record_in_scope(
514            &scope_a,
515            artifacts("test result: ok. 1 passed; 0 failed", 0),
516            Some(TestSummaryData {
517                passed: 1,
518                failed: 0,
519                skipped: 0,
520            }),
521        );
522        // No scope entered here.
523        let out = verdict_issue_builtin(&issue_arg(&handle)).expect("issue ok");
524        assert_eq!(outcome_of(&out), "unavailable");
525    }
526
527    /// NEGATIVE (exit gate): a nonzero exit with passing-looking output cannot
528    /// mint a pass, even in-scope with a green-looking summary.
529    #[test]
530    fn nonzero_exit_with_passing_text_is_not_a_pass() {
531        let _g = enter_execution_scope(mint_execution_scope());
532        let handle = store_run(
533            artifacts("test result: ok. 1 passed; 0 failed", 1),
534            Some(TestSummaryData {
535                passed: 1,
536                failed: 0,
537                skipped: 0,
538            }),
539        );
540        let out = verdict_issue_builtin(&issue_arg(&handle)).expect("issue ok");
541        assert_eq!(outcome_of(&out), "fail");
542    }
543
544    /// A real execution the host observed RED is a fail — read from the host's
545    /// frozen summary, not asserted by the caller.
546    #[test]
547    fn red_host_execution_issues_fail() {
548        let _g = enter_execution_scope(mint_execution_scope());
549        let handle = store_run(
550            artifacts("test result: FAILED. 1 passed; 1 failed", 1),
551            Some(TestSummaryData {
552                passed: 1,
553                failed: 1,
554                skipped: 0,
555            }),
556        );
557        let out = verdict_issue_builtin(&issue_arg(&handle)).expect("issue ok");
558        assert_eq!(outcome_of(&out), "fail");
559    }
560
561    /// A recorded execution whose output parsed to nothing (no host summary) is
562    /// unavailable, never a fabricated pass.
563    #[test]
564    fn recorded_but_unparsed_execution_is_unavailable() {
565        let _g = enter_execution_scope(mint_execution_scope());
566        let handle = store_run(artifacts("not test output", 0), None);
567        let out = verdict_issue_builtin(&issue_arg(&handle)).expect("issue ok");
568        assert_eq!(outcome_of(&out), "unavailable");
569    }
570}