From 37ff61e87b8d90d08f59747ac158594799cdfc04 Mon Sep 17 00:00:00 2001 From: Jason Cat Date: Sun, 11 Oct 2026 17:47:20 +0800 Subject: [PATCH] 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. --- backend/tests/test_channels.py | 12 ++-- backend/tests/test_skillscan_env_launcher.py | 60 +++++++++++++++----- 2 files changed, 52 insertions(+), 20 deletions(-) diff --git a/backend/tests/test_channels.py b/backend/tests/test_channels.py index 16fc950215..cd21ee0f06 100644 --- a/backend/tests/test_channels.py +++ b/backend/tests/test_channels.py @@ -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): diff --git a/backend/tests/test_skillscan_env_launcher.py b/backend/tests/test_skillscan_env_launcher.py index 3ea3af7312..9c3bb79904 100644 --- a/backend/tests/test_skillscan_env_launcher.py +++ b/backend/tests/test_skillscan_env_launcher.py @@ -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"] == []