Files
wehub-resource-sync 26382a7ac6
CI / Clippy (push) Failing after 15m13s
CI / Test (ubuntu-latest) (push) Failing after 16m1s
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Build (no embeddings / no ORT) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Cookbook (Node) (push) Has been cancelled
CI / Pi Extension (Node) (push) Has been cancelled
CI / Rust SDK (lean-ctx-client) (push) Has been cancelled
CI / Embed SDK (lean-ctx-sdk) (push) Has been cancelled
CI / Python SDK (leanctx) (push) Has been cancelled
CI / Hermes Plugin (Python) (push) Has been cancelled
CI / SDK Conformance Matrix (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / cargo-deny (push) Has been cancelled
CI / Adversarial Safety (push) Has been cancelled
CI / Benchmarks (push) Has been cancelled
CI / Output-Quality Gate (eval A/B) (push) Has been cancelled
CI / Documentation (push) Has been cancelled
CI / CI Green (push) Has been cancelled
JetBrains Plugin / Actionlint (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (rust) (push) Has been cancelled
JetBrains Plugin / Validation (push) Has been cancelled
JetBrains Plugin / Build (push) Has been cancelled
JetBrains Plugin / Test (push) Has been cancelled
Security Check / Security Scan (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:30 +08:00

300 lines
9.0 KiB
Rust

use lean_ctx::core::workflow::*;
fn minimal_spec() -> WorkflowSpec {
WorkflowSpec {
name: "test".to_string(),
description: None,
initial: "a".to_string(),
states: vec![
StateSpec {
name: "a".to_string(),
description: None,
allowed_tools: Some(vec!["ctx_read".to_string()]),
requires_evidence: None,
},
StateSpec {
name: "b".to_string(),
description: None,
allowed_tools: None,
requires_evidence: Some(vec!["tool:ctx_read".to_string()]),
},
StateSpec {
name: "done".to_string(),
description: None,
allowed_tools: None,
requires_evidence: None,
},
],
transitions: vec![
TransitionSpec {
from: "a".to_string(),
to: "b".to_string(),
description: None,
},
TransitionSpec {
from: "b".to_string(),
to: "done".to_string(),
description: None,
},
],
}
}
struct EnvVarGuard {
key: &'static str,
previous: Option<String>,
}
impl EnvVarGuard {
fn set(key: &'static str, value: &str) -> Self {
let previous = std::env::var(key).ok();
// TODO: Audit that the environment access only happens in single-threaded code.
unsafe { std::env::set_var(key, value) };
Self { key, previous }
}
}
impl Drop for EnvVarGuard {
fn drop(&mut self) {
if let Some(ref previous) = self.previous {
// TODO: Audit that the environment access only happens in single-threaded code.
unsafe { std::env::set_var(self.key, previous) };
} else {
// TODO: Audit that the environment access only happens in single-threaded code.
unsafe { std::env::remove_var(self.key) };
}
}
}
#[test]
fn validate_spec_accepts_valid_spec() {
let spec = minimal_spec();
assert!(validate_spec(&spec).is_ok());
}
#[test]
fn validate_spec_rejects_empty_states() {
let mut spec = minimal_spec();
spec.states.clear();
let err = validate_spec(&spec).unwrap_err();
assert!(err.contains("must not be empty"));
}
#[test]
fn validate_spec_rejects_empty_name() {
let mut spec = minimal_spec();
spec.name = " ".to_string();
let err = validate_spec(&spec).unwrap_err();
assert!(err.contains("name must not be empty"));
}
#[test]
fn validate_spec_rejects_duplicate_state_names() {
let mut spec = minimal_spec();
spec.states.push(StateSpec {
name: "a".to_string(),
description: None,
allowed_tools: None,
requires_evidence: None,
});
let err = validate_spec(&spec).unwrap_err();
assert!(err.contains("Duplicate state name"));
}
#[test]
fn validate_spec_rejects_initial_not_in_states() {
let mut spec = minimal_spec();
spec.initial = "nonexistent".to_string();
let err = validate_spec(&spec).unwrap_err();
assert!(err.contains("is not in states"));
}
#[test]
fn validate_spec_rejects_transition_with_unknown_from() {
let mut spec = minimal_spec();
spec.transitions.push(TransitionSpec {
from: "ghost".to_string(),
to: "a".to_string(),
description: None,
});
let err = validate_spec(&spec).unwrap_err();
assert!(err.contains("is not a state"));
}
#[test]
fn validate_spec_rejects_empty_allowed_tools_list() {
let mut spec = minimal_spec();
spec.states[0].allowed_tools = Some(vec![]);
let err = validate_spec(&spec).unwrap_err();
assert!(err.contains("must not be empty when present"));
}
#[test]
fn allowed_transitions_returns_correct_transitions() {
let spec = minimal_spec();
let from_a = allowed_transitions(&spec, "a");
assert_eq!(from_a.len(), 1);
assert_eq!(from_a[0].to, "b");
let from_done = allowed_transitions(&spec, "done");
assert!(from_done.is_empty());
}
#[test]
fn find_transition_works() {
let spec = minimal_spec();
assert!(find_transition(&spec, "a", "b").is_some());
assert!(find_transition(&spec, "a", "done").is_none());
assert!(find_transition(&spec, "b", "a").is_none());
}
#[test]
fn missing_evidence_returns_empty_when_no_requirements() {
let spec = minimal_spec();
let missing = missing_evidence_for_state(&spec, "a", |_| false);
assert!(missing.is_empty());
}
#[test]
fn missing_evidence_returns_keys_when_unsatisfied() {
let spec = minimal_spec();
let missing = missing_evidence_for_state(&spec, "b", |_| false);
assert_eq!(missing, vec!["tool:ctx_read"]);
}
#[test]
fn missing_evidence_returns_empty_when_satisfied() {
let spec = minimal_spec();
let missing = missing_evidence_for_state(&spec, "b", |k| k == "tool:ctx_read");
assert!(missing.is_empty());
}
#[test]
fn missing_evidence_unknown_state() {
let spec = minimal_spec();
let missing = missing_evidence_for_state(&spec, "nonexistent", |_| true);
assert_eq!(missing.len(), 1);
assert!(missing[0].starts_with("unknown_state:"));
}
#[test]
fn can_transition_succeeds_with_evidence() {
let spec = minimal_spec();
let result = can_transition(&spec, "a", "b", |k| k == "tool:ctx_read");
assert!(result.is_ok());
}
#[test]
fn can_transition_blocked_without_evidence() {
let spec = minimal_spec();
let result = can_transition(&spec, "a", "b", |_| false);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Missing evidence"));
}
#[test]
fn can_transition_blocked_for_invalid_transition() {
let spec = minimal_spec();
let result = can_transition(&spec, "a", "done", |_| true);
assert!(result.is_err());
assert!(result.unwrap_err().contains("No transition"));
}
#[test]
fn workflow_run_new_sets_initial_state() {
let spec = minimal_spec();
let run = WorkflowRun::new(spec);
assert_eq!(run.current, "a");
assert!(run.transitions.is_empty());
assert!(run.evidence.is_empty());
}
#[test]
fn workflow_run_add_manual_evidence() {
let spec = minimal_spec();
let mut run = WorkflowRun::new(spec);
run.add_manual_evidence("tool:ctx_read", Some("file.rs"));
assert_eq!(run.evidence.len(), 1);
assert_eq!(run.evidence[0].key, "tool:ctx_read");
assert_eq!(run.evidence[0].value.as_deref(), Some("file.rs"));
}
#[test]
fn builtin_plan_code_test_validates() {
let spec = WorkflowSpec::builtin_plan_code_test();
assert!(validate_spec(&spec).is_ok());
assert_eq!(spec.initial, "planning");
assert_eq!(spec.states.len(), 4);
assert_eq!(spec.transitions.len(), 3);
}
#[test]
fn builtin_spec_roundtrip_serde() {
let spec = WorkflowSpec::builtin_plan_code_test();
let json = serde_json::to_string(&spec).expect("serialize");
let back: WorkflowSpec = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.name, spec.name);
assert_eq!(back.states.len(), spec.states.len());
}
#[test]
fn store_roundtrip() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("active.json");
let spec = minimal_spec();
let mut run = WorkflowRun::new(spec);
run.add_manual_evidence("test-key", Some("test-value"));
let json = serde_json::to_string_pretty(&run).expect("serialize");
std::fs::write(&path, &json).expect("write");
let content = std::fs::read_to_string(&path).expect("read");
let loaded: WorkflowRun = serde_json::from_str(&content).expect("deserialize");
assert_eq!(loaded.current, "a");
assert_eq!(loaded.evidence.len(), 1);
assert_eq!(loaded.evidence[0].key, "test-key");
}
#[test]
fn store_load_active_returns_none_when_file_is_missing() {
let _env_lock = lean_ctx::core::data_dir::test_env_lock();
let tmp = tempfile::tempdir().expect("tempdir");
let data_dir = tmp.path().join("leanctx-data");
let _env = EnvVarGuard::set(
"LEAN_CTX_DATA_DIR",
data_dir.to_str().expect("data dir utf8"),
);
clear_active().expect("clear missing active should succeed");
let loaded = load_active().expect("load_active");
assert!(loaded.is_none());
}
#[test]
fn store_save_load_clear_active_roundtrip() {
let _env_lock = lean_ctx::core::data_dir::test_env_lock();
let tmp = tempfile::tempdir().expect("tempdir");
let data_dir = tmp.path().join("leanctx-data");
let _env = EnvVarGuard::set(
"LEAN_CTX_DATA_DIR",
data_dir.to_str().expect("data dir utf8"),
);
let mut run = WorkflowRun::new(minimal_spec());
run.add_manual_evidence("tool:ctx_read", Some("src/lib.rs"));
save_active(&run).expect("save_active");
let loaded = load_active()
.expect("load_active")
.expect("active workflow");
assert_eq!(loaded.current, "a");
assert_eq!(loaded.evidence.len(), 1);
assert_eq!(loaded.evidence[0].key, "tool:ctx_read");
clear_active().expect("clear_active");
let after_clear = load_active().expect("load after clear");
assert!(after_clear.is_none());
}