Skip to content

Commit 1d4a0c5

Browse files
refactor: split skill writer consolidation
1 parent 3b5e747 commit 1d4a0c5

8 files changed

Lines changed: 924 additions & 877 deletions

File tree

src/automation/skill_writer.rs

Lines changed: 10 additions & 543 deletions
Large diffs are not rendered by default.

src/automation/skill_writer/consolidation.rs

Lines changed: 560 additions & 0 deletions
Large diffs are not rendered by default.

tests/agent_suite/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ mod agent_test;
1313
mod claude_agent_test;
1414
mod copilot_agent_test;
1515
mod kiro_agent_test;
16+
mod managed_skill_archive_test;
1617
mod managed_skills_test;
1718
mod opencode_agent_test;
1819
mod plugin_skill_contract_test;
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
use tracedecay::automation::managed_skills::{
2+
approve_managed_skill, archive_managed_skill, create_managed_skill_draft,
3+
discard_pending_managed_skill_update, load_managed_skill, managed_skill_dir,
4+
save_managed_skill, stage_managed_skill_archive, ManagedSkillDraft, ManagedSkillProvenance,
5+
ManagedSkillSource, ManagedSkillState, ManagedSupportFile, SkillInstallTarget,
6+
};
7+
8+
fn draft() -> ManagedSkillDraft {
9+
ManagedSkillDraft {
10+
id: "repo-hygiene".to_string(),
11+
title: "Repository hygiene".to_string(),
12+
summary: "Keep repository maintenance guidance current.".to_string(),
13+
category: "maintenance".to_string(),
14+
targets: vec![SkillInstallTarget::Cursor, SkillInstallTarget::Codex],
15+
body_markdown: "Use focused checks before changing generated files.".to_string(),
16+
support_files: vec![ManagedSupportFile::new(
17+
"references/checklist.md",
18+
b"- check dirty tree\n- run focused tests\n".to_vec(),
19+
)
20+
.unwrap()],
21+
provenance: ManagedSkillProvenance {
22+
source: ManagedSkillSource::AutomationRun,
23+
actor: "tracedecay".to_string(),
24+
run_id: Some("run_123".to_string()),
25+
},
26+
}
27+
}
28+
29+
#[tokio::test]
30+
async fn staged_managed_skill_archive_keeps_content_until_approval() {
31+
let temp = tempfile::tempdir().unwrap();
32+
let profile_root = temp.path().join("profile");
33+
create_managed_skill_draft(&profile_root, draft())
34+
.await
35+
.unwrap();
36+
let active = approve_managed_skill(&profile_root, "repo-hygiene")
37+
.await
38+
.unwrap();
39+
let base_checksum = active.metadata.checksum.clone();
40+
let skill_dir = managed_skill_dir(&profile_root, "repo-hygiene").unwrap();
41+
42+
let staged = stage_managed_skill_archive(
43+
&profile_root,
44+
"repo-hygiene",
45+
&base_checksum,
46+
Some("overlaps with newer guidance".to_string()),
47+
)
48+
.await
49+
.unwrap();
50+
assert_eq!(staged.metadata.state, ManagedSkillState::PendingApproval);
51+
52+
let with_pending = load_managed_skill(&profile_root, "repo-hygiene")
53+
.await
54+
.unwrap();
55+
assert_eq!(with_pending.metadata.state, ManagedSkillState::Active);
56+
assert_eq!(with_pending.metadata.checksum, base_checksum);
57+
let pending = with_pending.pending_update.as_ref().unwrap();
58+
assert_eq!(pending.resulting_state, Some(ManagedSkillState::Archived));
59+
assert_eq!(
60+
pending.staged_reason.as_deref(),
61+
Some("overlaps with newer guidance")
62+
);
63+
64+
let approved = approve_managed_skill(&profile_root, "repo-hygiene")
65+
.await
66+
.unwrap();
67+
assert_eq!(approved.metadata.state, ManagedSkillState::Archived);
68+
assert_eq!(approved.body_markdown, active.body_markdown);
69+
assert!(approved.pending_update.is_none());
70+
assert!(skill_dir.join("SKILL.md").is_file());
71+
assert!(skill_dir.join("references/checklist.md").is_file());
72+
let reloaded = load_managed_skill(&profile_root, "repo-hygiene")
73+
.await
74+
.unwrap();
75+
assert_eq!(reloaded.metadata.state, ManagedSkillState::Archived);
76+
assert_eq!(reloaded.body_markdown, active.body_markdown);
77+
}
78+
79+
#[tokio::test]
80+
async fn staged_managed_skill_archive_can_be_discarded() {
81+
let temp = tempfile::tempdir().unwrap();
82+
let profile_root = temp.path().join("profile");
83+
create_managed_skill_draft(&profile_root, draft())
84+
.await
85+
.unwrap();
86+
let active = approve_managed_skill(&profile_root, "repo-hygiene")
87+
.await
88+
.unwrap();
89+
stage_managed_skill_archive(
90+
&profile_root,
91+
"repo-hygiene",
92+
&active.metadata.checksum,
93+
None,
94+
)
95+
.await
96+
.unwrap();
97+
98+
let discarded = discard_pending_managed_skill_update(&profile_root, "repo-hygiene")
99+
.await
100+
.unwrap();
101+
assert!(discarded.pending_update.is_none());
102+
let reloaded = load_managed_skill(&profile_root, "repo-hygiene")
103+
.await
104+
.unwrap();
105+
assert_eq!(reloaded.metadata.state, ManagedSkillState::Active);
106+
assert_eq!(reloaded.metadata.checksum, active.metadata.checksum);
107+
}
108+
109+
#[tokio::test]
110+
async fn staged_managed_skill_archive_rejects_pinned_stale_and_duplicates() {
111+
let temp = tempfile::tempdir().unwrap();
112+
let profile_root = temp.path().join("profile");
113+
create_managed_skill_draft(&profile_root, draft())
114+
.await
115+
.unwrap();
116+
let mut active = approve_managed_skill(&profile_root, "repo-hygiene")
117+
.await
118+
.unwrap();
119+
let base_checksum = active.metadata.checksum.clone();
120+
121+
let err = stage_managed_skill_archive(&profile_root, "repo-hygiene", "sha256:stale", None)
122+
.await
123+
.unwrap_err();
124+
assert!(err.to_string().contains("is stale"));
125+
126+
active.set_pinned(true);
127+
save_managed_skill(&profile_root, &active).await.unwrap();
128+
let err = stage_managed_skill_archive(&profile_root, "repo-hygiene", &base_checksum, None)
129+
.await
130+
.unwrap_err();
131+
assert!(err
132+
.to_string()
133+
.contains("pinned and exempt from staged archive"));
134+
135+
active.set_pinned(false);
136+
save_managed_skill(&profile_root, &active).await.unwrap();
137+
stage_managed_skill_archive(&profile_root, "repo-hygiene", &base_checksum, None)
138+
.await
139+
.unwrap();
140+
let err = stage_managed_skill_archive(&profile_root, "repo-hygiene", &base_checksum, None)
141+
.await
142+
.unwrap_err();
143+
assert!(err.to_string().contains("already has a pending update"));
144+
145+
discard_pending_managed_skill_update(&profile_root, "repo-hygiene")
146+
.await
147+
.unwrap();
148+
let archived = archive_managed_skill(&profile_root, "repo-hygiene")
149+
.await
150+
.unwrap();
151+
let err = stage_managed_skill_archive(
152+
&profile_root,
153+
"repo-hygiene",
154+
&archived.metadata.checksum,
155+
None,
156+
)
157+
.await
158+
.unwrap_err();
159+
assert!(err.to_string().contains("is already archived"));
160+
}

