v2.9.2 (#645)
Co-authored-by: Clément Drouin <clement.drouin@mistral.ai> Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai> Co-authored-by: Lucas Marandat <31749711+lucasmrdt@users.noreply.github.com> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
1fd7eea289
commit
3b8f65b306
16 changed files with 300 additions and 52 deletions
|
|
@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [2.9.2] - 2026-04-29
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Teleport surfaces the latest GitHub connection status while polling
|
||||||
|
|
||||||
|
|
||||||
## [2.9.1] - 2026-04-29
|
## [2.9.1] - 2026-04-29
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
id = "mistral-vibe"
|
id = "mistral-vibe"
|
||||||
name = "Mistral Vibe"
|
name = "Mistral Vibe"
|
||||||
description = "Mistral's open-source coding assistant"
|
description = "Mistral's open-source coding assistant"
|
||||||
version = "2.9.1"
|
version = "2.9.2"
|
||||||
schema_version = 1
|
schema_version = 1
|
||||||
authors = ["Mistral AI"]
|
authors = ["Mistral AI"]
|
||||||
repository = "https://github.com/mistralai/mistral-vibe"
|
repository = "https://github.com/mistralai/mistral-vibe"
|
||||||
|
|
@ -11,25 +11,25 @@ name = "Mistral Vibe"
|
||||||
icon = "./icons/mistral_vibe.svg"
|
icon = "./icons/mistral_vibe.svg"
|
||||||
|
|
||||||
[agent_servers.mistral-vibe.targets.darwin-aarch64]
|
[agent_servers.mistral-vibe.targets.darwin-aarch64]
|
||||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.1/vibe-acp-darwin-aarch64-2.9.1.zip"
|
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.2/vibe-acp-darwin-aarch64-2.9.2.zip"
|
||||||
cmd = "./vibe-acp"
|
cmd = "./vibe-acp"
|
||||||
|
|
||||||
[agent_servers.mistral-vibe.targets.darwin-x86_64]
|
[agent_servers.mistral-vibe.targets.darwin-x86_64]
|
||||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.1/vibe-acp-darwin-x86_64-2.9.1.zip"
|
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.2/vibe-acp-darwin-x86_64-2.9.2.zip"
|
||||||
cmd = "./vibe-acp"
|
cmd = "./vibe-acp"
|
||||||
|
|
||||||
[agent_servers.mistral-vibe.targets.linux-aarch64]
|
[agent_servers.mistral-vibe.targets.linux-aarch64]
|
||||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.1/vibe-acp-linux-aarch64-2.9.1.zip"
|
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.2/vibe-acp-linux-aarch64-2.9.2.zip"
|
||||||
cmd = "./vibe-acp"
|
cmd = "./vibe-acp"
|
||||||
|
|
||||||
[agent_servers.mistral-vibe.targets.linux-x86_64]
|
[agent_servers.mistral-vibe.targets.linux-x86_64]
|
||||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.1/vibe-acp-linux-x86_64-2.9.1.zip"
|
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.2/vibe-acp-linux-x86_64-2.9.2.zip"
|
||||||
cmd = "./vibe-acp"
|
cmd = "./vibe-acp"
|
||||||
|
|
||||||
[agent_servers.mistral-vibe.targets.windows-aarch64]
|
[agent_servers.mistral-vibe.targets.windows-aarch64]
|
||||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.1/vibe-acp-windows-aarch64-2.9.1.zip"
|
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.2/vibe-acp-windows-aarch64-2.9.2.zip"
|
||||||
cmd = "./vibe-acp.exe"
|
cmd = "./vibe-acp.exe"
|
||||||
|
|
||||||
[agent_servers.mistral-vibe.targets.windows-x86_64]
|
[agent_servers.mistral-vibe.targets.windows-x86_64]
|
||||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.1/vibe-acp-windows-x86_64-2.9.1.zip"
|
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.2/vibe-acp-windows-x86_64-2.9.2.zip"
|
||||||
cmd = "./vibe-acp.exe"
|
cmd = "./vibe-acp.exe"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[project]
|
[project]
|
||||||
name = "mistral-vibe"
|
name = "mistral-vibe"
|
||||||
version = "2.9.1"
|
version = "2.9.2"
|
||||||
description = "Minimal CLI coding agent by Mistral"
|
description = "Minimal CLI coding agent by Mistral"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ class TestACPInitialize:
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
assert response.agent_info == Implementation(
|
assert response.agent_info == Implementation(
|
||||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.9.1"
|
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.9.2"
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.auth_methods == []
|
assert response.auth_methods == []
|
||||||
|
|
@ -62,7 +62,7 @@ class TestACPInitialize:
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
assert response.agent_info == Implementation(
|
assert response.agent_info == Implementation(
|
||||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.9.1"
|
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.9.2"
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.auth_methods is not None
|
assert response.auth_methods is not None
|
||||||
|
|
|
||||||
128
tests/core/test_config_schema.py
Normal file
128
tests/core/test_config_schema.py
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from vibe.core.config import (
|
||||||
|
ConfigDefinitionError,
|
||||||
|
ConfigFragment,
|
||||||
|
ConfigSchema,
|
||||||
|
WithConcatMerge,
|
||||||
|
WithReplaceMerge,
|
||||||
|
WithShallowMerge,
|
||||||
|
WithUnionMerge,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ToolSettings(BaseModel):
|
||||||
|
enabled: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
class ModelDefinition(BaseModel):
|
||||||
|
alias: str
|
||||||
|
provider: str
|
||||||
|
|
||||||
|
|
||||||
|
class ToolsFragment(ConfigFragment):
|
||||||
|
disabled_tools: Annotated[list[str], WithConcatMerge()] = Field(
|
||||||
|
default_factory=list
|
||||||
|
)
|
||||||
|
tool_settings: Annotated[dict[str, ToolSettings], WithShallowMerge()] = Field(
|
||||||
|
default_factory=dict
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ModelsFragment(ConfigFragment):
|
||||||
|
default_model: Annotated[str, WithReplaceMerge()] = "devstral-small"
|
||||||
|
available_models: Annotated[
|
||||||
|
list[ModelDefinition], WithUnionMerge(merge_key="alias")
|
||||||
|
] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class MiniVibeConfigSchema(ConfigSchema):
|
||||||
|
active_model: Annotated[str, WithReplaceMerge()] = "devstral-2"
|
||||||
|
models: ModelsFragment = Field(default_factory=ModelsFragment)
|
||||||
|
tools: ToolsFragment = Field(default_factory=ToolsFragment)
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_fragment_accepts_merge_aware_fields() -> None:
|
||||||
|
fragment = ToolsFragment(
|
||||||
|
disabled_tools=["bash"], tool_settings={"search": ToolSettings(enabled=False)}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert fragment.disabled_tools == ["bash"]
|
||||||
|
assert fragment.tool_settings["search"].enabled is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_fragment_rejects_plain_fields() -> None:
|
||||||
|
with pytest.raises(
|
||||||
|
ConfigDefinitionError, match="BrokenFragment.name must declare merge metadata"
|
||||||
|
):
|
||||||
|
|
||||||
|
class BrokenFragment(ConfigFragment):
|
||||||
|
name: str = "bash"
|
||||||
|
|
||||||
|
|
||||||
|
def test_scenario_mini_vibe_config_schema() -> None:
|
||||||
|
config = MiniVibeConfigSchema(
|
||||||
|
active_model="codestral-latest",
|
||||||
|
models=ModelsFragment(
|
||||||
|
available_models=[
|
||||||
|
ModelDefinition(alias="devstral-small", provider="mistral"),
|
||||||
|
ModelDefinition(alias="codestral-latest", provider="mistral"),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
tools=ToolsFragment(disabled_tools=["bash"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert config.active_model == "codestral-latest"
|
||||||
|
assert config.models.default_model == "devstral-small"
|
||||||
|
assert [model.alias for model in config.models.available_models] == [
|
||||||
|
"devstral-small",
|
||||||
|
"codestral-latest",
|
||||||
|
]
|
||||||
|
assert config.tools.disabled_tools == ["bash"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_schema_rejects_plain_top_level_fields() -> None:
|
||||||
|
with pytest.raises(
|
||||||
|
ConfigDefinitionError,
|
||||||
|
match="BrokenSchema.active_model must declare merge metadata",
|
||||||
|
):
|
||||||
|
|
||||||
|
class BrokenSchema(ConfigSchema):
|
||||||
|
active_model: str = "devstral-2"
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_schema_rejects_plain_nested_models() -> None:
|
||||||
|
with pytest.raises(
|
||||||
|
ConfigDefinitionError,
|
||||||
|
match="BrokenSchema.tools must declare merge metadata or use a ConfigFragment subclass",
|
||||||
|
):
|
||||||
|
|
||||||
|
class BrokenSchema(ConfigSchema):
|
||||||
|
tools: ToolSettings = Field(default_factory=ToolSettings)
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_schema_rejects_merge_metadata_on_fragments() -> None:
|
||||||
|
with pytest.raises(
|
||||||
|
ConfigDefinitionError,
|
||||||
|
match="BrokenSchema.tools is a ConfigFragment field and must not declare merge metadata",
|
||||||
|
):
|
||||||
|
|
||||||
|
class BrokenSchema(ConfigSchema):
|
||||||
|
tools: Annotated[ToolsFragment, WithReplaceMerge()] = Field(
|
||||||
|
default_factory=ToolsFragment
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_schema_rejects_optional_fragments() -> None:
|
||||||
|
with pytest.raises(
|
||||||
|
ConfigDefinitionError,
|
||||||
|
match="BrokenSchema.tools must declare merge metadata or use a ConfigFragment subclass",
|
||||||
|
):
|
||||||
|
|
||||||
|
class BrokenSchema(ConfigSchema):
|
||||||
|
tools: ToolsFragment | None = None
|
||||||
|
|
@ -23,9 +23,10 @@ class TestWaitForGithubConnection:
|
||||||
connected = GitHubPublicData(status=GitHubStatus.CONNECTED)
|
connected = GitHubPublicData(status=GitHubStatus.CONNECTED)
|
||||||
client.get_github_integration = AsyncMock(return_value=connected)
|
client.get_github_integration = AsyncMock(return_value=connected)
|
||||||
|
|
||||||
result = await client.wait_for_github_connection("exec-1")
|
results = [data async for data in client.wait_for_github_connection("exec-1")]
|
||||||
|
|
||||||
assert result.connected is True
|
assert len(results) == 1
|
||||||
|
assert results[0].connected is True
|
||||||
client.get_github_integration.assert_called_once_with("exec-1")
|
client.get_github_integration.assert_called_once_with("exec-1")
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|
@ -40,9 +41,12 @@ class TestWaitForGithubConnection:
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch("vibe.core.teleport.nuage.asyncio.sleep", new_callable=AsyncMock):
|
with patch("vibe.core.teleport.nuage.asyncio.sleep", new_callable=AsyncMock):
|
||||||
result = await client.wait_for_github_connection("exec-1")
|
results = [
|
||||||
|
data async for data in client.wait_for_github_connection("exec-1")
|
||||||
|
]
|
||||||
|
|
||||||
assert result.connected is True
|
assert len(results) == 3
|
||||||
|
assert results[-1].connected is True
|
||||||
assert client.get_github_integration.call_count == 3
|
assert client.get_github_integration.call_count == 3
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|
@ -53,7 +57,8 @@ class TestWaitForGithubConnection:
|
||||||
client.get_github_integration = AsyncMock(return_value=error_data)
|
client.get_github_integration = AsyncMock(return_value=error_data)
|
||||||
|
|
||||||
with pytest.raises(ServiceTeleportError, match="App not installed"):
|
with pytest.raises(ServiceTeleportError, match="App not installed"):
|
||||||
await client.wait_for_github_connection("exec-1")
|
async for _ in client.wait_for_github_connection("exec-1"):
|
||||||
|
pass
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_raises_on_oauth_timeout(self, client: NuageClient) -> None:
|
async def test_raises_on_oauth_timeout(self, client: NuageClient) -> None:
|
||||||
|
|
@ -61,7 +66,8 @@ class TestWaitForGithubConnection:
|
||||||
client.get_github_integration = AsyncMock(return_value=timeout_data)
|
client.get_github_integration = AsyncMock(return_value=timeout_data)
|
||||||
|
|
||||||
with pytest.raises(ServiceTeleportError, match="oauth_timeout"):
|
with pytest.raises(ServiceTeleportError, match="oauth_timeout"):
|
||||||
await client.wait_for_github_connection("exec-1")
|
async for _ in client.wait_for_github_connection("exec-1"):
|
||||||
|
pass
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_raises_on_timeout(self, client: NuageClient) -> None:
|
async def test_raises_on_timeout(self, client: NuageClient) -> None:
|
||||||
|
|
@ -77,7 +83,8 @@ class TestWaitForGithubConnection:
|
||||||
patch("vibe.core.teleport.nuage.asyncio.sleep", new_callable=AsyncMock),
|
patch("vibe.core.teleport.nuage.asyncio.sleep", new_callable=AsyncMock),
|
||||||
pytest.raises(ServiceTeleportError, match="timed out"),
|
pytest.raises(ServiceTeleportError, match="timed out"),
|
||||||
):
|
):
|
||||||
await client.wait_for_github_connection("exec-1", timeout=600.0)
|
async for _ in client.wait_for_github_connection("exec-1", timeout=600.0):
|
||||||
|
pass
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_sleeps_with_correct_interval(self, client: NuageClient) -> None:
|
async def test_sleeps_with_correct_interval(self, client: NuageClient) -> None:
|
||||||
|
|
@ -88,8 +95,31 @@ class TestWaitForGithubConnection:
|
||||||
with patch(
|
with patch(
|
||||||
"vibe.core.teleport.nuage.asyncio.sleep", new_callable=AsyncMock
|
"vibe.core.teleport.nuage.asyncio.sleep", new_callable=AsyncMock
|
||||||
) as mock_sleep:
|
) as mock_sleep:
|
||||||
await client.wait_for_github_connection("exec-1", interval=5.0)
|
async for _ in client.wait_for_github_connection("exec-1", interval=5.0):
|
||||||
|
pass
|
||||||
|
|
||||||
mock_sleep.assert_called_once()
|
mock_sleep.assert_called_once()
|
||||||
sleep_duration = mock_sleep.call_args[0][0]
|
sleep_duration = mock_sleep.call_args[0][0]
|
||||||
assert sleep_duration <= 5.0
|
assert sleep_duration <= 5.0
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_yields_each_poll_result(self, client: NuageClient) -> None:
|
||||||
|
pending = GitHubPublicData(
|
||||||
|
status=GitHubStatus.WAITING_FOR_OAUTH,
|
||||||
|
error="Please connect GitHub",
|
||||||
|
oauth_url="https://github.com/auth",
|
||||||
|
)
|
||||||
|
connected = GitHubPublicData(status=GitHubStatus.CONNECTED)
|
||||||
|
client.get_github_integration = AsyncMock(
|
||||||
|
side_effect=[pending, pending, connected]
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("vibe.core.teleport.nuage.asyncio.sleep", new_callable=AsyncMock):
|
||||||
|
results = [
|
||||||
|
data async for data in client.wait_for_github_connection("exec-1")
|
||||||
|
]
|
||||||
|
|
||||||
|
assert len(results) == 3
|
||||||
|
assert results[0].error == "Please connect GitHub"
|
||||||
|
assert results[1].error == "Please connect GitHub"
|
||||||
|
assert results[2].connected is True
|
||||||
|
|
|
||||||
|
|
@ -260,11 +260,12 @@ class TestTeleportServiceExecute:
|
||||||
service._git.get_info = AsyncMock(return_value=git_info)
|
service._git.get_info = AsyncMock(return_value=git_info)
|
||||||
service._git.is_commit_pushed = AsyncMock(return_value=True)
|
service._git.is_commit_pushed = AsyncMock(return_value=True)
|
||||||
|
|
||||||
|
async def _connected_gen(*_a: object, **_kw: object): # type: ignore[no-untyped-def]
|
||||||
|
yield mock_github_connected
|
||||||
|
|
||||||
mock_nuage = MagicMock()
|
mock_nuage = MagicMock()
|
||||||
mock_nuage.start_workflow = AsyncMock(return_value="exec-123")
|
mock_nuage.start_workflow = AsyncMock(return_value="exec-123")
|
||||||
mock_nuage.get_github_integration = AsyncMock(
|
mock_nuage.wait_for_github_connection = _connected_gen
|
||||||
return_value=mock_github_connected
|
|
||||||
)
|
|
||||||
mock_nuage.get_chat_assistant_url = AsyncMock(
|
mock_nuage.get_chat_assistant_url = AsyncMock(
|
||||||
return_value="https://chat.example.com/123"
|
return_value="https://chat.example.com/123"
|
||||||
)
|
)
|
||||||
|
|
@ -279,9 +280,10 @@ class TestTeleportServiceExecute:
|
||||||
assert isinstance(events[0], TeleportCheckingGitEvent)
|
assert isinstance(events[0], TeleportCheckingGitEvent)
|
||||||
assert isinstance(events[1], TeleportStartingWorkflowEvent)
|
assert isinstance(events[1], TeleportStartingWorkflowEvent)
|
||||||
assert isinstance(events[2], TeleportWaitingForGitHubEvent)
|
assert isinstance(events[2], TeleportWaitingForGitHubEvent)
|
||||||
assert isinstance(events[3], TeleportFetchingUrlEvent)
|
assert isinstance(events[3], TeleportAuthCompleteEvent)
|
||||||
assert isinstance(events[4], TeleportCompleteEvent)
|
assert isinstance(events[4], TeleportFetchingUrlEvent)
|
||||||
assert events[4].url == "https://chat.example.com/123"
|
assert isinstance(events[5], TeleportCompleteEvent)
|
||||||
|
assert events[5].url == "https://chat.example.com/123"
|
||||||
workflow_params = mock_nuage.start_workflow.call_args.args[0]
|
workflow_params = mock_nuage.start_workflow.call_args.args[0]
|
||||||
assert workflow_params.integrations.chat_assistant is not None
|
assert workflow_params.integrations.chat_assistant is not None
|
||||||
assert workflow_params.integrations.chat_assistant.project_name is None
|
assert workflow_params.integrations.chat_assistant.project_name is None
|
||||||
|
|
@ -298,11 +300,12 @@ class TestTeleportServiceExecute:
|
||||||
service._git.get_unpushed_commit_count = AsyncMock(return_value=3)
|
service._git.get_unpushed_commit_count = AsyncMock(return_value=3)
|
||||||
service._git.push_current_branch = AsyncMock(return_value=True)
|
service._git.push_current_branch = AsyncMock(return_value=True)
|
||||||
|
|
||||||
|
async def _connected_gen(*_a: object, **_kw: object): # type: ignore[no-untyped-def]
|
||||||
|
yield mock_github_connected
|
||||||
|
|
||||||
mock_nuage = MagicMock()
|
mock_nuage = MagicMock()
|
||||||
mock_nuage.start_workflow = AsyncMock(return_value="exec-123")
|
mock_nuage.start_workflow = AsyncMock(return_value="exec-123")
|
||||||
mock_nuage.get_github_integration = AsyncMock(
|
mock_nuage.wait_for_github_connection = _connected_gen
|
||||||
return_value=mock_github_connected
|
|
||||||
)
|
|
||||||
mock_nuage.get_chat_assistant_url = AsyncMock(
|
mock_nuage.get_chat_assistant_url = AsyncMock(
|
||||||
return_value="https://chat.example.com/123"
|
return_value="https://chat.example.com/123"
|
||||||
)
|
)
|
||||||
|
|
@ -357,18 +360,22 @@ class TestTeleportServiceExecute:
|
||||||
github_pending = MagicMock()
|
github_pending = MagicMock()
|
||||||
github_pending.connected = False
|
github_pending.connected = False
|
||||||
github_pending.oauth_url = "https://github.com/login/oauth"
|
github_pending.oauth_url = "https://github.com/login/oauth"
|
||||||
github_pending.error = None
|
github_pending.error = "Please connect GitHub"
|
||||||
github_pending.status = GitHubStatus.WAITING_FOR_OAUTH
|
github_pending.status = GitHubStatus.WAITING_FOR_OAUTH
|
||||||
|
|
||||||
github_connected = MagicMock()
|
github_connected = MagicMock()
|
||||||
github_connected.connected = True
|
github_connected.connected = True
|
||||||
github_connected.oauth_url = None
|
github_connected.oauth_url = None
|
||||||
|
github_connected.error = None
|
||||||
github_connected.status = GitHubStatus.CONNECTED
|
github_connected.status = GitHubStatus.CONNECTED
|
||||||
|
|
||||||
|
async def _oauth_gen(*_a: object, **_kw: object): # type: ignore[no-untyped-def]
|
||||||
|
yield github_pending
|
||||||
|
yield github_connected
|
||||||
|
|
||||||
mock_nuage = MagicMock()
|
mock_nuage = MagicMock()
|
||||||
mock_nuage.start_workflow = AsyncMock(return_value="exec-123")
|
mock_nuage.start_workflow = AsyncMock(return_value="exec-123")
|
||||||
mock_nuage.get_github_integration = AsyncMock(return_value=github_pending)
|
mock_nuage.wait_for_github_connection = _oauth_gen
|
||||||
mock_nuage.wait_for_github_connection = AsyncMock(return_value=github_connected)
|
|
||||||
mock_nuage.get_chat_assistant_url = AsyncMock(
|
mock_nuage.get_chat_assistant_url = AsyncMock(
|
||||||
return_value="https://chat.example.com/123"
|
return_value="https://chat.example.com/123"
|
||||||
)
|
)
|
||||||
|
|
@ -383,9 +390,12 @@ class TestTeleportServiceExecute:
|
||||||
assert isinstance(events[0], TeleportCheckingGitEvent)
|
assert isinstance(events[0], TeleportCheckingGitEvent)
|
||||||
assert isinstance(events[1], TeleportStartingWorkflowEvent)
|
assert isinstance(events[1], TeleportStartingWorkflowEvent)
|
||||||
assert isinstance(events[2], TeleportWaitingForGitHubEvent)
|
assert isinstance(events[2], TeleportWaitingForGitHubEvent)
|
||||||
|
assert events[2].message is None
|
||||||
assert isinstance(events[3], TeleportAuthRequiredEvent)
|
assert isinstance(events[3], TeleportAuthRequiredEvent)
|
||||||
assert events[3].oauth_url == "https://github.com/login/oauth"
|
assert events[3].oauth_url == "https://github.com/login/oauth"
|
||||||
assert isinstance(events[4], TeleportAuthCompleteEvent)
|
assert isinstance(events[4], TeleportWaitingForGitHubEvent)
|
||||||
|
assert events[4].message == "Please connect GitHub"
|
||||||
|
assert isinstance(events[5], TeleportAuthCompleteEvent)
|
||||||
assert isinstance(events[-1], TeleportCompleteEvent)
|
assert isinstance(events[-1], TeleportCompleteEvent)
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|
@ -398,11 +408,12 @@ class TestTeleportServiceExecute:
|
||||||
service._git.get_info = AsyncMock(return_value=git_info)
|
service._git.get_info = AsyncMock(return_value=git_info)
|
||||||
service._git.is_commit_pushed = AsyncMock(return_value=True)
|
service._git.is_commit_pushed = AsyncMock(return_value=True)
|
||||||
|
|
||||||
|
async def _connected_gen(*_a: object, **_kw: object): # type: ignore[no-untyped-def]
|
||||||
|
yield mock_github_connected
|
||||||
|
|
||||||
mock_nuage = MagicMock()
|
mock_nuage = MagicMock()
|
||||||
mock_nuage.start_workflow = AsyncMock(return_value="exec-123")
|
mock_nuage.start_workflow = AsyncMock(return_value="exec-123")
|
||||||
mock_nuage.get_github_integration = AsyncMock(
|
mock_nuage.wait_for_github_connection = _connected_gen
|
||||||
return_value=mock_github_connected
|
|
||||||
)
|
|
||||||
mock_nuage.get_chat_assistant_url = AsyncMock(return_value=None)
|
mock_nuage.get_chat_assistant_url = AsyncMock(return_value=None)
|
||||||
service._nuage_client_instance = mock_nuage
|
service._nuage_client_instance = mock_nuage
|
||||||
|
|
||||||
|
|
@ -423,11 +434,12 @@ class TestTeleportServiceExecute:
|
||||||
service._git.get_info = AsyncMock(return_value=git_info)
|
service._git.get_info = AsyncMock(return_value=git_info)
|
||||||
service._git.is_commit_pushed = AsyncMock(return_value=True)
|
service._git.is_commit_pushed = AsyncMock(return_value=True)
|
||||||
|
|
||||||
|
async def _connected_gen(*_a: object, **_kw: object): # type: ignore[no-untyped-def]
|
||||||
|
yield mock_github_connected
|
||||||
|
|
||||||
mock_nuage = MagicMock()
|
mock_nuage = MagicMock()
|
||||||
mock_nuage.start_workflow = AsyncMock(return_value="exec-123")
|
mock_nuage.start_workflow = AsyncMock(return_value="exec-123")
|
||||||
mock_nuage.get_github_integration = AsyncMock(
|
mock_nuage.wait_for_github_connection = _connected_gen
|
||||||
return_value=mock_github_connected
|
|
||||||
)
|
|
||||||
mock_nuage.get_chat_assistant_url = AsyncMock(
|
mock_nuage.get_chat_assistant_url = AsyncMock(
|
||||||
return_value="https://chat.example.com/123"
|
return_value="https://chat.example.com/123"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
4
uv.lock
generated
4
uv.lock
generated
|
|
@ -3,7 +3,7 @@ revision = 3
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|
||||||
[options]
|
[options]
|
||||||
exclude-newer = "2026-04-22T13:12:26.10035Z"
|
exclude-newer = "2026-04-22T17:04:46.06461Z"
|
||||||
exclude-newer-span = "P7D"
|
exclude-newer-span = "P7D"
|
||||||
|
|
||||||
[options.exclude-newer-package]
|
[options.exclude-newer-package]
|
||||||
|
|
@ -808,7 +808,7 @@ wheels = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mistral-vibe"
|
name = "mistral-vibe"
|
||||||
version = "2.9.1"
|
version = "2.9.2"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "agent-client-protocol" },
|
{ name = "agent-client-protocol" },
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,4 @@ from __future__ import annotations
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
VIBE_ROOT = Path(__file__).parent
|
VIBE_ROOT = Path(__file__).parent
|
||||||
__version__ = "2.9.1"
|
__version__ = "2.9.2"
|
||||||
|
|
|
||||||
|
|
@ -1357,8 +1357,8 @@ class VibeApp(App): # noqa: PLR0904
|
||||||
teleport_msg.set_status("Syncing with remote...")
|
teleport_msg.set_status("Syncing with remote...")
|
||||||
case TeleportStartingWorkflowEvent():
|
case TeleportStartingWorkflowEvent():
|
||||||
teleport_msg.set_status("Teleporting...")
|
teleport_msg.set_status("Teleporting...")
|
||||||
case TeleportWaitingForGitHubEvent():
|
case TeleportWaitingForGitHubEvent(message=msg):
|
||||||
teleport_msg.set_status("Connecting to GitHub...")
|
teleport_msg.set_status(msg or "Connecting to GitHub...")
|
||||||
case TeleportAuthRequiredEvent(oauth_url=url, message=msg):
|
case TeleportAuthRequiredEvent(oauth_url=url, message=msg):
|
||||||
webbrowser.open(url)
|
webbrowser.open(url)
|
||||||
teleport_msg.set_status(msg or "Authorizing GitHub...")
|
teleport_msg.set_status(msg or "Authorizing GitHub...")
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,9 @@ from vibe.core.config.patch import (
|
||||||
SetField,
|
SetField,
|
||||||
)
|
)
|
||||||
from vibe.core.config.schema import (
|
from vibe.core.config.schema import (
|
||||||
|
ConfigDefinitionError,
|
||||||
|
ConfigFragment,
|
||||||
|
ConfigSchema,
|
||||||
DuplicateMergeMetadataError,
|
DuplicateMergeMetadataError,
|
||||||
MergeFieldMetadata,
|
MergeFieldMetadata,
|
||||||
WithConcatMerge,
|
WithConcatMerge,
|
||||||
|
|
@ -68,8 +71,11 @@ __all__ = [
|
||||||
"DEFAULT_TTS_PROVIDERS",
|
"DEFAULT_TTS_PROVIDERS",
|
||||||
"THINKING_LEVELS",
|
"THINKING_LEVELS",
|
||||||
"AppendToList",
|
"AppendToList",
|
||||||
|
"ConfigDefinitionError",
|
||||||
|
"ConfigFragment",
|
||||||
"ConfigLayer",
|
"ConfigLayer",
|
||||||
"ConfigLayerError",
|
"ConfigLayerError",
|
||||||
|
"ConfigSchema",
|
||||||
"ConnectorConfig",
|
"ConnectorConfig",
|
||||||
"DeleteField",
|
"DeleteField",
|
||||||
"DuplicateMergeMetadataError",
|
"DuplicateMergeMetadataError",
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
from pydantic.fields import FieldInfo
|
from pydantic.fields import FieldInfo
|
||||||
|
|
||||||
from vibe.core.utils.merge import MergeStrategy
|
from vibe.core.utils.merge import MergeStrategy
|
||||||
|
|
@ -11,6 +12,61 @@ class DuplicateMergeMetadataError(TypeError):
|
||||||
"""Raised when a field declares more than one MergeFieldMetadata marker."""
|
"""Raised when a field declares more than one MergeFieldMetadata marker."""
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigDefinitionError(TypeError):
|
||||||
|
"""Raised when a config schema or fragment is declared with invalid fields."""
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigSchema(BaseModel):
|
||||||
|
"""Base for composite config schemas composed of fragments and merge-aware fields."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __pydantic_on_complete__(cls) -> None:
|
||||||
|
super().__pydantic_on_complete__()
|
||||||
|
|
||||||
|
if cls.__name__ == "ConfigSchema" and cls.__module__ == __name__:
|
||||||
|
return
|
||||||
|
|
||||||
|
for field_name, field_info in cls.model_fields.items():
|
||||||
|
has_merge_metadata = MergeFieldMetadata.from_field(field_info) is not None
|
||||||
|
is_fragment = isinstance(field_info.annotation, type) and issubclass(
|
||||||
|
field_info.annotation, ConfigFragment
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_fragment and has_merge_metadata:
|
||||||
|
raise ConfigDefinitionError(
|
||||||
|
f"{cls.__name__}.{field_name} is a ConfigFragment field and "
|
||||||
|
"must not declare merge metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_fragment or has_merge_metadata:
|
||||||
|
continue
|
||||||
|
|
||||||
|
raise ConfigDefinitionError(
|
||||||
|
f"{cls.__name__}.{field_name} must declare merge metadata or use a "
|
||||||
|
"ConfigFragment subclass"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigFragment(BaseModel):
|
||||||
|
"""Base for domain config groups with merge-aware top-level fields."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __pydantic_on_complete__(cls) -> None:
|
||||||
|
super().__pydantic_on_complete__()
|
||||||
|
|
||||||
|
if cls.__name__ == "ConfigFragment" and cls.__module__ == __name__:
|
||||||
|
return
|
||||||
|
|
||||||
|
for field_name, field_info in cls.model_fields.items():
|
||||||
|
if MergeFieldMetadata.from_field(field_info) is not None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
raise ConfigDefinitionError(
|
||||||
|
f"{cls.__name__}.{field_name} must declare merge metadata via "
|
||||||
|
"MergeFieldMetadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class MergeFieldMetadata:
|
class MergeFieldMetadata:
|
||||||
"""Base Pydantic Annotated marker that declares how a config field merges across layers.
|
"""Base Pydantic Annotated marker that declares how a config field merges across layers.
|
||||||
|
|
|
||||||
|
|
@ -62,8 +62,8 @@ class TextOutputFormatter(OutputFormatter):
|
||||||
self._print("Syncing with remote...")
|
self._print("Syncing with remote...")
|
||||||
case TeleportStartingWorkflowEvent():
|
case TeleportStartingWorkflowEvent():
|
||||||
self._print("Teleporting...")
|
self._print("Teleporting...")
|
||||||
case TeleportWaitingForGitHubEvent():
|
case TeleportWaitingForGitHubEvent(message=msg):
|
||||||
self._print("Connecting to GitHub...")
|
self._print(msg or "Connecting to GitHub...")
|
||||||
case TeleportAuthRequiredEvent(oauth_url=url, message=msg):
|
case TeleportAuthRequiredEvent(oauth_url=url, message=msg):
|
||||||
self._print(msg or f"Open to authorize GitHub: {url}")
|
self._print(msg or f"Open to authorize GitHub: {url}")
|
||||||
case TeleportAuthCompleteEvent():
|
case TeleportAuthCompleteEvent():
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from collections.abc import AsyncGenerator
|
||||||
from enum import StrEnum, auto
|
from enum import StrEnum, auto
|
||||||
import time
|
import time
|
||||||
import types
|
import types
|
||||||
|
|
@ -184,12 +185,13 @@ class NuageClient:
|
||||||
|
|
||||||
async def wait_for_github_connection(
|
async def wait_for_github_connection(
|
||||||
self, execution_id: str, timeout: float = 600.0, interval: float = 2.0
|
self, execution_id: str, timeout: float = 600.0, interval: float = 2.0
|
||||||
) -> GitHubPublicData:
|
) -> AsyncGenerator[GitHubPublicData, None]:
|
||||||
deadline = time.monotonic() + timeout
|
deadline = time.monotonic() + timeout
|
||||||
while time.monotonic() < deadline:
|
while time.monotonic() < deadline:
|
||||||
github_data = await self.get_github_integration(execution_id)
|
github_data = await self.get_github_integration(execution_id)
|
||||||
|
yield github_data
|
||||||
if github_data.connected:
|
if github_data.connected:
|
||||||
return github_data
|
return
|
||||||
if github_data.is_error:
|
if github_data.is_error:
|
||||||
raise ServiceTeleportError(
|
raise ServiceTeleportError(
|
||||||
github_data.error
|
github_data.error
|
||||||
|
|
|
||||||
|
|
@ -184,14 +184,21 @@ class TeleportService:
|
||||||
)
|
)
|
||||||
|
|
||||||
yield TeleportWaitingForGitHubEvent()
|
yield TeleportWaitingForGitHubEvent()
|
||||||
github_data = await self._nuage_client.get_github_integration(execution_id)
|
|
||||||
|
|
||||||
if not github_data.connected:
|
auth_event_sent = False
|
||||||
if github_data.oauth_url:
|
async for github_data in self._nuage_client.wait_for_github_connection(
|
||||||
|
execution_id
|
||||||
|
):
|
||||||
|
if github_data.connected:
|
||||||
|
break
|
||||||
|
if not auth_event_sent and github_data.oauth_url:
|
||||||
yield TeleportAuthRequiredEvent(
|
yield TeleportAuthRequiredEvent(
|
||||||
oauth_url=github_data.oauth_url, message=github_data.error
|
oauth_url=github_data.oauth_url, message=github_data.error
|
||||||
)
|
)
|
||||||
await self._nuage_client.wait_for_github_connection(execution_id)
|
auth_event_sent = True
|
||||||
|
if github_data.error:
|
||||||
|
yield TeleportWaitingForGitHubEvent(message=github_data.error)
|
||||||
|
|
||||||
yield TeleportAuthCompleteEvent()
|
yield TeleportAuthCompleteEvent()
|
||||||
|
|
||||||
yield TeleportFetchingUrlEvent()
|
yield TeleportFetchingUrlEvent()
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ class TeleportPushingEvent(BaseEvent):
|
||||||
|
|
||||||
|
|
||||||
class TeleportWaitingForGitHubEvent(BaseEvent):
|
class TeleportWaitingForGitHubEvent(BaseEvent):
|
||||||
pass
|
message: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class TeleportFetchingUrlEvent(BaseEvent):
|
class TeleportFetchingUrlEvent(BaseEvent):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue