Files
logos-test-modules/test-unload-module-cpp
Dario LipicarandClaude Opus 5 3b6bf85acc test: a fixture for the module teardown contract (#46)
* test: a fixture for the module teardown contract

The aboutToUnload() contract was verified once, by hand, with a scratch module
and a stopwatch. This makes it repeatable.

    test-unload-module-cpp   one module, three behaviours
    tests/run_unload_tests.sh  three daemon lifecycles, one per behaviour

ONE MODULE, NOT THREE. Synchronous, Asynchronous-then-finishing and
Asynchronous-then-never-finishing are selected at RUNTIME from
LOGOS_UNLOAD_MODE. Three near-identical modules would drift apart; one module
whose behaviour the runner flips between daemon launches cannot.

A JOURNAL FILE, NOT STDERR. SubprocessContainer::syncKill closes the child's
stdout/stderr BEFORE sending the stop signal, so anything a module prints during
teardown is never relayed. My first probe used stderr and looked exactly like a
hook that never fired -- which is the wrong lesson to teach a reader and the
wrong signal to build a test on.

NO TIMING ASSERTIONS. "Teardown took more than 1.5s, so the host waited" is the
shape that flakes on a loaded CI box, and it fails in the direction that HIDES
regressions. The journal proves the same thing structurally:

  async  FINISHED is written 1.5s into teardown. A host that did not wait kills
         the module in well under a second, so the line cannot be there unless
         the host waited. Its PRESENCE is the proof.
  hang   the module asks to wait and never finishes. FINISHED must be ABSENT and
         the daemon must still exit -- absence plus a clean exit is the proof
         the deadline is enforced rather than merely configured.
  sync   ENTERED alone, which is what stops the other two passing for the wrong
         reason: a hook that never fires also never writes FINISHED.

Elapsed time is printed for a human and never asserted on. The runner also
fails loudly when the module did not load, because an empty journal otherwise
reads identically to a teardown hook that never fired.

Its own check rather than a case in `tests`: the thing under test is SHUTDOWN,
so it needs one daemon lifecycle per mode, which the shared long-lived daemon
in `tests` cannot provide.

REQUIRES A HOST THAT WAITS -- logos-module-loader-qt#9 (merged, dece0c8). The
wait lives in logos_host, so this check stays red until logos-liblogos relocks
onto it; liblogos master still pins acd07cf. Verified by hand against a
logoscore built with that host: mode=async gives "ENTERED FINISHED" and a 1.95s
teardown, against 0.6s with no hook at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ci: run the teardown-contract check

The check was added without a job, so nothing ran it -- a fixture CI never
executes is a fixture that rots.

Its own job rather than a case inside integration-tests, for the same reason it
is its own check: the thing under test is SHUTDOWN, so it needs one daemon
LIFECYCLE per behaviour, and the shared long-lived daemon there cannot provide
that.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(deps): track the merged teardown chain

The fixture needs two things that have now landed: LogosShutdown, from
logos-cpp-sdk#143, so the module compiles at all; and a logoscore whose
liblogos carries the host-side wait (logos-logoscore-cli#97 -> liblogos
cb9f27a -> logos-module-loader-qt#9), so the contract is actually exercised
rather than trivially satisfied.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: give the teardown fixture its own modules directory

Merging the fixture into the shared modulesDir broke five integration tests --
the proxy->provider binds in test_fullapi_proxy and its Rust twin, which failed
with METHOD_FAILED once a tenth module was present in the set they load from.

Established by control rather than inspection: test-modules master with the
IDENTICAL lock bump and no fixture passes 172/0, so the lock was not the cause
and the fixture was.

The check now gets a directory of its own. Two reasons, and the second is why
this is not merely tidier: it loads exactly one module, so a shared dir had it
discovering nine it never touches; and a fixture that exists to be TORN DOWN has
no business sitting in the set every other integration test loads, where its
only effect is to be one more thing in the way.

unload-contract still passes 3/0 against the fixture-only directory -- worth
confirming rather than assuming, since the check now runs somewhere the daemon
has nothing else to discover.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(deps): relock the SDK chain — the five proxy failures are fixed upstream

integration-tests on this branch was 167 passed / 5 failed, all of them
bound-interface calls reaching a Rust provider (probeArrays, proxy echoInt
via cpp, intEvent round-trip, and the two rust-proxy equivalents).

Not this branch's doing. It pinned logos-module-builder at 88e2f8f, which
is the commit that moved the SDK chain onto a logos-rust-sdk whose cdylib
scaffold did not define the protocol 0.5 teardown exports. Rust modules
then failed to dlopen with

    undefined symbol: logos_module_set_unload_done_callback

and test_fullapi_rust / test_fullapi_proxy_rust never came up, taking the
five tests that reach them with them. Fixed upstream in logos-rust-sdk#45;
this just moves onto it (module-builder 26a886b -> rust-sdk 49dbb07).

The same relock also picks up the module-impl ABI checks that landed since
(logos-protocol#66, logos-cpp-sdk#144, logos-rust-sdk#46,
logos-module-builder#207), so a repeat of that break now fails in the repo
that causes it rather than here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(deps): cut the logoscore-cli cycle — 1,180,078 lines -> 355,867

Bumping logos-module-builder does not shrink this lock: its whole subtree is
360 of 45,067 nodes. The weight was somewhere else entirely.

    root inputs by subtree size
      41225  logos-logoscore-cli      <- 91%
       3472  logos-liblogos
        360  logos-module-builder
          6  logos-plugin-qt

Two causes, and the second dominates.

FOLLOWS. logos-logoscore-cli declared none at all, while every other input
here does. 13,979 nodes carried a HARD logos-nix edge — and therefore their
own nixpkgs — against 2,029 that followed one. For contrast, logos-plugin-qt
has follows and costs SIX nodes. Adding them took it to 41,581 nodes: real,
but not the story.

THE CYCLE. logos-logoscore-cli takes logos-test-modules — this repo — as an
input, for its own doctests. Nothing in the package consumed here
(packages.<sys>.default) reads it. Left alone, that edge re-enters the graph
and each traversal unrolls a fresh copy of everything rather than meeting a
node it already has. Pointing it at a leaf cuts the cycle without changing
what is built:

    45,067 nodes / 1,180,078 lines   (before)
    41,581            1,084,906      (+ follows)
    13,634              355,867      (+ cycle cut)   -70%

The follows are chosen the same way the existing logos-plugin-qt ones were:
logos-cpp-sdk and logos-protocol follow the BUILDER's, so the SDK pair the
test binaries link cannot become a second copy on one link line.

Verified: checks.tests (the 172-case integration suite), fullapi-tests and
ipc-new-api-tests all build green against the slimmed lock. qml-modules
fails on macOS both before and after this change — pre-existing, and not run
in CI. thread-safety-tests is left to CI, which is Linux and had it green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 20:59:34 -03:00
..