* 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>
45 lines
1.3 KiB
Python
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
|