Compare commits

...
Author SHA1 Message Date
goatoush b35a4c8113 fix(desktop): preserve active chat when switching models (#648)
Switching models from the command palette called createConversation() on every change, creating a persisted empty "New chat" entry and pulling the user out of their active conversation. Because updateLastAssistant writes the visible messages array without checking the active conversation, a mid-stream switch could also clobber the new chat's view with the old conversation's messages.

Remove the conversation-creation side effect. Model switching now preserves the active chat (matching the pull-completion and delete-fallback paths, which already switched silently); the next request uses the newly selected model with the current conversation context. Preloading, loading state, and logging are unchanged.
2026-07-18 12:51:38 -07:00
github-actions[bot] f001e3b0ca chore: update clone traffic data [skip ci] 2026-07-18 07:44:44 +00:00
Cesar Schneider b6dba93ae5 fix: make pytest suite hermetic against local dev-machine state (#647)
Two test-isolation fixes: (1) an autouse conftest fixture sets OPENJARVIS_NO_UPDATE_CHECK=1 so the CLI's PyPI update-check banner (stderr, merged into CliRunner output) can never pollute JSON/CSV-parsing CLI tests on local runs; CI was already covered by CI=true. (2) test_dense.py's Ollama skip-guard now queries /api/tags and requires nomic-embed-text to be pulled instead of a bare TCP connect, so machines running Ollama without the embed model skip instead of erroring. The probe normalizes all documented OLLAMA_HOST forms (full URL, host:port, bare host) and the Ollama-backed tests construct DenseMemory against that same endpoint rather than the embedder's hard-coded localhost default, with unit tests covering the probe. Related: #645.
2026-07-17 13:21:36 -07:00
github-actions[bot] 3000116d18 chore: update clone traffic data [skip ci] 2026-07-17 08:05:34 +00:00
Elliot Slusky 9db21d37ef style: apply Ruff formatting to recently merged tests (#644)
Fix the Ruff formatter check on main by formatting tests changed in #639 and #640 with the repository's pinned Ruff 0.15.1. No behavior change.
2026-07-16 18:13:36 -07:00
Elliot Slusky 95480363b7 style(skills): wrap importer manifest parse call (#643)
Fix the Ruff E501 failure on main introduced during the #639 fix-up. The call was 89 characters against the repository's 88-character limit. No behavior change.
2026-07-16 18:07:44 -07:00
9 changed files with 134 additions and 29 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"schemaVersion": 1,
"label": "Git Clones",
"message": "159,322",
"message": "163,592",
"color": "green",
"namedLogo": "git"
}
+5 -3
View File
@@ -1,6 +1,6 @@
{
"total_clones": 159322,
"last_updated": "2026-07-16T08:08:14Z",
"total_clones": 163592,
"last_updated": "2026-07-18T07:44:44Z",
"daily": {
"2026-03-27": 2189,
"2026-03-28": 1874,
@@ -112,6 +112,8 @@
"2026-07-12": 1917,
"2026-07-13": 2102,
"2026-07-14": 2337,
"2026-07-15": 2362
"2026-07-15": 2362,
"2026-07-16": 2497,
"2026-07-17": 1773
}
}
+1 -2
View File
@@ -149,8 +149,7 @@ export function CommandPalette() {
setCommandPaletteOpen(false);
if (modelId !== previousModel) {
const { createConversation, setModelLoading, addLogEntry } = useAppStore.getState();
createConversation(modelId);
const { setModelLoading, addLogEntry } = useAppStore.getState();
setModelLoading(true);
addLogEntry({ timestamp: Date.now(), level: 'info', category: 'model', message: `Switching to ${modelId}...` });
try {
+3 -1
View File
@@ -104,7 +104,9 @@ class SkillImporter:
try:
frontmatter, body = self._read_skill_md(source_md)
manifest = self._parser.parse_frontmatter(frontmatter, markdown_content=body)
manifest = self._parser.parse_frontmatter(
frontmatter, markdown_content=body
)
except Exception as exc:
result.success = False
result.warnings.append(f"Parse error: {exc}")
+13
View File
@@ -30,6 +30,19 @@ from openjarvis.core.registry import (
)
@pytest.fixture(autouse=True)
def _no_update_check(monkeypatch: pytest.MonkeyPatch) -> None:
"""Never let the CLI's PyPI update-check nag run during tests.
``check_for_updates`` writes its banner to stderr, which ``CliRunner``
merges into ``result.output`` — polluting JSON/CSV output of any test
that invokes a CLI command. It already self-disables when ``CI`` is
set, but that only helps in CI; locally (e.g. a dev with a stale
version-check cache and network access) it fires for real.
"""
monkeypatch.setenv("OPENJARVIS_NO_UPDATE_CHECK", "1")
@pytest.fixture(autouse=True)
def _clean_registries() -> None:
"""Ensure each test starts with empty registries and a fresh event bus."""
+2 -6
View File
@@ -255,9 +255,7 @@ class TestDangerousCapabilityGate:
assert result.success
assert result.requires_confirmation
assert any("confirmed by caller" in w for w in result.warnings)
content = (
tmp_path / "skills" / "hermes" / "my-skill" / ".source"
).read_text()
content = (tmp_path / "skills" / "hermes" / "my-skill" / ".source").read_text()
assert 'trust_tier = "unreviewed"' in content
assert 'dangerous_capabilities = ["shell:execute"]' in content
@@ -269,8 +267,6 @@ class TestDangerousCapabilityGate:
assert result.success
assert not result.requires_confirmation
assert result.dangerous_capabilities == []
content = (
tmp_path / "skills" / "hermes" / "my-skill" / ".source"
).read_text()
content = (tmp_path / "skills" / "hermes" / "my-skill" / ".source").read_text()
assert 'trust_tier = "unreviewed"' in content
assert "dangerous_capabilities = []" in content
+1 -3
View File
@@ -171,9 +171,7 @@ class TestSkillExecutorCapabilities:
assert result.context.get("result") == "hello"
def test_policy_blocks_missing_capability(self):
executor = SkillExecutor(
ToolExecutor([EchoTool()]), allowed_capabilities=set()
)
executor = SkillExecutor(ToolExecutor([EchoTool()]), allowed_capabilities=set())
result = executor.run(self._manifest())
assert not result.success
assert len(result.step_results) == 1
+107 -10
View File
@@ -14,9 +14,9 @@ skipped if the server is unreachable.
from __future__ import annotations
import os
import socket
from pathlib import Path
import httpx
import pytest
from openjarvis.tools.storage.dense import (
@@ -25,20 +25,58 @@ from openjarvis.tools.storage.dense import (
chunk_markdown,
dedupe_chunks,
)
from openjarvis.tools.storage.embeddings import OllamaEmbedder
_FIXTURE_DIR = Path(__file__).resolve().parents[2] / "fixtures" / "docs"
_OLLAMA_HOST = os.environ.get("OLLAMA_HOST", "localhost")
_OLLAMA_PORT = int(os.environ.get("OLLAMA_PORT", "11434"))
_EMBED_MODEL = "nomic-embed-text"
def _ollama_base_url() -> str:
"""Resolve the Ollama base URL from ``OLLAMA_HOST``.
The project documents ``OLLAMA_HOST`` as a full URL
(``http://<remote-ip>:11434``), but Ollama's own convention also
allows bare ``host`` / ``host:port`` forms — accept all three so
the probe and the embedder under test agree on one endpoint.
"""
host = os.environ.get("OLLAMA_HOST", "")
if not host:
return "http://localhost:11434"
if host.startswith(("http://", "https://")):
return host.rstrip("/")
if ":" in host:
return f"http://{host}"
return f"http://{host}:11434"
def _ollama_up() -> bool:
"""True only if Ollama is reachable *and* the embed model is pulled.
A bare TCP connect isn't enough — a machine can run Ollama for chat
models without ever having pulled ``nomic-embed-text``, which makes
``/api/embed`` 404 instead of the tests skipping as intended.
"""
try:
with socket.create_connection((_OLLAMA_HOST, _OLLAMA_PORT), timeout=1.0):
return True
except OSError:
resp = httpx.get(f"{_ollama_base_url()}/api/tags", timeout=1.0)
resp.raise_for_status()
models = {m.get("model", "") for m in resp.json().get("models", [])}
return any(m.startswith(_EMBED_MODEL) for m in models)
except (httpx.HTTPError, ValueError):
return False
def _make_backend() -> DenseMemory:
"""DenseMemory wired to the same Ollama endpoint the probe checked.
``DenseMemory()`` alone would build an ``OllamaEmbedder`` with its
hard-coded localhost default, so a remote ``OLLAMA_HOST`` could pass
the probe and then have every test call the wrong server.
"""
return DenseMemory(
embedder=OllamaEmbedder(model=_EMBED_MODEL, base_url=_ollama_base_url())
)
ollama_required = pytest.mark.skipif(
not _ollama_up(),
reason=(
@@ -48,6 +86,65 @@ ollama_required = pytest.mark.skipif(
)
# ---------------------------------------------------------------------------
# Skip-guard unit tests (no Ollama required)
# ---------------------------------------------------------------------------
class _FakeTagsResponse:
def __init__(self, payload: dict) -> None:
self._payload = payload
def raise_for_status(self) -> None:
pass
def json(self) -> dict:
return self._payload
class TestOllamaProbe:
def test_base_url_accepts_all_documented_forms(self, monkeypatch):
cases = {
"http://remote:11434": "http://remote:11434",
"http://remote:11434/": "http://remote:11434",
"https://ollama.internal": "https://ollama.internal",
"remote:8080": "http://remote:8080",
"remote": "http://remote:11434",
}
for raw, expected in cases.items():
monkeypatch.setenv("OLLAMA_HOST", raw)
assert _ollama_base_url() == expected, raw
monkeypatch.delenv("OLLAMA_HOST", raising=False)
assert _ollama_base_url() == "http://localhost:11434"
def test_probe_false_when_server_down(self, monkeypatch):
def _refuse(url, timeout):
raise httpx.ConnectError("connection refused")
monkeypatch.setattr(httpx, "get", _refuse)
assert _ollama_up() is False
def test_probe_false_when_model_missing(self, monkeypatch):
monkeypatch.setattr(
httpx,
"get",
lambda url, timeout: _FakeTagsResponse({"models": [{"model": "llama3"}]}),
)
assert _ollama_up() is False
def test_probe_true_with_tagged_model_on_configured_url(self, monkeypatch):
monkeypatch.setenv("OLLAMA_HOST", "http://remote:9999")
seen = {}
def _get(url, timeout):
seen["url"] = url
return _FakeTagsResponse({"models": [{"model": "nomic-embed-text:latest"}]})
monkeypatch.setattr(httpx, "get", _get)
assert _ollama_up() is True
assert seen["url"] == "http://remote:9999/api/tags"
# ---------------------------------------------------------------------------
# Chunking unit tests (no Ollama required)
# ---------------------------------------------------------------------------
@@ -283,7 +380,7 @@ def indexed_backend():
if not _ollama_up():
pytest.skip("Ollama not reachable")
backend = DenseMemory()
backend = _make_backend()
md_files = sorted(_FIXTURE_DIR.glob("*.md"))
assert md_files, f"no fixtures at {_FIXTURE_DIR}"
@@ -452,7 +549,7 @@ def test_score_distribution_vs_threshold(indexed_backend, capsys):
class TestDenseMemoryAPI:
@ollama_required
def test_store_and_delete(self):
backend = DenseMemory()
backend = _make_backend()
doc_id = backend.store("the cat sat on the mat", source="a.txt")
assert backend.count() == 1
hits = backend.retrieve("where is the cat", top_k=1)
@@ -463,12 +560,12 @@ class TestDenseMemoryAPI:
@ollama_required
def test_empty_retrieve(self):
backend = DenseMemory()
backend = _make_backend()
assert backend.retrieve("anything", top_k=3) == []
@ollama_required
def test_clear(self):
backend = DenseMemory()
backend = _make_backend()
backend.store("foo")
backend.store("bar")
backend.clear()
+1 -3
View File
@@ -70,9 +70,7 @@ def test_allows_select_with_keyword_substring(store: KnowledgeStore) -> None:
from openjarvis.tools.knowledge_sql import KnowledgeSQLTool
tool = KnowledgeSQLTool(store=store)
result = tool.execute(
query="SELECT author AS created_author FROM knowledge_chunks"
)
result = tool.execute(query="SELECT author AS created_author FROM knowledge_chunks")
assert result.success, result.content
assert "Alice" in result.content