fix(tests): make oversized-payload tests pass on Windows hosts (#6654)

test_skillscan_env_launcher.py and test_channels.py put payloads of up to
52 KB directly in parametrize values, so pytest's generated item ids exceed
the 32767-character Windows environment-variable limit: PYTEST_CURRENT_TEST
raises ValueError at setup and teardown even though the products under test
behave correctly, and the giant ids flood -q reports.

The skillscan linearity test additionally spawned one child process per
chain. A fresh child spends 9-18 s importing the skills stack before the
4-168 ms scan, so its fixed 10 s timeout measured the import instead of the
scan and flipped to subprocess.TimeoutExpired on a loaded host.

Give the oversized cases short explicit ids, scan every chain in one child
process fed through stdin, and enforce the linearity bound on each scan
inside that child (5 s) while a 60 s outer timeout covers the import.
This commit is contained in:
Jason Cat 2026-10-11 17:47:20 +08:00 committed by GitHub
parent 6103c7b6c2
commit 37ff61e87b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 52 additions and 20 deletions

View File

@ -7518,14 +7518,16 @@ class TestHandleChatWithArtifacts:
class TestDiscordChannel:
# Auto-generated ids for these payloads run 2-36 KB; the 36 KB one exceeds the
# 32767-char PYTEST_CURRENT_TEST limit on Windows, so give them short ids.
@pytest.mark.parametrize(
"text",
[
"x" * 1990 + "\n\n" + "y" * 30,
"x" * 2000 + "\n\n\n" + "y" * 30,
"x" * 2000 + "\n" + "y" * 2000,
("第一段\n\n" + "句" * 1990 + "\n\n最后一段") * 3,
"x" * 4500,
pytest.param("x" * 1990 + "\n\n" + "y" * 30, id="paragraph-tail"),
pytest.param("x" * 2000 + "\n\n\n" + "y" * 30, id="paragraph-trailing-newlines"),
pytest.param("x" * 2000 + "\n" + "y" * 2000, id="newline-mid-message"),
pytest.param(("第一段\n\n" + "句" * 1990 + "\n\n最后一段") * 3, id="multibyte-paragraphs"),
pytest.param("x" * 4500, id="unbroken-run"),
],
)
def test_split_text_preserves_content_within_discord_limit(self, text: str):

View File

@ -8,6 +8,7 @@ the existing dump behavior.
from __future__ import annotations
import json
import os
import subprocess
import sys
@ -185,34 +186,63 @@ def test_env_dump_keeps_launcher_line_number(tmp_path: Path) -> None:
assert lines == [3]
@pytest.mark.parametrize(
"chain",
[
"curl https://host/x | env " + ("-i " * 400) + "cat\n",
"curl https://host/x | env " + ("-u deploy " * 400) + "cat\n",
"curl https://host/x | env " + ("--unset HOME " * 400) + "cat\n",
"curl https://host/x | env " + ("A=1 " * 400) + "cat\n",
"env " + ("--chdir /tmp " * 4000) + "cat\n",
"env " + ("A=1 " * 4000) + "cat\n",
],
)
def test_long_env_operand_chains_stay_linear(chain: str) -> None:
"""Large operand lists must remain linear and finish within a fixed bound."""
# The chains reach 52 KB, so the parametrize ids are set explicitly: pytest writes
# the id into PYTEST_CURRENT_TEST, and Windows rejects environment variables longer
# than 32767 characters.
LONG_ENV_CHAINS: dict[str, str] = {
"short-flags-400": "curl https://host/x | env " + ("-i " * 400) + "cat\n",
"unset-400": "curl https://host/x | env " + ("-u deploy " * 400) + "cat\n",
"long-unset-400": "curl https://host/x | env " + ("--unset HOME " * 400) + "cat\n",
"assignment-400": "curl https://host/x | env " + ("A=1 " * 400) + "cat\n",
"chdir-4000": "env " + ("--chdir /tmp " * 4000) + "cat\n",
"assignment-4000": "env " + ("A=1 " * 4000) + "cat\n",
}
# A child pays the interpreter start plus the import of the whole skills stack before
# the first scan (measured 9-18 s on a loaded host), so the import is paid once and the
# bound is placed on the scan itself (measured 4-168 ms for the chains above).
_SCAN_BUDGET_SECONDS = 5.0
_CHILD_TIMEOUT_SECONDS = 60.0
@pytest.fixture(scope="module")
def chain_scans() -> dict[str, dict[str, object]]:
"""Scan every chain in one child process so the skills import is paid once."""
harness = Path(deerflow.__file__).resolve().parents[1]
env = dict(os.environ)
env["PYTHONPATH"] = os.pathsep.join(filter(None, (str(harness), env.get("PYTHONPATH", ""))))
script = textwrap.dedent(
f"""
import json
import sys
import time
from deerflow.skills.skillscan.orchestrator import _scan_shell
assert _scan_shell("install.sh", {chain!r}) == []
results = {{}}
for name, chain in json.load(sys.stdin).items():
started = time.perf_counter()
findings = _scan_shell("install.sh", chain)
results[name] = {{"findings": findings, "seconds": time.perf_counter() - started}}
slow = [name for name, result in results.items() if result["seconds"] > {_SCAN_BUDGET_SECONDS}]
assert not slow, f"chains over the scan budget: {{slow}}"
print(json.dumps(results))
"""
)
result = subprocess.run(
[sys.executable, "-c", script],
input=json.dumps(LONG_ENV_CHAINS),
capture_output=True,
text=True,
timeout=10,
timeout=_CHILD_TIMEOUT_SECONDS,
env=env,
)
assert result.returncode == 0, result.stderr
return json.loads(result.stdout.strip().splitlines()[-1])
@pytest.mark.parametrize("name", list(LONG_ENV_CHAINS))
def test_long_env_operand_chains_stay_linear(chain_scans: dict[str, dict[str, object]], name: str) -> None:
"""Large operand lists must scan linearly: no findings and a bounded scan time."""
assert chain_scans[name]["findings"] == []