mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
test: keep multi-instance harness checks portable on Windows (#6972)
* test: fix Windows multi-instance harness checks * docs: avoid concurrent test guidance insertion conflict * docs: label multi-instance harness test guidance
This commit is contained in:
parent
ab43439a54
commit
1c1b8774ab
@ -358,3 +358,10 @@ setup, after the autouse fixtures initialize runtime, to avoid the package cycle
|
||||
### Web model provider presets
|
||||
|
||||
`test_managed_provider_presets.py` covers encrypted compatibility, auto IDs, key switching, snapshot isolation and native/compatible protocols. Its HTTP double is below actual LangChain/vendor SDK serialization; do not replace these adapters with successful stubs. `test_managed_presets_live.py` requires explicit opt-in and vendor credentials, and remains skipped in CI.
|
||||
|
||||
## Multi-instance harness
|
||||
|
||||
Multi-instance harness tests use `support.shell.require_script_bash()` for shell
|
||||
smoke checks. Keep config content and secret stability checks portable; isolate
|
||||
POSIX owner-only permission assertions so Windows skips only those checks.
|
||||
Nginx path expectations must include quoted-string escaping for native paths.
|
||||
|
||||
@ -11,6 +11,7 @@ from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import importlib.util
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
@ -21,6 +22,7 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
from support.shell import require_script_bash
|
||||
|
||||
from app.gateway.deps import _enforce_postgres_for_multi_worker, _validate_memory_retrieval_index
|
||||
from deerflow.config.app_config import AppConfig
|
||||
@ -361,7 +363,7 @@ def test_each_process_resolves_its_own_retrieval_index(tmp_path, monkeypatch):
|
||||
assert resolved[0] != resolved[1]
|
||||
|
||||
|
||||
def test_render_config_writes_a_private_file_with_a_generated_header(tmp_path):
|
||||
def test_render_config_writes_a_file_with_a_generated_header(tmp_path):
|
||||
out = tmp_path / "nested" / "config.yaml"
|
||||
|
||||
notes = mi.render_config(REPO_ROOT / "config.example.yaml", out, database_url=DATABASE_URL, redis_url=REDIS_URL, state_dir=tmp_path)
|
||||
@ -369,7 +371,6 @@ def test_render_config_writes_a_private_file_with_a_generated_header(tmp_path):
|
||||
assert isinstance(notes, list)
|
||||
text = out.read_text(encoding="utf-8")
|
||||
assert text.startswith("# Generated by scripts/dev_multi_instance.sh")
|
||||
assert stat.S_IMODE(out.stat().st_mode) == 0o600, "the base config may carry model API keys"
|
||||
assert yaml.safe_load(text)["deployment"]["multi_instance"] is True
|
||||
|
||||
|
||||
@ -378,7 +379,7 @@ def test_render_config_writes_a_private_file_with_a_generated_header(tmp_path):
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_secrets_file_is_private_complete_and_stable(tmp_path):
|
||||
def test_secrets_file_is_complete_and_stable(tmp_path):
|
||||
path = tmp_path / "secrets.env"
|
||||
|
||||
first = mi.ensure_secrets_file(path)
|
||||
@ -389,11 +390,22 @@ def test_secrets_file_is_private_complete_and_stable(tmp_path):
|
||||
assert all(value for value in first.values())
|
||||
assert len(base64.urlsafe_b64decode(first["DEER_FLOW_CREDENTIALS_KEY"])) == 32, "must be a raw Fernet key"
|
||||
assert first["AUTH_JWT_SECRET"] != first["DEER_FLOW_INTERNAL_AUTH_TOKEN"]
|
||||
assert stat.S_IMODE(path.stat().st_mode) == 0o600
|
||||
# Shell-sourceable KEY=value lines.
|
||||
assert sorted(line.split("=", 1)[0] for line in path.read_text(encoding="utf-8").splitlines() if line and not line.startswith("#")) == sorted(mi.SHARED_SECRET_NAMES)
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="chmod cannot express owner-only modes on Windows")
|
||||
@pytest.mark.parametrize("output_kind", ["config", "secrets"])
|
||||
def test_generated_files_have_owner_only_permissions(tmp_path, output_kind):
|
||||
path = tmp_path / ("config.yaml" if output_kind == "config" else "secrets.env")
|
||||
if output_kind == "config":
|
||||
mi.render_config(REPO_ROOT / "config.example.yaml", path, database_url=DATABASE_URL, redis_url=REDIS_URL, state_dir=tmp_path)
|
||||
else:
|
||||
mi.ensure_secrets_file(path)
|
||||
|
||||
assert stat.S_IMODE(path.stat().st_mode) == 0o600, "generated files may carry credentials"
|
||||
|
||||
|
||||
def test_nginx_conf_balances_both_gateways(tmp_path):
|
||||
text = mi.render_nginx_conf(listen_port=2027, gateway_ports=[8001, 8011], frontend_port=3000, state_dir=tmp_path)
|
||||
|
||||
@ -401,7 +413,7 @@ def test_nginx_conf_balances_both_gateways(tmp_path):
|
||||
assert "server 127.0.0.1:8001;" in text
|
||||
assert "server 127.0.0.1:8011;" in text
|
||||
assert "X-DeerFlow-Upstream $upstream_addr" in text
|
||||
assert f'"{tmp_path / "run" / "nginx.pid"}"' in text
|
||||
assert f"pid {json.dumps(str(tmp_path / 'run' / 'nginx.pid'), ensure_ascii=False)};" in text
|
||||
|
||||
|
||||
def test_nginx_conf_quotes_paths_with_whitespace(tmp_path):
|
||||
@ -409,9 +421,9 @@ def test_nginx_conf_quotes_paths_with_whitespace(tmp_path):
|
||||
|
||||
text = mi.render_nginx_conf(listen_port=2027, gateway_ports=[8001, 8011], frontend_port=3000, state_dir=state)
|
||||
|
||||
assert f'pid "{state / "run" / "nginx.pid"}";' in text
|
||||
assert f'access_log "{state / "logs" / "nginx-access.log"}";' in text
|
||||
assert f'proxy_temp_path "{state / "nginx" / "temp" / "proxy"}";' in text
|
||||
assert f"pid {json.dumps(str(state / 'run' / 'nginx.pid'), ensure_ascii=False)};" in text
|
||||
assert f"access_log {json.dumps(str(state / 'logs' / 'nginx-access.log'), ensure_ascii=False)};" in text
|
||||
assert f"proxy_temp_path {json.dumps(str(state / 'nginx' / 'temp' / 'proxy'), ensure_ascii=False)};" in text
|
||||
|
||||
|
||||
def test_nginx_conf_rejects_paths_nginx_would_interpolate(tmp_path):
|
||||
@ -511,7 +523,7 @@ def test_sse_read_enforces_its_budget_while_only_heartbeats_arrive(tmp_path):
|
||||
|
||||
|
||||
def test_shell_script_parses():
|
||||
result = subprocess.run(["bash", "-n", str(SHELL_PATH)], capture_output=True, text=True, timeout=30)
|
||||
result = subprocess.run([require_script_bash(), "-n", str(SHELL_PATH)], capture_output=True, text=True, timeout=30)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
|
||||
@ -520,7 +532,7 @@ def test_shell_script_parses():
|
||||
def test_shell_script_help_lists_every_command_without_side_effects(tmp_path, flag):
|
||||
env = {**os.environ, "DEERFLOW_MI_STATE_DIR": str(tmp_path / "state"), "PATH": os.environ.get("PATH", "")}
|
||||
|
||||
result = subprocess.run(["bash", str(SHELL_PATH), flag], capture_output=True, text=True, timeout=30, env=env)
|
||||
result = subprocess.run([require_script_bash(), str(SHELL_PATH), flag], capture_output=True, text=True, timeout=30, env=env)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
for command in ("up", "down", "status", "logs", "check", "restart"):
|
||||
@ -531,7 +543,7 @@ def test_shell_script_help_lists_every_command_without_side_effects(tmp_path, fl
|
||||
def test_shell_script_rejects_unknown_commands(tmp_path):
|
||||
env = {**os.environ, "DEERFLOW_MI_STATE_DIR": str(tmp_path / "state")}
|
||||
|
||||
result = subprocess.run(["bash", str(SHELL_PATH), "frobnicate"], capture_output=True, text=True, timeout=30, env=env)
|
||||
result = subprocess.run([require_script_bash(), str(SHELL_PATH), "frobnicate"], capture_output=True, text=True, timeout=30, env=env)
|
||||
|
||||
assert result.returncode != 0
|
||||
assert "Unknown command" in result.stderr
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user