tests/agent_suite/managed_skills_test.rs

Lines changed: 3 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@ use tracedecay::automation::managed_skills::{
22
approve_managed_skill, archive_managed_skill, create_managed_skill_draft,
33
disable_managed_skill, discard_pending_managed_skill_update, list_managed_skills,
44
load_managed_skill, managed_skill_dir, restore_managed_skill, save_managed_skill,
5-
set_managed_skill_state, stage_managed_skill_archive, stage_managed_skill_update,
6-
update_managed_skill, ManagedSkillDraft, ManagedSkillProvenance, ManagedSkillSource,
7-
ManagedSkillState, ManagedSkillUpdate, ManagedSupportFile, SkillInstallTarget,
8-
MAX_MANAGED_SKILL_BODY_BYTES,
5+
set_managed_skill_state, stage_managed_skill_update, update_managed_skill, ManagedSkillDraft,
6+
ManagedSkillProvenance, ManagedSkillSource, ManagedSkillState, ManagedSkillUpdate,
7+
ManagedSupportFile, SkillInstallTarget, MAX_MANAGED_SKILL_BODY_BYTES,
98
};
109
use tracedecay::automation::skill_usage::{
1110
ingest_analytics_events, load_skill_usage_records, record_skill_usage,
@@ -838,142 +837,6 @@ async fn staged_managed_skill_update_preserves_active_revision_until_approval()
838837
assert_eq!(reloaded.metadata.checksum, second_base);
839838
}
840839

