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