Files
UmbraAudio/tests/test_release_comments.py
2026-10-18 19:01:04 -07:00

216 lines
8.8 KiB
Python

# Copyright 2026 Umbra Tools (umbratools.com)
# SPDX-License-Identifier: Apache-2.0
"""Tests for release comments - the explicit step in a release (spec 22).
The comment is a gate, and its designator has to make two releases on the same day
unambiguous. Those are the two properties worth pinning.
"""
from __future__ import annotations
from datetime import UTC, datetime, timedelta
from pathlib import Path
from typing import TYPE_CHECKING
import pytest
if TYPE_CHECKING:
from pathlib import Path
from umbraaudio.release_comments import (
STATUS_APPROVED,
STATUS_DRAFT,
CommentError,
build_designator,
build_slug,
comment_path,
create_comment,
draft_comment_text,
list_comments,
load_comment,
next_sequence,
parse_comment,
require_approved,
update_comment,
)
T0 = datetime(2026, 10, 3, 14, 32, 5, tzinfo=UTC)
class TestDesignator:
def test_slug_carries_date_time_and_sequence(self) -> None:
assert build_slug(T0, 2) == "release-20261003-143205-02"
def test_designator_is_readable(self) -> None:
assert build_designator(T0, 2) == "2026-10-03 #2 14:32:05"
def test_sequence_is_per_date_not_global(self, tmp_path: Path) -> None:
create_comment(tmp_path, "First", now=T0)
create_comment(tmp_path, "Second", now=T0 + timedelta(seconds=30))
assert next_sequence(tmp_path, T0) == 3
# A new date starts at 1 again, because the date is part of the handle.
assert next_sequence(tmp_path, T0 + timedelta(days=1)) == 1
def test_same_day_sequence_disambiguates_two_releases(self, tmp_path: Path) -> None:
a = create_comment(tmp_path, "First attempt", now=T0)
b = create_comment(tmp_path, "Corrected attempt", now=T0 + timedelta(hours=1))
assert a.sequence == 1 and b.sequence == 2
assert a.date == b.date
assert a.slug != b.slug
assert "#2" in b.designator
def test_two_comments_in_the_same_second_still_get_distinct_slugs(
self, tmp_path: Path
) -> None:
"""The sequence is what makes this safe, not a timestamp collision check."""
a = create_comment(tmp_path, "One", now=T0)
b = create_comment(tmp_path, "Two", now=T0)
assert a.slug != b.slug
assert (a.sequence, b.sequence) == (1, 2)
assert load_comment(tmp_path, a.slug).summary == "One"
assert load_comment(tmp_path, b.slug).summary == "Two"
class TestParseComment:
def test_first_line_is_the_summary(self) -> None:
summary, body = parse_comment("Fix the padding\n\n2s lead and trail.")
assert summary == "Fix the padding"
assert body == "2s lead and trail."
def test_body_only_comment(self) -> None:
summary, body = parse_comment("Just a summary")
assert body == ""
def test_empty_comment_is_refused(self) -> None:
with pytest.raises(CommentError, match="at least a summary"):
parse_comment(" \n \n")
def test_leading_blank_lines_are_ignored(self) -> None:
"""Padding a note must not silently become the summary."""
summary, body = parse_comment("\n\nShipped the fix\n\nDetail.")
assert summary == "Shipped the fix"
assert body == "Detail."
class TestLifecycle:
def test_new_comment_is_a_draft(self, tmp_path: Path) -> None:
comment = create_comment(tmp_path, "Ship it", now=T0)
assert comment.status == STATUS_DRAFT
assert comment.approved_at is None
def test_draft_can_be_edited(self, tmp_path: Path) -> None:
created = create_comment(tmp_path, "Wrong summary", now=T0)
edited = update_comment(tmp_path, created.slug, text="Right summary\nwith detail")
assert edited.summary == "Right summary"
assert edited.body == "with detail"
def test_approved_comment_is_frozen(self, tmp_path: Path) -> None:
created = create_comment(tmp_path, "Original", now=T0)
update_comment(tmp_path, created.slug, status=STATUS_APPROVED)
with pytest.raises(CommentError, match="frozen"):
update_comment(tmp_path, created.slug, text="Changed my mind")
# ...and the stored text is genuinely unchanged.
assert load_comment(tmp_path, created.slug).summary == "Original"
def test_approve_records_the_time(self, tmp_path: Path) -> None:
created = create_comment(tmp_path, "Ready", now=T0)
approved = update_comment(tmp_path, created.slug, status=STATUS_APPROVED)
assert approved.status == STATUS_APPROVED
assert approved.approved_at is not None
def test_cannot_change_text_while_approving(self, tmp_path: Path) -> None:
created = create_comment(tmp_path, "As drafted", now=T0)
with pytest.raises(CommentError, match="Edit the draft first"):
update_comment(tmp_path, created.slug, text="Different", status=STATUS_APPROVED)
def test_publishing_requires_an_approved_comment(self, tmp_path: Path) -> None:
created = create_comment(tmp_path, "Draft only", now=T0)
with pytest.raises(CommentError, match="DRAFT"):
require_approved(tmp_path, created.slug)
def test_publishing_requires_a_comment_to_exist(self, tmp_path: Path) -> None:
with pytest.raises(CommentError, match="no release comment"):
require_approved(tmp_path, "release-20261003-000000-01")
def test_approved_comment_satisfies_the_gate(self, tmp_path: Path) -> None:
created = create_comment(tmp_path, "Good to go", now=T0)
update_comment(tmp_path, created.slug, status=STATUS_APPROVED)
assert require_approved(tmp_path, created.slug).slug == created.slug
def test_approved_at_creation_is_immediately_publishable(self, tmp_path: Path) -> None:
created = create_comment(tmp_path, "Pre-approved", now=T0, status=STATUS_APPROVED)
assert require_approved(tmp_path, created.slug).status == STATUS_APPROVED
class TestStorage:
def test_round_trips_through_disk(self, tmp_path: Path) -> None:
created = create_comment(
tmp_path, "Summary line\n\nBody text.", author="worstauthor", now=T0,
facts={"chapters": 32},
)
loaded = load_comment(tmp_path, created.slug)
assert loaded.summary == "Summary line"
assert loaded.body == "Body text."
assert loaded.facts["chapters"] == 32
assert loaded.author == "worstauthor"
def test_listing_is_newest_first(self, tmp_path: Path) -> None:
a = create_comment(tmp_path, "First", now=T0)
b = create_comment(tmp_path, "Second", now=T0 + timedelta(minutes=5))
rows = list_comments(tmp_path)
assert [r["slug"] for r in rows] == [b.slug, a.slug]
def test_missing_comment_is_none_not_an_error(self, tmp_path: Path) -> None:
assert load_comment(tmp_path, "release-19700101-000000-01") is None
def test_no_tmp_files_left_behind(self, tmp_path: Path) -> None:
create_comment(tmp_path, "Clean write", now=T0)
assert list(comment_path(tmp_path, "release-20261003-143205-01").parent.glob("*.tmp")) == []
def test_listing_empty_dir(self, tmp_path: Path) -> None:
assert list_comments(tmp_path) == []
class TestDraftFromBuild:
def _record(self) -> dict:
return {
"work": "Deal Hunter",
"edition_number": 1,
"package": {
"object_key": "export/Deal_Hunter.m4b",
"bytes": 325875309,
"built_at": "2026-09-09T11:21:11+00:00",
"sha256": "abc",
},
"delivery_profile": {
"sample_rate": 44100, "channels": 1, "bit_rate_kbps": 192,
"lead_silence_s": 2.0, "trail_silence_s": 2.0, "target_rms_db": -20.5,
},
"approvals": {
"voices_explicitly_approved": 24,
"voices_implicitly_approved": 29,
"voices_in_profile_without_lines": 10,
},
"chapters": [
{"mp3": {"loudness_ok": True, "peak_ok": True}, "warnings": ["4.0s longer"]},
{"mp3": {"loudness_ok": True, "peak_ok": True}, "warnings": []},
],
}
def test_comment_states_the_measured_facts(self) -> None:
text = draft_comment_text(self._record(), "worstauthor")
assert "Deal Hunter edition 1" in text
assert "2 chapters pinned" in text
assert "44100 Hz" in text
assert "+2.0s lead" in text
assert "24 explicitly approved" in text
assert "29 approved implicitly" in text
def test_comment_names_the_post_approval_delta(self) -> None:
text = draft_comment_text(self._record(), "worstauthor")
assert "1 chapters shipped longer or resampled" in text
def test_comment_is_first_line_summary(self) -> None:
summary, _ = parse_comment(draft_comment_text(self._record(), "x"))
assert "Deal Hunter" in summary