refactor(model-routing): centralize explicit foreground fallback policy (#6020)

* refactor(model-routing): centralize explicit foreground fallback policy

Make foreground fallback an explicit per-user, availability-only policy shared by streaming Chat, non-stream Chat, and Agent runs.

Preserve strict defaults, owner/model and credential boundaries, pinned Agent routes, and truthful per-round provenance/accounting. Carry provider-reported model identifiers through native streaming adapters, non-stream responses, and caches, and keep legacy default_model_fallbacks as tombstoned raw storage that generic settings APIs and agent tools cannot expose or mutate.

* fix(agent-loop): restore rebase-dropped qwen routing, workspace prompt, and temperature clamp

* fix(model-routing): thread selected endpoint identity, fix cost classification and fallback eligibility

* fix(chat): restore stream helpers and harden run stop lifecycle

* fix(model-routing): let numeric provider codes win over symbolic rate-limit statuses

* fix(agent-loop): apply qwen temperature and notes-tool clamps per fallback candidate

* fix(chat): honor queued stop across resend and reload canonical terminal on EOF

* fix(chat): track stop queue and cleanup ownership by per-send generation

* fix(agent-loop): preserve requested temperature for non-qwen fallback candidates

* fix(chat): reserve send ownership before any await and scope stop to the current send

* fix(chat): clear the previous run identity at send reservation

---------

Co-authored-by: RaresKeY <158580472+RaresKeY@users.noreply.github.com>
Co-authored-by: StressTestor <212606152+StressTestor@users.noreply.github.com>
This commit is contained in:
Alexandre Teixeira
2026-08-14 08:10:30 +01:00
committed by GitHub
co-authored by RaresKeY StressTestor
parent b52296471b
commit c4369305f0
46 changed files with 11418 additions and 1006 deletions
+4
View File
@@ -314,17 +314,21 @@ class TestComputeFinalMetrics:
def test_tool_events_included(self):
events = [{"tool": "bash", "duration": 1.0}]
texts = ["round 1 text"]
models = ["round-1-model"]
m = _compute_final_metrics(**self._base_args(
tool_events=events,
round_texts=texts,
round_models=models,
))
assert m["tool_events"] == events
assert m["round_texts"] == texts
assert m["round_models"] == models
def test_no_tool_events_excluded(self):
m = _compute_final_metrics(**self._base_args(tool_events=[], round_texts=[]))
assert "tool_events" not in m
assert "round_texts" not in m
assert "round_models" not in m
# ---------------------------------------------------------------------------
@@ -0,0 +1,237 @@
"""Saved Agent rounds must render and bill with actual per-round provenance."""
import json
from pathlib import Path
import re
import shutil
import subprocess
import pytest
_SOURCE = (
Path(__file__).resolve().parents[1] / "static" / "js" / "chatRenderer.js"
).read_text(encoding="utf-8")
_CHAT_SOURCE = (
Path(__file__).resolve().parents[1] / "static" / "js" / "chat.js"
).read_text(encoding="utf-8")
_SLASH_SOURCE = (
Path(__file__).resolve().parents[1] / "static" / "js" / "slashCommands.js"
).read_text(encoding="utf-8")
_HAS_NODE = shutil.which("node") is not None
def _function_source(name):
match = re.search(
rf"^(?:export )?function {name}\(.*?^\}}",
_SOURCE,
re.MULTILINE | re.DOTALL,
)
assert match, f"{name} not found"
return match.group(0).replace("export function", "function", 1)
def _run_node(source):
proc = subprocess.run(
["node", "--input-type=module"],
input=source,
capture_output=True,
text=True,
timeout=30,
)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout.strip())
def test_saved_agent_rounds_prefer_round_model_provenance():
assert "const roundModels = metadata.round_models || [];" in _SOURCE
assert "const contModel = roundModels[r] || pair.actualModel || pair.requestedModel;" in _SOURCE
assert "Array.isArray(metadata.round_texts) && metadata.round_texts.length > 1" in _SOURCE
assert "const roundEndpointIds = metadata.round_endpoint_ids || [];" in _SOURCE
assert "const roundEndpointLabels = metadata.round_endpoint_labels || [];" in _SOURCE
assert "r < roundEndpointIds.length" in _SOURCE
assert "r < roundEndpointLabels.length" in _SOURCE
assert "roundEndpointIds[r] || pair.actualEndpointId" not in _SOURCE
def test_metrics_cost_uses_actual_fallback_endpoint_classification():
assert "metrics.endpoint_cost_tracked" in _SOURCE
assert "endpointCostTracked === false" in _SOURCE
assert "endpointCostTracked !== true && !isCostTrackedEndpoint(selectedUrl)" in _SOURCE
assert "Array.isArray(metrics.usage_buckets)" in _SOURCE
@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH")
def test_agent_usage_buckets_sum_only_billable_answering_routes():
source = "\n".join([
"let currentUrl = '';",
"function _currentEndpointUrl() { return currentUrl; }",
"function isCostTrackedEndpoint(url) { return url === 'paid'; }",
"function getModelCost(_model, inputTokens, outputTokens) { return (inputTokens + outputTokens) / 1000; }",
_function_source("_billableCost"),
_function_source("_metricsBillableCost"),
"const paidSelected = {usage_buckets: [",
" {model: 'selected', input_tokens: 100, output_tokens: 10, endpoint_cost_tracked: true},",
" {model: 'local-fallback', input_tokens: 200, output_tokens: 20, endpoint_cost_tracked: false},",
"]};",
"const localSelected = {usage_buckets: [",
" {model: 'selected', input_tokens: 100, output_tokens: 10, endpoint_cost_tracked: false},",
" {model: 'paid-fallback', input_tokens: 200, output_tokens: 20, endpoint_cost_tracked: true},",
"]};",
"currentUrl = 'local';",
"const paidToLocal = _metricsBillableCost(paidSelected, 'final', 300, 30);",
"currentUrl = 'paid';",
"const localToPaid = _metricsBillableCost(localSelected, 'final', 300, 30);",
"console.log(JSON.stringify({paidToLocal, localToPaid}));",
])
assert _run_node(source) == {"paidToLocal": 0.11, "localToPaid": 0.22}
@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH")
def test_force_answer_synthesis_segment_is_included_in_fallback_cost():
source = "\n".join([
"function _currentEndpointUrl() { return 'local-selected'; }",
"function isCostTrackedEndpoint() { return false; }",
"function getModelCost(_model, inputTokens, outputTokens) { return (inputTokens + outputTokens) / 1000; }",
_function_source("_billableCost"),
_function_source("_metricsBillableCost"),
"const metrics = {usage_buckets: [",
" {round: 6, model: 'paid-fallback', input_tokens: 100, output_tokens: 0, endpoint_cost_tracked: true},",
" {round: 6, model: 'paid-fallback', input_tokens: 80, output_tokens: 20, endpoint_cost_tracked: true},",
"]};",
"console.log(JSON.stringify({cost: _metricsBillableCost(metrics, 'paid-fallback', 180, 20)}));",
])
assert _run_node(source) == {"cost": 0.2}
@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH")
def test_repeated_live_metrics_render_records_session_cost_once():
source = "\n".join([
"const _COST_KEY = 'ody-session-cost';",
"const state = {};",
"const localStorage = {",
" getItem(key) { return state[key] || null; },",
" setItem(key, value) { state[key] = value; },",
"};",
"const window = {sessionModule: {getCurrentSessionId() { return 'session'; }}};",
"function updateSessionCostUI() {}",
"function _currentEndpointUrl() { return 'local'; }",
"function isCostTrackedEndpoint(url) { return url === 'paid'; }",
"function getModelCost(_model, inputTokens, outputTokens) { return (inputTokens + outputTokens) / 1000; }",
_function_source("_billableCost"),
_function_source("_metricsBillableCost"),
_function_source("recordSessionMetricsCost"),
"const metrics = {model: 'paid-model', input_tokens: 100, output_tokens: 10, endpoint_cost_tracked: true};",
"recordSessionMetricsCost(metrics);",
"recordSessionMetricsCost(metrics);",
"console.log(JSON.stringify({cost: JSON.parse(state[_COST_KEY]).session, recorded: metrics._costRecorded}));",
])
assert _run_node(source) == {"cost": 0.11, "recorded": True}
@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH")
def test_replayed_metrics_use_run_identity_for_durable_cost_deduplication():
source = "\n".join([
"const _COST_KEY = 'ody-session-cost';",
"const _COST_RUNS_KEY = 'ody-session-cost-runs';",
"const _MAX_COST_RUNS_PER_SESSION = 256;",
"const state = {};",
"const localStorage = {",
" getItem(key) { return state[key] || null; },",
" setItem(key, value) { state[key] = value; },",
"};",
"const window = {sessionModule: {getCurrentSessionId() { return 'session'; }}};",
"function updateSessionCostUI() {}",
"function _currentEndpointUrl() { return 'local'; }",
"function isCostTrackedEndpoint(url) { return url === 'paid'; }",
"function getModelCost(_model, inputTokens, outputTokens) { return (inputTokens + outputTokens) / 1000; }",
_function_source("_billableCost"),
_function_source("_metricsBillableCost"),
_function_source("recordSessionMetricsCost"),
_function_source("getSessionCost"),
"const firstObject = {model: 'paid-model', input_tokens: 100, output_tokens: 10, endpoint_cost_tracked: true, _costRecordId: 'run-1'};",
"const replayedObject = {...firstObject};",
"recordSessionMetricsCost(firstObject);",
"recordSessionMetricsCost(replayedObject);",
"console.log(JSON.stringify({cost: getSessionCost('session'), runs: JSON.parse(state[_COST_RUNS_KEY]).session}));",
])
assert _run_node(source) == {"cost": 0.11, "runs": {"run-1": 0.11}}
@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH")
def test_run_cost_ledger_sums_segments_and_updates_repeated_segment_metrics():
source = "\n".join([
"const _COST_KEY = 'ody-session-cost';",
"const _COST_RUNS_KEY = 'ody-session-cost-runs';",
"const _MAX_COST_RUNS_PER_SESSION = 256;",
"const state = {};",
"const localStorage = {",
" getItem(key) { return state[key] || null; },",
" setItem(key, value) { state[key] = value; },",
"};",
"const window = {sessionModule: {getCurrentSessionId() { return 'session'; }}};",
"function updateSessionCostUI() {}",
"function _currentEndpointUrl() { return 'paid'; }",
"function isCostTrackedEndpoint() { return true; }",
"function getModelCost(_model, inputTokens, outputTokens) { return (inputTokens + outputTokens) / 1000; }",
_function_source("_billableCost"),
_function_source("_metricsBillableCost"),
_function_source("recordSessionMetricsCost"),
_function_source("getSessionCost"),
"recordSessionMetricsCost({model: 'student', input_tokens: 100, output_tokens: 10, _costRecordId: 'run:primary'});",
"recordSessionMetricsCost({model: 'student', input_tokens: 120, output_tokens: 20, _costRecordId: 'run:primary'});",
"recordSessionMetricsCost({model: 'teacher', input_tokens: 200, output_tokens: 30, _costRecordId: 'run:teacher'});",
"console.log(JSON.stringify({cost: getSessionCost('session'), runs: JSON.parse(state[_COST_RUNS_KEY]).session}));",
])
assert _run_node(source) == {
"cost": 0.37,
"runs": {"run:primary": 0.14, "run:teacher": 0.23},
}
@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH")
def test_local_selected_endpoint_does_not_erase_paid_fallback_ledger():
source = "\n".join([
"const _COST_KEY = 'ody-session-cost';",
"const _COST_RUNS_KEY = 'ody-session-cost-runs';",
"const state = {'ody-session-cost': JSON.stringify({session: 0.125})};",
"const localStorage = {",
" getItem(key) { return state[key] || null; },",
" setItem(key, value) { state[key] = value; },",
"};",
"const badge = {style: {}, textContent: ''};",
"const document = {getElementById() { return badge; }};",
"const window = {sessionModule: {getCurrentSessionId() { return 'session'; }, getCurrentEndpointUrl() { return 'local'; }}};",
_function_source("getSessionCost"),
_function_source("updateSessionCostUI"),
"updateSessionCostUI();",
"console.log(JSON.stringify({stored: JSON.parse(state[_COST_KEY]).session, display: badge.style.display, text: badge.textContent}));",
])
assert _run_node(source) == {
"stored": 0.125,
"display": "",
"text": "$0.125",
}
def test_live_and_resumed_terminal_events_apply_usage_metrics_before_reload():
assert "metrics = json.data || metrics;" in _CHAT_SOURCE
assert "displayMetrics(terminalMetricsTarget, metrics);" in _CHAT_SOURCE
assert "metricsData = json.data || metricsData;" in _CHAT_SOURCE
assert "displayMetrics(holder, metricsData);" in _CHAT_SOURCE
assert "json.type === 'agent_terminal' || json.type === 'chat_terminal'" in _CHAT_SOURCE
assert "chatRenderer.recordSessionMetricsCost(metrics, streamSessionId);" in _CHAT_SOURCE
assert "chatRenderer.recordSessionMetricsCost(metricsData, sessionId);" in _CHAT_SOURCE
assert "metricsData._costRecordId = _metricsCostRecordId(resumeRunId, json);" in _CHAT_SOURCE
assert "bgTerminal.status = 'completed';" in _CHAT_SOURCE
def test_usage_command_does_not_hide_existing_fallback_cost_for_local_selection():
assert "const cost = chatRenderer.getSessionCost" in _SLASH_SOURCE
assert "const cost = costTracked && chatRenderer.getSessionCost" not in _SLASH_SOURCE
+203
View File
@@ -0,0 +1,203 @@
"""Execute the round-aware live model-provenance state helper under Node."""
import json
import shutil
import subprocess
from pathlib import Path
import pytest
_REPO = Path(__file__).resolve().parents[1]
_MODULE = (_REPO / "static" / "js" / "chatModelProvenance.js").as_uri()
def test_round_two_fallback_then_provider_alias_does_not_relabel_round_one():
if not shutil.which("node"):
pytest.skip("node is not installed")
script = f"""
import {{ applyModelRouteEventState }} from {json.dumps(_MODULE)};
const round1 = {{ _requestedModel: 'selected-model', _actualModel: 'selected-model' }};
const round2 = {{ _requestedModel: 'selected-model', _actualModel: 'selected-model' }};
const fallbackTarget = applyModelRouteEventState({{
type: 'fallback', round: 2,
selected_model: 'selected-model', answered_by: 'backup-model'
}}, round1, round2, 'selected-model');
const aliasTarget = applyModelRouteEventState({{
type: 'model_actual', round: 2,
requested_model: 'selected-model', model: 'provider-backup-alias'
}}, round1, round2, 'selected-model');
console.log(JSON.stringify({{
fallbackIsRound2: fallbackTarget === round2,
aliasIsRound2: aliasTarget === round2,
round1,
round2,
}}));
"""
result = subprocess.run(
["node", "--input-type=module"],
input=script,
capture_output=True,
text=True,
cwd=_REPO,
timeout=30,
)
assert result.returncode == 0, result.stderr
state = json.loads(result.stdout)
assert state == {
"fallbackIsRound2": True,
"aliasIsRound2": True,
"round1": {
"_requestedModel": "selected-model",
"_actualModel": "selected-model",
},
"round2": {
"_requestedModel": "selected-model",
"_actualModel": "provider-backup-alias",
},
}
def test_next_round_and_final_metrics_preserve_each_agent_round_route():
if not shutil.which("node"):
pytest.skip("node is not installed")
script = f"""
import {{
applyModelMetricsState,
applyModelRouteEventState,
inheritModelRouteState,
}} from {json.dumps(_MODULE)};
const round1 = {{ _requestedModel: 'selected-model', _actualModel: 'selected-model' }};
const round2 = {{}};
inheritModelRouteState(round1, round1, round2, 'selected-model');
applyModelRouteEventState({{
type: 'fallback', round: 2,
selected_model: 'selected-model', answered_by: 'backup-model'
}}, round1, round2, 'selected-model');
applyModelRouteEventState({{
type: 'model_actual', round: 2,
requested_model: 'selected-model', model: 'provider-backup-alias'
}}, round1, round2, 'selected-model');
const round3 = {{}};
inheritModelRouteState(round1, round2, round3, 'selected-model');
const metricsTarget = applyModelMetricsState({{
requested_model: 'selected-model',
model: 'provider-backup-alias',
round_models: ['selected-model', 'provider-backup-alias', 'backup-model'],
}}, round1, round3, 'selected-model');
console.log(JSON.stringify({{
metricsIsRound3: metricsTarget === round3,
round1,
round2,
round3,
}}));
"""
result = subprocess.run(
["node", "--input-type=module"],
input=script,
capture_output=True,
text=True,
cwd=_REPO,
timeout=30,
)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"metricsIsRound3": True,
"round1": {
"_requestedModel": "selected-model",
"_actualModel": "selected-model",
},
"round2": {
"_requestedModel": "selected-model",
"_actualModel": "provider-backup-alias",
},
"round3": {
"_requestedModel": "selected-model",
"_actualModel": "backup-model",
},
}
def test_same_model_fallback_preserves_distinct_endpoint_route_state():
if not shutil.which("node"):
pytest.skip("node is not installed")
script = f"""
import {{ applyModelMetricsState, applyModelRouteEventState }} from {json.dumps(_MODULE)};
const holder = {{ _requestedModel: 'same-model', _actualModel: 'same-model' }};
applyModelRouteEventState({{
type: 'fallback',
selected_model: 'same-model', answered_by: 'same-model',
selected_endpoint_id: 'account-one', selected_endpoint_label: 'Account one',
answered_by_endpoint_id: 'account-two', answered_by_endpoint_label: 'Account two',
}}, holder, null, 'same-model');
applyModelMetricsState({{
requested_model: 'same-model', model: 'same-model',
requested_endpoint_id: 'account-one', requested_endpoint_label: 'Account one',
endpoint_id: 'account-two', endpoint_label: 'Account two',
}}, holder, null, 'same-model');
console.log(JSON.stringify(holder));
"""
result = subprocess.run(
["node", "--input-type=module"],
input=script,
capture_output=True,
text=True,
cwd=_REPO,
timeout=30,
)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"_requestedModel": "same-model",
"_actualModel": "same-model",
"_requestedEndpointId": "account-one",
"_requestedEndpointLabel": "Account one",
"_actualEndpointId": "account-two",
"_actualEndpointLabel": "Account two",
}
def test_metrics_preserve_explicitly_unknown_round_endpoint():
if not shutil.which("node"):
pytest.skip("node is not installed")
script = f"""
import {{ applyModelMetricsState }} from {json.dumps(_MODULE)};
const holder = {{
_requestedModel: 'same-model',
_actualModel: 'same-model',
_requestedEndpointId: 'account-one',
_requestedEndpointLabel: 'Account one',
}};
const roundHolder = {{}};
applyModelMetricsState({{
requested_model: 'same-model', model: 'same-model',
requested_endpoint_id: 'account-one', requested_endpoint_label: 'Account one',
endpoint_id: 'account-two', endpoint_label: 'Account two',
round_endpoint_ids: [null], round_endpoint_labels: [null],
}}, holder, roundHolder, 'same-model');
console.log(JSON.stringify(roundHolder));
"""
result = subprocess.run(
["node", "--input-type=module"],
input=script,
capture_output=True,
text=True,
cwd=_REPO,
timeout=30,
)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"_requestedModel": "same-model",
"_actualModel": "same-model",
"_requestedEndpointId": "account-one",
"_requestedEndpointLabel": "Account one",
"_actualEndpointId": None,
"_actualEndpointLabel": None,
}
+47
View File
@@ -0,0 +1,47 @@
"""Execute terminal stream-error classification under Node."""
import json
import shutil
import subprocess
from pathlib import Path
import pytest
_REPO = Path(__file__).resolve().parents[1]
_MODULE = (_REPO / "static" / "js" / "chatStreamErrors.js").as_uri()
def test_terminal_provider_errors_preserve_text_and_never_auto_retry():
if not shutil.which("node"):
pytest.skip("node is not installed")
script = f"""
import {{ createTerminalStreamError, isRecoverableStreamError }} from {json.dumps(_MODULE)};
const stringError = createTerminalStreamError({{ status: 401, error: 'invalid key' }});
const objectError = createTerminalStreamError({{ status: 404, error: {{ message: 'model missing' }} }});
console.log(JSON.stringify({{
stringMessage: stringError.message,
objectMessage: objectError.message,
terminalRecoverable: isRecoverableStreamError(stringError),
eofRecoverable: isRecoverableStreamError(new Error('Stream closed before completion')),
networkRecoverable: isRecoverableStreamError(new TypeError('fetch failed')),
}}));
"""
result = subprocess.run(
["node", "--input-type=module"],
input=script,
capture_output=True,
text=True,
cwd=_REPO,
timeout=30,
)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"stringMessage": "invalid key",
"objectMessage": "model missing",
"terminalRecoverable": False,
"eofRecoverable": True,
"networkRecoverable": True,
}
+174 -1
View File
@@ -81,6 +81,13 @@ def _make_stream_with_save(sink, chunks, *, hang_after=None):
return gen()
async def _collect_subscription(session_id, expected_run=None):
return [
event
async for event in agent_runs.subscribe(session_id, expected_run)
]
# --------------------------------------------------------------------------- #
# agent_runs: detached-run semantics (what NORMAL chat/agent streams use)
# --------------------------------------------------------------------------- #
@@ -136,7 +143,7 @@ async def test_stop_cancels_detached_run_and_saves_partial_exactly_once():
break
await sub.aclose()
stopped = agent_runs.stop(session_id)
stopped = agent_runs.stop(session_id, run.run_id)
assert stopped is True
await run.task # propagates promptly — not stuck on the hung await
@@ -165,6 +172,172 @@ async def test_normal_completion_saves_exactly_once_not_partial():
assert sink.saves == []
@pytest.mark.asyncio
async def test_detached_run_identity_is_stable_for_replay_and_unique_per_run():
session_id = "sess-detached-run-identity"
agent_runs._RUNS.pop(session_id, None)
first = agent_runs.start(session_id, _make_stream_with_save(_FakeSaveSink(), ["one"]))
first_id = first.run_id
assert agent_runs.get_run_id(session_id) == first_id
await first.task
assert agent_runs.get_run_id(session_id) == first_id
second = agent_runs.start(session_id, _make_stream_with_save(_FakeSaveSink(), ["two"]))
assert second.run_id != first_id
assert agent_runs.get_run_id(session_id) == second.run_id
await second.task
@pytest.mark.asyncio
async def test_lazy_subscription_stays_bound_to_header_run_after_replacement():
session_id = "sess-detached-lazy-subscription"
agent_runs._RUNS.pop(session_id, None)
async def stream(label):
yield f'data: {{"delta":"{label}"}}\n\n'
first = agent_runs.start(session_id, stream("first"))
await first.task
# StreamingResponse does not iterate its body until after construction.
# Capture the same exact run object used for its identity header.
lazy_body = agent_runs.subscribe(session_id, first)
second = agent_runs.start(session_id, stream("second"))
await second.task
replayed = [event async for event in lazy_body]
assert replayed == ['data: {"delta":"first"}\n\n']
assert agent_runs.get_run_id(session_id) == second.run_id
@pytest.mark.asyncio
async def test_stale_run_identity_cannot_stop_replacement_run():
session_id = "sess-detached-stale-stop"
agent_runs._RUNS.pop(session_id, None)
release = asyncio.Event()
async def finished():
yield 'data: {"delta":"old"}\n\n'
async def replacement():
yield 'data: {"delta":"new"}\n\n'
await release.wait()
first = agent_runs.start(session_id, finished())
await first.task
second = agent_runs.start(session_id, replacement())
await asyncio.sleep(0)
assert agent_runs.stop(session_id) is False
assert agent_runs.stop(session_id, first.run_id) is False
assert second.task is not None and not second.task.done()
assert agent_runs.stop(session_id, second.run_id) is True
await second.task
@pytest.mark.asyncio
async def test_triple_replacement_closes_middle_subscriber_and_preserves_save_order():
session_id = "sess-detached-triple-replacement"
agent_runs._RUNS.pop(session_id, None)
first_closing = asyncio.Event()
release_first = asyncio.Event()
third_started = asyncio.Event()
async def first_stream():
try:
yield 'data: {"delta":"first"}\n\n'
await asyncio.Event().wait()
finally:
first_closing.set()
await release_first.wait()
async def middle_stream():
yield 'data: {"delta":"middle"}\n\n'
async def third_stream():
third_started.set()
yield 'data: {"delta":"third"}\n\n'
first = agent_runs.start(session_id, first_stream())
while not first.buffer:
await asyncio.sleep(0)
middle = agent_runs.start(session_id, middle_stream())
await first_closing.wait()
assert middle.task is not None and not middle.task.done()
middle_events_task = asyncio.create_task(
_collect_subscription(session_id, middle)
)
while not middle.subscribers:
await asyncio.sleep(0)
third = agent_runs.start(session_id, third_stream())
# The superseded middle response closes immediately even though its task
# remains as the transitive barrier for the first run's partial save.
assert await asyncio.wait_for(middle_events_task, timeout=1) == []
assert middle.status == "stopped"
assert middle.task is not None and not middle.task.done()
assert not third_started.is_set()
release_first.set()
await asyncio.wait_for(first.task, timeout=1)
await asyncio.wait_for(middle.task, timeout=1)
await asyncio.wait_for(third.task, timeout=1)
assert first.status == "stopped"
assert middle.status == "stopped"
assert third.status == "done"
assert third_started.is_set()
@pytest.mark.asyncio
async def test_reconnect_replays_pinned_fallback_run_without_restarting_tools():
session_id = "sess-detached-fallback-resume"
agent_runs._RUNS.pop(session_id, None)
release = asyncio.Event()
tool_executions = 0
fallback = 'data: {"type":"fallback","answered_by":"backup","candidate_index":1}\n\n'
tool = 'data: {"type":"tool_output","tool":"bash","output":"ok"}\n\n'
async def pinned_run():
nonlocal tool_executions
yield fallback
tool_executions += 1
yield tool
await release.wait()
yield 'data: {"delta":"backup finished"}\n\n'
yield "data: [DONE]\n\n"
run = agent_runs.start(session_id, pinned_run())
first = agent_runs.subscribe(session_id)
first_events = []
async for event in first:
first_events.append(event)
if len(first_events) == 2:
break
await first.aclose()
assert run.status == "running"
assert tool_executions == 1
assert agent_runs._RUNS[session_id] is run
resumed_events = []
resumed = agent_runs.subscribe(session_id)
async for event in resumed:
resumed_events.append(event)
if len(resumed_events) == 2:
release.set()
await run.task
assert resumed_events[:2] == [fallback, tool]
assert resumed_events[-1] == "data: [DONE]\n\n"
assert tool_executions == 1
assert agent_runs._RUNS[session_id] is run
# --------------------------------------------------------------------------- #
# chat_stream: Compare panes must NOT be detached, so the Stop button (closing
# the SSE) cancels the upstream generator promptly — exercising the same
+61
View File
@@ -63,6 +63,23 @@ class TestSelfSummaryPrompt:
class TestTrimForContext:
def test_system_truncation_preserves_internal_route_metadata(self):
messages = [
{
"role": "system",
"content": "persona\n\n" + ("agent prompt " * 2000),
"_agent_injected": "merged_prompt",
"_agent_base_message": {"role": "system", "content": "persona"},
},
{"role": "user", "content": "latest"},
]
trimmed = trim_for_context(messages, context_length=1024, reserve_tokens=256)
system = next(message for message in trimmed if message.get("role") == "system")
assert system["_agent_injected"] == "merged_prompt"
assert system["_agent_base_message"] == {"role": "system", "content": "persona"}
def test_keeps_current_large_user_message_by_truncating(self):
huge = "A" * 20000
messages = [
@@ -194,6 +211,50 @@ class TestMaybeCompactFourthMessage:
assert len(result) == 3 and result[2] is True
@pytest.mark.asyncio
async def test_deferred_compaction_persists_only_after_route_commit(monkeypatch):
updates = []
state = {}
messages = [
{"role": "system", "content": "system " * 100},
{"role": "user", "content": "one"},
{"role": "assistant", "content": "two"},
{"role": "user", "content": "three"},
{"role": "assistant", "content": "four"},
{"role": "user", "content": "five"},
]
monkeypatch.setattr(cc, "get_context_length", lambda *args: 100)
monkeypatch.setattr(cc, "resolve_endpoint", lambda *args, **kwargs: (None, None, None))
async def fake_summary(*args, **kwargs):
return "route-specific summary"
monkeypatch.setattr(cc, "llm_call_async", fake_summary)
monkeypatch.setattr(
cc,
"_update_session_history",
lambda *args, **kwargs: updates.append((args, kwargs)),
)
_compacted, _context, was_compacted = await cc.maybe_compact(
object(),
"https://candidate.example/v1",
"candidate-model",
messages,
persist=False,
compaction_state=state,
)
assert was_compacted is True
assert updates == []
assert state["summary"] == "route-specific summary"
assert cc.apply_compaction_state(object(), state) is True
assert len(updates) == 1
assert cc.apply_compaction_state(object(), state) is False
assert len(updates) == 1
class TestResearchPrimerPreserved:
"""A research-spinoff primer (metadata research_spinoff_from) must never be
trimmed away — it is the Discuss chat's sole knowledge base (drift fix)."""
File diff suppressed because it is too large Load Diff
+5 -3
View File
@@ -8,15 +8,15 @@ from bs4 import BeautifulSoup
_REPO = Path(__file__).resolve().parents[1]
def test_legacy_default_fallback_editor_is_hidden():
def test_legacy_default_fallback_editor_is_absent():
soup = BeautifulSoup(
(_REPO / "static" / "index.html").read_text(encoding="utf-8"),
"html.parser",
)
editor = soup.find(id="set-defaultFallbacks")
assert editor is not None
assert editor.find_parent(class_="settings-row").has_attr("hidden")
assert editor is None
assert soup.find(id="set-defaultAddFallback") is None
def test_default_model_save_does_not_rewrite_legacy_fallbacks():
@@ -27,3 +27,5 @@ def test_default_model_save_does_not_rewrite_legacy_fallbacks():
assert "settings.default_model_fallbacks" not in default_chat_source
assert "default_model_fallbacks:" not in default_chat_source
assert "set-defaultFallbacks" not in default_chat_source
assert "set-defaultAddFallback" not in default_chat_source
@@ -0,0 +1,284 @@
"""Source contract for live multi-round fallback attribution."""
import json
from pathlib import Path
import shutil
import subprocess
import pytest
CHAT_JS = Path("static/js/chat.js").read_text(encoding="utf-8")
_HAS_NODE = shutil.which("node") is not None
def _resume_function_source():
body = CHAT_JS.split("export async function resumeStream", 1)[1].split(
"export function checkBackgroundStream", 1
)[0]
return "async function resumeStream" + body.rstrip()
def _run_node(source):
proc = subprocess.run(
["node", "--input-type=module"],
input=source,
capture_output=True,
text=True,
timeout=30,
)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout.strip())
def test_live_fallback_targets_the_active_round_and_replaces_actual_model():
fallback_block = CHAT_JS.split("json.type === 'fallback'", 1)[1].split(
"json.type === 'doc_stream_open'", 1
)[0]
assert "applyModelRouteEventState(json, holder, roundHolder, modelName)" in fallback_block
assert "_fallbackHolder.querySelector('.role')" in fallback_block
assert "_hasResolvedActual" not in fallback_block
def test_provider_alias_uses_the_same_round_aware_holder_selection():
actual_block = CHAT_JS.split("json.type === 'model_actual'", 1)[1].split(
"json.type === 'attachments'", 1
)[0]
assert "applyModelRouteEventState(json, holder, roundHolder, modelName)" in actual_block
assert "_modelHolder.querySelector('.role')" in actual_block
def test_new_round_and_final_metrics_target_the_active_round():
agent_step_block = CHAT_JS.split("} else if (json.type === 'agent_step')", 1)[1].split(
"json.type === 'budget_exceeded'", 1
)[0]
metrics_block = CHAT_JS.split("json.type === 'metrics'", 1)[1].split(
"json.type === 'message_saved'", 1
)[0]
final_block = CHAT_JS.split("const _isBgFinal", 1)[1].split(
"holder.dataset.raw", 1
)[0]
assert "inheritModelRouteState(holder, roundHolder, newWrap" in agent_step_block
assert "applyModelMetricsState(metrics, holder, roundHolder, modelName)" in metrics_block
assert "_finalModelHolder.querySelector('.role')" in final_block
assert "holder.querySelector('.role')" not in final_block
def test_terminal_sse_error_bypasses_eof_auto_recovery():
parser_block = CHAT_JS.split("if (_nextIsError || json.status >= 400)", 1)[1].split(
"if (json.delta", 1
)[0]
completion_gate = CHAT_JS.split("if (_streamTerminalError)", 1)[1].split(
"if (!_streamSawDone)", 1
)[0]
recovery_block = CHAT_JS.split("isRecoverableStreamError(err)", 1)[1].split(
"const errorHolder", 1
)[0]
assert "createTerminalStreamError(json)" in parser_block
assert "throw _streamTerminalError" in completion_gate
assert "if (err.terminalStreamError)" in recovery_block
assert "await sessionModule.selectSession(streamSessionId, { showLoading: false })" in recovery_block
def test_connection_recovery_resumes_detached_run_without_resubmitting_selected_model():
recovery = CHAT_JS.split("function _tryAutoRecover", 1)[1].split(
"function _removeStallBanner", 1
)[0]
assert "await resumeStream(sessionId, holder || null)" in recovery
assert "/api/chat_stream" not in recovery
assert ".click()" not in recovery
assert "_pendingContinue" not in recovery
assert "if (_streamSessionId === streamSessionId) _streamSessionId = null" in CHAT_JS
def test_detached_resume_reloads_canonical_terminal_failures():
resume = CHAT_JS.split("export async function resumeStream", 1)[1].split(
"export function checkBackgroundStream", 1
)[0]
assert "l.trim() === 'event: error'" in resume
assert "json.type === 'agent_terminal'" in resume
assert "rich = true" in resume
assert "Network drop or parse failure: fall through to the canonical reload" in resume
assert "if (onThisSession && !rich && roundText.trim())" in resume
assert "res.headers.get('X-Odysseus-Run-Id')" in resume
assert "chatRenderer.recordSessionMetricsCost(metricsData, sessionId)" in resume
@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH")
def test_detached_resume_surfaces_fallback_then_provider_alias_before_reload():
source = "\n".join([
"import { applyModelRouteEventState } from './static/js/chatModelProvenance.js';",
"class Element {",
" constructor(tag = 'div') { this.tag = tag; this.children = []; this.parentNode = null; this.style = {}; this.textContent = ''; this._html = ''; }",
" appendChild(child) { child.parentNode = this; this.children.push(child); return child; }",
" remove() { if (!this.parentNode) return; this.parentNode.children = this.parentNode.children.filter(c => c !== this); this.parentNode = null; }",
" set innerHTML(value) {",
" this._html = value;",
" if (value.includes('stream-content')) {",
" this._role = new Element('div'); this._role.parentNode = this;",
" this._body = new Element('div'); this._body.parentNode = this;",
" this._content = new Element('div'); this._body.appendChild(this._content);",
" }",
" }",
" get innerHTML() { return this._html; }",
" querySelector(selector) { if (selector === '.role') return this._role || null; if (selector === '.body') return this._body || null; if (selector === '.stream-content') return this._content || null; return null; }",
"}",
"const box = new Element('main');",
"const document = { getElementById(id) { return id === 'chat-history' ? box : null; }, createElement(tag) { return new Element(tag); } };",
"const window = {};",
"let selectCalls = 0; const labels = []; const toasts = [];",
"const sessionModule = { getSessions() { return [{id: 's1', model: 'selected-model'}]; }, getCurrentSessionId() { return 's1'; }, selectSession() { selectCalls += 1; }, loadSessions() {} };",
"const uiModule = { esc(value) { return String(value); }, scrollHistory() {}, showToast(value) { toasts.push(value); } };",
"const spinnerModule = { create() { return { element: null, createElement() { this.element = new Element('spinner'); return this.element; }, start() {}, destroy() { if (this.element) this.element.remove(); } }; } };",
"const markdownModule = { normalizeThinkingMarkup(v) { return v; }, mdToHtml(v) { return v; }, squashOutsideCode(v) { return v; } };",
"const documentModule = null; const chatRenderer = { recordSessionMetricsCost() {}, addMessage() {} };",
"const _resumingStreams = new Set(); const _streamRunIds = new Map(); const API_BASE = '';",
"function hasActiveStream() { return false; } function _shortModel(v) { return v; } function _applyModelColor() {}",
"function _setRoleModelLabel(role, requested, actual) { labels.push({requested, actual}); role.textContent = requested + ' -> ' + actual; }",
"function _streamDisplayText(v) { return v; } function _showDocumentWritingStatus() {} function _finishDocumentWritingStatus() {} function _metricsCostRecordId() { return 'run'; }",
"const events = [",
" 'data: {\"type\":\"fallback\",\"selected_model\":\"selected-model\",\"answered_by\":\"fallback-model\",\"reason\":\"429\"}\\n\\n',",
" 'data: {\"type\":\"model_actual\",\"model\":\"provider/fallback-alias\"}\\n\\n',",
" 'data: {\"delta\":\"hello\"}\\n\\n',",
" 'data: [DONE]\\n\\n',",
"].join('');",
"const encoded = new TextEncoder().encode(events); let reads = 0;",
"const reader = { async read() { return reads++ === 0 ? {done:false, value:encoded} : {done:true}; }, async cancel() {} };",
"async function fetch() { return { ok:true, body:{getReader(){return reader;}}, headers:{get(){return 'run-1';}} }; }",
_resume_function_source(),
"await resumeStream('s1');",
"console.log(JSON.stringify({labels, toasts, selectCalls, holderCount: box.children.length}));",
])
assert _run_node(source) == {
"labels": [
{"requested": "selected-model", "actual": "fallback-model"},
{"requested": "selected-model", "actual": "provider/fallback-alias"},
],
"toasts": ["Fallback: selected-model failed — answered by fallback-model"],
"selectCalls": 1,
"holderCount": 0,
}
@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH")
def test_detached_resume_renders_preoutput_error_without_empty_reload():
source = "\n".join([
"import { createTerminalStreamError } from './static/js/chatStreamErrors.js';",
"class Element {",
" constructor(tag = 'div') { this.tag = tag; this.children = []; this.parentNode = null; this.style = {}; this.textContent = ''; this._html = ''; }",
" appendChild(child) { child.parentNode = this; this.children.push(child); return child; }",
" remove() { if (!this.parentNode) return; this.parentNode.children = this.parentNode.children.filter(c => c !== this); this.parentNode = null; }",
" set innerHTML(value) {",
" this._html = value;",
" if (value.includes('stream-content')) {",
" this._role = new Element('div'); this._role.parentNode = this;",
" this._body = new Element('div'); this._body.parentNode = this;",
" this._content = new Element('div'); this._body.appendChild(this._content);",
" }",
" }",
" get innerHTML() { return this._html; }",
" querySelector(selector) { if (selector === '.role') return this._role || null; if (selector === '.body') return this._body || null; if (selector === '.stream-content') return this._content || null; return null; }",
"}",
"const box = new Element('main');",
"const document = { getElementById(id) { return id === 'chat-history' ? box : null; }, createElement(tag) { return new Element(tag); } };",
"const window = {};",
"let selectCalls = 0;",
"const sessionModule = { getSessions() { return [{id: 's1', model: 'selected'}]; }, getCurrentSessionId() { return 's1'; }, selectSession() { selectCalls += 1; }, loadSessions() {} };",
"const uiModule = { esc(value) { return String(value); }, scrollHistory() {} };",
"const spinnerModule = { create() { return { element: null, createElement() { this.element = new Element('spinner'); return this.element; }, start() {}, destroy() { if (this.element) this.element.remove(); } }; } };",
"const markdownModule = { normalizeThinkingMarkup(v) { return v; }, mdToHtml(v) { return v; }, squashOutsideCode(v) { return v; } };",
"const documentModule = null;",
"const chatRenderer = { recordSessionMetricsCost() {}, addMessage() {} };",
"const _resumingStreams = new Set(); const _streamRunIds = new Map(); const API_BASE = '';",
"function hasActiveStream() { return false; } function _shortModel(v) { return v; } function _applyModelColor() {}",
"function _streamDisplayText(v) { return v; } function _showDocumentWritingStatus() {} function _finishDocumentWritingStatus() {} function _metricsCostRecordId() { return 'run'; }",
"const encoded = new TextEncoder().encode('event: error\\ndata: {\"status\":401,\"error\":\"invalid key <img src=x>\"}\\n\\n');",
"let reads = 0; const reader = { async read() { return reads++ === 0 ? {done:false, value:encoded} : {done:true}; }, async cancel() {} };",
"async function fetch() { return { ok:true, body:{getReader(){return reader;}}, headers:{get(){return 'run-1';}} }; }",
_resume_function_source(),
"const result = await resumeStream('s1');",
"const holder = box.children[0]; const errorNode = holder && holder._content.children.find(node => node.textContent.startsWith('[Error:'));",
"console.log(JSON.stringify({result, selectCalls, holderCount: box.children.length, errorText: errorNode && errorNode.textContent}));",
])
assert _run_node(source) == {
"result": True,
"selectCalls": 0,
"holderCount": 1,
"errorText": "[Error: invalid key <img src=x>]",
}
def test_terminal_then_session_switch_preserves_completed_background_state():
terminal = CHAT_JS.split(
"json.type === 'agent_terminal' || json.type === 'chat_terminal'", 1
)[1].split("json.type === 'metrics'", 1)[0]
detach = CHAT_JS.split("export function detachCurrentStream", 1)[1].split(
"export async function resumeStream", 1
)[0]
background_catch = CHAT_JS.split("if (_isBgCatch)", 1)[1].split(
"} else {", 1
)[0]
assert "_terminalSavedStreams.add(streamSessionId)" in terminal
assert "terminalSaved ? 'completed' : 'running'" in detach
assert "!terminalSaved && sessionModule && sessionModule.markStreaming" in detach
assert "_terminalSavedStreams.has(streamSessionId)" in background_catch
def test_detached_run_identity_is_attached_to_live_metrics():
routes = Path("routes/chat_routes.py").read_text(encoding="utf-8")
assert "headers={\"X-Odysseus-Run-Id\": _detached_run.run_id}" in routes
assert "agent_runs.subscribe(session, _detached_run)" in routes
assert "agent_runs.subscribe(session_id, _active_run)" in routes
assert "const streamRunId = res.headers.get('X-Odysseus-Run-Id')" in CHAT_JS
assert "metrics._costRecordId = _metricsCostRecordId(streamRunId, json)" in CHAT_JS
assert "'X-Odysseus-Run-Id': runId" in CHAT_JS
assert "agent_runs.stop(session_id, _expected_run_id)" in routes
assert "_stopExactRun(streamSessionId)" in CHAT_JS
timeout_block = CHAT_JS.split("timeoutId = setTimeout", 1)[1].split(
"clearResponseTimeout", 1
)[0]
assert "/api/chat/stop/" not in timeout_block
def test_replay_cost_identity_distinguishes_primary_and_teacher_segments():
identity = CHAT_JS.split("function _metricsCostRecordId", 1)[1].split("\n }", 1)[0]
resume = CHAT_JS.split("export async function resumeStream", 1)[1].split(
"export function checkBackgroundStream", 1
)[0]
assert "event.teacher ? 'teacher' : 'primary'" in identity
assert "_metricsCostRecordId(resumeRunId, json)" in resume
metrics_block = resume.split("json.type === 'metrics'", 1)[1].split(
"json.type === 'agent_terminal'", 1
)[0]
assert "chatRenderer.recordSessionMetricsCost(metricsData, sessionId)" in metrics_block
routes = Path("routes/chat_routes.py").read_text(encoding="utf-8")
route_metrics = routes.split('elif data.get("type") == "metrics"', 1)[1].split(
"except json.JSONDecodeError", 1
)[0]
assert 'if data.get("teacher") is True' in route_metrics
assert '_metrics_event["teacher"] = True' in route_metrics
def test_foreground_terminal_error_reloads_saved_partial_without_typewriter_race():
parser = CHAT_JS.split("if (_nextIsError || json.status >= 400)", 1)[1].split(
"if (json.delta", 1
)[0]
terminal_catch = CHAT_JS.split("if (err.terminalStreamError)", 1)[1].split(
"const errorHolder", 1
)[0]
assert "typewriterInto" not in parser
assert "json.type === 'agent_terminal'" in CHAT_JS
assert "_canonicalTerminalSaved = true" in CHAT_JS
assert "await sessionModule.selectSession(streamSessionId, { showLoading: false })" in terminal_catch
File diff suppressed because it is too large Load Diff
+64 -1
View File
@@ -9,6 +9,8 @@ stream_llm only captured usage when the delta was exactly None / {} /
import asyncio
import json
import pytest
from src import llm_core
@@ -116,7 +118,8 @@ def test_null_choice_chunk_does_not_crash(monkeypatch):
def test_null_choice_with_null_usage_does_not_crash(monkeypatch):
# Chunk with both choices:[null] and usage:null — neither field should panic.
# Chunk with both choices:[null] and usage:null is a keepalive, not a real
# zero-token accounting record.
lines = [
'data: ' + json.dumps({"choices": [{"delta": {"content": "Hi"}}]}),
'data: ' + json.dumps({"choices": [None], "usage": None}),
@@ -124,6 +127,66 @@ def test_null_choice_with_null_usage_does_not_crash(monkeypatch):
]
result = _drive(monkeypatch, lines)
assert "Hi" in result
assert _usage_events(result) == []
def test_empty_usage_object_is_not_reported_as_real_zero_usage(monkeypatch):
lines = [
'data: ' + json.dumps({"choices": [{"delta": {"content": "Hi"}}]}),
'data: ' + json.dumps({"choices": [], "usage": {}}),
'data: [DONE]',
]
result = _drive(monkeypatch, lines)
assert "Hi" in result
assert _usage_events(result) == []
def test_explicit_zero_token_usage_is_preserved(monkeypatch):
lines = [
'data: ' + json.dumps({"choices": [{"delta": {"content": "Hi"}}]}),
'data: ' + json.dumps({
"choices": [],
"usage": {"prompt_tokens": 0, "completion_tokens": 0},
}),
'data: [DONE]',
]
usage = _usage_events(_drive(monkeypatch, lines))
assert usage == [{"input_tokens": 0, "output_tokens": 0}]
@pytest.mark.parametrize(
"usage_payload",
[
{"prompt_tokens": None, "completion_tokens": 1},
{"prompt_tokens": "bad", "completion_tokens": 1},
{"prompt_tokens": -1, "completion_tokens": 1},
{"prompt_tokens": True, "completion_tokens": 1},
{"prompt_tokens": 1.5, "completion_tokens": 1},
{"prompt_tokens": float("inf"), "completion_tokens": 1},
],
)
def test_malformed_token_values_do_not_emit_usage(monkeypatch, usage_payload):
lines = [
'data: ' + json.dumps({"choices": [{"delta": {"content": "Hi"}}]}),
'data: ' + json.dumps({"choices": [], "usage": usage_payload}),
'data: [DONE]',
]
result = _drive(monkeypatch, lines)
assert "Hi" in result
assert _usage_events(result) == []
def test_missing_usage_counterpart_defaults_to_zero(monkeypatch):
lines = [
'data: ' + json.dumps({"choices": [{"delta": {"content": "Hi"}}]}),
'data: ' + json.dumps({
"choices": [],
"usage": {"completion_tokens": 2},
}),
'data: [DONE]',
]
usage = _usage_events(_drive(monkeypatch, lines))
assert usage == [{"input_tokens": 0, "output_tokens": 2}]
def test_null_tool_call_in_delta_is_skipped(monkeypatch):
+21 -1
View File
@@ -98,6 +98,9 @@ def test_endpoint_cleanup_preserves_legacy_default_fallback_data():
{"endpoint_id": "dead", "model": "fallback-a"},
{"endpoint_id": "keep", "model": "fallback-b"},
],
"foreground_model_fallbacks": [
{"endpoint_id": "dead", "model": "foreground"},
],
"utility_model_fallbacks": [{"endpoint_id": "dead", "model": "utility"}],
"vision_model_fallbacks": [{"endpoint_id": "dead", "model": "vision"}],
"stt_provider": "endpoint:dead",
@@ -106,12 +109,14 @@ def test_endpoint_cleanup_preserves_legacy_default_fallback_data():
assert _endpoint_settings_using_endpoint(settings, "dead", include_speech=True) == [
"Default Model",
"Foreground Model Fallbacks",
"Utility Model Fallbacks",
"Vision Model Fallbacks",
"Speech to Text",
]
assert _clear_endpoint_settings_for_endpoint(settings, "dead", include_speech=True) == [
"Default Model",
"Foreground Model Fallbacks",
"Utility Model Fallbacks",
"Vision Model Fallbacks",
"Speech to Text",
@@ -122,6 +127,7 @@ def test_endpoint_cleanup_preserves_legacy_default_fallback_data():
{"endpoint_id": "dead", "model": "fallback-a"},
{"endpoint_id": "keep", "model": "fallback-b"},
]
assert settings["foreground_model_fallbacks"] == []
assert settings["utility_model_fallbacks"] == []
assert settings["vision_model_fallbacks"] == []
assert settings["stt_provider"] == "disabled"
@@ -130,10 +136,19 @@ def test_endpoint_cleanup_preserves_legacy_default_fallback_data():
def test_endpoint_cleanup_updates_active_scoped_prefs_but_preserves_legacy_data():
scoped = {
"foreground_model_fallbacks": [
{"endpoint_id": "dead", "model": "ownerless"},
],
"default_model_fallbacks": [
{"endpoint_id": "dead", "model": "legacy-ownerless"},
],
"_users": {
"alice": {
"utility_endpoint_id": "dead",
"utility_model": "utility",
"foreground_model_fallbacks": [
{"endpoint_id": "dead", "model": "foreground"},
],
"vision_model_fallbacks": [{"endpoint_id": "dead", "model": "vision"}],
},
"bob": {
@@ -142,10 +157,15 @@ def test_endpoint_cleanup_updates_active_scoped_prefs_but_preserves_legacy_data(
},
},
}
assert _clear_user_pref_endpoint_refs(scoped, "dead") == 1
assert _clear_user_pref_endpoint_refs(scoped, "dead") == 2
assert scoped["foreground_model_fallbacks"] == []
assert scoped["default_model_fallbacks"] == [
{"endpoint_id": "dead", "model": "legacy-ownerless"},
]
assert scoped["_users"]["alice"] == {
"utility_endpoint_id": "",
"utility_model": "",
"foreground_model_fallbacks": [],
"vision_model_fallbacks": [],
}
assert scoped["_users"]["bob"]["default_endpoint_id"] == "keep"
@@ -0,0 +1,933 @@
"""Executable regressions for the browser/run-lifecycle review of PR #6020.
These tests intentionally exercise JavaScript under Node rather than treating
``node --check`` or source-string presence as proof that the browser paths are
usable. The detached-run replacement case drives the real Python manager.
"""
from __future__ import annotations
import asyncio
import json
from pathlib import Path
import re
import shutil
import subprocess
import pytest
from src import agent_runs
_REPO = Path(__file__).resolve().parents[1]
_CHAT_PATH = _REPO / "static" / "js" / "chat.js"
_CHAT = _CHAT_PATH.read_text(encoding="utf-8")
_RENDERER = (_REPO / "static" / "js" / "chatRenderer.js").read_text(
encoding="utf-8"
)
_STREAM_ERRORS_URI = (_REPO / "static" / "js" / "chatStreamErrors.js").as_uri()
_HAS_NODE = shutil.which("node") is not None
def _extract_source(source: str, start: str, end: str) -> str:
"""Slice module source between two anchors, failing loudly if one moved.
The extracted region ships to Node verbatim, so the anchors must stay
unique strings in the module. A refactor that renames or duplicates an
anchor fails here with the anchor named, not with an opaque split error.
"""
assert source.count(start) == 1, f"start anchor not unique in source: {start!r}"
tail = source.split(start, 1)[1]
assert end in tail, f"end anchor not found after start anchor: {end!r}"
return start + tail.split(end, 1)[0]
def _run_node(source: str) -> dict:
proc = subprocess.run(
["node", "--input-type=module"],
input=source,
capture_output=True,
text=True,
cwd=_REPO,
timeout=30,
)
assert proc.returncode == 0, proc.stderr
# A few imported browser modules log optional-service status at startup.
# Keep the runtime smoke honest while reading only its final JSON result.
return json.loads(proc.stdout.strip().splitlines()[-1])
def _chat_smoke_source(extra_source: str) -> str:
"""Return chat.js source with its real imports made absolute."""
def absolute_import(match: re.Match[str]) -> str:
relative = match.group("relative")
path_part, separator, query = relative.partition("?")
target = (_CHAT_PATH.parent / path_part).resolve().as_uri()
if separator:
target += "?" + query
return match.group("prefix") + target + match.group("quote")
source = re.sub(
r"(?P<prefix>from\s+(?P<quote>['\"]))(?P<relative>\./[^'\"]+)(?P=quote)",
absolute_import,
_CHAT,
)
source += "\n" + extra_source
return source
@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH")
def test_chat_runtime_stream_state_helpers_are_all_callable(tmp_path):
"""Import the real module and execute all three rebased-away helpers."""
module_source = _chat_smoke_source(
"""
export function __pr6020StreamStateSmoke() {
const sid = 'pr6020-runtime-smoke';
const controller = { abort() {}, signal: { aborted: false } };
const originalGetSessionId = sessionModule.getCurrentSessionId;
sessionModule.getCurrentSessionId = () => sid;
try {
_activeStreams.set(sid, {
abortCtrl: controller,
holder: { id: 'holder' },
lastActivity: 0,
});
const active = _getForegroundStreamState();
const touchedAt = _touchStreamActivity(sid);
const synced = _syncForegroundStreamGlobals();
return {
activeController: active && active.abortCtrl === controller,
touched: touchedAt > 0 && _activeStreams.get(sid).lastActivity === touchedAt,
synced: synced === active && currentAbort === controller && isStreaming,
};
} finally {
_activeStreams.delete(sid);
sessionModule.getCurrentSessionId = originalGetSessionId;
}
}
"""
)
module_path = tmp_path / "chat-runtime-smoke.mjs"
module_path.write_text(module_source, encoding="utf-8")
module_uri = module_path.as_uri()
script = f"""
globalThis.window = globalThis;
globalThis.addEventListener = () => {{}};
globalThis.removeEventListener = () => {{}};
globalThis.dispatchEvent = () => {{}};
globalThis.requestAnimationFrame = () => 0;
globalThis.cancelAnimationFrame = () => {{}};
globalThis.fetch = async () => ({{
ok: false,
json: async () => ({{}}),
text: async () => '',
headers: {{ get() {{ return null; }} }},
}});
class Element {{
constructor() {{
this.children = [];
this.classList = {{
add() {{}}, remove() {{}}, toggle() {{}}, contains() {{ return false; }},
}};
this.style = {{ setProperty() {{}} }};
this.dataset = {{}};
}}
querySelector() {{ return null; }}
querySelectorAll() {{ return []; }}
appendChild(child) {{ this.children.push(child); return child; }}
addEventListener() {{}}
removeEventListener() {{}}
}}
class HTMLInputElement extends Element {{
get value() {{ return this._value || ''; }}
set value(value) {{ this._value = value; }}
}}
globalThis.HTMLInputElement = HTMLInputElement;
const root = new Element();
globalThis.document = {{
body: root,
head: root,
documentElement: root,
getElementById() {{ return null; }},
querySelector() {{ return null; }},
querySelectorAll() {{ return []; }},
createElement(tag) {{ return tag === 'input' ? new HTMLInputElement() : new Element(); }},
createTextNode(text) {{ return {{ textContent: text }}; }},
addEventListener() {{}},
removeEventListener() {{}},
}};
globalThis.localStorage = {{ getItem() {{ return null; }}, setItem() {{}}, removeItem() {{}} }};
globalThis.location = {{}};
globalThis.history = {{}};
globalThis.MutationObserver = class {{ observe() {{}} }};
globalThis.CustomEvent = class {{}};
globalThis.Storage = class {{}};
const chat = await import({json.dumps(module_uri)});
console.log(JSON.stringify(chat.__pr6020StreamStateSmoke()));
process.exit(0);
"""
assert _run_node(script) == {
"activeController": True,
"touched": True,
"synced": True,
}
@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH")
def test_canonical_terminal_followed_by_eof_is_not_auto_recovered():
"""A persisted terminal marker owns EOF; a plain premature EOF still retries."""
completion_gate = _extract_source(
_CHAT,
"if (_streamTerminalError)",
"// The final foreground render below is authoritative.",
)
script = f"""
import {{
createTerminalStreamError,
isRecoverableStreamError,
}} from {json.dumps(_STREAM_ERRORS_URI)};
function runCompletionGate(canonicalTerminalSaved) {{
let _streamTerminalError = null;
let _streamSawDone = false;
let _canonicalTerminalSaved = canonicalTerminalSaved;
try {{
{completion_gate}
return {{ recovered: false, completed: true }};
}} catch (error) {{
return {{
recovered: isRecoverableStreamError(error),
completed: false,
terminal: !!error.terminalStreamError,
message: error.message,
}};
}}
}}
console.log(JSON.stringify({{
savedTerminal: runCompletionGate(true),
plainEof: runCompletionGate(false),
}}));
"""
assert _run_node(script) == {
# A saved canonical terminal must neither auto-recover nor render as a
# clean success: it takes the terminal-error path, whose catch handler
# reloads the persisted record.
"savedTerminal": {
"recovered": False,
"completed": False,
"terminal": True,
"message": "Stream closed after canonical terminal event",
},
"plainEof": {
"recovered": True,
"completed": False,
"terminal": False,
"message": "Stream closed before completion",
},
}
@pytest.mark.asyncio
async def test_immediate_replacement_closes_subscriber_bound_to_never_started_run():
"""Cancellation before _drain's first instruction must still terminalize run 1."""
session_id = "pr6020-immediate-replacement"
agent_runs._RUNS.pop(session_id, None)
async def never_started():
yield 'data: {"delta":"old"}\n\n'
async def replacement():
yield 'data: {"delta":"new"}\n\n'
first = agent_runs.start(session_id, never_started())
first_subscription = asyncio.create_task(
_collect_run_events(session_id, first)
)
# Do not yield between starts: first.task is cancelled before _drain gets
# its first instruction, exactly the race a rapid double-send creates.
second = agent_runs.start(session_id, replacement())
assert await asyncio.wait_for(first_subscription, timeout=0.5) == []
await asyncio.wait_for(second.task, timeout=0.5)
assert first.status == "stopped"
assert second.status == "done"
async def _collect_run_events(session_id: str, run: object) -> list[str]:
return [event async for event in agent_runs.subscribe(session_id, run)]
@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH")
def test_stop_before_response_headers_waits_for_exact_run_identity():
"""Never send headerless Stop, but flush the queued Stop once headers arrive."""
state_and_stop = _extract_source(
_CHAT, "const _backgroundStreams", "// Sources box builder"
)
header_capture = _extract_source(
_CHAT,
"const streamRunId = res.headers.get('X-Odysseus-Run-Id')",
"// Mark the chat log busy",
)
script = f"""
const calls = [];
function _setForegroundChatBusy() {{}}
const window = {{}};
const sessionModule = {{ getCurrentSessionId() {{ return 'session-1'; }} }};
const fetch = async (url, options) => {{ calls.push({{ url, options }}); return {{ ok: true }}; }};
{state_and_stop}
{{
const streamSessionId = 'normal-session';
const streamGeneration = 1;
_streamGenerations.set(streamSessionId, streamGeneration);
const res = {{ headers: {{ get(name) {{
return name === 'X-Odysseus-Run-Id' ? 'normal-run' : null;
}} }} }};
{header_capture}
}}
await new Promise(resolve => setTimeout(resolve, 0));
const normalHeaderCalls = calls.length;
let beforeHeaders;
{{
const streamSessionId = 'session-1';
const streamGeneration = 1;
_streamGenerations.set(streamSessionId, streamGeneration);
_stopExactRun(streamSessionId);
beforeHeaders = calls.length;
const res = {{ headers: {{ get(name) {{
return name === 'X-Odysseus-Run-Id' ? 'run-1' : null;
}} }} }};
{header_capture}
}}
await new Promise(resolve => setTimeout(resolve, 0));
console.log(JSON.stringify({{
normalHeaderCalls,
beforeHeaders,
calls: calls.map(call => ({{
url: call.url,
method: call.options.method,
runId: call.options.headers['X-Odysseus-Run-Id'],
}})),
}}));
"""
assert _run_node(script) == {
"normalHeaderCalls": 0,
"beforeHeaders": 0,
"calls": [
{
"url": "/api/chat/stop/session-1",
"method": "POST",
"runId": "run-1",
}
],
}
@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH")
def test_timeout_before_response_headers_also_waits_for_exact_run_identity():
"""The automatic timeout must preserve the POST until its run id arrives."""
script = f"""
{_timeout_harness_prelude()}
callbacks[0]();
const beforeHeaders = {{ aborted: abortCtrl.signal.aborted, calls: calls.length }};
_rememberStreamRunId(streamSessionId, 'run-1', streamGeneration);
await Promise.resolve();
console.log(JSON.stringify({{
beforeHeaders,
afterHeaders: {{
aborted: abortCtrl.signal.aborted,
runId: calls[0] && calls[0].options.headers['X-Odysseus-Run-Id'],
}},
}}));
"""
assert _run_node(script) == {
"beforeHeaders": {"aborted": False, "calls": 0},
"afterHeaders": {"aborted": True, "runId": "run-1"},
}
@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH")
def test_resend_preserves_queued_stop_until_old_run_identity_arrives():
"""A replacement must not sever the superseded POST's identity channel.
The queued Stop stays generation-tagged and fires from the OLD send's own
header arrival, so the old run is cancelled even when the replacement dies
before its POST reaches the server (which is what would otherwise cancel
it). The old run id must not leak into the replacement's identity map.
"""
state_and_stop = _extract_source(
_CHAT, "const _backgroundStreams", "// Sources box builder"
)
resend_reset = _extract_source(
_CHAT,
"const streamGeneration = (_streamGenerations.get(streamSessionId) || 0) + 1;",
"_sendInFlight = false;",
)
header_capture = _extract_source(
_CHAT,
"const streamRunId = res.headers.get('X-Odysseus-Run-Id')",
"// Mark the chat log busy",
)
script = f"""
const calls = [];
function _setForegroundChatBusy() {{}}
const window = {{}};
const sessionModule = {{ getCurrentSessionId() {{ return 'session-1'; }} }};
const fetch = async (url, options) => {{ calls.push({{ url, options }}); return {{ ok: true }}; }};
{state_and_stop}
const oldCtrl = {{
_reason: '',
signal: {{ aborted: false }},
abort() {{ this.signal.aborted = true; }},
}};
// Old send (generation 1) queues a Stop before its headers arrive.
_streamGenerations.set('session-1', 1);
const oldGeneration = 1;
_stopExactRun('session-1', oldCtrl);
const queuedBefore = _pendingRunStops.has('session-1:1');
// Replacement send starts: bumps the generation, leaves the queued Stop.
{{
const streamSessionId = 'session-1';
{resend_reset}
}}
// The replacement is ALSO stopped before its headers arrive: both
// sends' cancellation intents must coexist, neither displacing the
// other (a single session-keyed slot loses the old send's Stop, and
// with it the only cancel for that run if this replacement dies
// before its own POST reaches the server).
const newCtrl = {{
_reason: '',
signal: {{ aborted: false }},
abort() {{ this.signal.aborted = true; }},
}};
_stopExactRun('session-1', newCtrl);
const afterResend = {{
oldQueuedKept: _pendingRunStops.has('session-1:1'),
newQueued: _pendingRunStops.has('session-1:2'),
oldAborted: oldCtrl.signal.aborted,
generation: _streamGenerations.get('session-1'),
}};
// The old POST's headers finally arrive: its queued Stop fires with its
// own run id, and the old controller aborts.
{{
const streamSessionId = 'session-1';
const streamGeneration = oldGeneration;
const res = {{ headers: {{ get(name) {{
return name === 'X-Odysseus-Run-Id' ? 'old-run' : null;
}} }} }};
{header_capture}
}}
await new Promise(resolve => setTimeout(resolve, 0));
console.log(JSON.stringify({{
queuedBefore,
afterResend,
afterOldHeaders: {{
oldQueued: _pendingRunStops.has('session-1:1'),
newQueuedKept: _pendingRunStops.has('session-1:2'),
oldAborted: oldCtrl.signal.aborted,
oldReason: oldCtrl._reason,
newAborted: newCtrl.signal.aborted,
currentRunIdPolluted: _streamRunIds.has('session-1'),
stopCalls: calls.map(call => ({{
url: call.url,
runId: call.options.headers['X-Odysseus-Run-Id'],
}})),
}},
}}));
"""
assert _run_node(script) == {
"queuedBefore": True,
"afterResend": {
"oldQueuedKept": True,
"newQueued": True,
"oldAborted": False,
"generation": 2,
},
"afterOldHeaders": {
"oldQueued": False,
# The replacement's own queued Stop must survive the old send's
# flush untouched.
"newQueuedKept": True,
"oldAborted": True,
"oldReason": "user-stop",
"newAborted": False,
# The stale send's run id must not become the replacement's
# identity, but its exact Stop must still go out.
"currentRunIdPolluted": False,
"stopCalls": [
{"url": "/api/chat/stop/session-1", "runId": "old-run"}
],
},
}
@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH")
def test_superseded_stream_cleanup_leaves_replacement_state_alone():
"""A stale send's finally must not clear state the replacement owns.
Ownership is decided by generation, which the replacement bumps at its
very first synchronous step — so the guard holds even in the window
BEFORE the replacement registers its own stream entry (where the old
finally still sees its own registration and controller identity alone
would call it the owner).
"""
state_and_stop = _extract_source(
_CHAT, "const _backgroundStreams", "// Sources box builder"
)
finally_cleanup = _extract_source(
_CHAT,
"const _ownsStreamState =",
"// Streaming done — let screen readers announce",
)
script = f"""
let currentAbort = null;
let isStreaming = false;
let currentHolder = null;
let _sendInFlight = false;
function _setForegroundChatBusy() {{}}
const window = {{}};
const sessionModule = {{ getCurrentSessionId() {{ return 'session-1'; }} }};
{state_and_stop}
function runCleanup(abortCtrl, streamGeneration) {{
const streamSessionId = 'session-1';
const _sendState = {{ generation: streamGeneration, abortCtrl }};
{finally_cleanup}
return _ownsStreamState;
}}
const oldCtrl = {{ signal: {{ aborted: true }}, abort() {{}} }};
const newCtrl = {{ signal: {{ aborted: false }}, abort() {{}} }};
// Pre-registration supersession: the replacement bumped the generation
// and set the session id, but has NOT registered its stream entry yet —
// the old send's own entry is still the one in the map.
_streamGenerations.set('session-1', 2);
_streamSessionId = 'session-1';
_activeStreams.set('session-1', {{ abortCtrl: oldCtrl, holder: null, lastActivity: 1 }});
_pendingRunStops.set('session-1:2', newCtrl);
const preRegOwns = runCleanup(oldCtrl, 1);
const afterPreReg = {{
ownEntryRemoved: !_activeStreams.has('session-1'),
replacementPendingKept: _pendingRunStops.has('session-1:2'),
sessionKept: _streamSessionId === 'session-1',
}};
// Post-registration supersession: the replacement's entry is in the map.
_activeStreams.set('session-1', {{ abortCtrl: newCtrl, holder: null, lastActivity: 2 }});
const postRegOwns = runCleanup(oldCtrl, 1);
const afterPostReg = {{
replacementRegistrationKept: _activeStreams.has('session-1'),
replacementPendingKept: _pendingRunStops.has('session-1:2'),
sessionKept: _streamSessionId === 'session-1',
}};
// Owner: the current-generation send cleans up normally.
const ownerOwns = runCleanup(newCtrl, 2);
const afterOwner = {{
registered: _activeStreams.has('session-1'),
pendingKept: _pendingRunStops.has('session-1:2'),
sessionCleared: _streamSessionId === null,
}};
console.log(JSON.stringify({{
preRegOwns, afterPreReg, postRegOwns, afterPostReg, ownerOwns, afterOwner,
}}));
"""
assert _run_node(script) == {
"preRegOwns": False,
"afterPreReg": {
"ownEntryRemoved": True,
"replacementPendingKept": True,
"sessionKept": True,
},
"postRegOwns": False,
"afterPostReg": {
"replacementRegistrationKept": True,
"replacementPendingKept": True,
"sessionKept": True,
},
"ownerOwns": True,
"afterOwner": {
"registered": False,
"pendingKept": False,
"sessionCleared": True,
},
}
@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH")
def test_real_reservation_supersedes_and_stale_cleanup_keeps_gate_closed():
"""Drive the REAL send-commit reservation, then a stale send's cleanup.
The reservation is synchronous, so the previous send is superseded before
any await runs; its cleanup must then neither clear session state nor
resync the foreground globals (a stale sync would set isStreaming false
while _sendInFlight is already false, reopening the send gate before the
replacement registers).
"""
state_and_stop = _extract_source(
_CHAT, "const _backgroundStreams", "// Sources box builder"
)
reservation = _extract_source(
_CHAT,
"const streamGeneration = (_streamGenerations.get(streamSessionId) || 0) + 1;",
"_sendInFlight = false;",
)
finally_cleanup = _extract_source(
_CHAT,
"const _ownsStreamState =",
"// Streaming done — let screen readers announce",
)
script = f"""
let currentAbort = null;
let isStreaming = true;
let currentHolder = null;
let _sendInFlight = false;
function _setForegroundChatBusy() {{}}
const window = {{}};
const sessionModule = {{ getCurrentSessionId() {{ return 'session-1'; }} }};
{state_and_stop}
const oldCtrl = {{ signal: {{ aborted: false }}, abort() {{ this.signal.aborted = true; }} }};
// Old send (generation 1) is mid-stream and registered.
_streamGenerations.set('session-1', 1);
_streamSessionId = 'session-1';
_activeStreams.set('session-1', {{ abortCtrl: oldCtrl, holder: null, lastActivity: 1 }});
// Replacement commits: run the REAL reservation block synchronously.
let installed;
{{
const streamSessionId = 'session-1';
{reservation}
installed = {{ generation: streamGeneration, sendState: _sendState }};
}}
const afterReservation = {{
generation: _streamGenerations.get('session-1'),
sendStateInstalled: _sendStates.get('session-1') === installed.sendState,
controllerPending: installed.sendState.abortCtrl === null,
}};
// Old send's cleanup runs mid-preflight (before the replacement
// registers): it must treat itself as superseded.
let staleOwns;
{{
const streamSessionId = 'session-1';
const streamGeneration = 1;
const abortCtrl = oldCtrl;
const _sendState = {{ generation: 1, abortCtrl: oldCtrl }};
{finally_cleanup}
staleOwns = _ownsStreamState;
}}
console.log(JSON.stringify({{
afterReservation,
staleOwns,
afterStaleCleanup: {{
sessionKept: _streamSessionId === 'session-1',
sendStateKept: _sendStates.get('session-1') === installed.sendState,
gateStillClosed: isStreaming === true,
}},
}}));
"""
assert _run_node(script) == {
"afterReservation": {
"generation": 2,
"sendStateInstalled": True,
"controllerPending": True,
},
"staleOwns": False,
"afterStaleCleanup": {
"sessionKept": True,
"sendStateKept": True,
# isStreaming untouched because the superseded send skipped the
# foreground resync entirely.
"gateStillClosed": True,
},
}
@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH")
def test_stale_preflight_bails_before_creating_controller():
"""A send superseded during preflight must not proceed to register/POST."""
state_and_stop = _extract_source(
_CHAT, "const _backgroundStreams", "// Sources box builder"
)
preflight_gate = _extract_source(
_CHAT, "// Superseded during preflight", "currentAbort = abortCtrl;"
) + "currentAbort = abortCtrl;"
script = f"""
let currentAbort = null;
let isStreaming = false;
let currentHolder = null;
let _sendInFlight = false;
function _setForegroundChatBusy() {{}}
const window = {{}};
const document = {{
createElement() {{ return {{ style: {{}}, textContent: '' }}; }},
}};
const sessionModule = {{ getCurrentSessionId() {{ return 'session-1'; }} }};
{state_and_stop}
function runPreflightGate(streamGeneration, _sendState, _userMsgEl) {{
const streamSessionId = 'session-1';
let abortCtrl = null;
{preflight_gate}
return abortCtrl;
}}
// Stale: generation 1 resumes after generation 2 reserved the session.
// Its optimistic user bubble must be marked undelivered, not left as a
// ghost that looks sent.
_streamGenerations.set('session-1', 2);
const staleState = {{ generation: 1, abortCtrl: null }};
const staleBubble = {{
parentNode: {{}},
notes: [],
appendChild(node) {{ this.notes.push(node.textContent); }},
}};
const staleResult = runPreflightGate(1, staleState, staleBubble);
// Current: generation 2 proceeds and wires its controller.
const currentState = {{ generation: 2, abortCtrl: null }};
const currentBubble = {{
parentNode: {{}},
notes: [],
appendChild(node) {{ this.notes.push(node.textContent); }},
}};
const currentResult = runPreflightGate(2, currentState, currentBubble);
console.log(JSON.stringify({{
staleBailed: staleResult === undefined,
staleControllerNever: staleState.abortCtrl === null,
staleBubbleNotes: staleBubble.notes,
currentProceeded: !!currentResult,
currentWired: currentState.abortCtrl === currentResult && currentAbort === currentResult,
currentBubbleNotes: currentBubble.notes,
}}));
"""
assert _run_node(script) == {
"staleBailed": True,
"staleControllerNever": True,
"staleBubbleNotes": ["[Not sent — superseded by a newer message]"],
"currentProceeded": True,
"currentWired": True,
"currentBubbleNotes": [],
}
@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH")
def test_stop_during_replacement_preflight_never_borrows_old_controller():
"""Stop must take the current send's controller from its send state.
During the replacement's preflight the stream registry still holds the
superseded send's entry; borrowing that controller would abort the only
identity channel able to name the old run while queueing a Stop for the
new one. A committed-but-pre-POST send has a null controller: the Stop
queues under the new generation and nothing is aborted yet.
"""
state_and_stop = _extract_source(
_CHAT, "const _backgroundStreams", "// Sources box builder"
)
abort_current = _extract_source(
_CHAT, "export function abortCurrentRequest", "// ── Stall watchdog"
).replace("export function", "function")
reservation = _extract_source(
_CHAT,
"const streamGeneration = (_streamGenerations.get(streamSessionId) || 0) + 1;",
"_sendInFlight = false;",
)
script = f"""
let currentAbort = null;
let isStreaming = false;
let currentHolder = null;
let _sendInFlight = false;
function _setForegroundChatBusy() {{}}
const calls = [];
const fetch = async (url, options) => {{ calls.push({{ url, options }}); return {{ ok: true }}; }};
const window = {{}};
const sessionModule = {{ getCurrentSessionId() {{ return 'session-1'; }} }};
{state_and_stop}
{abort_current}
const oldCtrl = {{ signal: {{ aborted: false }}, abort() {{ this.signal.aborted = true; }} }};
// Generation 1 is registered, streaming, and its run id is KNOWN — the
// exact window daybreak probed: a Stop right after the replacement
// commits must not consume the old run identity.
_streamGenerations.set('session-1', 1);
_streamRunIds.set('session-1', 'old-run');
_activeStreams.set('session-1', {{ abortCtrl: oldCtrl, holder: null, lastActivity: 1 }});
currentAbort = oldCtrl;
// Replacement (generation 2) commits via the REAL reservation block; no
// controller exists yet and the model-switch await has not resolved.
{{
const streamSessionId = 'session-1';
{reservation}
}}
abortCurrentRequest(true);
const preRegistration = {{
oldRunIdCleared: !_streamRunIds.has('session-1'),
queuedForNew: _pendingRunStops.has('session-1:2'),
queuedController: _pendingRunStops.get('session-1:2') || null,
oldAborted: oldCtrl.signal.aborted,
stopCalls: calls.length,
}};
// Normal case: the current send's own controller, run id known.
const ownCtrl = {{ _reason: '', signal: {{ aborted: false }}, abort() {{ this.signal.aborted = true; }} }};
_sendStates.set('session-1', {{ generation: 2, abortCtrl: ownCtrl }});
_streamRunIds.set('session-1', 'run-2');
abortCurrentRequest(true);
await new Promise(resolve => setTimeout(resolve, 0));
console.log(JSON.stringify({{
preRegistration,
normal: {{
ownAborted: ownCtrl.signal.aborted,
oldStillUntouched: oldCtrl.signal.aborted,
stopRunId: calls[0] && calls[0].options.headers['X-Odysseus-Run-Id'],
}},
}}));
"""
assert _run_node(script) == {
"preRegistration": {
# The old run identity dies at reservation: the Stop queues for
# the NEW send instead of firing against the old run and skipping
# the queue entirely.
"oldRunIdCleared": True,
"queuedForNew": True,
"queuedController": None,
"oldAborted": False,
"stopCalls": 0,
},
"normal": {
"ownAborted": True,
"oldStillUntouched": False,
"stopRunId": "run-2",
},
}
@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH")
def test_timeout_grace_hard_aborts_when_run_identity_never_arrives():
"""A POST hung before headers is still cancelled by the timeout's grace."""
script = f"""
{_timeout_harness_prelude()}
callbacks[0]();
const afterTimeout = {{ aborted: abortCtrl.signal.aborted, pending: callbacks.length }};
callbacks[1]();
console.log(JSON.stringify({{
afterTimeout,
afterGrace: {{ aborted: abortCtrl.signal.aborted, stopCalls: calls.length }},
}}));
"""
assert _run_node(script) == {
"afterTimeout": {"aborted": False, "pending": 2},
"afterGrace": {"aborted": True, "stopCalls": 0},
}
def _timeout_harness_prelude() -> str:
"""Shared Node harness: real stop/state and timeout blocks, fake timers."""
state_and_stop = _extract_source(
_CHAT, "const _backgroundStreams", "// Sources box builder"
)
timeout_setup = _extract_source(
_CHAT, "timeoutId = setTimeout(() =>", "}, timeoutMs);"
) + "}, timeoutMs);"
return f"""
const calls = [];
const callbacks = [];
function _setForegroundChatBusy() {{}}
function setTimeout(callback) {{ callbacks.push(callback); return 1; }}
const window = {{}};
const sessionModule = {{ getCurrentSessionId() {{ return 'session-1'; }} }};
const fetch = async (url, options) => {{ calls.push({{ url, options }}); return {{ ok: true }}; }};
const RUN_ID_ABORT_GRACE_MS = 2000;
{state_and_stop}
const streamSessionId = 'session-1';
const streamGeneration = 1;
_streamGenerations.set(streamSessionId, streamGeneration);
const timeoutMs = 1;
let timeoutId;
let timedOut = false;
const abortCtrl = {{
_reason: '',
signal: {{ aborted: false }},
abort() {{ this.signal.aborted = true; }},
}};
{timeout_setup}
"""
@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH")
def test_cost_ledger_serializes_stale_cross_tab_writers():
"""A stale writer must merge, not overwrite a distinct run recorded by a peer."""
ledger = _extract_source(
_RENDERER, "const _COST_KEY", "/** Create a timestamp span"
).replace("export function", "function")
script = f"""
const state = {{}};
let triggerPeerWrite = true;
let lockTail = Promise.resolve();
const navigator = {{ locks: {{
request(_name, callback) {{
const next = lockTail.then(callback);
lockTail = next.catch(() => {{}});
return next;
}},
}} }};
const window = {{ sessionModule: {{ getCurrentSessionId() {{ return 'session'; }} }} }};
const document = {{ getElementById() {{ return null; }} }};
function _metricsBillableCost(metrics) {{ return metrics.testCost; }}
const peerMetrics = {{ testCost: 0.22, _costRecordId: 'run-b' }};
let tabA;
let tabB;
const localStorage = {{
getItem(key) {{
const staleSnapshot = state[key] || null;
if (key === 'ody-session-cost-runs' && triggerPeerWrite) {{
triggerPeerWrite = false;
tabB.recordSessionMetricsCost(peerMetrics, 'session');
}}
return staleSnapshot;
}},
setItem(key, value) {{ state[key] = value; }},
}};
function createTab() {{
{ledger}
return {{ recordSessionMetricsCost }};
}}
tabA = createTab();
tabB = createTab();
const metricsA = {{ testCost: 0.11, _costRecordId: 'run-a' }};
tabA.recordSessionMetricsCost(metricsA, 'session');
const queued = {{
recorded: !!metricsA._costRecorded,
pending: !!metricsA._costRecordPending,
}};
await new Promise(resolve => setTimeout(resolve, 0));
await lockTail;
const runs = JSON.parse(state['ody-session-cost-runs'] || '{{}}').session || {{}};
console.log(JSON.stringify({{
queued,
settled: {{
recorded: !!metricsA._costRecorded,
pending: !!metricsA._costRecordPending,
}},
runs,
}}));
"""
assert _run_node(script) == {
# Recorded must not be claimed while the write only sits queued behind
# the lock; it flips once the write has actually run.
"queued": {"recorded": False, "pending": True},
"settled": {"recorded": True, "pending": False},
"runs": {"run-a": 0.11, "run-b": 0.22},
}
+335
View File
@@ -0,0 +1,335 @@
"""Executable regression coverage for behavior lost in PR #6020's rebase."""
import asyncio
import json
import src.agent_loop as agent_loop
ODY_QWEN = "odysseus-qwen3-4b"
NOTES_TOOLS = {
"manage_notes",
"manage_calendar",
"manage_tasks",
"ask_user",
"update_plan",
}
def _collect(generator):
async def _run():
return [chunk async for chunk in generator]
return asyncio.run(_run())
def _events(chunks):
return [
json.loads(chunk[6:])
for chunk in chunks
if chunk.startswith("data: ") and not chunk.startswith("data: [DONE]")
]
def _install_route_probe(monkeypatch):
prompt_calls = []
stream_calls = []
monkeypatch.setattr(agent_loop, "get_setting", lambda key, default=None: default)
monkeypatch.setattr(agent_loop, "get_mcp_manager", lambda: None)
monkeypatch.setattr(agent_loop, "estimate_tokens", lambda *args, **kwargs: 10)
monkeypatch.setattr(agent_loop, "blocked_tools_for_owner", lambda owner: set())
monkeypatch.setattr(
agent_loop,
"_agent_route_tool_mode",
lambda *args, **kwargs: (True, False, False),
)
def fake_build(
messages,
model,
_active_document,
_mcp_mgr,
disabled_tools=None,
**kwargs,
):
prompt_calls.append(
{
"model": model,
"relevant_tools": set(kwargs.get("relevant_tools") or set()),
"disabled_tools": set(disabled_tools or set()),
"workspace": kwargs.get("workspace"),
}
)
return (list(messages), [])
async def fake_stream(_candidates, _messages, **kwargs):
stream_calls.append(kwargs)
yield 'data: {"delta": "ok"}\n\n'
yield "data: [DONE]\n\n"
monkeypatch.setattr(agent_loop, "_build_system_prompt", fake_build)
monkeypatch.setattr(agent_loop, "stream_llm_with_fallback", fake_stream)
return prompt_calls, stream_calls
def _run_probe(messages, *, relevant_tools, **kwargs):
return _collect(
agent_loop.stream_agent_loop(
"https://api.example/v1",
kwargs.pop("model", ODY_QWEN),
messages,
max_rounds=1,
relevant_tools=set(relevant_tools),
_is_teacher_run=True,
**kwargs,
)
)
def test_odysseus_notes_mode_clamps_and_reenables_all_personal_managers(monkeypatch):
prompt_calls, _ = _install_route_probe(monkeypatch)
_run_probe(
[{"role": "user", "content": "Add buy milk to my notes."}],
relevant_tools={"bash", "manage_notes", "manage_calendar", "manage_tasks"},
disabled_tools={"manage_notes", "manage_calendar", "manage_tasks"},
)
route = prompt_calls[0]
assert route["relevant_tools"] == NOTES_TOOLS
assert route["disabled_tools"].isdisjoint(
{"manage_notes", "manage_calendar", "manage_tasks"}
)
def test_odysseus_general_mode_disables_every_tool(monkeypatch):
from src.tool_policy import known_tool_names
prompt_calls, _ = _install_route_probe(monkeypatch)
_run_probe(
[{"role": "user", "content": "Explain the CAP theorem."}],
relevant_tools={"bash", "manage_notes", "ask_user"},
)
route = prompt_calls[0]
assert route["relevant_tools"] == set()
assert known_tool_names() <= route["disabled_tools"]
def test_odysseus_calendar_intent_uses_notes_mode(monkeypatch):
prompt_calls, _ = _install_route_probe(monkeypatch)
_run_probe(
[{"role": "user", "content": "Add lunch tomorrow to my calendar."}],
relevant_tools={"manage_notes", "manage_calendar", "manage_tasks", "bash"},
)
assert prompt_calls[0]["relevant_tools"] == NOTES_TOOLS
def test_odysseus_calendar_followup_keeps_notes_mode(monkeypatch):
prompt_calls, _ = _install_route_probe(monkeypatch)
messages = [
{"role": "user", "content": "Add lunch tomorrow to my calendar."},
{
"role": "assistant",
"content": "Done.",
"metadata": {
"tool_events": [
{
"tool": "manage_calendar",
"command": '{"action":"create_event","summary":"Lunch"}',
"output": "Created event evt-123 at noon.",
}
]
},
},
{"role": "user", "content": "Move it to 3pm."},
]
_run_probe(
messages,
relevant_tools={"manage_notes", "manage_calendar", "manage_tasks", "bash"},
)
assert prompt_calls[0]["relevant_tools"] == NOTES_TOOLS
def test_agent_route_passes_workspace_to_system_prompt(monkeypatch):
prompt_calls, _ = _install_route_probe(monkeypatch)
_run_probe(
[{"role": "user", "content": "Fix the failing test in this project."}],
model="gpt-4o",
relevant_tools={"bash", "read_file", "apply_patch"},
workspace="/tmp/example-repo",
)
assert prompt_calls[0]["workspace"] == "/tmp/example-repo"
def test_odysseus_qwen_temperature_is_capped_for_agent_requests(monkeypatch):
_, stream_calls = _install_route_probe(monkeypatch)
_run_probe(
[{"role": "user", "content": "Add buy milk to my notes."}],
relevant_tools={"manage_notes"},
temperature=1.2,
)
assert stream_calls[0]["temperature"] == 0.2
def test_qwen_fallback_candidate_gets_capped_temperature(monkeypatch):
"""A non-qwen primary must not leak its temperature into a qwen fallback."""
_, stream_calls = _install_route_probe(monkeypatch)
_run_probe(
[{"role": "user", "content": "Explain the CAP theorem."}],
model="gpt-4o",
relevant_tools={"bash"},
temperature=1.2,
fallbacks=[("https://qwen.example/v1", ODY_QWEN, {})],
)
assert stream_calls[0]["temperature"] == 1.2
factory = stream_calls[0]["candidate_request_factory"]
request = asyncio.run(factory(1, "https://qwen.example/v1", ODY_QWEN, {}))
assert request["kwargs"]["temperature"] == 0.2
def test_non_qwen_fallback_keeps_requested_temperature(monkeypatch):
"""A qwen primary's 0.2 cap must not leak into a non-qwen fallback."""
_, stream_calls = _install_route_probe(monkeypatch)
_run_probe(
[{"role": "user", "content": "Add buy milk to my notes."}],
relevant_tools={"manage_notes"},
temperature=1.2,
fallbacks=[("https://backup.example/v1", "gpt-4o", {})],
)
assert stream_calls[0]["temperature"] == 0.2
factory = stream_calls[0]["candidate_request_factory"]
request = asyncio.run(factory(1, "https://backup.example/v1", "gpt-4o", {}))
assert request["kwargs"]["temperature"] == 1.2
def test_qwen_notes_fallback_reenables_personal_managers(monkeypatch):
"""The answering candidate's notes mode must unblock the managers for
execution, not just enable them in its own route schemas."""
_install_route_probe(monkeypatch)
stream_round = 0
resolve_round = 0
seen_exec = {}
async def fake_stream(_candidates, _messages, **kwargs):
nonlocal stream_round
stream_round += 1
if stream_round == 1:
yield (
"data: "
+ json.dumps(
{
"type": "fallback",
"answered_by": ODY_QWEN,
"candidate_index": 1,
}
)
+ "\n\n"
)
yield 'data: {"delta": "Adding the note."}\n\n'
else:
yield 'data: {"delta": "Done."}\n\n'
yield "data: [DONE]\n\n"
def fake_resolve(*args, **kwargs):
nonlocal resolve_round
resolve_round += 1
if resolve_round == 1:
return ([agent_loop.ToolBlock("manage_notes", "{}")], False, [])
return ([], False, [])
async def fake_execute(block, *args, **kwargs):
# Execution is the consumer daybreak's probe showed rejecting the
# managers: it receives the shared disabled_tools set, not the
# answering route's own tool state.
seen_exec["disabled_tools"] = set(kwargs.get("disabled_tools") or [])
return ("manage_notes: saved", {"output": "noted", "exit_code": 0})
monkeypatch.setattr(agent_loop, "stream_llm_with_fallback", fake_stream)
monkeypatch.setattr(agent_loop, "_resolve_tool_blocks", fake_resolve)
monkeypatch.setattr(agent_loop, "execute_tool_block", fake_execute)
_collect(
agent_loop.stream_agent_loop(
"https://api.example/v1",
"gpt-4o",
[{"role": "user", "content": "Add buy milk to my notes."}],
max_rounds=2,
relevant_tools={"manage_notes", "manage_calendar", "manage_tasks", "bash"},
disabled_tools={"manage_notes", "manage_calendar", "manage_tasks"},
fallbacks=[("https://qwen.example/v1", ODY_QWEN, {})],
_is_teacher_run=True,
)
)
assert seen_exec["disabled_tools"].isdisjoint(
{"manage_notes", "manage_calendar", "manage_tasks"}
)
def test_persisted_mcp_tool_event_keeps_description_and_resolved_name(monkeypatch):
_install_route_probe(monkeypatch)
stream_round = 0
resolve_round = 0
async def fake_stream(_candidates, _messages, **kwargs):
nonlocal stream_round
stream_round += 1
if stream_round == 1:
yield 'data: {"delta": "Calling calendar."}\n\n'
else:
yield 'data: {"delta": "Finished."}\n\n'
yield "data: [DONE]\n\n"
def fake_resolve(*args, **kwargs):
nonlocal resolve_round
resolve_round += 1
if resolve_round == 1:
return ([agent_loop.ToolBlock("mcp", "{}")], False, [])
return ([], False, [])
async def fake_execute(block, *args, **kwargs):
assert block.tool_type == "mcp"
return (
"mcp__calendar__create_event: created team sync",
{"output": "Created event evt-456.", "exit_code": 0},
)
monkeypatch.setattr(agent_loop, "stream_llm_with_fallback", fake_stream)
monkeypatch.setattr(agent_loop, "_resolve_tool_blocks", fake_resolve)
monkeypatch.setattr(agent_loop, "execute_tool_block", fake_execute)
chunks = _collect(
agent_loop.stream_agent_loop(
"https://api.example/v1",
"gpt-4o",
[{"role": "user", "content": "Create the team sync event."}],
max_rounds=2,
relevant_tools={"mcp"},
_is_teacher_run=True,
)
)
metrics = next(
event["data"] for event in _events(chunks) if event.get("type") == "metrics"
)
persisted = metrics["tool_events"][0]
assert persisted["tool"] == "mcp__calendar__create_event"
assert persisted["desc"] == "mcp__calendar__create_event: created team sync"
+23 -1
View File
@@ -17,4 +17,26 @@ def test_load_keeps_object_prefs_file(tmp_path, monkeypatch):
prefs_file.write_text(json.dumps({"theme": "dark"}), encoding="utf-8")
monkeypatch.setattr(prefs_routes, "PREFS_FILE", str(prefs_file))
assert prefs_routes._load_for_user("alice") == {"theme": "dark"}
assert prefs_routes._load_for_user(None) == {"theme": "dark"}
assert prefs_routes._load_for_user("alice") == {}
def test_named_preference_write_does_not_copy_flat_fallback_consent(tmp_path, monkeypatch):
prefs_file = tmp_path / "user_prefs.json"
prefs_file.write_text(json.dumps({
"theme": "light",
"foreground_fallback_enabled": True,
"foreground_model_fallbacks": [
{"endpoint_id": "legacy-single-user", "model": "legacy-model"},
],
}), encoding="utf-8")
monkeypatch.setattr(prefs_routes, "PREFS_FILE", str(prefs_file))
bob = prefs_routes._load_for_user("bob")
bob["theme"] = "dark"
prefs_routes._save_for_user("bob", bob)
raw = prefs_routes._load()
assert raw["_users"] == {"bob": {"theme": "dark"}}
assert raw["foreground_fallback_enabled"] is True
assert raw["foreground_model_fallbacks"][0]["endpoint_id"] == "legacy-single-user"
@@ -6,6 +6,9 @@ every other user's preferences (a realistic ops transition: auth turned off
on a deployment that previously ran multi-user). It must preserve the other
users and round-trip the change into the same (first) slot _load_for_user
reads from.
Foreground fallback keys are the exception: auth-disabled consent is stored
at the flat root so it can never become consent for the first named owner.
"""
import json
@@ -51,3 +54,58 @@ def test_named_user_save_unaffected(tmp_path, monkeypatch):
data = json.loads(f.read_text())
assert data["_users"]["alice"] == {"theme": "light"}
assert data["_users"]["bob"] == {"theme": "dark"}
def test_auth_disabled_fallback_consent_does_not_mutate_first_named_user(
tmp_path,
monkeypatch,
):
f = tmp_path / "user_prefs.json"
f.write_text(json.dumps({"_users": {
"alice": {"theme": "light"},
"bob": {"theme": "paper"},
}}), encoding="utf-8")
monkeypatch.setattr(pr, "PREFS_FILE", str(f))
current = pr._load_for_user(None)
current["foreground_fallback_enabled"] = True
current["foreground_model_fallbacks"] = [
{"endpoint_id": "single-user", "model": "single-model"},
]
pr._save_for_user(None, current)
data = json.loads(f.read_text(encoding="utf-8"))
assert data["foreground_fallback_enabled"] is True
assert data["foreground_model_fallbacks"][0]["endpoint_id"] == "single-user"
assert data["_users"]["alice"] == {"theme": "light"}
assert data["_users"]["bob"] == {"theme": "paper"}
def test_auth_disabled_save_preserves_named_fallback_consent(tmp_path, monkeypatch):
f = tmp_path / "user_prefs.json"
alice_fallbacks = [{"endpoint_id": "alice", "model": "alice-model"}]
f.write_text(json.dumps({"_users": {
"alice": {
"theme": "light",
"foreground_fallback_enabled": True,
"foreground_model_fallbacks": alice_fallbacks,
},
}}), encoding="utf-8")
monkeypatch.setattr(pr, "PREFS_FILE", str(f))
current = pr._load_for_user(None)
assert "foreground_fallback_enabled" not in current
assert "foreground_model_fallbacks" not in current
current["theme"] = "dark"
current["foreground_fallback_enabled"] = False
current["foreground_model_fallbacks"] = []
pr._save_for_user(None, current)
data = json.loads(f.read_text(encoding="utf-8"))
assert data["foreground_fallback_enabled"] is False
assert data["foreground_model_fallbacks"] == []
assert data["_users"]["alice"] == {
"theme": "dark",
"foreground_fallback_enabled": True,
"foreground_model_fallbacks": alice_fallbacks,
}
+150 -1
View File
@@ -3,8 +3,16 @@
import json
from types import SimpleNamespace
import pytest
import src.endpoint_resolver as endpoint_resolver
from src.endpoint_resolver import resolve_endpoint
from src.endpoint_resolver import (
endpoint_cost_tracked,
resolve_endpoint,
resolve_endpoint_by_id,
resolve_fallback_entries,
resolve_fallback_entries_with_descriptors,
)
class _FakeColumn:
@@ -34,6 +42,9 @@ class _FakeQuery:
def first(self):
return self.rows[0] if self.rows else None
def all(self):
return list(self.rows)
class _FakeDb:
def __init__(self, rows):
@@ -49,6 +60,7 @@ class _FakeDb:
def _endpoint(ep_id, model, *, hidden=None):
return SimpleNamespace(
id=ep_id,
name=f"Endpoint {ep_id}",
base_url=f"https://{ep_id}.example/v1",
api_key=f"key-{ep_id}",
cached_models=json.dumps([model]),
@@ -191,3 +203,140 @@ def test_hidden_configured_model_selects_first_enabled_chat_model(monkeypatch):
assert url == "https://default.example/v1/chat/completions"
assert model == "enabled-chat"
assert headers == {"Authorization": "Bearer key-default"}
def test_exact_fallback_drops_hidden_model_instead_of_substituting(monkeypatch):
endpoint = SimpleNamespace(
id="fallback",
base_url="https://fallback.example/v1",
api_key="key-fallback",
cached_models=json.dumps(["chosen-hidden", "different-live"]),
hidden_models=json.dumps(["chosen-hidden"]),
is_enabled=True,
)
_install_resolver_fakes(monkeypatch, {}, [endpoint])
assert resolve_endpoint_by_id(
"fallback",
"chosen-hidden",
require_exact_model=True,
) is None
assert resolve_endpoint_by_id("fallback", "chosen-hidden")[1] == "different-live"
def test_exact_fallback_drops_known_missing_model(monkeypatch):
_install_resolver_fakes(monkeypatch, {}, [_endpoint("fallback", "known-live")])
assert resolve_endpoint_by_id(
"fallback",
"unlisted-model",
require_exact_model=True,
) is None
def test_fallback_entry_resolution_preserves_credential_distinct_endpoints(monkeypatch):
seen = []
def fake_resolve(ep_id, model, owner=None, *, require_exact_model=False):
seen.append((ep_id, model, owner, require_exact_model))
return (
"https://provider.example/v1/chat/completions",
model,
{"Authorization": f"Bearer {ep_id}"},
)
monkeypatch.setattr(endpoint_resolver, "resolve_endpoint_by_id", fake_resolve)
entries = [
{"endpoint_id": "key-one", "model": "same-model"},
{"endpoint_id": "key-two", "model": "same-model"},
]
assert resolve_fallback_entries(
entries,
owner="alice",
require_exact_model=True,
) == [
("https://provider.example/v1/chat/completions", "same-model", {"Authorization": "Bearer key-one"}),
("https://provider.example/v1/chat/completions", "same-model", {"Authorization": "Bearer key-two"}),
]
assert seen == [
("key-one", "same-model", "alice", True),
("key-two", "same-model", "alice", True),
]
def test_descriptor_resolution_preserves_safe_endpoint_identity(monkeypatch):
_install_resolver_fakes(monkeypatch, {}, [_endpoint("backup", "backup-model")])
routes = resolve_fallback_entries_with_descriptors(
[{"endpoint_id": "backup", "model": "backup-model"}],
require_exact_model=True,
)
assert routes == [(
(
"https://backup.example/v1/chat/completions",
"backup-model",
{"Authorization": "Bearer key-backup"},
),
{
"endpoint_id": "backup",
"endpoint_label": "Endpoint backup",
"endpoint_cost_tracked": True,
},
)]
def test_exact_id_descriptor_wins_when_routes_are_identical(monkeypatch):
first = _endpoint("account-one", "same-model")
second = _endpoint("account-two", "same-model")
for endpoint in (first, second):
endpoint.base_url = "https://provider.example/v1"
endpoint.api_key = "shared-key"
_install_resolver_fakes(monkeypatch, {}, [first, second])
import src.auth_helpers as auth_helpers
seen_owners = []
def scoped(query, model_cls, owner, *, include_shared=True):
seen_owners.append(owner)
return query
monkeypatch.setattr(auth_helpers, "owner_filter", scoped)
resolver = getattr(endpoint_resolver, "resolve_route_descriptor_by_id", None)
assert resolver is not None
assert resolver(
"account-two",
"https://provider.example/v1/chat/completions",
"same-model",
{"Authorization": "Bearer shared-key"},
owner="alice",
) == {
"endpoint_id": "account-two",
"endpoint_label": "Endpoint account-two",
"endpoint_cost_tracked": True,
}
assert seen_owners == ["alice"]
def test_endpoint_cost_tracking_is_non_secret_route_classification():
assert endpoint_cost_tracked("http://localhost:11434/v1") is False
assert endpoint_cost_tracked("http://model-service:8000/v1") is False
assert endpoint_cost_tracked("http://192.168.1.20:8000/v1") is False
assert endpoint_cost_tracked("https://chatgpt.com/backend-api/codex") is False
assert endpoint_cost_tracked("https://api.example.com/v1") is True
assert endpoint_cost_tracked("http://192.168.1.20:8000/v1", "api") is True
assert endpoint_cost_tracked("https://api.example.com/v1", "local") is False
@pytest.mark.parametrize(
("url", "expected"),
[
("https://[2606:4700:4700::1111]/v1", True),
("http://169.254.10.20:8000/v1", False),
],
)
def test_endpoint_cost_tracking_classifies_public_ipv6_and_link_local_ipv4(url, expected):
assert endpoint_cost_tracked(url) is expected
@@ -163,53 +163,3 @@ def test_chatgpt_subscription_clears_previously_persisted_bearer(monkeypatch):
)
finally:
db.close()
def test_chatgpt_subscription_fallback_auth_is_not_written_to_sessions_table(monkeypatch):
"""Fallback endpoint selection must keep the resolved bearer request-local."""
TestSessionLocal = _mem_db(monkeypatch)
db = TestSessionLocal()
try:
db.add(ModelEndpoint(
id="ep1", name="ChatGPT Subscription", base_url=_CODEX_BASE,
provider_auth_id="auth1", owner="alice", is_enabled=True, api_key=None,
cached_models='["gpt-5.1-codex"]',
))
db.add(DbSession(
id="sess1", name="chat", endpoint_url="https://old.example/v1",
model="old-model", owner="alice", headers={},
))
db.commit()
finally:
db.close()
monkeypatch.setattr(
endpoint_resolver,
"resolve_endpoint_runtime",
lambda ep, owner=None: (_CODEX_BASE, "live-access-token"),
)
sess = types.SimpleNamespace(
id="sess1", endpoint_url="https://old.example/v1", model="old-model",
owner="alice", headers={},
)
result = chat_helpers.try_fallback_endpoint(sess, "sess1")
assert result == {
"model": "gpt-5.1-codex",
"endpoint_url": _CODEX_BASE + "/responses",
"endpoint_name": "ChatGPT Subscription",
}
assert sess.headers["Authorization"] == "Bearer live-access-token"
db = TestSessionLocal()
try:
row = db.query(DbSession).filter(DbSession.id == "sess1").first()
assert row.model == "gpt-5.1-codex"
assert row.endpoint_url == _CODEX_BASE + "/responses"
stored = row.headers or {}
assert not any(k.lower() == "authorization" for k in stored), (
f"ChatGPT fallback bearer leaked into sessions table: {stored}"
)
finally:
db.close()
+119
View File
@@ -0,0 +1,119 @@
"""Retired settings stay stored but cannot leak through generic interfaces."""
import asyncio
import json
from types import SimpleNamespace
import pytest
import core.database as database
import routes.auth_routes as auth_routes
import src.settings as settings_mod
from src.agent_tools.admin_tools import do_manage_settings
LEGACY_VALUE = [
{"endpoint_id": "private-endpoint-id", "model": "private-model-name"},
]
class _AuthManager:
def get_username_for_token(self, token):
return "admin" if token == "admin-session" else None
def is_admin(self, username):
return username == "admin"
class _Request(SimpleNamespace):
def __init__(self, body=None, *, admin=False):
super().__init__(
cookies={
auth_routes.SESSION_COOKIE: "admin-session"
} if admin else {},
_body=body,
)
async def json(self):
return self._body
def _route(router, path, method):
return next(
route.endpoint
for route in router.routes
if route.path == path and method in route.methods
)
@pytest.mark.asyncio
async def test_generic_settings_hide_and_preserve_retired_fallbacks(monkeypatch):
store = {
**settings_mod.DEFAULT_SETTINGS,
"default_model_fallbacks": list(LEGACY_VALUE),
"tts_enabled": True,
}
monkeypatch.setattr(auth_routes, "migrate_from_settings", lambda: None)
monkeypatch.setattr(auth_routes, "_load_settings", lambda: dict(store))
def save_settings(updated):
store.clear()
store.update(updated)
monkeypatch.setattr(auth_routes, "_save_settings", save_settings)
router = auth_routes.setup_auth_routes(_AuthManager())
get_settings = _route(router, "/api/auth/settings", "GET")
set_settings = _route(router, "/api/auth/settings", "POST")
anonymous = await get_settings(_Request())
admin = await get_settings(_Request(admin=True))
assert "default_model_fallbacks" not in anonymous
assert "default_model_fallbacks" not in admin
assert store["default_model_fallbacks"] == LEGACY_VALUE
response = await set_settings(_Request({
"default_model_fallbacks": [],
"tts_enabled": False,
}, admin=True))
assert "default_model_fallbacks" not in response
assert store["default_model_fallbacks"] == LEGACY_VALUE
assert store["tts_enabled"] is False
def test_manage_settings_tombstones_legacy_fallback_key(monkeypatch):
store = {
**settings_mod.DEFAULT_SETTINGS,
"default_model_fallbacks": list(LEGACY_VALUE),
}
save_calls = []
class _Db:
def close(self):
return None
monkeypatch.setattr(database, "SessionLocal", lambda: _Db())
monkeypatch.setattr(settings_mod, "load_settings", lambda: dict(store))
def save_settings(updated):
save_calls.append(dict(updated))
store.clear()
store.update(updated)
monkeypatch.setattr(settings_mod, "save_settings", save_settings)
listed = asyncio.run(do_manage_settings(json.dumps({"action": "list"})))
assert "default_model_fallbacks" not in listed["settings"]
for action in ("get", "set", "reset", "delete"):
payload = {"action": action, "key": "default_model_fallbacks"}
if action == "set":
payload["value"] = []
result = asyncio.run(do_manage_settings(json.dumps(payload)))
assert result["exit_code"] == 1
assert "Unknown setting" in result["error"]
assert save_calls == []
assert store["default_model_fallbacks"] == LEGACY_VALUE
+62 -1
View File
@@ -6,8 +6,15 @@ Verifies two critical cases:
2. api.deepseek.com must still be treated as tool-capable via the host
allow-list (_API_HOSTS), so cloud deepseek users keep working.
"""
from types import SimpleNamespace
import pytest
from src.agent_loop import _API_HOSTS, _endpoint_lookup_keys, _is_ollama_openai_compat_url
from src.agent_loop import (
_API_HOSTS,
_agent_route_tool_mode,
_endpoint_lookup_keys,
_is_ollama_openai_compat_url,
)
from src.llm_core import _is_ollama_native_url
@@ -164,3 +171,57 @@ class TestEndpointLookupKeys:
keys = _endpoint_lookup_keys("http://host.docker.internal:11434/api/chat")
assert "http://host.docker.internal:11434/api" in keys
def test_route_tool_mode_matches_credential_distinct_endpoint(monkeypatch):
from core import database
from src import endpoint_resolver
rows = [
SimpleNamespace(
id="one",
base_url="https://same.example/v1",
api_key="key-one",
provider_auth_id=None,
supports_tools=True,
),
SimpleNamespace(
id="two",
base_url="https://same.example/v1",
api_key="key-two",
provider_auth_id=None,
supports_tools=False,
),
]
class Query:
def filter(self, *args, **kwargs):
return self
def all(self):
return rows
class Db:
def query(self, *args, **kwargs):
return Query()
def close(self):
return None
monkeypatch.setattr(database, "SessionLocal", lambda: Db())
monkeypatch.setattr(
endpoint_resolver,
"resolve_endpoint_runtime",
lambda endpoint, owner=None: (endpoint.base_url, endpoint.api_key),
)
assert _agent_route_tool_mode(
"https://same.example/v1",
"custom-model",
headers={"Authorization": "Bearer key-one"},
)[0] is True
assert _agent_route_tool_mode(
"https://same.example/v1",
"custom-model",
headers={"Authorization": "Bearer key-two"},
)[0] is False
+25
View File
@@ -117,6 +117,31 @@ def test_agent_system_prompt_includes_shared_current_time(monkeypatch):
assert "Australia/Brisbane, UTC+10:00" in datetime_messages[0]["content"]
def test_route_prompt_rebuild_restores_leading_user_system_message(monkeypatch):
import src.agent_loop as agent_loop
monkeypatch.setattr(agent_loop, "_build_base_prompt", lambda *args, **kwargs: ("AGENT PROMPT", ""))
monkeypatch.setattr(agent_loop, "set_active_model", lambda model: None)
monkeypatch.setattr(agent_loop, "get_builtin_overrides", lambda: {})
monkeypatch.setattr(agent_loop, "_cached_base_prompt", None)
monkeypatch.setattr(agent_loop, "_cached_base_prompt_key", None)
original = [
{"role": "system", "content": "USER PERSONA"},
{"role": "user", "content": "hello"},
]
built, _ = agent_loop._build_system_prompt(
original,
model="selected-model",
active_document=None,
mcp_mgr=None,
)
assert built[0]["content"] == "USER PERSONA\n\nAGENT PROMPT"
assert built[0]["_agent_injected"] == "merged_prompt"
assert agent_loop._strip_agent_injected_messages(built) == original
def test_calendar_relative_time_parser_handles_dotted_pm(monkeypatch):
import routes.calendar_routes as calendar_routes