Compare commits

..
Author SHA1 Message Date
Arush WadhawanandElliot Slusky 9685b9b78f fix(telemetry): enable WAL and batch writes in TelemetryStore to avoid SQLITE_BUSY under concurrency (#597)
TelemetryStore opened SQLite without WAL, so concurrent readers (server, aggregator, dashboard) hitting the database under inference load raised SQLITE_BUSY, and every insert committed immediately, paying fsync on each record.

- Enable PRAGMA journal_mode=WAL with synchronous=NORMAL and busy_timeout=5000, matching TraceStore.
- Batch inserts in memory under a lock and flush via executemany() when a batch reaches batch_size (default 50), when a batch goes stale, on any read through the store, and on close().
- Run a background flusher thread (default 5s interval) so a partial batch written just before traffic stops still becomes visible to other connections; close() stops the thread with an ordering that prevents touching a closed connection.
- Tests cover batching deferral, read-triggered flushes, stale-batch flushes, and close() behavior.

Fixes #560

Co-authored-by: Elliot Slusky <elliot@slusky.com>
2026-07-20 14:11:10 -07:00
goatoush aa2d127de4 fix(frontend): remove jitter when scrolling up during chat autoscroll (#646)
The chat area re-armed autoscroll whenever the user was within 100px of the bottom, so scrolling up during a streaming response fought the incoming content ticks and produced jitter.

Autoscroll now disengages on any upward scroll (direction-based, no distance threshold), re-engages when scrolled back within 2px of the bottom (tolerating sub-pixel rounding at fractional zoom levels, where the at-bottom residual can reach 1px), and ignores sub-1px upward movement so macOS elastic-bounce settling does not disengage it. Sending a message pins the view to the bottom even if the user had scrolled up to read earlier messages.
2026-07-20 13:45:12 -07:00
github-actions[bot] 87f6238338 chore: update clone traffic data [skip ci] 2026-07-20 08:53:13 +00:00
github-actions[bot] 452bcc38cf chore: update clone traffic data [skip ci] 2026-07-19 08:12:04 +00:00
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
CurryrajandElliot Slusky 4419b76412 fix: catch ImportError in git tools, fall back to CLI when Rust ext missing (#636)
get_rust_module() was called outside the try block in GitStatusTool/GitDiffTool/GitLogTool.execute(), so on installs without the compiled openjarvis-rust extension (e.g. plain pip installs, where openjarvis-rust is a uv-only group since #624) the ImportError escaped uncaught instead of degrading. Move the call inside try and fall back to the git CLI via the existing _run_git helper on ImportError, matching the fallback git_log already had. Adds regression tests covering the fallback path for all three tools.

Co-authored-by: Elliot Slusky <elliot@slusky.com>
2026-07-16 17:59:25 -07:00
Jon Saad-FalconandClaude Opus 4.8 99bbc2054a Add arXiv badge to README (#642)
Add a red arXiv badge linking to the OpenJarvis paper (2605.17172) as
the first item in the header badge row, matching the style used on the
Intelligence-Per-Watt repo.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 17:42:20 -07:00
Jon Saad-FalconandClaude Opus 4.8 d5d8fddc94 Fix leaderboard savings lookup after provider-key rename (#635)
PR #634 renamed the Anthropic cost-comparison provider key
`claude-opus-4.6` -> `claude-fable-5` but missed one consumer:
App.tsx looks up the Anthropic entry by that key to compute the
`dollar_savings` value submitted to the leaderboard. After the rename
`per_provider.find(p => p.provider === 'claude-opus-4.6')` returned
undefined, so this path silently submitted dollar_savings = 0.

Point the lookup at the new key.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 17:33:38 -07:00
19 changed files with 472 additions and 117 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"schemaVersion": 1,
"label": "Git Clones",
"message": "159,322",
"message": "166,579",
"color": "green",
"namedLogo": "git"
}
+7 -3
View File
@@ -1,6 +1,6 @@
{
"total_clones": 159322,
"last_updated": "2026-07-16T08:08:14Z",
"total_clones": 166579,
"last_updated": "2026-07-20T08:53:12Z",
"daily": {
"2026-03-27": 2189,
"2026-03-28": 1874,
@@ -112,6 +112,10 @@
"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,
"2026-07-18": 1542,
"2026-07-19": 1445
}
}
+1
View File
@@ -4,6 +4,7 @@
<p><i>Personal AI, On Personal Devices.</i></p>
<p>
<a href="https://arxiv.org/abs/2605.17172"><img src="https://img.shields.io/badge/arXiv-2605.17172-b31b1b.svg" alt="arXiv"></a>
<a href="https://openjarvis.stanford.edu/"><img src="https://img.shields.io/badge/project-OpenJarvis-blue" alt="Project"></a>
<a href="https://open-jarvis.github.io/OpenJarvis/"><img src="https://img.shields.io/badge/docs-mkdocs-blue" alt="Docs"></a>
<img src="https://img.shields.io/badge/python-%3E%3D3.10-blue" alt="Python">
+1 -1
View File
@@ -89,7 +89,7 @@ export default function App() {
setSavings(data);
if (optInEnabled && optInDisplayName && data) {
const claudeEntry = data.per_provider.find(
(p) => p.provider === 'claude-opus-4.6',
(p) => p.provider === 'claude-fable-5',
);
const dollarSavings = claudeEntry ? claudeEntry.total_cost : 0;
const energySaved = data.per_provider.reduce(
+23 -2
View File
@@ -22,6 +22,8 @@ export function ChatArea() {
const navigate = useNavigate();
const listRef = useRef<HTMLDivElement>(null);
const shouldAutoScroll = useRef(true);
const wasStreaming = useRef(false);
const lastScrollTop = useRef(0);
// Check if any data sources are connected
const [hasConnectedSources, setHasConnectedSources] = useState<boolean | null>(null);
@@ -34,15 +36,34 @@ export function ChatArea() {
}, []);
useEffect(() => {
// Sending a message always pins the view to the bottom, even if the
// user had scrolled up to read earlier messages.
if (streamState.isStreaming && !wasStreaming.current) {
shouldAutoScroll.current = true;
}
wasStreaming.current = streamState.isStreaming;
if (shouldAutoScroll.current && listRef.current) {
listRef.current.scrollTop = listRef.current.scrollHeight;
}
}, [messages, streamState.content]);
}, [messages, streamState.content, streamState.isStreaming]);
const handleScroll = () => {
if (!listRef.current) return;
const { scrollTop, scrollHeight, clientHeight } = listRef.current;
shouldAutoScroll.current = scrollHeight - scrollTop - clientHeight < 100;
const distance = scrollHeight - scrollTop - clientHeight;
const scrolledUp = scrollTop < lastScrollTop.current;
lastScrollTop.current = scrollTop;
if (scrolledUp && distance >= 1) {
// Any upward scroll away from the bottom stops autoscroll immediately,
// so streaming content never fights the user (no jitter). Sub-1px
// upward movement (elastic bounce settling at the bottom) is ignored.
shouldAutoScroll.current = false;
} else if (!scrolledUp) {
// Re-engage when scrolled back to the bottom. < 2 rather than < 1:
// at fractional zoom levels the at-bottom residual can reach 1px,
// which would otherwise leave autoscroll permanently disengaged.
shouldAutoScroll.current = distance < 2;
}
};
const isEmpty = messages.length === 0 && !streamState.isStreaming;
+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}")
+192 -82
View File
@@ -108,6 +108,13 @@ INSERT INTO telemetry (
)
"""
_INSERT_MINING = """\
INSERT INTO mining_stats (
recorded_at, provider_id, shares_submitted, shares_accepted, blocks_found,
hashrate, uptime_seconds, last_share_at, last_error, payout_target, fees_owed
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"""
_MIGRATE_COLUMNS = [
("gpu_utilization_pct", "REAL NOT NULL DEFAULT 0.0"),
("gpu_memory_used_gb", "REAL NOT NULL DEFAULT 0.0"),
@@ -144,10 +151,35 @@ _MIGRATE_COLUMNS = [
class TelemetryStore:
"""Append-only SQLite store for inference telemetry records."""
"""Append-only SQLite store for inference telemetry records.
Writes are batched in memory and flushed to SQLite when a batch reaches
``batch_size``, when ``flush_interval_seconds`` elapses (a background
flusher thread guarantees this even with no further writes), on any read
through this store, and on ``close()``. Readers that open their OWN
connection to the database file (e.g. ``TelemetryAggregator``) therefore
see new rows within ``flush_interval_seconds`` at the latest; call
``flush()`` first for immediate visibility. Pass
``flush_interval_seconds=0`` to disable time-based flushing (batch-size
and read/close flushes still apply).
"""
def __init__(
self,
db_path: str | Path,
batch_size: int = 50,
flush_interval_seconds: float = 5.0,
) -> None:
if batch_size < 1:
raise ValueError("batch_size must be >= 1")
if flush_interval_seconds < 0:
raise ValueError("flush_interval_seconds must be >= 0")
def __init__(self, db_path: str | Path) -> None:
self._db_path = str(db_path)
if self._db_path != ":memory:":
from openjarvis.security.file_utils import secure_create
secure_create(Path(self._db_path))
self._conn = sqlite3.connect(self._db_path, check_same_thread=False)
self._lock = threading.Lock()
self._conn.execute("PRAGMA journal_mode=WAL")
@@ -158,6 +190,38 @@ class TelemetryStore:
self._conn.commit()
self._migrate_schema()
self._batch_size = batch_size
self._flush_interval_seconds = flush_interval_seconds
self._last_flush_time = time.monotonic()
self._telemetry_batch: list[tuple[Any, ...]] = []
self._mining_batch: list[tuple[Any, ...]] = []
self._closed = False
# Background flusher: without it, a partial batch written just before
# traffic stops would stay invisible to other connections until the
# NEXT write arrived (the stale check in ``_maybe_flush_unlocked``
# only runs inside record calls). Daemon so it never blocks exit.
self._stop_flusher = threading.Event()
self._flusher: threading.Thread | None = None
if flush_interval_seconds > 0:
self._flusher = threading.Thread(
target=self._flush_loop,
name="telemetry-store-flusher",
daemon=True,
)
self._flusher.start()
def _flush_loop(self) -> None:
"""Periodically flush pending batches until ``close()`` stops us."""
while not self._stop_flusher.wait(self._flush_interval_seconds):
with self._lock:
# ``close()`` sets the event BEFORE taking the lock, so seeing
# it unset here means the connection is still open.
if self._stop_flusher.is_set():
break
if self._telemetry_batch or self._mining_batch:
self._flush_unlocked()
def _migrate_schema(self) -> None:
"""Add new columns to existing databases (idempotent)."""
for col_name, col_def in _MIGRATE_COLUMNS:
@@ -171,54 +235,52 @@ class TelemetryStore:
def record(self, rec: TelemetryRecord) -> None:
"""Persist a single telemetry record."""
row = (
rec.timestamp,
rec.model_id,
rec.engine,
rec.agent,
rec.prompt_tokens,
rec.prompt_tokens_evaluated,
rec.completion_tokens,
rec.total_tokens,
rec.latency_seconds,
rec.ttft,
rec.cost_usd,
rec.energy_joules,
rec.power_watts,
rec.gpu_utilization_pct,
rec.gpu_memory_used_gb,
rec.gpu_temperature_c,
rec.throughput_tok_per_sec,
rec.prefill_latency_seconds,
rec.decode_latency_seconds,
rec.energy_method,
rec.energy_vendor,
rec.batch_id,
1 if rec.is_warmup else 0,
rec.cpu_energy_joules,
rec.gpu_energy_joules,
rec.dram_energy_joules,
rec.tokens_per_joule,
rec.energy_per_output_token_joules,
rec.throughput_per_watt,
rec.prefill_energy_joules,
rec.decode_energy_joules,
rec.mean_itl_ms,
rec.median_itl_ms,
rec.p90_itl_ms,
rec.p95_itl_ms,
rec.p99_itl_ms,
rec.std_itl_ms,
1 if rec.is_streaming else 0,
rec.token_counting_version,
rec.mining_session_id,
json.dumps(rec.metadata),
)
with self._lock:
self._conn.execute(
_INSERT,
(
rec.timestamp,
rec.model_id,
rec.engine,
rec.agent,
rec.prompt_tokens,
rec.prompt_tokens_evaluated,
rec.completion_tokens,
rec.total_tokens,
rec.latency_seconds,
rec.ttft,
rec.cost_usd,
rec.energy_joules,
rec.power_watts,
rec.gpu_utilization_pct,
rec.gpu_memory_used_gb,
rec.gpu_temperature_c,
rec.throughput_tok_per_sec,
rec.prefill_latency_seconds,
rec.decode_latency_seconds,
rec.energy_method,
rec.energy_vendor,
rec.batch_id,
1 if rec.is_warmup else 0,
rec.cpu_energy_joules,
rec.gpu_energy_joules,
rec.dram_energy_joules,
rec.tokens_per_joule,
rec.energy_per_output_token_joules,
rec.throughput_per_watt,
rec.prefill_energy_joules,
rec.decode_energy_joules,
rec.mean_itl_ms,
rec.median_itl_ms,
rec.p90_itl_ms,
rec.p95_itl_ms,
rec.p99_itl_ms,
rec.std_itl_ms,
1 if rec.is_streaming else 0,
rec.token_counting_version,
rec.mining_session_id,
json.dumps(rec.metadata),
),
)
self._conn.commit()
self._telemetry_batch.append(row)
self._maybe_flush_unlocked()
def record_mining_stats(self, stats: Any) -> None:
"""Persist one mining stats snapshot.
@@ -226,43 +288,70 @@ class TelemetryStore:
``stats`` is duck-typed to keep telemetry usable without importing the
optional mining package at module import time.
"""
row = (
time.time(),
stats.provider_id,
stats.shares_submitted,
stats.shares_accepted,
stats.blocks_found,
stats.hashrate,
stats.uptime_seconds,
stats.last_share_at,
stats.last_error,
stats.payout_target,
stats.fees_owed,
)
with self._lock:
self._conn.execute(
"""\
INSERT INTO mining_stats (
recorded_at, provider_id, shares_submitted, shares_accepted, blocks_found,
hashrate, uptime_seconds, last_share_at, last_error, payout_target, fees_owed
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
time.time(),
stats.provider_id,
stats.shares_submitted,
stats.shares_accepted,
stats.blocks_found,
stats.hashrate,
stats.uptime_seconds,
stats.last_share_at,
stats.last_error,
stats.payout_target,
stats.fees_owed,
),
)
self._conn.commit()
self._mining_batch.append(row)
self._maybe_flush_unlocked()
def flush(self) -> None:
"""Write all pending records to the database."""
with self._lock:
self._flush_unlocked()
def _flush_unlocked(self) -> None:
if self._telemetry_batch:
self._conn.executemany(_INSERT, self._telemetry_batch)
self._telemetry_batch.clear()
if self._mining_batch:
self._conn.executemany(_INSERT_MINING, self._mining_batch)
self._mining_batch.clear()
self._conn.commit()
self._last_flush_time = time.monotonic()
def _maybe_flush_unlocked(self) -> None:
"""Flush when the batch is full or has been pending too long."""
if not self._telemetry_batch and not self._mining_batch:
return
batch_full = (
len(self._telemetry_batch) >= self._batch_size
or len(self._mining_batch) >= self._batch_size
)
stale = (
self._flush_interval_seconds > 0
and time.monotonic() - self._last_flush_time >= self._flush_interval_seconds
)
if batch_full or stale:
self._flush_unlocked()
def list_recent(self, limit: int = 50) -> list[dict[str, Any]]:
"""Return recent telemetry rows as dictionaries."""
return self._select_dicts(
"SELECT * FROM telemetry ORDER BY timestamp DESC LIMIT ?",
(limit,),
)
with self._lock:
self._flush_unlocked()
return self._select_dicts_unlocked(
"SELECT * FROM telemetry ORDER BY timestamp DESC LIMIT ?",
(limit,),
)
def list_recent_mining_stats(self, limit: int = 50) -> list[dict[str, Any]]:
"""Return recent mining stats snapshots as dictionaries."""
return self._select_dicts(
"SELECT * FROM mining_stats ORDER BY recorded_at DESC LIMIT ?",
(limit,),
)
with self._lock:
self._flush_unlocked()
return self._select_dicts_unlocked(
"SELECT * FROM mining_stats ORDER BY recorded_at DESC LIMIT ?",
(limit,),
)
def subscribe_to_bus(self, bus: EventBus) -> None:
"""Subscribe to ``TELEMETRY_RECORD`` events on *bus*."""
@@ -277,15 +366,36 @@ INSERT INTO mining_stats (
logger.debug("Failed to record telemetry event: %s", exc)
def close(self) -> None:
"""Close the underlying SQLite connection."""
self._conn.close()
"""Flush pending records and close the underlying SQLite connection."""
# Set the stop event BEFORE taking the lock: a flusher iteration
# already waiting on the lock re-checks the event after acquiring it
# and exits instead of touching the closed connection.
self._stop_flusher.set()
with self._lock:
if self._closed:
return
self._flush_unlocked()
self._conn.close()
self._closed = True
if self._flusher is not None:
self._flusher.join(timeout=1.0)
self._flusher = None
# -- helpers for querying (used by tests) --------------------------------
def _fetchall(self, sql: str = "SELECT * FROM telemetry") -> list:
return self._conn.execute(sql).fetchall()
with self._lock:
self._flush_unlocked()
return self._conn.execute(sql).fetchall()
def _select_dicts(self, sql: str, params: tuple[Any, ...]) -> list[dict[str, Any]]:
with self._lock:
self._flush_unlocked()
return self._select_dicts_unlocked(sql, params)
def _select_dicts_unlocked(
self, sql: str, params: tuple[Any, ...]
) -> list[dict[str, Any]]:
cur = self._conn.execute(sql, params)
columns = [desc[0] for desc in cur.description]
return [dict(zip(columns, row)) for row in cur.fetchall()]
+9 -3
View File
@@ -139,8 +139,8 @@ class GitStatusTool(BaseTool):
def execute(self, **params: Any) -> ToolResult:
repo_path = params.get("repo_path", ".")
_rust = get_rust_module()
try:
_rust = get_rust_module()
output = _rust.GitStatusTool().execute(repo_path)
return ToolResult(
tool_name="git_status",
@@ -148,6 +148,8 @@ class GitStatusTool(BaseTool):
success=True,
metadata={"returncode": 0},
)
except ImportError as exc:
logger.debug("Rust git_status fallback to CLI: %s", exc)
except Exception as exc:
return ToolResult(
tool_name="git_status",
@@ -155,6 +157,8 @@ class GitStatusTool(BaseTool):
success=False,
)
return _run_git(["git", "status", "--porcelain"], cwd=repo_path)
# ---------------------------------------------------------------------------
# GitDiffTool
@@ -208,9 +212,9 @@ class GitDiffTool(BaseTool):
staged = params.get("staged", False)
file_path = params.get("path")
_rust = get_rust_module()
if not staged and not file_path:
try:
_rust = get_rust_module()
output = _rust.GitDiffTool().execute(repo_path)
return ToolResult(
tool_name="git_diff",
@@ -218,6 +222,8 @@ class GitDiffTool(BaseTool):
success=True,
metadata={"returncode": 0},
)
except ImportError as exc:
logger.debug("Rust git_diff fallback to CLI: %s", exc)
except Exception as exc:
return ToolResult(
tool_name="git_diff",
@@ -371,8 +377,8 @@ class GitLogTool(BaseTool):
count = params.get("count", 10)
oneline = params.get("oneline", True)
_rust = get_rust_module()
try:
_rust = get_rust_module()
output = _rust.GitLogTool().execute(repo_path, count)
return ToolResult(
tool_name="git_log",
+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
+3
View File
@@ -164,6 +164,7 @@ class TestDerivedMetricsInStore:
throughput_per_watt=0.5,
)
store.record(rec)
store.flush()
agg = TelemetryAggregator(tmp_path / "test.db")
stats = agg.per_model_stats()
@@ -185,6 +186,8 @@ class TestDerivedMetricsInStore:
throughput_per_watt=1.0 * (i + 1),
)
)
store.flush()
agg = TelemetryAggregator(tmp_path / "test.db")
summary = agg.summary()
assert summary.avg_energy_per_output_token_joules > 0
+1
View File
@@ -345,6 +345,7 @@ class TestItlStorage:
)
)
store.flush()
agg = TelemetryAggregator(tmp_path / "test.db")
stats = agg.per_model_stats()
assert len(stats) == 1
+1
View File
@@ -222,6 +222,7 @@ class TestPhaseEnergyStorage:
)
)
store.flush()
agg = TelemetryAggregator(tmp_path / "test.db")
stats = agg.per_model_stats()
assert len(stats) == 1
+57
View File
@@ -178,3 +178,60 @@ class TestTelemetryRecordFields:
def test_tokens_per_joule_set(self):
rec = TelemetryRecord(timestamp=1.0, model_id="test", tokens_per_joule=80.0)
assert rec.tokens_per_joule == 80.0
def test_batching_delays_commit(self, tmp_path: Path) -> None:
db_path = tmp_path / "test.db"
# flush_interval_seconds=0 disables the background flusher so the
# buffered/flushed states below are deterministic, not a race.
store = TelemetryStore(db_path, batch_size=2, flush_interval_seconds=0)
rec = TelemetryRecord(timestamp=time.time(), model_id="m1", engine="e1")
store.record(rec)
# Should not be in DB yet because batch_size is 2 and we haven't flushed
# Need a separate connection to check because store._fetchall() calls flush()!
assert _count_rows_via_own_connection(db_path) == 0
# Hit batch size
store.record(rec)
assert _count_rows_via_own_connection(db_path) == 2
store.close()
def test_background_flush_makes_records_visible(self, tmp_path: Path) -> None:
# A partial batch must become visible to OTHER connections within the
# flush interval even if no further write ever arrives — the background
# flusher covers the "traffic stopped mid-batch" case that per-record
# stale checks cannot.
db_path = tmp_path / "test.db"
store = TelemetryStore(db_path, batch_size=50, flush_interval_seconds=0.05)
rec = TelemetryRecord(timestamp=time.time(), model_id="m1", engine="e1")
store.record(rec)
deadline = time.time() + 5.0
rows = 0
while time.time() < deadline:
rows = _count_rows_via_own_connection(db_path)
if rows:
break
time.sleep(0.02)
assert rows == 1
store.close()
def test_close_flushes_and_is_idempotent(self, tmp_path: Path) -> None:
db_path = tmp_path / "test.db"
store = TelemetryStore(db_path, batch_size=50, flush_interval_seconds=0)
rec = TelemetryRecord(timestamp=time.time(), model_id="m1", engine="e1")
store.record(rec)
store.close()
assert _count_rows_via_own_connection(db_path) == 1
store.close() # second close must be a no-op, not a ProgrammingError
def _count_rows_via_own_connection(db_path: Path) -> int:
"""Count telemetry rows through a separate connection (like the aggregator)."""
import contextlib
import sqlite3
# NB: sqlite3's ``with conn`` is a TRANSACTION context (it does not close);
# ``contextlib.closing`` actually closes the connection.
with contextlib.closing(sqlite3.connect(db_path)) as conn:
return len(conn.execute("SELECT * FROM telemetry").fetchall())
+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()
+48
View File
@@ -537,3 +537,51 @@ class TestGitLogTool:
fn = tool.to_openai_function()
assert fn["type"] == "function"
assert fn["function"]["name"] == "git_log"
# ---------------------------------------------------------------------------
# CLI fallback when the Rust extension is missing
# ---------------------------------------------------------------------------
class TestCliFallbackWhenRustMissing:
"""When ``get_rust_module`` raises ImportError (extension not built,
e.g. a plain pip install), the read-only git tools must fall back to
the git CLI instead of letting the ImportError escape ``execute()``."""
def _patch_no_rust(self):
return patch(
"openjarvis.tools.git_tool.get_rust_module",
side_effect=ImportError("No module named 'openjarvis_rust'"),
)
def test_git_status_falls_back_to_cli(self, tmp_path):
_init_repo(tmp_path)
(tmp_path / "new_file.txt").write_text("hello")
with self._patch_no_rust():
result = GitStatusTool().execute(repo_path=str(tmp_path))
assert result.success is True
assert "new_file.txt" in result.content
def test_git_diff_falls_back_to_cli(self, tmp_path):
_init_repo(tmp_path)
(tmp_path / "README.md").write_text("# Modified\n")
with self._patch_no_rust():
result = GitDiffTool().execute(repo_path=str(tmp_path))
assert result.success is True
assert "README.md" in result.content
def test_git_log_falls_back_to_cli(self, tmp_path):
_init_repo(tmp_path)
with self._patch_no_rust():
result = GitLogTool().execute(repo_path=str(tmp_path))
assert result.success is True
assert "Initial commit" in result.content
def test_fallback_failure_is_a_tool_result_not_an_exception(self, tmp_path):
# Even when the fallback itself fails (not a git repo), the tool
# must return a failed ToolResult rather than raising.
with self._patch_no_rust():
result = GitStatusTool().execute(repo_path=str(tmp_path))
assert result.success is False
assert "not a git repository" in result.content
+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