fix: make WaveTone e2e resolver deterministic

This commit is contained in:
Shuhan Zhang
2026-06-07 13:47:17 +08:00
parent 69622005a4
commit f5b2212c84
7 changed files with 66 additions and 35 deletions
+2 -2
View File
@@ -1228,7 +1228,7 @@ gimp 107 passed ✅ (64 unit + 43 e2e)
blender 208 passed ✅ (150 unit + 58 e2e)
inkscape 202 passed ✅ (148 unit + 54 e2e)
audacity 161 passed ✅ (107 unit + 54 e2e)
wavetone 28 passed ✅ (23 unit + 5 default e2e, 2 backend-gated e2e skipped; 30 passed opt-in)
wavetone 30 passed ✅ (24 unit + 6 default e2e, 2 backend-gated e2e skipped; 32 passed opt-in)
libreoffice 158 passed ✅ (89 unit + 69 e2e)
mubu 96 passed ✅ (85 unit + 11 e2e)
obs-studio 153 passed ✅ (116 unit + 37 e2e)
@@ -1308,7 +1308,7 @@ cli-anything/
├── 🧊 blender/agent-harness/ # Blender CLI (208 tests)
├── ✏️ inkscape/agent-harness/ # Inkscape CLI (202 tests)
├── 🎵 audacity/agent-harness/ # Audacity CLI (161 tests)
├── wavetone/agent-harness/ # WaveTone CLI (30 tests: 28 default + 2 backend-gated e2e)
├── wavetone/agent-harness/ # WaveTone CLI (32 tests: 30 default + 2 backend-gated e2e)
├── 🌐 browser/agent-harness/ # Browser CLI (DOMShell MCP, new)
├── 📄 libreoffice/agent-harness/ # LibreOffice CLI (158 tests)
├── 📧 mailchimp/agent-harness/ # Mailchimp Marketing API CLI (303 commands, 36 unit tests)
+2 -2
View File
@@ -679,7 +679,7 @@ gimp 107 passed ✅ (64 unit + 43 e2e)
blender 208 passed ✅ (150 unit + 58 e2e)
inkscape 202 passed ✅ (148 unit + 54 e2e)
audacity 161 passed ✅ (107 unit + 54 e2e)
wavetone 28 passed ✅ (23 unit + 5 default e2e,2 backend-gated e2e skipped;30 passed opt-in)
wavetone 30 passed ✅ (24 unit + 6 default e2e,2 backend-gated e2e skipped;32 passed opt-in)
libreoffice 158 passed ✅ (89 unit + 69 e2e)
obs-studio 153 passed ✅ (116 unit + 37 e2e)
kdenlive 155 passed ✅ (111 unit + 44 e2e)
@@ -753,7 +753,7 @@ cli-anything/
├── 🧊 blender/agent-harness/ # Blender CLI(208 项测试)
├── ✏️ inkscape/agent-harness/ # Inkscape CLI(202 项测试)
├── 🎵 audacity/agent-harness/ # Audacity CLI(161 项测试)
├── wavetone/agent-harness/ # WaveTone CLI(30 项测试:28 default + 2 backend-gated e2e)
├── wavetone/agent-harness/ # WaveTone CLI(32 项测试:30 default + 2 backend-gated e2e)
├── 📄 libreoffice/agent-harness/ # LibreOffice CLI(158 项测试)
├── 📚 zotero/agent-harness/ # Zotero CLI(新增,支持文献导入)
├── 📓 joplin/agent-harness/ # Joplin CLI(134 测试:107 单元 + 27 e2e)
@@ -56,6 +56,5 @@ without WaveTone can still run the CLI-only suite. To opt into the real Windows
smoke, set `CLI_ANYTHING_WAVETONE_REAL_BACKEND=1` plus `WAVETONE_EXE` or
`WAVETONE_HOME` pointing at a WaveTone 2.61 extraction with `wavetone.exe` and
the bundled `data` directory. E2E subprocess tests run the in-tree
`python -m cli_anything.wavetone.wavetone_cli` module by default; set
`CLI_ANYTHING_FORCE_INSTALLED=1` only when intentionally testing an installed
entry point.
`python -m cli_anything.wavetone.wavetone_cli` module by default and do not
silently prefer an installed `cli-anything-wavetone` from `PATH`.
@@ -2,9 +2,9 @@
## Test Inventory Plan
- `test_core.py`: 23 unit tests for project manifests, audio probing, session logs,
- `test_core.py`: 24 unit tests for project manifests, audio probing, session logs,
and backend discovery.
- `test_full_e2e.py`: 7 tests covering default real-backend opt-in gating,
- `test_full_e2e.py`: 8 tests covering default real-backend opt-in gating,
source-module subprocess resolution, CLI workflows, and real WaveTone launch
smoke coverage.
@@ -17,6 +17,7 @@
- Add labels in sorted time order.
- Set tempo and analysis options.
- Preserve omitted CLI boolean analysis flags as tri-state values.
- Require attached WFD analysis files to exist and use the `.wfd` suffix.
- Reject non-finite numeric project values before JSON serialization.
- Probe a generated WAV file with the Python stdlib.
- Fall back to stat metadata for malformed WAV files.
@@ -48,7 +49,8 @@ Operations:
1. Generate a real WAV fixture.
2. Resolve the in-tree `python -m cli_anything.wavetone.wavetone_cli` module by
default, even if an installed `cli-anything-wavetone` exists in `PATH`.
default, even if an installed `cli-anything-wavetone` exists in `PATH` or
`CLI_ANYTHING_FORCE_INSTALLED` is set externally.
3. Run `cli-anything-wavetone --json project new`.
4. Run `project set-tempo`.
5. Run `project add-label`.
@@ -86,7 +88,7 @@ Default command:
```bash
$env:PATH = "$env:APPDATA\Python\Python313\Scripts;$env:PATH"
Remove-Item Env:CLI_ANYTHING_FORCE_INSTALLED -ErrorAction SilentlyContinue
$env:CLI_ANYTHING_FORCE_INSTALLED = "1"
Remove-Item Env:CLI_ANYTHING_WAVETONE_REAL_BACKEND -ErrorAction SilentlyContinue
Remove-Item Env:WAVETONE_EXE -ErrorAction SilentlyContinue
Remove-Item Env:WAVETONE_HOME -ErrorAction SilentlyContinue
@@ -96,7 +98,7 @@ python -m pytest cli_anything\wavetone\tests\ -v -s
Default result:
```text
collected 30 items
collected 32 items
cli_anything/wavetone/tests/test_core.py::test_create_project_manifest PASSED
cli_anything/wavetone/tests/test_core.py::test_rejects_unsupported_audio PASSED
@@ -104,6 +106,7 @@ cli_anything/wavetone/tests/test_core.py::test_save_load_project_roundtrip PASSE
cli_anything/wavetone/tests/test_core.py::test_labels_are_sorted PASSED
cli_anything/wavetone/tests/test_core.py::test_update_analysis_settings PASSED
cli_anything/wavetone/tests/test_core.py::test_cli_analysis_preserves_omitted_boolean_flags PASSED
cli_anything/wavetone/tests/test_core.py::test_cli_attach_wfd_requires_existing_wfd_file PASSED
cli_anything/wavetone/tests/test_core.py::test_rejects_non_finite_project_numbers PASSED
cli_anything/wavetone/tests/test_core.py::test_load_project_rejects_non_object_json PASSED
cli_anything/wavetone/tests/test_core.py::test_probe_wav_metadata PASSED
@@ -123,20 +126,21 @@ cli_anything/wavetone/tests/test_core.py::test_repl_skin_uses_wavetone_branding_
cli_anything/wavetone/tests/test_core.py::test_repl_reports_click_exit_without_unexpected_error PASSED
cli_anything/wavetone/tests/test_full_e2e.py::test_real_backend_requires_explicit_opt_in PASSED
cli_anything/wavetone/tests/test_full_e2e.py::test_resolve_cli_defaults_to_source_module PASSED
cli_anything/wavetone/tests/test_full_e2e.py::test_resolve_cli_uses_installed_only_when_requested PASSED
cli_anything/wavetone/tests/test_full_e2e.py::TestCLISubprocess::test_help PASSED
cli_anything/wavetone/tests/test_full_e2e.py::TestCLISubprocess::test_project_audio_workflow_json PASSED
cli_anything/wavetone/tests/test_full_e2e.py::TestCLISubprocess::test_formats_json PASSED
cli_anything/wavetone/tests/test_full_e2e.py::TestRealWaveToneBackend::test_doctor_real_backend SKIPPED
cli_anything/wavetone/tests/test_full_e2e.py::TestRealWaveToneBackend::test_launch_real_backend_with_wav SKIPPED
28 passed, 2 skipped in 2.10s
30 passed, 2 skipped in 1.44s
```
Real backend opt-in command:
```bash
$env:PATH = "$env:APPDATA\Python\Python313\Scripts;$env:PATH"
Remove-Item Env:CLI_ANYTHING_FORCE_INSTALLED -ErrorAction SilentlyContinue
$env:CLI_ANYTHING_FORCE_INSTALLED = "1"
$env:CLI_ANYTHING_WAVETONE_REAL_BACKEND = "1"
$env:WAVETONE_HOME = "C:\Users\Hp\Desktop\wavetone2.6.1"
Remove-Item Env:WAVETONE_EXE -ErrorAction SilentlyContinue
@@ -146,22 +150,23 @@ python -m pytest cli_anything\wavetone\tests\ -v -s
Real backend result:
```text
30 passed in 3.44s
32 passed in 3.03s
```
## Coverage Notes
- Unit tests cover manifest creation, validation, persistence, labels, tempo,
analysis settings, CLI analysis flag tri-state behavior, finite numeric
validation, project JSON schema validation, audio probing, malformed WAV
fallback, session logs, session schema and payload validation, backend
discovery, backend data artifact validation, ffprobe argument construction,
ffprobe metadata parsing, inherited CLI project and JSON context, failed
launch smoke reporting, launch runtime error reporting, Windows launch gating,
REPL Windows path splitting, REPL branding and local skill path display, and
REPL Click exit handling.
validation, WFD attachment validation, project JSON schema validation, audio
probing, malformed WAV fallback, session logs, session schema and payload
validation, backend discovery, backend data artifact validation, ffprobe
argument construction, ffprobe metadata parsing, inherited CLI project and
JSON context, failed launch smoke reporting, launch runtime error reporting,
Windows launch gating, REPL Windows path splitting, REPL branding and local
skill path display, and REPL Click exit handling.
- CLI subprocess tests resolve and use the in-tree
`python -m cli_anything.wavetone.wavetone_cli` module by default.
`python -m cli_anything.wavetone.wavetone_cli` module by default, regardless
of ambient `CLI_ANYTHING_FORCE_INSTALLED`.
- Real backend coverage launches `C:\Users\Hp\Desktop\wavetone2.6.1\wavetone.exe`
with a generated WAV and terminates it after a short wait.
- Real backend tests are skipped by default unless
@@ -99,6 +99,30 @@ def test_cli_analysis_preserves_omitted_boolean_flags(tmp_path: Path) -> None:
assert loaded["analysis"]["skip_analysis_dialog"] is True
def test_cli_attach_wfd_requires_existing_wfd_file(tmp_path: Path) -> None:
wav = make_wav(tmp_path / "tone.wav")
project_path = save_project(create_project(wav), tmp_path / "tone.wt.json")
missing = tmp_path / "missing.wfd"
result = CliRunner().invoke(cli, ["--project", str(project_path), "project", "attach-wfd", str(missing)])
assert result.exit_code != 0
assert "does not exist" in result.output
wrong_suffix = tmp_path / "analysis.txt"
wrong_suffix.write_text("wfd", encoding="utf-8")
result = CliRunner().invoke(cli, ["--project", str(project_path), "project", "attach-wfd", str(wrong_suffix)])
assert result.exit_code == 1
assert ".wfd" in result.output
wfd_path = tmp_path / "analysis.wfd"
wfd_path.write_text("wfd", encoding="utf-8")
result = CliRunner().invoke(cli, ["--project", str(project_path), "--json", "project", "attach-wfd", str(wfd_path)])
assert result.exit_code == 0, result.output
data = json.loads(result.output)
assert data["wfd_path"] == str(wfd_path.resolve())
def test_rejects_non_finite_project_numbers(tmp_path: Path) -> None:
wav = make_wav(tmp_path / "tone.wav")
project = create_project(wav)
@@ -15,11 +15,10 @@ from cli_anything.wavetone.utils import wavetone_backend
REAL_BACKEND_ENV = "CLI_ANYTHING_WAVETONE_REAL_BACKEND"
def _resolve_cli(name: str) -> list[str]:
def _resolve_cli(name: str, force_installed: bool = False) -> list[str]:
"""Run the in-tree module by default; installed entry points are opt-in."""
force = os.environ.get("CLI_ANYTHING_FORCE_INSTALLED", "").strip() == "1"
module = "cli_anything.wavetone.wavetone_cli"
if not force:
if not force_installed:
print(f"[_resolve_cli] Using source module: {sys.executable} -m {module}")
return [sys.executable, "-m", module]
@@ -66,17 +65,21 @@ def test_real_backend_requires_explicit_opt_in(monkeypatch: pytest.MonkeyPatch)
def test_resolve_cli_defaults_to_source_module(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("CLI_ANYTHING_FORCE_INSTALLED", raising=False)
monkeypatch.setattr(shutil, "which", lambda name: "C:/foreign/cli-anything-wavetone.exe")
monkeypatch.setenv("CLI_ANYTHING_FORCE_INSTALLED", "1")
monkeypatch.setattr(shutil, "which", lambda name: pytest.fail("default resolver should not inspect PATH"))
assert _resolve_cli("cli-anything-wavetone") == [sys.executable, "-m", "cli_anything.wavetone.wavetone_cli"]
class TestCLISubprocess:
CLI_BASE = _resolve_cli("cli-anything-wavetone")
def test_resolve_cli_uses_installed_only_when_requested(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(shutil, "which", lambda name: "C:/installed/cli-anything-wavetone.exe")
assert _resolve_cli("cli-anything-wavetone", force_installed=True) == ["C:/installed/cli-anything-wavetone.exe"]
class TestCLISubprocess:
def _run(self, args: list[str], check: bool = True) -> subprocess.CompletedProcess[str]:
return subprocess.run(self.CLI_BASE + args, capture_output=True, text=True, check=check)
return subprocess.run(_resolve_cli("cli-anything-wavetone") + args, capture_output=True, text=True, check=check)
def test_help(self) -> None:
result = self._run(["--help"])
@@ -115,10 +118,8 @@ class TestCLISubprocess:
@pytest.mark.skipif(not _REAL_BACKEND_READY, reason=_REAL_BACKEND_SKIP_REASON)
class TestRealWaveToneBackend:
CLI_BASE = _resolve_cli("cli-anything-wavetone")
def _run(self, args: list[str], check: bool = True) -> subprocess.CompletedProcess[str]:
return subprocess.run(self.CLI_BASE + args, capture_output=True, text=True, check=check)
return subprocess.run(_resolve_cli("cli-anything-wavetone") + args, capture_output=True, text=True, check=check)
def test_doctor_real_backend(self) -> None:
result = self._run(["--json", "wavetone", "doctor"])
@@ -227,12 +227,14 @@ def project_analysis(
@project_group.command("attach-wfd")
@click.argument("wfd_path", type=click.Path(dir_okay=False))
@click.argument("wfd_path", type=click.Path(exists=True, dir_okay=False))
@click.argument("project_path", required=False, type=click.Path(exists=True, dir_okay=False))
@click.pass_context
def project_attach_wfd(ctx: click.Context, wfd_path: str, project_path: str | None) -> None:
"""Attach a WFD analysis file saved by WaveTone."""
try:
if Path(wfd_path).suffix.lower() != ".wfd":
raise ValueError("WFD path must use the .wfd extension")
project, path = load_ctx_project(ctx, project_path)
set_wfd_path(project, wfd_path)
save_project(project, path)