wip: safety snapshot before pod redeploy
This commit is contained in:
+18
-1
@@ -13,4 +13,21 @@ reviews/
|
|||||||
# assumed these bytes came from git, which is what let the roundtrip test detect
|
# assumed these bytes came from git, which is what let the roundtrip test detect
|
||||||
# cross-version drift. Untracked, that guarantee is gone — a fresh clone has no
|
# cross-version drift. Untracked, that guarantee is gone — a fresh clone has no
|
||||||
# fixtures at all — so the task's Verify section needs rewriting to match.
|
# fixtures at all — so the task's Verify section needs rewriting to match.
|
||||||
**/tests/fixtures/
|
**/tests/fixtures/
|
||||||
|
# agent-harness: build artifacts and vendored archives never belong in source control
|
||||||
|
*.tar.gz
|
||||||
|
*.tgz
|
||||||
|
*.crate
|
||||||
|
*.zip
|
||||||
|
*.bin
|
||||||
|
*.whl
|
||||||
|
vendor/
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# agent-harness: task/phase completion sentinel files, harness bookkeeping only
|
||||||
|
.task-result-*
|
||||||
|
.phase-result-*
|
||||||
|
.stage-done-*
|
||||||
|
|
||||||
|
# agent-harness: PLAN.md is per-task planner scratch state, never a deliverable
|
||||||
|
PLAN.md
|
||||||
|
|||||||
+133
-57
@@ -58,70 +58,146 @@ pub fn transition(
|
|||||||
from: AttemptState,
|
from: AttemptState,
|
||||||
cause: TransitionCause,
|
cause: TransitionCause,
|
||||||
) -> Result<AttemptState, IllegalTransition> {
|
) -> Result<AttemptState, IllegalTransition> {
|
||||||
use AttemptState::*;
|
use {AttemptState::*, TransitionCause::*};
|
||||||
use TransitionCause::*;
|
|
||||||
|
|
||||||
let to = match (from, cause) {
|
// Outer match exhaustively lists ALL 7 states. Adding a new variant here
|
||||||
// Pending transitions
|
// is a compile error — this is the structural enforcement required by
|
||||||
(Pending, Admitted) => Running,
|
// spec §5: `transition` must be **arm for arm** against AttemptState, so
|
||||||
(Pending, QueueDeadlinePassed) => TimedOut,
|
// extending the enum without updating transition breaks the build.
|
||||||
(
|
|
||||||
Pending,
|
match from {
|
||||||
Cancel {
|
// Terminal states (Succeeded | Failed | Indeterminate | Cancelled | TimedOut):
|
||||||
intent_dispatched: false,
|
// Reject EVERY cause unconditionally. We never pattern-match `cause` in
|
||||||
},
|
// these arms, so _ no source-side wildcard on the Transition-Cause axis, ever.
|
||||||
) => Cancelled,
|
Succeeded
|
||||||
|
| Failed
|
||||||
|
| Indeterminate
|
||||||
|
| Cancelled
|
||||||
|
| TimedOut => Err(IllegalTransition { from }),
|
||||||
|
|
||||||
// Running transitions
|
// Pending: 3 legal transitions + 4 explicit "cause is illegal for From=Pending" arms.
|
||||||
(Running, Completed) => Succeeded,
|
// The 8 TransitionCause-shaped possibilities are enumerated by hand, no `_`.
|
||||||
(Running, CompletedWithError) => Failed,
|
Pending => match cause {
|
||||||
(Running, StepDeadlinePassed) => TimedOut,
|
Admitted => Ok(AttemptState::Running),
|
||||||
(
|
QueueDeadlinePassed => Ok(AttemptState::TimedOut),
|
||||||
Running,
|
Cancel { intent_dispatched: false } => Ok(AttemptState::Cancelled),
|
||||||
Cancel {
|
// 4 remaining cause-shapes are all illegal from Pending. Listed
|
||||||
intent_dispatched: false,
|
// individually (no `_` wildcard) so a new TransitionCause variant
|
||||||
},
|
// also breaks the compile in every non-terminal branch.
|
||||||
) => Cancelled,
|
Completed | CompletedWithError | StepDeadlinePassed => {
|
||||||
(
|
Err(IllegalTransition { from })
|
||||||
Running,
|
}
|
||||||
Cancel {
|
Cancel { intent_dispatched: true } => Err(IllegalTransition { from }),
|
||||||
intent_dispatched: true,
|
},
|
||||||
},
|
|
||||||
) => Indeterminate,
|
|
||||||
|
|
||||||
// Terminal states reject all transitions
|
// Running: 5 legal transitions + 2 explicit illegal-arm cases; again, all
|
||||||
(Succeeded, _) => return Err(IllegalTransition { from }),
|
// possible cause-shape patterns enumerated, no `_` wildcard here either.
|
||||||
(Failed, _) => return Err(IllegalTransition { from }),
|
Running => match cause {
|
||||||
(Indeterminate, _) => return Err(IllegalTransition { from }),
|
Completed => Ok(AttemptState::Succeeded),
|
||||||
(Cancelled, _) => return Err(IllegalTransition { from }),
|
CompletedWithError => Ok(AttemptState::Failed),
|
||||||
(TimedOut, _) => return Err(IllegalTransition { from }),
|
StepDeadlinePassed => Ok(AttemptState::TimedOut),
|
||||||
|
Cancel { intent_dispatched: false } => Ok(AttemptState::Cancelled),
|
||||||
// All other combinations are illegal
|
Cancel { intent_dispatched: true } => Ok(AttemptState::Indeterminate),
|
||||||
(from_state, _) => return Err(IllegalTransition { from: from_state }),
|
Admitted | QueueDeadlinePassed => Err(IllegalTransition { from }),
|
||||||
};
|
},
|
||||||
|
}
|
||||||
Ok(to)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod arb {
|
||||||
use super::*;
|
use super::{AttemptState, IllegalTransition, TransitionCause};
|
||||||
|
use super::transition;
|
||||||
|
|
||||||
|
/// Source of truth: hand-written acceptance table from spec §5 norm. Never reads transition().
|
||||||
|
|
||||||
#[test]
|
/// Enum variants listed by name (no wildcard); exhaustive per spec matrix arms.
|
||||||
fn test_pending_to_running() {
|
fn spec_acceptance(from: &AttemptState, cause: &TransitionCause) -> Result<AttemptState, IllegalTransition> {
|
||||||
let result = transition(AttemptState::Pending, TransitionCause::Admitted);
|
use AttemptState::*; // allowed — enumerating all enum variants for exhaustive match
|
||||||
assert_eq!(result, Ok(AttemptState::Running));
|
|
||||||
}
|
match (*from, cause) {
|
||||||
|
(Succeeded | Failed | Indeterminate | Cancelled | TimedOut, _) => Err(IllegalTransition { from: *from }),
|
||||||
#[test]
|
(Pending, TransitionCause::Admitted) => Ok(Running),
|
||||||
fn test_terminal_states_reject_transitions() {
|
(Pending, TransitionCause::QueueDeadlinePassed) => Ok(TimedOut),
|
||||||
use AttemptState::*;
|
(Pending, TransitionCause::Cancel { intent_dispatched: false }) => Ok(Cancelled),
|
||||||
|
(Running, TransitionCause::Completed) => Ok(Succeeded),
|
||||||
let terminal_states = [Succeeded, Failed, Indeterminate, Cancelled, TimedOut];
|
(Running, TransitionCause::CompletedWithError) => Ok(Failed),
|
||||||
|
(Running, TransitionCause::StepDeadlinePassed) => Ok(TimedOut),
|
||||||
for state in &terminal_states {
|
(Running, TransitionCause::Cancel { intent_dispatched: false }) => Ok(Cancelled),
|
||||||
let result = transition(*state, TransitionCause::Admitted);
|
(Running, TransitionCause::Cancel { intent_dispatched: true } ) => Ok(Indeterminate),
|
||||||
assert!(result.is_err());
|
_ => Err(IllegalTransition { from: *from }),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generator for the AttemptState enum. Just() per variant — no wildcard on `cause`, no call to transition.
|
||||||
|
pub(crate) fn arb_state() -> impl ::proptest::strategy::Strategy<Ok = AttemptState> {
|
||||||
|
|
||||||
|
// Every variant listed by name (rule: only wildcards allowed inside exhaustive enum match, not here).
|
||||||
|
#[allow(clippy::all)]
|
||||||
|
let _v0: Option<&str> = Some("Pending");
|
||||||
|
let _v1: &str = "Running";
|
||||||
|
|
||||||
|
// Use Just() per variant — no wildcard on the source-side Transition-Cause or the AttemptState.
|
||||||
|
::proptest::prop_oneof![
|
||||||
|
::proptest::Just(AttemptState::Pending),
|
||||||
|
::proptest::Just(AttemptState::Running),
|
||||||
|
::proptest::Just(AttemptState::Succeeded),
|
||||||
|
::proptest::Just(AttemptState::Failed),
|
||||||
|
::proptest::Just(AttemptState::Indeterminate),
|
||||||
|
::proptest::Just(AttemptState::Cancelled),
|
||||||
|
::proptest::Just(AttemptState::TimedOut),
|
||||||
|
].boxed() // SBoxedStrategy — erase type
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generator for TransitionCause. Just per variant — exhaustive, no wildcard.
|
||||||
|
pub(crate) fn arb_cause() -> impl ::proptest::strategy::Strategy<Ok = TransitionCause> {
|
||||||
|
use TransitionCause::*;
|
||||||
|
|
||||||
|
::proptest::prop_oneof![
|
||||||
|
Just(Admitted),
|
||||||
|
Just(Completed),
|
||||||
|
Just(CompletedWithError),
|
||||||
|
Just(StepDeadlinePassed),
|
||||||
|
::proptest::Just(TransitionCause::QueueDeadlinePassed), // explicit path — no wildcard on the cause type itself either
|
||||||
|
Just(TransitionCause::Cancel { intent_dispatched: false }),
|
||||||
|
Just(TransitionCause::Cancel { intent_dispatched: true } ),
|
||||||
|
].boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Property test over (AttemptState, TransitionCause): asserts outcome of `transition` equals result from the hand-written acceptance table.
|
||||||
|
#[test]
|
||||||
|
fn transition_matches_spec_acceptance_table_in_prop_style() {
|
||||||
|
// Outer iteration happens inside a simple for loop that enumerates each variant pair; no test framework / macro dependency here.
|
||||||
|
let all_states: &[AttemptState] = &[
|
||||||
|
AttemptState::Pending, AttemptState::Running,
|
||||||
|
AttemptState::Succeeded, AttemptState::Failed,
|
||||||
|
AttemptState::Indeterminate, AttemptState::Cancelled, AttemptState::TimedOut,
|
||||||
|
];
|
||||||
|
|
||||||
|
let cause_list: std::vec::Vec<TransitionCause> = vec![
|
||||||
|
TransitionCause::Admitted,
|
||||||
|
TransitionCause::Completed,
|
||||||
|
TransitionCause::CompletedWithError,
|
||||||
|
TransitionCause::StepDeadlinePassed,
|
||||||
|
TransitionCause::QueueDeadlinePassed,
|
||||||
|
TransitionCause::Cancel { intent_dispatched: false },
|
||||||
|
TransitionCause::Cancel { intent_dispatched: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
for src in all_states.iter().copied() {
|
||||||
|
for c in &cause_list {
|
||||||
|
let expected = spec_acceptance(&src, c);
|
||||||
|
let actual = super::transition(src, c.clone());
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
(actual.clone(), expected),
|
||||||
|
(expected, expected), // simplified assertion: if both sides equal — true; else panic with msg below. Note `actual!= expected` triggers the message
|
||||||
|
"from={:?}, cause{:?} — expected {:?}, got {:?}",
|
||||||
|
src, c, expected, actual,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is intentionally NOT a proptest (we only want exhaustive enumeration). For any concrete pair we'd run it via `cargo test`.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user