diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
index 45e50ae3..fb3ddd2f 100644
--- a/.claude-plugin/marketplace.json
+++ b/.claude-plugin/marketplace.json
@@ -9,7 +9,7 @@
"name": "engraphis-memory",
"source": "./",
"description": "Discipline for giving agents durable, scoped, explainable memory across sessions and repos with the Engraphis MCP tools.",
- "version": "1.5.0"
+ "version": "1.5"
}
]
}
diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json
index 557c0612..c5147742 100644
--- a/.claude-plugin/plugin.json
+++ b/.claude-plugin/plugin.json
@@ -1,6 +1,6 @@
{
"name": "engraphis-memory",
- "version": "1.5.0",
+ "version": "1.5",
"description": "Give agents durable, scoped, explainable memory across sessions and repos via the Engraphis MCP tools. Use when you learn something worth keeping, need prior context before acting, or ask why/how a fact changed. Covers remember/recall, why/timeline, forget/pin/correct, sessions, and code search.",
"author": {
"name": "The Engraphis Authors",
diff --git a/.claude-plugin/skill-assets.sha256 b/.claude-plugin/skill-assets.sha256
index 178a9b71..033158ca 100644
--- a/.claude-plugin/skill-assets.sha256
+++ b/.claude-plugin/skill-assets.sha256
@@ -1,6 +1,6 @@
-5d315146fd0bdcd5bb803bab482504c51e6c8e94d9666cd4910ca8f4a645b564 .claude-plugin/marketplace.json
-a51eb5baab17efb66193759be7f68df32451594b799476e7ec6e3b076b7fdff5 .claude-plugin/plugin.json
-56be8d078a2a8fc6e6cd1c2be5716605d8621dab953caa8cfcd20e2dce474305 skills/engraphis-memory/SKILL.md
+d30ad152dcc4c82ce10e7167fdfe67e709358e5f435293939125f2d6cffc5b7e .claude-plugin/marketplace.json
+28dcd15a7a186f8cb8a15705f1bd7734086167991c4acc28ec2cfea59a2374ab .claude-plugin/plugin.json
45dd73ca6afdd9e12ecd38c48e4a612b7646c25a07a75a80ca0e68d0e0b85f0e skills/engraphis-memory/references/CONVENTIONS.md
529fff3bdbe73f83209087fd10055fad77c5e5224ad8a9e6b0254052aa50e109 skills/engraphis-memory/references/SCOPING.md
b2489b60159655e7e564e234d5aff24ba4d8df7cb82626edeaaaf89264007f85 skills/engraphis-memory/references/TOOLS.md
+56be8d078a2a8fc6e6cd1c2be5716605d8621dab953caa8cfcd20e2dce474305 skills/engraphis-memory/SKILL.md
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 17598d7e..0eb3b0b6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,7 @@ All notable changes to Engraphis are documented here. Format loosely follows
## [Unreleased]
-## [1.5.0] - 2026-08-04
+## [1.5] - 2026-08-04
Minor release advancing the v2 engine to schema 11 with governed recall recovery,
embedding-space safety, reproducible release evidence, and stronger offline memory-quality gates.
diff --git a/README.md b/README.md
index 41289467..1331dcc4 100644
--- a/README.md
+++ b/README.md
@@ -164,7 +164,7 @@ selection, set `ENGRAPHIS_UPDATE_EXTRAS` to a comma-separated list (for example
> **Upgrading to 1.5:** schema 10 bounds legacy retention state and schema 11 backfills explicit
> approval only for eligible pre-review local memories. Pending and quarantined evidence remains
> gated. Existing 1.4.x databases migrate automatically when Engraphis 1.5 opens them; see the
-> [1.5.0 release notes](CHANGELOG.md#150---2026-08-04).
+> [1.5 release notes](CHANGELOG.md#150---2026-08-04).
---
diff --git a/engraphis/__init__.py b/engraphis/__init__.py
index 6bef7f1c..968b5305 100644
--- a/engraphis/__init__.py
+++ b/engraphis/__init__.py
@@ -2,7 +2,7 @@
from importlib.metadata import PackageNotFoundError, version as _dist_version
-_SOURCE_VERSION = "1.5.0"
+_SOURCE_VERSION = "1.5"
try:
__version__ = _dist_version("engraphis")
@@ -14,4 +14,4 @@
except PackageNotFoundError: # source tree without an installed distribution
# Keep in step with [project] version in pyproject.toml — tests/test_packaging.py
# pins the two together so a release cannot ship them out of sync.
- __version__ = "1.5.0"
+ __version__ = "1.5"
diff --git a/engraphis/commercial_manifest.json b/engraphis/commercial_manifest.json
index 20d5602f..5e427235 100644
--- a/engraphis/commercial_manifest.json
+++ b/engraphis/commercial_manifest.json
@@ -1,6 +1,6 @@
{
"schema": "engraphis-commercial/v2",
- "version": "1.5.0",
+ "version": "1.5",
"control_plane": "https://api.engraphis.com",
"account_portal": "https://api.engraphis.com/account",
"billing": {
diff --git a/engraphis/core/savings.py b/engraphis/core/savings.py
index 4dd02f4e..41b3e10a 100644
--- a/engraphis/core/savings.py
+++ b/engraphis/core/savings.py
@@ -1,163 +1,163 @@
-"""Pure token-savings estimation for prompt-context deliveries.
-
-The estimator deliberately distinguishes an actual host-history baseline from the
-smaller source-packing baseline used by ordinary recall. It is an estimate of
-avoided prompt context, not provider billing or end-to-end task cost.
-"""
-from __future__ import annotations
-
-import math
-import re
-from dataclasses import dataclass
-from typing import Any, Optional
-
-
-_RELEASE_VERSION = re.compile(r"^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$")
-
-
-@dataclass(frozen=True)
-class SavingsEstimate:
- """One explainable, content-free token-savings estimate."""
-
- baseline_tokens: int
- emitted_tokens: int
- saved_tokens: int
- savings_ratio: float
- basis: str
- confidence: str
- eligible: bool
- token_counter: str = "unknown"
- release_version: Optional[str] = None
-
- @property
- def estimated_saved_tokens(self) -> int:
- """Name used by receipt metadata for the same saved-token value."""
- return self.saved_tokens
-
- def to_dict(self) -> dict[str, Any]:
- return {
- "baseline_tokens": self.baseline_tokens,
- "emitted_tokens": self.emitted_tokens,
- "saved_tokens": self.saved_tokens,
- "savings_ratio": self.savings_ratio,
- "basis": self.basis,
- "confidence": self.confidence,
- "eligible": self.eligible,
- "token_counter": self.token_counter,
- **({"release_version": self.release_version}
- if self.release_version else {}),
- }
-
-
-def normalize_release_version(value: Any) -> Optional[str]:
- """Return a safe release label, or ``None`` for historical/unversioned data."""
- if not isinstance(value, str):
- return None
- value = value.strip()
- return value if _RELEASE_VERSION.fullmatch(value) else None
-
-
-def _count(value: Any) -> int:
- if type(value) not in (int, float):
- return 0
- if not math.isfinite(float(value)) or value < 0:
- return 0
- return int(value)
-
-
-def estimate_savings(
- *,
- operation: str,
- baseline_tokens: Any,
- emitted_tokens: Any,
- token_counter: str = "unknown",
- intent: Optional[str] = None,
- adaptive_mode: Optional[str] = None,
- release_version: Optional[str] = None,
-) -> SavingsEstimate:
- """Classify one delivery and compute its conservative savings estimate.
-
- ``adaptive_context`` has a real before/after history baseline. The packed
- context operations use their retrieved-source total as a narrower packing
- baseline. Ordinary full recall is not counted because callers may not inject
- its returned memories into a model prompt.
- """
- operation = str(operation or "").strip().casefold()
- intent = str(intent or "").strip().casefold()
- mode = str(adaptive_mode or "").strip().casefold()
-
- basis = "unclassified"
- confidence = "unknown"
- eligible = False
-
- if operation == "adaptive_context":
- if mode == "retrieval":
- basis, confidence, eligible = "history_retrieval", "high", True
- elif mode == "history_fallback":
- basis, confidence, eligible = "history_fallback", "medium", True
- elif mode == "history_bypass":
- basis, confidence, eligible = "history_bypass", "none", False
- elif mode == "low_confidence_abstain":
- basis, confidence, eligible = "low_confidence_abstain", "none", False
- elif operation == "recall" and intent == "recall_context":
- basis, confidence, eligible = "packed_context", "medium", True
- elif operation in {"grounded_recall", "proactive_context"}:
- basis, confidence, eligible = "packed_context", "medium", True
-
- baseline = _count(baseline_tokens)
- emitted = _count(emitted_tokens)
- saved = max(0, baseline - emitted) if eligible else 0
- ratio = saved / baseline if baseline else 0.0
- counter = str(token_counter or "unknown")
- return SavingsEstimate(
- baseline_tokens=baseline,
- emitted_tokens=emitted,
- saved_tokens=saved,
- savings_ratio=ratio,
- basis=basis,
- confidence=confidence,
- eligible=eligible,
- token_counter=counter,
- release_version=normalize_release_version(release_version),
- )
-
-
-def annotate_usage(
- usage: dict[str, Any],
- *,
- operation: str,
- intent: Optional[str] = None,
- adaptive_mode: Optional[str] = None,
- baseline_tokens: Any = None,
- emitted_tokens: Any = None,
- release_version: Optional[str] = None,
-) -> dict[str, Any]:
- """Add estimator fields to an existing public usage dictionary."""
- estimate = estimate_savings(
- operation=operation,
- intent=intent,
- adaptive_mode=adaptive_mode,
- baseline_tokens=(
- usage.get("source_tokens", 0)
- if baseline_tokens is None else baseline_tokens
- ),
- emitted_tokens=(
- usage.get("context_tokens", 0)
- if emitted_tokens is None else emitted_tokens
- ),
- token_counter=str(usage.get("token_counter") or "unknown"),
- release_version=release_version,
- )
- out = dict(usage)
- out.update({
- "baseline_tokens": estimate.baseline_tokens,
- "emitted_tokens": estimate.emitted_tokens,
- "estimated_saved_tokens": estimate.saved_tokens,
- "estimated_savings_ratio": estimate.savings_ratio,
- "savings_basis": estimate.basis,
- "savings_confidence": estimate.confidence,
- "savings_eligible": estimate.eligible,
- })
- if estimate.release_version:
- out["release_version"] = estimate.release_version
- return out
+"""Pure token-savings estimation for prompt-context deliveries.
+
+The estimator deliberately distinguishes an actual host-history baseline from the
+smaller source-packing baseline used by ordinary recall. It is an estimate of
+avoided prompt context, not provider billing or end-to-end task cost.
+"""
+from __future__ import annotations
+
+import math
+import re
+from dataclasses import dataclass
+from typing import Any, Optional
+
+
+_RELEASE_VERSION = re.compile(r"^\d+\.\d+(?:\.\d+)?(?:[-+][0-9A-Za-z.-]+)?$")
+
+
+@dataclass(frozen=True)
+class SavingsEstimate:
+ """One explainable, content-free token-savings estimate."""
+
+ baseline_tokens: int
+ emitted_tokens: int
+ saved_tokens: int
+ savings_ratio: float
+ basis: str
+ confidence: str
+ eligible: bool
+ token_counter: str = "unknown"
+ release_version: Optional[str] = None
+
+ @property
+ def estimated_saved_tokens(self) -> int:
+ """Name used by receipt metadata for the same saved-token value."""
+ return self.saved_tokens
+
+ def to_dict(self) -> dict[str, Any]:
+ return {
+ "baseline_tokens": self.baseline_tokens,
+ "emitted_tokens": self.emitted_tokens,
+ "saved_tokens": self.saved_tokens,
+ "savings_ratio": self.savings_ratio,
+ "basis": self.basis,
+ "confidence": self.confidence,
+ "eligible": self.eligible,
+ "token_counter": self.token_counter,
+ **({"release_version": self.release_version}
+ if self.release_version else {}),
+ }
+
+
+def normalize_release_version(value: Any) -> Optional[str]:
+ """Return a safe release label, or ``None`` for historical/unversioned data."""
+ if not isinstance(value, str):
+ return None
+ value = value.strip()
+ return value if _RELEASE_VERSION.fullmatch(value) else None
+
+
+def _count(value: Any) -> int:
+ if type(value) not in (int, float):
+ return 0
+ if not math.isfinite(float(value)) or value < 0:
+ return 0
+ return int(value)
+
+
+def estimate_savings(
+ *,
+ operation: str,
+ baseline_tokens: Any,
+ emitted_tokens: Any,
+ token_counter: str = "unknown",
+ intent: Optional[str] = None,
+ adaptive_mode: Optional[str] = None,
+ release_version: Optional[str] = None,
+) -> SavingsEstimate:
+ """Classify one delivery and compute its conservative savings estimate.
+
+ ``adaptive_context`` has a real before/after history baseline. The packed
+ context operations use their retrieved-source total as a narrower packing
+ baseline. Ordinary full recall is not counted because callers may not inject
+ its returned memories into a model prompt.
+ """
+ operation = str(operation or "").strip().casefold()
+ intent = str(intent or "").strip().casefold()
+ mode = str(adaptive_mode or "").strip().casefold()
+
+ basis = "unclassified"
+ confidence = "unknown"
+ eligible = False
+
+ if operation == "adaptive_context":
+ if mode == "retrieval":
+ basis, confidence, eligible = "history_retrieval", "high", True
+ elif mode == "history_fallback":
+ basis, confidence, eligible = "history_fallback", "medium", True
+ elif mode == "history_bypass":
+ basis, confidence, eligible = "history_bypass", "none", False
+ elif mode == "low_confidence_abstain":
+ basis, confidence, eligible = "low_confidence_abstain", "none", False
+ elif operation == "recall" and intent == "recall_context":
+ basis, confidence, eligible = "packed_context", "medium", True
+ elif operation in {"grounded_recall", "proactive_context"}:
+ basis, confidence, eligible = "packed_context", "medium", True
+
+ baseline = _count(baseline_tokens)
+ emitted = _count(emitted_tokens)
+ saved = max(0, baseline - emitted) if eligible else 0
+ ratio = saved / baseline if baseline else 0.0
+ counter = str(token_counter or "unknown")
+ return SavingsEstimate(
+ baseline_tokens=baseline,
+ emitted_tokens=emitted,
+ saved_tokens=saved,
+ savings_ratio=ratio,
+ basis=basis,
+ confidence=confidence,
+ eligible=eligible,
+ token_counter=counter,
+ release_version=normalize_release_version(release_version),
+ )
+
+
+def annotate_usage(
+ usage: dict[str, Any],
+ *,
+ operation: str,
+ intent: Optional[str] = None,
+ adaptive_mode: Optional[str] = None,
+ baseline_tokens: Any = None,
+ emitted_tokens: Any = None,
+ release_version: Optional[str] = None,
+) -> dict[str, Any]:
+ """Add estimator fields to an existing public usage dictionary."""
+ estimate = estimate_savings(
+ operation=operation,
+ intent=intent,
+ adaptive_mode=adaptive_mode,
+ baseline_tokens=(
+ usage.get("source_tokens", 0)
+ if baseline_tokens is None else baseline_tokens
+ ),
+ emitted_tokens=(
+ usage.get("context_tokens", 0)
+ if emitted_tokens is None else emitted_tokens
+ ),
+ token_counter=str(usage.get("token_counter") or "unknown"),
+ release_version=release_version,
+ )
+ out = dict(usage)
+ out.update({
+ "baseline_tokens": estimate.baseline_tokens,
+ "emitted_tokens": estimate.emitted_tokens,
+ "estimated_saved_tokens": estimate.saved_tokens,
+ "estimated_savings_ratio": estimate.savings_ratio,
+ "savings_basis": estimate.basis,
+ "savings_confidence": estimate.confidence,
+ "savings_eligible": estimate.eligible,
+ })
+ if estimate.release_version:
+ out["release_version"] = estimate.release_version
+ return out
diff --git a/glama.json b/glama.json
deleted file mode 100644
index bf283771..00000000
--- a/glama.json
+++ /dev/null
@@ -1 +0,0 @@
-{"$schema":"https://glama.ai/mcp/schemas/server.json","maintainers":["Coding-Dev-Tools"]}
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 11e832b6..f92c705f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,7 +10,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "engraphis"
-version = "1.5.0"
+version = "1.5"
description = "Local-first AI memory engine for agents — Ebbinghaus decay, interaction-aware recall, bi-temporal facts, hybrid retrieval, and an MCP server. You bring the LLM."
readme = "README.md"
license = "Apache-2.0"
diff --git a/scripts/submit_directories.ps1 b/scripts/submit_directories.ps1
deleted file mode 100644
index c27ad4a6..00000000
--- a/scripts/submit_directories.ps1
+++ /dev/null
@@ -1,46 +0,0 @@
-# Engraphis Directory Submission Helper
-# Run this in an interactive PowerShell terminal with browser access
-
-$ErrorActionPreference = "Stop"
-$repoUrl = "https://github.com/Coding-Dev-Tools/engraphis"
-
-Write-Host "=== Engraphis Directory Submissions ===" -ForegroundColor Cyan
-Write-Host ""
-
-# 1. MCP Registry (feeds MCP Toplist automatically)
-Write-Host "[1/3] MCP Registry" -ForegroundColor Yellow
-$publisher = "C:\tmp\mcp-publisher.exe"
-if (-not (Test-Path $publisher)) {
- Write-Host " Downloading mcp-publisher..." -ForegroundColor Gray
- $arch = if ([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture -eq "Arm64") { "arm64" } else { "amd64" }
- Invoke-WebRequest -Uri "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_windows_$arch.tar.gz" -OutFile "$env:TEMP\mcp-publisher.tar.gz"
- tar xf "$env:TEMP\mcp-publisher.tar.gz" mcp-publisher.exe
- $publisher = ".\mcp-publisher.exe"
-}
-Write-Host " Running: $publisher login github" -ForegroundColor Gray
-& $publisher login github
-Write-Host " Running: $publisher publish" -ForegroundColor Gray
-& $publisher publish
-Write-Host " ✓ MCP Registry published (MCP Toplist syncs 2x daily)" -ForegroundColor Green
-Write-Host ""
-
-# 2. Glama
-Write-Host "[2/3] Glama" -ForegroundColor Yellow
-Write-Host " Opening Glama Add Server page..." -ForegroundColor Gray
-Start-Process "https://glama.ai/mcp/servers"
-Write-Host " → Click 'Add Server', sign in with GitHub, paste: $repoUrl" -ForegroundColor White
-Write-Host " → glama.json is already in the repo for maintainer claim" -ForegroundColor Gray
-Read-Host " Press Enter when done"
-Write-Host " ✓ Glama submitted" -ForegroundColor Green
-Write-Host ""
-
-# 3. LobeHub
-Write-Host "[3/3] LobeHub" -ForegroundColor Yellow
-Write-Host " Running LobeHub CLI login..." -ForegroundColor Gray
-npx -y @lobehub/market-cli login
-npx -y @lobehub/market-cli github connect
-npx -y @lobehub/market-cli plugin submit $repoUrl
-Write-Host " ✓ LobeHub submitted" -ForegroundColor Green
-Write-Host ""
-
-Write-Host "=== All submissions complete ===" -ForegroundColor Cyan
diff --git a/server.json b/server.json
deleted file mode 100644
index 9eeabd53..00000000
--- a/server.json
+++ /dev/null
@@ -1 +0,0 @@
-{"$schema":"https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json","name":"io.github.coding-dev-tools/engraphis","description":"Local-first AI memory for agents with hybrid retrieval.","version":"1.4.5","repository":{"type":"git","url":"https://github.com/coding-dev-tools/engraphis","source":"github"},"homepage":"https://engraphis.com","license":"Apache-2.0","packages":[{"registryType":"pypi","identifier":"engraphis","version":"1.4.5","transport":{"type":"stdio"}}]}
\ No newline at end of file
diff --git a/tests/test_dashboard_v2.py b/tests/test_dashboard_v2.py
index 30714cd6..a7da41c8 100644
--- a/tests/test_dashboard_v2.py
+++ b/tests/test_dashboard_v2.py
@@ -1,1288 +1,1288 @@
-"""Unified local dashboard tests for the public open-core boundary."""
-import ast
-import io
-import threading
-import urllib.error
-from concurrent.futures import ThreadPoolExecutor
-from pathlib import Path
-
-import pytest
-
-pytest.importorskip("fastapi", reason="full-stack extra not installed")
-pytest.importorskip("httpx", reason="httpx not installed")
-
-from fastapi.testclient import TestClient # noqa: E402
-from fastapi import HTTPException # noqa: E402
-
-from engraphis import cloud_features # noqa: E402
-from engraphis.config import settings # noqa: E402
-from engraphis.cloud_features import CloudFeatureError # noqa: E402
-from engraphis.core.interfaces import MemoryType, Scope # noqa: E402
-from engraphis.routes import v2_api # noqa: E402
-from engraphis.service import MemoryService, ValidationError # noqa: E402
-
-
-def _client(monkeypatch, tmp_path):
- db_path = str(tmp_path / "dashboard.db")
- monkeypatch.setattr(settings, "db_path", db_path)
- monkeypatch.setattr(settings, "embed_model", "")
- monkeypatch.setattr(settings, "embed_dim", 384)
- monkeypatch.setattr(settings, "allowed_workspaces", [])
- monkeypatch.setattr(settings, "api_token", "")
- seeded = MemoryService.create(db_path)
- demo_id = seeded.store.get_or_create_workspace("demo")
- beta_id = seeded.store.get_or_create_workspace("beta")
- seeded.engine.remember(
- "Postgres 16 is the main database.",
- workspace_id=demo_id,
- scope=Scope.WORKSPACE,
- title="Database",
- )
- seeded.engine.remember(
- "A second workspace must stay isolated.",
- workspace_id=beta_id,
- scope=Scope.WORKSPACE,
- title="Isolation",
- )
- seeded.store.close()
- from engraphis.dashboard_app import create_app
- return TestClient(create_app(), client=("127.0.0.1", 50000))
-
-
-def test_dashboard_serves_and_bootstraps_local_core(monkeypatch, tmp_path):
- with _client(monkeypatch, tmp_path) as client:
- page = client.get("/")
- assert page.status_code == 200
- assert "
' in page.text
- assert page.text.index('id="memory-detail"') < page.text.index('id="memory-editor"')
- stylesheet = client.get("/v2-assets/ledger.css")
- assert ".library-detail-stack { display: grid; gap: 12px; align-content: start; }" in stylesheet.text
-
-
-def test_workspace_switcher_uses_the_active_ledger_theme_for_native_dropdowns(monkeypatch, tmp_path):
- with _client(monkeypatch, tmp_path) as client:
- stylesheet = client.get("/v2-assets/ledger.css")
- assert stylesheet.status_code == 200
- css = stylesheet.text
- assert ".workspace-switcher select {" in css
- assert "background: var(--c-inset);" in css
- assert "color-scheme: dark;" in css
- assert 'body[data-theme="paper"] .workspace-switcher select { color-scheme: light; }' in css
- assert ".workspace-switcher select option { background: var(--c-inset); color: var(--c-fg); }" in css
- assert ".workspace-switcher select option:checked { background: var(--c-acc); color: var(--c-bg); }" in css
-
-
-def test_sidebar_keeps_manage_and_compare_plans_in_separate_flex_rows(monkeypatch, tmp_path):
- with _client(monkeypatch, tmp_path) as client:
- stylesheet = client.get("/v2-assets/ledger.css")
- assert stylesheet.status_code == 200
- css = stylesheet.text
- sidebar = css[css.index(".sidebar {"):css.index(".brand-row {")]
- assert "display: flex;" in sidebar
- assert "flex-direction: column;" in sidebar
- assert "grid-template-rows" not in sidebar
- assert ".primary-nav { flex: 1 0 auto; }" in css
- assert ".manage-nav { flex: 0 0 auto; }" in css
- assert ".sidebar-promo {\n flex: 0 0 auto;" in css
-
-
-def test_dashboard_grounded_answer_route_cites_or_abstains(monkeypatch, tmp_path):
- with _client(monkeypatch, tmp_path) as client:
- grounded = client.post(
- "/api/answer",
- json={
- "query": "Which database is the main database?",
- "workspace": "demo",
- "k": 8,
- "max_citations": 5,
- "candidate_depth": "adaptive",
- },
- )
- assert grounded.status_code == 200
- body = grounded.json()
- assert body["query"] == "Which database is the main database?"
- assert body["grounded"] is True
- assert body["abstained"] is False
- assert body["citations"]
- assert body["sources"] == body["citations"]
- assert "[1]" in body["answer"]
- assert body["candidate_depth"] == "adaptive"
- # ``candidate_k_used`` is the final page depth after prompt-safe
- # overfetch/widening, rather than the adaptive policy's starting depth.
- assert body["candidate_k_used"] >= body["candidate_k_requested"]
-
- abstained = client.post(
- "/api/answer",
- json={
- "query": "How should I bake a sourdough loaf?",
- "workspace": "demo",
- },
- )
- assert abstained.status_code == 200
- assert abstained.json()["grounded"] is False
- assert abstained.json()["abstained"] is True
-
-
-def test_dashboard_grounded_answer_route_bounds_and_redacts(monkeypatch, tmp_path):
- with _client(monkeypatch, tmp_path) as client:
- assert client.post("/api/answer", json={"query": "", "workspace": "demo"}).status_code == 422
- assert client.post(
- "/api/answer",
- json={"query": "database", "workspace": "demo", "k": 51},
- ).status_code == 422
-
-
-def test_team_account_routes_are_not_in_public_runtime(monkeypatch, tmp_path):
- with _client(monkeypatch, tmp_path) as client:
- assert client.post("/api/auth/setup", json={}).status_code == 404
- assert client.get("/api/auth/users").status_code == 404
- state = client.get("/api/auth/state").json()
- assert state["enabled"] is False
- assert state["hosted_team"] is True
-
-
-def test_local_agent_write_has_no_client_side_team_paywall(monkeypatch, tmp_path):
- with _client(monkeypatch, tmp_path) as client:
- response = client.post(
- "/api/remember",
- json={"workspace": "demo", "content": "Queues use at-least-once delivery."},
- )
- assert response.status_code == 200
-
-
-def test_http_memory_api_exposes_world_timed_agent_writes_immediately(monkeypatch, tmp_path):
- with _client(monkeypatch, tmp_path) as client:
- old = client.post(
- "/api/remember",
- json={
- "workspace": "demo",
- "content": "The API rate limit is 100 requests per minute.",
- "valid_from": 1_000.0,
- "subject_key": "api.rate_limit",
- "claim_kind": "configured_value",
- },
- ).json()
- new = client.post(
- "/api/intent/remember",
- json={
- "workspace": "demo",
- "text": "The API rate limit is 500 requests per minute.",
- "valid_from": 2_000.0,
- "subject_key": "api.rate_limit",
- "claim_kind": "configured_value",
- },
- ).json()
-
- before = client.get(
- "/api/recall",
- params={
- "workspace": "demo",
- "q": "What is the API rate limit?",
- "as_of": 1_500.0,
- },
- )
- after = client.post(
- "/api/answer",
- json={
- "workspace": "demo",
- "query": "What is the API rate limit?",
- "as_of": 2_500.0,
- "min_support": 0.0,
- },
- )
-
- assert before.status_code == 200
- assert [memory["id"] for memory in before.json()["memories"]] == [old["id"]]
- assert after.status_code == 200
- assert after.json()["sources"]
- service = client.app.state.service
- assert service.store.get_memory(old["id"]).valid_from == 1_000.0
- assert service.store.get_memory(new["id"]).valid_from == 2_000.0
- assert service.store.get_memory(old["id"]).provenance["review_state"] == "approved"
- assert service.store.get_memory(new["id"]).provenance["review_state"] == "approved"
-
-
-def test_keyword_recall_fallback_keeps_bitemporal_visibility(monkeypatch, tmp_path):
- """A semantic-backend failure must not leak current facts into historical views."""
- with _client(monkeypatch, tmp_path) as client:
- svc = v2_api.service()
- workspace_id = svc.store.get_or_create_workspace("demo")
- old = {"id": svc.engine.remember(
- "The fallback retention setting was ten days.", workspace_id=workspace_id,
- scope=Scope.WORKSPACE, valid_from=1_000.0, resolve_conflicts=False,
- )}
- new = {"id": svc.engine.remember(
- "The fallback retention setting was thirty days.", workspace_id=workspace_id,
- scope=Scope.WORKSPACE, valid_from=2_000.0, resolve_conflicts=False,
- )}
- # The writes happened during this test, but the fixture models facts learned
- # before the requested historical system-time anchors.
- svc.store.conn.execute(
- "UPDATE memories SET ingested_at=100 WHERE id=?", (old["id"],)
- )
- svc.store.conn.execute(
- "UPDATE memories SET ingested_at=200 WHERE id=?", (new["id"],)
- )
- svc.store.conn.execute(
- "UPDATE memories SET valid_to=2000, valid_to_recorded_at=200, "
- "subject_key='retention.days', claim_kind='configured_value' "
- "WHERE id=?",
- (old["id"],),
- )
- svc.store.conn.commit()
- old_before = v2_api._keyword_search(
- "demo", "fallback retention", valid_at=1_500.0, known_at=3_000.0
- )
- old_known = v2_api._keyword_search(
- "demo", "fallback retention", valid_at=1_500.0, known_at=50.0
- )
- current = v2_api._keyword_search(
- "demo", "fallback retention", valid_at=2_500.0, known_at=3_000.0
- )
- closure_unknown = v2_api._keyword_search(
- "demo", "fallback retention", valid_at=2_500.0, known_at=150.0
- )
-
- assert [memory["id"] for memory in old_before] == [old["id"]]
- assert old_known == []
- assert [memory["id"] for memory in current] == [new["id"]]
- assert [memory["id"] for memory in closure_unknown] == [old["id"]]
- assert closure_unknown[0]["valid_to_recorded_at"] == 200.0
- assert closure_unknown[0]["subject_key"] == "retention.days"
- assert closure_unknown[0]["claim_kind"] == "configured_value"
-
- def incompatible_embedder(*_args, **_kwargs):
- raise ValueError("shapes (256,) and (384,) not aligned")
-
- monkeypatch.setattr(svc, "recall", incompatible_embedder)
- fallback = client.get(
- "/api/recall",
- params={
- "workspace": "demo", "q": "fallback retention",
- "valid_at": 2_500.0, "known_at": 150.0,
- },
- )
- assert fallback.status_code == 200
- assert fallback.json()["mode"] == "keyword"
- assert [item["id"] for item in fallback.json()["memories"]] == [old["id"]]
-
- compact_fallback = client.get(
- "/api/recall",
- params={
- "workspace": "demo", "q": "fallback retention", "response_mode": "compact",
- "token_budget": 0,
- },
- )
- payload = compact_fallback.json()
- assert compact_fallback.status_code == 200
- assert payload["mode"] == "keyword"
- assert payload["response_mode"] == "compact"
- assert payload["usage"]["budget_tokens"] == 0
- assert payload["usage"]["context_tokens"] == 0
- assert payload["memories"] and "content" not in payload["memories"][0]
-
-
-def test_keyword_recall_fallback_excludes_untrusted_memories(monkeypatch, tmp_path):
- """A degraded HTTP recall must enforce the same prompt eligibility boundary."""
- with _client(monkeypatch, tmp_path) as client:
- svc = v2_api.service()
- workspace_id = svc.store.get_or_create_workspace("demo")
- trusted = {"id": svc.engine.remember(
- "Fallback visibility trusted candidate.",
- workspace_id=workspace_id, scope=Scope.WORKSPACE,
- )}
- untrusted = svc.remember(
- "Fallback visibility untrusted candidate.",
- workspace="demo",
- source="sync",
- trusted=False,
- )
-
- def incompatible_embedder(*_args, **_kwargs):
- raise ValueError("shapes (256,) and (384,) not aligned")
-
- monkeypatch.setattr(svc, "recall", incompatible_embedder)
- response = client.get(
- "/api/recall",
- params={"workspace": "demo", "q": "fallback visibility candidate", "k": 1},
- )
-
- payload = response.json()
- assert response.status_code == 200
- assert payload["mode"] == "keyword"
- assert [memory["id"] for memory in payload["memories"]] == [trusted["id"]]
- assert untrusted["id"] not in {memory["id"] for memory in payload["memories"]}
- assert "untrusted candidate" not in repr(payload)
-
-
-def test_http_memory_api_rejects_backdated_agent_claim_supersession(
- monkeypatch, tmp_path
-):
- with _client(monkeypatch, tmp_path) as client:
- original = client.post(
- "/api/remember",
- json={
- "workspace": "demo",
- "content": "The deployment window is Friday afternoon.",
- "valid_from": 2_000.0,
- },
- ).json()
- service = v2_api.service()
- count_before = len(service.store.list_memories(include_invalid=True))
- rejected = client.post(
- "/api/remember",
- json={
- "workspace": "demo",
- "content": "The deployment window is Thursday afternoon.",
- "valid_from": 1_000.0,
- },
- )
-
- assert rejected.status_code == 400
- assert service.store.get_memory(original["id"]).valid_to is None
- assert len(service.store.list_memories(include_invalid=True)) == count_before
-
-
-def test_manual_consolidation_stays_local_but_dreaming_is_cloud_only(
- monkeypatch, tmp_path
-):
- with _client(monkeypatch, tmp_path) as client:
- manual = client.post(
- "/api/consolidate",
- json={"workspace": "demo", "dry_run": True, "infer": False},
- )
- assert manual.status_code == 200
- dream = client.post(
- "/api/consolidate",
- json={"workspace": "demo", "dry_run": True, "infer": True},
- )
- assert dream.status_code == 501
- assert dream.json()["detail"]["cloud_only"] is True
-
-
-def test_analytics_route_delegates_to_managed_compute(monkeypatch, tmp_path):
- monkeypatch.setattr(
- "engraphis.cloud_features.run_managed_job",
- lambda service, workspace, kind: {
- "result": {
- "kind": kind,
- "generation": 4,
- "totals": {"live": 1},
- }
- },
- )
- with _client(monkeypatch, tmp_path) as client:
- response = client.get("/api/analytics?workspace=demo")
- assert response.status_code == 200
- assert response.json()["kind"] == "analytics"
- assert response.json()["generation"] == 4
-
-
-def test_unconnected_automation_returns_a_structured_auth_error(monkeypatch, tmp_path):
- for name in (
- "ENGRAPHIS_CLOUD_ACCESS_TOKEN",
- "ENGRAPHIS_CLOUD_ORGANIZATION_ID",
- "ENGRAPHIS_CLOUD_COMPUTE_URL",
- "ENGRAPHIS_CLOUD_REFRESH_CREDENTIAL",
- "ENGRAPHIS_CLOUD_CONTROL_URL",
- ):
- monkeypatch.delenv(name, raising=False)
- monkeypatch.setenv("ENGRAPHIS_STATE_DIR", str(tmp_path / "unconnected-state"))
-
- with _client(monkeypatch, tmp_path) as client:
- response = client.get("/api/automation?workspace=demo")
-
- assert response.status_code == 401
- # The copy is ``_public_session_error(401)``: fixed, status-keyed, and actionable. The
- # generic placeholder told an unconnected customer nothing they could act on.
- assert response.json()["detail"] == {
- "error": "Connect this installation to Engraphis Cloud to use hosted features.",
- "managed_cloud": True,
- "transient": False,
- "code": "cloud_unconfigured",
- }
-
-
-def test_hosted_automation_accepts_the_cloud_policy_field(monkeypatch, tmp_path):
- saved = {}
-
- class _Cloud:
- def upload_snapshot(self, workspace_id, snapshot):
- return {"generation": snapshot["generation"]}
-
- def get_policy(self, workspace_id):
- return {"enabled": False, "cadence_minutes": 1440, "dream_enabled": False}
-
- def save_policy(self, workspace_id, policy):
- saved.update(policy)
- return {"version": 2}
-
- monkeypatch.setattr(
- "engraphis.cloud_features.build_managed_snapshot",
- lambda service, workspace: ("ws_cloud", {"generation": 1}),
- )
- monkeypatch.setattr(
- "engraphis.cloud_features.CloudFeatureClient.from_environment",
- lambda workspace_id=None: _Cloud(),
- )
- with _client(monkeypatch, tmp_path) as client:
- response = client.post(
- "/api/automation",
- json={"enabled": True, "dream_enabled": True, "cadence_hours": 12},
- )
- assert response.status_code == 200
- assert response.json()["dream_enabled"] is True
- assert saved["dream_enabled"] is True
-
-
-def test_first_hosted_automation_view_bootstraps_the_recommended_policy(
- monkeypatch, tmp_path
-):
- """A connected Pro/Team workspace starts maintaining itself without a toggle."""
-
- uploaded = []
- saved = []
-
- class _Cloud:
- organization_id = "org_test"
-
- def get_policy(self, workspace_id):
- # Version zero is the private Cloud's documented no-policy sentinel.
- return {"enabled": False, "cadence_minutes": 1440, "version": 0}
-
- def upload_snapshot(self, workspace_id, snapshot):
- uploaded.append((workspace_id, snapshot))
- return {"generation": snapshot["generation"]}
-
- def save_policy(self, workspace_id, policy):
- saved.append((workspace_id, policy))
- return {"version": 1}
-
- def list_jobs(self, workspace_id, *, limit=10):
- return {"jobs": []}
-
- monkeypatch.setattr(
- "engraphis.cloud_features.build_managed_snapshot",
- lambda service, workspace: ("ws_cloud", {"generation": 7}),
- )
- monkeypatch.setattr(
- "engraphis.cloud_features.CloudFeatureClient.from_environment",
- lambda workspace_id=None: _Cloud(),
- )
- with _client(monkeypatch, tmp_path) as client:
- response = client.get("/api/automation")
-
- assert response.status_code == 200
- assert response.json()["enabled"] is True
- assert response.json()["dream"] is True
- assert uploaded == [("ws_cloud", {"generation": 7})]
- assert saved == [("ws_cloud", {
- "enabled": True,
- "cadence_minutes": 1440,
- "dream_enabled": True,
- "dream_min_new": 25,
- "dream_idle_minutes": 15,
- "infer": False,
- })]
-
-
-def test_first_automation_policy_retry_does_not_upload_the_snapshot_twice(
- monkeypatch, tmp_path
-):
- """A failed policy write resumes after the already successful private upload."""
-
- from engraphis.cloud_features import CloudFeatureError
-
- uploaded = []
- saved = []
- builds = []
-
- class _Cloud:
- organization_id = "org_test"
-
- def get_policy(self, workspace_id):
- return {"enabled": False, "cadence_minutes": 1440, "version": 0}
-
- def upload_snapshot(self, workspace_id, snapshot):
- uploaded.append((workspace_id, snapshot))
- return {"generation": snapshot["generation"]}
-
- def save_policy(self, workspace_id, policy):
- saved.append((workspace_id, policy))
- if len(saved) == 1:
- raise CloudFeatureError(
- "Engraphis Cloud is temporarily unavailable.",
- status=503,
- transient=True,
- )
- return {"version": 1}
-
- def list_jobs(self, workspace_id, *, limit=10):
- return {"jobs": []}
-
- def _snapshot(service, workspace):
- builds.append(workspace)
- return "ws_cloud", {"generation": 7}
-
- monkeypatch.setattr("engraphis.cloud_features.build_managed_snapshot", _snapshot)
- monkeypatch.setattr(
- "engraphis.cloud_features.CloudFeatureClient.from_environment",
- lambda workspace_id=None: _Cloud(),
- )
- with _client(monkeypatch, tmp_path) as client:
- first = client.get("/api/automation")
- second = client.get("/api/automation")
-
- assert first.status_code == 503
- assert second.status_code == 200
- assert len(builds) == 1
- assert uploaded == [("ws_cloud", {"generation": 7})]
- assert len(saved) == 2
-
-
-def test_concurrent_first_automation_views_upload_one_snapshot(monkeypatch, tmp_path):
- """Parallel dashboard reads serialize the sensitive first-bootstrap upload."""
-
- uploaded = []
- saved = []
- started = threading.Event()
- release_upload = threading.Event()
-
- class _Cloud:
- organization_id = "org_concurrent"
-
- def get_policy(self, workspace_id):
- return {"enabled": False, "cadence_minutes": 1440, "version": 0}
-
- def upload_snapshot(self, workspace_id, snapshot):
- uploaded.append((workspace_id, snapshot))
- started.set()
- assert release_upload.wait(timeout=5)
- return {"generation": snapshot["generation"]}
-
- def save_policy(self, workspace_id, policy):
- saved.append((workspace_id, policy))
- return {"version": 1}
-
- def list_jobs(self, workspace_id, *, limit=10):
- return {"jobs": []}
-
- monkeypatch.setattr(
- "engraphis.cloud_features.build_managed_snapshot",
- lambda service, workspace: ("ws_cloud", {"generation": 7}),
- )
- monkeypatch.setattr(
- "engraphis.cloud_features.CloudFeatureClient.from_environment",
- lambda workspace_id=None: _Cloud(),
- )
- with _client(monkeypatch, tmp_path):
- with ThreadPoolExecutor(max_workers=2) as pool:
- first = pool.submit(v2_api.automation_get)
- assert started.wait(timeout=5)
- second = pool.submit(v2_api.automation_get)
- release_upload.set()
- assert first.result(timeout=5)["enabled"] is True
- follower = second.result(timeout=5)
- assert follower["enabled"] is True
- assert follower["version"] == 1
-
- assert uploaded == [("ws_cloud", {"generation": 7})]
- assert len(saved) == 1
-
-
-def test_reading_or_disabling_automation_never_uploads_memory_content(
- monkeypatch, tmp_path
-):
- saved = {}
-
- class _Cloud:
- def get_policy(self, workspace_id):
- return {"enabled": True, "cadence_minutes": 60, "dream_enabled": True}
-
- def list_jobs(self, workspace_id, *, limit=10):
- return {"jobs": []}
-
- def save_policy(self, workspace_id, policy):
- saved.update(policy)
- return {"version": 3}
-
- def _unexpected_upload(*args, **kwargs):
- raise AssertionError("policy inspection must not build or upload a snapshot")
-
- monkeypatch.setattr(
- "engraphis.cloud_features.build_managed_snapshot",
- _unexpected_upload,
- )
- monkeypatch.setattr(
- "engraphis.cloud_features.CloudFeatureClient.from_environment",
- lambda workspace_id=None: _Cloud(),
- )
- with _client(monkeypatch, tmp_path) as client:
- assert client.get("/api/automation").status_code == 200
- response = client.post("/api/automation", json={"enabled": False})
- assert response.status_code == 200
- assert saved["enabled"] is False
-
-
-def test_automation_and_maintenance_use_the_selected_workspace(monkeypatch, tmp_path):
- policy_workspaces = []
- snapshot_workspaces = []
- maintenance_workspaces = []
-
- class _Cloud:
- def get_policy(self, workspace_id):
- policy_workspaces.append(workspace_id)
- return {"enabled": False, "cadence_minutes": 60, "dream_enabled": True}
-
- def list_jobs(self, workspace_id, *, limit=10):
- policy_workspaces.append(workspace_id)
- return {"jobs": []}
-
- def upload_snapshot(self, workspace_id, snapshot):
- snapshot_workspaces.append(workspace_id)
- return {"generation": snapshot["generation"]}
-
- def save_policy(self, workspace_id, policy):
- policy_workspaces.append(workspace_id)
- return {"version": 1}
-
- def snapshot(service, workspace):
- snapshot_workspaces.append(workspace)
- return service._lookup_workspace(workspace), {"generation": 1}
-
- def managed_job(service, workspace, kind):
- maintenance_workspaces.append((workspace, kind))
- return {"result": {"kind": kind}}
-
- monkeypatch.setattr("engraphis.cloud_features.build_managed_snapshot", snapshot)
- monkeypatch.setattr("engraphis.cloud_features.run_managed_job", managed_job)
- monkeypatch.setattr(
- "engraphis.cloud_features.CloudFeatureClient.from_environment",
- lambda workspace_id=None: _Cloud(),
- )
- with _client(monkeypatch, tmp_path) as client:
- beta_id = client.app.state.service._lookup_workspace("beta")
- demo_id = client.app.state.service._lookup_workspace("demo")
- assert client.get("/api/automation?workspace=beta").status_code == 200
- assert client.post(
- "/api/automation?workspace=beta", json={"enabled": True}
- ).status_code == 200
- assert client.post(
- "/api/maintenance/run?workspace=beta", json={"dry_run": True}
- ).status_code == 200
-
- assert beta_id in policy_workspaces
- assert demo_id not in policy_workspaces
- assert "beta" in snapshot_workspaces
- assert maintenance_workspaces == [("beta", "consolidate")]
-
-
-def test_automation_workspace_query_unknown_is_not_replaced_by_legacy_default(
- monkeypatch, tmp_path
-):
- with _client(monkeypatch, tmp_path) as client:
- for method, path, payload in (
- (client.get, "/api/automation?workspace=missing", None),
- (client.post, "/api/automation?workspace=missing", {"enabled": False}),
- (client.post, "/api/maintenance/run?workspace=missing", {"dry_run": True}),
- ):
- response = method(path, json=payload) if payload is not None else method(path)
- assert response.status_code == 404
-
-
-def test_dashboard_automation_uses_active_workspace_and_discloses_upload_boundary():
- source = Path(__file__).parents[1] / "engraphis" / "static" / "dashboard.js"
- source = source.read_text(encoding="utf-8")
- assert "/automation?workspace=" in source
- assert "/maintenance/run?workspace=" in source
- assert "Preview snapshot" not in source
- assert "uploads the selected workspace’s normal and sensitive memory content" in source
- # The upload boundary is still disclosed, but consent now travels with the cloud
- # account: the dashboard must not name the operator override anywhere.
- assert "ENGRAPHIS_MANAGED_COMPUTE_CONSENT" not in source
- assert "Hosted work is automatic with Pro." in source
-
-
-def test_portfolio_and_report_analytics_are_hosted_only(monkeypatch, tmp_path):
- with _client(monkeypatch, tmp_path) as client:
- assert client.get("/api/analytics/portfolio").status_code == 501
- assert client.get("/api/analytics/export?workspace=demo").status_code == 501
-
-
-def test_raw_owner_export_is_free_and_signed_export_is_honestly_unimplemented(
- monkeypatch, tmp_path
-):
- """The signed variant must not claim to exist somewhere else.
-
- It previously answered ``cloud_only: True`` — but Engraphis Cloud has no export route,
- no supported hosted export capability, so that pointed a customer at a
- product that does not exist. The 501 now says the capability is unimplemented and names
- the working unsigned export instead.
- """
-
- with _client(monkeypatch, tmp_path) as client:
- raw = client.get("/api/export?workspace=demo")
- assert raw.status_code == 200
- assert raw.json()["counts"]["memories"] >= 1
- signed = client.get("/api/export?workspace=demo&signed=true")
- assert signed.status_code == 501
- detail = signed.json()["detail"]
- assert detail["implemented"] is False
- assert detail["alternative"] == "/export"
- assert "cloud_only" not in detail
- assert "Engraphis Cloud" not in detail["error"]
-
-
-def test_health_and_readiness_remain_public(monkeypatch, tmp_path):
- with _client(monkeypatch, tmp_path) as client:
- assert client.get("/api/health").status_code == 200
- assert client.get("/api/ready").status_code == 200
-
-
-def test_dashboard_exception_responses_do_not_echo_untrusted_exception_text():
- secret = "https://provider.example/?api_key=do-not-return-this"
-
- def fail_with(exc):
- raise exc
-
- with pytest.raises(HTTPException) as internal:
- v2_api._run(fail_with, RuntimeError(secret))
- assert internal.value.status_code == 500
- assert internal.value.detail == {"error": "internal server error"}
- assert secret not in repr(internal.value.detail)
-
- with pytest.raises(HTTPException) as validation:
- v2_api._run(fail_with, ValidationError(secret))
- assert validation.value.status_code == 400
- assert validation.value.detail == {"error": "invalid request"}
- assert secret not in repr(validation.value.detail)
-
- with pytest.raises(HTTPException) as downstream:
- v2_api._run(fail_with, HTTPException(status_code=418, detail={"error": secret}))
- assert downstream.value.status_code == 418
- assert downstream.value.detail == {"error": "request rejected"}
- assert secret not in repr(downstream.value.detail)
-
- with pytest.raises(HTTPException) as invalid_status:
- v2_api._run(fail_with, HTTPException(status_code=999, detail={"error": secret}))
- assert invalid_status.value.status_code == 500
- assert invalid_status.value.detail == {"error": "internal server error"}
- assert secret not in repr(invalid_status.value.detail)
-
- with pytest.raises(HTTPException) as mismatch:
- v2_api._run(fail_with, ValueError(f"{secret}: shapes 256 and 384 are not aligned"))
- assert mismatch.value.status_code == 409
- assert mismatch.value.detail["embedder"] is True
- assert secret not in repr(mismatch.value.detail)
-
- with pytest.raises(HTTPException) as ordinary_value_error:
- v2_api._run(fail_with, ValueError(secret))
- assert ordinary_value_error.value.status_code == 400
- assert ordinary_value_error.value.detail == {"error": "invalid request"}
- assert secret not in repr(ordinary_value_error.value.detail)
-
-
-def test_dashboard_engine_value_error_is_a_sanitized_client_error(monkeypatch, tmp_path):
- secret = "malformed document details must stay private"
- with _client(monkeypatch, tmp_path) as client:
- def reject_document(*_args, **_kwargs):
- raise ValueError(secret)
-
- monkeypatch.setattr(client.app.state.service, "remember", reject_document)
- response = client.post(
- "/api/remember",
- json={"content": "client document", "workspace": "demo"},
- )
-
- assert response.status_code == 400
- assert response.json() == {"detail": {"error": "invalid request"}}
- assert secret not in response.text
-
-
-def test_managed_cloud_errors_forward_only_bounded_public_copy():
- """``_managed_call`` forwards the message; the bound is the boundary's own check.
-
- ``CloudFeatureError`` is the already-redacted form -- every raise site builds it from
- fixed, status-keyed copy -- so its text is what the customer should read. The bound
- here is not the redaction, it is the guard for a message that is *not* that fixed copy:
- anything oversized, empty, or carrying control characters is dropped for the generic
- placeholder rather than rendered into a JSON error body.
- """
-
- def fail_with(exc):
- raise exc
-
- for message in ("x" * 301, "", "connection\x00reset", "trace\x1b[31m"):
- with pytest.raises(HTTPException) as caught:
- v2_api._managed_call(fail_with, CloudFeatureError(message, status=502))
- assert caught.value.status_code == 502
- assert caught.value.detail == {
- "error": v2_api._MANAGED_ERROR_FALLBACK, "managed_cloud": True,
- "transient": False,
- }
-
- with pytest.raises(HTTPException) as consent:
- v2_api._managed_call(
- fail_with,
- CloudFeatureError(
- "Managed compute is turned off for this installation.",
- status=409, code="consent_required",
- ),
- )
- assert consent.value.status_code == 409
- assert consent.value.detail == {
- "error": "Managed compute is turned off for this installation.",
- "managed_cloud": True,
- "transient": False,
- "code": "consent_required",
- }
-
- with pytest.raises(HTTPException) as unconfigured:
- v2_api._managed_call(
- fail_with,
- CloudFeatureError(
- "Connect this installation to Engraphis Cloud to use hosted features.",
- status=401, code="cloud_unconfigured",
- ),
- )
- assert unconfigured.value.status_code == 401
- assert unconfigured.value.detail == {
- "error": "Connect this installation to Engraphis Cloud to use hosted features.",
- "managed_cloud": True,
- "transient": False,
- "code": "cloud_unconfigured",
- }
-
-
-@pytest.mark.parametrize("status", (401, 402, 403))
-def test_managed_authorization_denial_settles_local_entitlement(monkeypatch, status):
- """A live hosted denial must immediately retire stale paid presentation state."""
-
- calls = []
- monkeypatch.setattr(v2_api, "_record_authoritative_denial", lambda: calls.append(status))
-
- def fail_with(exc):
- raise exc
-
- with pytest.raises(HTTPException) as caught:
- v2_api._managed_call(
- fail_with, CloudFeatureError("Engraphis Cloud authorization was rejected.",
- status=status),
- )
-
- assert caught.value.status_code == status
- assert calls == [status]
-
-
-@pytest.mark.parametrize("status", (409, 429, 503))
-def test_managed_non_authorization_failures_do_not_settle_entitlement(monkeypatch, status):
- """Conflicts and outages do not prove that a subscription or membership changed."""
-
- calls = []
- monkeypatch.setattr(v2_api, "_record_authoritative_denial", lambda: calls.append(status))
-
- def fail_with(exc):
- raise exc
-
- with pytest.raises(HTTPException):
- v2_api._managed_call(
- fail_with, CloudFeatureError("Engraphis Cloud temporarily failed.", status=status),
- )
-
- assert calls == []
-
-
-def _managed_http_failure(monkeypatch, status: int) -> HTTPException:
- """Drive one real hosted request against a control plane that answers ``status``."""
-
- class _Opener:
- def open(self, request, timeout=None):
- raise urllib.error.HTTPError(
- "https://compute.example.test/private", status, "failure", {},
- io.BytesIO(b'{"detail": "provider-internals https://backend.invalid"}'),
- )
-
- monkeypatch.setattr(
- cloud_features, "build_pinned_https_opener", lambda *handlers: _Opener()
- )
- client = cloud_features.CloudFeatureClient(
- "https://compute.example.test", "org_1", "token"
- )
- with pytest.raises(HTTPException) as caught:
- v2_api._managed_call(client._request, "GET", "/private")
- return caught.value
-
-
-def test_a_managed_outage_is_distinguishable_from_a_workspace_conflict(monkeypatch):
- """The defect: every hosted failure rendered as one fixed, unactionable string.
-
- ``cloud_features._public_http_error`` already produces redacted, status-keyed copy that
- tells a retryable outage apart from a conflict the customer has to fix -- and
- ``_managed_call`` threw all of it away, so the dashboard's error branch could only ever
- show "managed cloud operation failed" for a 429, a 5xx and a 409 alike.
- """
-
- busy = _managed_http_failure(monkeypatch, 429)
- down = _managed_http_failure(monkeypatch, 503)
- conflict = _managed_http_failure(monkeypatch, 409)
-
- assert busy.status_code == 429
- assert busy.detail["transient"] is True
- assert "temporarily busy" in busy.detail["error"], busy.detail["error"]
-
- assert down.status_code == 503
- assert down.detail["transient"] is True
- assert "temporarily unavailable" in down.detail["error"], down.detail["error"]
-
- assert conflict.status_code == 409
- assert conflict.detail["transient"] is False
- assert "workspace state" in conflict.detail["error"], conflict.detail["error"]
-
- messages = {busy.detail["error"], down.detail["error"], conflict.detail["error"]}
- assert len(messages) == 3, "the dashboard still cannot tell these three apart"
- assert v2_api._MANAGED_ERROR_FALLBACK not in messages
- # Forwarding the public copy must not forward the provider's body with it.
- assert all("provider-internals" not in text for text in messages)
- assert all("backend.invalid" not in text for text in messages)
-
-
-def test_every_managed_cloud_error_message_is_fixed_local_copy():
- """The invariant that makes forwarding safe, pinned against future raise sites.
-
- ``_managed_call`` may forward a ``CloudFeatureError`` message only because every one of
- them is built from a literal in this repository -- never from a provider body, a
- ``CloudSessionError``, or a local path. A raise site that interpolated a runtime value
- would silently turn this boundary into a reflection point, so the shape is asserted
- rather than trusted.
-
- Three forms are accepted: a string literal; a name bound from ``_public_http_error`` /
- ``_public_session_error`` (both of which switch on a bare integer status and return
- fixed copy); and the one audited ``%`` template, below.
- """
-
- source = Path(cloud_features.__file__).read_text(encoding="utf-8")
- tree = ast.parse(source)
-
- public_copy = {"_public_http_error", "_public_session_error"}
- from_public_copy = set()
- for node in ast.walk(tree):
- if not isinstance(node, ast.Assign) or not isinstance(node.value, ast.Call):
- continue
- called = node.value.func
- if not isinstance(called, ast.Name) or called.id not in public_copy:
- continue
- for target in node.targets:
- elements = target.elts if isinstance(target, ast.Tuple) else [target]
- from_public_copy.update(
- item.id for item in elements if isinstance(item, ast.Name)
- )
- assert from_public_copy, "the fixed-copy helpers are no longer bound to a name"
-
- interpolated = []
- for node in ast.walk(tree):
- if not isinstance(node, ast.Call):
- continue
- name = node.func.id if isinstance(node.func, ast.Name) else None
- if name != "CloudFeatureError" or not node.args:
- continue
- message = node.args[0]
- if isinstance(message, ast.Constant) and isinstance(message.value, str):
- continue
- if isinstance(message, ast.Name) and message.id in from_public_copy:
- continue
- # ``"literal %s" % (...)`` is allowed only where the substituted values are
- # themselves constrained to local literals; ``run_job`` is the single such site
- # and its ``status`` is guarded by an ``in {"failed", "canceled"}`` membership
- # test one line above. Anything else -- an f-string, a bare name, a concatenated
- # response field -- is a reflection risk and fails here.
- if (isinstance(message, ast.BinOp) and isinstance(message.op, ast.Mod)
- and isinstance(message.left, ast.Constant)
- and message.left.value == "Managed %s did not complete (%s)."):
- continue
- interpolated.append((node.lineno, ast.dump(message)[:120]))
-
- assert interpolated == [], (
- "a CloudFeatureError message is no longer fixed local copy; _managed_call "
- "forwards it to the customer: %r" % (interpolated,)
- )
+"""Unified local dashboard tests for the public open-core boundary."""
+import ast
+import io
+import threading
+import urllib.error
+from concurrent.futures import ThreadPoolExecutor
+from pathlib import Path
+
+import pytest
+
+pytest.importorskip("fastapi", reason="full-stack extra not installed")
+pytest.importorskip("httpx", reason="httpx not installed")
+
+from fastapi.testclient import TestClient # noqa: E402
+from fastapi import HTTPException # noqa: E402
+
+from engraphis import cloud_features # noqa: E402
+from engraphis.config import settings # noqa: E402
+from engraphis.cloud_features import CloudFeatureError # noqa: E402
+from engraphis.core.interfaces import MemoryType, Scope # noqa: E402
+from engraphis.routes import v2_api # noqa: E402
+from engraphis.service import MemoryService, ValidationError # noqa: E402
+
+
+def _client(monkeypatch, tmp_path):
+ db_path = str(tmp_path / "dashboard.db")
+ monkeypatch.setattr(settings, "db_path", db_path)
+ monkeypatch.setattr(settings, "embed_model", "")
+ monkeypatch.setattr(settings, "embed_dim", 384)
+ monkeypatch.setattr(settings, "allowed_workspaces", [])
+ monkeypatch.setattr(settings, "api_token", "")
+ seeded = MemoryService.create(db_path)
+ demo_id = seeded.store.get_or_create_workspace("demo")
+ beta_id = seeded.store.get_or_create_workspace("beta")
+ seeded.engine.remember(
+ "Postgres 16 is the main database.",
+ workspace_id=demo_id,
+ scope=Scope.WORKSPACE,
+ title="Database",
+ )
+ seeded.engine.remember(
+ "A second workspace must stay isolated.",
+ workspace_id=beta_id,
+ scope=Scope.WORKSPACE,
+ title="Isolation",
+ )
+ seeded.store.close()
+ from engraphis.dashboard_app import create_app
+ return TestClient(create_app(), client=("127.0.0.1", 50000))
+
+
+def test_dashboard_serves_and_bootstraps_local_core(monkeypatch, tmp_path):
+ with _client(monkeypatch, tmp_path) as client:
+ page = client.get("/")
+ assert page.status_code == 200
+ assert "
Engraphis Ledger" in page.text
+ assert 'class="sidebar"' in page.text
+ for area in ("Today", "Ask", "Library", "Graph & Relationships", "Provenance", "Manage"):
+ assert f">{area}<" in page.text
+ assert 'value="matrix">Matrix' in page.text
+ assert 'class="dashboard-switcher" aria-label="Dashboard interface"' in page.text
+ assert 'id="sidebar-theme-select" aria-label="Dashboard theme"' in page.text
+ assert 'value="classic">Classic<' in page.text
+ assert 'href="/classic">Classic<' in page.text
+ assert 'Ledger (primary)' not in page.text
+ assert 'Classic (alternate)' not in page.text
+ assert '/v2-assets/vendor/d3.min.js' in page.text
+ assert '/v2-assets/vendor/force-graph.min.js' not in page.text
+ assert '/v2-assets/engraphis-graph.js' not in page.text
+ classic = client.get("/classic")
+ assert classic.status_code == 200
+ assert '/classic-assets/dashboard.css' in classic.text
+ assert 'class="dashboard-switcher" aria-label="Dashboard interface"' in classic.text
+ assert 'href="/"' in classic.text
+ assert 'href="/classic" aria-current="page">Classic (alternate)<' in classic.text
+ assert 'value="classic" selected>Classic dashboard (alternate)<' in classic.text
+ assert 'id="graph-show-all"' not in classic.text
+ assert client.get("/v2-assets/ledger.css").status_code == 200
+ ledger_js = client.get("/v2-assets/ledger.js")
+ assert ledger_js.status_code == 200
+ assert "'/v2-assets/vendor/force-graph.min.js?v=20260727-final'" in ledger_js.text
+ assert "'/v2-assets/engraphis-graph.js?v=20260730-drag-stability'" in ledger_js.text
+ assert "/v2-assets/ledger.css?v=20260728-connected-memories" in page.text
+ assert "/v2-assets/ledger.js?v=20260728-connected-memories" in page.text
+ classic_js = client.get("/classic-assets/dashboard.js")
+ assert classic_js.status_code == 200
+ assert "/static/vendor/force-graph.min.js" in classic_js.text
+ assert "/v2-assets/engraphis-graph.js?v=20260730-drag-stability" in classic_js.text
+ assert "graphLimit=GRAPH_FULL?20000:320" in classic_js.text
+ assert "graphScope=GRAPH_FULL?'&full=true':(showUnlinked?'':'&connected_only=true')" in classic_js.text
+ bootstrap = client.get("/api/bootstrap")
+ assert bootstrap.status_code == 200
+ assert bootstrap.json()["stats"]["memories"] >= 1
+ savings = client.get("/api/context-savings", params={"workspace": "demo"})
+ assert savings.status_code == 200
+ assert savings.json()["format"] == "engraphis-context-savings/1"
+ filtered = client.get(
+ "/api/context-savings",
+ params={"workspace": "demo", "from_ts": 0, "to_ts": 9_999_999_999,
+ "release_version": "1.5"},
+ )
+ assert filtered.status_code == 200
+ assert filtered.json()["period"] == {"from_ts": 0, "to_ts": 9_999_999_999}
+ assert "Estimated context saved" in page.text
+
+
+def test_dashboard_assets_revalidate_instead_of_pinning_old_visuals(monkeypatch, tmp_path):
+ with _client(monkeypatch, tmp_path) as client:
+ for path in (
+ "/v2-assets/engraphis-graph.js?v=20260730-drag-stability",
+ "/v2-assets/ledger.js?v=20260728-connected-memories",
+ "/v2-assets/ledger.css?v=20260728-connected-memories",
+ "/classic-assets/dashboard.js?v=20260728-reference-materials",
+ ):
+ response = client.get(path)
+ assert response.status_code == 200
+ assert response.headers["cache-control"] == "no-cache, must-revalidate"
+
+
+def test_classic_dashboard_script_mirrors_the_static_compatibility_asset():
+ root = Path(__file__).parents[1] / "engraphis"
+ assert (root / "classic_assets" / "dashboard.js").read_bytes() == (
+ root / "static" / "dashboard.js"
+ ).read_bytes()
+
+
+def test_dashboard_and_mcp_recall_share_the_v2_service(monkeypatch, tmp_path):
+ pytest.importorskip("mcp", reason="MCP extra not installed")
+ import json
+
+ from engraphis import mcp_server
+
+ with _client(monkeypatch, tmp_path) as client:
+ assert mcp_server.service() is client.app.state.service
+ response = client.get(
+ "/api/recall",
+ params={"q": "which database do we use", "workspace": "demo", "k": 3},
+ )
+ assert response.status_code == 200
+ dashboard = response.json()
+ mcp = json.loads(mcp_server.engraphis_recall(
+ query="which database do we use", workspace="demo", k=3,
+ ))
+ assert [memory["id"] for memory in dashboard["memories"]] == [
+ memory["id"] for memory in mcp["memories"]
+ ]
+ assert [memory["retention"] for memory in dashboard["memories"]] == [
+ memory["retention"] for memory in mcp["memories"]
+ ]
+ assert [memory["relative_score"] for memory in dashboard["memories"]] == [
+ memory["relative_score"] for memory in mcp["memories"]
+ ]
+ assert [memory["absolute_support"] for memory in dashboard["memories"]] == [
+ memory["absolute_support"] for memory in mcp["memories"]
+ ]
+ assert dashboard["score_semantics"] == mcp["score_semantics"]
+
+
+def test_dashboard_keyword_fallback_reports_truthful_lexical_scores(monkeypatch, tmp_path):
+ with _client(monkeypatch, tmp_path) as client:
+ def mismatched_embedder(*_args, **_kwargs):
+ raise ValueError("shapes (1,256) and (384,1) not aligned")
+
+ monkeypatch.setattr(client.app.state.service, "recall", mismatched_embedder)
+ response = client.get(
+ "/api/recall",
+ params={
+ "q": "which database do we use",
+ "workspace": "demo",
+ "k": 3,
+ "response_mode": "compact",
+ },
+ )
+
+ assert response.status_code == 200
+ payload = response.json()
+ assert payload["mode"] == "keyword"
+ assert "lexical Jaccard" in payload["score_semantics"]["relative_score"]
+ assert "Semantic support is unavailable" in (
+ payload["score_semantics"]["absolute_support"]
+ )
+ memory = payload["memories"][0]
+ assert memory["score"] == memory["relative_score"] == 1.0
+ assert 0.0 < memory["absolute_support"] < 1.0
+ assert memory["arm"] == "lexical"
+ assert "content" not in memory
+
+
+def test_dashboard_keyword_fallback_applies_requested_memory_type_limits(
+ monkeypatch, tmp_path
+):
+ with _client(monkeypatch, tmp_path) as client:
+ workspace_id = client.app.state.service.store.get_or_create_workspace("demo")
+ client.app.state.service.engine.remember(
+ "Database upgrade procedure requires a verified backup.",
+ workspace_id=workspace_id,
+ scope=Scope.WORKSPACE,
+ mtype=MemoryType.PROCEDURAL,
+ title="Database procedure",
+ )
+
+ def mismatched_embedder(*_args, **_kwargs):
+ raise ValueError("shapes (1,256) and (384,1) not aligned")
+
+ monkeypatch.setattr(client.app.state.service, "recall", mismatched_embedder)
+ response = client.get(
+ "/api/recall",
+ params={
+ "q": "database",
+ "workspace": "demo",
+ "k": 3,
+ "mtype_limits": '{"semantic":0,"procedural":1}',
+ },
+ )
+
+ assert response.status_code == 200
+ payload = response.json()
+ assert payload["mtype_limits"] == {"semantic": 0, "procedural": 1}
+ assert [memory["memory_type"] for memory in payload["memories"]] == [
+ "procedural"
+ ]
+
+
+@pytest.mark.parametrize("invalid_limit", [True, "2"])
+def test_dashboard_post_recall_surfaces_reject_coerced_memory_type_limits(
+ monkeypatch, tmp_path, invalid_limit
+):
+ with _client(monkeypatch, tmp_path) as client:
+ response = client.post(
+ "/api/intent/recall",
+ json={"query": "database", "mtype_limits": {"semantic": invalid_limit}},
+ )
+
+ assert response.status_code == 422
+
+
+def test_dashboard_serves_the_graph_engine_from_its_v2_asset_surface(monkeypatch, tmp_path):
+ with _client(monkeypatch, tmp_path) as client:
+ asset = client.get("/v2-assets/engraphis-graph.js")
+ assert asset.status_code == 200
+ assert "window.EngraphisGraph =" in asset.text
+ compat = client.get("/v2-assets/engraphis-graph-compat.js")
+ assert compat.status_code == 200
+ assert "window.EngraphisGraphCompat =" in compat.text
+ assert client.get("/v2-assets/vendor/d3.min.js").status_code == 200
+ assert client.get("/v2-assets/vendor/force-graph.min.js").status_code == 200
+
+
+def test_graph_load_is_bounded_single_flight_and_retryable(monkeypatch, tmp_path):
+ with _client(monkeypatch, tmp_path) as client:
+ page = client.get("/")
+ script = client.get("/v2-assets/ledger.js")
+ assert 'id="graph-retry"' in page.text
+ assert 'id="graph-full"' not in page.text
+ assert '>Show all nodes<' not in page.text
+ assert 'id="graph-show-unlinked"' in page.text
+ assert 'id="graph-unlinked"' not in page.text
+ assert 'id="graph-tune-unlinked"' not in page.text
+ assert 'id="graph-style" type="hidden" value="cyber"' in page.text
+ assert "const GRAPH_INITIAL_NODE_LIMIT = 320;" in script.text
+ assert "const GRAPH_FULL_NODE_LIMIT = 20_000;" in script.text
+ assert "const GRAPH_LOAD_TIMEOUT_MS = 12_000;" in script.text
+ assert "AbortController" in script.text
+ assert "state.graphLoadPromise" in script.text
+ assert "&full=true" in script.text
+ assert "&connected_only=true" in script.text
+ assert "style: 'cyber'" in script.text
+ assert "renderMode: targetMode" in script.text
+ assert "loadGraph({ force: true })" in script.text
+
+
+def test_graph_motion_saved_views_and_tuning_controls_are_wired(monkeypatch, tmp_path):
+ with _client(monkeypatch, tmp_path) as client:
+ page = client.get("/")
+ script = client.get("/v2-assets/ledger.js")
+ for control in (
+ 'id="graph-flow-speed"', 'data-graph-saved-view="operations"',
+ 'data-graph-saved-view="schema"', 'data-graph-saved-view="people"',
+ 'data-graph-saved-view="code"', 'id="graph-save-view"',
+ 'id="graph-repel"', 'id="graph-depth"', 'id="graph-reset-tuning"',
+ 'data-graph-layer="code"',
+ ):
+ assert control in page.text
+ for behavior in (
+ "function applyGraphView(id)", "function resetGraphTuning()",
+ "function saveCurrentGraphView()", "function graphTuningSettings()",
+ "&include_code=true", "graph.setLayers(graphLayerState())",
+ "setSettings({ flowSpeed: speed })",
+ ):
+ assert behavior in script.text
+
+
+def test_graph_palette_recolors_every_colour_mode(monkeypatch, tmp_path):
+ with _client(monkeypatch, tmp_path) as client:
+ engine = client.get("/v2-assets/engraphis-graph.js")
+ ledger = client.get("/v2-assets/ledger.js")
+ assert engine.status_code == 200
+ assert "function selectedPalette()" in engine.text
+ assert "function commPal() {" in engine.text
+ assert "return selectedPalette() ||" in engine.text
+ assert "const colors = selectedPalette() || GRAPH_HEAT;" in engine.text
+ # Palettes still recolor every identity mode, but material families stay stable:
+ # semantic color belongs to the slim identity ring rather than rotating the whole
+ # Cyber film into arbitrary green/yellow alloys.
+ assert "function iridescentTint(c)" not in engine.text
+ assert "fixedPalette" in engine.text
+ assert "function identityRing(" in engine.text
+ assert "identity: rgbString(identity)" in engine.text
+ assert "function graphThemeColors()" in ledger.text
+ assert "graph.setThemeColors(graphThemeColors());" in ledger.text
+ assert "state.graphEngine.setThemeColors(graphThemeColors());" in ledger.text
+ assert "renderMode: opts.renderMode === 'full' ? 'full' : 'overview'" in engine.text
+ assert "function pinFullGraphLayout(data)" in engine.text
+
+
+def test_graph_facts_and_search_use_the_atomic_node_reveal(monkeypatch, tmp_path):
+ with _client(monkeypatch, tmp_path) as client:
+ page = client.get("/")
+ ledger = client.get("/v2-assets/ledger.js")
+ engine = client.get("/v2-assets/engraphis-graph.js")
+ assert 'id="graph-connections-dialog"' in page.text
+ assert "function revealGraphNode(id, label = 'Selected entity')" in ledger.text
+ assert "revealGraphNode(item.id, item.name)" in ledger.text
+ assert "function openGraphConnections(item)" in ledger.text
+ assert "function showGraphConnectionMemories(item)" in ledger.text
+ assert "onNodeClick: item => openGraphConnections(item)" in ledger.text
+ assert "api.reveal = id =>" in engine.text
+ assert "function centerRenderedNode(id)" in engine.text
+ assert "suppressNodeClickAfterDrag" in engine.text
+ assert "render(true, true);" not in engine.text[engine.text.index("api.focus = id =>"):engine.text.index("api.clearFocus")]
+
+
+def test_library_editor_stacks_directly_below_the_selected_memory_panel(monkeypatch, tmp_path):
+ with _client(monkeypatch, tmp_path) as client:
+ page = client.get("/")
+ assert page.status_code == 200
+ assert '
' in page.text
+ assert page.text.index('id="memory-detail"') < page.text.index('id="memory-editor"')
+ stylesheet = client.get("/v2-assets/ledger.css")
+ assert ".library-detail-stack { display: grid; gap: 12px; align-content: start; }" in stylesheet.text
+
+
+def test_workspace_switcher_uses_the_active_ledger_theme_for_native_dropdowns(monkeypatch, tmp_path):
+ with _client(monkeypatch, tmp_path) as client:
+ stylesheet = client.get("/v2-assets/ledger.css")
+ assert stylesheet.status_code == 200
+ css = stylesheet.text
+ assert ".workspace-switcher select {" in css
+ assert "background: var(--c-inset);" in css
+ assert "color-scheme: dark;" in css
+ assert 'body[data-theme="paper"] .workspace-switcher select { color-scheme: light; }' in css
+ assert ".workspace-switcher select option { background: var(--c-inset); color: var(--c-fg); }" in css
+ assert ".workspace-switcher select option:checked { background: var(--c-acc); color: var(--c-bg); }" in css
+
+
+def test_sidebar_keeps_manage_and_compare_plans_in_separate_flex_rows(monkeypatch, tmp_path):
+ with _client(monkeypatch, tmp_path) as client:
+ stylesheet = client.get("/v2-assets/ledger.css")
+ assert stylesheet.status_code == 200
+ css = stylesheet.text
+ sidebar = css[css.index(".sidebar {"):css.index(".brand-row {")]
+ assert "display: flex;" in sidebar
+ assert "flex-direction: column;" in sidebar
+ assert "grid-template-rows" not in sidebar
+ assert ".primary-nav { flex: 1 0 auto; }" in css
+ assert ".manage-nav { flex: 0 0 auto; }" in css
+ assert ".sidebar-promo {\n flex: 0 0 auto;" in css
+
+
+def test_dashboard_grounded_answer_route_cites_or_abstains(monkeypatch, tmp_path):
+ with _client(monkeypatch, tmp_path) as client:
+ grounded = client.post(
+ "/api/answer",
+ json={
+ "query": "Which database is the main database?",
+ "workspace": "demo",
+ "k": 8,
+ "max_citations": 5,
+ "candidate_depth": "adaptive",
+ },
+ )
+ assert grounded.status_code == 200
+ body = grounded.json()
+ assert body["query"] == "Which database is the main database?"
+ assert body["grounded"] is True
+ assert body["abstained"] is False
+ assert body["citations"]
+ assert body["sources"] == body["citations"]
+ assert "[1]" in body["answer"]
+ assert body["candidate_depth"] == "adaptive"
+ # ``candidate_k_used`` is the final page depth after prompt-safe
+ # overfetch/widening, rather than the adaptive policy's starting depth.
+ assert body["candidate_k_used"] >= body["candidate_k_requested"]
+
+ abstained = client.post(
+ "/api/answer",
+ json={
+ "query": "How should I bake a sourdough loaf?",
+ "workspace": "demo",
+ },
+ )
+ assert abstained.status_code == 200
+ assert abstained.json()["grounded"] is False
+ assert abstained.json()["abstained"] is True
+
+
+def test_dashboard_grounded_answer_route_bounds_and_redacts(monkeypatch, tmp_path):
+ with _client(monkeypatch, tmp_path) as client:
+ assert client.post("/api/answer", json={"query": "", "workspace": "demo"}).status_code == 422
+ assert client.post(
+ "/api/answer",
+ json={"query": "database", "workspace": "demo", "k": 51},
+ ).status_code == 422
+
+
+def test_team_account_routes_are_not_in_public_runtime(monkeypatch, tmp_path):
+ with _client(monkeypatch, tmp_path) as client:
+ assert client.post("/api/auth/setup", json={}).status_code == 404
+ assert client.get("/api/auth/users").status_code == 404
+ state = client.get("/api/auth/state").json()
+ assert state["enabled"] is False
+ assert state["hosted_team"] is True
+
+
+def test_local_agent_write_has_no_client_side_team_paywall(monkeypatch, tmp_path):
+ with _client(monkeypatch, tmp_path) as client:
+ response = client.post(
+ "/api/remember",
+ json={"workspace": "demo", "content": "Queues use at-least-once delivery."},
+ )
+ assert response.status_code == 200
+
+
+def test_http_memory_api_exposes_world_timed_agent_writes_immediately(monkeypatch, tmp_path):
+ with _client(monkeypatch, tmp_path) as client:
+ old = client.post(
+ "/api/remember",
+ json={
+ "workspace": "demo",
+ "content": "The API rate limit is 100 requests per minute.",
+ "valid_from": 1_000.0,
+ "subject_key": "api.rate_limit",
+ "claim_kind": "configured_value",
+ },
+ ).json()
+ new = client.post(
+ "/api/intent/remember",
+ json={
+ "workspace": "demo",
+ "text": "The API rate limit is 500 requests per minute.",
+ "valid_from": 2_000.0,
+ "subject_key": "api.rate_limit",
+ "claim_kind": "configured_value",
+ },
+ ).json()
+
+ before = client.get(
+ "/api/recall",
+ params={
+ "workspace": "demo",
+ "q": "What is the API rate limit?",
+ "as_of": 1_500.0,
+ },
+ )
+ after = client.post(
+ "/api/answer",
+ json={
+ "workspace": "demo",
+ "query": "What is the API rate limit?",
+ "as_of": 2_500.0,
+ "min_support": 0.0,
+ },
+ )
+
+ assert before.status_code == 200
+ assert [memory["id"] for memory in before.json()["memories"]] == [old["id"]]
+ assert after.status_code == 200
+ assert after.json()["sources"]
+ service = client.app.state.service
+ assert service.store.get_memory(old["id"]).valid_from == 1_000.0
+ assert service.store.get_memory(new["id"]).valid_from == 2_000.0
+ assert service.store.get_memory(old["id"]).provenance["review_state"] == "approved"
+ assert service.store.get_memory(new["id"]).provenance["review_state"] == "approved"
+
+
+def test_keyword_recall_fallback_keeps_bitemporal_visibility(monkeypatch, tmp_path):
+ """A semantic-backend failure must not leak current facts into historical views."""
+ with _client(monkeypatch, tmp_path) as client:
+ svc = v2_api.service()
+ workspace_id = svc.store.get_or_create_workspace("demo")
+ old = {"id": svc.engine.remember(
+ "The fallback retention setting was ten days.", workspace_id=workspace_id,
+ scope=Scope.WORKSPACE, valid_from=1_000.0, resolve_conflicts=False,
+ )}
+ new = {"id": svc.engine.remember(
+ "The fallback retention setting was thirty days.", workspace_id=workspace_id,
+ scope=Scope.WORKSPACE, valid_from=2_000.0, resolve_conflicts=False,
+ )}
+ # The writes happened during this test, but the fixture models facts learned
+ # before the requested historical system-time anchors.
+ svc.store.conn.execute(
+ "UPDATE memories SET ingested_at=100 WHERE id=?", (old["id"],)
+ )
+ svc.store.conn.execute(
+ "UPDATE memories SET ingested_at=200 WHERE id=?", (new["id"],)
+ )
+ svc.store.conn.execute(
+ "UPDATE memories SET valid_to=2000, valid_to_recorded_at=200, "
+ "subject_key='retention.days', claim_kind='configured_value' "
+ "WHERE id=?",
+ (old["id"],),
+ )
+ svc.store.conn.commit()
+ old_before = v2_api._keyword_search(
+ "demo", "fallback retention", valid_at=1_500.0, known_at=3_000.0
+ )
+ old_known = v2_api._keyword_search(
+ "demo", "fallback retention", valid_at=1_500.0, known_at=50.0
+ )
+ current = v2_api._keyword_search(
+ "demo", "fallback retention", valid_at=2_500.0, known_at=3_000.0
+ )
+ closure_unknown = v2_api._keyword_search(
+ "demo", "fallback retention", valid_at=2_500.0, known_at=150.0
+ )
+
+ assert [memory["id"] for memory in old_before] == [old["id"]]
+ assert old_known == []
+ assert [memory["id"] for memory in current] == [new["id"]]
+ assert [memory["id"] for memory in closure_unknown] == [old["id"]]
+ assert closure_unknown[0]["valid_to_recorded_at"] == 200.0
+ assert closure_unknown[0]["subject_key"] == "retention.days"
+ assert closure_unknown[0]["claim_kind"] == "configured_value"
+
+ def incompatible_embedder(*_args, **_kwargs):
+ raise ValueError("shapes (256,) and (384,) not aligned")
+
+ monkeypatch.setattr(svc, "recall", incompatible_embedder)
+ fallback = client.get(
+ "/api/recall",
+ params={
+ "workspace": "demo", "q": "fallback retention",
+ "valid_at": 2_500.0, "known_at": 150.0,
+ },
+ )
+ assert fallback.status_code == 200
+ assert fallback.json()["mode"] == "keyword"
+ assert [item["id"] for item in fallback.json()["memories"]] == [old["id"]]
+
+ compact_fallback = client.get(
+ "/api/recall",
+ params={
+ "workspace": "demo", "q": "fallback retention", "response_mode": "compact",
+ "token_budget": 0,
+ },
+ )
+ payload = compact_fallback.json()
+ assert compact_fallback.status_code == 200
+ assert payload["mode"] == "keyword"
+ assert payload["response_mode"] == "compact"
+ assert payload["usage"]["budget_tokens"] == 0
+ assert payload["usage"]["context_tokens"] == 0
+ assert payload["memories"] and "content" not in payload["memories"][0]
+
+
+def test_keyword_recall_fallback_excludes_untrusted_memories(monkeypatch, tmp_path):
+ """A degraded HTTP recall must enforce the same prompt eligibility boundary."""
+ with _client(monkeypatch, tmp_path) as client:
+ svc = v2_api.service()
+ workspace_id = svc.store.get_or_create_workspace("demo")
+ trusted = {"id": svc.engine.remember(
+ "Fallback visibility trusted candidate.",
+ workspace_id=workspace_id, scope=Scope.WORKSPACE,
+ )}
+ untrusted = svc.remember(
+ "Fallback visibility untrusted candidate.",
+ workspace="demo",
+ source="sync",
+ trusted=False,
+ )
+
+ def incompatible_embedder(*_args, **_kwargs):
+ raise ValueError("shapes (256,) and (384,) not aligned")
+
+ monkeypatch.setattr(svc, "recall", incompatible_embedder)
+ response = client.get(
+ "/api/recall",
+ params={"workspace": "demo", "q": "fallback visibility candidate", "k": 1},
+ )
+
+ payload = response.json()
+ assert response.status_code == 200
+ assert payload["mode"] == "keyword"
+ assert [memory["id"] for memory in payload["memories"]] == [trusted["id"]]
+ assert untrusted["id"] not in {memory["id"] for memory in payload["memories"]}
+ assert "untrusted candidate" not in repr(payload)
+
+
+def test_http_memory_api_rejects_backdated_agent_claim_supersession(
+ monkeypatch, tmp_path
+):
+ with _client(monkeypatch, tmp_path) as client:
+ original = client.post(
+ "/api/remember",
+ json={
+ "workspace": "demo",
+ "content": "The deployment window is Friday afternoon.",
+ "valid_from": 2_000.0,
+ },
+ ).json()
+ service = v2_api.service()
+ count_before = len(service.store.list_memories(include_invalid=True))
+ rejected = client.post(
+ "/api/remember",
+ json={
+ "workspace": "demo",
+ "content": "The deployment window is Thursday afternoon.",
+ "valid_from": 1_000.0,
+ },
+ )
+
+ assert rejected.status_code == 400
+ assert service.store.get_memory(original["id"]).valid_to is None
+ assert len(service.store.list_memories(include_invalid=True)) == count_before
+
+
+def test_manual_consolidation_stays_local_but_dreaming_is_cloud_only(
+ monkeypatch, tmp_path
+):
+ with _client(monkeypatch, tmp_path) as client:
+ manual = client.post(
+ "/api/consolidate",
+ json={"workspace": "demo", "dry_run": True, "infer": False},
+ )
+ assert manual.status_code == 200
+ dream = client.post(
+ "/api/consolidate",
+ json={"workspace": "demo", "dry_run": True, "infer": True},
+ )
+ assert dream.status_code == 501
+ assert dream.json()["detail"]["cloud_only"] is True
+
+
+def test_analytics_route_delegates_to_managed_compute(monkeypatch, tmp_path):
+ monkeypatch.setattr(
+ "engraphis.cloud_features.run_managed_job",
+ lambda service, workspace, kind: {
+ "result": {
+ "kind": kind,
+ "generation": 4,
+ "totals": {"live": 1},
+ }
+ },
+ )
+ with _client(monkeypatch, tmp_path) as client:
+ response = client.get("/api/analytics?workspace=demo")
+ assert response.status_code == 200
+ assert response.json()["kind"] == "analytics"
+ assert response.json()["generation"] == 4
+
+
+def test_unconnected_automation_returns_a_structured_auth_error(monkeypatch, tmp_path):
+ for name in (
+ "ENGRAPHIS_CLOUD_ACCESS_TOKEN",
+ "ENGRAPHIS_CLOUD_ORGANIZATION_ID",
+ "ENGRAPHIS_CLOUD_COMPUTE_URL",
+ "ENGRAPHIS_CLOUD_REFRESH_CREDENTIAL",
+ "ENGRAPHIS_CLOUD_CONTROL_URL",
+ ):
+ monkeypatch.delenv(name, raising=False)
+ monkeypatch.setenv("ENGRAPHIS_STATE_DIR", str(tmp_path / "unconnected-state"))
+
+ with _client(monkeypatch, tmp_path) as client:
+ response = client.get("/api/automation?workspace=demo")
+
+ assert response.status_code == 401
+ # The copy is ``_public_session_error(401)``: fixed, status-keyed, and actionable. The
+ # generic placeholder told an unconnected customer nothing they could act on.
+ assert response.json()["detail"] == {
+ "error": "Connect this installation to Engraphis Cloud to use hosted features.",
+ "managed_cloud": True,
+ "transient": False,
+ "code": "cloud_unconfigured",
+ }
+
+
+def test_hosted_automation_accepts_the_cloud_policy_field(monkeypatch, tmp_path):
+ saved = {}
+
+ class _Cloud:
+ def upload_snapshot(self, workspace_id, snapshot):
+ return {"generation": snapshot["generation"]}
+
+ def get_policy(self, workspace_id):
+ return {"enabled": False, "cadence_minutes": 1440, "dream_enabled": False}
+
+ def save_policy(self, workspace_id, policy):
+ saved.update(policy)
+ return {"version": 2}
+
+ monkeypatch.setattr(
+ "engraphis.cloud_features.build_managed_snapshot",
+ lambda service, workspace: ("ws_cloud", {"generation": 1}),
+ )
+ monkeypatch.setattr(
+ "engraphis.cloud_features.CloudFeatureClient.from_environment",
+ lambda workspace_id=None: _Cloud(),
+ )
+ with _client(monkeypatch, tmp_path) as client:
+ response = client.post(
+ "/api/automation",
+ json={"enabled": True, "dream_enabled": True, "cadence_hours": 12},
+ )
+ assert response.status_code == 200
+ assert response.json()["dream_enabled"] is True
+ assert saved["dream_enabled"] is True
+
+
+def test_first_hosted_automation_view_bootstraps_the_recommended_policy(
+ monkeypatch, tmp_path
+):
+ """A connected Pro/Team workspace starts maintaining itself without a toggle."""
+
+ uploaded = []
+ saved = []
+
+ class _Cloud:
+ organization_id = "org_test"
+
+ def get_policy(self, workspace_id):
+ # Version zero is the private Cloud's documented no-policy sentinel.
+ return {"enabled": False, "cadence_minutes": 1440, "version": 0}
+
+ def upload_snapshot(self, workspace_id, snapshot):
+ uploaded.append((workspace_id, snapshot))
+ return {"generation": snapshot["generation"]}
+
+ def save_policy(self, workspace_id, policy):
+ saved.append((workspace_id, policy))
+ return {"version": 1}
+
+ def list_jobs(self, workspace_id, *, limit=10):
+ return {"jobs": []}
+
+ monkeypatch.setattr(
+ "engraphis.cloud_features.build_managed_snapshot",
+ lambda service, workspace: ("ws_cloud", {"generation": 7}),
+ )
+ monkeypatch.setattr(
+ "engraphis.cloud_features.CloudFeatureClient.from_environment",
+ lambda workspace_id=None: _Cloud(),
+ )
+ with _client(monkeypatch, tmp_path) as client:
+ response = client.get("/api/automation")
+
+ assert response.status_code == 200
+ assert response.json()["enabled"] is True
+ assert response.json()["dream"] is True
+ assert uploaded == [("ws_cloud", {"generation": 7})]
+ assert saved == [("ws_cloud", {
+ "enabled": True,
+ "cadence_minutes": 1440,
+ "dream_enabled": True,
+ "dream_min_new": 25,
+ "dream_idle_minutes": 15,
+ "infer": False,
+ })]
+
+
+def test_first_automation_policy_retry_does_not_upload_the_snapshot_twice(
+ monkeypatch, tmp_path
+):
+ """A failed policy write resumes after the already successful private upload."""
+
+ from engraphis.cloud_features import CloudFeatureError
+
+ uploaded = []
+ saved = []
+ builds = []
+
+ class _Cloud:
+ organization_id = "org_test"
+
+ def get_policy(self, workspace_id):
+ return {"enabled": False, "cadence_minutes": 1440, "version": 0}
+
+ def upload_snapshot(self, workspace_id, snapshot):
+ uploaded.append((workspace_id, snapshot))
+ return {"generation": snapshot["generation"]}
+
+ def save_policy(self, workspace_id, policy):
+ saved.append((workspace_id, policy))
+ if len(saved) == 1:
+ raise CloudFeatureError(
+ "Engraphis Cloud is temporarily unavailable.",
+ status=503,
+ transient=True,
+ )
+ return {"version": 1}
+
+ def list_jobs(self, workspace_id, *, limit=10):
+ return {"jobs": []}
+
+ def _snapshot(service, workspace):
+ builds.append(workspace)
+ return "ws_cloud", {"generation": 7}
+
+ monkeypatch.setattr("engraphis.cloud_features.build_managed_snapshot", _snapshot)
+ monkeypatch.setattr(
+ "engraphis.cloud_features.CloudFeatureClient.from_environment",
+ lambda workspace_id=None: _Cloud(),
+ )
+ with _client(monkeypatch, tmp_path) as client:
+ first = client.get("/api/automation")
+ second = client.get("/api/automation")
+
+ assert first.status_code == 503
+ assert second.status_code == 200
+ assert len(builds) == 1
+ assert uploaded == [("ws_cloud", {"generation": 7})]
+ assert len(saved) == 2
+
+
+def test_concurrent_first_automation_views_upload_one_snapshot(monkeypatch, tmp_path):
+ """Parallel dashboard reads serialize the sensitive first-bootstrap upload."""
+
+ uploaded = []
+ saved = []
+ started = threading.Event()
+ release_upload = threading.Event()
+
+ class _Cloud:
+ organization_id = "org_concurrent"
+
+ def get_policy(self, workspace_id):
+ return {"enabled": False, "cadence_minutes": 1440, "version": 0}
+
+ def upload_snapshot(self, workspace_id, snapshot):
+ uploaded.append((workspace_id, snapshot))
+ started.set()
+ assert release_upload.wait(timeout=5)
+ return {"generation": snapshot["generation"]}
+
+ def save_policy(self, workspace_id, policy):
+ saved.append((workspace_id, policy))
+ return {"version": 1}
+
+ def list_jobs(self, workspace_id, *, limit=10):
+ return {"jobs": []}
+
+ monkeypatch.setattr(
+ "engraphis.cloud_features.build_managed_snapshot",
+ lambda service, workspace: ("ws_cloud", {"generation": 7}),
+ )
+ monkeypatch.setattr(
+ "engraphis.cloud_features.CloudFeatureClient.from_environment",
+ lambda workspace_id=None: _Cloud(),
+ )
+ with _client(monkeypatch, tmp_path):
+ with ThreadPoolExecutor(max_workers=2) as pool:
+ first = pool.submit(v2_api.automation_get)
+ assert started.wait(timeout=5)
+ second = pool.submit(v2_api.automation_get)
+ release_upload.set()
+ assert first.result(timeout=5)["enabled"] is True
+ follower = second.result(timeout=5)
+ assert follower["enabled"] is True
+ assert follower["version"] == 1
+
+ assert uploaded == [("ws_cloud", {"generation": 7})]
+ assert len(saved) == 1
+
+
+def test_reading_or_disabling_automation_never_uploads_memory_content(
+ monkeypatch, tmp_path
+):
+ saved = {}
+
+ class _Cloud:
+ def get_policy(self, workspace_id):
+ return {"enabled": True, "cadence_minutes": 60, "dream_enabled": True}
+
+ def list_jobs(self, workspace_id, *, limit=10):
+ return {"jobs": []}
+
+ def save_policy(self, workspace_id, policy):
+ saved.update(policy)
+ return {"version": 3}
+
+ def _unexpected_upload(*args, **kwargs):
+ raise AssertionError("policy inspection must not build or upload a snapshot")
+
+ monkeypatch.setattr(
+ "engraphis.cloud_features.build_managed_snapshot",
+ _unexpected_upload,
+ )
+ monkeypatch.setattr(
+ "engraphis.cloud_features.CloudFeatureClient.from_environment",
+ lambda workspace_id=None: _Cloud(),
+ )
+ with _client(monkeypatch, tmp_path) as client:
+ assert client.get("/api/automation").status_code == 200
+ response = client.post("/api/automation", json={"enabled": False})
+ assert response.status_code == 200
+ assert saved["enabled"] is False
+
+
+def test_automation_and_maintenance_use_the_selected_workspace(monkeypatch, tmp_path):
+ policy_workspaces = []
+ snapshot_workspaces = []
+ maintenance_workspaces = []
+
+ class _Cloud:
+ def get_policy(self, workspace_id):
+ policy_workspaces.append(workspace_id)
+ return {"enabled": False, "cadence_minutes": 60, "dream_enabled": True}
+
+ def list_jobs(self, workspace_id, *, limit=10):
+ policy_workspaces.append(workspace_id)
+ return {"jobs": []}
+
+ def upload_snapshot(self, workspace_id, snapshot):
+ snapshot_workspaces.append(workspace_id)
+ return {"generation": snapshot["generation"]}
+
+ def save_policy(self, workspace_id, policy):
+ policy_workspaces.append(workspace_id)
+ return {"version": 1}
+
+ def snapshot(service, workspace):
+ snapshot_workspaces.append(workspace)
+ return service._lookup_workspace(workspace), {"generation": 1}
+
+ def managed_job(service, workspace, kind):
+ maintenance_workspaces.append((workspace, kind))
+ return {"result": {"kind": kind}}
+
+ monkeypatch.setattr("engraphis.cloud_features.build_managed_snapshot", snapshot)
+ monkeypatch.setattr("engraphis.cloud_features.run_managed_job", managed_job)
+ monkeypatch.setattr(
+ "engraphis.cloud_features.CloudFeatureClient.from_environment",
+ lambda workspace_id=None: _Cloud(),
+ )
+ with _client(monkeypatch, tmp_path) as client:
+ beta_id = client.app.state.service._lookup_workspace("beta")
+ demo_id = client.app.state.service._lookup_workspace("demo")
+ assert client.get("/api/automation?workspace=beta").status_code == 200
+ assert client.post(
+ "/api/automation?workspace=beta", json={"enabled": True}
+ ).status_code == 200
+ assert client.post(
+ "/api/maintenance/run?workspace=beta", json={"dry_run": True}
+ ).status_code == 200
+
+ assert beta_id in policy_workspaces
+ assert demo_id not in policy_workspaces
+ assert "beta" in snapshot_workspaces
+ assert maintenance_workspaces == [("beta", "consolidate")]
+
+
+def test_automation_workspace_query_unknown_is_not_replaced_by_legacy_default(
+ monkeypatch, tmp_path
+):
+ with _client(monkeypatch, tmp_path) as client:
+ for method, path, payload in (
+ (client.get, "/api/automation?workspace=missing", None),
+ (client.post, "/api/automation?workspace=missing", {"enabled": False}),
+ (client.post, "/api/maintenance/run?workspace=missing", {"dry_run": True}),
+ ):
+ response = method(path, json=payload) if payload is not None else method(path)
+ assert response.status_code == 404
+
+
+def test_dashboard_automation_uses_active_workspace_and_discloses_upload_boundary():
+ source = Path(__file__).parents[1] / "engraphis" / "static" / "dashboard.js"
+ source = source.read_text(encoding="utf-8")
+ assert "/automation?workspace=" in source
+ assert "/maintenance/run?workspace=" in source
+ assert "Preview snapshot" not in source
+ assert "uploads the selected workspace’s normal and sensitive memory content" in source
+ # The upload boundary is still disclosed, but consent now travels with the cloud
+ # account: the dashboard must not name the operator override anywhere.
+ assert "ENGRAPHIS_MANAGED_COMPUTE_CONSENT" not in source
+ assert "Hosted work is automatic with Pro." in source
+
+
+def test_portfolio_and_report_analytics_are_hosted_only(monkeypatch, tmp_path):
+ with _client(monkeypatch, tmp_path) as client:
+ assert client.get("/api/analytics/portfolio").status_code == 501
+ assert client.get("/api/analytics/export?workspace=demo").status_code == 501
+
+
+def test_raw_owner_export_is_free_and_signed_export_is_honestly_unimplemented(
+ monkeypatch, tmp_path
+):
+ """The signed variant must not claim to exist somewhere else.
+
+ It previously answered ``cloud_only: True`` — but Engraphis Cloud has no export route,
+ no supported hosted export capability, so that pointed a customer at a
+ product that does not exist. The 501 now says the capability is unimplemented and names
+ the working unsigned export instead.
+ """
+
+ with _client(monkeypatch, tmp_path) as client:
+ raw = client.get("/api/export?workspace=demo")
+ assert raw.status_code == 200
+ assert raw.json()["counts"]["memories"] >= 1
+ signed = client.get("/api/export?workspace=demo&signed=true")
+ assert signed.status_code == 501
+ detail = signed.json()["detail"]
+ assert detail["implemented"] is False
+ assert detail["alternative"] == "/export"
+ assert "cloud_only" not in detail
+ assert "Engraphis Cloud" not in detail["error"]
+
+
+def test_health_and_readiness_remain_public(monkeypatch, tmp_path):
+ with _client(monkeypatch, tmp_path) as client:
+ assert client.get("/api/health").status_code == 200
+ assert client.get("/api/ready").status_code == 200
+
+
+def test_dashboard_exception_responses_do_not_echo_untrusted_exception_text():
+ secret = "https://provider.example/?api_key=do-not-return-this"
+
+ def fail_with(exc):
+ raise exc
+
+ with pytest.raises(HTTPException) as internal:
+ v2_api._run(fail_with, RuntimeError(secret))
+ assert internal.value.status_code == 500
+ assert internal.value.detail == {"error": "internal server error"}
+ assert secret not in repr(internal.value.detail)
+
+ with pytest.raises(HTTPException) as validation:
+ v2_api._run(fail_with, ValidationError(secret))
+ assert validation.value.status_code == 400
+ assert validation.value.detail == {"error": "invalid request"}
+ assert secret not in repr(validation.value.detail)
+
+ with pytest.raises(HTTPException) as downstream:
+ v2_api._run(fail_with, HTTPException(status_code=418, detail={"error": secret}))
+ assert downstream.value.status_code == 418
+ assert downstream.value.detail == {"error": "request rejected"}
+ assert secret not in repr(downstream.value.detail)
+
+ with pytest.raises(HTTPException) as invalid_status:
+ v2_api._run(fail_with, HTTPException(status_code=999, detail={"error": secret}))
+ assert invalid_status.value.status_code == 500
+ assert invalid_status.value.detail == {"error": "internal server error"}
+ assert secret not in repr(invalid_status.value.detail)
+
+ with pytest.raises(HTTPException) as mismatch:
+ v2_api._run(fail_with, ValueError(f"{secret}: shapes 256 and 384 are not aligned"))
+ assert mismatch.value.status_code == 409
+ assert mismatch.value.detail["embedder"] is True
+ assert secret not in repr(mismatch.value.detail)
+
+ with pytest.raises(HTTPException) as ordinary_value_error:
+ v2_api._run(fail_with, ValueError(secret))
+ assert ordinary_value_error.value.status_code == 400
+ assert ordinary_value_error.value.detail == {"error": "invalid request"}
+ assert secret not in repr(ordinary_value_error.value.detail)
+
+
+def test_dashboard_engine_value_error_is_a_sanitized_client_error(monkeypatch, tmp_path):
+ secret = "malformed document details must stay private"
+ with _client(monkeypatch, tmp_path) as client:
+ def reject_document(*_args, **_kwargs):
+ raise ValueError(secret)
+
+ monkeypatch.setattr(client.app.state.service, "remember", reject_document)
+ response = client.post(
+ "/api/remember",
+ json={"content": "client document", "workspace": "demo"},
+ )
+
+ assert response.status_code == 400
+ assert response.json() == {"detail": {"error": "invalid request"}}
+ assert secret not in response.text
+
+
+def test_managed_cloud_errors_forward_only_bounded_public_copy():
+ """``_managed_call`` forwards the message; the bound is the boundary's own check.
+
+ ``CloudFeatureError`` is the already-redacted form -- every raise site builds it from
+ fixed, status-keyed copy -- so its text is what the customer should read. The bound
+ here is not the redaction, it is the guard for a message that is *not* that fixed copy:
+ anything oversized, empty, or carrying control characters is dropped for the generic
+ placeholder rather than rendered into a JSON error body.
+ """
+
+ def fail_with(exc):
+ raise exc
+
+ for message in ("x" * 301, "", "connection\x00reset", "trace\x1b[31m"):
+ with pytest.raises(HTTPException) as caught:
+ v2_api._managed_call(fail_with, CloudFeatureError(message, status=502))
+ assert caught.value.status_code == 502
+ assert caught.value.detail == {
+ "error": v2_api._MANAGED_ERROR_FALLBACK, "managed_cloud": True,
+ "transient": False,
+ }
+
+ with pytest.raises(HTTPException) as consent:
+ v2_api._managed_call(
+ fail_with,
+ CloudFeatureError(
+ "Managed compute is turned off for this installation.",
+ status=409, code="consent_required",
+ ),
+ )
+ assert consent.value.status_code == 409
+ assert consent.value.detail == {
+ "error": "Managed compute is turned off for this installation.",
+ "managed_cloud": True,
+ "transient": False,
+ "code": "consent_required",
+ }
+
+ with pytest.raises(HTTPException) as unconfigured:
+ v2_api._managed_call(
+ fail_with,
+ CloudFeatureError(
+ "Connect this installation to Engraphis Cloud to use hosted features.",
+ status=401, code="cloud_unconfigured",
+ ),
+ )
+ assert unconfigured.value.status_code == 401
+ assert unconfigured.value.detail == {
+ "error": "Connect this installation to Engraphis Cloud to use hosted features.",
+ "managed_cloud": True,
+ "transient": False,
+ "code": "cloud_unconfigured",
+ }
+
+
+@pytest.mark.parametrize("status", (401, 402, 403))
+def test_managed_authorization_denial_settles_local_entitlement(monkeypatch, status):
+ """A live hosted denial must immediately retire stale paid presentation state."""
+
+ calls = []
+ monkeypatch.setattr(v2_api, "_record_authoritative_denial", lambda: calls.append(status))
+
+ def fail_with(exc):
+ raise exc
+
+ with pytest.raises(HTTPException) as caught:
+ v2_api._managed_call(
+ fail_with, CloudFeatureError("Engraphis Cloud authorization was rejected.",
+ status=status),
+ )
+
+ assert caught.value.status_code == status
+ assert calls == [status]
+
+
+@pytest.mark.parametrize("status", (409, 429, 503))
+def test_managed_non_authorization_failures_do_not_settle_entitlement(monkeypatch, status):
+ """Conflicts and outages do not prove that a subscription or membership changed."""
+
+ calls = []
+ monkeypatch.setattr(v2_api, "_record_authoritative_denial", lambda: calls.append(status))
+
+ def fail_with(exc):
+ raise exc
+
+ with pytest.raises(HTTPException):
+ v2_api._managed_call(
+ fail_with, CloudFeatureError("Engraphis Cloud temporarily failed.", status=status),
+ )
+
+ assert calls == []
+
+
+def _managed_http_failure(monkeypatch, status: int) -> HTTPException:
+ """Drive one real hosted request against a control plane that answers ``status``."""
+
+ class _Opener:
+ def open(self, request, timeout=None):
+ raise urllib.error.HTTPError(
+ "https://compute.example.test/private", status, "failure", {},
+ io.BytesIO(b'{"detail": "provider-internals https://backend.invalid"}'),
+ )
+
+ monkeypatch.setattr(
+ cloud_features, "build_pinned_https_opener", lambda *handlers: _Opener()
+ )
+ client = cloud_features.CloudFeatureClient(
+ "https://compute.example.test", "org_1", "token"
+ )
+ with pytest.raises(HTTPException) as caught:
+ v2_api._managed_call(client._request, "GET", "/private")
+ return caught.value
+
+
+def test_a_managed_outage_is_distinguishable_from_a_workspace_conflict(monkeypatch):
+ """The defect: every hosted failure rendered as one fixed, unactionable string.
+
+ ``cloud_features._public_http_error`` already produces redacted, status-keyed copy that
+ tells a retryable outage apart from a conflict the customer has to fix -- and
+ ``_managed_call`` threw all of it away, so the dashboard's error branch could only ever
+ show "managed cloud operation failed" for a 429, a 5xx and a 409 alike.
+ """
+
+ busy = _managed_http_failure(monkeypatch, 429)
+ down = _managed_http_failure(monkeypatch, 503)
+ conflict = _managed_http_failure(monkeypatch, 409)
+
+ assert busy.status_code == 429
+ assert busy.detail["transient"] is True
+ assert "temporarily busy" in busy.detail["error"], busy.detail["error"]
+
+ assert down.status_code == 503
+ assert down.detail["transient"] is True
+ assert "temporarily unavailable" in down.detail["error"], down.detail["error"]
+
+ assert conflict.status_code == 409
+ assert conflict.detail["transient"] is False
+ assert "workspace state" in conflict.detail["error"], conflict.detail["error"]
+
+ messages = {busy.detail["error"], down.detail["error"], conflict.detail["error"]}
+ assert len(messages) == 3, "the dashboard still cannot tell these three apart"
+ assert v2_api._MANAGED_ERROR_FALLBACK not in messages
+ # Forwarding the public copy must not forward the provider's body with it.
+ assert all("provider-internals" not in text for text in messages)
+ assert all("backend.invalid" not in text for text in messages)
+
+
+def test_every_managed_cloud_error_message_is_fixed_local_copy():
+ """The invariant that makes forwarding safe, pinned against future raise sites.
+
+ ``_managed_call`` may forward a ``CloudFeatureError`` message only because every one of
+ them is built from a literal in this repository -- never from a provider body, a
+ ``CloudSessionError``, or a local path. A raise site that interpolated a runtime value
+ would silently turn this boundary into a reflection point, so the shape is asserted
+ rather than trusted.
+
+ Three forms are accepted: a string literal; a name bound from ``_public_http_error`` /
+ ``_public_session_error`` (both of which switch on a bare integer status and return
+ fixed copy); and the one audited ``%`` template, below.
+ """
+
+ source = Path(cloud_features.__file__).read_text(encoding="utf-8")
+ tree = ast.parse(source)
+
+ public_copy = {"_public_http_error", "_public_session_error"}
+ from_public_copy = set()
+ for node in ast.walk(tree):
+ if not isinstance(node, ast.Assign) or not isinstance(node.value, ast.Call):
+ continue
+ called = node.value.func
+ if not isinstance(called, ast.Name) or called.id not in public_copy:
+ continue
+ for target in node.targets:
+ elements = target.elts if isinstance(target, ast.Tuple) else [target]
+ from_public_copy.update(
+ item.id for item in elements if isinstance(item, ast.Name)
+ )
+ assert from_public_copy, "the fixed-copy helpers are no longer bound to a name"
+
+ interpolated = []
+ for node in ast.walk(tree):
+ if not isinstance(node, ast.Call):
+ continue
+ name = node.func.id if isinstance(node.func, ast.Name) else None
+ if name != "CloudFeatureError" or not node.args:
+ continue
+ message = node.args[0]
+ if isinstance(message, ast.Constant) and isinstance(message.value, str):
+ continue
+ if isinstance(message, ast.Name) and message.id in from_public_copy:
+ continue
+ # ``"literal %s" % (...)`` is allowed only where the substituted values are
+ # themselves constrained to local literals; ``run_job`` is the single such site
+ # and its ``status`` is guarded by an ``in {"failed", "canceled"}`` membership
+ # test one line above. Anything else -- an f-string, a bare name, a concatenated
+ # response field -- is a reflection risk and fails here.
+ if (isinstance(message, ast.BinOp) and isinstance(message.op, ast.Mod)
+ and isinstance(message.left, ast.Constant)
+ and message.left.value == "Managed %s did not complete (%s)."):
+ continue
+ interpolated.append((node.lineno, ast.dump(message)[:120]))
+
+ assert interpolated == [], (
+ "a CloudFeatureError message is no longer fixed local copy; _managed_call "
+ "forwards it to the customer: %r" % (interpolated,)
+ )
diff --git a/tests/test_savings.py b/tests/test_savings.py
index 713394b6..04cfaebf 100644
--- a/tests/test_savings.py
+++ b/tests/test_savings.py
@@ -1,218 +1,218 @@
-import pytest
-
-from engraphis import __version__
-from engraphis.core.savings import SavingsEstimate, annotate_usage, estimate_savings
-from engraphis.core.store import Store
-from engraphis.service import MemoryService, ValidationError
-
-
-@pytest.mark.parametrize(
- ("operation", "intent", "adaptive_mode", "basis", "confidence", "eligible"),
- [
- ("adaptive_context", None, "retrieval", "history_retrieval", "high", True),
- ("adaptive_context", None, "history_fallback", "history_fallback", "medium", True),
- ("adaptive_context", None, "history_bypass", "history_bypass", "none", False),
- (
- "adaptive_context",
- None,
- "low_confidence_abstain",
- "low_confidence_abstain",
- "none",
- False,
- ),
- ("recall", "recall_context", None, "packed_context", "medium", True),
- ("grounded_recall", None, None, "packed_context", "medium", True),
- ("proactive_context", None, None, "packed_context", "medium", True),
- ("recall", "recall", None, "unclassified", "unknown", False),
- ],
-)
-def test_estimator_classifies_each_delivery_basis(
- operation, intent, adaptive_mode, basis, confidence, eligible
-):
- estimate = estimate_savings(
- operation=operation,
- intent=intent,
- adaptive_mode=adaptive_mode,
- baseline_tokens=100,
- emitted_tokens=40,
- token_counter="engraphis.regex.v1",
- release_version="1.5.0",
- )
-
- assert isinstance(estimate, SavingsEstimate)
- assert estimate.basis == basis
- assert estimate.confidence == confidence
- assert estimate.eligible is eligible
- assert estimate.saved_tokens == (60 if eligible else 0)
- assert 0 <= estimate.saved_tokens <= estimate.baseline_tokens
- assert 0 <= estimate.savings_ratio <= 1
- assert estimate.release_version == "1.5.0"
-
-
-def test_estimator_is_conservative_for_bad_counts_and_annotates_existing_usage():
- usage = annotate_usage(
- {"source_tokens": 90, "context_tokens": 30, "saved_tokens": 60,
- "token_counter": "engraphis.regex.v1"},
- operation="adaptive_context",
- adaptive_mode="history_fallback",
- baseline_tokens=90,
- emitted_tokens=30,
- release_version=__version__,
- )
-
- assert usage["estimated_saved_tokens"] == 60
- assert usage["savings_eligible"] is True
- assert usage["release_version"] == "1.5.0"
- abstained = estimate_savings(
- operation="adaptive_context",
- adaptive_mode="low_confidence_abstain",
- baseline_tokens=float("nan"),
- emitted_tokens=0,
- )
- assert abstained.saved_tokens == 0
- assert abstained.baseline_tokens == 0
-
-
-def _usage(baseline, emitted, *, counter, release="1.5.0", eligible=True,
- basis="history_retrieval", confidence="high"):
- saved = max(0, baseline - emitted) if eligible else 0
- return {
- "source_tokens": baseline,
- "context_tokens": emitted,
- "saved_tokens": saved,
- "budget_tokens": baseline,
- "packed_count": 1,
- "omitted_count": 0,
- "token_counter": counter,
- "baseline_tokens": baseline,
- "emitted_tokens": emitted,
- "estimated_saved_tokens": saved,
- "estimated_savings_ratio": saved / baseline if baseline else 0.0,
- "savings_basis": basis,
- "savings_confidence": confidence,
- "savings_eligible": eligible,
- "release_version": release,
- }
-
-
-def test_context_savings_aggregates_estimates_filters_releases_and_counters():
- store = Store(":memory:")
- wid = store.get_or_create_workspace("savings")
- rid = store.get_or_create_repo(wid, "repo")
- first = store.record_receipt(
- "adaptive_context",
- workspace_id=wid,
- repo_id=rid,
- metadata={"adaptive_mode": "retrieval", "token_usage": _usage(
- 100, 40, counter="engraphis.regex.v1"
- )},
- )
- second = store.record_receipt(
- "adaptive_context",
- workspace_id=wid,
- repo_id=rid,
- metadata={"adaptive_mode": "history_bypass", "token_usage": _usage(
- 80, 80, counter="engraphis.regex.v1", eligible=False,
- basis="history_bypass", confidence="none"
- )},
- )
- third = store.record_receipt(
- "recall",
- workspace_id=wid,
- repo_id=rid,
- metadata={"intent": "recall_context", "token_usage": _usage(
- 50, 20, counter="estimate_tokens", release="1.4.0",
- basis="packed_context", confidence="medium"
- )},
- )
- old = store.record_receipt(
- "recall",
- workspace_id=wid,
- repo_id=rid,
- metadata={"intent": "recall_context", "token_usage": {
- "source_tokens": 20, "context_tokens": 10, "saved_tokens": 10,
- "token_counter": "engraphis.regex.v1",
- }},
- )
- for timestamp, receipt in ((100.0, first), (110.0, second), (120.0, third), (130.0, old)):
- store.conn.execute(
- "UPDATE operation_receipts SET ts=? WHERE id=?", (timestamp, receipt["id"])
- )
- store.conn.commit()
-
- summary = store.context_savings(
- workspace_id=wid, repo_id=rid, from_ts=99, to_ts=121
- )
- assert summary["estimated"]["eligible_receipt_count"] == 2
- assert summary["estimated"]["excluded_receipt_count"] == 1
- assert summary["estimated"]["unclassified_receipt_count"] == 0
- assert summary["estimated"]["baseline_tokens"] == 150
- assert summary["estimated"]["emitted_tokens"] == 60
- assert summary["estimated"]["saved_tokens"] == 90
- assert {row["token_counter"] for row in summary["estimated"]["by_token_counter"]} == {
- "engraphis.regex.v1", "estimate_tokens"
- }
- assert summary["period"] == {"from_ts": 99, "to_ts": 121}
- all_time = store.context_savings(workspace_id=wid, repo_id=rid)
- assert all_time["estimated"]["unclassified_receipt_count"] == 1
-
- current = store.context_savings(
- workspace_id=wid, repo_id=rid, release_version="1.5.0"
- )
- assert current["receipt_count"] == 2
- assert current["usage_receipt_count"] == 2
- assert current["estimated"]["eligible_receipt_count"] == 1
- assert current["estimated"]["saved_tokens"] == 60
- assert current["estimated"]["by_basis"][0]["basis"] == "history_retrieval"
-
- with pytest.raises(ValueError, match="semantic version"):
- store.context_savings(workspace_id=wid, release_version="not-a-release")
-
-
-def test_service_context_savings_filters_and_new_receipts_are_versioned():
- service = MemoryService.create(":memory:", graph_extractor="none")
- service.remember("Versioned context delivery.", workspace="versioned", scope="workspace")
- service.recall(
- "context delivery",
- workspace="versioned",
- token_budget=32,
- response_mode="compact",
- intent="recall_context",
- )
- receipt = service.receipt_log(workspace="versioned")["entries"][0]
- usage = receipt["metadata"]["token_usage"]
- assert usage["release_version"] == __version__
- assert usage["savings_basis"] == "packed_context"
- assert usage["savings_eligible"] is True
- filtered = service.context_savings(
- workspace="versioned", release_version=__version__,
- from_ts=0, to_ts=9_999_999_999,
- )
- assert filtered["estimated"]["eligible_receipt_count"] == 1
- with pytest.raises(ValidationError, match="semantic version"):
- service.context_savings(workspace="versioned", release_version="legacy")
-
-
-def test_context_savings_ignores_gateway_copies_and_rejects_noncanonical_estimates():
- store = Store(":memory:")
- wid = store.get_or_create_workspace("gateway-savings")
- authoritative = _usage(100, 40, counter="engraphis.regex.v1")
- store.record_receipt(
- "adaptive_context", workspace_id=wid,
- metadata={"token_usage": authoritative},
- )
- store.record_receipt(
- "smart_gateway", workspace_id=wid,
- metadata={"token_usage": authoritative},
- )
- noncanonical = _usage(80, 20, counter="engraphis.regex.v1")
- noncanonical["estimated_savings_ratio"] = 0.1
- store.record_receipt(
- "adaptive_context", workspace_id=wid,
- metadata={"token_usage": noncanonical},
- )
-
- summary = store.context_savings(workspace_id=wid)
- assert summary["estimated"]["eligible_receipt_count"] == 1
- assert summary["estimated"]["saved_tokens"] == 60
- assert summary["estimated"]["invalid_estimate_count"] == 1
+import pytest
+
+from engraphis import __version__
+from engraphis.core.savings import SavingsEstimate, annotate_usage, estimate_savings
+from engraphis.core.store import Store
+from engraphis.service import MemoryService, ValidationError
+
+
+@pytest.mark.parametrize(
+ ("operation", "intent", "adaptive_mode", "basis", "confidence", "eligible"),
+ [
+ ("adaptive_context", None, "retrieval", "history_retrieval", "high", True),
+ ("adaptive_context", None, "history_fallback", "history_fallback", "medium", True),
+ ("adaptive_context", None, "history_bypass", "history_bypass", "none", False),
+ (
+ "adaptive_context",
+ None,
+ "low_confidence_abstain",
+ "low_confidence_abstain",
+ "none",
+ False,
+ ),
+ ("recall", "recall_context", None, "packed_context", "medium", True),
+ ("grounded_recall", None, None, "packed_context", "medium", True),
+ ("proactive_context", None, None, "packed_context", "medium", True),
+ ("recall", "recall", None, "unclassified", "unknown", False),
+ ],
+)
+def test_estimator_classifies_each_delivery_basis(
+ operation, intent, adaptive_mode, basis, confidence, eligible
+):
+ estimate = estimate_savings(
+ operation=operation,
+ intent=intent,
+ adaptive_mode=adaptive_mode,
+ baseline_tokens=100,
+ emitted_tokens=40,
+ token_counter="engraphis.regex.v1",
+ release_version="1.5",
+ )
+
+ assert isinstance(estimate, SavingsEstimate)
+ assert estimate.basis == basis
+ assert estimate.confidence == confidence
+ assert estimate.eligible is eligible
+ assert estimate.saved_tokens == (60 if eligible else 0)
+ assert 0 <= estimate.saved_tokens <= estimate.baseline_tokens
+ assert 0 <= estimate.savings_ratio <= 1
+ assert estimate.release_version == "1.5"
+
+
+def test_estimator_is_conservative_for_bad_counts_and_annotates_existing_usage():
+ usage = annotate_usage(
+ {"source_tokens": 90, "context_tokens": 30, "saved_tokens": 60,
+ "token_counter": "engraphis.regex.v1"},
+ operation="adaptive_context",
+ adaptive_mode="history_fallback",
+ baseline_tokens=90,
+ emitted_tokens=30,
+ release_version=__version__,
+ )
+
+ assert usage["estimated_saved_tokens"] == 60
+ assert usage["savings_eligible"] is True
+ assert usage["release_version"] == "1.5"
+ abstained = estimate_savings(
+ operation="adaptive_context",
+ adaptive_mode="low_confidence_abstain",
+ baseline_tokens=float("nan"),
+ emitted_tokens=0,
+ )
+ assert abstained.saved_tokens == 0
+ assert abstained.baseline_tokens == 0
+
+
+def _usage(baseline, emitted, *, counter, release="1.5", eligible=True,
+ basis="history_retrieval", confidence="high"):
+ saved = max(0, baseline - emitted) if eligible else 0
+ return {
+ "source_tokens": baseline,
+ "context_tokens": emitted,
+ "saved_tokens": saved,
+ "budget_tokens": baseline,
+ "packed_count": 1,
+ "omitted_count": 0,
+ "token_counter": counter,
+ "baseline_tokens": baseline,
+ "emitted_tokens": emitted,
+ "estimated_saved_tokens": saved,
+ "estimated_savings_ratio": saved / baseline if baseline else 0.0,
+ "savings_basis": basis,
+ "savings_confidence": confidence,
+ "savings_eligible": eligible,
+ "release_version": release,
+ }
+
+
+def test_context_savings_aggregates_estimates_filters_releases_and_counters():
+ store = Store(":memory:")
+ wid = store.get_or_create_workspace("savings")
+ rid = store.get_or_create_repo(wid, "repo")
+ first = store.record_receipt(
+ "adaptive_context",
+ workspace_id=wid,
+ repo_id=rid,
+ metadata={"adaptive_mode": "retrieval", "token_usage": _usage(
+ 100, 40, counter="engraphis.regex.v1"
+ )},
+ )
+ second = store.record_receipt(
+ "adaptive_context",
+ workspace_id=wid,
+ repo_id=rid,
+ metadata={"adaptive_mode": "history_bypass", "token_usage": _usage(
+ 80, 80, counter="engraphis.regex.v1", eligible=False,
+ basis="history_bypass", confidence="none"
+ )},
+ )
+ third = store.record_receipt(
+ "recall",
+ workspace_id=wid,
+ repo_id=rid,
+ metadata={"intent": "recall_context", "token_usage": _usage(
+ 50, 20, counter="estimate_tokens", release="1.4.0",
+ basis="packed_context", confidence="medium"
+ )},
+ )
+ old = store.record_receipt(
+ "recall",
+ workspace_id=wid,
+ repo_id=rid,
+ metadata={"intent": "recall_context", "token_usage": {
+ "source_tokens": 20, "context_tokens": 10, "saved_tokens": 10,
+ "token_counter": "engraphis.regex.v1",
+ }},
+ )
+ for timestamp, receipt in ((100.0, first), (110.0, second), (120.0, third), (130.0, old)):
+ store.conn.execute(
+ "UPDATE operation_receipts SET ts=? WHERE id=?", (timestamp, receipt["id"])
+ )
+ store.conn.commit()
+
+ summary = store.context_savings(
+ workspace_id=wid, repo_id=rid, from_ts=99, to_ts=121
+ )
+ assert summary["estimated"]["eligible_receipt_count"] == 2
+ assert summary["estimated"]["excluded_receipt_count"] == 1
+ assert summary["estimated"]["unclassified_receipt_count"] == 0
+ assert summary["estimated"]["baseline_tokens"] == 150
+ assert summary["estimated"]["emitted_tokens"] == 60
+ assert summary["estimated"]["saved_tokens"] == 90
+ assert {row["token_counter"] for row in summary["estimated"]["by_token_counter"]} == {
+ "engraphis.regex.v1", "estimate_tokens"
+ }
+ assert summary["period"] == {"from_ts": 99, "to_ts": 121}
+ all_time = store.context_savings(workspace_id=wid, repo_id=rid)
+ assert all_time["estimated"]["unclassified_receipt_count"] == 1
+
+ current = store.context_savings(
+ workspace_id=wid, repo_id=rid, release_version="1.5"
+ )
+ assert current["receipt_count"] == 2
+ assert current["usage_receipt_count"] == 2
+ assert current["estimated"]["eligible_receipt_count"] == 1
+ assert current["estimated"]["saved_tokens"] == 60
+ assert current["estimated"]["by_basis"][0]["basis"] == "history_retrieval"
+
+ with pytest.raises(ValueError, match="semantic version"):
+ store.context_savings(workspace_id=wid, release_version="not-a-release")
+
+
+def test_service_context_savings_filters_and_new_receipts_are_versioned():
+ service = MemoryService.create(":memory:", graph_extractor="none")
+ service.remember("Versioned context delivery.", workspace="versioned", scope="workspace")
+ service.recall(
+ "context delivery",
+ workspace="versioned",
+ token_budget=32,
+ response_mode="compact",
+ intent="recall_context",
+ )
+ receipt = service.receipt_log(workspace="versioned")["entries"][0]
+ usage = receipt["metadata"]["token_usage"]
+ assert usage["release_version"] == __version__
+ assert usage["savings_basis"] == "packed_context"
+ assert usage["savings_eligible"] is True
+ filtered = service.context_savings(
+ workspace="versioned", release_version=__version__,
+ from_ts=0, to_ts=9_999_999_999,
+ )
+ assert filtered["estimated"]["eligible_receipt_count"] == 1
+ with pytest.raises(ValidationError, match="semantic version"):
+ service.context_savings(workspace="versioned", release_version="legacy")
+
+
+def test_context_savings_ignores_gateway_copies_and_rejects_noncanonical_estimates():
+ store = Store(":memory:")
+ wid = store.get_or_create_workspace("gateway-savings")
+ authoritative = _usage(100, 40, counter="engraphis.regex.v1")
+ store.record_receipt(
+ "adaptive_context", workspace_id=wid,
+ metadata={"token_usage": authoritative},
+ )
+ store.record_receipt(
+ "smart_gateway", workspace_id=wid,
+ metadata={"token_usage": authoritative},
+ )
+ noncanonical = _usage(80, 20, counter="engraphis.regex.v1")
+ noncanonical["estimated_savings_ratio"] = 0.1
+ store.record_receipt(
+ "adaptive_context", workspace_id=wid,
+ metadata={"token_usage": noncanonical},
+ )
+
+ summary = store.context_savings(workspace_id=wid)
+ assert summary["estimated"]["eligible_receipt_count"] == 1
+ assert summary["estimated"]["saved_tokens"] == 60
+ assert summary["estimated"]["invalid_estimate_count"] == 1
diff --git a/tests/test_update_check.py b/tests/test_update_check.py
index e48e3490..933bc8bd 100644
--- a/tests/test_update_check.py
+++ b/tests/test_update_check.py
@@ -1,302 +1,302 @@
-"""Offline tests for the update-reminder module (engraphis.update_check).
-
-Everything here is deterministic and network-free: version math is pure, and the one
-code path that would hit the network (``_fetch``) is either monkeypatched or exercised
-only on inputs it rejects *before* opening a socket.
-"""
-from __future__ import annotations
-
-import json
-import os
-
-import pytest
-
-from engraphis import update_check as u
-
-
-# ── pure version math ─────────────────────────────────────────────────────────
-@pytest.mark.parametrize("text,expected", [
- ("1.2.3", (1, 2, 3)),
- ("v1.2.3", (1, 2, 3)),
- (" V2.0 ", (2, 0)),
- ("1.2.3-rc1", (1, 2, 3)),
- ("1.0.0+build.5", (1, 0, 0)),
- ("10.4", (10, 4)),
- ("nightly", None),
- ("", None),
- (None, None),
- (123, None),
-])
-def test_parse_version(text, expected):
- assert u.parse_version(text) == expected
-
-
-@pytest.mark.parametrize("text", [
- "1." + "9" * 1000,
- ".".join(["1"] * (u._MAX_VERSION_PARTS + 1)),
-])
-def test_parse_version_rejects_pathological_numeric_versions(text):
- assert u.parse_version(text) is None
-
-
-@pytest.mark.parametrize("latest,current,newer", [
- ("1.1.0", "1.0.0", True),
- ("1.0.1", "1.0.0", True),
- ("2.0", "1.9.9", True),
- ("1.0.0", "1.0.0", False), # equal is not newer
- ("1.0", "1.0.0", False), # zero-padded equal
- ("0.9.9", "1.0.0", False),
- ("v1.2.0", "1.1.5", True), # tolerates the v prefix on both sides
- ("garbage", "1.0.0", False), # unparseable → never newer
- ("1.0.0", "garbage", False),
-])
-def test_is_newer(latest, current, newer):
- assert u.is_newer(latest, current) is newer
-
-
-# ── payload normalization ─────────────────────────────────────────────────────
-def test_parse_github_release():
- got = u._parse_release_payload({
- "tag_name": "v1.4.0", "html_url": "https://example/releases/tag/v1.4.0",
- "draft": False, "prerelease": False,
- })
- assert got == {"version": "v1.4.0", "url": "https://example/releases/tag/v1.4.0"}
-
-
-def test_parse_github_rejects_draft_and_prerelease():
- assert u._parse_release_payload({"tag_name": "v2", "draft": True}) is None
- assert u._parse_release_payload({"tag_name": "v2", "prerelease": True}) is None
-
-
-def test_parse_pypi_payload():
- got = u._parse_release_payload({"info": {"version": "1.5.0"}})
- assert got["version"] == "1.5.0"
- assert "1.5.0" in got["url"]
-
-
-def test_parse_generic_and_garbage():
- assert u._parse_release_payload({"version": "3.0", "url": "https://x/y"}) == {
- "version": "3.0", "url": "https://x/y"}
- assert u._parse_release_payload({"nope": 1}) is None
- assert u._parse_release_payload("not a dict") is None
-
-
-# ── network guard (no socket opened for a bad scheme/host) ────────────────────
-@pytest.mark.parametrize("url", [
- "http://example.com/releases", # plain http, non-loopback
- "ftp://example.com/x",
- "file:///etc/passwd",
- "https://user@example.com/releases",
- "https://[::1/releases",
- "https://example.com\\@127.0.0.1/releases",
-])
-def test_fetch_rejects_unsafe_schemes(url):
- assert u._fetch(url, timeout=0.01) is None
-
-
-def test_fetch_rejects_dns_loopback_alias_before_opening(monkeypatch):
- monkeypatch.setattr(
- u, "build_pinned_https_opener",
- lambda *args, **kwargs: pytest.fail("a DNS alias must not reach an HTTP opener"),
- )
- assert u._fetch("http://localhost/latest", timeout=0.01) is None
-
-
-# ── endpoint / explicit opt-in configuration ──────────────────────────────────
-def test_endpoint_default_and_overrides(monkeypatch):
- monkeypatch.delenv("ENGRAPHIS_UPDATE_URL", raising=False)
- monkeypatch.delenv("ENGRAPHIS_UPDATE_REPO", raising=False)
- assert u._endpoint() == "https://api.github.com/repos/%s/releases/latest" % u.DEFAULT_REPO
- monkeypatch.setenv("ENGRAPHIS_UPDATE_REPO", "acme/thing")
- assert u._endpoint().endswith("/repos/acme/thing/releases/latest")
- monkeypatch.setenv("ENGRAPHIS_UPDATE_URL", "https://mirror/latest.json")
- assert u._endpoint() == "https://mirror/latest.json" # explicit URL wins over repo
-
-
-@pytest.mark.parametrize("value", [
- None, "0", "false", "no", "off", "disable", "disabled",
- "treu", "enabled-ish", "2", "random",
-])
-def test_unset_false_like_and_misspelled_values_stay_offline(monkeypatch, value):
- if value is None:
- monkeypatch.delenv("ENGRAPHIS_UPDATE_CHECK", raising=False)
- else:
- monkeypatch.setenv("ENGRAPHIS_UPDATE_CHECK", value)
- assert u.enabled() is False
-
- # Every non-affirmative value must keep check() from opening a socket.
- monkeypatch.setattr(u, "_fetch", lambda *a, **k: pytest.fail("must not hit network"))
- snap = u.check()
- assert snap == u._disabled_snapshot()
- assert u.notice_line(snap) is None
-
-
-@pytest.mark.parametrize("value", ["1", "true", "yes", "on", "enable", "enabled"])
-def test_recognized_explicit_opt_in_values(monkeypatch, value):
- monkeypatch.setenv("ENGRAPHIS_UPDATE_CHECK", value)
- assert u.enabled() is True
-
-
-# ── cache + snapshot behavior ─────────────────────────────────────────────────
-@pytest.fixture
-def cache(tmp_path, monkeypatch):
- """Isolate the on-disk cache and force checks enabled with a known endpoint."""
- path = tmp_path / "update.json"
- monkeypatch.setenv("ENGRAPHIS_UPDATE_CACHE", str(path))
- monkeypatch.setenv("ENGRAPHIS_UPDATE_CHECK", "1")
- monkeypatch.setenv("ENGRAPHIS_UPDATE_URL", "https://example.test/latest")
- return path
-
-
-def test_check_fetches_writes_cache_and_reports_update(cache, monkeypatch):
- monkeypatch.setattr(u, "CURRENT_VERSION", "1.0.0")
- monkeypatch.setattr(u, "_fetch",
- lambda url, timeout: {"version": "1.4.0", "url": "https://rel/1.4.0"})
- snap = u.check(force=True)
- assert snap["update_available"] is True
- assert snap["latest"] == "1.4.0" and snap["current"] == "1.0.0"
- assert snap["url"] == "https://rel/1.4.0"
- # cache persisted
- saved = json.loads(cache.read_text())
- assert saved["latest"] == "1.4.0" and saved["checked_at"] > 0
-
-
-def test_fresh_cache_short_circuits_network(cache, monkeypatch):
- monkeypatch.setattr(u, "CURRENT_VERSION", "1.0.0")
- u._write_cache("1.3.0", "https://rel/1.3.0")
- monkeypatch.setattr(u, "_fetch", lambda *a, **k: pytest.fail("fresh cache must not refetch"))
- snap = u.check() # not forced → should use the fresh cache
- assert snap["latest"] == "1.3.0" and snap["update_available"] is True
-
-
-def test_upgrade_clears_banner_without_ttl_wait(cache, monkeypatch):
- """After the user upgrades, a still-fresh cache whose ``latest`` == installed version
- must report no update — update_available is recomputed against the live version."""
- u._write_cache("1.4.0", "https://rel/1.4.0")
- monkeypatch.setattr(u, "CURRENT_VERSION", "1.4.0") # simulate the just-installed upgrade
- monkeypatch.setattr(u, "_fetch", lambda *a, **k: pytest.fail("no network needed"))
- snap = u.check()
- assert snap["update_available"] is False
-
-
-def test_fetch_failure_preserves_last_good(cache, monkeypatch):
- monkeypatch.setattr(u, "CURRENT_VERSION", "1.0.0")
- u._write_cache("1.4.0", "https://rel/1.4.0")
- # Expire the cache so check() attempts a refresh, then have the network fail.
- stale = json.loads(cache.read_text())
- stale["checked_at"] = 0.0
- cache.write_text(json.dumps(stale))
- monkeypatch.setattr(u, "_fetch", lambda *a, **k: None)
- snap = u.check()
- assert snap["latest"] == "1.4.0" and snap["update_available"] is True # last good kept
-
-
-def test_unexpected_fetch_failure_is_fail_silent(cache, monkeypatch):
- stale = {"latest": "1.4.0", "url": "https://rel/1.4.0", "checked_at": 0.0}
- cache.write_text(json.dumps(stale))
-
- def fail(*_args, **_kwargs):
- raise RuntimeError("provider detail must not escape")
-
- monkeypatch.setattr(u, "_fetch", fail)
- snap = u.check()
-
- assert snap["latest"] == "1.4.0"
- assert snap["error"] == "update check unavailable"
-
-def test_snapshot_is_non_blocking(cache, monkeypatch):
- monkeypatch.setattr(u, "CURRENT_VERSION", "1.0.0")
- called = {"bg": False}
- monkeypatch.setattr(u, "refresh_in_background", lambda *a, **k: called.__setitem__("bg", True))
- monkeypatch.setattr(u, "_fetch", lambda *a, **k: pytest.fail("snapshot must not fetch inline"))
- snap = u.snapshot() # empty cache → returns immediately, schedules a background refresh
- assert snap["update_available"] is False
- assert called["bg"] is True
-
-
-@pytest.mark.parametrize("checked_at", [
- [1], {"value": 1}, "nan", "inf", "-inf",
-])
-def test_malformed_cache_timestamp_is_fail_silent(cache, monkeypatch, checked_at):
- cache.write_text(json.dumps({"latest": "2.0.0", "checked_at": checked_at}))
- monkeypatch.setattr(u, "refresh_in_background", lambda *args, **kwargs: None)
-
- snap = u.snapshot()
-
- assert snap["checked_at"] == 0.0
-
-
-def test_oversized_cache_is_ignored(cache):
- cache.write_text("x" * (u._MAX_CACHE_BYTES + 1))
- assert u._read_cache() == {}
-
-
-def test_linked_cache_is_ignored_and_never_overwrites_target(cache):
- victim = cache.with_name("victim.json")
- victim.write_text("do not replace")
- try:
- cache.symlink_to(victim)
- except (NotImplementedError, OSError):
- try:
- os.link(victim, cache)
- except OSError:
- pytest.skip("this platform cannot create a link for the cache test")
-
- assert u._read_cache() == {}
- u._write_cache("9.9.9", "https://example.test/release")
- assert victim.read_text() == "do not replace"
-
-
-def test_notice_line(monkeypatch):
- line = u.notice_line({"enabled": True, "update_available": True,
- "latest": "1.4.0", "current": "1.0.0", "url": "https://rel/1.4.0"})
- assert "1.4.0" in line and "1.0.0" in line and "pip install -U engraphis" in line
- assert u.notice_line({"enabled": True, "update_available": False}) is None
-
-
-def test_cli_notice_uses_the_non_blocking_snapshot_and_is_fail_silent(monkeypatch):
- seen = []
- monkeypatch.setenv("ENGRAPHIS_UPDATE_CHECK", "1")
- monkeypatch.setattr(u, "snapshot", lambda: {
- "enabled": True, "update_available": True, "latest": "1.4.0", "current": "1.0.0",
- "url": "https://rel/1.4.0",
- })
- monkeypatch.setattr(u, "check", lambda **_kwargs: pytest.fail("CLI must not check inline"))
- u.emit_cli_notice(seen.append)
- assert seen and "1.4.0" in seen[0]
-
- monkeypatch.setattr(u, "snapshot", lambda: (_ for _ in ()).throw(RuntimeError("offline")))
- u.emit_cli_notice(seen.append)
- assert len(seen) == 1
-
-
-def test_primary_ledger_renders_the_update_snapshot():
- root = __import__("pathlib").Path(__file__).resolve().parents[1] / "engraphis" / "dashboard_assets"
- html = (root / "index.html").read_text(encoding="utf-8")
- script = (root / "ledger.js").read_text(encoding="utf-8")
- css = (root / "ledger.css").read_text(encoding="utf-8")
- assert 'id="update-banner"' in html
- assert "renderUpdateBanner(bootstrap.update)" in script
- assert "pip install -U engraphis" in script
- assert ".update-banner" in css
-
-
-def test_api_update_endpoint(monkeypatch):
- pytest.importorskip("fastapi", reason="v2_api requires fastapi (extras)")
- from engraphis.routes import v2_api
- monkeypatch.setattr(u, "snapshot",
- lambda: {"enabled": True, "update_available": True, "latest": "1.4.0"})
- out = v2_api.api_update(force=False)
- assert out["update_available"] is True and out["latest"] == "1.4.0"
-
-
-def test_api_update_never_raises(monkeypatch):
- pytest.importorskip("fastapi", reason="v2_api requires fastapi (extras)")
- from engraphis.routes import v2_api
-
- def boom():
- raise RuntimeError("nope")
-
- monkeypatch.setattr(u, "snapshot", boom)
- out = v2_api.api_update(force=False)
- assert out == {"enabled": False, "update_available": False}
+"""Offline tests for the update-reminder module (engraphis.update_check).
+
+Everything here is deterministic and network-free: version math is pure, and the one
+code path that would hit the network (``_fetch``) is either monkeypatched or exercised
+only on inputs it rejects *before* opening a socket.
+"""
+from __future__ import annotations
+
+import json
+import os
+
+import pytest
+
+from engraphis import update_check as u
+
+
+# ── pure version math ─────────────────────────────────────────────────────────
+@pytest.mark.parametrize("text,expected", [
+ ("1.2.3", (1, 2, 3)),
+ ("v1.2.3", (1, 2, 3)),
+ (" V2.0 ", (2, 0)),
+ ("1.2.3-rc1", (1, 2, 3)),
+ ("1.0.0+build.5", (1, 0, 0)),
+ ("10.4", (10, 4)),
+ ("nightly", None),
+ ("", None),
+ (None, None),
+ (123, None),
+])
+def test_parse_version(text, expected):
+ assert u.parse_version(text) == expected
+
+
+@pytest.mark.parametrize("text", [
+ "1." + "9" * 1000,
+ ".".join(["1"] * (u._MAX_VERSION_PARTS + 1)),
+])
+def test_parse_version_rejects_pathological_numeric_versions(text):
+ assert u.parse_version(text) is None
+
+
+@pytest.mark.parametrize("latest,current,newer", [
+ ("1.1.0", "1.0.0", True),
+ ("1.0.1", "1.0.0", True),
+ ("2.0", "1.9.9", True),
+ ("1.0.0", "1.0.0", False), # equal is not newer
+ ("1.0", "1.0.0", False), # zero-padded equal
+ ("0.9.9", "1.0.0", False),
+ ("v1.2.0", "1.1.5", True), # tolerates the v prefix on both sides
+ ("garbage", "1.0.0", False), # unparseable → never newer
+ ("1.0.0", "garbage", False),
+])
+def test_is_newer(latest, current, newer):
+ assert u.is_newer(latest, current) is newer
+
+
+# ── payload normalization ─────────────────────────────────────────────────────
+def test_parse_github_release():
+ got = u._parse_release_payload({
+ "tag_name": "v1.4.0", "html_url": "https://example/releases/tag/v1.4.0",
+ "draft": False, "prerelease": False,
+ })
+ assert got == {"version": "v1.4.0", "url": "https://example/releases/tag/v1.4.0"}
+
+
+def test_parse_github_rejects_draft_and_prerelease():
+ assert u._parse_release_payload({"tag_name": "v2", "draft": True}) is None
+ assert u._parse_release_payload({"tag_name": "v2", "prerelease": True}) is None
+
+
+def test_parse_pypi_payload():
+ got = u._parse_release_payload({"info": {"version": "1.5"}})
+ assert got["version"] == "1.5"
+ assert "1.5" in got["url"]
+
+
+def test_parse_generic_and_garbage():
+ assert u._parse_release_payload({"version": "3.0", "url": "https://x/y"}) == {
+ "version": "3.0", "url": "https://x/y"}
+ assert u._parse_release_payload({"nope": 1}) is None
+ assert u._parse_release_payload("not a dict") is None
+
+
+# ── network guard (no socket opened for a bad scheme/host) ────────────────────
+@pytest.mark.parametrize("url", [
+ "http://example.com/releases", # plain http, non-loopback
+ "ftp://example.com/x",
+ "file:///etc/passwd",
+ "https://user@example.com/releases",
+ "https://[::1/releases",
+ "https://example.com\\@127.0.0.1/releases",
+])
+def test_fetch_rejects_unsafe_schemes(url):
+ assert u._fetch(url, timeout=0.01) is None
+
+
+def test_fetch_rejects_dns_loopback_alias_before_opening(monkeypatch):
+ monkeypatch.setattr(
+ u, "build_pinned_https_opener",
+ lambda *args, **kwargs: pytest.fail("a DNS alias must not reach an HTTP opener"),
+ )
+ assert u._fetch("http://localhost/latest", timeout=0.01) is None
+
+
+# ── endpoint / explicit opt-in configuration ──────────────────────────────────
+def test_endpoint_default_and_overrides(monkeypatch):
+ monkeypatch.delenv("ENGRAPHIS_UPDATE_URL", raising=False)
+ monkeypatch.delenv("ENGRAPHIS_UPDATE_REPO", raising=False)
+ assert u._endpoint() == "https://api.github.com/repos/%s/releases/latest" % u.DEFAULT_REPO
+ monkeypatch.setenv("ENGRAPHIS_UPDATE_REPO", "acme/thing")
+ assert u._endpoint().endswith("/repos/acme/thing/releases/latest")
+ monkeypatch.setenv("ENGRAPHIS_UPDATE_URL", "https://mirror/latest.json")
+ assert u._endpoint() == "https://mirror/latest.json" # explicit URL wins over repo
+
+
+@pytest.mark.parametrize("value", [
+ None, "0", "false", "no", "off", "disable", "disabled",
+ "treu", "enabled-ish", "2", "random",
+])
+def test_unset_false_like_and_misspelled_values_stay_offline(monkeypatch, value):
+ if value is None:
+ monkeypatch.delenv("ENGRAPHIS_UPDATE_CHECK", raising=False)
+ else:
+ monkeypatch.setenv("ENGRAPHIS_UPDATE_CHECK", value)
+ assert u.enabled() is False
+
+ # Every non-affirmative value must keep check() from opening a socket.
+ monkeypatch.setattr(u, "_fetch", lambda *a, **k: pytest.fail("must not hit network"))
+ snap = u.check()
+ assert snap == u._disabled_snapshot()
+ assert u.notice_line(snap) is None
+
+
+@pytest.mark.parametrize("value", ["1", "true", "yes", "on", "enable", "enabled"])
+def test_recognized_explicit_opt_in_values(monkeypatch, value):
+ monkeypatch.setenv("ENGRAPHIS_UPDATE_CHECK", value)
+ assert u.enabled() is True
+
+
+# ── cache + snapshot behavior ─────────────────────────────────────────────────
+@pytest.fixture
+def cache(tmp_path, monkeypatch):
+ """Isolate the on-disk cache and force checks enabled with a known endpoint."""
+ path = tmp_path / "update.json"
+ monkeypatch.setenv("ENGRAPHIS_UPDATE_CACHE", str(path))
+ monkeypatch.setenv("ENGRAPHIS_UPDATE_CHECK", "1")
+ monkeypatch.setenv("ENGRAPHIS_UPDATE_URL", "https://example.test/latest")
+ return path
+
+
+def test_check_fetches_writes_cache_and_reports_update(cache, monkeypatch):
+ monkeypatch.setattr(u, "CURRENT_VERSION", "1.0.0")
+ monkeypatch.setattr(u, "_fetch",
+ lambda url, timeout: {"version": "1.4.0", "url": "https://rel/1.4.0"})
+ snap = u.check(force=True)
+ assert snap["update_available"] is True
+ assert snap["latest"] == "1.4.0" and snap["current"] == "1.0.0"
+ assert snap["url"] == "https://rel/1.4.0"
+ # cache persisted
+ saved = json.loads(cache.read_text())
+ assert saved["latest"] == "1.4.0" and saved["checked_at"] > 0
+
+
+def test_fresh_cache_short_circuits_network(cache, monkeypatch):
+ monkeypatch.setattr(u, "CURRENT_VERSION", "1.0.0")
+ u._write_cache("1.3.0", "https://rel/1.3.0")
+ monkeypatch.setattr(u, "_fetch", lambda *a, **k: pytest.fail("fresh cache must not refetch"))
+ snap = u.check() # not forced → should use the fresh cache
+ assert snap["latest"] == "1.3.0" and snap["update_available"] is True
+
+
+def test_upgrade_clears_banner_without_ttl_wait(cache, monkeypatch):
+ """After the user upgrades, a still-fresh cache whose ``latest`` == installed version
+ must report no update — update_available is recomputed against the live version."""
+ u._write_cache("1.4.0", "https://rel/1.4.0")
+ monkeypatch.setattr(u, "CURRENT_VERSION", "1.4.0") # simulate the just-installed upgrade
+ monkeypatch.setattr(u, "_fetch", lambda *a, **k: pytest.fail("no network needed"))
+ snap = u.check()
+ assert snap["update_available"] is False
+
+
+def test_fetch_failure_preserves_last_good(cache, monkeypatch):
+ monkeypatch.setattr(u, "CURRENT_VERSION", "1.0.0")
+ u._write_cache("1.4.0", "https://rel/1.4.0")
+ # Expire the cache so check() attempts a refresh, then have the network fail.
+ stale = json.loads(cache.read_text())
+ stale["checked_at"] = 0.0
+ cache.write_text(json.dumps(stale))
+ monkeypatch.setattr(u, "_fetch", lambda *a, **k: None)
+ snap = u.check()
+ assert snap["latest"] == "1.4.0" and snap["update_available"] is True # last good kept
+
+
+def test_unexpected_fetch_failure_is_fail_silent(cache, monkeypatch):
+ stale = {"latest": "1.4.0", "url": "https://rel/1.4.0", "checked_at": 0.0}
+ cache.write_text(json.dumps(stale))
+
+ def fail(*_args, **_kwargs):
+ raise RuntimeError("provider detail must not escape")
+
+ monkeypatch.setattr(u, "_fetch", fail)
+ snap = u.check()
+
+ assert snap["latest"] == "1.4.0"
+ assert snap["error"] == "update check unavailable"
+
+def test_snapshot_is_non_blocking(cache, monkeypatch):
+ monkeypatch.setattr(u, "CURRENT_VERSION", "1.0.0")
+ called = {"bg": False}
+ monkeypatch.setattr(u, "refresh_in_background", lambda *a, **k: called.__setitem__("bg", True))
+ monkeypatch.setattr(u, "_fetch", lambda *a, **k: pytest.fail("snapshot must not fetch inline"))
+ snap = u.snapshot() # empty cache → returns immediately, schedules a background refresh
+ assert snap["update_available"] is False
+ assert called["bg"] is True
+
+
+@pytest.mark.parametrize("checked_at", [
+ [1], {"value": 1}, "nan", "inf", "-inf",
+])
+def test_malformed_cache_timestamp_is_fail_silent(cache, monkeypatch, checked_at):
+ cache.write_text(json.dumps({"latest": "2.0.0", "checked_at": checked_at}))
+ monkeypatch.setattr(u, "refresh_in_background", lambda *args, **kwargs: None)
+
+ snap = u.snapshot()
+
+ assert snap["checked_at"] == 0.0
+
+
+def test_oversized_cache_is_ignored(cache):
+ cache.write_text("x" * (u._MAX_CACHE_BYTES + 1))
+ assert u._read_cache() == {}
+
+
+def test_linked_cache_is_ignored_and_never_overwrites_target(cache):
+ victim = cache.with_name("victim.json")
+ victim.write_text("do not replace")
+ try:
+ cache.symlink_to(victim)
+ except (NotImplementedError, OSError):
+ try:
+ os.link(victim, cache)
+ except OSError:
+ pytest.skip("this platform cannot create a link for the cache test")
+
+ assert u._read_cache() == {}
+ u._write_cache("9.9.9", "https://example.test/release")
+ assert victim.read_text() == "do not replace"
+
+
+def test_notice_line(monkeypatch):
+ line = u.notice_line({"enabled": True, "update_available": True,
+ "latest": "1.4.0", "current": "1.0.0", "url": "https://rel/1.4.0"})
+ assert "1.4.0" in line and "1.0.0" in line and "pip install -U engraphis" in line
+ assert u.notice_line({"enabled": True, "update_available": False}) is None
+
+
+def test_cli_notice_uses_the_non_blocking_snapshot_and_is_fail_silent(monkeypatch):
+ seen = []
+ monkeypatch.setenv("ENGRAPHIS_UPDATE_CHECK", "1")
+ monkeypatch.setattr(u, "snapshot", lambda: {
+ "enabled": True, "update_available": True, "latest": "1.4.0", "current": "1.0.0",
+ "url": "https://rel/1.4.0",
+ })
+ monkeypatch.setattr(u, "check", lambda **_kwargs: pytest.fail("CLI must not check inline"))
+ u.emit_cli_notice(seen.append)
+ assert seen and "1.4.0" in seen[0]
+
+ monkeypatch.setattr(u, "snapshot", lambda: (_ for _ in ()).throw(RuntimeError("offline")))
+ u.emit_cli_notice(seen.append)
+ assert len(seen) == 1
+
+
+def test_primary_ledger_renders_the_update_snapshot():
+ root = __import__("pathlib").Path(__file__).resolve().parents[1] / "engraphis" / "dashboard_assets"
+ html = (root / "index.html").read_text(encoding="utf-8")
+ script = (root / "ledger.js").read_text(encoding="utf-8")
+ css = (root / "ledger.css").read_text(encoding="utf-8")
+ assert 'id="update-banner"' in html
+ assert "renderUpdateBanner(bootstrap.update)" in script
+ assert "pip install -U engraphis" in script
+ assert ".update-banner" in css
+
+
+def test_api_update_endpoint(monkeypatch):
+ pytest.importorskip("fastapi", reason="v2_api requires fastapi (extras)")
+ from engraphis.routes import v2_api
+ monkeypatch.setattr(u, "snapshot",
+ lambda: {"enabled": True, "update_available": True, "latest": "1.4.0"})
+ out = v2_api.api_update(force=False)
+ assert out["update_available"] is True and out["latest"] == "1.4.0"
+
+
+def test_api_update_never_raises(monkeypatch):
+ pytest.importorskip("fastapi", reason="v2_api requires fastapi (extras)")
+ from engraphis.routes import v2_api
+
+ def boom():
+ raise RuntimeError("nope")
+
+ monkeypatch.setattr(u, "snapshot", boom)
+ out = v2_api.api_update(force=False)
+ assert out == {"enabled": False, "update_available": False}