fix(help): forwarders must work programmatically, not only from M-x

CI caught this on all four test legs. The forwarder body called
`pmacs.command.invoke_interactive`, which raises when the alias is
reached through `pmacs.command.invoke` — and
`tests/config_registry_acceptance.rs` does exactly that, three times.

The acceptance pin passed throughout because it drives the M-x path,
which is the path the framing spent three review rounds getting right.
Being right about one entry point is not the same as covering the
command, and a rename touches every caller of the old name regardless
of how it is reached.

Plain `invoke` is also the correct semantics rather than merely the
working one: the interactive-command boundary is rotated once, by
whatever entry point the user actually used, for the name they actually
typed. Rotating again on the inner call would record a second boundary
for a command the user never invoked.

Adds `d8c`, which invokes both forwarders programmatically. Bitten by
restoring `invoke_interactive`: the new pin fails alongside the three
config-registry tests that found it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
This commit is contained in:
Levi Neuwirth 2026-07-31 20:31:00 -04:00
parent f11af434cd
commit 5cc1a83583
2 changed files with 34 additions and 1 deletions

View File

@ -380,11 +380,21 @@ pmacs.command.define {
-- the bounded price of not breaking documented commands, and it carries
-- a deprecation path a later stage can take.
-- `invoke`, NOT `invoke_interactive`. The forwarder must work however it
-- was itself reached, and `pmacs.command.invoke('editor.describe-setting')`
-- is a real caller (`tests/config_registry_acceptance.rs`) — CI caught
-- that, because the acceptance pin here only drove the M-x path.
--
-- Plain `invoke` is also the correct semantics, not merely the working
-- one: the interactive-command boundary is rotated once, by whatever
-- entry point the user actually used, for the name the user actually
-- typed. Rotating again on the inner call would record a second
-- boundary for a command the user never invoked.
local function forward(old_name, new_name)
pmacs.command.define {
name = old_name,
description = string.format("Deprecated alias for `%s`.", new_name),
fn = function() pmacs.command.invoke_interactive(new_name) end,
fn = function() pmacs.command.invoke(new_name) end,
}
end

View File

@ -434,6 +434,29 @@ fn d8_preservation_the_old_names_forward() {
);
}
/// **P (acceptance 8, cont.)** — the forwarders also work when invoked
/// **programmatically**, not only from M-x.
///
/// This pin exists because its absence shipped a bug: the forwarder body
/// used `invoke_interactive`, which raises when the alias is reached
/// through `pmacs.command.invoke` — a real caller in
/// `config_registry_acceptance`. The M-x pin above passed throughout,
/// because M-x is not the only way in.
#[test]
fn d8c_preservation_the_forwarders_work_programmatically() {
let s = editor();
for old in ["editor.describe-command", "editor.describe-setting"] {
let ok: bool = eval(
&s,
&format!("return pcall(pmacs.command.invoke, {old:?}) and true or false"),
);
assert!(
ok,
"{old} must be invocable programmatically, not only from the palette"
);
}
}
/// **P (acceptance 8, cont.)** — the untouched list commands still work.
#[test]
fn d8b_preservation_list_buffers_and_workers_are_untouched() {