mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-27 08:28:00 +00:00
* feat: emit structured runtime metadata * fix: avoid subagent import cycle in replay gateway * fix: preserve legacy subtask result parsing * refactor: tighten runtime metadata contracts * fix(middleware): keep recovery hint on task exception wrapper content The structured-metadata stamp overwrote the wrapper text with the bare task-failure message, dropping the model-facing 'Continue with available context, or choose an alternative tool.' guidance that every other tool exception keeps. Append the shared hint after the formatted message. * fix(subagents): require lowercase hex for result_sha256 reader Length-only validation accepted any 64-char string; a faulty serializer or relaying wrapper could store a non-digest value in the delegation ledger. Enforce the producer's hexdigest shape with a fullmatch. --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
120 lines
4.5 KiB
Python
120 lines
4.5 KiB
Python
"""Contract tests for ``deerflow.subagents.status_contract``."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
from deerflow.subagents.status_contract import (
|
|
SUBAGENT_ERROR_KEY,
|
|
SUBAGENT_METADATA_TEXT_MAX_CHARS,
|
|
SUBAGENT_RESULT_BRIEF_KEY,
|
|
SUBAGENT_RESULT_SHA256_KEY,
|
|
SUBAGENT_STATUS_KEY,
|
|
SUBAGENT_STATUS_VALUES,
|
|
_bound_metadata_text,
|
|
make_subagent_additional_kwargs,
|
|
read_subagent_result_metadata,
|
|
)
|
|
|
|
_REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
_CONTRACT_PATH = _REPO_ROOT / "contracts" / "subagent_status_contract.json"
|
|
|
|
|
|
def _load_contract() -> dict:
|
|
return json.loads(_CONTRACT_PATH.read_text(encoding="utf-8"))
|
|
|
|
|
|
def test_contract_file_exists():
|
|
assert _CONTRACT_PATH.is_file(), f"missing shared fixture: {_CONTRACT_PATH}"
|
|
|
|
|
|
def test_status_values_match_contract():
|
|
"""Backend status enum stays aligned with the contract document."""
|
|
contract = _load_contract()
|
|
assert set(SUBAGENT_STATUS_VALUES) == set(contract["valid_status_values"])
|
|
|
|
|
|
def test_make_subagent_additional_kwargs_includes_status():
|
|
kwargs = make_subagent_additional_kwargs("completed")
|
|
assert kwargs == {SUBAGENT_STATUS_KEY: "completed"}
|
|
|
|
|
|
def test_make_subagent_additional_kwargs_includes_error_when_present():
|
|
kwargs = make_subagent_additional_kwargs("failed", error="boom")
|
|
assert kwargs == {SUBAGENT_STATUS_KEY: "failed", SUBAGENT_ERROR_KEY: "boom"}
|
|
|
|
|
|
def test_make_subagent_additional_kwargs_includes_bounded_result_metadata():
|
|
kwargs = make_subagent_additional_kwargs("completed", result="done")
|
|
assert kwargs[SUBAGENT_STATUS_KEY] == "completed"
|
|
assert kwargs[SUBAGENT_RESULT_BRIEF_KEY] == "done"
|
|
assert len(kwargs[SUBAGENT_RESULT_SHA256_KEY]) == 64
|
|
assert SUBAGENT_ERROR_KEY not in kwargs
|
|
|
|
|
|
def test_make_subagent_additional_kwargs_bounds_large_result_metadata():
|
|
huge = "x" * (SUBAGENT_METADATA_TEXT_MAX_CHARS + 5000)
|
|
kwargs = make_subagent_additional_kwargs("completed", result=huge)
|
|
assert len(kwargs[SUBAGENT_RESULT_BRIEF_KEY]) <= SUBAGENT_METADATA_TEXT_MAX_CHARS
|
|
assert kwargs[SUBAGENT_RESULT_BRIEF_KEY] != huge
|
|
assert len(kwargs[SUBAGENT_RESULT_SHA256_KEY]) == 64
|
|
|
|
|
|
def test_bound_metadata_text_respects_small_caps():
|
|
text = "A" * 100
|
|
|
|
assert _bound_metadata_text(text, cap=0) == ""
|
|
assert _bound_metadata_text(text, cap=1) == "A"
|
|
assert len(_bound_metadata_text(text, cap=15)) <= 15
|
|
|
|
|
|
def test_make_subagent_additional_kwargs_omits_blank_error():
|
|
"""Empty / whitespace error must not leak as ``subagent_error: ""``."""
|
|
assert make_subagent_additional_kwargs("failed", error="") == {SUBAGENT_STATUS_KEY: "failed"}
|
|
assert make_subagent_additional_kwargs("failed", error=" ") == {SUBAGENT_STATUS_KEY: "failed"}
|
|
assert make_subagent_additional_kwargs("failed", error=None) == {SUBAGENT_STATUS_KEY: "failed"}
|
|
|
|
|
|
def test_make_subagent_additional_kwargs_bounds_large_error_metadata():
|
|
huge = "boom " * 2000
|
|
kwargs = make_subagent_additional_kwargs("failed", error=huge)
|
|
assert kwargs[SUBAGENT_STATUS_KEY] == "failed"
|
|
assert len(kwargs[SUBAGENT_ERROR_KEY]) <= SUBAGENT_METADATA_TEXT_MAX_CHARS
|
|
assert SUBAGENT_RESULT_BRIEF_KEY not in kwargs
|
|
|
|
|
|
def test_read_subagent_result_metadata_returns_bounded_payload():
|
|
parsed = read_subagent_result_metadata(
|
|
{
|
|
SUBAGENT_STATUS_KEY: "completed",
|
|
SUBAGENT_RESULT_BRIEF_KEY: "structured",
|
|
SUBAGENT_RESULT_SHA256_KEY: "a" * 64,
|
|
SUBAGENT_ERROR_KEY: "ignored",
|
|
}
|
|
)
|
|
assert parsed == {
|
|
"status": "completed",
|
|
"result_brief": "structured",
|
|
"result_sha256": "a" * 64,
|
|
}
|
|
|
|
|
|
def test_read_subagent_result_metadata_rejects_unknown_status():
|
|
assert read_subagent_result_metadata({SUBAGENT_STATUS_KEY: "future"}) is None
|
|
|
|
|
|
def test_read_subagent_result_metadata_rejects_non_hex_sha256():
|
|
"""A 64-char value that is not a lowercase hex digest must be dropped."""
|
|
base = {SUBAGENT_STATUS_KEY: "completed", SUBAGENT_RESULT_BRIEF_KEY: "structured"}
|
|
for bad_hash in ("z" * 64, "A" * 64, "a" * 63, "a" * 65, ("a" * 63) + " "):
|
|
parsed = read_subagent_result_metadata({**base, SUBAGENT_RESULT_SHA256_KEY: bad_hash})
|
|
assert parsed == {"status": "completed", "result_brief": "structured"}, bad_hash
|
|
|
|
|
|
def test_make_subagent_additional_kwargs_rejects_unknown_status():
|
|
import pytest
|
|
|
|
with pytest.raises(ValueError, match="invalid subagent status"):
|
|
make_subagent_additional_kwargs("garbage") # type: ignore[arg-type]
|