Skip to content

docs(standard): lifecycle verification omits reload after unload #585

Description

@ss-o

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:pluginPlugin behavior or plugin-facing work.type:maintenanceNon-feature maintenance, cleanup, or org work.

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions