mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-09-10 10:12:20 +02:00
action_tidy_research took an `owner` argument and never used it. Any user's scheduled tidy task swept data/deep_research globally, unlinking every empty or unparseable file regardless of who owned it. A broken file has no readable owner stamp, so it cannot be matched against `owner` the way _find_owned_research_path does, which is why the HTTP path and manage_research already treat parse failure as not-owned. Clearing one is a privileged act rather than an ownership one, so gate it on the canonical owner_is_admin_or_single_user helper: admins and the single-user operator keep the janitor, a regular user does not, and neither does the pre-setup window before an admin exists. Returns before the directory glob rather than filtering inside the loop, so a denied run reports why instead of reporting "none broken" over files it never inspected. That reason string surfaces in Activity as a skipped row.
160 lines
5.3 KiB
Python
160 lines
5.3 KiB
Python
"""Owner-scope tests for action_tidy_research.
|
|
|
|
Broken research files (empty or unparseable JSON) have no readable owner
|
|
stamp. The HTTP path and _find_owned_research_path treat parse failure as
|
|
not-owned, so a regular user's tidy task must not unlink them. Clearing a
|
|
corrupt record is a privileged act instead: admins, and the operator in
|
|
single-user mode (AUTH_ENABLED=false), keep the janitor.
|
|
|
|
Every case installs a fake AuthManager rather than relying on ambient state.
|
|
Setting AUTH_ENABLED=true alone leaves is_configured False, so the admin check
|
|
these tests exist to pin would never be reached.
|
|
"""
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from src.builtin_actions import TaskNoop, action_tidy_research
|
|
|
|
|
|
@pytest.fixture
|
|
def research_dir(tmp_path, monkeypatch):
|
|
data_dir = tmp_path / "deep_research"
|
|
data_dir.mkdir()
|
|
monkeypatch.setattr("src.builtin_actions.DEEP_RESEARCH_DIR", str(data_dir))
|
|
return data_dir
|
|
|
|
|
|
def _install_auth(monkeypatch, *, is_configured: bool):
|
|
"""Auth enabled, with `root` as the only admin. `is_configured` False models
|
|
the pre-setup window, where no admin exists yet."""
|
|
|
|
import core.auth as core_auth
|
|
|
|
configured = is_configured
|
|
|
|
class FakeAuthManager:
|
|
is_configured = configured
|
|
|
|
def is_admin(self, user):
|
|
return user == "root"
|
|
|
|
monkeypatch.setenv("AUTH_ENABLED", "true")
|
|
monkeypatch.setattr(core_auth, "AuthManager", FakeAuthManager)
|
|
|
|
|
|
@pytest.fixture
|
|
def configured_auth(monkeypatch):
|
|
_install_auth(monkeypatch, is_configured=True)
|
|
|
|
|
|
def _write(data_dir, name: str, text: str):
|
|
path = data_dir / f"{name}.json"
|
|
path.write_text(text, encoding="utf-8")
|
|
return path
|
|
|
|
|
|
def _write_owned(data_dir, name: str, owner: str):
|
|
return _write(data_dir, name, json.dumps({"owner": owner, "query": name}))
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_configured_non_admin_skips_unparseable_and_empty_files(
|
|
research_dir, configured_auth
|
|
):
|
|
alice = _write_owned(research_dir, "alice-ok", "alice")
|
|
bob = _write_owned(research_dir, "bob-ok", "bob")
|
|
empty = _write(research_dir, "broken-empty", "")
|
|
garbage = _write(research_dir, "broken-json", "{not json")
|
|
alice_txt = alice.read_text(encoding="utf-8")
|
|
bob_txt = bob.read_text(encoding="utf-8")
|
|
|
|
with pytest.raises(TaskNoop, match="not permitted"):
|
|
await action_tidy_research("alice")
|
|
|
|
assert alice.exists()
|
|
assert alice.read_text(encoding="utf-8") == alice_txt
|
|
assert bob.exists()
|
|
assert bob.read_text(encoding="utf-8") == bob_txt
|
|
assert empty.exists(), "empty file has no owner stamp; non-admin tidy must not unlink it"
|
|
assert garbage.exists(), "unparseable file has no owner stamp; non-admin tidy must not unlink it"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_configured_non_admin_skip_reason_does_not_claim_none_broken(
|
|
research_dir, configured_auth
|
|
):
|
|
"""The skip reason reaches Activity as `skipped -- <reason>`, so it must not
|
|
report a clean scan over files it declined to look at."""
|
|
_write(research_dir, "broken-empty", "")
|
|
|
|
with pytest.raises(TaskNoop) as excinfo:
|
|
await action_tidy_research("alice")
|
|
|
|
assert "none broken" not in str(excinfo.value)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_configured_admin_still_removes_broken_files(research_dir, configured_auth):
|
|
valid = _write_owned(research_dir, "ok", "alice")
|
|
empty = _write(research_dir, "broken-empty", " ")
|
|
garbage = _write(research_dir, "broken-json", "not-json")
|
|
|
|
message, ok = await action_tidy_research("root")
|
|
|
|
assert ok is True
|
|
assert "Removed 2" in message
|
|
assert valid.exists(), "admin sweep clears corrupt records, not valid ones"
|
|
assert not empty.exists()
|
|
assert not garbage.exists()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pre_setup_denies_owner_who_would_be_admin_once_configured(
|
|
research_dir, monkeypatch
|
|
):
|
|
"""Auth on, no admin created yet: fail closed even for `root`.
|
|
|
|
The owner here is non-empty and would satisfy is_admin, so this fails if the
|
|
is_configured guard is removed. An empty owner would not discriminate -- it
|
|
is denied by the `owner and ...` short-circuit either way.
|
|
"""
|
|
_install_auth(monkeypatch, is_configured=False)
|
|
empty = _write(research_dir, "broken-empty", "")
|
|
garbage = _write(research_dir, "broken-json", "{")
|
|
|
|
with pytest.raises(TaskNoop, match="not permitted"):
|
|
await action_tidy_research("root")
|
|
|
|
assert empty.exists()
|
|
assert garbage.exists()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_empty_owner_does_not_delete_broken_files(research_dir, configured_auth):
|
|
empty = _write(research_dir, "broken-empty", "")
|
|
garbage = _write(research_dir, "broken-json", "{")
|
|
|
|
with pytest.raises(TaskNoop, match="not permitted"):
|
|
await action_tidy_research("")
|
|
|
|
assert empty.exists()
|
|
assert garbage.exists()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_auth_disabled_still_removes_broken_files(research_dir, monkeypatch):
|
|
monkeypatch.setenv("AUTH_ENABLED", "false")
|
|
valid = _write_owned(research_dir, "ok", "alice")
|
|
empty = _write(research_dir, "broken-empty", " ")
|
|
garbage = _write(research_dir, "broken-json", "not-json")
|
|
|
|
message, ok = await action_tidy_research("alice")
|
|
|
|
assert ok is True
|
|
assert "Removed 2" in message
|
|
assert valid.exists()
|
|
assert not empty.exists()
|
|
assert not garbage.exists()
|