diff --git a/src/automation/runner.rs b/src/automation/runner.rs index 1ed540a3..31535a4a 100644 --- a/src/automation/runner.rs +++ b/src/automation/runner.rs @@ -108,6 +108,10 @@ pub struct SkillWriterAutomationOptions { pub trigger: AutomationTrigger, #[serde(default, skip_serializing_if = "Option::is_none")] pub run_id: Option, + #[serde(default = "default_lcm_storage_scope")] + pub storage_scope: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub hermes_home: Option, #[serde(default = "default_skill_writer_provider")] pub provider: String, #[serde(default = "default_skill_writer_query")] @@ -123,6 +127,8 @@ impl Default for SkillWriterAutomationOptions { Self { trigger: AutomationTrigger::ManualCli, run_id: None, + storage_scope: default_lcm_storage_scope(), + hermes_home: None, provider: default_skill_writer_provider(), query: default_skill_writer_query(), evidence_limit: default_skill_writer_evidence_limit(), @@ -154,7 +160,7 @@ enum SkillWriterEvidenceOutcome { }, } -enum SessionReflectorLcmStore { +enum LcmAutomationStore { Available(PathBuf), NotIngested, } @@ -190,15 +196,17 @@ pub async fn run_session_reflector_with_backend( } }; - let sessions_db_path = match session_reflector_lcm_db_path(cg, &storage_scope, &options)? { - SessionReflectorLcmStore::Available(path) => path, - SessionReflectorLcmStore::NotIngested => { + let sessions_db_path = match automation_lcm_db_path( + cg, + &storage_scope, + options.hermes_home.as_ref(), + "session_reflector", + )? { + LcmAutomationStore::Available(path) => path, + LcmAutomationStore::NotIngested => { return skipped_session_reflector_run(&run, "lcm_not_ingested", None).await; } }; - if !sessions_db_path.is_file() { - return skipped_session_reflector_run(&run, "lcm_not_ingested", None).await; - } let Some(lcm_db) = GlobalDb::open_read_only_at(&sessions_db_path).await else { return skipped_session_reflector_run(&run, "lcm_unavailable", None).await; }; @@ -500,18 +508,27 @@ async fn build_skill_writer_evidence( Some(path) => path, None => crate::storage::default_profile_root()?, }; + let storage_scope = + normalized_non_empty(&options.storage_scope).unwrap_or_else(default_lcm_storage_scope); let provider = normalized_non_empty(&options.provider).unwrap_or_else(default_skill_writer_provider); let query = normalized_non_empty(&options.query).unwrap_or_else(default_skill_writer_query); let evidence_limit = options.evidence_limit.clamp(1, 50); - let sessions_db_path = cg.store_layout().sessions_db_path.clone(); - if !sessions_db_path.is_file() { - return Ok(SkillWriterEvidenceOutcome::Skipped { - reason: "lcm_not_ingested", - evidence_hash: None, - }); - } + let sessions_db_path = match automation_lcm_db_path( + cg, + &storage_scope, + options.hermes_home.as_ref(), + "skill_writer", + )? { + LcmAutomationStore::Available(path) => path, + LcmAutomationStore::NotIngested => { + return Ok(SkillWriterEvidenceOutcome::Skipped { + reason: "lcm_not_ingested", + evidence_hash: None, + }); + } + }; let Some(lcm_db) = GlobalDb::open_read_only_at(&sessions_db_path).await else { return Ok(SkillWriterEvidenceOutcome::Skipped { reason: "lcm_unavailable", @@ -569,6 +586,8 @@ async fn build_skill_writer_evidence( &underused_tool_families, ); let evidence = json!({ + "storage_scope": storage_scope, + "hermes_home": options.hermes_home.as_ref().map(|path| path.display().to_string()), "provider": provider, "query": query, "hits": hits, @@ -650,7 +669,7 @@ async fn skipped_skill_writer_run( fn build_session_reflector_prompt(evidence: &Value) -> String { format!( - "Review these bounded TraceDecay session snippets and propose only durable memory facts. Return only JSON with a facts array. Each fact must include content, category, optional tags, optional entities, trust, source_span, and reason. Category must be one of general, user_pref, project, tool, decision, or code_area. Use trust, not confidence. source_span must cite one bounded evidence hit by session_id plus message_id for raw messages, by store_id for raw messages, or by node_id for summaries. Do not include secrets or ephemeral status.\n{}", + "Review these bounded TraceDecay session snippets and propose only durable memory facts. Return only JSON with a facts array. Each fact must include content, category, optional tags, optional entities, trust, source_span, and reason. Category must be one of general, user_pref, project, tool, decision, or code_area. Use trust, not confidence; trust must be a JSON number from 0.0 to 1.0. Do not use string labels like high, medium, or low. source_span must cite one bounded evidence hit by session_id plus message_id for raw messages, by store_id for raw messages, or by node_id for summaries. Do not include secrets or ephemeral status.\n{}", serde_json::to_string_pretty(evidence).unwrap_or_else(|_| "{}".to_string()) ) } @@ -671,30 +690,39 @@ fn normalized_non_empty(value: &str) -> Option { } } -fn session_reflector_lcm_db_path( +/// Resolves the LCM sessions database for an automation task, reporting +/// `NotIngested` when the store does not exist yet so callers can skip +/// without re-checking the path. +fn automation_lcm_db_path( cg: &TraceDecay, storage_scope: &str, - options: &SessionReflectorAutomationOptions, -) -> Result { + hermes_home: Option<&PathBuf>, + task_name: &str, +) -> Result { match storage_scope { - "project_local" => Ok(SessionReflectorLcmStore::Available( - cg.store_layout().sessions_db_path.clone(), - )), + "project_local" => { + let path = cg.store_layout().sessions_db_path.clone(); + if path.is_file() { + Ok(LcmAutomationStore::Available(path)) + } else { + Ok(LcmAutomationStore::NotIngested) + } + } "hermes_profile" => { - let hermes_home = options.hermes_home.as_ref().ok_or_else(|| TraceDecayError::Config { - message: "session_reflector hermes_profile storage requires hermes_home".to_string(), + let hermes_home = hermes_home.ok_or_else(|| TraceDecayError::Config { + message: format!("{task_name} hermes_profile storage requires hermes_home"), })?; match resolve_hermes_profile_session_db_readonly(hermes_home) { - HermesProfileDbReadOnly::Exists(path) => Ok(SessionReflectorLcmStore::Available(path)), - HermesProfileDbReadOnly::NotIngested(_) => Ok(SessionReflectorLcmStore::NotIngested), + HermesProfileDbReadOnly::Exists(path) => Ok(LcmAutomationStore::Available(path)), + HermesProfileDbReadOnly::NotIngested(_) => Ok(LcmAutomationStore::NotIngested), HermesProfileDbReadOnly::ConfigError(message) => Err(TraceDecayError::Config { - message: format!("invalid session_reflector hermes_home: {message}"), + message: format!("invalid {task_name} hermes_home: {message}"), }), } } other => Err(TraceDecayError::Config { message: format!( - "unknown session_reflector storage_scope '{other}'; expected project_local or hermes_profile" + "unknown {task_name} storage_scope '{other}'; expected project_local or hermes_profile" ), }), } diff --git a/src/automation_cli.rs b/src/automation_cli.rs index 539ad049..35642c74 100644 --- a/src/automation_cli.rs +++ b/src/automation_cli.rs @@ -500,7 +500,7 @@ async fn handle_automation_run_command( trigger: tracedecay::automation::run_ledger::AutomationTrigger::ManualCli, run_id: None, storage_scope, - hermes_home: hermes_home.map(Into::into), + hermes_home, provider, query, scope: lcm_scope, @@ -522,6 +522,8 @@ async fn handle_automation_run_command( provider, query, evidence_limit, + storage_scope, + hermes_home, path, } => { if !dry_run { @@ -550,6 +552,8 @@ async fn handle_automation_run_command( SkillWriterAutomationOptions { trigger: tracedecay::automation::run_ledger::AutomationTrigger::ManualCli, run_id: None, + storage_scope, + hermes_home, provider, query, evidence_limit, diff --git a/src/cli/automation.rs b/src/cli/automation.rs index 4e14a936..e20ff377 100644 --- a/src/cli/automation.rs +++ b/src/cli/automation.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use clap::{Subcommand, ValueEnum}; #[allow(clippy::large_enum_variant)] @@ -215,7 +217,7 @@ pub enum AutomationRunAction { storage_scope: String, /// Absolute Hermes profile home directory when --storage-scope hermes_profile. #[arg(long)] - hermes_home: Option, + hermes_home: Option, /// LCM grep scope: all, session, or current. #[arg(long, default_value = "all")] scope: String, @@ -250,8 +252,8 @@ pub enum AutomationRunAction { /// Keep the run non-mutating. This is currently the only supported mode. #[arg(long, default_value_t = true, action = clap::ArgAction::Set)] dry_run: bool, - /// LCM provider to inspect. - #[arg(long, default_value = "cursor")] + /// LCM provider to inspect. Use all for unified cross-provider evidence. + #[arg(long, default_value = "all")] provider: String, /// LCM grep query used to collect bounded evidence. #[arg( @@ -262,6 +264,12 @@ pub enum AutomationRunAction { /// Maximum LCM evidence snippets included in the backend review request. #[arg(long, default_value_t = 20)] evidence_limit: usize, + /// LCM storage scope: project_local or hermes_profile. + #[arg(long, default_value = "project_local")] + storage_scope: String, + /// Absolute Hermes profile home directory when --storage-scope hermes_profile. + #[arg(long)] + hermes_home: Option, /// Project path (default: current directory, with discovery). #[arg(short, long)] path: Option, diff --git a/src/cli/parse_tests.rs b/src/cli/parse_tests.rs index 862ce41a..ad6f2076 100644 --- a/src/cli/parse_tests.rs +++ b/src/cli/parse_tests.rs @@ -1,3 +1,5 @@ +use std::path::Path; + use super::{ AutomationAction, AutomationConfigAction, AutomationConfigScope, AutomationRunAction, AutomationRunsAction, AutomationSkillsAction, AutomationSkillsInstallTarget, BranchAction, Cli, @@ -695,7 +697,7 @@ fn automation_run_session_reflection_parses_manual_dry_run_flags() { && query == "remember decisions" && evidence_limit == 12 && storage_scope == "hermes_profile" - && hermes_home.as_deref() == Some("/tmp/hermes-profile") + && hermes_home.as_deref() == Some(Path::new("/tmp/hermes-profile")) && scope == "session" && session_id.as_deref() == Some("session-123") && !include_summaries @@ -739,6 +741,8 @@ fn automation_run_skill_writing_parses_manual_dry_run_flags() { provider, query, evidence_limit, + storage_scope, + hermes_home, path, } } @@ -746,10 +750,34 @@ fn automation_run_skill_writing_parses_manual_dry_run_flags() { && provider == "cursor" && query == "workflow corrections" && evidence_limit == 9 + && storage_scope == "project_local" + && hermes_home.is_none() && path.as_deref() == Some("/tmp/project") )); } +#[test] +fn automation_run_skill_writing_defaults_to_all_providers() { + let cli = Cli::try_parse_from(["tracedecay", "automation", "run", "skill-writing"]) + .expect("automation skill-writing run should parse with defaults"); + + assert!(matches!( + cli.command, + Some(Commands::Automation { + action: + AutomationAction::Run { + action: + AutomationRunAction::SkillWriting { + provider, + storage_scope, + hermes_home, + .. + } + } + }) if provider == "all" && storage_scope == "project_local" && hermes_home.is_none() + )); +} + #[test] fn automation_runs_commands_parse_history_flags() { let list = Cli::try_parse_from([ diff --git a/src/dashboard/automation_run_api.rs b/src/dashboard/automation_run_api.rs index e7737e3e..45f8e4a8 100644 --- a/src/dashboard/automation_run_api.rs +++ b/src/dashboard/automation_run_api.rs @@ -106,6 +106,8 @@ pub(crate) struct SkillWritingRunBody { provider: Option, query: Option, evidence_limit: Option, + storage_scope: Option, + hermes_home: Option, } impl From for SkillWritingRunRequest { @@ -114,6 +116,8 @@ impl From for SkillWritingRunRequest { provider: body.provider, query: body.query, evidence_limit: body.evidence_limit, + storage_scope: body.storage_scope, + hermes_home: body.hermes_home, } } } diff --git a/src/dashboard/automation_run_service.rs b/src/dashboard/automation_run_service.rs index 593f48e6..59cd55e2 100644 --- a/src/dashboard/automation_run_service.rs +++ b/src/dashboard/automation_run_service.rs @@ -250,6 +250,8 @@ pub(crate) struct SkillWritingRunRequest { pub provider: Option, pub query: Option, pub evidence_limit: Option, + pub storage_scope: Option, + pub hermes_home: Option, } pub(crate) async fn session_reflection_run_payload_with_run_id( @@ -395,6 +397,12 @@ pub(crate) async fn skill_writing_run_payload_with_run_id( if let Some(evidence_limit) = request.evidence_limit { options.evidence_limit = evidence_limit; } + if let Some(storage_scope) = request.storage_scope { + options.storage_scope = storage_scope; + } + if let Some(hermes_home) = request.hermes_home { + options.hermes_home = Some(hermes_home); + } let run = match run_skill_writer_with_backend( &run_context.cg, &run_context.config, diff --git a/tests/automation_runner_support/mod.rs b/tests/automation_runner_support/mod.rs index 61b4ed2b..94c19ff6 100644 --- a/tests/automation_runner_support/mod.rs +++ b/tests/automation_runner_support/mod.rs @@ -752,6 +752,37 @@ pub(crate) async fn read_artifact( .unwrap() } +/// Standalone automation config with only the skill writer task enabled on a +/// manual schedule; override fields with struct update syntax where needed. +pub(crate) fn enabled_skill_writer_config() -> AutomationConfig { + AutomationConfig { + enabled: true, + backend: AutomationBackend::CodexAppServer, + host_mode: AutomationHostMode::Standalone, + tasks: AutomationTaskSet { + skill_writer: AutomationTaskConfig { + enabled: true, + schedule: Some("manual".to_string()), + ..AutomationTaskConfig::default() + }, + ..AutomationTaskSet::default() + }, + ..AutomationConfig::default() + } +} + +/// Manual-trigger skill writer options matching the seeded "automation" fixture +/// evidence, rooted at the given managed-skill profile directory. +pub(crate) fn manual_skill_writer_options(profile_root: &Path) -> SkillWriterAutomationOptions { + SkillWriterAutomationOptions { + provider: "cursor".to_string(), + query: "automation".to_string(), + evidence_limit: 5, + profile_root: Some(profile_root.to_path_buf()), + ..SkillWriterAutomationOptions::default() + } +} + pub(crate) fn scheduler_config( interval_secs: Option, cooldown_secs: Option, diff --git a/tests/automation_session_reflector_runner_test.rs b/tests/automation_session_reflector_runner_test.rs index bad2ae0f..73b2ed56 100644 --- a/tests/automation_session_reflector_runner_test.rs +++ b/tests/automation_session_reflector_runner_test.rs @@ -93,6 +93,15 @@ async fn session_reflector_runner_validates_fact_proposals_without_applying() { "source_span": {"session_id": "session-reflect-1", "message_id": "session-reflect-1-message-001"}, "reason": "missing trust should be rejected" }, + { + "content": "Session reflection trust must be a numeric score", + "category": "project", + "tags": ["automation"], + "entities": ["TraceDecay"], + "trust": "high", + "source_span": {"session_id": "session-reflect-1", "message_id": "session-reflect-1-message-001"}, + "reason": "string trust labels should be rejected" + }, { "content": "Session reflection facts require a rationale", "category": "project", @@ -153,7 +162,7 @@ async fn session_reflector_runner_validates_fact_proposals_without_applying() { assert_eq!(run.ledger_record.task, AgentTaskKind::SessionReflector); assert_eq!(run.ledger_record.status, AutomationRunStatus::Succeeded); assert_eq!(run.ledger_record.accepted_count, 2); - assert_eq!(run.ledger_record.rejected_count, 7); + assert_eq!(run.ledger_record.rejected_count, 8); assert_eq!( run.report["accepted_facts"][0]["add_fact_request"]["source"], json!("session_reflector") @@ -189,6 +198,7 @@ async fn session_reflector_runner_validates_fact_proposals_without_applying() { "source_span must cite a bounded session reflection evidence hit" )); assert!(has_rejection_reason("trust is required")); + assert!(has_rejection_reason("trust must be between 0 and 1")); assert!(has_rejection_reason("reason is required")); assert!(has_rejection_reason( "confidence is not supported; use trust" @@ -249,7 +259,7 @@ async fn session_reflector_runner_validates_fact_proposals_without_applying() { ); let eval_payload = read_artifact(&cg, &run.run_id, &run.ledger_record, "generated_evals").await; assert_eq!(eval_payload["task"], json!("session_reflector")); - assert_eq!(eval_payload["summary"]["eval_count"], json!(9)); + assert_eq!(eval_payload["summary"]["eval_count"], json!(10)); assert!(eval_payload["eval_definitions"] .as_array() .unwrap() @@ -329,7 +339,7 @@ async fn session_reflector_runner_validates_fact_proposals_without_applying() { assert_eq!(records.len(), 1); assert_eq!(records[0].run_id, run.run_id); assert_eq!(records[0].accepted_count, 2); - assert_eq!(records[0].rejected_count, 7); + assert_eq!(records[0].rejected_count, 8); assert!(records[0].applied_ops.is_none()); } diff --git a/tests/automation_skill_writer_runner_test.rs b/tests/automation_skill_writer_runner_test.rs index 9404e8e4..a00abe93 100644 --- a/tests/automation_skill_writer_runner_test.rs +++ b/tests/automation_skill_writer_runner_test.rs @@ -63,26 +63,62 @@ async fn skill_writer_default_provider_searches_all_providers() { .await; let _global_db = isolate_global_db(&cg); let backend = SkillJsonBackend::new(json!({"skills": []})); - let config = AutomationConfig { - enabled: true, - backend: AutomationBackend::CodexAppServer, - host_mode: AutomationHostMode::Standalone, - tasks: AutomationTaskSet { - skill_writer: AutomationTaskConfig { - enabled: true, - schedule: Some("manual".to_string()), - ..AutomationTaskConfig::default() - }, - ..AutomationTaskSet::default() + let config = enabled_skill_writer_config(); + + let run = run_skill_writer_with_backend( + &cg, + &config, + &backend, + SkillWriterAutomationOptions { + profile_root: Some(profile_root), + ..SkillWriterAutomationOptions::default() }, - ..AutomationConfig::default() - }; + ) + .await + .unwrap(); + + assert_eq!(backend.calls(), 1); + assert_eq!(run.ledger_record.status, AutomationRunStatus::Succeeded); +} + +#[tokio::test] +async fn skill_writer_runner_reads_hermes_profile_lcm() { + let temp = tempdir().unwrap(); + let profile_root = temp.path().join("profile"); + let cg = init_project(temp.path()).await; + + let hermes_home = tempdir().unwrap(); + let profile_db_path = resolve_hermes_profile_session_db_path(hermes_home.path()).unwrap(); + let profile_db = GlobalDb::open_at(&profile_db_path) + .await + .expect("hermes profile session db open"); + seed_session_message_in_db( + &profile_db, + hermes_home.path(), + SeedSessionMessage { + provider: "cursor", + session_id: "hermes-skill-writer-1", + message_id: "hermes-skill-writer-1-message-001", + role: "assistant", + timestamp: 1_715_100_005, + text: "Hermes profile-only skill writer evidence should draft reusable workflow guidance.", + source: Some("hermes_profile_lcm"), + }, + ) + .await; + + let backend = SkillJsonBackend::new(json!({"skills": []})); + let config = enabled_skill_writer_config(); let run = run_skill_writer_with_backend( &cg, &config, &backend, SkillWriterAutomationOptions { + storage_scope: "hermes_profile".to_string(), + hermes_home: Some(hermes_home.path().to_path_buf()), + provider: "cursor".to_string(), + query: "profile-only skill writer evidence".to_string(), profile_root: Some(profile_root), ..SkillWriterAutomationOptions::default() }, @@ -136,33 +172,15 @@ async fn skill_writer_runner_creates_pending_skill_drafts_for_approval() { ] })); let config = AutomationConfig { - enabled: true, - backend: AutomationBackend::CodexAppServer, - host_mode: AutomationHostMode::Standalone, model: Some("configured-model".to_string()), - tasks: AutomationTaskSet { - skill_writer: AutomationTaskConfig { - enabled: true, - schedule: Some("manual".to_string()), - ..AutomationTaskConfig::default() - }, - ..AutomationTaskSet::default() - }, - ..AutomationConfig::default() + ..enabled_skill_writer_config() }; let run = run_skill_writer_with_backend( &cg, &config, &backend, - SkillWriterAutomationOptions { - trigger: AutomationTrigger::ManualCli, - provider: "cursor".to_string(), - query: "automation".to_string(), - evidence_limit: 5, - profile_root: Some(profile_root.clone()), - run_id: None, - }, + manual_skill_writer_options(&profile_root), ) .await .unwrap(); @@ -352,33 +370,13 @@ async fn skill_writer_evidence_imports_project_skill_usage_analytics_before_summ .await .unwrap(); let backend = InspectSkillWriterUsageBackend; - let config = AutomationConfig { - enabled: true, - backend: AutomationBackend::CodexAppServer, - host_mode: AutomationHostMode::Standalone, - tasks: AutomationTaskSet { - skill_writer: AutomationTaskConfig { - enabled: true, - schedule: Some("manual".to_string()), - ..AutomationTaskConfig::default() - }, - ..AutomationTaskSet::default() - }, - ..AutomationConfig::default() - }; + let config = enabled_skill_writer_config(); let run = run_skill_writer_with_backend( &cg, &config, &backend, - SkillWriterAutomationOptions { - trigger: AutomationTrigger::ManualCli, - provider: "cursor".to_string(), - query: "automation".to_string(), - evidence_limit: 5, - profile_root: Some(profile_root), - run_id: None, - }, + manual_skill_writer_options(&profile_root), ) .await .unwrap(); @@ -403,20 +401,7 @@ async fn skill_writer_evidence_includes_underused_tool_family_summary() { seed_search_underuse_session_evidence(&cg).await; let _global_db = isolate_global_db(&cg); let backend = InspectSkillWriterUnderusedBackend; - let config = AutomationConfig { - enabled: true, - backend: AutomationBackend::CodexAppServer, - host_mode: AutomationHostMode::Standalone, - tasks: AutomationTaskSet { - skill_writer: AutomationTaskConfig { - enabled: true, - schedule: Some("manual".to_string()), - ..AutomationTaskConfig::default() - }, - ..AutomationTaskSet::default() - }, - ..AutomationConfig::default() - }; + let config = enabled_skill_writer_config(); let run = run_skill_writer_with_backend( &cg, @@ -492,33 +477,15 @@ async fn skill_writer_runner_auto_enables_when_config_explicitly_allows() { "auto_enable_after_validation", ); let config = AutomationConfig { - enabled: true, - backend: AutomationBackend::CodexAppServer, - host_mode: AutomationHostMode::Standalone, auto_enable_skills: true, - tasks: AutomationTaskSet { - skill_writer: AutomationTaskConfig { - enabled: true, - schedule: Some("manual".to_string()), - ..AutomationTaskConfig::default() - }, - ..AutomationTaskSet::default() - }, - ..AutomationConfig::default() + ..enabled_skill_writer_config() }; let run = run_skill_writer_with_backend( &cg, &config, &backend, - SkillWriterAutomationOptions { - trigger: AutomationTrigger::ManualCli, - provider: "cursor".to_string(), - query: "automation".to_string(), - evidence_limit: 5, - profile_root: Some(profile_root.clone()), - run_id: None, - }, + manual_skill_writer_options(&profile_root), ) .await .unwrap(); @@ -646,33 +613,13 @@ async fn skill_writer_runner_updates_existing_skills_with_checksum_precondition( } ] })); - let config = AutomationConfig { - enabled: true, - backend: AutomationBackend::CodexAppServer, - host_mode: AutomationHostMode::Standalone, - tasks: AutomationTaskSet { - skill_writer: AutomationTaskConfig { - enabled: true, - schedule: Some("manual".to_string()), - ..AutomationTaskConfig::default() - }, - ..AutomationTaskSet::default() - }, - ..AutomationConfig::default() - }; + let config = enabled_skill_writer_config(); let run = run_skill_writer_with_backend( &cg, &config, &backend, - SkillWriterAutomationOptions { - trigger: AutomationTrigger::ManualCli, - provider: "cursor".to_string(), - query: "automation".to_string(), - evidence_limit: 5, - profile_root: Some(profile_root.clone()), - run_id: None, - }, + manual_skill_writer_options(&profile_root), ) .await .unwrap(); @@ -783,33 +730,13 @@ async fn skill_writer_runner_ledgers_malformed_backend_output() { seed_session_evidence(&cg).await; let _global_db = isolate_global_db(&cg); let backend = SkillTextBackend::new("not json"); - let config = AutomationConfig { - enabled: true, - backend: AutomationBackend::CodexAppServer, - host_mode: AutomationHostMode::Standalone, - tasks: AutomationTaskSet { - skill_writer: AutomationTaskConfig { - enabled: true, - schedule: Some("manual".to_string()), - ..AutomationTaskConfig::default() - }, - ..AutomationTaskSet::default() - }, - ..AutomationConfig::default() - }; + let config = enabled_skill_writer_config(); let err = run_skill_writer_with_backend( &cg, &config, &backend, - SkillWriterAutomationOptions { - trigger: AutomationTrigger::ManualCli, - provider: "cursor".to_string(), - query: "automation".to_string(), - evidence_limit: 5, - profile_root: Some(profile_root), - run_id: None, - }, + manual_skill_writer_options(&profile_root), ) .await .unwrap_err(); @@ -854,33 +781,13 @@ async fn skill_writer_runner_ledgers_missing_skills_array() { let _global_db = isolate_global_db(&cg); let output = json!({"summary": "no skills"}); let backend = SkillJsonBackend::new(output.clone()); - let config = AutomationConfig { - enabled: true, - backend: AutomationBackend::CodexAppServer, - host_mode: AutomationHostMode::Standalone, - tasks: AutomationTaskSet { - skill_writer: AutomationTaskConfig { - enabled: true, - schedule: Some("manual".to_string()), - ..AutomationTaskConfig::default() - }, - ..AutomationTaskSet::default() - }, - ..AutomationConfig::default() - }; + let config = enabled_skill_writer_config(); let err = run_skill_writer_with_backend( &cg, &config, &backend, - SkillWriterAutomationOptions { - trigger: AutomationTrigger::ManualCli, - provider: "cursor".to_string(), - query: "automation".to_string(), - evidence_limit: 5, - profile_root: Some(profile_root), - run_id: None, - }, + manual_skill_writer_options(&profile_root), ) .await .unwrap_err(); @@ -921,33 +828,13 @@ async fn skill_writer_runner_records_noop_fallback_when_backend_run_task_fails() seed_session_evidence(&cg).await; let _global_db = isolate_global_db(&cg); let backend = FailingBackend::new(AgentTaskKind::SkillWriter); - let config = AutomationConfig { - enabled: true, - backend: AutomationBackend::CodexAppServer, - host_mode: AutomationHostMode::Standalone, - tasks: AutomationTaskSet { - skill_writer: AutomationTaskConfig { - enabled: true, - schedule: Some("manual".to_string()), - ..AutomationTaskConfig::default() - }, - ..AutomationTaskSet::default() - }, - ..AutomationConfig::default() - }; + let config = enabled_skill_writer_config(); let run = run_skill_writer_with_backend( &cg, &config, &backend, - SkillWriterAutomationOptions { - trigger: AutomationTrigger::ManualCli, - provider: "cursor".to_string(), - query: "automation".to_string(), - evidence_limit: 5, - run_id: None, - profile_root: Some(profile_root), - }, + manual_skill_writer_options(&profile_root), ) .await .unwrap(); diff --git a/tests/dashboard_automation_api_test.rs b/tests/dashboard_automation_api_test.rs index 9b5a08de..7118a2a5 100644 --- a/tests/dashboard_automation_api_test.rs +++ b/tests/dashboard_automation_api_test.rs @@ -104,7 +104,8 @@ fn curation_agent_plan_skips_when_automation_is_disabled_and_records_history() { "dry_run": true, "provider": "cursor", "query": "workflow corrections", - "evidence_limit": 7 + "evidence_limit": 7, + "storage_scope": "project_local" }), ); assert_eq!(status, 202); @@ -125,7 +126,7 @@ fn curation_agent_plan_skips_when_automation_is_disabled_and_records_history() { .post(&format!("{base_url}/api/automation/run/skill-writing")) .send_json(serde_json::json!({ "dry_run": true, - "storage_scope": "project_local" + "unsupported_field": true })) .expect("skill-writing request with unsupported field should receive response"); let rejected_skill_status = rejected_skill_shape.status().as_u16(); @@ -135,7 +136,7 @@ fn curation_agent_plan_skips_when_automation_is_disabled_and_records_history() { .expect("skill-writing rejection body should be readable"); assert_eq!(rejected_skill_status, 422); assert!( - rejected_skill_body.contains("storage_scope"), + rejected_skill_body.contains("unsupported_field"), "rejection should name the unsupported field: {rejected_skill_body}" );