Files
audio-scribe/tests/test_jobs_state.py
T
JMR-devandClaude Opus 5 605275e37e feat(jobs): add run configuration and the per-job runner
Retention deletions are sequenced so the guarantee actually holds: the video
goes only after the FLAC is extracted, verified and committed, and the FLAC only
after every output is written and committed. Each deletion records itself both
in state.json and in events.jsonl, so a later run can tell "removed as
requested" from "gone and should not be".

Two bugs the tests caught while writing this:

- params_changed compared the requested model id against the resolved one, so
  every re-run looked like a parameter change and re-transcribed. An unpinned
  model now matches whatever ran; only an explicit --model can disagree.
- chain=DEFAULT_CHAIN as a default argument binds at import time, which made
  the backend chain impossible to override. It is resolved at call time now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 15:04:43 -05:00

168 lines
6.2 KiB
Python

from __future__ import annotations
import pytest
from ccn_transcribe import errors
from ccn_transcribe.jobs import state as job_state
def make() -> job_state.JobState:
return job_state.JobState.new(job_id="youtube-abc", source_url="https://y.test/watch?v=abc")
class TestNew:
def test_starts_with_everything_missing(self) -> None:
job = make()
assert job.video.status is job_state.ArtifactStatus.MISSING
assert job.audio.status is job_state.ArtifactStatus.MISSING
assert job.outputs.status is job_state.ArtifactStatus.MISSING
def test_records_identity_and_timestamps(self) -> None:
job = make()
assert job.job_id == "youtube-abc"
assert job.source_url == "https://y.test/watch?v=abc"
assert job.created_at and job.updated_at
def test_carries_the_current_schema_version(self) -> None:
assert make().schema_version == job_state.SCHEMA_VERSION
class TestRoundTrip:
def test_survives_serialization(self) -> None:
job = make()
job.video = job_state.Artifact(
job_state.ArtifactStatus.PRESENT, path="media/v.mkv", duration_s=12.5
)
job.title = "A title"
assert job_state.JobState.from_dict(job.to_dict()) == job
def test_enums_serialize_as_plain_strings(self) -> None:
doc = make().to_dict()
assert doc["artifacts"]["video"]["status"] == "missing"
def test_outputs_paths_round_trip(self) -> None:
job = make()
job.outputs = job_state.Outputs(
job_state.ArtifactStatus.PRESENT, paths={"srt": "out/transcript.srt"}
)
assert job_state.JobState.from_dict(job.to_dict()).outputs.paths == {
"srt": "out/transcript.srt"
}
def test_transcription_record_round_trips(self) -> None:
job = make()
job.transcription = job_state.TranscriptionRecord(
model_id="m",
model_revision="r",
backend="openvino",
device="GPU",
language="en",
task="transcribe",
num_beams=1,
)
restored = job_state.JobState.from_dict(job.to_dict())
assert restored.transcription is not None
assert restored.transcription.device == "GPU"
def test_unknown_keys_are_ignored_not_fatal(self) -> None:
doc = make().to_dict()
doc["something_from_the_future"] = 1
assert job_state.JobState.from_dict(doc).job_id == "youtube-abc"
class TestSchemaVersioning:
def test_a_newer_schema_is_refused(self) -> None:
doc = make().to_dict()
doc["schema_version"] = job_state.SCHEMA_VERSION + 1
# Guessing at a future layout risks destroying the user's state dir.
with pytest.raises(errors.SchemaTooNewError):
job_state.JobState.from_dict(doc)
def test_the_current_schema_is_accepted(self) -> None:
assert job_state.JobState.from_dict(make().to_dict()) is not None
class TestParamsChanged:
def _record(self, **kw: object) -> job_state.TranscriptionRecord:
base: dict[str, object] = {
"model_id": "m",
"model_revision": "r",
"backend": "openvino",
"device": "GPU",
"language": "en",
"task": "transcribe",
"num_beams": 1,
}
base.update(kw)
return job_state.TranscriptionRecord(**base) # type: ignore[arg-type]
def test_no_prior_run_counts_as_changed(self) -> None:
assert make().params_changed(self._record()) is True
def test_identical_params_are_unchanged(self) -> None:
job = make()
job.transcription = self._record()
assert job.params_changed(self._record()) is False
@pytest.mark.parametrize(
("field", "value"),
[("model_id", "other"), ("language", "de"), ("task", "translate"), ("num_beams", 5)],
)
def test_a_different_request_forces_a_rerun(self, field: str, value: object) -> None:
job = make()
job.transcription = self._record()
assert job.params_changed(self._record(**{field: value})) is True
def test_device_and_backend_do_not_force_a_rerun(self) -> None:
# Falling back from GPU to CPU must not invalidate a good transcript.
job = make()
job.transcription = self._record()
assert job.params_changed(self._record(device="CPU", backend="x")) is False
class TestDefaultsFromSparseDocuments:
def test_a_document_with_no_artifacts_key_gets_defaults(self) -> None:
doc = make().to_dict()
del doc["artifacts"]
restored = job_state.JobState.from_dict(doc)
assert restored.video.status is job_state.ArtifactStatus.MISSING
assert not restored.outputs.paths
def test_an_empty_artifact_entry_gets_defaults(self) -> None:
doc = make().to_dict()
doc["artifacts"] = {"video": None, "audio": {}, "outputs": None}
restored = job_state.JobState.from_dict(doc)
assert restored.video.status is job_state.ArtifactStatus.MISSING
assert restored.audio.status is job_state.ArtifactStatus.MISSING
assert restored.outputs.status is job_state.ArtifactStatus.MISSING
class TestModelComparison:
def _record(self, model_id: str | None) -> job_state.TranscriptionRecord:
return job_state.TranscriptionRecord(
model_id=model_id,
model_revision=None,
backend="openvino",
device="GPU",
language="en",
task="transcribe",
num_beams=1,
)
def test_an_unpinned_model_matches_whatever_ran(self) -> None:
# The request carries None; the record carries the resolved id. Comparing
# them directly would make every re-run look like a parameter change.
job = make()
job.transcription = self._record("OpenVINO/whisper-large-v3-turbo-int8-ov")
assert job.params_changed(self._record(None)) is False
def test_an_explicit_pin_that_differs_forces_a_rerun(self) -> None:
job = make()
job.transcription = self._record("model-a")
assert job.params_changed(self._record("model-b")) is True
def test_an_explicit_pin_that_matches_does_not(self) -> None:
job = make()
job.transcription = self._record("model-a")
assert job.params_changed(self._record("model-a")) is False