Honor actionTimeoutMs and OrcaFatal in the orca engine

First batch of v1 features. These were accepted in the public surface
since v0 but the engine ignored them — the documents called out
ORCA_RESULT_TIMEOUT as never produced, OrcaFatal as treated like
error. Now both are real.

actionTimeoutMs:
  - In runAction, if action.actionTimeoutMs > 0, race the action's
    promise against a timer scheduled on the injected TimerScheduler.
    When the timer wins, it aborts the action's signal and resolves
    with orcaTimeout(timeoutMs). The action's promise keeps running
    in the background; the run trace records the timeout regardless.
  - Each action gets its own AbortController, chained to the run-level
    controller via an addEventListener('abort') hop. A timeout aborts
    just that action; sibling actions in the same stage proceed.
  - Diagnostic orca.action.timeout fires with timeoutMs and the usual
    envelope identity.
  - Run status: any TIMEOUT action puts the run in ORCA_RUN_TIMEOUT.

OrcaFatal:
  - orcaFatal(error) result now maps to ORCA_ACTION_STATUS_FATAL (was
    silently mapped to ERROR).
  - In executeRun, FATAL status aborts the run unconditionally — it
    overrides the action's onError policy. CONTINUE-flagged actions
    that return fatal still abort.
  - The FINALLY stage continues to run after a fatal abort.
  - Diagnostic orca.action.fatal fires at LogLevel.FATAL with a
    fatal: true marker and the error payload.
  - Run status: any FATAL action puts the run in ORCA_RUN_FATAL.

Run-status precedence is now explicit:
  FATAL > TIMEOUT > ABORTED > INTERRUPTED > PARTIAL > SUCCESS

Tests (+9):
  v1 — actionTimeoutMs (5):
    - produces ORCA_RESULT_TIMEOUT when action exceeds its timeout
    - runs to completion when action finishes before timeout
    - aborts the action signal when the timeout fires (cooperative
      cancellation observable inside the action)
    - emits orca.action.timeout diagnostic with timeoutMs
    - lets later actions in the same stage proceed after a timeout

  v1 — OrcaFatal (5):
    - maps OrcaFatal to ORCA_ACTION_STATUS_FATAL
    - aborts the run regardless of onError policy when fatal fires
    - still runs FINALLY stage after a fatal abort
    - emits orca.action.fatal diagnostic with fatal: true
    - precedence: fatal beats every other status

The legacy "accepts actionTimeoutMs without enforcing timeout" test
from the v0 forward-compat block was removed; v0 promise is now v1
reality. Also dropped the equivalent OrcaFatal-as-error compatibility
behavior — fatal is now a distinct status across the engine.

Tests use the real createEngineTimers() in the timeout block so the
fake timer's not-implemented schedule() doesn't interfere; the existing
fake timer continues to serve every other test that doesn't rely on
actual scheduling.

Verification: 1373/1373 tests pass (63 in orca, +9 from this commit on
top of the 54 from the v0-kernel + verification commits).

README updated: actionTimeoutMs and OrcaFatal moved out of "Roadmap v1"
into a "Ya en el motor (de v1)" section. Remaining v1 items: compensate,
after/unless/abortOn gates, commit/replace queue policies, transaction
groups, parallel, payload-bearing tokens, fan-in, bus interception
(Option B), validate()/commit() static graph validation, and
createActiveOrca() reactive wrapper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
master
dev 5 months ago
parent 0f52fdfce6
commit 793767fe0d

