Repository navigation
chore(studio): Copy BE wiring for coding agents in Studio #111
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
aa25920
chore(studio): Copy BE wiring and setup FF for coding agents in Studio
dmariali d023cee
cleanup
dmariali 9060e77
Merge branch 'main' into dali/coding-agents-studio
dmariali 841a229
codeQL issue
dmariali a9dcdac
Merge branch 'main' into dali/coding-agents-studio
dmariali daa20d4
Merge branch 'main' into dali/coding-agents-studio
dmariali 6d3d306
Merge branch 'main' into dali/coding-agents-studio
dmariali bb17949
remove extra flagging logic
dmariali ee50725
Merge branch 'dali/coding-agents-studio' of https://github.com/NVIDIA…
dmariali e01fd5d
cleanup flagging logic
dmariali 061c135
Update config-reference.md
dmariali bf2d112
Update config.py
dmariali 705d888
Merge branch 'main' into dali/coding-agents-studio
dmariali d9f5d65
Merge branch 'main' into dali/coding-agents-studio
dmariali f027dc1
Matt's comments
dmariali 4406f7b
Merge branch 'main' into dali/coding-agents-studio
dmariali File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,240 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| """Unit tests for the Studio local coding-agent bridge.""" | ||
|
|
||
| import json | ||
| import uuid | ||
| from pathlib import Path | ||
| from typing import Any | ||
|
|
||
| import pytest | ||
| from fastapi import FastAPI | ||
| from fastapi.testclient import TestClient | ||
| from nmp.studio import coding_agents | ||
| from nmp.studio.config import StudioConfig | ||
| from nmp.studio.service import StudioService | ||
|
|
||
|
|
||
| @pytest.fixture(autouse=True) | ||
| def reset_coding_agent_state(): | ||
| """Reset module-level bridge state between tests.""" | ||
| coding_agents._initialized_sessions.clear() | ||
| coding_agents._session_streams.clear() | ||
| coding_agents._pending_permissions.clear() | ||
| yield | ||
| coding_agents._initialized_sessions.clear() | ||
| coding_agents._session_streams.clear() | ||
| coding_agents._pending_permissions.clear() | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def service_client() -> TestClient: | ||
| service = StudioService().with_config(coding_agents_enabled_config()) | ||
| return TestClient(service.app) | ||
|
|
||
|
|
||
| def coding_agents_enabled_config() -> StudioConfig: | ||
| return StudioConfig(feature_flags={"coding_agents_enabled": True}) | ||
|
|
||
|
|
||
| def test_create_session_returns_uuid(service_client: TestClient): | ||
| response = service_client.post("/v2/coding-agents/sessions") | ||
|
|
||
| assert response.status_code == 200 | ||
| uuid.UUID(response.json()["session_id"]) | ||
|
|
||
|
|
||
| def test_build_claude_argv_uses_new_session_then_resume_flag(): | ||
| session_id = str(uuid.uuid4()) | ||
|
|
||
| argv = coding_agents._build_claude_argv(session_id, "hello", "http://test/mcp") | ||
| assert argv[:3] == ["claude", "-p", "hello"] | ||
| assert "--output-format" in argv | ||
| assert "stream-json" in argv | ||
| assert "--permission-prompt-tool" in argv | ||
| assert f"mcp__{coding_agents.CLAUDE_MCP_SERVER_NAME}__approval_prompt" in argv | ||
| assert "--session-id" in argv | ||
| assert session_id in argv | ||
|
|
||
| coding_agents._initialized_sessions.add(session_id) | ||
| resumed_argv = coding_agents._build_claude_argv(session_id, "again", "http://test/mcp") | ||
| assert "-r" in resumed_argv | ||
| assert "--session-id" not in resumed_argv | ||
|
|
||
|
|
||
| def test_list_and_get_history_sessions( | ||
| service_client: TestClient, | ||
| tmp_path: Path, | ||
| monkeypatch: pytest.MonkeyPatch, | ||
| ): | ||
| workdir = tmp_path / "repo" | ||
| projects_dir = tmp_path / "claude-projects" | ||
| project_dir = projects_dir / str(workdir).replace("/", "-") | ||
| project_dir.mkdir(parents=True) | ||
| session_id = str(uuid.uuid4()) | ||
| history = project_dir / f"{session_id}.jsonl" | ||
| history.write_text( | ||
| "\n".join( | ||
| [ | ||
| json.dumps({"type": "user", "message": {"content": "first prompt"}}), | ||
| json.dumps( | ||
| { | ||
| "type": "assistant", | ||
| "message": { | ||
| "content": [ | ||
| {"type": "thinking", "thinking": "checking"}, | ||
| {"type": "text", "text": "done"}, | ||
| {"type": "tool_use", "name": "Bash", "input": {"command": "pwd"}}, | ||
| ] | ||
| }, | ||
| } | ||
| ), | ||
| json.dumps({"type": "user", "isSidechain": True, "message": {"content": "ignored"}}), | ||
| "not-json", | ||
| ] | ||
| ) | ||
| ) | ||
|
|
||
| monkeypatch.setattr(coding_agents, "SERVER_CWD", workdir) | ||
| monkeypatch.setattr(coding_agents, "CLAUDE_PROJECTS_DIR", projects_dir) | ||
|
|
||
| list_response = service_client.get("/v2/coding-agents/history/sessions") | ||
|
|
||
| assert list_response.status_code == 200 | ||
| assert list_response.json() == [ | ||
| { | ||
| "session_id": session_id, | ||
| "mtime": history.stat().st_mtime, | ||
| "first_prompt": "first prompt", | ||
| "message_count": 1, | ||
| } | ||
| ] | ||
|
|
||
| history_response = service_client.get(f"/v2/coding-agents/history/sessions/{session_id}") | ||
|
|
||
| assert history_response.status_code == 200 | ||
| assert history_response.json() == { | ||
| "session_id": session_id, | ||
| "items": [ | ||
| {"kind": "user", "text": "first prompt"}, | ||
| { | ||
| "kind": "assistant", | ||
| "parts": [ | ||
| {"type": "thinking", "thinking": "checking"}, | ||
| {"type": "text", "text": "done"}, | ||
| {"type": "tool_use", "name": "Bash", "input": {"command": "pwd"}}, | ||
| ], | ||
| }, | ||
| ], | ||
| } | ||
| assert session_id in coding_agents._initialized_sessions | ||
|
|
||
|
|
||
| def test_invalid_session_id_returns_400(service_client: TestClient): | ||
| response = service_client.get("/v2/coding-agents/history/sessions/not-a-uuid") | ||
|
|
||
| assert response.status_code == 400 | ||
| assert response.json()["detail"] == "session_id must be a UUID" | ||
|
|
||
|
|
||
| def test_mcp_initialize_and_tools_list(service_client: TestClient): | ||
| session_id = str(uuid.uuid4()) | ||
|
|
||
| initialize_response = service_client.post( | ||
| f"/v2/coding-agents/mcp/{session_id}", | ||
| json={ | ||
| "jsonrpc": "2.0", | ||
| "id": 1, | ||
| "method": "initialize", | ||
| "params": {"protocolVersion": "2025-06-18"}, | ||
| }, | ||
| ) | ||
| tools_response = service_client.post( | ||
| f"/v2/coding-agents/mcp/{session_id}", | ||
| json={"jsonrpc": "2.0", "id": 2, "method": "tools/list"}, | ||
| ) | ||
|
|
||
| assert initialize_response.status_code == 200 | ||
| assert initialize_response.json()["result"]["serverInfo"]["name"] == "nemo-studio-permissions" | ||
| assert tools_response.status_code == 200 | ||
| assert tools_response.json()["result"]["tools"][0]["name"] == "approval_prompt" | ||
|
|
||
|
|
||
| def test_mcp_tools_call_denies_without_active_stream(service_client: TestClient): | ||
| session_id = str(uuid.uuid4()) | ||
|
|
||
| response = service_client.post( | ||
| f"/v2/coding-agents/mcp/{session_id}", | ||
| json={ | ||
| "jsonrpc": "2.0", | ||
| "id": 1, | ||
| "method": "tools/call", | ||
| "params": { | ||
| "name": "approval_prompt", | ||
| "arguments": {"tool_name": "Bash", "input": {"command": "pwd"}}, | ||
| }, | ||
| }, | ||
| ) | ||
|
|
||
| assert response.status_code == 200 | ||
| result_text = response.json()["result"]["content"][0]["text"] | ||
| assert json.loads(result_text) == { | ||
| "behavior": "deny", | ||
| "message": "no active Studio coding-agent session", | ||
| } | ||
|
|
||
|
|
||
| def test_platform_route_stream_uses_public_mcp_callback(monkeypatch: pytest.MonkeyPatch): | ||
| service = StudioService().with_config(coding_agents_enabled_config()) | ||
| app = FastAPI() | ||
| app.include_router(service.app.router, prefix="/apis/studio") | ||
| service.configure_app(app) | ||
| client = TestClient(app) | ||
| session_id = str(uuid.uuid4()) | ||
| captured: dict[str, Any] = {} | ||
|
|
||
| async def fake_stream(session_id: str, message: str, mcp_url: str): | ||
| captured.update({"session_id": session_id, "message": message, "mcp_url": mcp_url}) | ||
| yield coding_agents._sse(json.dumps({"type": "system", "subtype": "init"})) | ||
| yield coding_agents._sse("", event="done") | ||
|
|
||
| monkeypatch.setattr(coding_agents, "_stream_claude", fake_stream) | ||
|
|
||
| response = client.post( | ||
| f"/apis/studio/v2/coding-agents/sessions/{session_id}/messages", | ||
| json={"message": "hello"}, | ||
| ) | ||
|
|
||
| assert response.status_code == 200 | ||
| assert response.headers["content-type"].startswith("text/event-stream") | ||
| assert "event: done" in response.text | ||
| assert captured == { | ||
| "session_id": session_id, | ||
| "message": "hello", | ||
| "mcp_url": f"http://testserver/studio/api/coding-agents/mcp/{session_id}", | ||
| } | ||
|
|
||
|
|
||
| def test_public_mcp_route_is_mounted_before_static_fallback(): | ||
| service = StudioService().with_config(coding_agents_enabled_config()) | ||
| app = FastAPI() | ||
| service.configure_app(app) | ||
| client = TestClient(app) | ||
| session_id = str(uuid.uuid4()) | ||
|
|
||
| response = client.post( | ||
| f"/studio/api/coding-agents/mcp/{session_id}", | ||
| json={"jsonrpc": "2.0", "id": 1, "method": "tools/list"}, | ||
| ) | ||
|
|
||
| assert response.status_code == 200 | ||
| assert response.json()["result"]["tools"][0]["name"] == "approval_prompt" | ||
|
|
||
|
|
||
| def test_coding_agent_routes_are_disabled_by_default(): | ||
| client = TestClient(StudioService().app) | ||
|
|
||
| response = client.post("/v2/coding-agents/sessions") | ||
|
|
||
| assert response.status_code == 404 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.