intake: several configured intake channels (mcp, folder, command) all end in quarantine/ (was mcp-upload/), and upload collect pulls the synced-folder and command channels into it (#213); 10.0.0-beta.11
Files changed: - .gitignore - CHANGES.md - VERSION - docs/explanation/architecture.md - docs/explanation/pipeline-rationale.md - docs/how-to/README.md - docs/how-to/back-up-to-a-synced-folder.md - docs/how-to/collect-from-a-synced-folder.md - docs/how-to/ingest-a-source.md - docs/how-to/set-up-the-mcp-server.md - docs/reference/commands/README.md - docs/reference/commands/git.md - docs/reference/commands/instance-health.md - docs/reference/commands/raw-material-and-uploads.md - docs/reference/configuration.md - instructions/gates.md - instructions/ingest-queue.md - instructions/mcp-read-server.md - instructions/migrations/10.0.0-settings-tiers.md - instructions/wiki-ingest/SKILL.md - raw/CONTRACT.md - tools/CONTRACT.md - tools/README.md - tools/chemenu/cli_contract.py - tools/chemenu/collect.py - tools/chemenu/commands/backup_cmd.py - tools/chemenu/commands/docs_verify.py - tools/chemenu/commands/doctor.py - tools/chemenu/commands/raw/pending.py - tools/chemenu/commands/upload_cmd.py - tools/chemenu/config.py - tools/chemenu/config_shapes.py - tools/chemenu/legacy_settings.py - tools/chemenu/mcp/server.py - tools/chemenu/reference_doc.py - tools/chemenu/tests/test_backup.py - tools/chemenu/tests/test_collect.py - tools/chemenu/tests/test_config_shapes.py - tools/chemenu/tests/test_doctor.py - tools/chemenu/tests/test_legacy_settings.py - tools/chemenu/tests/test_mcp_server.py - tools/chemenu/tests/test_upload.py - tools/chemenu/tests/test_upload_cmd.py - tools/chemenu/upload.py Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
This commit is contained in:
1 parent
3627168403
commit
cceb9a7bfe
44 files changed
+1910
-346
No files matched your search
@@ -47,7 +47,7 @@ def instance(tmp_path, monkeypatch):
|
||||
(root / "incoming").mkdir()
|
||||
(root / "incoming" / ".gitkeep").write_text("", encoding="utf-8")
|
||||
(root / ".gitignore").write_text(
|
||||
"/incoming/*\n!/incoming/.gitkeep\n/.wikitool-local.json\n/ENVIRONMENT.md\n/mcp-upload/\n"
|
||||
"/incoming/*\n!/incoming/.gitkeep\n/.wikitool-local.json\n/ENVIRONMENT.md\n/quarantine/\n"
|
||||
"/reports/\n/.wikitool-tools.json\n", encoding="utf-8")
|
||||
(root / "README.md").write_text("init\n", encoding="utf-8")
|
||||
_git(root, "add", "-A")
|
||||
@@ -222,7 +222,7 @@ def _fill(root: Path) -> None:
|
||||
(root / "incoming" / "paper.pdf").write_bytes(b"%PDF-1.7 fixture")
|
||||
(root / "incoming" / "old").mkdir()
|
||||
(root / "incoming" / "old" / "note.md").write_text("note\n", encoding="utf-8")
|
||||
for relative in ("mcp-upload/sub-1/x.md", "reports/lint.md", ".wikitool-tools.json"):
|
||||
for relative in ("quarantine/sub-1/x.md", "reports/lint.md", ".wikitool-tools.json"):
|
||||
(root / relative).parent.mkdir(parents=True, exist_ok=True)
|
||||
(root / relative).write_text("not mirrored\n", encoding="utf-8")
|
||||
(root / ".claude").mkdir()
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
"""Tests for the pulled intake channels (`chemenu/collect.py`) and `wikitool
|
||||
upload collect`: the `folder` and `command` handlers, and what the command does
|
||||
across several channels."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import typer
|
||||
|
||||
from chemenu import collect, config, upload
|
||||
from chemenu.commands.upload_cmd import upload_collect_command, upload_list_command
|
||||
|
||||
OLD = time.time() - 3600
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def root(tmp_path, monkeypatch):
|
||||
instance = tmp_path / "instance"
|
||||
instance.mkdir()
|
||||
(instance / "incoming").mkdir()
|
||||
monkeypatch.setattr(config, "ROOT", instance)
|
||||
return instance
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def drop(tmp_path):
|
||||
folder = tmp_path / "drop"
|
||||
folder.mkdir()
|
||||
return folder
|
||||
|
||||
|
||||
def _put(folder: Path, name: str, data: bytes = b"content\n", age: float = 3600) -> Path:
|
||||
path = folder / name
|
||||
path.write_bytes(data)
|
||||
stamp = time.time() - age
|
||||
os.utime(path, (stamp, stamp))
|
||||
return path
|
||||
|
||||
|
||||
def _folder_channel(drop: Path, **overrides) -> upload.Channel:
|
||||
fields = dict(
|
||||
name="onedrive", handler="folder", max_bytes=1024, allowed_extensions=(".md", ".pdf"),
|
||||
path=drop, settle_seconds=60,
|
||||
)
|
||||
fields.update(overrides)
|
||||
return upload.Channel(**fields)
|
||||
|
||||
|
||||
def _command_channel(argv: list[str], **overrides) -> upload.Channel:
|
||||
fields = dict(
|
||||
name="mail", handler="command", max_bytes=1024, allowed_extensions=(".md", ".pdf"),
|
||||
command_name="scan", command=tuple(argv),
|
||||
)
|
||||
fields.update(overrides)
|
||||
return upload.Channel(**fields)
|
||||
|
||||
|
||||
def _script(body: str) -> list[str]:
|
||||
return [sys.executable, "-c", f"import sys, pathlib\nout = pathlib.Path(sys.argv[1])\n{body}"]
|
||||
|
||||
|
||||
def _write_intake(root: Path, body: dict) -> None:
|
||||
(root / config.LOCAL_SETTINGS_FILENAME).write_text(
|
||||
json.dumps({"schema": 1, "intake": body}), encoding="utf-8")
|
||||
|
||||
|
||||
def _tree(folder: Path) -> dict[str, bytes]:
|
||||
return {str(p.relative_to(folder)): p.read_bytes() for p in sorted(folder.rglob("*")) if p.is_file()}
|
||||
|
||||
|
||||
def _manifests(root: Path) -> list[dict]:
|
||||
return upload.list_submissions(root)
|
||||
|
||||
|
||||
# --- folder -----------------------------------------------------------------
|
||||
|
||||
def test_a_folder_pull_stores_each_file_and_empties_the_source(root, drop):
|
||||
_put(drop, "a.md", b"alpha\n")
|
||||
_put(drop, "b.pdf", b"bravo\n")
|
||||
result = collect.collect(root, _folder_channel(drop))
|
||||
assert result.ok and len(result.taken) == 2
|
||||
assert _tree(drop) == {}
|
||||
manifests = {m["filename"]: m for m in _manifests(root)}
|
||||
assert set(manifests) == {"a.md", "b.pdf"}
|
||||
first = manifests["a.md"]
|
||||
assert (first["channel"], first["handler"]) == ("onedrive", "folder")
|
||||
assert first["origin"] == str(drop / "a.md")
|
||||
assert (first["submitter"], first["submitter_source"]) == ("unknown", "folder")
|
||||
assert (root / "quarantine" / first["id"] / "a.md").read_bytes() == b"alpha\n"
|
||||
|
||||
|
||||
def test_a_folder_pull_leaves_what_it_does_not_take_untouched(root, drop):
|
||||
(drop / "sub").mkdir()
|
||||
(drop / "sub" / "inner.md").write_bytes(b"inner")
|
||||
_put(drop, ".x.pdf", b"hidden")
|
||||
_put(drop, "~$a.docx", b"lock")
|
||||
_put(drop, "notes.exe", b"bad extension")
|
||||
_put(drop, "big.md", b"x" * 2000)
|
||||
target = _put(drop, "real.md", b"target")
|
||||
link = drop / "link.md"
|
||||
link.symlink_to(target)
|
||||
target.unlink()
|
||||
before = _tree(drop)
|
||||
result = collect.collect(root, _folder_channel(drop))
|
||||
assert result.ok and result.taken == [] and _manifests(root) == []
|
||||
assert _tree(drop) == before
|
||||
assert link.is_symlink()
|
||||
reasons = dict(result.skipped)
|
||||
assert reasons["sub"] == "subdirectory"
|
||||
assert reasons[".x.pdf"] == "hidden" and reasons["~$a.docx"] == "hidden"
|
||||
assert reasons["notes.exe"] == "extension not allowed"
|
||||
assert reasons["link.md"] == "symbolic link"
|
||||
assert "max_bytes" in reasons["big.md"]
|
||||
|
||||
|
||||
def test_a_file_younger_than_settle_seconds_waits_for_the_next_run(root, drop):
|
||||
_put(drop, "fresh.md", b"arriving", age=5)
|
||||
result = collect.collect(root, _folder_channel(drop, settle_seconds=60))
|
||||
assert result.taken == [] and "still in motion" in dict(result.skipped)["fresh.md"]
|
||||
assert (drop / "fresh.md").read_bytes() == b"arriving"
|
||||
os.utime(drop / "fresh.md", (OLD, OLD))
|
||||
later = collect.collect(root, _folder_channel(drop, settle_seconds=60))
|
||||
assert [name for name, _ in later.taken] == ["fresh.md"]
|
||||
|
||||
|
||||
def test_a_source_that_changes_during_the_copy_is_kept_and_nothing_is_stored(root, drop, monkeypatch):
|
||||
source = _put(drop, "a.md", b"one\n")
|
||||
monkeypatch.setattr(collect, "source_matches", lambda *args: False)
|
||||
result = collect.collect(root, _folder_channel(drop))
|
||||
assert result.taken == [] and "changed while it was copied" in dict(result.skipped)["a.md"]
|
||||
assert source.read_bytes() == b"one\n"
|
||||
assert _manifests(root) == []
|
||||
assert [p for p in (root / "quarantine").iterdir() if p.is_dir()] == []
|
||||
|
||||
|
||||
def test_a_duplicate_is_reported_with_the_existing_id_and_the_source_is_removed(root, drop):
|
||||
_put(drop, "a.md", b"same\n")
|
||||
first = collect.collect(root, _folder_channel(drop))
|
||||
existing = first.taken[0][1]
|
||||
_put(drop, "again.md", b"same\n")
|
||||
second = collect.collect(root, _folder_channel(drop))
|
||||
assert second.taken == [] and second.duplicates == [("again.md", existing)]
|
||||
assert not (drop / "again.md").exists()
|
||||
assert len(_manifests(root)) == 1
|
||||
|
||||
|
||||
def test_a_missing_folder_fails_the_channel(root, tmp_path):
|
||||
result = collect.collect(root, _folder_channel(tmp_path / "gone"))
|
||||
assert not result.ok and "does not exist" in result.error
|
||||
|
||||
|
||||
# --- command ----------------------------------------------------------------
|
||||
|
||||
def test_a_command_hands_over_the_files_it_left_in_the_staging_directory(root):
|
||||
channel = _command_channel(_script("(out / 'mail.md').write_bytes(b'from a script\\n')"))
|
||||
result = collect.collect(root, channel)
|
||||
assert result.ok and [name for name, _ in result.taken] == ["mail.md"]
|
||||
manifest = _manifests(root)[0]
|
||||
assert (manifest["channel"], manifest["handler"], manifest["origin"]) == ("mail", "command", "command:scan")
|
||||
assert (manifest["submitter"], manifest["submitter_source"]) == ("unknown", "command")
|
||||
assert not (root / "quarantine" / ".staging").exists()
|
||||
|
||||
|
||||
def test_a_failing_command_takes_nothing_and_shows_its_stderr_tail(root):
|
||||
channel = _command_channel(_script(
|
||||
"(out / 'half.md').write_bytes(b'x')\nsys.stderr.write('boom ' * 600 + 'THE END')\nsys.exit(3)"))
|
||||
result = collect.collect(root, channel)
|
||||
assert not result.ok and result.taken == []
|
||||
assert "exit 3" in result.error and result.error.rstrip().endswith("THE END")
|
||||
assert len(result.error) < 2500
|
||||
assert _manifests(root) == []
|
||||
assert not (root / "quarantine" / ".staging").exists()
|
||||
|
||||
|
||||
def test_a_command_that_times_out_fails_its_channel_and_cleans_up(root, monkeypatch):
|
||||
monkeypatch.setattr(collect, "COMMAND_TIMEOUT_SECONDS", 1)
|
||||
channel = _command_channel(_script("(out / 'a.md').write_bytes(b'x')\nimport time\ntime.sleep(30)"))
|
||||
result = collect.collect(root, channel)
|
||||
assert not result.ok and "did not finish" in result.error
|
||||
assert _manifests(root) == []
|
||||
assert not (root / "quarantine" / ".staging").exists()
|
||||
|
||||
|
||||
def test_a_command_that_does_not_exist_fails_its_channel(root, tmp_path):
|
||||
result = collect.collect(root, _command_channel([str(tmp_path / "nope.sh")]))
|
||||
assert not result.ok and "does not exist" in result.error
|
||||
|
||||
|
||||
def test_a_command_result_skips_what_is_not_a_plain_allowed_file(root):
|
||||
channel = _command_channel(_script(
|
||||
"(out / 'sub').mkdir()\n(out / 'x.exe').write_bytes(b'x')\n(out / 'ok.md').write_bytes(b'ok')"))
|
||||
result = collect.collect(root, channel)
|
||||
assert [name for name, _ in result.taken] == ["ok.md"]
|
||||
assert dict(result.skipped) == {"sub": "not a regular file", "x.exe": "extension not allowed"}
|
||||
|
||||
|
||||
# --- upload collect ---------------------------------------------------------
|
||||
|
||||
def _two_channels(root, drop):
|
||||
_write_intake(root, {
|
||||
"commands": {"scan": _script("(out / 'mail.md').write_bytes(b'mail\\n')")},
|
||||
"channels": {
|
||||
"onedrive": {"handler": "folder", "path": str(drop), "max_bytes": 1024,
|
||||
"allowed_extensions": [".md"]},
|
||||
"mail": {"handler": "command", "command": "scan", "max_bytes": 1024,
|
||||
"allowed_extensions": [".md"]},
|
||||
"server": {"handler": "mcp", "max_bytes": 1024, "allowed_extensions": [".md"],
|
||||
"quota_submissions_per_day": 1, "quota_bytes_per_day": 1000},
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
def test_collect_without_a_pull_channel_has_nothing_to_do(root, capsys):
|
||||
upload_collect_command(names=None)
|
||||
assert "nothing to collect" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_collect_pulls_every_pull_channel_and_reports_per_channel(root, drop, capsys):
|
||||
_two_channels(root, drop)
|
||||
_put(drop, "a.md", b"alpha\n")
|
||||
upload_collect_command(names=None)
|
||||
out = capsys.readouterr().out
|
||||
assert "onedrive (folder)" in out and "mail (command)" in out
|
||||
assert {m["channel"] for m in _manifests(root)} == {"onedrive", "mail"}
|
||||
|
||||
|
||||
def test_collect_names_one_channel_and_leaves_the_others(root, drop):
|
||||
_two_channels(root, drop)
|
||||
_put(drop, "a.md", b"alpha\n")
|
||||
upload_collect_command(names=["mail"])
|
||||
assert {m["channel"] for m in _manifests(root)} == {"mail"}
|
||||
assert (drop / "a.md").exists()
|
||||
|
||||
|
||||
def test_a_failing_channel_does_not_stop_the_others_but_the_exit_is_1(root, drop, tmp_path, capsys):
|
||||
_two_channels(root, drop)
|
||||
body = json.loads((root / config.LOCAL_SETTINGS_FILENAME).read_text())
|
||||
body["intake"]["channels"]["onedrive"]["path"] = str(tmp_path / "gone")
|
||||
(root / config.LOCAL_SETTINGS_FILENAME).write_text(json.dumps(body), encoding="utf-8")
|
||||
with pytest.raises(typer.Exit) as excinfo:
|
||||
upload_collect_command(names=None)
|
||||
assert excinfo.value.exit_code == 1
|
||||
assert "FAILED" in capsys.readouterr().out
|
||||
assert {m["channel"] for m in _manifests(root)} == {"mail"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("names", [["nope"], ["server"], ["mail", "nope"], ["mail", "server"]])
|
||||
def test_an_unknown_or_mcp_argument_exits_1_before_any_effect(root, drop, names):
|
||||
_two_channels(root, drop)
|
||||
_put(drop, "a.md", b"alpha\n")
|
||||
before = _tree(drop)
|
||||
with pytest.raises(typer.Exit) as excinfo:
|
||||
upload_collect_command(names=names)
|
||||
assert excinfo.value.exit_code == 1
|
||||
assert _tree(drop) == before
|
||||
assert _manifests(root) == []
|
||||
|
||||
|
||||
def test_a_malformed_intake_section_exits_1(root):
|
||||
_write_intake(root, {"channels": {"a": {"handler": "folder"}}})
|
||||
with pytest.raises(typer.Exit) as excinfo:
|
||||
upload_collect_command(names=None)
|
||||
assert excinfo.value.exit_code == 1
|
||||
|
||||
|
||||
def test_the_command_refuses_while_a_legacy_quarantine_holds_submissions(root, drop):
|
||||
_two_channels(root, drop)
|
||||
_put(drop, "a.md", b"alpha\n")
|
||||
(root / "mcp-upload" / "sub-1").mkdir(parents=True)
|
||||
(root / "mcp-upload" / "sub-1" / "a.md").write_bytes(b"waiting")
|
||||
with pytest.raises(typer.Exit) as excinfo:
|
||||
upload_collect_command(names=None)
|
||||
assert excinfo.value.exit_code == 1
|
||||
assert (drop / "a.md").exists()
|
||||
with pytest.raises(typer.Exit):
|
||||
upload_list_command(json_out=False)
|
||||
|
||||
|
||||
# --- the quarantine's own .gitignore ----------------------------------------
|
||||
|
||||
def _git(root: Path, *args: str) -> str:
|
||||
return subprocess.run(["git", *args], cwd=root, check=True, capture_output=True, text=True).stdout
|
||||
|
||||
|
||||
def test_collected_material_shows_in_no_git_status_even_without_a_root_entry(root, drop):
|
||||
_git(root, "init", "-q")
|
||||
_put(drop, "a.md", b"alpha\n")
|
||||
collect.collect(root, _folder_channel(drop))
|
||||
collect.collect(root, _command_channel(_script("(out / 'm.md').write_bytes(b'mail')")))
|
||||
assert (root / "quarantine" / ".gitignore").read_text().strip() == "*"
|
||||
assert _git(root, "status", "--porcelain", "--untracked-files=all") == ""
|
||||
@@ -29,10 +29,10 @@ runner = CliRunner()
|
||||
|
||||
PACKAGE = Path(config_shapes.__file__).parent
|
||||
|
||||
UPLOAD_OK = {
|
||||
"identity_header": "X-User", "max_bytes": 1000, "allowed_extensions": [".md", ".PDF"],
|
||||
INTAKE_OK = {"channels": {"mcp": {
|
||||
"handler": "mcp", "identity_header": "X-User", "max_bytes": 1000, "allowed_extensions": [".md", ".PDF"],
|
||||
"quota_submissions_per_day": 5, "quota_bytes_per_day": 5000,
|
||||
}
|
||||
}}}
|
||||
TASKS_OK = {"provider": "caldav", "stalled_waiting_days": 14, "unpaged_project_weeks": 3,
|
||||
"someday_stale_months": 5}
|
||||
CALDAV_OK = {"url": "https://dav.example/", "username": "u", "app_password": "pw",
|
||||
@@ -78,7 +78,7 @@ def test_the_telemetry_policy_reads_the_record():
|
||||
|
||||
def test_one_record_per_section():
|
||||
assert [shape.key for shape in config_shapes.SETTINGS_SECTIONS] == [
|
||||
"telemetry", "remotes", "upload", "tasks", "superproductivity", "caldav", "sites", "fetch",
|
||||
"telemetry", "remotes", "intake", "tasks", "superproductivity", "caldav", "sites", "fetch",
|
||||
"backup",
|
||||
]
|
||||
|
||||
@@ -126,24 +126,30 @@ def _swap(monkeypatch, attr, shape):
|
||||
)
|
||||
|
||||
|
||||
def test_upload_follows_a_field_made_required_or_optional(root, monkeypatch):
|
||||
def test_intake_follows_a_field_made_required(root, monkeypatch):
|
||||
from chemenu import upload
|
||||
|
||||
original = config_shapes.UPLOAD
|
||||
write_settings(root, monkeypatch, upload=UPLOAD_OK)
|
||||
original = config_shapes.INTAKE
|
||||
write_settings(root, monkeypatch, intake=INTAKE_OK)
|
||||
assert upload.read_config(root).max_bytes == 1000
|
||||
_swap(monkeypatch, "UPLOAD", _add_required(original))
|
||||
_swap(monkeypatch, "INTAKE", _add_required(original))
|
||||
with pytest.raises(ValidationError, match="added is missing"):
|
||||
upload.read_config(root)
|
||||
|
||||
_swap(monkeypatch, "UPLOAD", original)
|
||||
(root / config.LOCAL_SETTINGS_FILENAME).unlink()
|
||||
(root / config.INSTANCE_SETTINGS_FILENAME).unlink()
|
||||
write_settings(root, monkeypatch, upload={k: v for k, v in UPLOAD_OK.items() if k != "max_bytes"})
|
||||
with pytest.raises(ValidationError):
|
||||
|
||||
def test_intake_rules_across_fields_are_checked_by_the_record(root, monkeypatch):
|
||||
from chemenu import upload
|
||||
|
||||
write_settings(root, monkeypatch, intake={"channels": {"mcp": {"handler": "mcp", "max_bytes": 1}}})
|
||||
with pytest.raises(ValidationError, match="quota_submissions_per_day"):
|
||||
upload.read_config(root)
|
||||
_swap(monkeypatch, "UPLOAD", _not_required(original, "max_bytes", 99))
|
||||
assert upload.read_config(root).max_bytes == 99
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key", ["intake.channels", "intake.commands"])
|
||||
def test_the_intake_fields_are_not_settable_by_config_set(root, key):
|
||||
result = runner.invoke(app, ["config", "set", key, "{}"])
|
||||
assert result.exit_code == 42, result.output
|
||||
assert not (root / config.LOCAL_SETTINGS_FILENAME).exists()
|
||||
|
||||
|
||||
def test_tasks_follows_a_field_made_required(root, monkeypatch):
|
||||
|
||||
@@ -350,27 +350,52 @@ def test_publish_remotes_warns_on_several_remotes_without_an_allowlist(instance)
|
||||
def test_upload_intake_ok_when_no_section(instance):
|
||||
checks = doctor.run_doctor()
|
||||
assert _status(checks, "upload-intake") == "OK"
|
||||
assert "not registered" in _detail(checks, "upload-intake")
|
||||
assert "no channel" in _detail(checks, "upload-intake")
|
||||
|
||||
|
||||
def test_upload_intake_ok_and_armed_when_section_is_valid(instance, monkeypatch):
|
||||
write_settings(config.ROOT, monkeypatch, upload={
|
||||
"max_bytes": 100, "allowed_extensions": [".md"],
|
||||
"quota_submissions_per_day": 1, "quota_bytes_per_day": 100,
|
||||
})
|
||||
_MCP = {"handler": "mcp", "max_bytes": 100, "allowed_extensions": [".md"],
|
||||
"quota_submissions_per_day": 1, "quota_bytes_per_day": 100}
|
||||
|
||||
|
||||
def test_upload_intake_names_each_channel_with_its_handler(instance, monkeypatch, tmp_path_factory):
|
||||
folder = tmp_path_factory.mktemp("synced")
|
||||
write_settings(config.ROOT, monkeypatch, intake={"channels": {
|
||||
"mcp": _MCP,
|
||||
"onedrive": {"handler": "folder", "path": str(folder), "max_bytes": 100, "allowed_extensions": [".md"]},
|
||||
}})
|
||||
checks = doctor.run_doctor()
|
||||
assert _status(checks, "upload-intake") == "OK"
|
||||
detail = _detail(checks, "upload-intake")
|
||||
assert "armed" in detail
|
||||
assert "mcp (mcp)" in detail and "onedrive (folder)" in detail
|
||||
assert "0 submission(s) waiting" in detail
|
||||
|
||||
|
||||
def test_upload_intake_warns_when_a_folder_does_not_exist(instance, monkeypatch, tmp_path_factory):
|
||||
write_settings(config.ROOT, monkeypatch, intake={"channels": {
|
||||
"onedrive": {"handler": "folder", "path": str(tmp_path_factory.mktemp("outside") / "gone"), "max_bytes": 100,
|
||||
"allowed_extensions": [".md"]},
|
||||
}})
|
||||
checks = doctor.run_doctor()
|
||||
assert _status(checks, "upload-intake") == "WARN"
|
||||
assert "onedrive" in _detail(checks, "upload-intake")
|
||||
|
||||
|
||||
def test_upload_intake_fails_on_a_malformed_section(instance, monkeypatch):
|
||||
write_settings(config.ROOT, monkeypatch, upload={"max_bytes": "many"})
|
||||
write_settings(config.ROOT, monkeypatch, intake={"channels": {"mcp": {"handler": "mcp", "max_bytes": "many"}}})
|
||||
checks = doctor.run_doctor()
|
||||
assert _status(checks, "upload-intake") == "FAIL"
|
||||
|
||||
|
||||
def test_upload_intake_fails_on_a_leftover_mcp_upload_directory(instance):
|
||||
waiting = config.ROOT / "mcp-upload" / "sub-1"
|
||||
waiting.mkdir(parents=True)
|
||||
(waiting / "a.md").write_bytes(b"waiting")
|
||||
checks = doctor.run_doctor()
|
||||
assert _status(checks, "upload-intake") == "FAIL"
|
||||
assert "mv mcp-upload quarantine" in _detail(checks, "upload-intake")
|
||||
assert (waiting / "a.md").read_bytes() == b"waiting"
|
||||
|
||||
|
||||
def test_tasks_provider_ok_when_no_section(instance):
|
||||
checks = doctor.run_doctor()
|
||||
assert _status(checks, "tasks-provider") == "OK"
|
||||
|
||||
@@ -30,6 +30,9 @@ APP_PASSWORD = "sekrit-app-password-0815"
|
||||
THRESHOLDS = {"stalled_waiting_days": 14, "unpaged_project_weeks": 3, "someday_stale_months": 5}
|
||||
|
||||
# name -> (old files by file name, what is in effect afterwards)
|
||||
_MCP_CHANNEL = {"name": "mcp", "handler": "mcp", "path": None, "settle_seconds": 60,
|
||||
"command_name": "", "command": ()}
|
||||
|
||||
CASES = {
|
||||
"telemetry-full": (
|
||||
{".wikitool-telemetry.json": {"schema": 1, "enabled": False, "max_session_bytes": 1000,
|
||||
@@ -54,7 +57,7 @@ CASES = {
|
||||
"schema": 1, "identity_header": "X-Forwarded-User", "max_bytes": 2048,
|
||||
"allowed_extensions": [".md", ".txt"],
|
||||
"quota": {"submissions_per_day": 4, "bytes_per_day": 9000}}},
|
||||
{"upload": {"identity_header": "X-Forwarded-User", "max_bytes": 2048,
|
||||
{"upload": {**_MCP_CHANNEL, "identity_header": "X-Forwarded-User", "max_bytes": 2048,
|
||||
"allowed_extensions": (".md", ".txt"), "submissions_per_day": 4,
|
||||
"bytes_per_day": 9000}},
|
||||
),
|
||||
@@ -185,7 +188,8 @@ def _expected(case: dict) -> dict:
|
||||
for key, value in case.items():
|
||||
if key == "upload" and isinstance(value, str):
|
||||
value = {
|
||||
"identity_header": _default("upload", "identity_header"),
|
||||
**_MCP_CHANNEL,
|
||||
"identity_header": upload.DEFAULT_IDENTITY_HEADER,
|
||||
"max_bytes": 100, "allowed_extensions": (".md",),
|
||||
"submissions_per_day": 1, "bytes_per_day": 5,
|
||||
}
|
||||
@@ -210,6 +214,17 @@ def test_every_shape_keeps_its_effective_value(checkout, case):
|
||||
assert _normal(_effective(checkout)) == _normal(_expected(expected))
|
||||
|
||||
|
||||
def test_the_old_upload_file_becomes_the_mcp_channel_of_the_intake_section(checkout):
|
||||
_write_old(checkout, CASES["upload-full"][0])
|
||||
legacy_settings.migrate(checkout)
|
||||
data = json.loads((checkout / config.LOCAL_SETTINGS_FILENAME).read_text(encoding="utf-8"))
|
||||
assert "upload" not in data
|
||||
assert data["intake"] == {"channels": {"mcp": {
|
||||
"handler": "mcp", "identity_header": "X-Forwarded-User", "max_bytes": 2048,
|
||||
"allowed_extensions": [".md", ".txt"],
|
||||
"quota_submissions_per_day": 4, "quota_bytes_per_day": 9000}}}
|
||||
|
||||
|
||||
# --- b: only the tools file remains; no secret in the tree or the index -------
|
||||
|
||||
|
||||
@@ -376,7 +391,7 @@ def test_the_telemetry_hook_neither_reads_nor_refuses_over_an_old_file(checkout)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key,value", [
|
||||
("upload.max_bytes", "100"),
|
||||
("intake.channels", '{"mcp": {"handler": "mcp"}}'),
|
||||
("remotes.allowed_push_urls", '["ssh://git@example.net/a.git"]'),
|
||||
])
|
||||
def test_config_set_on_a_gated_section_exits_42_and_writes_nothing(checkout, key, value):
|
||||
|
||||
@@ -9,8 +9,8 @@ The acceptance criteria that are not about a value coming back:
|
||||
importable from the server - structural, not filtered;
|
||||
- every response carries the commit it was computed from;
|
||||
- telemetry cannot land inside the served checkout;
|
||||
- the sixth tool, `submit`, exists only when the `upload` section of `.wikitool-local.json` opts a
|
||||
checkout in, writes only under `mcp-upload/`, and never resolves an
|
||||
- the sixth tool, `submit`, exists only when the `intake` section of `.wikitool-local.json` opts a
|
||||
checkout in, writes only under `quarantine/`, and never resolves an
|
||||
identity from anywhere but the request's own headers.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
@@ -44,13 +44,14 @@ from chemenu.mcp.server import ( # noqa: E402 - after the skip guard
|
||||
|
||||
_UPLOAD_CFG = {
|
||||
"schema": 1,
|
||||
"upload": {
|
||||
"intake": {"channels": {"mcp": {
|
||||
"handler": "mcp",
|
||||
"identity_header": "X-Forwarded-User",
|
||||
"max_bytes": 1024,
|
||||
"allowed_extensions": [".md"],
|
||||
"quota_submissions_per_day": 10,
|
||||
"quota_bytes_per_day": 100_000,
|
||||
},
|
||||
}}},
|
||||
}
|
||||
|
||||
|
||||
@@ -368,13 +369,38 @@ def test_submit_is_present_once_armed(corpus):
|
||||
assert names == {"search", "types", "describe_type", "lint", "status", "submit"}
|
||||
|
||||
|
||||
def test_submit_is_absent_when_the_section_has_only_pulled_channels(corpus, tmp_path_factory):
|
||||
folder = tmp_path_factory.mktemp("synced")
|
||||
section = {"schema": 1, "intake": {"channels": {"onedrive": {
|
||||
"handler": "folder", "path": str(folder), "max_bytes": 1024, "allowed_extensions": [".md"]}}}}
|
||||
(corpus / ".wikitool-local.json").write_text(json.dumps(section), encoding="utf-8")
|
||||
names = {tool.name for tool in asyncio.run(build_server(corpus, check_trace=False).list_tools())}
|
||||
assert "submit" not in names
|
||||
|
||||
|
||||
def test_submit_carries_the_limits_of_the_mcp_channel(corpus):
|
||||
_arm_upload(corpus)
|
||||
tools = asyncio.run(build_server(corpus, check_trace=False).list_tools())
|
||||
submit = next(tool for tool in tools if tool.name == "submit")
|
||||
assert "1024 bytes" in submit.description and ".md" in submit.description
|
||||
|
||||
|
||||
def test_a_leftover_mcp_upload_directory_refuses_to_build(corpus):
|
||||
_arm_upload(corpus)
|
||||
(corpus / "mcp-upload" / "sub-1").mkdir(parents=True)
|
||||
(corpus / "mcp-upload" / "sub-1" / "a.md").write_bytes(b"waiting")
|
||||
with pytest.raises(ValidationError, match="mcp-upload"):
|
||||
build_server(corpus, check_trace=False)
|
||||
assert (corpus / "mcp-upload" / "sub-1" / "a.md").read_bytes() == b"waiting"
|
||||
|
||||
|
||||
def test_a_malformed_upload_config_refuses_to_build(corpus):
|
||||
(corpus / ".wikitool-local.json").write_text("{not json", encoding="utf-8")
|
||||
with pytest.raises(ValidationError):
|
||||
build_server(corpus, check_trace=False)
|
||||
|
||||
|
||||
def test_submit_writes_only_into_mcp_upload_incoming_stays_untouched(corpus):
|
||||
def test_submit_writes_only_into_the_quarantine_incoming_stays_untouched(corpus):
|
||||
_arm_upload(corpus)
|
||||
server = build_server(corpus, check_trace=False)
|
||||
before = _tree(corpus)
|
||||
@@ -391,15 +417,16 @@ def test_submit_writes_only_into_mcp_upload_incoming_stays_untouched(corpus):
|
||||
|
||||
assert manifest["submitter"] == "alice"
|
||||
assert manifest["submitter_source"] == "X-Forwarded-User"
|
||||
assert (manifest["channel"], manifest["handler"], manifest["origin"]) == ("mcp", "mcp", "mcp:submit")
|
||||
|
||||
after = _tree(corpus)
|
||||
changed = {p for p in after if after.get(p) != before.get(p)}
|
||||
assert changed
|
||||
assert all(p.startswith("mcp-upload/") for p in changed)
|
||||
assert all(p.startswith("quarantine/") for p in changed)
|
||||
assert list((corpus / "incoming").iterdir()) == []
|
||||
|
||||
# git itself is untouched - the fixture's own committed tree (kb/, types/)
|
||||
# is unchanged, and HEAD did not move. `mcp-upload/` and the config file
|
||||
# is unchanged, and HEAD did not move. `quarantine/` and the config file
|
||||
# are new, untracked paths (this fixture carries no .gitignore of its
|
||||
# own, unlike the real repo), which `git status` reports and is exactly
|
||||
# what a real checkout's `.gitignore` would hide - not a write to git.
|
||||
|
||||
@@ -20,8 +20,10 @@ CONTENT = b"hello world, this is a test submission\n"
|
||||
CONTENT_B64 = base64.b64encode(CONTENT).decode("ascii")
|
||||
|
||||
|
||||
def _cfg(**overrides) -> upload.UploadConfig:
|
||||
def _cfg(**overrides) -> upload.Channel:
|
||||
defaults = dict(
|
||||
name="mcp",
|
||||
handler="mcp",
|
||||
identity_header="X-Forwarded-User",
|
||||
max_bytes=1024,
|
||||
allowed_extensions=(".md", ".pdf"),
|
||||
@@ -29,7 +31,7 @@ def _cfg(**overrides) -> upload.UploadConfig:
|
||||
bytes_per_day=10_000,
|
||||
)
|
||||
defaults.update(overrides)
|
||||
return upload.UploadConfig(**defaults)
|
||||
return upload.Channel(**defaults)
|
||||
|
||||
|
||||
def _tree(root: Path) -> dict[str, bytes]:
|
||||
@@ -53,15 +55,21 @@ def test_sanitize_filename_accepts_a_bare_name():
|
||||
assert upload.sanitize_filename("report.md") == "report.md"
|
||||
|
||||
|
||||
# --- read_config -------------------------------------------------------------
|
||||
# --- read_config / read_channels -----------------------------------------------
|
||||
|
||||
def test_read_config_absent_is_none(tmp_path):
|
||||
assert upload.read_config(tmp_path) is None
|
||||
assert upload.read_channels(tmp_path) == {}
|
||||
|
||||
|
||||
def _section(tmp_path, **fields):
|
||||
channel = dict({"handler": "mcp"}, **fields)
|
||||
_intake(tmp_path, {"channels": {"mcp": channel}})
|
||||
|
||||
|
||||
def _intake(tmp_path, body):
|
||||
(tmp_path / config.LOCAL_SETTINGS_FILENAME).write_text(
|
||||
json.dumps({"schema": 1, "upload": fields}), encoding="utf-8")
|
||||
json.dumps({"schema": 1, "intake": body}), encoding="utf-8")
|
||||
|
||||
|
||||
GOOD = {"max_bytes": 100, "allowed_extensions": [".md"],
|
||||
@@ -110,6 +118,7 @@ def test_read_config_valid_file_parses(tmp_path):
|
||||
_section(tmp_path, identity_header="X-User", max_bytes=2048, allowed_extensions=[".MD", ".pdf"],
|
||||
quota_submissions_per_day=3, quota_bytes_per_day=999)
|
||||
cfg = upload.read_config(tmp_path)
|
||||
assert cfg.name == "mcp" and cfg.handler == "mcp"
|
||||
assert cfg.identity_header == "X-User"
|
||||
assert cfg.max_bytes == 2048
|
||||
assert cfg.allowed_extensions == (".md", ".pdf")
|
||||
@@ -128,13 +137,70 @@ def test_read_config_refuses_a_file_of_an_earlier_version(tmp_path):
|
||||
upload.read_config(tmp_path)
|
||||
|
||||
|
||||
def test_two_mcp_channels_are_an_error(tmp_path):
|
||||
channel = dict({"handler": "mcp"}, **GOOD)
|
||||
_intake(tmp_path, {"channels": {"a": channel, "b": channel}})
|
||||
with pytest.raises(ValidationError, match="mcp"):
|
||||
upload.read_channels(tmp_path)
|
||||
|
||||
|
||||
def test_a_field_that_does_not_match_the_handler_is_an_error(tmp_path, tmp_path_factory):
|
||||
outside = tmp_path_factory.mktemp("outside")
|
||||
channel = {"handler": "folder", "path": str(outside), "max_bytes": 100, "allowed_extensions": [".md"],
|
||||
"identity_header": "X-User"}
|
||||
_intake(tmp_path, {"channels": {"f": channel}})
|
||||
with pytest.raises(ValidationError, match="identity_header"):
|
||||
upload.read_channels(tmp_path)
|
||||
|
||||
|
||||
def test_a_command_channel_needs_a_commands_entry(tmp_path):
|
||||
channel = {"handler": "command", "command": "scanner", "max_bytes": 100, "allowed_extensions": [".md"]}
|
||||
_intake(tmp_path, {"channels": {"s": channel}})
|
||||
with pytest.raises(ValidationError, match="scanner"):
|
||||
upload.read_channels(tmp_path)
|
||||
|
||||
|
||||
def test_a_folder_path_must_be_absolute_and_outside_the_instance(tmp_path, tmp_path_factory):
|
||||
base = {"handler": "folder", "max_bytes": 100, "allowed_extensions": [".md"]}
|
||||
_intake(tmp_path, {"channels": {"f": dict(base, path="relative/dir")}})
|
||||
with pytest.raises(ValidationError, match="absolute"):
|
||||
upload.read_channels(tmp_path)
|
||||
inside = tmp_path / "drop"
|
||||
_intake(tmp_path, {"channels": {"f": dict(base, path=str(inside))}})
|
||||
with pytest.raises(ValidationError, match="inside the instance"):
|
||||
upload.read_channels(tmp_path)
|
||||
outside = tmp_path_factory.mktemp("outside")
|
||||
_intake(tmp_path, {"channels": {"f": dict(base, path=str(outside))}})
|
||||
channel = upload.read_channels(tmp_path)["f"]
|
||||
assert channel.handler == "folder" and channel.path == outside
|
||||
assert channel.settle_seconds == 60
|
||||
assert upload.read_config(tmp_path) is None
|
||||
|
||||
|
||||
def test_a_leftover_mcp_upload_directory_is_refused_and_untouched(tmp_path):
|
||||
legacy = tmp_path / "mcp-upload" / "sub-1"
|
||||
legacy.mkdir(parents=True)
|
||||
(legacy / "a.md").write_bytes(b"waiting")
|
||||
before = _tree(tmp_path)
|
||||
problem = upload.legacy_dir_problem(tmp_path)
|
||||
assert "mv mcp-upload quarantine" in problem and "Move-Item" in problem
|
||||
with pytest.raises(ValidationError, match="mcp-upload"):
|
||||
upload.refuse_legacy_dir(tmp_path)
|
||||
assert _tree(tmp_path) == before
|
||||
|
||||
|
||||
def test_an_empty_mcp_upload_directory_is_no_problem(tmp_path):
|
||||
(tmp_path / "mcp-upload").mkdir()
|
||||
assert upload.legacy_dir_problem(tmp_path) is None
|
||||
|
||||
|
||||
# --- the write choke point ----------------------------------------------------
|
||||
|
||||
def test_write_primitive_refuses_a_relative_escape(tmp_path):
|
||||
with pytest.raises(ValidationError):
|
||||
upload._write_atomic_within(tmp_path, Path("../outside.txt"), b"x")
|
||||
assert not (tmp_path / "outside.txt").exists()
|
||||
assert not (tmp_path / "mcp-upload").exists()
|
||||
assert not (tmp_path / "quarantine").exists()
|
||||
|
||||
|
||||
def test_write_primitive_refuses_an_absolute_path(tmp_path):
|
||||
@@ -146,7 +212,7 @@ def test_write_primitive_refuses_an_absolute_path(tmp_path):
|
||||
def test_write_primitive_refuses_a_symlink_escape(tmp_path):
|
||||
outside = tmp_path / "outside"
|
||||
outside.mkdir()
|
||||
upload_dir = tmp_path / "mcp-upload"
|
||||
upload_dir = tmp_path / "quarantine"
|
||||
upload_dir.mkdir()
|
||||
(upload_dir / "escape").symlink_to(outside, target_is_directory=True)
|
||||
|
||||
@@ -173,12 +239,12 @@ def test_submit_happy_path_writes_manifest_and_content(tmp_path):
|
||||
assert manifest["submitter_source"] == "X-Forwarded-User"
|
||||
assert manifest["size"] == len(CONTENT)
|
||||
|
||||
submission_dir = tmp_path / "mcp-upload" / manifest["id"]
|
||||
submission_dir = tmp_path / "quarantine" / manifest["id"]
|
||||
assert (submission_dir / "report.md").read_bytes() == CONTENT
|
||||
stored_manifest = json.loads((submission_dir / "manifest.json").read_text())
|
||||
assert stored_manifest == manifest
|
||||
|
||||
ledger_lines = (tmp_path / "mcp-upload" / "ledger.jsonl").read_text().splitlines()
|
||||
ledger_lines = (tmp_path / "quarantine" / "ledger.jsonl").read_text().splitlines()
|
||||
assert len(ledger_lines) == 1
|
||||
event = json.loads(ledger_lines[0])
|
||||
assert event["event"] == "submitted"
|
||||
@@ -342,10 +408,10 @@ def test_promote_moves_file_deletes_dir_and_ledgers_accepted(tmp_path):
|
||||
|
||||
assert dest == tmp_path / "incoming" / "a.md"
|
||||
assert dest.read_bytes() == CONTENT
|
||||
assert not (tmp_path / "mcp-upload" / manifest["id"]).exists()
|
||||
assert not (tmp_path / "quarantine" / manifest["id"]).exists()
|
||||
|
||||
events = [json.loads(line) for line in
|
||||
(tmp_path / "mcp-upload" / "ledger.jsonl").read_text().splitlines()]
|
||||
(tmp_path / "quarantine" / "ledger.jsonl").read_text().splitlines()]
|
||||
assert [e["event"] for e in events] == ["submitted", "accepted"]
|
||||
|
||||
# nothing else in the tree changed
|
||||
@@ -363,7 +429,7 @@ def test_promote_refuses_when_the_file_is_missing_from_disk(tmp_path):
|
||||
defense in depth, since nothing in this module deletes a content file
|
||||
without its manifest."""
|
||||
manifest = upload.submit(tmp_path, _cfg(), filename="a.md", content_b64=CONTENT_B64, submitter="t")
|
||||
(tmp_path / "mcp-upload" / manifest["id"] / "a.md").unlink()
|
||||
(tmp_path / "quarantine" / manifest["id"] / "a.md").unlink()
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
upload.promote(tmp_path, manifest["id"])
|
||||
@@ -378,7 +444,7 @@ def test_promote_refuses_when_incoming_already_has_the_name(tmp_path):
|
||||
upload.promote(tmp_path, manifest["id"])
|
||||
|
||||
# nothing moved: quarantine still holds it, incoming/ untouched
|
||||
assert (tmp_path / "mcp-upload" / manifest["id"] / "a.md").read_bytes() == CONTENT
|
||||
assert (tmp_path / "quarantine" / manifest["id"] / "a.md").read_bytes() == CONTENT
|
||||
assert (tmp_path / "incoming" / "a.md").read_text() == "already here"
|
||||
|
||||
|
||||
@@ -388,9 +454,9 @@ def test_reject_deletes_material_and_ledgers_with_reason(tmp_path):
|
||||
manifest = upload.submit(tmp_path, _cfg(), filename="a.md", content_b64=CONTENT_B64, submitter="t")
|
||||
upload.reject(tmp_path, manifest["id"], "license unclear")
|
||||
|
||||
assert not (tmp_path / "mcp-upload" / manifest["id"]).exists()
|
||||
assert not (tmp_path / "quarantine" / manifest["id"]).exists()
|
||||
events = [json.loads(line) for line in
|
||||
(tmp_path / "mcp-upload" / "ledger.jsonl").read_text().splitlines()]
|
||||
(tmp_path / "quarantine" / "ledger.jsonl").read_text().splitlines()]
|
||||
assert events[-1]["event"] == "rejected"
|
||||
assert events[-1]["reason"] == "license unclear"
|
||||
assert events[-1]["sha256"] == manifest["sha256"]
|
||||
@@ -401,7 +467,7 @@ def test_reject_requires_a_reason(tmp_path):
|
||||
with pytest.raises(ValidationError):
|
||||
upload.reject(tmp_path, manifest["id"], "")
|
||||
# nothing deleted
|
||||
assert (tmp_path / "mcp-upload" / manifest["id"]).exists()
|
||||
assert (tmp_path / "quarantine" / manifest["id"]).exists()
|
||||
|
||||
|
||||
def test_reject_unknown_id_is_rejected(tmp_path):
|
||||
|
||||
@@ -20,7 +20,9 @@ from chemenu.commands.upload_cmd import (
|
||||
CONTENT = b"a test submission for the CLI layer\n"
|
||||
CONTENT_B64 = base64.b64encode(CONTENT).decode("ascii")
|
||||
|
||||
_CFG = upload.UploadConfig(
|
||||
_CFG = upload.Channel(
|
||||
name="mcp",
|
||||
handler="mcp",
|
||||
identity_header="X-Forwarded-User",
|
||||
max_bytes=1024,
|
||||
allowed_extensions=(".md",),
|
||||
@@ -50,7 +52,7 @@ def test_accept_without_a_token_needs_clearance(root, capsys):
|
||||
assert manifest["sha256"] in out
|
||||
assert "--confirm" in out
|
||||
# nothing moved
|
||||
assert (root / "mcp-upload" / manifest["id"]).exists()
|
||||
assert (root / "quarantine" / manifest["id"]).exists()
|
||||
assert not (root / "incoming" / manifest["filename"]).exists()
|
||||
|
||||
|
||||
@@ -59,7 +61,7 @@ def test_accept_with_a_stale_token_needs_clearance_again(root):
|
||||
with pytest.raises(typer.Exit) as excinfo:
|
||||
upload_accept_command(submission_id=manifest["id"], confirm="not-the-real-token")
|
||||
assert excinfo.value.exit_code == EXIT_NEEDS_CLEARANCE
|
||||
assert (root / "mcp-upload" / manifest["id"]).exists()
|
||||
assert (root / "quarantine" / manifest["id"]).exists()
|
||||
|
||||
|
||||
def test_accept_with_the_right_token_promotes(root):
|
||||
@@ -67,13 +69,13 @@ def test_accept_with_the_right_token_promotes(root):
|
||||
token = upload.confirm_token(manifest)
|
||||
upload_accept_command(submission_id=manifest["id"], confirm=token)
|
||||
assert (root / "incoming" / manifest["filename"]).read_bytes() == CONTENT
|
||||
assert not (root / "mcp-upload" / manifest["id"]).exists()
|
||||
assert not (root / "quarantine" / manifest["id"]).exists()
|
||||
|
||||
|
||||
def test_reject_deletes_and_needs_no_gate(root):
|
||||
manifest = _seed(root)
|
||||
upload_reject_command(submission_id=manifest["id"], reason="license unclear")
|
||||
assert not (root / "mcp-upload" / manifest["id"]).exists()
|
||||
assert not (root / "quarantine" / manifest["id"]).exists()
|
||||
|
||||
|
||||
def test_reject_without_a_reason_is_a_validation_error(root):
|
||||
@@ -81,7 +83,7 @@ def test_reject_without_a_reason_is_a_validation_error(root):
|
||||
with pytest.raises(typer.Exit) as excinfo:
|
||||
upload_reject_command(submission_id=manifest["id"], reason="")
|
||||
assert excinfo.value.exit_code == 1
|
||||
assert (root / "mcp-upload" / manifest["id"]).exists()
|
||||
assert (root / "quarantine" / manifest["id"]).exists()
|
||||
|
||||
|
||||
def test_list_json_matches_pending_manifests(root, capsys):
|
||||
|
||||
Reference in new issue
Block a user