mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
feat(toolchain): flag + WorkSession toolchain columns + migration
ROBOCO_TOOLCHAIN_MATCH_ENABLED (default-off) gates the whole subsystem. Adds work_sessions.toolchain_python / toolchain_status (nullable VARCHAR(20)) recording the interpreter the workspace was provisioned with and whether the target's suite can be executed (ok | broken | unknown). Migration 042 verified with a real upgrade head + downgrade -1 + re-upgrade on a throwaway Postgres.
This commit is contained in:
@@ -0,0 +1,45 @@
|
|||||||
|
"""WorkSession toolchain columns.
|
||||||
|
|
||||||
|
Adds two nullable columns to ``work_sessions`` for agent-runtime toolchain
|
||||||
|
matching:
|
||||||
|
|
||||||
|
- ``toolchain_python`` (VARCHAR(20)) — the Python version the agent's workspace
|
||||||
|
was provisioned with (resolved from the target project's ``requires-python``).
|
||||||
|
- ``toolchain_status`` (VARCHAR(20)) — whether the project's test suite can
|
||||||
|
actually be executed in that interpreter: ``ok`` | ``broken`` | ``unknown``.
|
||||||
|
|
||||||
|
Pure schema change; no data backfill. Inert until ``ROBOCO_TOOLCHAIN_MATCH_ENABLED``.
|
||||||
|
|
||||||
|
Revision ID: 042_worksession_toolchain
|
||||||
|
Revises: 041_structured_content_columns
|
||||||
|
Create Date: 2026-06-22
|
||||||
|
|
||||||
|
NOTE: revision id is 25 chars — alembic's ``alembic_version.version_num`` is
|
||||||
|
``VARCHAR(32)`` and a longer id raises at record time.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision = "042_worksession_toolchain"
|
||||||
|
down_revision = "041_structured_content_columns"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column(
|
||||||
|
"work_sessions",
|
||||||
|
sa.Column("toolchain_python", sa.String(length=20), nullable=True),
|
||||||
|
)
|
||||||
|
op.add_column(
|
||||||
|
"work_sessions",
|
||||||
|
sa.Column("toolchain_status", sa.String(length=20), nullable=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column("work_sessions", "toolchain_status")
|
||||||
|
op.drop_column("work_sessions", "toolchain_python")
|
||||||
@@ -175,6 +175,22 @@ class Settings(BaseSettings):
|
|||||||
description="Base URL for Ollama native API (embeddings, model mgmt)",
|
description="Base URL for Ollama native API (embeddings, model mgmt)",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# Agent runtime toolchain matching (default-off)
|
||||||
|
# ==========================================================================
|
||||||
|
# When enabled, an agent's workspace is provisioned with the Python the
|
||||||
|
# TARGET project declares (uv resolves requires-python), and a delivery role
|
||||||
|
# that cannot execute the suite blocks instead of passing on a source read.
|
||||||
|
# When off, provisioning behaves exactly as today (system interpreter).
|
||||||
|
toolchain_match_enabled: bool = Field(
|
||||||
|
default=False,
|
||||||
|
description=(
|
||||||
|
"Provision the agent workspace with the target project's Python "
|
||||||
|
"(uv resolves requires-python) and block delivery gates when the "
|
||||||
|
"suite cannot be executed. Off => today's behavior."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
# ==========================================================================
|
# ==========================================================================
|
||||||
# Web Research (pluggable external search/fetch for Board + PM roles)
|
# Web Research (pluggable external search/fetch for Board + PM roles)
|
||||||
# ==========================================================================
|
# ==========================================================================
|
||||||
|
|||||||
@@ -727,6 +727,11 @@ class WorkSessionTable(Base):
|
|||||||
nullable=True,
|
nullable=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Toolchain matching — the Python the workspace was provisioned with, and
|
||||||
|
# whether the project's test suite can actually be executed in it.
|
||||||
|
toolchain_python: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
||||||
|
toolchain_status: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
||||||
|
|
||||||
# Timestamps
|
# Timestamps
|
||||||
created_at: Mapped[datetime] = mapped_column(
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
DateTime(timezone=True), default=lambda: datetime.now(UTC), nullable=False
|
DateTime(timezone=True), default=lambda: datetime.now(UTC), nullable=False
|
||||||
|
|||||||
@@ -78,6 +78,16 @@ class WorkSession(TimestampMixin):
|
|||||||
default=None, description="Agent who approved/merged the PR"
|
default=None, description="Agent who approved/merged the PR"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Toolchain matching
|
||||||
|
toolchain_python: str | None = Field(
|
||||||
|
default=None,
|
||||||
|
description="Python version the workspace was provisioned with",
|
||||||
|
)
|
||||||
|
toolchain_status: str | None = Field(
|
||||||
|
default=None,
|
||||||
|
description="Whether the project's suite can run: ok | broken | unknown",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class WorkSessionCreate(RobocoBase):
|
class WorkSessionCreate(RobocoBase):
|
||||||
"""Schema for creating a work session."""
|
"""Schema for creating a work session."""
|
||||||
@@ -102,4 +112,6 @@ class WorkSessionUpdate(RobocoBase):
|
|||||||
pr_status: str | None = None
|
pr_status: str | None = None
|
||||||
pr_created_at: datetime | None = None
|
pr_created_at: datetime | None = None
|
||||||
pr_merged_at: datetime | None = None
|
pr_merged_at: datetime | None = None
|
||||||
|
toolchain_python: str | None = None
|
||||||
|
toolchain_status: str | None = None
|
||||||
merged_by: UUID | None = None
|
merged_by: UUID | None = None
|
||||||
|
|||||||
@@ -440,6 +440,19 @@ def _seed_ws(setup: dict, **kwargs: Any) -> WorkSessionTable:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_worksession_toolchain_columns_round_trip(
|
||||||
|
ws_client: dict, db_session: AsyncSession
|
||||||
|
) -> None:
|
||||||
|
"""The toolchain_python / toolchain_status columns persist + round-trip."""
|
||||||
|
ws = _seed_ws(ws_client, toolchain_python="3.14", toolchain_status="ok")
|
||||||
|
db_session.add(ws)
|
||||||
|
await db_session.flush()
|
||||||
|
await db_session.refresh(ws)
|
||||||
|
assert ws.toolchain_python == "3.14"
|
||||||
|
assert ws.toolchain_status == "ok"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_session_by_id_seeded(
|
async def test_get_session_by_id_seeded(
|
||||||
ws_client: dict, db_session: AsyncSession
|
ws_client: dict, db_session: AsyncSession
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
"""The toolchain-match subsystem is gated by a default-off flag."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from roboco.config import Settings
|
||||||
|
|
||||||
|
|
||||||
|
def test_toolchain_match_disabled_by_default() -> None:
|
||||||
|
assert Settings().toolchain_match_enabled is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_toolchain_match_reads_env(monkeypatch) -> None:
|
||||||
|
monkeypatch.setenv("ROBOCO_TOOLCHAIN_MATCH_ENABLED", "true")
|
||||||
|
assert Settings().toolchain_match_enabled is True
|
||||||
Reference in New Issue
Block a user