Files
VoiceStudio/backend/tests/test_audio_validation.py
T
Palash DebnathandClaude Fable 5 0ee62b2261 feat(gallery): save gallery voices as profiles, with validated audio references (#1542)
* feat(gallery): save gallery voices as profiles, with validated audio references

Work-in-progress lifted from the concurrent gallery session at the
owner's request (its uncommitted working tree, preserved verbatim from
base 92b1ee5d; safety snapshot remains at rescue/gallery-wip):

- gallery voices can be saved as local profiles: audio is copied into
  the profile store with content-addressed filenames, existing profiles
  are detected and refreshed only when the source clip changed
- backend/core/audio_validation.py: symlink-rejecting, root-contained
  resolution for persisted profile WAV references, with tests
- archetype/community routers and the Voice Gallery UI updated for the
  save-as-profile handoff (spec: docs/specs/longform/26-gallery-use-handoff.md)
- locale updates for the new gallery strings across all 21 files

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: drop a stray local screenshot script that rode in with the tree copy

* fix(community): explain the tolerated Content-Length parse failure; drop an unused import

CodeQL on #1542: the empty except now says why it is safe (the streamed
byte counter enforces the same cap regardless), and the test file loses
an unused Path import.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(gallery): review findings — copy outside the write lock, no stale completions

CodeRabbit on #1542, all findings addressed:
- the profile-audio copy stages to a .part temp BEFORE BEGIN IMMEDIATE
  and publishes via atomic os.replace inside it — other backend writers
  no longer block for the duration of an audio copy; a mid-copy failure
  leaves no temp droppings and no profile row (both pinned by tests)
- VoiceGallery async ops carry per-operation generation tokens: a
  preview or save-as-profile that resolves after unmount (or after a
  newer operation) can no longer play audio, redirect into a workspace,
  or touch state — three fail-before regression tests
- VoiceGalleryActions imports the page at test runtime; the e2e locator
  uses a stable data-testid instead of a translated string; symlink
  tests skip cleanly where the OS can't create symlinks; the changelog
  line carries its PR ref

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: static ffmpeg fallback when the chocolatey feed is down

Third feed outage to break a PR run (2026-07-20, 2026-07-28, today —
three attempts, three 'installed 0/1'). Chocolatey is a distribution
channel, not the dependency: after the retry loop exhausts, fetch the
static gyan.dev build from its GitHub release mirror and put it on
PATH — same binary, no feed in the path. URL verified live (HTTP 200).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 22:48:05 +00:00

45 lines
1.3 KiB
Python

"""Regression tests for the lightweight persisted-WAV trust boundary."""
from __future__ import annotations
import struct
from core.audio_validation import is_playable_wav, resolve_regular_file
def test_oversized_declared_wav_payload_is_not_treated_as_playable(tmp_path):
"""A hostile frame count must be bounded and backed by real payload bytes."""
path = tmp_path / "oversized.wav"
declared_size = 0xFFFF_FFF0
header = struct.pack(
"<4sI4s4sIHHIIHH4sI",
b"RIFF",
0xFFFF_FFFF,
b"WAVE",
b"fmt ",
16,
1,
1,
24_000,
48_000,
2,
16,
b"data",
declared_size,
)
path.write_bytes(header + b"\x00\x01")
assert not is_playable_wav(path)
def test_profile_wav_resolution_rejects_escape_and_symlink(tmp_path, symlinks_supported):
root = tmp_path / "voices"
root.mkdir()
outside = tmp_path / "outside.wav"
outside.write_bytes(b"outside")
assert resolve_regular_file(root, "../outside.wav") is None
assert resolve_regular_file(root, str(outside)) is None
if symlinks_supported: # Windows needs Developer Mode to create symlinks
(root / "linked.wav").symlink_to(outside)
assert resolve_regular_file(root, "linked.wav") is None