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
72
flake.nix
72
flake.nix
|
|
@ -47,24 +47,23 @@
|
|||
treefmt-eval = treefmt-nix.lib.evalModule pkgs treefmt-config;
|
||||
craneLib = crane.mkLib pkgs;
|
||||
# Narrowed source tree the rust derivations consume.
|
||||
# `commonCargoSources` is crane's standard "everything cargo
|
||||
# cares about" filter (Cargo.toml/Cargo.lock + *.rs); we
|
||||
# union it with the one non-rust path the workspace still
|
||||
# references — `hive-ag3nt/prompts/` — which is read at
|
||||
# compile time by `hive-ag3nt::prompt::tests` via
|
||||
# `include_str!`. Branding assets + claude prompts at
|
||||
# runtime live in the `hyperhive-assets` derivation
|
||||
# (#555), so a tweak to e.g. `branding/hyperhive.svg`,
|
||||
# `README.md`, the `nix/` modules, or the frontend tree
|
||||
# does NOT bust this src hash and the rust derivations
|
||||
# stay cached.
|
||||
cleanSrc = lib.fileset.toSource {
|
||||
root = ./.;
|
||||
fileset = lib.fileset.unions [
|
||||
(craneLib.fileset.commonCargoSources ./.)
|
||||
./hive-ag3nt/prompts
|
||||
];
|
||||
};
|
||||
# `cleanCargoSource` is crane's standard "everything cargo
|
||||
# cares about" filter (Cargo.toml/Cargo.lock + *.rs). All
|
||||
# non-rust runtime assets — branding + the claude system
|
||||
# prompt template + claude-settings.json — live in the
|
||||
# separate `hyperhive-assets` derivation (#555) and are
|
||||
# loaded by the binaries at runtime from `$HIVE_ASSETS_DIR`.
|
||||
# The unit tests in `hive-ag3nt::prompt` read the same
|
||||
# `prompts/system.md` directly from the workspace tree at
|
||||
# *test* runtime (via `env!("CARGO_MANIFEST_DIR")` — a
|
||||
# compile-time string, no file open at compile), so the
|
||||
# prompt template doesn't have to be in this fileset to
|
||||
# keep `cargo test` honest. Net effect: tweaks to any
|
||||
# non-`*.rs` / non-`Cargo.*` file (README, branding,
|
||||
# nix modules, frontend tree, OR `hive-ag3nt/prompts/*`)
|
||||
# do NOT bust this src hash, so the rust derivations
|
||||
# stay fully cached.
|
||||
cleanSrc = craneLib.cleanCargoSource ./.;
|
||||
# Build the workspace's dependency tree once, cached as
|
||||
# its own derivation. `buildPackage` and `cargoClippy`
|
||||
# both reuse this via `inherit cargoArtifacts;` so a
|
||||
|
|
@ -111,12 +110,24 @@
|
|||
...
|
||||
}:
|
||||
{
|
||||
# Build the workspace binaries without running tests. Tests
|
||||
# are run as a separate check (`checks.cargo-test`) that
|
||||
# carries the `hyperhive-assets` build input — `hive-ag3nt::
|
||||
# prompt::tests` reads the production prompt template at test
|
||||
# runtime through `$HIVE_ASSETS_DIR`, so wiring the env var
|
||||
# into the build phase here would make the prompt's hash a
|
||||
# build input of `default` (defeats #555's cache goal: a
|
||||
# prompt edit would still bust the binary derivation, even
|
||||
# though no .rs file changed). Keeping tests in a separate
|
||||
# check derivation localises the asset-rebuild blast radius
|
||||
# to that one check — `nix flake check` still exercises them.
|
||||
default = craneLib.buildPackage {
|
||||
src = cleanSrc;
|
||||
inherit cargoArtifacts nativeBuildInputs;
|
||||
pname = "hyperhive-workspace";
|
||||
version = "0.1.0";
|
||||
meta.description = "hyperhive workspace (hive-c0re, hive-ag3nt, hive-m1nd)";
|
||||
doCheck = false;
|
||||
};
|
||||
# Bundled browser assets — see ./nix/frontend.nix. Output is
|
||||
# $out/{dashboard,agent}/ which the Rust binaries serve via
|
||||
|
|
@ -125,10 +136,12 @@
|
|||
branding-svg = ./branding/hyperhive.svg;
|
||||
};
|
||||
# Static runtime assets the rust binaries read via
|
||||
# `assets::path()` (#555): branding/* + hive-ag3nt/prompts/*,
|
||||
# `hive_sh4re::assets::*` (#555): branding/* + prompts/*,
|
||||
# plus the rendered agent-configs.png. Split out of the
|
||||
# rust derivation so a tweak to e.g. system.md doesn't bust
|
||||
# the cargo cache.
|
||||
# the cargo cache. Build input of the `cargo-test` check but
|
||||
# NOT of `packages.default`, so the binary derivation stays
|
||||
# cached when a prompt edit ripples through.
|
||||
assets = pkgs.callPackage ./nix/assets.nix { };
|
||||
# Pre-built per-container system closures. Exposed as packages
|
||||
# so operators can `nix build .#agent-base-toplevel` (or wire
|
||||
|
|
@ -266,6 +279,8 @@
|
|||
|
||||
checks = forAllSystems (
|
||||
{
|
||||
pkgs,
|
||||
system,
|
||||
treefmt-eval,
|
||||
craneLib,
|
||||
cleanSrc,
|
||||
|
|
@ -290,6 +305,23 @@
|
|||
version = "0.1.0";
|
||||
cargoClippyExtraArgs = "--workspace --all-targets -- -D warnings";
|
||||
};
|
||||
# `cargo test --workspace` lifted out of `buildPackage` so the
|
||||
# `hyperhive-assets` dep (which `hive-ag3nt::prompt::tests`
|
||||
# needs via `HIVE_ASSETS_DIR` to assert against the actual
|
||||
# production prompt template) is scoped to this one check
|
||||
# instead of bleeding into the binary derivation's input
|
||||
# hash. Net: editing `hive-ag3nt/prompts/system.md` still
|
||||
# rebuilds this test check (correct — the tests assert
|
||||
# against its wording), but `packages.default` and the
|
||||
# per-container toplevels stay fully cached.
|
||||
cargo-test = craneLib.cargoTest {
|
||||
src = cleanSrc;
|
||||
inherit cargoArtifacts nativeBuildInputs;
|
||||
pname = "hyperhive-workspace";
|
||||
version = "0.1.0";
|
||||
cargoTestExtraArgs = "--workspace";
|
||||
HIVE_ASSETS_DIR = "${self.packages.${system}.assets}/share/hyperhive";
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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