bcode_workflow_plugin 0.0.1-alpha.0

Workflow discovery, commands, status, and TUI for bcode
use super::*;

fn request() -> ExecutionContributorEvidenceRequest {
    parse(json!({"expected_revision":1,"action":{"kind":"discover","after_sequence":null,"limit":10}})).unwrap()
}

#[test]
fn service_failure_codes_survive_without_untrusted_messages() {
    for code in [
        "workflow_contributor_evidence_cancelled",
        "workflow_contributor_evidence_unauthorized",
        "workflow_contributor_evidence_timed_out",
        "workflow_contributor_evidence_storage_busy",
        "workflow_contributor_evidence_storage_unavailable",
        "workflow_contributor_evidence_unavailable",
        "future-secret-code",
    ] {
        let wire = ServiceBridgeResponse::Service(ToolInvocationServiceResolution::Failed {
            code: code.into(),
            message: "secret prompt /private/path".into(),
        });
        let decoded = serde_json::from_slice(&serde_json::to_vec(&wire).unwrap()).unwrap();
        let response = invocation_response(decoded, &request());
        assert!(response.payload.is_empty());
        let error = response.error.unwrap();
        let expected = if matches!(
            code,
            "future-secret-code" | "workflow_contributor_evidence_unavailable"
        ) {
            "evidence_unavailable"
        } else {
            code
        };
        assert_eq!(error.code, expected);
        assert!(!error.message.contains("secret"));
        assert!(!error.message.contains("/private"));
        assert!(error.message.len() <= 128);
    }
}

#[test]
fn portable_envelope_normalizes_and_preserves_strict_typed_validation() {
    let tool = definition();
    let normalized = bcode_model_schema::normalize(
        &tool.input_schema,
        &bcode_model_schema::SchemaDialect {
            object_properties: bcode_model_schema::ObjectPropertyPolicy::RequireAllAndClose,
            one_of: bcode_model_schema::OneOfPolicy::CollapseAnnotatedConstants,
            reference_siblings:
                bcode_model_schema::ReferenceSiblingPolicy::RemoveAnnotationsRejectSemantic,
            ..bcode_model_schema::SchemaDialect::default()
        },
    )
    .expect("portable evidence envelope");
    let validator = jsonschema::validator_for(&normalized).unwrap();
    for arguments in [
        json!({"expected_revision":1,"action":{"kind":"discover","after_sequence":null,"limit":10}}),
        json!({"expected_revision":1,"action":{"kind":"read","invocation_id":"shell","request_sequence":72,"result_sequence":78,"byte_limit":8192}}),
    ] {
        let wrapped = json!({"request_json":arguments.to_string()});
        assert!(validator.validate(&wrapped).is_ok());
        assert_eq!(parse(wrapped).unwrap(), parse(arguments).unwrap());
    }
    assert!(parse(json!({"request_json":"not JSON"})).is_err());
    assert!(parse(json!({"request_json":123})).is_err());
    let mut arguments = serde_json::to_value(request()).unwrap();
    arguments["session_id"] = json!("forged");
    assert!(parse(json!({"request_json":arguments.to_string()})).is_err());
    assert!(parse(json!({"request_json":serde_json::to_value(request()).unwrap().to_string(),"expected_revision":1})).is_err());
    let invalid_bound = json!({"expected_revision":1,"action":{"kind":"discover","after_sequence":null,"limit":101}});
    assert!(parse(json!({"request_json":invalid_bound.to_string()})).is_err());
}

#[test]
fn additive_generated_listing_and_old_response_compatibility() {
    use bcode_workflow::contributor_evidence::{
        ContributorScopeKind, GeneratedContributorProvenance,
    };
    let old = serde_json::to_value(response()).unwrap();
    let decoded: ExecutionContributorEvidenceResponse = serde_json::from_value(old).unwrap();
    assert!(decoded.contributors.is_empty());
    let query = parse(json!({"expected_revision":2,"action":{"kind":"contributors","after_node_id":null,"limit":1}})).unwrap();
    let mut listing = response();
    let mut scope = listing.scope.take().unwrap();
    scope.graph_revision = 2;
    listing.scope_kind = Some(ContributorScopeKind::GeneratedExecution);
    listing.generated_provenance = Some(GeneratedContributorProvenance {
        mutation_id: "delegate".into(),
        source_activation_id: "original".into(),
    });
    listing.contributors.push(scope);
    assert!(valid_response(&listing, &query));
    listing.generated_provenance = None;
    assert!(!valid_response(&listing, &query));
    let selected = parse(json!({"expected_revision":2,"action":{"kind":"discover","contributor_node_id":"worker","after_sequence":null,"limit":1}})).unwrap();
    assert!(matches!(
        selected.action,
        ContributorEvidenceAction::Discover {
            contributor_node_id: Some(_),
            ..
        }
    ));
    assert!(parse(json!({"expected_revision":2,"action":{"kind":"discover","contributor_node_id":"worker","session_id":"forged","after_sequence":null,"limit":1}})).is_err());
}

