mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-14 00:16:20 +00:00
fixes 3
This commit is contained in:
1 parent
7553e74a96
commit
33d93ba0ca
13 files changed
+612
-85
No files matched your search
@@ -703,3 +703,122 @@ def test_action_override_for_removed_action_warns(pg_conn, monkeypatch):
|
||||
assert chat_id["filled_by_llm"] is False and chat_id["value"] == "42"
|
||||
assert any("telegram_removed_action" in w for w in result["warnings"])
|
||||
assert any("gone_param" in w for w in result["warnings"])
|
||||
|
||||
|
||||
def _fake_api_tool(monkeypatch):
|
||||
"""api_tool's live surface: no declared config, no column actions."""
|
||||
fake_tool = Mock()
|
||||
fake_tool.get_config_requirements.return_value = {}
|
||||
fake_tool.get_actions_metadata.return_value = []
|
||||
monkeypatch.setattr(
|
||||
"application.api.user.agents.portability._tool_instance",
|
||||
lambda tool_type: fake_tool,
|
||||
)
|
||||
# The sanitizer SSRF-gates each action URL; DNS isn't available in tests.
|
||||
monkeypatch.setattr(
|
||||
"application.api.user.agents.portability.validate_url", lambda url: url
|
||||
)
|
||||
return fake_tool
|
||||
|
||||
|
||||
def _stored_api_tool_config():
|
||||
"""``config["actions"]`` as ToolConfig persists it (dict keyed by name)."""
|
||||
return {
|
||||
"actions": {
|
||||
"get_pet": {
|
||||
"name": "get_pet",
|
||||
"url": "https://api.example.com/pets/{petId}?api_key=QSSECRET",
|
||||
"method": "GET",
|
||||
"active": True,
|
||||
"headers": {
|
||||
"properties": {
|
||||
"Authorization": {
|
||||
"type": "string",
|
||||
"filled_by_llm": False,
|
||||
"value": "Bearer sk-live-XYZ",
|
||||
}
|
||||
}
|
||||
},
|
||||
"query_params": {
|
||||
"properties": {
|
||||
"petId": {
|
||||
"type": "string",
|
||||
"filled_by_llm": True,
|
||||
"value": "",
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test_api_tool_actions_round_trip_redacted(pg_conn, monkeypatch):
|
||||
"""api_tool actions live in config, not the actions column — they must
|
||||
travel (redacted) or the imported tool is dead (executor resolves calls
|
||||
exclusively through ``config["actions"]``)."""
|
||||
user = "u_api_tool"
|
||||
_fake_api_tool(monkeypatch)
|
||||
tool = UserToolsRepository(pg_conn).create(
|
||||
user,
|
||||
"api_tool",
|
||||
config=_stored_api_tool_config(),
|
||||
custom_name="Petstore",
|
||||
display_name="Petstore",
|
||||
description="pets",
|
||||
config_requirements={},
|
||||
actions=[],
|
||||
)
|
||||
agent = AgentsRepository(pg_conn).create(
|
||||
user, "Pet Bot", "published", description="d", tools=[str(tool["id"])]
|
||||
)
|
||||
|
||||
export = serialize_agent(pg_conn, agent, user)
|
||||
text = agent_to_yaml(export)
|
||||
assert "sk-live-XYZ" not in text and "QSSECRET" not in text
|
||||
exported = export["spec"]["tools"][0]["config"]["actions"]["get_pet"]
|
||||
# Shape travels; the URL loses its query string; the pinned header
|
||||
# value is blanked while its flag and schema survive.
|
||||
assert exported["url"] == "https://api.example.com/pets/{petId}"
|
||||
auth = exported["headers"]["properties"]["Authorization"]
|
||||
assert auth["filled_by_llm"] is False and auth["value"] == ""
|
||||
assert exported["query_params"]["properties"]["petId"]["filled_by_llm"] is True
|
||||
|
||||
doc = parse_agent_yaml(text)
|
||||
importer = "u_api_tool_2"
|
||||
result = apply_import(
|
||||
pg_conn,
|
||||
importer,
|
||||
doc,
|
||||
resolution={"tools": {"tool-0": {"decision": "create"}}},
|
||||
)
|
||||
imported_agent = AgentsRepository(pg_conn).get(result["agent_id"], importer)
|
||||
row = UserToolsRepository(pg_conn).get_any(imported_agent["tools"][0], importer)
|
||||
action = row["config"]["actions"]["get_pet"]
|
||||
assert action["url"] == "https://api.example.com/pets/{petId}"
|
||||
assert action["method"] == "GET"
|
||||
# The blanked pinned header is called out — the request would silently
|
||||
# omit it until the importer sets a value in the tool's settings.
|
||||
assert any("Authorization" in w for w in result["warnings"])
|
||||
|
||||
|
||||
def test_api_tool_without_actions_imports_with_warning(pg_conn, monkeypatch):
|
||||
"""Older exports carried no actions for api_tool; the tool is created but
|
||||
the dead state is surfaced instead of silent."""
|
||||
user = "u_api_tool_empty"
|
||||
_fake_api_tool(monkeypatch)
|
||||
doc = _doc(
|
||||
name="Dead API Bot",
|
||||
_slug="dead-api",
|
||||
tools=[{"type": "api_tool", "name": "Legacy", "config": {}}],
|
||||
)
|
||||
result = apply_import(
|
||||
pg_conn,
|
||||
user,
|
||||
doc,
|
||||
resolution={"tools": {"tool-0": {"decision": "create"}}},
|
||||
)
|
||||
assert any("without any actions" in w for w in result["warnings"])
|
||||
agent = AgentsRepository(pg_conn).get(result["agent_id"], user)
|
||||
row = UserToolsRepository(pg_conn).get_any(agent["tools"][0], user)
|
||||
assert row["config"]["actions"] == {}
|
||||
@@ -131,7 +131,11 @@ def test_export_rewrites_node_refs_and_redacts_secrets(pg_conn):
|
||||
# The collected sections use the classic serializers' portable identities.
|
||||
assert spec["tools"][0]["type"] == "brave"
|
||||
assert spec["tools"][0]["requires_secrets"] == ["token"]
|
||||
assert spec["tools"][1] == {"type": "read_document", "builtin": True}
|
||||
assert spec["tools"][1] == {
|
||||
"type": "read_document",
|
||||
"builtin": True,
|
||||
"ref": default_tool_id("read_document"),
|
||||
}
|
||||
assert spec["sources"][0]["name"] == "KB"
|
||||
assert spec["model"]["available"][0]["display_name"] == "My Llama"
|
||||
assert spec["prompt"] == "default"
|
||||
@@ -378,8 +382,29 @@ def test_import_with_null_workflow_keeps_published_but_clears_draft(pg_conn):
|
||||
assert str(updated["workflow_id"]) == str(wf["id"])
|
||||
assert any("kept its existing" in w for w in result["warnings"])
|
||||
|
||||
# Draft agent: explicit null clears the link.
|
||||
# Draft agent: explicit null clears the link AND reaps the now-orphaned
|
||||
# workflow row — there is no workflow-list API independent of agents, so
|
||||
# an unlinked row would be unreachable forever.
|
||||
AgentsRepository(pg_conn).update(str(agent["id"]), user, {"status": "draft"})
|
||||
apply_import(pg_conn, user, doc)
|
||||
updated = AgentsRepository(pg_conn).get(str(agent["id"]), user)
|
||||
assert updated["workflow_id"] is None
|
||||
assert WorkflowsRepository(pg_conn).get(str(wf["id"]), user) is None
|
||||
|
||||
|
||||
def test_null_workflow_keeps_row_still_referenced_by_another_agent(pg_conn):
|
||||
user = "u_wf_null_shared"
|
||||
agent, wf = _seed_workflow_agent(pg_conn, user)
|
||||
# A second draft agent points at the same workflow.
|
||||
AgentsRepository(pg_conn).create(
|
||||
user, "Sibling", "draft", agent_type="workflow", workflow_id=str(wf["id"])
|
||||
)
|
||||
doc = parse_agent_yaml(agent_to_yaml(serialize_agent(pg_conn, agent, user)))
|
||||
doc["spec"]["workflow"] = None
|
||||
|
||||
AgentsRepository(pg_conn).update(str(agent["id"]), user, {"status": "draft"})
|
||||
apply_import(pg_conn, user, doc)
|
||||
updated = AgentsRepository(pg_conn).get(str(agent["id"]), user)
|
||||
assert updated["workflow_id"] is None
|
||||
# Still referenced — must not be deleted.
|
||||
assert WorkflowsRepository(pg_conn).get(str(wf["id"]), user) is not None
|
||||
@@ -181,8 +181,12 @@ class TestAdoptWorkflowAgent:
|
||||
assert {n["node_id"] for n in nodes} == {"start-1", "agent-1", "end-1"}
|
||||
assert len(edges) == 2
|
||||
|
||||
def test_adopt_survives_unresolvable_workflow(self, app, pg_conn):
|
||||
"""A template pointing at a graph its owner doesn't own adopts without one."""
|
||||
def test_adopt_refuses_unresolvable_workflow(self, app, pg_conn):
|
||||
"""A template pointing at a graph its owner doesn't own cannot adopt.
|
||||
|
||||
Publishing the agent anyway would hand the adopter a workflow agent
|
||||
with no graph — permanently unrunnable — so the adopt fails instead.
|
||||
"""
|
||||
from application.api.user.agents.routes import AdoptAgent
|
||||
from application.storage.db.repositories.agents import AgentsRepository
|
||||
|
||||
@@ -203,17 +207,87 @@ class TestAdoptWorkflowAgent:
|
||||
|
||||
request.decoded_token = {"sub": "u-wf-adopter2"}
|
||||
response = AdoptAgent().post()
|
||||
assert response.status_code == 200
|
||||
adopted = next(
|
||||
assert response.status_code == 500
|
||||
assert not [
|
||||
a
|
||||
for a in repo.list_for_user("u-wf-adopter2")
|
||||
if a["name"] == "Broken WF Template"
|
||||
)
|
||||
assert adopted["workflow_id"] is None
|
||||
]
|
||||
# The unowned graph was not cloned or mutated.
|
||||
nodes, edges = _graph(pg_conn, wf["id"])
|
||||
assert len(nodes) == 3 and len(edges) == 2
|
||||
|
||||
def test_adopt_strips_foreign_node_refs(self, app, pg_conn):
|
||||
"""Cloned agent nodes keep builtin tool ids but shed the owner's refs."""
|
||||
from application.agents.default_tools import default_tool_id
|
||||
from application.api.user.agents.routes import AdoptAgent
|
||||
from application.storage.db.repositories.agents import AgentsRepository
|
||||
from application.storage.db.repositories.user_tools import (
|
||||
UserToolsRepository,
|
||||
)
|
||||
from application.storage.db.repositories.workflow_nodes import (
|
||||
WorkflowNodesRepository,
|
||||
)
|
||||
from application.storage.db.repositories.workflows import WorkflowsRepository
|
||||
|
||||
owner_tool = UserToolsRepository(pg_conn).create(
|
||||
"__system__", "brave", config={}, display_name="Brave"
|
||||
)
|
||||
builtin_id = default_tool_id("artifact_generator")
|
||||
wf = WorkflowsRepository(pg_conn).create("__system__", "Reffy WF")
|
||||
WorkflowNodesRepository(pg_conn).bulk_create(
|
||||
str(wf["id"]),
|
||||
1,
|
||||
[
|
||||
{"node_id": "start-1", "node_type": "start", "title": "Start"},
|
||||
{
|
||||
"node_id": "agent-1",
|
||||
"node_type": "agent",
|
||||
"title": "Agent",
|
||||
"config": {
|
||||
"tools": [builtin_id, str(owner_tool["id"])],
|
||||
"sources": ["00000000-0000-0000-0000-00000000dead"],
|
||||
"model_id": "00000000-0000-0000-0000-00000000beef",
|
||||
},
|
||||
},
|
||||
],
|
||||
)
|
||||
repo = AgentsRepository(pg_conn)
|
||||
template = repo.create(
|
||||
"__system__",
|
||||
"Reffy Template",
|
||||
"template",
|
||||
agent_type="workflow",
|
||||
workflow_id=str(wf["id"]),
|
||||
tools=[builtin_id, str(owner_tool["id"])],
|
||||
)
|
||||
|
||||
with _patch_db(pg_conn), app.test_request_context(
|
||||
f"/api/adopt_agent?id={template['id']}", method="POST"
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u-wf-adopter3"}
|
||||
response = AdoptAgent().post()
|
||||
assert response.status_code == 200
|
||||
|
||||
adopted = next(
|
||||
a
|
||||
for a in repo.list_for_user("u-wf-adopter3")
|
||||
if a["name"] == "Reffy Template"
|
||||
)
|
||||
# Classic-column tools: the builtin survives, the foreign row doesn't.
|
||||
assert adopted["tools"] == [builtin_id]
|
||||
nodes, _ = _graph(pg_conn, adopted["workflow_id"])
|
||||
agent_node = next(n for n in nodes if n["node_id"] == "agent-1")
|
||||
assert agent_node["config"]["tools"] == [builtin_id]
|
||||
assert agent_node["config"]["sources"] == []
|
||||
assert agent_node["config"]["model_id"] is None
|
||||
# The template's own graph is untouched.
|
||||
src_nodes, _ = _graph(pg_conn, wf["id"])
|
||||
src_agent = next(n for n in src_nodes if n["node_id"] == "agent-1")
|
||||
assert str(owner_tool["id"]) in src_agent["config"]["tools"]
|
||||
|
||||
|
||||
class TestDeleteWorkflowAgent:
|
||||
def test_delete_removes_owned_workflow(self, app, pg_conn):
|
||||
|
||||
Reference in new issue
Block a user