use assert_cmd::Command;
use predicates::str::contains;
use std::fs;
#[test]
fn bp_build_writes_parseable_json_with_top_level_keys() {
let tmp = tempfile::tempdir().expect("tempdir");
let out_path = tmp.path().join("out.json");
Command::cargo_bin("mse")
.expect("mse binary")
.args(["bp", "build", "tests/fixtures/pipeline.bp.lua", "-o"])
.arg(&out_path)
.assert()
.success();
let out = fs::read_to_string(&out_path).expect("out.json written");
let value: serde_json::Value = serde_json::from_str(&out).expect("out.json is valid JSON");
for key in ["id", "flow", "agents", "operators", "strategy", "metadata"] {
assert!(value.get(key).is_some(), "missing top-level key: {key}");
}
}
#[test]
fn bp_build_reports_undeclared_verdict_literal_as_a_compile_error() {
let tmp = tempfile::tempdir().expect("tempdir");
let script_path = tmp.path().join("bad_verdict.bp.lua");
fs::write(
&script_path,
r#"
local F = require("flow_dsl")
return {
id = "bad-verdict-lint",
flow = F.seq({
F.step({ agent = "checker", input = F.lit("go"), out = F.p("$.checker") }),
F.branch({
cond = F.p('$.checker.parts["verdict"]'):eq("NOT_A_DECLARED_VALUE"),
on_true = F.assign({ at = F.p("$.halted_at"), value = F.lit("checker") }),
on_false = F.assign({ at = F.p("$.done"), value = F.lit(true) }),
}),
}),
agents = {
{
name = "checker",
kind = "operator",
spec = { operator_ref = "main-ai" },
-- worker_binding is set here so this fixture exercises the
-- GH #50 verdict-contract lint specifically — without it, the
-- GH #61 worker_binding lint would short-circuit first.
profile = { system_prompt = "check", tools = {}, worker_binding = "claude" },
verdict = { channel = "part", values = { "PASS", "BLOCKED" } },
},
},
operators = {
{ name = "main-ai" },
},
}
"#,
)
.expect("write bad-verdict fixture script");
Command::cargo_bin("mse")
.expect("mse binary")
.args(["bp", "build"])
.arg(&script_path)
.assert()
.failure()
.stderr(contains("compile lint FAILED"))
.stderr(contains("is not a member of the declared values"))
.stderr(contains("fix hint (verdict-value-not-in-contract)"))
.stderr(contains("`agents[N].verdict.values`"));
}
#[test]
fn bp_build_reports_missing_worker_binding_on_operator_agent_as_a_compile_error() {
let tmp = tempfile::tempdir().expect("tempdir");
let script_path = tmp.path().join("missing_worker_binding.bp.lua");
fs::write(
&script_path,
r#"
local B = require("bp_dsl")
local flow = B.pipeline({
B.stage "greet" { agent = "greeter" },
halted_at = "$.halted_at",
done = "$.pipeline_complete",
})
return {
id = "gh61-missing-worker-binding",
flow = flow,
agents = {
{ name = "greeter", kind = "operator",
spec = { operator_ref = "main-ai" },
profile = { system_prompt = "Say hello.", tools = {} } }, -- worker_binding intentionally missing
},
operators = { { name = "main-ai" } },
strategy = { strict_refs = true, strict_kind = true },
}
"#,
)
.expect("write missing-worker-binding fixture script");
Command::cargo_bin("mse")
.expect("mse binary")
.args(["bp", "build"])
.arg(&script_path)
.assert()
.failure()
.stderr(contains("compile lint FAILED"))
.stderr(contains("profile.worker_binding is required"))
.stderr(contains("agents[N].profile.worker_binding"))
.stderr(contains("$agent_md file ref"))
.stderr(contains("fix hint (worker-binding-missing)"))
.stderr(contains("operator agent 'greeter'"))
.stderr(contains(
"runner = { backend = \"ws_operator\", variant = \"claude\", tools = {} }",
));
}
#[test]
fn bp_build_dsl_pipeline_sample_still_lints_ok_after_worker_binding_tightening() {
let tmp = tempfile::tempdir().expect("tempdir");
let out_path = tmp.path().join("out.json");
Command::cargo_bin("mse")
.expect("mse binary")
.args([
"bp",
"build",
"src/mcp/resources/samples/07-dsl-pipeline.bp.lua",
"-o",
])
.arg(&out_path)
.assert()
.success()
.stderr(contains("compile lint: OK"));
}
#[test]
fn bp_new_pipeline_scaffold_round_trips_through_bp_build() {
let tmp = tempfile::tempdir().expect("tempdir");
let scaffold_path = tmp.path().join("hello.bp.lua");
Command::cargo_bin("mse")
.expect("mse binary")
.args([
"bp",
"new",
"pipeline",
"hello",
"--stages",
"greet,echo",
"-o",
])
.arg(&scaffold_path)
.assert()
.success()
.stderr(contains("mse bp new: wrote"));
Command::cargo_bin("mse")
.expect("mse binary")
.args(["bp", "build"])
.arg(&scaffold_path)
.assert()
.success()
.stderr(contains("compile lint: OK"));
}
#[test]
fn bp_new_single_scaffold_round_trips_through_bp_build() {
let tmp = tempfile::tempdir().expect("tempdir");
let scaffold_path = tmp.path().join("solo.bp.lua");
Command::cargo_bin("mse")
.expect("mse binary")
.args(["bp", "new", "single", "solo-run", "--agent", "solo", "-o"])
.arg(&scaffold_path)
.assert()
.success();
Command::cargo_bin("mse")
.expect("mse binary")
.args(["bp", "build"])
.arg(&scaffold_path)
.assert()
.success()
.stderr(contains("compile lint: OK"));
}
#[test]
fn bp_new_verdict_scaffold_round_trips_through_bp_build() {
let tmp = tempfile::tempdir().expect("tempdir");
let scaffold_path = tmp.path().join("review-loop.bp.lua");
Command::cargo_bin("mse")
.expect("mse binary")
.args(["bp", "new", "verdict", "review-loop", "-o"])
.arg(&scaffold_path)
.assert()
.success();
Command::cargo_bin("mse")
.expect("mse binary")
.args(["bp", "build"])
.arg(&scaffold_path)
.assert()
.success()
.stderr(contains("compile lint: OK"));
}
#[test]
fn bp_build_unresolved_ref_default_emits_raw_and_warns() {
let tmp = tempfile::tempdir().expect("tempdir");
let script_path = tmp.path().join("unresolved.bp.lua");
let out_path = tmp.path().join("out.json");
fs::write(
&script_path,
r#"
local F = require("flow_dsl")
return {
id = "unresolved-ref-warn",
flow = F.step({ id = "solo", agent = "solo", input = F.lit(""), out = F.p("$.solo") }),
agents = {
{
name = "solo",
kind = "operator",
spec = { operator_ref = "main-ai" },
["$agent_md"] = "definitely-not-present.md",
},
},
operators = { { name = "main-ai", kind = "main_ai" } },
strategy = { strict_refs = true, strict_kind = true },
}
"#,
)
.expect("write unresolved-ref fixture script");
Command::cargo_bin("mse")
.expect("mse binary")
.args(["bp", "build"])
.arg(&script_path)
.args(["-o"])
.arg(&out_path)
.assert()
.success()
.stderr(contains("compile lint: WARN"))
.stderr(contains("could not resolve $file/$agent_md refs"));
let out = fs::read_to_string(&out_path).expect("out.json written");
let value: serde_json::Value = serde_json::from_str(&out).expect("out.json is valid JSON");
let agent = value
.get("agents")
.and_then(|a| a.get(0))
.expect("agents[0] present");
assert_eq!(
agent.get("$agent_md").and_then(|v| v.as_str()),
Some("definitely-not-present.md"),
"raw emit must preserve the unresolved $agent_md ref"
);
}
#[test]
fn bp_build_unresolved_ref_strict_embed_hard_fails() {
let tmp = tempfile::tempdir().expect("tempdir");
let script_path = tmp.path().join("unresolved-strict.bp.lua");
let out_path = tmp.path().join("out.json");
fs::write(
&script_path,
r#"
local F = require("flow_dsl")
return {
id = "unresolved-ref-strict",
flow = F.step({ id = "solo", agent = "solo", input = F.lit(""), out = F.p("$.solo") }),
agents = {
{
name = "solo",
kind = "operator",
spec = { operator_ref = "main-ai" },
["$agent_md"] = "definitely-not-present.md",
},
},
operators = { { name = "main-ai", kind = "main_ai" } },
strategy = { strict_refs = true, strict_kind = true },
}
"#,
)
.expect("write unresolved-strict fixture script");
Command::cargo_bin("mse")
.expect("mse binary")
.args(["bp", "build", "--strict-embed"])
.arg(&script_path)
.args(["-o"])
.arg(&out_path)
.assert()
.failure()
.stderr(contains("compile lint: WARN"))
.stderr(contains("--strict-embed"))
.stderr(contains("refusing to emit"));
assert!(
!out_path.exists(),
"--strict-embed must not write out.json when the linker fails"
);
}
#[test]
fn bp_build_resolves_bare_agent_md_ref_via_bundled_default_tier() {
let tmp = tempfile::tempdir().expect("tempdir");
let script_path = tmp.path().join("bundled-tier.bp.lua");
let out_path = tmp.path().join("out.json");
fs::write(
&script_path,
r#"
local F = require("flow_dsl")
return {
id = "bundled-tier6-round-trip",
flow = F.step({ id = "solo", agent = "researcher", input = F.lit(""), out = F.p("$.solo") }),
agents = {
{
-- Bare filename: no `agents/` prefix, so tier 1 (this script's
-- parent = a fresh tempdir) misses. There are no in-bp includes,
-- no `MSE_BLUEPRINT_INCLUDES` set for this test, and no `--include`
-- flag on the command line — so the ref must resolve through the
-- tier-6 bundled default (`src/mcp/resources/samples/agents/`).
["$agent_md"] = "researcher.md",
spec = { operator_ref = "main-ai" },
},
},
operators = { { name = "main-ai" } },
strategy = { strict_refs = true, strict_kind = true },
}
"#,
)
.expect("write bundled-tier fixture script");
Command::cargo_bin("mse")
.expect("mse binary")
.env_remove("MSE_BLUEPRINT_INCLUDES")
.args(["bp", "build"])
.arg(&script_path)
.args(["-o"])
.arg(&out_path)
.assert()
.success()
.stderr(contains("compile lint: OK"));
let out = fs::read_to_string(&out_path).expect("out.json written");
let value: serde_json::Value = serde_json::from_str(&out).expect("out.json is valid JSON");
let agent = value
.get("agents")
.and_then(|a| a.get(0))
.expect("agents[0] present");
assert_eq!(
agent.get("$agent_md").and_then(|v| v.as_str()),
Some("researcher.md"),
"raw emit preserves the ref that tier-6 resolved during lint"
);
}
#[test]
fn bp_build_accepts_an_agent_block_agent_under_strict_kind() {
let tmp = tempfile::tempdir().expect("tempdir");
let script_path = tmp.path().join("agent_block_gate.bp.lua");
fs::write(
&script_path,
r#"
local F = require("flow_dsl")
return {
id = "gh86-agent-block-gate",
flow = F.step({
agent = "gate-danger",
input = F.p("$.d.gate_danger"),
out = F.p("$.danger_result"),
}),
agents = {
{
name = "gate-danger",
kind = "agent_block",
-- ScriptBasedAgent mode; `tools = {}` is the enforced-empty grant
-- this mode requires (the script drives its own `mcp.connect`).
spec = { script_path = "gate.lua" },
runner = { backend = "agent_block_in_process", tools = {} },
},
},
operators = {},
strategy = { strict_refs = true, strict_kind = true },
}
"#,
)
.expect("write agent_block fixture script");
Command::cargo_bin("mse")
.expect("mse binary")
.args(["bp", "build"])
.arg(&script_path)
.assert()
.success()
.stderr(contains("compile lint: OK"));
}
#[test]
fn bp_build_rejects_agent_block_script_mode_with_a_declared_mcp_grant() {
let tmp = tempfile::tempdir().expect("tempdir");
let script_path = tmp.path().join("agent_block_bad_grant.bp.lua");
fs::write(
&script_path,
r#"
local F = require("flow_dsl")
return {
id = "gh86-agent-block-bad-grant",
flow = F.step({
agent = "gate-danger",
input = F.p("$.d.gate_danger"),
out = F.p("$.danger_result"),
}),
agents = {
{
name = "gate-danger",
kind = "agent_block",
spec = { script_path = "gate.lua" },
runner = { backend = "agent_block_in_process", tools = { "mcp__outline__list_docs" } },
},
},
operators = {},
strategy = { strict_refs = true, strict_kind = true },
}
"#,
)
.expect("write agent_block bad-grant fixture script");
Command::cargo_bin("mse")
.expect("mse binary")
.args(["bp", "build"])
.arg(&script_path)
.assert()
.failure()
.stderr(contains("compile lint FAILED"))
.stderr(contains("mcp__outline__list_docs"))
.stderr(contains("PromptBasedAgent mode"));
}
#[test]
fn bp_new_unknown_template_exits_error_with_accepted_list() {
Command::cargo_bin("mse")
.expect("mse binary")
.args(["bp", "new", "bogus", "foo"])
.assert()
.failure()
.stderr(contains("unknown template 'bogus'"))
.stderr(contains("pipeline"))
.stderr(contains("single"))
.stderr(contains("verdict"));
}