mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
* fix(board): LEARN decisions name the item, not its per-cycle index A cycle's reject reasons are rendered into the NEXT cycle's exploration prompt, but the ref recorded alongside each reason was the item's stored id (item-0/item-1) — a per-cycle index that means something different every cycle and appears nowhere the explorer can resolve. The reason survived the loop; what it was about did not. Record the item's title instead, via a shared learn_ref() helper (falls back to the id when title-less, and reads target_task_title for Scales, whose items name the live task they mutate). * chore(lint): satisfy ruff 0.16 — keyword-only signatures and markdown formatting The dev toolchain resolved ruff 0.16.0, which stabilises PLR0917 (too many positional arguments) and formats python code blocks inside markdown. Both fired repo-wide and neither had anything to do with the code they flagged. - 36 signatures gain a `*` so their tail arguments are keyword-only, and the 104 call sites that passed them positionally are converted. mypy was the safety net for the static ones; the full suite caught nine more that only bind at runtime (the MCP tool functions, whose real callers already pass named JSON arguments). - 28 markdown files reformatted by 0.16's code-block formatter. - One RUF036 (`None` mid-union) autofixed in the GitLab provider. * fix(gateway): log the reason when a verb rejects A rejected envelope rides an HTTP 200, its body is never logged, and there is no trace table — so in the access log a verb an agent could not satisfy looks identical to one that worked. On 2026-07-25 four Board Programs (Periscope, Sentinel, Scales, Barfly) each POSTed their propose verb three or four times, persisted nothing, and left their exploration tasks PENDING; the reason was unrecoverable afterwards, from the logs or from the agents' own transcripts. Log error/message/remediate/missing plus the calling agent at envelope_to_response — the one chokepoint every v1 flow and do route returns through. Success envelopes stay silent. --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com>
117 lines
2.7 KiB
Markdown
117 lines
2.7 KiB
Markdown
# Testing Standards
|
|
|
|
## Coverage Target
|
|
|
|
Minimum 80% code coverage for all modules.
|
|
|
|
```bash
|
|
# Run with coverage
|
|
uv run pytest --cov=roboco --cov-report=term-missing
|
|
```
|
|
|
|
## Async Tests
|
|
|
|
Use pytest-asyncio:
|
|
|
|
```python
|
|
import pytest
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fetch_user() -> None:
|
|
user = await fetch_user("test-123")
|
|
assert user.name == "Test User"
|
|
```
|
|
|
|
## Test Structure
|
|
|
|
Follow AAA pattern: Arrange, Act, Assert.
|
|
|
|
```python
|
|
@pytest.mark.asyncio
|
|
async def test_task_claim_success() -> None:
|
|
# Arrange
|
|
task = await create_test_task(status=TaskStatus.PENDING)
|
|
agent = await create_test_agent()
|
|
|
|
# Act
|
|
claimed_task = await task_service.claim(task.id, agent.id)
|
|
|
|
# Assert
|
|
assert claimed_task.status == TaskStatus.CLAIMED
|
|
assert claimed_task.assigned_to == agent.id
|
|
```
|
|
|
|
## Test Factories
|
|
|
|
Use factory-boy for test data:
|
|
|
|
```python
|
|
from factory import Factory, Faker, LazyAttribute
|
|
|
|
|
|
class TaskFactory(Factory):
|
|
class Meta:
|
|
model = Task
|
|
|
|
title = Faker("sentence")
|
|
status = TaskStatus.PENDING
|
|
created_at = LazyAttribute(lambda _: datetime.now(UTC))
|
|
```
|
|
|
|
## Before Submitting to QA
|
|
|
|
Run full test suite:
|
|
|
|
```bash
|
|
# Backend
|
|
uv run pytest
|
|
uv run ruff check .
|
|
uv run mypy roboco/
|
|
|
|
# Frontend
|
|
pnpm test
|
|
pnpm lint
|
|
pnpm typecheck
|
|
```
|
|
|
|
## Type Narrowing and mypy
|
|
|
|
When you assign `None` to an attribute inside a test function, mypy narrows that attribute's type to `None`, and does **not** invalidate this narrowing after a function call — even when the called function takes the object as `Any` or modifies it.
|
|
|
|
**Problem:** This causes mypy to treat subsequent assertions as unreachable, failing the quality gate.
|
|
|
|
```python
|
|
# ❌ BAD: mypy narrows notes to None and treats the assertion as unreachable
|
|
def test_example() -> None:
|
|
t = _Task()
|
|
t.notes = None # mypy narrows type to None
|
|
process(t) # Even though process may write to t.notes
|
|
assert t.notes is not None # [unreachable] — mypy sees this as always False
|
|
```
|
|
|
|
**Solution:** Use a helper class whose `__init__` declares the attribute with its full union type, so mypy uses the declared type (not a narrowed literal) when accessed in your test:
|
|
|
|
```python
|
|
# ✅ GOOD: Annotation-typed class preserves union type
|
|
class _TaskWithNoNotes:
|
|
"""Variant where notes starts as None (no prior state)."""
|
|
|
|
def __init__(self) -> None:
|
|
self.id = uuid4()
|
|
self.notes: dict[str, Any] | None = None # Declared as union, not narrowed
|
|
|
|
|
|
def test_example() -> None:
|
|
t = _TaskWithNoNotes() # Use the helper instead
|
|
process(t)
|
|
assert t.notes is not None # ✅ Reachable — mypy sees the union type
|
|
```
|
|
|
|
## Quality Gates
|
|
|
|
All tests MUST pass before:
|
|
- Submitting for verification
|
|
- Creating pull request
|
|
- Merging to main
|