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
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
//! Committed snapshots of the fully composed PM prompt (#4183).
//!
//! Why: #4249 could gate itself on strict byte-equality against the previous
//! delivered prompt, because it was a mechanical re-sourcing that changed no
//! content. The section-authoring work cannot — it changes the delivered prompt
//! on purpose — and removing that gate with nothing in its place would leave the
//! single highest-blast-radius artifact in the product (the system prompt every
//! PM session receives) with no automatic diff at all. Every future edit to a
//! section source would land as an invisible change.
//!
//! What: one golden file per composition configuration, checked byte-for-byte.
//! A content edit does not "break" these tests in any meaningful sense — it
//! makes them print the change and requires the author to commit the updated
//! snapshot, so the delivered-prompt diff appears in the PR where a reviewer
//! reads it as MEANING. That is the acceptance artifact epic #4183 asks for.
//!
//! Two configurations, deliberately, because they exercise the two composers:
//!
//! | golden | configuration | composer |
//! |---|---|---|
//! | `pm-prompt-bundled-fallback.md` | no `.trusty-mpm/` override, roster present | `InstructionPackage` |
//! | `pm-prompt-roster-absent.md` | no agent deployed in any tier | legacy assembly |
//! | `pm-prompt-claude-md-override.md` | `CLAUDE.md` named sections (#4286) | `InstructionPackage` |
//! | `pm-prompt-launch-with-style.md` | roster present, default style injected (#8453) | `InstructionPackage` + style |
//! | `supervisor-prompt.md` | `profile = "supervisor"` (#8453) | `session_profile` |
//!
//! The second is not redundant. It is the check that a project composing through
//! the string assembly still receives the *same* Core/Memory/Search/Workflow and
//! floor text as one composing through the package — the split-brain that
//! authoring sections would otherwise have introduced, since the legacy path
//! formerly read separate monolithic assets.
//!
//! Regenerate with `UPDATE_GOLDEN=1 cargo test -p trusty-mpm golden`. Review the
//! resulting `git diff` before committing it; that diff IS the deliverable.
use crate::core::content_source::test_support::rc;
use std::path::{Path, PathBuf};
use crate::core::bundled_pm_package::compose_bundled_fallback_with_overrides;
use crate::core::instruction_overrides::resolve_pm_prompt_with_roster;
use tempfile::TempDir;
/// A fixed, deterministic roster.
///
/// The real roster is scanned from disk and varies per machine, which would make
/// the snapshot environment-dependent. Nothing here depends on its content.
const FIXED_ROSTER: &str = "## Delegation Authority\n\n\
### ticketing\n\nHandles ticketing work. Model: sonnet.\n\n\
### rust-engineer\n\nHandles Rust work. Model: sonnet.";
/// A fixed stack-profile block, standing in for `stack_profile_section`.
const FIXED_STACK: &str =
"## Project Stack Profile\n\nDetected stack: Rust. Route implementation to `rust-engineer`.";
/// Directory holding the committed snapshots.
fn golden_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("src")
.join("core")
.join("testdata")
}
/// Compare `actual` against the committed snapshot `name`, or rewrite it.
///
/// Why: an `assert_eq!` on two ~40 KB strings is unreadable, and a snapshot with
/// no regeneration path gets deleted the first time it is inconvenient. This
/// reports the first differing byte with context and names the exact command
/// that updates it, so the correct response to an intentional content change is
/// obvious and cheap.
/// What: with `UPDATE_GOLDEN` set, writes `actual` and passes. Otherwise reads
/// the snapshot and asserts byte-equality.
/// Test: this IS the assertion helper; exercised by both golden tests below.
fn assert_golden(name: &str, actual: &str) {
let path = golden_dir().join(name);
if std::env::var_os("UPDATE_GOLDEN").is_some() {
std::fs::create_dir_all(golden_dir()).expect("create testdata dir");
std::fs::write(&path, actual).expect("write golden");
return;
}
let expected = std::fs::read_to_string(&path)
.unwrap_or_else(|err| panic!("missing golden {}: {err}", path.display()));
if expected == actual {
return;
}
let (e, a) = (expected.as_bytes(), actual.as_bytes());
let at = e
.iter()
.zip(a.iter())
.position(|(x, y)| x != y)
.unwrap_or_else(|| e.len().min(a.len()));
/// Bytes of context shown either side of the divergence.
const WINDOW: usize = 200;
let from = at.saturating_sub(WINDOW);
let window = |s: &[u8]| String::from_utf8_lossy(&s[from..s.len().min(at + WINDOW)]).to_string();
panic!(
"DELIVERED PM PROMPT CHANGED — {name}\n\
first difference at byte offset {at} (golden {} bytes, composed {} bytes)\n\
--- golden [{from}..] ---\n{}\n\
--- composed [{from}..] ---\n{}\n\
If this change is intended, regenerate and review the diff:\n\
\x20 UPDATE_GOLDEN=1 cargo test -p trusty-mpm golden\n",
e.len(),
a.len(),
window(e),
window(a),
);
}
#[test]
fn golden_bundled_fallback_prompt() {
// Configuration 1: what every project with no `.trusty-mpm/` override
// receives, composed through `InstructionPackage`.
let composed =
compose_bundled_fallback_with_overrides(rc(), FIXED_STACK, FIXED_ROSTER, None, &[])
.0
.expect("package composes");
assert_golden("pm-prompt-bundled-fallback.md", &composed);
}
#[test]
fn golden_roster_absent_assembly_prompt() {
// Configuration 2, REPLACING the retired `.trusty-mpm/AGENT_DELEGATION.md`
// snapshot (#4286). That configuration no longer exists — no file can force
// the string assembly any more — so the golden that covered it would have
// pinned an unreachable code path.
//
// The string assembly is still reachable by exactly one route: no agent
// deployed in any tier, so there is no roster for the packaged composer to
// consume. That is the configuration snapshotted here, and it serves the
// same purpose the old one did — proving a project on the non-packaged path
// receives the same Core/Memory/Search/Workflow and floor text as everyone
// else, rather than being frozen on a divergent copy.
let tmp = TempDir::new().expect("tempdir");
let (prompt, _) = resolve_pm_prompt_with_roster(rc(), tmp.path(), || None);
assert_golden("pm-prompt-roster-absent.md", &prompt);
}
#[test]
fn golden_claude_md_override_prompt() {
// Configuration 3 (#4286): named-section overrides marked out in the
// project's own `CLAUDE.md`. The snapshot is what makes the DELIVERED shape
// of an override reviewable — that WORKFLOW is replaced wholesale, that
// AGENT-DELEGATION replaces the doctrine but NOT the live roster below it,
// and that the framework floor is byte-for-byte the floor every other
// configuration receives.
let tmp = TempDir::new().expect("tempdir");
std::fs::write(
tmp.path().join("CLAUDE.md"),
"# Project Instructions\n\n\
Prose outside the markers is not instruction content and is ignored.\n\n\
<!-- TRUSTY-MPM: WORKFLOW START v=1 -->\n\
# Workflow (project override)\n\n\
Two phases only: implement, then verify.\n\
<!-- TRUSTY-MPM: WORKFLOW END -->\n\n\
<!-- TRUSTY-MPM: AGENT-DELEGATION START v=1 -->\n\
# Routing (project override)\n\n\
Route every implementation task to `rust-engineer`.\n\
<!-- TRUSTY-MPM: AGENT-DELEGATION END -->\n",
)
.expect("write CLAUDE.md");
let (prompt, _) =
resolve_pm_prompt_with_roster(rc(), tmp.path(), || Some(FIXED_ROSTER.to_string()));
assert_golden("pm-prompt-claude-md-override.md", &prompt);
}
#[test]
fn golden_pm_launch_prompt_with_output_style() {
// #8453: the supervisor profile must leave a PM session untouched. This
// snapshot was written from the pre-#8453 tree: the roster-present prompt
// with the default output style injected ahead of it (the path a Claude
// Code without native `outputStyle` support takes), so it pins the PM
// instructions and the PM output style in one byte-exact comparison.
let tmp = TempDir::new().expect("tempdir");
let (prompt, _) =
resolve_pm_prompt_with_roster(rc(), tmp.path(), || Some(FIXED_ROSTER.to_string()));
let styled = crate::core::output_style::apply_output_style_to_prompt_with_native(
rc(),
tmp.path(),
Some(crate::core::bundle::DEFAULT_OUTPUT_STYLE_ID),
prompt,
false,
);
assert_golden("pm-prompt-launch-with-style.md", &styled);
}
#[test]
fn golden_supervisor_prompt() {
// #8453: the supervisor profile's composed instructions — what a
// `profile = "supervisor"` project receives in place of the PM prompt.
let tmp = TempDir::new().expect("tempdir");
std::fs::write(
tmp.path().join(".trusty-mpm.toml"),
"profile = \"supervisor\"\n",
)
.expect("write config");
let (prompt, _) = crate::core::instruction_overrides::resolve_pm_prompt_with_roster_for(
rc(),
tmp.path(),
crate::core::session_profile::SessionProfile::Supervisor,
|| Some(FIXED_ROSTER.to_string()),
);
assert_eq!(
prompt,
crate::core::session_profile::supervisor_prompt(rc())
);
assert_golden("supervisor-prompt.md", &prompt);
}
/// The memory section as DELIVERED, or `None` when the heading is absent.
///
/// Why: the claim under test is about one section, not the whole prompt, and a
/// whole-prompt assertion would also fire on an unrelated section that
/// legitimately names a hook event.
/// What: the slice from the memory heading up to the next `## ` heading.
/// Test: used by `the_memory_section_claims_no_per_prompt_context_hook`.
fn memory_section(prompt: &str) -> Option<&str> {
let start = prompt.find("## Memory Protocol (Context-First)")?;
let rest = &prompt[start..];
let end = rest[1..]
.find("\n## ")
.map(|at| at + 1)
.unwrap_or(rest.len());
Some(&rest[..end])
}
#[test]
fn the_memory_section_claims_no_per_prompt_context_hook() {
// #7835: the section told the PM that a `UserPromptSubmit` hook injects a
// palace-context block into EVERY prompt, and on that basis told it not to
// re-fetch. No such hook was registered in any settings tier, so the PM
// skipped `memory_recall` waiting on a block that never arrived. Palace
// context in fact arrives once, as the launch-time catch-up seed
// (`core::session_launch`, `catchup_context`), re-readable on demand through
// the `session_context_catchup` MCP tool.
//
// Asserted over all three compositions because the two composers deliver
// this section independently; one of them silently keeping the old claim is
// exactly the split-brain the goldens above exist to catch.
let bundled =
compose_bundled_fallback_with_overrides(rc(), FIXED_STACK, FIXED_ROSTER, None, &[])
.0
.expect("package composes");
let tmp = TempDir::new().expect("tempdir");
let (roster_absent, _) = resolve_pm_prompt_with_roster(rc(), tmp.path(), || None);
let (with_roster, _) =
resolve_pm_prompt_with_roster(rc(), tmp.path(), || Some(FIXED_ROSTER.to_string()));
for (label, prompt) in [
("bundled fallback", &bundled),
("roster absent", &roster_absent),
("roster present", &with_roster),
] {
let section = memory_section(prompt)
.unwrap_or_else(|| panic!("{label}: the memory section is missing from the prompt"));
assert!(
!section.contains("UserPromptSubmit"),
"{label}: the memory section still promises a per-prompt hook \
injection that nothing registers (#7835):\n{section}"
);
assert!(
section.contains("session_context_catchup"),
"{label}: the memory section must name the mechanism that really \
delivers palace context (#7835):\n{section}"
);
}
}