Watch
0
0
Fork
You've already forked hyperhive
0

templates: the agent system prompt is a Jinja template, rendered through hive-template

system.md carries {{ label }}-style variables and an {% if not archetype %} block in place of the config:repo / config:archetype marker pairs, so prompt.rs drops its line filter, marker parsers and string replaces. The rendered prompt is byte for byte the old one for a custom agent, an archetype agent and an empty archetype name, with and without hive, swarm, docs and operators. hive-template now drops a block tag's own line.
This commit is contained in:
damocles 2026-11-11 02:26:25 +01:00 • committed by mara
commit c0eb6780ec
3 changed files with 165 additions and 242 deletions

View file

@ -28,6 +28,8 @@ hive-agent-sock.workspace = true
hive-core-agent-sock.workspace = true
hive-log.workspace = true
hive-sh4re.workspace = true
# The system prompt template renders through the workspace template language.
hive-template.workspace = true
hive-sock-client.workspace = true
hive-types.workspace = true
libc.workspace = true

View file

@ -1,7 +1,7 @@
You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity}{swarm_identity} in a multi-agent system.
You are hyperhive agent `{{ label }}` (qualified: `{{ qualified_label }}`){{ hive_identity }}{{ swarm_identity }} in a multi-agent system.
The operator is the human at the dashboard; say "the operator" when you mention the operator in third person (e.g. when relaying to a peer or the manager).
When you're talking to or about a peer on a different hive, use the qualified form (`name@hive`) in text, never as a `send_dm` recipient, so the operator + peers can disambiguate.
Within your own hive the short form is fine.{operators}
Within your own hive the short form is fine.{{ operators }}
Tools: the hyperhive MCP tools and any extra MCP servers describe themselves in their own tool schema, so this prompt does not repeat them.
What the schema doesn't tell you:
@ -14,26 +14,25 @@ What the schema doesn't tell you:
- For a large todo backlog (`get_loose_ends` caps at 40 rows), clear reviewed ids in bulk with `mark_todos_done` rather than cancelling one at a time.
There's no blind range-clear, only ids you've actually looked at.
<!-- config:repo -->
{% if not archetype %}
Need new packages, env vars, or other NixOS config for yourself?
You can't edit your own config directly — open a PR on your `agent-configs/<name>` repo, or contact the operator directly.
All changes flow through operator-approved commits.
Your config repo is `agent-configs/{label}` on the forge: `agent.nix` plus whatever extra files define you (declared packages, env vars, MCP servers).
Clone it with `hive-forge -r agent-configs/{label} clone` and read it to see exactly what defines you before asking for a change, so you can point at the precise file and line.
Your config repo is `agent-configs/{{ label }}` on the forge: `agent.nix` plus whatever extra files define you (declared packages, env vars, MCP servers).
Clone it with `hive-forge -r agent-configs/{{ label }} clone` and read it to see exactly what defines you before asking for a change, so you can point at the precise file and line.
To propose a change, commit in that clone and open the PR with `hive-forge pr create --agit` (see the hive-forge docs), which needs no push access to the repo.
_Changing_ any agent's config is not a tool call at all — it's a forge PR on the agent's `agent-configs/<name>` repo, which an operator reviews and merges on the forge; the merge deploys it.
<!-- /config:repo -->
<!-- config:archetype -->
{% else %}
You are built from the `{archetype}` archetype and have no config repo of your own: there is no `agent-configs/<name>` repo for you.
You are built from the `{{ archetype }}` archetype and have no config repo of your own: there is no `agent-configs/<name>` repo for you.
Need new packages, env vars, or other NixOS config for yourself?
Ask the operator: what defines you is the archetype, shared with every agent built from it.
<!-- /config:archetype -->
{% endif %}
Operator boundary: starting, stopping and rebuilding containers is the operator's — ask them when any agent needs one.
_Creating_ a new agent is not something you can do from here — ask the operator, who creates agents at swarm level.
@ -48,12 +47,12 @@ Call `get_loose_ends` to see them.
Durable knowledge:
- write to `/agents/{label}/state/notes.md` (free-form) or any other path under `/agents/{label}/state/`.
- write to `/agents/{{ label }}/state/notes.md` (free-form) or any other path under `/agents/{{ label }}/state/`.
That directory is bind-mounted from the host and persists across container destroy/recreate.
claude's `--continue` session only carries short-term context, but `/agents/{label}/state/` is forever.
claude's `--continue` session only carries short-term context, but `/agents/{{ label }}/state/` is forever.
Read it back at the start of relevant turns to remember things across resets.
**Work dir**: `/agents/{label}/work/` is the place for git worktrees, clones, build trees and other bulky scratch.
**Work dir**: `/agents/{{ label }}/work/` is the place for git worktrees, clones, build trees and other bulky scratch.
It is bind-mounted from the host, persists across container destroy/recreate and is backed up.
Keep `state/` for notes and small durable files.
Don't keep work under `/home`, `/tmp` or other container paths: the container discards them every time it stops.
@ -69,7 +68,7 @@ A file that belongs to an issue or PR is attached to that forge thread; any othe
The README also covers how to contribute a doc.
**Code forge**: a private Forgejo (base URL in `$HIVE_FORGE_URL`) is available when `$HIVE_FORGE_TOKEN_FILE` is set (`hive-forge` and git read the token from there; no file in `state/` is needed, so don't look for one).
You have your own user account (named `{label}`).
You have your own user account (named `{{ label }}`).
Use `hive-forge` (see below) for all forge operations — issues, PRs, comments, labels, etc.
For git operations use plain `git` directly against `$HIVE_FORGE_URL/<org>/<repo>.git` (credentials are pre-configured).
@ -101,7 +100,7 @@ Don't narrate intent — act.
The harness runs one claude turn per inbox message; when you stop, it acknowledges that message and your `--continue` session is saved to disk.
Ending the turn is how you commit progress — both the session and the inbox acknowledgement.
If the container restarts _while a turn is still running_, the message that drove it was never acknowledged, so it gets redelivered on the next boot, prefixed `[redelivered after harness restart — may already be handled]`.
A long single turn that does step after step widens the window where a restart loses work and forces that redelivery, so prefer short turns: do a unit of work, write anything durable under `/agents/{label}/state/` **at every natural boundary, not just when a turn happens to end**, and stop.
A long single turn that does step after step widens the window where a restart loses work and forces that redelivery, so prefer short turns: do a unit of work, write anything durable under `/agents/{{ label }}/state/` **at every natural boundary, not just when a turn happens to end**, and stop.
For multi-step work (long builds, sequential edits) that spans more than one turn: end the turn and let an external wake drive the next one — a new inbox message, a `remind` you scheduled, or a backgrounded bash task's completion.
Ending the turn is never a same-turn continuation: it's the checkpoint itself, and the only place an in-container todo wake (bash-task completion, matrix unread, forge activity) can reach you.

View file

@ -1,7 +1,7 @@
//! System-prompt renderer. Single `prompts/system.md` with
//! HTML-comment markers gating role-specific and config-repo-specific
//! blocks; this module assembles the final prompt (always "agent" role — there is only one
//! role). Marker grammar + placeholder substitution rules in
//! System-prompt renderer. Single `prompts/system.md`, a Jinja template rendered
//! through `hive-template` with an if/else block for the config-repo paragraphs;
//! this module builds its variables and assembles the final prompt. The template
//! rules and variables are in
//! `docs/4-implementation/turn-loop/claude-invocation.md::On-boot files` (`claude-system-prompt.md`).
use std::path::{Path, PathBuf};
@ -16,36 +16,45 @@ const OPERATORS_TIMEOUT: Duration = Duration::from_secs(5);
/// Assemble the system prompt for a given label + optional hive /
/// swarm display names + the swarm's operators. Pure function — no I/O. Splits out from
/// [`write_system_prompt`] so the marker logic + substitution is unit-testable
/// [`write_system_prompt`] so the template rendering is unit-testable
/// in isolation. The caller supplies the template body so tests can pass an
/// inline fixture and production reads it once at harness startup via
/// [`hive_sh4re::assets::prompt_template`]
/// (`$HIVE_ASSETS_DIR/prompts/system.md`). Substitution placeholders +
/// marker grammar documented in
/// (`$HIVE_ASSETS_DIR/prompts/system.md`). The template rules and
/// variables are documented in
/// `docs/4-implementation/turn-loop/claude-invocation.md::On-boot files` (`claude-system-prompt.md`).
#[must_use]
pub fn render(
template: &str,
label: &str,
archetype: Option<&str>,
hive_name: Option<&str>,
swarm_name: Option<&str>,
docs_dir: Option<&str>,
operators: &[OperatorProfile],
) -> String {
let body = filter_blocks(template, "role", "agent");
let qualified = crate::identity::qualify(label);
let hive_identity = hive_name
.filter(|n| !n.is_empty())
.map_or(String::new(), |n| format!(" on hive `{n}`"));
let swarm_identity = swarm_name
.filter(|n| !n.is_empty())
.map_or(String::new(), |n| format!(" in swarm `{n}`"));
let rendered = body
.replace("{label}", label)
.replace("{qualified_label}", &qualified)
.replace("{hive_identity}", &hive_identity)
.replace("{swarm_identity}", &swarm_identity)
.replace("{operators}", &operators_line(operators));
let context = serde_json::json!({
"label": label,
"qualified_label": crate::identity::qualify(label),
"hive_identity": hive_identity,
"swarm_identity": swarm_identity,
"operators": operators_line(operators),
// Absent for a custom agent, which has a config repo of its own.
"archetype": archetype.filter(|a| !a.is_empty()),
});
let rendered = match hive_template::render(template, &context) {
Ok(text) => text,
Err(why) => {
tracing::error!(%why, "prompt: the system prompt template does not render; using it as written");
template.to_owned()
}
};
// When the reference docs are mounted in-container (`services.hyperhive.agent.docs.enable`
// wires `HIVE_DOCS_DIR` + `claude --add-dir`), append a single pointer
// sentence so the agent knows they exist. Additive: it doesn't replace the
@ -118,67 +127,6 @@ async fn within_timeout<E: std::fmt::Display>(
}
}
/// Walk `template` line-by-line, stripping `<!-- KIND:X -->` /
/// `<!-- /KIND:X -->` marker lines and suppressing the lines inside a block
/// unless `X == target`. A close marker ends the current block. Markers of
/// any other kind are left in place, for the pass that filters that kind.
/// Two kinds exist: `role` (always `agent`; the grammar stays wired for a
/// future manager / multi-role prompt) and `config` ([`config_blocks`]).
fn filter_blocks(template: &str, kind: &str, target: &str) -> String {
let mut out = String::with_capacity(template.len());
// None = outside any block; Some(value) = inside a block of this kind.
let mut active: Option<&str> = None;
for line in template.lines() {
let trimmed = line.trim();
if let Some(value) = parse_open_marker(trimmed, kind) {
active = Some(value);
continue;
}
if parse_close_marker(trimmed, kind).is_some() {
active = None;
continue;
}
if active.is_none_or(|value| value == target) {
out.push_str(line);
out.push('\n');
}
}
out
}
/// `template` with its `config:repo` blocks for a custom agent, or its
/// `config:archetype` blocks and `{archetype}` filled in for an agent built
/// from `archetype`. An empty name is a custom agent.
fn config_blocks(template: &str, archetype: Option<&str>) -> String {
match archetype.filter(|a| !a.is_empty()) {
Some(name) => filter_blocks(template, "config", "archetype").replace("{archetype}", name),
None => filter_blocks(template, "config", "repo"),
}
}
/// `<!-- role:agent -->` with `kind = "role"` → `Some("agent")`. Anything
/// else returns None. Whitespace inside the marker is tolerated so a future
/// author's `<!--role:foo-->` (no spaces) still parses; the dashboard
/// markdown renderer is equally lenient. Close tags (`/role:...`) can't
/// accidentally match — the `strip_prefix(kind)` rejects the leading slash
/// before we'd ever see it.
fn parse_open_marker<'a>(line: &'a str, kind: &str) -> Option<&'a str> {
let inside = line.strip_prefix("<!--")?.strip_suffix("-->")?.trim();
let value = inside.strip_prefix(kind)?.strip_prefix(':')?.trim();
Some(value)
}
/// `<!-- /role:agent -->` with `kind = "role"` → `Some("agent")`. Mirror of
/// [`parse_open_marker`] for the closing tag.
fn parse_close_marker<'a>(line: &'a str, kind: &str) -> Option<&'a str> {
let inside = line.strip_prefix("<!--")?.strip_suffix("-->")?.trim();
inside
.strip_prefix('/')?
.strip_prefix(kind)?
.strip_prefix(':')
.map(str::trim)
}
/// Write the assembled prompt to a stable path next to the harness
/// socket and return the path. The Rust harness passes this path to
/// `claude --system-prompt-file` so the per-turn prompts only carry
@ -212,10 +160,11 @@ pub async fn write_system_prompt(_socket: &Path, label: &str) -> Result<PathBuf>
.filter(|d| !d.is_empty());
// The meta flake sets HIVE_ARCHETYPE on an agent built from an archetype,
// which has no config repo of its own.
let template = config_blocks(&template, std::env::var("HIVE_ARCHETYPE").ok().as_deref());
let archetype = std::env::var("HIVE_ARCHETYPE").ok();
let body = render(
&template,
label,
archetype.as_deref(),
hive_name.as_deref(),
swarm_name.as_deref(),
docs_dir.as_deref(),
@ -261,78 +210,10 @@ mod tests {
.unwrap_or_else(|e| panic!("read production prompt template at {path}: {e}"))
});
const SAMPLE: &str = "\
shared opener
<!-- role:agent -->
agent-only line
<!-- /role:agent -->
<!-- role:manager -->
manager-only line
<!-- /role:manager -->
shared closer
";
#[test]
fn filter_keeps_shared_and_target_role() {
let agent = filter_blocks(SAMPLE, "role", "agent");
assert!(agent.contains("shared opener"));
assert!(agent.contains("agent-only line"));
assert!(!agent.contains("manager-only line"));
assert!(agent.contains("shared closer"));
// Marker lines themselves are stripped — no `<!--` left behind.
assert!(!agent.contains("<!--"));
}
#[test]
fn filter_for_manager_picks_manager_block() {
let manager = filter_blocks(SAMPLE, "role", "manager");
assert!(manager.contains("shared opener"));
assert!(!manager.contains("agent-only line"));
assert!(manager.contains("manager-only line"));
assert!(manager.contains("shared closer"));
assert!(!manager.contains("<!--"));
}
#[test]
fn parse_open_marker_handles_whitespace_variants() {
assert_eq!(
parse_open_marker("<!-- role:agent -->", "role"),
Some("agent")
);
assert_eq!(
parse_open_marker("<!--role:agent-->", "role"),
Some("agent")
);
assert_eq!(
parse_open_marker("<!-- role:manager -->", "role"),
Some("manager")
);
// Close tags must NOT match open-tag parser.
assert_eq!(parse_open_marker("<!-- /role:agent -->", "role"), None);
// Non-markers pass through (return None).
assert_eq!(parse_open_marker("just text", "role"), None);
assert_eq!(parse_open_marker("<!-- not a role -->", "role"), None);
}
#[test]
fn parse_close_marker_handles_whitespace_variants() {
assert_eq!(
parse_close_marker("<!-- /role:agent -->", "role"),
Some("agent")
);
assert_eq!(
parse_close_marker("<!--/role:manager-->", "role"),
Some("manager")
);
// Open tags must NOT match close-tag parser.
assert_eq!(parse_close_marker("<!-- role:agent -->", "role"), None);
assert_eq!(parse_close_marker("just text", "role"), None);
}
#[test]
fn render_substitutes_label_and_names_no_operator_pronouns() {
// Real template's first agent line.
let rendered = render(&PRODUCTION_TEMPLATE, "alice", None, None, None, &[]);
let rendered = render(&PRODUCTION_TEMPLATE, "alice", None, None, None, None, &[]);
assert!(rendered.contains("hyperhive agent `alice`"));
assert!(rendered.contains("say \"the operator\""));
assert!(!rendered.contains("pronoun"));
@ -341,11 +222,11 @@ shared closer
}
#[test]
fn render_no_role_markers_in_output() {
// No raw role markers should survive into the rendered prompt.
let rendered = render(&PRODUCTION_TEMPLATE, "alice", None, None, None, &[]);
assert!(!rendered.contains("<!-- role:"));
assert!(!rendered.contains("<!-- /role:"));
fn render_leaves_no_template_syntax_in_output() {
// No raw template tag should survive into the rendered prompt.
let rendered = render(&PRODUCTION_TEMPLATE, "alice", None, None, None, None, &[]);
assert!(!rendered.contains("{{"));
assert!(!rendered.contains("{%"));
// The guidance the tool schemas cannot give still appears.
assert!(rendered.contains("mcp__matrix__send_dm"));
assert!(rendered.contains("ending the turn"));
@ -353,75 +234,87 @@ shared closer
#[test]
fn render_uses_agent_opener() {
let rendered = render(&PRODUCTION_TEMPLATE, "alice", None, None, None, &[]);
let rendered = render(&PRODUCTION_TEMPLATE, "alice", None, None, None, None, &[]);
assert!(rendered.starts_with("You are hyperhive agent"));
}
#[test]
fn render_points_agents_at_the_work_dir() {
let rendered = render(&PRODUCTION_TEMPLATE, "alice", None, None, None, &[]);
let rendered = render(&PRODUCTION_TEMPLATE, "alice", None, None, None, None, &[]);
assert!(rendered.contains("`/agents/alice/work/`"), "{rendered}");
}
const CONFIG_FIXTURE: &str = "\
shared
<!-- config:repo -->
{% if not archetype %}
open a PR on agent-configs
<!-- /config:repo -->
<!-- config:archetype -->
built from `{archetype}`, no config repo
<!-- /config:archetype -->
{% else %}
built from `{{ archetype }}`, no config repo
{% endif %}
";
/// A custom agent gets the config-repo lines, an archetype agent the
/// archetype lines with its name, and an empty name is a custom agent.
#[test]
fn config_blocks_pick_the_block_by_archetype() {
fn the_config_block_follows_the_archetype() {
for custom in [None, Some("")] {
let picked = config_blocks(CONFIG_FIXTURE, custom);
assert!(picked.contains("open a PR on agent-configs"), "{picked}");
assert!(!picked.contains("no config repo"), "{picked}");
let picked = render(CONFIG_FIXTURE, "alice", custom, None, None, None, &[]);
assert_eq!(picked, "shared\nopen a PR on agent-configs\n");
}
let picked = config_blocks(CONFIG_FIXTURE, Some("android-dev"));
assert!(picked.contains("built from `android-dev`, no config repo"));
assert!(!picked.contains("agent-configs"), "{picked}");
assert!(picked.contains("shared"));
assert!(!picked.contains("<!--"), "{picked}");
let picked = render(
CONFIG_FIXTURE,
"alice",
Some("android-dev"),
None,
None,
None,
&[],
);
assert_eq!(picked, "shared\nbuilt from `android-dev`, no config repo\n");
}
#[test]
fn the_production_prompt_keeps_no_config_markers() {
fn the_production_prompt_keeps_no_template_syntax() {
for archetype in [None, Some("reviewer")] {
let rendered = render(
&config_blocks(&PRODUCTION_TEMPLATE, archetype),
&PRODUCTION_TEMPLATE,
"alice",
archetype,
None,
None,
None,
&[],
);
assert!(!rendered.contains("<!-- config:"), "{rendered}");
assert!(!rendered.contains("<!-- /config:"), "{rendered}");
assert!(!rendered.contains("{archetype}"), "{rendered}");
for left in ["{{", "{%", "<!-- config:", "<!-- /config:", "{archetype}"] {
assert!(!rendered.contains(left), "{left} left in:\n{rendered}");
}
}
}
// Inline fixture for the {hive_identity} / {swarm_identity}
// placeholders. Cargo's `cargo test` resolves `PRODUCTION_TEMPLATE`
// Inline fixture for the {{ hive_identity }} / {{ swarm_identity }}
// variables. Cargo's `cargo test` resolves `PRODUCTION_TEMPLATE`
// against `$HIVE_ASSETS_DIR/prompts/system.md`, which the flake
// builds at derivation time — a fresh placeholder added on the
// source side isn't in the shipped asset until the flake rebuilds,
// so PRODUCTION_TEMPLATE can't be the fixture here. The string
// below carries just enough of the opener shape to exercise the
// substitution logic; nothing here depends on the production
// template's flavor markers.
// template's config block.
const IDENTITY_FIXTURE: &str = "\
You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity}{swarm_identity} in a multi-agent system.
You are hyperhive agent `{{ label }}` (qualified: `{{ qualified_label }}`){{ hive_identity }}{{ swarm_identity }} in a multi-agent system.
";
#[test]
fn render_substitutes_hive_identity_when_set() {
let rendered = render(IDENTITY_FIXTURE, "alice", Some("pr1ma"), None, None, &[]);
let rendered = render(
IDENTITY_FIXTURE,
"alice",
None,
Some("pr1ma"),
None,
None,
&[],
);
assert!(rendered.contains("on hive `pr1ma`"), "{rendered}");
// swarm clause stays absent when only hive is set.
assert!(!rendered.contains("in swarm"));
@ -436,6 +329,7 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
IDENTITY_FIXTURE,
"ruth",
None,
None,
Some("constellat1on"),
None,
&[],
@ -449,6 +343,7 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
let rendered = render(
IDENTITY_FIXTURE,
"iris",
None,
Some("pr1ma"),
Some("constellat1on"),
None,
@ -463,7 +358,7 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
fn render_omits_identity_when_unset() {
// None / None must round-trip the non-identity opener verbatim
// — single-hive deployments see zero diff.
let rendered = render(IDENTITY_FIXTURE, "alice", None, None, None, &[]);
let rendered = render(IDENTITY_FIXTURE, "alice", None, None, None, None, &[]);
assert!(!rendered.contains("on hive"));
assert!(!rendered.contains("in swarm"));
assert!(!rendered.contains("{hive_identity}"));
@ -476,7 +371,15 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
// through `identity::hive_name()` as None (the accessor
// filters empty), but `render` should still no-op on a
// direct `Some("")` from a test fixture or a future caller.
let rendered = render(IDENTITY_FIXTURE, "alice", Some(""), Some(""), None, &[]);
let rendered = render(
IDENTITY_FIXTURE,
"alice",
None,
Some(""),
Some(""),
None,
&[],
);
assert!(!rendered.contains("on hive"));
assert!(!rendered.contains("in swarm"));
}
@ -488,6 +391,7 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
"alice",
None,
None,
None,
Some("/run/hive-docs"),
&[],
);
@ -502,7 +406,7 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
#[test]
fn render_no_docs_pointer_when_docs_dir_absent_or_empty() {
for docs in [None, Some("")] {
let rendered = render(&PRODUCTION_TEMPLATE, "alice", None, None, docs, &[]);
let rendered = render(&PRODUCTION_TEMPLATE, "alice", None, None, None, docs, &[]);
assert!(
!rendered.contains("mounted read-only at"),
"docs pointer must not appear for {docs:?}:\n{rendered}"
@ -517,17 +421,33 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
}
}
const OPERATORS_FIXTURE: &str = "say \"the operator\".{operators}\n\nnext paragraph\n";
const OPERATORS_FIXTURE: &str = "say \"the operator\".{{ operators }}\n\nnext paragraph\n";
#[test]
fn render_lists_operators_with_their_pronouns() {
let operators = [operator("mara", Some("she/her")), operator("sol", None)];
let rendered = render(OPERATORS_FIXTURE, "alice", None, None, None, &operators);
let rendered = render(
OPERATORS_FIXTURE,
"alice",
None,
None,
None,
None,
&operators,
);
assert_eq!(
rendered,
"say \"the operator\".\n\nOperators: mara (she/her), sol.\n\nnext paragraph\n"
);
let rendered = render(&PRODUCTION_TEMPLATE, "alice", None, None, None, &operators);
let rendered = render(
&PRODUCTION_TEMPLATE,
"alice",
None,
None,
None,
None,
&operators,
);
assert!(
rendered.contains("Operators: mara (she/her), sol."),
"{rendered}"
@ -538,17 +458,18 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
/// byte for byte.
#[test]
fn render_with_no_operators_is_the_template_without_the_placeholder() {
let rendered = render(OPERATORS_FIXTURE, "alice", None, None, None, &[]);
let rendered = render(OPERATORS_FIXTURE, "alice", None, None, None, None, &[]);
assert_eq!(rendered, "say \"the operator\".\n\nnext paragraph\n");
assert!(PRODUCTION_TEMPLATE.contains("{operators}"));
assert!(PRODUCTION_TEMPLATE.contains("{{ operators }}"));
assert_eq!(
render(&PRODUCTION_TEMPLATE, "alice", None, None, None, &[]),
render(&PRODUCTION_TEMPLATE, "alice", None, None, None, None, &[]),
render(
&PRODUCTION_TEMPLATE.replace("{operators}", ""),
&PRODUCTION_TEMPLATE.replace("{{ operators }}", ""),
"alice",
None,
None,
None,
None,
&[]
)
);