841-
#[tokio::test]
842-
async fn staged_managed_skill_archive_keeps_content_until_approval() {
843-
let temp = tempfile::tempdir().unwrap();
844-
let profile_root = temp.path().join("profile");
845-
create_managed_skill_draft(&profile_root, draft())
846-
.await
847-
.unwrap();
848-
let active = approve_managed_skill(&profile_root, "repo-hygiene")
849-
.await
850-
.unwrap();
851-
let base_checksum = active.metadata.checksum.clone();
852-
let skill_dir = managed_skill_dir(&profile_root, "repo-hygiene").unwrap();
853-
854-
let staged = stage_managed_skill_archive(
855-
&profile_root,
856-
"repo-hygiene",
857-
&base_checksum,
858-
Some("overlaps with newer guidance".to_string()),
859-
)
860-
.await
861-
.unwrap();
862-
assert_eq!(staged.metadata.state, ManagedSkillState::PendingApproval);
863-
864-
// Staging must not touch the active revision.
865-
let with_pending = load_managed_skill(&profile_root, "repo-hygiene")
866-
.await
867-
.unwrap();
868-
assert_eq!(with_pending.metadata.state, ManagedSkillState::Active);
869-
assert_eq!(with_pending.metadata.checksum, base_checksum);
870-
let pending = with_pending.pending_update.as_ref().unwrap();
871-
assert_eq!(pending.resulting_state, Some(ManagedSkillState::Archived));
872-
assert_eq!(
873-
pending.staged_reason.as_deref(),
874-
Some("overlaps with newer guidance")
875-
);
876-
877-
// Approval archives without deleting any content.
878-
let approved = approve_managed_skill(&profile_root, "repo-hygiene")
879-
.await
880-
.unwrap();
881-
assert_eq!(approved.metadata.state, ManagedSkillState::Archived);
882-
assert_eq!(approved.body_markdown, active.body_markdown);
883-
assert!(approved.pending_update.is_none());
884-
assert!(skill_dir.join("SKILL.md").is_file());
885-
assert!(skill_dir.join("references/checklist.md").is_file());
886-
let reloaded = load_managed_skill(&profile_root, "repo-hygiene")
887-
.await
888-
.unwrap();
889-
assert_eq!(reloaded.metadata.state, ManagedSkillState::Archived);
890-
assert_eq!(reloaded.body_markdown, active.body_markdown);
891-
}
892-
893-
#[tokio::test]
894-
async fn staged_managed_skill_archive_can_be_discarded() {
895-
let temp = tempfile::tempdir().unwrap();
896-
let profile_root = temp.path().join("profile");
897-
create_managed_skill_draft(&profile_root, draft())
898-
.await
899-
.unwrap();
900-
let active = approve_managed_skill(&profile_root, "repo-hygiene")
901-
.await
902-
.unwrap();
903-
stage_managed_skill_archive(
904-
&profile_root,
905-
"repo-hygiene",
906-
&active.metadata.checksum,
907-
None,
908-
)
909-
.await
910-
.unwrap();
911-
912-
let discarded = discard_pending_managed_skill_update(&profile_root, "repo-hygiene")
913-
.await
914-
.unwrap();
915-
assert!(discarded.pending_update.is_none());
916-
let reloaded = load_managed_skill(&profile_root, "repo-hygiene")
917-
.await
918-
.unwrap();
919-
assert_eq!(reloaded.metadata.state, ManagedSkillState::Active);
920-
assert_eq!(reloaded.metadata.checksum, active.metadata.checksum);
921-
}
922-
923-
#[tokio::test]
924-
async fn staged_managed_skill_archive_rejects_pinned_stale_and_duplicates() {
925-
let temp = tempfile::tempdir().unwrap();
926-
let profile_root = temp.path().join("profile");
927-
create_managed_skill_draft(&profile_root, draft())
928-
.await
929-
.unwrap();
930-
let mut active = approve_managed_skill(&profile_root, "repo-hygiene")
931-
.await
932-
.unwrap();
933-
let base_checksum = active.metadata.checksum.clone();
934-
935-
let err = stage_managed_skill_archive(&profile_root, "repo-hygiene", "sha256:stale", None)
936-
.await
937-
.unwrap_err();
938-
assert!(err.to_string().contains("is stale"));
939-
940-
active.set_pinned(true);
941-
save_managed_skill(&profile_root, &active).await.unwrap();
942-
let err = stage_managed_skill_archive(&profile_root, "repo-hygiene", &base_checksum, None)
943-
.await
944-
.unwrap_err();
945-
assert!(err
946-
.to_string()
947-
.contains("pinned and exempt from staged archive"));
948-
949-
active.set_pinned(false);
950-
save_managed_skill(&profile_root, &active).await.unwrap();
951-
stage_managed_skill_archive(&profile_root, "repo-hygiene", &base_checksum, None)
952-
.await
953-
.unwrap();
954-
let err = stage_managed_skill_archive(&profile_root, "repo-hygiene", &base_checksum, None)
955-
.await
956-
.unwrap_err();
957-
assert!(err.to_string().contains("already has a pending update"));
958-
959-
// Already-archived skills cannot be staged for archive again.
960-
discard_pending_managed_skill_update(&profile_root, "repo-hygiene")
961-
.await
962-
.unwrap();
963-
let archived = archive_managed_skill(&profile_root, "repo-hygiene")
964-
.await
965-
.unwrap();
966-
let err = stage_managed_skill_archive(
967-
&profile_root,
968-
"repo-hygiene",
969-
&archived.metadata.checksum,
970-
None,
971-
)
972-
.await
973-
.unwrap_err();
974-
assert!(err.to_string().contains("is already archived"));
975-
}
976-
977840
#[tokio::test]
978841
async fn staged_managed_skill_update_rejects_no_op_patch() {
979842
let temp = tempfile::tempdir().unwrap();

tests/automation_runner_test/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ mod runner;
2323
mod scheduler;
2424
mod session_reflector;
2525
mod skill_writer;
26+
mod skill_writer_consolidation;

0 commit comments

Comments
 (0)