devflow_core/agents/
antigravity.rs1use super::AgentDriver;
28use crate::phase_id::PhaseId;
29
30pub struct AntigravityDriver;
34
35impl AgentDriver for AntigravityDriver {
36 fn name(&self) -> &'static str {
37 "Antigravity"
38 }
39
40 fn render_prompt(&self, intent: &crate::prompt::StageIntent) -> String {
41 crate::prompt::render_claude_style(intent)
42 }
43
44 fn build_command(
54 &self,
55 _phase: PhaseId,
56 _prompt: &str,
57 _extra_writable_roots: &[std::path::PathBuf],
58 ) -> (&'static str, Vec<String>) {
59 (
60 "agy",
61 vec![
62 "--input-format".into(),
63 "stream-json".into(),
64 "--output-format".into(),
65 "stream-json".into(),
66 "--print-timeout".into(),
67 "60m".into(),
68 ],
69 )
70 }
71
72 fn parse_completion(&self, output: &str) -> Option<crate::agent_result::AgentResult> {
76 crate::agent_result::parse_antigravity_event_result(output)
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83 use crate::agent_result::AgentStatus;
84
85 #[test]
86 fn antigravity_driver_build_command_is_exact() {
87 let (program, args) = AntigravityDriver.build_command(PhaseId::new(0), "x", &[]);
88 assert_eq!(program, "agy");
89 assert_eq!(
90 args,
91 vec![
92 "--input-format".to_string(),
93 "stream-json".to_string(),
94 "--output-format".to_string(),
95 "stream-json".to_string(),
96 "--print-timeout".to_string(),
97 "60m".to_string(),
98 ],
99 "exact argv: no -p, no --dangerously-skip-permissions, no prompt, \
100 --print-timeout above the 5m default (D-01/D-02/F3)"
101 );
102 }
103
104 #[test]
105 fn antigravity_driver_render_prompt_delegates_to_claude_style() {
106 let intent =
107 crate::prompt::StageIntent::for_stage(crate::stage::Stage::Code, PhaseId::new(7));
108 let rendered = AntigravityDriver.render_prompt(&intent);
109 assert_eq!(
110 rendered,
111 crate::prompt::render_claude_style(&intent),
112 "D-05"
113 );
114 assert!(
115 rendered.contains("DEVFLOW_RESULT"),
116 "contract: prompt must carry the marker instruction"
117 );
118 }
119
120 #[test]
121 fn antigravity_driver_parse_completion_delegates() {
122 let capture = concat!(
124 "{\"event\":\"init\",\"model\":\"stub\"}\n",
125 "{\"event\":\"result\",\"result\":{\"status\":\"SUCCESS\",\"response\":\"DEVFLOW_RESULT: {\\\"status\\\":\\\"success\\\"}\"}}\n",
126 );
127 let got = AntigravityDriver
128 .parse_completion(capture)
129 .expect("marker must resolve");
130 assert_eq!(got.status, AgentStatus::Success);
131
132 let error = concat!(
134 "{\"event\":\"init\",\"model\":\"stub\"}\n",
135 "{\"event\":\"result\",\"result\":{\"status\":\"ERROR\",\"response\":\"\",\"error\":\"stream input message is missing the \\\"event\\\" field\"}}\n",
136 );
137 let got = AntigravityDriver
138 .parse_completion(error)
139 .expect("ERROR envelope must resolve");
140 assert_eq!(got.status, AgentStatus::Failed);
141 assert!(
142 got.reason
143 .as_deref()
144 .unwrap()
145 .contains("missing the \"event\" field")
146 );
147
148 let marker_less = concat!(
150 "{\"event\":\"init\",\"model\":\"stub\"}\n",
151 "{\"event\":\"result\",\"result\":{\"status\":\"SUCCESS\",\"response\":\"all done\"}}\n",
152 );
153 assert!(AntigravityDriver.parse_completion(marker_less).is_none());
154 }
155
156 #[test]
162 fn antigravity_driver_spawn_argv_smoke() {
163 let dir = tempfile::tempdir().unwrap();
164 let bin_dir = dir.path().join("bin");
165 let argv_file = dir.path().join("argv.txt");
166 let turn_file = dir.path().join("turn.txt");
167 std::fs::create_dir_all(&bin_dir).unwrap();
168
169 let stub = format!(
170 r#"#!/bin/sh
171printf '%s\n' "$@" > '{}'
172IFS= read -r turn
173printf '%s\n' "$turn" > '{}'
174printf '%s\n' '{{"event":"init","model":"stub","inputFormat":"stream-json","outputFormat":"stream-json"}}'
175printf '%s\n' '{{"event":"result","result":{{"status":"SUCCESS","response":"DEVFLOW_RESULT: {{\"status\":\"success\"}}"}}}}'
176"#,
177 argv_file.display(),
178 turn_file.display(),
179 );
180 std::fs::write(bin_dir.join("agy"), stub).unwrap();
181 #[cfg(unix)]
182 {
183 use std::os::unix::fs::PermissionsExt;
184 std::fs::set_permissions(bin_dir.join("agy"), std::fs::Permissions::from_mode(0o755))
185 .unwrap();
186 }
187
188 let (program, args) = AntigravityDriver.build_command(PhaseId::new(0), "x", &[]);
189 let out = std::process::Command::new(program)
190 .args(&args)
191 .env("PATH", &bin_dir)
192 .stdin(std::process::Stdio::piped())
193 .stdout(std::process::Stdio::piped())
194 .spawn()
195 .and_then(|mut child| {
196 use std::io::Write;
197 child.stdin.take().unwrap().write_all(
198 b"{\"event\":\"user\",\"message\":{\"role\":\"user\",\"content\":\"x\"}}\n",
199 )?;
200 child.wait_with_output()
201 })
202 .expect("stub agy must spawn");
203
204 assert!(out.status.success(), "stub exited {:?}", out.status.code());
205
206 let received: Vec<String> = std::fs::read_to_string(&argv_file)
209 .unwrap()
210 .lines()
211 .map(str::to_string)
212 .collect();
213 assert_eq!(
214 received, args,
215 "spawned argv must equal build_command's argv: {received:?}"
216 );
217
218 let parsed = AntigravityDriver
220 .parse_completion(&String::from_utf8_lossy(&out.stdout))
221 .expect("stub stream must parse");
222 assert_eq!(parsed.status, AgentStatus::Success);
223
224 let turn = std::fs::read_to_string(&turn_file).unwrap();
227 assert!(
228 turn.contains("\"event\":\"user\""),
229 "first turn must be event-key: {turn}"
230 );
231 assert!(!turn.contains("\"type\":"), "no type key allowed: {turn}");
232 }
233
234 #[test]
235 fn antigravity_driver_name_is_correct() {
236 assert_eq!(AntigravityDriver.name(), "Antigravity");
237 }
238}