Files
hermes-webui/tests/test_goal_command_webui.py
T
nesquena-hermes dc9911739d test(goal): assert /goal membership, not the exact passthrough list literal
#6962 adds 'stop' to the busy-command passthrough list in messages.js so a busy
/stop cancels immediately. test_frontend_has_goal_slash_command_and_status_event_handler
asserted the exact list literal, so it failed on a change that does not affect /goal.

Assert that 'goal' is a member of the list instead. Verified the assertion still
fails when 'goal' is actually removed from the list.
2026-09-20 12:55:11 +00:00

1072 lines
41 KiB
Python

"""Regression tests for first-class WebUI /goal command parity."""
import io
import json
import re
import threading
from pathlib import Path
from types import SimpleNamespace
import pytest
REPO_ROOT = Path(__file__).resolve().parents[1]
COMMANDS_JS = (REPO_ROOT / "static" / "commands.js").read_text(encoding="utf-8")
MESSAGES_JS = (REPO_ROOT / "static" / "messages.js").read_text(encoding="utf-8")
ROUTES_PY = (REPO_ROOT / "api" / "routes.py").read_text(encoding="utf-8")
STREAMING_PY = (REPO_ROOT / "api" / "streaming.py").read_text(encoding="utf-8")
def test_goal_command_payload_matches_gateway_controls(monkeypatch):
"""The backend command helper mirrors gateway /goal status/pause/resume/clear/set."""
from api import goals as webui_goals
calls = []
class FakeState:
goal = "ship the feature"
status = "active"
turns_used = 0
max_turns = 20
last_verdict = None
last_reason = None
paused_reason = None
class FakeGoalManager:
def __init__(self, session_id, default_max_turns=20):
calls.append(("init", session_id, default_max_turns))
self.state = None
def status_line(self):
return "No active goal. Set one with /goal <text>."
def pause(self, reason="user-paused"):
calls.append(("pause", reason))
return FakeState()
def resume(self, reset_budget=True):
calls.append(("resume", reset_budget))
return FakeState()
def has_goal(self):
return True
def clear(self):
calls.append(("clear",))
def set(self, goal):
calls.append(("set", goal))
state = FakeState()
state.goal = goal
self.state = state
return state
monkeypatch.setattr(webui_goals, "GoalManager", FakeGoalManager)
monkeypatch.setattr(webui_goals, "_default_max_turns", lambda: 20)
status = webui_goals.goal_command_payload("sid-123", "status")
pause = webui_goals.goal_command_payload("sid-123", "pause")
resume = webui_goals.goal_command_payload("sid-123", "resume")
clear = webui_goals.goal_command_payload("sid-123", "clear")
set_goal = webui_goals.goal_command_payload("sid-123", "ship the feature")
assert status["message"] == "No active goal. Set one with /goal <text>."
assert status["message_key"] == "goal_status_none"
assert pause["message"] == "⏸ Goal paused: ship the feature"
assert pause["message_key"] == "goal_paused"
assert pause["message_args"] == ["ship the feature"]
assert resume["message"].startswith("▶ Goal resumed: ship the feature")
assert resume["message_key"] == "goal_resumed"
assert resume["message_args"] == ["ship the feature"]
assert clear["message"] == "Goal cleared."
assert clear["message_key"] == "goal_cleared"
assert set_goal["action"] == "set"
assert set_goal["message_key"] == "goal_set"
assert set_goal["message_args"] == [20, "ship the feature"]
assert set_goal["kickoff_prompt"] == "ship the feature"
assert "⊙ Goal set (20-turn budget): ship the feature" in set_goal["message"]
assert ("set", "ship the feature") in calls
def test_goal_command_payload_rejects_new_goal_while_stream_running(monkeypatch):
"""Status/control subcommands are safe mid-run; replacing the goal is not."""
from api import goals as webui_goals
class FakeGoalManager:
def __init__(self, session_id, default_max_turns=20):
pass
def status_line(self):
return "⊙ Goal (active, 1/20 turns): existing"
monkeypatch.setattr(webui_goals, "GoalManager", FakeGoalManager)
monkeypatch.setattr(webui_goals, "_default_max_turns", lambda: 20)
status = webui_goals.goal_command_payload("sid-123", "status", stream_running=True)
rejected = webui_goals.goal_command_payload("sid-123", "replace it", stream_running=True)
assert status["ok"] is True
assert rejected["ok"] is False
assert rejected["error"] == "agent_running"
assert "use /goal status / pause / clear mid-run" in rejected["message"]
def test_has_active_goal_reports_only_active_state(monkeypatch):
"""Streaming can avoid showing an evaluating spinner when no standing goal is active."""
from api import goals as webui_goals
class FakeGoalManager:
def __init__(self, session_id, default_max_turns=20):
self.session_id = session_id
def is_active(self):
return self.session_id == "sid-active-goal"
monkeypatch.setattr(webui_goals, "GoalManager", FakeGoalManager)
monkeypatch.setattr(webui_goals, "_default_max_turns", lambda: 20)
assert webui_goals.has_active_goal("sid-active-goal") is True
assert webui_goals.has_active_goal("sid-idle-goal") is False
assert webui_goals.has_active_goal("") is False
def test_profile_goal_evaluation_supports_native_judge_contract(monkeypatch, tmp_path):
"""Profile goals accept the current five-field native judge result."""
from api import goals as webui_goals
native_goals = pytest.importorskip("hermes_cli.goals", reason="hermes-agent not installed")
judge = lambda *args, **kwargs: ("continue", "work remains", False, None, False)
monkeypatch.setattr(webui_goals, "judge_goal", judge)
monkeypatch.setattr(native_goals, "judge_goal", judge)
monkeypatch.syspath_prepend(str(Path(native_goals.__file__).resolve().parents[1]))
from hermes_constants import get_hermes_home
original_home = get_hermes_home()
webui_goals._DB_CACHE.clear()
native_goals._DB_CACHE.clear()
__import__("hermes_state")
home = tmp_path / "profile"
webui_goals.goal_command_payload("sid-native-contract", "finish it", profile_home=home)
decision = webui_goals.evaluate_goal_after_turn(
"sid-native-contract",
"not finished",
profile_home=home,
)
state = webui_goals.goal_state_snapshot("sid-native-contract", profile_home=home)
assert decision["verdict"] == "continue"
assert decision["should_continue"] is True
assert state.turns_used == 1
assert state.last_verdict == "continue"
snapshot = webui_goals.goal_state_snapshot("sid-native-contract", profile_home=home)
webui_goals.goal_command_payload("sid-native-contract", "replacement", profile_home=home)
webui_goals.restore_goal_state("sid-native-contract", snapshot, profile_home=home)
restored = webui_goals.goal_state_snapshot("sid-native-contract", profile_home=home)
assert restored.goal == "finish it"
assert restored.turns_used == 1
assert get_hermes_home() == original_home
def test_profile_goal_evaluation_preserves_native_wait_semantics(monkeypatch, tmp_path):
"""Profile goals park when the native judge returns a wait directive."""
from api import goals as webui_goals
native_goals = pytest.importorskip("hermes_cli.goals", reason="hermes-agent not installed")
judge = lambda *args, **kwargs: (
"wait",
"rate limited",
False,
{"seconds": 30},
False,
)
monkeypatch.setattr(webui_goals, "judge_goal", judge)
monkeypatch.setattr(native_goals, "judge_goal", judge)
monkeypatch.syspath_prepend(str(Path(native_goals.__file__).resolve().parents[1]))
webui_goals._DB_CACHE.clear()
native_goals._DB_CACHE.clear()
__import__("hermes_state")
home = tmp_path / "profile"
webui_goals.goal_command_payload("sid-native-wait", "finish it", profile_home=home)
decision = webui_goals.evaluate_goal_after_turn(
"sid-native-wait",
"rate limited",
profile_home=home,
)
state = webui_goals.goal_state_snapshot("sid-native-wait", profile_home=home)
assert decision["verdict"] == "wait"
assert decision["should_continue"] is False
assert state.waiting_until > 0
assert state.waiting_reason == "rate limited"
def test_profile_goal_context_isolated_across_threads(monkeypatch, tmp_path):
"""Concurrent profiles with the same session id cannot cross-write goals."""
from api import goals as webui_goals
native_goals = pytest.importorskip("hermes_cli.goals", reason="hermes-agent not installed")
monkeypatch.syspath_prepend(str(Path(native_goals.__file__).resolve().parents[1]))
from hermes_constants import get_hermes_home
original_home = get_hermes_home()
webui_goals._DB_CACHE.clear()
native_goals._DB_CACHE.clear()
__import__("hermes_state")
barrier = threading.Barrier(2)
failures = []
def set_goal(profile):
try:
barrier.wait()
result = webui_goals.goal_command_payload(
"shared-session",
f"goal-{profile}",
profile_home=tmp_path / profile,
)
assert result["ok"] is True
except Exception as exc:
failures.append(exc)
threads = [threading.Thread(target=set_goal, args=(profile,)) for profile in ("a", "b")]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
assert failures == []
assert webui_goals.goal_state_snapshot(
"shared-session", profile_home=tmp_path / "a"
).goal == "goal-a"
assert webui_goals.goal_state_snapshot(
"shared-session", profile_home=tmp_path / "b"
).goal == "goal-b"
assert get_hermes_home() == original_home
def test_profile_goal_falls_back_when_session_db_path_is_frozen(monkeypatch, tmp_path):
"""A context API alone cannot make an import-time SessionDB path profile-safe."""
from api import goals as webui_goals
native_goals = pytest.importorskip("hermes_cli.goals", reason="hermes-agent not installed")
# Ensure the agent dir is on sys.path before importing the sibling top-level
# `hermes_state` module. importorskip("hermes_cli.goals") can succeed from a
# cached sys.modules entry even after a prior shard test stripped the agent
# dir from sys.path, but the bare `import hermes_state` below is uncached and
# would then fail with ModuleNotFoundError under full-suite ordering (every
# other test in this file already does this prepend).
monkeypatch.syspath_prepend(str(Path(native_goals.__file__).resolve().parents[1]))
import hermes_state
frozen_db_path = tmp_path / "frozen-home" / "state.db"
monkeypatch.setattr(hermes_state, "DEFAULT_DB_PATH", frozen_db_path)
webui_goals._DB_CACHE.clear()
native_goals._DB_CACHE.clear()
profile_a = tmp_path / "profile-a"
profile_b = tmp_path / "profile-b"
try:
assert webui_goals.goal_command_payload(
"shared-session", "goal-a", profile_home=profile_a
)["ok"] is True
assert webui_goals.goal_command_payload(
"shared-session", "goal-b", profile_home=profile_b
)["ok"] is True
assert webui_goals.goal_state_snapshot(
"shared-session", profile_home=profile_a
).goal == "goal-a"
assert webui_goals.goal_state_snapshot(
"shared-session", profile_home=profile_b
).goal == "goal-b"
finally:
for cache in (webui_goals._DB_CACHE, native_goals._DB_CACHE):
for db in cache.values():
close = getattr(db, "close", None)
if close is not None:
close()
cache.clear()
def test_profile_goal_context_resets_when_native_call_raises(monkeypatch, tmp_path):
"""A failed delegated call cannot leak its profile into the next task."""
from api import goals as webui_goals
native_goals = pytest.importorskip("hermes_cli.goals", reason="hermes-agent not installed")
monkeypatch.syspath_prepend(str(Path(native_goals.__file__).resolve().parents[1]))
from hermes_constants import (
get_hermes_home,
reset_hermes_home_override,
set_hermes_home_override,
)
class RaisingGoalManager:
state = None
def __init__(self, session_id, default_max_turns=20):
self.session_id = session_id
self.default_max_turns = default_max_turns
def explode(self):
raise RuntimeError("boom")
original_home = get_hermes_home()
monkeypatch.setattr(webui_goals, "_NativeGoalManager", RaisingGoalManager)
manager = webui_goals._ProfileGoalManager(
"sid-context-reset",
profile_home=tmp_path / "profile",
context_api=(set_hermes_home_override, reset_hermes_home_override),
)
with pytest.raises(RuntimeError, match="boom"):
manager.explode()
assert get_hermes_home() == original_home
def test_goal_continuation_decision_emits_status_and_normal_user_prompt(monkeypatch):
"""Post-turn hook returns the visible status event plus a normal continuation prompt."""
from api import goals as webui_goals
class FakeGoalManager:
def __init__(self, session_id, default_max_turns=20):
self.session_id = session_id
def is_active(self):
return True
def evaluate_after_turn(self, last_response, user_initiated=True):
return {
"status": "active",
"should_continue": True,
"continuation_prompt": "[Continuing toward your standing goal]\nGoal: ship it",
"verdict": "continue",
"reason": "one step remains",
"message": "↻ Continuing toward goal (1/20): one step remains",
}
monkeypatch.setattr(webui_goals, "GoalManager", FakeGoalManager)
monkeypatch.setattr(webui_goals, "_default_max_turns", lambda: 20)
decision = webui_goals.evaluate_goal_after_turn("sid-123", "not done yet", user_initiated=False)
assert decision["message_key"] == "goal_continuing"
assert decision["message_args"] == [1, 20, "one step remains"]
assert decision["message"].startswith("↻ Continuing toward goal")
assert decision["should_continue"] is True
assert decision["continuation_prompt"].startswith("[Continuing toward your standing goal]")
@pytest.mark.parametrize("gateway_owned", [False, True])
def test_goal_endpoint_sets_goal_and_starts_kickoff_stream(
monkeypatch, tmp_path, gateway_owned
):
"""POST /api/goal uses GoalManager state and launches the first goal turn."""
from api import goals as webui_goals
from api import routes
class FakeState:
goal = "ship the feature"
status = "active"
turns_used = 0
max_turns = 20
last_verdict = None
last_reason = None
paused_reason = None
class FakeGoalManager:
def __init__(self, session_id, default_max_turns=20):
self.session_id = session_id
self.default_max_turns = default_max_turns
def set(self, goal):
state = FakeState()
state.goal = goal
return state
class FakeSession:
session_id = "sid-goal-route"
profile = "default"
workspace = str(tmp_path)
model = "gpt-5.5"
model_provider = "openai-codex"
messages = []
context_messages = []
pending_user_message = None
active_stream_id = None
monkeypatch.setattr(webui_goals, "GoalManager", FakeGoalManager)
monkeypatch.setattr(routes, "get_session", lambda sid: FakeSession())
monkeypatch.setattr(routes, "resolve_trusted_workspace", lambda workspace, **_kw: tmp_path)
monkeypatch.setattr(
routes,
"webui_gateway_chat_enabled",
lambda _cfg: gateway_owned,
)
monkeypatch.setattr(routes, "get_config", lambda: {})
monkeypatch.setattr(
routes,
"_resolve_compatible_session_model_state",
lambda model, provider, **_: (model, provider, False),
)
started = []
def fake_start(session, **kwargs):
started.append(kwargs)
return {"stream_id": "goal-stream", "session_id": session.session_id, "pending_started_at": 123.0}
monkeypatch.setattr(routes, "_start_chat_stream_for_session", fake_start)
monkeypatch.setattr(routes, "j", lambda handler, payload, status=200, **kwargs: {"status": status, "payload": payload})
result = routes._handle_goal_command(
object(),
{
"session_id": "sid-goal-route",
"args": "ship the feature",
"workspace": str(tmp_path),
"model": "gpt-5.5",
"model_provider": "openai-codex",
},
)
assert result["status"] == 200
assert result["payload"]["action"] == "set"
assert result["payload"]["stream_id"] == "goal-stream"
assert started and started[0]["msg"] == "ship the feature"
assert started[0]["model_provider"] == "openai-codex"
assert started[0]["external_runtime_owned"] is gateway_owned
def test_goal_endpoint_preserves_response_shape_under_runtime_adapter_flag(monkeypatch, tmp_path):
"""The Slice 3c adapter path delegates /goal without adding adapter-only fields."""
from api import goals as webui_goals
from api import routes
class FakeState:
goal = "ship the feature"
status = "active"
turns_used = 1
max_turns = 20
last_verdict = None
last_reason = None
paused_reason = None
class FakeGoalManager:
def __init__(self, session_id, default_max_turns=20):
self.state = FakeState()
class FakeSession:
session_id = "sid-goal-route"
profile = "default"
workspace = str(tmp_path)
model = "gpt-5.5"
model_provider = "openai-codex"
messages = []
context_messages = []
pending_user_message = None
active_stream_id = None
monkeypatch.setenv("HERMES_WEBUI_RUNTIME_ADAPTER", "legacy-journal")
monkeypatch.setattr(webui_goals, "GoalManager", FakeGoalManager)
monkeypatch.setattr(routes, "get_session", lambda sid: FakeSession())
monkeypatch.setattr(routes, "j", lambda handler, payload, status=200, **kwargs: {"status": status, "payload": payload})
result = routes._handle_goal_command(object(), {"session_id": "sid-goal-route", "args": "status"})
assert result["status"] == 200
assert result["payload"]["action"] == "status"
assert result["payload"]["message_key"] == "goal_status_active"
assert "run_id" not in result["payload"]
assert "active_controls" not in result["payload"]
def test_goal_endpoint_adapter_keeps_full_set_text_and_legacy_payload_status(monkeypatch, tmp_path):
"""The adapter action label must not replace legacy parsing of full goal text."""
from api import goals as webui_goals
from api import routes
set_calls = []
class FakeState:
goal = ""
status = "active"
turns_used = 0
max_turns = 20
last_verdict = None
last_reason = None
paused_reason = None
class FakeGoalManager:
def __init__(self, session_id, default_max_turns=20):
self.state = FakeState()
def set(self, text):
set_calls.append(text)
self.state.goal = text
return self.state
class FakeSession:
session_id = "sid-goal-route"
profile = "default"
workspace = str(tmp_path)
model = "gpt-5.5"
model_provider = "openai-codex"
messages = []
context_messages = []
pending_user_message = None
active_stream_id = None
monkeypatch.setenv("HERMES_WEBUI_RUNTIME_ADAPTER", "legacy-journal")
monkeypatch.setattr(webui_goals, "GoalManager", FakeGoalManager)
monkeypatch.setattr(routes, "get_session", lambda sid: FakeSession())
monkeypatch.setattr(routes, "resolve_trusted_workspace", lambda workspace, **_kw: tmp_path)
monkeypatch.setattr(
routes,
"_resolve_compatible_session_model_state",
lambda model, provider, **_: (model, provider, False),
)
monkeypatch.setattr(
routes,
"_start_chat_stream_for_session",
lambda session, **kwargs: {"stream_id": "goal-stream", "session_id": session.session_id},
)
monkeypatch.setattr(routes, "j", lambda handler, payload, status=200, **kwargs: {"status": status, "payload": payload})
result = routes._handle_goal_command(object(), {"session_id": "sid-goal-route", "args": "set foo"})
assert result["status"] == 200
assert result["payload"]["action"] == "set"
assert result["payload"]["kickoff_prompt"] == "set foo"
assert set_calls == ["set foo"]
def test_goal_endpoint_adapter_error_payload_still_controls_http_status(monkeypatch, tmp_path):
"""The /goal route preserves legacy error/status handling under the adapter flag."""
from api import goals as webui_goals
from api import routes
class FakeGoalManager:
state = None
def __init__(self, session_id, default_max_turns=20):
pass
class FakeSession:
session_id = "sid-goal-route"
profile = "default"
workspace = str(tmp_path)
model = "gpt-5.5"
model_provider = "openai-codex"
messages = []
context_messages = []
pending_user_message = None
active_stream_id = "running-stream"
monkeypatch.setenv("HERMES_WEBUI_RUNTIME_ADAPTER", "legacy-journal")
monkeypatch.setattr(webui_goals, "GoalManager", FakeGoalManager)
monkeypatch.setattr(routes, "get_session", lambda sid: FakeSession())
monkeypatch.setitem(routes.STREAMS, "running-stream", {"queue": object()})
monkeypatch.setattr(routes, "j", lambda handler, payload, status=200, **kwargs: {"status": status, "payload": payload})
result = routes._handle_goal_command(object(), {"session_id": "sid-goal-route", "args": "ship it"})
assert result["status"] == 409
assert result["payload"]["ok"] is False
assert result["payload"]["error"] == "agent_running"
def test_routes_register_goal_endpoint_and_kickoff_stream():
assert 'if parsed.path == "/api/goal"' in ROUTES_PY
assert "return _handle_goal_command(handler, body)" in ROUTES_PY
assert "goal_command_payload" in ROUTES_PY
assert "kickoff_prompt" in ROUTES_PY
assert "_start_chat_stream_for_session" in ROUTES_PY
def test_chat_start_forwards_goal_related_to_gateway_worker(monkeypatch, tmp_path):
from api import routes
import api.turn_journal as turn_journal
class FakeSession:
session_id = "sid-goal-related-gateway"
active_stream_id = None
pending_started_at = 0.0
title = "Goal Chat"
profile = "default"
captured = {}
class FakeThread:
def __init__(self, *, target, args, kwargs, daemon):
captured["target"] = target
captured["args"] = args
captured["kwargs"] = kwargs
captured["daemon"] = daemon
def start(self):
captured["started"] = True
def fake_prepare(session, **kwargs):
session.pending_started_at = 123.0
session.title = "Goal Chat"
monkeypatch.setattr(routes, "_get_session_agent_lock", lambda *args, **kwargs: threading.Lock())
monkeypatch.setattr(routes, "_active_stream_blocks_chat_start", lambda *args, **kwargs: False)
monkeypatch.setattr(routes, "_active_run_stream_for_session", lambda *args, **kwargs: None)
monkeypatch.setattr(routes, "_prepare_chat_start_session_for_stream", fake_prepare)
monkeypatch.setattr(routes, "_is_hidden_empty_session", lambda *args, **kwargs: False)
monkeypatch.setattr(routes, "publish_session_list_changed", lambda *args, **kwargs: None)
monkeypatch.setattr(routes, "set_last_workspace", lambda *args, **kwargs: None)
monkeypatch.setattr(turn_journal, "append_turn_journal_event", lambda *args, **kwargs: {})
monkeypatch.setattr(routes, "webui_gateway_chat_enabled", lambda *args, **kwargs: True)
monkeypatch.setattr(routes.threading, "Thread", FakeThread)
monkeypatch.setattr(routes.uuid, "uuid4", lambda: SimpleNamespace(hex="goal-stream-id"))
response = routes._start_chat_stream_for_session(
FakeSession(),
msg="continue the goal",
attachments=[],
workspace=str(tmp_path),
model="gpt-5.5",
model_provider="openai-codex",
goal_related=True,
)
assert response["stream_id"] == "goal-stream-id"
assert captured["target"] is routes._run_gateway_chat_streaming
assert captured["kwargs"]["goal_related"] is True
assert captured["kwargs"]["model_provider"] == "openai-codex"
assert captured["started"] is True
def test_streaming_post_turn_goal_hook_surfaces_and_continues():
assert "evaluate_goal_after_turn" in STREAMING_PY
assert "put('goal'" in STREAMING_PY
assert "decision.get('should_continue')" in STREAMING_PY
assert "continuation_prompt" in STREAMING_PY
assert "put('goal_continue'" in STREAMING_PY
goal_idx = STREAMING_PY.find("evaluate_goal_after_turn")
done_idx = STREAMING_PY.find("put('done'", goal_idx)
assert goal_idx != -1 and done_idx != -1
assert goal_idx < done_idx, "goal status should be emitted before the terminal done payload"
def test_streaming_goal_hook_emits_evaluating_state_before_judge():
evaluating_idx = STREAMING_PY.find("'state': 'evaluating'")
judge_idx = STREAMING_PY.find("_goal_decision = evaluate_goal_after_turn")
done_idx = STREAMING_PY.find("put('done'", judge_idx)
assert evaluating_idx != -1, "goal hook should emit an evaluating state before judge round-trip"
assert judge_idx != -1 and done_idx != -1
assert evaluating_idx < judge_idx < done_idx
assert "Evaluating goal progress…" in STREAMING_PY
assert "'state': 'continuing' if decision.get('should_continue') else 'idle'" in STREAMING_PY
def test_frontend_has_goal_slash_command_and_status_event_handler():
assert "{name:'goal'" in COMMANDS_JS
assert "subArgs:['status','pause','resume','clear']" in COMMANDS_JS
assert "function cmdGoal" in COMMANDS_JS
assert "api('/api/goal'" in COMMANDS_JS
assert "stream_id" in COMMANDS_JS
assert "goal'" in MESSAGES_JS
assert "source.addEventListener('goal'" in MESSAGES_JS
assert "source.addEventListener('goal_continue'" in MESSAGES_JS
# #6962 added 'stop' to this busy-command passthrough list so a busy /stop
# cancels immediately. Assert /goal's membership rather than the exact list
# literal, so adding another passthrough command does not fail this test.
_passthrough = re.search(
r"if\(_pc&&\[([^\]]*)\]\.includes\(_pc\.name\)\)", MESSAGES_JS
)
assert _passthrough, "busy-command passthrough list not found in messages.js"
assert "'goal'" in _passthrough.group(1)
assert "queueSessionMessage" in MESSAGES_JS
def test_frontend_goal_evaluating_state_uses_calm_composer_indicator():
assert "const goalState=String(d.state||'').trim();" in MESSAGES_JS
assert "t('goal_evaluating_progress')" in MESSAGES_JS
assert "if(goalState==='evaluating')" in MESSAGES_JS
assert "setComposerStatus(goalEvaluatingMessage);" in MESSAGES_JS
assert "return;" in MESSAGES_JS
def test_goal_kickoff_forwards_explicit_model_pick_to_resolver(monkeypatch, tmp_path):
"""#6703: /api/goal must carry explicit_model_pick to the model resolver.
Without it a persisted cross-provider pick (e.g. provider1:model1 chosen
for the session) is treated as stale by _resolve_compatible_session_model_state
and "repaired" back to the profile default mid-session, switching the
provider while /goal runs.
"""
from api import goals as webui_goals
from api import routes
class FakeState:
goal = "ship the feature"
status = "active"
turns_used = 0
max_turns = 20
last_verdict = None
last_reason = None
paused_reason = None
class FakeGoalManager:
def __init__(self, session_id, default_max_turns=20):
self.session_id = session_id
self.default_max_turns = default_max_turns
def set(self, goal):
state = FakeState()
state.goal = goal
return state
class FakeSession:
session_id = "sid-goal-explicit"
profile = "default"
workspace = str(tmp_path)
model = "deepseek/deepseek-v4-flash"
model_provider = "deepseek"
messages = []
context_messages = []
pending_user_message = None
active_stream_id = None
resolver_kwargs = {}
def fake_resolve(model, provider, **kwargs):
resolver_kwargs.update(kwargs)
return model, provider, False
monkeypatch.setattr(webui_goals, "GoalManager", FakeGoalManager)
monkeypatch.setattr(routes, "get_session", lambda sid: FakeSession())
monkeypatch.setattr(routes, "resolve_trusted_workspace", lambda workspace, **_kw: tmp_path)
monkeypatch.setattr(routes, "webui_gateway_chat_enabled", lambda _cfg: False)
monkeypatch.setattr(routes, "get_config", lambda: {})
monkeypatch.setattr(routes, "_resolve_compatible_session_model_state", fake_resolve)
started = []
def fake_start(session, **kwargs):
started.append(kwargs)
return {"stream_id": "goal-stream", "session_id": session.session_id, "pending_started_at": 123.0}
monkeypatch.setattr(routes, "_start_chat_stream_for_session", fake_start)
monkeypatch.setattr(routes, "j", lambda handler, payload, status=200, **kwargs: {"status": status, "payload": payload})
result = routes._handle_goal_command(
object(),
{
"session_id": "sid-goal-explicit",
"args": "ship the feature",
"workspace": str(tmp_path),
"model": "deepseek/deepseek-v4-flash",
"model_provider": "deepseek",
"explicit_model_pick": True,
},
)
assert result["status"] == 200
assert started and started[0]["model"] == "deepseek/deepseek-v4-flash"
assert started[0]["model_provider"] == "deepseek"
# The explicit-pick marker must reach the resolver so the persisted
# cross-provider model is honored instead of reverted to the default.
assert resolver_kwargs.get("explicit_model_pick") is True
def test_goal_kickoff_defaults_explicit_model_pick_false(monkeypatch, tmp_path):
"""#6703: without the marker the resolver receives explicit_model_pick=False."""
from api import goals as webui_goals
from api import routes
class FakeState:
goal = "ship the feature"
status = "active"
turns_used = 0
max_turns = 20
last_verdict = None
last_reason = None
paused_reason = None
class FakeGoalManager:
def __init__(self, session_id, default_max_turns=20):
self.session_id = session_id
self.default_max_turns = default_max_turns
def set(self, goal):
state = FakeState()
state.goal = goal
return state
class FakeSession:
session_id = "sid-goal-default"
profile = "default"
workspace = str(tmp_path)
model = "gpt-5.5"
model_provider = "openai-codex"
messages = []
context_messages = []
pending_user_message = None
active_stream_id = None
resolver_kwargs = {}
def fake_resolve(model, provider, **kwargs):
resolver_kwargs.update(kwargs)
return model, provider, False
monkeypatch.setattr(webui_goals, "GoalManager", FakeGoalManager)
monkeypatch.setattr(routes, "get_session", lambda sid: FakeSession())
monkeypatch.setattr(routes, "resolve_trusted_workspace", lambda workspace, **_kw: tmp_path)
monkeypatch.setattr(routes, "webui_gateway_chat_enabled", lambda _cfg: False)
monkeypatch.setattr(routes, "get_config", lambda: {})
monkeypatch.setattr(routes, "_resolve_compatible_session_model_state", fake_resolve)
started = []
def fake_start(session, **kwargs):
started.append(kwargs)
return {"stream_id": "goal-stream", "session_id": session.session_id, "pending_started_at": 123.0}
monkeypatch.setattr(routes, "_start_chat_stream_for_session", fake_start)
monkeypatch.setattr(routes, "j", lambda handler, payload, status=200, **kwargs: {"status": status, "payload": payload})
result = routes._handle_goal_command(
object(),
{
"session_id": "sid-goal-default",
"args": "ship the feature",
"workspace": str(tmp_path),
"model": "gpt-5.5",
"model_provider": "openai-codex",
},
)
assert result["status"] == 200
assert resolver_kwargs.get("explicit_model_pick") is False
def test_frontend_goal_sends_explicit_model_pick():
"""#6703: cmdGoal must include explicit_model_pick in the /api/goal payload.
Mirrors the chat/start marker (messages.js) so the server honors a
persisted cross-provider session pick instead of reverting to the default.
"""
goal_idx = COMMANDS_JS.find("function cmdGoal")
assert goal_idx != -1
goal_fn = COMMANDS_JS[goal_idx : COMMANDS_JS.find("\n}", goal_idx)]
assert "explicit_model_pick:_explicitPick" in goal_fn
assert "api('/api/goal'" in goal_fn
assert "_isCrossProviderPick" in goal_fn
assert "_readPendingSessionModel" in goal_fn
def test_goal_kickoff_stamps_explicit_pick_signature(monkeypatch, tmp_path):
"""#6703: /api/goal must stamp model_explicit_pick_signature on explicit picks.
Parity with /api/chat/start (#5979): the streaming resolver preserves a
custom-proxy vendor namespace on a cold catalog only when the persisted
signature matches the current routing context. Without the stamp, a first
/goal launch after a deliberate custom-provider pick loses the vendor
namespace and the provider reverts to the profile default mid-session.
"""
from api import goals as webui_goals
from api import routes
class FakeState:
goal = "ship the feature"
status = "active"
turns_used = 0
max_turns = 20
last_verdict = None
last_reason = None
paused_reason = None
class FakeGoalManager:
def __init__(self, session_id, default_max_turns=20):
self.session_id = session_id
self.default_max_turns = default_max_turns
def set(self, goal):
state = FakeState()
state.goal = goal
return state
class FakeSession:
session_id = "sid-goal-sig"
profile = "default"
workspace = str(tmp_path)
model = "deepseek/deepseek-v4-flash"
model_provider = "deepseek"
messages = []
context_messages = []
pending_user_message = None
active_stream_id = None
model_explicit_pick_signature = None
def fake_resolve(model, provider, **kwargs):
return model, provider, False
monkeypatch.setattr(webui_goals, "GoalManager", FakeGoalManager)
monkeypatch.setattr(routes, "get_session", lambda sid: FakeSession())
monkeypatch.setattr(routes, "resolve_trusted_workspace", lambda workspace, **_kw: tmp_path)
monkeypatch.setattr(routes, "webui_gateway_chat_enabled", lambda _cfg: False)
monkeypatch.setattr(routes, "get_config", lambda: {})
monkeypatch.setattr(routes, "_resolve_compatible_session_model_state", fake_resolve)
def fake_start(session, **kwargs):
return {"stream_id": "goal-stream", "session_id": session.session_id, "pending_started_at": 123.0}
monkeypatch.setattr(routes, "_start_chat_stream_for_session", fake_start)
monkeypatch.setattr(routes, "j", lambda handler, payload, status=200, **kwargs: {"status": status, "payload": payload})
session = FakeSession()
monkeypatch.setattr(routes, "get_session", lambda sid: session)
result = routes._handle_goal_command(
object(),
{
"session_id": "sid-goal-sig",
"args": "ship the feature",
"workspace": str(tmp_path),
"model": "deepseek/deepseek-v4-flash",
"model_provider": "deepseek",
"explicit_model_pick": True,
},
)
assert result["status"] == 200
from api.models import model_explicit_pick_signature as _mk_sig
assert session.model_explicit_pick_signature == _mk_sig("deepseek/deepseek-v4-flash", "deepseek")
def test_goal_kickoff_does_not_stamp_signature_without_explicit_pick(monkeypatch, tmp_path):
"""#6703: without the marker no signature is stamped (parity with chat/start)."""
from api import goals as webui_goals
from api import routes
class FakeState:
goal = "ship the feature"
status = "active"
turns_used = 0
max_turns = 20
last_verdict = None
last_reason = None
paused_reason = None
class FakeGoalManager:
def __init__(self, session_id, default_max_turns=20):
self.session_id = session_id
self.default_max_turns = default_max_turns
def set(self, goal):
state = FakeState()
state.goal = goal
return state
class FakeSession:
session_id = "sid-goal-nosig"
profile = "default"
workspace = str(tmp_path)
model = "deepseek/deepseek-v4-flash"
model_provider = "deepseek"
messages = []
context_messages = []
pending_user_message = None
active_stream_id = None
model_explicit_pick_signature = "stale-previous-sig"
def fake_resolve(model, provider, **kwargs):
return model, provider, False
monkeypatch.setattr(webui_goals, "GoalManager", FakeGoalManager)
monkeypatch.setattr(routes, "resolve_trusted_workspace", lambda workspace, **_kw: tmp_path)
monkeypatch.setattr(routes, "webui_gateway_chat_enabled", lambda _cfg: False)
monkeypatch.setattr(routes, "get_config", lambda: {})
monkeypatch.setattr(routes, "_resolve_compatible_session_model_state", fake_resolve)
def fake_start(session, **kwargs):
return {"stream_id": "goal-stream", "session_id": session.session_id, "pending_started_at": 123.0}
monkeypatch.setattr(routes, "_start_chat_stream_for_session", fake_start)
monkeypatch.setattr(routes, "j", lambda handler, payload, status=200, **kwargs: {"status": status, "payload": payload})
session = FakeSession()
monkeypatch.setattr(routes, "get_session", lambda sid: session)
result = routes._handle_goal_command(
object(),
{
"session_id": "sid-goal-nosig",
"args": "ship the feature",
"workspace": str(tmp_path),
"model": "deepseek/deepseek-v4-flash",
"model_provider": "deepseek",
},
)
assert result["status"] == 200
# A non-explicit kickoff leaves any prior signature untouched (chat-start parity).
assert session.model_explicit_pick_signature == "stale-previous-sig"
def test_frontend_goal_consumes_pending_model_marker():
"""#6703/#6705: cmdGoal must consume the one-shot pending session-model marker.
Mirrors the chat/start path (messages.js) so a later /goal with an unchanged
dropdown isn't treated as an explicit pick. The consume-clear is deferred to
AFTER a successful kickoff (r.stream_id) and gated on a re-read of the stored
marker that still matches the captured model/provider (#6705), so a control
command (e.g. /goal status, which never produces a stream_id) leaves the
marker intact for the next real send.
"""
goal_idx = COMMANDS_JS.find("function cmdGoal")
assert goal_idx != -1
goal_fn = COMMANDS_JS[goal_idx : COMMANDS_JS.find("\n}", goal_idx)]
assert "_clearPendingSessionModel(activeSid)" in goal_fn
assert "_pendingPickMatch && typeof _readPendingSessionModel==='function' && typeof _clearPendingSessionModel==='function'" in goal_fn
# The consume-clear must come AFTER the kickoff guard: a control command
# (no stream_id) returns before reaching it.
assert goal_fn.index("_clearPendingSessionModel(activeSid)") > goal_fn.index("if(!r||!r.stream_id)return;")
# Re-check: the marker is re-read and only cleared while it still matches
# the captured model/provider.
assert "_stillPending.model===_goalModel" in goal_fn
assert "_stillPending.model_provider" in goal_fn
def test_frontend_goal_control_command_keeps_pending_marker():
"""#6705: a control-only /goal (e.g. `/goal status`) must NOT consume the
pending explicit-pick marker.
The server skips model resolution for control commands (api/routes.py
will_kickoff excludes status/pause/resume/clear/stop/done), so clearing the
marker before the request would drop the explicit pick without using it and
the next real send would revert to the default model. The marker must
survive the control round-trip and still be read for the next kickoff.
"""
goal_idx = COMMANDS_JS.find("function cmdGoal")
assert goal_idx != -1
goal_fn = COMMANDS_JS[goal_idx : COMMANDS_JS.find("\n}", goal_idx)]
# 1) No clear may run before the /api/goal request: the marker must
# survive a control command.
pre_request = goal_fn.split("const r=await api('/api/goal'")[0]
assert "_clearPendingSessionModel" not in pre_request, (
"the pending explicit-pick marker must NOT be cleared before /api/goal — "
"a control command like /goal status never kicks off, so a pre-request "
"clear would drop the pick without using it (#6705)"
)
# 2) The consume-clear sits after the kickoff guard (r.stream_id present).
post_request = goal_fn.split("const r=await api('/api/goal'")[1]
assert "if(!r||!r.stream_id)return;" in post_request
assert "_clearPendingSessionModel(activeSid)" in post_request
assert post_request.index("if(!r||!r.stream_id)return;") < post_request.index("_clearPendingSessionModel(activeSid)")
# 3) The marker is re-read and only cleared while it still matches the
# captured model/provider.
assert "_stillPending=_readPendingSessionModel(activeSid)" in post_request
assert re.search(r"_stillPending\.model\s*===\s*_goalModel", post_request)
assert re.search(r"_stillPending\.model_provider", post_request)
assert re.search(r"_clearPendingSessionModel\(activeSid\)", post_request)