Files
privateGPT/tests/server/files/test_files_router.py
Javier Martinez 21d42fd97a feat: code execution v4 (#2295)
* feat: add bundle to remove

* fix: spaces

* feat: add xml render as skill spec

* feat: add skill volume root to cache

* fix: deduplicate values

* fix: change the mount path to skill_id

* fix: use different paths

* feat: add principal

(cherry picked from commit 5db64fe721d5706440ce9f342b1388ffe742bc16)

# Conflicts:
#	private_gpt/components/code_execution/base.py
#	private_gpt/components/code_execution/code_execution_component.py
#	private_gpt/components/code_execution/local.py
#	private_gpt/components/environment/manager.py
#	private_gpt/components/tools/builders/bash_tool_builder.py
#	private_gpt/components/tools/builders/text_editor_tool_builder.py
#	private_gpt/components/tools/processors/bash_processor.py

* fix: mypy

...

* fix: config

* fix: sandbox config

* feat: add forward cookies

* fix: loop

* feat: add present server

* feat: add feature flag for tools

* refactor: move principal to another better place

* feat: add api key principal

* docs: fix docs

* docs: add present server

* fix: principal

* fix: mypy

* fix: avoid to block the loop

* fix: blocks in expansion

* fix: remove maximum concurrent users

...

* fix: multiplexer

* fix: readers

* fix: more fixes

...

* fix: impl

* feat: tool scheduler

* feat: add adaptative

* feat: add chat worker

* fix: config

* fix: max

* feat: add chat/tools workers

* fix: mypy

* feat: add generic scheduler

* fix: get result

* feat: do serializable the tool executor

* fix: tools

* fix: config

* fix: config

* fix: args

* fix: config

* fix: serializer

* Revert "fix: blocks in expansion"

This reverts commit a2110f94a8.

* fix: unify all logic

* feat: add ingestion scheduler

* fix: settings

* fix: config

* feat: add arq worker to chat

* fix: arq worker

* fix: add nest

* fix: mypy

* fix: await

* fix: script stress

* fix: tokenizer

* fix: chat scheduler

* fix: mypy

* fix: add async tokenizer

* fix: improve condense

* fix: tool scheduler

* feat: add initial real async chat worker

* fix: mypy

* fix: do resumable local executor

...

...

...

fix: revert usleess changes

fix: remove parent chat job

fix: refactor

fix: loop

ref: rename models

fix: chat engine

fix: mypy

...

...

...

fix: fix deps

* fix: tests

* fix: tests

* ...

* fix: stream

* fix: config

* fix: scheduler

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Handle PGPT_WORKER_MODE=celery in health check worker status

* fix: cancel

* fix: arch

* fix: test ingestion

* fix: deserialization of chat messages

* fix: broken results

* fix: mypy

* fix: test

* fix: config

* fix: remove arq tool worker

* fix: output cls

* fix: preserve early resumable tool callbacks

* fix: preserve async tool result order

* refactor: address worker PR review comments

* fix: mypy

* test: colocate ARQ chat enqueue coverage

* fix: remove redis from tests

* fix: mypy

* fix: tests

* test: isolate chat mocks and cancellation timing

* fix: tests

* fix: tests

* fix: test

(cherry picked from commit f8ee460af2)

* fix: worker config

* test: remove flaky chat cancellation assertion

(cherry picked from commit 1115ff2349)

# Conflicts:
#	tests/server/chat/test_chat_routes.py

* fix: emit chat pings from stream listeners

* fix: don't duplciate the output

* fix: rss memory

...

* fix: pass the args

* fix: threads

* fix: websearch

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-07-15 13:05:23 +02:00

155 lines
5.3 KiB
Python

"""Direct router tests for POST/GET/DELETE /v1/files."""
import io
from pathlib import Path
from urllib.parse import quote
from fastapi.testclient import TestClient
from private_gpt.server.files.file_models import (
DeletedFile,
FileListResponse,
FileMetadata,
)
from private_gpt.server.files.file_service import _decode_file_id, _encode_file_id
from tests.server.files.conftest import (
_FILE_CONTENT,
_FILE_NAME,
_MIME_TYPE,
_SESSION_ID,
)
def _file_url(file_id: str, scope_id: str, suffix: str = "") -> str:
"""Build a URL for a file endpoint, URL-encoding the absolute path ID."""
encoded = quote(file_id, safe="")
return f"/v1/files/{encoded}{suffix}?scope_id={scope_id}"
def test_upload_returns_file_metadata(
files_client: TestClient, volume_root: Path
) -> None:
resp = files_client.post(
f"/v1/files?scope_id={_SESSION_ID}",
files={"file": (_FILE_NAME, io.BytesIO(_FILE_CONTENT), _MIME_TYPE)},
)
assert resp.status_code == 200
meta = FileMetadata.model_validate(resp.json())
assert meta.filename == _FILE_NAME
assert meta.size_bytes == len(_FILE_CONTENT)
assert meta.downloadable is False
assert meta.scope.id == _SESSION_ID
# ID is a base64-encoded canonical path pointing into uploads/
decoded = _decode_file_id(meta.id)
assert decoded.startswith("/")
assert "uploads" in decoded
assert decoded.endswith(_FILE_NAME)
def test_list_files_empty_session(files_client: TestClient) -> None:
resp = files_client.get(f"/v1/files?scope_id={_SESSION_ID}")
assert resp.status_code == 200
listing = FileListResponse.model_validate(resp.json())
assert listing.data == []
assert listing.has_more is False
def test_list_files_after_upload(files_client: TestClient) -> None:
files_client.post(
f"/v1/files?scope_id={_SESSION_ID}",
files={"file": (_FILE_NAME, io.BytesIO(_FILE_CONTENT), _MIME_TYPE)},
)
resp = files_client.get(f"/v1/files?scope_id={_SESSION_ID}")
assert resp.status_code == 200
listing = FileListResponse.model_validate(resp.json())
assert len(listing.data) == 1
assert listing.data[0].filename == _FILE_NAME
def test_get_file_metadata(files_client: TestClient) -> None:
upload_resp = files_client.post(
f"/v1/files?scope_id={_SESSION_ID}",
files={"file": (_FILE_NAME, io.BytesIO(_FILE_CONTENT), _MIME_TYPE)},
)
file_id = upload_resp.json()["id"]
resp = files_client.get(_file_url(file_id, _SESSION_ID))
assert resp.status_code == 200
meta = FileMetadata.model_validate(resp.json())
assert meta.id == file_id
assert meta.filename == _FILE_NAME
def test_get_file_metadata_not_found(files_client: TestClient) -> None:
encoded_id = _encode_file_id("/nonexistent/path/file.txt")
resp = files_client.get(_file_url(encoded_id, _SESSION_ID))
assert resp.status_code == 404
def test_download_uploaded_file(files_client: TestClient) -> None:
upload_resp = files_client.post(
f"/v1/files?scope_id={_SESSION_ID}",
files={"file": (_FILE_NAME, io.BytesIO(_FILE_CONTENT), _MIME_TYPE)},
)
file_id = upload_resp.json()["id"]
resp = files_client.get(_file_url(file_id, _SESSION_ID, suffix="/content"))
assert resp.status_code == 200
assert resp.content == _FILE_CONTENT
def test_download_output_file(files_client: TestClient, volume_root: Path) -> None:
"""A sandbox output file can be downloaded via its absolute path ID."""
output_path = volume_root / "outputs" / _SESSION_ID / "result.csv"
output_path.write_bytes(_FILE_CONTENT)
canonical = "/mnt/user-data/outputs/result.csv"
file_id = _encode_file_id(canonical)
resp = files_client.get(_file_url(file_id, _SESSION_ID, suffix="/content"))
assert resp.status_code == 200
assert resp.content == _FILE_CONTENT
def test_list_includes_outputs(files_client: TestClient, volume_root: Path) -> None:
output_path = volume_root / "outputs" / _SESSION_ID / "result.png"
output_path.write_bytes(b"\x89PNG")
canonical = "/mnt/user-data/outputs/result.png"
output_id = _encode_file_id(canonical)
resp = files_client.get(f"/v1/files?scope_id={_SESSION_ID}")
listing = FileListResponse.model_validate(resp.json())
ids = [f.id for f in listing.data]
assert output_id in ids
downloadable = {f.id: f.downloadable for f in listing.data}
assert downloadable[output_id] is True
def test_delete_uploaded_file(files_client: TestClient) -> None:
upload_resp = files_client.post(
f"/v1/files?scope_id={_SESSION_ID}",
files={"file": (_FILE_NAME, io.BytesIO(_FILE_CONTENT), _MIME_TYPE)},
)
file_id = upload_resp.json()["id"]
del_resp = files_client.delete(_file_url(file_id, _SESSION_ID))
assert del_resp.status_code == 200
deleted = DeletedFile.model_validate(del_resp.json())
assert deleted.id == file_id
assert deleted.type == "file_deleted"
assert files_client.get(_file_url(file_id, _SESSION_ID)).status_code == 404
def test_delete_output_returns_404(files_client: TestClient, volume_root: Path) -> None:
output_path = volume_root / "outputs" / _SESSION_ID / "result.csv"
output_path.write_bytes(b"a,b\n1,2")
canonical = "/mnt/user-data/outputs/result.csv"
output_id = _encode_file_id(canonical)
resp = files_client.delete(_file_url(output_id, _SESSION_ID))
assert resp.status_code == 404