From 08a47339561cd168e8431e21662299016a06c007 Mon Sep 17 00:00:00 2001 From: DLANSAMA <258674612+DLANSAMA@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:44:17 -0400 Subject: [PATCH 1/3] fix: job --dry-run would_slice matches real-run slicing decision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `plate job/send --dry-run` predicted `would_slice` from the raw URL-path extension, which is absent for Printables model pages and extension-less direct links. In those cases the predictor returned `would_slice: false`, but the real run downloads a model file (the downloader falls back to `.stl` when it cannot infer an extension) and slices it — so the dry-run pre-check disagreed with what actually happened. Fix: share one slicing predicate between the predictor and the doer. - `_ext_would_slice(ext)` in bambu_cli/job/predict.py is now the single rule (`ext in SLICEABLE_EXTENSIONS`, applied to the single-suffix `_file_extension` so `foo.gcode.3mf` stays print-ready). The real-run local, ZIP-member, and post-download branches in orchestrate.py all route through it. - `_predicted_download_slice_extension(url, args)` resolves the URL's predicted extension by reusing the downloader's own `_download_source_extension`, which falls back to `.stl` for sources whose extension is unknowable offline. `--name` is intentionally ignored because the real download overrides the `--name` extension with the URL/resolved-name extension. `would_slice` remains a boolean; no JSON shape or schema change. Tests: extended the direct-URL dry-run matrix (.stl/.step/.obj/.zip/.3mf and .gcode.3mf), added Printables-model-URL and extension-less-URL predictions, a predictor/doer shared-predicate equivalence test, local .gcode.3mf print-ready, and a ZIP-member matrix (.stl slices, .3mf/.gcode.3mf do not). Strengthened the Printables dry-run agent smoke to assert would_slice=True. --- CHANGELOG.md | 10 +++ bambu_cli/job/__init__.py | 2 + bambu_cli/job/orchestrate.py | 18 +++-- bambu_cli/job/predict.py | 36 ++++++++++ tests/agent_cli_smoke.py | 5 ++ tests/test_job.py | 129 +++++++++++++++++++++++++++++++++-- 6 files changed, 188 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dfdd00..3caa97b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version ## [Unreleased] +### Fixed + +- `plate job/send --dry-run` now reports `would_slice` consistently with + the real run. The dry-run predictor previously returned `would_slice: false` + for sources whose extension it could not read from the URL path (Printables + model pages, extension-less direct links), even though the real run downloads + a model file (falling back to `.stl`) and slices it. The prediction and the + real run now share one slicing predicate, so a dry-run pre-check no longer + disagrees with what actually happens. + ## [0.4.0] - 2026-07-29 ### Added diff --git a/bambu_cli/job/__init__.py b/bambu_cli/job/__init__.py index 8c94cd3..e06f7bb 100644 --- a/bambu_cli/job/__init__.py +++ b/bambu_cli/job/__init__.py @@ -14,6 +14,8 @@ generate_print_payload, ) from bambu_cli.job.predict import ( # noqa: F401 + _ext_would_slice, + _predicted_download_slice_extension, _predicted_sliced_remote_name, _predicted_url_download_extension, _predicted_url_remote_name, diff --git a/bambu_cli/job/orchestrate.py b/bambu_cli/job/orchestrate.py index dcf22ac..a373ed7 100644 --- a/bambu_cli/job/orchestrate.py +++ b/bambu_cli/job/orchestrate.py @@ -46,8 +46,9 @@ from bambu_cli.errors import BambuError from bambu_cli.job.payload import _parse_print_options, _print_next_command from bambu_cli.job.predict import ( + _ext_would_slice, + _predicted_download_slice_extension, _predicted_sliced_remote_name, - _predicted_url_download_extension, _predicted_url_remote_name, _slice_args_for_job, ) @@ -175,10 +176,17 @@ def _run_job(ctx, args, steps=None): logger.info(" Re-run without --dry-run to download and prepare the model.") summary["status"] = "dry_run_url_skipped" summary["would_download"] = True - source_ext = _predicted_url_download_extension(source, args) + # Predict the extension the download will actually hand to the + # pipeline, then decide via the SAME predicate the real run uses + # below (`_ext_would_slice`). `_predicted_download_slice_extension` + # mirrors the downloader's own fallback to ".stl" for sources whose + # extension is unknowable offline (Printables pages, ?id= links), so + # would_slice here matches what the real run does instead of + # defaulting to False. + source_ext = _predicted_download_slice_extension(source, args) if source_ext in ARCHIVE_DOWNLOAD_EXTENSIONS: summary["would_extract"] = True - elif source_ext in SLICEABLE_EXTENSIONS: + elif _ext_would_slice(source_ext): summary["would_slice"] = True summary["remote_name"] = predicted_remote_name summary["would_upload"] = True @@ -296,7 +304,7 @@ def _run_job(ctx, args, steps=None): ) summary["status"] = "dry_run_local_skipped" summary["archive_entry"] = member_filename - summary["would_slice"] = member_ext in SLICEABLE_EXTENSIONS + summary["would_slice"] = _ext_would_slice(member_ext) summary["remote_name"] = predicted_remote_name summary["would_upload"] = True summary["would_print"] = bool(getattr(args, "confirm", False)) and not bool( @@ -320,7 +328,7 @@ def _run_job(ctx, args, steps=None): summary["extracted_path"] = extracted_path summary["archive_entry"] = archive_entry - if ext in SLICEABLE_EXTENSIONS: + if _ext_would_slice(ext): summary["would_slice"] = True predicted_remote_name = _predicted_sliced_remote_name(source_path, getattr(args, "copies", 1)) if _safe_remote_name(predicted_remote_name) is None: diff --git a/bambu_cli/job/predict.py b/bambu_cli/job/predict.py index 5aa5c57..d45d422 100644 --- a/bambu_cli/job/predict.py +++ b/bambu_cli/job/predict.py @@ -13,6 +13,7 @@ SLICEABLE_EXTENSIONS, ) from bambu_cli.download import ( + _download_source_extension, _download_target_filename, _file_extension, _is_printables_model_url, @@ -22,6 +23,41 @@ from bambu_cli.slicer import _sliced_output_path +def _ext_would_slice(ext): + """The one slicing predicate shared by the dry-run predictor and the real run. + + Given the extension of the file that will actually be handed to the + upload/print pipeline — the downloaded file, the selected ZIP member, or the + local file — return whether job/send will slice it. The ``--dry-run`` + prediction and the real run both route through this, so ``would_slice`` in a + dry-run payload cannot disagree with what the real run does. ``ext`` must be a + single-suffix, lowercased extension from ``_file_extension`` so that, e.g., + ``foo.gcode.3mf`` is treated as print-ready ``.3mf`` on both sides. + """ + return ext in SLICEABLE_EXTENSIONS + + +def _predicted_download_slice_extension(url, args): + """Predict the extension a URL download hands to the slicer, matching the doer. + + The real run decides ``would_slice`` from the *downloaded* file's extension + (orchestrate.py). Offline we cannot resolve Printables pages, redirects, or + Content-Disposition, so we reuse the downloader's own inference, + ``_download_source_extension``: it takes an extension from the URL path when + one is present and otherwise falls back to ``.stl`` — exactly what the real + download lands for a model source with no determinable extension (Printables + model pages, ``?id=`` links). Predicting that same ``.stl`` fallback is what + makes ``would_slice`` agree with the real run instead of defaulting to False. + + ``--name`` is deliberately ignored here: the real download overrides the + ``--name`` extension with the URL/resolved-name extension + (``_download_filename_with_extension``), so honoring it in the predictor would + reintroduce a divergence. Archive (``.zip``) URLs keep their real extension so + callers can report ``would_extract`` instead. + """ + return _download_source_extension(url) + + def _slice_args_for_job(filepath, args, output_dir): """Build a slice command namespace from job-level arguments.""" return argparse.Namespace( diff --git a/tests/agent_cli_smoke.py b/tests/agent_cli_smoke.py index 3ab0ad7..36337fc 100644 --- a/tests/agent_cli_smoke.py +++ b/tests/agent_cli_smoke.py @@ -411,6 +411,11 @@ def smoke_url_job_dry_run_json(root): assert False, f"URL dry-run payload did not report planned download/upload: {payload}" if payload.get("would_print") is not True: assert False, f"URL dry-run payload did not report planned print: {payload}" + # A Printables model page cannot be resolved offline, but the real run + # downloads a model file (STL/STEP preferred) and slices it, so the dry-run + # prediction must report would_slice=True rather than defaulting to False. + if payload.get("would_slice") is not True or payload.get("would_extract") is not False: + assert False, f"Printables model URL dry-run did not predict slicing: {payload}" if payload.get("downloaded_path") or payload.get("uploaded") or payload.get("printed"): assert False, f"URL dry-run payload reported side effects: {payload}" print("url-job-dry-run-json smoke ok") diff --git a/tests/test_job.py b/tests/test_job.py index 1b45e7f..2a3868b 100644 --- a/tests/test_job.py +++ b/tests/test_job.py @@ -347,15 +347,21 @@ def test_printed_success(tmp_path, capsys): @pytest.mark.parametrize( - "ext,would_slice,would_extract", + "filename,would_slice,would_extract", [ - (".stl", True, False), - (".zip", False, True), - (".3mf", False, False), + ("model.stl", True, False), + ("model.step", True, False), + ("model.obj", True, False), + ("model.zip", False, True), + ("model.3mf", False, False), + # ".gcode.3mf" is this project's primary printer-ready format; the + # last-suffix extension is ".3mf", so it must NOT be predicted as + # sliceable — the real run treats it as print-ready and skips slicing. + ("plate.gcode.3mf", False, False), ], ) -def test_dry_run_direct_url(ext, would_slice, would_extract, capsys): - url = f"https://example.com/model{ext}" +def test_dry_run_direct_url(filename, would_slice, would_extract, capsys): + url = f"https://example.com/{filename}" args = _parse(["job", url, "--dry-run", "--json"]) _run_job(_ctx(), args, JobSteps()) payload = _read_json(capsys) @@ -365,10 +371,79 @@ def test_dry_run_direct_url(ext, would_slice, would_extract, capsys): assert payload["would_extract"] is would_extract assert payload["would_upload"] is True assert payload["would_print"] is False - if ext != ".zip": + if not would_extract: assert payload["remote_name"] is not None +def test_dry_run_printables_model_url_predicts_slice(capsys): + """Regression: a Printables model page dry-run must report would_slice=True. + + The predictor cannot resolve the page offline, but the real run downloads a + model file (STL/STEP preferred) and slices it. The shared predicate now + falls back to ".stl" — the same fallback the downloader lands — so the + dry-run prediction matches the real run instead of defaulting to False. + """ + url = "https://printables.com/model/12345-agent-smoke" + args = _parse(["job", url, "--dry-run", "--json"]) + _run_job(_ctx(), args, JobSteps()) + payload = _read_json(capsys) + assert payload["status"] == "dry_run_url_skipped" + assert payload["would_download"] is True + assert payload["would_slice"] is True + assert payload["would_extract"] is False + assert payload["would_upload"] is True + # Remote name stays unpredictable offline for a Printables page; would_slice + # being True must not fabricate one. + assert payload["remote_name"] is None + + +def test_predictor_and_doer_share_slice_predicate(): + """The dry-run URL prediction routes through the same predicate as the real run. + + For any URL, the predicted-download extension fed to _ext_would_slice must + equal the decision the doer would make for that same extension. Guards + against the two paths drifting apart again (the original bug: predictor said + would_slice=False for sources the doer sliced). + """ + from bambu_cli.download import _file_extension + from bambu_cli.job import _ext_would_slice, _predicted_download_slice_extension + + cases = { + "https://printables.com/model/12345-foo": True, + "https://example.com/model.stl": True, + "https://example.com/model.step": True, + "https://example.com/model.obj": True, + "https://example.com/model.3mf": False, + "https://example.com/plate.gcode.3mf": False, + "https://example.com/bundle.zip": False, + "https://example.com/download?id=1": True, + } + for url, expected in cases.items(): + args = _parse(["job", url, "--dry-run", "--json"]) + predicted_ext = _predicted_download_slice_extension(url, args) + # Predictor's decision. + assert _ext_would_slice(predicted_ext) is expected, url + # Doer applies the identical predicate to the file it actually handles; + # simulate by feeding the predicted extension back through it. + assert _ext_would_slice(_file_extension(f"x{predicted_ext}")) is expected, url + + +def test_dry_run_extensionless_url_predicts_slice(capsys): + """An extension-less direct link predicts slicing, matching the doer's fallback. + + The real download resolves the filename via Content-Disposition/content and + falls back to ".stl" when nothing determines it, then slices. The dry-run + prediction must agree. + """ + url = "https://example.com/download?id=1" + args = _parse(["job", url, "--dry-run", "--json"]) + _run_job(_ctx(), args, JobSteps()) + payload = _read_json(capsys) + assert payload["status"] == "dry_run_url_skipped" + assert payload["would_slice"] is True + assert payload["would_extract"] is False + + def test_dry_run_local_model_file(tmp_path, capsys): stl = tmp_path / "model.stl" stl.write_bytes(b"solid x") @@ -407,6 +482,46 @@ def test_dry_run_local_printer_ready_file(tmp_path, capsys): assert payload["printable_path"] == _display_path(str(ready)) +def test_dry_run_local_gcode_3mf_is_print_ready_not_sliced(tmp_path, capsys): + """A local .gcode.3mf is print-ready: dry-run must report would_slice=False.""" + ready = tmp_path / "plate.gcode.3mf" + ready.write_bytes(b"x" * 10) + args = _parse(["job", str(ready), "--dry-run", "--json"]) + _run_job(_ctx(), args, JobSteps()) + payload = _read_json(capsys) + assert payload["status"] == "dry_run_local_skipped" + assert payload["would_slice"] is False + assert payload["would_upload"] is True + assert payload["remote_name"] == "plate.gcode.3mf" + + +@pytest.mark.parametrize( + "member,would_slice", + [ + ("model.stl", True), + ("model.3mf", False), + # Last-suffix extension of a ".gcode.3mf" member is ".3mf": print-ready. + ("plate.gcode.3mf", False), + ], +) +def test_dry_run_local_zip_member_slice_matches_predicate(tmp_path, member, would_slice, capsys): + """ZIP dry-run reports would_slice per the selected member's extension. + + Uses the same _ext_would_slice predicate as the real run, so a .3mf or + .gcode.3mf member is reported print-ready while an .stl member is sliceable. + """ + zpath = tmp_path / "bundle.zip" + with zipfile.ZipFile(zpath, "w") as zf: + zf.writestr(member, b"content") + args = _parse(["job", str(zpath), "--dry-run", "--json"]) + _run_job(_ctx(), args, JobSteps()) + payload = _read_json(capsys) + assert payload["status"] == "dry_run_local_skipped" + assert payload["archive_entry"] == member + assert payload["would_slice"] is would_slice + assert payload["would_upload"] is True + + def test_dry_run_local_printer_ready_empty_file_fails(tmp_path): ready = tmp_path / "model.3mf" ready.write_bytes(b"") From d4f9c97fb273f5c0e70c7af48a488e49d226c525 Mon Sep 17 00:00:00 2001 From: DLANSAMA <258674612+DLANSAMA@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:44:26 -0400 Subject: [PATCH 2/3] test: hermetic fake OrcaSlicer stub; de-mock slice tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add tests/fakes/orca_stub — a standalone fake OrcaSlicer script invoked as the real slicer binary via a cross-platform launcher (sys.executable + script path; sh wrapper on POSIX, .cmd on Windows). It reproduces the narrow Orca CLI contract cmd_slice drives (--load-settings/--load-filaments/--slice/ --export-3mf/--outputdir) and selects scenarios by env var: success, benign headless-GL non-zero exit, empty/corrupt/missing output, hard failure, real-error-with-GL-noise, garbage stdout, progress, and hang (timeout). tests/test_slice_stub_integration.py runs cmd_slice end-to-end against the stub with a real profiles_dir + STL under tmp_path and no subprocess/os.path.exists mocking, exercising the real _run_orcaslicer process loop (previously skipped via pragma) and _finalize_slice parsing branches. slicer/output.py line coverage rises 79.8% -> 92.7%. No bambu_cli/ changes: settings_ctx (orca_slicer/profiles_dir) is the existing DI seam. Docs (quality-roadmap C.4, test-backlog, mutation-baseline) updated; snapshot counts refreshed to the measured 1032 collected / 1031 passing. --- docs/mutation-baseline.md | 6 +- docs/quality-roadmap.md | 6 +- docs/test-backlog.md | 8 +- tests/fakes/__init__.py | 1 + tests/fakes/orca_stub/__init__.py | 133 ++++++++++++++++ tests/fakes/orca_stub/orca_stub.py | 218 +++++++++++++++++++++++++ tests/test_slice_stub_integration.py | 230 +++++++++++++++++++++++++++ 7 files changed, 592 insertions(+), 10 deletions(-) create mode 100644 tests/fakes/__init__.py create mode 100644 tests/fakes/orca_stub/__init__.py create mode 100644 tests/fakes/orca_stub/orca_stub.py create mode 100644 tests/test_slice_stub_integration.py diff --git a/docs/mutation-baseline.md b/docs/mutation-baseline.md index 5e2eaaf..be961ad 100644 --- a/docs/mutation-baseline.md +++ b/docs/mutation-baseline.md @@ -73,7 +73,7 @@ Per-module (approx. killed / accounted on a clean run): | `download/naming.py` | ~55–56% | Injection + sanitize properties; CD/header edges survive | | `job/predict.py` | ~33% | Dry-run heuristics under-specified; many equivalent branches | | `download/validation.py` | ~30–31% | Message/emit paths + normalize edge strings | -| `slicer/output.py` | ~21% | **Low:** `_finalize_slice` I/O/logging mutates with little unit signal; `_is_valid_sliced_3mf` itself is much better covered | +| `slicer/output.py` | ~21% baseline; **not yet re-measured** since the C.4 hermetic Orca stub landed | **Was low:** `_finalize_slice` I/O/logging mutated with little unit signal. `tests/test_slice_stub_integration.py` (roadmap C.4) now drives its benign-GL / empty / corrupt / missing-output / real-error branches through the real slicer subprocess (line coverage 79.8%→~93%), so a re-run of `mutmut` on this module should score materially higher — update this row after the next mutation run. | **Honest reading:** the overall score **dropped vs Phase 1** because scope **widened** into harder modules (especially `output._finalize_slice` and `predict`/`validation` emit paths). That is intentional. Do **not** restore a high score by shrinking back to only well-covered files. @@ -92,7 +92,7 @@ Enforced by `./scripts/run_mutation_baseline.sh` after `mutmut export-cicd-stats Categories (not an exhaustive dump of 861 IDs): 1. **Equivalent / cosmetic** — error-message string literals, log format, `getattr` default when tests always set the attribute, `ZipFile(..., "r")` vs default mode. -2. **`_finalize_slice` (output.py)** — subprocess exit interpretation, JSON emit, path display. Deferred: needs hermetic fake-Orca fixtures; not pure safety. Dominates the low output.py score. +2. **`_finalize_slice` (output.py)** — subprocess exit interpretation, JSON emit, path display. **Addressed (C.4):** `tests/fakes/orca_stub` + `tests/test_slice_stub_integration.py` now run these branches against a real fake-slicer subprocess. Residual survivors here should be cosmetic (log strings / path display); re-measure before treating any as "accepted". 3. **DNS cache / hop bookkeeping (netsafety)** — TTL, cache size clear, attribute names on redirect requests. Core `is_global` refuse path is well killed. 4. **URL normalize / Content-Disposition edges (validation/naming)** — ambiguous scheme-less inputs and RFC2231 header tuples; behavior partially covered; full combinatorial matrix deferred. 5. **Dry-run prediction (predict.py)** — Printables/archive/extension branches that return `None` early; many mutants are observationally equivalent under the focused suite. @@ -121,4 +121,4 @@ Artifacts (`mutants/`, `.mutmut-cache`, `.hypothesis/`) are gitignored. - mutmut 3.x needs Python ≥ 3.10 (CI mutation job uses 3.12). - Hypothesis property tests live in `tests/test_properties_safety.py` and are part of the focused mutmut suite. -- Raising the score further: add hermetic tests for `_finalize_slice` decision branches, or move pure 3mf validation to a tiny module so mutmut does not spend budget on I/O. +- Raising the score further: the hermetic `_finalize_slice` tests now exist (C.4, `tests/test_slice_stub_integration.py`); re-run `mutmut` on `slicer/output.py` and update the per-module row. Optionally still move pure 3mf validation to a tiny module so mutmut does not spend budget on I/O. diff --git a/docs/quality-roadmap.md b/docs/quality-roadmap.md index ce2318b..37b4875 100644 --- a/docs/quality-roadmap.md +++ b/docs/quality-roadmap.md @@ -56,7 +56,7 @@ security is not yet **A+**. | Correctness / bugs | **A** | dead flags fixed (global `--json` before subcommand); structured errors; purity greps; version single-sourced | | Typing | **A** | `uvx mypy -p bambu_cli` full package with `check_untyped_defs = true`; no residual excludes | | Error model | **A** | `sys.exit` only in `cli.py` (errors.py hits are docstrings); domain uses `abort` / `BambuError` | -| Tests | **A−** | **982** non-live tests collected / **980** passing (2026-07-27; the latest additions cover the `plate go` interactive wizard state machine); **84.49%** coverage measured 2026-07-27 on Linux; CI floor **83**; per-module floors not enforced | +| Tests | **A−** | **1032** non-live tests collected / **1031** passing (2026-07-29; latest additions are the C.4 hermetic fake-OrcaSlicer slice tests exercising the real slicer subprocess); **84.8%** coverage measured 2026-07-29 on Linux; CI floor **83**; per-module floors not enforced | | CI / release | **A−** | single pytest path; purity greps; bandit/audit/mypy blocking; **`--cov-fail-under=83`** (A+ target remains 92) | | Docs / governance | **A−** | roadmap + backlog + SECURITY + AGENTS aligned (2026-07-24); prior AGENTS mypy-blocklist / backlog ≥98% claims corrected | | Product polish | **B+** | quality gates in place; still pre-1.0 Beta (version is single-sourced from `pyproject.toml`); coverage ratchet + camera defaults remain for 1.0 A+ | @@ -428,7 +428,7 @@ pytest -W error::ResourceWarning --cov=bambu_cli --cov-fail-under=85 | C.1 | **T2** remaining wizard/mDNS cases | | C.2 | **T3** full download/extract matrix | | C.3 | **T4** slicer + doctor + print safety | -| C.4 | Hermetic **fake OrcaSlicer** script in `tests/fakes/orca_stub` (exit codes, stdout, profile paths) | +| C.4 | **Done.** Hermetic **fake OrcaSlicer** script in `tests/fakes/orca_stub` (exit codes, stdout, profile paths); `tests/test_slice_stub_integration.py` runs `cmd_slice` end-to-end through the real `_run_orcaslicer`/`_finalize_slice` instead of mocking `subprocess.Popen` | | C.5 | Coverage total ≥**92%**; module floors per scorecard A+ column for transport/setup/download | #### Typing @@ -459,7 +459,7 @@ bandit + pip-audit blocking #### DoD - [ ] Scorecard **A** column green for Tests and Typing (Typing A− OK if strict not yet full-package). -- [ ] Fake Orca used by default in slice unit tests. +- [x] Fake Orca available (`tests/fakes/orca_stub`) and used by the hermetic slice suite (`tests/test_slice_stub_integration.py`); remaining `test_slice_cmd.py` unit tests keep their mocks where they assert argv assembly / profile-resolution branches. - [ ] `docs/test-backlog.md` reduced to “nice-to-have” only (or empty P1–P5). #### Score impact diff --git a/docs/test-backlog.md b/docs/test-backlog.md index d78dc3b..f9cbf87 100644 --- a/docs/test-backlog.md +++ b/docs/test-backlog.md @@ -10,13 +10,13 @@ Do not treat historical “≥98% coverage” claims as current — see the snap | Metric | Current (honest) | A+ / 1.0 target | |--------|------------------|-----------------| -| Non-live tests collected | **982** collected / **980** passing (measured 2026-07-27) | ≥550 with zero known flakes ✅ size | -| Line/branch coverage (CI) | **84.32%** Linux / ~**83.9%** Windows measured 2026-07-26; **floor 83** (Windows is the binding leg) | **≥92%** total; optional module floors | +| Non-live tests collected | **1032** collected / **1031** passing (measured 2026-07-29; +13 hermetic Orca slice tests, C.4) | ≥550 with zero known flakes ✅ size | +| Line/branch coverage (CI) | **84.8%** Linux measured 2026-07-29 (was 84.32% on 2026-07-26; +C.4 hermetic Orca tests lift `slicer/output.py` 79.8%→92.7%); ~**83.9%** Windows measured 2026-07-26; **floor 83** (Windows is the binding leg) | **≥92%** total; optional module floors | | Typing | Full package mypy + `check_untyped_defs` | keep; optional full `strict` later | | Error model | `sys.exit` only in `cli.py` | keep | | `@mockable` / test-awareness | **0** (CI greps) | keep | | JSON schemas | **19** files under `docs/schemas/` | every `--json` command + monitor goldens | -| Mutation baseline | Pure safety modules; floor **40%** | optional raise after hermetic Orca stub | +| Mutation baseline | Pure safety modules; floor **40%** | hermetic Orca stub landed (C.4); re-run `mutmut` on `slicer/output.py` to raise its row | | Live printer | Documented opt-in harness | manual pre-release (optional scheduled lab) | | Product version | pre-1.0 Beta (single-sourced from `pyproject.toml`) | **v1.0.0** when roadmap §5 is complete | @@ -50,7 +50,7 @@ Tracked in [SECURITY.md](../SECURITY.md) known limitations: |-----|-------| | Raise CI floor 83 → 85 → 88 → **92** | Residual: mqtt/ftps pin paths, pool recovery, wizard TTY, Orca process | | Per-module floors (optional) | mqtt / ftps / netsafety / download / camera | -| Hermetic fake Orca binary | Raises mutation kill rate on `slicer/output._finalize_slice`; slice unit tests less mock-heavy | +| ~~Hermetic fake Orca binary~~ | **Done (C.4).** `tests/fakes/orca_stub` + `tests/test_slice_stub_integration.py` run `cmd_slice` end-to-end through the real slicer subprocess (`_run_orcaslicer`/`_finalize_slice`); `slicer/output.py` line coverage 79.8%→~93%. Mutation re-run on that module still pending. | ### P2 — Contracts & agent surface diff --git a/tests/fakes/__init__.py b/tests/fakes/__init__.py new file mode 100644 index 0000000..94386e8 --- /dev/null +++ b/tests/fakes/__init__.py @@ -0,0 +1 @@ +"""Hermetic test fakes (fake OrcaSlicer, etc.) — see roadmap A.3 / C.4.""" diff --git a/tests/fakes/orca_stub/__init__.py b/tests/fakes/orca_stub/__init__.py new file mode 100644 index 0000000..454cc12 --- /dev/null +++ b/tests/fakes/orca_stub/__init__.py @@ -0,0 +1,133 @@ +"""Hermetic fake OrcaSlicer harness (roadmap C.4). + +Provides: + +* ``orca_stub.py`` — a standalone fake-slicer script (invoked as the binary). +* ``make_orca_launcher`` — write a cross-platform launcher for that script so + ``settings.orca_slicer`` can point at a single directly-executable path that + ``subprocess.Popen`` can run on Linux, macOS, and Windows (no shebang/exec-bit + reliance on Windows). +* ``build_profiles_dir`` — materialise a real OrcaSlicer-style ``profiles_dir`` + (machine/process/filament JSONs) under a tmp dir so ``cmd_slice`` finds real + files instead of mocking ``os.path.exists``/``os.listdir``. +* ``write_stl`` — write a tiny real ASCII STL model input. +""" + +from __future__ import annotations + +import json +import os +import stat +import sys + +STUB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "orca_stub.py") + + +def make_orca_launcher(dest_dir: str, name: str = "orca-slicer") -> str: + """Write a launcher for ``orca_stub.py`` and return its path. + + The launcher is what ``settings.orca_slicer`` points at. ``cmd_slice`` runs + it via ``subprocess.Popen([launcher, ...args])`` and + ``_slicer_executable_problem`` checks it exists + is X_OK on POSIX, so the + launcher must be a single directly-executable file on every OS: + + * POSIX: a ``sh`` wrapper that ``exec``s `` orca_stub.py "$@"``, + chmod +x. (The X_OK check itself requires this on POSIX.) + * Windows: a ``.cmd`` batch file ``@ orca_stub.py %*`` — Popen can + launch a ``.cmd`` directly; there is no exec bit to set. + + Both embed the current interpreter (``sys.executable``) so no shebang or + ``PATH`` python lookup is needed. + """ + python = sys.executable + if os.name == "nt": + launcher = os.path.join(dest_dir, name + ".cmd") + # %* forwards all args; quote python + script for spaced paths. + content = f'@"{python}" "{STUB_PATH}" %*\r\n' + with open(launcher, "w", encoding="utf-8", newline="") as fh: + fh.write(content) + return launcher + + launcher = os.path.join(dest_dir, name) + content = f'#!/bin/sh\nexec "{python}" "{STUB_PATH}" "$@"\n' + with open(launcher, "w", encoding="utf-8") as fh: + fh.write(content) + mode = os.stat(launcher).st_mode + os.chmod(launcher, mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + return launcher + + +# ---- Minimal but realistic profile JSONs ------------------------------------ +# cmd_slice looks for, in ``profiles_dir``: +# machine/ nozzle.json (default P1P 0.4) +# process/ Standard @BBL P1P.json (quality-derived name) +# filament/**@base*.json (fuzzy match, default PLA Basic) +# The names below match the default P1P / 0.4 / standard / PLA Basic path. + +_MACHINE_JSON = { + "type": "machine", + "name": "Bambu Lab P1P 0.4 nozzle", + "nozzle_diameter": ["0.4"], + "printer_model": "Bambu Lab P1P", +} + +_PROCESS_JSON = { + "type": "process", + "name": "0.20mm Standard @BBL P1P", + "layer_height": "0.2", + "compatible_printers": ["Bambu Lab P1P 0.4 nozzle"], +} + +_FILAMENT_JSON = { + "type": "filament", + "name": "Bambu PLA Basic @base", + "filament_type": ["PLA"], +} + + +def build_profiles_dir(root: str) -> str: + """Create a real profiles_dir tree under *root* and return its path. + + Matches the default slice path (P1P, 0.4 nozzle, standard quality, PLA + Basic) so ``cmd_slice`` resolves every profile from real files on disk. + """ + profiles_dir = os.path.join(root, "profiles") + for sub, name, payload in ( + ("machine", "Bambu Lab P1P 0.4 nozzle.json", _MACHINE_JSON), + ("process", "0.20mm Standard @BBL P1P.json", _PROCESS_JSON), + ("filament", "Bambu PLA Basic @base.json", _FILAMENT_JSON), + ): + d = os.path.join(profiles_dir, sub) + os.makedirs(d, exist_ok=True) + with open(os.path.join(d, name), "w", encoding="utf-8") as fh: + json.dump(payload, fh) + return profiles_dir + + +# A minimal valid ASCII STL (single degenerate triangle) — enough for the stub +# to receive a real model input path; the stub never actually parses it. +_STL = """solid cube +facet normal 0 0 0 + outer loop + vertex 0 0 0 + vertex 1 0 0 + vertex 0 1 0 + endloop +endfacet +endsolid cube +""" + + +def write_stl(path: str) -> str: + """Write a tiny real ASCII STL to *path* and return it.""" + with open(path, "w", encoding="utf-8") as fh: + fh.write(_STL) + return path + + +__all__ = [ + "STUB_PATH", + "make_orca_launcher", + "build_profiles_dir", + "write_stl", +] diff --git a/tests/fakes/orca_stub/orca_stub.py b/tests/fakes/orca_stub/orca_stub.py new file mode 100644 index 0000000..6f4d6a3 --- /dev/null +++ b/tests/fakes/orca_stub/orca_stub.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python3 +"""Hermetic fake OrcaSlicer CLI (roadmap C.4). + +This script stands in for the real ``orca-slicer`` binary so slice tests can +exercise the REAL subprocess-invocation, stdout-pump, and output-parsing code in +``bambu_cli/slicer/`` (``orca._run_orcaslicer`` and ``output._finalize_slice``) +instead of mocking ``subprocess.Popen`` and ``os.path.exists``. + +It reproduces only the narrow slice of the OrcaSlicer CLI surface our code drives +(see ``bambu_cli/slicer/orca._build_orcaslicer_cmd``):: + + orca --load-settings ";" \ + --load-filaments \ + --slice 0 --export-3mf --outputdir \ + [--arrange 1] [--threads N] [ ...] + +Behaviour is selected via the ``ORCA_STUB_SCENARIO`` environment variable so a +test can point ``settings.orca_slicer`` at a generated launcher for this script +and choose a scenario without patching internals: + + success write a plausible, valid sliced .3mf; exit 0 (default) + benign_gl write a valid .3mf but emit GL/thumbnail noise on stderr and + exit non-zero (the "headless GL failed but slice is fine" + path ``_finalize_slice`` treats as benign) + empty_output write a zero-byte output file; exit 0 + corrupt_output write non-zip garbage to the output path; exit 0 + missing_output write nothing; exit 0 (parser sees no output file) + fail emit an ``[error]`` line and exit non-zero; write nothing + fail_real_gl emit GL noise AND a real "slicing error" line, exit non-zero, + write a valid .3mf (must NOT be treated as benign) + garbage_stdout print unparseable progress-ish noise, still succeed + hang sleep longer than the test's timeout (write nothing) + progress print several ``NN%`` / "slicing" lines then succeed + (drives the stdout progress-parsing branch) + +Extra knobs (all optional): + ORCA_STUB_SLEEP float seconds to sleep before finishing (default 0) + ORCA_STUB_EXIT override the exit code + ORCA_STUB_STDOUT extra literal text to print to stdout + ORCA_STUB_STDERR extra literal text to print to stderr + ORCA_STUB_MARKER if set, a file at this path is appended to when the stub + runs (lets a test assert the real binary was invoked) + +The stub never touches anything outside the ``--outputdir`` it is told to use +(plus the optional marker file), so tests stay confined to ``tmp_path``. +""" + +from __future__ import annotations + +import os +import sys +import time +import zipfile + +# ---- Minimal valid sliced-3MF payload --------------------------------------- +# ``bambu_cli.slicer.output._is_valid_sliced_3mf`` requires a non-corrupt OPC zip +# containing ``[Content_Types].xml`` plus either ``3D/3dmodel.model`` or a +# ``Metadata/plate_*.gcode`` plate. We ship both so the fixture resembles a real +# Bambu/Orca export. + +_CONTENT_TYPES = ( + '\n' + '' + '' + '' + "" +) + +_MODEL_XML = ( + '\n' + '' + "" +) + +_PLATE_GCODE = "; sliced by orca_stub (fake OrcaSlicer)\nG28\nG1 Z0.2 F600\n" + + +def _write_valid_3mf(path: str) -> None: + """Write a small but structurally valid sliced .3mf zip package.""" + with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as zf: + zf.writestr("[Content_Types].xml", _CONTENT_TYPES) + zf.writestr("3D/3dmodel.model", _MODEL_XML) + zf.writestr("Metadata/plate_1.gcode", _PLATE_GCODE) + + +def _resolve_output_path(argv: list[str]) -> str | None: + """Recover the sliced-output path the way the real CLI would use it. + + Mirrors ``_build_orcaslicer_cmd``: ``--export-3mf `` (a bare + filename) resolved against ``--outputdir ``. + """ + outfile = None + outdir = None + for i, tok in enumerate(argv): + if tok == "--export-3mf" and i + 1 < len(argv): + outfile = argv[i + 1] + elif tok == "--outputdir" and i + 1 < len(argv): + outdir = argv[i + 1] + if outfile is None: + return None + if outdir: + return os.path.join(outdir, outfile) + return outfile + + +def main(argv: list[str]) -> int: + scenario = os.environ.get("ORCA_STUB_SCENARIO", "success") + + marker = os.environ.get("ORCA_STUB_MARKER") + if marker: + # Record that the real binary path was invoked (used by tests to assert + # the launcher/DI seam actually reached this script). + with open(marker, "a", encoding="utf-8") as fh: + fh.write(scenario + "\n") + + outpath = _resolve_output_path(argv) + + # A hang must block regardless of the sleep knob so timeout handling in + # ``cmd_slice`` (subprocess.TimeoutExpired -> EXIT_TIMEOUT) is exercised. + if scenario == "hang": + time.sleep(float(os.environ.get("ORCA_STUB_SLEEP", "3600"))) + return 0 + + sleep_s = float(os.environ.get("ORCA_STUB_SLEEP", "0")) + if sleep_s > 0: + time.sleep(sleep_s) + + stdout = sys.stdout + stderr = sys.stderr + + if scenario == "success": + print("Loading configuration...", file=stdout) + print("slicing plate 1", file=stdout) + print("100%", file=stdout) + if outpath: + _write_valid_3mf(outpath) + rc = 0 + + elif scenario == "progress": + for pct in (0, 17, 42, 73, 99): + print(f"exporting {pct}% ...", file=stdout) + print("slicing complete", file=stdout) + if outpath: + _write_valid_3mf(outpath) + rc = 0 + + elif scenario == "garbage_stdout": + # Non-progress noise that must not crash the stdout line handler. + print("\x00\x01 not-a-percentage ~~~ \r\r partial", file=stdout) + print("gibberish line without newline", file=stdout, end="") + if outpath: + _write_valid_3mf(outpath) + rc = 0 + + elif scenario == "benign_gl": + # OrcaSlicer wrote a valid .3mf but the headless GL/thumbnail step failed + # and it exited non-zero. ``_finalize_slice`` should treat this as success. + if outpath: + _write_valid_3mf(outpath) + print("slicing plate 1", file=stdout) + print("[GLFW] init OpenGL failed; skip thumbnail generation", file=stderr) + print("glew: no usable GL context", file=stderr) + rc = 1 + + elif scenario == "fail_real_gl": + # GL noise present but so is a real slicing error: must NOT be benign. + if outpath: + _write_valid_3mf(outpath) + print("[GLFW] init opengl failed", file=stderr) + print("[error] slicing error: model exceeds build volume", file=stderr) + rc = 1 + + elif scenario == "empty_output": + if outpath: + open(outpath, "w").close() # zero bytes + print("done", file=stdout) + rc = 0 + + elif scenario == "corrupt_output": + if outpath: + with open(outpath, "wb") as fh: + fh.write(b"this is not a zip file, just garbage bytes" * 8) + print("done", file=stdout) + rc = 0 + + elif scenario == "missing_output": + # Report success but never write the file. + print("slicing plate 1", file=stdout) + print("100%", file=stdout) + rc = 0 + + elif scenario == "fail": + print("Loading configuration...", file=stdout) + print("[error] nothing to be sliced, please check your model", file=stderr) + rc = 1 + + else: + print(f"orca_stub: unknown scenario {scenario!r}", file=stderr) + rc = 2 + + extra_out = os.environ.get("ORCA_STUB_STDOUT") + if extra_out: + print(extra_out, file=stdout) + extra_err = os.environ.get("ORCA_STUB_STDERR") + if extra_err: + print(extra_err, file=stderr) + + override = os.environ.get("ORCA_STUB_EXIT") + if override is not None: + rc = int(override) + + stdout.flush() + stderr.flush() + return rc + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/tests/test_slice_stub_integration.py b/tests/test_slice_stub_integration.py new file mode 100644 index 0000000..876efe6 --- /dev/null +++ b/tests/test_slice_stub_integration.py @@ -0,0 +1,230 @@ +"""Hermetic end-to-end slice tests against the fake OrcaSlicer (roadmap C.4). + +These tests point ``settings.orca_slicer`` at a real launcher for +``tests/fakes/orca_stub/orca_stub.py`` and give ``cmd_slice`` a real +``profiles_dir`` and a real STL under ``tmp_path``. Nothing is mocked at the +``subprocess`` / ``os.path.exists`` layer, so the whole pipeline runs for real: + + _build_orcaslicer_cmd -> _run_orcaslicer (Popen + stdout pump) + -> _finalize_slice (returncode + 3MF validation + JSON/error emit) + +The point is to kill mutants in ``bambu_cli/slicer/output.py`` (the benign-GL +branch, empty/corrupt/missing output handling, error-line extraction) that the +mock-heavy unit tests could not reach, and to actually execute the real +``_run_orcaslicer`` process loop that unit tests skip via ``# pragma: no cover``. +""" + +from __future__ import annotations + +import argparse +import json +import os + +import pytest + +from bambu_cli.errors import BambuError +from bambu_cli.slicer import cmd_slice +from tests.bambu_test_base import settings_ctx +from tests.fakes.orca_stub import build_profiles_dir, make_orca_launcher, write_stl + + +def _last_json_object(text: str) -> dict: + """Return the last top-level JSON object in *text*. + + ``emit_json`` / ``emit_json_error`` pretty-print with ``indent=2``, so the + envelope spans multiple lines; scan for the last balanced ``{...}`` block. + """ + decoder = json.JSONDecoder() + last = None + idx = 0 + while idx < len(text): + brace = text.find("{", idx) + if brace == -1: + break + try: + obj, end = decoder.raw_decode(text, brace) + except json.JSONDecodeError: + idx = brace + 1 + continue + last = obj + idx = end + assert last is not None, f"no JSON object found in output: {text!r}" + return last + + +def _slice_args(model_path: str, outdir: str, **overrides) -> argparse.Namespace: + """A minimal real Namespace for cmd_slice (no MagicMock truthy-attr traps).""" + ns = argparse.Namespace( + file=model_path, + output=outdir, + quality="standard", + copies=1, + infill=15, + pattern="3dhoneycomb", + supports=False, + nozzle_temp=220, + bed_temp=60, + filament="PLA Basic", + json=False, + threads=None, + list_settings=False, + ) + for k, v in overrides.items(): + setattr(ns, k, v) + return ns + + +@pytest.fixture +def orca_env(tmp_path, monkeypatch): + """Real profiles + STL + fake-slicer launcher wired via settings_ctx. + + Returns a callable ``run(scenario, **stub_env)`` that installs the launcher + as ``settings.orca_slicer`` and invokes ``cmd_slice`` with the given stub + scenario, plus the resolved output path and model path. + """ + # A DISPLAY makes _build_orcaslicer_cmd skip the xvfb-run prefix on Linux so + # the launcher runs directly. Harmless on macOS/Windows. + monkeypatch.setenv("DISPLAY", ":0") + monkeypatch.delenv("ORCA_STUB_SCENARIO", raising=False) + + launcher = make_orca_launcher(str(tmp_path)) + profiles = build_profiles_dir(str(tmp_path)) + model = write_stl(str(tmp_path / "model.stl")) + outdir = tmp_path / "out" + outdir.mkdir() + # cmd_slice derives outfile from the source basename: model.stl -> model_sliced.3mf + outpath = str(outdir / "model_sliced.3mf") + + def run(scenario: str, args=None, **stub_env): + monkeypatch.setenv("ORCA_STUB_SCENARIO", scenario) + for k, v in stub_env.items(): + monkeypatch.setenv(k, str(v)) + ns = args if args is not None else _slice_args(model, str(outdir)) + with settings_ctx(orca_slicer=launcher, profiles_dir=profiles): + return cmd_slice(ns) + + run.model = model + run.outdir = str(outdir) + run.outpath = outpath + run.profiles = profiles + run.launcher = launcher + return run + + +# --- Success paths ----------------------------------------------------------- + + +def test_success_writes_valid_3mf(orca_env): + result = orca_env("success") + assert result == orca_env.outpath + assert os.path.exists(result) + import zipfile + + assert zipfile.is_zipfile(result) + names = set(zipfile.ZipFile(result).namelist()) + assert "[Content_Types].xml" in names + assert "Metadata/plate_1.gcode" in names + + +def test_success_emits_json_envelope(orca_env, capsys): + args = _slice_args(orca_env.model, orca_env.outdir, json=True) + result = orca_env("success", args=args) + assert result == orca_env.outpath + payload = _last_json_object(capsys.readouterr().out) + assert payload["status"] == "sliced" + assert payload["command"] == "slice" + assert payload["path"] == orca_env.outpath + assert payload["filename"] == "model_sliced.3mf" + assert payload["bytes"] > 0 + assert payload["step_converted"] is False + + +def test_progress_scenario_still_succeeds(orca_env): + # Drives the stdout progress-line parsing in _run_orcaslicer with a + # human-facing (non-json) run so the Rich/progress branch executes. + result = orca_env("progress") + assert os.path.exists(result) + + +def test_garbage_stdout_does_not_break_pump(orca_env): + # Null bytes / stray \r / no-newline tail must not crash the stdout handler. + result = orca_env("garbage_stdout") + assert os.path.exists(result) + + +# --- The benign non-zero (headless GL) branch -------------------------------- + + +def test_benign_gl_nonzero_exit_is_treated_as_success(orca_env): + # returncode != 0 + valid .3mf + GL/thumbnail noise + no real error text + # => _finalize_slice continues (the hardest-to-mock branch). + result = orca_env("benign_gl") + assert result == orca_env.outpath + assert os.path.exists(result) + + +def test_real_error_with_gl_noise_is_not_benign(orca_env): + # GL noise present BUT a real "slicing error" line => must still fail even + # though a valid .3mf was written. Guards the `not _real_err` condition. + with pytest.raises(BambuError): + orca_env("fail_real_gl") + + +# --- Failure / bad-output paths ---------------------------------------------- + + +def test_nonzero_exit_failure_aborts(orca_env): + with pytest.raises(BambuError): + orca_env("fail") + + +def test_nonzero_exit_failure_json_envelope(orca_env, capsys): + args = _slice_args(orca_env.model, orca_env.outdir, json=True) + with pytest.raises(BambuError): + orca_env("fail", args=args) + payload = _last_json_object(capsys.readouterr().out) + assert payload["status"] == "error" + assert payload["command"] == "slice" + assert payload["failed_step"] == "slicer" + assert payload["returncode"] == 1 + + +def test_empty_output_file_aborts_and_is_removed(orca_env): + with pytest.raises(BambuError): + orca_env("empty_output") + # _finalize_slice removes the partial/empty file. + assert not os.path.exists(orca_env.outpath) + + +def test_corrupt_output_file_aborts_and_is_removed(orca_env): + with pytest.raises(BambuError): + orca_env("corrupt_output") + assert not os.path.exists(orca_env.outpath) + + +def test_missing_output_file_aborts(orca_env): + with pytest.raises(BambuError): + orca_env("missing_output") + assert not os.path.exists(orca_env.outpath) + + +# --- Timeout ----------------------------------------------------------------- + + +def test_hang_hits_slice_timeout(orca_env, monkeypatch): + # get_slicer_timeout reads args.slicer_timeout; pin it tiny so the stub's + # sleep trips the real subprocess.TimeoutExpired -> EXIT_TIMEOUT path. + args = _slice_args(orca_env.model, orca_env.outdir, slicer_timeout=1) + with pytest.raises(BambuError): + orca_env("hang", args=args, ORCA_STUB_SLEEP=30) + + +# --- The DI/launcher seam actually reached the fake binary ------------------- + + +def test_stub_binary_was_actually_invoked(orca_env, tmp_path): + marker = tmp_path / "invoked.log" + result = orca_env("success", ORCA_STUB_MARKER=str(marker)) + assert os.path.exists(result) + assert marker.exists() + assert marker.read_text().strip() == "success" From 4d25b5c6345b98b47fa73bc688991568b38515e7 Mon Sep 17 00:00:00 2001 From: DLANSAMA <258674612+DLANSAMA@users.noreply.github.com> Date: Wed, 29 Jul 2026 20:54:47 -0400 Subject: [PATCH 3/3] fix: expect emit_json's home-compacted display path (Windows CI) --- tests/test_slice_stub_integration.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_slice_stub_integration.py b/tests/test_slice_stub_integration.py index 876efe6..bb4fc36 100644 --- a/tests/test_slice_stub_integration.py +++ b/tests/test_slice_stub_integration.py @@ -133,7 +133,11 @@ def test_success_emits_json_envelope(orca_env, capsys): payload = _last_json_object(capsys.readouterr().out) assert payload["status"] == "sliced" assert payload["command"] == "slice" - assert payload["path"] == orca_env.outpath + # emit_json compacts $HOME to ~ in every string value; on Windows CI the + # pytest tmpdir lives under the user profile, so expect the display form. + from bambu_cli.utils import _display_path + + assert payload["path"] == _display_path(orca_env.outpath) assert payload["filename"] == "model_sliced.3mf" assert payload["bytes"] > 0 assert payload["step_converted"] is False