#[test]
fn preparation_binds_exact_action_revision_and_operation() {
    let payload = serde_json::to_value(request()).unwrap();
    let descriptor = json!({"operation":OPERATION,"edit":payload,"route_id":"authenticated"});
    assert!(prepared_matches(&descriptor, &payload));
    let mut changed = payload.clone();
    changed["expected_revision"] = json!(2);
    assert!(!prepared_matches(&descriptor, &changed));
    changed = payload.clone();
    changed["action"]["limit"] = json!(1);
    assert!(!prepared_matches(&descriptor, &changed));
    let mut wrong_operation = descriptor;
    wrong_operation["operation"] = json!("execution_context");
    assert!(!prepared_matches(&wrong_operation, &payload));
}

#[test]
fn bounded_actions_reject_selectors_unknown_fields_and_invalid_pairs() {
    let discover = request();
    assert_eq!(discover.expected_revision, 1);
    for selector in ["session_id", "contributor", "run_id"] {
        let mut arguments = serde_json::to_value(&discover).unwrap();
        arguments[selector] = json!("forged");
        assert!(parse(arguments).is_err());
    }
    assert!(parse(json!({"expected_revision":1,"action":{"kind":"discover","after_sequence":null,"limit":101}})).is_err());
    assert!(parse(json!({"expected_revision":0,"action":{"kind":"discover","after_sequence":null,"limit":1}})).is_err());
    assert!(parse(json!({"expected_revision":1,"action":{"kind":"read","invocation_id":"shell","request_sequence":78,"result_sequence":72,"byte_limit":100}})).is_err());
    assert!(parse(json!({"expected_revision":1,"action":{"kind":"read","invocation_id":"shell","request_sequence":72,"result_sequence":78,"byte_limit":65537}})).is_err());
    let exact = parse(json!({"expected_revision":1,"action":{"kind":"read","invocation_id":"shell","request_sequence":72,"result_sequence":78,"byte_limit":8192}})).unwrap();
    assert!(matches!(
        exact.action,
        ContributorEvidenceAction::Read {
            request_sequence: 72,
            result_sequence: 78,
            ..
        }
    ));
}

fn response() -> ExecutionContributorEvidenceResponse {
    ExecutionContributorEvidenceResponse {
        contract: CONTRACT_ID.into(),
        scope_kind: None,
        contributors: Vec::new(),
        next_node_id: None,
        generated_provenance: None,
        status: ContributorEvidenceStatus::Available,
        scope: Some(
            bcode_workflow::contributor_evidence::ContributorEvidenceScope {
                run_id: "run".into(),
                consumer_activation_id: "evaluate".into(),
                graph_revision: 1,
                node_id: "implement".into(),
                activation_id: "implementation".into(),
                attempt: 1,
                session_id: "canonical".into(),
                output_id: "output".into(),
                output_checksum_sha256: "checksum".into(),
            },
        ),
        events: Vec::new(),
        scanned_events: 0,
        next_sequence: None,
        evidence: None,
    }
}

#[test]
fn response_contract_and_scope_are_checked_not_guessed() {
    let request = request();
    let mut response = response();
    assert!(valid_response(&response, &request));
    response.contract = "bcode.workflow.execution_contributor_evidence.v2".into();
    assert!(!valid_response(&response, &request));
    response.contract = CONTRACT_ID.into();
    response.scope.as_mut().unwrap().graph_revision = 2;
    assert!(!valid_response(&response, &request));
    response.status = ContributorEvidenceStatus::Unsupported;
    assert!(!valid_response(&response, &request));
    response.scope = None;
    assert!(valid_response(&response, &request));
}

