fix(delivery): the on_green_tests gate ran the suite in the live checkout
Fourth instance of the same defect, and the last of the three commands that run as root against a mission tree. `verify_tests` execs the project's test command with `workdir = repo` — the live checkout — inside a container running as ROOT. `cargo test` writes `target/`, so the checkout ends up owned by two uids and the next phase's cargo hits permission-denied. The harness reported `uids=0,65532` the first time this gate ever ran end to end. It survived because it had never run. Every one of the ten harness fixtures used `commit_policy: "always"`; `on_green_tests` and `on_reviewer_approval` were parsed, implemented, and never exercised — and `Gate`'s own doc already records that three recipes carried this policy while it "did precisely nothing" for want of a reader. A policy that is never exercised is indistinguishable from one that is ignored. Consolidated rather than fixed a third time. `root_copy` now owns the pattern — copy through `mission_fs::pack_dir` into a SIBLING of the mission dir, run there, and purge FROM INSIDE THE CONTAINER, because the copy's `target/` is root-owned and the server (uid 65532) cannot delete it. `benchmark_runner` moved onto it; `evaluator_tools::Sandbox` keeps its own copy logic for now (it carries an allow-list and a judge-facing API, so folding it in is a larger change than this moment warrants — noted, not done). The gate fails CLOSED if the copy cannot be made: an unverifiable suite must not license a push. Also adds the `refactor` scenario, which is what found this. I had written it off as "structurally identical to four existing scenarios" — wrong: it is the only recipe carrying `on_green_tests`, and that made it the only one testing this code path at all. 245 lib tests, 20 test binaries.
This commit is contained in:
@@ -171,11 +171,11 @@ pub async fn run(
|
|||||||
// same way: the harness's uid probe, reporting `uids=0,65532`. Measurement
|
// same way: the harness's uid probe, reporting `uids=0,65532`. Measurement
|
||||||
// must not mutate what it measures — the rule this codebase already applies
|
// must not mutate what it measures — the rule this codebase already applies
|
||||||
// to the judge and to the `verifier` subagent.
|
// to the judge and to the `verifier` subagent.
|
||||||
let copy_root = bench_copy_path(mission_id);
|
let copy_root = crate::root_copy::copy_root("_bench", mission_id);
|
||||||
// A stale copy from a previous run is ROOT-owned (see `purge_copy`), so it
|
// A stale copy from a previous run is ROOT-owned (see `purge_copy`), so it
|
||||||
// must be removed the same way it was created — from inside the container.
|
// must be removed the same way it was created — from inside the container.
|
||||||
purge_copy(&container, ©_root).await;
|
crate::root_copy::purge(&container, ©_root).await;
|
||||||
let copy = BenchCopy::of(&workdir, ©_root)?;
|
let copy = crate::root_copy::RootCopy::of(&workdir, ©_root)?;
|
||||||
let cmd = harness.command();
|
let cmd = harness.command();
|
||||||
let result = docker_exec(&container, copy.workdir(), &cmd)
|
let result = docker_exec(&container, copy.workdir(), &cmd)
|
||||||
.await
|
.await
|
||||||
@@ -184,7 +184,7 @@ pub async fn run(
|
|||||||
// writes `target/` as root, and the server process is uid 65532: its
|
// writes `target/` as root, and the server process is uid 65532: its
|
||||||
// `remove_dir_all` cannot delete root-owned files and silently leaves the
|
// `remove_dir_all` cannot delete root-owned files and silently leaves the
|
||||||
// whole copy behind — measured at 1.2 MB per run, growing forever.
|
// whole copy behind — measured at 1.2 MB per run, growing forever.
|
||||||
purge_copy(&container, ©_root).await;
|
crate::root_copy::purge(&container, ©_root).await;
|
||||||
let raw = result?;
|
let raw = result?;
|
||||||
let metrics = parse_output(&raw, &harness);
|
let metrics = parse_output(&raw, &harness);
|
||||||
Ok((metrics, harness.driver_name().to_string()))
|
Ok((metrics, harness.driver_name().to_string()))
|
||||||
@@ -259,74 +259,6 @@ async fn phase_config(pool: &PgPool, phase_id: Uuid) -> Result<Value, String> {
|
|||||||
|
|
||||||
/// Post-task-#23: shared runtime container + per-mission working dir.
|
/// Post-task-#23: shared runtime container + per-mission working dir.
|
||||||
/// See security_scan::exec_target for the same convention.
|
/// See security_scan::exec_target for the same convention.
|
||||||
/// `<missions_root>/_bench/<mission>` — a sibling of the per-mission dirs, like
|
|
||||||
/// `_verify` and `_outputs`, so reaping a mission never races a running bench.
|
|
||||||
fn bench_copy_path(mission_id: Uuid) -> std::path::PathBuf {
|
|
||||||
crate::mission_workspace::missions_root()
|
|
||||||
.join("_bench")
|
|
||||||
.join(mission_id.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete a benchmark copy from inside the container, as root.
|
|
||||||
///
|
|
||||||
/// The copy's `target/` belongs to root because the benchmark that created it
|
|
||||||
/// ran as root. `std::fs::remove_dir_all` from the server (uid 65532) fails on
|
|
||||||
/// those files, so the tree survives — quietly, because the error was
|
|
||||||
/// discarded. Removing it where it was written is the only thing that works.
|
|
||||||
async fn purge_copy(container: &str, root: &std::path::Path) {
|
|
||||||
let cmd = vec![
|
|
||||||
"rm".to_string(),
|
|
||||||
"-rf".to_string(),
|
|
||||||
root.display().to_string(),
|
|
||||||
];
|
|
||||||
if let Err(e) = docker_exec(container, std::path::Path::new("/"), &cmd).await {
|
|
||||||
eprintln!(
|
|
||||||
"benchmark_runner: could not remove bench copy {}: {e}",
|
|
||||||
root.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A throwaway copy of the checkout for one benchmark run.
|
|
||||||
///
|
|
||||||
/// Removed on drop, including on the error paths — a `target/` left behind is
|
|
||||||
/// both disk and a stale tree a later run could measure by mistake.
|
|
||||||
struct BenchCopy {
|
|
||||||
root: std::path::PathBuf,
|
|
||||||
workdir: std::path::PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BenchCopy {
|
|
||||||
fn of(source: &std::path::Path, root: &std::path::Path) -> Result<Self, String> {
|
|
||||||
// Through the transport packer, so the copy carries exactly what the
|
|
||||||
// delivered diff carries: no `target/`, no `node_modules/`. One
|
|
||||||
// exclusion list, now four consumers.
|
|
||||||
let _ = std::fs::remove_dir_all(root);
|
|
||||||
let archive = crate::mission_fs::pack_dir(source, "repo")
|
|
||||||
.map_err(|e| format!("pack checkout for benchmark: {e}"))?;
|
|
||||||
crate::mission_fs::unpack_into(&archive, root)
|
|
||||||
.map_err(|e| format!("unpack benchmark copy: {e}"))?;
|
|
||||||
let workdir = root.join("repo");
|
|
||||||
if !workdir.is_dir() {
|
|
||||||
return Err(format!("benchmark copy missing at {}", workdir.display()));
|
|
||||||
}
|
|
||||||
Ok(Self { root: root.to_path_buf(), workdir })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn workdir(&self) -> &std::path::Path {
|
|
||||||
&self.workdir
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for BenchCopy {
|
|
||||||
/// Best-effort only. This CANNOT remove the root-owned `target/` a benchmark
|
|
||||||
/// leaves behind — `purge_copy` is what actually clears it, and this stays
|
|
||||||
/// as a fallback for the early-error paths where nothing ran as root yet.
|
|
||||||
fn drop(&mut self) {
|
|
||||||
let _ = std::fs::remove_dir_all(&self.root);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn exec_target(
|
async fn exec_target(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
mission_id: Uuid,
|
mission_id: Uuid,
|
||||||
@@ -508,7 +440,7 @@ mod bench_copy_tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn a_benchmark_runs_in_a_copy_outside_the_mission_directory() {
|
fn a_benchmark_runs_in_a_copy_outside_the_mission_directory() {
|
||||||
let mission = Uuid::now_v7();
|
let mission = Uuid::now_v7();
|
||||||
let copy = bench_copy_path(mission);
|
let copy = crate::root_copy::copy_root("_bench", mission);
|
||||||
let live = crate::mission_workspace::checkout_path(mission);
|
let live = crate::mission_workspace::checkout_path(mission);
|
||||||
assert_ne!(copy, live, "the bench copy must not be the checkout");
|
assert_ne!(copy, live, "the bench copy must not be the checkout");
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ pub mod mission_runtime;
|
|||||||
pub mod mission_workspace;
|
pub mod mission_workspace;
|
||||||
pub mod node_rules;
|
pub mod node_rules;
|
||||||
pub mod phase_runner;
|
pub mod phase_runner;
|
||||||
|
pub mod root_copy;
|
||||||
pub mod phase_summarizer;
|
pub mod phase_summarizer;
|
||||||
pub mod quota;
|
pub mod quota;
|
||||||
mod recursive_exec;
|
mod recursive_exec;
|
||||||
|
|||||||
@@ -345,7 +345,29 @@ pub async fn capture_phase_diff_at(
|
|||||||
if gate == Gate::OnGreenTests {
|
if gate == Gate::OnGreenTests {
|
||||||
let container = std::env::var("CLAWMATES_RUNTIME_CONTAINER")
|
let container = std::env::var("CLAWMATES_RUNTIME_CONTAINER")
|
||||||
.unwrap_or_else(|_| "clawmates-runtime".to_string());
|
.unwrap_or_else(|_| "clawmates-runtime".to_string());
|
||||||
let o = verify_tests(&repo, &container).await;
|
// Against a COPY, never the checkout. `verify_tests` execs
|
||||||
|
// `cargo test` in a container running as ROOT, which writes
|
||||||
|
// `target/` — in the live tree that leaves root-owned build
|
||||||
|
// output in a checkout owned by uid 65532 and breaks the
|
||||||
|
// single-writer invariant. Measured the first time this gate
|
||||||
|
// ever ran end to end: `uids=0,65532`.
|
||||||
|
//
|
||||||
|
// The gate had been implemented but never exercised (every
|
||||||
|
// harness fixture used `commit_policy: "always"`), which is why
|
||||||
|
// a bug this mechanical survived in it.
|
||||||
|
let gate_root = crate::root_copy::copy_root("_gate", mission_id);
|
||||||
|
crate::root_copy::purge(&container, &gate_root).await;
|
||||||
|
let o = match crate::root_copy::RootCopy::of(&repo, &gate_root) {
|
||||||
|
Ok(copy) => {
|
||||||
|
let r = verify_tests(copy.workdir(), &container).await;
|
||||||
|
crate::root_copy::purge(&container, &gate_root).await;
|
||||||
|
r
|
||||||
|
}
|
||||||
|
// Fail-closed: an unverifiable suite must not license a push.
|
||||||
|
Err(e) => TestOutcome::CouldNotRun(format!(
|
||||||
|
"could not copy the checkout to test it: {e}"
|
||||||
|
)),
|
||||||
|
};
|
||||||
// An infrastructure fault must be loud. The gate degrades
|
// An infrastructure fault must be loud. The gate degrades
|
||||||
// safely either way, but "we could not run the suite" is a
|
// safely either way, but "we could not run the suite" is a
|
||||||
// problem with the platform and needs to look like one.
|
// problem with the platform and needs to look like one.
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
//! A throwaway copy of a mission checkout, for commands that run as ROOT.
|
||||||
|
//!
|
||||||
|
//! Three places in this codebase run a real command against a mission's tree —
|
||||||
|
//! the judge's verification (`evaluator_tools::Sandbox`), the benchmark runner,
|
||||||
|
//! and the `on_green_tests` delivery gate. All three enter a container running as
|
||||||
|
//! root with the missions root bind-mounted, and all three run something that
|
||||||
|
//! writes `target/`.
|
||||||
|
//!
|
||||||
|
//! Run against the live checkout, that breaks the single-writer invariant: the
|
||||||
|
//! tree is owned by uid 65532 and now contains root-owned build output, so the
|
||||||
|
//! next phase's `cargo` hits permission-denied on a directory it cannot write.
|
||||||
|
//! The harness's uid probe reports it as `uids=0,65532`.
|
||||||
|
//!
|
||||||
|
//! # The cleanup half, which is the part that keeps being got wrong
|
||||||
|
//!
|
||||||
|
//! The copy inherits the same problem: its `target/` is root-owned, so the
|
||||||
|
//! server process (uid 65532) **cannot delete it**. A `Drop` calling
|
||||||
|
//! `std::fs::remove_dir_all` fails, and because that error is discarded the tree
|
||||||
|
//! survives forever — measured at 1.2 MB per benchmark run and 16 MB of stranded
|
||||||
|
//! judge sandboxes before this existed.
|
||||||
|
//!
|
||||||
|
//! So removal goes back through the container, as root, where the files were
|
||||||
|
//! written. `Drop` remains only as a fallback for the paths where nothing has
|
||||||
|
//! run as root yet, and does not pretend to be more.
|
||||||
|
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
/// Where throwaway copies live: siblings of the per-mission directories, like
|
||||||
|
/// `_outputs` and `_verify`, so reaping a mission cannot race a running command.
|
||||||
|
pub fn copy_root(kind: &str, mission_id: uuid::Uuid) -> PathBuf {
|
||||||
|
crate::mission_workspace::missions_root()
|
||||||
|
.join(kind)
|
||||||
|
.join(mission_id.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a copy from inside the container that wrote it.
|
||||||
|
///
|
||||||
|
/// Best-effort and loud: a housekeeping failure must not cost a real verdict or
|
||||||
|
/// a real benchmark, but it must not be silent either — silence is how the leaks
|
||||||
|
/// this module exists for went unnoticed for a day.
|
||||||
|
pub async fn purge(container: &str, root: &Path) {
|
||||||
|
let Ok(docker) = crate::container_exec::connect() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let argv = vec![
|
||||||
|
"rm".to_string(),
|
||||||
|
"-rf".to_string(),
|
||||||
|
root.display().to_string(),
|
||||||
|
];
|
||||||
|
if let Err(e) = crate::container_exec::exec(
|
||||||
|
&docker,
|
||||||
|
container,
|
||||||
|
Some("/"),
|
||||||
|
&argv,
|
||||||
|
std::time::Duration::from_secs(120),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
eprintln!(
|
||||||
|
"root_copy: could not remove {} from {container}: {e}",
|
||||||
|
root.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A copy of a checkout, removed when it goes out of scope.
|
||||||
|
pub struct RootCopy {
|
||||||
|
root: PathBuf,
|
||||||
|
workdir: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RootCopy {
|
||||||
|
/// Copy `source` into `root`, returning a handle whose `workdir` is the tree
|
||||||
|
/// to run in.
|
||||||
|
///
|
||||||
|
/// Packed through `mission_fs::pack_dir`, so the copy carries exactly what a
|
||||||
|
/// delivered diff carries — no `target/`, no `node_modules/`. One exclusion
|
||||||
|
/// list, four consumers.
|
||||||
|
pub fn of(source: &Path, root: &Path) -> Result<RootCopy, String> {
|
||||||
|
let archive = crate::mission_fs::pack_dir(source, "repo")
|
||||||
|
.map_err(|e| format!("pack {} for a root-run command: {e}", source.display()))?;
|
||||||
|
crate::mission_fs::unpack_into(&archive, root)
|
||||||
|
.map_err(|e| format!("unpack copy into {}: {e}", root.display()))?;
|
||||||
|
let workdir = root.join("repo");
|
||||||
|
if !workdir.is_dir() {
|
||||||
|
return Err(format!("copy missing at {}", workdir.display()));
|
||||||
|
}
|
||||||
|
Ok(RootCopy {
|
||||||
|
root: root.to_path_buf(),
|
||||||
|
workdir,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn workdir(&self) -> &Path {
|
||||||
|
&self.workdir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for RootCopy {
|
||||||
|
/// Fallback only. This CANNOT remove root-owned build output — see
|
||||||
|
/// [`purge`], which is what actually clears a copy something has run in.
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = std::fs::remove_dir_all(&self.root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// A copy must be a SIBLING of the per-mission directory, never inside it:
|
||||||
|
/// `teardown_container` removes `<missions_root>/<mission_id>` wholesale and
|
||||||
|
/// would take a running command's tree with it.
|
||||||
|
#[test]
|
||||||
|
fn copies_live_beside_the_mission_directory_not_inside_it() {
|
||||||
|
let mission = uuid::Uuid::now_v7();
|
||||||
|
let mission_dir = crate::mission_workspace::missions_root().join(mission.to_string());
|
||||||
|
for kind in ["_bench", "_gate"] {
|
||||||
|
let root = copy_root(kind, mission);
|
||||||
|
assert!(!root.starts_with(&mission_dir), "{root:?}");
|
||||||
|
assert!(
|
||||||
|
root.starts_with(crate::mission_workspace::missions_root().join(kind)),
|
||||||
|
"{root:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The copy is not the checkout. Stated as a test because the whole defect
|
||||||
|
/// class is "ran the real command against the real tree".
|
||||||
|
#[test]
|
||||||
|
fn a_copy_is_never_the_checkout() {
|
||||||
|
let mission = uuid::Uuid::now_v7();
|
||||||
|
let live = crate::mission_workspace::checkout_path(mission);
|
||||||
|
assert_ne!(copy_root("_bench", mission).join("repo"), live);
|
||||||
|
assert_ne!(copy_root("_gate", mission).join("repo"), live);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -579,6 +579,54 @@ assert_security() { # <token> <mission> <report>
|
|||||||
|| fail "security: nothing delivered mentions gitleaks — the scan may not have run"
|
|| fail "security: nothing delivered mentions gitleaks — the scan may not have run"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Scenario: refactor — the on_green_tests commit gate ───────────
|
||||||
|
#
|
||||||
|
# The fifth and last recipe, and NOT just another coding phase: it declares
|
||||||
|
# `commit_policy = "on_green_tests"`, and all ten other fixtures use `"always"`.
|
||||||
|
# So the gate that decides whether work is published on the mission branch or
|
||||||
|
# diverted for review has never run end to end — and `Gate`'s own doc records
|
||||||
|
# that three recipes carried this policy while it "did precisely nothing",
|
||||||
|
# because it had no reader at all.
|
||||||
|
#
|
||||||
|
# A policy that is parsed but never exercised is indistinguishable from one that
|
||||||
|
# is ignored. This runs it: tests pass, so the work must land on the mission
|
||||||
|
# branch, not a review branch.
|
||||||
|
REFACTOR_BODY=$(cat <<JSON
|
||||||
|
{"title":"verify: the on_green_tests gate publishes when tests pass",
|
||||||
|
"template_kind":"refactor",
|
||||||
|
"team_template_id":"$TEAM_TEMPLATE",
|
||||||
|
"repo_id":"$REPO_ID",
|
||||||
|
"description":"Prove commit_policy=on_green_tests reaches delivery.",
|
||||||
|
"phases":[
|
||||||
|
{"kind":"coding","order_idx":0,"config":{"commit_policy":"on_green_tests","max_iterations":1,
|
||||||
|
"task":"Add a file KEEP.md at the repository root containing one sentence about this crate. Do not modify any existing file, and do not add or change any test."}}
|
||||||
|
]}
|
||||||
|
JSON
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_refactor() { # <token> <mission> <report>
|
||||||
|
local report="$3" branch_seen=""
|
||||||
|
while read -r idx status files pushed branch cerr perr; do
|
||||||
|
[ "$status" = "completed" ] \
|
||||||
|
&& pass "refactor: phase $idx completed" \
|
||||||
|
|| fail "refactor: phase $idx status=$status (commit_error=$cerr push_error=$perr)"
|
||||||
|
case "$files" in
|
||||||
|
0|-) fail "refactor: phase $idx delivered no files" ;;
|
||||||
|
*) pass "refactor: phase $idx delivered $files file(s)" ;;
|
||||||
|
esac
|
||||||
|
branch_seen="$branch"
|
||||||
|
done <<<"$report"
|
||||||
|
|
||||||
|
# The gate's whole job is WHERE the work lands. A failed gate does not discard
|
||||||
|
# work — it diverts it to a review branch — so a green gate must NOT divert.
|
||||||
|
case "$branch_seen" in
|
||||||
|
"" |-) fail "refactor: no branch recorded — the gate's outcome is unobservable" ;;
|
||||||
|
*-review|*-needs-review)
|
||||||
|
fail "refactor: work landed on a REVIEW branch ($branch_seen) though the gate should have passed" ;;
|
||||||
|
*) pass "refactor: on_green_tests published to the mission branch ($branch_seen)" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
# ── Scenario: the two engines composed ───────────────────────────
|
# ── Scenario: the two engines composed ───────────────────────────
|
||||||
#
|
#
|
||||||
# A `team_engine=composed` mission is a durable ZeroClaw graph whose every node
|
# A `team_engine=composed` mission is a durable ZeroClaw graph whose every node
|
||||||
@@ -977,6 +1025,9 @@ case "${1:-all}" in
|
|||||||
security)
|
security)
|
||||||
run_scenario security "$(echo "$SECURITY_BODY" | tr -d '\n')" assert_security
|
run_scenario security "$(echo "$SECURITY_BODY" | tr -d '\n')" assert_security
|
||||||
;;
|
;;
|
||||||
|
refactor)
|
||||||
|
run_scenario refactor "$(echo "$REFACTOR_BODY" | tr -d '\n')" assert_refactor
|
||||||
|
;;
|
||||||
composed)
|
composed)
|
||||||
run_scenario composed "$(echo "$COMPOSED_BODY" | tr -d '\n')" assert_composed
|
run_scenario composed "$(echo "$COMPOSED_BODY" | tr -d '\n')" assert_composed
|
||||||
;;
|
;;
|
||||||
@@ -995,11 +1046,12 @@ case "${1:-all}" in
|
|||||||
run_scenario research-only "$(echo "$RESEARCH_ONLY_BODY" | tr -d '\n')" assert_research_only no-checkout
|
run_scenario research-only "$(echo "$RESEARCH_ONLY_BODY" | tr -d '\n')" assert_research_only no-checkout
|
||||||
run_scenario benchmark "$(echo "$BENCHMARK_BODY" | tr -d '\n')" assert_benchmark
|
run_scenario benchmark "$(echo "$BENCHMARK_BODY" | tr -d '\n')" assert_benchmark
|
||||||
run_scenario security "$(echo "$SECURITY_BODY" | tr -d '\n')" assert_security
|
run_scenario security "$(echo "$SECURITY_BODY" | tr -d '\n')" assert_security
|
||||||
|
run_scenario refactor "$(echo "$REFACTOR_BODY" | tr -d '\n')" assert_refactor
|
||||||
run_scenario composed "$(echo "$COMPOSED_BODY" | tr -d '\n')" assert_composed
|
run_scenario composed "$(echo "$COMPOSED_BODY" | tr -d '\n')" assert_composed
|
||||||
scenario_roster
|
scenario_roster
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
die "unknown scenario: $1 (selftest|uids|chain|multirole|noop|microvm|gatecap|research-only|benchmark|security|composed|roster|all)"
|
die "unknown scenario: $1 (selftest|uids|chain|multirole|noop|microvm|gatecap|research-only|benchmark|security|refactor|composed|roster|all)"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user