diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-10 19:23:13 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-10 19:23:13 +0300 |
| commit | 82468a0e03360219898ebf3735bdf559182009f8 (patch) | |
| tree | 0ee5213fe041a539f23e846f9148493528249512 /src/detect/manifest | |
| parent | 36a1b7f86444871f0c38ff36510a6e3c2eec48c6 (diff) | |
feat: add manifest-based detection engine
Diffstat (limited to 'src/detect/manifest')
| -rw-r--r-- | src/detect/manifest/tests.rs | 204 |
1 files changed, 191 insertions, 13 deletions
diff --git a/src/detect/manifest/tests.rs b/src/detect/manifest/tests.rs index e5b03c0b..0f425384 100644 --- a/src/detect/manifest/tests.rs +++ b/src/detect/manifest/tests.rs @@ -156,7 +156,7 @@ line_regex = ["^exact line$"] #[test] fn remote_manifest_loads_between_local_override_and_bundled() { with_manifest_dirs("remote-source", || { - write_remote_codex(&remote_manifest("2026.06.10.1", "blocked", "remote-ready")); + write_remote_codex(&remote_manifest("2026.06.10.3", "blocked", "remote-ready")); let explain = explain(Agent::Codex, "remote-ready"); @@ -165,10 +165,10 @@ fn remote_manifest_loads_between_local_override_and_bundled() { explain.source, Some(ManifestSource::Remote { .. }) )); - assert_eq!(explain.manifest_version.as_deref(), Some("2026.06.10.1")); + assert_eq!(explain.manifest_version.as_deref(), Some("2026.06.10.3")); assert_eq!( explain.cached_remote_version.as_deref(), - Some("2026.06.10.1") + Some("2026.06.10.3") ); }); } @@ -176,7 +176,7 @@ fn remote_manifest_loads_between_local_override_and_bundled() { #[test] fn fallback_explain_preserves_active_manifest_version() { with_manifest_dirs("fallback-version", || { - write_remote_codex(&remote_manifest("2026.06.10.1", "blocked", "remote-ready")); + write_remote_codex(&remote_manifest("2026.06.10.3", "blocked", "remote-ready")); let explain = explain(Agent::Codex, "ordinary prompt text"); @@ -185,7 +185,7 @@ fn fallback_explain_preserves_active_manifest_version() { explain.fallback_reason.as_deref(), Some(DEFAULT_KNOWN_AGENT_IDLE_FALLBACK) ); - assert_eq!(explain.manifest_version.as_deref(), Some("2026.06.10.1")); + assert_eq!(explain.manifest_version.as_deref(), Some("2026.06.10.3")); assert!(matches!( explain.source, Some(ManifestSource::Remote { .. }) @@ -216,7 +216,7 @@ fn older_cached_remote_manifest_does_not_shadow_newer_bundled_manifest() { #[test] fn local_override_shadows_cached_remote_manifest() { with_manifest_dirs("local-shadows-remote", || { - write_remote_codex(&remote_manifest("2026.06.10.1", "blocked", "remote-ready")); + write_remote_codex(&remote_manifest("2026.06.10.3", "blocked", "remote-ready")); write_local_codex(&local_manifest("idle", "local-ready")); let explain = explain(Agent::Codex, "local-ready"); @@ -226,7 +226,7 @@ fn local_override_shadows_cached_remote_manifest() { assert!(explain.local_override_shadowing_remote); assert_eq!( explain.cached_remote_version.as_deref(), - Some("2026.06.10.1") + Some("2026.06.10.3") ); }); } @@ -234,7 +234,7 @@ fn local_override_shadows_cached_remote_manifest() { #[test] fn invalid_local_override_falls_back_to_cached_remote_manifest() { with_manifest_dirs("invalid-local-remote-fallback", || { - write_remote_codex(&remote_manifest("2026.06.10.1", "blocked", "remote-ready")); + write_remote_codex(&remote_manifest("2026.06.10.3", "blocked", "remote-ready")); write_local_codex("id = "); let explain = explain(Agent::Codex, "remote-ready"); @@ -251,7 +251,7 @@ fn invalid_local_override_falls_back_to_cached_remote_manifest() { #[test] fn detection_uses_cached_manifest_until_explicit_reload() { with_manifest_dirs("cache-boundary", || { - write_remote_codex(&remote_manifest("2026.06.10.1", "blocked", "cached-ready")); + write_remote_codex(&remote_manifest("2026.06.10.3", "blocked", "cached-ready")); let cached = explain(Agent::Codex, "cached-ready"); assert_eq!(cached.state, AgentState::Blocked); @@ -261,7 +261,7 @@ fn detection_uses_cached_manifest_until_explicit_reload() { Some("test") ); - write_remote_codex_without_reload(&remote_manifest("2026.06.10.2", "working", "new-ready")); + write_remote_codex_without_reload(&remote_manifest("2026.06.10.4", "working", "new-ready")); let unchanged = explain(Agent::Codex, "new-ready"); assert_eq!(unchanged.state, AgentState::Idle); @@ -271,7 +271,7 @@ fn detection_uses_cached_manifest_until_explicit_reload() { ); assert_eq!( unchanged.cached_remote_version.as_deref(), - Some("2026.06.10.1") + Some("2026.06.10.3") ); reload_manifests(); @@ -280,7 +280,7 @@ fn detection_uses_cached_manifest_until_explicit_reload() { assert_eq!(reloaded.state, AgentState::Working); assert_eq!( reloaded.cached_remote_version.as_deref(), - Some("2026.06.10.2") + Some("2026.06.10.4") ); assert_eq!( reloaded.matched_rule.as_ref().map(|rule| rule.id.as_str()), @@ -492,7 +492,185 @@ fn bottom_non_empty_lines_uses_bottom_occurrence_for_repeated_text() { let content = "marker\nold\n\nmiddle\nmarker\nnew\n"; assert_eq!( - region(content, "bottom_non_empty_lines(2)"), + region( + DetectionInput { + screen: content, + osc_title: "", + osc_progress: "", + }, + "bottom_non_empty_lines(2)" + ), "marker\nnew\n" ); } + +// --------------------------------------------------------------------------- +// OSC rule tests — exercise the new osc_title / osc_progress regions against +// the bundled Claude and Codex manifests. +// --------------------------------------------------------------------------- + +fn osc_explain( + agent: Agent, + screen: &str, + osc_title: &str, + osc_progress: &str, +) -> DetectionExplain { + explain_with_input( + agent, + DetectionInput { + screen, + osc_title, + osc_progress, + }, + ) +} + +// --- Claude OSC rules --- + +#[test] +fn claude_osc_title_braille_prefix_is_working() { + // "⠂" is U+2802, in the braille block U+2800-U+28FF + let result = osc_explain(Agent::Claude, "", "⠂ project", ""); + assert_eq!(result.state, AgentState::Working); + assert_eq!( + result.matched_rule.as_ref().map(|r| r.id.as_str()), + Some("osc_title_working") + ); + assert!(result.visible_working); +} + +#[test] +fn claude_osc_title_static_prefix_is_idle() { + // "✳" is U+2733, static prefix when Claude is not working + let result = osc_explain(Agent::Claude, "", "✳ Claude Code", ""); + assert_eq!(result.state, AgentState::Idle); + assert_eq!( + result.matched_rule.as_ref().map(|r| r.id.as_str()), + Some("osc_title_idle") + ); + assert!(result.visible_idle); +} + +#[test] +fn claude_osc_progress_4_3_alone_does_not_force_working() { + // Claude leaves progress stuck at 4;3 while waiting for permission, so + // 4;3 must not be a working signal on its own. With no other evidence it + // falls back to idle; blocked screen rules can win when present. + let result = osc_explain(Agent::Claude, "", "", "4;3;"); + assert_eq!(result.state, AgentState::Idle); + assert_eq!( + result.fallback_reason.as_deref(), + Some(DEFAULT_KNOWN_AGENT_IDLE_FALLBACK) + ); + assert!(!result.visible_working); +} + +#[test] +fn claude_blocker_screen_outranks_stale_osc_progress() { + // Regression: progress 4;3 persists during permission prompts. The + // blocked form on screen must win because no rule treats 4;3 as working. + let blocker_screen = + "──────────\n 1. Yes\n 2. No\n\nEnter to select · ↑/↓ to navigate · Esc to cancel\n"; + let result = osc_explain(Agent::Claude, blocker_screen, "✳ Task title", "4;3;"); + assert_eq!(result.state, AgentState::Blocked); + assert!(result.visible_blocker); +} + +#[test] +fn claude_osc_progress_4_0_is_idle() { + let result = osc_explain(Agent::Claude, "", "", "4;0;"); + assert_eq!(result.state, AgentState::Idle); + assert_eq!( + result.matched_rule.as_ref().map(|r| r.id.as_str()), + Some("osc_progress_idle") + ); +} + +#[test] +fn claude_blocker_screen_outranks_osc_idle_title() { + // When the OSC title shows ✳ (idle) but the screen has a bash permission + // prompt, the blocked rule at priority 850 beats osc_title_idle at 250. + let blocker_screen = "do you want to proceed?\n\ + bash command: rm -rf /tmp/test\n\ + ❯ 1. Yes\n 2. No\n\n\ + Esc to cancel · Tab to amend · ctrl+e to explain\n"; + let result = osc_explain(Agent::Claude, blocker_screen, "✳ Claude Code", ""); + assert_eq!(result.state, AgentState::Blocked); + assert!(result.visible_blocker); +} + +#[test] +fn claude_empty_osc_empty_screen_is_idle_fallback() { + // No OSC data, no matching screen rule → fallback idle (unchanged V3 behavior) + let result = osc_explain(Agent::Claude, "", "", ""); + assert_eq!(result.state, AgentState::Idle); + assert_eq!( + result.fallback_reason.as_deref(), + Some(DEFAULT_KNOWN_AGENT_IDLE_FALLBACK) + ); + assert!(!result.visible_idle); +} + +// --- Codex OSC rules --- + +#[test] +fn codex_osc_title_braille_spinner_is_working() { + // "⠋" is U+280B, in the braille block + let result = osc_explain(Agent::Codex, "", "⠋ llm-proxy", ""); + assert_eq!(result.state, AgentState::Working); + assert_eq!( + result.matched_rule.as_ref().map(|r| r.id.as_str()), + Some("osc_title_working") + ); + assert!(result.visible_working); +} + +#[test] +fn codex_osc_title_action_required_is_blocked() { + let result = osc_explain(Agent::Codex, "", "[ . ] Action Required | llm-proxy", ""); + assert_eq!(result.state, AgentState::Blocked); + assert_eq!( + result.matched_rule.as_ref().map(|r| r.id.as_str()), + Some("osc_title_blocked") + ); + assert!(result.visible_blocker); +} + +#[test] +fn codex_osc_title_plain_is_idle() { + let result = osc_explain(Agent::Codex, "", "llm-proxy", ""); + assert_eq!(result.state, AgentState::Idle); + assert_eq!( + result.matched_rule.as_ref().map(|r| r.id.as_str()), + Some("osc_title_idle") + ); + assert!(result.visible_idle); +} + +#[test] +fn codex_background_terminal_screen_beats_osc_idle() { + // Codex doesn't set is_task_running() for background terminals, so title is + // plain (idle). The screen rule background_terminal_working at priority 800 + // beats osc_title_idle at priority 100. + let screen = "background terminal running · /ps to view · /stop to close\n"; + let result = osc_explain(Agent::Codex, screen, "llm-proxy", ""); + assert_eq!(result.state, AgentState::Working); + assert_eq!( + result.matched_rule.as_ref().map(|r| r.id.as_str()), + Some("background_terminal_working") + ); + assert!(result.visible_working); +} + +#[test] +fn codex_osc_working_beats_weak_blocker_screen() { + // A stale [y/n] on screen triggers weak_blocker at priority 600, but an + // active braille spinner in the OSC title is priority 1050 — OSC wins. + let screen = "do you want to continue? [y/n]\n"; + let result = osc_explain(Agent::Codex, screen, "⠋ llm-proxy", ""); + assert_eq!(result.state, AgentState::Working); + assert_eq!( + result.matched_rule.as_ref().map(|r| r.id.as_str()), + Some("osc_title_working") + ); +} |
