mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
7a06f5e8bc
* refactor(session): make ModelLane the provider boundary (#979) ## Summary This closes the model-lane ownership gap left by #832: `ChatSession` no longer stores raw provider/client handles. `ResolvedModelBinding` now carries the provider, client, model, capabilities, registry generation, and backend-auth configuration as one coherent snapshot. - Atomically rebind existing sessions after model-registry changes while pinning each in-flight send, fallback, judge, output guard, task agent, title, compaction, perception, and voice operation to its initiating principal and binding. - Fence UI publication, canonical trajectory folds, durable writes, streams, retries, child scopes, and judge work by generation. Stop can hand off to a successor without accepting late state; cancelled tools retain typed effect receipts, and concurrent approval batches resolve by exact cycle or call. - Make create, fork, open, close, and delete race-safe with hidden `creating` reservations, incarnation-aware state tails, and an ACL-rechecked transaction that clones checkpoint-bounded history, configuration, project/persona state, and attachment references. - Extend REST/OpenAPI and Python/TypeScript SDK contracts for create/fork inputs, routed-create metadata, live-workstream probes, targeted approvals, and structured cancellation results. - Update architecture, storage, authentication, judge, channel, console, API, and SDK documentation, including regenerated architecture diagrams and OpenAPI artifacts. ## Validation - SQLite suite: 11,188 passed, 9 skipped, 10 deselected - PostgreSQL suite: 11,195 passed, 2 skipped, 10 deselected - Live backend: 3 passed - SSE recovery: 6 passed; browser recovery harness passed all scenarios - Ruff: clean; 595 files correctly formatted - mypy: 243 source files clean - TypeScript: typecheck/build and 35 tests passed - OpenAPI artifacts fresh; all 14 changed diagrams reproduce byte-for-byte - `git diff --check` and Git LFS integrity clean Closes #979. * fix(deps): update nanoid for GHSA-2v37-7h3g-55p8 Refresh the transitive lock entry admitted by PostCSS so the TypeScript security gate no longer resolves the vulnerable custom-generator implementation. Validation: - npm ci - npm audit --audit-level=moderate: 0 vulnerabilities - TypeScript typecheck and build - TypeScript tests: 35 passed * fix(test): assert canonical model registry URLs Replace prefix checks with exact canonical base URL assertions so the tests do not model incomplete URL validation. Validation: tests/test_model_registry.py (185 passed); Ruff check/format; mypy.
85 lines
2.8 KiB
Python
85 lines
2.8 KiB
Python
"""Shared test helpers — kept out of conftest.py since these are factories,
|
|
not fixtures, and several test files want to import them directly."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
from typing import TYPE_CHECKING, Any
|
|
from unittest.mock import MagicMock
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Callable
|
|
|
|
|
|
def wait_until(cond: Callable[[], bool], timeout: float = 5.0) -> None:
|
|
"""Poll ``cond`` to True within ``timeout`` or fail the test.
|
|
|
|
The worker/wake tests can't join threads by identity:
|
|
``session_worker.send`` assigns ``ws.worker_thread`` under the lock
|
|
BEFORE ``t.start()``, so the instant a dispatching call returns, a
|
|
fast worker may already have run its exit backstop and installed the
|
|
(not-yet-started) wake thread — joining whatever ``ws.worker_thread``
|
|
points at races ``RuntimeError: cannot join thread before it is
|
|
started``. Poll outcomes instead.
|
|
"""
|
|
deadline = time.monotonic() + timeout
|
|
while time.monotonic() < deadline:
|
|
if cond():
|
|
return
|
|
time.sleep(0.005)
|
|
if cond():
|
|
# Final re-check: the condition can become true during the last
|
|
# sleep (or a CI descheduling stall past the deadline) — failing
|
|
# without re-looking makes the helper itself a flake source.
|
|
return
|
|
raise AssertionError("condition not met within timeout")
|
|
|
|
|
|
def make_chat_session(**overrides: Any) -> Any:
|
|
"""Build a minimal ``ChatSession`` with sane test defaults.
|
|
|
|
Caller passes any constructor arg as a kwarg to override the default —
|
|
e.g. ``make_chat_session(memory_config=MemoryConfig(fetch_limit=5))``.
|
|
"""
|
|
from turnstone.core.session import ChatSession
|
|
|
|
defaults: dict[str, Any] = {
|
|
"client": MagicMock(),
|
|
"model": "test-model",
|
|
"ui": MagicMock(),
|
|
"instructions": None,
|
|
"temperature": 0.5,
|
|
"max_tokens": 4096,
|
|
"tool_timeout": 30,
|
|
}
|
|
defaults.update(overrides)
|
|
return ChatSession(**defaults)
|
|
|
|
|
|
def patch_session_storage(
|
|
monkeypatch: Any,
|
|
*,
|
|
active: bool = True,
|
|
raise_on_is_active: bool = False,
|
|
) -> list[str]:
|
|
"""Patch ``session.get_storage`` to a stub whose ``is_watch_active``
|
|
returns *active* (or raises if *raise_on_is_active*). Returns the
|
|
list of ``watch_id``s the predicate was called with.
|
|
"""
|
|
from turnstone.core import session as session_mod
|
|
|
|
calls: list[str] = []
|
|
|
|
class _Stub:
|
|
def get_workstream(self, ws_id: str) -> None:
|
|
return None
|
|
|
|
def is_watch_active(self, watch_id: str) -> bool:
|
|
calls.append(watch_id)
|
|
if raise_on_is_active:
|
|
raise RuntimeError("storage down")
|
|
return active
|
|
|
|
monkeypatch.setattr(session_mod, "get_storage", lambda: _Stub())
|
|
return calls
|