Problem
The lifecycle verification contract enumerates the states a plugin's runtime
suite must prove, and every copy of that enumeration omits the same one:
loading again after an unload.
Four surfaces carry the list, and none requires it:
z-shell/wiki, community/03_zsh_plugin_standard.mdx: "Also test repeated
source, partial initialization failure, hostile caller options,
non-interactive loading, and user changes made after load."
decisions/0020-adopt-zsh-plugin-standard-2.md point 7: "the declared load
surface, repeated source, partial failure, hostile caller state, and exact
unload behavior."
.github/instructions/testing.instructions.md: "the documented load
allowlist, harmless repeated source, cleanup after partial failure, hostile
caller options, non-interactive behavior, and exact unload restoration."
z-shell/zunit#18: "Cover repeated source, partial failure, hostile caller
state, post-load user changes."
Repeated source (load, load) is not the same state as reload
(load, unload, load). A suite can prove the first and still miss a plugin that
is permanently broken after the second.
Evidence
Found while reviewing z-shell/z-a-meta-plugins#54. Zi exposes no annex
unregister API (z-shell/zi#483), so the annex's unload must leave its
before-load handler defined and inert rather than removing it, or Zi dispatches
a missing function on every later plugin load. autoload -Uz name does not
replace an already-defined function, so the next load kept the inert stub:
$ zsh -fc '
@zi-register-annex() { :; }; typeset -g PMSPEC=f
source ./z-a-meta-plugins.plugin.zsh
print "fresh: [${functions[_z_a_meta_plugins_before_load_handler]:0:22}]"
z-a-meta-plugins_plugin_unload
source ./z-a-meta-plugins.plugin.zsh
print "reloaded: [${functions[_z_a_meta_plugins_before_load_handler]:0:22}]"'
fresh: [builtin autoload -XU]
reloaded: [ return 0]
The annex then reports a clean load while every meta-plugin expansion silently
does nothing, for the rest of that shell. The existing suite walked
source, source, unload and passed throughout, because it never loaded again
after unloading.
The failure is silent by construction: the handler is present, callable, and
returns 0. An assertion that the handler exists and succeeds is satisfied by
the broken state, so the enumeration gap is not covered incidentally by the
neighbouring cases.
Why this recurs
Any plugin whose unload cannot fully remove a resource, or that relies on
autoload, add-zsh-hook, or a manager registry it does not solely own, has
the same exposure. ADR-0020 point 5 requires an unload function from every
maintained plugin, so the shape is becoming universal rather than incidental.
Proposed change
Add reload to the enumeration on each surface, keeping the wiki as the owner of
the portable wording and the organization surfaces as mirrors, per ADR-0020
point 1 and runbooks/instruction-update.md:
load, unload, and load again, and prove the second load restores the same
declared surface as the first
Suggested assertion, stronger than presence or exit status: after the second
load, the resource must do its job, not merely exist. For an annex handler that
means observing its effect, not (( ${+functions[...]} )).
z-shell/zunit is the mechanical owner: a reload case in the portable
lifecycle assertion set (z-shell/zunit#18) makes this checkable rather than
prose.
Acceptance criteria
References
Problem
The lifecycle verification contract enumerates the states a plugin's runtime
suite must prove, and every copy of that enumeration omits the same one:
loading again after an unload.
Four surfaces carry the list, and none requires it:
z-shell/wiki,community/03_zsh_plugin_standard.mdx: "Also test repeatedsource, partial initialization failure, hostile caller options,
non-interactive loading, and user changes made after load."
decisions/0020-adopt-zsh-plugin-standard-2.mdpoint 7: "the declared loadsurface, repeated source, partial failure, hostile caller state, and exact
unload behavior."
.github/instructions/testing.instructions.md: "the documented loadallowlist, harmless repeated source, cleanup after partial failure, hostile
caller options, non-interactive behavior, and exact unload restoration."
z-shell/zunit#18: "Cover repeated source, partial failure, hostile callerstate, post-load user changes."
Repeated source (load, load) is not the same state as reload
(load, unload, load). A suite can prove the first and still miss a plugin that
is permanently broken after the second.
Evidence
Found while reviewing z-shell/z-a-meta-plugins#54. Zi exposes no annex
unregister API (z-shell/zi#483), so the annex's unload must leave its
before-load handler defined and inert rather than removing it, or Zi dispatches
a missing function on every later plugin load.
autoload -Uz namedoes notreplace an already-defined function, so the next load kept the inert stub:
The annex then reports a clean load while every meta-plugin expansion silently
does nothing, for the rest of that shell. The existing suite walked
source, source, unload and passed throughout, because it never loaded again
after unloading.
The failure is silent by construction: the handler is present, callable, and
returns 0. An assertion that the handler exists and succeeds is satisfied by
the broken state, so the enumeration gap is not covered incidentally by the
neighbouring cases.
Why this recurs
Any plugin whose unload cannot fully remove a resource, or that relies on
autoload,add-zsh-hook, or a manager registry it does not solely own, hasthe same exposure. ADR-0020 point 5 requires an unload function from every
maintained plugin, so the shape is becoming universal rather than incidental.
Proposed change
Add reload to the enumeration on each surface, keeping the wiki as the owner of
the portable wording and the organization surfaces as mirrors, per ADR-0020
point 1 and
runbooks/instruction-update.md:Suggested assertion, stronger than presence or exit status: after the second
load, the resource must do its job, not merely exist. For an annex handler that
means observing its effect, not
(( ${+functions[...]} )).z-shell/zunitis the mechanical owner: a reload case in the portablelifecycle assertion set (z-shell/zunit#18) makes this checkable rather than
prose.
Acceptance criteria
testing.instructions.md, and the ZUnit lifecycleassertion set carry the same requirement without contradicting each other.
of refactor(plugin): adopt Zsh Plugin Standard 2 (#54) z-a-meta-plugins#59 and refactor(plugin): adopt Zsh Plugin Standard 2 (#119) zsh-eza#122.
References