返回 CodeWhale
review.rs
根目录 / crates / tui / src / commands / groups / skills / review.rs
1 //! Review command: activate review skill and send a target immediately.
2 //!
3 //! FEAT-022 Phase 4: portable contextual dispatch. The host performs the
4 //! discovery + side effects (`CommandSkillGroupContext::run_review`); the
5 //! portable handler composes the exact error text and the `SendMessage` action.
6
7 use codewhale_command_contract::facets::{CommandSkillGroupContext, ReviewOutcome};
8 use codewhale_command_contract::handler::{CommandContexts, CommandHandler};
9 use codewhale_command_contract::metadata::{CommandInfo, RegisterCommand};
10
11 use crate::commands::CommandResult;
12 use crate::tui::app::AppAction;
13
14 /// Render the review warnings suffix (baseline `warnings_suffix`).
15 fn warnings_suffix(warnings: &[String]) -> String {
16 if warnings.is_empty() {
17 return String::new();
18 }
19
20 format!("\n\nWarnings:\n- {}", warnings.join("\n- "))
21 }
22
23 pub(in crate::commands) const COMMAND_INFO: CommandInfo = CommandInfo {
24 name: "review",
25 aliases: &["shencha"],
26 usage: "/review <target>",
27 description_key: "cmd_review_description",
28 };
29
30 pub(in crate::commands) struct ReviewCmd;
31
32 impl RegisterCommand<CommandResult> for ReviewCmd {
33 fn info() -> &'static CommandInfo {
34 &COMMAND_INFO
35 }
36
37 fn handler() -> CommandHandler<CommandResult> {
38 CommandHandler::Contextual {
39 capabilities: codewhale_command_contract::handler::CommandCapabilities::SKILL_GROUP,
40 handler: review_contextual,
41 }
42 }
43 }
44
45 /// Contextual `/review` dispatch: exactly the skill-group facet. The baseline
46 /// command never refreshed the shared skill cache, so `/review` must not
47 /// request the unrelated SKILLS facet.
48 fn review_contextual(contexts: CommandContexts<'_>, arg: Option<&str>) -> CommandResult {
49 let mut parts = contexts.into_parts();
50 let Some(skill_group) = parts.skill_group.as_deref_mut() else {
51 return CommandResult::error("Command capability unavailable: skill_group");
52 };
53 review(skill_group, arg)
54 }
55
56 /// Portable `/review` dispatch — byte-identical to the baseline handler.
57 ///
58 /// The host performs discovery, warning merge, session-message insertion, and
59 /// active-skill mutation (`run_review`); the handler validates the target,
60 /// renders the not-found error, and emits the `SendMessage` action. The
61 /// baseline success path renders no message and does not refresh the cache.
62 fn review(group: &mut dyn CommandSkillGroupContext, arg: Option<&str>) -> CommandResult {
63 let target = arg.unwrap_or("").trim();
64 if target.is_empty() {
65 return CommandResult::error("Usage: /review <target>");
66 }
67
68 match group.run_review() {
69 Ok(ReviewOutcome::Ready) => {
70 CommandResult::action(AppAction::SendMessage(target.to_string()))
71 }
72 Ok(ReviewOutcome::NotFound {
73 skills_dir,
74 global_dir,
75 warnings,
76 }) => {
77 let warnings = warnings_suffix(&warnings);
78 CommandResult::error(format!(
79 "Review skill not found in {} or {}. Create ~/.codewhale/skills/review/SKILL.md.{}",
80 skills_dir, global_dir, warnings
81 ))
82 }
83 Err(err) => CommandResult::error(err),
84 }
85 }
86
87 #[cfg(test)]
88 mod tests {
89 use super::*;
90 use codewhale_command_contract::facets::{
91 CommandApprovalState, RemoteRegistryOutcome, SkillActivationError, SkillMutationReceipt,
92 SkillRecommendation, SkillSyncOutcome, SkillTargetScope, SnapshotEntry,
93 };
94
95 struct FakeSkillGroup {
96 review: Result<ReviewOutcome, String>,
97 approval: CommandApprovalState,
98 }
99 impl FakeSkillGroup {
100 fn ready() -> Self {
101 Self {
102 review: Ok(ReviewOutcome::Ready),
103 approval: CommandApprovalState {
104 yolo: true,
105 trust_mode: false,
106 },
107 }
108 }
109 }
110 impl CommandSkillGroupContext for FakeSkillGroup {
111 fn skill_registry_projection(
112 &self,
113 ) -> codewhale_command_contract::facets::SkillRegistryProjection {
114 unimplemented!("not used by review tests")
115 }
116 fn activate_skill(
117 &mut self,
118 _name: &str,
119 ) -> Result<codewhale_command_contract::facets::SkillActivationOutcome, SkillActivationError>
120 {
121 unimplemented!("not used by review tests")
122 }
123 fn install_skill(
124 &mut self,
125 _scope: Option<SkillTargetScope>,
126 _spec: &str,
127 ) -> Result<SkillMutationReceipt, String> {
128 unimplemented!("not used by review tests")
129 }
130 fn update_skill(
131 &mut self,
132 _scope: Option<SkillTargetScope>,
133 _name: &str,
134 ) -> Result<SkillMutationReceipt, String> {
135 unimplemented!("not used by review tests")
136 }
137 fn uninstall_skill(
138 &mut self,
139 _scope: Option<SkillTargetScope>,
140 _name: &str,
141 ) -> Result<SkillMutationReceipt, String> {
142 unimplemented!("not used by review tests")
143 }
144 fn trust_skill(
145 &mut self,
146 _scope: Option<SkillTargetScope>,
147 _name: &str,
148 ) -> Result<SkillMutationReceipt, String> {
149 unimplemented!("not used by review tests")
150 }
151 fn fetch_remote_registry(&mut self) -> Result<RemoteRegistryOutcome, String> {
152 unimplemented!("not used by review tests")
153 }
154 fn recommend_skills(&mut self, _task: &str) -> Result<Vec<SkillRecommendation>, String> {
155 unimplemented!("not used by review tests")
156 }
157 fn sync_registry(&mut self) -> Result<SkillSyncOutcome, String> {
158 unimplemented!("not used by review tests")
159 }
160 fn run_review(&mut self) -> Result<ReviewOutcome, String> {
161 self.review.clone()
162 }
163 fn snapshot_list(&mut self, _limit: usize) -> Result<Vec<SnapshotEntry>, String> {
164 unimplemented!("not used by review tests")
165 }
166 fn restore_snapshot(&mut self, _id: &str) -> Result<(), String> {
167 unimplemented!("not used by review tests")
168 }
169 fn approval_state(&self) -> CommandApprovalState {
170 self.approval
171 }
172 }
173
174 #[test]
175 fn review_without_target_prints_usage() {
176 let mut group = FakeSkillGroup::ready();
177 let result = review(&mut group, None);
178 assert!(result.is_error);
179 assert!(result.message.unwrap().contains("Usage: /review"));
180 }
181
182 #[test]
183 fn review_ready_sends_target_without_skills_context() {
184 let mut group = FakeSkillGroup::ready();
185 let contexts = CommandContexts::empty().with_skill_group(&mut group);
186 let result = review_contextual(contexts, Some("file.rs"));
187 assert!(result.message.is_none());
188 assert!(matches!(
189 result.action,
190 Some(AppAction::SendMessage(ref t)) if t == "file.rs"
191 ));
192 }
193
194 #[test]
195 fn review_not_found_renders_exact_error_with_warnings() {
196 let mut group = FakeSkillGroup::ready();
197 group.review = Ok(ReviewOutcome::NotFound {
198 skills_dir: "/ws/skills".to_string(),
199 global_dir: "/home/u/.codewhale/skills".to_string(),
200 warnings: vec!["one warning".to_string()],
201 });
202 let result = review(&mut group, Some("file.rs"));
203 assert!(result.is_error);
204 let msg = result.message.unwrap();
205 assert!(
206 msg.contains(
207 "Review skill not found in /ws/skills or /home/u/.codewhale/skills. Create ~/.codewhale/skills/review/SKILL.md."
208 ),
209 "{msg}"
210 );
211 assert!(msg.contains("Warnings:\n- one warning"), "{msg}");
212 }
213
214 #[test]
215 fn review_missing_facet_errors_are_safe() {
216 let result = review_contextual(CommandContexts::empty(), Some("file.rs"));
217 assert!(result.is_error);
218 assert_eq!(
219 result.message.unwrap(),
220 "Error: Command capability unavailable: skill_group"
221 );
222 }
223 }
224
224 lines RUST