Skip to content

Commit a8664f0

Browse files
authored
test(coverage): batch 1–4 — Rust unit tests toward 80% for critical modules (tinyhumansai#530) (tinyhumansai#581)
* test(coverage): phase 1 — webhooks/types, workspace/ops, webhooks/schemas Lines: webhooks/types 0% → 100%; workspace/ops 0% → 96.98%; webhooks/schemas 35.40% → 79.18%. - webhooks/types.rs: serde round-trip tests for WebhookRequest / WebhookResponseData / TunnelRegistration (including the default_webhook_target_kind fallback) / WebhookActivityEntry / WebhookDebugLogEntry / the three debug result wrappers / and WebhookDebugEvent, exercising every `#[serde(default)]` and camel-case rename. - workspace/ops.rs: ensure_workspace_file covers create / leave / force / write-error branches; BOOTSTRAP_FILES contract test locks in SOUL and IDENTITY presence; init_workspace covers fresh-install, idempotent second-call, and forced-overwrite paths against a temp OPENHUMAN_WORKSPACE (serialised via the shared TEST_ENV_LOCK). - webhooks/schemas.rs: catalog integrity (length / namespace / uniqueness / schema↔handler parity), per-function required-field assertions for all 11 RPC methods plus the unknown fallback, and deserialize_params / json_output / to_json coverage. Also fixes a pre-existing test bug in composio/ops.rs discovered while running llvm-cov: fetch_connected_integrations_via_mock_aggregates_tools was not mocking /composio/toolkits, so list_toolkits failed and the expected aggregation never happened. Adds the missing route so the test now observes the 2-integration result it asserts. * test(coverage): phase 2 — webhooks/bus, webhooks/ops Lines: webhooks/bus 0% → 100%; webhooks/ops 14.34% → 97.96%. - webhooks/bus.rs: base64_encode / error_body helpers; Default vs new equivalence; EventHandler name and domain filter; handle() on a non-webhook variant (early return) and on a WebhookIncomingRequest without a registered socket manager (graceful fallthrough). - webhooks/ops.rs: require_token for missing / whitespace / valid stored sessions; the four stub RPC ops (list_registrations, list_logs including ignored-limit, clear_logs, register/unregister_echo) lock in their current payload + log shape; build_echo_response decodes back to the expected echo body and sets the echo-target header; trimmed-input validation for create_tunnel (empty/whitespace name) and the id-bearing ops (get/update/delete); and full mock-backend round-trips for list_tunnels, create_tunnel (trim + drop-whitespace description), get_tunnel, update_tunnel, delete_tunnel, and get_bandwidth, plus a guard asserting authed HTTP calls fail fast without a session token. * test(coverage): phase 3 — voice/postprocess, voice/text_input Lines: voice/postprocess 58.94% → 92.06%; voice/text_input 18.83% → 51.18%. voice/postprocess.rs - Convert existing short-circuit tests to #[tokio::test] so the async function is awaited directly instead of via a freshly-constructed runtime per case. - Add `enabled_but_llm_not_ready_returns_raw_text` for the "cleanup is on but the local LLM hasn't reached ready/degraded yet" branch. - Add five LLM-ready tests that spin up a mock Ollama behind the OPENHUMAN_OLLAMA_BASE_URL override: happy-path cleanup + trim, whitespace-only response fallback, HTTP 500 fallback, conversation context embedded in the prompt, and whitespace-only context ignored. Assertions are permissive about "LLM called → cleaned" vs "LLM short-circuited → raw" because ~30 sibling tests touch the shared `local_ai::global()` singleton without LOCAL_AI_TEST_MUTEX and can race our `state = "ready"` setup. Either branch is documented as acceptable; the function must always return a deterministic String and never panic. Full end-to-end correctness of the cleanup output is pinned by the deterministic short-circuit tests above. voice/text_input.rs - Add `\\t` / `\\n`-only input case and verify the OpenWhispr timing constants (PASTE_DELAY 120ms, CLIPBOARD_RESTORE_DELAY 450ms) so nobody silently shortens them and breaks paste reliability. - macOS: escape_applescript_string backslash/quote/idempotence cases and restore_focus_to_app error path against a bogus app name. - Ceiling note: the clipboard/enigo body of insert_text is not testable in a headless environment (Clipboard::new / Enigo::new fail without a display). Pushing past ~51% here requires dependency-injecting those traits — a production refactor, not a test addition. * test(coverage): batch 4.1 — skills/bus, text_input/{types,ops} Lines: skills/bus 0% → 100%; text_input/types 0% → 100%; text_input/ops 0% → 57.67% (accessibility-gated ceiling). - skills/bus.rs: idempotent no-op for the legacy register_skill_cleanup_subscriber() hook. - text_input/types.rs: FieldBounds ↔ accessibility::ElementBounds round-trip, ReadFieldParams default/omitted-key serde, and round-trips for every request/result struct (InsertText, ShowGhostText, DismissGhostText, AcceptGhostText). - text_input/ops.rs: empty-text guard assertions for insert_text, show_ghost, and accept_ghost; dismiss_ghost idempotent-success contract; plus a deterministic-shape check that the accessibility failure path wraps the error into InsertTextResult rather than panicking. Anything past the guard reaches `accessibility::*`, which requires a live focused field on an OS display and is therefore not reachable in a headless unit-test environment — lifting the ceiling past ~58% requires dependency-injecting the accessibility surface, a production refactor. * test(coverage): batch 4.2 — service/bus, migration/ops, referral/ops Lines: service/bus 0% → 85.25%; migration/ops 0% → 89.71%; referral/ops 0% → 94.63%. - service/bus.rs: RestartSubscriber name and domain metadata, plus two handle() branches that are safe to exercise — non-restart event (early return) and duplicate-suppression (gate already set). Deliberately does NOT cover the success path of a real SystemRestartRequested — it spawns a tokio task that calls std::process::exit(0) and would terminate the test runner. register_restart_subscriber idempotency test confirms the OnceLock guard skips re-registration. - migration/ops.rs: dry-run against an empty temp workspace returns the canonical "migration completed" log; missing source-workspace path exercises the Err-propagation branch. - referral/ops.rs: require_token for missing / whitespace / valid sessions; get_stats + claim_referral guard fast-fail without a session; claim_referral round-trip against a mock backend asserting (a) the referral code is trimmed, (b) a whitespace-only deviceFingerprint is dropped from the outgoing body, and (c) a non-empty fingerprint is trimmed and forwarded. * test(coverage): batch 4.3 — security/ops, service/{bus,ops}, update/{ops,scheduler} Adds unit tests covering: - security/ops: security_policy_info payload shape + default values - service/ops: daemon_host_get/set happy path and error branches - service/bus: fix register_restart_subscriber test to use #[tokio::test] so it has a runtime under `cargo llvm-cov` - update/ops: validate_asset_name and validate_download_url edge cases, update_apply short-circuit guards - update/scheduler: min-interval invariant, disabled-run short-circuit, tick resilience when the event bus is uninitialised All 3583 lib tests pass under `cargo llvm-cov`. Refs tinyhumansai#530. * style: cargo fmt cleanup in coverage test modules Auto-fixes from `cargo fmt` on test code added in batches 4.1–4.3. No behavioral changes. * test(coverage): batch 5.1 — cron/{ops,schemas} - cron/ops.rs: 74.73% → 91.20% (+add_once_at, pause/resume, async cron_list/update/remove/runs, enabled/disabled and empty-id branches) - cron/schemas.rs: 29.11% → 77.00% (all schemas() branches, registry helpers, read_required/read_optional_u64/type_name) 30 new deterministic tests. Refs tinyhumansai#530. * test(coverage): batch 5.2 — memory/{rpc_models,schemas} - memory/rpc_models.rs: 70.91% (targets resolved_limit priority across QueryNamespace/RecallContext/RecallMemories, deny_unknown_fields enforcement, ApiError/ApiMeta/ApiEnvelope round-trips) - memory/schemas.rs: 45.67% (all 31 controller schemas present, registry parity with handlers, parse_params success + error paths, unknown function placeholder) 19 new deterministic tests. Refs tinyhumansai#530. * test(coverage): batch 5.3 — socket/manager webhook router paths - socket/manager.rs: 67.54% (adds set_webhook_router populates/overwrites paths and emit-after-disconnect guard) 3 new deterministic tests. Refs tinyhumansai#530. * test(coverage): batch 5.4 — config/{schemas, schema/observability} - config/schemas.rs: 52.37% (adds required_string / optional_string / optional_bool field builder coverage, exercises deserialize_params across ModelSettings / MemorySettings / WorkspaceOnboarding / SetBrowserAllowAll / OnboardingCompleted params, pins DEFAULT_ONBOARDING_FLAG_NAME constant) - config/schema/observability.rs: 66.67% (default-values invariant, serde defaults for optional fields, explicit analytics flag, round-trip) 16 new deterministic tests. Refs tinyhumansai#530. * test(coverage): batch 5.5 — local_ai/{core,gif_decision} - local_ai/core.rs: 33.33% (pins model_artifact_path structure: models/local-ai dir, `.ollama` suffix, colon→dash normalisation for Windows-safe filenames; Arc sharing across global() calls) - local_ai/gif_decision.rs: 44.04% (trim whitespace in parse, length/word-count boundary cases, tenor_search empty-query guard, local_ai_should_send_gif empty-message early return) 10 new deterministic tests. Refs tinyhumansai#530. * test(coverage): batch 5.6 — local_ai/sentiment, migration/schemas, referral/schemas - local_ai/sentiment.rs: 68.03% (negative-confidence clamp to zero, unknown valence fallback, all documented emotion/valence labels accepted, neutral() constructor invariants, empty-message early return) - migration/schemas.rs: 36.73% (controller registry parity, openclaw input shape, MigrateOpenClawParams defaults + round-trip, unknown function placeholder, to_json wrapping) - referral/schemas.rs: 37.18% (controller registry parity, claim input required/optional mapping, ReferralClaimParams camelCase alias + missing-code rejection, json_output + to_json helpers) 25 new deterministic tests. Refs tinyhumansai#530. * test(coverage): batch 5.7 — tools/traits, local_ai/device - tools/traits.rs: 76.77% (Tool default-method values for permission/scope/category, PermissionLevel total order + default + Display + serde round-trip, ToolCategory default + Display + snake_case serde, ToolScope variant distinctness) - local_ai/device.rs: 63.16% (total_ram_gb truncation for sub-GB and partial-GB, detect_gpu branches for Apple-brand / ARM-on-mac / Intel-Mac, DeviceProfile serde round-trip) 17 new deterministic tests. Refs tinyhumansai#530. * test(coverage): address PR review — tighten assertions, fix flaky test, add env-var RAII guard Inline review fixes: - migration/ops.rs: migrate_openclaw_returns_error_for_missing_source_workspace now requires Err() (the underlying helper bails when the source dir doesn't exist) and asserts a non-empty error message. - text_input/ops.rs: replace tautological `!inserted || inserted` in insert_text_surfaces_accessibility_failure_as_inserted_false with the real contract: require Ok(..) and pin `inserted`↔`error` mutual exclusion. Headless runs legitimately see inserted=false, so the assertion still holds while catching regressions in either branch. - update/scheduler.rs: remove tick_runs_without_panicking_when_event_bus_is_uninitialised — it hit real api.github.com HTTPS and was flaky under offline CI / rate limits. Replace with a comment documenting the decision and the integration-test path for exercising tick(). Nitpick fixes: - security/ops.rs: extend security_policy_info_matches_default_policy_values with assertions for autonomy and allowed_commands so the full default shape is pinned. - voice/postprocess.rs: tighten ready_llm_with_whitespace_only_context_never_embeds_header from `assert_eq!(result.trim(), "raw text")` to exact equality (cleanup_transcription trims internally). The pre-existing doc comment on with_ready_llm plus the in-module block comment at lines 255-268 already document the LOCAL_AI_TEST_MUTEX contract and the deliberate non-use in permissive tests — no new docs needed. - webhooks/ops.rs: list_tunnels_hits_webhooks_core_endpoint_and_returns_payload now asserts the inbound Authorization header equals `Bearer test-session-token`; get_tunnel_encodes_id_in_path uses an id full of reserved URL chars so the test actually verifies percent-encoding rather than just trimming. - workspace/ops.rs: introduce a WorkspaceEnvGuard RAII helper; replace all six unsafe set_var/remove_var pairs in the init_workspace tests so OPENHUMAN_WORKSPACE is cleared on panic too. Contract is documented inline: guard requires the caller to hold ENV_LOCK. Refs tinyhumansai#530. * test(coverage): tighten sentry_dsn round-trip assertion to exact value round_trip_preserves_all_fields previously only checked `back.sentry_dsn.is_some()`, which would still pass if serde dropped or corrupted the DSN string. Compare the full decoded value instead so any regression in the Option<String> path is caught. Refs tinyhumansai#530.
1 parent 19aa50a commit a8664f0

31 files changed

Lines changed: 3719 additions & 20 deletions

File tree

src/openhuman/config/schema/observability.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,66 @@ impl Default for ObservabilityConfig {
4141
}
4242
}
4343
}
44+
45+
#[cfg(test)]
46+
mod tests {
47+
use super::*;
48+
use serde_json::json;
49+
50+
#[test]
51+
fn default_disables_backend_and_enables_analytics() {
52+
let cfg = ObservabilityConfig::default();
53+
assert_eq!(cfg.backend, "none");
54+
assert!(cfg.otel_endpoint.is_none());
55+
assert!(cfg.otel_service_name.is_none());
56+
assert!(cfg.sentry_dsn.is_none());
57+
assert!(cfg.analytics_enabled);
58+
}
59+
60+
#[test]
61+
fn default_analytics_enabled_helper_returns_true() {
62+
assert!(default_analytics_enabled());
63+
}
64+
65+
#[test]
66+
fn deserialize_missing_optional_fields_uses_defaults() {
67+
let cfg: ObservabilityConfig = serde_json::from_value(json!({
68+
"backend": "log"
69+
}))
70+
.unwrap();
71+
assert_eq!(cfg.backend, "log");
72+
assert!(cfg.otel_endpoint.is_none());
73+
assert!(cfg.analytics_enabled, "analytics default must be true");
74+
}
75+
76+
#[test]
77+
fn deserialize_respects_explicit_analytics_flag() {
78+
let cfg: ObservabilityConfig = serde_json::from_value(json!({
79+
"backend": "otel",
80+
"analytics_enabled": false
81+
}))
82+
.unwrap();
83+
assert!(!cfg.analytics_enabled);
84+
}
85+
86+
#[test]
87+
fn round_trip_preserves_all_fields() {
88+
let original = ObservabilityConfig {
89+
backend: "otel".into(),
90+
otel_endpoint: Some("http://localhost:4318".into()),
91+
otel_service_name: Some("openhuman-test".into()),
92+
sentry_dsn: Some("https://token@sentry.io/1".into()),
93+
analytics_enabled: false,
94+
};
95+
let s = serde_json::to_string(&original).unwrap();
96+
let back: ObservabilityConfig = serde_json::from_str(&s).unwrap();
97+
assert_eq!(back.backend, "otel");
98+
assert_eq!(back.otel_endpoint.as_deref(), Some("http://localhost:4318"));
99+
assert_eq!(back.otel_service_name.as_deref(), Some("openhuman-test"));
100+
assert_eq!(
101+
back.sentry_dsn.as_deref(),
102+
Some("https://token@sentry.io/1")
103+
);
104+
assert!(!back.analytics_enabled);
105+
}
106+
}