#[test]
fn discovery_cannot_exceed_scan_bound_or_claim_a_pair() {
    let request = request();
    let mut response = response();
    response.scanned_events = 11;
    assert!(!valid_response(&response, &request));
    response.scanned_events = 1;
    response.events = vec![
        bcode_workflow::contributor_evidence::ContributorEventReference {
            sequence: 72,
            invocation_id: "shell".into(),
            role: bcode_workflow::contributor_evidence::ContributorEventRole::Request,
        };
        2
    ];
    assert!(!valid_response(&response, &request));
}

#[test]
fn producer_shaped_exact_read_survives_invocation_response_boundary() {
    use bcode_workflow::contributor_evidence::{
        ContributorEvidenceText, ContributorInvocationEvidence,
    };
    let query = parse(json!({"expected_revision":1,"action":{"kind":"read","invocation_id":"shell","request_sequence":72,"result_sequence":78,"byte_limit":8192}})).unwrap();
    let mut producer = response();
    producer.scanned_events = 2;
    let arguments = "{\"command\":\"printf ok\"}";
    producer.evidence = Some(ContributorInvocationEvidence {
        invocation_id: "shell".into(),
        request_sequence: 72,
        result_sequence: 78,
        producer_plugin_id: Some("bcode.shell".into()),
        tool_name: "shell.run".into(),
        arguments_json: ContributorEvidenceText {
            text: arguments.into(),
            original_bytes: arguments.len(),
            retained_bytes: arguments.len(),
            truncated: false,
        },
        model_output: ContributorEvidenceText {
            text: "ok".into(),
            original_bytes: 2,
            retained_bytes: 2,
            truncated: false,
        },
        is_error: false,
        producer_capture_complete: None,
    });
    let wire = ServiceBridgeResponse::Service(ToolInvocationServiceResolution::Responded {
        payload: serde_json::to_value(&producer).unwrap(),
    });
    let serialized = serde_json::to_vec(&wire).unwrap();
    let decoded: ServiceBridgeResponse = serde_json::from_slice(&serialized).unwrap();
    let invoked = invocation_response(decoded, &query);
    assert!(
        invoked.error.is_none(),
        "producer-shaped successful pair must be readable"
    );
    let tool: bcode_tool::ToolInvocationResponse =
        serde_json::from_slice(&invoked.payload).unwrap();
    assert!(!tool.is_error);
    let displayed: ExecutionContributorEvidenceResponse =
        serde_json::from_str(&tool.output).unwrap();
    assert_eq!(displayed, producer);
    producer.scanned_events = 0;
    assert!(!valid_response(&producer, &query));
}

#[test]
fn exact_pair_checks_identity_text_bounds_and_truncation() {
    use bcode_workflow::contributor_evidence::{
        ContributorEvidenceText, ContributorInvocationEvidence,
    };
    let request = parse(json!({"expected_revision":1,"action":{"kind":"read","invocation_id":"shell","request_sequence":72,"result_sequence":78,"byte_limit":4}})).unwrap();
    let mut response = response();
    response.scanned_events = 2;
    let text = ContributorEvidenceText {
        text: "ok".into(),
        original_bytes: 2,
        retained_bytes: 2,
        truncated: false,
    };
    response.evidence = Some(ContributorInvocationEvidence {
        invocation_id: "shell".into(),
        request_sequence: 72,
        result_sequence: 78,
        producer_plugin_id: None,
        tool_name: "shell.run".into(),
        arguments_json: text.clone(),
        model_output: text,
        is_error: false,
        producer_capture_complete: None,
    });
    assert!(valid_response(&response, &request));
    response.evidence.as_mut().unwrap().result_sequence = 79;
    assert!(!valid_response(&response, &request));
    response.evidence.as_mut().unwrap().result_sequence = 78;
    response.evidence.as_mut().unwrap().model_output.truncated = true;
    assert!(!valid_response(&response, &request));
    response
        .evidence
        .as_mut()
        .unwrap()
        .model_output
        .original_bytes = 3;
    assert!(valid_response(&response, &request));
    response
        .evidence
        .as_mut()
        .unwrap()
        .model_output
        .text
        .push('!');
    assert!(!valid_response(&response, &request));
}