aboutsummaryrefslogtreecommitdiff
path: root/crates/detect-core/src/manifest/tests.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/detect-core/src/manifest/tests.rs')
-rw-r--r--crates/detect-core/src/manifest/tests.rs830
1 files changed, 830 insertions, 0 deletions
diff --git a/crates/detect-core/src/manifest/tests.rs b/crates/detect-core/src/manifest/tests.rs
new file mode 100644
index 0000000..3d9a5a2
--- /dev/null
+++ b/crates/detect-core/src/manifest/tests.rs
@@ -0,0 +1,830 @@
+use super::*;
+
+// Codex is only a registry key here; behavior tests supply synthetic rules.
+fn remote_manifest(version: &str, state: &str, contains: &str) -> String {
+ format!(
+ r#"
+id = "codex"
+version = "{version}"
+min_engine_version = 1
+updated_at = "2026-06-10T12:00:00Z"
+
+[[rules]]
+id = "test"
+state = "{state}"
+contains = ["{contains}"]
+"#
+ )
+}
+
+fn local_manifest(state: &str, contains: &str) -> String {
+ format!(
+ r#"
+id = "codex"
+
+[[rules]]
+id = "test"
+state = "{state}"
+contains = ["{contains}"]
+"#
+ )
+}
+
+fn rules_manifest(rules: &str) -> String {
+ format!(
+ r#"
+id = "codex"
+
+{rules}
+"#
+ )
+}
+
+fn with_manifest_dirs<T>(name: &str, f: impl FnOnce() -> T) -> T {
+ let _guard = crate::test_env_lock().lock().unwrap();
+ let old_config = std::env::var_os("XDG_CONFIG_HOME");
+ let old_state = std::env::var_os("XDG_STATE_HOME");
+ let base = std::env::temp_dir().join(format!(
+ "herdr-manifest-loader-{name}-{}",
+ std::process::id()
+ ));
+ let config_dir = base.join("config");
+ let state_dir = base.join("state");
+ let _ = std::fs::remove_dir_all(&base);
+ std::env::set_var("XDG_CONFIG_HOME", &config_dir);
+ std::env::set_var("XDG_STATE_HOME", &state_dir);
+ reload_manifests();
+ let result = f();
+ match old_config {
+ Some(value) => std::env::set_var("XDG_CONFIG_HOME", value),
+ None => std::env::remove_var("XDG_CONFIG_HOME"),
+ }
+ match old_state {
+ Some(value) => std::env::set_var("XDG_STATE_HOME", value),
+ None => std::env::remove_var("XDG_STATE_HOME"),
+ }
+ reload_manifests();
+ let _ = std::fs::remove_dir_all(&base);
+ result
+}
+
+fn write_remote_codex(content: &str) {
+ let path = crate::manifest_update::remote_manifest_path(Agent::Codex);
+ std::fs::create_dir_all(path.parent().unwrap()).unwrap();
+ std::fs::write(path, content).unwrap();
+ reload_manifests();
+}
+
+fn write_remote_codex_without_reload(content: &str) {
+ let path = crate::manifest_update::remote_manifest_path(Agent::Codex);
+ std::fs::create_dir_all(path.parent().unwrap()).unwrap();
+ std::fs::write(path, content).unwrap();
+}
+
+fn write_local_codex(content: &str) {
+ let path = override_path(Agent::Codex).unwrap();
+ std::fs::create_dir_all(path.parent().unwrap()).unwrap();
+ std::fs::write(path, content).unwrap();
+ reload_manifests();
+}
+
+#[test]
+fn known_agent_no_match_defaults_to_idle_fallback() {
+ with_manifest_dirs("no-match", || {
+ write_local_codex(&local_manifest("working", "active-marker"));
+ let explain = explain(Agent::Codex, "unmatched-marker");
+
+ assert_eq!(explain.state, AgentState::Idle);
+ assert!(!explain.visible_idle);
+ assert_eq!(
+ explain.fallback_reason.as_deref(),
+ Some(DEFAULT_KNOWN_AGENT_IDLE_FALLBACK)
+ );
+ });
+}
+
+#[test]
+fn rule_semantics_apply_gates_priority_and_line_regex() {
+ with_manifest_dirs("rule-semantics", || {
+ write_local_codex(&rules_manifest(
+ r#"
+[[rules]]
+id = "low_contains"
+state = "idle"
+priority = 1
+contains = ["match"]
+
+[[rules]]
+id = "high_nested_gates"
+state = "working"
+priority = 10
+contains = ["match"]
+all = [
+ { any = [{ regex = ["w[io]n"] }, { contains = ["fallback"] }] },
+]
+not = [
+ { contains = ["blocked"] },
+]
+
+[[rules]]
+id = "line_regex"
+state = "blocked"
+priority = 20
+line_regex = ["^exact line$"]
+"#,
+ ));
+
+ let high = explain(Agent::Codex, "match win");
+ assert_eq!(high.state, AgentState::Working);
+ assert_eq!(
+ high.matched_rule.as_ref().map(|rule| rule.id.as_str()),
+ Some("high_nested_gates")
+ );
+
+ let not_gate = explain(Agent::Codex, "match win blocked");
+ assert_eq!(not_gate.state, AgentState::Idle);
+ assert_eq!(
+ not_gate.matched_rule.as_ref().map(|rule| rule.id.as_str()),
+ Some("low_contains")
+ );
+
+ let line = explain(Agent::Codex, "before\nexact line\nafter");
+ assert_eq!(line.state, AgentState::Blocked);
+ assert_eq!(
+ line.matched_rule.as_ref().map(|rule| rule.id.as_str()),
+ Some("line_regex")
+ );
+ });
+}
+
+#[test]
+fn paired_alternatives_require_complete_gates_at_bottom_buffer() {
+ with_manifest_dirs("paired-alternatives", || {
+ write_local_codex(&rules_manifest(
+ r#"
+[[rules]]
+id = "paired"
+state = "blocked"
+region = "bottom_non_empty_lines(3)"
+visible_blocker = true
+any = [
+ { all = [
+ { contains = ["alpha heading"] },
+ { regex = ['alpha\s+control\s*\z'] },
+ ] },
+ { all = [
+ { contains = ["beta heading"] },
+ { regex = ['beta\s+control\s*\z'] },
+ ] },
+]
+"#,
+ ));
+ for text in [
+ "alpha heading\nalpha control",
+ "beta heading\nbeta\ncontrol",
+ ] {
+ let result = explain(Agent::Codex, text);
+ assert_eq!(result.state, AgentState::Blocked);
+ assert!(result.visible_blocker);
+ }
+ for text in [
+ "alpha heading\nbeta control",
+ "beta heading\nalpha control",
+ "beta control",
+ "beta heading",
+ "beta heading\nbeta control\nnewer content",
+ "beta heading\nfiller one\nfiller two\nbeta control",
+ ] {
+ let result = explain(Agent::Codex, text);
+ assert!(result.matched_rule.is_none(), "{text}");
+ assert!(!result.visible_blocker, "{text}");
+ }
+ });
+}
+
+#[test]
+fn remote_manifest_loads_between_local_override_and_bundled() {
+ with_manifest_dirs("remote-source", || {
+ write_remote_codex(&remote_manifest("9999.01.01.1", "blocked", "remote-ready"));
+
+ let explain = explain(Agent::Codex, "remote-ready");
+
+ assert_eq!(explain.state, AgentState::Blocked);
+ assert!(matches!(
+ explain.source,
+ Some(ManifestSource::Remote { .. })
+ ));
+ assert_eq!(explain.manifest_version.as_deref(), Some("9999.01.01.1"));
+ assert_eq!(
+ explain.cached_remote_version.as_deref(),
+ Some("9999.01.01.1")
+ );
+ });
+}
+
+#[test]
+fn fallback_explain_preserves_active_manifest_version() {
+ with_manifest_dirs("fallback-version", || {
+ write_remote_codex(&remote_manifest("9999.01.01.1", "blocked", "remote-ready"));
+
+ let explain = explain(Agent::Codex, "ordinary prompt text");
+
+ assert_eq!(explain.state, AgentState::Idle);
+ assert_eq!(
+ explain.fallback_reason.as_deref(),
+ Some(DEFAULT_KNOWN_AGENT_IDLE_FALLBACK)
+ );
+ assert_eq!(explain.manifest_version.as_deref(), Some("9999.01.01.1"));
+ assert!(matches!(
+ explain.source,
+ Some(ManifestSource::Remote { .. })
+ ));
+ });
+}
+
+#[test]
+fn older_cached_remote_manifest_does_not_shadow_newer_bundled_manifest() {
+ with_manifest_dirs("older-remote-bundled-fallback", || {
+ write_remote_codex(&remote_manifest("2026.06.10.0", "blocked", "remote-ready"));
+
+ let explain = explain(Agent::Codex, "remote-ready");
+
+ assert!(matches!(explain.source, Some(ManifestSource::Bundled)));
+ assert_eq!(
+ explain.cached_remote_version.as_deref(),
+ Some("2026.06.10.0")
+ );
+ assert!(explain
+ .warning
+ .as_deref()
+ .is_some_and(|warning| warning.contains("older than bundled")));
+ });
+}
+
+#[test]
+fn local_override_shadows_cached_remote_manifest() {
+ with_manifest_dirs("local-shadows-remote", || {
+ write_remote_codex(&remote_manifest("9999.01.01.1", "blocked", "remote-ready"));
+ write_local_codex(&local_manifest("idle", "local-ready"));
+
+ let explain = explain(Agent::Codex, "local-ready");
+
+ assert_eq!(explain.state, AgentState::Idle);
+ assert!(matches!(explain.source, Some(ManifestSource::Override(_))));
+ assert!(explain.local_override_shadowing_remote);
+ assert_eq!(
+ explain.cached_remote_version.as_deref(),
+ Some("9999.01.01.1")
+ );
+ });
+}
+
+#[test]
+fn invalid_local_override_falls_back_to_cached_remote_manifest() {
+ with_manifest_dirs("invalid-local-remote-fallback", || {
+ write_remote_codex(&remote_manifest("9999.01.01.1", "blocked", "remote-ready"));
+ write_local_codex("id = ");
+
+ let explain = explain(Agent::Codex, "remote-ready");
+
+ assert_eq!(explain.state, AgentState::Blocked);
+ assert!(matches!(
+ explain.source,
+ Some(ManifestSource::Remote { .. })
+ ));
+ assert!(explain.warning.is_some());
+ });
+}
+
+#[test]
+fn detection_uses_cached_manifest_until_explicit_reload() {
+ with_manifest_dirs("cache-boundary", || {
+ write_remote_codex(&remote_manifest("9999.01.01.1", "blocked", "cached-ready"));
+
+ let cached = explain(Agent::Codex, "cached-ready");
+ assert_eq!(cached.state, AgentState::Blocked);
+ assert!(matches!(cached.source, Some(ManifestSource::Remote { .. })));
+ assert_eq!(
+ cached.matched_rule.as_ref().map(|rule| rule.id.as_str()),
+ Some("test")
+ );
+
+ write_remote_codex_without_reload(&remote_manifest("9999.01.01.2", "working", "new-ready"));
+
+ let unchanged = explain(Agent::Codex, "new-ready");
+ assert_eq!(unchanged.state, AgentState::Idle);
+ assert_eq!(
+ unchanged.fallback_reason.as_deref(),
+ Some(DEFAULT_KNOWN_AGENT_IDLE_FALLBACK)
+ );
+ assert_eq!(
+ unchanged.cached_remote_version.as_deref(),
+ Some("9999.01.01.1")
+ );
+
+ reload_manifests();
+
+ let reloaded = explain(Agent::Codex, "new-ready");
+ assert_eq!(reloaded.state, AgentState::Working);
+ assert_eq!(
+ reloaded.cached_remote_version.as_deref(),
+ Some("9999.01.01.2")
+ );
+ assert_eq!(
+ reloaded.matched_rule.as_ref().map(|rule| rule.id.as_str()),
+ Some("test")
+ );
+ });
+}
+
+#[test]
+fn compiled_rules_are_shared_until_manifest_reload() {
+ with_manifest_dirs("shared-compiled-rules", || {
+ write_remote_codex(&format!(
+ "{}\nregex = ['^cached-[a-z]+$']\n",
+ remote_manifest("9999.01.01.1", "blocked", "cached-ready")
+ ));
+ let first = load_manifest(Agent::Codex).unwrap();
+ let second = load_manifest(Agent::Codex).unwrap();
+ assert!(!first.compiled_rules.is_empty());
+ assert_eq!(
+ first.compiled_rules.as_ptr(),
+ second.compiled_rules.as_ptr(),
+ "cached loads must retain the same compiled rules and regex search caches"
+ );
+
+ write_remote_codex_without_reload(&format!(
+ "{}\nregex = ['^new-[a-z]+$']\n",
+ remote_manifest("9999.01.01.2", "working", "new-ready")
+ ));
+ let unchanged = load_manifest(Agent::Codex).unwrap();
+ assert_eq!(
+ first.compiled_rules.as_ptr(),
+ unchanged.compiled_rules.as_ptr()
+ );
+
+ reload_manifests_for_agents(&[Agent::Codex]);
+ let reloaded = load_manifest(Agent::Codex).unwrap();
+ let shared_reload = load_manifest(Agent::Codex).unwrap();
+ assert_ne!(
+ first.compiled_rules.as_ptr(),
+ reloaded.compiled_rules.as_ptr()
+ );
+ assert_eq!(
+ reloaded.compiled_rules.as_ptr(),
+ shared_reload.compiled_rules.as_ptr()
+ );
+ assert!(compiled_rule_matches(
+ &first.compiled_rules[0],
+ "cached-ready"
+ ));
+ assert!(!compiled_rule_matches(
+ &first.compiled_rules[0],
+ "new-ready"
+ ));
+ assert_eq!(
+ explain(Agent::Codex, "new-ready").state,
+ AgentState::Working
+ );
+
+ std::thread::scope(|scope| {
+ for _ in 0..4 {
+ let reloaded = &reloaded;
+ scope.spawn(move || {
+ let loaded = load_manifest(Agent::Codex).unwrap();
+ assert_eq!(
+ loaded.compiled_rules.as_ptr(),
+ reloaded.compiled_rules.as_ptr()
+ );
+ for _ in 0..8 {
+ assert_eq!(detect(Agent::Codex, "new-ready").state, AgentState::Working);
+ }
+ });
+ }
+ });
+ });
+}
+
+#[test]
+fn osc_regions_use_separate_inputs_and_share_rule_priority() {
+ with_manifest_dirs("osc-regions", || {
+ write_local_codex(&rules_manifest(
+ r#"
+[[rules]]
+id = "screen"
+state = "idle"
+priority = 10
+region = "whole_recent"
+visible_idle = true
+contains = ["screen-marker"]
+
+[[rules]]
+id = "title"
+state = "working"
+priority = 20
+region = "osc_title"
+visible_working = true
+regex = ['^title-marker$']
+
+[[rules]]
+id = "progress"
+state = "blocked"
+priority = 30
+region = "osc_progress"
+visible_blocker = true
+regex = ['^progress-marker$']
+"#,
+ ));
+ for (screen, title, progress, state, rule) in [
+ ("screen-marker", "", "", AgentState::Idle, "screen"),
+ (
+ "screen-marker",
+ "title-marker",
+ "",
+ AgentState::Working,
+ "title",
+ ),
+ (
+ "screen-marker",
+ "title-marker",
+ "progress-marker",
+ AgentState::Blocked,
+ "progress",
+ ),
+ (
+ "screen-marker title-marker progress-marker",
+ "",
+ "",
+ AgentState::Idle,
+ "screen",
+ ),
+ ] {
+ let input = DetectionInput {
+ screen,
+ osc_title: title,
+ osc_progress: progress,
+ };
+ let result = explain_with_input(Agent::Codex, input);
+ assert_eq!(result.state, state);
+ assert_eq!(
+ result
+ .matched_rule
+ .as_ref()
+ .map(|matched| matched.id.as_str()),
+ Some(rule)
+ );
+ let detection =
+ crate::detect_agent_with_osc(Some(Agent::Codex), screen, title, progress);
+ assert_eq!(detection.state, state);
+ assert_eq!(detection.visible_idle, state == AgentState::Idle);
+ assert_eq!(detection.visible_working, state == AgentState::Working);
+ assert_eq!(detection.visible_blocker, state == AgentState::Blocked);
+ }
+ let swapped = explain_with_input(
+ Agent::Codex,
+ DetectionInput {
+ screen: "",
+ osc_title: "progress-marker",
+ osc_progress: "title-marker",
+ },
+ );
+ assert!(swapped.matched_rule.is_none());
+ });
+}
+
+#[test]
+fn skip_rule_suppresses_state_update_without_visible_state_evidence() {
+ with_manifest_dirs("skip-rule", || {
+ write_local_codex(&rules_manifest(
+ r#"
+[[rules]]
+id = "activity"
+state = "working"
+priority = 10
+visible_working = true
+contains = ["activity-marker"]
+
+[[rules]]
+id = "overlay"
+state = "unknown"
+priority = 20
+skip_state_update = true
+contains = ["overlay-marker"]
+"#,
+ ));
+ let screen = "activity-marker overlay-marker";
+ let result = explain(Agent::Codex, screen);
+ assert_eq!(result.state, AgentState::Unknown);
+ assert!(result.skip_state_update);
+ assert_eq!(
+ result.skipped_update_reason.as_deref(),
+ Some("matched_rule:overlay")
+ );
+ assert!(!result.visible_idle);
+ assert!(!result.visible_working);
+ assert!(!result.visible_blocker);
+ assert!(detect(Agent::Codex, screen).skip_state_update);
+ });
+}
+
+#[test]
+fn screen_regions_extract_structure_without_classifying_agent_state() {
+ for (screen, spec, expected) in [
+ ("old\n\nnew\n", "bottom_lines(2)", "\nnew\n"),
+ (
+ "before\n› input\nafter\n",
+ "after_last_prompt_marker",
+ "after\n",
+ ),
+ (
+ "before\n› input\nafter\n",
+ "before_current_prompt_marker",
+ "before\n",
+ ),
+ (
+ "before\n› input\nafter\n",
+ "whole_recent_without_current_prompt_marker",
+ "",
+ ),
+ (
+ "no marker\n",
+ "whole_recent_without_current_prompt_marker",
+ "no marker\n",
+ ),
+ (
+ "• old\n■ latest\n› input\n",
+ "current_prompt_block_marker",
+ "■ latest",
+ ),
+ (
+ "• old\n■ latest\n› input\n",
+ "after_current_prompt_block_marker",
+ "■ latest\n› input\n",
+ ),
+ ("› old\n• new\n", "current_prompt_block_marker", ""),
+ (
+ "above\n\n───\nbody\n───\nfooter\n",
+ "above_prompt_box",
+ "above\n\n",
+ ),
+ (
+ "above\n\n───\nbody\n───\nfooter\n",
+ "last_non_empty_above_prompt_box",
+ "above",
+ ),
+ (
+ "above\n───\nbody\n───\nfooter\n",
+ "prompt_box_body",
+ "body\n",
+ ),
+ (
+ "above\n───\nbody\n───\nfooter\n",
+ "after_last_horizontal_rule",
+ "footer\n",
+ ),
+ ] {
+ assert_eq!(
+ region(
+ DetectionInput {
+ screen,
+ osc_title: "",
+ osc_progress: ""
+ },
+ spec
+ ),
+ expected,
+ "region={spec}"
+ );
+ }
+}
+
+#[test]
+fn all_bundled_manifests_parse_and_validate() {
+ for agent in Agent::SCREEN_MANIFEST_AGENTS {
+ assert!(
+ bundled_manifest(agent).is_some(),
+ "missing bundled manifest for {}",
+ agent_label(agent)
+ );
+ }
+}
+
+#[test]
+fn manifest_validation_rejects_unknown_fields_empty_rules_invalid_regions_and_regexes() {
+ assert!(parse_manifest(
+ r#"
+id = "codex"
+
+[[rules]]
+id = "typo"
+state = "working"
+contain = ["Working"]
+"#
+ )
+ .is_err());
+ assert!(parse_manifest(
+ r#"
+id = "codex"
+
+[[rules]]
+id = "empty"
+state = "working"
+"#
+ )
+ .is_err());
+ assert!(parse_manifest(
+ r#"
+id = "codex"
+
+[[rules]]
+id = "bad_region"
+state = "working"
+region = "after_last_promt_marker"
+contains = ["Working"]
+"#
+ )
+ .is_err());
+ assert!(parse_manifest(
+ r#"
+id = "codex"
+
+[[rules]]
+id = "bad_regex"
+state = "working"
+regex = ["["]
+"#
+ )
+ .is_err());
+ assert!(parse_manifest(
+ r#"
+id = "codex"
+
+[[rules]]
+id = "bad_nested_regex"
+state = "working"
+any = [{ line_regex = ["["] }]
+"#
+ )
+ .is_err());
+}
+
+#[test]
+fn manifest_validation_keeps_skip_rules_neutral() {
+ assert!(parse_manifest(
+ r#"
+id = "codex"
+
+[[rules]]
+id = "bad_skip_state"
+state = "idle"
+skip_state_update = true
+contains = ["menu"]
+"#
+ )
+ .is_err());
+ assert!(parse_manifest(
+ r#"
+id = "codex"
+
+[[rules]]
+id = "bad_skip_visible"
+state = "unknown"
+skip_state_update = true
+visible_blocker = true
+contains = ["menu"]
+"#
+ )
+ .is_err());
+}
+
+#[test]
+fn manifest_validation_rejects_excessive_rule_count() {
+ let mut manifest = String::from(
+ r#"
+id = "codex"
+"#,
+ );
+ for index in 0..129 {
+ manifest.push_str(&format!(
+ r#"
+[[rules]]
+id = "rule_{index}"
+state = "idle"
+contains = ["ready"]
+"#
+ ));
+ }
+ assert!(parse_manifest(&manifest).is_err());
+}
+
+#[test]
+fn manifest_validation_rejects_excessive_gate_depth() {
+ let manifest = r#"
+id = "codex"
+
+[[rules]]
+id = "deep"
+state = "idle"
+contains = ["ready"]
+all = [
+ { contains = ["1"], all = [
+ { contains = ["2"], all = [
+ { contains = ["3"], all = [
+ { contains = ["4"], all = [
+ { contains = ["5"], all = [
+ { contains = ["6"], all = [
+ { contains = ["7"], all = [
+ { contains = ["8"], all = [
+ { contains = ["9"] },
+ ] },
+ ] },
+ ] },
+ ] },
+ ] },
+ ] },
+ ] },
+ ] },
+]
+"#;
+ assert!(parse_manifest(manifest).is_err());
+}
+
+#[test]
+fn manifest_validation_rejects_excessive_matchers() {
+ let matchers = (0..33)
+ .map(|index| format!(r#""m{index}""#))
+ .collect::<Vec<_>>()
+ .join(", ");
+ let manifest = format!(
+ r#"
+id = "codex"
+
+[[rules]]
+id = "many"
+state = "idle"
+contains = [{matchers}]
+"#
+ );
+ assert!(parse_manifest(&manifest).is_err());
+}
+
+#[test]
+fn bottom_non_empty_lines_uses_bottom_occurrence_for_repeated_text() {
+ let content = "marker\nold\n\nmiddle\nmarker\nnew\n";
+ assert_eq!(
+ region(
+ DetectionInput {
+ screen: content,
+ osc_title: "",
+ osc_progress: ""
+ },
+ "bottom_non_empty_lines(2)"
+ ),
+ "marker\nnew\n"
+ );
+}
+
+#[test]
+fn top_non_empty_lines_uses_top_occurrence_for_repeated_text() {
+ let content = "\nmarker\nold\n\nmiddle\nmarker\nnew\n";
+ assert_eq!(
+ region(
+ DetectionInput {
+ screen: content,
+ osc_title: "",
+ osc_progress: ""
+ },
+ "top_non_empty_lines(2)"
+ ),
+ "\nmarker\nold\n"
+ );
+}
+
+#[test]
+fn top_non_empty_lines_requires_a_canonical_positive_bounded_count() {
+ let name = "top_non_empty_lines";
+ assert!(validate_region_name(&format!("{name}(1)")).is_ok());
+ assert!(validate_region_name(&format!("{name}({})", u16::MAX)).is_ok());
+ for count in ["0", "01", "+1", "65536", "999999999999999999999999"] {
+ assert!(
+ validate_region_name(&format!("{name}({count})")).is_err(),
+ "{name} accepted invalid count {count}"
+ );
+ }
+}
+
+#[test]
+fn top_non_empty_lines_requires_engine_three_when_declared() {
+ let manifest = r#"
+id = "codex"
+version = "1"
+min_engine_version = 2
+
+[[rules]]
+id = "background"
+state = "working"
+region = " top_non_empty_lines(1) "
+contains = ["active"]
+"#;
+ assert!(parse_manifest(manifest).is_err());
+}