2026-01-30 23:22:05 -08:00
|
|
|
import contextlib
|
|
|
|
|
import json
|
|
|
|
|
import os
|
|
|
|
|
import socket
|
|
|
|
|
import subprocess
|
|
|
|
|
import sys
|
|
|
|
|
import tempfile
|
|
|
|
|
import time
|
fix(assets): remove unused delete_content param from deleteAsset (#14241)
* fix(assets): remove unused delete_content param from deleteAsset
The delete_content query param on DELETE /api/assets/{id} was introduced
in #12125 and had its default flipped to false in #12621. In practice no
client sends it: the frontend issues a bare DELETE /assets/{id}, so every
real caller already gets the default soft-delete (the reference is hidden,
content preserved). The only thing that set delete_content=true was this
repo's own test teardown.
Remove the param from the route and the OpenAPI spec so the contract
matches what clients actually use (and lines up with the cloud surface).
The route now always soft-deletes. The underlying delete_asset_reference
helper keeps its delete_content_if_orphan option, so orphan reclamation
remains available internally for a future GC path — it's just no longer
exposed on the public endpoint. Tests that used delete_content=true for
hard cleanup now soft-delete; test_delete_upon_reference_count asserts
content preservation instead of orphan removal.
* test/docs: address review on deleteAsset delete_content removal
- Rename test_delete_upon_reference_count ->
test_soft_delete_preserves_asset_identity_across_references; the old name
implied last-ref cleanup, but it now verifies the opposite (soft delete
preserves identity across references).
- Strengthen the re-association assertion: also check asset_hash == src_hash
so it proves content reuse rather than relying on the now-tautological
created_new is False.
- Document delete_asset_reference: the orphan-reclamation branch is
intentionally internal-only; the public endpoint always soft-deletes.
- Normalize the soft-delete comment phrasing.
* test(assets): make seed content unique per test for isolation
Removing the delete_content param means delete is always a soft delete, so
content created by one test now survives into the next. The suite had been
relying on hard-delete teardown for isolation, so shared fixed-content
fixtures started colliding: seeded_asset (b"A"*4096) and
make_asset_bytes (deterministic on name) produced the same hash every test,
so the second seed deduped to the surviving asset and returned 200 instead
of 201, cascading into ~14 failures/errors.
Salt both fixtures with a per-test uuid so each test creates fresh content
(created_new True, 201), while keeping content deterministic within a test
(same name/size -> same bytes) and preserving exact byte length so size-based
list/sort assertions are unaffected.
2026-06-09 21:52:14 -07:00
|
|
|
import uuid
|
2026-01-30 23:22:05 -08:00
|
|
|
from pathlib import Path
|
|
|
|
|
from typing import Callable, Iterator, Optional
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
import requests
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def pytest_addoption(parser: pytest.Parser) -> None:
|
|
|
|
|
"""
|
|
|
|
|
Allow overriding the database URL used by the spawned ComfyUI process.
|
|
|
|
|
Priority:
|
|
|
|
|
1) --db-url command line option
|
|
|
|
|
2) ASSETS_TEST_DB_URL environment variable (used by CI)
|
|
|
|
|
3) default: None (will use file-backed sqlite in temp dir)
|
|
|
|
|
"""
|
|
|
|
|
parser.addoption(
|
|
|
|
|
"--db-url",
|
|
|
|
|
action="store",
|
|
|
|
|
default=os.environ.get("ASSETS_TEST_DB_URL"),
|
|
|
|
|
help="SQLAlchemy DB URL (e.g. sqlite:///path/to/db.sqlite3)",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _free_port() -> int:
|
|
|
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
|
|
|
s.bind(("127.0.0.1", 0))
|
|
|
|
|
return s.getsockname()[1]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _make_base_dirs(root: Path) -> None:
|
|
|
|
|
for sub in ("models", "custom_nodes", "input", "output", "temp", "user"):
|
|
|
|
|
(root / sub).mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _wait_http_ready(base: str, session: requests.Session, timeout: float = 90.0) -> None:
|
|
|
|
|
start = time.time()
|
|
|
|
|
last_err = None
|
|
|
|
|
while time.time() - start < timeout:
|
|
|
|
|
try:
|
|
|
|
|
r = session.get(base + "/api/assets", timeout=5)
|
|
|
|
|
if r.status_code in (200, 400):
|
|
|
|
|
return
|
|
|
|
|
except Exception as e:
|
|
|
|
|
last_err = e
|
|
|
|
|
time.sleep(0.25)
|
|
|
|
|
raise RuntimeError(f"ComfyUI HTTP did not become ready: {last_err}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
|
|
|
def comfy_tmp_base_dir() -> Path:
|
|
|
|
|
env_base = os.environ.get("ASSETS_TEST_BASE_DIR")
|
|
|
|
|
created_by_fixture = False
|
|
|
|
|
if env_base:
|
|
|
|
|
tmp = Path(env_base)
|
|
|
|
|
tmp.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
else:
|
|
|
|
|
tmp = Path(tempfile.mkdtemp(prefix="comfyui-assets-tests-"))
|
|
|
|
|
created_by_fixture = True
|
|
|
|
|
_make_base_dirs(tmp)
|
|
|
|
|
yield tmp
|
|
|
|
|
if created_by_fixture:
|
|
|
|
|
with contextlib.suppress(Exception):
|
|
|
|
|
for p in sorted(tmp.rglob("*"), reverse=True):
|
|
|
|
|
if p.is_file() or p.is_symlink():
|
|
|
|
|
p.unlink(missing_ok=True)
|
|
|
|
|
for p in sorted(tmp.glob("**/*"), reverse=True):
|
|
|
|
|
with contextlib.suppress(Exception):
|
|
|
|
|
p.rmdir()
|
|
|
|
|
tmp.rmdir()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
|
|
|
def comfy_url_and_proc(comfy_tmp_base_dir: Path, request: pytest.FixtureRequest):
|
|
|
|
|
"""
|
|
|
|
|
Boot ComfyUI subprocess with:
|
|
|
|
|
- sandbox base dir
|
|
|
|
|
- file-backed sqlite DB in temp dir
|
|
|
|
|
- autoscan disabled
|
|
|
|
|
Returns (base_url, process, port)
|
|
|
|
|
"""
|
|
|
|
|
port = _free_port()
|
|
|
|
|
db_url = request.config.getoption("--db-url")
|
|
|
|
|
if not db_url:
|
|
|
|
|
# Use a file-backed sqlite database in the temp directory
|
|
|
|
|
db_path = comfy_tmp_base_dir / "assets-test.sqlite3"
|
|
|
|
|
db_url = f"sqlite:///{db_path}"
|
|
|
|
|
|
|
|
|
|
logs_dir = comfy_tmp_base_dir / "logs"
|
|
|
|
|
logs_dir.mkdir(exist_ok=True)
|
|
|
|
|
out_log = open(logs_dir / "stdout.log", "w", buffering=1)
|
|
|
|
|
err_log = open(logs_dir / "stderr.log", "w", buffering=1)
|
|
|
|
|
|
|
|
|
|
comfy_root = Path(__file__).resolve().parent.parent.parent
|
|
|
|
|
if not (comfy_root / "main.py").is_file():
|
|
|
|
|
raise FileNotFoundError(f"main.py not found under {comfy_root}")
|
|
|
|
|
|
|
|
|
|
proc = subprocess.Popen(
|
|
|
|
|
args=[
|
|
|
|
|
sys.executable,
|
|
|
|
|
"main.py",
|
|
|
|
|
f"--base-directory={str(comfy_tmp_base_dir)}",
|
|
|
|
|
f"--database-url={db_url}",
|
2026-03-07 17:37:25 -08:00
|
|
|
"--enable-assets",
|
2026-01-30 23:22:05 -08:00
|
|
|
"--listen",
|
|
|
|
|
"127.0.0.1",
|
|
|
|
|
"--port",
|
|
|
|
|
str(port),
|
|
|
|
|
"--cpu",
|
|
|
|
|
],
|
|
|
|
|
stdout=out_log,
|
|
|
|
|
stderr=err_log,
|
|
|
|
|
cwd=str(comfy_root),
|
|
|
|
|
env={**os.environ},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
for _ in range(50):
|
|
|
|
|
if proc.poll() is not None:
|
|
|
|
|
out_log.flush()
|
|
|
|
|
err_log.flush()
|
|
|
|
|
raise RuntimeError(f"ComfyUI exited early with code {proc.returncode}")
|
|
|
|
|
time.sleep(0.1)
|
|
|
|
|
|
|
|
|
|
base_url = f"http://127.0.0.1:{port}"
|
|
|
|
|
try:
|
|
|
|
|
with requests.Session() as s:
|
|
|
|
|
_wait_http_ready(base_url, s, timeout=90.0)
|
|
|
|
|
yield base_url, proc, port
|
|
|
|
|
except Exception as e:
|
|
|
|
|
with contextlib.suppress(Exception):
|
|
|
|
|
proc.terminate()
|
|
|
|
|
proc.wait(timeout=10)
|
|
|
|
|
with contextlib.suppress(Exception):
|
|
|
|
|
out_log.flush()
|
|
|
|
|
err_log.flush()
|
|
|
|
|
raise RuntimeError(f"ComfyUI did not become ready: {e}")
|
|
|
|
|
|
|
|
|
|
if proc and proc.poll() is None:
|
|
|
|
|
with contextlib.suppress(Exception):
|
|
|
|
|
proc.terminate()
|
|
|
|
|
proc.wait(timeout=15)
|
|
|
|
|
out_log.close()
|
|
|
|
|
err_log.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def http() -> Iterator[requests.Session]:
|
|
|
|
|
with requests.Session() as s:
|
|
|
|
|
s.timeout = 120
|
|
|
|
|
yield s
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def api_base(comfy_url_and_proc) -> str:
|
|
|
|
|
base_url, _proc, _port = comfy_url_and_proc
|
|
|
|
|
return base_url
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _post_multipart_asset(
|
|
|
|
|
session: requests.Session,
|
|
|
|
|
base: str,
|
|
|
|
|
*,
|
|
|
|
|
name: str,
|
|
|
|
|
tags: list[str],
|
|
|
|
|
meta: dict,
|
|
|
|
|
data: bytes,
|
|
|
|
|
extra_fields: Optional[dict] = None,
|
|
|
|
|
) -> tuple[int, dict]:
|
|
|
|
|
files = {"file": (name, data, "application/octet-stream")}
|
|
|
|
|
form_data = {
|
|
|
|
|
"tags": json.dumps(tags),
|
|
|
|
|
"name": name,
|
|
|
|
|
"user_metadata": json.dumps(meta),
|
|
|
|
|
}
|
|
|
|
|
if extra_fields:
|
|
|
|
|
for k, v in extra_fields.items():
|
|
|
|
|
form_data[k] = v
|
|
|
|
|
r = session.post(base + "/api/assets", files=files, data=form_data, timeout=120)
|
|
|
|
|
return r.status_code, r.json()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_asset_bytes() -> Callable[[str, int], bytes]:
|
fix(assets): remove unused delete_content param from deleteAsset (#14241)
* fix(assets): remove unused delete_content param from deleteAsset
The delete_content query param on DELETE /api/assets/{id} was introduced
in #12125 and had its default flipped to false in #12621. In practice no
client sends it: the frontend issues a bare DELETE /assets/{id}, so every
real caller already gets the default soft-delete (the reference is hidden,
content preserved). The only thing that set delete_content=true was this
repo's own test teardown.
Remove the param from the route and the OpenAPI spec so the contract
matches what clients actually use (and lines up with the cloud surface).
The route now always soft-deletes. The underlying delete_asset_reference
helper keeps its delete_content_if_orphan option, so orphan reclamation
remains available internally for a future GC path — it's just no longer
exposed on the public endpoint. Tests that used delete_content=true for
hard cleanup now soft-delete; test_delete_upon_reference_count asserts
content preservation instead of orphan removal.
* test/docs: address review on deleteAsset delete_content removal
- Rename test_delete_upon_reference_count ->
test_soft_delete_preserves_asset_identity_across_references; the old name
implied last-ref cleanup, but it now verifies the opposite (soft delete
preserves identity across references).
- Strengthen the re-association assertion: also check asset_hash == src_hash
so it proves content reuse rather than relying on the now-tautological
created_new is False.
- Document delete_asset_reference: the orphan-reclamation branch is
intentionally internal-only; the public endpoint always soft-deletes.
- Normalize the soft-delete comment phrasing.
* test(assets): make seed content unique per test for isolation
Removing the delete_content param means delete is always a soft delete, so
content created by one test now survives into the next. The suite had been
relying on hard-delete teardown for isolation, so shared fixed-content
fixtures started colliding: seeded_asset (b"A"*4096) and
make_asset_bytes (deterministic on name) produced the same hash every test,
so the second seed deduped to the surviving asset and returned 200 instead
of 201, cascading into ~14 failures/errors.
Salt both fixtures with a per-test uuid so each test creates fresh content
(created_new True, 201), while keeping content deterministic within a test
(same name/size -> same bytes) and preserving exact byte length so size-based
list/sort assertions are unaffected.
2026-06-09 21:52:14 -07:00
|
|
|
# Salt content per test so it never collides with assets left over from
|
|
|
|
|
# earlier tests. Delete is now always a soft delete (content is preserved),
|
|
|
|
|
# so the suite can no longer rely on hard-deleting content for isolation.
|
|
|
|
|
# Deterministic within a test: the same (name, size) yields the same bytes.
|
|
|
|
|
salt = uuid.uuid4().bytes
|
|
|
|
|
|
2026-01-30 23:22:05 -08:00
|
|
|
def _make(name: str, size: int = 8192) -> bytes:
|
|
|
|
|
seed = sum(ord(c) for c in name) % 251
|
fix(assets): remove unused delete_content param from deleteAsset (#14241)
* fix(assets): remove unused delete_content param from deleteAsset
The delete_content query param on DELETE /api/assets/{id} was introduced
in #12125 and had its default flipped to false in #12621. In practice no
client sends it: the frontend issues a bare DELETE /assets/{id}, so every
real caller already gets the default soft-delete (the reference is hidden,
content preserved). The only thing that set delete_content=true was this
repo's own test teardown.
Remove the param from the route and the OpenAPI spec so the contract
matches what clients actually use (and lines up with the cloud surface).
The route now always soft-deletes. The underlying delete_asset_reference
helper keeps its delete_content_if_orphan option, so orphan reclamation
remains available internally for a future GC path — it's just no longer
exposed on the public endpoint. Tests that used delete_content=true for
hard cleanup now soft-delete; test_delete_upon_reference_count asserts
content preservation instead of orphan removal.
* test/docs: address review on deleteAsset delete_content removal
- Rename test_delete_upon_reference_count ->
test_soft_delete_preserves_asset_identity_across_references; the old name
implied last-ref cleanup, but it now verifies the opposite (soft delete
preserves identity across references).
- Strengthen the re-association assertion: also check asset_hash == src_hash
so it proves content reuse rather than relying on the now-tautological
created_new is False.
- Document delete_asset_reference: the orphan-reclamation branch is
intentionally internal-only; the public endpoint always soft-deletes.
- Normalize the soft-delete comment phrasing.
* test(assets): make seed content unique per test for isolation
Removing the delete_content param means delete is always a soft delete, so
content created by one test now survives into the next. The suite had been
relying on hard-delete teardown for isolation, so shared fixed-content
fixtures started colliding: seeded_asset (b"A"*4096) and
make_asset_bytes (deterministic on name) produced the same hash every test,
so the second seed deduped to the surviving asset and returned 200 instead
of 201, cascading into ~14 failures/errors.
Salt both fixtures with a per-test uuid so each test creates fresh content
(created_new True, 201), while keeping content deterministic within a test
(same name/size -> same bytes) and preserving exact byte length so size-based
list/sort assertions are unaffected.
2026-06-09 21:52:14 -07:00
|
|
|
body = bytearray((i * 31 + seed) % 256 for i in range(size))
|
|
|
|
|
body[: len(salt)] = salt[:size]
|
|
|
|
|
return bytes(body)
|
2026-01-30 23:22:05 -08:00
|
|
|
return _make
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def asset_factory(http: requests.Session, api_base: str):
|
|
|
|
|
"""
|
|
|
|
|
Returns create(name, tags, meta, data) -> response dict
|
|
|
|
|
Tracks created ids and deletes them after the test.
|
|
|
|
|
"""
|
|
|
|
|
created: list[str] = []
|
|
|
|
|
|
|
|
|
|
def create(name: str, tags: list[str], meta: dict, data: bytes) -> dict:
|
|
|
|
|
status, body = _post_multipart_asset(http, api_base, name=name, tags=tags, meta=meta, data=data)
|
|
|
|
|
assert status in (200, 201), body
|
|
|
|
|
created.append(body["id"])
|
|
|
|
|
return body
|
|
|
|
|
|
|
|
|
|
yield create
|
|
|
|
|
|
|
|
|
|
for aid in created:
|
|
|
|
|
with contextlib.suppress(Exception):
|
fix(assets): remove unused delete_content param from deleteAsset (#14241)
* fix(assets): remove unused delete_content param from deleteAsset
The delete_content query param on DELETE /api/assets/{id} was introduced
in #12125 and had its default flipped to false in #12621. In practice no
client sends it: the frontend issues a bare DELETE /assets/{id}, so every
real caller already gets the default soft-delete (the reference is hidden,
content preserved). The only thing that set delete_content=true was this
repo's own test teardown.
Remove the param from the route and the OpenAPI spec so the contract
matches what clients actually use (and lines up with the cloud surface).
The route now always soft-deletes. The underlying delete_asset_reference
helper keeps its delete_content_if_orphan option, so orphan reclamation
remains available internally for a future GC path — it's just no longer
exposed on the public endpoint. Tests that used delete_content=true for
hard cleanup now soft-delete; test_delete_upon_reference_count asserts
content preservation instead of orphan removal.
* test/docs: address review on deleteAsset delete_content removal
- Rename test_delete_upon_reference_count ->
test_soft_delete_preserves_asset_identity_across_references; the old name
implied last-ref cleanup, but it now verifies the opposite (soft delete
preserves identity across references).
- Strengthen the re-association assertion: also check asset_hash == src_hash
so it proves content reuse rather than relying on the now-tautological
created_new is False.
- Document delete_asset_reference: the orphan-reclamation branch is
intentionally internal-only; the public endpoint always soft-deletes.
- Normalize the soft-delete comment phrasing.
* test(assets): make seed content unique per test for isolation
Removing the delete_content param means delete is always a soft delete, so
content created by one test now survives into the next. The suite had been
relying on hard-delete teardown for isolation, so shared fixed-content
fixtures started colliding: seeded_asset (b"A"*4096) and
make_asset_bytes (deterministic on name) produced the same hash every test,
so the second seed deduped to the surviving asset and returned 200 instead
of 201, cascading into ~14 failures/errors.
Salt both fixtures with a per-test uuid so each test creates fresh content
(created_new True, 201), while keeping content deterministic within a test
(same name/size -> same bytes) and preserving exact byte length so size-based
list/sort assertions are unaffected.
2026-06-09 21:52:14 -07:00
|
|
|
http.delete(f"{api_base}/api/assets/{aid}", timeout=30)
|
2026-01-30 23:22:05 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def seeded_asset(request: pytest.FixtureRequest, http: requests.Session, api_base: str) -> dict:
|
|
|
|
|
"""
|
|
|
|
|
Upload one asset with ".safetensors" extension into models/checkpoints/unit-tests/<name>.
|
|
|
|
|
Returns response dict with id, asset_hash, tags, etc.
|
|
|
|
|
"""
|
|
|
|
|
name = "unit_1_example.safetensors"
|
|
|
|
|
p = getattr(request, "param", {}) or {}
|
|
|
|
|
tags: Optional[list[str]] = p.get("tags")
|
|
|
|
|
if tags is None:
|
feat(assets): add namespaced model_type tags and align tag semantics (#14511)
* feat(assets): add namespaced model type tags
* fix(assets): mark path-derived upload tags automatic
* fix(assets): merge duplicate scan specs
* test(assets): make duplicate path normalization portable
* feat(assets): add loader_path as the authoritative loader locator (#14796)
* fix(assets): filter model_type tags by bucket extension sets
Buckets sharing a base directory (e.g. diffusion_models and a custom
unet_gguf) tagged every file in the directory regardless of whether the
bucket could load it, so .safetensors files were tagged
model_type:unet_gguf and vice versa. Carry each bucket's registered
extension set through get_comfy_models_folders and only emit a
model_type tag when the file extension matches, keeping the empty-set
match-all convention from folder_paths.filter_files_extensions.
Files under a model base matching no bucket now keep only the models
tag instead of every directory-matching model_type tag.
* feat(assets): replace response file_path with persisted loader_path
The old file_path response field was a namespaced storage locator
(models/checkpoints/foo.safetensors): not an absolute path, not unique
identity, and not the value a loader consumes. Nothing needs that shape
on the wire (hash/ID-based locating is the long-term direction), so it
is dropped rather than renamed; the storage-root matching stays internal,
powering display_name.
What loaders DO need is the in-root loader path (category dropped:
models/checkpoints/foo/bar.safetensors -> foo/bar.safetensors). Serve it
as a first-class loader_path field, persisted on asset_references
(migration 0006) and written by every ingest pipeline at insert, so
responses read the column verbatim.
Like the model_type tags, loader_path is a seed-time derivative of the
model folder registry, maintained by the same scan lifecycle (new files seed
fresh values, pruning retires rows whose bucket disappeared). Rows
predating the column serve a null loader_path; databases from before
this stack already need recreating for the base branch's tag changes.
loader_path resolves every registered base including extra_model_paths
entries; display_name only the canonical storage roots. A file can
therefore be loadable with no display name (extra-path models) or the
reverse (unregistered files under the models root), and loader_path is
null exactly when no loader can resolve the file.
* test(assets): lock loader_path matrix (asymmetry, null, persist/read)
Cover the behaviour that has no production change but is easy to regress:
the extra-path asymmetry (loadable but no storage namespace), null
loader_path persistence for orphan files, and the response reading the
stored column with a compute fallback for un-backfilled rows.
* fix(assets): persist subfolder-qualified loader_path for ingested outputs
ingest_existing_file built its seed spec with the file's basename, so
outputs saved into a subfolder persisted loader_path (and the
user_metadata filename that preview URLs split for their subfolder
param) as just the basename: the served locator pointed at a file that
does not exist at that path. Scanner and seeder specs already derive
fname via compute_loader_path; use the same derivation here.
* fix(assets): only extension-matching buckets contribute a loader_path
The model-base match in get_asset_category_and_relative_path ignored
each bucket's extension set, so a file inside a registered base whose
extension the bucket cannot load (e.g. a .txt uploaded into
model_type:checkpoints) advertised a loader_path that no loader list
would ever resolve, while the tag side of the same stack already
excluded it. Apply the extension check used for backend tags (empty set
accepts any extension), keeping loader_path null exactly when no loader
can resolve the file.
* fix(assets): refresh loader_path when re-ingesting an existing reference
upsert_reference only wrote loader_path on the INSERT branch, so
re-ingesting an existing reference (an output overwritten in place, or a
file re-registered after its loader_path derivation changed) kept the
stale or NULL value forever. Write it on the UPDATE branch too, with a
null-safe change guard so a loader_path difference alone is enough to
trigger the update, and identical values stay a no-op.
* fix(assets): repair semantic merge breakage from #14796 and master
Two textually-clean but semantically-broken merges:
- routes.py lost its folder_paths import when #14796's import block
superseded the base's, while the content-type hardening added via the
base's master merge still calls folder_paths.is_dangerous_content_type.
- master's SVG download-hardening test uploads with the pre-namespacing
bare checkpoints tag, which this branch's destination validation
rejects; use model_type:checkpoints.
---------
Co-authored-by: guill <jacob.e.segal@gmail.com>
2026-07-09 17:00:08 +12:00
|
|
|
tags = ["models", "model_type:checkpoints", "unit-tests", "alpha"]
|
2026-01-30 23:22:05 -08:00
|
|
|
meta = {"purpose": "test", "epoch": 1, "flags": ["x", "y"], "nullable": None}
|
fix(assets): remove unused delete_content param from deleteAsset (#14241)
* fix(assets): remove unused delete_content param from deleteAsset
The delete_content query param on DELETE /api/assets/{id} was introduced
in #12125 and had its default flipped to false in #12621. In practice no
client sends it: the frontend issues a bare DELETE /assets/{id}, so every
real caller already gets the default soft-delete (the reference is hidden,
content preserved). The only thing that set delete_content=true was this
repo's own test teardown.
Remove the param from the route and the OpenAPI spec so the contract
matches what clients actually use (and lines up with the cloud surface).
The route now always soft-deletes. The underlying delete_asset_reference
helper keeps its delete_content_if_orphan option, so orphan reclamation
remains available internally for a future GC path — it's just no longer
exposed on the public endpoint. Tests that used delete_content=true for
hard cleanup now soft-delete; test_delete_upon_reference_count asserts
content preservation instead of orphan removal.
* test/docs: address review on deleteAsset delete_content removal
- Rename test_delete_upon_reference_count ->
test_soft_delete_preserves_asset_identity_across_references; the old name
implied last-ref cleanup, but it now verifies the opposite (soft delete
preserves identity across references).
- Strengthen the re-association assertion: also check asset_hash == src_hash
so it proves content reuse rather than relying on the now-tautological
created_new is False.
- Document delete_asset_reference: the orphan-reclamation branch is
intentionally internal-only; the public endpoint always soft-deletes.
- Normalize the soft-delete comment phrasing.
* test(assets): make seed content unique per test for isolation
Removing the delete_content param means delete is always a soft delete, so
content created by one test now survives into the next. The suite had been
relying on hard-delete teardown for isolation, so shared fixed-content
fixtures started colliding: seeded_asset (b"A"*4096) and
make_asset_bytes (deterministic on name) produced the same hash every test,
so the second seed deduped to the surviving asset and returned 200 instead
of 201, cascading into ~14 failures/errors.
Salt both fixtures with a per-test uuid so each test creates fresh content
(created_new True, 201), while keeping content deterministic within a test
(same name/size -> same bytes) and preserving exact byte length so size-based
list/sort assertions are unaffected.
2026-06-09 21:52:14 -07:00
|
|
|
# Unique content per test so the seed always creates a fresh asset (201).
|
|
|
|
|
# Delete is now always a soft delete, so content from a prior test survives
|
|
|
|
|
# and would otherwise dedup this upload into an existing asset (200).
|
|
|
|
|
content = uuid.uuid4().bytes + b"A" * (4096 - 16)
|
|
|
|
|
files = {"file": (name, content, "application/octet-stream")}
|
2026-01-30 23:22:05 -08:00
|
|
|
form_data = {
|
|
|
|
|
"tags": json.dumps(tags),
|
|
|
|
|
"name": name,
|
|
|
|
|
"user_metadata": json.dumps(meta),
|
|
|
|
|
}
|
|
|
|
|
r = http.post(api_base + "/api/assets", files=files, data=form_data, timeout=120)
|
|
|
|
|
body = r.json()
|
|
|
|
|
assert r.status_code == 201, body
|
2026-05-25 11:21:35 -07:00
|
|
|
from helpers import assert_hash_fields_consistent
|
|
|
|
|
assert_hash_fields_consistent(body)
|
2026-01-30 23:22:05 -08:00
|
|
|
return body
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
|
def autoclean_unit_test_assets(http: requests.Session, api_base: str):
|
|
|
|
|
"""Ensure isolation by removing all AssetInfo rows tagged with 'unit-tests' after each test."""
|
|
|
|
|
yield
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
r = http.get(
|
|
|
|
|
api_base + "/api/assets",
|
|
|
|
|
params={"include_tags": "unit-tests", "limit": "500", "sort": "name"},
|
|
|
|
|
timeout=30,
|
|
|
|
|
)
|
|
|
|
|
if r.status_code != 200:
|
|
|
|
|
break
|
|
|
|
|
body = r.json()
|
|
|
|
|
ids = [a["id"] for a in body.get("assets", [])]
|
|
|
|
|
if not ids:
|
|
|
|
|
break
|
|
|
|
|
for aid in ids:
|
|
|
|
|
with contextlib.suppress(Exception):
|
fix(assets): remove unused delete_content param from deleteAsset (#14241)
* fix(assets): remove unused delete_content param from deleteAsset
The delete_content query param on DELETE /api/assets/{id} was introduced
in #12125 and had its default flipped to false in #12621. In practice no
client sends it: the frontend issues a bare DELETE /assets/{id}, so every
real caller already gets the default soft-delete (the reference is hidden,
content preserved). The only thing that set delete_content=true was this
repo's own test teardown.
Remove the param from the route and the OpenAPI spec so the contract
matches what clients actually use (and lines up with the cloud surface).
The route now always soft-deletes. The underlying delete_asset_reference
helper keeps its delete_content_if_orphan option, so orphan reclamation
remains available internally for a future GC path — it's just no longer
exposed on the public endpoint. Tests that used delete_content=true for
hard cleanup now soft-delete; test_delete_upon_reference_count asserts
content preservation instead of orphan removal.
* test/docs: address review on deleteAsset delete_content removal
- Rename test_delete_upon_reference_count ->
test_soft_delete_preserves_asset_identity_across_references; the old name
implied last-ref cleanup, but it now verifies the opposite (soft delete
preserves identity across references).
- Strengthen the re-association assertion: also check asset_hash == src_hash
so it proves content reuse rather than relying on the now-tautological
created_new is False.
- Document delete_asset_reference: the orphan-reclamation branch is
intentionally internal-only; the public endpoint always soft-deletes.
- Normalize the soft-delete comment phrasing.
* test(assets): make seed content unique per test for isolation
Removing the delete_content param means delete is always a soft delete, so
content created by one test now survives into the next. The suite had been
relying on hard-delete teardown for isolation, so shared fixed-content
fixtures started colliding: seeded_asset (b"A"*4096) and
make_asset_bytes (deterministic on name) produced the same hash every test,
so the second seed deduped to the surviving asset and returned 200 instead
of 201, cascading into ~14 failures/errors.
Salt both fixtures with a per-test uuid so each test creates fresh content
(created_new True, 201), while keeping content deterministic within a test
(same name/size -> same bytes) and preserving exact byte length so size-based
list/sort assertions are unaffected.
2026-06-09 21:52:14 -07:00
|
|
|
http.delete(f"{api_base}/api/assets/{aid}", timeout=30)
|