f0a40afecc
CI / build (push) Successful in 22s
CI / lint (push) Failing after 24s
CI / helm (push) Successful in 24s
CI / quality (push) Successful in 3m41s
CI / typecheck (push) Successful in 3m55s
CI / coverage (push) Has been skipped
CI / benchmark-regression (push) Has been skipped
CI / security (push) Successful in 4m4s
CI / unit_tests (push) Failing after 5m43s
CI / docker (push) Has been skipped
CI / benchmark-publish (push) Has been cancelled
CI / integration_tests (push) Has been cancelled
CI / e2e_tests (push) Has been cancelled
CI / status-check (push) Has been cancelled
Implements a write-behind queue with a background daemon thread in AuditService so that record() returns immediately without blocking the calling domain operation on a synchronous SQLite INSERT + COMMIT. Changes: - AuditService: add _writer_loop(), _write_payload(), flush() methods and a write-behind queue with a background thread - record() enqueues the payload and returns a placeholder entry with id=-1 in async mode - close() calls flush() to drain the queue before closing the session - Settings: add audit_async (default True) and audit_queue_maxsize (default 10000) fields - 20 BDD scenarios covering non-blocking record(), flush/close lifecycle, ordering, sync fallback, and settings Closes #718 Co-authored-by: Jeffrey Phillips Freeman <the@jeffreyfreeman.me> Co-committed-by: Jeffrey Phillips Freeman <the@jeffreyfreeman.me>
113 lines
4.9 KiB
Gherkin
113 lines
4.9 KiB
Gherkin
Feature: Async audit recording (issue #718)
|
|
As a system operator
|
|
I want audit recording to be non-blocking
|
|
So that domain operations are not delayed by SQLite writes
|
|
|
|
# ── Non-blocking record() ─────────────────────────────────────
|
|
|
|
Scenario: record() returns immediately without blocking in async mode
|
|
Given an async audit service
|
|
When I record a "plan_applied" event in async mode
|
|
Then the record call should return immediately
|
|
And the returned entry should have event_type "plan_applied"
|
|
|
|
Scenario: Async record returns placeholder id of -1
|
|
Given an async audit service
|
|
When I record a "plan_applied" event in async mode
|
|
Then the returned entry id should be -1
|
|
|
|
Scenario: Entries are persisted after flush
|
|
Given an async audit service
|
|
When I record a "plan_applied" event in async mode
|
|
And I flush the async audit service
|
|
Then the audit log should contain 1 persisted entry
|
|
|
|
Scenario: Multiple entries are all persisted after flush
|
|
Given an async audit service
|
|
When I record 5 "plan_applied" events in async mode
|
|
And I flush the async audit service
|
|
Then the audit log should contain 5 persisted entries
|
|
|
|
Scenario: Flush waits for all enqueued entries to be written
|
|
Given an async audit service
|
|
When I record 10 "config_changed" events in async mode
|
|
And I flush the async audit service
|
|
Then the audit log should contain 10 persisted entries
|
|
|
|
# ── No data loss ──────────────────────────────────────────────
|
|
|
|
Scenario: Close flushes all pending entries before shutting down
|
|
Given an async audit service
|
|
When I record 3 "session_created" events in async mode
|
|
And I close the async audit service
|
|
Then the audit log should contain 3 persisted entries after close
|
|
|
|
Scenario: Context manager flushes on exit
|
|
Given an async audit service used as context manager
|
|
When I record a "plan_cancelled" event inside the context manager
|
|
Then the audit log should contain 1 persisted entry after context exit
|
|
|
|
# ── Synchronous fallback ──────────────────────────────────────
|
|
|
|
Scenario: Synchronous mode returns real id immediately
|
|
Given a synchronous audit service
|
|
When I record a "plan_applied" event synchronously
|
|
Then the returned entry id should be a positive integer
|
|
|
|
Scenario: Synchronous mode persists immediately without flush
|
|
Given a synchronous audit service
|
|
When I record a "plan_applied" event synchronously
|
|
Then the audit log should contain 1 persisted entry immediately
|
|
|
|
# ── Settings ──────────────────────────────────────────────────
|
|
|
|
Scenario: Default audit_async setting is True
|
|
Given default settings
|
|
Then audit_async should be True
|
|
|
|
Scenario: audit_async can be disabled via settings
|
|
Given settings with audit_async False
|
|
Then audit_async should be False
|
|
|
|
Scenario: audit_queue_maxsize default is 10000
|
|
Given default settings
|
|
Then audit_queue_maxsize should be 10000
|
|
|
|
# ── Background thread ─────────────────────────────────────────
|
|
|
|
Scenario: Background writer thread is started in async mode
|
|
Given an async audit service
|
|
Then the background writer thread should be alive
|
|
|
|
Scenario: Background writer thread stops after flush
|
|
Given an async audit service
|
|
When I flush the async audit service
|
|
Then the background writer thread should be stopped
|
|
|
|
# ── Ordering guarantees ───────────────────────────────────────
|
|
|
|
Scenario: Entries are written in enqueue order
|
|
Given an async audit service
|
|
When I record events with plan_ids "first" "second" "third" in async mode
|
|
And I flush the async audit service
|
|
Then the entries should be persisted in enqueue order
|
|
|
|
# ── Error resilience ──────────────────────────────────────────
|
|
|
|
Scenario: Invalid event_type raises ValueError immediately in async mode
|
|
Given an async audit service
|
|
When I record an event with invalid type "not_a_real_event" in async mode
|
|
Then a ValueError should be raised immediately
|
|
|
|
Scenario: flush() is idempotent
|
|
Given an async audit service
|
|
When I flush the async audit service
|
|
And I flush the async audit service again
|
|
Then no async audit error should be raised
|
|
|
|
Scenario: close() is idempotent
|
|
Given an async audit service
|
|
When I close the async audit service
|
|
And I close the async audit service again
|
|
Then no async audit error should be raised
|