[21e195cd] Panel-wide UI standardization and usability pass (#194)

* [4c179e3a] Add git pull, fetch, and rebase backend endpoints (#190)

* [f966f772] feat(git): add pull, fetch, and rebase endpoints with integration tests (#185)

- Add GitPullRequest/Response, GitFetchRequest/Response, GitRebaseRequest/Response schemas
- Add GitService.pull(), fetch(), and rebase() methods using _network_git_timeout()
- Add POST /api/git/pull, /api/git/fetch, /api/git/rebase route handlers
- Rebase detects conflicts via git diff --name-only --diff-filter=U and aborts cleanly
- Integration tests cover success path and GitCommandError→500 for all three endpoints
- Rebase conflict test verifies conflict=True with populated conflicted_files list

Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>

* [26e2b7af] test(git): add AsyncMock unit tests for rebase_onto_base conflict-state handling (#186)

New test_git_rebase.py covers three branches of rebase_onto_base:
- success path: rebase exits 0, returns rebased status, abort never called
- conflict path: non-zero exit → diff → abort → returns conflict+files
- resilience: both rebase and abort exit non-zero, still returns conflict dict without exception

All tests use AsyncMock with side_effect sequences to mock _run_git at the service-method level.

Co-authored-by: Backend Developer 2 <be-dev-2@agents.roboco.dev>

---------

Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>
Co-authored-by: Backend Developer 2 <be-dev-2@agents.roboco.dev>

* [551b1dbf] Panel-wide frontend UI standardization and page fixes (#193)

* [1ec787b2] feat(panel): design-system sweep — full-width layouts, scrollbar fix, Secretary button, component audit (#188)

- Settings page: remove max-w-3xl, wrap cards in grid-cols-1 lg:grid-cols-2 two-column layout
- AI Providers page: remove max-w-5xl so AIRoutingCard fills available width
- Journals AgentList: replace ScrollArea with overflow-y-auto div to eliminate nested scrollbar
- Secretary chat input: add items-stretch to flex row so Send/Start button matches Textarea height
- Component audit: replace all raw <button>/<input>/hand-rolled badge spans outside components/ui/ with canonical Button, Checkbox, Badge variants across 15 files:
  - ai-routing-card.tsx: ModeButton → Button, checkbox → Checkbox, badge spans → Badge
  - self-hosted-section.tsx: eye-toggle → Button ghost icon-sm, badge spans → Badge
  - journals/agent-item.tsx, communications/channel-item.tsx → Button ghost
  - kb-search-bar.tsx, kb-filters.tsx → Button ghost
  - kb-category-nav.tsx, git-log-panel.tsx → Button ghost
  - communications/page.tsx (channel + group lists) → Button ghost
  - projects/project-table.tsx, products/product-table.tsx → Button link
  - git-branch-panel.tsx (local + remote lists) → Button ghost
  - tasks/dependency-selector.tsx: Button ghost + Checkbox for visual indicator
  - tasks/task-table.tsx: sortable header + expand toggle → Button ghost
  - business/goals-tab.tsx: hidden button → Button

Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>

* [435b37b4] feat(metrics,notifications): URL-persisted tab state, semantic chart colors, humanized counts (#187)

- Notifications page: replace useState with useSearchParams/useRouter for
  ?tab= URL parameter (all/unread/pending, default: unread); Suspense wrapper
  with skeleton fallback for SSR compatibility.

- Metrics page: split into Performance tab (Velocity + Task Status + Agent
  Status + Team Health) and Token Usage tab (TokenUsageCostsSection) with
  ?tab= URL parameter (default: performance); Suspense wrapper; Refresh button
  moved inside PerformanceTabContent; humanizeCount() helper applies K/M
  suffixes to all MetricCard numeric values >= 1000.

- Chart components (usage-time-series, agent-usage, team-usage, model-donut):
  replace var(--chart-N) CSS vars with explicit semantic hex colors —
  #3b82f6 blue for informational, #f59e0b amber for warning/pending,
  #22c55e green for success/healthy, #ef4444 red for error/blocked,
  #a855f7 purple for supplemental.

pnpm lint and pnpm typecheck pass with zero new errors.

Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>

* [ccd256f4] Kanban mobile viewport: 375px layout, column navigation, 44px touch targets (#191)

* [ccd256f4] feat(kanban): mobile 375px layout with column navigator and 44px touch targets

- KanbanBoard: add activeColumnIndex state + mobile prev/next column
  navigator (lg:hidden); existing horizontal-scroll layout hidden on
  mobile (hidden lg:flex). Desktop DnD behavior unchanged.
- KanbanColumn: add optional className prop (cn-based) so mobile view
  can pass w-full/sm:w-full to fill the viewport.
- KanbanCard: bump all action buttons to min-h-11 (44px) touch targets
  (Assign, Pass, Fail, Move-forward).

* [ccd256f4] fix(kanban): change breakpoint from lg to sm for mobile/desktop layout switch

AC3 requires >=640px viewport to show multi-column layout (sm: breakpoint).
Previous impl used lg: (1024px), leaving 640-1023px in single-column mode.

Change:
- Mobile navigator div: lg:hidden → sm:hidden
- Desktop multi-column div: hidden lg:flex → hidden sm:flex

At <640px: single-column with prev/next navigator (375px mobile use case).
At >=640px: full horizontal-scroll multi-column layout (per AC3).
DnD behavior and all other layout unchanged.

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>

* [23f02af4] Agents page On-Demand section + Board composition; Overview Quick Actions visibility + Team Health Intake/Secretary (#189)

* [23f02af4] feat(agents,overview): On-Demand section, Board composition fix, Intake/Secretary in Quick Actions + Team Health

- agent-definitions.ts: remove AgentRole.MAIN_PM from getBoardAgents
  (Main PM has its own dedicated section; including it there was redundant).
  Add getOnDemandAgents() that catches agents not in any standard team
  (board/main_pm/backend/frontend/ux_ui/marketing) and not a standard cell
  role — surfaces prompter/intake agents that the API may return.

- agents/page.tsx: import getOnDemandAgents; add a conditional
  'On-Demand Agents' AgentGrid section (only rendered when the API returns
  at least one matching agent, e.g. the Intake interviewer).

- quick-actions-bar.tsx: add 'Task Intake' button (→/prompter, Sparkles
  icon) and 'Secretary' button (→/business?tab=secretary, Bot icon)
  alongside existing quick actions so operators can reach on-demand agents
  from the Overview in one click.

- team-health-cards.tsx: add OnDemandAgentCard sub-component (link card
  with On-Demand badge) and render static cards for 'Task Intake' and
  'Secretary' appended after the API-driven TeamHealthCard list, giving
  them equal visual presence in the Team Health section.

pnpm lint and pnpm typecheck pass with zero new errors.

* [23f02af4] fix(agents,overview): QA revision — enum entries, QuickActions placement, On-Demand title, Board PR_REVIEWER

AC3: types/index.ts AgentRole enum adds PR_REVIEWER, PROMPTER, SECRETARY.
     agent-selector.tsx ROLE_LABELS exhaustive Record updated accordingly.

AC4: command-center.tsx QuickActionsBar moved to after Team Health section,
     before CEO Approval Queue and data-heavy grid rows — visible without
     scrolling on a 900px viewport.

AC1: agents/page.tsx On-Demand AgentGrid title fixed to 'On-Demand'
     (was 'On-Demand Agents' in prior commit).

AC2: agent-definitions.ts getBoardAgents adds explicit PR_REVIEWER inclusion
     and uses inclusion-based getOnDemandAgents (PROMPTER|SECRETARY roles).

AC5: team-health-cards.tsx static OnDemandAgentCard implementation refined
     with correct fallback rendering when no API team data.

AC6: pnpm lint and pnpm typecheck (src only) pass with zero new errors.

---------

Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>
Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>

* [b1c59206] Git page: Pull, Fetch, Rebase buttons wired to backend; Rebase destructive confirmation dialog (#192)

* [b1c59206] feat(git): add Pull, Fetch, Rebase operations to Git page with destructive confirmation dialog for Rebase

- Add GitPullRequest/Response, GitFetchRequest/Response, GitRebaseRequest/Response types
- Add gitApi.pull(), gitApi.fetch(), gitApi.rebase() with mock stubs for /git/pull, /git/fetch, /git/rebase
- Add useGitPull, useGitFetch, useGitRebase mutation hooks with cache invalidation; exported via useGitOperations
- Add Pull (Download icon), Fetch (RefreshCcw icon), Rebase (GitGraph icon) buttons to GitActionsPanel
- Rebase button triggers AlertDialog with destructive confirmation before calling API
- Wire handlePull, handleFetch, handleRebase handlers in git-browser.tsx with toast feedback

* [b1c59206] fix(git): add destructive styling and branch name to Rebase AlertDialog

- Add className='border-destructive bg-destructive/5' to AlertDialogContent
  so the dialog container has the required red-tinted styling (AC3)
- Update AlertDialogDescription to interpolate status?.current_branch so
  the dialog body explicitly names the branch being rebased (AC3)

---------

Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>

* [3f305ed9] Frontend: Fix git control contract, complete Secretary restyling, and apply polish (CEO revision) (#199)

* [72de8a65] fix(git): correct Pull/Fetch/Rebase types, API mocks, request fields, and toast handlers (#197)

- types/git.ts: GitPullResponse and GitFetchResponse now have current_branch,
  has_changes, staged_files, unstaged_files, untracked_files, ahead, behind
  (matching backend GitStatusResponse); removed nonexistent commits_received/
  refs_updated/remote fields
- types/git.ts: GitRebaseRequest now uses target_branch: string (not onto?: string);
  GitRebaseResponse now has conflict: boolean and conflicted_files: string[]
  (removed branch/onto/commits_rebased); task_id made optional on all three
  request types
- lib/api/git.ts: Updated mock returns for pull/fetch/rebase to match new types
- git-actions-panel.tsx: onRebase prop now (targetBranch: string) => void;
  Rebase AlertDialog now contains an Input for target_branch; AlertDialogAction
  disabled when targetBranch empty and passes the value to onRebase
- git-browser.tsx: handlePull and handleFetch toast references result.current_branch;
  handleRebase accepts targetBranch, sends target_branch in payload, toasts
  result.conflict and result.conflicted_files; no 'manual' task_id for any
  pull/fetch/rebase operation

Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>

* [be6a17fc] feat(ui): design-system polish — chart tokens, KB aria-label, Kanban touch targets (#196)

- kb-search-bar.tsx: add aria-label="Clear search" to the clear (X) button
- model-usage-donut.tsx: replace hex CHART_COLORS with var(--chart-1)…var(--chart-5)
- usage-time-series-chart.tsx: replace hex stopColor/stroke with var(--chart-1)/var(--chart-2)
- agent-usage-chart.tsx: Bar fill hex → var(--chart-1)
- team-usage-chart.tsx: Bar fill hex → var(--chart-1)
- kanban-card.tsx: min-h-11 → max-sm:min-h-11 (44px touch target mobile-only, 3 buttons)
- secretary-tab.tsx: already compliant (Button + design-system tokens), no change needed

Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>

* [d62036bd] Backend: Fix git endpoint schemas, add safety gates, and unit tests (CEO revision) (#200)

* [d0593fe3] feat(git): remove agent_id from schemas and add service-layer safety gates (#195)

- Remove agent_id field from all 9 git request schemas (GitCreateBranchRequest, GitCheckoutRequest, GitCommitRequest, GitPushRequest, GitCreatePRRequest, GitMergePRRequest, GitPullRequest, GitFetchRequest, GitRebaseRequest); agent identity comes from JWT auth context
- Make task_id Optional[UUID]=None in GitPullRequest, GitFetchRequest, GitRebaseRequest
- Add field_validator to GitRebaseRequest rejecting target_branch starting with '-' or equal to 'master'/'main'
- Add lightweight PullRequest, FetchRequest, RebaseRequest schemas for gateway layer
- Add dirty-workspace check to GitService.pull() (raises ValidationError if porcelain output)
- Switch GitService.pull() to --ff-only; raises ValidationError with diverged-branch message on non-zero exit
- Add master/main guard to GitService.rebase() for both head_branch and target_branch
- Update callers: routes/tasks.py (2x), services/task.py, tests/unit/services/test_git.py (2x)

Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>

* [a2f96961] Add role-gated rebase endpoint and unit tests (test_git_rebase.py) (#198)

* [a2f96961] feat(git): add role-gated rebase endpoint and unit tests

Add role-gate to POST /rebase restricting access to DEVELOPER and
CELL_PM roles; add master/main protected-branch guard to
GitService.rebase() before any git subprocess runs; add 4 unit tests
in tests/unit/services/test_git_rebase.py covering both
target-branch and head-branch REBASE_FORBIDDEN cases

* [a2f96961] fix(git): invert rebase role gate, add ownership check, schema validator, and missing tests

- _REBASE_ALLOWED_ROLES changed from {DEVELOPER, CELL_PM} to {CEO, CELL_PM, MAIN_PM}
  so developers correctly receive 403 per AC1/AC2
- rebase_branch() now verifies task ownership for non-CEO PM callers: if task_id
  is provided and the task is not assigned to the calling agent, returns 403/404
- GitRebaseRequest.target_branch gets a @field_validator rejecting '-' prefix
  names and protected branch names (main, master, develop)
- GitService.pull() gains pre-flight safety gates: raises ValidationError
  DIRTY_TREE when staged/unstaged changes exist, DIVERGED_BRANCH when
  ahead > 0 and behind > 0
- test_git_rebase.py adds 9 new tests: pull() dirty-tree ValidationError,
  pull() diverged-branch ValidationError, pull() success path, schema
  validator for '-' prefix and protected names, and route-level tests
  confirming HTTP 403 for DEVELOPER and HTTP 200 for CELL_PM on POST /rebase

* [a2f96961] fix(tests): add type annotations for tuple variables in test_git_rebase.py

mypy needs explicit tuple type annotations when assigning bare tuples
to variables used as mock side_effect return values — fixes var-annotated
error caught by the server-side quality gate

---------

Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>
Co-authored-by: Renn F <rennf93@users.noreply.github.com>

---------

Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>
Co-authored-by: Renn F <rennf93@users.noreply.github.com>

* [94015c6d] Frontend R3: Fix legacy git taskId coercion + rebase placeholder + phantom fields (#204)

* [401ddb40] fix(git): remove phantom fields from GitPullRequest/GitFetchRequest and make task_id optional in write request interfaces; use taskId || undefined in git-browser.tsx handlers to avoid 422 errors when no task context is active (#201)

Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>

* [cca8d0c0] fix(git): fix rebase placeholder and surface backend error in toast (#202)

git-actions-panel.tsx: change rebase target_branch Input placeholder
from "e.g. main or origin/main" to "Remote ref (e.g. origin/HEAD)" so
no default branch name (main/master/develop) is suggested.

git-browser.tsx: import getErrorMessage from @/lib/api/client and use
it in handleRebase catch block instead of the hardcoded string "Failed
to rebase". getErrorMessage extracts the real detail from
AxiosError.response.data.detail and falls back to a non-empty generic
message, satisfying both the detail-surfacing and fallback criteria.

Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>

* [1ea0fbcb] Backend R3: Relax legacy git schemas + fix integration tests (#206)

* [219c539b] Make task_id Optional in git request schemas and update service methods (#205)

* [219c539b] feat(git): make task_id Optional in git schemas and add None-guards in service methods

- GitCommitRequest, GitPushRequest, GitCreatePRRequest, GitMergePRRequest now have task_id: UUID | None = None
- commit_for_task, push_for_task, create_pr_for_task, merge_pr_for_task skip ownership/state checks when task_id is None and proceed to the git operation
- Added 16 unit tests in tests/unit/api/routes/test_git_optional_task_id.py covering schema validation and HTTP endpoint responses
- Added 4 integration tests in tests/integration/test_git_routes.py for no-422 behaviour
- All quality gates pass: ruff format, ruff check, mypy, pytest

* [219c539b] fix(tests): remove unused type-ignore comments, redundant cast, and invalid agent_id kwarg in git_optional_task_id unit tests

---------

Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>

* [de95ce94] test(git): fix 3 rebase integration tests to use non-protected target_branch (#203)

- Add pm_git_client fixture (CELL_PM role) needed for the role-gated rebase endpoint
- Change target_branch from 'main' to 'develop' in test_rebase_success, test_rebase_conflict, and test_rebase_git_command_error
- Remove task_id from request bodies (optional field; random UUIDs trigger 404)
- Switch all 3 rebase tests to use pm_git_client instead of git_client

Co-authored-by: Backend Developer 2 <be-dev-2@agents.roboco.dev>

---------

Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>
Co-authored-by: Backend Developer 2 <be-dev-2@agents.roboco.dev>

* chore: ruff format test_agent_image_registry.py (unblock quality gate)

---------

Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>
Co-authored-by: Backend Developer 2 <be-dev-2@agents.roboco.dev>
Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-06-17 06:36:28 +02:00
committed by GitHub
co-authored by Backend Developer 2 Backend Developer 1 Frontend Developer 1 Frontend Developer 2 Renn F
parent e71a746499
commit 818f2ac7a6
48 changed files with 2867 additions and 575 deletions
+315
View File
@@ -85,6 +85,59 @@ async def git_client(
app.dependency_overrides.clear()
@pytest_asyncio.fixture
async def pm_git_client(
db_session: AsyncSession,
) -> AsyncIterator[dict]:
"""Like git_client but with CELL_PM role — required for the rebase endpoint."""
agent = AgentTable(
id=uuid4(),
name="PM",
slug=f"be-pm-{uuid4().hex[:8]}",
role=AgentRole.CELL_PM,
team=Team.BACKEND,
status=AgentStatus.ACTIVE,
model_config={},
system_prompt="pm",
capabilities=[],
permissions={},
metrics={},
)
db_session.add(agent)
await db_session.flush()
project = ProjectTable(
id=uuid4(),
name="GitProj",
slug=f"git-proj-{uuid4().hex[:6]}",
git_url="https://example.com/r.git",
assigned_cell=Team.BACKEND,
created_by=agent.id,
)
db_session.add(project)
await db_session.flush()
app = FastAPI()
app.include_router(git_router, prefix="/api/git")
async def _override_db() -> AsyncGenerator[AsyncSession]:
yield db_session
async def _override_agent() -> AgentContext:
return AgentContext(
agent_id=cast("uuid.UUID", agent.id),
role=AgentRole.CELL_PM,
team=Team.BACKEND,
)
app.dependency_overrides[get_db] = _override_db
app.dependency_overrides[get_agent_context] = _override_agent
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
yield {"client": client, "agent": agent, "project": project, "db": db_session}
app.dependency_overrides.clear()
_HDR = {"X-Agent-ID": str(uuid4()), "X-Agent-Role": "developer"}
@@ -681,5 +734,267 @@ async def test_status_generic_service_error(git_client: dict) -> None:
assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR
# ---------------------------------------------------------------------------
# pull
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_pull_success(git_client: dict) -> None:
with patch("roboco.api.routes.git.get_git_service") as mock_get:
svc = AsyncMock()
svc.get_workspace = AsyncMock(return_value="/tmp/ws")
svc.pull = AsyncMock(return_value=("main", False, [], [], [], 0, 0))
mock_get.return_value = svc
response = await git_client["client"].post(
"/api/git/pull",
json={
"project_slug": git_client["project"].slug,
"task_id": str(uuid4()),
"agent_id": str(uuid4()),
},
headers=_HDR,
)
assert response.status_code == HTTPStatus.OK
data = response.json()
assert data["current_branch"] == "main"
assert "ahead" in data
assert "behind" in data
assert "has_changes" in data
assert "staged_files" in data
assert "unstaged_files" in data
assert "untracked_files" in data
@pytest.mark.asyncio
async def test_pull_git_command_error(git_client: dict) -> None:
with patch("roboco.api.routes.git.get_git_service") as mock_get:
svc = AsyncMock()
svc.get_workspace = AsyncMock(return_value="/tmp/ws")
svc.pull = AsyncMock(side_effect=GitCommandError("pull", "network error"))
mock_get.return_value = svc
response = await git_client["client"].post(
"/api/git/pull",
json={
"project_slug": git_client["project"].slug,
"task_id": str(uuid4()),
"agent_id": str(uuid4()),
},
headers=_HDR,
)
assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR
# ---------------------------------------------------------------------------
# fetch
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_fetch_success(git_client: dict) -> None:
with patch("roboco.api.routes.git.get_git_service") as mock_get:
svc = AsyncMock()
svc.get_workspace = AsyncMock(return_value="/tmp/ws")
_ahead = 2
_behind = 1
svc.fetch = AsyncMock(
return_value=("feature/x", True, ["a.py"], [], [], _ahead, _behind)
)
mock_get.return_value = svc
response = await git_client["client"].post(
"/api/git/fetch",
json={
"project_slug": git_client["project"].slug,
"task_id": str(uuid4()),
"agent_id": str(uuid4()),
},
headers=_HDR,
)
assert response.status_code == HTTPStatus.OK
data = response.json()
assert data["current_branch"] == "feature/x"
assert data["ahead"] == _ahead
assert data["behind"] == _behind
assert data["has_changes"] is True
assert "staged_files" in data
assert "unstaged_files" in data
assert "untracked_files" in data
@pytest.mark.asyncio
async def test_fetch_git_command_error(git_client: dict) -> None:
with patch("roboco.api.routes.git.get_git_service") as mock_get:
svc = AsyncMock()
svc.get_workspace = AsyncMock(return_value="/tmp/ws")
svc.fetch = AsyncMock(side_effect=GitCommandError("fetch", "network error"))
mock_get.return_value = svc
response = await git_client["client"].post(
"/api/git/fetch",
json={
"project_slug": git_client["project"].slug,
"task_id": str(uuid4()),
"agent_id": str(uuid4()),
},
headers=_HDR,
)
assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR
# ---------------------------------------------------------------------------
# rebase
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_rebase_success(pm_git_client: dict) -> None:
with patch("roboco.api.routes.git.get_git_service") as mock_get:
svc = AsyncMock()
svc.get_workspace = AsyncMock(return_value="/tmp/ws")
svc.rebase = AsyncMock(return_value=(False, []))
mock_get.return_value = svc
response = await pm_git_client["client"].post(
"/api/git/rebase",
json={
"project_slug": pm_git_client["project"].slug,
"target_branch": "develop",
},
headers=_HDR,
)
assert response.status_code == HTTPStatus.OK
data = response.json()
assert data["conflict"] is False
assert data["conflicted_files"] == []
@pytest.mark.asyncio
async def test_rebase_conflict(pm_git_client: dict) -> None:
with patch("roboco.api.routes.git.get_git_service") as mock_get:
svc = AsyncMock()
svc.get_workspace = AsyncMock(return_value="/tmp/ws")
svc.rebase = AsyncMock(return_value=(True, ["src/foo.py", "src/bar.py"]))
mock_get.return_value = svc
response = await pm_git_client["client"].post(
"/api/git/rebase",
json={
"project_slug": pm_git_client["project"].slug,
"target_branch": "develop",
},
headers=_HDR,
)
assert response.status_code == HTTPStatus.OK
data = response.json()
assert data["conflict"] is True
assert data["conflicted_files"] == ["src/foo.py", "src/bar.py"]
@pytest.mark.asyncio
async def test_rebase_git_command_error(pm_git_client: dict) -> None:
with patch("roboco.api.routes.git.get_git_service") as mock_get:
svc = AsyncMock()
svc.get_workspace = AsyncMock(return_value="/tmp/ws")
svc.rebase = AsyncMock(side_effect=GitCommandError("rebase", "fatal error"))
mock_get.return_value = svc
response = await pm_git_client["client"].post(
"/api/git/rebase",
json={
"project_slug": pm_git_client["project"].slug,
"target_branch": "develop",
},
headers=_HDR,
)
assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR
# ---------------------------------------------------------------------------
# task_id Optional — no 422 when task_id is omitted
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_commit_without_task_id_no_422(git_client: dict) -> None:
"""POST /commit without task_id must not return 422 (schema validation error)."""
with patch("roboco.api.routes.git.get_git_service") as mock_get:
svc = AsyncMock()
svc.commit_for_task = AsyncMock(
return_value=("abc123", "feat: add thing", 1, 5, 2)
)
mock_get.return_value = svc
response = await git_client["client"].post(
"/api/git/commit",
json={
"project_slug": git_client["project"].slug,
"agent_id": str(uuid4()),
"message": "add a new thing",
"commit_type": "feat",
},
headers=_HDR,
)
assert response.status_code != HTTPStatus.UNPROCESSABLE_ENTITY
assert response.status_code == HTTPStatus.OK
@pytest.mark.asyncio
async def test_push_without_task_id_no_422(git_client: dict) -> None:
"""POST /push without task_id must not return 422 (schema validation error)."""
with patch("roboco.api.routes.git.get_git_service") as mock_get:
svc = AsyncMock()
svc.push_for_task = AsyncMock(return_value=("feature/x", 3))
mock_get.return_value = svc
response = await git_client["client"].post(
"/api/git/push",
json={
"project_slug": git_client["project"].slug,
},
headers=_HDR,
)
assert response.status_code != HTTPStatus.UNPROCESSABLE_ENTITY
assert response.status_code == HTTPStatus.OK
@pytest.mark.asyncio
async def test_create_pr_without_task_id_no_422(git_client: dict) -> None:
"""POST /pr/create without task_id must not return 422 (schema validation error)."""
with patch("roboco.api.routes.git.get_git_service") as mock_get:
svc = AsyncMock()
svc.create_pr_for_task = AsyncMock(
return_value=(
7,
"https://github.com/x/y/pull/7",
"feat: add thing",
"feat/x",
"main",
)
)
mock_get.return_value = svc
response = await git_client["client"].post(
"/api/git/pr/create",
json={
"project_slug": git_client["project"].slug,
},
headers=_HDR,
)
assert response.status_code != HTTPStatus.UNPROCESSABLE_ENTITY
assert response.status_code == HTTPStatus.OK
@pytest.mark.asyncio
async def test_merge_pr_without_task_id_no_422(git_client: dict) -> None:
"""POST /pr/merge without task_id must not return 422 (schema validation error)."""
with patch("roboco.api.routes.git.get_git_service") as mock_get:
svc = AsyncMock()
svc.merge_pr_for_task = AsyncMock(return_value=("main", "deadbeef"))
mock_get.return_value = svc
response = await git_client["client"].post(
"/api/git/pr/merge",
json={
"project_slug": git_client["project"].slug,
"pr_number": 99,
},
headers=_HDR,
)
assert response.status_code != HTTPStatus.UNPROCESSABLE_ENTITY
assert response.status_code == HTTPStatus.OK
# Re-export to keep import alive (TC reorders imports)
_ = SimpleNamespace