[sweep] strip Fxxx audit-ID tokens + trim bloated comments/docstrings + add behavior-change docs

Post-audit sweep over the 135 audit-fix commits since 19a474d3:

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:
Renn F
2026-06-29 01:25:40 +02:00
parent fb850e8235
commit 3441e37120
131 changed files with 842 additions and 1391 deletions
@@ -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
+3 -11
View File
@@ -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
+6 -6
View File
@@ -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()
+8 -16
View File
@@ -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"
+9 -23
View File
@@ -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] = []
+7 -7
View File
@@ -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