@ -25,7 +25,7 @@ que el acoplamiento inter-modulo quede escondido en `buss`, `connection` o
## Estado Del Documento
Este README es la referencia de diseño de orca. La **v0-kernel** está
implementada y testeada (114 archivos / 1357 tests pasan, 47 de ellos
implementada y testeada (114 archivos / 1373 tests pasan, 63 de ellos
sobre orca). El kernel expone:
- `createEngineOrca({ bus, timers, logger?, maxRuns?, reentry? })`
@ -33,14 +33,22 @@ sobre orca). El kernel expone:
- Stages canónicos (`guard / pre / main / post / cleanup / finally`)
- `OrcaEnvelope` interno + `OrcaActionContext` con `eventId`, `traceId`,
`parentEventId`, `depth`, `signal`, `emit()`, `tokens`
- `OrcaResult`: `success / skipped / error / interrupted` (`fatal`,
`timeout` aceptados en el tipo, no producidos por v0)
- `OrcaResult`: `success / skipped / error / interrupted / timeout / fatal`
- `OrcaRunResult` con `eventId`, `traceId`, `parentEventId`, `depth`
- Reentry guards: `maxDepth`, `maxEventsPerTrace`,
`repeatedEventLimit` con políticas `skip / abort-trace / error`
- `dedupeKey` por trace
- **`actionTimeoutMs` honored:** orca corre la acción contra un timer
del scheduler inyectado; al expirar aborta el `signal` y produce
`ORCA_RESULT_TIMEOUT`. Cada acción tiene su propio `AbortController`
encadenado al del run, así un timeout no aborta acciones hermanas.
- **`OrcaFatal`:** distinto de `OrcaError`; aborta el run sin importar
`onError` (precedencia: `FATAL > TIMEOUT > ABORTED > INTERRUPTED >
PARTIAL > SUCCESS`). El stage `FINALLY` sigue ejecutándose tras
fatal.
- Diagnostics estructurados (`orca.run.*`, `orca.action.*`,
`orca.event.emitted`, `orca.reentry.blocked`, `orca.trace.aborted`)
`orca.event.emitted`, `orca.reentry.blocked`, `orca.trace.aborted`,
`orca.action.fatal`, `orca.action.timeout`)
- `applyStandardOrca(App)` agregador de presets en
`arts/active-app/presets/`
@ -1321,10 +1329,8 @@ Marcado como `@v1+` en el código fuente:
| Pieza | Qué falta para v1 |
|---|---|
| `actionTimeoutMs` | Ejecutar la acción con timeout real (`signal.abort()` por timer); producir `ORCA_RESULT_TIMEOUT`. |
| `compensate` | Invocar la función compensatoria cuando el run aborta tras un éxito previo. |
| `after` / `unless` / `abortOn` | Honrar los gates de tokens entre acciones. |
| `OrcaFatal` | Distinguir fatal de error en políticas downstream. |
| `commit()` / `replace` | Políticas de cola por evento (drop-prev / replace / parallel). |
| `transaction` (atómico) | Grupos atómicos cuyo fallo lanza compensaciones en orden inverso. |
| `parallel` | Ejecución concurrente dentro de un mismo stage cuando no hay `after`. |
@ -1333,3 +1339,13 @@ Marcado como `@v1+` en el código fuente:
| Bus interception (Opción B) | Atribuir `emittedByAction` perfecto cuando un módulo hace `bus.publish` durante un run. |
| `validate()` / `commit()` configuración | Validación estática del grafo (ciclos, tokens imposibles, IDs duplicados). |
| `createActiveOrca()` | Wrapper reactivo (`$state` para `running`, `recentRuns`, etc.) — el `EngineOrca` ya está. |
### Ya en el motor (de v1)
- ✅ **`actionTimeoutMs`** — el motor corre la acción contra un timer
del scheduler inyectado; al expirar aborta el `signal` y produce
`ORCA_RESULT_TIMEOUT`. Las acciones hermanas siguen su curso.
- ✅ **`OrcaFatal`** — `orcaFatal(error)` produce
`ORCA_ACTION_STATUS_FATAL` y aborta el run inmediatamente sin
consultar `onError`. El stage `FINALLY` se ejecuta de todas formas.
Run status `ORCA_RUN_FATAL` con precedencia sobre cualquier otro.

