flake: lift cargo test out of buildPackage so prompt edits don't bust the binary cache (#555)
Per mara's review on PR #561: the previous commit kept `./hive-ag3nt/prompts` in `cleanSrc` because `hive-ag3nt::prompt::tests` had a compile-time `include_str!("../prompts/system.md")`. That meant a prompt edit still busted the cargo cache. This change: - Replaces the test-side `include_str!` with a runtime read from `$HIVE_ASSETS_DIR/prompts/system.md` (with a CARGO_MANIFEST_DIR fallback for plain `cargo test` from a checked-out repo). - Drops `./hive-ag3nt/prompts` from `cleanSrc` — it's now `craneLib.cleanCargoSource ./.` (Cargo.* + *.rs only). - Sets `doCheck = false` on `packages.default` and lifts `cargo test` into a separate `checks.cargo-test` derivation that carries the `hyperhive-assets` build input. That scopes the asset rebuild blast radius to the test check — `nix flake check` still exercises the suite, but the binary derivation no longer carries the assets dep. Verified cache-invariance matrix (via `echo '' >> <f>; nix eval .#default.outPath`): | edit | default | cargo-test | clippy | |-------------------------|---------|------------|--------| | README.md | stable | stable | stable | | branding/hyperhive.svg | stable | CHANGED | stable | | nix/modules/* | stable | stable | stable | | prompts/system.md | stable | CHANGED | stable | | hive-c0re/src/main.rs | CHANGED | CHANGED | CHANGED | (`cargo-test` CHANGED on prompts/branding is correct — tests read the production template + need the assets output.)
This commit is contained in:
parent
0058ed1e59
commit
b3f675e367
2 changed files with 93 additions and 42 deletions
|
|
@ -117,18 +117,19 @@ fn parse_close_marker(line: &str) -> Option<&str> {
|
|||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the system prompt file cannot be written.
|
||||
pub async fn write_system_prompt(
|
||||
socket: &Path,
|
||||
label: &str,
|
||||
flavor: Flavor,
|
||||
) -> Result<PathBuf> {
|
||||
pub async fn write_system_prompt(socket: &Path, label: &str, flavor: Flavor) -> Result<PathBuf> {
|
||||
let parent = socket.parent().unwrap_or_else(|| Path::new("/run/hive"));
|
||||
tokio::fs::create_dir_all(parent).await.ok();
|
||||
let pronouns = std::env::var("HIVE_OPERATOR_PRONOUNS").unwrap_or_else(|_| "she/her".to_owned());
|
||||
let template_path = hive_sh4re::assets::prompt_template();
|
||||
let template = tokio::fs::read_to_string(&template_path)
|
||||
.await
|
||||
.with_context(|| format!("read claude system prompt template from {}", template_path.display()))?;
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"read claude system prompt template from {}",
|
||||
template_path.display()
|
||||
)
|
||||
})?;
|
||||
let body = render(&template, flavor, label, &pronouns);
|
||||
let path = parent.join("claude-system-prompt.md");
|
||||
tokio::fs::write(&path, body).await?;
|
||||
|
|
@ -139,18 +140,36 @@ pub async fn write_system_prompt(
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
// #555: production reads the system-prompt template from
|
||||
// `$HIVE_ASSETS_DIR/prompts/system.md` at startup. The test module
|
||||
// still `include_str!`s it directly because:
|
||||
// 1. the "real template still substitutes / still filters" tests
|
||||
// below need the actual production wording to be honest;
|
||||
// 2. embedding it at compile time keeps `cargo test --workspace`
|
||||
// runnable without setting `HIVE_ASSETS_DIR`;
|
||||
// 3. this is the ONLY remaining compile-time reference to
|
||||
// `prompts/system.md` from the rust workspace — production
|
||||
// code loads it at runtime.
|
||||
const PRODUCTION_TEMPLATE: &str = include_str!("../prompts/system.md");
|
||||
// #555: the production template lives at
|
||||
// `$HIVE_ASSETS_DIR/prompts/system.md` and is loaded at runtime.
|
||||
// The unit tests below want to assert against the actual production
|
||||
// wording (so the renderer + tool surface stay honest), so they
|
||||
// resolve the same path at test runtime via two fallbacks:
|
||||
// 1. `$HIVE_ASSETS_DIR/prompts/system.md` — the runtime contract
|
||||
// production uses. The flake's `checks.cargo-test` derivation
|
||||
// sets this to the `hyperhive-assets` output so `cargo test`
|
||||
// inside the nix sandbox finds the file without needing
|
||||
// `prompts/` in the cargo source tree. `packages.default`
|
||||
// explicitly does NOT carry the assets dep, so a prompt edit
|
||||
// doesn't bust the binary derivation — only this test check.
|
||||
// 2. `env!("CARGO_MANIFEST_DIR")/prompts/system.md` — for plain
|
||||
// `cargo test --workspace` from a checked-out repo where the
|
||||
// env var isn't set; `env!` is a compile-time string lookup,
|
||||
// no file open at compile, so this still doesn't pull
|
||||
// `prompts/` into the build hash.
|
||||
// The combined effect is that the flake's `cleanSrc` no longer
|
||||
// unions `./hive-ag3nt/prompts` — tweaks to system.md don't bust
|
||||
// the cargo cache anymore.
|
||||
static PRODUCTION_TEMPLATE: LazyLock<String> = LazyLock::new(|| {
|
||||
let path = match std::env::var("HIVE_ASSETS_DIR") {
|
||||
Ok(v) if !v.is_empty() => format!("{v}/prompts/system.md"),
|
||||
_ => concat!(env!("CARGO_MANIFEST_DIR"), "/prompts/system.md").to_owned(),
|
||||
};
|
||||
std::fs::read_to_string(&path)
|
||||
.unwrap_or_else(|e| panic!("read production prompt template at {path}: {e}"))
|
||||
});
|
||||
|
||||
const SAMPLE: &str = "\
|
||||
shared opener
|
||||
|
|
@ -251,7 +270,7 @@ shared closer
|
|||
// Real template's first agent line — keeps the renderer
|
||||
// honest about the {label} / {operator_pronouns} pair the
|
||||
// harness already relied on.
|
||||
let rendered = render(PRODUCTION_TEMPLATE, Flavor::Agent, "alice", "they/them");
|
||||
let rendered = render(&PRODUCTION_TEMPLATE, Flavor::Agent, "alice", "they/them");
|
||||
assert!(rendered.contains("hyperhive agent `alice`"));
|
||||
assert!(rendered.contains("**they/them** pronouns"));
|
||||
assert!(!rendered.contains("{label}"));
|
||||
|
|
@ -264,7 +283,7 @@ shared closer
|
|||
// kill, schedule_*) MUST NOT appear in the agent's rendered
|
||||
// prompt. Drift between flavor and tool surface bites every
|
||||
// time it happens (cf. #511 missing-allow-list bug).
|
||||
let rendered = render(PRODUCTION_TEMPLATE, Flavor::Agent, "alice", "she/her");
|
||||
let rendered = render(&PRODUCTION_TEMPLATE, Flavor::Agent, "alice", "she/her");
|
||||
assert!(!rendered.contains("request_init_config"));
|
||||
assert!(!rendered.contains("request_apply_commit"));
|
||||
assert!(!rendered.contains("get_logs"));
|
||||
|
|
@ -275,7 +294,7 @@ shared closer
|
|||
|
||||
#[test]
|
||||
fn render_manager_includes_manager_only_tools() {
|
||||
let rendered = render(PRODUCTION_TEMPLATE, Flavor::Manager, "hm1nd", "she/her");
|
||||
let rendered = render(&PRODUCTION_TEMPLATE, Flavor::Manager, "hm1nd", "she/her");
|
||||
assert!(rendered.contains("request_init_config"));
|
||||
assert!(rendered.contains("request_apply_commit"));
|
||||
assert!(rendered.contains("get_logs"));
|
||||
|
|
@ -287,9 +306,9 @@ shared closer
|
|||
|
||||
#[test]
|
||||
fn render_uses_correct_role_opener() {
|
||||
let agent = render(PRODUCTION_TEMPLATE, Flavor::Agent, "alice", "she/her");
|
||||
let agent = render(&PRODUCTION_TEMPLATE, Flavor::Agent, "alice", "she/her");
|
||||
assert!(agent.starts_with("You are hyperhive agent"));
|
||||
let manager = render(PRODUCTION_TEMPLATE, Flavor::Manager, "hm1nd", "she/her");
|
||||
let manager = render(&PRODUCTION_TEMPLATE, Flavor::Manager, "hm1nd", "she/her");
|
||||
assert!(manager.starts_with("You are the hyperhive manager"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue