from __future__ import annotations
import io
from pathlib import Path
import pytest
from mcp2term_client.file_command import (
FileCommandHelp,
FileCommandParseError,
ManageFileCommand,
parse_manage_file_command,
)
@pytest.mark.parametrize("use_real_dependencies", [False, True])
def test_parse_manage_file_inline_content(tmp_path: Path, use_real_dependencies: bool) -> None:
command: ManageFileCommand = parse_manage_file_command(
[
"create",
"notes.txt",
"--content",
"hello world",
"--create-parents",
]
)
assert command.operation == "create"
assert command.path == "notes.txt"
assert command.content == "hello world"
assert command.create_parents is True
assert command.encoding == "utf-8"
assert command.escape_profile == "auto"
@pytest.mark.parametrize("use_real_dependencies", [False, True])
def test_parse_manage_file_from_stdin(tmp_path: Path, use_real_dependencies: bool) -> None:
stdin_stream = io.StringIO("line-one\nline-two\n")
command = parse_manage_file_command(
["write", "story.txt", "--stdin", "--encoding", "utf-8"],
stdin=stdin_stream,
)
assert command.operation == "write"
assert command.content == "line-one\nline-two\n"
assert command.encoding == "utf-8"
assert command.escape_profile == "auto"
assert command.pattern is None
assert command.use_regex is False
assert command.ignore_case is False
assert command.max_replacements is None
@pytest.mark.parametrize("use_real_dependencies", [False, True])
def test_parse_manage_file_from_file(tmp_path: Path, use_real_dependencies: bool) -> None:
content_file = tmp_path / "payload.txt"
content_file.write_text("payload", encoding="utf-8")
command = parse_manage_file_command(
[
"append",
"story.txt",
"--content-from-file",
str(content_file),
"--no-create-if-missing",
]
)
assert command.operation == "append"
assert command.content == "payload"
assert command.create_if_missing is False
assert command.escape_profile == "auto"
@pytest.mark.parametrize("use_real_dependencies", [False, True])
def test_parse_manage_file_patch_from_file(tmp_path: Path, use_real_dependencies: bool) -> None:
diff_file = tmp_path / "delta.diff"
diff_file.write_text(
"""--- a/sample.txt\n+++ b/sample.txt\n@@ -1 +1 @@\n-old\n+new\n""",
encoding="utf-8",
)
command = parse_manage_file_command(
[
"patch",
"sample.txt",
"--content-from-file",
str(diff_file),
]
)
assert command.operation == "patch"
assert "@@ -1 +1 @@" in (command.content or "")
assert command.escape_profile == "auto"
@pytest.mark.parametrize("use_real_dependencies", [False, True])
def test_parse_manage_file_patch_requires_content(
tmp_path: Path, use_real_dependencies: bool
) -> None:
with pytest.raises(FileCommandParseError):
parse_manage_file_command([
"patch",
"sample.txt",
])
@pytest.mark.parametrize("use_real_dependencies", [False, True])
def test_parse_manage_file_rejects_conflicting_sources(
tmp_path: Path, use_real_dependencies: bool
) -> None:
with pytest.raises(FileCommandParseError):
parse_manage_file_command(
[
"write",
"story.txt",
"--content",
"text",
"--stdin",
]
)
@pytest.mark.parametrize("use_real_dependencies", [False, True])
def test_parse_manage_file_help(tmp_path: Path, use_real_dependencies: bool) -> None:
with pytest.raises(FileCommandHelp):
parse_manage_file_command(["--help"])
@pytest.mark.parametrize("use_real_dependencies", [False, True])
def test_parse_manage_file_decodes_inline_patch_escape_sequences(
tmp_path: Path, use_real_dependencies: bool
) -> None:
inline_patch = (
f"--- a/sample.txt\\n"
f"+++ b/sample.txt\\n"
"@@ -1 +1 @@\\n"
"-old\\n"
"+new\\n"
)
command = parse_manage_file_command(
[
"patch",
"sample.txt",
"--content",
inline_patch,
]
)
assert command.content is not None
assert command.content.splitlines()[0] == f"--- a/sample.txt"
assert command.content.splitlines()[-1] == "+new"
assert command.escape_profile == "auto"
@pytest.mark.parametrize("use_real_dependencies", [False, True])
def test_parse_manage_file_preserves_backslash_leading_lines(
tmp_path: Path, use_real_dependencies: bool
) -> None:
inline_patch = (
f"--- a/sample.txt\\n"
f"+++ b/sample.txt\\n"
"@@ -1,2 +1,2 @@\\n"
" alpha\\n"
"-beta\\n"
"\\ No newline at end of file\\n"
"+beta-updated\\n"
"\\ No newline at end of file\\n"
)
command = parse_manage_file_command(
[
"patch",
"sample.txt",
"--content",
inline_patch,
]
)
assert command.content is not None
assert "\\ No newline at end of file" in command.content
assert command.escape_profile == "auto"
@pytest.mark.parametrize("use_real_dependencies", [False, True])
def test_parse_manage_file_escape_profile_none_disables_decoding(
tmp_path: Path, use_real_dependencies: bool
) -> None:
inline_patch = "line-one\\nline-two"
command = parse_manage_file_command(
[
"append",
"sample.txt",
"--content",
inline_patch,
"--escape-profile",
"none",
]
)
assert command.escape_profile == "none"
assert command.content == inline_patch
assert command.pattern is None
@pytest.mark.parametrize("use_real_dependencies", [False, True])
def test_parse_manage_file_rejects_unknown_escape_profile(
tmp_path: Path, use_real_dependencies: bool
) -> None:
with pytest.raises(FileCommandParseError):
parse_manage_file_command(
[
"append",
"sample.txt",
"--content",
"text",
"--escape-profile",
"unknown-profile",
]
)
@pytest.mark.parametrize("use_real_dependencies", [False, True])
def test_parse_manage_file_prepend_requires_content(
tmp_path: Path, use_real_dependencies: bool
) -> None:
with pytest.raises(FileCommandParseError):
parse_manage_file_command(["prepend", "story.txt"])
@pytest.mark.parametrize("use_real_dependencies", [False, True])
def test_parse_manage_file_substitute_literal(tmp_path: Path, use_real_dependencies: bool) -> None:
command = parse_manage_file_command(
[
"substitute",
"story.txt",
"--pattern",
"alpha",
"--content",
"beta",
]
)
assert command.operation == "substitute"
assert command.pattern == "alpha"
assert command.content == "beta"
assert command.use_regex is False
assert command.ignore_case is False
assert command.max_replacements is None
@pytest.mark.parametrize("use_real_dependencies", [False, True])
def test_parse_manage_file_substitute_from_file(
tmp_path: Path, use_real_dependencies: bool
) -> None:
pattern_path = tmp_path / "pattern.txt"
pattern_path.write_text("needle", encoding="utf-8")
command = parse_manage_file_command(
[
"substitute",
"story.txt",
"--pattern-from-file",
str(pattern_path),
"--content",
"replacement",
"--ignore-case",
"--max-replacements",
"2",
]
)
assert command.pattern == "needle"
assert command.ignore_case is True
assert command.max_replacements == 2
assert command.use_regex is False
@pytest.mark.parametrize("use_real_dependencies", [False, True])
def test_parse_manage_file_substitute_regex(tmp_path: Path, use_real_dependencies: bool) -> None:
command = parse_manage_file_command(
[
"substitute",
"story.txt",
"--pattern",
"(?P<word>alpha)",
"--content",
"{word}",
"--regex",
]
)
assert command.use_regex is True
assert command.pattern == "(?P<word>alpha)"
assert command.content == "{word}"
@pytest.mark.parametrize("use_real_dependencies", [False, True])
def test_parse_manage_file_insert_with_anchor(tmp_path: Path, use_real_dependencies: bool) -> None:
command = parse_manage_file_command(
[
"insert",
"story.txt",
"--content",
"new-line\n",
"--anchor",
"beta",
"--anchor-after",
"--anchor-occurrence",
"2",
]
)
assert command.operation == "insert"
assert command.anchor_text == "beta"
assert command.anchor_after is True
assert command.anchor_occurrence == 2
assert command.line is None
@pytest.mark.parametrize("use_real_dependencies", [False, True])
def test_parse_manage_file_insert_anchor_regex(tmp_path: Path, use_real_dependencies: bool) -> None:
command = parse_manage_file_command(
[
"insert",
"story.txt",
"--content",
"new-line\n",
"--anchor",
r"^alpha$",
"--anchor-regex",
"--anchor-ignore-case",
]
)
assert command.anchor_use_regex is True
assert command.anchor_ignore_case is True
assert command.anchor_occurrence == 1
@pytest.mark.parametrize("use_real_dependencies", [False, True])
def test_parse_manage_file_insert_anchor_validation(
tmp_path: Path, use_real_dependencies: bool
) -> None:
with pytest.raises(FileCommandParseError):
parse_manage_file_command(
[
"insert",
"story.txt",
"--content",
"value",
"--line",
"2",
"--anchor",
"beta",
]
)
with pytest.raises(FileCommandParseError):
parse_manage_file_command(
[
"insert",
"story.txt",
"--content",
"value",
"--anchor-occurrence",
"0",
]
)
with pytest.raises(FileCommandParseError):
parse_manage_file_command(
[
"insert",
"story.txt",
"--content",
"value",
"--anchor-after",
]
)
@pytest.mark.parametrize("use_real_dependencies", [False, True])
def test_parse_manage_file_substitute_rejects_invalid_usage(
tmp_path: Path, use_real_dependencies: bool
) -> None:
with pytest.raises(FileCommandParseError):
parse_manage_file_command(
[
"write",
"story.txt",
"--pattern",
"needle",
]
)
with pytest.raises(FileCommandParseError):
parse_manage_file_command(
[
"replace",
"story.txt",
"--start-line",
"1",
"--content",
"alpha",
"--max-replacements",
"3",
]
)
with pytest.raises(FileCommandParseError):
parse_manage_file_command(
[
"substitute",
"story.txt",
"--pattern",
"needle",
"--content",
"replacement",
"--max-replacements",
"0",
]
)