返回 CodeWhale
single_turn_loop.rs
根目录 / crates / core / tests / single_turn_loop.rs
1 //! The workspace must contain exactly one turn loop.
2 //!
3 //! `crates/core` carried a placeholder `engine/` tree whose `Engine::run`
4 //! accepted `Op::SendMessage`, appended to a journal, and emitted
5 //! `TurnComplete { status: "completed" }` without ever contacting a model. It
6 //! had no callers, but its doc comments ("the real turn loop is wired here in
7 //! the next slice") were load-bearing for `docs/ARCHITECTURE.md`'s claim that
8 //! core owns the agent loop, and a reader could reasonably have built on it.
9 //!
10 //! This guard is deliberately a source scan rather than a type check: the thing
11 //! being prevented is a *second implementation*, which by definition would not
12 //! be reachable from the first.
13 //!
14 //! Interim exception, recorded not hidden (#6088): `acp_server.rs` runs its
15 //! own agentic tool loop (`run_agentic_prompt_turn`) for ACP IDE sessions,
16 //! which do not run on the full thread/turn runtime yet. #5835 (IDE stage 2)
17 //! converges them onto `Engine::run_turn` and deletes this exception along
18 //! with the loop. Until then the scan below asserts the exception set is
19 //! exactly these two owners — a third loop fails the same way a second
20 //! used to.
21
22 use std::path::{Path, PathBuf};
23
24 fn workspace_root() -> PathBuf {
25 // crates/core/tests -> crates/core -> crates -> <root>
26 Path::new(env!("CARGO_MANIFEST_DIR"))
27 .ancestors()
28 .nth(2)
29 .expect("workspace root above crates/core")
30 .to_path_buf()
31 }
32
33 fn rust_sources(dir: &Path, out: &mut Vec<PathBuf>) {
34 let Ok(entries) = std::fs::read_dir(dir) else {
35 return;
36 };
37 for entry in entries.flatten() {
38 let path = entry.path();
39 let name = entry.file_name();
40 let name = name.to_string_lossy();
41 if path.is_dir() {
42 if name == "target" || name == ".git" || name == "node_modules" {
43 continue;
44 }
45 rust_sources(&path, out);
46 } else if path.extension().is_some_and(|e| e == "rs") {
47 out.push(path);
48 }
49 }
50 }
51
52 #[test]
53 fn workspace_declares_exactly_one_turn_loop() {
54 let root = workspace_root();
55 let crates = root.join("crates");
56 assert!(crates.is_dir(), "expected {} to exist", crates.display());
57
58 let mut files = Vec::new();
59 rust_sources(&crates, &mut files);
60 assert!(
61 files.len() > 100,
62 "source scan found too few files to trust"
63 );
64
65 let mut found = Vec::new();
66 let mut excepted = Vec::new();
67 for file in &files {
68 let Ok(text) = std::fs::read_to_string(file) else {
69 continue;
70 };
71 for (idx, line) in text.lines().enumerate() {
72 let trimmed = line.trim_start();
73 if trimmed.starts_with("async fn run_turn")
74 || trimmed.starts_with("pub async fn run_turn")
75 || trimmed.starts_with("pub(crate) async fn run_turn")
76 || trimmed.starts_with("pub(super) async fn run_turn")
77 {
78 found.push((
79 file.strip_prefix(&root).unwrap_or(file).to_path_buf(),
80 idx + 1,
81 ));
82 }
83 // Interim #6088 exception: the ACP IDE loop, matched by its own
84 // name so it cannot hide behind the `run_turn` spelling.
85 if trimmed.starts_with("async fn run_agentic_prompt_turn")
86 || trimmed.starts_with("pub(crate) async fn run_agentic_prompt_turn")
87 {
88 excepted.push((
89 file.strip_prefix(&root).unwrap_or(file).to_path_buf(),
90 idx + 1,
91 ));
92 }
93 }
94 }
95
96 assert_eq!(
97 found.len(),
98 1,
99 "expected exactly one turn loop in the workspace, found {}: {found:#?}\n\
100 A second `run_turn` means two implementations of the agent loop. If the \
101 runtime is being migrated, move the one that exists rather than adding \
102 another beside it.",
103 found.len()
104 );
105 let expected_owner = Path::new("crates")
106 .join("tui")
107 .join("src")
108 .join("core")
109 .join("engine")
110 .join("turn_loop.rs");
111 let (owner_path, owner_line) = &found[0];
112 assert_eq!(
113 owner_path,
114 &expected_owner,
115 "the turn loop moved to {}:{} — update this guard and docs/ARCHITECTURE.md \
116 together so the documented owner stays true",
117 owner_path.display(),
118 owner_line
119 );
120 let expected_exception = Path::new("crates")
121 .join("tui")
122 .join("src")
123 .join("acp_server.rs");
124 assert_eq!(
125 excepted.len(),
126 1,
127 "expected exactly one recorded #6088 exception (acp_server's agentic \
128 loop), found {}: {excepted:#?}\n\
129 A second `run_agentic_prompt_turn` is a third turn loop — converge it \
130 onto Engine::run_turn instead. If #5835 deleted the ACP loop, delete \
131 this exception with it.",
132 excepted.len()
133 );
134 assert_eq!(
135 &excepted[0].0,
136 &expected_exception,
137 "the #6088 exception moved to {} — update this guard, #6088, and #5835 \
138 together so the recorded owner stays true",
139 excepted[0].0.display(),
140 );
141 }
142
143 #[test]
144 fn core_does_not_reintroduce_a_placeholder_engine_module() {
145 let core_src = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
146 assert!(
147 !core_src.join("engine").exists(),
148 "crates/core/src/engine/ is back. It was removed in v0.9.11 because it \
149 emitted TurnComplete without calling a model and had no consumers; a \
150 boundary type that does real work belongs in a named module, not a \
151 second `engine`."
152 );
153 }
154
154 lines RUST