From 31d3aee1a517a5fded0c916b1e766b581858146f Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sun, 12 Jul 2026 04:55:39 +0000 Subject: [PATCH] fix(automation): protect managed skill ownership --- .config/nextest.toml | 15 ++++ src/agents/mod.rs | 10 +++ src/automation/skill_materialization.rs | 3 + src/automation/skill_writer.rs | 17 +++-- src/automation/skill_writer/consolidation.rs | 21 ++++-- .../agent_suite/skill_materialization_test.rs | 70 ++++++++++++------- tests/agent_suite/skill_targets_test.rs | 27 +++++-- tests/automation_runner_test/skill_writer.rs | 50 ++++++++++--- .../cli_non_interactive_test.rs | 6 +- tests/dashboard_api_test/automation_skills.rs | 2 +- 10 files changed, 164 insertions(+), 57 deletions(-) diff --git a/.config/nextest.toml b/.config/nextest.toml index b2ddb93a0..222aa79f3 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -1,7 +1,11 @@ +[profile.default] +test-threads = 8 + [profile.ci] fail-fast = false retries = 1 flaky-result = "fail" +test-threads = 8 slow-timeout = { period = "10s", terminate-after = 36 } final-status-level = "slow" @@ -22,6 +26,7 @@ filter = '(binary(=daemon_suite) & test(/^git_watch_test::(fifty_commit_rebase_n slow-timeout = { period = "60s", terminate-after = 10 } [test-groups] +cli-subprocess = { max-threads = 1 } windows-tracedecay-init = { max-threads = 32 } windows-init-heavy = { max-threads = 32 } windows-profile-storage = { max-threads = 32 } @@ -47,6 +52,16 @@ threads-required = "num-cpus" filter = 'binary(=core_cli_suite) & test(/^tool_daemon_test::(daemon_socket_is_owner_only|daemon_sigterm_exits_while_project_client_is_connected)$/)' threads-required = "num-cpus" +[[profile.ci.overrides]] +filter = '(binary(=core_cli_suite) & test(/^cli_non_interactive_test::/)) | (binary(=mcp_suite) & test(/^(mcp_cli_serve_test|serve_degraded_mode_test|serve_template_path_test)::/))' +test-group = 'cli-subprocess' +threads-required = "num-cpus" + +[[profile.default.overrides]] +filter = '(binary(=core_cli_suite) & test(/^cli_non_interactive_test::/)) | (binary(=mcp_suite) & test(/^(mcp_cli_serve_test|serve_degraded_mode_test|serve_template_path_test)::/))' +test-group = 'cli-subprocess' +threads-required = "num-cpus" + [[profile.ci.overrides]] filter = 'binary(=agent_suite) | (binary(=storage_suite) & test(/^branch_db_safety_test::/)) | (binary(=core_cli_suite) & test(/^cli_non_interactive_test::/)) | (binary(=mcp_suite) & test(/^(mcp_cli_serve_test|serve_template_path_test)::/))' platform = { host = 'cfg(windows)' } diff --git a/src/agents/mod.rs b/src/agents/mod.rs index 1ed323c11..20aa86a37 100644 --- a/src/agents/mod.rs +++ b/src/agents/mod.rs @@ -93,6 +93,10 @@ pub struct ManagedSkillExportReport { pub error: Option, } +pub(crate) fn uses_default_user_profile(home: &Path, profile_root: &Path) -> bool { + profile_root == home.join(".tracedecay") +} + /// Re-runs the managed-skill overlay/prompt-index export for every agent /// integration that already has tracedecay installed under `home`, so a /// lifecycle change (approve/disable/archive/restore) deploys without @@ -106,6 +110,9 @@ pub fn export_managed_skills_to_agents( home: &Path, profile_root: &Path, ) -> Vec { + if !uses_default_user_profile(home, profile_root) { + return Vec::new(); + } let mut reports = Vec::new(); for ag in all_integrations() { match ag.export_managed_skills(home, profile_root) { @@ -136,6 +143,9 @@ pub fn export_managed_skills_to_agent_hosts( project_root: &Path, profile_root: &Path, ) -> Vec { + if !uses_default_user_profile(home, profile_root) { + return Vec::new(); + } let mut reports = Vec::new(); for ag in all_integrations() { let mut exports = Vec::new(); diff --git a/src/automation/skill_materialization.rs b/src/automation/skill_materialization.rs index 2c0d19d6d..3bd69c92e 100644 --- a/src/automation/skill_materialization.rs +++ b/src/automation/skill_materialization.rs @@ -1723,6 +1723,9 @@ pub fn reconcile_detected_scopes( home: &Path, project_root: &Path, ) -> (Vec, Vec) { + if !crate::agents::uses_default_user_profile(home, profile_root) { + return (Vec::new(), Vec::new()); + } let mut results = Vec::new(); let mut errors = Vec::new(); let skills = match load_active_managed_skills(profile_root) { diff --git a/src/automation/skill_writer.rs b/src/automation/skill_writer.rs index 8c9d928f9..c7aa099e7 100644 --- a/src/automation/skill_writer.rs +++ b/src/automation/skill_writer.rs @@ -553,7 +553,7 @@ fn refresh_managed_skill_exports_after_auto_enable(profile_root: &Path) -> Value let Some(home) = crate::agents::home_dir() else { return json!({"status": "skipped", "reason": "home_unavailable"}); }; - if !should_refresh_managed_skill_exports(profile_root, &home) { + if !crate::agents::uses_default_user_profile(&home, profile_root) { return json!({"status": "skipped", "reason": "non_default_profile_root"}); } let start = std::env::current_dir().unwrap_or_else(|_| home.clone()); @@ -582,10 +582,6 @@ fn refresh_managed_skill_exports_after_auto_enable(profile_root: &Path) -> Value }) } -fn should_refresh_managed_skill_exports(profile_root: &Path, home: &Path) -> bool { - profile_root == home.join(".tracedecay") -} - fn accepted_skill_approval_status( action: SkillProposalAction, auto_enable_skills: bool, @@ -688,6 +684,9 @@ fn skill_update_from_proposal( let existing = existing_skills .get(&id) .ok_or_else(|| format!("managed skill id '{id}' does not exist"))?; + if existing.metadata.provenance.source != ManagedSkillSource::AutomationRun { + return Err(format!("managed skill '{id}' is not automation-owned")); + } let base_checksum = required_proposal_string(object.get("base_checksum"), "base_checksum")?; if base_checksum != existing.metadata.checksum { return Err(format!( @@ -870,13 +869,13 @@ mod tests { #[test] fn managed_skill_exports_only_refresh_for_the_user_profile() { let home = Path::new("/home/test-user"); - assert!(should_refresh_managed_skill_exports( - Path::new("/home/test-user/.tracedecay"), + assert!(crate::agents::uses_default_user_profile( home, + Path::new("/home/test-user/.tracedecay"), )); - assert!(!should_refresh_managed_skill_exports( - Path::new("/tmp/tracedecay-test-profile"), + assert!(!crate::agents::uses_default_user_profile( home, + Path::new("/tmp/tracedecay-test-profile"), )); } diff --git a/src/automation/skill_writer/consolidation.rs b/src/automation/skill_writer/consolidation.rs index 9418b4f93..ff133595e 100644 --- a/src/automation/skill_writer/consolidation.rs +++ b/src/automation/skill_writer/consolidation.rs @@ -49,10 +49,8 @@ fn consolidation_guard<'a>( "managed skill '{id}' is pinned and exempt from consolidation" )); } - if skill.metadata.provenance.source == ManagedSkillSource::UserDraft { - return Err(format!( - "managed skill '{id}' is user-authored and exempt from consolidation" - )); + if skill.metadata.provenance.source != ManagedSkillSource::AutomationRun { + return Err(format!("managed skill '{id}' is not automation-owned")); } if skill.metadata.state == ManagedSkillState::Archived { return Err(format!("managed skill '{id}' is already archived")); @@ -302,6 +300,7 @@ mod tests { fixture_skill("workflow-b", ManagedSkillSource::AutomationRun, false), fixture_skill("pinned-skill", ManagedSkillSource::AutomationRun, true), fixture_skill("user-skill", ManagedSkillSource::UserDraft, false), + fixture_skill("imported-skill", ManagedSkillSource::Import, false), archived, ] .into_iter() @@ -392,7 +391,19 @@ mod tests { }), &skills, ), - "managed skill 'user-skill' is user-authored and exempt from consolidation", + "managed skill 'user-skill' is not automation-owned", + ); + assert_err_eq( + skill_archive_from_proposal( + &json!({ + "action": "archive", + "id": "imported-skill", + "base_checksum": checksum(&skills, "imported-skill"), + "reason": "x" + }), + &skills, + ), + "managed skill 'imported-skill' is not automation-owned", ); assert_err_eq( skill_archive_from_proposal( diff --git a/tests/agent_suite/skill_materialization_test.rs b/tests/agent_suite/skill_materialization_test.rs index 480ee0311..ecb927abf 100644 --- a/tests/agent_suite/skill_materialization_test.rs +++ b/tests/agent_suite/skill_materialization_test.rs @@ -80,7 +80,7 @@ async fn materialize_on_activate_writes_global_scope_only_by_default() { let (_temp, root) = canonical_tempdir(); let home = root.join("home"); let project = root.join("project"); - let profile_root = root.join("profile"); + let profile_root = home.join(".tracedecay"); install_fake_hosts(&home); install_fake_hosts(&project); @@ -134,11 +134,31 @@ async fn materialize_on_activate_writes_global_scope_only_by_default() { } } +#[tokio::test] +async fn isolated_profile_cannot_remove_user_profile_materializations() { + let (_temp, root) = canonical_tempdir(); + let home = root.join("home"); + let project = root.join("project"); + let user_profile = home.join(".tracedecay"); + let isolated_profile = root.join("test-profile/.tracedecay"); + install_fake_hosts(&home); + + activate_skill(&user_profile, "code-slop-cleanup").await; + reconcile_detected_scopes(&user_profile, &home, &project); + let materialized = home.join(".codex/skills/code-slop-cleanup/SKILL.md"); + assert!(materialized.is_file()); + + let (results, errors) = reconcile_detected_scopes(&isolated_profile, &home, &project); + assert!(results.is_empty()); + assert!(errors.is_empty()); + assert!(materialized.is_file()); +} + #[tokio::test] async fn materialized_file_carries_provenance_frontmatter() { let (_temp, root) = canonical_tempdir(); let home = root.join("home"); - let profile_root = root.join("profile"); + let profile_root = home.join(".tracedecay"); install_fake_hosts(&home); activate_skill(&profile_root, "code-slop-cleanup").await; @@ -163,7 +183,7 @@ async fn materialized_file_carries_provenance_frontmatter() { async fn remove_on_deactivate_deletes_materialized_file() { let (_temp, root) = canonical_tempdir(); let home = root.join("home"); - let profile_root = root.join("profile"); + let profile_root = home.join(".tracedecay"); install_fake_hosts(&home); activate_skill(&profile_root, "code-slop-cleanup").await; @@ -195,7 +215,7 @@ async fn remove_on_deactivate_deletes_materialized_file() { async fn idempotent_reconcile_is_unchanged_on_rerun() { let (_temp, root) = canonical_tempdir(); let home = root.join("home"); - let profile_root = root.join("profile"); + let profile_root = home.join(".tracedecay"); install_fake_hosts(&home); activate_skill(&profile_root, "code-slop-cleanup").await; @@ -218,7 +238,7 @@ async fn idempotent_reconcile_is_unchanged_on_rerun() { async fn body_update_re_materializes_the_file() { let (_temp, root) = canonical_tempdir(); let home = root.join("home"); - let profile_root = root.join("profile"); + let profile_root = home.join(".tracedecay"); install_fake_hosts(&home); activate_skill(&profile_root, "code-slop-cleanup").await; @@ -248,7 +268,7 @@ async fn body_update_re_materializes_the_file() { async fn metadata_update_re_materializes_the_file() { let (_temp, root) = canonical_tempdir(); let home = root.join("home"); - let profile_root = root.join("profile"); + let profile_root = home.join(".tracedecay"); install_fake_hosts(&home); activate_skill(&profile_root, "code-slop-cleanup").await; @@ -277,7 +297,7 @@ async fn metadata_update_re_materializes_the_file() { async fn support_update_removes_only_stale_owned_files() { let (_temp, root) = canonical_tempdir(); let home = root.join("home"); - let profile_root = root.join("profile"); + let profile_root = home.join(".tracedecay"); install_fake_hosts(&home); activate_skill(&profile_root, "code-slop-cleanup").await; @@ -314,7 +334,7 @@ async fn support_update_removes_only_stale_owned_files() { async fn user_edited_support_file_is_fork_protected() { let (_temp, root) = canonical_tempdir(); let home = root.join("home"); - let profile_root = root.join("profile"); + let profile_root = home.join(".tracedecay"); install_fake_hosts(&home); activate_skill(&profile_root, "code-slop-cleanup").await; @@ -342,7 +362,7 @@ async fn user_edited_support_file_is_fork_protected() { async fn deactivation_removes_only_owned_artifacts() { let (_temp, root) = canonical_tempdir(); let home = root.join("home"); - let profile_root = root.join("profile"); + let profile_root = home.join(".tracedecay"); install_fake_hosts(&home); activate_skill(&profile_root, "code-slop-cleanup").await; @@ -374,7 +394,7 @@ async fn package_symlink_is_rejected_before_materialization() { let (_temp, root) = canonical_tempdir(); let home = root.join("home"); - let profile_root = root.join("profile"); + let profile_root = home.join(".tracedecay"); let external = root.join("external"); install_fake_hosts(&home); std::fs::create_dir_all(&external).unwrap(); @@ -403,7 +423,7 @@ async fn nested_support_symlink_is_rejected_before_write() { let (_temp, root) = canonical_tempdir(); let home = root.join("home"); - let profile_root = root.join("profile"); + let profile_root = home.join(".tracedecay"); let external = root.join("external"); install_fake_hosts(&home); std::fs::create_dir_all(&external).unwrap(); @@ -433,7 +453,7 @@ async fn nested_support_symlink_is_rejected_before_remove() { let (_temp, root) = canonical_tempdir(); let home = root.join("home"); - let profile_root = root.join("profile"); + let profile_root = home.join(".tracedecay"); let external = root.join("external"); install_fake_hosts(&home); @@ -468,7 +488,7 @@ async fn nested_support_symlink_is_rejected_before_remove() { async fn interrupted_manifest_commit_recovers_on_retry() { let (_temp, root) = canonical_tempdir(); let home = root.join("home"); - let profile_root = root.join("profile"); + let profile_root = home.join(".tracedecay"); install_fake_hosts(&home); activate_skill(&profile_root, "code-slop-cleanup").await; @@ -508,7 +528,7 @@ async fn interrupted_manifest_commit_recovers_on_retry() { async fn fork_protection_leaves_user_edited_file_and_doctor_flags_it() { let (_temp, root) = canonical_tempdir(); let home = root.join("home"); - let profile_root = root.join("profile"); + let profile_root = home.join(".tracedecay"); install_fake_hosts(&home); activate_skill(&profile_root, "code-slop-cleanup").await; @@ -553,7 +573,7 @@ async fn fork_protection_leaves_user_edited_file_and_doctor_flags_it() { async fn foreign_file_is_never_touched_and_doctor_reports_conflict() { let (_temp, root) = canonical_tempdir(); let home = root.join("home"); - let profile_root = root.join("profile"); + let profile_root = home.join(".tracedecay"); install_fake_hosts(&home); // A user (or repo-local dev skill) already owns this slug — no provenance. @@ -597,7 +617,7 @@ async fn foreign_file_is_never_touched_and_doctor_reports_conflict() { async fn doctor_reports_missing_and_orphan_drift() { let (_temp, root) = canonical_tempdir(); let home = root.join("home"); - let profile_root = root.join("profile"); + let profile_root = home.join(".tracedecay"); install_fake_hosts(&home); // Active skill, nothing materialized yet -> Missing. @@ -666,7 +686,7 @@ async fn detect_scopes_only_covers_installed_hosts() { async fn reconcile_scope_removes_only_managed_orphans() { let (_temp, root) = canonical_tempdir(); let home = root.join("home"); - let profile_root = root.join("profile"); + let profile_root = home.join(".tracedecay"); install_fake_hosts(&home); let _ = &profile_root; @@ -708,7 +728,7 @@ async fn load_skill(profile_root: &Path, id: &str) -> ManagedSkill { async fn lost_manifest_pristine_file_is_rederived_not_forked() { let (_temp, root) = canonical_tempdir(); let home = root.join("home"); - let profile_root = root.join("profile"); + let profile_root = home.join(".tracedecay"); install_fake_hosts(&home); activate_skill(&profile_root, "code-slop-cleanup").await; @@ -769,7 +789,7 @@ async fn project_scope_filters_out_global_skills() { let (_temp, root) = canonical_tempdir(); let home = root.join("home"); let project = root.join("project"); - let profile_root = root.join("profile"); + let profile_root = home.join(".tracedecay"); install_fake_hosts(&home); install_fake_hosts(&project); @@ -800,7 +820,7 @@ async fn project_scope_protects_another_installations_committed_package() { let (_temp, root) = canonical_tempdir(); let home = root.join("home"); let project = root.join("project"); - let profile_root = root.join("profile"); + let profile_root = home.join(".tracedecay"); install_fake_hosts(&home); install_fake_hosts(&project); @@ -966,7 +986,7 @@ async fn doctor_reports_own_inactive_skill_as_plain_orphan() { async fn concurrent_materialize_does_not_wedge_forked() { let (_temp, root) = canonical_tempdir(); let home = root.join("home"); - let profile_root = root.join("profile"); + let profile_root = home.join(".tracedecay"); install_fake_hosts(&home); activate_skill(&profile_root, "code-slop-cleanup").await; @@ -1007,7 +1027,7 @@ async fn symlinked_scope_root_materializes_through_link() { let (_temp, root) = canonical_tempdir(); let home = root.join("home"); let real_claude = root.join("dotfiles/claude"); - let profile_root = root.join("profile"); + let profile_root = home.join(".tracedecay"); std::fs::create_dir_all(&home).unwrap(); std::fs::create_dir_all(&real_claude).unwrap(); // ~/.claude is a symlink into a dotfiles repo — a normal setup. @@ -1037,7 +1057,7 @@ async fn doctor_reports_other_drift_when_one_package_errors() { let (_temp, root) = canonical_tempdir(); let home = root.join("home"); let external = root.join("external"); - let profile_root = root.join("profile"); + let profile_root = home.join(".tracedecay"); install_fake_hosts(&home); std::fs::create_dir_all(&external).unwrap(); @@ -1081,7 +1101,7 @@ async fn reconcile_continues_past_one_poisoned_package() { let (_temp, root) = canonical_tempdir(); let home = root.join("home"); let external = root.join("external"); - let profile_root = root.join("profile"); + let profile_root = home.join(".tracedecay"); install_fake_hosts(&home); std::fs::create_dir_all(&external).unwrap(); @@ -1114,7 +1134,7 @@ async fn reconcile_continues_past_one_poisoned_package() { async fn colliding_host_slugs_are_disambiguated_and_warned() { let (_temp, root) = canonical_tempdir(); let home = root.join("home"); - let profile_root = root.join("profile"); + let profile_root = home.join(".tracedecay"); install_fake_hosts(&home); // "team-sync" and "team_sync" both normalize to slug "team-sync". diff --git a/tests/agent_suite/skill_targets_test.rs b/tests/agent_suite/skill_targets_test.rs index 01848f1e9..d4b5a9471 100644 --- a/tests/agent_suite/skill_targets_test.rs +++ b/tests/agent_suite/skill_targets_test.rs @@ -819,7 +819,7 @@ fn install_fake_cursor_plugin(home: &std::path::Path) -> std::path::PathBuf { async fn lifecycle_export_sweep_deploys_and_retracts_across_detected_agents() { let temp = tempfile::tempdir().unwrap(); let home = temp.path().join("home"); - let profile_root = temp.path().join("profile"); + let profile_root = home.join(".tracedecay"); let claude_md = install_fake_claude(&home); let cursor_plugin = install_fake_cursor_plugin(&home); @@ -877,7 +877,7 @@ async fn lifecycle_export_sweep_deploys_and_retracts_across_detected_agents() { async fn lifecycle_export_sweep_isolates_per_agent_failures() { let temp = tempfile::tempdir().unwrap(); let home = temp.path().join("home"); - let profile_root = temp.path().join("profile"); + let profile_root = home.join(".tracedecay"); let claude_md = install_fake_claude(&home); let cursor_plugin = install_fake_cursor_plugin(&home); // A directory where the prompt file should be makes the Claude export @@ -923,7 +923,7 @@ async fn lifecycle_export_sweep_isolates_per_agent_failures() { async fn lifecycle_export_sweep_skips_agents_without_installs() { let temp = tempfile::tempdir().unwrap(); let home = temp.path().join("home"); - let profile_root = temp.path().join("profile"); + let profile_root = home.join(".tracedecay"); std::fs::create_dir_all(&home).unwrap(); create_managed_skill_draft(&profile_root, draft("repo-hygiene", "Repository hygiene")) @@ -940,12 +940,31 @@ async fn lifecycle_export_sweep_skips_agents_without_installs() { ); } +#[tokio::test] +async fn lifecycle_export_sweep_skips_non_default_profiles() { + let temp = tempfile::tempdir().unwrap(); + let home = temp.path().join("home"); + let profile_root = temp.path().join("test-profile/.tracedecay"); + let claude_md = install_fake_claude(&home); + let original = std::fs::read_to_string(&claude_md).unwrap(); + + create_managed_skill_draft(&profile_root, draft("repo-hygiene", "Repository hygiene")) + .await + .unwrap(); + approve_managed_skill(&profile_root, "repo-hygiene") + .await + .unwrap(); + + assert!(export_managed_skills_to_agents(&home, &profile_root).is_empty()); + assert_eq!(std::fs::read_to_string(claude_md).unwrap(), original); +} + #[tokio::test] async fn local_lifecycle_export_skips_unrelated_project_configs() { let temp = tempfile::tempdir().unwrap(); let home = temp.path().join("home"); let project_root = temp.path().join("project"); - let profile_root = temp.path().join("profile"); + let profile_root = home.join(".tracedecay"); std::fs::create_dir_all(&home).unwrap(); let claude_md = project_root.join(".claude/CLAUDE.md"); diff --git a/tests/automation_runner_test/skill_writer.rs b/tests/automation_runner_test/skill_writer.rs index f122732e9..c84b77f19 100644 --- a/tests/automation_runner_test/skill_writer.rs +++ b/tests/automation_runner_test/skill_writer.rs @@ -565,9 +565,9 @@ async fn skill_writer_runner_auto_enables_when_config_explicitly_allows() { body_markdown: "Check the run ledger before approving changes.".to_string(), support_files: Vec::new(), provenance: ManagedSkillProvenance { - source: ManagedSkillSource::UserDraft, - actor: "test".to_string(), - run_id: None, + source: ManagedSkillSource::AutomationRun, + actor: "tracedecay".to_string(), + run_id: Some("seed-run".to_string()), }, }, ) @@ -685,9 +685,9 @@ async fn skill_writer_runner_updates_existing_skills_with_checksum_precondition( ManagedSupportFile::new("references/old.md", b"old checklist".to_vec()).unwrap(), ], provenance: ManagedSkillProvenance { - source: ManagedSkillSource::UserDraft, - actor: "test".to_string(), - run_id: None, + source: ManagedSkillSource::AutomationRun, + actor: "tracedecay".to_string(), + run_id: Some("seed-run".to_string()), }, }, ) @@ -697,6 +697,28 @@ async fn skill_writer_runner_updates_existing_skills_with_checksum_precondition( .await .unwrap(); let base_checksum = active.metadata.checksum.clone(); + create_managed_skill_draft( + &profile_root, + ManagedSkillDraft { + id: "user-owned-review".to_string(), + title: "User-owned review".to_string(), + summary: "User-authored workflow.".to_string(), + category: "workflow".to_string(), + targets: tracedecay::automation::managed_skills::default_managed_skill_targets(), + body_markdown: "Keep this user-authored content unchanged.".to_string(), + support_files: Vec::new(), + provenance: ManagedSkillProvenance { + source: ManagedSkillSource::UserDraft, + actor: "test-user".to_string(), + run_id: None, + }, + }, + ) + .await + .unwrap(); + let user_owned = approve_managed_skill(&profile_root, "user-owned-review") + .await + .unwrap(); let backend = SkillJsonBackend::new(json!({ "skills": [ { @@ -732,6 +754,12 @@ async fn skill_writer_runner_updates_existing_skills_with_checksum_precondition( "id": "missing-skill", "base_checksum": "sha256:missing", "summary": "Unknown update should be rejected." + }, + { + "action": "update", + "id": "user-owned-review", + "base_checksum": user_owned.metadata.checksum, + "summary": "Automation must not edit this user-owned skill." } ] })); @@ -749,7 +777,7 @@ async fn skill_writer_runner_updates_existing_skills_with_checksum_precondition( assert_eq!(backend.calls(), 1); assert_eq!(run.ledger_record.status, AutomationRunStatus::Succeeded); assert_eq!(run.ledger_record.accepted_count, 1); - assert_eq!(run.ledger_record.rejected_count, 3); + assert_eq!(run.ledger_record.rejected_count, 4); assert_eq!(run.report["created_skills"], json!([])); assert_eq!( run.report["updated_skills"][0]["metadata"]["id"], @@ -829,12 +857,18 @@ async fn skill_writer_runner_updates_existing_skills_with_checksum_precondition( assert!(!skill_dir.join("references/old.md").exists()); assert!(skill_dir.join("references/checklist.md").is_file()); + let user_owned = load_managed_skill(&profile_root, "user-owned-review") + .await + .unwrap(); + assert_eq!(user_owned.metadata.summary, "User-authored workflow."); + assert!(user_owned.pending_update.is_none()); + let records = load_run_records(&cg.store_layout().dashboard_root, 10) .await .unwrap(); assert_eq!(records.len(), 1); assert_eq!(records[0].accepted_count, 1); - assert_eq!(records[0].rejected_count, 3); + assert_eq!(records[0].rejected_count, 4); assert_eq!( records[0].proposed_ops.as_ref().unwrap()["updated_skills"][0]["metadata"]["id"], json!("automation-run-review") diff --git a/tests/core_cli_suite/cli_non_interactive_test.rs b/tests/core_cli_suite/cli_non_interactive_test.rs index e4c04d1b2..6aa4f3b63 100644 --- a/tests/core_cli_suite/cli_non_interactive_test.rs +++ b/tests/core_cli_suite/cli_non_interactive_test.rs @@ -61,11 +61,7 @@ fn tracedecay_command_with_stdin(home: &std::path::Path, project: &std::path::Pa } fn cli_timeout() -> Duration { - if cfg!(windows) { - Duration::from_secs(90) - } else { - Duration::from_secs(30) - } + Duration::from_secs(90) } fn add_tracedecay_path_shim(command: &mut Command, home: &Path) -> PathBuf { diff --git a/tests/dashboard_api_test/automation_skills.rs b/tests/dashboard_api_test/automation_skills.rs index 9ec2a6a02..c93bd9bdb 100644 --- a/tests/dashboard_api_test/automation_skills.rs +++ b/tests/dashboard_api_test/automation_skills.rs @@ -274,8 +274,8 @@ fn managed_skills_are_dashboard_controllable_with_explicit_approval() { .unwrap_or_else(|err| panic!("failed to canonicalize temp root: {err}")); let project_root = tmp_root.join("project"); let global_db_path = tmp_root.join("global").join("global.db"); - let profile_root = tmp_root.join("profile").join(".tracedecay"); let home = tmp_root.join("home"); + let profile_root = home.join(".tracedecay"); std::fs::create_dir_all(&home).unwrap(); let _env_guard = EnvVarGuard::set(GLOBAL_DB_ENV, &global_db_path); let _data_dir_guard = EnvVarGuard::set(USER_DATA_DIR_ENV, &profile_root);