use super::tools::ToolResult;
use crate::config::Config;
use crate::network::{Live, Rail};
use serde_json::{Value, json};
use std::path::Path;
use std::path::PathBuf;
#[path = "site_end_to_end/approval.rs"]
mod approval;
#[path = "site_end_to_end/server.rs"]
mod server;
#[path = "site_end_to_end/wire.rs"]
mod wire;
#[path = "site_end_to_end/worker.rs"]
mod worker;
use approval::{approve, approve_refused, grants, http};
use server::ServerProcess;
use wire::Watch;
use worker::{GoWorker, build_worker};
type TestResult = Result<(), Box<dyn std::error::Error>>;
fn required(name: &str) -> Result<String, Box<dyn std::error::Error>> {
std::env::var(name)
.map_err(|_| format!("{name} is required for the real construction fixture").into())
}
fn require(value: bool, reason: &'static str) -> TestResult {
if value { Ok(()) } else { Err(reason.into()) }
}
fn config(role: &str) -> Result<Config, Box<dyn std::error::Error>> {
Ok(Config::load(Path::new(&required(&format!(
"AGT003_{role}_CONFIG"
))?))?)
}
fn tool(
role: &str,
name: &str,
arguments: &Value,
) -> Result<ToolResult, Box<dyn std::error::Error>> {
let config = config(role)?;
let rail = Live::new(&config)?;
let arguments = arguments
.as_object()
.ok_or("fixture arguments must be an object")?;
super::plan_tools::tool(&rail, name, arguments)
.or_else(|| super::template_tools::tool(&rail, name, arguments))
.ok_or_else(|| "construction tool is absent".into())
}
fn health() -> Result<Value, Box<dyn std::error::Error>> {
http(
required("AGT003_ADMIN_HTTP")?.parse()?,
"GET",
"/health",
None,
&json!({}),
config("APPROVER")?.limits.record_bytes,
)
}
fn create_and_bind(site: &str, subject: &str, builder: &str, topology: &mut Watch) -> TestResult {
let missing = tool(
"ASSISTANT",
"create_site",
&json!({"name":site,"purpose":"Echo validated requests.","subjects":[subject],"contracts":{}}),
)?;
require(missing.is_error, "site without contracts was admitted")?;
require(
missing.structured["error"].as_str() == Some(&format!("subject {subject} has no schema")),
"missing contract refusal words differ",
)?;
let mut contracts = serde_json::Map::new();
contracts.insert(subject.to_owned(),json!({"input_schema_json":"{\"type\":\"object\"}","output_schema_json":"{\"type\":\"object\"}"}));
let created = tool(
"ASSISTANT",
"create_site",
&json!({"name":site,"purpose":"Echo validated requests.","subjects":[subject],"contracts":contracts}),
)?;
approve(&created)?;
let created = topology.station(site, "building")?;
require(
created["contracts"]
.as_array()
.is_some_and(|contracts| contracts.len() == 1 && contracts[0]["subject"] == subject),
"building topology omitted the contract",
)?;
approve(&tool(
"ASSISTANT",
"bind_builder",
&json!({"service":site,"agent":builder}),
)?)?;
let bound = topology.station(site, "building")?;
require(
bound["builder"] == builder,
"building topology omitted the builder",
)?;
approve_refused(
&tool(
"ASSISTANT",
"bind_builder",
&json!({"service":site,"agent":builder}),
)?,
&format!("site {site} has builder {builder}"),
)?;
approve(&tool(
"ASSISTANT",
"unbind_builder",
&json!({"service":site}),
)?)?;
let revoked = grants()?;
require(
revoked["grants"]
.as_array()
.ok_or("issuer grants absent")?
.iter()
.all(|grant| {
grant["holder"] != builder
|| grant["resource"]["id"] != site
|| grant["standing"]["stands"] == false
}),
"unbind left a builder site grant standing",
)?;
approve(&tool(
"ASSISTANT",
"bind_builder",
&json!({"service":site,"agent":builder}),
)?)?;
let rights = grants()?;
let owned = rights["grants"]
.as_array()
.ok_or("issuer grant list absent")?
.iter()
.filter(|grant| {
grant["holder"] == builder
&& grant["resource"]["kind"] == "liminal.service"
&& grant["resource"]["id"] == site
&& grant["standing"]["stands"] == true
})
.collect::<Vec<_>>();
require(
owned.len() == 2,
"bound builder does not hold exactly two site grants",
)?;
require(
owned
.iter()
.any(|grant| grant["actions"] == json!(["plan"]))
&& owned
.iter()
.any(|grant| grant["actions"] == json!(["serve"])),
"builder's site grants contain other actions",
)?;
Ok(())
}
fn check_plan(site: &str) -> Result<Watch, Box<dyn std::error::Error>> {
let set = tool(
"BUILDER",
"plan_set",
&json!({"service":site,"steps":[{"title":"Design"},{"title":"Build"}]}),
)?;
require(!set.is_error, "bound builder plan_set refused")?;
let mut plan = Watch::new("APPROVER", &format!("services.{site}.plan"))?;
require(
plan.value()?["revision"] == 1,
"retained current plan did not arrive first",
)?;
let changed = tool(
"BUILDER",
"plan_step",
&json!({"service":site,"step":1,"status":"doing"}),
)?;
require(!changed.is_error, "bound builder plan_step refused")?;
let next = plan.value()?;
require(
next["revision"] == 2 && next["current"] == 1,
"plan change differs from record",
)?;
let foreign = tool(
"ASSISTANT",
"plan_set",
&json!({"service":site,"steps":[{"title":"Design"}]}),
)?;
require(foreign.is_error, "assistant wrote a builder's plan")?;
let duplicate = tool(
"BUILDER",
"plan_step",
&json!({"service":site,"step":2,"status":"doing"}),
)?;
require(
duplicate.is_error && duplicate.structured["error"] == "step 1 is already doing",
"second doing step was not refused exactly",
)?;
let assistant = required("AGT003_ASSISTANT")?;
require(
foreign.structured["error"]
== format!("pass holder {assistant} is not the builder of {site}"),
"foreign builder refusal words differ",
)?;
let done = tool(
"BUILDER",
"plan_step",
&json!({"service":site,"step":1,"status":"done"}),
)?;
require(!done.is_error, "builder could not finish its step")?;
require(
plan.value()?["revision"] == 3,
"finished step transition not delivered",
)?;
let removed = tool(
"BUILDER",
"plan_set",
&json!({"service":site,"steps":[{"title":"Changed"}]}),
)?;
require(
removed.is_error
&& removed.structured["error"] == "done step 1 cannot be removed or renamed",
"finished step was removed or renamed",
)?;
Ok(plan)
}
fn trial_and_complete(
site: &str,
subject: &str,
binary: &Path,
builder: &str,
topology: &mut Watch,
) -> TestResult {
let mut mismatch = GoWorker::start(binary, subject, "{\"type\":\"string\"}")?;
let refused = mismatch.signal()?;
require(
refused["ok"] == false
&& refused["error"]["message"]
== format!("subject {subject}: registered output schema differs from site"),
"wrong output schema was not refused by subject",
)?;
drop(mismatch);
let mut trial = GoWorker::start(binary, subject, "{\"type\":\"object\"}")?;
require(
trial.signal()?["event"] == "ready",
"real Go trial failed registration",
)?;
let station = topology.station(site, "trial")?;
let connection = station["trial_connection"]
.as_u64()
.ok_or("trial connection absent")?;
let approver = Live::new(&config("APPROVER")?)?;
let third = Live::new(&config("THIRD")?)?;
let refused = third
.request(subject, &json!({"echo":true}), true)
.err()
.ok_or("third party called trial")?;
require(
refused.to_string() == format!("service {site} is a trial"),
"trial refusal words differ",
)?;
require(
approver.request(subject, &json!({"echo":true}), true)? == Some(json!({"echo":true})),
"approver trial call was not answered",
)?;
let proposal = tool(
"BUILDER",
"propose_template",
&json!({"service":site,"command":binary,"args":[],"env":[],"stop_timeout_ms":1000}),
)?;
let words = proposal.structured["words"]
.as_str()
.ok_or("template held words absent")?;
let words: Value = serde_json::from_str(words)?;
require(
words["change"]["detail"]["command"] == serde_json::to_value(binary)?,
"draft does not carry the approved command",
)?;
require(
words["change"]["detail"]["subjects"] == json!([subject]),
"draft subjects differ from site",
)?;
approve(&proposal)?;
let premature = tool("BUILDER", "complete", &json!({"service":site}))?;
approve_refused(
&premature,
&format!("trial still registered on connection {connection}"),
)?;
trial.stop()?;
topology.state(site, "building")?;
approve(&tool("BUILDER", "complete", &json!({"service":site}))?)?;
topology.state(site, "asleep")?;
let rights = grants()?;
require(
rights["grants"]
.as_array()
.ok_or("issuer grants absent")?
.iter()
.all(|grant| {
grant["holder"] != builder
|| grant["resource"]["id"] != site
|| grant["actions"] != json!(["serve"])
|| grant["standing"]["stands"] == false
}),
"completion left the builder's serve grant standing",
)?;
Ok(())
}
fn run_approved_template(
site: &str,
subject: &str,
binary: &Path,
started: &Value,
topology: &mut Watch,
) -> TestResult {
let creator = Live::new(&config("APPROVER")?)?;
check_changed_command(&creator, site, binary)?;
let deployed = creator.request(
"services.deploy",
&json!({"operation":"agt003-start","payload":{"id":site}}),
true,
)?;
require(
deployed
.as_ref()
.is_some_and(|answer| answer["outcome"] == "acted"),
"record template deployment was held or refused",
)?;
topology.state(site, "live")?;
require(
creator.request(subject, &json!({"live":true}), true)? == Some(json!({"live":true})),
"approved template did not answer",
)?;
let after = health()?;
require(
&after["started_at"] == started,
"exchange restarted during template approval",
)?;
let stopped = creator.request(
"services.hold",
&json!({"operation":"agt003-stop","payload":{"id":site}}),
true,
)?;
require(
stopped
.as_ref()
.is_some_and(|answer| answer["outcome"] == "acted"),
"template stop was held or refused",
)?;
topology.state(site, "held")?;
Ok(())
}
fn check_changed_command(rail: &Live, site: &str, binary: &Path) -> TestResult {
use sha2::{Digest, Sha256};
use std::io::Read;
let text = std::fs::read_to_string(required("AGT003_SERVER_CONFIG")?)?;
let config: toml::Value = toml::from_str(&text)?;
let bound = u64::try_from(
config["auth"]["lys"]["templates"]["command_bytes"]
.as_integer()
.ok_or("template command_bytes absent in real fixture")?,
)?;
require(bound > 0, "template command_bytes is zero")?;
let mut approved = Vec::new();
std::fs::File::open(binary)?
.take(bound.saturating_add(1))
.read_to_end(&mut approved)?;
require(
u64::try_from(approved.len())? <= bound,
"fixture program exceeds template command_bytes",
)?;
let changed = b"a replaced approved executable";
std::fs::write(binary, changed)?;
let result = rail.request(
"services.deploy",
&json!({"operation":"agt003-changed","payload":{"id":site}}),
true,
);
std::fs::write(binary, &approved)?;
let refused = result.err().ok_or("changed command was executed")?;
let words = refused.to_string();
require(
words.contains("is not the file approved on")
&& words.contains(&format!(
"digest {} != {}",
hex::encode(Sha256::digest(&approved)),
hex::encode(Sha256::digest(changed))
)),
"changed command digest refusal words differ",
)
}
fn site_record(site: &str) -> Result<Value, Box<dyn std::error::Error>> {
let rail = Live::new(&config("APPROVER")?)?;
let answer = rail
.request(
"services.list",
&json!({"operation":"agt003-read","payload":null}),
true,
)?
.ok_or("construction record list absent")?;
answer["services"]
.as_array()
.ok_or("construction services absent")?
.iter()
.find(|record| record["name"] == site)
.cloned()
.ok_or_else(|| "construction record absent".into())
}
#[test]
fn construction_site_go_worker_approval_plan_trial_and_live_template_journey() -> TestResult {
let site = required("AGT003_SITE_NAME")?;
let subject = required("AGT003_SITE_SUBJECT")?;
let root = PathBuf::from(required("AGT003_TEMPLATE_ROOT")?);
let (built, binary) = build_worker(&root)?;
let server = ServerProcess::start()?;
let mut topology = Watch::new("APPROVER", "console.topology")?;
let builder = required("AGT003_BUILDER")?;
create_and_bind(&site, &subject, &builder, &mut topology)?;
let plan = check_plan(&site)?;
let before = site_record(&site)?;
require(
before["plan"]["revision"] == 3
&& before["plan"]["transitions"]
.as_array()
.is_some_and(|history| history.len() == 3),
"record plan history differs from its writes",
)?;
drop(plan);
drop(topology);
server.stop()?;
let server = ServerProcess::start()?;
let mut topology = Watch::new("APPROVER", "console.topology")?;
topology.state(&site, "building")?;
let after = site_record(&site)?;
require(
after["plan"] == before["plan"],
"real exchange restart changed plan revision or history",
)?;
let mut plan = Watch::new("APPROVER", &format!("services.{site}.plan"))?;
require(
plan.value()?["revision"] == 3,
"restart did not retain the current plan",
)?;
let started = health()?["started_at"].clone();
require(!started.is_null(), "health.started_at is absent")?;
trial_and_complete(&site, &subject, &binary, &builder, &mut topology)?;
run_approved_template(&site, &subject, &binary, &started, &mut topology)?;
drop(topology);
drop(plan);
server.stop()?;
drop(built);
Ok(())
}