1use std::path::PathBuf;
21
22use crate::agents::planning as agent_planning;
23use crate::error::AgentConfigError;
24use crate::integration::{
25 InstallReport, InstructionSurface, Integration, McpSurface, SkillSurface, UninstallReport,
26};
27use crate::paths;
28use crate::plan::{InstallPlan, PlanTarget, UninstallPlan};
29use crate::scope::{Scope, ScopeKind};
30use crate::spec::{HookSpec, InstructionSpec, Matcher, McpSpec, SkillSpec};
31use crate::status::StatusReport;
32use crate::util::{hooks_json, instructions_dir, mcp_json_object, ownership, rules_dir, skills_dir};
33
34const RULES_DIR: &str = ".agents/rules";
35const LEGACY_RULES_DIR: &str = ".agent/rules";
36
37#[derive(Debug, Clone, Copy, Default)]
39pub struct AntigravityAgent {
40 _private: (),
41}
42
43impl AntigravityAgent {
44 pub const fn new() -> Self {
46 Self { _private: () }
47 }
48
49 fn project_root<'a>(&self, scope: &'a Scope) -> Result<&'a std::path::Path, AgentConfigError> {
50 match scope {
51 Scope::Local(p) => Ok(p),
52 Scope::Global => Err(AgentConfigError::UnsupportedScope {
53 id: "antigravity",
54 scope: ScopeKind::Global,
55 }),
56 }
57 }
58
59 fn skills_root(scope: &Scope) -> Result<PathBuf, AgentConfigError> {
63 Ok(match scope {
64 Scope::Global => paths::gemini_home()?.join("antigravity").join("skills"),
65 Scope::Local(p) => p.join(".agents").join("skills"),
66 })
67 }
68
69 fn legacy_skills_root(scope: &Scope) -> Option<PathBuf> {
70 match scope {
71 Scope::Global => None,
72 Scope::Local(p) => Some(p.join(".agent").join("skills")),
73 }
74 }
75
76 fn existing_skills_root(scope: &Scope, name: &str) -> Result<PathBuf, AgentConfigError> {
77 SkillSpec::validate_name(name)?;
78 let root = Self::skills_root(scope)?;
79 let (dir, _, ledger) = skills_dir::paths_for_status(&root, name);
80 if dir.exists() || ownership::owner_of(&ledger, name)?.is_some() {
81 return Ok(root);
82 }
83
84 if let Some(legacy) = Self::legacy_skills_root(scope) {
85 let (dir, _, ledger) = skills_dir::paths_for_status(&legacy, name);
86 if dir.exists() || ownership::owner_of(&ledger, name)?.is_some() {
87 return Ok(legacy);
88 }
89 }
90
91 Ok(root)
92 }
93
94 fn hooks_path(scope: &Scope) -> Result<PathBuf, AgentConfigError> {
95 Ok(match scope {
96 Scope::Global => paths::gemini_home()?.join("config").join("hooks.json"),
97 Scope::Local(p) => p.join(".agents").join("hooks.json"),
98 })
99 }
100
101 fn mcp_path(scope: &Scope) -> Result<PathBuf, AgentConfigError> {
102 Ok(match scope {
103 Scope::Global => paths::antigravity_mcp_global_file()?,
104 Scope::Local(p) => p.join(".agents").join("mcp_config.json"),
105 })
106 }
107
108 fn existing_mcp_path(scope: &Scope) -> Result<PathBuf, AgentConfigError> {
109 let primary = Self::mcp_path(scope)?;
110 if primary.exists() {
111 return Ok(primary);
112 }
113 if let Scope::Local(p) = scope {
114 let legacy = p.join(".agent").join("mcp_config.json");
115 if legacy.exists() {
116 return Ok(legacy);
117 }
118 }
119 Ok(primary)
120 }
121}
122
123impl Integration for AntigravityAgent {
124 fn id(&self) -> &'static str {
125 "antigravity"
126 }
127
128 fn display_name(&self) -> &'static str {
129 "Google Antigravity"
130 }
131
132 fn supported_scopes(&self) -> &'static [ScopeKind] {
133 &[ScopeKind::Global, ScopeKind::Local]
134 }
135
136 fn status(&self, scope: &Scope, tag: &str) -> Result<StatusReport, AgentConfigError> {
137 HookSpec::validate_tag(tag)?;
138
139 if let Scope::Local(p) = scope {
141 let path = rules_dir::target_path(p, RULES_DIR, tag);
142 if path.exists() {
143 return Ok(StatusReport::for_file_hook(tag, path));
144 }
145 let legacy = rules_dir::target_path(p, LEGACY_RULES_DIR, tag);
146 if legacy.exists() {
147 return Ok(StatusReport::for_file_hook(tag, legacy));
148 }
149 }
150
151 let hooks_path = Self::hooks_path(scope)?;
153 let presence = hooks_json::config_presence(&hooks_path, tag)?;
154 if let crate::status::ConfigPresence::Absent = presence {
155 if let Scope::Local(p) = scope {
156 let path = rules_dir::target_path(p, RULES_DIR, tag);
157 Ok(StatusReport::for_file_hook(tag, path))
158 } else {
159 Ok(StatusReport::for_tagged_hook(tag, hooks_path, presence))
160 }
161 } else {
162 Ok(StatusReport::for_tagged_hook(tag, hooks_path, presence))
163 }
164 }
165
166 fn plan_install(
167 &self,
168 scope: &Scope,
169 spec: &HookSpec,
170 ) -> Result<InstallPlan, AgentConfigError> {
171 HookSpec::validate_tag(&spec.tag)?;
172 let target = PlanTarget::Hook {
173 integration_id: Integration::id(self),
174 scope: scope.clone(),
175 tag: spec.tag.clone(),
176 };
177 let mut changes = Vec::new();
178
179 let hooks_path = Self::hooks_path(scope)?;
181 hooks_json::plan_install(&mut changes, &hooks_path, spec, build_hook_value)?;
182
183 if let Some(rules) = &spec.rules {
185 let root = self.project_root(scope);
186 let root = match root {
187 Ok(root) => root,
188 Err(AgentConfigError::UnsupportedScope { .. }) => {
189 return Ok(InstallPlan::refused(
190 target,
191 None,
192 crate::plan::RefusalReason::UnsupportedScope,
193 ));
194 }
195 Err(e) => return Err(e),
196 };
197 let rule_changes = rules_dir::plan_install(root, RULES_DIR, &spec.tag, &rules.content)?;
198 changes.extend(rule_changes);
199 }
200
201 Ok(InstallPlan::from_changes(target, changes))
202 }
203
204 fn plan_uninstall(&self, scope: &Scope, tag: &str) -> Result<UninstallPlan, AgentConfigError> {
205 HookSpec::validate_tag(tag)?;
206 let target = PlanTarget::Hook {
207 integration_id: Integration::id(self),
208 scope: scope.clone(),
209 tag: tag.to_string(),
210 };
211 let mut changes = Vec::new();
212
213 let hooks_path = Self::hooks_path(scope)?;
215 hooks_json::plan_uninstall(&mut changes, &hooks_path, tag)?;
216
217 if let Scope::Local(p) = scope {
219 let current = rules_dir::target_path(p, RULES_DIR, tag);
220 let legacy = rules_dir::target_path(p, LEGACY_RULES_DIR, tag);
221 let rules_dir_name = if !current.exists() && legacy.exists() {
222 LEGACY_RULES_DIR
223 } else {
224 RULES_DIR
225 };
226 let rule_changes = rules_dir::plan_uninstall(p, rules_dir_name, tag)?;
227 changes.extend(rule_changes);
228 }
229
230 Ok(UninstallPlan::from_changes(target, changes))
231 }
232
233 fn install(&self, scope: &Scope, spec: &HookSpec) -> Result<InstallReport, AgentConfigError> {
234 HookSpec::validate_tag(&spec.tag)?;
235 let mut report = InstallReport::default();
236
237 let hooks_path = Self::hooks_path(scope)?;
239 let hook_report = hooks_json::install(scope, &hooks_path, spec, build_hook_value)?;
240 report.created.extend(hook_report.created);
241 report.patched.extend(hook_report.patched);
242 report.backed_up.extend(hook_report.backed_up);
243 report.already_installed = hook_report.already_installed;
244
245 if let Some(rules) = &spec.rules {
247 let _ = self.project_root(scope)?;
248 let rules_report = rules_dir::install(scope, RULES_DIR, &spec.tag, &rules.content)?;
249 report.created.extend(rules_report.created);
250 report.patched.extend(rules_report.patched);
251 report.backed_up.extend(rules_report.backed_up);
252 if !rules_report.already_installed {
253 report.already_installed = false;
254 }
255 }
256
257 Ok(report)
258 }
259
260 fn uninstall(&self, scope: &Scope, tag: &str) -> Result<UninstallReport, AgentConfigError> {
261 HookSpec::validate_tag(tag)?;
262 let mut report = UninstallReport::default();
263
264 let hooks_path = Self::hooks_path(scope)?;
266 let hook_report = hooks_json::uninstall(scope, &hooks_path, tag)?;
267 report.removed.extend(hook_report.removed);
268 report.patched.extend(hook_report.patched);
269 report.restored.extend(hook_report.restored);
270 report.not_installed = hook_report.not_installed;
271
272 if let Scope::Local(p) = scope {
274 let current = rules_dir::target_path(p, RULES_DIR, tag);
275 let legacy = rules_dir::target_path(p, LEGACY_RULES_DIR, tag);
276 let rules_dir_name = if !current.exists() && legacy.exists() {
277 LEGACY_RULES_DIR
278 } else {
279 RULES_DIR
280 };
281 let rules_report = rules_dir::uninstall(scope, rules_dir_name, tag)?;
282 report.removed.extend(rules_report.removed);
283 report.patched.extend(rules_report.patched);
284 report.restored.extend(rules_report.restored);
285 if !rules_report.not_installed {
286 report.not_installed = false;
287 }
288 }
289
290 Ok(report)
291 }
292}
293
294impl McpSurface for AntigravityAgent {
295 fn id(&self) -> &'static str {
296 "antigravity"
297 }
298
299 fn supported_mcp_scopes(&self) -> &'static [ScopeKind] {
300 &[ScopeKind::Global, ScopeKind::Local]
301 }
302
303 fn mcp_status(
304 &self,
305 scope: &Scope,
306 name: &str,
307 expected_owner: &str,
308 ) -> Result<StatusReport, AgentConfigError> {
309 McpSpec::validate_name(name)?;
310 let cfg = Self::existing_mcp_path(scope)?;
311 let ledger = ownership::mcp_ledger_for(&cfg);
312 let presence = mcp_json_object::config_presence(&cfg, name)?;
313 let recorded = ownership::owner_of(&ledger, name)?;
314 Ok(StatusReport::for_mcp(
315 name,
316 cfg,
317 ledger,
318 presence,
319 expected_owner,
320 recorded,
321 ))
322 }
323
324 fn plan_install_mcp(
325 &self,
326 scope: &Scope,
327 spec: &McpSpec,
328 ) -> Result<InstallPlan, AgentConfigError> {
329 agent_planning::mcp_json_object_install(
330 McpSurface::id(self),
331 scope,
332 spec,
333 Self::existing_mcp_path(scope),
334 )
335 }
336
337 fn plan_uninstall_mcp(
338 &self,
339 scope: &Scope,
340 name: &str,
341 owner_tag: &str,
342 ) -> Result<UninstallPlan, AgentConfigError> {
343 agent_planning::mcp_json_object_uninstall(
344 McpSurface::id(self),
345 scope,
346 name,
347 owner_tag,
348 Self::existing_mcp_path(scope),
349 )
350 }
351
352 fn install_mcp(
353 &self,
354 scope: &Scope,
355 spec: &McpSpec,
356 ) -> Result<InstallReport, AgentConfigError> {
357 spec.validate()?;
358 let cfg = Self::existing_mcp_path(scope)?;
359 spec.validate_local_secret_policy(scope)?;
360 scope.ensure_contained(&cfg)?;
361 let ledger = ownership::mcp_ledger_for(&cfg);
362 mcp_json_object::install(&cfg, &ledger, spec)
363 }
364
365 fn uninstall_mcp(
366 &self,
367 scope: &Scope,
368 name: &str,
369 owner_tag: &str,
370 ) -> Result<UninstallReport, AgentConfigError> {
371 McpSpec::validate_name(name)?;
372 HookSpec::validate_tag(owner_tag)?;
373 let cfg = Self::existing_mcp_path(scope)?;
374 scope.ensure_contained(&cfg)?;
375 let ledger = ownership::mcp_ledger_for(&cfg);
376 mcp_json_object::uninstall(&cfg, &ledger, name, owner_tag, "mcp server")
377 }
378}
379
380impl SkillSurface for AntigravityAgent {
381 fn id(&self) -> &'static str {
382 "antigravity"
383 }
384
385 fn supported_skill_scopes(&self) -> &'static [ScopeKind] {
386 &[ScopeKind::Global, ScopeKind::Local]
387 }
388
389 fn skill_status(
390 &self,
391 scope: &Scope,
392 name: &str,
393 expected_owner: &str,
394 ) -> Result<StatusReport, AgentConfigError> {
395 SkillSpec::validate_name(name)?;
396 let root = Self::skills_root(scope)?;
397 let (dir, manifest, ledger) = skills_dir::paths_for_status(&root, name);
398 if !dir.exists() && ownership::owner_of(&ledger, name)?.is_none() {
399 if let Some(legacy) = Self::legacy_skills_root(scope) {
400 let (legacy_dir, legacy_manifest, legacy_ledger) =
401 skills_dir::paths_for_status(&legacy, name);
402 if legacy_dir.exists() || ownership::owner_of(&legacy_ledger, name)?.is_some() {
403 let recorded = ownership::owner_of(&legacy_ledger, name)?;
404 return Ok(StatusReport::for_skill(
405 name,
406 legacy_dir,
407 legacy_manifest,
408 legacy_ledger,
409 expected_owner,
410 recorded,
411 ));
412 }
413 }
414 }
415 let recorded = ownership::owner_of(&ledger, name)?;
416 Ok(StatusReport::for_skill(
417 name,
418 dir,
419 manifest,
420 ledger,
421 expected_owner,
422 recorded,
423 ))
424 }
425
426 fn plan_install_skill(
427 &self,
428 scope: &Scope,
429 spec: &SkillSpec,
430 ) -> Result<InstallPlan, AgentConfigError> {
431 agent_planning::skill_install(
432 SkillSurface::id(self),
433 scope,
434 spec,
435 Self::skills_root(scope),
436 )
437 }
438
439 fn plan_uninstall_skill(
440 &self,
441 scope: &Scope,
442 name: &str,
443 owner_tag: &str,
444 ) -> Result<UninstallPlan, AgentConfigError> {
445 agent_planning::skill_uninstall(
446 SkillSurface::id(self),
447 scope,
448 name,
449 owner_tag,
450 Self::existing_skills_root(scope, name),
451 )
452 }
453
454 fn install_skill(
455 &self,
456 scope: &Scope,
457 spec: &SkillSpec,
458 ) -> Result<InstallReport, AgentConfigError> {
459 spec.validate()?;
460 let root = Self::skills_root(scope)?;
461 scope.ensure_contained(&root)?;
462 skills_dir::install(&root, spec)
463 }
464
465 fn uninstall_skill(
466 &self,
467 scope: &Scope,
468 name: &str,
469 owner_tag: &str,
470 ) -> Result<UninstallReport, AgentConfigError> {
471 SkillSpec::validate_name(name)?;
472 HookSpec::validate_tag(owner_tag)?;
473 let root = Self::existing_skills_root(scope, name)?;
474 scope.ensure_contained(&root)?;
475 skills_dir::uninstall(&root, name, owner_tag)
476 }
477}
478
479impl AntigravityAgent {
480 fn standalone_layout(
481 &self,
482 scope: &Scope,
483 ) -> Result<instructions_dir::StandaloneLayout, AgentConfigError> {
484 let root = self.project_root(scope)?;
485 Ok(instructions_dir::StandaloneLayout {
486 config_dir: root.join(".agents"),
487 instruction_dir: root.join(RULES_DIR),
488 })
489 }
490
491 fn legacy_standalone_layout(
492 &self,
493 scope: &Scope,
494 ) -> Result<instructions_dir::StandaloneLayout, AgentConfigError> {
495 let root = self.project_root(scope)?;
496 Ok(instructions_dir::StandaloneLayout {
497 config_dir: root.join(".agent"),
498 instruction_dir: root.join(LEGACY_RULES_DIR),
499 })
500 }
501
502 fn existing_standalone_layout(
503 &self,
504 scope: &Scope,
505 name: &str,
506 ) -> Result<instructions_dir::StandaloneLayout, AgentConfigError> {
507 InstructionSpec::validate_name(name)?;
508 let primary = self.standalone_layout(scope)?;
509 let primary_file = primary.instruction_dir.join(format!("{name}.md"));
510 let primary_ledger = instructions_dir::ledger_path(&primary.config_dir);
511 if primary_file.exists() || ownership::owner_of(&primary_ledger, name)?.is_some() {
512 return Ok(primary);
513 }
514
515 let legacy = self.legacy_standalone_layout(scope)?;
516 let legacy_file = legacy.instruction_dir.join(format!("{name}.md"));
517 let legacy_ledger = instructions_dir::ledger_path(&legacy.config_dir);
518 if legacy_file.exists() || ownership::owner_of(&legacy_ledger, name)?.is_some() {
519 return Ok(legacy);
520 }
521
522 Ok(primary)
523 }
524}
525
526impl InstructionSurface for AntigravityAgent {
527 fn id(&self) -> &'static str {
528 "antigravity"
529 }
530
531 fn supported_instruction_scopes(&self) -> &'static [ScopeKind] {
532 &[ScopeKind::Local]
533 }
534
535 fn instruction_status(
536 &self,
537 scope: &Scope,
538 name: &str,
539 expected_owner: &str,
540 ) -> Result<StatusReport, AgentConfigError> {
541 instructions_dir::standalone_status(
542 self.existing_standalone_layout(scope, name)?,
543 name,
544 expected_owner,
545 )
546 }
547
548 fn plan_install_instruction(
549 &self,
550 scope: &Scope,
551 spec: &InstructionSpec,
552 ) -> Result<InstallPlan, AgentConfigError> {
553 instructions_dir::standalone_plan_install(
554 InstructionSurface::id(self),
555 scope,
556 self.standalone_layout(scope),
557 spec,
558 )
559 }
560
561 fn plan_uninstall_instruction(
562 &self,
563 scope: &Scope,
564 name: &str,
565 owner_tag: &str,
566 ) -> Result<UninstallPlan, AgentConfigError> {
567 instructions_dir::standalone_plan_uninstall(
568 InstructionSurface::id(self),
569 scope,
570 self.existing_standalone_layout(scope, name),
571 name,
572 owner_tag,
573 )
574 }
575
576 fn install_instruction(
577 &self,
578 scope: &Scope,
579 spec: &InstructionSpec,
580 ) -> Result<InstallReport, AgentConfigError> {
581 instructions_dir::standalone_install(scope, self.standalone_layout(scope)?, spec)
582 }
583
584 fn uninstall_instruction(
585 &self,
586 scope: &Scope,
587 name: &str,
588 owner_tag: &str,
589 ) -> Result<UninstallReport, AgentConfigError> {
590 instructions_dir::standalone_uninstall(
591 scope,
592 self.existing_standalone_layout(scope, name)?,
593 name,
594 owner_tag,
595 )
596 }
597}
598
599fn matcher_to_antigravity(m: &Matcher) -> String {
600 match m {
601 Matcher::All => "*".to_string(),
602 Matcher::Bash => "run_command".to_string(),
603 Matcher::Exact(s) => s.clone(),
604 Matcher::AnyOf(names) => names.join("|"),
605 Matcher::Regex(s) => s.clone(),
606 }
607}
608
609fn build_hook_value(spec: &HookSpec) -> serde_json::Value {
610 let matcher_str = matcher_to_antigravity(&spec.matcher);
611 let command_str = spec.command.render_shell();
612 serde_json::json!([
613 {
614 "matcher": matcher_str,
615 "hooks": [
616 {
617 "type": "command",
618 "command": command_str,
619 "timeout": 10
620 }
621 ]
622 }
623 ])
624}
625
626#[cfg(test)]
627mod tests {
628 use super::*;
629 use crate::spec::InstructionPlacement;
630 use std::fs;
631 use tempfile::tempdir;
632
633 fn rules_spec(tag: &str, body: &str) -> HookSpec {
634 HookSpec::builder(tag)
635 .command_program("noop", [] as [&str; 0])
636 .rules(body)
637 .build()
638 }
639
640 fn hook_only_spec(tag: &str) -> HookSpec {
641 HookSpec::builder(tag)
642 .command_program("myapp", ["hook"])
643 .matcher(Matcher::Bash)
644 .event(crate::spec::Event::PreToolUse)
645 .build()
646 }
647
648 fn skill(name: &str, owner: &str) -> SkillSpec {
649 SkillSpec::builder(name)
650 .owner(owner)
651 .description("Format Git commits.")
652 .body("## Goal\nFormat them.\n")
653 .build()
654 }
655
656 fn mcp_spec(name: &str, owner: &str) -> McpSpec {
657 McpSpec::builder(name)
658 .owner(owner)
659 .stdio("npx", ["-y", "@example/server"])
660 .build()
661 }
662
663 fn instruction(name: &str, owner: &str) -> InstructionSpec {
664 InstructionSpec::builder(name)
665 .owner(owner)
666 .placement(InstructionPlacement::StandaloneFile)
667 .body("Use Antigravity instructions.\n")
668 .build()
669 }
670
671 #[test]
672 fn install_rules_uses_plural_dot_agents() {
673 let dir = tempdir().unwrap();
674 let agent = AntigravityAgent::new();
675 let scope = Scope::Local(dir.path().to_path_buf());
676 agent.install(&scope, &rules_spec("alpha", "body")).unwrap();
677 assert!(dir.path().join(".agents/rules/alpha.md").exists());
678 assert!(!dir.path().join(".agent/rules/alpha.md").exists());
679 }
680
681 #[test]
682 fn legacy_dot_agent_rules_status_and_uninstall_still_work() {
683 let dir = tempdir().unwrap();
684 let agent = AntigravityAgent::new();
685 let scope = Scope::Local(dir.path().to_path_buf());
686 fs::create_dir_all(dir.path().join(".agent/rules")).unwrap();
687 fs::write(dir.path().join(".agent/rules/alpha.md"), "legacy\n").unwrap();
688
689 assert!(agent.is_installed(&scope, "alpha").unwrap());
690 agent.uninstall(&scope, "alpha").unwrap();
691 assert!(!dir.path().join(".agent/rules/alpha.md").exists());
692 }
693
694 #[test]
695 fn rules_install_idempotent() {
696 let dir = tempdir().unwrap();
697 let agent = AntigravityAgent::new();
698 let scope = Scope::Local(dir.path().to_path_buf());
699 let s = rules_spec("alpha", "body");
700 agent.install(&scope, &s).unwrap();
701 let r = agent.install(&scope, &s).unwrap();
702 assert!(r.already_installed);
703 }
704
705 #[test]
706 fn install_skill_writes_under_dot_agents_skills() {
707 let dir = tempdir().unwrap();
708 let agent = AntigravityAgent::new();
709 let scope = Scope::Local(dir.path().to_path_buf());
710 agent
711 .install_skill(&scope, &skill("alpha", "myapp"))
712 .unwrap();
713 assert!(dir.path().join(".agents/skills/alpha/SKILL.md").exists());
714 assert!(!dir.path().join(".agent/skills/alpha/SKILL.md").exists());
715 let s = fs::read_to_string(dir.path().join(".agents/skills/alpha/SKILL.md")).unwrap();
716 assert!(s.contains("name: alpha"));
717 assert!(s.contains("description: Format Git commits."));
718 }
719
720 #[test]
721 fn legacy_dot_agent_skill_status_and_uninstall_still_work() {
722 let dir = tempdir().unwrap();
723 let agent = AntigravityAgent::new();
724 let scope = Scope::Local(dir.path().to_path_buf());
725 let legacy_root = dir.path().join(".agent/skills");
726 skills_dir::install(&legacy_root, &skill("alpha", "myapp")).unwrap();
727
728 assert!(agent
729 .is_skill_installed(&scope, "alpha")
730 .expect("legacy skill status"));
731 agent.uninstall_skill(&scope, "alpha", "myapp").unwrap();
732 assert!(!dir.path().join(".agent/skills/alpha").exists());
733 }
734
735 #[test]
736 fn skill_install_idempotent() {
737 let dir = tempdir().unwrap();
738 let agent = AntigravityAgent::new();
739 let scope = Scope::Local(dir.path().to_path_buf());
740 let s = skill("alpha", "myapp");
741 agent.install_skill(&scope, &s).unwrap();
742 let r = agent.install_skill(&scope, &s).unwrap();
743 assert!(r.already_installed);
744 }
745
746 #[test]
747 fn skill_uninstall_round_trip() {
748 let dir = tempdir().unwrap();
749 let agent = AntigravityAgent::new();
750 let scope = Scope::Local(dir.path().to_path_buf());
751 agent
752 .install_skill(&scope, &skill("alpha", "myapp"))
753 .unwrap();
754 agent.uninstall_skill(&scope, "alpha", "myapp").unwrap();
755 assert!(!dir.path().join(".agents/skills/alpha").exists());
756 }
757
758 #[test]
759 fn skill_uninstall_owner_mismatch_refused() {
760 let dir = tempdir().unwrap();
761 let agent = AntigravityAgent::new();
762 let scope = Scope::Local(dir.path().to_path_buf());
763 agent
764 .install_skill(&scope, &skill("alpha", "appA"))
765 .unwrap();
766 let err = agent.uninstall_skill(&scope, "alpha", "appB").unwrap_err();
767 assert!(matches!(err, AgentConfigError::NotOwnedByCaller { .. }));
768 }
769
770 #[test]
771 fn skill_supports_both_scopes() {
772 let agent = AntigravityAgent::new();
773 let scopes = agent.supported_skill_scopes();
774 assert!(scopes.contains(&ScopeKind::Local));
775 assert!(scopes.contains(&ScopeKind::Global));
776 }
777
778 #[test]
779 fn install_instruction_writes_under_dot_agents_rules() {
780 let dir = tempdir().unwrap();
781 let agent = AntigravityAgent::new();
782 let scope = Scope::Local(dir.path().to_path_buf());
783 agent
784 .install_instruction(&scope, &instruction("alpha", "myapp"))
785 .unwrap();
786 assert!(dir.path().join(".agents/rules/alpha.md").exists());
787 assert!(!dir.path().join(".agent/rules/alpha.md").exists());
788 }
789
790 #[test]
791 fn legacy_dot_agent_instruction_status_and_uninstall_still_work() {
792 let dir = tempdir().unwrap();
793 let agent = AntigravityAgent::new();
794 let scope = Scope::Local(dir.path().to_path_buf());
795 let legacy = instructions_dir::StandaloneLayout {
796 config_dir: dir.path().join(".agent"),
797 instruction_dir: dir.path().join(".agent/rules"),
798 };
799 instructions_dir::standalone_install(&scope, legacy, &instruction("alpha", "myapp"))
800 .unwrap();
801
802 assert!(agent
803 .is_instruction_installed(&scope, "alpha")
804 .expect("legacy instruction status"));
805 agent
806 .uninstall_instruction(&scope, "alpha", "myapp")
807 .unwrap();
808 assert!(!dir.path().join(".agent/rules/alpha.md").exists());
809 }
810
811 #[test]
812 fn install_mcp_writes_dot_agents_mcp_config() {
813 let dir = tempdir().unwrap();
814 let agent = AntigravityAgent::new();
815 let scope = Scope::Local(dir.path().to_path_buf());
816 agent
817 .install_mcp(&scope, &mcp_spec("github", "myapp"))
818 .unwrap();
819 let cfg = dir.path().join(".agents/mcp_config.json");
820 let v: serde_json::Value = serde_json::from_slice(&fs::read(cfg).unwrap()).unwrap();
821 assert_eq!(
822 v["mcpServers"]["github"]["command"],
823 serde_json::json!("npx")
824 );
825 }
826
827 #[test]
828 fn uninstall_mcp_owner_mismatch_refused() {
829 let dir = tempdir().unwrap();
830 let agent = AntigravityAgent::new();
831 let scope = Scope::Local(dir.path().to_path_buf());
832 agent
833 .install_mcp(&scope, &mcp_spec("github", "appA"))
834 .unwrap();
835 let err = agent.uninstall_mcp(&scope, "github", "appB").unwrap_err();
836 assert!(matches!(err, AgentConfigError::NotOwnedByCaller { .. }));
837 }
838
839 #[test]
840 fn install_hook_writes_hooks_json() {
841 let dir = tempdir().unwrap();
842 let agent = AntigravityAgent::new();
843 let scope = Scope::Local(dir.path().to_path_buf());
844 agent.install(&scope, &hook_only_spec("alpha")).unwrap();
845 let cfg = dir.path().join(".agents/hooks.json");
846 let v: serde_json::Value = serde_json::from_slice(&fs::read(cfg).unwrap()).unwrap();
847 assert_eq!(
848 v["alpha"]["PreToolUse"][0]["matcher"],
849 serde_json::json!("run_command")
850 );
851 }
852}