diff --git a/CHANGELOG.md b/CHANGELOG.md index 3aee3765e4..9dfbf6e62c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2404,6 +2404,12 @@ This release closes that milestone with **772 merged pull requests**. ### Fixed +- **dev:** `make stop` / `make dev` can now reclaim dev ports held by a sibling + worktree whose path contains spaces. `serve.sh` built its worktree-root list + with `awk '{print $2}'` over `git worktree list --porcelain`, whose paths are + unquoted, so `.../deer flow two` was recorded as `.../deer`; a Gateway or + frontend started from that worktree was never recognised as deer-flow's and + the start aborted with "port already in use". The whole path is kept now. ([#5856]) - **uploads:** A malformed `files[*].size` in a run's message metadata no longer fails the whole run. `UploadsMiddleware` validated every other field of a client-supplied file entry fail-soft but passed `size` straight to @@ -6086,5 +6092,6 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#5845]: https://github.com/bytedance/deer-flow/pull/5845 [#5848]: https://github.com/bytedance/deer-flow/pull/5848 [#5855]: https://github.com/bytedance/deer-flow/pull/5855 +[#5856]: https://github.com/bytedance/deer-flow/pull/5856 [#5859]: https://github.com/bytedance/deer-flow/pull/5859 diff --git a/CHANGELOG_zh.md b/CHANGELOG_zh.md index fbc0cbeeca..76023f95f1 100644 --- a/CHANGELOG_zh.md +++ b/CHANGELOG_zh.md @@ -2120,6 +2120,11 @@ ### 修复 +- **开发:** 当同级 worktree 的路径包含空格时,`make stop` / `make dev` 现在也能回收它占用的 + 开发端口。`serve.sh` 用 `awk '{print $2}'` 解析 `git worktree list --porcelain` 来构建 + worktree 根目录列表,而该输出中的路径不加引号,因此 `.../deer flow two` 被记录成了 + `.../deer`;从那个 worktree 启动的 Gateway 或前端永远不会被识别为 deer-flow 的进程, + 启动会以“端口已被占用”中止。现在会保留整条路径。([#5856]) - **上传:** 运行消息元数据中格式错误的 `files[*].size` 不再导致整个运行失败。 `UploadsMiddleware` 对客户端提供的文件条目的其他字段都做了容错校验,唯独把 `size` 直接交给 `int()`,因此 `"abc"` 或列表这样的值会在调用模型之前从 `before_agent` 抛出——而且由于该条目 @@ -5181,4 +5186,5 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#5845]: https://github.com/bytedance/deer-flow/pull/5845 [#5848]: https://github.com/bytedance/deer-flow/pull/5848 [#5855]: https://github.com/bytedance/deer-flow/pull/5855 +[#5856]: https://github.com/bytedance/deer-flow/pull/5856 [#5859]: https://github.com/bytedance/deer-flow/pull/5859 diff --git a/backend/tests/test_serve_worktree_roots.py b/backend/tests/test_serve_worktree_roots.py new file mode 100644 index 0000000000..17d7ea5330 --- /dev/null +++ b/backend/tests/test_serve_worktree_roots.py @@ -0,0 +1,80 @@ +"""``scripts/serve.sh`` must discover every deer-flow worktree root verbatim. + +``DEERFLOW_ROOTS`` is the set of checkout roots whose dev-port holders +``make stop`` / ``make dev`` may reclaim. It is built from +``git worktree list --porcelain``, whose ``worktree `` lines are not +quoted, so a root containing whitespace must be taken as the whole remainder +of the line — ``awk '{print $2}'`` keeps only the first word, and a sibling +worktree at ``.../deer flow two`` was recorded as ``.../deer``, which +``_is_deerflow_pid`` can never match. +""" + +from __future__ import annotations + +import os +import shlex +import subprocess +from pathlib import Path + +import pytest +from support.shell import require_script_bash + +REPO_ROOT = Path(__file__).resolve().parents[2] +SERVE_SH = REPO_ROOT / "scripts" / "serve.sh" + +pytestmark = pytest.mark.skipif(os.name == "nt", reason="exercises git worktree paths through a POSIX shell") + + +def _extract_deerflow_roots_block() -> str: + text = SERVE_SH.read_text(encoding="utf-8") + start = text.index('DEERFLOW_ROOTS="$(') + chunks: list[str] = [] + for line in text[start:].splitlines(keepends=True): + chunks.append(line) + if line.rstrip("\n") == ')"': + return "".join(chunks) + raise AssertionError("Could not extract the DEERFLOW_ROOTS block from serve.sh") + + +def _git(*args: str, cwd: Path) -> None: + subprocess.run(["git", *args], cwd=cwd, check=True, capture_output=True, env={**os.environ, "GIT_CONFIG_GLOBAL": os.devnull}) + + +def _repo_with_spaced_worktree(tmp_path: Path) -> tuple[Path, Path]: + main = tmp_path / "deer-flow" + main.mkdir() + _git("init", "-q", cwd=main) + _git("-c", "user.email=t@example.com", "-c", "user.name=t", "commit", "-q", "--allow-empty", "-m", "init", cwd=main) + linked = tmp_path / "deer flow two" + _git("worktree", "add", "-q", str(linked), "-b", "wt-two", cwd=main) + return main.resolve(), linked.resolve() + + +def _deerflow_roots(repo_root: Path) -> list[str]: + bash = require_script_bash() + script = f""" +REPO_ROOT={shlex.quote(str(repo_root))} +{_extract_deerflow_roots_block()} +printf '%s\\n' "$DEERFLOW_ROOTS" +""" + result = subprocess.run([bash, "-c", script], check=True, capture_output=True, text=True) + return [line for line in result.stdout.splitlines() if line] + + +def test_worktree_root_with_spaces_is_kept_whole(tmp_path): + main, linked = _repo_with_spaced_worktree(tmp_path) + + roots = _deerflow_roots(main) + + assert str(linked) in roots + assert all(Path(root).is_dir() for root in roots), roots # no truncated fragment such as ".../deer" + assert sorted(roots, key=len, reverse=True) == roots # most-specific (longest) root first + + +def test_repo_root_with_spaces_is_listed_once(tmp_path): + main, linked = _repo_with_spaced_worktree(tmp_path) + + roots = _deerflow_roots(linked) # the checkout we run from is itself the spaced path + + assert roots.count(str(linked)) == 1 + assert set(roots) == {str(main), str(linked)} # and no truncated fragment alongside them diff --git a/scripts/serve.sh b/scripts/serve.sh index f1e3968c47..1cc3f9f76c 100755 --- a/scripts/serve.sh +++ b/scripts/serve.sh @@ -98,7 +98,7 @@ DEERFLOW_ROOTS="$( { printf '%s\n' "$REPO_ROOT" git -C "$REPO_ROOT" worktree list --porcelain 2>/dev/null | - awk '/^worktree /{print $2}' + awk '/^worktree /{sub(/^worktree /, ""); print}' # whole line: paths are unquoted and may contain spaces } | awk 'NF && !seen[$0]++ {print length($0)"\t"$0}' | sort -rn | sed 's/^[0-9]*\t//' )"