Repository navigation
docs(standard): lifecycle verification omits reload after unload #585
Description
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.mdpoint 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
- The wiki verification paragraph names reload after unload.
- ADR-0020 point 7,
testing.instructions.md, and the ZUnit lifecycle
assertion set carry the same requirement without contradicting each other. - The reload assertion checks observable effect, not presence.
- z-shell/z-a-meta-plugins and z-shell/zsh-eza satisfy it. Both already do as
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
- refactor(plugin): adopt Zsh Plugin Standard 2 z-a-meta-plugins#54 and build(deps): bump actions/dependency-review-action from 2.5.1 to 3.0.0 #59, where this was found and fixed
- fix(annex): no unregister API, so an annex cannot complete its unload contract zi#483, the missing unregister API that forces the inert-stub shape
- [dev]: Add portable plugin lifecycle contract assertions zunit#18, the portable lifecycle assertion set
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
ss-o commentedon Sep 27, 2026
Progress update:
ss-o commentedon Sep 30, 2026
Delivery update. The remaining acceptance criterion here, the ZUnit lifecycle assertion set carrying reload after unload, now has its own implementation issue: z-shell/zunit#34 (z-shell/zunit#18 is closed). It is linked as blocking this issue, from this issue's own record: "Issue remains open pending mechanical reload verification in ZUnit (z-shell/zunit#18)."
Status:
In Progress, because the required deliverable is now active.Premise re-checked on z-shell/zunit
main(1dc51f1): the shipped suite passes 11 of 11 cases but never loads again after unloading. A copy of the fixture changed to leave an inert stub on unload passes the current repeated-source check, and it fails both a reload surface comparison and an effect check. That reproduction is in z-shell/zunit#34.Next action: implement z-shell/zunit#34 with a failing reload case first; once it merges, re-check z-shell/z-a-meta-plugins and z-shell/zsh-eza against it (both already satisfy it per this issue's body).
ss-o commentedon Sep 30, 2026
Reopened: this was closed automatically when z-shell/zunit#35 merged, because that PR's handoff text read as a closing keyword. The reload-after-unload case has landed in zunit (6349b24). Remaining: re-check z-shell/z-a-meta-plugins and z-shell/zsh-eza against the new case, and z-shell/zunit#34, which is being closed as completed now. Follow-up gap: z-shell/zunit#36.
ss-o commentedon Sep 30, 2026
Premise re-checked for the last acceptance criterion ("z-shell/z-a-meta-plugins and z-shell/zsh-eza satisfy it"), with Zsh 5.9.2:
main(bc4e2c8): satisfied.tests/entrypoint-state.zshsources the annex again after unload and checks that the reloaded handler still expands a known meta-plugin; the whole suite passes.main(39dc948): the plugin behaves correctly on load, unload and load (same aliases, hook registered again, and the hook runsezawith the same arguments), but its test suite never loads again after unloading, so the requirement is not verified there. That test is tracked in test: prove reload after unload zsh-eza#130, now linked as blocking this issue.Next action: land z-shell/zsh-eza#130. This issue can close after it merges.
test: prove reload after unload (#130) (#131)
ss-o commentedon Sep 30, 2026
Status: every acceptance criterion is now delivered.
testing.instructions.mdname reload after unload (docs(standard): require reload verification after unload #687, mirroring docs(standard): require reload verification after unload wiki#927).6349b24).tests/entrypoint-state.zshreloads and checks the handler's effect onmain(bc4e2c8).d66b880, closing test: prove reload after unload zsh-eza#130.Follow-ups outside this issue: z-shell/zunit#36 (post-reload unload leak) and z-shell/zsh-eza#133 (deeper unload-restoration checks).
Project 28 status moves to
In Review; closing this issue waits for the maintainer.ss-o commentedon Oct 1, 2026
Closing as completed. Every acceptance criterion is delivered; the evidence is in #585 (comment) (2026-09-30): the standard, ADR-0020 point 7 and
testing.instructions.mdname reload after unload (#687, mirroring z-shell/wiki#927), the ZUnit lifecycle assertion set checks it by effect (z-shell/zunit#35), and both reference plugins carry a reload-after-unload test (z-shell/zsh-eza#131; z-a-meta-plugins already had one). Both native blockers (z-shell/zsh-eza#130, z-shell/zunit#34) are closed.