fix(preview): unpack all three values returned by _auto_compile_prompts#1721
fix(preview): unpack all three values returned by _auto_compile_prompts#1721fmferrari wants to merge 2 commits into
Conversation
_auto_compile_prompts() returns a 3-tuple: (compiled_command, compiled_prompt_files, runtime_content) The preview() command was unpacking only 2 values in both its rich-UI path and its fallback except (ImportError, NameError) path, causing apm preview to always crash with: ValueError: too many values to unpack (expected 2) Fix: add _ to capture the unused runtime_content in both call-sites. _execute_script_command already unpacks all three values correctly. Fixes: apm preview <script> crashes for any script definition
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Updates the preview command to match an updated return signature from script_runner._auto_compile_prompts, preventing tuple-unpacking errors during command preview.
Changes:
- Adjusts tuple unpacking to ignore the new third return value from
_auto_compile_prompts. - Adds an inline comment documenting the updated return shape and why only the first two items are used.
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 2 | 1 | Fix is correct; update 2 unit test mocks to 3-tuple and fix return annotation to str or None to prevent future arity drift. |
| CLI Logging Expert | 0 | 0 | 1 | Fix is output-neutral and correct; both branches updated symmetrically. One nit: comment could name the discarded field for future maintainers. |
| DevX UX Expert | 0 | 1 | 1 | Fix is correct and symmetric across both render paths. runtime_content (compiled AI prompt body) is silently dropped -- a pre-existing gap now reliably reachable after this fix. |
| Supply Chain Security Expert | 0 | 0 | 1 | Fix introduces no new attack surface. *_ discard of runtime_content is correct for a preview-only path; no execution side-effects added. |
| OSS Growth Hacker | 0 | 1 | 1 | preview crash on .prompt.md scripts is a silent first-run killer; fix is correct but the [Unreleased] CHANGELOG is empty and deserves a visible user-facing entry. |
| Doc Writer | 0 | 1 | 0 | Docs for preview command are accurate; CHANGELOG [Unreleased] is missing a Fixed entry for this user-visible crash fix. |
| Test Coverage Expert | 0 | 2 | 0 | Bug fix has no regression trap: 2-tuple mock makes all preview unit tests pass both before and after the fix. Update mock to 3-tuple; add integration test. |
B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.
Top 5 follow-ups
- [Python Architect + Test Coverage Expert] Update
_auto_compile_promptsmock from 2-tuple to 3-tuple intest_run_phase3.py:36andtest_run_command_surface.py:36-- Both panelists independently produced evidence-backed findings (outcome: missing) showing the 2-tuple mock makes all preview unit tests pass both before and after the fix; no CI signal guards against arity regression. Changing to(compiled_cmd, prompt_files, None)closes the regression trap with a one-line fix per file. Feasible in-PR before merge. - [Test Coverage Expert] Add integration test
TestPreviewCLIIntegration:CliRunner().invoke(preview, ['greet'])with a real ScriptRunner against anapm.ymlfixture -- All existing preview tests are unit-tier with a mocked ScriptRunner. The original crash would only surface at integration tier. Without this, the CLI surface has no integration-tier regression trap. - [Doc Writer + OSS Growth Hacker] Add
[Unreleased] ### Fixedentry inCHANGELOG.mdfor theapm previewValueError crash fix -- The[Unreleased]block is currently empty. This is a user-visible crash on the primary documented use case ofapm preview. Omitting an entry makes the fix invisible to users scanning release notes deciding whether to upgrade. - [Python Architect] Fix return annotation on
_auto_compile_promptsinscript_runner.py:226fromtuple[str, list[str], str]totuple[str, list[str], str or None]--runtime_contentinitializes toNoneunconditionally at line 239 and is only set inside theis_runtime_cmdbranch. The declaredstrannotation is false; the docstring already acknowledgesruntime_content_or_none. - [DevX UX Expert + Supply Chain Security Expert] Surface
runtime_contentin preview output when non-None, labeled 'Compiled prompt content (sent to runtime)' -- Three panelists flag this pre-existing gap: for LLM/copilot runtime scripts, preview drops the compiled prompt body with*_and users cannot inspect what text would be sent to the AI service before execution. Mirrorscript_runner.py:154'sformat_content_preview().
Architecture
classDiagram
direction LR
class run_py {
<<Module>>
+preview(ctx, script_name, param, verbose)
}
class ScriptRunner {
+list_scripts() dict
+_execute_script_command(command, params) bool
+_auto_compile_prompts(command, params) tuple
}
class CompileResult {
<<ValueObject>>
compiled_command str
compiled_prompt_files list
runtime_content str
}
run_py ..> ScriptRunner : instantiates
ScriptRunner ..> CompileResult : returns 3-tuple
run_py ..> CompileResult : partial unpack star-discard
note for CompileResult "annotation declares str but value initializes to None -- fix: str or None"
class run_py:::touched
class CompileResult:::touched
classDef touched fill:#fff3b0,stroke:#d47600
flowchart TD
A["apm run preview SCRIPT"] --> B["ScriptRunner.list_scripts()"]
B --> C{script found?}
C -- no --> D[sys.exit 1]
C -- yes --> E["try: _rich_panel Original command"]
E --> F["_auto_compile_prompts(command, params)"]
F -->|3-tuple| G["compiled_command, compiled_prompt_files, star_ = ...\nrun.py:141 FIXED hunk-1"]
G --> H{compiled_prompt_files?}
H -- yes --> I["_rich_panel: Compiled command + files"]
H -- no --> J["_rich_panel: No compilation + warning"]
I --> K["logger.success()"]
J --> K
E -- ImportError or NameError --> M["fallback: click.echo Original"]
M --> N["_auto_compile_prompts(command, params)"]
N -->|3-tuple| O["compiled_command, compiled_prompt_files, star_ = ...\nrun.py:181 FIXED hunk-2"]
O --> K
Recommendation
Ship this PR. The fix is correct, minimal, and unanimous across all active panelists -- no findings prevent merge. Track the 3-tuple mock update (test_run_phase3.py:36 + test_run_command_surface.py:36) as the highest-priority follow-up: it is a genuine regression trap that a one-line change per file closes and the only gap that would allow this exact class of arity bug to recur silently through CI. Add a CHANGELOG [Unreleased] Fixed entry before the next release cycle. The runtime_content preview transparency gap is a meaningful follow-up but is pre-existing and warrants its own tracked issue rather than delaying this crash fix.
Full per-persona findings
Python Architect
-
[recommended] Return annotation
tuple[str, list[str], str]is wrong;runtime_contentinitializes to None atsrc/apm_cli/core/script_runner.py:226
Atscript_runner.py:239,runtime_content = Noneis set unconditionally. It is only assigned a non-None value inside theif is_runtime_cmd:branch (line 263-264). The declared return type lies to the type checker and to callers. The docstring at line 234 already acknowledges this:runtime_content_or_none. The correct annotation is-> tuple[str, list[str], str | None].
Suggested:) -> tuple[str, list[str], str | None]: # aligns with docstring line 234 -
[recommended] Two unit test mocks set
_auto_compile_promptsreturn to 2-tuple, hiding future arity regressions attests/unit/commands/test_run_phase3.py:36
test_run_phase3.py:36andtest_run_command_surface.py:36both setrunner._auto_compile_prompts.return_value = (compiled_cmd, prompt_files). This is the exact mechanism that let the original bug survive CI: the mock satisfied the old 2-var unpack and satisfies the fixed*_unpack equally, so the tests are neutral to the regression. The mocks must reflect the real 3-slot contract.
Suggested:runner._auto_compile_prompts.return_value = (compiled_cmd, prompt_files, None)
Proof (missing at unit):tests/unit/commands/test_run_phase3.py::_make_script_runner-- proves: unit mock reflects the real 3-slot return contract so arity regressions are caught before runtime. -
[nit] NamedTuple return type would make the 3-slot contract self-documenting and prevent positional drift
Three callers each needing different slots (preview needs slots 0-1,_execute_script_commandneeds all 3) is the threshold where named access (result.runtime_content) is cleaner than positional unpacking. A NamedTuple (e.g.CompileResult) is backward-compatible with tuple unpacking and would have prevented both the original ValueError and the mock arity mismatch. The*_fix is correct and idiomatic at this scope; NamedTuple refactor is deferrable.
CLI Logging Expert
- [nit] Comment could name the discarded return value to aid future maintainers at
src/apm_cli/commands/run.py:140
The*_discard is correct, but the comment does not name the thrown-away field. If_auto_compile_promptsgains a 4th return value later, a reader has no signal about what was already intentionally dropped. Namingruntime_contentmakes the contract self-documenting.
Suggested:# preview only needs the command and compiled prompt file list; runtime_content (3rd return value) is intentionally discarded here.
DevX UX Expert
-
[recommended]
runtime_content(compiled prompt body sent to AI runtime) is silently discarded in preview; users cannot see what text the AI would receive atsrc/apm_cli/commands/run.py:141
apm preview's stated contract is 'show what would be executed'. For scripts using copilot/codex/llm/gemini runtimes,_auto_compile_promptsreturnsruntime_content-- the full compiled prompt text piped to the AI subprocess. Theruncommand already surfaces this viaformat_content_preview().previewdrops it with*_and only shows the file path list. This gap was pre-existing but unreachable (command crashed first); this PR makes it reliably visible on every runtime-command script.
Suggested: Aftercompiled_command, compiled_prompt_files, *rest = ..., extractruntime_content = rest[0] if rest else Noneand render a panel titled 'Compiled prompt content (sent to runtime)' when non-None. Mirrorscript_runner.py:154'sformat_content_preview(). -
[nit]
help=text 'Preview a script's compiled prompt files' undersells the command atsrc/apm_cli/commands/run.py:95
The current help string mentions only the file list. A developer scanningapm --helpcannot infer the dry-run scope. Compare npm--dry-run, cargo--dry-run, pipinstall --dry-run.
Suggested: Change tohelp="Dry-run a script: show original and compiled commands without executing".
Supply Chain Security Expert
- [nit]
runtime_content(compiled LLM prompt body) is discarded silently; preview never shows what content would be sent to external runtimes atsrc/apm_cli/commands/run.py:141
For commands targeting copilot/codex/llm/gemini,runtime_contentis the exact text that would be submitted to the external service. Being able to inspect what will be sent to an external LLM before execution is a meaningful trust signal and aligns with secure-by-default posture. The fix is correct (the crash was worse); surfacingruntime_contentin preview output is a worthwhile follow-up.
OSS Growth Hacker
-
[recommended] CHANGELOG
[Unreleased]is empty; this crash fix needs a user-facing Fixed entry before the next release atCHANGELOG.md:8
apm run previewis documented as the canonical 'inspect before run' path. AValueError: too many values to unpackcrash on any.prompt.mdscript is a silent funnel exit -- the user sees a Python traceback, infers the tool is broken, and abandons before filing an issue. That regression belongs in the CHANGELOG under### Fixed.
Suggested: Add to[Unreleased] ### Fixed:apm run previewno longer crashes withValueError: too many values to unpackwhen the script uses.prompt.mdprompt compilation. (fix(preview): unpack all three values returned by _auto_compile_prompts #1721) -
[nit] Release note angle: 'preview now works with prompt-compiled scripts' reinforces the preview-before-run mental model for social/newsletter copy.
Doc Writer
- [recommended] CHANGELOG
[Unreleased]has no entry for thisapm previewcrash fix atCHANGELOG.md
The project follows Keep a Changelog strictly. This PR resolves a user-visible ValueError that breaksapm previewwhenever_auto_compile_promptsreturns more than 2 values -- the primary documented use-case. Omitting a CHANGELOG entry means the fix is invisible to users reading release notes.
Suggested: Add to## [Unreleased] / ### Fixed:apm previewno longer raisesValueError: too many values to unpackwhen_auto_compile_promptsreturns additional values; previewing scripts that reference.prompt.mdfiles now works as documented.
Test Coverage Expert
-
[recommended] Missing regression trap:
_make_script_runnerreturns 2-tuple so no test fails if the arity unpack bug is re-introduced attests/unit/commands/test_run_phase3.py:36
The bug wasValueError: too many values to unpack. The fix uses*_but the test mock returns a 2-tuple(compiled_cmd, prompt_files). Botha, b = (x, y)anda, b, *_ = (x, y)succeed with a 2-tuple mock -- all preview tests pass BOTH before and after the fix. Verified via grep: bothtest_run_phase3.py:36andtest_run_command_surface.py:36setreturn_value = (compiled_cmd, prompt_files).
Suggested: Change mock to:runner._auto_compile_prompts.return_value = (compiled_cmd, prompt_files, runtime_content)and update all callsites to pass 3-tuples, e.g.auto_compile_result=("make all", [], None).
Proof (missing at unit):tests/unit/commands/test_run_phase3.py::_make_script_runner-- proves: apm preview raises ValueError if_auto_compile_promptsreturns more values than the unpack target expects [devx] -
[recommended] No integration test invokes the preview CLI command with a real ScriptRunner; CLI surface tier-floor is integration-with-fixtures at
tests/integration/test_script_runner_phase3w4.py
All existing preview tests are unit-tier with a mocked ScriptRunner. Grep confirmed: no integration test intests/integration/invokes preview CLI with a real ScriptRunner. The original crash would only surface in an integration test that callsCliRunner().invoke(preview, ['myscript'])with a real ScriptRunner and real.prompt.mdfixture.
Suggested: AddTestPreviewCLIIntegrationtotests/integration/test_script_runner_phase3w4.py(or a newtest_run_preview_e2e.py):CliRunner().invoke(preview, ['greet'])against a realapm.ymlfixture with a real ScriptRunner instance.
Proof (missing at integration-with-fixtures):tests/integration/test_script_runner_phase3w4.py::TestPreviewCLIIntegration::test_preview_with_real_script_runner_no_crash-- proves: apm preview executes end-to-end through real ScriptRunner without ValueError [devx]
Auth Expert -- inactive
Only src/apm_cli/commands/run.py is changed; no auth, token management, credential resolution, host classification, or remote-host fallback code is touched.
Performance Expert -- inactive
The only touched file is src/apm_cli/commands/run.py (preview command display path); no cache, transport, materialize, resolve, or install pipeline code is affected.
This panel is advisory. It does not block merge. Re-apply the panel-review label after addressing feedback to re-run.
Generated by PR Review Panel for issue #1721 · sonnet46 10.3M · ◷
Problem
apm preview <script>crashes with:This happens for every script definition regardless of content.
Root Cause
_auto_compile_prompts()insrc/apm_cli/core/script_runner.pyhas return typetuple[str, list[str], str]and always returns three values:_execute_script_command()correctly unpacks all three:But
preview()insrc/apm_cli/commands/run.pyunpacks only two values in both its code paths:tryblock (line ~142)except (ImportError, NameError)fallback path (line ~182)Fix
Add
_to capture the unusedruntime_contentin both call-sites, matching how_execute_script_commandalready handles the return value. Preview does not execute the script, soruntime_contentis not needed.Reproduction
Any
apm.ymlwith ascripts:block:Tested against APM 0.19.0.