src/openhuman/config/schemas.rs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -870,4 +870,117 @@ mod tests {
870870
.expect("serialize");
871871
assert!(v.get("logs").is_some() || v.get("result").is_some());
872872
}
873+
874+
// ── Field builder helpers ────────────────────────────────────
875+
876+
#[test]
877+
fn required_string_builds_required_string_field() {
878+
let f = required_string("api_key", "Auth key");
879+
assert_eq!(f.name, "api_key");
880+
assert_eq!(f.comment, "Auth key");
881+
assert!(f.required);
882+
assert!(matches!(f.ty, TypeSchema::String));
883+
}
884+
885+
#[test]
886+
fn optional_string_builds_option_string_field() {
887+
let f = optional_string("model", "model name");
888+
assert!(!f.required);
889+
match &f.ty {
890+
TypeSchema::Option(inner) => assert!(matches!(**inner, TypeSchema::String)),
891+
other => panic!("expected Option<String>, got {other:?}"),
892+
}
893+
}
894+
895+
#[test]
896+
fn optional_bool_builds_option_bool_field() {
897+
let f = optional_bool("enabled", "Whether enabled");
898+
assert!(!f.required);
899+
match &f.ty {
900+
TypeSchema::Option(inner) => assert!(matches!(**inner, TypeSchema::Bool)),
901+
other => panic!("expected Option<Bool>, got {other:?}"),
902+
}
903+
}
904+
905+
// ── deserialize_params helper ────────────────────────────────
906+
907+
#[test]
908+
fn deserialize_params_parses_model_settings_update() {
909+
let mut m = Map::new();
910+
m.insert("api_key".into(), Value::String("sk-123".into()));
911+
m.insert(
912+
"default_temperature".into(),
913+
Value::Number(serde_json::Number::from_f64(0.7).unwrap()),
914+
);
915+
let out: ModelSettingsUpdate = deserialize_params(m).unwrap();
916+
assert_eq!(out.api_key.as_deref(), Some("sk-123"));
917+
assert_eq!(out.default_temperature, Some(0.7));
918+
assert!(out.api_url.is_none());
919+
assert!(out.default_model.is_none());
920+
}
921+
922+
#[test]
923+
fn deserialize_params_parses_memory_settings_update() {
924+
let mut m = Map::new();
925+
m.insert("backend".into(), Value::String("sqlite".into()));
926+
m.insert("auto_save".into(), Value::Bool(true));
927+
m.insert(
928+
"embedding_dimensions".into(),
929+
Value::Number(serde_json::Number::from(1536)),
930+
);
931+
let out: MemorySettingsUpdate = deserialize_params(m).unwrap();
932+
assert_eq!(out.backend.as_deref(), Some("sqlite"));
933+
assert_eq!(out.auto_save, Some(true));
934+
assert_eq!(out.embedding_dimensions, Some(1536));
935+
}
936+
937+
#[test]
938+
fn deserialize_params_parses_workspace_onboarding_flag_params() {
939+
let out: WorkspaceOnboardingFlagParams = deserialize_params(Map::new()).unwrap();
940+
assert!(out.flag_name.is_none());
941+
942+
let mut m = Map::new();
943+
m.insert("flag_name".into(), Value::String(".custom_marker".into()));
944+
let out: WorkspaceOnboardingFlagParams = deserialize_params(m).unwrap();
945+
assert_eq!(out.flag_name.as_deref(), Some(".custom_marker"));
946+
}
947+
948+
#[test]
949+
fn deserialize_params_parses_workspace_onboarding_flag_set_params() {
950+
let mut m = Map::new();
951+
m.insert("value".into(), Value::Bool(true));
952+
let out: WorkspaceOnboardingFlagSetParams = deserialize_params(m).unwrap();
953+
assert_eq!(out.value, true);
954+
assert!(out.flag_name.is_none());
955+
}
956+
957+
#[test]
958+
fn deserialize_params_rejects_wrong_types_with_invalid_params_prefix() {
959+
let mut m = Map::new();
960+
m.insert(
961+
"default_temperature".into(),
962+
Value::String("not-a-number".into()),
963+
);
964+
let err = deserialize_params::<ModelSettingsUpdate>(m).unwrap_err();
965+
assert!(err.starts_with("invalid params"));
966+
}
967+
968+
#[test]
969+
fn deserialize_params_requires_value_on_set_onboarding() {
970+
let err = deserialize_params::<OnboardingCompletedSetParams>(Map::new()).unwrap_err();
971+
assert!(err.contains("invalid params"));
972+
}
973+
974+
#[test]
975+
fn deserialize_params_rejects_missing_required_for_set_browser_allow_all() {
976+
let err = deserialize_params::<SetBrowserAllowAllParams>(Map::new()).unwrap_err();
977+
assert!(err.contains("invalid params"));
978+
}
979+
980+
#[test]
981+
fn default_onboarding_flag_constant_points_to_hidden_marker() {
982+
// Keeps the constant's observable value pinned so tool behavior
983+
// stays stable across refactors.
984+
assert_eq!(DEFAULT_ONBOARDING_FLAG_NAME, ".skip_onboarding");
985+
}
873986
}

