mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-17 18:35:19 +02:00
hitl/wire: rename 'always' decision-type to 'approve_always'
Renames the SurfSense HITL extension decision-type from "always" to "approve_always" so it sits in the same verb-first family as "approve", "reject", and "edit". The Python constant is now SURFSENSE_DECISION_APPROVE_ALWAYS; the wire value, the permission-domain decision_type, and the FE union members all match (no wire/internal mismatch). Both the multi_agent_chat permission middleware and the legacy new_chat one accept the new wire value; the FE types.ts union is updated accordingly. The "context.always" payload key is intentionally left untouched - it's the patterns-to-promote field, semantically distinct from the decision type.
This commit is contained in:
parent
6671c91841
commit
c8b756ae8f
16 changed files with 85 additions and 75 deletions
|
|
@ -225,7 +225,7 @@ async def test_parallel_self_gated_and_middleware_gated_route_and_resume_cleanly
|
|||
|
||||
# Resume order: same order the SSE stream would emit (interrupts list).
|
||||
decision_self = {"type": "approve"}
|
||||
decision_mw = {"type": "always"}
|
||||
decision_mw = {"type": "approve_always"}
|
||||
flat_decisions = [
|
||||
# Match `pending` order.
|
||||
decision_self if pending[0][0] == tcid_self else decision_mw,
|
||||
|
|
@ -268,5 +268,5 @@ async def test_parallel_self_gated_and_middleware_gated_route_and_resume_cleanly
|
|||
assert self_payloads[0]["decision_type"] == "approve"
|
||||
assert self_payloads[0]["rejected"] is False
|
||||
|
||||
# Middleware-gated always → canonical permission shape with always.
|
||||
assert mw_payloads[0]["decision"] == {"decision_type": "always"}
|
||||
# Middleware-gated approve_always → canonical permission shape unchanged.
|
||||
assert mw_payloads[0]["decision"] == {"decision_type": "approve_always"}
|
||||
|
|
|
|||
|
|
@ -64,8 +64,8 @@ async def test_permission_ask_payload_uses_lc_hitl_shape():
|
|||
], f"REGRESSION: permission ask reverted to legacy shape; got {value!r}"
|
||||
review = value.get("review_configs")
|
||||
assert isinstance(review, list) and len(review) == 1
|
||||
# ``always`` must be in the palette so the FE can render the promote button.
|
||||
assert "always" in review[0]["allowed_decisions"]
|
||||
# ``approve_always`` must be in the palette so the FE can render the promote button.
|
||||
assert "approve_always" in review[0]["allowed_decisions"]
|
||||
assert value.get("interrupt_type") == "permission_ask"
|
||||
# SurfSense context rides through verbatim for FE explainability.
|
||||
assert value["context"]["patterns"] == ["rm/*"]
|
||||
|
|
@ -88,18 +88,18 @@ async def test_resume_with_approve_envelope_returns_once_decision():
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resume_with_always_envelope_projects_to_always():
|
||||
"""``always`` reply must project unchanged so the middleware can promote the rule."""
|
||||
async def test_resume_with_approve_always_envelope_projects_unchanged():
|
||||
"""``approve_always`` reply must project unchanged so the middleware can promote the rule."""
|
||||
checkpointer = InMemorySaver()
|
||||
graph = _build_graph_calling_request_permission_decision(checkpointer)
|
||||
config = {"configurable": {"thread_id": "perm-always"}}
|
||||
config = {"configurable": {"thread_id": "perm-approve-always"}}
|
||||
await graph.ainvoke({"messages": [HumanMessage(content="seed")]}, config)
|
||||
|
||||
await graph.ainvoke(
|
||||
Command(resume={"decisions": [{"type": "always"}]}), config
|
||||
Command(resume={"decisions": [{"type": "approve_always"}]}), config
|
||||
)
|
||||
final = await graph.aget_state(config)
|
||||
assert final.values.get("final_decision") == {"decision_type": "always"}
|
||||
assert final.values.get("final_decision") == {"decision_type": "approve_always"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""``always`` decisions for MCP tools are saved via the trusted-tool saver."""
|
||||
"""``approve_always`` decisions for MCP tools are saved via the trusted-tool saver."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -90,7 +90,7 @@ def _build_graph(pm, tool_name: str):
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_always_decision_saves_mcp_tool_via_callback():
|
||||
async def test_approve_always_decision_saves_mcp_tool_via_callback():
|
||||
saved: list[tuple[int, str]] = []
|
||||
|
||||
async def trusted_tool_saver(connector_id: int, tool_name: str) -> None:
|
||||
|
|
@ -106,9 +106,11 @@ async def test_always_decision_saves_mcp_tool_via_callback():
|
|||
assert pm is not None
|
||||
|
||||
graph = _build_graph(pm, tool.name)
|
||||
config = {"configurable": {"thread_id": "always-mcp"}}
|
||||
config = {"configurable": {"thread_id": "approve-always-mcp"}}
|
||||
await graph.ainvoke({"messages": [HumanMessage(content="seed")]}, config)
|
||||
await graph.ainvoke(Command(resume={"decisions": [{"type": "always"}]}), config)
|
||||
await graph.ainvoke(
|
||||
Command(resume={"decisions": [{"type": "approve_always"}]}), config
|
||||
)
|
||||
|
||||
assert saved == [(7, "linear_create_issue")]
|
||||
|
||||
|
|
@ -138,7 +140,7 @@ async def test_once_decision_does_not_save():
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_always_decision_for_native_tool_skips_save():
|
||||
async def test_approve_always_decision_for_native_tool_skips_save():
|
||||
"""Native tools have no ``mcp_connector_id`` so there is nowhere to persist trust."""
|
||||
saved: list[tuple[int, str]] = []
|
||||
|
||||
|
|
@ -155,15 +157,17 @@ async def test_always_decision_for_native_tool_skips_save():
|
|||
assert pm is not None
|
||||
|
||||
graph = _build_graph(pm, tool.name)
|
||||
config = {"configurable": {"thread_id": "always-native"}}
|
||||
config = {"configurable": {"thread_id": "approve-always-native"}}
|
||||
await graph.ainvoke({"messages": [HumanMessage(content="seed")]}, config)
|
||||
await graph.ainvoke(Command(resume={"decisions": [{"type": "always"}]}), config)
|
||||
await graph.ainvoke(
|
||||
Command(resume={"decisions": [{"type": "approve_always"}]}), config
|
||||
)
|
||||
|
||||
assert saved == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_always_decision_with_no_saver_callback_is_a_noop():
|
||||
async def test_approve_always_decision_with_no_saver_callback_is_a_noop():
|
||||
"""Anonymous turns build the middleware without a ``trusted_tool_saver``; must not crash."""
|
||||
tool = _make_mcp_tool(name="linear_create_issue", connector_id=7)
|
||||
pm = build_permission_mw(
|
||||
|
|
@ -175,6 +179,8 @@ async def test_always_decision_with_no_saver_callback_is_a_noop():
|
|||
assert pm is not None
|
||||
|
||||
graph = _build_graph(pm, tool.name)
|
||||
config = {"configurable": {"thread_id": "anon-always"}}
|
||||
config = {"configurable": {"thread_id": "anon-approve-always"}}
|
||||
await graph.ainvoke({"messages": [HumanMessage(content="seed")]}, config)
|
||||
await graph.ainvoke(Command(resume={"decisions": [{"type": "always"}]}), config)
|
||||
await graph.ainvoke(
|
||||
Command(resume={"decisions": [{"type": "approve_always"}]}), config
|
||||
)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ from app.agents.multi_agent_chat.subagents.shared.hitl.wire import (
|
|||
LC_DECISION_APPROVE,
|
||||
LC_DECISION_EDIT,
|
||||
LC_DECISION_REJECT,
|
||||
SURFSENSE_DECISION_ALWAYS,
|
||||
SURFSENSE_DECISION_APPROVE_ALWAYS,
|
||||
build_lc_hitl_payload,
|
||||
parse_lc_envelope,
|
||||
)
|
||||
|
|
@ -83,7 +83,7 @@ class TestBuildLcHitlPayload:
|
|||
allowed_decisions=[
|
||||
LC_DECISION_APPROVE,
|
||||
LC_DECISION_REJECT,
|
||||
SURFSENSE_DECISION_ALWAYS,
|
||||
SURFSENSE_DECISION_APPROVE_ALWAYS,
|
||||
],
|
||||
interrupt_type="permission_ask",
|
||||
context=ctx,
|
||||
|
|
@ -111,7 +111,7 @@ class TestParseLcEnvelope:
|
|||
assert parsed.message is None
|
||||
|
||||
def test_bare_scalar_string_passes_through_lowercased(self):
|
||||
assert parse_lc_envelope("ALWAYS").decision_type == "always"
|
||||
assert parse_lc_envelope("APPROVE_ALWAYS").decision_type == "approve_always"
|
||||
assert parse_lc_envelope("once").decision_type == "once"
|
||||
|
||||
def test_non_dict_non_string_collapses_to_reject(self):
|
||||
|
|
|
|||
|
|
@ -106,9 +106,9 @@ class TestAsk:
|
|||
# No new rule persisted
|
||||
assert mw._runtime_ruleset.rules == []
|
||||
|
||||
def test_always_persists_runtime_rule(self) -> None:
|
||||
def test_approve_always_persists_runtime_rule(self) -> None:
|
||||
mw = PermissionMiddleware(rulesets=[])
|
||||
mw._raise_interrupt = lambda **kw: {"decision_type": "always"} # type: ignore[assignment]
|
||||
mw._raise_interrupt = lambda **kw: {"decision_type": "approve_always"} # type: ignore[assignment]
|
||||
state = {"messages": [_msg({"name": "send_email", "args": {}, "id": "1"})]}
|
||||
out = mw.after_model(state, _FakeRuntime())
|
||||
assert out is None # call kept
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue