|
|
|
@@ -13,6 +13,17 @@
|
|
|
|
|
//! intentionally not part of the vocabulary — a Substrate asking for
|
|
|
|
|
//! them gets a refusal, not a half-measure.
|
|
|
|
|
//!
|
|
|
|
|
//! Phase B language-advantage pass (dichotomy wave): the command
|
|
|
|
|
//! vocabulary, its interface-op mapping and the lifecycle phase lattice
|
|
|
|
|
//! are typed enums — `CommandType::parse` + exhaustive `match` replace
|
|
|
|
|
//! string-arm chains, so a future vocabulary member cannot be handled at
|
|
|
|
|
//! one site and forgotten at another (the oracle's defensive
|
|
|
|
|
//! "unreachable validator branch" becomes structurally impossible);
|
|
|
|
|
//! `PhaseClass` carries RESUME's terminal refusal pair and
|
|
|
|
|
//! ACKNOWLEDGE_BLOCKER's blockable set as lattice data instead of
|
|
|
|
|
//! string-compare chains. Behavior is byte-identical to the Phase A
|
|
|
|
|
//! parity port (the frozen battery is the referee — zero truth changes).
|
|
|
|
|
//!
|
|
|
|
|
//! FFI doc-envelope law: {"ok":true,"value":...} | {"ok":false,"err":...};
|
|
|
|
|
//! NO ccm_free_string export (sibling crates sharing one Python process
|
|
|
|
|
//! already export identical free symbols; callers free through
|
|
|
|
@@ -33,17 +44,107 @@ pub const COMMANDS: [&str; 6] = [
|
|
|
|
|
"PROVIDE_CONTEXT",
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/// The inbound command vocabulary, typed. Parsing IS the
|
|
|
|
|
/// unknown-command refusal surface: no string arm can be added without
|
|
|
|
|
/// the compiler demanding handling in `to_op`, `_validate` and the
|
|
|
|
|
/// dispatch executor table (the oracle's defensive
|
|
|
|
|
/// "unreachable validator branch" becomes structurally impossible).
|
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
|
|
|
enum CommandType {
|
|
|
|
|
RequestState,
|
|
|
|
|
Start,
|
|
|
|
|
Resume,
|
|
|
|
|
AcknowledgeBlocker,
|
|
|
|
|
ProvideApproval,
|
|
|
|
|
ProvideContext,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl CommandType {
|
|
|
|
|
fn parse(s: &str) -> Option<Self> {
|
|
|
|
|
match s {
|
|
|
|
|
"REQUEST_STATE" => Some(Self::RequestState),
|
|
|
|
|
"START" => Some(Self::Start),
|
|
|
|
|
"RESUME" => Some(Self::Resume),
|
|
|
|
|
"ACKNOWLEDGE_BLOCKER" => Some(Self::AcknowledgeBlocker),
|
|
|
|
|
"PROVIDE_APPROVAL" => Some(Self::ProvideApproval),
|
|
|
|
|
"PROVIDE_CONTEXT" => Some(Self::ProvideContext),
|
|
|
|
|
_ => None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Command → the interface operation it asks for (the interface
|
|
|
|
|
/// still decides: validation failures return refused, never raise
|
|
|
|
|
/// into storage).
|
|
|
|
|
fn to_op(self) -> Op {
|
|
|
|
|
match self {
|
|
|
|
|
Self::RequestState | Self::Resume => Op::Resume, // read-only state
|
|
|
|
|
Self::Start | Self::ProvideApproval | Self::AcknowledgeBlocker => Op::Transition,
|
|
|
|
|
Self::ProvideContext => Op::AmendScope, // context = scope amendment
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The interface operation a validated command asks for, typed.
|
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
|
|
|
enum Op {
|
|
|
|
|
Resume,
|
|
|
|
|
Transition,
|
|
|
|
|
AmendScope,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Op {
|
|
|
|
|
fn as_str(self) -> &'static str {
|
|
|
|
|
match self {
|
|
|
|
|
Self::Resume => "resume",
|
|
|
|
|
Self::Transition => "transition",
|
|
|
|
|
Self::AmendScope => "amend_scope",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Parse back from the intent envelope's op string (the dispatch
|
|
|
|
|
/// executor table matches on this, so an unknown op string stays a
|
|
|
|
|
/// "no executor" envelope, never a panic).
|
|
|
|
|
fn parse(s: &str) -> Option<Self> {
|
|
|
|
|
match s {
|
|
|
|
|
"resume" => Some(Self::Resume),
|
|
|
|
|
"transition" => Some(Self::Transition),
|
|
|
|
|
"amend_scope" => Some(Self::AmendScope),
|
|
|
|
|
_ => None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Command → the interface operation it asks for. The interface still
|
|
|
|
|
/// decides: validation failures return refused, never raise into storage.
|
|
|
|
|
pub fn command_to_operation(command: &str) -> Option<&'static str> {
|
|
|
|
|
match command {
|
|
|
|
|
"REQUEST_STATE" => Some("resume"),
|
|
|
|
|
"START" => Some("transition"),
|
|
|
|
|
"RESUME" => Some("resume"),
|
|
|
|
|
"ACKNOWLEDGE_BLOCKER" => Some("transition"),
|
|
|
|
|
"PROVIDE_APPROVAL" => Some("transition"),
|
|
|
|
|
"PROVIDE_CONTEXT" => Some("amend_scope"),
|
|
|
|
|
_ => None,
|
|
|
|
|
CommandType::parse(command).map(|cmd| cmd.to_op().as_str())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The lifecycle phase lattice as seen by the validator (Phase B):
|
|
|
|
|
/// RESUME's terminal refusal pair (closed/abandoned), the
|
|
|
|
|
/// ACKNOWLEDGE_BLOCKER blockable set (failed/building/verifying) and
|
|
|
|
|
/// START/PROVIDE_APPROVAL's draft gate are lattice data instead of
|
|
|
|
|
/// string-compare chains. Unknown phases (and a missing/null phase)
|
|
|
|
|
/// classify to `Other` and fail every privileged gate — the oracle
|
|
|
|
|
/// accepts a RESUME on an arbitrary phase string but refuses
|
|
|
|
|
/// START/ACKNOWLEDGE_BLOCKER/PROVIDE_APPROVAL on it; the tuple match in
|
|
|
|
|
/// `_validate_branches` encodes exactly that, exhaustively.
|
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
|
|
|
enum PhaseClass {
|
|
|
|
|
Draft,
|
|
|
|
|
Blockable,
|
|
|
|
|
Terminal,
|
|
|
|
|
Other,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl PhaseClass {
|
|
|
|
|
fn classify(phase: Option<&str>) -> Self {
|
|
|
|
|
match phase {
|
|
|
|
|
Some("draft") => Self::Draft,
|
|
|
|
|
Some("failed" | "building" | "verifying") => Self::Blockable,
|
|
|
|
|
Some("closed" | "abandoned") => Self::Terminal,
|
|
|
|
|
_ => Self::Other,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -165,41 +266,42 @@ pub fn command(
|
|
|
|
|
replayed["idempotent_replay"] = json!(true);
|
|
|
|
|
return replayed;
|
|
|
|
|
}
|
|
|
|
|
if !COMMANDS.contains(&command_type) {
|
|
|
|
|
return _refuse(
|
|
|
|
|
command_id,
|
|
|
|
|
if let Some(cmd) = CommandType::parse(command_type) {
|
|
|
|
|
if construction_id.is_empty() {
|
|
|
|
|
return _refuse(
|
|
|
|
|
command_id,
|
|
|
|
|
construction_id,
|
|
|
|
|
correlation_id,
|
|
|
|
|
"construction_id is required",
|
|
|
|
|
now,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return _validate(
|
|
|
|
|
cmd,
|
|
|
|
|
construction_id,
|
|
|
|
|
correlation_id,
|
|
|
|
|
&format!(
|
|
|
|
|
"unknown command '{}' (supported: {})",
|
|
|
|
|
command_type,
|
|
|
|
|
COMMANDS.join(", ")
|
|
|
|
|
),
|
|
|
|
|
now,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if construction_id.is_empty() {
|
|
|
|
|
return _refuse(
|
|
|
|
|
command_id,
|
|
|
|
|
construction_id,
|
|
|
|
|
correlation_id,
|
|
|
|
|
"construction_id is required",
|
|
|
|
|
payload,
|
|
|
|
|
now,
|
|
|
|
|
scaffold_doc,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
_validate(
|
|
|
|
|
command_type,
|
|
|
|
|
_refuse(
|
|
|
|
|
command_id,
|
|
|
|
|
construction_id,
|
|
|
|
|
correlation_id,
|
|
|
|
|
command_id,
|
|
|
|
|
payload,
|
|
|
|
|
&format!(
|
|
|
|
|
"unknown command '{}' (supported: {})",
|
|
|
|
|
command_type,
|
|
|
|
|
COMMANDS.join(", ")
|
|
|
|
|
),
|
|
|
|
|
now,
|
|
|
|
|
scaffold_doc,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
|
|
|
fn _validate(
|
|
|
|
|
command_type: &str,
|
|
|
|
|
cmd: CommandType,
|
|
|
|
|
construction_id: &str,
|
|
|
|
|
correlation_id: &str,
|
|
|
|
|
command_id: &str,
|
|
|
|
@@ -240,7 +342,7 @@ fn _validate(
|
|
|
|
|
};
|
|
|
|
|
// Python get("phase", "") only falls back to "" for a MISSING key;
|
|
|
|
|
// a present null renders "None" in the refusal messages.
|
|
|
|
|
let phase_py: Option<&str> = contract.get("phase").and_then(Value::as_str);
|
|
|
|
|
let phase = PhaseClass::classify(contract.get("phase").and_then(Value::as_str));
|
|
|
|
|
let phase_render = match contract.get("phase") {
|
|
|
|
|
Some(v) => py_render(v),
|
|
|
|
|
None => String::new(),
|
|
|
|
@@ -251,107 +353,121 @@ fn _validate(
|
|
|
|
|
} else {
|
|
|
|
|
payload
|
|
|
|
|
};
|
|
|
|
|
_validate_branches(
|
|
|
|
|
cmd,
|
|
|
|
|
construction_id,
|
|
|
|
|
correlation_id,
|
|
|
|
|
command_id,
|
|
|
|
|
payload,
|
|
|
|
|
now,
|
|
|
|
|
phase,
|
|
|
|
|
&phase_render,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
match command_type {
|
|
|
|
|
"REQUEST_STATE" => _accept(
|
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
|
|
|
fn _validate_branches(
|
|
|
|
|
cmd: CommandType,
|
|
|
|
|
construction_id: &str,
|
|
|
|
|
correlation_id: &str,
|
|
|
|
|
command_id: &str,
|
|
|
|
|
payload: &Value,
|
|
|
|
|
now: &str,
|
|
|
|
|
phase: PhaseClass,
|
|
|
|
|
phase_render: &str,
|
|
|
|
|
) -> Value {
|
|
|
|
|
// Exhaustive over the (command, phase-class) lattice: adding a
|
|
|
|
|
// CommandType or PhaseClass variant breaks compilation here, so a
|
|
|
|
|
// future vocabulary member cannot be handled at one site and
|
|
|
|
|
// forgotten at another (the oracle's defensive "unreachable
|
|
|
|
|
// validator branch" is structurally impossible).
|
|
|
|
|
match (cmd, phase) {
|
|
|
|
|
(CommandType::RequestState, _) => _accept(
|
|
|
|
|
command_id,
|
|
|
|
|
construction_id,
|
|
|
|
|
correlation_id,
|
|
|
|
|
"resume",
|
|
|
|
|
Op::Resume.as_str(),
|
|
|
|
|
json!({"contract_id": construction_id}),
|
|
|
|
|
now,
|
|
|
|
|
),
|
|
|
|
|
"RESUME" => {
|
|
|
|
|
if phase_py == Some("closed") || phase_py == Some("abandoned") {
|
|
|
|
|
_refuse(
|
|
|
|
|
// RESUME refuses only on a terminal construction; any other
|
|
|
|
|
// phase string (even an unknown one) re-reads canonical state.
|
|
|
|
|
(CommandType::Resume, PhaseClass::Terminal) => _refuse(
|
|
|
|
|
command_id,
|
|
|
|
|
construction_id,
|
|
|
|
|
correlation_id,
|
|
|
|
|
&format!("construction is {phase_render} — resume is invalid"),
|
|
|
|
|
now,
|
|
|
|
|
),
|
|
|
|
|
(CommandType::Resume, _) => _accept(
|
|
|
|
|
command_id,
|
|
|
|
|
construction_id,
|
|
|
|
|
correlation_id,
|
|
|
|
|
Op::Resume.as_str(),
|
|
|
|
|
json!({"contract_id": construction_id}),
|
|
|
|
|
now,
|
|
|
|
|
),
|
|
|
|
|
(CommandType::Start, PhaseClass::Draft) => _accept(
|
|
|
|
|
command_id,
|
|
|
|
|
construction_id,
|
|
|
|
|
correlation_id,
|
|
|
|
|
Op::Transition.as_str(),
|
|
|
|
|
json!({"contract_id": construction_id, "phase": "approved"}),
|
|
|
|
|
now,
|
|
|
|
|
),
|
|
|
|
|
(CommandType::Start, _) => _refuse(
|
|
|
|
|
command_id,
|
|
|
|
|
construction_id,
|
|
|
|
|
correlation_id,
|
|
|
|
|
&format!("START requires draft phase (current: {phase_render})"),
|
|
|
|
|
now,
|
|
|
|
|
),
|
|
|
|
|
(CommandType::ProvideApproval, PhaseClass::Draft) => {
|
|
|
|
|
match payload.get("approved_by").and_then(Value::as_str) {
|
|
|
|
|
Some("user") => _accept(
|
|
|
|
|
command_id,
|
|
|
|
|
construction_id,
|
|
|
|
|
correlation_id,
|
|
|
|
|
&format!("construction is {phase_render} — resume is invalid"),
|
|
|
|
|
now,
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
_accept(
|
|
|
|
|
command_id,
|
|
|
|
|
construction_id,
|
|
|
|
|
correlation_id,
|
|
|
|
|
"resume",
|
|
|
|
|
json!({"contract_id": construction_id}),
|
|
|
|
|
now,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
"START" => {
|
|
|
|
|
if phase_py != Some("draft") {
|
|
|
|
|
_refuse(
|
|
|
|
|
command_id,
|
|
|
|
|
construction_id,
|
|
|
|
|
correlation_id,
|
|
|
|
|
&format!("START requires draft phase (current: {phase_render})"),
|
|
|
|
|
now,
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
_accept(
|
|
|
|
|
command_id,
|
|
|
|
|
construction_id,
|
|
|
|
|
correlation_id,
|
|
|
|
|
"transition",
|
|
|
|
|
json!({"contract_id": construction_id, "phase": "approved"}),
|
|
|
|
|
now,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
"PROVIDE_APPROVAL" => {
|
|
|
|
|
if phase_py != Some("draft") {
|
|
|
|
|
_refuse(
|
|
|
|
|
command_id,
|
|
|
|
|
construction_id,
|
|
|
|
|
correlation_id,
|
|
|
|
|
&format!("approval applies to draft (current: {phase_render})"),
|
|
|
|
|
now,
|
|
|
|
|
)
|
|
|
|
|
} else if payload.get("approved_by").and_then(Value::as_str) != Some("user") {
|
|
|
|
|
// lifecycle rule: approval provenance must be the user —
|
|
|
|
|
// Substrate cannot approve on the user's behalf
|
|
|
|
|
_refuse(
|
|
|
|
|
command_id,
|
|
|
|
|
construction_id,
|
|
|
|
|
correlation_id,
|
|
|
|
|
"approval provenance must be user (Substrate cannot approve for the user)",
|
|
|
|
|
now,
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
_accept(
|
|
|
|
|
command_id,
|
|
|
|
|
construction_id,
|
|
|
|
|
correlation_id,
|
|
|
|
|
"transition",
|
|
|
|
|
Op::Transition.as_str(),
|
|
|
|
|
json!({"contract_id": construction_id, "phase": "approved", "approved_by": "user"}),
|
|
|
|
|
now,
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
_ => {
|
|
|
|
|
// lifecycle rule: approval provenance must be the user —
|
|
|
|
|
// Substrate cannot approve on the user's behalf
|
|
|
|
|
_refuse(
|
|
|
|
|
command_id,
|
|
|
|
|
construction_id,
|
|
|
|
|
correlation_id,
|
|
|
|
|
"approval provenance must be user (Substrate cannot approve for the user)",
|
|
|
|
|
now,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
"ACKNOWLEDGE_BLOCKER" => {
|
|
|
|
|
if !matches!(phase_py, Some("failed") | Some("building") | Some("verifying")) {
|
|
|
|
|
_refuse(
|
|
|
|
|
command_id,
|
|
|
|
|
construction_id,
|
|
|
|
|
correlation_id,
|
|
|
|
|
&format!("no blockable phase (current: {phase_render})"),
|
|
|
|
|
now,
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
_accept(
|
|
|
|
|
command_id,
|
|
|
|
|
construction_id,
|
|
|
|
|
correlation_id,
|
|
|
|
|
"transition",
|
|
|
|
|
json!({"contract_id": construction_id, "phase": "building"}),
|
|
|
|
|
now,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
"PROVIDE_CONTEXT" => {
|
|
|
|
|
(CommandType::ProvideApproval, _) => _refuse(
|
|
|
|
|
command_id,
|
|
|
|
|
construction_id,
|
|
|
|
|
correlation_id,
|
|
|
|
|
&format!("approval applies to draft (current: {phase_render})"),
|
|
|
|
|
now,
|
|
|
|
|
),
|
|
|
|
|
(CommandType::AcknowledgeBlocker, PhaseClass::Blockable) => _accept(
|
|
|
|
|
command_id,
|
|
|
|
|
construction_id,
|
|
|
|
|
correlation_id,
|
|
|
|
|
Op::Transition.as_str(),
|
|
|
|
|
json!({"contract_id": construction_id, "phase": "building"}),
|
|
|
|
|
now,
|
|
|
|
|
),
|
|
|
|
|
(CommandType::AcknowledgeBlocker, _) => _refuse(
|
|
|
|
|
command_id,
|
|
|
|
|
construction_id,
|
|
|
|
|
correlation_id,
|
|
|
|
|
&format!("no blockable phase (current: {phase_render})"),
|
|
|
|
|
now,
|
|
|
|
|
),
|
|
|
|
|
(CommandType::ProvideContext, _) => {
|
|
|
|
|
let paths = python_or_empty_list(payload.get("add_paths"));
|
|
|
|
|
if !python_truthy(Some(&paths)) {
|
|
|
|
|
_refuse(
|
|
|
|
@@ -374,7 +490,7 @@ fn _validate(
|
|
|
|
|
command_id,
|
|
|
|
|
construction_id,
|
|
|
|
|
correlation_id,
|
|
|
|
|
"amend_scope",
|
|
|
|
|
Op::AmendScope.as_str(),
|
|
|
|
|
json!({
|
|
|
|
|
"contract_id": construction_id,
|
|
|
|
|
"add_paths": paths,
|
|
|
|
@@ -385,13 +501,6 @@ fn _validate(
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ => _refuse(
|
|
|
|
|
command_id,
|
|
|
|
|
construction_id,
|
|
|
|
|
correlation_id,
|
|
|
|
|
"unreachable validator branch",
|
|
|
|
|
now,
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -484,25 +593,28 @@ pub fn dispatch(
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
let op = intent.get("op").and_then(Value::as_str).unwrap_or("");
|
|
|
|
|
let op_str = intent.get("op").and_then(Value::as_str).unwrap_or("");
|
|
|
|
|
let args = intent.get("args").cloned().unwrap_or(json!({}));
|
|
|
|
|
if !matches!(op, "resume" | "transition" | "amend_scope") {
|
|
|
|
|
return json!({
|
|
|
|
|
"kind": "final",
|
|
|
|
|
"record": true,
|
|
|
|
|
"intent": intent,
|
|
|
|
|
"envelope": {
|
|
|
|
|
"accepted": true,
|
|
|
|
|
"executed": false,
|
|
|
|
|
"success": false,
|
|
|
|
|
"error": format!("no executor for op '{}'", op),
|
|
|
|
|
"command_type": command_type,
|
|
|
|
|
"construction_id": construction_id,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
let op = match Op::parse(op_str) {
|
|
|
|
|
Some(op) => op,
|
|
|
|
|
None => {
|
|
|
|
|
return json!({
|
|
|
|
|
"kind": "final",
|
|
|
|
|
"record": true,
|
|
|
|
|
"intent": intent,
|
|
|
|
|
"envelope": {
|
|
|
|
|
"accepted": true,
|
|
|
|
|
"executed": false,
|
|
|
|
|
"success": false,
|
|
|
|
|
"error": format!("no executor for op '{}'", op_str),
|
|
|
|
|
"command_type": command_type,
|
|
|
|
|
"construction_id": construction_id,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let evidence_ref = match op {
|
|
|
|
|
"transition" => {
|
|
|
|
|
Op::Transition => {
|
|
|
|
|
let suffix = match args.get("approved_by") {
|
|
|
|
|
Some(v) if python_truthy(Some(v)) => format!(" approved_by:{}", py_render(v)),
|
|
|
|
|
_ => String::new(),
|
|
|
|
@@ -512,12 +624,12 @@ pub fn dispatch(
|
|
|
|
|
intent.get("command_id").and_then(Value::as_str).unwrap_or("")
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
_ => String::new(),
|
|
|
|
|
Op::Resume | Op::AmendScope => String::new(),
|
|
|
|
|
};
|
|
|
|
|
json!({
|
|
|
|
|
"kind": "exec",
|
|
|
|
|
"record": true,
|
|
|
|
|
"op": op,
|
|
|
|
|
"op": op.as_str(),
|
|
|
|
|
"args": args,
|
|
|
|
|
"evidence_ref": evidence_ref,
|
|
|
|
|
"intent": intent,
|
|
|
|
@@ -641,9 +753,6 @@ pub extern "C" fn ccm_dispatch_finish(arg: *const c_char) -> *mut c_char {
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod branch_battery;
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
@@ -688,13 +797,32 @@ mod tests {
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn command_vocabulary_maps_to_interface_ops() {
|
|
|
|
|
assert_eq!(command_to_operation("REQUEST_STATE"), Some("resume"));
|
|
|
|
|
assert_eq!(command_to_operation("START"), Some("transition"));
|
|
|
|
|
assert_eq!(command_to_operation("RESUME"), Some("resume"));
|
|
|
|
|
assert_eq!(command_to_operation("ACKNOWLEDGE_BLOCKER"), Some("transition"));
|
|
|
|
|
assert_eq!(command_to_operation("PROVIDE_APPROVAL"), Some("transition"));
|
|
|
|
|
assert_eq!(command_to_operation("PROVIDE_CONTEXT"), Some("amend_scope"));
|
|
|
|
|
// the full lattices, exhausted through the typed enums
|
|
|
|
|
for (name, cmd, op) in [
|
|
|
|
|
("REQUEST_STATE", CommandType::RequestState, Op::Resume),
|
|
|
|
|
("START", CommandType::Start, Op::Transition),
|
|
|
|
|
("RESUME", CommandType::Resume, Op::Resume),
|
|
|
|
|
("ACKNOWLEDGE_BLOCKER", CommandType::AcknowledgeBlocker, Op::Transition),
|
|
|
|
|
("PROVIDE_APPROVAL", CommandType::ProvideApproval, Op::Transition),
|
|
|
|
|
("PROVIDE_CONTEXT", CommandType::ProvideContext, Op::AmendScope),
|
|
|
|
|
] {
|
|
|
|
|
assert_eq!(CommandType::parse(name), Some(cmd));
|
|
|
|
|
assert_eq!(cmd.to_op(), op);
|
|
|
|
|
assert_eq!(command_to_operation(name), Some(op.as_str()));
|
|
|
|
|
}
|
|
|
|
|
assert_eq!(command_to_operation("PAUSE"), None);
|
|
|
|
|
// the RESUME terminal pair and the ACK blockable set live in
|
|
|
|
|
// the phase lattice, not in string-compare chains
|
|
|
|
|
assert_eq!(PhaseClass::classify(Some("draft")), PhaseClass::Draft);
|
|
|
|
|
for phase in ["failed", "building", "verifying"] {
|
|
|
|
|
assert_eq!(PhaseClass::classify(Some(phase)), PhaseClass::Blockable);
|
|
|
|
|
}
|
|
|
|
|
for phase in ["closed", "abandoned"] {
|
|
|
|
|
assert_eq!(PhaseClass::classify(Some(phase)), PhaseClass::Terminal);
|
|
|
|
|
}
|
|
|
|
|
for phase in [None, Some(""), Some("weird"), Some("DRAFT")] {
|
|
|
|
|
assert_eq!(PhaseClass::classify(phase), PhaseClass::Other);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
@@ -738,4 +866,7 @@ mod tests {
|
|
|
|
|
assert_eq!(err.chars().count(), 200);
|
|
|
|
|
assert!(err.starts_with("serving unavailable: "));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod branch_battery;
|