1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
//! Happy-path test: full orchestration with valid inputs, over the
//! brazen `bz` data plane (ARCH §4.4).
//!
//! Asserts the conversation branch is spawned, the dispatch commit lays
//! goal.md + soul.md, the diagnostic step record lands at the conv-repo
//! root outside the worktree (§2.2 / §2.3), the request is a typed
//! canonical request, and the response is brazen `v=1` NDJSON with a
//! terminal `end`.
use super::fixtures::*;
use super::stubs::STUB_SHA;
use crate::prompt::run;
use crate::prompt::step::StepMeta;
use std::ffi::OsStr;
#[test]
fn run_happy_path_writes_branch_worktree_and_two_commits() {
let repo = scaffold_repo(VALID_PER_REPO_PROVIDERS_YAML, Some("system body"));
let harness = scaffold_harness_root();
let adapter = StubAdapter::happy(&happy_response_bytes());
let git = StubGit::ok();
let (clock, id) = (FixedClock::default(), FixedIdGen);
let (sleeper, tool_executor) = (StubSleeper::default(), StubToolExecutor::ok());
let branch = run(
repo.path(),
"hello",
None,
None,
None,
crate::prompt::PinnedDocs::none(),
&valid_deps(
&adapter,
&sleeper,
&git,
&clock,
&id,
&tool_executor,
harness.path(),
),
)
.unwrap();
assert_eq!(branch, "ct-1-deadbeef");
let worktree = worktree_path(repo.path());
let repo_git = crate::workspace::repo_git(repo.path());
let goal = std::fs::read_to_string(worktree.join("goal.md")).unwrap();
assert_eq!(goal, "hello");
let soul = std::fs::read_to_string(worktree.join("soul.md")).unwrap();
assert_eq!(soul, "system body");
assert!(!worktree.join("steps").exists());
let step_dir = repo.path().join("steps/ct-1-deadbeef/001");
let request: serde_json::Value =
serde_json::from_slice(&std::fs::read(step_dir.join("request.json")).unwrap()).unwrap();
assert_eq!(request["model"], "claude-sonnet-5");
// Goal is prepended to the soul and rides as a canonical
// `Content::Text` in `system[0]` (§2.8, §4.4 typed request).
assert_eq!(
request["system"][0]["text"].as_str().unwrap(),
"<goal>\nhello\n</goal>\n\nsystem body"
);
assert_eq!(request["messages"][0]["role"], "user");
// The initial user message entered through the front door (§2.11):
// deposited into the agent's own inbox, then delivered by the step-1
// drain. Its `from:` / `deposited_at:` frontmatter travels with the
// file and is model-visible by design (§2.11) — `deposited_at` is the
// first `now_iso8601` tick (`iso-1`).
assert_eq!(
request["messages"][0]["content"][0]["text"],
"---\nfrom: user\ndeposited_at: iso-1\n---\nhello"
);
assert_eq!(request["max_tokens"], 4096);
// `stream` is not set by lernie — brazen's default governs (§4.4).
// The typed request serializes an unset Option as JSON `null`.
assert!(request["stream"].is_null());
// response.json is brazen `v=1` NDJSON: first line message_start,
// and the terminal line is `{"type":"end"}` (§4.4).
let lines = parse_jsonl(&std::fs::read(step_dir.join("response.json")).unwrap());
assert!(lines.len() >= 2, "expected event stream, got {lines:?}");
assert_eq!(lines.first().unwrap()["type"], "message_start");
let text = lines
.iter()
.find(|e| e["type"] == "content_delta")
.expect("expected a content_delta");
assert_eq!(text["delta"]["text_delta"], "hi there");
assert_eq!(lines.last().unwrap()["type"], "end");
let finish = lines.iter().find(|e| e["type"] == "finish").unwrap();
assert_eq!(finish["reason"], "stop");
// meta.json carries the branch-tip sha at step-start (§2.10). The
// stub git's revision captures return the fixed stub sha.
// The deposit's `deposited_at` consumed `iso-1`, so the step-1 model
// call bookends at `iso-2` / `iso-3`.
let meta: StepMeta =
serde_json::from_slice(&std::fs::read(step_dir.join("meta.json")).unwrap()).unwrap();
assert_eq!(meta.commit, STUB_SHA);
assert_eq!(meta.started_at, "iso-2");
assert_eq!(meta.ended_at, "iso-3");
// Adapter called twice: the version guard (`bz --version`) then the
// model call (`bz --json --provider anthropic`, request on stdin).
let invocations = adapter.observed.borrow().clone();
assert_eq!(invocations.len(), 2, "version guard + model call");
let (binary, args, stdin) = invocations[0].clone();
assert_eq!(binary, OsStr::new("bz"));
assert_eq!(args, vec!["--version"]);
assert!(stdin.is_empty());
let (binary, args, stdin) = invocations[1].clone();
assert_eq!(binary, OsStr::new("bz"));
assert_eq!(args, vec!["--json", "--provider", "anthropic"]);
// The model-call stdin matches request.json byte-for-byte in
// content (pretty-print differs, so compare parsed).
let wire: serde_json::Value = serde_json::from_slice(&stdin).unwrap();
assert_eq!(wire, request);
// Git sequence: 8 (the start's preamble against repo.git — the
// fork-point lineage query, §2.3; then control resolution from the
// config commit, §2.2: the `config/*` head enumeration and its
// merge-base, the ancestry derivation of the governing commit, plus
// five `show` reads — `version` first, the §10 schema-version guard,
// then providers/workflow/manifest/soul)
// + 1 (branch spawn off the fork point) + 9 (dispatch commit:
// control-file removal, the descriptor derivation's four `cat-file
// -e` existence reads and one `checkout` — §3.3 — the settled-name
// stage (§2.3, `workspace::agent_name`), then add, commit —
// §2.3 step 2) + 1 (drain
// stray-probe, §2.11) + 2 (user-message delivery commit, §2.11) + 1
// (rev-parse) + 2 (model-output transcript entry add + commit) + 1
// (terminal result-deposit rev-parse, §2.6) + 1 (the deposit's
// durable returned-mark `update-ref`, §8 — this rig's compact ids
// make the agent parse as a child, so the deposit is real).
// Merge-back is gone (§2.6): the root branch persists on its own
// ref. The version guard runs no git.
let runs = git.runs.borrow();
assert_eq!(runs.len(), 26);
for (dest, _args) in &runs[0..9] {
assert_eq!(dest, &repo_git, "control + spawn run against repo.git");
}
// The start's preamble: the fork point (§2.3) — the lineage pool
// the default `--config` is checked against — then the ancestry
// derivation of its governing config commit (§2.2), taken against
// the fork point itself.
assert_eq!(runs[0].1[1], "--format=%(refname:short)");
assert_eq!(runs[1].1[1], "--format=%(refname)");
assert_eq!(
runs[2].1,
vec!["merge-base", "config/default", "refs/heads/config/default"]
);
assert_eq!(runs[3].1, vec!["show", &format!("{STUB_SHA}:version")]);
assert_eq!(
runs[4].1,
vec!["show", &format!("{STUB_SHA}:providers.yaml")]
);
assert_eq!(
runs[5].1,
vec!["show", &format!("{STUB_SHA}:workflow.yaml")]
);
assert_eq!(
runs[6].1,
vec!["show", &format!("{STUB_SHA}:manifest.yaml")]
);
assert_eq!(
runs[7].1,
vec!["show", &format!("{STUB_SHA}:souls/worker.md")]
);
let args8 = &runs[8].1;
assert_eq!(
args8[..4],
["worktree", "add", "-b", "agents/ct-1-deadbeef"]
);
assert_eq!(args8[4], worktree.to_string_lossy().to_string());
assert_eq!(args8[5], "config/default");
for (dest, _args) in &runs[9..25] {
assert_eq!(dest, &worktree, "post-spawn git runs inside the worktree");
}
// Dispatch commit (§2.3 step 2): the config commit's control files
// leave the agent's tree (§2.2), then goal + soul commit.
assert_eq!(runs[9].1[..5], ["rm", "-r", "-q", "--ignore-unmatch", "--"]);
let removed: Vec<&str> = runs[9].1[5..].iter().map(String::as_str).collect();
assert_eq!(removed, crate::workspace::CONTROL_PATHS);
// 10-14 are the descriptor derivation (§3.3): the grant checked
// against the governing config commit, then checked out of it —
// asserted arg-for-arg in [`super::descriptor_prune`].
// 13 stages the settled name (§2.3): the trim's fourth part, always
// written — empty here, since this root was started without one.
assert_eq!(runs[15].1, vec!["add", "name"]);
assert_eq!(std::fs::read_to_string(worktree.join("name")).unwrap(), "");
assert_eq!(runs[16].1, vec!["add", "goal.md", "soul.md"]);
assert_eq!(runs[17].1[0], "commit");
assert!(runs[17].1[2].contains("step 001: dispatch"));
assert!(runs[17].1[2].contains("[ct-1-deadbeef]"));
// The step-1 drain (§2.11 *Delivery*): a stray-recovery probe over
// messages/ (clean here — no add/commit), then the initial user
// message delivered from the inbox as the first transcript entry,
// before step 1's read state is captured.
assert_eq!(runs[18].1, vec!["status", "--porcelain", "--", "messages"]);
assert_eq!(runs[19].1, vec!["add", "messages/001-user.md"]);
assert!(runs[20].1[2].contains("transcript 001: user"));
assert_eq!(runs[21].1, vec!["rev-parse", "HEAD"]);
// The transcript writer commits the model-output entry (§2.3): the
// sealed staging file is renamed to messages/002-<model-id>.json —
// the origin token is the model that authored it (§2.3) — and
// committed.
assert_eq!(runs[22].1, vec!["add", "messages/002-claude-sonnet-5.json"]);
assert_eq!(runs[23].1[0], "commit");
assert!(runs[23].1[2].contains("transcript 002: claude-sonnet-5"));
assert!(runs[23].1[2].contains("[ct-1-deadbeef]"));
// The renamed entry is on disk in the worktree and holds the
// canonical model-output blocks (the "hi there" text block) plus the
// provider's own token usage (§2.3 *Usage rides the entry*) — so a transcript reader
// states real counts from the committed bytes alone.
let entry = worktree.join("messages/002-claude-sonnet-5.json");
let committed: serde_json::Value =
serde_json::from_slice(&std::fs::read(&entry).unwrap()).unwrap();
assert_eq!(committed["content"][0]["type"], "text");
assert_eq!(committed["content"][0]["text"], "hi there");
assert_eq!(
committed["usage"],
serde_json::json!({"input_tokens": 5, "output_tokens": 3})
);
// The staging file left by rename — no debris under steps/.
assert!(!step_dir.join("staging.json").exists());
// The terminal result deposit (§2.6, §2.3 step 5) reads the branch
// tip as its terminal ref (`rev-parse HEAD`); the deposit itself is a
// structural no-op for a root (no parent inbox, §2.4), so it lands no
// git op of its own and no merge-back follows.
assert_eq!(runs[24].0, worktree);
assert_eq!(runs[24].1, vec!["rev-parse", "HEAD"]);
}