src/openhuman/cron/ops.rs

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,4 +448,146 @@ mod tests {
448448
assert!(add_once(&config, "", "cmd").is_err());
449449
assert!(add_once(&config, "5x", "cmd").is_err());
450450
}
451+
452+
// ── add_once_at ─────────────────────────────────────────────────
453+
454+
#[test]
455+
fn add_once_at_stores_exact_timestamp() {
456+
let tmp = TempDir::new().unwrap();
457+
let config = test_config(&tmp);
458+
let when = chrono::Utc::now() + chrono::Duration::hours(1);
459+
let job = add_once_at(&config, when, "echo hi").unwrap();
460+
match job.schedule {
461+
Schedule::At { at } => assert_eq!(at, when),
462+
other => panic!("expected At schedule, got {other:?}"),
463+
}
464+
}
465+
466+
// ── pause_job / resume_job ──────────────────────────────────────
467+
468+
#[test]
469+
fn pause_and_resume_toggle_enabled_flag() {
470+
let tmp = TempDir::new().unwrap();
471+
let config = test_config(&tmp);
472+
let job = make_job(&config, "*/5 * * * *", None, "echo test");
473+
assert!(job.enabled);
474+
475+
let paused = pause_job(&config, &job.id).unwrap();
476+
assert!(!paused.enabled);
477+
478+
let resumed = resume_job(&config, &job.id).unwrap();
479+
assert!(resumed.enabled);
480+
}
481+
482+
// ── cron_list / cron_update / cron_remove / cron_runs ───────────
483+
484+
fn disabled_cron_config(tmp: &TempDir) -> Config {
485+
let mut config = test_config(tmp);
486+
config.cron.enabled = false;
487+
config
488+
}
489+
490+
#[tokio::test]
491+
async fn cron_list_errors_when_cron_disabled() {
492+
let tmp = TempDir::new().unwrap();
493+
let config = disabled_cron_config(&tmp);
494+
let err = cron_list(&config).await.unwrap_err();
495+
assert!(err.contains("cron is disabled"));
496+
}
497+
498+
#[tokio::test]
499+
async fn cron_list_returns_jobs_when_enabled() {
500+
let tmp = TempDir::new().unwrap();
501+
let config = test_config(&tmp);
502+
let job = make_job(&config, "*/5 * * * *", None, "echo test");
503+
let out = cron_list(&config).await.unwrap();
504+
assert!(out.value.iter().any(|j| j.id == job.id));
505+
assert!(out.logs.iter().any(|l| l.contains("cron jobs listed")));
506+
}
507+
508+
#[tokio::test]
509+
async fn cron_update_rejects_empty_job_id() {
510+
let tmp = TempDir::new().unwrap();
511+
let config = test_config(&tmp);
512+
let err = cron_update(&config, " ", CronJobPatch::default())
513+
.await
514+
.unwrap_err();
515+
assert!(err.contains("Missing 'job_id'"));
516+
}
517+
518+
#[tokio::test]
519+
async fn cron_update_errors_when_cron_disabled() {
520+
let tmp = TempDir::new().unwrap();
521+
let config = disabled_cron_config(&tmp);
522+
let err = cron_update(&config, "some-id", CronJobPatch::default())
523+
.await
524+
.unwrap_err();
525+
assert!(err.contains("cron is disabled"));
526+
}
527+
528+
#[tokio::test]
529+
async fn cron_update_mutates_existing_job() {
530+
let tmp = TempDir::new().unwrap();
531+
let config = test_config(&tmp);
532+
let job = make_job(&config, "*/5 * * * *", None, "echo test");
533+
let patch = CronJobPatch {
534+
name: Some("renamed".to_string()),
535+
..CronJobPatch::default()
536+
};
537+
let out = cron_update(&config, &job.id, patch).await.unwrap();
538+
assert_eq!(out.value.name.as_deref(), Some("renamed"));
539+
assert!(out.logs.iter().any(|l| l.contains("cron job updated")));
540+
}
541+
542+
#[tokio::test]
543+
async fn cron_remove_rejects_empty_job_id() {
544+
let tmp = TempDir::new().unwrap();
545+
let config = test_config(&tmp);
546+
let err = cron_remove(&config, "").await.unwrap_err();
547+
assert!(err.contains("Missing 'job_id'"));
548+
}
549+
550+
#[tokio::test]
551+
async fn cron_remove_errors_when_cron_disabled() {
552+
let tmp = TempDir::new().unwrap();
553+
let config = disabled_cron_config(&tmp);
554+
let err = cron_remove(&config, "abc").await.unwrap_err();
555+
assert!(err.contains("cron is disabled"));
556+
}
557+
558+
#[tokio::test]
559+
async fn cron_remove_returns_removed_true_on_success() {
560+
let tmp = TempDir::new().unwrap();
561+
let config = test_config(&tmp);
562+
let job = make_job(&config, "*/5 * * * *", None, "echo test");
563+
let out = cron_remove(&config, &job.id).await.unwrap();
564+
assert_eq!(out.value["job_id"], json!(job.id));
565+
assert_eq!(out.value["removed"], json!(true));
566+
}
567+
568+
#[tokio::test]
569+
async fn cron_runs_rejects_empty_job_id() {
570+
let tmp = TempDir::new().unwrap();
571+
let config = test_config(&tmp);
572+
let err = cron_runs(&config, "", None).await.unwrap_err();
573+
assert!(err.contains("Missing 'job_id'"));
574+
}
575+
576+
#[tokio::test]
577+
async fn cron_runs_errors_when_cron_disabled() {
578+
let tmp = TempDir::new().unwrap();
579+
let config = disabled_cron_config(&tmp);
580+
let err = cron_runs(&config, "abc", Some(5)).await.unwrap_err();
581+
assert!(err.contains("cron is disabled"));
582+
}
583+
584+
#[tokio::test]
585+
async fn cron_runs_returns_empty_history_for_new_job() {
586+
let tmp = TempDir::new().unwrap();
587+
let config = test_config(&tmp);
588+
let job = make_job(&config, "*/5 * * * *", None, "echo test");
589+
let out = cron_runs(&config, &job.id, Some(10)).await.unwrap();
590+
assert!(out.value.is_empty());
591+
assert!(out.logs.iter().any(|l| l.contains("cron run history")));
592+
}
451593
}

0 commit comments

Comments
 (0)