From af753a0397306b8891dc68d8ee7cbde2e538e950 Mon Sep 17 00:00:00 2001 From: Edouard Vanbelle Date: Sun, 30 Aug 2026 19:14:28 +0200 Subject: [PATCH] test(transcode): pin transcode_import's contract and empty-tree run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Covers the job's surface: that it is registered, declares the metadata the admin panel switches on (`mutates: always`, recoverable, a repair description), and that a run against a drained tree completes cleanly with zeroed counters. It deliberately does NOT cover the re-keying, which is the part that matters most. That needs `.transcoded/webp/` entries on disk before the run, and nothing reachable over HTTP can create them — since the write path moved to the derived tier, only hash-less callers still write there, and hurl cannot place files in the server's storage directory. The migration is validated by a snapshot restore instead, the way the thumbnail one was; the file says so rather than implying coverage it does not have. The empty-tree assertions still earn their place. A drained tree is what every run after the first sees, so it is the overwhelming majority of this job's lifetime, and "does nothing, quietly" is a real property: the thumbnail teardown warned `could not be removed / No such file or directory` on every boot after its migration finished — warning about success forever — and that was caught by eye, not by a test. Counters are asserted as exact zeros, since finding work in a directory the API cannot populate is the shape a re-keying bug would take. Co-Authored-By: Claude Opus 5 (1M context) --- tests/api/run.sh | 1 + tests/api/transcode_import.hurl | 148 ++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 tests/api/transcode_import.hurl diff --git a/tests/api/run.sh b/tests/api/run.sh index f829274d..1c483ea1 100755 --- a/tests/api/run.sh +++ b/tests/api/run.sh @@ -170,6 +170,7 @@ hurl --variables-file "$API_DIR/test.env" --file-root "$REPO_ROOT/tests" --test "$API_DIR/thumbnail_etag_content_keyed.hurl" \ "$API_DIR/attached_thumbnail_copy.hurl" \ "$API_DIR/transcode_cache.hurl" \ + "$API_DIR/transcode_import.hurl" \ "$API_DIR/dedup_admin_gate.hurl" \ "$API_DIR/admin_jobs.hurl" \ "$API_DIR/recoverable_jobs.hurl" \ diff --git a/tests/api/transcode_import.hurl b/tests/api/transcode_import.hurl new file mode 100644 index 00000000..9ac9d0ea --- /dev/null +++ b/tests/api/transcode_import.hurl @@ -0,0 +1,148 @@ +# ============================================================= +# OxiCloud – transcode_import: registration, contract, empty-tree run +# +# ## What this covers, and what it deliberately does not +# +# It covers the job's SURFACE: that it is registered, that it declares +# the metadata the admin panel switches on, and that a run against an +# already-drained tree completes cleanly with zeroed counters rather +# than erroring or reporting phantom work. +# +# It does NOT cover the re-keying — the part that matters most, where +# `{file_id}.webp` is resolved through `storage.files` to a content +# hash, and several entries naming the same content collapse into one +# row. That needs `.transcoded/webp/` entries on disk before the run, +# and nothing reachable over HTTP can create them: since the write path +# moved to the derived tier, only callers with no content hash +# (external mounts) still write there, and hurl cannot place files in +# the server's storage directory either. +# +# So the migration itself is validated by a snapshot restore against a +# populated `.transcoded/`, the same way the thumbnail migration was — +# an exercise that found three bugs the test suite did not. This file +# guards the contract that surrounds it; it is not a substitute. +# +# The empty-tree assertions are still worth having. A drained tree is +# the steady state of every install after the first boot, so this is +# what the job does on the overwhelming majority of its runs, and +# "does nothing, quietly, forever" is a real property — the thumbnail +# equivalent warned on every boot for exactly this case until it was +# caught by eye. +# +# Prerequisites: setup.hurl must have run (admin user exists). +# +# Run: +# hurl --variables-file tests/api/test.env --file-root tests \ +# --test tests/api/transcode_import.hurl +# ============================================================= + + +# ───────────────────────────────────────────────────────────── +# Step 1 – Login +# ───────────────────────────────────────────────────────────── +POST {{base_url}}/api/auth/login +Content-Type: application/json +{ + "username": "{{username}}", + "password": "{{password}}" +} + +HTTP 200 +[Captures] +token: jsonpath "$.access_token" + + +# ───────────────────────────────────────────────────────────── +# Step 2 – The job is registered, and declares itself correctly. +# +# The admin panel keys its read-only badge and its repair toggle off +# these three fields, so a job that stops declaring them degrades the +# UI silently: `mutates` wrong means a destructive job renders as safe, +# and a missing `repair_description` removes the only way to reach the +# deletion from the interface. +# +# `always` because a plain run inserts rows and writes blobs — the +# repair flag adds deletion on top, which is why the two are +# independent rather than one implying the other. +# ───────────────────────────────────────────────────────────── +GET {{base_url}}/api/admin/jobs +Authorization: Bearer {{token}} + +HTTP 200 +[Asserts] +jsonpath "$[*].name" contains "transcode_import" +jsonpath "$[?(@.name=='transcode_import')].mutates" == "always" +jsonpath "$[?(@.name=='transcode_import')].recoverable" == true + + +# ───────────────────────────────────────────────────────────── +# Step 3 – A discovery-only run over a drained tree. +# +# No `?repair=true`: the default must import without deleting, which is +# what the daily-tick and the no-silent-auto-repair rule both depend +# on. Counters are asserted to exact zeros rather than "exists" — a +# non-zero here would mean the job found work in a directory the API +# cannot have put anything into, which is the shape a re-keying bug +# would take. +# ───────────────────────────────────────────────────────────── +POST {{base_url}}/api/admin/jobs/transcode_import/trigger +Authorization: Bearer {{token}} + +HTTP 200 +[Asserts] +jsonpath "$.ok" == true +jsonpath "$.outcome.outcome" == "ok" +jsonpath "$.outcome.extra.completed" == true +jsonpath "$.outcome.extra.extra_stats.imported" == 0 +jsonpath "$.outcome.extra.extra_stats.negatives" == 0 +jsonpath "$.outcome.extra.extra_stats.file_gone" == 0 +jsonpath "$.outcome.extra.extra_stats.deleted" == 0 +jsonpath "$.outcome.extra.extra_stats.failed" == 0 +jsonpath "$.outcome.extra.extra_stats.unverified" == 0 +# A clean run raises no findings. `file_gone` entries are recorded as +# anomalies, so a non-zero count here would contradict the zero above. +jsonpath "$.outcome.extra.finding_count" == 0 + + +# ───────────────────────────────────────────────────────────── +# Step 4 – The same run under `?repair=true`. +# +# On a drained tree this must be indistinguishable from the run above: +# nothing to import, nothing to delete, and — the part worth pinning — +# no complaint about the directory being absent. The thumbnail teardown +# warned `could not be removed / No such file or directory` on every +# boot after its migration finished, warning about success forever. +# Absence is the end state, not a failure, and the job has to treat it +# that way because this is what every run after the first looks like. +# ───────────────────────────────────────────────────────────── +POST {{base_url}}/api/admin/jobs/transcode_import/trigger?repair=true +Authorization: Bearer {{token}} + +HTTP 200 +[Asserts] +jsonpath "$.ok" == true +jsonpath "$.outcome.outcome" == "ok" +jsonpath "$.outcome.extra.extra_stats.imported" == 0 +jsonpath "$.outcome.extra.extra_stats.negatives" == 0 +jsonpath "$.outcome.extra.extra_stats.deleted" == 0 +jsonpath "$.outcome.extra.extra_stats.failed" == 0 +jsonpath "$.outcome.extra.finding_count" == 0 + + +# ───────────────────────────────────────────────────────────── +# Step 5 – Idempotence. +# +# The job is registered as a daily tick AND named in +# OXICLOUD_STARTUP_JOBS, so on a long-lived install it runs +# unattended, repeatedly, forever. Re-running must stay a no-op — +# anything that accumulated across runs would accumulate unattended. +# ───────────────────────────────────────────────────────────── +POST {{base_url}}/api/admin/jobs/transcode_import/trigger +Authorization: Bearer {{token}} + +HTTP 200 +[Asserts] +jsonpath "$.outcome.outcome" == "ok" +jsonpath "$.outcome.extra.extra_stats.imported" == 0 +jsonpath "$.outcome.extra.extra_stats.negatives" == 0 +jsonpath "$.outcome.extra.finding_count" == 0