Attribute Devii AI spend to its invoking action, fix quiz question-at-a-time review, DB API/isslop result routes, workspace docs, and drop redundant docstrings
- Route Devii-driven AI gateway cost to the action/tool that triggered it instead of a blanket "internal" bucket, so per-feature AI spend is attributable. - Fix the quiz attempt review to show one previously-answered question at a time instead of all of them at once, and stop a quiz endpoint linked from the quiz flow from responding with raw JSON. - Add DB API async query result route and AI Usage Analyzer annotated source/media routes, with traversal-safe uid/path handling and matching tests. - Add Code Farm action audit logging (plant/harvest/buy-plot/upgrade/ fertilize) and related admin workspace/services/trash/gateway route and doc touch-ups. - Drop redundant docstrings from access_tokens.py per the no-comments convention. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VL9Xn57W5UR3HZbbuuzxdK
This commit is contained in:
@@ -1,8 +1,13 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import json
|
||||
import shutil
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
from starlette.websockets import WebSocketDisconnect
|
||||
|
||||
from devplacepy.config import DBAPI_DIR
|
||||
from devplacepy.services.manager import service_manager
|
||||
|
||||
|
||||
@@ -74,3 +79,73 @@ def test_ws_non_owner_retries(client, auth):
|
||||
assert info.value.code == 4013
|
||||
finally:
|
||||
service_manager.set_lock_owner(True)
|
||||
|
||||
|
||||
def test_result_route_serves_the_persisted_rows(client, auth):
|
||||
enqueued = client.post(
|
||||
"/dbapi/query/async", json={"sql": "SELECT 1 AS n"}, headers=auth
|
||||
)
|
||||
uid = enqueued.json()["uid"]
|
||||
output_dir = DBAPI_DIR / uid
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
result = {
|
||||
"sql": "SELECT 1 AS n",
|
||||
"row_count": 1,
|
||||
"truncated": False,
|
||||
"suspicious": [],
|
||||
"rows": [{"n": 1}],
|
||||
}
|
||||
(output_dir / "result.json").write_text(json.dumps(result), encoding="utf-8")
|
||||
try:
|
||||
response = client.get(f"/dbapi/query/{uid}/result", headers=auth)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json()["rows"] == [{"n": 1}]
|
||||
finally:
|
||||
shutil.rmtree(output_dir, ignore_errors=True)
|
||||
|
||||
|
||||
def test_result_route_404s_without_a_written_result(client, auth):
|
||||
enqueued = client.post(
|
||||
"/dbapi/query/async", json={"sql": "SELECT 1 AS n"}, headers=auth
|
||||
)
|
||||
uid = enqueued.json()["uid"]
|
||||
response = client.get(f"/dbapi/query/{uid}/result", headers=auth)
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_result_route_rejects_a_traversal_uid(client, auth):
|
||||
from devplacepy.database import get_table
|
||||
|
||||
evil_uid = ".."
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
get_table("jobs").insert(
|
||||
{
|
||||
"uid": evil_uid,
|
||||
"kind": "dbquery",
|
||||
"status": "done",
|
||||
"owner_kind": "user",
|
||||
"owner_id": "traversal-owner",
|
||||
"preferred_name": "",
|
||||
"payload": "{}",
|
||||
"result": "{}",
|
||||
"error": "",
|
||||
"retry_count": 0,
|
||||
"created_at": now,
|
||||
"started_at": now,
|
||||
"completed_at": now,
|
||||
"updated_at": now,
|
||||
"duration_ms": 0,
|
||||
"last_accessed_at": "",
|
||||
"expires_at": "",
|
||||
"bytes_in": 0,
|
||||
"bytes_out": 0,
|
||||
"item_count": 0,
|
||||
}
|
||||
)
|
||||
escape_target = DBAPI_DIR.parent / "result.json"
|
||||
try:
|
||||
response = client.get(f"/dbapi/query/{evil_uid}/result", headers=auth)
|
||||
assert response.status_code in (400, 404), response.text
|
||||
assert not escape_target.exists()
|
||||
finally:
|
||||
get_table("jobs").delete(uid=evil_uid)
|
||||
|
||||
@@ -61,8 +61,10 @@ def _instance(**overrides) -> dict:
|
||||
|
||||
def test_open_workspace_requires_enabled_setting():
|
||||
set_setting("workspace_enabled", "0")
|
||||
assert can_open_workspace(_project(), {"uid": OWNER, "role": "Member"}) is False
|
||||
set_setting("workspace_enabled", "1")
|
||||
try:
|
||||
assert can_open_workspace(_project(), {"uid": OWNER, "role": "Member"}) is False
|
||||
finally:
|
||||
set_setting("workspace_enabled", "1")
|
||||
assert can_open_workspace(_project(), {"uid": OWNER, "role": "Member"}) is True
|
||||
|
||||
|
||||
@@ -98,11 +100,13 @@ def test_create_or_resume_is_idempotent():
|
||||
|
||||
def test_workspace_quota_blocks_beyond_limit():
|
||||
set_setting("workspace_max_per_user", "1")
|
||||
user = {"uid": OWNER, "username": "owner"}
|
||||
run_async(provision.ensure(_project("p-a"), user))
|
||||
with pytest.raises(WorkspaceError):
|
||||
run_async(provision.ensure(_project("p-b"), user))
|
||||
set_setting("workspace_max_per_user", "2")
|
||||
try:
|
||||
user = {"uid": OWNER, "username": "owner"}
|
||||
run_async(provision.ensure(_project("p-a"), user))
|
||||
with pytest.raises(WorkspaceError):
|
||||
run_async(provision.ensure(_project("p-b"), user))
|
||||
finally:
|
||||
set_setting("workspace_max_per_user", "2")
|
||||
|
||||
|
||||
def test_tunnel_revives_rather_than_duplicates():
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import time
|
||||
import uuid
|
||||
|
||||
import requests
|
||||
|
||||
from tests.conftest import BASE_URL
|
||||
from devplacepy.database import get_table, refresh_snapshot
|
||||
from devplacepy.services.jobs.isslop import store
|
||||
from devplacepy.utils import generate_uid
|
||||
|
||||
_counter_isslop = [0]
|
||||
|
||||
@@ -192,3 +195,103 @@ def test_guest_history_claimed_on_signup(app_server):
|
||||
assert get_table("isslop_analyses").count(uid=uid) == 1
|
||||
finally:
|
||||
_clear_isslop_data()
|
||||
|
||||
|
||||
def test_source_route_serves_the_annotated_file(app_server):
|
||||
uid = generate_uid()
|
||||
store.create_analysis(uid, "https://github.com/owner/repository", "guest", "src-owner")
|
||||
source_name = "s" + uuid.uuid4().hex[:16] + ".txt"
|
||||
store.insert_file_result(
|
||||
uid,
|
||||
{
|
||||
"path": "app.py",
|
||||
"language": "python",
|
||||
"lines": 1,
|
||||
"origin_score": 0.1,
|
||||
"quality_deficit_score": 0.1,
|
||||
"category": "human-authored",
|
||||
"signals": "[]",
|
||||
"source": source_name,
|
||||
},
|
||||
)
|
||||
media_dir = store.media_dir_for(uid)
|
||||
media_dir.mkdir(parents=True, exist_ok=True)
|
||||
(media_dir / source_name).write_text("print('hello')\n", encoding="utf-8")
|
||||
try:
|
||||
r = requests.get(
|
||||
f"{BASE_URL}/tools/isslop/{uid}/source",
|
||||
params={"path": "app.py"},
|
||||
headers=_json_headers(),
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
body = r.json()
|
||||
assert "print" in body["source"]
|
||||
assert body["path"] == "app.py"
|
||||
finally:
|
||||
store.purge_analysis(uid)
|
||||
|
||||
|
||||
def test_source_route_rejects_an_unsafe_source_token(app_server):
|
||||
uid = generate_uid()
|
||||
store.create_analysis(uid, "https://github.com/owner/repository", "guest", "src-traversal-owner")
|
||||
store.insert_file_result(
|
||||
uid,
|
||||
{
|
||||
"path": "app.py",
|
||||
"language": "python",
|
||||
"lines": 1,
|
||||
"origin_score": 0.0,
|
||||
"quality_deficit_score": 0.0,
|
||||
"category": "human-authored",
|
||||
"signals": "[]",
|
||||
"source": "../../../../etc/passwd",
|
||||
},
|
||||
)
|
||||
try:
|
||||
r = requests.get(
|
||||
f"{BASE_URL}/tools/isslop/{uid}/source",
|
||||
params={"path": "app.py"},
|
||||
headers=_json_headers(),
|
||||
)
|
||||
assert r.status_code == 404
|
||||
finally:
|
||||
store.purge_analysis(uid)
|
||||
|
||||
|
||||
def test_media_route_serves_a_thumbnail(app_server):
|
||||
uid = generate_uid()
|
||||
store.create_analysis(uid, "https://github.com/owner/repository", "guest", "media-owner")
|
||||
name = uuid.uuid4().hex[:16] + ".webp"
|
||||
store.insert_image_result(
|
||||
uid,
|
||||
{
|
||||
"path": "assets/hero.png",
|
||||
"ai_probability": 0.2,
|
||||
"grade": "n/a",
|
||||
"verdict": "uncertain",
|
||||
"image_kind": "image",
|
||||
"tells": "[]",
|
||||
"description": "",
|
||||
"thumb": name,
|
||||
},
|
||||
)
|
||||
media_dir = store.media_dir_for(uid)
|
||||
media_dir.mkdir(parents=True, exist_ok=True)
|
||||
(media_dir / name).write_bytes(b"not-a-real-webp-but-bytes")
|
||||
try:
|
||||
r = requests.get(f"{BASE_URL}/tools/isslop/{uid}/media/{name}")
|
||||
assert r.status_code == 200, r.text
|
||||
assert r.headers["content-type"].startswith("image/webp")
|
||||
assert r.content == b"not-a-real-webp-but-bytes"
|
||||
finally:
|
||||
store.purge_analysis(uid)
|
||||
|
||||
|
||||
def test_media_route_rejects_a_name_outside_the_hex_pattern(app_server):
|
||||
uid = generate_uid()
|
||||
store.create_analysis(uid, "https://github.com/owner/repository", "guest", "media-traversal-owner")
|
||||
try:
|
||||
r = requests.get(f"{BASE_URL}/tools/isslop/{uid}/media/..%2fetc%2fpasswd")
|
||||
assert r.status_code == 404, r.text
|
||||
finally:
|
||||
store.purge_analysis(uid)
|
||||
|
||||
@@ -77,7 +77,7 @@ def test_formdata_list_field_mixed_empty_and_real():
|
||||
|
||||
def test_formdata_literal_fields_are_scalar():
|
||||
"""Literal-typed fields (e.g. target_type in CommentForm) are treated as
|
||||
scalars — the origin check must not crash on typing.Literal."""
|
||||
scalars - the origin check must not crash on typing.Literal."""
|
||||
fd = FormData([
|
||||
("content", "Great comment here!"),
|
||||
("target_type", "post"),
|
||||
@@ -108,14 +108,14 @@ def test_formdata_literal_form_from_browser():
|
||||
|
||||
|
||||
def test_formdata_vote_form():
|
||||
"""value: int field with form data — string '1' is kept for Pydantic coerce."""
|
||||
"""value: int field with form data - string '1' is kept for Pydantic coerce."""
|
||||
fd = FormData([("value", "1")])
|
||||
body = _formdata_to_dict(fd, VoteForm)
|
||||
assert body["value"] == "1" # Pydantic coerces str→int
|
||||
|
||||
|
||||
def test_formdata_issue_status_literal():
|
||||
"""IssueStatusForm has Literal['open','closed'] — must not crash."""
|
||||
"""IssueStatusForm has Literal['open','closed'] - must not crash."""
|
||||
fd = FormData([("status", "open")])
|
||||
body = _formdata_to_dict(fd, IssueStatusForm)
|
||||
assert body["status"] == "open"
|
||||
|
||||
@@ -81,7 +81,7 @@ def test_issue_token_returns_expected_fields(local_db):
|
||||
|
||||
|
||||
def test_resolve_token_valid(local_db):
|
||||
"""Issue a token then resolve it — returns the correct user."""
|
||||
"""Issue a token then resolve it - returns the correct user."""
|
||||
uid = _seed_user("resolver", "resolver@test.dev")
|
||||
user = get_table("users").find_one(uid=uid)
|
||||
|
||||
@@ -239,10 +239,10 @@ def test_resolve_user_access_token_priority_after_api_key(local_db):
|
||||
"""api_key user wins over access token when the same string matches both."""
|
||||
shared_key = secrets.token_hex(32) # 64-char hex
|
||||
|
||||
# User A — owns the api_key
|
||||
# User A - owns the api_key
|
||||
uid_a = _seed_user("apikey_winner", "apikeywinner@test.dev", api_key=shared_key)
|
||||
|
||||
# User B — owns an access token with the same string as the key
|
||||
# User B - owns an access token with the same string as the key
|
||||
uid_b = _seed_user("acctok_loser", "acctokloser@test.dev")
|
||||
get_table("access_tokens").insert(
|
||||
{
|
||||
@@ -257,14 +257,14 @@ def test_resolve_user_access_token_priority_after_api_key(local_db):
|
||||
}
|
||||
)
|
||||
|
||||
# X-API-KEY path — api_key user wins
|
||||
# X-API-KEY path - api_key user wins
|
||||
request = _MockRequest(headers={"X-API-KEY": shared_key})
|
||||
resolved = _resolve_user(request)
|
||||
assert resolved is not None
|
||||
assert resolved["uid"] == uid_a
|
||||
assert resolved["username"] == "apikey_winner"
|
||||
|
||||
# Bearer path — api_key user wins
|
||||
# Bearer path - api_key user wins
|
||||
request2 = _MockRequest(headers={"Authorization": f"Bearer {shared_key}"})
|
||||
resolved2 = _resolve_user(request2)
|
||||
assert resolved2 is not None
|
||||
|
||||
@@ -68,7 +68,7 @@ def _seed_token(user_uid, key, expire_time):
|
||||
# ── resolve_user_by_key tests ────────────────────────────────────────────────
|
||||
|
||||
def test_resolve_user_by_key_valid(local_db):
|
||||
"""Create a devrant token for a user, then resolve it by key — the right user is returned."""
|
||||
"""Create a devrant token for a user, then resolve it by key - the right user is returned."""
|
||||
uid = _seed_user("dr_valid", "drvalid@test.dev")
|
||||
token_key = secrets.token_hex(20)
|
||||
expire_time = int(datetime.now(timezone.utc).timestamp()) + 86400
|
||||
@@ -81,13 +81,13 @@ def test_resolve_user_by_key_valid(local_db):
|
||||
|
||||
|
||||
def test_resolve_user_by_key_not_found(local_db):
|
||||
"""Look up a random key that has no token — verify None."""
|
||||
"""Look up a random key that has no token - verify None."""
|
||||
random_key = secrets.token_hex(20)
|
||||
assert resolve_user_by_key(random_key) is None
|
||||
|
||||
|
||||
def test_resolve_user_by_key_expired(local_db):
|
||||
"""Create an expired token — resolve returns None."""
|
||||
"""Create an expired token - resolve returns None."""
|
||||
uid = _seed_user("dr_expired", "drexpired@test.dev")
|
||||
token_key = secrets.token_hex(20)
|
||||
expire_time = int(datetime.now(timezone.utc).timestamp()) - 86400
|
||||
@@ -97,7 +97,7 @@ def test_resolve_user_by_key_expired(local_db):
|
||||
|
||||
|
||||
def test_resolve_user_by_key_inactive_user(local_db):
|
||||
"""Create a token for an inactive user — resolve returns None."""
|
||||
"""Create a token for an inactive user - resolve returns None."""
|
||||
uid = _seed_user("dr_inactive", "drinactive@test.dev", is_active=False)
|
||||
token_key = secrets.token_hex(20)
|
||||
expire_time = int(datetime.now(timezone.utc).timestamp()) + 86400
|
||||
@@ -162,10 +162,10 @@ def test_resolve_user_prefers_api_key_over_devrant(local_db):
|
||||
"""If a key matches both an api_key and a devrant token, the api_key user wins."""
|
||||
shared_key = secrets.token_hex(20)
|
||||
|
||||
# User A — owns the api_key
|
||||
# User A - owns the api_key
|
||||
uid_a = _seed_user("api_key_owner", "apikey@test.dev", api_key=shared_key)
|
||||
|
||||
# User B — owns the devrant token with the same key
|
||||
# User B - owns the devrant token with the same key
|
||||
uid_b = _seed_user("devrant_owner", "drowner@test.dev")
|
||||
expire_time = int(datetime.now(timezone.utc).timestamp()) + 86400
|
||||
_seed_token(uid_b, shared_key, expire_time)
|
||||
|
||||
Reference in New Issue
Block a user