Skip to content

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

Closed
z-shell/zunit
#35

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

  1. added
    type:maintenanceNon-feature maintenance, cleanup, or org work.
    area:pluginPlugin behavior or plugin-facing work.
    on Sep 2, 2026
  2. moved this from Triage to Todo in Z-shell Deliveryon Sep 18, 2026
  3. ss-o commented on Sep 27, 2026

    @ss-o
    MemberAuthor

    Progress update:

  4. ss-o commented on Sep 30, 2026

    @ss-o
    MemberAuthor

    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).

  5. ss-o commented on Sep 30, 2026

    @ss-o
    MemberAuthor

    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.

  6. reopened this on Sep 30, 2026
  7. ss-o commented on Sep 30, 2026

    @ss-o
    MemberAuthor

    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:

    • z-shell/z-a-meta-plugins main (bc4e2c8): satisfied. tests/entrypoint-state.zsh sources the annex again after unload and checks that the reloaded handler still expands a known meta-plugin; the whole suite passes.
    • z-shell/zsh-eza main (39dc948): the plugin behaves correctly on load, unload and load (same aliases, hook registered again, and the hook runs eza with 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.

  8. added a commit that references this issue on Sep 30, 2026
  9. ss-o commented on Sep 30, 2026

    @ss-o
    MemberAuthor

    Status: every acceptance criterion is now delivered.

    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.

  10. ss-o commented on Oct 1, 2026

    @ss-o
    MemberAuthor

    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.md name 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.

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