use std::path::{Path, PathBuf};
use serde_json::{json, Value};
use crate::harness::{agent, double, plan_of, repo_file, World, REFUSED};
fn proposed() -> Value {
let record = std::fs::read_to_string(repo_file("docs/contract-divergences.md"))
.expect("the divergence record ships");
let entry = record
.split("\n## ")
.find(|entry| entry.starts_with("41."))
.expect("the record still carries entry 41");
let block = entry
.split("```json")
.nth(1)
.and_then(|rest| rest.split("```").next())
.expect("entry 41 carries the json block these journeys drive");
serde_json::from_str::<Value>(block).expect("entry 41's block is JSON")["validator"].clone()
}
fn spelling(named: &str) -> String {
proposed()[named]
.as_str()
.unwrap_or_else(|| panic!("entry 41 no longer names the validator's {named}"))
.to_string()
}
fn validator_named(world: &World, name: &str) -> String {
let path = world
.root
.join(format!("{name}{}", std::env::consts::EXE_SUFFIX));
std::fs::copy(double("node-validator"), &path).expect("the validator is placed");
path.to_string_lossy().into_owned()
}
fn offered(world: &World) -> Vec<(String, Value)> {
let path = world.fakes.join("validator.jsonl");
let recorded = match std::fs::read_to_string(&path) {
Ok(text) => text,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => String::new(),
Err(error) => panic!(
"the validator's record at {} cannot be read ({error}), so what it was offered is \
unknown rather than nothing",
path.display()
),
};
recorded
.lines()
.filter(|line| !line.trim().is_empty())
.map(|line| {
let record: Value = serde_json::from_str(line).expect("the validator records JSON");
(
record["as"].as_str().expect("it names itself").to_string(),
record["node"].clone(),
)
})
.collect()
}
const RULES: &str = "this node's criteria name a procedure rather than a property";
fn envelope(commands: Value) -> String {
json!({"version": 2, "commands": commands}).to_string()
}
fn live_run(world: &World, name: &str, extra: &[&str]) -> String {
world.script("slow.wait", "hold");
let path = world.plan(name, &plan_of(name, vec![agent("slow", &[])]));
let mut args = vec!["start".to_string(), path.clone()];
args.extend(extra.iter().map(|arg| (*arg).to_string()));
args.push("--detach".to_string());
let borrowed: Vec<&str> = args.iter().map(String::as_str).collect();
world.run(&borrowed).exited(0);
world.until("the held node to be running", |world| {
world
.run(&["status", name])
.stdout
.contains("slow: running")
});
name.to_string()
}
fn settled(world: &World, run: &str, node: &str, status: &str) -> bool {
world
.run(&["results", run])
.stdout
.lines()
.any(|line| line.trim_start().starts_with(node) && line.contains(status))
}
#[test]
fn a_node_the_validator_refuses_is_refused_with_its_own_words_and_never_joins_the_graph() {
let world = World::new("validator-refuses");
let validator = validator_named(&world, "check-node");
let run = live_run(
&world,
"validatorrefuses",
&["--node-validator", &validator],
);
let refusal = "acceptance criterion 2 names a procedure — `run just gate` — rather than a \
property of the finished tree";
world.script("validator.refuse", refusal);
world
.run_with_stdin(
&["reply", &run],
&envelope(json!([{"op": "add", "node": agent("fresh", &[])}])),
)
.exited(REFUSED)
.err_has(refusal);
let seen = offered(&world);
assert_eq!(seen.len(), 1, "{seen:?}");
assert_eq!(seen[0].1["id"], "fresh");
assert!(
seen[0].1["task"]
.as_str()
.expect("the task crossed")
.contains("Acceptance criteria"),
"{seen:?}"
);
world.run(&["results", &run]).exited(0).out_lacks("fresh");
world.run(&["status", &run]).exited(0).out_lacks("fresh");
std::fs::remove_file(world.fakes.join("validator.refuse")).expect("the rule is lifted");
let narration = "checked 14 rules against the resolved review bar";
world.script("validator.chatter", narration);
let applied = world.run_with_stdin(
&["reply", &run],
&envelope(json!([{"op": "add", "node": agent("fresh", &[])}])),
);
applied
.exited(0)
.out_has("\"applied\"")
.out_lacks(narration);
let verdict: Value = serde_json::from_str(applied.stdout.trim())
.unwrap_or_else(|e| panic!("`reply` printed something other than its verdict: {e}"));
assert_eq!(verdict["state"], json!("applied"), "{verdict}");
world.until("the accepted node to settle", |world| {
settled(world, &run, "fresh", "done")
});
world.release("slow.go");
}
#[test]
fn every_op_that_introduces_or_changes_a_task_is_offered_and_nothing_else_is() {
let world = World::new("validator-offered");
let validator = validator_named(&world, "check-node");
world.script("build.fail", "");
world.script("slow.wait", "hold");
let path = world.plan(
"validatoroffered",
&plan_of(
"validatoroffered",
vec![
agent("slow", &[]),
agent("build", &[]),
agent("spare", &["slow"]),
],
),
);
world
.run(&["start", &path, "--node-validator", &validator, "--detach"])
.exited(0);
let run = "validatoroffered".to_string();
world.until("the node that fails to settle", |world| {
settled(world, &run, "build", "failed")
});
for command in [
json!({"op": "add", "node": agent("fresh", &[])}),
json!({"op": "retry", "id": "build", "node": agent("build-2", &[])}),
json!({"op": "amend", "id": "spare", "text": "the ruling"}),
json!({"op": "cancel", "id": "spare"}),
json!({"op": "requeue", "id": "spare", "amend": {"task": "## What\nsomething else"}}),
json!({"op": "cancel", "id": "spare"}),
json!({"op": "requeue", "id": "spare", "amend": {"max_turns": 9}}),
json!({"op": "note", "id": "spare", "addressee": "worker",
"text": "the fixture moved", "deliver": "next"}),
] {
world
.run_with_stdin(&["reply", &run], &envelope(json!([command])))
.exited(0);
}
let seen: Vec<String> = offered(&world)
.into_iter()
.map(|(_, node)| node["id"].as_str().expect("a node id").to_string())
.collect();
assert!(
seen.chunks(2)
.all(|pair| pair.len() == 2 && pair[0] == pair[1]),
"an edit was not offered to the validator at both the submission check \
and the reconcile: {seen:?}"
);
let each: Vec<&String> = seen.iter().step_by(2).collect();
assert_eq!(
each,
vec!["fresh", "build-2", "spare", "spare"],
"the validator was offered the wrong edits"
);
world.release("slow.go");
}
#[test]
fn the_flag_beats_the_environment_which_beats_the_config_and_naming_none_runs_nothing() {
let precedence: Vec<String> = serde_json::from_value(proposed()["precedence"].clone())
.expect("entry 41 states the precedence it proposes");
assert_eq!(
precedence,
vec!["flag", "environment", "config_key"],
"entry 41 proposes a different order than this journey drives"
);
let world = World::new("validator-precedence");
let by_flag = validator_named(&world, "by-flag");
let by_environment = validator_named(&world, "by-environment");
let by_config = validator_named(&world, "by-config");
let config = world.root.join("launch.yaml");
std::fs::write(
&config,
format!(
"schema_version: {}\n{}: {by_config}\n",
proposed()["config_schema_version"]
.as_u64()
.expect("entry 41 states the version the key arrived at"),
spelling("config_key"),
),
)
.expect("the launch config is written");
world.script("validator.refuse", RULES);
for (which, extra, environment) in [
("by-config", vec![], None),
("by-environment", vec![], Some(by_environment.clone())),
(
"by-flag",
vec![spelling("flag"), by_flag.clone()],
Some(by_environment.clone()),
),
] {
let name = format!("precedence-{which}");
let path = world.plan(&name, &plan_of(&name, vec![agent("slow", &[])]));
let _ = std::fs::remove_file(world.fakes.join("slow.go"));
world.script("slow.wait", "hold");
let mut args = vec![
"start".to_string(),
path.clone(),
"--launch-config".to_string(),
config.to_string_lossy().into_owned(),
];
args.extend(extra);
args.push("--detach".to_string());
let borrowed: Vec<&str> = args.iter().map(String::as_str).collect();
let mut command = world.cmd(&borrowed);
match &environment {
Some(value) => command.env(spelling("environment"), value),
None => command.env_remove(spelling("environment")),
};
world.run_on(command, "start").exited(0);
world.until("the held node to be running", |world| {
world
.run(&["status", &name])
.stdout
.contains("slow: running")
});
let mut reply = world.cmd(&["reply", &name]);
reply.env_remove(spelling("environment"));
let refused = world.run_with_stdin_on(
reply,
&envelope(json!([{"op": "add", "node": agent("fresh", &[])}])),
);
refused
.exited(REFUSED)
.err_has(&format!("{which}: {RULES}"));
for other in ["by-flag", "by-environment", "by-config"] {
if other != which {
refused.err_lacks(other);
}
}
world.release("slow.go");
}
for (at, (which, names_a_config, extra, environment)) in [
("no rung at all", false, vec![], None),
(
"a blank flag",
true,
vec![spelling("flag"), " ".to_string()],
None,
),
("a blank variable", true, vec![], Some(String::new())),
]
.into_iter()
.enumerate()
{
let before = offered(&world).len();
let name = format!("precedence-none-{at}");
let path = world.plan(&name, &plan_of(&name, vec![agent("slow", &[])]));
let _ = std::fs::remove_file(world.fakes.join("slow.go"));
world.script("slow.wait", "hold");
let mut args = vec!["start".to_string(), path.clone()];
if names_a_config {
args.push("--launch-config".to_string());
args.push(config.to_string_lossy().into_owned());
}
args.extend(extra);
args.push("--detach".to_string());
let borrowed: Vec<&str> = args.iter().map(String::as_str).collect();
let mut command = world.cmd(&borrowed);
match &environment {
Some(value) => command.env(spelling("environment"), value),
None => command.env_remove(spelling("environment")),
};
world.run_on(command, "start").exited(0);
world.until("the held node to be running", |world| {
world
.run(&["status", &name])
.stdout
.contains("slow: running")
});
let mut reply = world.cmd(&["reply", &name]);
match &environment {
Some(value) => reply.env(spelling("environment"), value),
None => reply.env_remove(spelling("environment")),
};
world
.run_with_stdin_on(
reply,
&envelope(json!([{"op": "add", "node": agent("fresh", &[])}])),
)
.exited(0)
.out_has("\"applied\"")
.err_lacks(RULES);
assert_eq!(
offered(&world).len(),
before,
"a launch naming {which} ran a validator"
);
world.release("slow.go");
}
}
#[test]
fn the_resolved_validator_is_in_the_launch_record_and_survives_an_adoption() {
let world = World::new("validator-adopt");
let chosen = validator_named(&world, "by-flag");
let elsewhere = validator_named(&world, "somewhere-else");
let name = "validatoradopt";
let path = world.plan(name, &plan_of(name, vec![agent("only", &[])]));
let mut launch = world.cmd(&["start", &path, &spelling("flag"), &chosen, "--attach"]);
launch.env(spelling("environment"), &elsewhere);
world.run_on(launch, "start").exited(0).settled();
let mut adopt = world.cmd(&["adopt", name]);
adopt.env(spelling("environment"), &elsewhere);
world.run_on(adopt, "adopt").exited(0);
world.script("validator.refuse", RULES);
let mut reply = world.cmd(&["reply", name]);
reply.env(spelling("environment"), &elsewhere);
let refused = world.run_with_stdin_on(
reply,
&envelope(json!([{"op": "add", "node": agent("fresh", &[])}])),
);
refused
.exited(REFUSED)
.err_has(&format!("by-flag: {RULES}"))
.err_lacks("somewhere-else");
}
#[test]
fn a_refusal_carries_what_the_validator_said_without_its_escape_codes_or_its_trace() {
let world = World::new("validator-loud");
let validator = validator_named(&world, "check-node");
let run = live_run(&world, "validatorloud", &["--node-validator", &validator]);
let sentence = "rule 3 failed: criterion 2 names a procedure";
let esc = '\u{1b}';
world.script(
"validator.refuse",
&format!("{esc}[31m{sentence}{esc}[0m\nsee the trace below"),
);
let flood = 100_000;
world.script("validator.flood", &flood.to_string());
let refused = world.run_with_stdin(
&["reply", &run],
&envelope(json!([{"op": "add", "node": agent("fresh", &[])}])),
);
refused.exited(REFUSED).err_has(sentence);
assert!(
!refused.stderr.contains('\u{1b}'),
"a validator's escape sequences reached the refusal: {:?}",
refused.stderr
);
assert!(
refused.stderr.lines().count() == 1,
"the refusal is not one line: {:?}",
refused.stderr
);
assert!(
refused.stderr.len() < flood / 4,
"the refusal grew with the validator's trace: {} bytes",
refused.stderr.len()
);
world.run(&["results", &run]).exited(0).out_lacks("fresh");
world.release("slow.go");
}
#[test]
fn a_validator_that_says_nothing_and_one_that_cannot_be_started_both_refuse_loudly() {
let world = World::new("validator-silent");
let validator = validator_named(&world, "check-node");
let run = live_run(&world, "validatorsilent", &["--node-validator", &validator]);
world.script("validator.silent", "");
world
.run_with_stdin(
&["reply", &run],
&envelope(json!([{"op": "add", "node": agent("fresh", &[])}])),
)
.exited(REFUSED)
.err_has("exited 3");
world.run(&["results", &run]).exited(0).out_lacks("fresh");
std::fs::remove_file(world.fakes.join("validator.silent")).expect("the scenario is lifted");
#[cfg(unix)]
{
world.script("validator.signal", "");
world
.run_with_stdin(
&["reply", &run],
&envelope(json!([{"op": "add", "node": agent("fresh", &[])}])),
)
.exited(REFUSED)
.err_has("without a status");
world.run(&["results", &run]).exited(0).out_lacks("fresh");
std::fs::remove_file(world.fakes.join("validator.signal")).expect("the scenario is lifted");
}
let missing: PathBuf = world.root.join("no-such-validator");
assert!(!Path::new(&missing).exists());
let path = world.plan(
"validatormissing",
&plan_of("validatormissing", vec![agent("slow", &[])]),
);
world
.run(&[
"start",
&path,
"--node-validator",
&missing.to_string_lossy(),
"--detach",
])
.exited(0);
world.until("the held node to be running", |world| {
world
.run(&["status", "validatormissing"])
.stdout
.contains("slow: running")
});
world
.run_with_stdin(
&["reply", "validatormissing"],
&envelope(json!([{"op": "add", "node": agent("fresh", &[])}])),
)
.exited(REFUSED)
.err_has("could not be started")
.err_has("checked by nothing");
world
.run(&["results", "validatormissing"])
.exited(0)
.out_lacks("fresh");
world.release("slow.go");
}
#[test]
#[cfg(unix)]
fn a_validator_variable_this_build_cannot_read_refuses_the_launch_by_its_name() {
use std::ffi::OsString;
use std::os::unix::ffi::OsStringExt;
let world = World::new("validator-not-text");
let chosen = validator_named(&world, "check-node");
let name = "validatornottext";
let path = world.plan(name, &plan_of(name, vec![agent("only", &[])]));
let variable = spelling("environment");
let not_text = OsString::from_vec(vec![0x63, 0x68, 0xff, 0x6b]);
let mut refused = world.cmd(&["start", &path, "--detach"]);
refused.env(&variable, ¬_text);
world
.run_on(refused, "start")
.exited(REFUSED)
.err_has(&variable)
.err_has("cannot read as text");
let mut named = world.cmd(&["start", &path, &spelling("flag"), &chosen, "--attach"]);
named.env(&variable, ¬_text);
world.run_on(named, "start").exited(0).settled();
}
#[test]
fn a_config_naming_the_key_at_a_version_that_never_had_it_is_refused_by_that_name() {
let world = World::new("validator-config-version");
let key = spelling("config_key");
let arrived = proposed()["config_schema_version"]
.as_u64()
.expect("entry 41 states the version the key arrived at");
let early = world.root.join("early.yaml");
std::fs::write(
&early,
format!("schema_version: {}\n{key}: ./check\n", arrived - 1),
)
.expect("the config is written");
let path = world.plan(
"configearly",
&plan_of("configearly", vec![agent("a", &[])]),
);
world
.run(&[
"start",
&path,
"--launch-config",
&early.to_string_lossy(),
"--detach",
])
.exited(REFUSED)
.err_has(&format!("`{key}`"))
.err_has(&format!("schema {arrived} key"));
let blank = world.root.join("blank-validator.yaml");
std::fs::write(
&blank,
format!("schema_version: {arrived}\n{key}: \" \"\n"),
)
.expect("the config is written");
world
.run(&[
"start",
&path,
"--launch-config",
&blank.to_string_lossy(),
"--detach",
])
.exited(REFUSED)
.err_has(&format!("`{key}`"))
.err_has("names nothing");
let earlier = world.root.join("earlier.yaml");
std::fs::write(
&earlier,
format!(
"schema_version: {}\npr_author_graph: ./graphs/dag-scope.yaml\n",
arrived - 1
),
)
.expect("the config is written");
world
.run(&[
"start",
&path,
"--launch-config",
&earlier.to_string_lossy(),
"--attach",
])
.exited(0)
.settled();
}
#[test]
fn a_config_carrying_a_blank_drafting_graph_still_launches_a_run() {
let world = World::new("validator-blank-drafting");
let plan_path = world.plan(
"blankdrafting",
&plan_of("blankdrafting", vec![agent("only", &[])]),
);
for (at, version) in [2, proposed()["config_schema_version"].as_u64().unwrap_or(3)]
.into_iter()
.enumerate()
{
let config = world.root.join(format!("blank-drafting-v{version}.yaml"));
std::fs::write(
&config,
format!("schema_version: {version}\npr_author_graph: \"\"\n"),
)
.expect("the config is written");
let name = format!("blankdrafting-{at}");
let plan_for = world.plan(&name, &plan_of(&name, vec![agent("only", &[])]));
world
.run(&[
"start",
&plan_for,
"--launch-config",
&config.to_string_lossy(),
"--attach",
])
.exited(0)
.settled();
world
.run(&["results", &name])
.exited(0)
.out_has("complete")
.out_has("only");
assert!(
settled(&world, &name, "only", "done"),
"a config carrying a blank drafting graph no longer runs its plan"
);
}
world
.run(&["start", &plan_path, "--attach"])
.exited(0)
.settled();
}
#[test]
fn a_blank_drafting_graph_records_what_naming_none_records() {
let world = World::new("validator-blank-drafting-omitted");
let version = proposed()["config_schema_version"].as_u64().unwrap_or(3);
let declared_graph = world.root.join("drafting.yaml");
std::fs::write(&declared_graph, "version: 1\nname: pr-author\n")
.expect("the drafting graph is written");
let recorded = |run: &str, declared: &str, extra: &[&str]| -> Value {
let config = world.root.join(format!("{run}.yaml"));
std::fs::write(&config, format!("schema_version: {version}\n{declared}"))
.expect("the config is written");
let path = world.plan(run, &plan_of(run, vec![agent("only", &[])]));
let mut args = [
"start",
&path,
"--launch-config",
&config.to_string_lossy(),
"--attach",
]
.iter()
.map(|arg| (*arg).to_string())
.collect::<Vec<_>>();
args.extend(extra.iter().map(|arg| (*arg).to_string()));
world
.run(&args.iter().map(String::as_str).collect::<Vec<_>>())
.exited(0)
.settled();
world.run_json(run, "launch.json")["pr_author_graph"].clone()
};
let named = format!("pr_author_graph: {}\n", declared_graph.display());
let blank = recorded("blankkey", "pr_author_graph: \"\"\n", &[]);
let omitted = recorded("nokey", "", &[]);
assert_eq!(
blank, omitted,
"a blank drafting graph did not launch the run the config omitting the key launches"
);
assert!(
blank.is_null(),
"a blank drafting graph reached the record as a graph this launch names: {blank}"
);
assert_eq!(
recorded("declaredgraph", &named, &[]),
json!(declared_graph.to_string_lossy()),
"the config's own drafting graph did not reach the record"
);
let overridden = recorded("blankflag", &named, &["--pr-author-graph", ""]);
assert!(
overridden.is_null(),
"a blank --pr-author-graph fell through to the config it overrides: {overridden}"
);
}