mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
[sweep] strip Fxxx audit-ID tokens + trim bloated comments/docstrings + add behavior-change docs
Post-audit sweep over the 135 audit-fix commits since19a474d3: 1. Stripped every # Fxxx: audit-ID token from comments AND every Fxxx token from docstring openings across 211 blocks / ~626 lines. The CEO flagged these twice: audit-issue IDs in code confuse future devs/agents. The descriptive text is preserved; only the Fxxx token is removed (and bloated narrative blocks trimmed to 1-3 lines keeping the one non-obvious invariant). 2. Trimmed bloated comments/docstrings to the concise standard (1-3 lines). 3. Added missing behavior-change docs for the audit-fix batch: prompts/roles (documenter, pr_reviewer, qa), user-facing docs (api auth, websockets, agent-gateway, megatask, merge-model, task-lifecycle, grok, resilience, conventions, panel, security, troubleshooting), and the RAG corpus (cell-pm, main-pm, pr-reviewer, qa roles; conventions; messaging-tools; escalation; megatask; task-claiming workflows). Comment/docstring/prose ONLY — zero code-line edits (verified: the diff contains no def/class/return/if/for/await/assignment/call lines). Gates green: ruff format + ruff check clean, mypy clean on roboco/. The only pytest failures are the pre-existing sync_branch tracing-decision gap (B1,250be5c2) — not sweep-caused and tracked separately.
This commit is contained in:
@@ -1,26 +1,7 @@
|
||||
"""F054: the LEARNINGS index must not leak private (shareable=False) entries
|
||||
through ANY shared retrieval path.
|
||||
|
||||
A private LEARNING journal entry is recorded into the LEARNINGS index with
|
||||
``shareable=False`` (journal.py records it for completeness but it is never
|
||||
meant to surface to other agents). The shared retrieval paths all reach the
|
||||
plugin's retrieval with no ``include_private`` opt-in:
|
||||
|
||||
- ``OptimalService.search`` (used by the briefing / ``similar_memory``) calls
|
||||
``search_with_embedding`` directly with no filters.
|
||||
- ``search_learnings`` (shareable_only=True, the default) and the
|
||||
``get_learnings_by_category`` / ``get_learnings_by_role`` /
|
||||
``get_team_learnings`` cross-agent views call ``search`` with a filters dict
|
||||
that does NOT carry a ``shareable`` key.
|
||||
|
||||
The base ``_citations_to_results`` only filters when a ``shareable`` filter is
|
||||
present, so a ``shareable=False`` chunk sails through into another agent's
|
||||
briefing — a private reflection leaked across the cross-agent corpus.
|
||||
|
||||
The fix: the LEARNINGS plugin forces ``shareable=True`` on retrieval unless the
|
||||
caller explicitly opts into the private view via ``include_private=True`` (the
|
||||
``search_learnings(shareable_only=False)`` audit/admin path). An empty filters
|
||||
dict does NOT opt out — shareable is the safe default on every shared path.
|
||||
"""The LEARNINGS index must not leak private (``shareable=False``) entries
|
||||
through any shared retrieval path. The plugin forces ``shareable=True`` on
|
||||
retrieval unless the caller opts into the private view via
|
||||
``include_private=True``; an empty filters dict does NOT opt out.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -44,8 +44,8 @@ def test_build_source_uri_none_when_missing() -> None:
|
||||
|
||||
|
||||
def test_delete_playbook_removes_its_chunks_by_source() -> None:
|
||||
"""F011: deleting a playbook removes its embedded chunks from the vector
|
||||
store by the playbook's source URI (idempotent — no-op if absent). A
|
||||
"""Deleting a playbook removes its embedded chunks from the vector store
|
||||
by the playbook's source URI (idempotent — no-op if absent). A
|
||||
rejected/archived playbook must not stay retrievable in the PLAYBOOKS index."""
|
||||
plugin = PlaybooksIndexPlugin.__new__(PlaybooksIndexPlugin)
|
||||
store = MagicMock()
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
"""F108 — ``VectorStore.replace_chunks`` must be a single atomic transaction.
|
||||
|
||||
The replace-on-reingest path used to be ``delete_by_source`` (one pool
|
||||
connection) followed by ``add_chunks`` (a *second* pool connection). Two
|
||||
concurrent re-indexes of the same source interleaved across those two
|
||||
connections and produced duplicate chunk rows; an add failure after a
|
||||
successful delete also lost the source's index rows. The fix is a single
|
||||
``replace_chunks(source, chunks)`` that deletes + inserts on ONE connection
|
||||
inside ONE asyncpg transaction, so the whole replace is atomic.
|
||||
|
||||
These tests mock the asyncpg pool/connection to assert the atomicity
|
||||
invariant (single acquire, transaction entered, delete + insert on the
|
||||
same connection) without standing up a pgvector DB.
|
||||
"""``VectorStore.replace_chunks`` is a single atomic transaction: delete +
|
||||
insert on ONE connection inside ONE asyncpg transaction, so concurrent
|
||||
re-indexes can't interleave and an insert failure can't lose the source's
|
||||
rows. These tests mock the asyncpg pool to assert that invariant.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""ConventionsService._cache_put isolates a concurrent-duplicate insert (F042).
|
||||
"""ConventionsService._cache_put isolates a concurrent-duplicate insert.
|
||||
|
||||
Two task creates for the same project/HEAD can race to populate the
|
||||
conventions cache; the loser's INSERT fails the partial-unique index with
|
||||
@@ -84,10 +84,10 @@ def _mapping() -> ConventionsStandard:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cache_put_tolerates_concurrent_duplicate_without_poisoning() -> None:
|
||||
# F042: the loser of a concurrent cache-populate race must not crash the
|
||||
# shared task-create session. The duplicate IntegrityError is contained to
|
||||
# a savepoint; _cache_put returns cleanly, the session is not poisoned, and
|
||||
# no full rollback undoes the outer task-create transaction.
|
||||
# The loser of a concurrent cache-populate race must not crash the shared
|
||||
# task-create session: the duplicate IntegrityError is contained to a
|
||||
# savepoint, the session is not poisoned, and no full rollback undoes the
|
||||
# outer task-create transaction.
|
||||
session = _FakeSession(duplicate=True)
|
||||
svc = ConventionsService(session=cast("Any", session))
|
||||
|
||||
|
||||
@@ -430,11 +430,8 @@ async def test_is_board_advisory_agent_classifies_roles() -> None:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_escalation_refuses_completed_task() -> None:
|
||||
# F043: a COMPLETED task is terminal — apply_escalation must not resurrect
|
||||
# it to BLOCKED. The HTTP escalate route bypasses the spec gate, so the
|
||||
# single write primitive must refuse terminal tasks itself. Returns False
|
||||
# so callers (escalate / HTTP route) can surface a clean invalid_state / 409
|
||||
# instead of mutating a finished task.
|
||||
# apply_escalation must refuse terminal tasks: the HTTP route bypasses the
|
||||
# spec gate, so the primitive guards itself and returns False for a 409.
|
||||
svc = _service()
|
||||
original_assignee = uuid4()
|
||||
task = MagicMock(
|
||||
@@ -466,7 +463,7 @@ async def test_apply_escalation_refuses_completed_task() -> None:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_escalation_refuses_cancelled_task() -> None:
|
||||
# F043: cancelled is terminal too — must not be resurrected via escalation.
|
||||
# cancelled is terminal too — must not be resurrected via escalation.
|
||||
svc = _service()
|
||||
task = MagicMock(
|
||||
id=uuid4(),
|
||||
@@ -495,8 +492,8 @@ async def test_apply_escalation_refuses_cancelled_task() -> None:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_escalation_blocks_non_terminal_task() -> None:
|
||||
# F043: the terminal guard must not over-restrict — a normal in_progress
|
||||
# task still escalates (blocked + reassigned) and returns True.
|
||||
# the terminal guard must not over-restrict: a normal in_progress task
|
||||
# still escalates (blocked + reassigned) and returns True.
|
||||
svc = _service()
|
||||
target_id = uuid4()
|
||||
task = MagicMock(
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
"""F051: open_conventions_pr must not operate on a dirty working tree.
|
||||
|
||||
``open_conventions_pr`` cuts its scaffold branch in an agent's clone (or the
|
||||
project's shared ``workspace_path``). It does ``checkout <base>`` with
|
||||
``check=False`` and then ``checkout -B <scaffold>``. On a dirty tree the
|
||||
``checkout <base>`` either no-ops (already on base) or is refused and silently
|
||||
swallowed; ``checkout -B <scaffold>`` then carries the agent's uncommitted
|
||||
work onto the scaffold branch, and the ``commit`` sweeps it into the
|
||||
project-level conventions commit — the agent's in-progress change is gone
|
||||
from their working tree and rides a PR they never intended. Refuse a dirty
|
||||
tree up front (return None, no checkout) so an active workspace is never
|
||||
touched.
|
||||
"""``open_conventions_pr`` refuses a dirty working tree up front (returns
|
||||
None, no checkout) so an active agent workspace is never swept into a
|
||||
project-level conventions commit.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
"""F019 — a git mutation op killed by ``_run_git``'s timeout orphans lock files.
|
||||
|
||||
``subprocess.run(..., timeout=...)`` sends SIGKILL on timeout. A git mutation
|
||||
(commit / merge --ff-only / rebase / reset --hard / add) killed mid-write
|
||||
orphaned ``.git/index.lock`` (+ ``HEAD.lock`` / ``refs/**.lock`` /
|
||||
``packed-refs.lock``), wedging the workspace for every subsequent op —
|
||||
including the next fresh-claim ``reset --hard`` — with
|
||||
"Another git process seems to be running in this repository". The fix
|
||||
best-effort removes stale ``.git/**/*.lock`` files in the timeout branch
|
||||
before re-raising, since the git process is dead by the time the timeout
|
||||
fires.
|
||||
"""A git mutation op killed by ``_run_git``'s timeout best-effort removes
|
||||
orphaned ``.git/**/*.lock`` files before re-raising — the SIGKILL'd git
|
||||
process can't clean up itself, and the locks wedge every subsequent op.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -167,13 +167,9 @@ async def test_merge_does_not_retry_when_method_allowed(
|
||||
async def test_merge_already_merged_pr_is_idempotent_success(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""F049: the CEO ``merge_pull_request`` path must treat an already-merged PR
|
||||
as idempotent success, not raise GitError — mirroring ``_merge_with_retry``
|
||||
(the agent-facing path). A merge PUT on an already-merged PR returns the
|
||||
same 405 as a genuine "not mergeable" refusal, so without disambiguation a
|
||||
CEO retry (double-click, or a re-merge after a network blip where the first
|
||||
PUT actually landed) raises GitError instead of no-opping — surfacing a
|
||||
spurious failure on the very master-merge path the CEO owns.
|
||||
"""The CEO ``merge_pull_request`` path treats an already-merged PR as
|
||||
idempotent success (not GitError) — a merge PUT on an already-merged PR
|
||||
returns the same 405 as a genuine refusal, so they must be disambiguated.
|
||||
"""
|
||||
|
||||
svc = _git_service()
|
||||
@@ -216,8 +212,8 @@ async def test_merge_already_merged_pr_is_idempotent_success(
|
||||
async def test_merge_raises_when_not_merged_and_refused(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""F049: a genuine merge refusal (not mergeable, NOT already-merged) still
|
||||
raises GitError — the idempotency guard must not mask a real failure."""
|
||||
"""A genuine merge refusal (not mergeable, NOT already-merged) still raises
|
||||
GitError — the idempotency guard must not mask a real failure."""
|
||||
|
||||
svc = _git_service()
|
||||
monkeypatch.setattr(
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
"""F050: merge_pr_for_task must not merge a caller-provided pr_number that
|
||||
doesn't match the task's recorded PR.
|
||||
|
||||
``GitMergePRRequest.pr_number`` is caller-provided. When a ``task_id`` is
|
||||
present the service knows the task's *own* recorded PR (``task.pr_number``),
|
||||
set when the PR was opened. Without a match check a caller (a buggy client, a
|
||||
stale panel form, an agent that cached an old PR number) can ask the CEO/PM
|
||||
merge path to merge PR #N for task T whose recorded PR is #M — merging the
|
||||
wrong PR against the wrong task's work-session and auto-complete. The recorded
|
||||
PR is the source of truth; the caller's number must agree with it.
|
||||
"""``merge_pr_for_task`` rejects a caller-provided ``pr_number`` that doesn't
|
||||
match the task's recorded ``task.pr_number`` — the recorded PR is the source
|
||||
of truth, so a stale caller number can't merge the wrong PR for a task.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
"""F053: _token_for_project must log a Fernet decryption failure with the
|
||||
project context, not swallow it silently as 'no token'.
|
||||
"""Log a Fernet decryption failure with the project slug before returning None.
|
||||
|
||||
On an encryption-key rotation the stored PAT (encrypted with the old key) can't
|
||||
be decrypted — ``crypto.decrypt_token`` raises ``EncryptionError``. The
|
||||
crypto layer logs a generic message, but ``_token_for_project`` catches the
|
||||
``EncryptionError`` and returns ``None`` with no project context, so every
|
||||
best-effort workspace git op (push, PR, clone-with-token) silently looks like
|
||||
'this project has no token' — indistinguishable from a project that genuinely
|
||||
never set one. The operator can't tell which project is wedged by a key
|
||||
rotation. Log the failure with the project slug before returning None (the
|
||||
best-effort skip behavior is preserved — this only makes the cause
|
||||
diagnosable).
|
||||
On a key rotation the stored PAT can't be decrypted; ``_token_for_project`` must
|
||||
not mask that as a silent 'no token' — log the project slug so the cause is
|
||||
diagnosable (best-effort skip behavior preserved).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
"""F055: ``get_or_create_channel_by_slug`` must recover from a concurrent
|
||||
auto-create race on the channel slug's UNIQUE constraint instead of crashing
|
||||
the caller with an ``IntegrityError``.
|
||||
"""Recover from a concurrent auto-create race on the channel slug's UNIQUE
|
||||
constraint instead of crashing the caller with an ``IntegrityError``.
|
||||
|
||||
Two concurrent callers (e.g. two Main-PM group-create requests hitting the
|
||||
groups route) both miss the lookup, both auto-create the same seed channel,
|
||||
and the loser's ``flush`` raises ``IntegrityError`` on ``channels.slug``
|
||||
unique. With no handling that propagates as a 500 to whichever caller lost the
|
||||
race, even though the channel they wanted now exists. The fix: isolate the
|
||||
insert in a savepoint, and on a unique-conflict ``IntegrityError`` re-fetch
|
||||
the now-existing channel (the winner's row) and return it. A conflict that
|
||||
did NOT produce a row on re-fetch is re-raised (don't mask a real failure).
|
||||
Isolate the insert in a savepoint; on a unique-conflict ``IntegrityError``
|
||||
re-fetch the winner's row. A conflict that did NOT produce a row is re-raised.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -1,18 +1,8 @@
|
||||
"""F056: ``create_session`` (and its delegate ``get_or_create_active_session``,
|
||||
plus the L1868 channel-post adapter that routes through it) must not orphan an
|
||||
ACTIVE session under concurrent posts.
|
||||
"""``create_session`` must not orphan an ACTIVE session under concurrent posts.
|
||||
|
||||
``create_session`` does a plain check-then-create: read ``group.active_session_id``,
|
||||
reuse if ACTIVE, else INSERT a new ACTIVE session and point the group at it.
|
||||
Two concurrent posts can both miss the active session, both INSERT, and the
|
||||
second ``flush`` overwrites ``group.active_session_id`` — the first session
|
||||
stays ACTIVE but unreferenced (orphaned) forever. There is no DB uniqueness on
|
||||
``(group_id, status='active')`` (tables.py:1121-1125 only carries indexes), so
|
||||
nothing stops the double-insert.
|
||||
|
||||
The fix: lock the group row (``SELECT ... FOR UPDATE``) and re-read
|
||||
``active_session_id`` under the lock before deciding to create, so concurrent
|
||||
callers serialize per group and the loser reuses the winner's session.
|
||||
Lock the group row (``SELECT ... FOR UPDATE``) and re-read
|
||||
``active_session_id`` under the lock before creating, so concurrent callers
|
||||
serialize per group and the loser reuses the winner's session.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -325,7 +325,7 @@ async def test_create_notification_skips_when_no_resolvable_recipients(
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# F009 — requires_ack must follow ACK_REQUIRED_BY_TYPE, not the True default
|
||||
# requires_ack must follow ACK_REQUIRED_BY_TYPE, not the True default
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -333,8 +333,8 @@ async def test_create_notification_skips_when_no_resolvable_recipients(
|
||||
async def test_informational_notification_does_not_require_ack(
|
||||
svc: NotificationService,
|
||||
) -> None:
|
||||
"""F009: REVIEW_REQUEST / DOCUMENTATION_REQUEST / A2A_REQUEST are
|
||||
informational (pickup proves receipt) — requires_ack must be False, not the
|
||||
"""REVIEW_REQUEST / DOCUMENTATION_REQUEST / A2A_REQUEST are informational
|
||||
(pickup proves receipt) — requires_ack must be False, not the
|
||||
NotificationTable True default. A False type forced to True inflates the
|
||||
recipient's unacked set and soft-blocks i_am_idle → respawn churn."""
|
||||
aid = uuid4()
|
||||
@@ -368,7 +368,7 @@ async def test_informational_notification_does_not_require_ack(
|
||||
async def test_action_required_notification_still_requires_ack(
|
||||
svc: NotificationService,
|
||||
) -> None:
|
||||
"""F009: BLOCKER_ESCALATION / APPROVAL / ALERT are action-required —
|
||||
"""BLOCKER_ESCALATION / APPROVAL / ALERT are action-required —
|
||||
requires_ack stays True (ACK_REQUIRED_BY_TYPE maps them True)."""
|
||||
aid = uuid4()
|
||||
db = _FakeDb(agent_uuid=aid)
|
||||
@@ -389,8 +389,8 @@ async def test_action_required_notification_still_requires_ack(
|
||||
async def test_create_notification_requires_ack_derives_from_type(
|
||||
svc: NotificationService,
|
||||
) -> None:
|
||||
"""F009: a raw _create_notification call derives requires_ack from the type
|
||||
via ACK_REQUIRED_BY_TYPE (KNOWLEDGE_SHARE → False)."""
|
||||
"""A raw _create_notification call derives requires_ack from the type via
|
||||
ACK_REQUIRED_BY_TYPE (KNOWLEDGE_SHARE → False)."""
|
||||
aid = uuid4()
|
||||
db = _FakeDb(agent_uuid=aid)
|
||||
with _patch_db_context(db):
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
"""F057: the PLAYBOOKS RAG index write must not commit independently of — and
|
||||
BEFORE — the playbook status transaction.
|
||||
"""The PLAYBOOKS RAG index write must not commit independently of — and BEFORE —
|
||||
the playbook status transaction.
|
||||
|
||||
``approve()`` / ``reject()`` used to call ``_index_approved`` / ``_unindex_playbook``
|
||||
inline, AFTER ``flush()`` but BEFORE the caller's ``commit()``. The vector store
|
||||
writes chunks via its OWN pool connection (vector_store.py:211-237), which
|
||||
auto-commits immediately and independently of the SQLAlchemy session
|
||||
transaction. So a status-commit failure (or a crash between the index write and
|
||||
the commit) left the RAG corpus with an approved/archived playbook whose DB row
|
||||
was still DRAFT/APPROVED — a divergence agents then surfaced in briefings.
|
||||
|
||||
The fix: ``approve()`` / ``reject()`` flush the status ONLY; the index/unindex
|
||||
is a separate post-commit step (``index_approved`` / ``unindex_playbook``) the
|
||||
caller runs AFTER the status transaction commits. Both entry points — the panel
|
||||
route (playbooks.py) and the Auditor gateway verb (content_actions.
|
||||
_curate_playbook) — commit-then-index, and skip the index if the commit fails.
|
||||
``approve()`` / ``reject()`` flush the status only; the index/unindex is a
|
||||
separate post-commit step the caller runs after the status commits (skipped if
|
||||
the commit fails), so the RAG corpus never diverges from the DB row.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -61,7 +51,7 @@ async def test_approve_does_not_index_before_commit(
|
||||
) -> None:
|
||||
"""``approve()`` flushes the status change but must NOT write to the RAG
|
||||
index — that writes through its own auto-committing connection, so it would
|
||||
durably land before the caller commits the status (the F057 divergence)."""
|
||||
durably land before the caller commits the status (the divergence)."""
|
||||
monkeypatch.setattr(settings, "org_memory_enabled", True)
|
||||
session = AsyncMock()
|
||||
session.flush = AsyncMock()
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
"""F110 — ``PlaybookService.draft`` slug TOCTOU must not 500.
|
||||
"""``PlaybookService.draft`` slug TOCTOU must not 500.
|
||||
|
||||
``draft`` pre-checks the slug with ``_get_by_slug`` then INSERTs. Two concurrent
|
||||
same-title drafts both miss the pre-check (neither sees the other's uncommitted
|
||||
row), so the loser's flush hits the ``playbooks.slug`` UNIQUE constraint and
|
||||
raises ``IntegrityError``. The pre-check alone cannot close the race — the DB
|
||||
constraint is the authoritative guard. The fix wraps the insert in a savepoint
|
||||
and converts the ``IntegrityError`` into a clean ``ConflictError`` (the same
|
||||
error the pre-check raises), so the loser gets a 409, not an unhandled 500.
|
||||
|
||||
Playbooks are distinct curated content (unlike shared-infrastructure channels,
|
||||
where the loser reuses the winner's row): two same-title drafts are two
|
||||
different procedures that collided on the derived slug, so the loser must be
|
||||
told to retry with a distinct title — it must NOT silently reuse the winner's
|
||||
row (that would drop the loser's content).
|
||||
Two concurrent same-title drafts both miss the pre-check; the loser's flush
|
||||
hits the ``playbooks.slug`` UNIQUE constraint. The fix wraps the insert in a
|
||||
savepoint and converts ``IntegrityError`` into a clean ``ConflictError`` (409),
|
||||
so the loser is told to retry with a distinct title — it must NOT silently
|
||||
reuse the winner's row (that would drop the loser's content).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -68,7 +60,7 @@ def _create(title: str = "Retry flaky pg") -> PlaybookCreate:
|
||||
async def test_draft_slug_race_raises_conflict_not_integrity_error() -> None:
|
||||
"""Concurrent same-title loser: pre-check misses (None), flush raises
|
||||
IntegrityError on the UNIQUE slug — draft must convert it to a clean
|
||||
ConflictError, not let it propagate as an unhandled 500 (F110)."""
|
||||
ConflictError, not let it propagate as an unhandled 500."""
|
||||
svc, session = _svc(flush_side_effect=_integrity_error())
|
||||
# Pre-check misses the row (the race window: the other draft is uncommitted).
|
||||
object.__setattr__(svc, "_get_by_slug", AsyncMock(return_value=None))
|
||||
@@ -84,7 +76,7 @@ async def test_draft_slug_race_raises_conflict_not_integrity_error() -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_draft_happy_path_still_inserts() -> None:
|
||||
"""No race: pre-check misses, flush succeeds — the savepoint path is used
|
||||
and the row is added (regression guard for the F110 wrap)."""
|
||||
and the row is added (regression guard for the savepoint wrap)."""
|
||||
svc, session = _svc()
|
||||
object.__setattr__(svc, "_get_by_slug", AsyncMock(return_value=None))
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
"""F012 — ``_GitReleaseOps.commit_and_push`` must be fail-closed on commit.
|
||||
"""``_GitReleaseOps.commit_and_push`` must be fail-closed on commit.
|
||||
|
||||
The release commit step discarded the ``git add`` / ``git commit`` return codes:
|
||||
on a failed commit (gpgsign unavailable, pre-commit hook rejection, nothing to
|
||||
commit after a no-op bump) the code still ran ``rev-parse HEAD`` + pushed the
|
||||
pre-bump base, so ``gh release create`` would tag the *old* tree as the new
|
||||
version. The fix checks both return codes and raises before any push.
|
||||
A failed ``git add`` / ``git commit`` (gpgsign unavailable, pre-commit rejection,
|
||||
no-op bump) must raise before any push — otherwise ``gh release create`` tags
|
||||
the pre-bump base as the new version.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
"""F078 — ``_GitReleaseOps`` subprocesses (git, ``make quality``, ``gh release
|
||||
create``, the release-clone ``git clone``) had no deadline: a hung child would
|
||||
block the CEO-gated release loop indefinitely.
|
||||
|
||||
The fix wraps each ``proc.communicate()`` in ``asyncio.wait_for`` and, on
|
||||
expiry, ``proc.kill()``s the child and returns a non-zero rc (124) so the
|
||||
caller fails closed — mirroring the quality-gate ``_run_one`` kill-on-timeout
|
||||
idiom. The deadlines are generous (a full ``make quality`` run, a network
|
||||
push/clone can legitimately take minutes) so a healthy release is never
|
||||
wrongly aborted.
|
||||
"""``_GitReleaseOps`` subprocesses (git, ``make quality``, ``gh release create``,
|
||||
the release-clone ``git clone``) are wrapped in ``asyncio.wait_for`` with a
|
||||
kill-on-timeout fail-close so a hung child cannot block the release loop.
|
||||
|
||||
These tests hang the subprocess (a never-resolving ``communicate``) and patch
|
||||
the timeout constants tiny so a deterministic fail-close is asserted in well
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
"""F013 — concurrent approve races on the shared release clone.
|
||||
|
||||
The approve flow ran the ~40min ``ReleaseExecutor.execute`` with no guard, so
|
||||
two concurrent CEO ``POST /proposal/approve`` calls (double-click, panel retry)
|
||||
both found the same held proposal and raced on the shared, ``rm -rf``'d writable
|
||||
release clone — interleaving ``git add``/``commit``/``push`` and corrupting the
|
||||
release. The fix acquires a Redis ``SET NX`` mutex keyed by the proposal id
|
||||
before execute (TTL > the 40min CI ceiling) and releases it on completion; a
|
||||
second concurrent approve sees the lock held and refuses instead of racing.
|
||||
"""Concurrent CEO approve races on the shared release clone are serialized by a
|
||||
Redis ``SET NX`` mutex keyed by the proposal id; a second concurrent approve
|
||||
sees the lock held and refuses instead of racing on the writable clone.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -1,17 +1,6 @@
|
||||
"""F058: the FIRST release (no prior ``chore(release):`` commit) must still
|
||||
produce a non-empty version-bump plan.
|
||||
|
||||
``_canonical_bump_files`` derived the bump-target set from the previous
|
||||
``chore(release):`` commit's touched files. On the first release ever there is
|
||||
no such commit, so it returned ``[]`` → ``assess`` set
|
||||
``version_bump_plan=[]`` → ``ReleaseExecutor.apply_version_bumps`` bumped NO
|
||||
files and published a tag masquerading as X.Y.Z with nothing actually changed.
|
||||
|
||||
The fix: when no prior release commit exists, fall back to the version-
|
||||
reference scan — the files currently embedding the version string are exactly
|
||||
the set a first release must bump (and the set a subsequent release's
|
||||
``chore(release):`` commit would record as canonical). This is read-only
|
||||
derivation only; the CEO-approval gate and fail-closed executor are untouched.
|
||||
"""The FIRST release (no prior ``chore(release):`` commit) must still produce a
|
||||
non-empty version-bump plan: ``_canonical_bump_files`` falls back to the
|
||||
version-reference scan when no prior release commit exists.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -60,7 +49,7 @@ def _first_release_repo(tmp_path: Path) -> Path:
|
||||
|
||||
def test_canonical_bump_files_falls_back_on_first_release(tmp_path: Path) -> None:
|
||||
"""No prior ``chore(release):`` commit ⇒ the canonical set is the version-
|
||||
reference scan, NOT empty (the F058 regression: it returned ``[]``)."""
|
||||
reference scan, NOT empty."""
|
||||
root = _first_release_repo(tmp_path)
|
||||
files = _canonical_bump_files(root, "0.1.0")
|
||||
assert files # non-empty
|
||||
|
||||
@@ -151,9 +151,8 @@ async def test_originate_creates_pending_main_pm_assigned_task(
|
||||
# Assigned to the Main PM agent up front (not just team=main_pm) so that, once
|
||||
# the CEO approves it, the orchestrator dispatches it straight to that agent.
|
||||
assert task.assigned_to == MAIN_PM_UUID
|
||||
# F059: held for the CEO's Approve-&-Start — NOT auto-confirmed. The
|
||||
# orchestrator + give_me_work keep it out of dispatch until the CEO
|
||||
# approves it (approve_and_start flips this True).
|
||||
# held for the CEO's Approve-&-Start — NOT auto-confirmed: the orchestrator
|
||||
# + give_me_work keep it out of dispatch until approve_and_start flips this.
|
||||
assert task.confirmed_by_human is False
|
||||
assert task.team == Team.MAIN_PM
|
||||
assert task.source == "self_heal"
|
||||
|
||||
@@ -1065,16 +1065,10 @@ async def test_ensure_branch_raises_when_neither_project_nor_product() -> None:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_finalize_claim_rollback_emits_reversal_audit() -> None:
|
||||
"""F060: when branch creation fails mid-claim, the rollback must emit a
|
||||
REVERSAL audit row (CLAIMED -> original) so the audit journey doesn't
|
||||
diverge from the real (rolled-back) task state.
|
||||
|
||||
The audit service writes on its own connection (fire-and-forget), so the
|
||||
forward `task.claimed` row committed at the pre-branch flush is NOT undone
|
||||
by the rollback's flush. Without a matching reversal row, the journey's
|
||||
last event stays `task.claimed` while the task is back to PENDING — the
|
||||
audit trail diverges from real state and corrupts every downstream metric
|
||||
reconstructed from `task.<status>` events (cycle time, bottlenecks).
|
||||
"""When branch creation fails mid-claim, the rollback must emit a REVERSAL
|
||||
audit row (CLAIMED -> original) so the audit journey matches the real
|
||||
(rolled-back) task state. The audit service writes on its own connection, so
|
||||
the forward `task.claimed` row is NOT undone by the rollback's flush.
|
||||
"""
|
||||
session = MagicMock()
|
||||
session.flush = AsyncMock()
|
||||
@@ -1121,19 +1115,11 @@ async def test_finalize_claim_rollback_emits_reversal_audit() -> None:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_emit_status_transition_audit_writes_in_session_atomically() -> None:
|
||||
"""F061/F073/F075: the status-transition audit row is written into the
|
||||
CALLER's session (same transaction as the transition), not fire-and-forget
|
||||
on a separate connection.
|
||||
|
||||
Fire-and-forget decouples the audit commit from the transition commit:
|
||||
a transition that rolls back inside a verb savepoint leaves a PHANTOM audit
|
||||
row (F075), and a swallowed persist failure means a committed transition
|
||||
can have NO audit row (F073) — silently corrupting the cycle-time /
|
||||
bottleneck metrics reconstructed from ``task.<status>`` events (F061).
|
||||
Writing the row in-session makes it commit/roll back atomically with the
|
||||
transition, closing all three. Asserted at the unit level: the row is
|
||||
``session.add``-ed (same txn) with the metric-reconstruction details, and
|
||||
NO fire-and-forget background task is spawned.
|
||||
"""The status-transition audit row is written into the CALLER's session (same
|
||||
transaction as the transition), not fire-and-forget on a separate connection,
|
||||
so it commits/rolls back atomically with the transition and cannot diverge
|
||||
from real state. Asserted at the unit level: the row is ``session.add``-ed
|
||||
(same txn) and NO fire-and-forget background task is spawned.
|
||||
"""
|
||||
session = MagicMock()
|
||||
added: list[object] = []
|
||||
|
||||
@@ -78,7 +78,7 @@ async def test_has_unpushed_commits_false_when_session_missing() -> None:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# F062 — merge_pr must be idempotent + active-guarded like close()/complete()
|
||||
# merge_pr must be idempotent + active-guarded like close()/complete()
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -114,9 +114,9 @@ async def test_merge_pr_completes_active_session() -> None:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_merge_pr_idempotent_on_already_completed_preserves_audit_trail() -> None:
|
||||
"""F062 (mode 1): a retried merge after a successful-but-unconfirmed GitHub
|
||||
merge must NOT overwrite the original ``merged_by`` / ``pr_merged_at`` — the
|
||||
merge audit trail is preserved. Mirrors close()'s idempotency guard."""
|
||||
"""A retried merge after a successful-but-unconfirmed GitHub merge must NOT
|
||||
overwrite the original ``merged_by`` / ``pr_merged_at`` — the merge audit
|
||||
trail is preserved. Mirrors close()'s idempotency guard."""
|
||||
|
||||
original_merger = uuid4()
|
||||
original_ts = datetime(2026, 6, 1, 12, 0, tzinfo=UTC)
|
||||
@@ -144,9 +144,9 @@ async def test_merge_pr_idempotent_on_already_completed_preserves_audit_trail()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_merge_pr_does_not_resurrect_abandoned_session() -> None:
|
||||
"""F062 (mode 2): merge_pr on an ABANDONED session must NOT flip it to
|
||||
COMPLETED — that would silently undo the single-active invariant's
|
||||
abandonment and make discarded work look like a successful merge."""
|
||||
"""``merge_pr`` on an ABANDONED session must NOT flip it to COMPLETED — that
|
||||
would silently undo the single-active invariant's abandonment and make
|
||||
discarded work look like a successful merge."""
|
||||
|
||||
ws = MagicMock(status=WorkSessionStatus.ABANDONED, pr_number=42, merged_by=None)
|
||||
svc, session = _merge_service()
|
||||
|
||||
@@ -1,21 +1,9 @@
|
||||
"""F063 — a failed ``_configure_git`` must not leave the project PAT on disk.
|
||||
"""A failed ``_configure_git`` must not leave the project PAT on disk.
|
||||
|
||||
``_clone_repo`` runs ``_do_clone`` (which writes the tokenized auth URL into
|
||||
``.git/config``), then ``_configure_git`` (which scrubs it via
|
||||
``git remote set-url origin <git_url>``), then ``_assert_no_pat_leak``. If
|
||||
``_configure_git`` raises ``CalledProcessError`` BEFORE the scrub completes
|
||||
(disk error, permission issue, broken git), the PAT stays in ``.git/config``
|
||||
and ``_assert_no_pat_leak`` never runs. The except clauses raised
|
||||
``WorkspaceError`` without removing the workspace, so on the next
|
||||
``ensure_workspace`` the health short-circuit (a valid ``.git`` with HEAD +
|
||||
objects) skipped straight past the leak — the agent was then mounted on a
|
||||
workspace whose ``.git/config`` still carried ``https://TOKEN@github.com/...``,
|
||||
letting it read and exfiltrate the project PAT.
|
||||
|
||||
The fix: the clone-failure except clauses ``rmtree`` the workspace before
|
||||
raising, so a half-configured clone is destroyed and the next
|
||||
``ensure_workspace`` re-clones from scratch instead of short-circuiting past
|
||||
the leak.
|
||||
The clone-failure except clauses ``rmtree`` the workspace before raising, so a
|
||||
half-configured clone (PAT still in ``.git/config``) is destroyed and the next
|
||||
``ensure_workspace`` re-clones from scratch instead of short-circuiting past the
|
||||
leak on a valid ``.git`` health check.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
Reference in New Issue
Block a user