5809ad3adb
Shared runtime - One agent entry point (runtime.invoke_agent_for_user / prepare_agent_turn) for web, email, iMessage, WhatsApp, scheduled and cron, so identity, permissions, memories, summaries and metrics match across channels. - Serialize turns per checkpoint thread; /query validates input and returns 409 while a durable run owns the conversation. Reconnectable chat (durable runs) - POST /runs creates an idempotent, owner-scoped run; GET .../events replays an ordered SSE log by cursor, so generation survives refresh and device switches. Stop cancels; disconnect only detaches. /stream-query is a compatibility adapter. - Coalesce streamed deltas (150 ms) and wake same-process subscribers instead of polling; keep buffered text when a run is stopped. - Drain runs on shutdown; serve via Hypercorn with a shutdown timeout covering the drain (and no longer in debug mode). CHAT_RUN_* settings are forwarded. Scheduled messages and cron - Atomic delivery claims, attempt history and explicit retries; ambiguous sends become "uncertain" and are never resent automatically. - Cron triggers need a run_id/Idempotency-Key; legacy jobs need an explicit owner. - Cancelling never rewrites a started delivery; series stop via tombstones. Legacy sent rows are not forked into new series. Malformed bodies return 400. History, integrations and retrieval - Message-content search, pinned conversations, JSON/Markdown export and a memory editor. - Integration status panel and YNAB data-freshness provenance. - Date-filtered document/note search with stable citations and a source viewer. Migration 20 is generated by aerich. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
112 lines
3.6 KiB
Python
112 lines
3.6 KiB
Python
from datetime import datetime, timedelta, timezone
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock
|
|
|
|
import pytest
|
|
from quart import Quart
|
|
from quart_jwt_extended import JWTManager, create_access_token
|
|
from tortoise import Tortoise
|
|
|
|
from blueprints import integrations
|
|
from blueprints.users.models import User
|
|
from blueprints.ynab import tools
|
|
from blueprints.ynab.sync import tool_data_freshness
|
|
|
|
|
|
async def test_stale_ynab_status_hides_provider_error(monkeypatch):
|
|
monkeypatch.setattr(integrations, "ynab_configured", lambda: True)
|
|
monkeypatch.setattr(
|
|
integrations,
|
|
"_current_state",
|
|
AsyncMock(
|
|
return_value=SimpleNamespace(
|
|
last_synced_at=datetime.now(timezone.utc) - timedelta(hours=1),
|
|
last_error="Authorization: secret-provider-key",
|
|
)
|
|
),
|
|
)
|
|
result = await integrations._ynab_status()
|
|
assert result["status"] == "stale"
|
|
assert result["last_success_at"]
|
|
assert "secret-provider-key" not in str(result)
|
|
|
|
|
|
async def test_fresh_ynab_status_is_ready(monkeypatch):
|
|
monkeypatch.setattr(integrations, "ynab_configured", lambda: True)
|
|
monkeypatch.setattr(
|
|
integrations,
|
|
"_current_state",
|
|
AsyncMock(
|
|
return_value=SimpleNamespace(
|
|
last_synced_at=datetime.now(timezone.utc), last_error=None
|
|
)
|
|
),
|
|
)
|
|
assert (await integrations._ynab_status())["status"] == "ready"
|
|
|
|
|
|
async def test_stale_tool_data_warns_model_and_carries_artifact_provenance():
|
|
@tools.ynab_tool_errors
|
|
async def result():
|
|
tool_data_freshness.set(
|
|
{"system": "ynab", "as_of": "2026-10-01T00:00:00+00:00", "stale": True}
|
|
)
|
|
return "Total: $100", {"kind": "ynab.summary", "data": {"total": 100}}
|
|
|
|
text, artifact = await result()
|
|
assert "cached budget data is stale" in text
|
|
assert "2026-10-01" in text
|
|
assert artifact["source_ref"]["stale"] is True
|
|
assert artifact["data"] == {"total": 100}
|
|
|
|
|
|
async def test_tool_freshness_does_not_leak_from_previous_call():
|
|
tool_data_freshness.set({"system": "ynab", "as_of": "old", "stale": True})
|
|
|
|
@tools.ynab_tool_errors
|
|
async def result():
|
|
return "Dates must be YYYY-MM-DD."
|
|
|
|
assert await result() == "Dates must be YYYY-MM-DD."
|
|
|
|
|
|
@pytest.fixture
|
|
async def status_app(monkeypatch):
|
|
await Tortoise.init(
|
|
db_url="sqlite://:memory:", modules={"models": ["blueprints.users.models"]}
|
|
)
|
|
await Tortoise.generate_schemas()
|
|
user = await User.create(username="owner", email="owner@example.com")
|
|
app = Quart(__name__)
|
|
app.config["JWT_SECRET_KEY"] = "test-secret"
|
|
JWTManager(app)
|
|
app.register_blueprint(integrations.integrations_blueprint)
|
|
monkeypatch.setattr(
|
|
integrations,
|
|
"integration_statuses",
|
|
AsyncMock(
|
|
return_value=[
|
|
{
|
|
"name": "YNAB",
|
|
"status": "stale",
|
|
"message": "Cached data",
|
|
"last_success_at": None,
|
|
}
|
|
]
|
|
),
|
|
)
|
|
async with app.app_context():
|
|
token = create_access_token(identity=str(user.id))
|
|
yield app, token
|
|
await Tortoise.close_connections()
|
|
|
|
|
|
async def test_status_requires_access_token(status_app):
|
|
app, token = status_app
|
|
assert (await app.test_client().get("/api/integrations/")).status_code == 401
|
|
response = await app.test_client().get(
|
|
"/api/integrations/", headers={"Authorization": f"Bearer {token}"}
|
|
)
|
|
assert response.status_code == 200
|
|
assert (await response.get_json())["integrations"][0]["status"] == "stale"
|