99 lines
3.1 KiB
Python
99 lines
3.1 KiB
Python
"""next_iteration mutators per content class.
|
|
|
|
Stage 3b — read-modify-write contract: each editor takes a previous
|
|
body and returns a plausible next iteration. Append-only for logs;
|
|
small in-place edits for user content.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import random
|
|
|
|
import pytest
|
|
|
|
from decnet.realism.bodies import next_iteration
|
|
from decnet.realism.taxonomy import ContentClass
|
|
|
|
|
|
def test_todo_edit_can_flip_an_unchecked_box() -> None:
|
|
prev = "- [ ] rotate keys\n- [ ] review pr\n"
|
|
seen_flip = False
|
|
for seed in range(40):
|
|
new = next_iteration(
|
|
ContentClass.TODO, "admin", prev, rand=random.Random(seed),
|
|
)
|
|
if "[x]" in new and "rotate" in new and "[x] rotate" in new:
|
|
seen_flip = True
|
|
if "[x]" in new and "[x] review" in new:
|
|
seen_flip = True
|
|
if seen_flip:
|
|
break
|
|
assert seen_flip, "no checkbox flip across 40 seeds — mutator broken"
|
|
|
|
|
|
def test_todo_edit_grows_or_holds_line_count() -> None:
|
|
prev = "- [ ] rotate keys\n"
|
|
new = next_iteration(
|
|
ContentClass.TODO, "admin", prev, rand=random.Random(0),
|
|
)
|
|
# Mutators may flip a box (same line count) or append (more lines)
|
|
# — but never shrink the file.
|
|
assert len(new.splitlines()) >= len(prev.splitlines())
|
|
|
|
|
|
def test_log_cron_edit_is_append_only() -> None:
|
|
prev = (
|
|
"Apr 27 09:00:01 hostname CRON[1234]: (root) CMD (run-parts /etc/cron.daily)\n"
|
|
)
|
|
new = next_iteration(
|
|
ContentClass.LOG_CRON, "admin", prev, rand=random.Random(0),
|
|
)
|
|
assert new.startswith(prev.rstrip())
|
|
assert len(new.splitlines()) > len(prev.splitlines())
|
|
|
|
|
|
def test_log_daemon_edit_is_append_only() -> None:
|
|
prev = "Apr 27 09:00:01 hostname systemd[1]: Started Daily apt download activities.\n"
|
|
new = next_iteration(
|
|
ContentClass.LOG_DAEMON, "admin", prev, rand=random.Random(0),
|
|
)
|
|
assert new.startswith(prev.rstrip())
|
|
|
|
|
|
def test_note_edit_grows_the_body() -> None:
|
|
prev = "remember to ping the on-call\n"
|
|
new = next_iteration(
|
|
ContentClass.NOTE, "admin", prev, rand=random.Random(0),
|
|
)
|
|
assert prev in new
|
|
assert len(new) > len(prev)
|
|
|
|
|
|
def test_draft_edit_appends_paragraph() -> None:
|
|
prev = "Hi team,\n\nQuick update.\n"
|
|
new = next_iteration(
|
|
ContentClass.DRAFT, "admin", prev, rand=random.Random(0),
|
|
)
|
|
assert new.startswith(prev.rstrip())
|
|
assert len(new) > len(prev)
|
|
|
|
|
|
def test_script_edit_appends_comment() -> None:
|
|
prev = "#!/usr/bin/env bash\nset -e\necho 'hi'\n"
|
|
new = next_iteration(
|
|
ContentClass.SCRIPT, "admin", prev, rand=random.Random(0),
|
|
)
|
|
assert new.startswith(prev.rstrip())
|
|
# New tail must be a comment (the editor's contract); never a
|
|
# silently-injected new exec line.
|
|
new_tail = new[len(prev.rstrip()):].strip()
|
|
assert new_tail.startswith("#")
|
|
|
|
|
|
@pytest.mark.parametrize("cls", [
|
|
ContentClass.CACHE_TMP, ContentClass.EMAIL,
|
|
ContentClass.CANARY_AWS_CREDS, ContentClass.CANARY_HONEYDOC,
|
|
])
|
|
def test_unsupported_classes_raise_in_edit(cls: ContentClass) -> None:
|
|
with pytest.raises(KeyError):
|
|
next_iteration(cls, "admin", "anything")
|