- tests/integration: frontend served by backend, runtime error mapping, backend + real pi image with scripted model/Confluence (shared example, variants, failure paths, cancellation/busy gate, isolation canaries, HTTP download ownership, 16 MiB prompt round trip), real OpenAI-compatible adapter + real image over a scripted transport, real Chrome against the real backend with a scripted runtime peer, backend crash/restart reconciliation, and an opt-in live model check (marker: live). - backend: map runtime terminal codes (model_output_limit, model_context_exceeded, query_timeout, connectivity_failed) to the contract's HTTP statuses; make the artifact 404 body identical for no-session, wrong-session, unknown and expired IDs. - Makefile, scripts/run-backend.sh, deploy/confluence-web.env.example, root README for the integrated application; integration pytest marker.
475 lines
22 KiB
Python
475 lines
22 KiB
Python
"""Backend + real pi runtime image with scripted model and Confluence peers.
|
|
|
|
Proves the actual bridge peers connect: real SDK turns, tool arguments and
|
|
results, collection ordering, exact artifact bytes, cleanup, cancellation,
|
|
timeouts, busy gating, and container isolation canaries. No network, no
|
|
credentials: Confluence is a scripted transport and the model is scripted.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import httpx
|
|
import pytest
|
|
from httpx import ASGITransport
|
|
|
|
from backend.app import create_app, SESSION_COOKIE_NAME
|
|
from backend.artifacts import ArtifactStore
|
|
from backend.containers import DockerContainerManager
|
|
from backend.errors import AppError, BusyError, ModelOutputLimitError, QueryTimeoutError, UpstreamFailedError
|
|
from backend.runner import QueryRunner
|
|
from backend.settings import Settings
|
|
|
|
from tests.integration.conftest import (
|
|
APPROVED_ORIGIN,
|
|
CANARY_PAT,
|
|
CHECKLIST_BYTES,
|
|
DENIED_PAGE_ID,
|
|
FINAL_ANSWER,
|
|
PAGE_ID,
|
|
PAGE_URL,
|
|
RUNTIME_IMAGE,
|
|
ScriptedModelAdapter,
|
|
example_script,
|
|
make_confluence_factory,
|
|
requires_runtime_image,
|
|
text_turn,
|
|
tool_message_text,
|
|
tool_turn,
|
|
)
|
|
|
|
pytestmark = [pytest.mark.integration, requires_runtime_image()]
|
|
|
|
EXPECTED_TOOLS = {"confluence_search", "confluence_view", "confluence_list_spaces", "bash", "read", "write", "edit"}
|
|
|
|
|
|
def make_settings(tmp_path: Path, **overrides) -> Settings:
|
|
base = dict(
|
|
runtime_image=RUNTIME_IMAGE,
|
|
approved_confluence_origins=[APPROVED_ORIGIN],
|
|
query_timeout_seconds=120.0,
|
|
cleanup_timeout_seconds=10.0,
|
|
container_label_value=f"integration-{tmp_path.name[-8:]}",
|
|
artifact_storage_dir=tmp_path / "artifacts",
|
|
)
|
|
base.update(overrides)
|
|
return Settings(**base)
|
|
|
|
|
|
def make_runner(tmp_path: Path, script, **overrides):
|
|
settings = make_settings(tmp_path, **overrides)
|
|
store = ArtifactStore(settings.artifact_storage_dir)
|
|
mgr = DockerContainerManager(settings)
|
|
adapter = ScriptedModelAdapter(script)
|
|
calls = []
|
|
runner = QueryRunner(
|
|
settings=settings,
|
|
container_manager=mgr,
|
|
artifact_store=store,
|
|
model_adapter=adapter,
|
|
confluence_client_factory=make_confluence_factory(calls),
|
|
)
|
|
return runner, settings, store, mgr, adapter, calls
|
|
|
|
|
|
async def assert_no_containers(mgr: DockerContainerManager, settings: Settings) -> None:
|
|
code, out, _ = await mgr._exec_docker(
|
|
["ps", "-a", "-q", "--filter", f"label={settings.container_label_key}={settings.container_label_value}"]
|
|
)
|
|
assert code == 0
|
|
assert out.strip() == "", "no application container may remain after the run"
|
|
|
|
|
|
async def run_query(runner: QueryRunner, prompt: str = "How do I deploy service X? Create a checklist file too.", **kwargs):
|
|
return await runner.run(
|
|
prompt=prompt,
|
|
confluence_url=APPROVED_ORIGIN,
|
|
confluence_pat=CANARY_PAT,
|
|
session_id=kwargs.pop("session_id", "cw_sess_integration"),
|
|
**kwargs,
|
|
)
|
|
|
|
|
|
# --- shared example ---------------------------------------------------------------
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_shared_example_end_to_end(tmp_path: Path):
|
|
runner, settings, store, mgr, adapter, calls = make_runner(tmp_path, example_script())
|
|
started = time.monotonic()
|
|
result = await run_query(runner)
|
|
elapsed = time.monotonic() - started
|
|
|
|
# Container removed before the result is returned.
|
|
await assert_no_containers(mgr, settings)
|
|
|
|
# Authoritative history: exactly the two Confluence calls, in start order.
|
|
hist = result["tool_history"]
|
|
assert [h["tool"] for h in hist] == ["confluence_search", "confluence_view"]
|
|
assert all(h["status"] == "success" and h["error"] is None and h["cache_hit"] is False for h in hist)
|
|
assert hist[0]["parameters"]["query"] == "deploy service X"
|
|
assert hist[0]["parameters"]["limit"] == 10 and hist[0]["parameters"]["offset"] == 0
|
|
assert hist[0]["result"]["pages"][0]["page_id"] == PAGE_ID
|
|
assert hist[0]["result"]["pages"][0]["url"] == PAGE_URL
|
|
assert hist[0]["result"]["pagination"] == {"offset": 0, "limit": 10, "has_more": False}
|
|
assert hist[1]["parameters"] == {"page_id": PAGE_ID}
|
|
assert hist[1]["result"]["markdown"].strip() == "Deploy service X using the release checklist."
|
|
assert hist[1]["result"]["truncated"] is False
|
|
assert hist[0]["tool_call_id"] != hist[1]["tool_call_id"]
|
|
assert all(h["tool_call_id"].startswith("a_") for h in hist)
|
|
assert hist[0]["started_at"] <= hist[1]["started_at"]
|
|
|
|
# Search matches never count; the read does.
|
|
assert [p["page_id"] for p in result["pages_accessed"]] == [PAGE_ID]
|
|
assert result["pages_accessed"][0]["url"] == PAGE_URL
|
|
assert result["pages_accessed"][0]["accessed_at"]
|
|
|
|
# Final answer and artifact.
|
|
assert result["markdown"] == FINAL_ANSWER
|
|
assert PAGE_URL in result["markdown"]
|
|
assert len(result["artifacts"]) == 1
|
|
art = result["artifacts"][0]
|
|
assert art["name"] == "checklist.md" and art["size_bytes"] == 32
|
|
dl = store.get_artifact_for_download(art["id"], session_id="cw_sess_integration")
|
|
assert dl is not None
|
|
path, name, size = dl
|
|
assert (path.read_bytes(), name, size) == (CHECKLIST_BYTES, "checklist.md", 32)
|
|
store.release_reader(art["id"])
|
|
assert store.get_artifact_for_download(art["id"], session_id="cw_other") is None
|
|
assert result["duration_seconds"] >= 0 and elapsed < 90
|
|
assert result["warnings"] == []
|
|
|
|
# Actual SDK turns reached the scripted model with the neutral contract shape.
|
|
reqs = adapter.received_requests
|
|
assert len(reqs) == 4
|
|
assert all(r["system_instruction"] for r in reqs)
|
|
assert all(m["role"] != "system" for r in reqs for m in r["messages"])
|
|
assert {t["name"] for t in reqs[0]["tools"]} == EXPECTED_TOOLS
|
|
assert all(isinstance(t["input_schema"], dict) for t in reqs[0]["tools"])
|
|
assert reqs[0]["messages"][0]["role"] == "user"
|
|
assert "deploy service X" in reqs[0]["messages"][0]["content"][0]["text"]
|
|
|
|
# Turn 2 carries the assistant tool call and the search result as a tool message.
|
|
roles = [m["role"] for m in reqs[1]["messages"]]
|
|
assert roles == ["user", "assistant", "tool"]
|
|
assistant = reqs[1]["messages"][1]
|
|
call = next(c for c in assistant["content"] if c["type"] == "tool_call")
|
|
assert call["name"] == "confluence_search" and call["arguments"] == {"query": "deploy service X"}
|
|
assert isinstance(call["arguments"], dict)
|
|
search_text = tool_message_text(reqs[1], call["id"])
|
|
assert "Deployment Guide" in search_text and PAGE_URL in search_text
|
|
assert reqs[1]["messages"][2]["is_error"] is False
|
|
|
|
# Turn 4 carries the local write result; local tools stay out of history.
|
|
write_call = next(c for m in reqs[3]["messages"] if m["role"] == "assistant" for c in m["content"] if c["type"] == "tool_call" and c["name"] == "write")
|
|
assert write_call["arguments"]["path"] == "/work/artifacts/checklist.md"
|
|
assert reqs[3]["messages"][-1]["role"] == "tool" and reqs[3]["messages"][-1]["is_error"] is False
|
|
assert "write" not in [h["tool"] for h in hist]
|
|
|
|
# Backend called Confluence exactly twice, with backend-constructed CQL.
|
|
assert [c["path"] for c in calls] == ["/rest/api/content/search", f"/rest/api/content/{PAGE_ID}"]
|
|
assert 'text ~ "deploy service X"' in calls[0]["params"]["cql"]
|
|
|
|
# The PAT never reaches the model or the result.
|
|
assert CANARY_PAT not in json.dumps(reqs) and CANARY_PAT not in json.dumps(result)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_no_artifacts_and_empty_search(tmp_path: Path):
|
|
script = [
|
|
tool_turn([("call_1", "confluence_search", {"query": "nothing here", "limit": 5})]),
|
|
text_turn("No matching pages were found."),
|
|
]
|
|
runner, settings, store, mgr, adapter, calls = make_runner(tmp_path, script)
|
|
result = await run_query(runner, prompt="Find nothing here")
|
|
await assert_no_containers(mgr, settings)
|
|
assert result["artifacts"] == [] and result["pages_accessed"] == []
|
|
assert len(result["tool_history"]) == 1
|
|
entry = result["tool_history"][0]
|
|
assert entry["status"] == "success"
|
|
assert entry["result"] == {"pages": [], "pagination": {"offset": 0, "limit": 5, "has_more": False}}
|
|
assert "[]" in tool_message_text(adapter.received_requests[1], "call_1")
|
|
assert result["markdown"] == "No matching pages were found."
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cached_repeat_view_and_denied_page(tmp_path: Path):
|
|
script = [
|
|
tool_turn([("call_1", "confluence_view", {"page_id": PAGE_ID})]),
|
|
tool_turn([("call_2", "confluence_view", {"page_id": PAGE_ID})]),
|
|
tool_turn([("call_3", "confluence_view", {"page_id": DENIED_PAGE_ID})]),
|
|
text_turn(f"Only [Deployment Guide]({PAGE_URL}) was readable."),
|
|
]
|
|
runner, settings, store, mgr, adapter, calls = make_runner(tmp_path, script)
|
|
result = await run_query(runner)
|
|
await assert_no_containers(mgr, settings)
|
|
|
|
hist = result["tool_history"]
|
|
assert [h["tool"] for h in hist] == ["confluence_view"] * 3
|
|
assert [h["cache_hit"] for h in hist] == [False, True, False]
|
|
assert hist[1]["result"] == hist[0]["result"]
|
|
assert len({h["tool_call_id"] for h in hist}) == 3
|
|
assert hist[2]["status"] == "error" and hist[2]["result"] is None
|
|
err = hist[2]["error"]
|
|
assert set(err) == {"code", "message"} and err["code"] and len(err["message"].encode()) <= 1024
|
|
assert "SECRET-UPSTREAM-BODY" not in json.dumps(result)
|
|
assert "SECRET-UPSTREAM-BODY" not in json.dumps(adapter.received_requests)
|
|
|
|
# Cache hit did not call upstream again; the denied read did.
|
|
assert [c["path"] for c in calls] == [f"/rest/api/content/{PAGE_ID}", f"/rest/api/content/{DENIED_PAGE_ID}"]
|
|
# One unique page, the denied one never counted.
|
|
assert [p["page_id"] for p in result["pages_accessed"]] == [PAGE_ID]
|
|
# The agent saw the failure as a recoverable tool error and continued.
|
|
denied_msg = next(m for m in adapter.received_requests[3]["messages"] if m["role"] == "tool" and m["tool_call_id"] == "call_3")
|
|
assert denied_msg["is_error"] is True
|
|
assert result["markdown"].startswith("Only")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_empty_artifact_and_html_artifact(tmp_path: Path):
|
|
script = [
|
|
tool_turn([("call_1", "bash", {"command": "touch /work/artifacts/empty.txt; printf '<script>alert(1)</script>' > /work/artifacts/page.html"})]),
|
|
text_turn("Created two files."),
|
|
]
|
|
runner, settings, store, mgr, adapter, calls = make_runner(tmp_path, script)
|
|
result = await run_query(runner)
|
|
await assert_no_containers(mgr, settings)
|
|
by_name = {a["name"]: a for a in result["artifacts"]}
|
|
assert set(by_name) == {"empty.txt", "page.html"}
|
|
assert by_name["empty.txt"]["size_bytes"] == 0
|
|
dl = store.get_artifact_for_download(by_name["empty.txt"]["id"], session_id="cw_sess_integration")
|
|
assert dl is not None and dl[0].read_bytes() == b"" and dl[2] == 0
|
|
store.release_reader(by_name["empty.txt"]["id"])
|
|
assert result["pages_accessed"] == [] and result["tool_history"] == []
|
|
|
|
|
|
# --- failure paths ---------------------------------------------------------------------
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_model_upstream_failure_discards_exports(tmp_path: Path):
|
|
script = [
|
|
tool_turn([("call_1", "bash", {"command": "printf 'partial' > /work/artifacts/partial.txt"})]),
|
|
UpstreamFailedError("scripted provider outage"),
|
|
]
|
|
runner, settings, store, mgr, adapter, calls = make_runner(tmp_path, script)
|
|
with pytest.raises(AppError) as excinfo:
|
|
await run_query(runner)
|
|
assert excinfo.value.code in {"upstream_failed", "execution_failed"}
|
|
await assert_no_containers(mgr, settings)
|
|
assert store.global_reserved_bytes == 0
|
|
assert not any(p.is_file() for p in settings.artifact_storage_dir.rglob("*"))
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_model_length_stop_is_output_limit(tmp_path: Path):
|
|
script = [text_turn("Truncated answer that never finished", stop_reason="length")]
|
|
runner, settings, store, mgr, adapter, calls = make_runner(tmp_path, script)
|
|
with pytest.raises(AppError) as excinfo:
|
|
await run_query(runner)
|
|
assert isinstance(excinfo.value, ModelOutputLimitError), excinfo.value.code
|
|
assert excinfo.value.status_code == 502
|
|
await assert_no_containers(mgr, settings)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_empty_terminal_answer_fails(tmp_path: Path):
|
|
runner, settings, store, mgr, adapter, calls = make_runner(tmp_path, [text_turn("")])
|
|
with pytest.raises(AppError) as excinfo:
|
|
await run_query(runner)
|
|
assert excinfo.value.code == "execution_failed"
|
|
await assert_no_containers(mgr, settings)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_timeout_during_model_work(tmp_path: Path):
|
|
async def stall():
|
|
await asyncio.sleep(120)
|
|
runner, settings, store, mgr, adapter, calls = make_runner(tmp_path, [stall], query_timeout_seconds=8.0)
|
|
started = time.monotonic()
|
|
with pytest.raises(QueryTimeoutError):
|
|
await run_query(runner)
|
|
assert time.monotonic() - started < 40
|
|
await assert_no_containers(mgr, settings)
|
|
assert store.global_reserved_bytes == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_disconnect_cancels_and_busy_gate_holds(tmp_path: Path):
|
|
async def stall():
|
|
await asyncio.sleep(120)
|
|
runner, settings, store, mgr, adapter, calls = make_runner(
|
|
tmp_path, [stall] + example_script(), query_timeout_seconds=90.0
|
|
)
|
|
disconnected = False
|
|
first = asyncio.create_task(run_query(runner, is_disconnected=lambda: disconnected))
|
|
await asyncio.wait_for(adapter.started.wait(), 60)
|
|
|
|
# Busy while the first run is active.
|
|
with pytest.raises(BusyError):
|
|
await run_query(runner, session_id="cw_second")
|
|
|
|
disconnected = True
|
|
with pytest.raises(BaseException) as excinfo:
|
|
await first
|
|
assert isinstance(excinfo.value, (asyncio.CancelledError, AppError)), type(excinfo.value)
|
|
await assert_no_containers(mgr, settings)
|
|
assert store.global_reserved_bytes == 0
|
|
|
|
# After cleanup the gate admits a new run, which completes normally.
|
|
result = await run_query(runner, session_id="cw_third")
|
|
assert result["artifacts"][0]["size_bytes"] == 32
|
|
await assert_no_containers(mgr, settings)
|
|
|
|
|
|
# --- isolation canaries under the actual image and launch flags ------------------------
|
|
|
|
PROBE = r"""
|
|
set +e
|
|
echo "UID=$(id -u) GID=$(id -g)"
|
|
echo "WORK=$(ls -A /work | tr '\n' ' ')"
|
|
echo "HOME_LS=$(ls -A "$HOME" | tr '\n' ' ')"
|
|
echo "TMP_LS=$(ls -A /tmp | tr '\n' ' ')"
|
|
echo "NET=$(ls /sys/class/net | tr '\n' ' ')"
|
|
echo "CAPEFF=$(grep CapEff /proc/self/status)"
|
|
echo "NNP=$(grep NoNewPrivs /proc/self/status)"
|
|
echo "ENV_BEGIN"; env; echo "ENV_END"
|
|
echo "PID1_ENVIRON=$(cat /proc/1/environ 2>&1 | tr '\0' ' ' | head -c 300)"
|
|
echo "PROCS=$(ps -eo pid,comm --no-headers | tr '\n' ';')"
|
|
echo "OPT_WRITABLE=$(touch /opt/agent/x 2>&1 || echo readonly)"
|
|
echo "ROOT_WRITABLE=$(touch /x 2>&1 || echo readonly)"
|
|
echo "DOCKER_SOCK=$(ls /var/run/docker.sock /run/docker.sock 2>&1)"
|
|
echo "HOST_FILES=$(ls /etc/shadow ~/.pi /root 2>&1 | tr '\n' ' ')"
|
|
echo "MOUNTS=$(awk '{print $2":"$3}' /proc/mounts | tr '\n' ' ')"
|
|
"""
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fresh_workspace_and_canaries_across_runs(tmp_path: Path):
|
|
seed = [
|
|
tool_turn([("call_1", "bash", {"command": "echo scratch > /work/scratch.txt; echo m > $HOME/marker; echo t > /tmp/tmarker; echo ok"})]),
|
|
text_turn("seeded"),
|
|
]
|
|
runner, settings, store, mgr, adapter, calls = make_runner(tmp_path, seed + [
|
|
tool_turn([("call_2", "bash", {"command": PROBE})]),
|
|
text_turn("probed"),
|
|
])
|
|
first = await run_query(runner)
|
|
assert first["markdown"] == "seeded"
|
|
second = await run_query(runner)
|
|
assert second["markdown"] == "probed"
|
|
await assert_no_containers(mgr, settings)
|
|
|
|
probe_out = tool_message_text(adapter.received_requests[3], "call_2")
|
|
assert "UID=10001 GID=10001" in probe_out
|
|
assert "scratch.txt" not in probe_out and "marker" not in probe_out
|
|
assert "WORK=artifacts" in probe_out
|
|
assert "NET=lo" in probe_out
|
|
assert "CapEff:\t0000000000000000" in probe_out
|
|
assert "NoNewPrivs:\t1" in probe_out
|
|
env_block = probe_out.split("ENV_BEGIN")[1].split("ENV_END")[0]
|
|
env_names = {line.split("=", 1)[0] for line in env_block.strip().splitlines() if "=" in line}
|
|
assert env_names <= {"HOME", "LANG", "PATH", "PWD", "SHLVL", "_", "OLDPWD", "NODE_OPTIONS", "AGENT_WORK_DIR"}, env_names
|
|
assert CANARY_PAT not in probe_out
|
|
assert "MODEL_API_KEY" not in probe_out and "CONFLUENCE" not in env_block
|
|
assert "PID1_ENVIRON=cat: /proc/1/environ: Permission denied" in probe_out or "PID1_ENVIRON=" in probe_out
|
|
assert "Read-only file system" in probe_out.split("OPT_WRITABLE=")[1].split("ROOT_WRITABLE=")[0]
|
|
assert "Read-only file system" in probe_out.split("ROOT_WRITABLE=")[1].split("DOCKER_SOCK=")[0]
|
|
assert "No such file" in probe_out.split("DOCKER_SOCK=")[1].split("\n")[0]
|
|
host_files = probe_out.split("HOST_FILES=")[1].split("MOUNTS=")[0]
|
|
assert "cannot access '/home/agent/.pi'" in host_files and "'/root': Permission denied" in host_files
|
|
assert "/work:tmpfs" in probe_out and "/tmp:tmpfs" in probe_out and "/home/agent:tmpfs" in probe_out
|
|
assert CANARY_PAT not in json.dumps(adapter.received_requests)
|
|
|
|
|
|
# --- HTTP surface with the real image --------------------------------------------------
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_http_query_and_download_ownership(tmp_path: Path):
|
|
settings = make_settings(tmp_path)
|
|
store = ArtifactStore(settings.artifact_storage_dir)
|
|
mgr = DockerContainerManager(settings)
|
|
adapter = ScriptedModelAdapter(example_script())
|
|
app = create_app(
|
|
settings=settings,
|
|
container_manager=mgr,
|
|
artifact_store=store,
|
|
model_adapter=adapter,
|
|
confluence_client_factory=make_confluence_factory(),
|
|
)
|
|
transport = ASGITransport(app=app)
|
|
origin = "http://testserver"
|
|
async with httpx.AsyncClient(transport=transport, base_url=origin) as a, httpx.AsyncClient(transport=transport, base_url=origin) as b:
|
|
boot = await a.get("/")
|
|
assert boot.status_code == 200 and SESSION_COOKIE_NAME in boot.cookies
|
|
verify = await a.post("/api/v1/auth/verify", json={"url": APPROVED_ORIGIN, "pat": CANARY_PAT}, headers={"Origin": origin})
|
|
assert verify.status_code == 200 and verify.json() == {"valid": True}
|
|
assert verify.headers["cache-control"] == "no-store"
|
|
|
|
resp = await a.post(
|
|
"/api/v1/query",
|
|
json={"prompt": "How do I deploy service X?", "credentials": {"url": APPROVED_ORIGIN, "pat": CANARY_PAT}},
|
|
headers={"Origin": origin},
|
|
timeout=120,
|
|
)
|
|
assert resp.status_code == 200, resp.text
|
|
assert resp.headers["cache-control"] == "no-store"
|
|
body = resp.json()
|
|
assert set(body) == {"session_id", "markdown", "pages_accessed", "tool_history", "artifacts", "warnings", "duration_seconds"}
|
|
await assert_no_containers(mgr, settings)
|
|
art = body["artifacts"][0]
|
|
assert art["id"] != body["session_id"]
|
|
|
|
dl = await a.get(f"/api/v1/artifacts/{art['id']}")
|
|
assert dl.status_code == 200 and dl.content == CHECKLIST_BYTES
|
|
assert dl.headers["content-type"].startswith("application/octet-stream")
|
|
assert dl.headers["content-disposition"].startswith("attachment")
|
|
assert dl.headers["x-content-type-options"] == "nosniff"
|
|
assert dl.headers["cache-control"] == "no-store"
|
|
|
|
# Session B, unknown ID and expired ID are indistinguishable 404s.
|
|
await b.get("/")
|
|
wrong = await b.get(f"/api/v1/artifacts/{art['id']}")
|
|
unknown = await a.get("/api/v1/artifacts/does-not-exist")
|
|
store._committed[art["id"]].expires_at_ts = 0 # force expiry
|
|
expired = await a.get(f"/api/v1/artifacts/{art['id']}")
|
|
for r in (wrong, unknown, expired):
|
|
assert r.status_code == 404
|
|
assert r.json() == {"error": {"code": "artifact_not_found", "message": unknown.json()["error"]["message"]}}
|
|
assert r.headers["cache-control"] == "no-store"
|
|
assert wrong.content == unknown.content == expired.content
|
|
|
|
|
|
# --- generated boundaries through the whole stack ---------------------------------------
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_large_prompt_and_answer_round_trip(tmp_path: Path):
|
|
"""Exactly 16 MiB decoded prompt with heavy JSON escaping, and a multi-MiB answer."""
|
|
unit = 'line "quoted" \\ back\ttab é中\U0001f600 \x01\n' # quotes, backslash, control chars, multibyte
|
|
prompt = unit * (16 * 1024 * 1024 // len(unit.encode("utf-8")))
|
|
prompt += "x" * (16 * 1024 * 1024 - len(prompt.encode("utf-8")))
|
|
assert len(prompt.encode("utf-8")) == 16 * 1024 * 1024
|
|
section = "## Section\n\n" + ("Deploy service X carefully. " * 40) + "\n\n```\ncode \"block\" \\ \n```\n\n"
|
|
answer = "# Big answer\n\n" + section * (4 * 1024 * 1024 // len(section.encode("utf-8"))) + f"\nSee [Deployment Guide]({PAGE_URL}).\n"
|
|
script = [
|
|
tool_turn([("call_1", "bash", {"command": "wc -c /work/artifacts 2>/dev/null; echo ok"})]),
|
|
text_turn(answer, in_tok=1000, out_tok=1000),
|
|
]
|
|
runner, settings, store, mgr, adapter, calls = make_runner(tmp_path, script)
|
|
started = time.monotonic()
|
|
result = await run_query(runner, prompt=prompt)
|
|
elapsed = time.monotonic() - started
|
|
await assert_no_containers(mgr, settings)
|
|
assert result["markdown"] == answer
|
|
first_user = adapter.received_requests[0]["messages"][0]["content"][0]["text"]
|
|
assert first_user == prompt, "prompt must reach the model byte-for-byte through the bridge"
|
|
assert elapsed < 150, elapsed
|
|
|
|
# One byte over the decoded limit is rejected before any container starts.
|
|
with pytest.raises(AppError) as excinfo:
|
|
await run_query(runner, prompt=prompt + "y")
|
|
assert excinfo.value.code == "invalid_input"
|
|
await assert_no_containers(mgr, settings)
|