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_gated_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::tools::inspect_test_results::{store_run, RawArtifacts, TestSummaryData};
224    use harn_lexer::Lexer;
225    use harn_parser::Parser;
226    use harn_vm::orchestration::RunExecutionRecord;
227    use harn_vm::{enter_execution_scope, mint_execution_scope, register_vm_stdlib, Compiler, Vm};
228    use std::path::Path;
229    use std::sync::Arc;
230    use tempfile::TempDir;
231
232    /// Build the `issue` arg — a single dict `{result_handle}` — the way the VM
233    /// dispatcher passes it.
234    fn issue_arg(result_handle: &str) -> Vec<VmValue> {
235        let mut map = DictMap::new();
236        map.put_str("result_handle", result_handle);
237        vec![VmValue::dict_map(map)]
238    }
239
240    fn outcome_of(v: &VmValue) -> String {
241        v.as_dict()
242            .and_then(|d| d.get("outcome"))
243            .map(|o| o.as_str_cow().into_owned())
244            .unwrap_or_default()
245    }
246
247    fn artifacts(stdout: &str, exit_code: i32) -> RawArtifacts {
248        RawArtifacts {
249            stdout: stdout.to_string(),
250            stderr: String::new(),
251            exit_code,
252            junit_path: None,
253            ecosystem: None,
254            argv: Vec::new(),
255            authorized_test_plan: None,
256        }
257    }
258
259    fn cargo_fixture(parent: &Path, name: &str) -> TempDir {
260        let dir = tempfile::Builder::new()
261            .prefix(name)
262            .tempdir_in(parent)
263            .expect("fixture dir");
264        std::fs::write(
265            dir.path().join("Cargo.toml"),
266            format!(
267                "[package]\nname = \"{name}\"\nversion = \"0.0.0\"\nedition = \"2021\"\n\n[lib]\npath = \"lib.rs\"\n"
268            ),
269        )
270        .expect("fixture manifest");
271        std::fs::write(
272            dir.path().join("lib.rs"),
273            "#[cfg(test)]\nmod tests {\n    #[test]\n    fn green() {}\n}\n",
274        )
275        .expect("fixture source");
276        dir
277    }
278
279    fn run_test_in(cwd: &Path, argv: Option<Vec<&str>>, filter: Option<&str>) -> VmValue {
280        let mut req = DictMap::new();
281        req.put_str("cwd", cwd.to_string_lossy());
282        if let Some(argv) = argv {
283            req.put(
284                "argv",
285                VmValue::List(Arc::new(argv.into_iter().map(VmValue::string).collect())),
286            );
287        }
288        req.put_opt_str("filter", filter);
289        crate::tools::run_test::handle(&[VmValue::dict_map(req)]).expect("run_test ok")
290    }
291
292    fn result_handle(result: &VmValue) -> String {
293        result
294            .as_dict()
295            .and_then(|dict| dict.get("result_handle"))
296            .map(|handle| handle.as_str_cow().into_owned())
297            .expect("run_test returns a result_handle")
298    }
299
300    fn compile(source: &str) -> harn_vm::Chunk {
301        let mut lexer = Lexer::new(source);
302        let tokens = lexer.tokenize().expect("tokenize");
303        let mut parser = Parser::new(tokens);
304        let program = parser.parse().expect("parse");
305        Compiler::new().compile(&program).expect("compile")
306    }
307
308    fn harn_string(value: &Path) -> String {
309        value
310            .to_string_lossy()
311            .replace('\\', "\\\\")
312            .replace('"', "\\\"")
313    }
314
315    fn overlap_vm(barrier: Arc<tokio::sync::Barrier>) -> Vm {
316        let mut vm = Vm::new();
317        register_vm_stdlib(&mut vm);
318        let _ = crate::install_default(&mut vm);
319        vm.register_async_builtin("__test_overlap", move |_ctx, _args| {
320            let barrier = barrier.clone();
321            async move {
322                barrier.wait().await;
323                Ok(VmValue::Nil)
324            }
325        });
326        vm
327    }
328
329    /// Record a run under `scope`, dropping the scope guard afterward — the owner
330    /// is frozen into the `StoredRun` at record time. Used to stage the
331    /// cross-scope / no-active-scope NEGATIVES (the POSITIVE below deliberately
332    /// drives the real execution path instead).
333    fn record_in_scope(
334        scope: &Arc<str>,
335        arts: RawArtifacts,
336        summary: Option<TestSummaryData>,
337    ) -> String {
338        let _g = enter_execution_scope(scope.clone());
339        store_run(arts, summary)
340    }
341
342    /// EXECUTED NEGATIVE: arbitrary caller argv can print perfect passing test
343    /// prose, but it has no host-discovered plan identity and cannot mint PASS.
344    #[cfg(unix)]
345    #[test]
346    fn arbitrary_passing_command_cannot_issue_pass() {
347        let _scope = enter_execution_scope(mint_execution_scope());
348        let cwd = std::env::current_dir().expect("cwd");
349        let res = run_test_in(
350            &cwd,
351            Some(vec![
352                "sh",
353                "-c",
354                "printf 'running 1 test\\ntest a ... ok\\n\\ntest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out\\n'",
355            ]),
356            None,
357        );
358        let handle = result_handle(&res);
359        let out = verdict_issue_builtin(&issue_arg(&handle)).expect("issue ok");
360        assert_eq!(outcome_of(&out), "unavailable");
361    }
362
363    /// EXECUTED POSITIVE: omitting argv delegates plan selection to the host's
364    /// workspace discovery boundary. The exact discovered Cargo plan executes,
365    /// records measured green results, and can issue inside its owning run.
366    #[test]
367    fn host_discovered_test_plan_issues_pass() {
368        let workspace = TempDir::new().expect("workspace");
369        let fixture = cargo_fixture(workspace.path(), "verdict_positive");
370        harn_vm::stdlib::process::set_thread_execution_context(Some(RunExecutionRecord {
371            cwd: Some(fixture.path().to_string_lossy().into_owned()),
372            project_root: Some(fixture.path().to_string_lossy().into_owned()),
373            ..RunExecutionRecord::default()
374        }));
375        let _scope = enter_execution_scope(mint_execution_scope());
376        let res = run_test_in(fixture.path(), None, None);
377        let out = verdict_issue_builtin(&issue_arg(&result_handle(&res))).expect("issue ok");
378        harn_vm::stdlib::process::set_thread_execution_context(None);
379        assert_eq!(outcome_of(&out), "pass");
380    }
381
382    /// EXECUTED NEGATIVE: a caller-selected filter narrows the discovered plan
383    /// to a partial diagnostic run, so even a green result is non-authoritative.
384    #[test]
385    fn filtered_discovered_test_plan_cannot_issue_pass() {
386        let workspace = TempDir::new().expect("workspace");
387        let fixture = cargo_fixture(workspace.path(), "verdict_filtered");
388        harn_vm::stdlib::process::set_thread_execution_context(Some(RunExecutionRecord {
389            cwd: Some(fixture.path().to_string_lossy().into_owned()),
390            project_root: Some(fixture.path().to_string_lossy().into_owned()),
391            ..RunExecutionRecord::default()
392        }));
393        let _scope = enter_execution_scope(mint_execution_scope());
394        let res = run_test_in(fixture.path(), None, Some("green"));
395        let out = verdict_issue_builtin(&issue_arg(&result_handle(&res))).expect("issue ok");
396        harn_vm::stdlib::process::set_thread_execution_context(None);
397        assert_eq!(outcome_of(&out), "unavailable");
398    }
399
400    /// Deterministic top-level overlap oracle. Both VMs execute and record their
401    /// own host-discovered plan, then rendezvous while both top-level futures are
402    /// pending on one thread. Per-poll ambient isolation must restore each VM's
403    /// owner before issuance, so both receipts mint under their producing run.
404    #[tokio::test(flavor = "current_thread")]
405    async fn overlapping_top_level_vms_issue_only_their_own_receipts() {
406        tokio::task::LocalSet::new()
407            .run_until(async {
408                let workspace = TempDir::new().expect("workspace");
409                let fixture = cargo_fixture(workspace.path(), "verdict_overlap");
410                harn_vm::stdlib::process::set_thread_execution_context(Some(RunExecutionRecord {
411                    cwd: Some(fixture.path().to_string_lossy().into_owned()),
412                    project_root: Some(fixture.path().to_string_lossy().into_owned()),
413                    ..RunExecutionRecord::default()
414                }));
415
416                let source = |cwd: &Path| {
417                    format!(
418                        r#"
419import {{ verdict_disposition, verdict_from_run }} from "std/agent/verdict"
420
421pipeline default(_task) {{
422  hostlib_enable("tools:deterministic")
423  const run = hostlib_tools_run_test({{cwd: "{}", timeout_ms: 120000}})
424  __test_overlap()
425  return verdict_disposition(verdict_from_run(harness, run))
426}}
427"#,
428                        harn_string(cwd)
429                    )
430                };
431                let chunk_a = compile(&source(fixture.path()));
432                let chunk_b = compile(&source(fixture.path()));
433                let barrier = Arc::new(tokio::sync::Barrier::new(2));
434                let mut vm_a = overlap_vm(barrier.clone());
435                let mut vm_b = overlap_vm(barrier);
436
437                let (result_a, result_b) =
438                    tokio::join!(vm_a.execute(&chunk_a), vm_b.execute(&chunk_b));
439                harn_vm::stdlib::process::set_thread_execution_context(None);
440                assert_eq!(result_a.expect("vm A").display(), "pass");
441                assert_eq!(result_b.expect("vm B").display(), "pass");
442            })
443            .await;
444    }
445
446    /// NEGATIVE: a handle the host never recorded — the shape of a caller-authored
447    /// "evidence" file or a fabricated handle string — can NEVER reach a pass.
448    #[test]
449    fn unrecorded_handle_is_never_a_pass() {
450        let _scope = enter_execution_scope(mint_execution_scope());
451        let out = verdict_issue_builtin(&issue_arg("htr-deadbeef-999999")).expect("issue ok");
452        assert_eq!(outcome_of(&out), "unavailable");
453    }
454
455    /// NEGATIVE (cross-run replay): a green handle recorded in run A cannot be
456    /// issued while run B is active — the owner does not match the active scope.
457    #[test]
458    fn cross_scope_handle_is_rejected() {
459        let scope_a: Arc<str> = mint_execution_scope();
460        let handle = record_in_scope(
461            &scope_a,
462            artifacts("test result: ok. 2 passed; 0 failed", 0),
463            Some(TestSummaryData {
464                passed: 2,
465                failed: 0,
466                skipped: 0,
467            }),
468        );
469        let _scope_b = enter_execution_scope(mint_execution_scope());
470        let out = verdict_issue_builtin(&issue_arg(&handle)).expect("issue ok");
471        assert_eq!(outcome_of(&out), "unavailable");
472    }
473
474    /// NEGATIVE (fail-closed): a green handle cannot be issued with NO active
475    /// scope — issuance outside any owning run is refused.
476    #[test]
477    fn no_active_scope_is_rejected() {
478        let scope_a: Arc<str> = mint_execution_scope();
479        let handle = record_in_scope(
480            &scope_a,
481            artifacts("test result: ok. 1 passed; 0 failed", 0),
482            Some(TestSummaryData {
483                passed: 1,
484                failed: 0,
485                skipped: 0,
486            }),
487        );
488        // No scope entered here.
489        let out = verdict_issue_builtin(&issue_arg(&handle)).expect("issue ok");
490        assert_eq!(outcome_of(&out), "unavailable");
491    }
492
493    /// NEGATIVE (exit gate): a nonzero exit with passing-looking output cannot
494    /// mint a pass, even in-scope with a green-looking summary.
495    #[test]
496    fn nonzero_exit_with_passing_text_is_not_a_pass() {
497        let _g = enter_execution_scope(mint_execution_scope());
498        let handle = store_run(
499            artifacts("test result: ok. 1 passed; 0 failed", 1),
500            Some(TestSummaryData {
501                passed: 1,
502                failed: 0,
503                skipped: 0,
504            }),
505        );
506        let out = verdict_issue_builtin(&issue_arg(&handle)).expect("issue ok");
507        assert_eq!(outcome_of(&out), "fail");
508    }
509
510    /// A real execution the host observed RED is a fail — read from the host's
511    /// frozen summary, not asserted by the caller.
512    #[test]
513    fn red_host_execution_issues_fail() {
514        let _g = enter_execution_scope(mint_execution_scope());
515        let handle = store_run(
516            artifacts("test result: FAILED. 1 passed; 1 failed", 1),
517            Some(TestSummaryData {
518                passed: 1,
519                failed: 1,
520                skipped: 0,
521            }),
522        );
523        let out = verdict_issue_builtin(&issue_arg(&handle)).expect("issue ok");
524        assert_eq!(outcome_of(&out), "fail");
525    }
526
527    /// A recorded execution whose output parsed to nothing (no host summary) is
528    /// unavailable, never a fabricated pass.
529    #[test]
530    fn recorded_but_unparsed_execution_is_unavailable() {
531        let _g = enter_execution_scope(mint_execution_scope());
532        let handle = store_run(artifacts("not test output", 0), None);
533        let out = verdict_issue_builtin(&issue_arg(&handle)).expect("issue ok");
534        assert_eq!(outcome_of(&out), "unavailable");
535    }
536}