837ff4217b
CI / lint (pull_request) Successful in 15s
CI / benchmark-publish (pull_request) Has been skipped
CI / quality (pull_request) Successful in 18s
CI / build (pull_request) Successful in 14s
CI / typecheck (pull_request) Successful in 38s
CI / security (pull_request) Successful in 46s
CI / integration_tests (pull_request) Successful in 3m3s
CI / unit_tests (pull_request) Successful in 4m39s
CI / coverage (pull_request) Successful in 5m8s
CI / docker (pull_request) Successful in 55s
CI / lint (push) Successful in 12s
CI / quality (push) Successful in 16s
CI / security (push) Successful in 30s
CI / typecheck (push) Successful in 39s
CI / build (push) Successful in 15s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 2m20s
CI / docker (push) Successful in 39s
CI / integration_tests (push) Successful in 3m2s
CI / coverage (push) Successful in 4m37s
CI / benchmark-regression (pull_request) Failing after 16m37s
CI / benchmark-publish (push) Failing after 9m16s
- Add AsyncJob domain model with status state machine and Pydantic validation
- Add AsyncWorker service with configurable concurrency and job store
- Add CancellationToken, WorkerHealthReport, InMemoryJobStore
- Add AsyncJobModel SQLAlchemy model and Alembic migration (m6_003)
- Add 5 async config keys to Settings (worker_id, concurrency, poll_interval, max_retries, timeout)
- Add _check_async_worker_health diagnostic check in system.py
- Add comprehensive Behave BDD tests (~60 scenarios) with full step definitions
- Add Robot Framework integration tests (6 smoke tests)
- Add ASV benchmark suite for async execution
- Add architecture documentation
- Update vulture_whitelist with new public API symbols
- All quality gates pass: lint, typecheck, unit_tests, integration_tests, coverage_report (97%)
1. Wire async job creation into PlanLifecycleService:
- Add optional job_store parameter to __init__
- Add _maybe_enqueue_async_job() helper that checks settings.async_enabled
and job store presence before creating and enqueuing an AsyncJob
- Call helper from execute_plan() (phase="execute") and apply_plan()
(phase="apply") after phase transitions
- When async is disabled or no job store is configured, behaviour is
unchanged (silent no-op)
2. Redact secrets in failed job error messages:
- Apply shared.redaction.redact_value() to the error string before
persisting to AsyncJob.error_message, preventing accidental secret
leakage (e.g. API keys in exception text) into the audit trail
Documentation:
- S1: Added specification reconciliation note (ADR-style) to
async_architecture.md addressing tension between "No Plan Queuing"
clause and the async subsystem authorised by issue #312
ISSUES CLOSED: #312
556 lines
23 KiB
Gherkin
556 lines
23 KiB
Gherkin
Feature: Async Job Execution and Workers
|
|
As a developer
|
|
I want plan phases to execute as background jobs
|
|
So that long-running operations don't block the CLI
|
|
|
|
# ---- AsyncJob Domain Model ----
|
|
|
|
Scenario: Create an async job with default values
|
|
Given an async job store is initialised
|
|
And I create a new async job for plan "01HXYZ01HXYZ01HXYZ01HXYZ01" phase "execute"
|
|
Then the async job status should be "queued"
|
|
And the async job phase should be "execute"
|
|
And the async job schema_version should be 1
|
|
|
|
Scenario: AsyncJob status transitions follow the state machine
|
|
Given an async job store is initialised
|
|
And I create a new async job for plan "01HXYZ01HXYZ01HXYZ01HXYZ01" phase "execute"
|
|
When I mark the async job as running with worker "worker-001"
|
|
Then the async job status should be "running"
|
|
And the async job worker_id should be "worker-001"
|
|
And the async job started_at should be set
|
|
|
|
Scenario: AsyncJob transition from running to succeeded
|
|
Given an async job store is initialised
|
|
And I create a running async job
|
|
When I mark the async job as succeeded
|
|
Then the async job status should be "succeeded"
|
|
And the async job finished_at should be set
|
|
|
|
Scenario: AsyncJob transition from running to failed
|
|
Given an async job store is initialised
|
|
And I create a running async job
|
|
When I mark the async job as failed
|
|
Then the async job status should be "failed"
|
|
And the async job finished_at should be set
|
|
|
|
Scenario: AsyncJob transition from queued to cancelled
|
|
Given an async job store is initialised
|
|
And I create a new async job for plan "01HXYZ01HXYZ01HXYZ01HXYZ01" phase "execute"
|
|
When I mark the async job as cancelled
|
|
Then the async job status should be "cancelled"
|
|
And the async job finished_at should be set
|
|
|
|
Scenario: AsyncJob transition from running to cancelled
|
|
Given an async job store is initialised
|
|
And I create a running async job
|
|
When I mark the async job as cancelled
|
|
Then the async job status should be "cancelled"
|
|
|
|
Scenario: Invalid transition from queued to succeeded raises error
|
|
Given an async job store is initialised
|
|
And I create a new async job for plan "01HXYZ01HXYZ01HXYZ01HXYZ01" phase "execute"
|
|
When I try to mark the async job as succeeded
|
|
Then an InvalidJobTransitionError should be raised
|
|
|
|
Scenario: Invalid transition from succeeded to running raises error
|
|
Given an async job store is initialised
|
|
And I create a running async job
|
|
And I mark the async job as succeeded
|
|
When I try to mark the async job as running with worker "worker-002"
|
|
Then an InvalidJobTransitionError should be raised
|
|
|
|
Scenario: Invalid transition from failed raises error
|
|
Given an async job store is initialised
|
|
And I create a running async job
|
|
And I mark the async job as failed
|
|
When I try to mark the async job as succeeded
|
|
Then an InvalidJobTransitionError should be raised
|
|
|
|
# ---- Worker Pickup and Execution ----
|
|
|
|
Scenario: Worker picks up and executes a queued job
|
|
Given an async worker is initialised with max_workers 2
|
|
And a queued async job exists
|
|
When the worker picks up and executes the job
|
|
Then the async job status should be "succeeded"
|
|
And the worker health should show 1 job processed
|
|
|
|
Scenario: Worker marks job as failed on execution error
|
|
Given an async worker is initialised with a failing executor
|
|
And a queued async job exists
|
|
When the worker picks up and executes the job
|
|
Then the async job status should be "failed"
|
|
And the worker health should show 1 job failed
|
|
|
|
# ---- Job Cancellation ----
|
|
|
|
Scenario: Cancel a running job via cancellation token
|
|
Given an async worker is initialised with max_workers 2
|
|
And a queued async job exists
|
|
When I cancel the async job
|
|
Then the async job status should be "cancelled"
|
|
|
|
# ---- Stuck Job Detection ----
|
|
|
|
Scenario: Detect and fail stuck jobs with expired heartbeat
|
|
Given an async worker is initialised with job_timeout 1
|
|
And a running async job with expired heartbeat exists
|
|
When the worker detects stuck jobs
|
|
Then the stuck job should be marked as "failed"
|
|
|
|
# ---- Job TTL and Cleanup ----
|
|
|
|
Scenario: Cleanup completed jobs older than retention TTL
|
|
Given an async worker is initialised with job_ttl 1
|
|
And a completed async job older than TTL exists
|
|
When the worker runs job cleanup
|
|
Then the completed job should be removed from the store
|
|
|
|
# ---- Graceful Shutdown ----
|
|
|
|
Scenario: Worker stops gracefully on shutdown request
|
|
Given an async worker is initialised with max_workers 2
|
|
When the worker is started
|
|
And a shutdown is requested
|
|
Then the worker should stop running
|
|
|
|
# ---- Payload Serialization ----
|
|
|
|
Scenario: Serialize and deserialize job payload with schema version
|
|
When I serialize a job payload for plan "01HXYZ01HXYZ01HXYZ01HXYZ01" phase "execute"
|
|
Then the serialized payload should contain schema_version 1
|
|
And the deserialized payload should match the original
|
|
|
|
Scenario: Serialize payload with extra data
|
|
When I serialize a job payload with extra data
|
|
Then the serialized payload should contain the extra data
|
|
|
|
Scenario: Deserialize invalid JSON raises error
|
|
When I try to deserialize invalid JSON
|
|
Then a ValueError should be raised for async deserialize
|
|
|
|
# ---- Worker Health Report ----
|
|
|
|
Scenario: Worker health report contains expected fields
|
|
Given an async worker is initialised with max_workers 4
|
|
When I request the worker health report
|
|
Then the health report should contain worker_id
|
|
And the health report should contain config details
|
|
|
|
# ---- AsyncWorkerConfig Validation ----
|
|
|
|
Scenario: AsyncWorkerConfig rejects invalid max_workers
|
|
When I try to create AsyncWorkerConfig with max_workers 0
|
|
Then a ValueError should be raised for config
|
|
|
|
Scenario: AsyncWorkerConfig rejects invalid poll_interval
|
|
When I try to create AsyncWorkerConfig with poll_interval 0
|
|
Then a ValueError should be raised for config
|
|
|
|
# ---- InMemoryJobStore ----
|
|
|
|
Scenario: Job store rejects duplicate job IDs
|
|
Given an async job store is initialised
|
|
And I create a new async job for plan "01HXYZ01HXYZ01HXYZ01HXYZ01" phase "execute"
|
|
When I try to add the same job again
|
|
Then a ValueError should be raised for duplicate job
|
|
|
|
Scenario: Job store lists jobs by status
|
|
Given an async job store is initialised
|
|
And I create 3 queued async jobs
|
|
When I list jobs by status "queued"
|
|
Then the job list should have 3 entries
|
|
|
|
Scenario: AsyncJob heartbeat recording
|
|
Given an async job store is initialised
|
|
And I create a running async job
|
|
When I record a heartbeat on the async job
|
|
Then the async job last_heartbeat should be updated
|
|
|
|
Scenario: Heartbeat on non-running job raises error
|
|
Given an async job store is initialised
|
|
And I create a new async job for plan "01HXYZ01HXYZ01HXYZ01HXYZ01" phase "execute"
|
|
When I try to record a heartbeat on the async job
|
|
Then a ValueError should be raised for heartbeat
|
|
|
|
Scenario: AsyncJob is_terminal property
|
|
Given an async job store is initialised
|
|
And I create a running async job
|
|
When I mark the async job as succeeded
|
|
Then the async job should be terminal
|
|
|
|
Scenario: AsyncJob is_running property
|
|
Given an async job store is initialised
|
|
And I create a running async job
|
|
Then the async job should be running
|
|
|
|
Scenario: AsyncJob as_cli_dict returns expected keys
|
|
Given an async job store is initialised
|
|
And I create a running async job
|
|
When I get the async job CLI dict
|
|
Then the async CLI dict should contain key "job_id"
|
|
And the async CLI dict should contain key "status"
|
|
And the async CLI dict should contain key "worker_id"
|
|
|
|
Scenario: Invalid phase raises validation error
|
|
When I try to create an async job with phase "invalid"
|
|
Then a validation error should be raised for phase
|
|
|
|
Scenario: Invalid payload JSON raises validation error
|
|
When I try to create an async job with invalid payload JSON
|
|
Then a validation error should be raised for payload
|
|
|
|
Scenario: Mark running with empty worker_id raises error
|
|
Given an async job store is initialised
|
|
And I create a new async job for plan "01HXYZ01HXYZ01HXYZ01HXYZ01" phase "execute"
|
|
When I try to mark the async job as running with an empty worker_id
|
|
Then a ValueError should be raised for worker_id
|
|
|
|
Scenario: CancellationToken cooperative cancellation
|
|
Given a cancellation token is created
|
|
When I cancel the async token
|
|
Then the token should be cancelled
|
|
And the token wait should return True
|
|
|
|
Scenario: CancellationToken reset
|
|
Given a cancellation token is created
|
|
And I cancel the async token
|
|
When I reset the token
|
|
Then the token should not be cancelled
|
|
|
|
Scenario: WorkerHealthReport tracks jobs
|
|
Given a worker health report for "test-worker"
|
|
When I record 3 completed jobs and 1 failed job
|
|
Then the health report should show 3 processed and 1 failed
|
|
|
|
Scenario: Serialize payload with empty plan_id raises error
|
|
When I try to serialize a payload with empty plan_id
|
|
Then a ValueError should be raised for serialize
|
|
|
|
Scenario: Deserialize empty payload raises error
|
|
When I try to deserialize an empty payload
|
|
Then a ValueError should be raised for deserialize
|
|
|
|
Scenario: can_transition_job with invalid types raises TypeError
|
|
When I call can_transition_job with invalid types
|
|
Then a TypeError should be raised for async transition
|
|
|
|
# ---- Additional Coverage: Validation Guards ----
|
|
|
|
Scenario: WorkerHealthReport with empty worker_id raises error
|
|
When I try to create a WorkerHealthReport with empty worker_id
|
|
Then a ValueError should be raised for empty worker_id
|
|
|
|
Scenario: AsyncWorkerConfig with invalid enabled type raises error
|
|
When I try to create AsyncWorkerConfig with non-bool enabled
|
|
Then a TypeError should be raised for async config enabled
|
|
|
|
Scenario: AsyncWorkerConfig with invalid job_timeout raises error
|
|
When I try to create AsyncWorkerConfig with job_timeout 0
|
|
Then a ValueError should be raised for async config validation
|
|
|
|
Scenario: AsyncWorkerConfig with invalid job_ttl raises error
|
|
When I try to create AsyncWorkerConfig with job_ttl 0
|
|
Then a ValueError should be raised for async config validation
|
|
|
|
Scenario: InMemoryJobStore add with non-AsyncJob raises error
|
|
Given an async job store is initialised
|
|
When I try to add a non-AsyncJob to the store
|
|
Then a TypeError should be raised for async store type
|
|
|
|
Scenario: InMemoryJobStore get with empty job_id raises error
|
|
Given an async job store is initialised
|
|
When I try to get a job with empty job_id
|
|
Then a ValueError should be raised for async store get
|
|
|
|
Scenario: InMemoryJobStore list_by_status with invalid type raises error
|
|
Given an async job store is initialised
|
|
When I try to list jobs by invalid status type
|
|
Then a TypeError should be raised for async store type
|
|
|
|
Scenario: InMemoryJobStore update with non-AsyncJob raises error
|
|
Given an async job store is initialised
|
|
When I try to update with a non-AsyncJob
|
|
Then a TypeError should be raised for async store type
|
|
|
|
Scenario: InMemoryJobStore update with non-existent job raises error
|
|
Given an async job store is initialised
|
|
When I try to update a non-existent job
|
|
Then a ValueError should be raised for async store update
|
|
|
|
Scenario: InMemoryJobStore remove with empty job_id raises error
|
|
Given an async job store is initialised
|
|
When I try to remove a job with empty job_id
|
|
Then a ValueError should be raised for async store remove
|
|
|
|
Scenario: AsyncWorker with invalid config type raises error
|
|
When I try to create AsyncWorker with invalid config
|
|
Then a TypeError should be raised for async worker config
|
|
|
|
Scenario: AsyncWorker with invalid job_store type raises error
|
|
When I try to create AsyncWorker with invalid job_store
|
|
Then a TypeError should be raised for async worker store
|
|
|
|
Scenario: AsyncWorker pickup with non-AsyncJob raises error
|
|
Given an async worker is initialised with max_workers 2
|
|
When I try to pickup a non-AsyncJob
|
|
Then a TypeError should be raised for async worker pickup
|
|
|
|
Scenario: AsyncWorker cancel_job with empty job_id raises error
|
|
Given an async worker is initialised with max_workers 2
|
|
When I try to cancel a job with empty job_id
|
|
Then a ValueError should be raised for async worker cancel
|
|
|
|
Scenario: AsyncWorker worker_id property returns value
|
|
Given an async worker is initialised with max_workers 2
|
|
Then the worker should have a worker_id starting with "worker-"
|
|
|
|
Scenario: AsyncWorker config property returns config
|
|
Given an async worker is initialised with max_workers 2
|
|
Then the worker config max_workers should be 2
|
|
|
|
Scenario: AsyncWorker start when already running does nothing
|
|
Given an async worker is initialised with max_workers 2
|
|
When I start the worker
|
|
And I start the worker again
|
|
Then the worker should still be running
|
|
|
|
Scenario: AsyncWorker stop after start stops cleanly
|
|
Given an async worker is initialised with max_workers 2
|
|
When I start the worker
|
|
And I stop the worker
|
|
Then the worker should not be running
|
|
|
|
Scenario: AsyncWorker stop when not running does nothing
|
|
Given an async worker is initialised with max_workers 2
|
|
When I stop the worker
|
|
Then the worker should not be running
|
|
|
|
Scenario: AsyncWorker cancel job that does not exist returns false
|
|
Given an async worker is initialised with max_workers 2
|
|
Then cancelling a non-existent job should return false
|
|
|
|
Scenario: AsyncJob payload property deserializes JSON
|
|
Given an async job store is initialised
|
|
And I create a new async job for plan "01HXYZ01HXYZ01HXYZ01HXYZ01" phase "execute"
|
|
Then the async job payload property should return a dict
|
|
|
|
Scenario: AsyncJob validate_payload_json rejects invalid JSON
|
|
When I try to create an async job with payload_json "not-json{"
|
|
Then a validation error should be raised for payload
|
|
|
|
Scenario: AsyncJob as_cli_dict includes optional fields when set
|
|
Given an async job store is initialised
|
|
And I create a running async job
|
|
And I mark the async job as succeeded
|
|
When I get the async job CLI dict
|
|
Then the async CLI dict should contain key "started_at"
|
|
And the async CLI dict should contain key "finished_at"
|
|
|
|
# ---- Review Fixes: Concurrent Execution (T2) ----
|
|
|
|
Scenario: Worker executes multiple jobs concurrently
|
|
Given an async worker is initialised with max_workers 4
|
|
And 3 queued async jobs exist
|
|
When the worker executes all queued jobs concurrently
|
|
Then all 3 jobs should be in a terminal state
|
|
And the worker health should show 3 jobs processed total
|
|
|
|
# ---- Review Fixes: Cancel records cancelled counter (B2) ----
|
|
|
|
Scenario: Cancelled job increments cancelled counter not processed counter
|
|
Given an async worker is initialised with max_workers 2
|
|
And a queued async job exists
|
|
When I cancel the async job
|
|
Then the async job status should be "cancelled"
|
|
And the worker health should show 0 jobs processed
|
|
And the worker health should show 0 jobs failed
|
|
|
|
# ---- Review Fixes: Error message audit trail (SEC1) ----
|
|
|
|
Scenario: Failed job records error message for audit trail
|
|
Given an async worker is initialised with a failing executor
|
|
And a queued async job exists
|
|
When the worker picks up and executes the job
|
|
Then the async job status should be "failed"
|
|
And the async job should have an error_message set
|
|
|
|
# ---- Review Fixes: DB model round-trip (T1) ----
|
|
|
|
Scenario: AsyncJobModel round-trip through SQLAlchemy
|
|
When I create and persist an AsyncJobModel to the database
|
|
Then the AsyncJobModel should be retrievable with correct fields
|
|
And the database CHECK constraint should reject invalid status
|
|
|
|
# ---- Coverage: Safe Initialisation and Cleanup ----
|
|
|
|
Scenario: Worker start creates thread pool and stop destroys it cleanly
|
|
Given an async worker is initialised with max_workers 2
|
|
And a queued async job exists
|
|
When I start the worker
|
|
And the worker processes all queued jobs within 3 seconds
|
|
And I stop the worker
|
|
Then the worker should not be running
|
|
And the async job status should be "succeeded"
|
|
|
|
Scenario: Worker poll loop dispatches jobs via thread pool
|
|
Given an async worker is initialised with max_workers 4
|
|
And 2 queued async jobs exist
|
|
When I start the worker
|
|
And all created jobs reach a terminal state within 3 seconds
|
|
And I stop the worker
|
|
Then all 2 jobs should be in a terminal state
|
|
And the worker health should show 2 jobs processed total
|
|
|
|
Scenario: Worker stop cancels in-flight jobs via tokens
|
|
Given an async worker is initialised with a blocking executor
|
|
And a queued async job exists
|
|
When I start the worker
|
|
And the blocking executor has been entered
|
|
And I stop the worker
|
|
Then the cancellation token for the job should be signalled
|
|
|
|
# ---- Coverage: Cancellation Paths ----
|
|
|
|
Scenario: Running job cancelled via token records cancelled count
|
|
Given an async worker is initialised with a blocking executor
|
|
And a queued async job exists
|
|
When I dispatch the job in a background thread
|
|
And the blocking executor has been entered
|
|
And I cancel the running async job via the worker
|
|
And the background job thread completes
|
|
Then the async job status should be "cancelled"
|
|
And the worker health should show 1 jobs cancelled
|
|
And the worker health should show 0 jobs processed
|
|
|
|
Scenario: Pickup and execute skips already-terminal job safely
|
|
Given an async worker is initialised with max_workers 2
|
|
And a queued async job exists
|
|
And the async job is already cancelled
|
|
When the worker picks up and executes the job
|
|
Then the async job status should be "cancelled"
|
|
And the worker health should show 0 jobs processed
|
|
|
|
Scenario: Executor that moves job to terminal during execution is handled
|
|
Given an async worker is initialised with a terminal-forcing executor
|
|
And a queued async job exists
|
|
When the worker picks up and executes the job
|
|
Then the async job should be in a terminal state
|
|
And the worker health should show 0 jobs processed
|
|
|
|
# ---- Coverage: Job Store Operations ----
|
|
|
|
Scenario: Job store list_all returns all jobs regardless of status
|
|
Given an async job store is initialised
|
|
And I add 3 jobs in mixed statuses to the store
|
|
When I call list_all on the job store
|
|
Then the store should return 3 jobs
|
|
And the store count should be 3
|
|
|
|
Scenario: Job store remove deletes a job and count reflects it
|
|
Given an async job store is initialised
|
|
And a queued async job exists in the store
|
|
When I remove the job from the store
|
|
Then the store count should be 0
|
|
|
|
# ---- Coverage: Domain Model Validators ----
|
|
|
|
Scenario: transition_to rejects non-AsyncJobStatus target
|
|
Given a queued async job exists standalone
|
|
When I try to transition_to with a string instead of enum
|
|
Then an async TypeError should be raised
|
|
|
|
Scenario: can_transition_job rejects non-enum to_status argument
|
|
When I call can_transition_job with a string to_status
|
|
Then an async TypeError should be raised
|
|
|
|
Scenario: serialize_job_payload rejects empty phase
|
|
When I try to serialize a payload with an empty phase
|
|
Then an async ValueError should be raised matching "phase"
|
|
|
|
Scenario: serialize_job_payload rejects zero schema version
|
|
When I try to serialize a payload with schema_version 0
|
|
Then an async ValueError should be raised matching "schema_version"
|
|
|
|
Scenario: deserialize_job_payload rejects non-object JSON
|
|
When I try to deserialize the JSON string "[1,2,3]"
|
|
Then an async ValueError should be raised matching "JSON object"
|
|
|
|
Scenario: Pickup handles already-running job gracefully
|
|
Given an async worker is initialised with max_workers 2
|
|
And a queued async job exists
|
|
And the async job is already running with worker "other-worker"
|
|
When the worker picks up and executes the job
|
|
Then the async job status should be "running"
|
|
And the worker health should show 0 jobs processed
|
|
|
|
Scenario: Executor exception on already-terminal job skips mark_failed
|
|
Given an async worker is initialised with a crash-after-terminal executor
|
|
And a queued async job exists
|
|
When the worker picks up and executes the job
|
|
Then the async job should be in a terminal state
|
|
|
|
Scenario: detect_stuck_jobs tolerates already-terminal jobs
|
|
Given an async worker is initialised with job_timeout 1
|
|
And a running async job with expired heartbeat exists
|
|
And the running async job has already been cancelled externally
|
|
When the worker detects stuck jobs
|
|
Then no additional stuck jobs should be reported
|
|
|
|
Scenario: CLI dict includes error_message for failed job
|
|
Given a queued async job exists standalone
|
|
When I mark the async job as running with worker "w-err"
|
|
And I mark the async job as failed with error "SomeError: detail"
|
|
And I get the async job CLI dict
|
|
Then the async CLI dict should contain key "error_message"
|
|
And the async CLI dict error_message should be "SomeError: detail"
|
|
|
|
# ---- Review Fix: Plan lifecycle async wiring (P1) ----
|
|
|
|
Scenario: execute_plan enqueues async job when async is enabled
|
|
Given a plan lifecycle service with async enabled and a job store
|
|
And a plan in Strategize/COMPLETE state exists
|
|
When I execute the plan via the lifecycle service
|
|
Then the plan should be in Execute/QUEUED state
|
|
And the job store should contain 1 async job for phase "execute"
|
|
And the enqueued job plan_id should match the plan
|
|
|
|
Scenario: apply_plan enqueues async job when async is enabled
|
|
Given a plan lifecycle service with async enabled and a job store
|
|
And a plan in Execute/COMPLETE state exists
|
|
When I apply the plan via the lifecycle service
|
|
Then the plan should be in Apply/QUEUED state
|
|
And the job store should contain 1 async job for phase "apply"
|
|
And the enqueued job plan_id should match the plan
|
|
|
|
Scenario: execute_plan does not enqueue when async is disabled
|
|
Given a plan lifecycle service with async disabled
|
|
And a plan in Strategize/COMPLETE state exists
|
|
When I execute the plan via the lifecycle service
|
|
Then the plan should be in Execute/QUEUED state
|
|
And no async jobs should be in the job store
|
|
|
|
Scenario: apply_plan does not enqueue when async is disabled
|
|
Given a plan lifecycle service with async disabled
|
|
And a plan in Execute/COMPLETE state exists
|
|
When I apply the plan via the lifecycle service
|
|
Then the plan should be in Apply/QUEUED state
|
|
And no async jobs should be in the job store
|
|
|
|
Scenario: execute_plan does not enqueue when no job store is configured
|
|
Given a plan lifecycle service with async enabled but no job store
|
|
And a plan in Strategize/COMPLETE state exists
|
|
When I execute the plan via the lifecycle service
|
|
Then the plan should be in Execute/QUEUED state
|
|
|
|
# ---- Review Fix: Error message redaction (P2) ----
|
|
|
|
Scenario: Failed job error message redacts secrets
|
|
Given an async worker is initialised with a secret-leaking executor
|
|
And a queued async job exists
|
|
When the worker picks up and executes the job
|
|
Then the async job status should be "failed"
|
|
And the async job error_message should not contain the raw secret
|
|
And the async job error_message should contain "***REDACTED***"
|