mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
fix(scripts): reject a non-file --out path in dev_multi_instance (#7289)
`render-config`, `secrets` and `nginx-conf` write `--out` through `os.replace`, so an existing directory surfaced as a raw `PermissionError` traceback, and a file ancestor as `OSError` from `mkdir`. Validate the first existing ancestor once in `main()` before any writer runs, and state the file contract in `--help` for all three commands.
This commit is contained in:
parent
991459829b
commit
15a31e30eb
@ -445,6 +445,64 @@ def test_nginx_conf_is_accepted_by_nginx(tmp_path, state_name):
|
||||
assert result.returncode == 0, result.stderr
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# --out validation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_OUT_ARGV = {
|
||||
"render-config": lambda out: ["render-config", "--base", str(REPO_ROOT / "config.example.yaml"), "--out", str(out), "--database-url", DATABASE_URL, "--redis-url", REDIS_URL, "--state-dir", str(out.parent / "state")],
|
||||
"secrets": lambda out: ["secrets", "--out", str(out)],
|
||||
"nginx-conf": lambda out: ["nginx-conf", "--out", str(out), "--state-dir", str(out.parent / "state"), "--listen-port", "2027", "--gateway-port", "8001", "--gateway-port", "8011"],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("command", sorted(_OUT_ARGV))
|
||||
def test_cli_rejects_a_directory_out_before_writing(tmp_path, capsys, command):
|
||||
out = tmp_path / "bundle"
|
||||
out.mkdir()
|
||||
|
||||
with pytest.raises(SystemExit) as exit_info:
|
||||
mi.main(_OUT_ARGV[command](out))
|
||||
|
||||
assert exit_info.value.code == 2
|
||||
assert f"--out must be a file path, not a directory: {out}" in capsys.readouterr().err
|
||||
assert list(tmp_path.iterdir()) == [out], "a rejected --out must not leave a temporary file behind"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("command", sorted(_OUT_ARGV))
|
||||
def test_cli_rejects_an_out_whose_parent_is_a_file(tmp_path, capsys, command):
|
||||
parent = tmp_path / "not-a-dir"
|
||||
parent.write_text("", encoding="utf-8")
|
||||
|
||||
with pytest.raises(SystemExit) as exit_info:
|
||||
mi.main(_OUT_ARGV[command](parent / "bundle"))
|
||||
|
||||
assert exit_info.value.code == 2
|
||||
assert f"--out parent must be a directory: {parent}" in capsys.readouterr().err
|
||||
|
||||
|
||||
@pytest.mark.parametrize("missing", ["missing", "a/b"])
|
||||
@pytest.mark.parametrize("command", sorted(_OUT_ARGV))
|
||||
def test_cli_rejects_a_file_ancestor_behind_missing_directories(tmp_path, capsys, command, missing):
|
||||
blocker = tmp_path / "blocker"
|
||||
blocker.write_text("", encoding="utf-8")
|
||||
|
||||
with pytest.raises(SystemExit) as exit_info:
|
||||
mi.main(_OUT_ARGV[command](blocker / missing / "secrets.env"))
|
||||
|
||||
assert exit_info.value.code == 2
|
||||
assert f"--out parent must be a directory: {blocker}" in capsys.readouterr().err
|
||||
|
||||
|
||||
@pytest.mark.parametrize("command", sorted(_OUT_ARGV))
|
||||
def test_cli_still_creates_missing_out_parents(tmp_path, command):
|
||||
out = tmp_path / "a" / "b" / "secrets.env"
|
||||
|
||||
assert mi.main(_OUT_ARGV[command](out)) == 0
|
||||
|
||||
assert out.is_file()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@ -213,6 +213,8 @@ multi-instance gate and every endpoint/data root inside its state dir
|
||||
Unless `keep_channels` is requested, disable provider flags in both `channels`
|
||||
and `channel_connections`; UI credentials can otherwise connect a bot
|
||||
later. Preserve `channels.leader_lease` and connection metadata.
|
||||
`--out` is a file path for `render-config`, `secrets` and `nginx-conf`; a
|
||||
directory, or a file ancestor, is rejected before any write.
|
||||
|
||||
## Shell Script Invocation Contract
|
||||
|
||||
|
||||
@ -740,17 +740,17 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
|
||||
render = sub.add_parser("render-config", help="write the multi-instance config.yaml")
|
||||
render.add_argument("--base", required=True, type=Path)
|
||||
render.add_argument("--out", required=True, type=Path)
|
||||
render.add_argument("--out", required=True, type=Path, help="output file path (not a directory)")
|
||||
render.add_argument("--database-url", required=True)
|
||||
render.add_argument("--redis-url", required=True)
|
||||
render.add_argument("--state-dir", required=True, type=Path)
|
||||
render.add_argument("--keep-channels", action="store_true")
|
||||
|
||||
secrets_cmd = sub.add_parser("secrets", help="create the shared secrets file if missing")
|
||||
secrets_cmd.add_argument("--out", required=True, type=Path)
|
||||
secrets_cmd.add_argument("--out", required=True, type=Path, help="output file path (not a directory)")
|
||||
|
||||
nginx = sub.add_parser("nginx-conf", help="write the round-robin nginx config")
|
||||
nginx.add_argument("--out", required=True, type=Path)
|
||||
nginx.add_argument("--out", required=True, type=Path, help="output file path (not a directory)")
|
||||
nginx.add_argument("--state-dir", required=True, type=Path)
|
||||
nginx.add_argument("--listen-port", required=True, type=int)
|
||||
nginx.add_argument("--gateway-port", required=True, type=int, action="append")
|
||||
@ -765,6 +765,17 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
out = getattr(args, "out", None)
|
||||
if out is not None:
|
||||
# Both shapes reach the writers as a raw OSError (EISDIR/ENOTDIR) after doing work.
|
||||
if out.is_dir():
|
||||
parser.error(f"--out must be a file path, not a directory: {out}")
|
||||
for ancestor in out.parents:
|
||||
if ancestor.exists():
|
||||
if not ancestor.is_dir():
|
||||
parser.error(f"--out parent must be a directory: {ancestor}")
|
||||
break
|
||||
|
||||
if args.command == "render-config":
|
||||
for note in render_config(args.base, args.out, database_url=args.database_url, redis_url=args.redis_url, state_dir=args.state_dir, keep_channels=args.keep_channels):
|
||||
print(f" note: {note}")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user