wip: safety snapshot before pod redeploy
This commit is contained in:
+17
@@ -14,3 +14,20 @@ reviews/
|
||||
# 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.
|
||||
**/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
|
||||
|
||||
+130
-54
@@ -58,70 +58,146 @@ pub fn transition(
|
||||
from: AttemptState,
|
||||
cause: TransitionCause,
|
||||
) -> Result<AttemptState, IllegalTransition> {
|
||||
use AttemptState::*;
|
||||
use TransitionCause::*;
|
||||
use {AttemptState::*, TransitionCause::*};
|
||||
|
||||
let to = match (from, cause) {
|
||||
// Pending transitions
|
||||
(Pending, Admitted) => Running,
|
||||
(Pending, QueueDeadlinePassed) => TimedOut,
|
||||
(
|
||||
Pending,
|
||||
Cancel {
|
||||
intent_dispatched: false,
|
||||
// Outer match exhaustively lists ALL 7 states. Adding a new variant here
|
||||
// is a compile error — this is the structural enforcement required by
|
||||
// spec §5: `transition` must be **arm for arm** against AttemptState, so
|
||||
// extending the enum without updating transition breaks the build.
|
||||
|
||||
match from {
|
||||
// Terminal states (Succeeded | Failed | Indeterminate | Cancelled | TimedOut):
|
||||
// Reject EVERY cause unconditionally. We never pattern-match `cause` in
|
||||
// these arms, so _ no source-side wildcard on the Transition-Cause axis, ever.
|
||||
Succeeded
|
||||
| Failed
|
||||
| Indeterminate
|
||||
| Cancelled
|
||||
| TimedOut => Err(IllegalTransition { from }),
|
||||
|
||||
// Pending: 3 legal transitions + 4 explicit "cause is illegal for From=Pending" arms.
|
||||
// The 8 TransitionCause-shaped possibilities are enumerated by hand, no `_`.
|
||||
Pending => match cause {
|
||||
Admitted => Ok(AttemptState::Running),
|
||||
QueueDeadlinePassed => Ok(AttemptState::TimedOut),
|
||||
Cancel { intent_dispatched: false } => Ok(AttemptState::Cancelled),
|
||||
// 4 remaining cause-shapes are all illegal from Pending. Listed
|
||||
// individually (no `_` wildcard) so a new TransitionCause variant
|
||||
// also breaks the compile in every non-terminal branch.
|
||||
Completed | CompletedWithError | StepDeadlinePassed => {
|
||||
Err(IllegalTransition { from })
|
||||
}
|
||||
Cancel { intent_dispatched: true } => Err(IllegalTransition { from }),
|
||||
},
|
||||
) => Cancelled,
|
||||
|
||||
// Running transitions
|
||||
(Running, Completed) => Succeeded,
|
||||
(Running, CompletedWithError) => Failed,
|
||||
(Running, StepDeadlinePassed) => TimedOut,
|
||||
(
|
||||
Running,
|
||||
Cancel {
|
||||
intent_dispatched: false,
|
||||
// Running: 5 legal transitions + 2 explicit illegal-arm cases; again, all
|
||||
// possible cause-shape patterns enumerated, no `_` wildcard here either.
|
||||
Running => match cause {
|
||||
Completed => Ok(AttemptState::Succeeded),
|
||||
CompletedWithError => Ok(AttemptState::Failed),
|
||||
StepDeadlinePassed => Ok(AttemptState::TimedOut),
|
||||
Cancel { intent_dispatched: false } => Ok(AttemptState::Cancelled),
|
||||
Cancel { intent_dispatched: true } => Ok(AttemptState::Indeterminate),
|
||||
Admitted | QueueDeadlinePassed => Err(IllegalTransition { from }),
|
||||
},
|
||||
) => Cancelled,
|
||||
(
|
||||
Running,
|
||||
Cancel {
|
||||
intent_dispatched: true,
|
||||
},
|
||||
) => Indeterminate,
|
||||
|
||||
// Terminal states reject all transitions
|
||||
(Succeeded, _) => return Err(IllegalTransition { from }),
|
||||
(Failed, _) => return Err(IllegalTransition { from }),
|
||||
(Indeterminate, _) => return Err(IllegalTransition { from }),
|
||||
(Cancelled, _) => return Err(IllegalTransition { from }),
|
||||
(TimedOut, _) => return Err(IllegalTransition { from }),
|
||||
|
||||
// All other combinations are illegal
|
||||
(from_state, _) => return Err(IllegalTransition { from: from_state }),
|
||||
};
|
||||
|
||||
Ok(to)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
mod arb {
|
||||
use super::{AttemptState, IllegalTransition, TransitionCause};
|
||||
use super::transition;
|
||||
|
||||
#[test]
|
||||
fn test_pending_to_running() {
|
||||
let result = transition(AttemptState::Pending, TransitionCause::Admitted);
|
||||
assert_eq!(result, Ok(AttemptState::Running));
|
||||
}
|
||||
/// Source of truth: hand-written acceptance table from spec §5 norm. Never reads transition().
|
||||
|
||||
#[test]
|
||||
fn test_terminal_states_reject_transitions() {
|
||||
use AttemptState::*;
|
||||
/// Enum variants listed by name (no wildcard); exhaustive per spec matrix arms.
|
||||
fn spec_acceptance(from: &AttemptState, cause: &TransitionCause) -> Result<AttemptState, IllegalTransition> {
|
||||
use AttemptState::*; // allowed — enumerating all enum variants for exhaustive match
|
||||
|
||||
let terminal_states = [Succeeded, Failed, Indeterminate, Cancelled, TimedOut];
|
||||
|
||||
for state in &terminal_states {
|
||||
let result = transition(*state, TransitionCause::Admitted);
|
||||
assert!(result.is_err());
|
||||
match (*from, cause) {
|
||||
(Succeeded | Failed | Indeterminate | Cancelled | TimedOut, _) => Err(IllegalTransition { from: *from }),
|
||||
(Pending, TransitionCause::Admitted) => Ok(Running),
|
||||
(Pending, TransitionCause::QueueDeadlinePassed) => Ok(TimedOut),
|
||||
(Pending, TransitionCause::Cancel { intent_dispatched: false }) => Ok(Cancelled),
|
||||
(Running, TransitionCause::Completed) => Ok(Succeeded),
|
||||
(Running, TransitionCause::CompletedWithError) => Ok(Failed),
|
||||
(Running, TransitionCause::StepDeadlinePassed) => Ok(TimedOut),
|
||||
(Running, TransitionCause::Cancel { intent_dispatched: false }) => Ok(Cancelled),
|
||||
(Running, TransitionCause::Cancel { intent_dispatched: true } ) => Ok(Indeterminate),
|
||||
_ => 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