forked from retoor/devplacepy
fix: correct "bugs" to "issues" in routing table and README references across multiple documentation files
This commit is contained in:
@@ -157,9 +157,10 @@ def test_record_never_raises(monkeypatch):
|
||||
raise RuntimeError("db exploded")
|
||||
|
||||
monkeypatch.setattr(store, "insert_event", boom)
|
||||
# must swallow the error and return None rather than propagate
|
||||
assert audit.record_system("test.unit.raise", actor_kind="system") is None
|
||||
assert audit.record(None, "test.unit.raise2", user=None) is None
|
||||
# persistence is deferred to the background task queue (inline under tests); a
|
||||
# failing write is swallowed there and must never propagate into the caller
|
||||
audit.record_system("test.unit.raise", actor_kind="system")
|
||||
audit.record(None, "test.unit.raise2", user=None)
|
||||
|
||||
|
||||
def test_record_with_none_request_uses_explicit_user():
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
@@ -0,0 +1,96 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import random
|
||||
|
||||
from devplacepy.services.bot.config import SEARCH_TERMS
|
||||
from devplacepy.services.bot.handles import (
|
||||
MAX_HANDLE_LEN,
|
||||
MIN_HANDLE_LEN,
|
||||
apply_leet,
|
||||
make_handle,
|
||||
sanitize_handle,
|
||||
)
|
||||
|
||||
SAMPLE_COUNT = 300
|
||||
|
||||
|
||||
def _is_valid_username(handle: str) -> bool:
|
||||
return (
|
||||
handle.isascii()
|
||||
and bool(handle)
|
||||
and all(character.isalnum() or character in ("-", "_") for character in handle)
|
||||
)
|
||||
|
||||
|
||||
def test_make_handle_only_valid_username_characters():
|
||||
generator = random.Random(1)
|
||||
for _ in range(SAMPLE_COUNT):
|
||||
handle = make_handle([], generator)
|
||||
assert _is_valid_username(handle), handle
|
||||
|
||||
|
||||
def test_make_handle_never_contains_dot():
|
||||
generator = random.Random(2)
|
||||
for _ in range(SAMPLE_COUNT):
|
||||
handle = make_handle(["first app", "raspberry pi"], generator)
|
||||
assert "." not in handle
|
||||
|
||||
|
||||
def test_make_handle_respects_length_bounds():
|
||||
generator = random.Random(3)
|
||||
for _ in range(SAMPLE_COUNT):
|
||||
handle = make_handle([], generator)
|
||||
assert MIN_HANDLE_LEN <= len(handle) <= MAX_HANDLE_LEN
|
||||
|
||||
|
||||
def test_make_handle_valid_for_every_persona():
|
||||
generator = random.Random(4)
|
||||
for persona, interests in SEARCH_TERMS.items():
|
||||
for _ in range(SAMPLE_COUNT):
|
||||
handle = make_handle(interests, generator)
|
||||
assert _is_valid_username(handle), (persona, handle)
|
||||
assert MIN_HANDLE_LEN <= len(handle) <= MAX_HANDLE_LEN
|
||||
|
||||
|
||||
def test_make_handle_is_deterministic_for_seeded_rng():
|
||||
first = [make_handle(["rust", "kubernetes"], random.Random(99)) for _ in range(20)]
|
||||
second = [make_handle(["rust", "kubernetes"], random.Random(99)) for _ in range(20)]
|
||||
assert first == second
|
||||
|
||||
|
||||
def test_make_handle_produces_variety():
|
||||
generator = random.Random(5)
|
||||
handles = {make_handle([], generator) for _ in range(SAMPLE_COUNT)}
|
||||
assert len(handles) > SAMPLE_COUNT // 2
|
||||
|
||||
|
||||
def test_sanitize_handle_strips_dots_and_invalid_characters():
|
||||
assert sanitize_handle("john.smith.dev!!") == "johnsmithdev"
|
||||
assert sanitize_handle("hello world") == "helloworld"
|
||||
assert sanitize_handle("a@b#c$d") == "abcd"
|
||||
|
||||
|
||||
def test_sanitize_handle_collapses_and_trims_separators():
|
||||
assert sanitize_handle("__null__byte__") == "null_byte"
|
||||
assert sanitize_handle("--seg--fault--") == "seg-fault"
|
||||
|
||||
|
||||
def test_sanitize_handle_caps_length():
|
||||
assert len(sanitize_handle("a" * 50)) == MAX_HANDLE_LEN
|
||||
|
||||
|
||||
def test_sanitize_handle_empty_input():
|
||||
assert sanitize_handle("...") == ""
|
||||
assert sanitize_handle("") == ""
|
||||
|
||||
|
||||
def test_apply_leet_only_substitutes_mapped_characters():
|
||||
generator = random.Random(7)
|
||||
result = apply_leet("xyzq", generator)
|
||||
assert result == "xyzq"
|
||||
|
||||
|
||||
def test_apply_leet_can_substitute_known_characters():
|
||||
always = random.Random(0)
|
||||
seen = {apply_leet("aeiost", always) for _ in range(50)}
|
||||
assert any(character.isdigit() for handle in seen for character in handle)
|
||||
@@ -0,0 +1,50 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import json
|
||||
|
||||
from devplacepy.services.bot.handles import MAX_HANDLE_LEN
|
||||
from devplacepy.services.bot.llm import LLMClient
|
||||
|
||||
|
||||
def _client(reply: str) -> LLMClient:
|
||||
client = LLMClient(
|
||||
api_key="test",
|
||||
api_url="http://example.invalid",
|
||||
model="molodetz",
|
||||
input_cost_per_1m=0.0,
|
||||
output_cost_per_1m=0.0,
|
||||
)
|
||||
client._raw_call = lambda system, prompt, temperature=1.0: reply
|
||||
return client
|
||||
|
||||
|
||||
def test_generate_handle_candidates_parses_and_sanitizes():
|
||||
reply = json.dumps({"handles": ["null_byte", "john.smith", "seg fault!!", "c0d3r"]})
|
||||
handles = _client(reply).generate_handle_candidates("grumpy_senior")
|
||||
assert handles == ["null_byte", "johnsmith", "segfault", "c0d3r"]
|
||||
|
||||
|
||||
def test_generate_handle_candidates_dedupes_case_insensitively():
|
||||
reply = json.dumps({"handles": ["NullByte", "nullbyte", "Daemon", "daemon"]})
|
||||
handles = _client(reply).generate_handle_candidates("minimalist")
|
||||
assert [handle.lower() for handle in handles] == ["nullbyte", "daemon"]
|
||||
|
||||
|
||||
def test_generate_handle_candidates_drops_empty_results():
|
||||
reply = json.dumps({"handles": ["", "...", "!!!", "kernelpanic"]})
|
||||
handles = _client(reply).generate_handle_candidates("rebel")
|
||||
assert handles == ["kernelpanic"]
|
||||
|
||||
|
||||
def test_generate_handle_candidates_caps_length():
|
||||
reply = json.dumps({"handles": ["x" * 40]})
|
||||
handles = _client(reply).generate_handle_candidates("mentor")
|
||||
assert len(handles[0]) == MAX_HANDLE_LEN
|
||||
|
||||
|
||||
def test_generate_handle_candidates_handles_bad_json():
|
||||
assert _client("not json at all").generate_handle_candidates("academic_type") == []
|
||||
|
||||
|
||||
def test_generate_handle_candidates_handles_missing_key():
|
||||
assert _client(json.dumps({"other": []})).generate_handle_candidates("mentor") == []
|
||||
@@ -10,7 +10,7 @@ def _msgs(text):
|
||||
|
||||
def test_channels_isolate_one_owner(local_db):
|
||||
store = ConversationStore()
|
||||
owner = f"chan-{generate_uid()[:8]}"
|
||||
owner = f"chan-{generate_uid()[-12:]}"
|
||||
store.save("user", owner, _msgs("main thread"), "main")
|
||||
store.save("user", owner, _msgs("docs thread"), "docs")
|
||||
assert store.load("user", owner, "main") == _msgs("main thread")
|
||||
@@ -19,7 +19,7 @@ def test_channels_isolate_one_owner(local_db):
|
||||
|
||||
def test_clear_one_channel_keeps_other(local_db):
|
||||
store = ConversationStore()
|
||||
owner = f"chan-{generate_uid()[:8]}"
|
||||
owner = f"chan-{generate_uid()[-12:]}"
|
||||
store.save("user", owner, _msgs("main"), "main")
|
||||
store.save("user", owner, _msgs("docs"), "docs")
|
||||
store.clear("user", owner, "docs")
|
||||
@@ -29,7 +29,7 @@ def test_clear_one_channel_keeps_other(local_db):
|
||||
|
||||
def test_save_updates_in_place_per_channel(local_db):
|
||||
store = ConversationStore()
|
||||
owner = f"chan-{generate_uid()[:8]}"
|
||||
owner = f"chan-{generate_uid()[-12:]}"
|
||||
store.save("user", owner, _msgs("first"), "docs")
|
||||
store.save("user", owner, _msgs("second"), "docs")
|
||||
assert store.load("user", owner, "docs") == _msgs("second")
|
||||
@@ -37,7 +37,7 @@ def test_save_updates_in_place_per_channel(local_db):
|
||||
|
||||
def test_default_channel_is_main(local_db):
|
||||
store = ConversationStore()
|
||||
owner = f"chan-{generate_uid()[:8]}"
|
||||
owner = f"chan-{generate_uid()[-12:]}"
|
||||
store.save("user", owner, _msgs("default"))
|
||||
assert store.load("user", owner) == _msgs("default")
|
||||
assert store.load("user", owner, "main") == _msgs("default")
|
||||
|
||||
@@ -12,13 +12,13 @@ from devplacepy.services.gitea import runtime, store
|
||||
from devplacepy.services.gitea.config import gitea_config
|
||||
from devplacepy.services.gitea.enhance import enhance_ticket
|
||||
from devplacepy.services.gitea.fake import FakeGiteaClient
|
||||
from devplacepy.services.gitea.service import BugTrackerService
|
||||
from devplacepy.services.gitea.service import IssueTrackerService
|
||||
from devplacepy.services.jobs import queue
|
||||
from devplacepy.services.jobs.bug_create_service import BugCreateService
|
||||
from devplacepy.services.jobs.issue_create_service import IssueCreateService
|
||||
from tests.conftest import run_async
|
||||
_counter_bugs_gitea = [0]
|
||||
_counter_issues_gitea = [0]
|
||||
@pytest.fixture(autouse=True)
|
||||
def _init_db_bugs_gitea():
|
||||
def _init_db_issues_gitea():
|
||||
init_db()
|
||||
yield
|
||||
@pytest.fixture
|
||||
@@ -29,18 +29,18 @@ def gitea_env():
|
||||
set_setting("gitea_owner", "retoor")
|
||||
set_setting("gitea_repo", "pydevplace")
|
||||
set_setting("gitea_token", "test-token")
|
||||
set_setting("bug_ai_enhance", "0")
|
||||
set_setting("issue_ai_enhance", "0")
|
||||
yield fake
|
||||
runtime.set_client(None)
|
||||
for table in ("bug_tickets", "bug_comment_authors"):
|
||||
for table in ("issue_tickets", "issue_comment_authors"):
|
||||
for row in list(get_table(table).find()):
|
||||
get_table(table).delete(uid=row["uid"])
|
||||
for row in list(get_table("jobs").find(kind="bug_create")):
|
||||
for row in list(get_table("jobs").find(kind="issue_create")):
|
||||
get_table("jobs").delete(uid=row["uid"])
|
||||
def _make_user_bugs_gitea():
|
||||
_counter_bugs_gitea[0] += 1
|
||||
uid = f"bugtest-user-{_counter_bugs_gitea[0]}"
|
||||
username = f"bugtester{_counter_bugs_gitea[0]}"
|
||||
def _make_user_issues_gitea():
|
||||
_counter_issues_gitea[0] += 1
|
||||
uid = f"issuetest-user-{_counter_issues_gitea[0]}"
|
||||
username = f"issuetester{_counter_issues_gitea[0]}"
|
||||
get_table("users").insert(
|
||||
{
|
||||
"uid": uid,
|
||||
@@ -56,17 +56,17 @@ def _unread(user_uid):
|
||||
return [
|
||||
n
|
||||
for n in get_table("notifications").find(user_uid=user_uid)
|
||||
if n.get("type") == "bug"
|
||||
if n.get("type") == "issue"
|
||||
]
|
||||
def _drive_jobs():
|
||||
async def run():
|
||||
svc = BugCreateService()
|
||||
svc = IssueCreateService()
|
||||
for _ in range(200):
|
||||
await svc.run_once()
|
||||
refresh_snapshot()
|
||||
pending = [
|
||||
r
|
||||
for r in get_table("jobs").find(kind="bug_create")
|
||||
for r in get_table("jobs").find(kind="issue_create")
|
||||
if r["status"] in ("pending", "running")
|
||||
]
|
||||
if not pending and not svc._inflight:
|
||||
@@ -75,7 +75,7 @@ def _drive_jobs():
|
||||
|
||||
run_async(run())
|
||||
def _role_user(role: str):
|
||||
uid, username = _make_user_bugs_gitea()
|
||||
uid, username = _make_user_issues_gitea()
|
||||
get_table("users").update({"uid": uid, "role": role}, ["uid"])
|
||||
return uid, username
|
||||
def _open_ticket(fake, role: str):
|
||||
@@ -130,7 +130,7 @@ def test_fake_client_state_and_comments(gitea_env):
|
||||
|
||||
|
||||
def test_store_records_and_maps(gitea_env):
|
||||
uid, _ = _make_user_bugs_gitea()
|
||||
uid, _ = _make_user_issues_gitea()
|
||||
store.record_ticket(7, uid, "orig", "desc", "Better", "https://x/7", "open")
|
||||
assert store.author_uid_for_issue(7) == uid
|
||||
assert store.author_map([7, 8]) == {7: uid}
|
||||
@@ -149,9 +149,9 @@ def test_enhance_fallback_when_disabled(gitea_env):
|
||||
|
||||
|
||||
def test_create_job_files_issue(gitea_env):
|
||||
uid, username = _make_user_bugs_gitea()
|
||||
uid, username = _make_user_issues_gitea()
|
||||
job_uid = queue.enqueue(
|
||||
"bug_create",
|
||||
"issue_create",
|
||||
{"author_uid": uid, "title": "Login broken", "description": "cannot log in"},
|
||||
"user",
|
||||
uid,
|
||||
@@ -171,9 +171,9 @@ def test_create_job_files_issue(gitea_env):
|
||||
|
||||
def test_create_job_fails_without_config(gitea_env):
|
||||
set_setting("gitea_token", "")
|
||||
uid, _ = _make_user_bugs_gitea()
|
||||
uid, _ = _make_user_issues_gitea()
|
||||
job_uid = queue.enqueue(
|
||||
"bug_create",
|
||||
"issue_create",
|
||||
{"author_uid": uid, "title": "x", "description": "y"},
|
||||
"user",
|
||||
uid,
|
||||
@@ -186,45 +186,45 @@ def test_create_job_fails_without_config(gitea_env):
|
||||
|
||||
|
||||
def test_poller_notifies_on_developer_reply(gitea_env):
|
||||
uid, _ = _make_user_bugs_gitea()
|
||||
issue = run_async(gitea_env.create_issue("bug", "body"))
|
||||
uid, _ = _make_user_issues_gitea()
|
||||
issue = run_async(gitea_env.create_issue("issue", "body"))
|
||||
number = issue["number"]
|
||||
store.record_ticket(number, uid, "bug", "body", "bug", issue["html_url"], "open")
|
||||
store.record_ticket(number, uid, "issue", "body", "issue", issue["html_url"], "open")
|
||||
store.update_ticket_cache(number, "open", 0)
|
||||
|
||||
gitea_env.add_external_comment(number, "developer", "looking into it")
|
||||
before = len(_unread(uid))
|
||||
run_async(BugTrackerService().run_once())
|
||||
run_async(IssueTrackerService().run_once())
|
||||
refresh_snapshot()
|
||||
assert len(_unread(uid)) == before + 1
|
||||
assert int(store.get_ticket(number)["last_comment_count"]) == 1
|
||||
|
||||
|
||||
def test_poller_ignores_local_comment(gitea_env):
|
||||
uid, _ = _make_user_bugs_gitea()
|
||||
issue = run_async(gitea_env.create_issue("bug", "body"))
|
||||
uid, _ = _make_user_issues_gitea()
|
||||
issue = run_async(gitea_env.create_issue("issue", "body"))
|
||||
number = issue["number"]
|
||||
store.record_ticket(number, uid, "bug", "body", "bug", issue["html_url"], "open")
|
||||
store.record_ticket(number, uid, "issue", "body", "issue", issue["html_url"], "open")
|
||||
store.update_ticket_cache(number, "open", 0)
|
||||
|
||||
comment = run_async(gitea_env.create_comment(number, "me again"))
|
||||
store.record_comment_author(comment["id"], number, uid)
|
||||
before = len(_unread(uid))
|
||||
run_async(BugTrackerService().run_once())
|
||||
run_async(IssueTrackerService().run_once())
|
||||
refresh_snapshot()
|
||||
assert len(_unread(uid)) == before
|
||||
|
||||
|
||||
def test_poller_notifies_on_status_change(gitea_env):
|
||||
uid, _ = _make_user_bugs_gitea()
|
||||
issue = run_async(gitea_env.create_issue("bug", "body"))
|
||||
uid, _ = _make_user_issues_gitea()
|
||||
issue = run_async(gitea_env.create_issue("issue", "body"))
|
||||
number = issue["number"]
|
||||
store.record_ticket(number, uid, "bug", "body", "bug", issue["html_url"], "open")
|
||||
store.record_ticket(number, uid, "issue", "body", "issue", issue["html_url"], "open")
|
||||
store.update_ticket_cache(number, "open", 0)
|
||||
|
||||
run_async(gitea_env.set_state(number, "closed"))
|
||||
before = len(_unread(uid))
|
||||
run_async(BugTrackerService().run_once())
|
||||
run_async(IssueTrackerService().run_once())
|
||||
refresh_snapshot()
|
||||
assert len(_unread(uid)) == before + 1
|
||||
assert store.get_ticket(number)["last_status"] == "closed"
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from devplacepy.services.jobs.deepsearch.pdf import extract_pdf_text, is_pdf
|
||||
|
||||
|
||||
def _make_pdf(message: str) -> bytes:
|
||||
content = b"BT /F1 24 Tf 72 720 Td (" + message.encode("latin-1") + b") Tj ET"
|
||||
bodies = [
|
||||
b"<< /Type /Catalog /Pages 2 0 R >>",
|
||||
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
|
||||
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] "
|
||||
b"/Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>",
|
||||
b"<< /Length " + str(len(content)).encode() + b" >>\nstream\n" + content + b"\nendstream",
|
||||
b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
|
||||
]
|
||||
pdf = bytearray(b"%PDF-1.4\n")
|
||||
offsets: list[int] = []
|
||||
for index, body in enumerate(bodies, start=1):
|
||||
offsets.append(len(pdf))
|
||||
pdf += str(index).encode() + b" 0 obj\n" + body + b"\nendobj\n"
|
||||
xref_pos = len(pdf)
|
||||
pdf += b"xref\n0 " + str(len(bodies) + 1).encode() + b"\n"
|
||||
pdf += b"0000000000 65535 f \n"
|
||||
for offset in offsets:
|
||||
pdf += ("%010d 00000 n \n" % offset).encode()
|
||||
pdf += b"trailer\n<< /Size " + str(len(bodies) + 1).encode() + b" /Root 1 0 R >>\n"
|
||||
pdf += b"startxref\n" + str(xref_pos).encode() + b"\n%%EOF"
|
||||
return bytes(pdf)
|
||||
|
||||
|
||||
def test_is_pdf_detects_content_type():
|
||||
assert is_pdf("application/pdf", "https://example.com/file", b"") is True
|
||||
assert is_pdf("application/x-pdf; charset=binary", "https://example.com/file", b"") is True
|
||||
|
||||
|
||||
def test_is_pdf_detects_url_suffix():
|
||||
assert is_pdf("text/html", "https://example.com/report.PDF", b"<html>") is True
|
||||
assert is_pdf("", "https://example.com/docs/spec.pdf?v=2", b"") is True
|
||||
|
||||
|
||||
def test_is_pdf_detects_magic_bytes():
|
||||
assert is_pdf("application/octet-stream", "https://example.com/download", b"%PDF-1.7") is True
|
||||
|
||||
|
||||
def test_is_pdf_rejects_html():
|
||||
assert is_pdf("text/html", "https://example.com/page", b"<!DOCTYPE html>") is False
|
||||
|
||||
|
||||
def test_extract_pdf_text_returns_content():
|
||||
title, text = extract_pdf_text(_make_pdf("Hello DeepSearch PDF indexing"))
|
||||
assert "Hello DeepSearch PDF indexing" in text
|
||||
|
||||
|
||||
def test_extract_pdf_text_rejects_non_pdf():
|
||||
assert extract_pdf_text(b"this is not a pdf at all") == ("", "")
|
||||
assert extract_pdf_text(b"") == ("", "")
|
||||
|
||||
|
||||
def test_extract_pdf_text_handles_corrupt_pdf():
|
||||
title, text = extract_pdf_text(b"%PDF-1.4\ngarbage that is not valid pdf structure")
|
||||
assert title == ""
|
||||
assert text == ""
|
||||
@@ -16,7 +16,7 @@ def _init_db_fork_jobs():
|
||||
@pytest.fixture
|
||||
def fork_env(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"devplacepy.services.jobs.fork_service.STAGING_DIR", tmp_path / "staging"
|
||||
"devplacepy.services.jobs.fork_service.FORK_STAGING_DIR", tmp_path / "staging"
|
||||
)
|
||||
monkeypatch.setattr("devplacepy.project_files.PROJECT_FILES_DIR", tmp_path / "pf")
|
||||
yield tmp_path
|
||||
|
||||
@@ -22,7 +22,7 @@ def zip_env(tmp_path, monkeypatch):
|
||||
"devplacepy.services.jobs.zip_service.ZIPS_DIR", tmp_path / "zips"
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"devplacepy.services.jobs.zip_service.STAGING_DIR", tmp_path / "staging"
|
||||
"devplacepy.services.jobs.zip_service.ZIP_STAGING_DIR", tmp_path / "staging"
|
||||
)
|
||||
monkeypatch.setattr("devplacepy.project_files.PROJECT_FILES_DIR", tmp_path / "pf")
|
||||
yield tmp_path
|
||||
|
||||
Reference in New Issue
Block a user