ruvnet/ruflo · error

Invalid state transition from '${from}' to '${to}'. Allowed

Error message

Invalid state transition from '${from}' to '${to}'. Allowed transitions from '${from}': ${allowed?.join(', ') ?? 'none'}

What it means

assertValidStateTransition() checks that to is listed in allowedTransitions[from]; it throws when from has no entry at all or to is not permitted, printing the allowed set or none. It encodes a state-machine table in tests, so a failure means the code under test attempted (or was claimed to attempt) an illegal transition.

Source

Thrown at v3/@claude-flow/testing/src/helpers/assertion-helpers.ts:487

/**
 * Assert state transition is valid
 *
 * @example
 * assertValidStateTransition(
 *   'pending',
 *   'running',
 *   { pending: ['running', 'cancelled'], running: ['completed', 'failed'] }
 * );
 */
export function assertValidStateTransition<T extends string>(
  from: T,
  to: T,
  allowedTransitions: Record<T, T[]>
): void {
  const allowed = allowedTransitions[from];

  if (!allowed || !allowed.includes(to)) {
    throw new Error(
      `Invalid state transition from '${from}' to '${to}'. ` +
      `Allowed transitions from '${from}': ${allowed?.join(', ') ?? 'none'}`
    );
  }
}

/**
 * Assert that a retry policy was followed
 *
 * @example
 * assertRetryPattern(mockFn, { attempts: 3, backoffPattern: 'exponential' });
 */
export function assertRetryPattern(
  mock: Mock,
  options: RetryPatternOptions
): void {
  const calls = mock.mock.calls;

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Compare the thrown allowed list against the attempted transition; either the code took a bad path or the table is stale
  2. Add the missing state key or transition pair to allowedTransitions if the transition is now legal
  3. Derive the test table from the same constant the production state machine uses

Example fix

// before
assertValidStateTransition('pending', 'completed', TRANSITIONS); // throws

// after
assertValidStateTransition('pending', 'running', TRANSITIONS);
// or, if skipping is now legal: TRANSITIONS.pending.push('completed')
Defensive patterns

Strategy: type-guard

Validate before calling

function isAllowedTransition<T extends string>(from: T, to: T, table: Record<T, T[]>): boolean {
  return table[from]?.includes(to) ?? false;
}

if (!isAllowedTransition(from, to, TRANSITIONS)) {
  throw new Error(`Refusing illegal transition ${from} -> ${to}`);
}

Type guard

function isAllowedTransition<T extends string>(from: T, to: T, table: Record<T, T[]>): to is T {
  return table[from]?.includes(to) ?? false;
}

Prevention

When it happens

Trigger: Asserting pending to completed when the table only allows pending to running or cancelled; a from value that is not a key in the table (reads as no transitions); enum string mismatches such as Running vs running.

Common situations: State machine extended with new states but the test table not updated; transition maps copied and drifting from the production machine; renames or casing changes after refactors.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/13542faab988702d. Report an issue: GitHub.