a3s-effect 0.2.0

Effect-style actor runtime for projecting an A3S Code harness from an immutable event log
Documentation
#![cfg(test)]

use super::*;

#[test]
fn closed_sets_refuse_unknown_values_and_round_trip() {
    assert_eq!(
        ToolExecutionOutcome::parse("failed").unwrap().as_str(),
        "failed"
    );
    assert!(ToolExecutionOutcome::parse("ok")
        .unwrap_err()
        .to_string()
        .contains("ToolExecutionOutcome"));
    assert!(ToolRecoveryPolicy::parse("retry").is_err());
    assert!(ToolObservationCompleteness::parse("partial").is_err());
    assert!(ToolFileKind::parse("link").is_err());
}

#[test]
fn complete_changes_require_byte_identity() {
    let effects = ToolEffectObservationV1::complete(vec![ToolPathChangeV1 {
        path: "hello".into(),
        before: None,
        after: ToolFileIdentityV1 {
            kind: ToolFileKind::File,
            content_digest: None,
            mode: None,
        },
    }]);
    assert!(effects.validate().is_err());
    assert!(ToolEffectObservationV1::incomplete("read failed")
        .validate()
        .is_ok());
}

fn prepared() -> ToolPreparedV1 {
    let args = serde_json::json!({"path": "target.txt"});
    ToolPreparedV1 {
        schema: TOOL_PREPARED_SCHEMA.into(),
        identity: ToolInvocationIdentityV1 {
            turn: 1,
            cycle: 0,
            tool_call_id: "call".into(),
            batch_index: 0,
            admission: Some(ToolAdmissionIdentityV1 {
                run_id: "run".into(),
                thread_id: "thread".into(),
                capability_binding_digest: format!("sha256:{}", "a".repeat(64)),
            }),
        },
        tool_name: "write".into(),
        original_args_digest: tool_args_digest(&args).unwrap(),
        effective_args_digest: tool_args_digest(&args).unwrap(),
        recovery: ToolRecoveryPolicy::NeverReplay,
        baseline: None,
    }
}

#[test]
fn version_identity_and_prepared_digest_are_required() {
    let prepared = prepared();
    let result = ToolExecutionResultV1::unknown(&prepared, "interrupted").unwrap();
    result.ensure_prepared(&prepared).unwrap();
    let mut foreign = result.clone();
    foreign.identity.admission.as_mut().unwrap().run_id = "another-run".into();
    assert!(foreign
        .ensure_prepared(&prepared)
        .unwrap_err()
        .to_string()
        .contains("tool_effect_identity_mismatch"));
    let mut altered = prepared.clone();
    altered.effective_args_digest =
        tool_args_digest(&serde_json::json!({"path":"different"})).unwrap();
    assert!(result.ensure_prepared(&altered).is_err());
    altered.schema = "future.v2".into();
    assert!(altered
        .validate()
        .unwrap_err()
        .to_string()
        .contains("tool_effect_schema_unsupported"));
    let mut invalid = prepared;
    invalid.identity.turn = 0;
    assert!(invalid.validate().is_err());
}

#[test]
fn wire_unknown_outcome_names_the_closed_set() {
    let result = ToolExecutionResultV1::unknown(&prepared(), "interrupted").unwrap();
    let mut wire = serde_json::to_value(result).unwrap();
    wire["outcome"] = serde_json::json!("future_state");
    let error = serde_json::from_value::<ToolExecutionResultV1>(wire).unwrap_err();
    assert!(error.to_string().contains("ToolExecutionOutcome"));
}

#[test]
fn observation_bound_and_missing_reason_refuse_complete_claims() {
    let missing_reason = ToolEffectObservationV1 {
        completeness: ToolObservationCompleteness::Incomplete,
        reason: None,
        changes: vec![],
        unresolved_external_operations: vec![],
    };
    assert!(missing_reason.validate().is_err());
    let mut oversized = prepared();
    oversized.baseline = Some(Value::String("x".repeat(MAX_TOOL_EFFECT_CANONICAL_BYTES)));
    assert!(oversized.validate().is_err());
    let duplicate = ToolPathChangeV1 {
        path: "removed".into(),
        before: None,
        after: ToolFileIdentityV1 {
            kind: ToolFileKind::Missing,
            content_digest: None,
            mode: None,
        },
    };
    assert!(ToolEffectObservationV1::complete(vec![duplicate.clone()])
        .validate()
        .is_ok());
    assert!(
        ToolEffectObservationV1::complete(vec![duplicate.clone(), duplicate])
            .validate()
            .is_err()
    );
}

#[test]
fn typed_paths_refuse_workspace_escape_and_preserve_legal_spelling() {
    let observation = |path: &str| {
        ToolEffectObservationV1::complete(vec![ToolPathChangeV1 {
            path: path.into(),
            before: None,
            after: ToolFileIdentityV1 {
                kind: ToolFileKind::Missing,
                content_digest: None,
                mode: None,
            },
        }])
    };
    for path in ["/outside", "../outside", "safe/../../outside", "nul\0path"] {
        assert!(
            observation(path).validate().is_err(),
            "unsafe path accepted: {path:?}"
        );
        let mut unknown = observation(path);
        unknown.completeness = ToolObservationCompleteness::Incomplete;
        unknown.reason = Some("interrupted".into());
        assert!(
            unknown.validate().is_err(),
            "Unknown must not bypass workspace scope: {path:?}"
        );
    }
    for path in [" padded ", "数据.txt", "line\nbreak", "tab\tname"] {
        let original = observation(path);
        original.validate().unwrap();
        let decoded: ToolEffectObservationV1 =
            serde_json::from_value(serde_json::to_value(original).unwrap()).unwrap();
        assert_eq!(decoded.changes[0].path, path);
    }
    #[cfg(unix)]
    for path in [r"literal\name", r".a3s\legal-file", r"C:\legal-unix-file"] {
        observation(path).validate().unwrap();
    }
    #[cfg(windows)]
    for path in [r"C:\outside", r"\\server\share\outside", r"safe\..\outside"] {
        assert!(observation(path).validate().is_err());
    }
}