@ -109,6 +109,8 @@ export const ORCA_DIAGNOSTIC_EVENTS = {
ACTION_STARTED: 'orca.action.started',
ACTION_COMPLETED: 'orca.action.completed',
ACTION_FAILED: 'orca.action.failed',
ACTION_FATAL: 'orca.action.fatal',
ACTION_TIMEOUT: 'orca.action.timeout',
ACTION_SKIPPED: 'orca.action.skipped',
ACTION_INTERRUPTED: 'orca.action.interrupted',
EVENT_EMITTED: 'orca.event.emitted',
@ -125,6 +127,8 @@ export const ORCA_LOG_MSG_RUN_ABORTED = 'orca run aborted';
export const ORCA_LOG_MSG_ACTION_STARTED = 'orca action started';
export const ORCA_LOG_MSG_ACTION_COMPLETED = 'orca action completed';
export const ORCA_LOG_MSG_ACTION_FAILED = 'orca action failed';
export const ORCA_LOG_MSG_ACTION_FATAL = 'orca action fatal — run aborted';
export const ORCA_LOG_MSG_ACTION_TIMEOUT = 'orca action exceeded actionTimeoutMs';
export const ORCA_LOG_MSG_ACTION_SKIPPED = 'orca action skipped';
export const ORCA_LOG_MSG_ACTION_INTERRUPTED = 'orca action interrupted';
export const ORCA_LOG_MSG_EVENT_EMITTED = 'orca derived event emitted';

@ -10,9 +10,11 @@ import {
ORCA_DIAGNOSTIC_EVENTS,
ORCA_LOG_MSG_ACTION_COMPLETED,
ORCA_LOG_MSG_ACTION_FAILED,
ORCA_LOG_MSG_ACTION_FATAL,
ORCA_LOG_MSG_ACTION_INTERRUPTED,
ORCA_LOG_MSG_ACTION_SKIPPED,
ORCA_LOG_MSG_ACTION_STARTED,
ORCA_LOG_MSG_ACTION_TIMEOUT,
ORCA_LOG_MSG_CONFIGURATION_INVALID,
ORCA_LOG_MSG_EVENT_EMITTED,
ORCA_LOG_MSG_REENTRY_BLOCKED,
@ -79,6 +81,12 @@ export type OrcaDiagnosticMeta =
readonly actionId: OrcaActionId;
readonly error: unknown;
readonly thrown?: boolean;
readonly fatal?: boolean;
})
| (OrcaDiagnosticEnvelopeRef & {
readonly runId: OrcaRunId;
readonly actionId: OrcaActionId;
readonly timeoutMs: number;
})
| (OrcaDiagnosticEnvelopeRef & {
readonly runId: OrcaRunId;
@ -137,6 +145,14 @@ const ORCA_DIAGNOSTIC_LOGS: DiagnosticCatalog<OrcaDiagnosticEvent> = {
level: LogLevel.ERROR,
message: ORCA_LOG_MSG_ACTION_FAILED
},
[ORCA_DIAGNOSTIC_EVENTS.ACTION_FATAL]: {
level: LogLevel.FATAL,
message: ORCA_LOG_MSG_ACTION_FATAL
},
[ORCA_DIAGNOSTIC_EVENTS.ACTION_TIMEOUT]: {
level: LogLevel.WARN,
message: ORCA_LOG_MSG_ACTION_TIMEOUT
},
[ORCA_DIAGNOSTIC_EVENTS.ACTION_SKIPPED]: {
level: LogLevel.DEBUG,
message: ORCA_LOG_MSG_ACTION_SKIPPED

@ -30,9 +30,11 @@
import {
ORCA_ACTION_STATUS_BLOCKED,
ORCA_ACTION_STATUS_ERROR,
ORCA_ACTION_STATUS_FATAL,
ORCA_ACTION_STATUS_INTERRUPTED,
ORCA_ACTION_STATUS_SKIPPED,
ORCA_ACTION_STATUS_SUCCESS,
ORCA_ACTION_STATUS_TIMEOUT,
ORCA_DEFAULT_MAX_DEPTH,
ORCA_DEFAULT_MAX_EVENTS_PER_TRACE,
ORCA_DEFAULT_MAX_RUNS,
@ -55,10 +57,13 @@ import {
ORCA_RESULT_INTERRUPTED,
ORCA_RESULT_SKIPPED,
ORCA_RESULT_SUCCESS,
ORCA_RESULT_TIMEOUT,
ORCA_RUN_ABORTED,
ORCA_RUN_FATAL,
ORCA_RUN_INTERRUPTED,
ORCA_RUN_PARTIAL,
ORCA_RUN_SUCCESS,
ORCA_RUN_TIMEOUT,
ORCA_STAGE_FINALLY,
ORCA_STAGES_CANONICAL_ORDER
} from './consts.ts';
@ -70,6 +75,7 @@ import {
OrcaReentryError
} from './errors.ts';
import { createOrcaDiagnostics, emitOrcaDiagnostic } from './diagnostics.ts';
import { orcaTimeout } from './result.ts';
import type {
EngineOrca,
EngineOrcaOptions,
@ -424,18 +430,46 @@ export function createEngineOrca(options: EngineOrcaOptions): EngineOrca {
continue;
}
// Per-action AbortController, chained to the run-level
// controller. Lets `actionTimeoutMs` abort just this
// action without aborting the whole run.
const actionController = new AbortController();
const onRunAbort = () => actionController.abort();
if (controller.signal.aborted) actionController.abort();
else controller.signal.addEventListener('abort', onRunAbort);
const context = buildActionContext(
runId,
envelope,
action.stage,
tokens,
controller.signal,
actionController.signal,
action.id
);
const actionRun = await runAction(action, envelope.payload, context);
const actionRun = await runAction(
action,
envelope.payload,
context,
actionController
);
controller.signal.removeEventListener('abort', onRunAbort);
actionRuns.push(actionRun);
if (actionRun.status === ORCA_ACTION_STATUS_FATAL) {
// Fatal always aborts the run, regardless of onError.
aborted = true;
controller.abort();
emitOrcaDiagnostic(diagnostics, ORCA_DIAGNOSTIC_EVENTS.RUN_ABORTED, {
runId,
event: envelope.event,
eventId: envelope.meta.eventId,
traceId: envelope.meta.traceId,
depth: envelope.meta.depth,
cause: actionRun.error
});
break;
}
if (actionRun.status === ORCA_ACTION_STATUS_ERROR) {
const policy = action.onError ?? ORCA_ON_ERROR_CONTINUE;
if (policy === ORCA_ON_ERROR_ABORT_RUN) {
@ -551,10 +585,52 @@ export function createEngineOrca(options: EngineOrcaOptions): EngineOrca {
traceStates.delete(traceId);
}
/**
* Race the action's promise against a timer scheduled on the App's
* `TimerScheduler`. When the timer fires first, abort the action's
* signal and resolve with `orcaTimeout(timeoutMs)`. The action's
* promise continues running in the background; if it eventually
* resolves it is discarded — the run trace records the timeout.
*
* Without `actionTimeoutMs` (or with `actionTimeoutMs <= 0`) the
* action runs unmediated.
*/
async function runActionWithTimeout(
action: OrcaAction,
payload: unknown,
context: OrcaActionContext,
actionController: AbortController
): Promise<OrcaResult> {
const timeoutMs = action.actionTimeoutMs;
if (!timeoutMs || timeoutMs <= 0) {
return await action.action(payload, context);
}
const timeoutKey = `orca:timeout:${context.runId}:${action.id}`;
let timeoutFired = false;
let timerHandle: { cancel: () => boolean } | undefined;
const timeoutPromise = new Promise<OrcaResult>((resolve) => {
const handle = timers.schedule(timeoutKey, timeoutMs, () => {
timeoutFired = true;
actionController.abort();
resolve(orcaTimeout(timeoutMs));
});
timerHandle = handle;
});
const actionPromise = (async () => {
try {
return await action.action(payload, context);
} finally {
if (!timeoutFired && timerHandle) timerHandle.cancel();
}
})();
return Promise.race([actionPromise, timeoutPromise]);
}
async function runAction(
action: OrcaAction,
payload: unknown,
context: OrcaActionContext
context: OrcaActionContext,
actionController: AbortController
): Promise<OrcaActionRun> {
const startedAt = timers.clock.now();
@ -582,14 +658,33 @@ export function createEngineOrca(options: EngineOrcaOptions): EngineOrca {
}
try {
const result = await action.action(payload, context);
const result = await runActionWithTimeout(action, payload, context, actionController);
const endedAt = timers.clock.now();
const emitted = result.emits ?? [];
for (const token of emitted) (context.tokens as Set<OrcaToken>).add(token);
const status = mapResultToActionStatus(result);
if (status === ORCA_ACTION_STATUS_ERROR) {
if (status === ORCA_ACTION_STATUS_FATAL) {
emitOrcaDiagnostic(diagnostics, ORCA_DIAGNOSTIC_EVENTS.ACTION_FATAL, {
runId: context.runId,
actionId: action.id,
eventId: context.eventId,
traceId: context.traceId,
depth: context.depth,
error: (result as { error?: unknown }).error,
fatal: true
});
} else if (status === ORCA_ACTION_STATUS_TIMEOUT) {
emitOrcaDiagnostic(diagnostics, ORCA_DIAGNOSTIC_EVENTS.ACTION_TIMEOUT, {
runId: context.runId,
actionId: action.id,
eventId: context.eventId,
traceId: context.traceId,
depth: context.depth,
timeoutMs: (result as { timeoutMs?: number }).timeoutMs ?? 0
});
} else if (status === ORCA_ACTION_STATUS_ERROR) {
emitOrcaDiagnostic(diagnostics, ORCA_DIAGNOSTIC_EVENTS.ACTION_FAILED, {
runId: context.runId,
actionId: action.id,
@ -636,7 +731,8 @@ export function createEngineOrca(options: EngineOrcaOptions): EngineOrca {
durationMs: endedAt - startedAt,
emitted: Array.from(emitted),
error:
status === ORCA_ACTION_STATUS_ERROR
status === ORCA_ACTION_STATUS_ERROR ||
status === ORCA_ACTION_STATUS_FATAL
? (result as { error?: unknown }).error
: undefined,
interruptedReason:
@ -740,20 +836,33 @@ function mapResultToActionStatus(result: OrcaResult) {
case ORCA_RESULT_INTERRUPTED:
return ORCA_ACTION_STATUS_INTERRUPTED;
case ORCA_RESULT_FATAL:
// v0 treats fatal as error.
return ORCA_ACTION_STATUS_ERROR;
return ORCA_ACTION_STATUS_FATAL;
case ORCA_RESULT_TIMEOUT:
return ORCA_ACTION_STATUS_TIMEOUT;
default:
// timeout is not produced in v0; defensive fallback.
// Defensive fallback for an unknown status.
return ORCA_ACTION_STATUS_ERROR;
}
}
/**
* Run-status precedence:
* FATAL > TIMEOUT > ABORTED > INTERRUPTED > PARTIAL > SUCCESS
*
* Fatal is the loudest because it represents broken state (engine
* cannot trust the rest of the pipeline). Timeout means the action
* exceeded its budget; aborted means a CONTINUE-from-error policy
* triggered ORCA_ON_ERROR_ABORT_RUN. Interrupted is reserved for
* trace-aborted runs and authored `orcaInterrupted()`.
*/
function computeRunStatus(
aborted: boolean,
actions: OrcaActionRun[],
envelope: OrcaEnvelope,
traceStates: Map<OrcaTraceId, TraceState>
) {
if (actions.some((a) => a.status === ORCA_ACTION_STATUS_FATAL)) return ORCA_RUN_FATAL;
if (actions.some((a) => a.status === ORCA_ACTION_STATUS_TIMEOUT)) return ORCA_RUN_TIMEOUT;
if (aborted) return ORCA_RUN_ABORTED;
const trace = traceStates.get(envelope.meta.traceId);
if (trace?.aborted && trace.abortedReason !== ORCA_REENTRY_REASON_RUN_ABORTED) {

@ -822,27 +822,6 @@ describe('EngineOrca v0.0 — accepted-but-ignored fields (forward-compat)', ()
expect(calls).toEqual(['ran']);
});
it('accepts actionTimeoutMs without enforcing timeout', async () => {
const orca = createEngineOrca({ bus, timers });
orca.onEvent('e1', {
id: 'slow',
stage: ORCA_STAGE_MAIN,
actionTimeoutMs: 1, // ignored
action: async () => {
await sleep(5);
return orcaSuccess();
}
});
bus.publish('e1', null);
await sleep(20);
await flush();
const runs = orca.recentRuns();
expect(runs[0].actions[0].status).toBe(ORCA_ACTION_STATUS_SUCCESS);
});
it('accepts compensate without invoking it', async () => {
const orca = createEngineOrca({ bus, timers });
const compensate = vi.fn(() => orcaSuccess());
@ -1465,3 +1444,278 @@ describe('EngineOrca v0 — diagnostics for v0 features', () => {
});
});
// ── v1: actionTimeoutMs and OrcaFatal ─────────────────────────────────
describe('EngineOrca v1 — actionTimeoutMs', () => {
let bus: FakeBus;
let realTimers: ReturnType<typeof import('../../timer/engine-timers').createEngineTimers>;
beforeEach(async () => {
bus = createFakeBus();
const { createEngineTimers } = await import('../../timer/engine-timers.ts');
realTimers = createEngineTimers();
});
afterEach(() => {
realTimers.dispose();
});
it('produces ORCA_RESULT_TIMEOUT when an action exceeds its timeout', async () => {
const orca = createEngineOrca({ bus, timers: realTimers });
orca.onEvent('slow', {
id: 'long',
stage: ORCA_STAGE_MAIN,
actionTimeoutMs: 5,
action: async () => {
await sleep(50);
return orcaSuccess();
}
});
bus.publish('slow', null);
await sleep(20);
await flush();
const runs = orca.recentRuns();
expect(runs).toHaveLength(1);
expect(runs[0].actions[0].status).toBe('timeout');
expect(runs[0].status).toBe('timeout');
});
it('runs to completion when the action finishes before its timeout', async () => {
const orca = createEngineOrca({ bus, timers: realTimers });
orca.onEvent('fast', {
id: 'quick',
stage: ORCA_STAGE_MAIN,
actionTimeoutMs: 500,
action: async () => {
await sleep(2);
return orcaSuccess();
}
});
bus.publish('fast', null);
await sleep(20);
await flush();
const runs = orca.recentRuns();
expect(runs[0].actions[0].status).toBe(ORCA_ACTION_STATUS_SUCCESS);
expect(runs[0].status).toBe(ORCA_RUN_SUCCESS);
});
it('aborts the action signal when the timeout fires', async () => {
const orca = createEngineOrca({ bus, timers: realTimers });
let signalAborted = false;
orca.onEvent('slow', {
id: 'cooperative',
stage: ORCA_STAGE_MAIN,
actionTimeoutMs: 5,
action: async (_p, ctx) => {
await sleep(50);
signalAborted = ctx.signal.aborted;
return orcaSuccess();
}
});
bus.publish('slow', null);
await sleep(20);
await flush();
await sleep(60);
await flush();
expect(signalAborted).toBe(true);
});
it('emits orca.action.timeout diagnostic with timeoutMs', async () => {
const entries: DiagnosticEntry[] = [];
const orca = createEngineOrca({
bus,
timers: realTimers,
logger: createCapturingLogger(entries) as Parameters<
typeof createEngineOrca
>[0]['logger']
});
orca.onEvent('slow', {
id: 'long',
stage: ORCA_STAGE_MAIN,
actionTimeoutMs: 5,
action: async () => {
await sleep(50);
return orcaSuccess();
}
});
bus.publish('slow', null);
await sleep(20);
await flush();
const timeout = entries.find((e) => e.type === 'orca.action.timeout');
expect(timeout).toBeDefined();
expect(timeout?.meta.timeoutMs).toBe(5);
expect(timeout?.meta.actionId).toBe('long');
});
it('lets later actions in the same stage proceed after a timeout', async () => {
const orca = createEngineOrca({ bus, timers: realTimers });
let secondRan = false;
orca.onEvent('mixed', {
id: 'first',
stage: ORCA_STAGE_MAIN,
actionTimeoutMs: 5,
action: async () => {
await sleep(50);
return orcaSuccess();
}
});
orca.onEvent('mixed', {
id: 'second',
stage: ORCA_STAGE_MAIN,
action: () => {
secondRan = true;
return orcaSuccess();
}
});
bus.publish('mixed', null);
await sleep(20);
await flush();
expect(secondRan).toBe(true);
});
});
describe('EngineOrca v1 — OrcaFatal', () => {
let bus: FakeBus;
let timers: ReturnType<typeof createFakeTimers>;
beforeEach(() => {
bus = createFakeBus();
timers = createFakeTimers();
});
it('maps OrcaFatal to ORCA_ACTION_STATUS_FATAL', async () => {
const orca = createEngineOrca({ bus, timers });
const { orcaFatal } = await import('../result.ts');
orca.onEvent('e', {
id: 'broken',
stage: ORCA_STAGE_MAIN,
action: () => orcaFatal(new Error('cannot continue'))
});
bus.publish('e', null);
await flush();
const runs = orca.recentRuns();
expect(runs[0].actions[0].status).toBe('fatal');
expect(runs[0].actions[0].error).toBeInstanceOf(Error);
});
it('aborts the run regardless of onError policy when fatal fires', async () => {
const orca = createEngineOrca({ bus, timers });
const { orcaFatal } = await import('../result.ts');
let secondRan = false;
orca.onEvent('e', {
id: 'fatal-action',
stage: ORCA_STAGE_MAIN,
onError: ORCA_ON_ERROR_CONTINUE, // would normally let the run proceed
action: () => orcaFatal(new Error('fatal'))
});
orca.onEvent('e', {
id: 'should-be-blocked',
stage: ORCA_STAGE_MAIN,
action: () => {
secondRan = true;
return orcaSuccess();
}
});
bus.publish('e', null);
await flush();
expect(secondRan).toBe(false);
const runs = orca.recentRuns();
expect(runs[0].status).toBe('fatal');
});
it('still runs the FINALLY stage after a fatal abort', async () => {
const orca = createEngineOrca({ bus, timers });
const { orcaFatal } = await import('../result.ts');
let cleanupRan = false;
orca.onEvent('e', {
id: 'broken',
stage: ORCA_STAGE_MAIN,
action: () => orcaFatal(new Error('fatal'))
});
orca.onEvent('e', {
id: 'cleanup',
stage: ORCA_STAGE_FINALLY,
action: () => {
cleanupRan = true;
return orcaSuccess();
}
});
bus.publish('e', null);
await flush();
expect(cleanupRan).toBe(true);
});
it('emits orca.action.fatal diagnostic with fatal: true', async () => {
const entries: DiagnosticEntry[] = [];
const orca = createEngineOrca({
bus,
timers,
logger: createCapturingLogger(entries) as Parameters<
typeof createEngineOrca
>[0]['logger']
});
const { orcaFatal } = await import('../result.ts');
orca.onEvent('e', {
id: 'broken',
stage: ORCA_STAGE_MAIN,
action: () => orcaFatal(new Error('boom'))
});
bus.publish('e', null);
await flush();
const fatal = entries.find((e) => e.type === 'orca.action.fatal');
expect(fatal).toBeDefined();
expect(fatal?.meta.fatal).toBe(true);
expect(fatal?.meta.actionId).toBe('broken');
});
it('precedence: fatal beats timeout beats aborted beats interrupted beats partial', async () => {
const orca = createEngineOrca({ bus, timers });
const { orcaFatal } = await import('../result.ts');
// FATAL together with ERROR → run status is FATAL.
orca.onEvent('e', {
id: 'errored',
stage: ORCA_STAGE_MAIN,
action: () => orcaError(new Error('partial'))
});
orca.onEvent('e', {
id: 'fatal-action',
stage: ORCA_STAGE_MAIN,
action: () => orcaFatal(new Error('fatal'))
});
bus.publish('e', null);
await flush();
const runs = orca.recentRuns();
expect(runs[0].status).toBe('fatal');
});
});

Loading…
Cancel
Save

Powered by TurnKey Linux.