Skip to main content

greentic_operator/
qa_flow_handler.rs

1//! Operator-side handler for `qa.process` flow nodes.
2//!
3//! When the operator encounters a flow node whose component type is a QA
4//! processor, this module intercepts the invocation and runs the QA
5//! collect→validate→apply flow inline, returning the result as the node's output.
6
7use std::path::Path;
8
9use anyhow::Result;
10use serde_json::{Value, json};
11
12use crate::component_qa_ops::{self, QaMode};
13use crate::demo::runner_host::OperatorContext;
14use crate::domains::{Domain, ProviderPack};
15use crate::qa_setup_wizard;
16use crate::setup_to_formspec;
17
18/// Check whether a node component identifier represents a QA processor.
19pub fn is_qa_process_node(component_id: &str) -> bool {
20    component_id == "component-qa"
21        || component_id == "ai.greentic.component-qa"
22        || component_id.ends_with("/component-qa")
23        || component_id.contains("qa.process")
24}
25
26/// Handle a QA process flow node.
27///
28/// This runs the full QA wizard inline:
29/// 1. Determine provider from node config
30/// 2. Load FormSpec from the provider pack
31/// 3. Collect answers (from node config or interactively)
32/// 4. Call apply-answers on the provider WASM component
33/// 5. Return the config output as node result
34pub fn handle_qa_process_node(
35    root: &Path,
36    node_config: &Value,
37    provider_pack: &ProviderPack,
38    provider_id: &str,
39    domain: Domain,
40    ctx: &OperatorContext,
41    interactive: bool,
42) -> Result<Value> {
43    // Extract mode from node config (default: "setup")
44    let mode_str = node_config
45        .get("mode")
46        .and_then(Value::as_str)
47        .unwrap_or("setup");
48    let mode = match mode_str {
49        "remove" => QaMode::Remove,
50        "upgrade" => QaMode::Upgrade,
51        "default" => QaMode::Default,
52        _ => QaMode::Setup,
53    };
54
55    // Extract pre-supplied answers from node config
56    let supplied_answers = node_config.get("answers").cloned();
57
58    // Build FormSpec from the provider pack
59    let form_spec = setup_to_formspec::pack_to_form_spec(&provider_pack.path, provider_id);
60
61    // Collect answers
62    let answers = if let Some(answers) = supplied_answers {
63        // Validate pre-supplied answers against FormSpec
64        if let Some(ref spec) = form_spec {
65            qa_setup_wizard::validate_answers_against_form_spec(spec, &answers)?;
66        }
67        answers
68    } else if interactive {
69        // Run interactive wizard
70        let (answers, _spec) = qa_setup_wizard::run_qa_setup(
71            &provider_pack.path,
72            provider_id,
73            None,
74            true,
75            form_spec.clone(),
76        )?;
77        answers
78    } else {
79        json!({})
80    };
81
82    // Read current config
83    let providers_root = root
84        .join("state")
85        .join("runtime")
86        .join(&ctx.tenant)
87        .join("providers");
88    let current_config = crate::provider_config_envelope::read_provider_config_envelope(
89        &providers_root,
90        provider_id,
91    )?
92    .map(|envelope| envelope.config);
93
94    // Call apply-answers via component QA
95    match component_qa_ops::apply_answers_via_component_qa(
96        root,
97        domain,
98        &ctx.tenant,
99        ctx.team.as_deref(),
100        provider_pack,
101        provider_id,
102        mode,
103        current_config.as_ref(),
104        &answers,
105    ) {
106        Ok(Some(config)) => Ok(json!({
107            "status": "ok",
108            "config": config,
109            "mode": mode_str,
110            "provider": provider_id,
111        })),
112        Ok(None) => Ok(json!({
113            "status": "skip",
114            "reason": "provider does not support QA contract",
115            "provider": provider_id,
116        })),
117        Err(diag) => Ok(json!({
118            "status": "error",
119            "code": diag.code.as_str(),
120            "message": diag.message,
121            "provider": provider_id,
122        })),
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn detects_qa_process_node() {
132        assert!(is_qa_process_node("component-qa"));
133        assert!(is_qa_process_node("ai.greentic.component-qa"));
134        assert!(is_qa_process_node("root:component/component-qa"));
135        assert!(is_qa_process_node("qa.process.setup"));
136        assert!(!is_qa_process_node("component-llm-openai"));
137        assert!(!is_qa_process_node("messaging-telegram"));
138    }
139}