fix(modules): stop a reinstall from switching a disabled module back on

installModule() ended with an unconditional setState(Enabled). Store updates,
zip uploads and plain reinstalls all pass through it, so switching a module off
in the panel survived only until its next update — which is how a disabled plex
came back and then blocked `watch` from being disabled at all.

The admin's choice is now captured before the Installing transition overwrites
it and restored at the end; a first install still enables, since there is no
choice to preserve. stateAfterInstall() reads the raw override rather than
listModules() so the transient Installing / Failed states cannot be mistaken
for a deliberate "disabled".

Restoring that state uses the new writeState() instead of setState(): putting a
module back into the state it already had is a lifecycle transition, not an
admin "disable", and must not be vetoed by the dependents guard — otherwise
updating a disabled `watch` would fail at the last step because `plex` exists.
setState() keeps the guard for the deliberate case.

Three tests cover it; two of them fail against the previous behaviour.

Verified: 860 tests, make gates, CRAP gate.
This commit is contained in:
Divarion_D
2026-09-22 20:56:43 +03:00
parent b9de28f624
commit aab782b9c2
2 changed files with 76 additions and 2 deletions
@@ -348,6 +348,45 @@ final class ModuleManagerMigrationsTest extends TestCase {
$this->assertSame([], $byName['ok-consumer']['dependency_warnings']);
}
// ── install keeps the admin's on/off decision ─────────────────────────
public function testReinstallKeepsAModuleTheAdminDisabled(): void {
// A store update lands in installModule(), which used to finish with an
// unconditional Enabled — silently switching a disabled module back on.
$this->createModule('keep-off', '1.0.0');
$this->writeOverrides(['keep-off' => ['state' => 'disabled', 'installed_version' => '1.0.0']]);
$this->manager()->installModule('keep-off');
$overrides = $this->readOverrides();
$this->assertSame('disabled', $overrides['keep-off']['state'] ?? null);
$this->assertSame('1.0.0', $overrides['keep-off']['installed_version'] ?? null);
}
public function testFirstInstallEnablesTheModule(): void {
$this->createModule('turn-on', '1.0.0');
$this->manager()->installModule('turn-on');
// Enabled is the clean default, recorded by the ABSENCE of a state key.
$this->assertArrayNotHasKey('state', $this->readOverrides()['turn-on']);
}
public function testReinstallRestoresDisabledEvenWithADependent(): void {
// Restoring Disabled is a lifecycle transition, not an admin "disable",
// so the dependents guard must not veto it and fail the install.
$this->createModuleWithDeps('dep-off', '1.0.0', []);
$this->createModuleWithDeps('dep-user', '1.0.0', ['dep-off']);
$this->writeOverrides([
'dep-off' => ['state' => 'disabled', 'installed_version' => '1.0.0'],
'dep-user' => ['installed_version' => '1.0.0'],
]);
$this->manager()->installModule('dep-off');
$this->assertSame('disabled', $this->readOverrides()['dep-off']['state'] ?? null);
}
// ── update/uninstall failure containment ──────────────────────────────
public function testUpdateKeepsWatermarkAtLastCompletedVersionOnFailure(): void {