Compare commits

..
Author SHA1 Message Date
Typosaurus a2a50e0144 ticket #65 attempt 1 2026-07-19 19:00:51 +00:00
Typosaurus c119e39813 ticket #65 attempt 1 2026-07-19 18:35:37 +00:00
Typosaurus 7cc39076bf ticket #65 attempt 1 2026-07-19 18:16:23 +00:00
25 changed files with 100 additions and 86 deletions
File diff suppressed because one or more lines are too long
+4
View File
@@ -39,6 +39,10 @@ NGINX_MAX_BODY_SIZE=50m
NGINX_CACHE_ENABLED=false
NGINX_CACHE_MAX_SIZE=1g
# Max length of comment text (applied server-side on write and client-side on input).
# Change at any time via the env var; the DB has no length constraint.
DEVPLACE_COMMENT_MAX_LENGTH=1000
# Run the app container as this host user so shared files keep dev ownership.
DEVPLACE_UID=1000
DEVPLACE_GID=1000
+1
View File
@@ -169,6 +169,7 @@ The log is **administrator-only**. `/admin/audit-log` is a paginated, filterable
| `DEVPLACE_PRESENCE_TIMEOUT_SECONDS` | `60` | Online-presence window: a user counts as online for this many seconds after their last activity. `last_seen` is refreshed by a throttled in-place update at most once per half this interval per worker (no per-load inserts, no data growth) |
| `DEVPLACE_PRESENCE_ONLINE_LIMIT` | `30` | Maximum avatars shown in the feed's live "Online now" panel (ordered alphabetically by username) |
| `DEVPLACE_PRESENCE_ONLINE_MARGIN_SECONDS` | `20` | Grace margin before an online user drops to offline (hysteresis): online at the timeout, offline only after timeout + this. Prevents online/offline flicker for users hovering at the boundary |
| `DEVPLACE_COMMENT_MAX_LENGTH` | `1000` | Maximum character length for comment body text. Applied server-side via Pydantic validation and client-side via `maxLength` on textareas. The database has no column-length constraint, so this can be changed at any time via the environment variable (requires restart) |
### Runtime settings
+1
View File
@@ -68,6 +68,7 @@ INTERNAL_GATEWAY_URL = f"{INTERNAL_BASE_URL}/openai/v1/chat/completions"
INTERNAL_EMBED_URL = f"{INTERNAL_BASE_URL}/openai/v1/embeddings"
INTERNAL_MODEL = "molodetz"
INTERNAL_EMBED_MODEL = "molodetz~embed"
COMMENT_MAX_LENGTH = int(environ.get("DEVPLACE_COMMENT_MAX_LENGTH", "1000"))
DEFAULT_CORRECTION_PROMPT = "Leave literary as is, only do punctuation and casing"
DEFAULT_MODIFIER_PROMPT = (
"Execute what is behind `@ai` (the prompt) and replace that part including `@ai`"
+3 -2
View File
@@ -1,5 +1,6 @@
# retoor <retoor@molodetz.nl>
from devplacepy.config import COMMENT_MAX_LENGTH
from .._shared import COMMENT_TARGETS, GIST_LANGUAGES, PROJECT_TYPES, endpoint, field
from devplacepy.constants import TOPICS
@@ -229,7 +230,7 @@ four ways to sign requests.
"textarea",
True,
"Nice work.",
"Body, 3-1000 characters.",
f"Body, 3-{COMMENT_MAX_LENGTH} characters.",
),
field(
"target_uid",
@@ -290,7 +291,7 @@ four ways to sign requests.
"textarea",
True,
"Edited body.",
"New body, 3-1000 characters.",
f"New body, 3-{COMMENT_MAX_LENGTH} characters.",
),
],
sample_response={
+3 -1
View File
@@ -1,5 +1,7 @@
# retoor <retoor@molodetz.nl>
from devplacepy.config import COMMENT_MAX_LENGTH
ROLE_LABELS = {"public": "Public", "user": "Member"}
@@ -237,7 +239,7 @@ DEVRANT_GROUPS = {
encoding="form",
params=[
field("rant_id", "path", type="int", required=True, example="1", description="Rant id."),
field("comment", "body", type="textarea", required=True, example="Great rant!", description="Comment text, 1-1000 chars."),
field("comment", "body", type="textarea", required=True, example="Great rant!", description=f"Comment text, 1-{COMMENT_MAX_LENGTH} chars."),
],
sample_response={"success": True},
),
+3 -3
View File
@@ -6,7 +6,7 @@ from datetime import datetime
from typing import Literal, Optional
from pydantic import BaseModel, Field, field_validator, model_validator
from devplacepy.constants import TOPICS, REACTION_EMOJI
from devplacepy.config import DEFAULT_CORRECTION_PROMPT, DEFAULT_MODIFIER_PROMPT
from devplacepy.config import COMMENT_MAX_LENGTH, DEFAULT_CORRECTION_PROMPT, DEFAULT_MODIFIER_PROMPT
def normalize_european_date(value):
@@ -157,7 +157,7 @@ class PostEditForm(BaseModel):
class CommentForm(BaseModel):
content: str = Field(min_length=3, max_length=1000)
content: str = Field(min_length=3, max_length=COMMENT_MAX_LENGTH)
target_uid: str = Field(default="", max_length=36)
post_uid: str = Field(default="", max_length=36)
target_type: Literal["post", "project", "news", "issue", "gist"] = "post"
@@ -172,7 +172,7 @@ class CommentForm(BaseModel):
class CommentEditForm(BaseModel):
content: str = Field(min_length=3, max_length=1000)
content: str = Field(min_length=3, max_length=COMMENT_MAX_LENGTH)
class ProjectForm(BaseModel):
+2 -1
View File
@@ -69,7 +69,8 @@ async def edit_comment(request: Request, comment_id: str):
if not is_owner(comment, user):
return dr_error("You can only edit your own comments.", fail_reason="not_owner")
text = (params.get("comment") or "").strip()
if len(text) < 1 or len(text) > 1000:
from devplacepy.config import COMMENT_MAX_LENGTH
if len(text) < 1 or len(text) > COMMENT_MAX_LENGTH:
return dr_error("Invalid comment length.", fail_reason="length")
get_table("comments").update(
{
+2 -1
View File
@@ -227,7 +227,8 @@ async def comment_rant(request: Request, rant_id: str):
text = (params.get("comment") or "").strip()
if len(text) < 1:
return dr_error("Your comment is too short.")
if len(text) > 1000:
from devplacepy.config import COMMENT_MAX_LENGTH
if len(text) > COMMENT_MAX_LENGTH:
return dr_error("Your comment is too long.")
create_comment_record(request, user, "post", post["uid"], text)
return dr_ok()
+1 -4
View File
@@ -227,8 +227,7 @@ def _session_context(request: Request, uid: str, job: dict, session: dict) -> di
user = get_current_user(request)
viewer_is_admin = is_admin(user)
done = bool(report) or job.get("status") == queue.DONE
cost_usd = report.get("cost_usd", 0.0) if report else 0.0
ctx = {
return {
"uid": uid,
"status": queue.DONE if done else job.get("status", ""),
"query": report.get("query") or session.get("query"),
@@ -248,7 +247,6 @@ def _session_context(request: Request, uid: str, job: dict, session: dict) -> di
"export_md_url": f"/tools/deepsearch/{uid}/export.md" if done else None,
"export_json_url": f"/tools/deepsearch/{uid}/export.json" if done else None,
"export_pdf_url": f"/tools/deepsearch/{uid}/export.pdf" if done else None,
"cost_usd": cost_usd if viewer_is_admin else None,
"viewer_is_admin": viewer_is_admin,
"viewer_owns": _owns(request, session),
"created_at": session.get("created_at"),
@@ -257,7 +255,6 @@ def _session_context(request: Request, uid: str, job: dict, session: dict) -> di
"user": user,
"meta_robots": "noindex,nofollow",
}
return ctx
@router.get("/{uid}/session")
async def deepsearch_session(request: Request, uid: str):
-1
View File
@@ -136,7 +136,6 @@ class DeepsearchSessionOut(_Out):
export_md_url: Optional[str] = None
export_json_url: Optional[str] = None
export_pdf_url: Optional[str] = None
cost_usd: Optional[float] = None
viewer_is_admin: bool = False
viewer_owns: bool = False
created_at: Optional[str] = None
+1 -1
View File
@@ -72,7 +72,7 @@ MENTION_POLL_SECONDS = int(os.environ.get("MENTION_POLL_SECONDS", "30"))
DM_POLL_SECONDS = int(os.environ.get("DM_POLL_SECONDS", "10"))
BOT_USERNAME = os.environ.get("BOT_USERNAME", "")
COMMENT_CHAR_LIMIT = 1000
COMMENT_CHAR_LIMIT = int(os.environ.get("DEVPLACE_COMMENT_MAX_LENGTH", "1000"))
MESSAGE_CHAR_LIMIT = 2000
PART_SUFFIX_RESERVE = 12
PART_DELIVERY_DELAY = 0.5
+1 -5
View File
@@ -7,7 +7,6 @@ import time
from devplacepy import stealth
from devplacepy.config import INTERNAL_GATEWAY_URL, INTERNAL_MODEL
from devplacepy.services.openai_gateway.usage import parse_usage_headers
logger = logging.getLogger(__name__)
@@ -42,10 +41,7 @@ async def request_completion(
if response.status_code >= 400:
raise RuntimeError(f"chat gateway returned {response.status_code}")
data: dict = response.json()
cost_info = parse_usage_headers(response.headers) or {}
usage = data.get("usage") or {}
usage["cost_usd"] = cost_info.get("cost_usd", 0.0)
return data, usage, elapsed_ms
return data, data.get("usage") or {}, elapsed_ms
async def complete_chat(
@@ -2,6 +2,7 @@
from __future__ import annotations
from devplacepy.config import COMMENT_MAX_LENGTH
from ..spec import Action
from ._shared import ATTACHMENTS, TARGET_TYPE, body, confirm, path
@@ -28,7 +29,7 @@ COMMENTS_ACTIONS: tuple[Action, ...] = (
summary="Edit the body of one of your own comments",
params=(
path("comment_uid", "Uid of the comment."),
body("content", "New comment body, 3-1000 characters.", required=True),
body("content", f"New comment body, 3-{COMMENT_MAX_LENGTH} characters.", required=True),
),
),
Action(
@@ -5,11 +5,10 @@ from __future__ import annotations
import json
import logging
import re
from typing import Callable, Tuple
from typing import Callable
from devplacepy import stealth
from devplacepy.config import INTERNAL_GATEWAY_URL, INTERNAL_MODEL
from devplacepy.services.openai_gateway.usage import parse_usage_headers
from .phases import PHASE_PLANNING
@@ -62,7 +61,7 @@ def _noop(frame: dict) -> None:
async def plan_queries(
query: str, api_key: str, emit: Callable[[dict], None] = _noop
) -> Tuple[list[str], float]:
) -> list[str]:
emit(
{
"type": "substep",
@@ -90,8 +89,6 @@ async def plan_queries(
)
if response.status_code >= 400:
raise RuntimeError(f"planner gateway returned {response.status_code}")
cost_info = parse_usage_headers(response.headers) or {}
planner_cost = cost_info.get("cost_usd", 0.0)
data = response.json()
content = (
(data.get("choices") or [{}])[0].get("message", {}).get("content") or ""
@@ -105,7 +102,7 @@ async def plan_queries(
"message": f"Planned {len(parsed)} search angles",
}
)
return parsed, planner_cost
return parsed
except Exception as exc:
logger.warning("deepsearch query planner failed, using fallback: %s", exc)
fallback = _fallback(query)
@@ -116,4 +113,4 @@ async def plan_queries(
"message": f"Planner unavailable, using {len(fallback)} heuristic angles",
}
)
return fallback, 0.0
return fallback
@@ -40,7 +40,6 @@ class Orchestration:
source_diversity: float = 0.0
score: int = 0
synthesis: str = "agents"
total_cost_usd: float = 0.0
def _domain(url: str) -> str:
@@ -166,7 +165,6 @@ async def _complete(
"tokens_in": int(raw_usage.get("prompt_tokens") or prompt_chars // 4),
"tokens_out": int(raw_usage.get("completion_tokens") or len(text) // 4),
"elapsed_ms": elapsed_ms,
"cost_usd": float(raw_usage.get("cost_usd", 0.0)),
}
return text, usage
@@ -298,7 +296,6 @@ def _agent_done(emit: Callable[[dict], None], agent: str, usage: dict) -> None:
"elapsed_ms": usage.get("elapsed_ms", 0),
"tokens_in": usage.get("tokens_in", 0),
"tokens_out": usage.get("tokens_out", 0),
"cost_usd": usage.get("cost_usd", 0.0),
}
)
@@ -349,7 +346,6 @@ async def orchestrate(
emit: Callable[[dict], None],
store=None,
queries: list[str] | None = None,
planner_cost_usd: float = 0.0,
) -> Orchestration:
diversity = source_diversity(pages)
if not pages:
@@ -422,12 +418,6 @@ async def orchestrate(
score = int(
min(SCORE_MAX, (confidence * 0.5 + diversity * 0.3 + coverage * 0.2) * SCORE_MAX)
)
total_cost = (
planner_cost_usd
+ float(summary_usage.get("cost_usd", 0.0))
+ float(findings_usage.get("cost_usd", 0.0))
+ float(linker_usage.get("cost_usd", 0.0))
)
return Orchestration(
summary=summary,
findings=findings,
@@ -435,7 +425,6 @@ async def orchestrate(
source_diversity=diversity,
score=score,
synthesis="agents",
total_cost_usd=total_cost,
)
except Exception as exc:
logger.warning("deepsearch orchestration failed, using heuristic: %s", exc)
@@ -165,7 +165,7 @@ async def _run(payload: dict, output_dir: Path) -> dict:
should_stop = _make_stop(output_dir)
_stage("planning", "Planning research queries", PHASE_PLANNING)
queries, planner_cost = await plan_queries(query, api_key, _emit)
queries = await plan_queries(query, api_key, _emit)
_emit({"type": "queries", "queries": queries})
_stage("searching", "Searching the web", PHASE_SEARCHING)
@@ -203,8 +203,7 @@ async def _run(payload: dict, output_dir: Path) -> dict:
_stage("analysis", "Running research agents", PHASE_ANALYSIS)
result = await orchestrate(
query, outcome.pages, api_key, _emit, store=store, queries=queries,
planner_cost_usd=planner_cost,
query, outcome.pages, api_key, _emit, store=store, queries=queries
)
_stage("synthesis", "Compiling cited report", PHASE_SYNTHESIS)
@@ -229,7 +228,6 @@ async def _run(payload: dict, output_dir: Path) -> dict:
"chunk_count": chunk_count,
"embed_backend": embed_backend,
"collection": collection,
"cost_usd": result.total_cost_usd,
}
(output_dir / "report.json").write_text(
json.dumps(report, ensure_ascii=False), encoding="utf-8"
@@ -246,7 +244,6 @@ async def _run(payload: dict, output_dir: Path) -> dict:
"synthesis": report["synthesis"],
"page_count": report["page_count"],
"chunk_count": report["chunk_count"],
"cost_usd": report["cost_usd"],
}
)
return report
+2
View File
@@ -83,6 +83,8 @@ export class CommentManager {
textarea.className = "emoji-picker-target";
textarea.value = text.dataset.raw || text.textContent;
textarea.rows = 3;
const ref = document.querySelector("[data-comment-maxlength]");
textarea.maxLength = ref ? parseInt(ref.dataset.commentMaxlength, 10) : 1000;
form.appendChild(textarea);
const actions = document.createElement("div");
+2 -2
View File
@@ -1,9 +1,9 @@
{% if user %}
<form class="comment-form" method="POST" action="/comments/create">
<form class="comment-form" method="POST" action="/comments/create" data-comment-maxlength="{{ comment_max_length }}">
<input type="hidden" name="target_uid" value="{{ _comment_target_uid }}">
<input type="hidden" name="target_type" value="{{ _comment_target_type }}">
{% set _user = user %}{% set _size = 32 %}{% set _size_class = "sm" %}{% include "_avatar_link.html" %}
<textarea name="content" placeholder="Your opinion goes here..." required aria-required="true" aria-label="Comment" maxlength="1000" class="emoji-picker-target" data-mention></textarea>
<textarea name="content" placeholder="Your opinion goes here..." required aria-required="true" aria-label="Comment" maxlength="{{ comment_max_length }}" class="emoji-picker-target" data-mention></textarea>
<div class="comment-form-actions">
<dp-upload multiple
max-size="{{ max_upload_size_mb() }}"
@@ -16,9 +16,6 @@
<span class="ds-metric"><strong>{{ source_diversity }}</strong> diversity</span>
<span class="ds-metric"><strong>{{ page_count }}</strong> sources</span>
<span class="ds-metric"><strong>{{ chunk_count }}</strong> chunks</span>
{% if viewer_is_admin and cost_usd is not none %}
<span class="ds-metric"><strong>${{ "%.6f"|format(cost_usd) }}</strong> cost</span>
{% endif %}
</div>
{% if export_md_url %}
<div class="ds-exports">
+2 -1
View File
@@ -6,7 +6,7 @@ import jinja2
from fastapi.templating import Jinja2Templates
from markupsafe import Markup, escape
from devplacepy.cache import TTLCache
from devplacepy.config import STATIC_VERSION, TEMPLATES_DIR, TEMPLATE_AUTO_RELOAD, PRESENCE_TIMEOUT_SECONDS
from devplacepy.config import COMMENT_MAX_LENGTH, STATIC_VERSION, TEMPLATES_DIR, TEMPLATE_AUTO_RELOAD, PRESENCE_TIMEOUT_SECONDS
from devplacepy.constants import TOPICS, REACTION_EMOJI
from devplacepy.database import get_int_setting, get_setting, get_table
from devplacepy.avatar import avatar_url, avatar_seed
@@ -67,6 +67,7 @@ templates.env.globals["is_self"] = is_self
templates.env.globals["guest_disabled"] = guest_disabled
templates.env.globals["is_online"] = presence.is_online
templates.env.globals["presence_timeout"] = PRESENCE_TIMEOUT_SECONDS
templates.env.globals["comment_max_length"] = COMMENT_MAX_LENGTH
from devplacepy.docs_devrant import devrant_endpoints
+28 -8
View File
@@ -1,8 +1,28 @@
2026-07-19T17:31:36 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T17:31:36 DEBUG model=molodetz-pro fps=30
2026-07-19T17:31:36 INFO read task from file: /workspace/prompts/research-10.txt
2026-07-19T17:31:36 INFO settings merged: model=<default> allow=0 deny=0 ask=0
2026-07-19T19:05:24 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T19:05:24 DEBUG model=molodetz-pro fps=30
2026-07-19T19:05:24 INFO read task from file: /workspace/prompts/execution-1.txt
2026-07-19T19:05:24 INFO settings merged: model=<default> allow=0 deny=0 ask=0
2026-07-19T08:11:05 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T08:11:05 DEBUG model=molodetz-pro fps=30
2026-07-19T08:11:05 INFO read task from file: /workspace/prompts/research-1.txt
2026-07-19T08:11:05 INFO settings merged: model=<default> allow=0 deny=0 ask=0
2026-07-19T15:03:09 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T15:03:09 DEBUG model=molodetz-pro fps=30
2026-07-19T15:03:09 INFO read task from file: /workspace/prompts/research-2.txt
2026-07-19T15:03:09 INFO settings merged: model=<default> allow=0 deny=0 ask=0
2026-07-19T15:40:16 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T15:40:16 DEBUG model=molodetz-pro fps=30
2026-07-19T15:40:16 INFO read task from file: /workspace/prompts/research-3.txt
2026-07-19T15:40:16 INFO settings merged: model=<default> allow=0 deny=0 ask=0
2026-07-19T17:35:33 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T17:35:33 DEBUG model=molodetz-pro fps=30
2026-07-19T17:35:33 INFO read task from file: /workspace/prompts/research-4.txt
2026-07-19T17:35:33 INFO settings merged: model=<default> allow=0 deny=0 ask=0
2026-07-19T18:11:47 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T18:11:47 DEBUG model=molodetz-pro fps=30
2026-07-19T18:11:47 INFO read task from file: /workspace/prompts/execution-1.txt
2026-07-19T18:11:47 INFO settings merged: model=<default> allow=0 deny=0 ask=0
2026-07-19T18:31:31 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T18:31:31 DEBUG model=molodetz-pro fps=30
2026-07-19T18:31:31 INFO read task from file: /workspace/prompts/execution-2.txt
2026-07-19T18:31:31 INFO settings merged: model=<default> allow=0 deny=0 ask=0
2026-07-19T18:58:51 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T18:58:51 DEBUG model=molodetz-pro fps=30
2026-07-19T18:58:51 INFO read task from file: /workspace/prompts/execution-3.txt
2026-07-19T18:58:51 INFO settings merged: model=<default> allow=0 deny=0 ask=0
+19
View File
@@ -345,3 +345,22 @@ def test_comment_links_multiple_attachments(app_server):
assert u1 in r.text and u2 in r.text, (
"both attachments must be linked and displayed on the comment"
)
def test_create_comment_too_long_rejected(app_server):
from devplacepy.config import COMMENT_MAX_LENGTH
s, _ = _session_comments()
post_uid = _create_post_comments(s, f"cmt-long-{int(time.time() * 1000)}")
r = s.post(
f"{BASE_URL}/comments/create",
data={
"content": "x" * (COMMENT_MAX_LENGTH + 1),
"target_type": "post",
"post_uid": post_uid,
"target_uid": post_uid,
},
allow_redirects=False,
)
assert r.status_code in (400, 422), r.text[:300]
refresh_snapshot()
assert get_table("comments").find_one(target_uid=post_uid) is None
+16
View File
@@ -129,3 +129,19 @@ def test_edit_comment_too_short_rejected(app_server):
assert r.status_code in (400, 422)
refresh_snapshot()
assert get_table("comments").find_one(uid=comment["uid"])["content"] == "Long enough body"
def test_edit_comment_too_long_rejected(app_server):
from devplacepy.config import COMMENT_MAX_LENGTH
s, _ = _session_edit()
post_uid = _create_post_edit(s, f"edit-long-{int(time.time() * 1000)}")
comment = _create_comment_edit(s, post_uid, "Normal body length")
r = s.post(
f"{BASE_URL}/comments/edit/{comment['uid']}",
headers=JSON_edit,
data={"content": "x" * (COMMENT_MAX_LENGTH + 1)},
allow_redirects=False,
)
assert r.status_code in (400, 422), r.text[:300]
refresh_snapshot()
assert get_table("comments").find_one(uid=comment["uid"])["content"] == "Normal body length"
-28
View File
@@ -39,7 +39,6 @@ def _seed_done_session(owner_id="ds-session-owner"):
"source_diversity": 0.5,
"page_count": 3,
"chunk_count": 12,
"cost_usd": 0.123456,
}
result = {
"query": "the question",
@@ -48,7 +47,6 @@ def _seed_done_session(owner_id="ds-session-owner"):
"source_diversity": 0.5,
"page_count": 3,
"chunk_count": 12,
"cost_usd": 0.123456,
"report": report,
}
get_table("jobs").update(
@@ -99,7 +97,6 @@ def test_session_json_shape(app_server):
assert body["sources"]
assert "viewer_is_admin" in body
assert "viewer_owns" in body
assert body.get("cost_usd") is None # guest user, not admin
finally:
_clear()
@@ -111,7 +108,6 @@ def test_session_html_renders_without_jinja_global_collision(app_server):
assert r.status_code == 200, r.text
assert "the question" in r.text
assert "dp-deepsearch-chat" in r.text
assert "0.123456" not in r.text # guest user, cost hidden
finally:
_clear()
@@ -146,7 +142,6 @@ def test_session_reads_disk_report_before_result_commit(app_server):
"synthesis": "agents",
"page_count": 12,
"chunk_count": 61,
"cost_usd": 0.123456,
}
session_dir = DEEPSEARCH_DIR / uid
session_dir.mkdir(parents=True, exist_ok=True)
@@ -208,26 +203,3 @@ def test_export_json(app_server):
def test_export_unknown_uid_404(app_server):
r = requests.get(f"{BASE_URL}/tools/deepsearch/nope/export.md")
assert r.status_code == 404
def test_session_admin_sees_cost(app_server, seeded_db):
try:
uid = _seed_done_session("alice_test")
s = requests.Session()
creds = seeded_db["alice"]
s.post(
f"{BASE_URL}/auth/login",
data={"email": creds["email"], "password": creds["password"]},
allow_redirects=True,
)
r = s.get(f"{BASE_URL}/tools/deepsearch/{uid}/session", headers=_json_headers())
assert r.status_code == 200, r.text
body = r.json()
assert body.get("cost_usd") == 0.123456
html = s.get(f"{BASE_URL}/tools/deepsearch/{uid}/session")
assert html.status_code == 200, html.text
assert "0.123456" in html.text
assert "cost" in html.text
finally:
_clear()